@0xmaxma/claude-gateway 1.0.0
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/README.md +791 -0
- package/config.template.json +87 -0
- package/dist/agent/context-isolation.d.ts +28 -0
- package/dist/agent/context-isolation.d.ts.map +1 -0
- package/dist/agent/context-isolation.js +80 -0
- package/dist/agent/context-isolation.js.map +1 -0
- package/dist/agent/runner.d.ts +211 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +1802 -0
- package/dist/agent/runner.js.map +1 -0
- package/dist/agent/workspace-loader.d.ts +31 -0
- package/dist/agent/workspace-loader.d.ts.map +1 -0
- package/dist/agent/workspace-loader.js +207 -0
- package/dist/agent/workspace-loader.js.map +1 -0
- package/dist/api/auth.d.ts +25 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +77 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/cron-router.d.ts +22 -0
- package/dist/api/cron-router.d.ts.map +1 -0
- package/dist/api/cron-router.js +197 -0
- package/dist/api/cron-router.js.map +1 -0
- package/dist/api/gateway-router.d.ts +52 -0
- package/dist/api/gateway-router.d.ts.map +1 -0
- package/dist/api/gateway-router.js +215 -0
- package/dist/api/gateway-router.js.map +1 -0
- package/dist/api/router.d.ts +5 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +1098 -0
- package/dist/api/router.js.map +1 -0
- package/dist/api/skills-router.d.ts +5 -0
- package/dist/api/skills-router.d.ts.map +1 -0
- package/dist/api/skills-router.js +424 -0
- package/dist/api/skills-router.js.map +1 -0
- package/dist/api/workspace-router.d.ts +4 -0
- package/dist/api/workspace-router.d.ts.map +1 -0
- package/dist/api/workspace-router.js +149 -0
- package/dist/api/workspace-router.js.map +1 -0
- package/dist/config/loader.d.ts +16 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +235 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/migrator.d.ts +70 -0
- package/dist/config/migrator.d.ts.map +1 -0
- package/dist/config/migrator.js +343 -0
- package/dist/config/migrator.js.map +1 -0
- package/dist/config/watcher.d.ts +30 -0
- package/dist/config/watcher.d.ts.map +1 -0
- package/dist/config/watcher.js +170 -0
- package/dist/config/watcher.js.map +1 -0
- package/dist/cron/manager.d.ts +48 -0
- package/dist/cron/manager.d.ts.map +1 -0
- package/dist/cron/manager.js +580 -0
- package/dist/cron/manager.js.map +1 -0
- package/dist/cron/scheduler.d.ts +50 -0
- package/dist/cron/scheduler.d.ts.map +1 -0
- package/dist/cron/scheduler.js +231 -0
- package/dist/cron/scheduler.js.map +1 -0
- package/dist/discord/receiver.d.ts +17 -0
- package/dist/discord/receiver.d.ts.map +1 -0
- package/dist/discord/receiver.js +108 -0
- package/dist/discord/receiver.js.map +1 -0
- package/dist/heartbeat/history.d.ts +24 -0
- package/dist/heartbeat/history.d.ts.map +1 -0
- package/dist/heartbeat/history.js +50 -0
- package/dist/heartbeat/history.js.map +1 -0
- package/dist/heartbeat/parser.d.ts +26 -0
- package/dist/heartbeat/parser.d.ts.map +1 -0
- package/dist/heartbeat/parser.js +283 -0
- package/dist/heartbeat/parser.js.map +1 -0
- package/dist/history/cleanup.d.ts +21 -0
- package/dist/history/cleanup.d.ts.map +1 -0
- package/dist/history/cleanup.js +171 -0
- package/dist/history/cleanup.js.map +1 -0
- package/dist/history/db.d.ts +27 -0
- package/dist/history/db.d.ts.map +1 -0
- package/dist/history/db.js +323 -0
- package/dist/history/db.js.map +1 -0
- package/dist/history/media-store.d.ts +27 -0
- package/dist/history/media-store.d.ts.map +1 -0
- package/dist/history/media-store.js +158 -0
- package/dist/history/media-store.js.map +1 -0
- package/dist/history/types.d.ts +62 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/history/types.js +3 -0
- package/dist/history/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +483 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +81 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/manager.d.ts +45 -0
- package/dist/memory/manager.d.ts.map +1 -0
- package/dist/memory/manager.js +178 -0
- package/dist/memory/manager.js.map +1 -0
- package/dist/session/compactor.d.ts +22 -0
- package/dist/session/compactor.d.ts.map +1 -0
- package/dist/session/compactor.js +151 -0
- package/dist/session/compactor.js.map +1 -0
- package/dist/session/process.d.ts +76 -0
- package/dist/session/process.d.ts.map +1 -0
- package/dist/session/process.js +729 -0
- package/dist/session/process.js.map +1 -0
- package/dist/session/store.d.ts +102 -0
- package/dist/session/store.d.ts.map +1 -0
- package/dist/session/store.js +533 -0
- package/dist/session/store.js.map +1 -0
- package/dist/skills/index.d.ts +9 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +15 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/invoker.d.ts +16 -0
- package/dist/skills/invoker.d.ts.map +1 -0
- package/dist/skills/invoker.js +36 -0
- package/dist/skills/invoker.js.map +1 -0
- package/dist/skills/loader.d.ts +23 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +183 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/parser.d.ts +55 -0
- package/dist/skills/parser.d.ts.map +1 -0
- package/dist/skills/parser.js +178 -0
- package/dist/skills/parser.js.map +1 -0
- package/dist/skills/sync.d.ts +10 -0
- package/dist/skills/sync.d.ts.map +1 -0
- package/dist/skills/sync.js +130 -0
- package/dist/skills/sync.js.map +1 -0
- package/dist/skills/watcher.d.ts +16 -0
- package/dist/skills/watcher.d.ts.map +1 -0
- package/dist/skills/watcher.js +25 -0
- package/dist/skills/watcher.js.map +1 -0
- package/dist/telegram/markdown.d.ts +25 -0
- package/dist/telegram/markdown.d.ts.map +1 -0
- package/dist/telegram/markdown.js +237 -0
- package/dist/telegram/markdown.js.map +1 -0
- package/dist/telegram/receiver.d.ts +17 -0
- package/dist/telegram/receiver.d.ts.map +1 -0
- package/dist/telegram/receiver.js +104 -0
- package/dist/telegram/receiver.js.map +1 -0
- package/dist/types.d.ts +224 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/web-ui.d.ts +6 -0
- package/dist/ui/web-ui.d.ts.map +1 -0
- package/dist/ui/web-ui.js +227 -0
- package/dist/ui/web-ui.js.map +1 -0
- package/dist/watch/factory.d.ts +26 -0
- package/dist/watch/factory.d.ts.map +1 -0
- package/dist/watch/factory.js +43 -0
- package/dist/watch/factory.js.map +1 -0
- package/dist/webhook/manager.d.ts +34 -0
- package/dist/webhook/manager.d.ts.map +1 -0
- package/dist/webhook/manager.js +120 -0
- package/dist/webhook/manager.js.map +1 -0
- package/mcp/bun.lock +264 -0
- package/mcp/channel-manager.ts +115 -0
- package/mcp/package.json +15 -0
- package/mcp/router.ts +69 -0
- package/mcp/server.ts +129 -0
- package/mcp/tools/agent/handlers.ts +578 -0
- package/mcp/tools/agent/module.ts +163 -0
- package/mcp/tools/browser/module.ts +321 -0
- package/mcp/tools/browser/skills/open-browser/SKILL.md +89 -0
- package/mcp/tools/cron/client.ts +65 -0
- package/mcp/tools/cron/module.ts +158 -0
- package/mcp/tools/cron/skills/cron/SKILL.md +46 -0
- package/mcp/tools/discord/access.ts +197 -0
- package/mcp/tools/discord/client.ts +31 -0
- package/mcp/tools/discord/commands.ts +48 -0
- package/mcp/tools/discord/inbound.ts +53 -0
- package/mcp/tools/discord/module.ts +442 -0
- package/mcp/tools/discord/outbound.ts +90 -0
- package/mcp/tools/discord/receiver-server.ts +58 -0
- package/mcp/tools/discord/skills/access/SKILL.md +86 -0
- package/mcp/tools/discord/skills/configure/SKILL.md +53 -0
- package/mcp/tools/discord/threading.ts +37 -0
- package/mcp/tools/discord/types.ts +106 -0
- package/mcp/tools/skills/handlers.ts +196 -0
- package/mcp/tools/skills/module.ts +163 -0
- package/mcp/tools/telegram/module.ts +454 -0
- package/mcp/tools/telegram/pure.ts +224 -0
- package/mcp/tools/telegram/receiver-server.ts +1656 -0
- package/mcp/tools/telegram/skills/access/SKILL.md +169 -0
- package/mcp/tools/telegram/skills/configure/SKILL.md +118 -0
- package/mcp/tools/telegram/typing.ts +327 -0
- package/mcp/types.ts +128 -0
- package/package.json +62 -0
- package/resource/claude_gateway.svg +112 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { writeTelegramAccess, writeDiscordAccess } from '../../../lib/pairing';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Regex constants (exported for tests)
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export const NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]{1,31}$/;
|
|
11
|
+
export const TELEGRAM_TOKEN_REGEX = /^\d{8,12}:[A-Za-z0-9_-]{35,}$/;
|
|
12
|
+
export const DISCORD_TOKEN_REGEX = /^\w{24,}\.\w{6}\.[\w-]{27,}$/;
|
|
13
|
+
// Telegram user IDs: 6–15 decimal digits
|
|
14
|
+
export const TELEGRAM_USER_ID_REGEX = /^\d{6,15}$/;
|
|
15
|
+
// Discord Snowflakes: 17–19 decimal digits
|
|
16
|
+
export const DISCORD_USER_ID_REGEX = /^\d{17,19}$/;
|
|
17
|
+
|
|
18
|
+
const FILENAME_SAFE_REGEX = /^[A-Z][A-Z0-9_-]*\.md$/i;
|
|
19
|
+
const STANDARD_STUB_FILES = ['HEARTBEAT.md', 'MEMORY.md', 'SOUL.md', 'USER.md'];
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Path helpers (local copies — same as scripts/create-agent.ts)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export function expandHome(p: string): string {
|
|
26
|
+
if (p.startsWith('~/') || p === '~') {
|
|
27
|
+
return path.join(os.homedir(), p.slice(2));
|
|
28
|
+
}
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function configPath(): string {
|
|
33
|
+
const envPath = process.env['GATEWAY_CONFIG'];
|
|
34
|
+
if (envPath) return expandHome(envPath);
|
|
35
|
+
return path.join(os.homedir(), '.claude-gateway', 'config.json');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function gatewayDir(): string {
|
|
39
|
+
return path.dirname(configPath());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function agentRootDir(agentId: string): string {
|
|
43
|
+
return path.join(gatewayDir(), 'agents', agentId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function workspaceDir(agentId: string): string {
|
|
47
|
+
return path.join(agentRootDir(agentId), 'workspace');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Config I/O
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
interface RawConfig {
|
|
55
|
+
gateway: { logDir: string; timezone: string };
|
|
56
|
+
agents: RawAgentEntry[];
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface RawAgentEntry {
|
|
61
|
+
id: string;
|
|
62
|
+
description: string;
|
|
63
|
+
workspace: string;
|
|
64
|
+
env: string;
|
|
65
|
+
telegram?: { botToken: string };
|
|
66
|
+
discord?: { botToken: string };
|
|
67
|
+
claude: { model: string; dangerouslySkipPermissions: boolean; extraFlags: string[] };
|
|
68
|
+
signatureEmoji?: string;
|
|
69
|
+
[key: string]: unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadRawConfig(): RawConfig {
|
|
73
|
+
const cp = configPath();
|
|
74
|
+
return JSON.parse(fs.readFileSync(cp, 'utf8')) as RawConfig;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function saveRawConfig(config: RawConfig): void {
|
|
78
|
+
const cp = configPath();
|
|
79
|
+
fs.mkdirSync(path.dirname(cp), { recursive: true });
|
|
80
|
+
fs.writeFileSync(cp, JSON.stringify(config, null, 2), 'utf8');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Token verification (fetch — Bun native, no https module)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
export async function verifyTelegramToken(token: string): Promise<{ ok: boolean; username: string }> {
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`https://api.telegram.org/bot${token}/getMe`);
|
|
90
|
+
const json = await res.json() as { ok: boolean; result?: { username?: string } };
|
|
91
|
+
if (json.ok && json.result?.username) {
|
|
92
|
+
return { ok: true, username: json.result.username };
|
|
93
|
+
}
|
|
94
|
+
return { ok: false, username: '' };
|
|
95
|
+
} catch {
|
|
96
|
+
return { ok: false, username: '' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function verifyDiscordToken(token: string): Promise<{ ok: boolean; username: string }> {
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch('https://discord.com/api/v10/users/@me', {
|
|
103
|
+
headers: { Authorization: `Bot ${token}` },
|
|
104
|
+
});
|
|
105
|
+
const json = await res.json() as { username?: string };
|
|
106
|
+
return { ok: res.status === 200, username: json.username ?? '' };
|
|
107
|
+
} catch {
|
|
108
|
+
return { ok: false, username: '' };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Helpers
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function firstNonEmptyLine(text: string): string {
|
|
117
|
+
for (const line of text.split('\n')) {
|
|
118
|
+
const trimmed = line.replace(/^#+\s*/, '').trim();
|
|
119
|
+
if (trimmed) return trimmed;
|
|
120
|
+
}
|
|
121
|
+
return text.trim().slice(0, 80);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function envVarName(agentId: string, channel: 'telegram' | 'discord'): string {
|
|
125
|
+
const base = agentId.toUpperCase().replace(/-/g, '_');
|
|
126
|
+
return channel === 'telegram' ? `${base}_BOT_TOKEN` : `${base}_DISCORD_BOT_TOKEN`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function howToFindUserId(channel: 'telegram' | 'discord'): string {
|
|
130
|
+
const tool = channel === 'telegram' ? 'telegram:access' : 'discord:discord-access';
|
|
131
|
+
return `No user_id provided — allowFrom is empty. Use the ${tool} tool to add users later.`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// agent_create
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
export interface CreateAgentArgs {
|
|
139
|
+
id: string;
|
|
140
|
+
description: string;
|
|
141
|
+
channel: 'telegram' | 'discord';
|
|
142
|
+
bot_token: string;
|
|
143
|
+
/** @deprecated use telegram_user_id or discord_user_id */
|
|
144
|
+
user_id?: string;
|
|
145
|
+
telegram_user_id?: string;
|
|
146
|
+
discord_user_id?: string;
|
|
147
|
+
dm_policy?: 'open' | 'allowlist' | 'pairing';
|
|
148
|
+
model?: string;
|
|
149
|
+
signature_emoji?: string;
|
|
150
|
+
agents_md?: string;
|
|
151
|
+
soul_md?: string;
|
|
152
|
+
user_md?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function createAgent(args: CreateAgentArgs): Promise<string> {
|
|
156
|
+
const { id, description, channel, bot_token } = args;
|
|
157
|
+
const dmPolicy = args.dm_policy ?? 'allowlist';
|
|
158
|
+
const model = args.model ?? 'claude-sonnet-4-6';
|
|
159
|
+
|
|
160
|
+
// 1. Validate id
|
|
161
|
+
if (!NAME_REGEX.test(id)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Invalid agent id "${id}": must start with a letter, 2-32 chars, letters/numbers/underscores/hyphens only`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const agentId = id.toLowerCase();
|
|
167
|
+
|
|
168
|
+
// 2. Validate description
|
|
169
|
+
if (!description || description.trim().length === 0) {
|
|
170
|
+
throw new Error('description is required and must not be empty');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 3. Validate channel
|
|
174
|
+
if (channel !== 'telegram' && channel !== 'discord') {
|
|
175
|
+
throw new Error(`Unsupported channel "${channel}". Must be "telegram" or "discord"`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 4. Validate token format
|
|
179
|
+
if (channel === 'telegram' && !TELEGRAM_TOKEN_REGEX.test(bot_token)) {
|
|
180
|
+
throw new Error('Invalid Telegram bot token format. Expected: 123456789:AAHfiq...');
|
|
181
|
+
}
|
|
182
|
+
if (channel === 'discord' && !DISCORD_TOKEN_REGEX.test(bot_token)) {
|
|
183
|
+
throw new Error('Invalid Discord bot token format. Expected: MTAxMTk....AAAA.xxx...');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5. Check uniqueness in config.json before any file writes
|
|
187
|
+
let config: RawConfig;
|
|
188
|
+
try {
|
|
189
|
+
config = loadRawConfig();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
throw new Error(`Cannot read config.json: ${(err as Error).message}`);
|
|
192
|
+
}
|
|
193
|
+
if (config.agents.some((a) => a.id === agentId)) {
|
|
194
|
+
throw new Error(`Agent "${agentId}" already exists in config.json`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 6. Verify token via upstream API (fail fast before any file writes)
|
|
198
|
+
let botUsername: string;
|
|
199
|
+
if (channel === 'telegram') {
|
|
200
|
+
const result = await verifyTelegramToken(bot_token);
|
|
201
|
+
if (!result.ok) {
|
|
202
|
+
throw new Error('Telegram token verification failed: getMe returned not ok. Check the token.');
|
|
203
|
+
}
|
|
204
|
+
botUsername = result.username;
|
|
205
|
+
} else {
|
|
206
|
+
const result = await verifyDiscordToken(bot_token);
|
|
207
|
+
if (!result.ok) {
|
|
208
|
+
throw new Error('Discord token verification failed: /api/v10/users/@me returned non-200. Check the token.');
|
|
209
|
+
}
|
|
210
|
+
botUsername = result.username;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 7. Create workspace directory
|
|
214
|
+
const wsDir = workspaceDir(agentId);
|
|
215
|
+
fs.mkdirSync(wsDir, { recursive: true });
|
|
216
|
+
|
|
217
|
+
// 8. Write workspace files
|
|
218
|
+
const agentsMdContent =
|
|
219
|
+
args.agents_md ??
|
|
220
|
+
`# Agent: ${agentId.charAt(0).toUpperCase() + agentId.slice(1)}\n\n${description}`;
|
|
221
|
+
fs.writeFileSync(path.join(wsDir, 'AGENTS.md'), agentsMdContent, 'utf8');
|
|
222
|
+
|
|
223
|
+
if (args.soul_md) {
|
|
224
|
+
fs.writeFileSync(path.join(wsDir, 'SOUL.md'), args.soul_md, 'utf8');
|
|
225
|
+
}
|
|
226
|
+
if (args.user_md) {
|
|
227
|
+
fs.writeFileSync(path.join(wsDir, 'USER.md'), args.user_md, 'utf8');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Write stub files for any standard files not yet present
|
|
231
|
+
for (const stub of STANDARD_STUB_FILES) {
|
|
232
|
+
const stubPath = path.join(wsDir, stub);
|
|
233
|
+
if (!fs.existsSync(stubPath)) {
|
|
234
|
+
fs.writeFileSync(stubPath, '', 'utf8');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 9. Write .env for the agent (mode 0o600 — owner read-only)
|
|
239
|
+
const envVar = envVarName(agentId, channel);
|
|
240
|
+
const agentEnvFile = path.join(agentRootDir(agentId), '.env');
|
|
241
|
+
fs.writeFileSync(agentEnvFile, `${envVar}=${bot_token}\n`, { mode: 0o600 });
|
|
242
|
+
|
|
243
|
+
// 10. Create channel state dir + write access.json immediately (no pairing flow)
|
|
244
|
+
const stateDir = path.join(wsDir, `.${channel}-state`);
|
|
245
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
246
|
+
|
|
247
|
+
const rawUserId = (channel === 'telegram'
|
|
248
|
+
? (args.telegram_user_id ?? args.user_id)
|
|
249
|
+
: (args.discord_user_id ?? args.user_id)
|
|
250
|
+
)?.trim() ?? '';
|
|
251
|
+
|
|
252
|
+
// Validate ID format to prevent cross-channel mix-ups
|
|
253
|
+
if (rawUserId) {
|
|
254
|
+
if (channel === 'telegram' && !TELEGRAM_USER_ID_REGEX.test(rawUserId)) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Invalid telegram_user_id "${rawUserId}": must be 6–15 digits. ` +
|
|
257
|
+
`Discord Snowflake IDs (17–19 digits) are NOT valid Telegram IDs.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (channel === 'discord' && !DISCORD_USER_ID_REGEX.test(rawUserId)) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Invalid discord_user_id "${rawUserId}": must be 17–19 digits (Discord Snowflake). ` +
|
|
263
|
+
`Telegram user IDs (6–15 digits) are NOT valid Discord IDs.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const userId = rawUserId;
|
|
268
|
+
if (channel === 'telegram') {
|
|
269
|
+
if (userId) {
|
|
270
|
+
writeTelegramAccess(stateDir, userId);
|
|
271
|
+
} else {
|
|
272
|
+
fs.writeFileSync(
|
|
273
|
+
path.join(stateDir, 'access.json'),
|
|
274
|
+
JSON.stringify({ dmPolicy: 'allowlist', allowFrom: [], groups: {}, pending: {} }, null, 2),
|
|
275
|
+
{ mode: 0o600 }
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
if (userId) {
|
|
280
|
+
writeDiscordAccess(stateDir, userId);
|
|
281
|
+
} else {
|
|
282
|
+
fs.writeFileSync(
|
|
283
|
+
path.join(stateDir, 'access.json'),
|
|
284
|
+
JSON.stringify(
|
|
285
|
+
{ dmPolicy: 'allowlist', allowFrom: [], guildAllowlist: [], channelAllowlist: [], roleAllowlist: [], pending: {} },
|
|
286
|
+
null,
|
|
287
|
+
2
|
|
288
|
+
) + '\n',
|
|
289
|
+
{ mode: 0o600 }
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 11. Update config.json (gateway hot-adds the agent automatically)
|
|
295
|
+
const descriptionText = firstNonEmptyLine(agentsMdContent);
|
|
296
|
+
const newAgent: RawAgentEntry = {
|
|
297
|
+
id: agentId,
|
|
298
|
+
description: descriptionText,
|
|
299
|
+
workspace: wsDir.replace(os.homedir(), '~'),
|
|
300
|
+
env: '',
|
|
301
|
+
claude: {
|
|
302
|
+
model,
|
|
303
|
+
dangerouslySkipPermissions: true,
|
|
304
|
+
extraFlags: [],
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (channel === 'telegram') {
|
|
309
|
+
newAgent.telegram = { botToken: `\${${envVar}}` };
|
|
310
|
+
} else {
|
|
311
|
+
newAgent.discord = { botToken: `\${${envVar}}` };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (args.signature_emoji) {
|
|
315
|
+
newAgent.signatureEmoji = args.signature_emoji;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
config.agents.push(newAgent);
|
|
319
|
+
saveRawConfig(config);
|
|
320
|
+
|
|
321
|
+
const accessNote = userId
|
|
322
|
+
? `Access granted to user ID: ${userId}`
|
|
323
|
+
: howToFindUserId(channel);
|
|
324
|
+
|
|
325
|
+
return [
|
|
326
|
+
`Agent "${agentId}" created successfully!`,
|
|
327
|
+
`Bot: @${botUsername}`,
|
|
328
|
+
`Workspace: ${wsDir.replace(os.homedir(), '~')}`,
|
|
329
|
+
`Channel: ${channel} (dm_policy: ${dmPolicy})`,
|
|
330
|
+
'',
|
|
331
|
+
accessNote,
|
|
332
|
+
'',
|
|
333
|
+
'The gateway will detect the new agent and start it automatically (hot-add).',
|
|
334
|
+
'If the agent does not appear, restart the gateway: make restart (or: pm2 restart gateway)',
|
|
335
|
+
].join('\n');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// agent_update
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
export interface UpdateAgentArgs {
|
|
343
|
+
id: string;
|
|
344
|
+
action: 'add_channel' | 'remove_channel' | 'update_workspace_file';
|
|
345
|
+
channel?: 'telegram' | 'discord';
|
|
346
|
+
bot_token?: string;
|
|
347
|
+
/** @deprecated use telegram_user_id or discord_user_id */
|
|
348
|
+
user_id?: string;
|
|
349
|
+
telegram_user_id?: string;
|
|
350
|
+
discord_user_id?: string;
|
|
351
|
+
dm_policy?: 'open' | 'allowlist' | 'pairing';
|
|
352
|
+
filename?: string;
|
|
353
|
+
content?: string;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function updateAgent(args: UpdateAgentArgs): Promise<string> {
|
|
357
|
+
const { id, action } = args;
|
|
358
|
+
|
|
359
|
+
// Validate id
|
|
360
|
+
if (!NAME_REGEX.test(id)) {
|
|
361
|
+
throw new Error(`Invalid agent id "${id}"`);
|
|
362
|
+
}
|
|
363
|
+
const agentId = id.toLowerCase();
|
|
364
|
+
|
|
365
|
+
// Load config
|
|
366
|
+
let config: RawConfig;
|
|
367
|
+
try {
|
|
368
|
+
config = loadRawConfig();
|
|
369
|
+
} catch (err) {
|
|
370
|
+
throw new Error(`Cannot read config.json: ${(err as Error).message}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const agentIdx = config.agents.findIndex((a) => a.id === agentId);
|
|
374
|
+
if (agentIdx < 0) {
|
|
375
|
+
throw new Error(`Agent "${agentId}" not found in config.json`);
|
|
376
|
+
}
|
|
377
|
+
const agent = config.agents[agentIdx];
|
|
378
|
+
const wsDir = expandHome(agent.workspace);
|
|
379
|
+
|
|
380
|
+
// ── Action: add_channel ──────────────────────────────────────────────────
|
|
381
|
+
if (action === 'add_channel') {
|
|
382
|
+
const channel = args.channel;
|
|
383
|
+
const bot_token = args.bot_token;
|
|
384
|
+
const dmPolicy = args.dm_policy ?? 'allowlist';
|
|
385
|
+
|
|
386
|
+
if (!channel || (channel !== 'telegram' && channel !== 'discord')) {
|
|
387
|
+
throw new Error('add_channel requires channel: "telegram" or "discord"');
|
|
388
|
+
}
|
|
389
|
+
if (!bot_token) {
|
|
390
|
+
throw new Error('add_channel requires bot_token');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Assert channel not already configured
|
|
394
|
+
if (channel === 'telegram' && agent.telegram?.botToken) {
|
|
395
|
+
throw new Error(`Agent "${agentId}" already has a Telegram channel configured`);
|
|
396
|
+
}
|
|
397
|
+
if (channel === 'discord' && agent.discord?.botToken) {
|
|
398
|
+
throw new Error(`Agent "${agentId}" already has a Discord channel configured`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Validate token format
|
|
402
|
+
if (channel === 'telegram' && !TELEGRAM_TOKEN_REGEX.test(bot_token)) {
|
|
403
|
+
throw new Error('Invalid Telegram bot token format');
|
|
404
|
+
}
|
|
405
|
+
if (channel === 'discord' && !DISCORD_TOKEN_REGEX.test(bot_token)) {
|
|
406
|
+
throw new Error('Invalid Discord bot token format');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Verify token
|
|
410
|
+
let botUsername: string;
|
|
411
|
+
if (channel === 'telegram') {
|
|
412
|
+
const result = await verifyTelegramToken(bot_token);
|
|
413
|
+
if (!result.ok) throw new Error('Telegram token verification failed');
|
|
414
|
+
botUsername = result.username;
|
|
415
|
+
} else {
|
|
416
|
+
const result = await verifyDiscordToken(bot_token);
|
|
417
|
+
if (!result.ok) throw new Error('Discord token verification failed');
|
|
418
|
+
botUsername = result.username;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Append token to agent .env
|
|
422
|
+
const envVar = envVarName(agentId, channel);
|
|
423
|
+
const agentEnvFile = path.join(agentRootDir(agentId), '.env');
|
|
424
|
+
fs.mkdirSync(path.dirname(agentEnvFile), { recursive: true });
|
|
425
|
+
let existing = '';
|
|
426
|
+
try {
|
|
427
|
+
existing = fs.readFileSync(agentEnvFile, 'utf8');
|
|
428
|
+
} catch {}
|
|
429
|
+
if (!existing.includes(`${envVar}=`)) {
|
|
430
|
+
fs.appendFileSync(agentEnvFile, `${envVar}=${bot_token}\n`, { mode: 0o600 });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Create state dir + write access.json immediately (no pairing flow)
|
|
434
|
+
const stateDir = path.join(wsDir, `.${channel}-state`);
|
|
435
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
436
|
+
|
|
437
|
+
const rawAddUserId = (channel === 'telegram'
|
|
438
|
+
? (args.telegram_user_id ?? args.user_id)
|
|
439
|
+
: (args.discord_user_id ?? args.user_id)
|
|
440
|
+
)?.trim() ?? '';
|
|
441
|
+
|
|
442
|
+
// Validate ID format to prevent cross-channel mix-ups
|
|
443
|
+
if (rawAddUserId) {
|
|
444
|
+
if (channel === 'telegram' && !TELEGRAM_USER_ID_REGEX.test(rawAddUserId)) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`Invalid telegram_user_id "${rawAddUserId}": must be 6–15 digits. ` +
|
|
447
|
+
`Discord Snowflake IDs (17–19 digits) are NOT valid Telegram IDs.`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (channel === 'discord' && !DISCORD_USER_ID_REGEX.test(rawAddUserId)) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`Invalid discord_user_id "${rawAddUserId}": must be 17–19 digits (Discord Snowflake). ` +
|
|
453
|
+
`Telegram user IDs (6–15 digits) are NOT valid Discord IDs.`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const addUserId = rawAddUserId;
|
|
458
|
+
|
|
459
|
+
if (channel === 'telegram') {
|
|
460
|
+
if (addUserId) {
|
|
461
|
+
writeTelegramAccess(stateDir, addUserId);
|
|
462
|
+
} else {
|
|
463
|
+
fs.writeFileSync(
|
|
464
|
+
path.join(stateDir, 'access.json'),
|
|
465
|
+
JSON.stringify({ dmPolicy: 'allowlist', allowFrom: [], groups: {}, pending: {} }, null, 2),
|
|
466
|
+
{ mode: 0o600 }
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
if (addUserId) {
|
|
471
|
+
writeDiscordAccess(stateDir, addUserId);
|
|
472
|
+
} else {
|
|
473
|
+
fs.writeFileSync(
|
|
474
|
+
path.join(stateDir, 'access.json'),
|
|
475
|
+
JSON.stringify(
|
|
476
|
+
{ dmPolicy: 'allowlist', allowFrom: [], guildAllowlist: [], channelAllowlist: [], roleAllowlist: [], pending: {} },
|
|
477
|
+
null,
|
|
478
|
+
2
|
|
479
|
+
) + '\n',
|
|
480
|
+
{ mode: 0o600 }
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Update config.json (so gateway can hot-add)
|
|
486
|
+
if (channel === 'telegram') {
|
|
487
|
+
config.agents[agentIdx].telegram = { botToken: `\${${envVar}}` };
|
|
488
|
+
} else {
|
|
489
|
+
config.agents[agentIdx].discord = { botToken: `\${${envVar}}` };
|
|
490
|
+
}
|
|
491
|
+
saveRawConfig(config);
|
|
492
|
+
|
|
493
|
+
const addAccessNote = addUserId
|
|
494
|
+
? `Access granted to user ID: ${addUserId}`
|
|
495
|
+
: howToFindUserId(channel);
|
|
496
|
+
|
|
497
|
+
return [
|
|
498
|
+
`Channel "${channel}" added to agent "${agentId}". Bot: @${botUsername}`,
|
|
499
|
+
'',
|
|
500
|
+
addAccessNote,
|
|
501
|
+
'',
|
|
502
|
+
'If the channel does not connect, restart the gateway: make restart (or: pm2 restart gateway)',
|
|
503
|
+
].join('\n');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ── Action: remove_channel ───────────────────────────────────────────────
|
|
507
|
+
if (action === 'remove_channel') {
|
|
508
|
+
const channel = args.channel;
|
|
509
|
+
if (!channel || (channel !== 'telegram' && channel !== 'discord')) {
|
|
510
|
+
throw new Error('remove_channel requires channel: "telegram" or "discord"');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Assert channel exists on agent
|
|
514
|
+
if (channel === 'telegram' && !agent.telegram?.botToken) {
|
|
515
|
+
throw new Error(`Agent "${agentId}" does not have a Telegram channel configured`);
|
|
516
|
+
}
|
|
517
|
+
if (channel === 'discord' && !agent.discord?.botToken) {
|
|
518
|
+
throw new Error(`Agent "${agentId}" does not have a Discord channel configured`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Remove state dir
|
|
522
|
+
const stateDir = path.join(wsDir, `.${channel}-state`);
|
|
523
|
+
try {
|
|
524
|
+
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
525
|
+
} catch {}
|
|
526
|
+
|
|
527
|
+
// Strip token line from agent .env
|
|
528
|
+
const envVar = envVarName(agentId, channel);
|
|
529
|
+
const agentEnvFile = path.join(agentRootDir(agentId), '.env');
|
|
530
|
+
try {
|
|
531
|
+
const lines = fs
|
|
532
|
+
.readFileSync(agentEnvFile, 'utf8')
|
|
533
|
+
.split('\n')
|
|
534
|
+
.filter((l) => !l.startsWith(`${envVar}=`));
|
|
535
|
+
fs.writeFileSync(agentEnvFile, lines.join('\n'), { mode: 0o600 });
|
|
536
|
+
} catch {}
|
|
537
|
+
|
|
538
|
+
// Remove from config.json
|
|
539
|
+
if (channel === 'telegram') {
|
|
540
|
+
delete config.agents[agentIdx].telegram;
|
|
541
|
+
} else {
|
|
542
|
+
delete config.agents[agentIdx].discord;
|
|
543
|
+
}
|
|
544
|
+
saveRawConfig(config);
|
|
545
|
+
|
|
546
|
+
return `Channel "${channel}" removed from agent "${agentId}".`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ── Action: update_workspace_file ────────────────────────────────────────
|
|
550
|
+
if (action === 'update_workspace_file') {
|
|
551
|
+
const filename = args.filename;
|
|
552
|
+
const content = args.content;
|
|
553
|
+
|
|
554
|
+
if (!filename) throw new Error('update_workspace_file requires filename');
|
|
555
|
+
if (content === undefined) throw new Error('update_workspace_file requires content');
|
|
556
|
+
|
|
557
|
+
// Validate filename — no path traversal, no path components
|
|
558
|
+
if (
|
|
559
|
+
!FILENAME_SAFE_REGEX.test(filename) ||
|
|
560
|
+
filename.includes('/') ||
|
|
561
|
+
filename.includes('\\') ||
|
|
562
|
+
filename.includes('..')
|
|
563
|
+
) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
'Invalid filename. Must match /^[A-Z][A-Z0-9_-]*\\.md$/i with no path components'
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const filePath = path.join(wsDir, filename);
|
|
570
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
571
|
+
|
|
572
|
+
return `File "${filename}" updated in workspace for agent "${agentId}". Gateway workspace-watcher will auto-reload CLAUDE.md.`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
throw new Error(
|
|
576
|
+
`Unknown action "${action}". Must be one of: add_channel, remove_channel, update_workspace_file`
|
|
577
|
+
);
|
|
578
|
+
}
|