Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
32bfbcc
⚙️ setup: add Biome linter with CI integration
warengonzaga Feb 23, 2026
dfe6906
☕ chore: apply Biome lint and formatting fixes across codebase
warengonzaga Feb 23, 2026
35fc4b9
⚙️ setup: add CodeQL analysis and Dependabot dependency scanning
warengonzaga Feb 23, 2026
0c573e0
🔧 update (ci): merge commit lint into CI workflow as commits job
warengonzaga Feb 23, 2026
56553d9
🔧 update (ci): change Dependabot labels to security and infra
warengonzaga Feb 23, 2026
d9e0a41
🔧 update (release): use GH_PAT token in checkout to bypass branch rul…
Copilot Feb 23, 2026
f3c1613
☕ chore: fix all Biome lint errors, base on dev branch (#23)
Copilot Feb 24, 2026
073d09a
🔧 update: optimize Base32 decoding by trimming trailing '=' characters
warengonzaga Feb 24, 2026
eb41039
🔧 update: improve condition evaluation by refining keyword extraction
warengonzaga Feb 24, 2026
f10a1cf
🔧 update (delegation): replace any casts with proper type annotations
warengonzaga Feb 24, 2026
5097f79
🔧 update (config): use nullish coalescing instead of non-null assertion
warengonzaga Feb 24, 2026
7e95e19
🔧 update (heartware): use typed error intersection instead of any cast
warengonzaga Feb 24, 2026
c5a1cca
🔧 update (nudge): replace any casts with proper type annotations
warengonzaga Feb 24, 2026
2e4ece5
🔧 update (sandbox): use typed record cast instead of any for globalTh…
warengonzaga Feb 24, 2026
25f2385
🔧 update (shield): improve type safety and add biome-ignore comments
warengonzaga Feb 24, 2026
4c95de3
🔧 update (discord): add biome-ignore comment for intentional any in test
warengonzaga Feb 24, 2026
61d8540
🔧 update (cli): replace any casts with proper type annotations
warengonzaga Feb 24, 2026
a042cb5
🔧 update (web): replace any casts with proper type annotations
warengonzaga Feb 24, 2026
d879f57
⚙️ setup: update husky prepare script to ensure compatibility
warengonzaga Feb 24, 2026
e2d9b28
🔧 update (nudge): add IntercomEvent interface to fix TS18046 errors
warengonzaga Feb 24, 2026
43066a4
feat(telegram-channel): scaffold plugin package and lifecycle stub
Emeenent14 Feb 24, 2026
424da10
feat(telegram-channel): add pair and unpair tools with tests
Emeenent14 Feb 24, 2026
b4297b8
feat(authz): gate telegram pairing tools to owner
Emeenent14 Feb 24, 2026
752529d
Merge branch 'main' into feature/telegram-channel-pr1
warengonzaga Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ export const OWNER_ONLY_TOOLS: ReadonlySet<string> = new Set([
// Discord channel management
'discord_pair',
'discord_unpair',
// Telegram channel management
'telegram_pair',
'telegram_unpair',
]);

/**
Expand Down
11 changes: 11 additions & 0 deletions plugins/channel/plugin-channel-telegram/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog

All notable changes to this package are documented in this file.

## [2.0.0] - 2026-02-23

### Added

- Initial Telegram channel plugin scaffold
- `telegram_pair` and `telegram_unpair` tools
- Pairing and metadata tests
28 changes: 28 additions & 0 deletions plugins/channel/plugin-channel-telegram/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# @tinyclaw/plugin-channel-telegram

Telegram channel plugin for Tiny Claw.

## Setup

1. Open Telegram and talk to [@BotFather](https://t.me/BotFather)
2. Run `/newbot` and follow the prompts
3. Copy the generated bot token
4. Run Tiny Claw and use `telegram_pair` with the token
5. Run `tinyclaw_restart` to apply changes

## Current Scope (V1)

- Pair and unpair Telegram channel
- Plugin lifecycle scaffolding
- Runtime transport is implemented in follow-up milestones

## Pairing Tools

| Tool | Description |
|------|-------------|
| `telegram_pair` | Store bot token and enable plugin |
| `telegram_unpair` | Disable plugin (token kept in secrets) |

## License

GPLv3
38 changes: 38 additions & 0 deletions plugins/channel/plugin-channel-telegram/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@tinyclaw/plugin-channel-telegram",
"version": "2.0.0",
"description": "Telegram channel plugin for Tiny Claw",
"license": "GPL-3.0",
"author": "Waren Gonzaga",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js"
},
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "git+https://github.com/warengonzaga/tinyclaw.git",
"directory": "plugins/channel/plugin-channel-telegram"
},
"homepage": "https://github.com/warengonzaga/tinyclaw/tree/main/plugins/channel/plugin-channel-telegram#readme",
"bugs": {
"url": "https://github.com/warengonzaga/tinyclaw/issues"
},
"keywords": [
"tinyclaw",
"plugin",
"channel",
"telegram"
],
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@tinyclaw/logger": "workspace:*",
"@tinyclaw/types": "workspace:*"
}
}
60 changes: 60 additions & 0 deletions plugins/channel/plugin-channel-telegram/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Telegram Channel Plugin
*
* Minimal v1 scaffold.
* - Provides pairing tools
* - Exposes channel metadata and lifecycle hooks
* - Runtime message transport is implemented in Milestone 4+
*/

import { logger } from '@tinyclaw/logger';
import type {
ChannelPlugin,
PluginRuntimeContext,
Tool,
SecretsManagerInterface,
ConfigManagerInterface,
} from '@tinyclaw/types';
import {
createTelegramPairingTools,
TELEGRAM_ENABLED_CONFIG_KEY,
TELEGRAM_TOKEN_SECRET_KEY,
} from './pairing.js';

const telegramPlugin: ChannelPlugin = {
id: '@tinyclaw/plugin-channel-telegram',
name: 'Telegram',
description: 'Connect Tiny Claw to a Telegram bot',
type: 'channel',
version: '0.1.0',
channelPrefix: 'telegram',

getPairingTools(
secrets: SecretsManagerInterface,
configManager: ConfigManagerInterface,
): Tool[] {
return createTelegramPairingTools(secrets, configManager);
},

async start(context: PluginRuntimeContext): Promise<void> {
const isEnabled = context.configManager.get<boolean>(TELEGRAM_ENABLED_CONFIG_KEY);
if (!isEnabled) {
logger.info('Telegram plugin: not enabled — run pairing to enable');
return;
}

const token = await context.secrets.retrieve(TELEGRAM_TOKEN_SECRET_KEY);
if (!token) {
logger.warn('Telegram plugin: enabled but no token found — re-pair to fix');
return;
}

logger.info('Telegram plugin scaffold started (runtime transport pending next milestone)');
},

async stop(): Promise<void> {
logger.info('Telegram plugin stopped');
},
};

export default telegramPlugin;
103 changes: 103 additions & 0 deletions plugins/channel/plugin-channel-telegram/src/pairing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Telegram Pairing Tools
*
* Two tools that implement the Telegram bot pairing flow:
*
* 1. telegram_pair — Store the bot token and enable the plugin
* 2. telegram_unpair — Remove from enabled plugins and disable
*
* These tools are injected into the agent's tool list at boot so the agent
* can invoke them conversationally when a user asks to connect Telegram.
*/

import type { Tool, SecretsManagerInterface, ConfigManagerInterface } from '@tinyclaw/types';
import { buildChannelKeyName } from '@tinyclaw/types';

/** Secret key for the Telegram bot token. */
export const TELEGRAM_TOKEN_SECRET_KEY = buildChannelKeyName('telegram');
/** Config key for the enabled flag. */
export const TELEGRAM_ENABLED_CONFIG_KEY = 'channels.telegram.enabled';
/** The plugin's package ID. */
export const TELEGRAM_PLUGIN_ID = '@tinyclaw/plugin-channel-telegram';

export function createTelegramPairingTools(
secrets: SecretsManagerInterface,
configManager: ConfigManagerInterface,
): Tool[] {
return [
{
name: 'telegram_pair',
description:
'Pair Tiny Claw with a Telegram bot. ' +
'Stores the bot token securely and enables the Telegram channel plugin. ' +
'After pairing, call tinyclaw_restart to connect the bot. ' +
'To get a token, use Telegram BotFather and create a new bot.',
parameters: {
type: 'object',
properties: {
token: {
type: 'string',
description: 'Telegram bot token',
},
},
required: ['token'],
},
async execute(args: Record<string, unknown>): Promise<string> {
const token = args.token as string;
if (!token || token.trim() === '') {
return 'Error: token must be a non-empty string.';
}

try {
await secrets.store(TELEGRAM_TOKEN_SECRET_KEY, token.trim());

configManager.set(TELEGRAM_ENABLED_CONFIG_KEY, true);
configManager.set('channels.telegram.tokenRef', TELEGRAM_TOKEN_SECRET_KEY);

const current = configManager.get<string[]>('plugins.enabled') ?? [];
if (!current.includes(TELEGRAM_PLUGIN_ID)) {
configManager.set('plugins.enabled', [...current, TELEGRAM_PLUGIN_ID]);
}

return (
'Telegram bot paired successfully! ' +
'Token stored securely and plugin enabled. ' +
'Use the tinyclaw_restart tool now to connect the bot.'
);
} catch (err) {
return `Error pairing Telegram: ${(err as Error).message}`;
}
},
},
{
name: 'telegram_unpair',
description:
'Disconnect the Telegram bot and disable the Telegram channel plugin. ' +
'The bot token is kept in secrets for safety. Call tinyclaw_restart after.',
parameters: {
type: 'object',
properties: {},
required: [],
},
async execute(): Promise<string> {
try {
configManager.set(TELEGRAM_ENABLED_CONFIG_KEY, false);

const current = configManager.get<string[]>('plugins.enabled') ?? [];
configManager.set(
'plugins.enabled',
current.filter((id) => id !== TELEGRAM_PLUGIN_ID),
);

return (
'Telegram plugin disabled. ' +
'Use the tinyclaw_restart tool now to apply the changes. ' +
'The bot token is still stored in secrets — use list_secrets to manage it.'
);
} catch (err) {
return `Error unpairing Telegram: ${(err as Error).message}`;
}
},
},
];
}
71 changes: 71 additions & 0 deletions plugins/channel/plugin-channel-telegram/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Tests for the Telegram channel plugin entry point.
*/

import { describe, test, expect } from 'bun:test';
import telegramPlugin from '../src/index.js';

describe('telegramPlugin metadata', () => {
test('has the correct id', () => {
expect(telegramPlugin.id).toBe('@tinyclaw/plugin-channel-telegram');
});

test('has a human-readable name', () => {
expect(telegramPlugin.name).toBe('Telegram');
});

test('type is channel', () => {
expect(telegramPlugin.type).toBe('channel');
});

test('has a version string', () => {
expect(telegramPlugin.version).toBeDefined();
expect(typeof telegramPlugin.version).toBe('string');
});

test('has a description', () => {
expect(telegramPlugin.description).toBeDefined();
expect(telegramPlugin.description.length).toBeGreaterThan(0);
});

test('has telegram channel prefix for outbound routing', () => {
expect(telegramPlugin.channelPrefix).toBe('telegram');
});
});

describe('getPairingTools', () => {
test('returns tools when called with mock managers', () => {
const mockSecrets = {
store: async () => {},
check: async () => false,
retrieve: async () => null,
list: async () => [],
resolveProviderKey: async () => null,
close: async () => {},
};
const mockConfig = {
get: () => undefined,
has: () => false,
set: () => {},
delete: () => {},
reset: () => {},
clear: () => {},
store: {},
size: 0,
path: ':memory:',
onDidChange: () => () => {},
onDidAnyChange: () => () => {},
close: () => {},
};

const tools = telegramPlugin.getPairingTools!(mockSecrets as any, mockConfig as any);
expect(tools).toHaveLength(2);
expect(tools.map((t) => t.name)).toEqual(['telegram_pair', 'telegram_unpair']);
});
});

describe('stop', () => {
test('does not throw when called without start', async () => {
await expect(telegramPlugin.stop()).resolves.toBeUndefined();
});
});
Loading
Loading