@0xmaxma/claude-gateway 1.4.8 → 1.5.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 +25 -2
- package/config.template.json +1 -0
- package/dist/agent/runner.d.ts +14 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +59 -0
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/gateway-router.d.ts +20 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +214 -0
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/line-webhook-router.d.ts +5 -0
- package/dist/api/line-webhook-router.d.ts.map +1 -1
- package/dist/api/line-webhook-router.js +63 -0
- package/dist/api/line-webhook-router.js.map +1 -1
- package/dist/apps/agent-manager.d.ts.map +1 -1
- package/dist/apps/agent-manager.js +14 -0
- package/dist/apps/agent-manager.js.map +1 -1
- package/dist/cli-viewer/pairing-store.d.ts +117 -0
- package/dist/cli-viewer/pairing-store.d.ts.map +1 -0
- package/dist/cli-viewer/pairing-store.js +196 -0
- package/dist/cli-viewer/pairing-store.js.map +1 -0
- package/dist/cli-viewer/telegram-initdata.d.ts +21 -0
- package/dist/cli-viewer/telegram-initdata.d.ts.map +1 -0
- package/dist/cli-viewer/telegram-initdata.js +74 -0
- package/dist/cli-viewer/telegram-initdata.js.map +1 -0
- package/dist/cli-viewer/url.d.ts +13 -0
- package/dist/cli-viewer/url.d.ts.map +1 -0
- package/dist/cli-viewer/url.js +27 -0
- package/dist/cli-viewer/url.js.map +1 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +7 -0
- package/dist/session/process.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/cli-viewer-ui.d.ts +37 -0
- package/dist/ui/cli-viewer-ui.d.ts.map +1 -0
- package/dist/ui/cli-viewer-ui.js +274 -0
- package/dist/ui/cli-viewer-ui.js.map +1 -0
- package/mcp/tools/discord/module.ts +99 -1
- package/mcp/tools/telegram/receiver-server.ts +41 -1
- package/package.json +1 -1
|
@@ -33,6 +33,18 @@ import type { DiscordMessageContext } from './types';
|
|
|
33
33
|
|
|
34
34
|
const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
|
|
35
35
|
|
|
36
|
+
/** AgentRunner callback base (origin of CLAUDE_CHANNEL_CALLBACK, "" when unset).
|
|
37
|
+
* Used to mint/approve `/cli` pairings via the runner, the same bridge the
|
|
38
|
+
* Telegram receiver uses for /models. */
|
|
39
|
+
function cliCallbackBase(): string {
|
|
40
|
+
try {
|
|
41
|
+
const u = new URL(process.env.CLAUDE_CHANNEL_CALLBACK ?? '');
|
|
42
|
+
return `${u.protocol}//${u.host}`;
|
|
43
|
+
} catch {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
export class DiscordModule implements ChannelModule {
|
|
37
49
|
id = 'discord' as ChannelId;
|
|
38
50
|
toolVisibility: ToolVisibility = 'current-channel';
|
|
@@ -281,6 +293,45 @@ export class DiscordModule implements ChannelModule {
|
|
|
281
293
|
}
|
|
282
294
|
|
|
283
295
|
// action === 'deliver'
|
|
296
|
+
// `/cli` opens the live terminal viewer: mint a pairing (link + Approve
|
|
297
|
+
// button) instead of forwarding the text to the agent. Only reached after
|
|
298
|
+
// gate() authorized this user, so it inherits the channel's access control.
|
|
299
|
+
const cliText = (msg.content ?? '').replace(/<@!?\d+>/g, '').trim().toLowerCase();
|
|
300
|
+
if (cliText === '/cli') {
|
|
301
|
+
const base = cliCallbackBase();
|
|
302
|
+
try {
|
|
303
|
+
const res = await fetch(base + '/command', {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
headers: { 'Content-Type': 'application/json' },
|
|
306
|
+
body: JSON.stringify({ command: 'cli_pair', payload: { channel: 'discord', user_id: context.userId } }),
|
|
307
|
+
});
|
|
308
|
+
const data = (await res.json()) as { success?: boolean; url?: string; code?: string; pairingId?: string; error?: string };
|
|
309
|
+
if (!data.success || !data.url || !data.pairingId) {
|
|
310
|
+
await msg.channel.send(
|
|
311
|
+
data.error === 'not_configured'
|
|
312
|
+
? 'Terminal viewer is not configured. Set gateway.publicUrl in config.json first.'
|
|
313
|
+
: 'Could not open the terminal viewer right now.',
|
|
314
|
+
).catch(() => {});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const components = [{
|
|
318
|
+
type: 1,
|
|
319
|
+
components: [
|
|
320
|
+
{ type: 2, style: 5, label: '\u{1F5A5} Open terminal', url: data.url },
|
|
321
|
+
{ type: 2, style: 2, label: `Approve ${data.code}`, custom_id: `cli:approve:${data.pairingId}` },
|
|
322
|
+
{ type: 2, style: 4, label: 'Deny', custom_id: `cli:deny:${data.pairingId}` },
|
|
323
|
+
],
|
|
324
|
+
}];
|
|
325
|
+
await msg.channel.send({
|
|
326
|
+
content: 'Live terminal viewer — open the link, confirm the code matches, then tap **Approve** to unlock (read-only by default).',
|
|
327
|
+
components,
|
|
328
|
+
}).catch(() => {});
|
|
329
|
+
} catch {
|
|
330
|
+
await msg.channel.send('Could not open the terminal viewer right now.').catch(() => {});
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
284
335
|
// Start typing indicator before handing off to runner
|
|
285
336
|
const channelId = context.channelId;
|
|
286
337
|
const typingFileDir = path.join(this.stateDir, 'typing');
|
|
@@ -347,12 +398,59 @@ export class DiscordModule implements ChannelModule {
|
|
|
347
398
|
message?: { id: string };
|
|
348
399
|
client: { user?: { id: string } };
|
|
349
400
|
reply(opts: { content: string; ephemeral: boolean }): Promise<unknown>;
|
|
350
|
-
update(opts: { components: unknown[] }): Promise<unknown>;
|
|
401
|
+
update(opts: { components: unknown[]; content?: string }): Promise<unknown>;
|
|
351
402
|
}
|
|
352
403
|
this.client.on('interactionCreate', async (interaction: ButtonInteraction) => {
|
|
353
404
|
try {
|
|
354
405
|
if (!interaction.isButton?.()) return;
|
|
355
406
|
|
|
407
|
+
// `/cli` approve/deny — unlock (or reject) a pending terminal-viewer
|
|
408
|
+
// pairing. Re-runs the access gate on the tapping user, then relays the
|
|
409
|
+
// decision to the runner. The pairing itself checks the user id matches.
|
|
410
|
+
const cliM = /^cli:(approve|deny):([0-9a-f]{36})$/.exec(interaction.customId ?? '');
|
|
411
|
+
if (cliM) {
|
|
412
|
+
const isDM = !interaction.guildId;
|
|
413
|
+
const isThread = interaction.channel?.isThread?.() ?? false;
|
|
414
|
+
const context: DiscordMessageContext = {
|
|
415
|
+
guildId: interaction.guildId ?? null,
|
|
416
|
+
channelId: interaction.channelId,
|
|
417
|
+
threadId: isThread ? interaction.channelId : null,
|
|
418
|
+
userId: interaction.user.id,
|
|
419
|
+
username: interaction.user.username,
|
|
420
|
+
messageId: interaction.message?.id ?? '',
|
|
421
|
+
isDM,
|
|
422
|
+
isThread,
|
|
423
|
+
mentionsBot: true,
|
|
424
|
+
};
|
|
425
|
+
const access = loadAccessFn();
|
|
426
|
+
const result = gate(access, context, saveAccessFn, () => randomBytes(3).toString('hex'));
|
|
427
|
+
if (result.action !== 'deliver') {
|
|
428
|
+
await interaction.reply({ content: 'Not authorized.', ephemeral: true }).catch(() => {});
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const deny = cliM[1] === 'deny';
|
|
432
|
+
const base = cliCallbackBase();
|
|
433
|
+
let ok = false;
|
|
434
|
+
try {
|
|
435
|
+
const res = await fetch(base + '/command', {
|
|
436
|
+
method: 'POST',
|
|
437
|
+
headers: { 'Content-Type': 'application/json' },
|
|
438
|
+
body: JSON.stringify({
|
|
439
|
+
command: 'cli_approve',
|
|
440
|
+
payload: { channel: 'discord', pairing_id: cliM[2], user_id: interaction.user.id, deny },
|
|
441
|
+
}),
|
|
442
|
+
});
|
|
443
|
+
ok = !!((await res.json()) as { success?: boolean }).success;
|
|
444
|
+
} catch {}
|
|
445
|
+
const content = deny
|
|
446
|
+
? '\u{1F6AB} Denied.'
|
|
447
|
+
: ok
|
|
448
|
+
? '✅ Approved — return to the browser.'
|
|
449
|
+
: '⚠️ Could not approve (the link may have expired). Send /cli again.';
|
|
450
|
+
await interaction.update({ components: [], content }).catch(() => {});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
356
454
|
// Cancel button: send ESC sentinel to dismiss the pending menu cleanly.
|
|
357
455
|
if ((interaction.customId ?? '') === 'menu:cancel') {
|
|
358
456
|
const isDM = !interaction.guildId;
|
|
@@ -881,6 +881,7 @@ const BOT_COMMANDS = [
|
|
|
881
881
|
{ command: 'restart', description: 'Graceful restart session' },
|
|
882
882
|
{ command: 'model', description: 'Show current AI model' },
|
|
883
883
|
{ command: 'models', description: 'Switch AI model' },
|
|
884
|
+
{ command: 'cli', description: 'Open the live terminal viewer' },
|
|
884
885
|
{ command: 'start', description: 'Welcome and setup guide' },
|
|
885
886
|
{ command: 'status', description: 'Check your pairing status' },
|
|
886
887
|
{ command: 'help', description: 'What this bot can do' },
|
|
@@ -1146,7 +1147,8 @@ bot.command('help', async ctx => {
|
|
|
1146
1147
|
`/restart — graceful restart session\n\n` +
|
|
1147
1148
|
`*Agent*\n` +
|
|
1148
1149
|
`/model — show current AI model\n` +
|
|
1149
|
-
`/models — switch AI model\n
|
|
1150
|
+
`/models — switch AI model\n` +
|
|
1151
|
+
`/cli — open the live terminal viewer\n\n` +
|
|
1150
1152
|
`*Account*\n` +
|
|
1151
1153
|
`/start — pairing instructions\n` +
|
|
1152
1154
|
`/status — check your pairing state`,
|
|
@@ -1238,6 +1240,44 @@ bot.command('models', async ctx => {
|
|
|
1238
1240
|
}
|
|
1239
1241
|
})
|
|
1240
1242
|
|
|
1243
|
+
// /cli — open the live terminal viewer as a Telegram Mini App. The Mini App
|
|
1244
|
+
// page reads Telegram's signed initData and the gateway verifies it (HMAC with
|
|
1245
|
+
// this bot's token), so nothing secret rides in the URL. Private chat + allowlist
|
|
1246
|
+
// only, matching every other command.
|
|
1247
|
+
bot.command('cli', async ctx => {
|
|
1248
|
+
if (ctx.chat?.type !== 'private') return
|
|
1249
|
+
const access = loadAccess()
|
|
1250
|
+
if (!access.allowFrom.includes(String(ctx.from!.id))) return
|
|
1251
|
+
if (!CALLBACK_URL_BASE) return
|
|
1252
|
+
|
|
1253
|
+
try {
|
|
1254
|
+
const res = await fetch(CALLBACK_URL_BASE + '/command', {
|
|
1255
|
+
method: 'POST',
|
|
1256
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1257
|
+
body: JSON.stringify({
|
|
1258
|
+
command: 'cli_pair',
|
|
1259
|
+
payload: { channel: 'telegram', user_id: String(ctx.from!.id) },
|
|
1260
|
+
}),
|
|
1261
|
+
})
|
|
1262
|
+
const data = (await res.json()) as { success?: boolean; url?: string; error?: string }
|
|
1263
|
+
if (!data.success || !data.url) {
|
|
1264
|
+
if (data.error === 'not_configured') {
|
|
1265
|
+
await ctx.reply('Terminal viewer is not configured. Set gateway.publicUrl in config.json first.')
|
|
1266
|
+
} else {
|
|
1267
|
+
await ctx.reply('Could not open the terminal viewer right now.')
|
|
1268
|
+
}
|
|
1269
|
+
return
|
|
1270
|
+
}
|
|
1271
|
+
// web_app button launches the Mini App; Telegram requires an HTTPS URL.
|
|
1272
|
+
const keyboard = new InlineKeyboard().webApp('\u{1F5A5} Open terminal', data.url)
|
|
1273
|
+
await ctx.reply('Live terminal viewer (read-only by default — toggle input inside):', {
|
|
1274
|
+
reply_markup: keyboard,
|
|
1275
|
+
})
|
|
1276
|
+
} catch {
|
|
1277
|
+
await ctx.reply('Could not open the terminal viewer right now.')
|
|
1278
|
+
}
|
|
1279
|
+
})
|
|
1280
|
+
|
|
1241
1281
|
// /compact — show compact confirmation keyboard (receiver mode only)
|
|
1242
1282
|
bot.command('compact', async ctx => {
|
|
1243
1283
|
if (ctx.chat?.type !== 'private') return
|