@0xmaxma/claude-gateway 1.3.7 → 1.3.9
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/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +63 -4
- package/dist/agent/runner.js.map +1 -1
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +8 -0
- package/dist/session/process.js.map +1 -1
- package/dist/shell/claude-pty-shell.js +100 -9
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/emitter.d.ts +16 -0
- package/dist/shell/emitter.d.ts.map +1 -1
- package/dist/shell/emitter.js +20 -0
- package/dist/shell/emitter.js.map +1 -1
- package/dist/shell/screen.d.ts +22 -0
- package/dist/shell/screen.d.ts.map +1 -1
- package/dist/shell/screen.js +26 -1
- package/dist/shell/screen.js.map +1 -1
- package/mcp/tools/discord/module.ts +36 -0
- package/mcp/tools/discord/outbound.ts +5 -0
- package/mcp/tools/telegram/menu-parser.ts +4 -0
- package/mcp/tools/telegram/receiver-server.ts +41 -4
- package/package.json +1 -1
|
@@ -281,6 +281,42 @@ export class DiscordModule implements ChannelModule {
|
|
|
281
281
|
this.client.on('interactionCreate', async (interaction: ButtonInteraction) => {
|
|
282
282
|
try {
|
|
283
283
|
if (!interaction.isButton?.()) return;
|
|
284
|
+
|
|
285
|
+
// Cancel button: send ESC sentinel to dismiss the pending menu cleanly.
|
|
286
|
+
if ((interaction.customId ?? '') === 'menu:cancel') {
|
|
287
|
+
const isDM = !interaction.guildId;
|
|
288
|
+
const isThread = interaction.channel?.isThread?.() ?? false;
|
|
289
|
+
const context: DiscordMessageContext = {
|
|
290
|
+
guildId: interaction.guildId ?? null,
|
|
291
|
+
channelId: interaction.channelId,
|
|
292
|
+
threadId: isThread ? interaction.channelId : null,
|
|
293
|
+
userId: interaction.user.id,
|
|
294
|
+
username: interaction.user.username,
|
|
295
|
+
messageId: interaction.message?.id ?? '',
|
|
296
|
+
isDM,
|
|
297
|
+
isThread,
|
|
298
|
+
};
|
|
299
|
+
const access = loadAccessFn();
|
|
300
|
+
const result = gate(access, context, saveAccessFn, () => randomBytes(3).toString('hex'));
|
|
301
|
+
if (result.action !== 'deliver') {
|
|
302
|
+
await interaction.reply({ content: 'Not authorized.', ephemeral: true }).catch(() => {});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
await interaction.update({ components: [] }).catch(() => {});
|
|
306
|
+
const inbound: InboundMessage = {
|
|
307
|
+
channel: 'discord',
|
|
308
|
+
accountId: interaction.client.user?.id ?? 'discord',
|
|
309
|
+
senderId: interaction.user.id,
|
|
310
|
+
chatId: interaction.channelId,
|
|
311
|
+
chatType: isDM ? 'direct' : 'group',
|
|
312
|
+
text: '__MENU_CANCEL__',
|
|
313
|
+
messageId: interaction.message?.id ?? '',
|
|
314
|
+
ts: Date.now(),
|
|
315
|
+
};
|
|
316
|
+
await handler(inbound);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
284
320
|
const m = /^choice:(\d+)$/.exec(interaction.customId ?? '');
|
|
285
321
|
if (!m) return;
|
|
286
322
|
|
|
@@ -88,6 +88,11 @@ export function buildChoiceComponents(options: Array<{ label: string }>): unknow
|
|
|
88
88
|
});
|
|
89
89
|
rows.push({ type: 1, components: buttons }); // ActionRow
|
|
90
90
|
}
|
|
91
|
+
// Always append a cancel button (Danger style) so the user can dismiss the
|
|
92
|
+
// menu cleanly. Sends ESC to the PTY without injecting text into Claude's context.
|
|
93
|
+
if (rows.length > 0 && rows.length < 5) {
|
|
94
|
+
rows.push({ type: 1, components: [{ type: 2, style: 4, label: '❌ Cancel', custom_id: 'menu:cancel' }] });
|
|
95
|
+
}
|
|
91
96
|
return rows;
|
|
92
97
|
}
|
|
93
98
|
|
|
@@ -33,5 +33,9 @@ export function parseMenuFileContent(raw: string): MenuMessage | null {
|
|
|
33
33
|
text: `${i + 1}. ${label}`.slice(0, 60),
|
|
34
34
|
callback_data: `choice:${i + 1}`,
|
|
35
35
|
}]);
|
|
36
|
+
// Always append a cancel button so the user can dismiss the menu without
|
|
37
|
+
// sending a numbered reply. Tapping it sends ESC to the PTY, clearing
|
|
38
|
+
// pendingMenu cleanly without injecting spurious text into Claude's context.
|
|
39
|
+
inline_keyboard.push([{ text: '❌ Cancel', callback_data: 'menu:cancel' }]);
|
|
36
40
|
return { text, inline_keyboard };
|
|
37
41
|
}
|
|
@@ -1153,6 +1153,13 @@ bot.command('clear', async ctx => {
|
|
|
1153
1153
|
).catch(() => {})
|
|
1154
1154
|
})
|
|
1155
1155
|
|
|
1156
|
+
// Shared auth check for all callback_query:data handlers.
|
|
1157
|
+
// Mirrors the text-reply path: sender must be in allowFrom.
|
|
1158
|
+
function isCallbackAuthorized(ctx: Context): boolean {
|
|
1159
|
+
const access = loadAccess()
|
|
1160
|
+
return access.allowFrom.includes(String(ctx.from?.id ?? ''))
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1156
1163
|
// Inline-button handler for permission requests. Callback data is
|
|
1157
1164
|
// `perm:allow:<id>`, `perm:deny:<id>`, or `perm:more:<id>`.
|
|
1158
1165
|
// Security mirrors the text-reply path: allowFrom must contain the sender.
|
|
@@ -1165,8 +1172,7 @@ bot.on('callback_query:data', async ctx => {
|
|
|
1165
1172
|
// text path: the tapper must be in allowFrom.
|
|
1166
1173
|
const choiceMatch = /^choice:(\d+)$/.exec(data)
|
|
1167
1174
|
if (choiceMatch) {
|
|
1168
|
-
|
|
1169
|
-
if (!access.allowFrom.includes(String(ctx.from.id))) {
|
|
1175
|
+
if (!isCallbackAuthorized(ctx)) {
|
|
1170
1176
|
await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
|
|
1171
1177
|
return
|
|
1172
1178
|
}
|
|
@@ -1198,11 +1204,42 @@ bot.on('callback_query:data', async ctx => {
|
|
|
1198
1204
|
return
|
|
1199
1205
|
}
|
|
1200
1206
|
|
|
1207
|
+
// Handle interactive-menu cancel: dismiss the pending menu without sending
|
|
1208
|
+
// any text to the session. Posts a special sentinel that the PTY wrapper
|
|
1209
|
+
// translates to ESC, clearing pendingMenu cleanly.
|
|
1210
|
+
if (data === 'menu:cancel') {
|
|
1211
|
+
if (!isCallbackAuthorized(ctx)) {
|
|
1212
|
+
await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
|
|
1213
|
+
return
|
|
1214
|
+
}
|
|
1215
|
+
const chat_id = String(ctx.callbackQuery.message?.chat.id ?? ctx.from.id)
|
|
1216
|
+
await ctx.editMessageReplyMarkup({ reply_markup: undefined }).catch(() => {})
|
|
1217
|
+
await ctx.answerCallbackQuery({ text: '✓ Cancelled' }).catch(() => {})
|
|
1218
|
+
const callbackUrl = process.env.CLAUDE_CHANNEL_CALLBACK
|
|
1219
|
+
if (callbackUrl) {
|
|
1220
|
+
fetch(callbackUrl, {
|
|
1221
|
+
method: 'POST',
|
|
1222
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1223
|
+
body: JSON.stringify({
|
|
1224
|
+
content: '__MENU_CANCEL__',
|
|
1225
|
+
meta: {
|
|
1226
|
+
chat_id,
|
|
1227
|
+
user: ctx.from.username ?? String(ctx.from.id),
|
|
1228
|
+
user_id: String(ctx.from.id),
|
|
1229
|
+
ts: new Date().toISOString(),
|
|
1230
|
+
},
|
|
1231
|
+
}),
|
|
1232
|
+
}).catch(err => {
|
|
1233
|
+
process.stderr.write(`telegram channel: menu cancel callback POST failed: ${err}\n`)
|
|
1234
|
+
})
|
|
1235
|
+
}
|
|
1236
|
+
return
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1201
1239
|
// Handle model selection callback: model:<model_id>
|
|
1202
1240
|
const modelMatch = /^model:(.+)$/.exec(data)
|
|
1203
1241
|
if (modelMatch) {
|
|
1204
|
-
|
|
1205
|
-
if (!access.allowFrom.includes(String(ctx.from.id))) {
|
|
1242
|
+
if (!isCallbackAuthorized(ctx)) {
|
|
1206
1243
|
await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
|
|
1207
1244
|
return
|
|
1208
1245
|
}
|