@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.
- package/CHANGELOG.md +36 -0
- package/README.md +284 -0
- package/dist/index.cjs +17385 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +873 -0
- package/dist/index.d.ts +873 -0
- package/dist/index.js +17344 -0
- package/dist/index.js.map +1 -0
- package/examples/README.md +142 -0
- package/examples/custom-channel.ts +206 -0
- package/examples/discord.ts +65 -0
- package/examples/multi-channel.ts +105 -0
- package/examples/slack.ts +66 -0
- package/examples/telegram.ts +53 -0
- package/package.json +77 -0
|
@@ -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
|
+
});
|