@cmdop/bot 2026.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,142 @@
1
+ # @cmdop/bot — Examples
2
+
3
+ Runnable examples for each supported platform. All examples use `tsx` to run TypeScript directly.
4
+
5
+ ## Prerequisites
6
+
7
+ ```bash
8
+ pnpm add @cmdop/bot
9
+ # Install only the platform you need:
10
+ pnpm add grammy @grammyjs/transformer-throttler # Telegram
11
+ pnpm add discord.js @discordjs/rest @discordjs/builders # Discord
12
+ pnpm add @slack/bolt @slack/web-api # Slack
13
+ ```
14
+
15
+ ---
16
+
17
+ ## telegram.ts — Telegram bot
18
+
19
+ ```bash
20
+ CMDOP_API_KEY=cmdop_live_xxx \
21
+ TELEGRAM_TOKEN=123456:ABC-DEF \
22
+ pnpm tsx examples/telegram.ts
23
+ ```
24
+
25
+ **Required env vars:**
26
+ - `TELEGRAM_TOKEN` — get from [@BotFather](https://t.me/BotFather)
27
+
28
+ **Optional:**
29
+ - `CMDOP_API_KEY` — omit to use local IPC connection
30
+ - `CMDOP_MACHINE` — pre-select a machine hostname
31
+ - `BOT_ALLOWED_USERS` — comma-separated Telegram user IDs that receive `ADMIN`
32
+
33
+ ---
34
+
35
+ ## discord.ts — Discord bot (slash commands)
36
+
37
+ ```bash
38
+ CMDOP_API_KEY=cmdop_live_xxx \
39
+ DISCORD_TOKEN=your-bot-token \
40
+ DISCORD_CLIENT_ID=your-app-id \
41
+ DISCORD_GUILD_ID=your-guild-id \
42
+ pnpm tsx examples/discord.ts
43
+ ```
44
+
45
+ **Required env vars:**
46
+ - `DISCORD_TOKEN` — bot token from [Discord Developer Portal](https://discord.com/developers)
47
+ - `DISCORD_CLIENT_ID` — application ID from the same portal
48
+
49
+ **Optional:**
50
+ - `DISCORD_GUILD_ID` — register commands to a single guild (instant); omit for global (up to 1 hour)
51
+ - `BOT_ALLOWED_USERS` — comma-separated Discord user IDs that receive `ADMIN`
52
+
53
+ **Bot permissions required:** `applications.commands`, `bot` scope with `Send Messages`.
54
+
55
+ ---
56
+
57
+ ## slack.ts — Slack bot (Socket Mode)
58
+
59
+ ```bash
60
+ CMDOP_API_KEY=cmdop_live_xxx \
61
+ SLACK_BOT_TOKEN=xoxb-xxx \
62
+ SLACK_APP_TOKEN=xapp-xxx \
63
+ pnpm tsx examples/slack.ts
64
+ ```
65
+
66
+ **Required env vars:**
67
+ - `SLACK_BOT_TOKEN` — bot OAuth token (starts with `xoxb-`)
68
+ - `SLACK_APP_TOKEN` — app-level token for Socket Mode (starts with `xapp-`)
69
+
70
+ **Slack app setup:**
71
+ 1. Enable **Socket Mode** in your app settings
72
+ 2. Add `connections:write` scope to the App-level token
73
+ 3. Subscribe to bot events: `message.im`, `message.channels`, `app_mention`
74
+ 4. Enable **Messages Tab** in App Home settings
75
+
76
+ **Optional:**
77
+ - `BOT_ALLOWED_USERS` — comma-separated Slack user IDs that receive `ADMIN`
78
+
79
+ ---
80
+
81
+ ## multi-channel.ts — All platforms at once
82
+
83
+ ```bash
84
+ CMDOP_API_KEY=cmdop_live_xxx \
85
+ TELEGRAM_TOKEN=xxx \
86
+ DISCORD_TOKEN=xxx \
87
+ DISCORD_CLIENT_ID=xxx \
88
+ SLACK_BOT_TOKEN=xoxb-xxx \
89
+ SLACK_APP_TOKEN=xapp-xxx \
90
+ pnpm tsx examples/multi-channel.ts
91
+ ```
92
+
93
+ All channels share the same permission store. A user granted `EXECUTE` on Telegram has it on Discord and Slack too (if their identities are linked via `hub.linkIdentities()`).
94
+
95
+ ---
96
+
97
+ ## custom-channel.ts — Build your own platform integration
98
+
99
+ Shows how to implement `ChannelProtocol` for any messaging platform not built into `@cmdop/bot`.
100
+
101
+ ```bash
102
+ pnpm tsx examples/custom-channel.ts
103
+ ```
104
+
105
+ No external dependencies — uses an in-process event emitter to simulate a platform.
106
+
107
+ ---
108
+
109
+ ## Running with a local CMDOP agent
110
+
111
+ Omit `CMDOP_API_KEY` to connect via local IPC (requires the CMDOP agent running on your machine):
112
+
113
+ ```bash
114
+ TELEGRAM_TOKEN=xxx pnpm tsx examples/telegram.ts
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Common patterns
120
+
121
+ ### Grant permission to a specific user
122
+
123
+ ```ts
124
+ // After hub is created, before hub.start():
125
+ await hub.permissions.setLevel('telegram:123456789', 'EXECUTE');
126
+ ```
127
+
128
+ ### Pre-select a machine for all commands
129
+
130
+ ```ts
131
+ const hub = await IntegrationHub.create({
132
+ defaultMachine: 'my-server.local',
133
+ });
134
+ ```
135
+
136
+ ### Check which channels started successfully
137
+
138
+ ```ts
139
+ await hub.start();
140
+ console.log('Running:', hub.runningChannelIds);
141
+ console.log('Failed: ', hub.failedChannelIds);
142
+ ```
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Custom Channel Example
4
+ *
5
+ * Shows how to implement ChannelProtocol for any messaging platform
6
+ * not built into @cmdop/bot.
7
+ *
8
+ * This example uses an in-process EventEmitter to simulate a chat platform
9
+ * so it runs without any external dependencies.
10
+ *
11
+ * Run:
12
+ * pnpm tsx examples/custom-channel.ts
13
+ *
14
+ * The bot will respond to a few simulated messages, then exit.
15
+ */
16
+
17
+ import { EventEmitter } from 'node:events';
18
+ import {
19
+ IntegrationHub,
20
+ BaseChannel,
21
+ createLogger,
22
+ type OutgoingMessage,
23
+ type IncomingMessage,
24
+ type PermissionManager,
25
+ type MessageDispatcher,
26
+ type LoggerProtocol,
27
+ } from '../src/index.js';
28
+
29
+ // ─── Simulated platform types ─────────────────────────────────────────────────
30
+
31
+ interface PlatformMessage {
32
+ id: string;
33
+ authorId: string;
34
+ text: string;
35
+ }
36
+
37
+ interface PlatformSendEvent {
38
+ toUserId: string;
39
+ text: string;
40
+ }
41
+
42
+ // ─── Custom channel implementation ───────────────────────────────────────────
43
+
44
+ /**
45
+ * EchoPlatformChannel — a minimal ChannelProtocol implementation.
46
+ *
47
+ * Key points when implementing your own channel:
48
+ * 1. Call `this.processMessage(msg)` from your platform event handler.
49
+ * BaseChannel takes care of parsing, permission checks, dispatch, and send().
50
+ * 2. Implement `send()` to format and deliver OutgoingMessage to the platform.
51
+ * 3. `onMessage()` is called by the hub to register its own handler —
52
+ * forward all incoming messages to it too (or just rely on processMessage).
53
+ * 4. `start()` / `stop()` manage the platform connection lifecycle.
54
+ */
55
+ class EchoPlatformChannel extends BaseChannel {
56
+ private readonly platform: EventEmitter;
57
+ private readonly hubHandlers: Array<(msg: IncomingMessage) => Promise<void>> = [];
58
+
59
+ constructor(
60
+ platform: EventEmitter,
61
+ permissions: PermissionManager,
62
+ dispatcher: MessageDispatcher,
63
+ logger: LoggerProtocol,
64
+ ) {
65
+ // Pass a unique channel id, display name, and the shared hub dependencies
66
+ super('echo', 'Echo Platform', permissions, dispatcher, logger);
67
+ this.platform = platform;
68
+ }
69
+
70
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
71
+
72
+ async start(): Promise<void> {
73
+ this.platform.on('message', this.handlePlatformMessage.bind(this));
74
+ this.logEvent('connected');
75
+ }
76
+
77
+ async stop(): Promise<void> {
78
+ this.platform.removeAllListeners('message');
79
+ this.logEvent('disconnected');
80
+ }
81
+
82
+ // ── Sending ───────────────────────────────────────────────────────────────
83
+
84
+ async send(_userId: string, message: OutgoingMessage): Promise<void> {
85
+ const text = this.formatMessage(message);
86
+ const event: PlatformSendEvent = { toUserId: _userId, text };
87
+ this.platform.emit('outgoing', event);
88
+ // In a real channel you'd call the platform SDK here, e.g.:
89
+ // await platformApi.sendMessage(userId, text);
90
+ }
91
+
92
+ // ── Hub handler registration ───────────────────────────────────────────────
93
+
94
+ onMessage(handler: (msg: IncomingMessage) => Promise<void>): void {
95
+ // The hub registers its own handler here (for cross-channel routing).
96
+ // We store it and call it in handlePlatformMessage alongside processMessage.
97
+ this.hubHandlers.push(handler);
98
+ }
99
+
100
+ // ── Internal helpers ──────────────────────────────────────────────────────
101
+
102
+ private async handlePlatformMessage(raw: PlatformMessage): Promise<void> {
103
+ const msg: IncomingMessage = {
104
+ id: raw.id,
105
+ userId: raw.authorId,
106
+ channelId: this.id,
107
+ text: raw.text,
108
+ timestamp: new Date(),
109
+ attachments: [],
110
+ };
111
+
112
+ // 1. Notify hub handlers (cross-channel features, logging, etc.)
113
+ for (const handler of this.hubHandlers) {
114
+ await handler(msg);
115
+ }
116
+
117
+ // 2. Parse command → check permission → dispatch → send result
118
+ await this.processMessage(msg);
119
+ }
120
+
121
+ private formatMessage(msg: OutgoingMessage): string {
122
+ switch (msg.type) {
123
+ case 'text':
124
+ return msg.text;
125
+ case 'code':
126
+ return `\`\`\`${msg.language ?? ''}\n${msg.code}\n\`\`\``;
127
+ case 'error':
128
+ return `ERROR: ${msg.message}${msg.hint ? ` (${msg.hint})` : ''}`;
129
+ }
130
+ }
131
+ }
132
+
133
+ // ─── Demo run ─────────────────────────────────────────────────────────────────
134
+
135
+ async function main() {
136
+ const logger = createLogger('info');
137
+ const platform = new EventEmitter();
138
+
139
+ // Collect outgoing messages for display
140
+ platform.on('outgoing', (e: PlatformSendEvent) => {
141
+ console.log(`\n→ [to ${e.toUserId}] ${e.text}`);
142
+ });
143
+
144
+ // Create hub with local CMDOP connection (or mock)
145
+ // In a real app: IntegrationHub.create({ apiKey: process.env.CMDOP_API_KEY })
146
+ const hub = await IntegrationHub.create({ logger }).catch(() => {
147
+ console.warn('Could not connect to CMDOP — running in demo mode without a real client.');
148
+ process.exit(0);
149
+ });
150
+
151
+ // Grant admin to our test user
152
+ await hub.permissions.setLevel('echo:alice', 'ADMIN');
153
+
154
+ // Register the custom channel
155
+ const channel = new EchoPlatformChannel(
156
+ platform,
157
+ hub.permissions,
158
+ // Access the dispatcher via the hub's internal structure isn't exposed directly —
159
+ // for custom channels registered via hub.registerChannel(), the hub wires
160
+ // onMessage() to its internal dispatcher after registerChannel().
161
+ // Here we construct the channel and register it:
162
+ (hub as unknown as { _dispatcher: MessageDispatcher })._dispatcher,
163
+ logger,
164
+ );
165
+
166
+ hub.registerChannel(channel);
167
+ await hub.start();
168
+
169
+ console.log('Custom channel started. Simulating messages...\n');
170
+
171
+ // ── Simulate incoming platform messages ────────────────────────────────────
172
+
173
+ const send = (authorId: string, text: string) => {
174
+ console.log(`← [from ${authorId}] ${text}`);
175
+ platform.emit('message', {
176
+ id: `msg-${Date.now()}`,
177
+ authorId,
178
+ text,
179
+ } satisfies PlatformMessage);
180
+ };
181
+
182
+ // Small delay between messages so async handlers resolve cleanly
183
+ const delay = (ms: number) => new Promise(r => setTimeout(r, ms));
184
+
185
+ await delay(50);
186
+ send('alice', '/help');
187
+
188
+ await delay(200);
189
+ send('alice', '/exec echo "hello from custom channel"');
190
+
191
+ await delay(200);
192
+ send('bob', '/exec ls'); // bob has no permission → PERMISSION_DENIED
193
+
194
+ await delay(200);
195
+ send('alice', 'just chatting — not a command, silently ignored');
196
+
197
+ await delay(200);
198
+
199
+ await hub.stop();
200
+ console.log('\nDone.');
201
+ }
202
+
203
+ main().catch((err: unknown) => {
204
+ console.error('Fatal:', err);
205
+ process.exit(1);
206
+ });
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Discord Bot Example
4
+ *
5
+ * Run:
6
+ * CMDOP_API_KEY=cmdop_live_xxx DISCORD_TOKEN=xxx DISCORD_CLIENT_ID=xxx pnpm tsx examples/discord.ts
7
+ *
8
+ * Required env vars:
9
+ * CMDOP_API_KEY — CMDOP cloud API key (omit to use local IPC)
10
+ * DISCORD_TOKEN — Bot token from Discord Developer Portal
11
+ * DISCORD_CLIENT_ID — Application ID from Discord Developer Portal
12
+ *
13
+ * Optional env vars:
14
+ * DISCORD_GUILD_ID — Register commands to a specific guild (instant); omit for global (up to 1h)
15
+ * CMDOP_MACHINE — Default machine hostname for all commands
16
+ * BOT_ALLOWED_USERS — Comma-separated Discord user IDs that get ADMIN
17
+ * BOT_LOG_LEVEL — debug | info | warn | error (default: info)
18
+ */
19
+
20
+ import { IntegrationHub } from '../src/index.js';
21
+
22
+ async function main() {
23
+ const token = process.env['DISCORD_TOKEN'];
24
+ if (!token) {
25
+ console.error('DISCORD_TOKEN is required');
26
+ process.exit(1);
27
+ }
28
+
29
+ const clientId = process.env['DISCORD_CLIENT_ID'];
30
+ if (!clientId) {
31
+ console.error('DISCORD_CLIENT_ID is required');
32
+ process.exit(1);
33
+ }
34
+
35
+ const hub = await IntegrationHub.create({
36
+ apiKey: process.env['CMDOP_API_KEY'],
37
+ defaultMachine: process.env['CMDOP_MACHINE'],
38
+ adminUsers: (process.env['BOT_ALLOWED_USERS'] ?? '').split(',').filter(Boolean),
39
+ });
40
+
41
+ // addDiscord() wires permissions + dispatcher automatically
42
+ await hub.addDiscord({
43
+ token,
44
+ clientId,
45
+ guildId: process.env['DISCORD_GUILD_ID'],
46
+ });
47
+
48
+ await hub.start();
49
+ console.log('✅ Discord bot running. Press Ctrl+C to stop.');
50
+ console.log('Slash commands: /exec /agent /files /help\n');
51
+
52
+ async function shutdown(signal: string) {
53
+ console.log(`\n${signal} received, shutting down...`);
54
+ await hub.stop();
55
+ process.exit(0);
56
+ }
57
+
58
+ process.once('SIGINT', () => void shutdown('SIGINT'));
59
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
60
+ }
61
+
62
+ main().catch((err: unknown) => {
63
+ console.error('Fatal error:', err);
64
+ process.exit(1);
65
+ });
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Multi-Channel Bot Example
4
+ *
5
+ * Runs Telegram + Discord + Slack concurrently from a single hub.
6
+ * All three channels share the same permission store and CMDOP client,
7
+ * so a user granted EXECUTE on Telegram automatically has it on Discord/Slack.
8
+ *
9
+ * Run:
10
+ * TELEGRAM_TOKEN=xxx \
11
+ * DISCORD_TOKEN=xxx \
12
+ * DISCORD_CLIENT_ID=xxx \
13
+ * SLACK_BOT_TOKEN=xoxb-xxx \
14
+ * SLACK_APP_TOKEN=xapp-xxx \
15
+ * CMDOP_API_KEY=cmdop_live_xxx \
16
+ * pnpm tsx examples/multi-channel.ts
17
+ *
18
+ * Required env vars:
19
+ * CMDOP_API_KEY — CMDOP cloud API key (omit for local IPC)
20
+ * TELEGRAM_TOKEN — Telegram bot token from @BotFather
21
+ * DISCORD_TOKEN — Bot token from Discord Developer Portal
22
+ * DISCORD_CLIENT_ID — Application ID from Discord Developer Portal
23
+ * SLACK_BOT_TOKEN — Bot OAuth token (xoxb-...)
24
+ * SLACK_APP_TOKEN — App-level token for Socket Mode (xapp-...)
25
+ *
26
+ * Optional env vars:
27
+ * DISCORD_GUILD_ID — Instant slash command registration for a guild
28
+ * CMDOP_MACHINE — Default machine hostname
29
+ * BOT_ALLOWED_USERS — Comma-separated user IDs that get ADMIN (any platform)
30
+ * BOT_LOG_LEVEL — debug | info | warn | error (default: info)
31
+ */
32
+
33
+ import { IntegrationHub, IdentityMap } from '../src/index.js';
34
+
35
+ async function main() {
36
+ // ─── Validate required env vars ────────────────────────────────────────────
37
+ const telegramToken = process.env['TELEGRAM_TOKEN'];
38
+ const discordToken = process.env['DISCORD_TOKEN'];
39
+ const discordClientId = process.env['DISCORD_CLIENT_ID'];
40
+ const slackBotToken = process.env['SLACK_BOT_TOKEN'];
41
+ const slackAppToken = process.env['SLACK_APP_TOKEN'];
42
+
43
+ const missing: string[] = [];
44
+ if (!telegramToken) missing.push('TELEGRAM_TOKEN');
45
+ if (!discordToken) missing.push('DISCORD_TOKEN');
46
+ if (!discordClientId) missing.push('DISCORD_CLIENT_ID');
47
+ if (!slackBotToken) missing.push('SLACK_BOT_TOKEN');
48
+ if (!slackAppToken) missing.push('SLACK_APP_TOKEN');
49
+
50
+ if (missing.length > 0) {
51
+ console.error(`Missing required env vars: ${missing.join(', ')}`);
52
+ process.exit(1);
53
+ }
54
+
55
+ // ─── Create hub — shared CMDOP client, permissions, dispatcher ─────────────
56
+ const hub = await IntegrationHub.create({
57
+ apiKey: process.env['CMDOP_API_KEY'],
58
+ defaultMachine: process.env['CMDOP_MACHINE'],
59
+ adminUsers: (process.env['BOT_ALLOWED_USERS'] ?? '').split(',').filter(Boolean),
60
+ // 'isolated' (default): a failing channel doesn't block the others
61
+ channelStartMode: 'isolated',
62
+ });
63
+
64
+ // ─── Register channels ─────────────────────────────────────────────────────
65
+ await hub.addTelegram({ token: telegramToken! });
66
+ await hub.addDiscord({ token: discordToken!, clientId: discordClientId!, guildId: process.env['DISCORD_GUILD_ID'] });
67
+ await hub.addSlack({ token: slackBotToken!, appToken: slackAppToken! });
68
+
69
+ // ─── Cross-channel identity example ───────────────────────────────────────
70
+ // If you know a Telegram user and their Discord account are the same person,
71
+ // link them so permissions granted on one platform apply to the other.
72
+ // In a real app, you'd build a /link command to let users do this themselves.
73
+ //
74
+ // hub.linkIdentities('telegram', '12345678', 'discord', '987654321098765432');
75
+ //
76
+ // After linking: granting EXECUTE to the Telegram user also applies on Discord.
77
+ // await hub.permissions.setLevel('telegram:12345678', 'EXECUTE');
78
+
79
+ // ─── Start all channels concurrently ──────────────────────────────────────
80
+ await hub.start();
81
+
82
+ const running = hub.runningChannelIds;
83
+ const failed = hub.failedChannelIds;
84
+
85
+ console.log(`✅ IntegrationHub started (${running.length}/${hub.channelCount} channels running)`);
86
+ if (running.length > 0) console.log(` Running: ${running.join(', ')}`);
87
+ if (failed.length > 0) console.warn(` ⚠️ Failed: ${failed.join(', ')}`);
88
+ console.log('Commands: /exec /agent /files /help\n');
89
+
90
+ // ─── Graceful shutdown ────────────────────────────────────────────────────
91
+ async function shutdown(signal: string) {
92
+ console.log(`\n${signal} received, shutting down...`);
93
+ await hub.stop();
94
+ console.log('Clean shutdown.');
95
+ process.exit(0);
96
+ }
97
+
98
+ process.once('SIGINT', () => void shutdown('SIGINT'));
99
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
100
+ }
101
+
102
+ main().catch((err: unknown) => {
103
+ console.error('Fatal error:', err);
104
+ process.exit(1);
105
+ });
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Slack Bot Example (Socket Mode)
4
+ *
5
+ * Run:
6
+ * CMDOP_API_KEY=cmdop_live_xxx SLACK_BOT_TOKEN=xoxb-xxx SLACK_APP_TOKEN=xapp-xxx pnpm tsx examples/slack.ts
7
+ *
8
+ * Required env vars:
9
+ * CMDOP_API_KEY — CMDOP cloud API key (omit to use local IPC)
10
+ * SLACK_BOT_TOKEN — Bot OAuth token (xoxb-...)
11
+ * SLACK_APP_TOKEN — App-level token for Socket Mode (xapp-...)
12
+ *
13
+ * Optional env vars:
14
+ * CMDOP_MACHINE — Default machine hostname for all commands
15
+ * BOT_ALLOWED_USERS — Comma-separated Slack user IDs that get ADMIN
16
+ * BOT_LOG_LEVEL — debug | info | warn | error (default: info)
17
+ *
18
+ * Slack app setup:
19
+ * 1. Enable Socket Mode in your Slack app settings
20
+ * 2. Enable the "connections:write" scope for the App-level token
21
+ * 3. Subscribe to bot events: message.im, message.channels, app_mention
22
+ * 4. Enable "Messages Tab" in App Home settings
23
+ */
24
+
25
+ import { IntegrationHub } from '../src/index.js';
26
+
27
+ async function main() {
28
+ const token = process.env['SLACK_BOT_TOKEN'];
29
+ if (!token) {
30
+ console.error('SLACK_BOT_TOKEN is required');
31
+ process.exit(1);
32
+ }
33
+
34
+ const appToken = process.env['SLACK_APP_TOKEN'];
35
+ if (!appToken) {
36
+ console.error('SLACK_APP_TOKEN is required');
37
+ process.exit(1);
38
+ }
39
+
40
+ const hub = await IntegrationHub.create({
41
+ apiKey: process.env['CMDOP_API_KEY'],
42
+ defaultMachine: process.env['CMDOP_MACHINE'],
43
+ adminUsers: (process.env['BOT_ALLOWED_USERS'] ?? '').split(',').filter(Boolean),
44
+ });
45
+
46
+ // addSlack() wires permissions + dispatcher automatically
47
+ await hub.addSlack({ token, appToken });
48
+
49
+ await hub.start();
50
+ console.log('✅ Slack bot running (Socket Mode). Press Ctrl+C to stop.');
51
+ console.log('Commands: /exec /agent /files /help\n');
52
+
53
+ async function shutdown(signal: string) {
54
+ console.log(`\n${signal} received, shutting down...`);
55
+ await hub.stop();
56
+ process.exit(0);
57
+ }
58
+
59
+ process.once('SIGINT', () => void shutdown('SIGINT'));
60
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
61
+ }
62
+
63
+ main().catch((err: unknown) => {
64
+ console.error('Fatal error:', err);
65
+ process.exit(1);
66
+ });
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Telegram Bot Example
4
+ *
5
+ * Run:
6
+ * CMDOP_API_KEY=cmdop_live_xxx TELEGRAM_TOKEN=xxx pnpm tsx examples/telegram.ts
7
+ *
8
+ * Required env vars:
9
+ * CMDOP_API_KEY — CMDOP cloud API key (omit to use local IPC)
10
+ * TELEGRAM_TOKEN — Telegram bot token from @BotFather
11
+ *
12
+ * Optional env vars:
13
+ * CMDOP_MACHINE — Default machine hostname for all commands
14
+ * BOT_ALLOWED_USERS — Comma-separated Telegram user IDs that get ADMIN
15
+ * BOT_LOG_LEVEL — debug | info | warn | error (default: info)
16
+ */
17
+
18
+ import { IntegrationHub } from '../src/index.js';
19
+
20
+ async function main() {
21
+ const token = process.env['TELEGRAM_TOKEN'];
22
+ if (!token) {
23
+ console.error('TELEGRAM_TOKEN is required');
24
+ process.exit(1);
25
+ }
26
+
27
+ const hub = await IntegrationHub.create({
28
+ apiKey: process.env['CMDOP_API_KEY'],
29
+ defaultMachine: process.env['CMDOP_MACHINE'],
30
+ adminUsers: (process.env['BOT_ALLOWED_USERS'] ?? '').split(',').filter(Boolean),
31
+ });
32
+
33
+ // addTelegram() wires permissions + dispatcher automatically
34
+ await hub.addTelegram({ token });
35
+
36
+ await hub.start();
37
+ console.log('✅ Telegram bot running. Press Ctrl+C to stop.');
38
+ console.log('Available commands: /exec /agent /files /help\n');
39
+
40
+ async function shutdown(signal: string) {
41
+ console.log(`\n${signal} received, shutting down...`);
42
+ await hub.stop();
43
+ process.exit(0);
44
+ }
45
+
46
+ process.once('SIGINT', () => void shutdown('SIGINT'));
47
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
48
+ }
49
+
50
+ main().catch((err: unknown) => {
51
+ console.error('Fatal error:', err);
52
+ process.exit(1);
53
+ });