appback-remoteagent 0.17.1 → 0.20.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/dist/bot.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import fsSync from "node:fs";
3
3
  import fs from "node:fs/promises";
4
+ import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { randomUUID } from "node:crypto";
6
7
  import { promisify } from "node:util";
@@ -10,6 +11,7 @@ import { ProviderSetupService } from "./services/provider-setup-service.js";
10
11
  import { RemoteShellService } from "./services/remote-shell-service.js";
11
12
  import { AgentMemoryService } from "./services/agent-memory-service.js";
12
13
  import { WorkspaceCleanupService } from "./services/workspace-cleanup-service.js";
14
+ import { exportSecrets } from "./services/secret-transfer-service.js";
13
15
  import { deleteTelegramCommandMenu, setTelegramCommandMenu } from "./telegram-command-menu.js";
14
16
  const execFileAsync = promisify(execFile);
15
17
  const HELP_TEXT = [
@@ -32,7 +34,7 @@ const HELP_TEXT = [
32
34
  "/state [clear|note <text>]",
33
35
  "/artifacts list|cleanup <days>",
34
36
  "/cleanup",
35
- "/secret set|list|remove",
37
+ "/secret set|list|remove|export",
36
38
  "/docs pin|find|list|remove|reinforce",
37
39
  "/macro set|list|remove|<alias|number>",
38
40
  "/매크로 set|list|remove|<alias|number>",
@@ -104,6 +106,41 @@ const workLoopTails = new Map();
104
106
  const workLoopGenerations = new Map();
105
107
  const queuedWorkLoops = new Map();
106
108
  let nextQueuedWorkSequence = 1;
109
+ class InlineActionRegistry {
110
+ records = new Map();
111
+ ttlMs = 6 * 60 * 60 * 1000;
112
+ maxRecords = 1_000;
113
+ register(action, chatId, userId) {
114
+ this.prune();
115
+ while (this.records.size >= this.maxRecords) {
116
+ const oldest = this.records.keys().next().value;
117
+ if (!oldest) {
118
+ break;
119
+ }
120
+ this.records.delete(oldest);
121
+ }
122
+ const id = randomUUID().replace(/-/g, "").slice(0, 20);
123
+ this.records.set(id, { action, chatId, userId, createdAt: Date.now() });
124
+ return `remoteagent:action:${id}`;
125
+ }
126
+ resolve(data, chatId, userId) {
127
+ this.prune();
128
+ const match = /^remoteagent:action:([a-f0-9]{20})$/i.exec(data);
129
+ const record = match ? this.records.get(match[1]) : undefined;
130
+ if (!record || record.chatId !== chatId || record.userId !== userId) {
131
+ return undefined;
132
+ }
133
+ return record.action;
134
+ }
135
+ prune() {
136
+ const expiresBefore = Date.now() - this.ttlMs;
137
+ for (const [id, record] of this.records) {
138
+ if (record.createdAt < expiresBefore) {
139
+ this.records.delete(id);
140
+ }
141
+ }
142
+ }
143
+ }
107
144
  const REPORT_CONTINUE_PROMPT = [
108
145
  "Continue the same task now.",
109
146
  "Do more concrete work before replying again.",
@@ -185,6 +222,7 @@ class AutoContinueController {
185
222
  }
186
223
  export function createBot(token, bridge, botManagement, botInfo) {
187
224
  const bot = new Bot(token, { botInfo });
225
+ const inlineActions = new InlineActionRegistry();
188
226
  const autoContinue = new AutoContinueController(path.join(config.dataDir, "stop-gates.json"));
189
227
  const shellService = new RemoteShellService(config.commandTimeoutMs);
190
228
  const memoryService = new AgentMemoryService(config.dataDir);
@@ -229,6 +267,20 @@ export function createBot(token, bridge, botManagement, botInfo) {
229
267
  throw error;
230
268
  }
231
269
  };
270
+ const actionButton = (ctx, text, action) => {
271
+ if (!ctx.chat || !ctx.from) {
272
+ throw new Error("Telegram action button requires chat and user context.");
273
+ }
274
+ return {
275
+ text,
276
+ callback_data: inlineActions.register(action, String(ctx.chat.id), String(ctx.from.id)),
277
+ };
278
+ };
279
+ const keyboardOptions = (rows) => {
280
+ return rows.length > 0
281
+ ? { reply_markup: JSON.stringify({ inline_keyboard: rows }) }
282
+ : undefined;
283
+ };
232
284
  const removeQueuedInstruction = async (botId, chatId, selector) => {
233
285
  const mapping = await bridge.status(botId, chatId);
234
286
  const activeKey = workLoopKey(botId, chatId, mapping?.session.sessionId);
@@ -329,14 +381,14 @@ ${bridge.formatStatus(mapping)}`);
329
381
  bot.command("help", async (ctx) => {
330
382
  await reply(ctx, HELP_TEXT);
331
383
  });
332
- const replySessionList = async (ctx) => {
384
+ const replySessionList = async (ctx, requestedShowAll) => {
333
385
  if (!ctx.chat) {
334
386
  throw new Error("Telegram chat context is missing.");
335
387
  }
336
388
  const botId = getBotId();
337
389
  const chatId = String(ctx.chat.id);
338
390
  const { args } = parseCommand(ctx.message?.text, 1);
339
- const showAll = args[0] === "-a" || args[0] === "--all";
391
+ const showAll = requestedShowAll ?? (args[0] === "-a" || args[0] === "--all");
340
392
  const [mapping, sessions] = await Promise.all([
341
393
  bridge.status(botId, chatId),
342
394
  bridge.listSessions(),
@@ -345,9 +397,33 @@ ${bridge.formatStatus(mapping)}`);
345
397
  const sessionList = showAll
346
398
  ? await bridge.formatSessionListDetailed(sessions, mapping?.session.sessionId, await bridge.listActiveSessionIds())
347
399
  : bridge.formatSessionList(sessions, mapping?.session.sessionId);
348
- for (const chunk of flattenChunks([`${sessionList}\n\n${botSummary}`], 3900)) {
349
- await reply(ctx, chunk);
400
+ const sessionButtons = sessions.slice(0, showAll ? 20 : 10).map((session) => [
401
+ actionButton(ctx, truncateButtonLabel(`${session.sessionId === mapping?.session.sessionId ? "✓ " : ""}${session.publicId} · ${workspaceLeaf(session.workspace)}`), { kind: "session.switch", selector: session.publicId }),
402
+ ]);
403
+ if (!showAll && sessions.length > 10) {
404
+ sessionButtons.push([actionButton(ctx, `Show all ${sessions.length} sessions`, { kind: "session.list", showAll: true })]);
405
+ }
406
+ const chunks = flattenChunks([`${sessionList}\n\n${botSummary}`], 3900);
407
+ for (const [index, chunk] of chunks.entries()) {
408
+ await reply(ctx, chunk, index === chunks.length - 1 ? keyboardOptions(sessionButtons) : undefined);
409
+ }
410
+ };
411
+ const switchChatSession = async (ctx, selector) => {
412
+ if (!ctx.chat) {
413
+ throw new Error("Telegram chat context is missing.");
414
+ }
415
+ const botId = getBotId();
416
+ const chatId = String(ctx.chat.id);
417
+ const previous = await bridge.status(botId, chatId).catch(() => undefined);
418
+ const mapping = await bridge.switchSession(botId, chatId, selector);
419
+ if (previous && previous.session.sessionId !== mapping.session.sessionId) {
420
+ autoContinue.requestSessionStop(previous.session.sessionId);
421
+ cancelQueuedWorkLoops(botId, chatId, previous.session.sessionId);
422
+ messageBatcher.cancelPending(botId, chatId);
423
+ messageBatcher.cancelManual(botId, chatId);
424
+ await bridge.stopSessionRun(previous.session.sessionId, botId, chatId, "Chat switched to another session; previous session execution was stopped.");
350
425
  }
426
+ return `Switched this chat to session ${mapping.session.publicId}.\n\n${bridge.formatCurrentSession(mapping)}`;
351
427
  };
352
428
  bot.command("list", async (ctx) => {
353
429
  await replySessionList(ctx);
@@ -361,8 +437,6 @@ ${bridge.formatStatus(mapping)}`);
361
437
  await reply(ctx, `Created and bound a new ${mapping.session.mode} session.\n\n${bridge.formatCurrentSession(mapping)}`);
362
438
  });
363
439
  bot.command("switch", async (ctx) => {
364
- const botId = getBotId();
365
- const chatId = String(ctx.chat.id);
366
440
  const { args } = parseCommand(ctx.message?.text, 1);
367
441
  const sessionId = args[0];
368
442
  if (!sessionId) {
@@ -371,16 +445,7 @@ ${bridge.formatStatus(mapping)}`);
371
445
  });
372
446
  return;
373
447
  }
374
- const previous = await bridge.status(botId, chatId).catch(() => undefined);
375
- const mapping = await bridge.switchSession(botId, chatId, sessionId);
376
- if (previous && previous.session.sessionId !== mapping.session.sessionId) {
377
- autoContinue.requestSessionStop(previous.session.sessionId);
378
- cancelQueuedWorkLoops(botId, chatId, previous.session.sessionId);
379
- messageBatcher.cancelPending(botId, chatId);
380
- messageBatcher.cancelManual(botId, chatId);
381
- await bridge.stopSessionRun(previous.session.sessionId, botId, chatId, "Chat switched to another session; previous session execution was stopped.");
382
- }
383
- await reply(ctx, `Switched this chat to session ${sessionId}.\n\n${bridge.formatCurrentSession(mapping)}`);
448
+ await reply(ctx, await switchChatSession(ctx, sessionId));
384
449
  });
385
450
  bot.command("plan", async (ctx) => {
386
451
  const { args, rest } = parseCommand(ctx.message?.text, 1);
@@ -449,6 +514,14 @@ ${bridge.formatStatus(mapping)}`);
449
514
 
450
515
  ${bridge.formatStatus(mapping)}`);
451
516
  });
517
+ const setChatModel = async (ctx, model) => {
518
+ if (!ctx.chat) {
519
+ throw new Error("Telegram chat context is missing.");
520
+ }
521
+ const mapping = await bridge.setModel(getBotId(), String(ctx.chat.id), model);
522
+ const selectedModel = mapping.session[mapping.session.mode]?.model ?? model;
523
+ return `Set ${mapping.session.mode} model to ${selectedModel}.\n\n${bridge.formatStatus(mapping)}`;
524
+ };
452
525
  bot.command("model", async (ctx) => {
453
526
  const botId = getBotId();
454
527
  const chatId = String(ctx.chat.id);
@@ -461,13 +534,17 @@ ${bridge.formatStatus(mapping)}`);
461
534
  return;
462
535
  }
463
536
  if (!model) {
537
+ const selection = await bridge.getModelSelection(botId, chatId);
538
+ const rows = selection.presets.map((preset) => [
539
+ actionButton(ctx, `${preset === selection.currentModel ? "✓ " : ""}${preset}`, { kind: "model.set", model: preset }),
540
+ ]);
464
541
  await reply(ctx, await bridge.formatModelSelection(botId, chatId), {
465
542
  parse_mode: "Markdown",
543
+ ...(keyboardOptions(rows) ?? {}),
466
544
  });
467
545
  return;
468
546
  }
469
- const mapping = await bridge.setModel(botId, chatId, model);
470
- await reply(ctx, `Set ${mapping.session.mode} model to ${model}.\n\n${bridge.formatStatus(mapping)}`);
547
+ await reply(ctx, await setChatModel(ctx, model));
471
548
  });
472
549
  bot.command("queue", async (ctx) => {
473
550
  const botId = getBotId();
@@ -482,7 +559,15 @@ ${bridge.formatStatus(mapping)}`);
482
559
  return;
483
560
  }
484
561
  if (!action || action === "list") {
485
- await reply(ctx, formatQueuedWorkLoops(activeKey, mapping?.session.publicId));
562
+ const queued = listQueuedWorkLoops(activeKey);
563
+ const rows = queued.map((entry) => [{
564
+ text: `Remove ${entry.id}`,
565
+ callback_data: `remoteagent:queue:remove:${entry.id}`,
566
+ }]);
567
+ if (queued.length > 0) {
568
+ rows.push([{ text: "Remove latest", callback_data: "remoteagent:queue:del" }]);
569
+ }
570
+ await reply(ctx, formatQueuedWorkLoops(activeKey, mapping?.session.publicId), keyboardOptions(rows));
486
571
  return;
487
572
  }
488
573
  if ((action === "remove" || action === "rm") && !selector) {
@@ -533,7 +618,16 @@ ${bridge.formatStatus(mapping)}`);
533
618
  const option = args[0]?.toLowerCase();
534
619
  const value = args[1];
535
620
  if (!option) {
536
- await reply(ctx, formatRuntimeOptions());
621
+ await reply(ctx, formatRuntimeOptions(), keyboardOptions([
622
+ [
623
+ actionButton(ctx, "Retry", { kind: "option.show", option: "retry" }),
624
+ actionButton(ctx, "Timeout", { kind: "option.show", option: "timeout" }),
625
+ ],
626
+ [
627
+ actionButton(ctx, "Intent", { kind: "option.show", option: "intent" }),
628
+ actionButton(ctx, "Command menu", { kind: "option.show", option: "command-menu" }),
629
+ ],
630
+ ]));
537
631
  return;
538
632
  }
539
633
  if (option !== "retry" && option !== "timeout" && option !== "intent" && option !== "command-menu") {
@@ -543,14 +637,7 @@ ${bridge.formatStatus(mapping)}`);
543
637
  return;
544
638
  }
545
639
  if (!value) {
546
- const current = option === "retry"
547
- ? `Current automatic continuation retry limit: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)}\n\nUsage: \`/option retry <count>\``
548
- : option === "intent"
549
- ? `Current untagged intent retry limit: ${formatRetryLimit(config.telegramUntaggedIntentRetries)}\n\nUsage: \`/option intent <count>\``
550
- : option === "command-menu"
551
- ? `Current Telegram command menu: ${config.telegramCommandMenuEnabled ? "on" : "off"}\n\nUsage: \`/option command-menu on\`, \`/option command-menu off\`, or \`/option command-menu refresh\``
552
- : `Current provider execution timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)}\n\nUsage: \`/option timeout <seconds>\``;
553
- await reply(ctx, current, {
640
+ await reply(ctx, formatRuntimeOptionDetail(option), {
554
641
  parse_mode: "Markdown",
555
642
  });
556
643
  return;
@@ -697,9 +784,46 @@ ${bridge.formatStatus(mapping)}`);
697
784
  return;
698
785
  }
699
786
  await memoryService.setSecret(key, rest.trim());
787
+ if (ctx.chat && ctx.message?.message_id) {
788
+ await deleteTelegramMessage(token, ctx.chat.id, ctx.message.message_id).catch((error) => {
789
+ console.warn(`[secret] failed to delete source message for ${key}: ${error instanceof Error ? error.message : String(error)}`);
790
+ });
791
+ }
700
792
  await reply(ctx, `Stored secret key ${key}. Value is hidden from agents and chat output.`);
701
793
  return;
702
794
  }
795
+ if (action === "export") {
796
+ if (!key || !ctx.chat) {
797
+ await reply(ctx, formatSecretHelp(), { parse_mode: "Markdown" });
798
+ return;
799
+ }
800
+ const passphrase = await memoryService.getSecret(key);
801
+ if (!passphrase) {
802
+ await reply(ctx, `Secret key was not found: ${key}`);
803
+ return;
804
+ }
805
+ const selectedKeys = rest
806
+ ?.split(/\s+/)
807
+ .map((value) => value.trim().toUpperCase())
808
+ .filter(Boolean);
809
+ const mapping = await bridge.status(getBotId(), String(ctx.chat.id)).catch(() => undefined);
810
+ const exportDir = await fs.mkdtemp(path.join(os.tmpdir(), "remoteagent-secret-export-"));
811
+ const exportPath = path.join(exportDir, `remoteagent-secrets-${mapping?.session.publicId ?? "install"}-${new Date().toISOString().slice(0, 10)}.ra-secrets`);
812
+ try {
813
+ const result = await exportSecrets(config.dataDir, exportPath, passphrase, {
814
+ includeKeys: selectedKeys,
815
+ excludeKeys: [key],
816
+ });
817
+ await sendTelegramDocument(token, ctx.chat.id, {
818
+ path: result.outputPath,
819
+ caption: `Encrypted RemoteAgent Secret bundle (${result.count} key(s)). Secret values are not shown.`,
820
+ });
821
+ }
822
+ finally {
823
+ await fs.rm(exportDir, { recursive: true, force: true }).catch(() => undefined);
824
+ }
825
+ return;
826
+ }
703
827
  if (action === "remove") {
704
828
  if (!key) {
705
829
  await reply(ctx, formatSecretHelp(), { parse_mode: "Markdown" });
@@ -762,15 +886,29 @@ ${bridge.formatStatus(mapping)}`);
762
886
  }
763
887
  await reply(ctx, "Usage: `/docs list`, `/docs find <keyword>`, `/docs pin <keyword> <path>`, `/docs remove <keyword>`, or `/docs reinforce <1-10>`", { parse_mode: "Markdown" });
764
888
  });
765
- const handleMacroCommand = async (ctx, text) => {
889
+ const runMacro = async (ctx, target) => {
766
890
  if (!ctx.chat) {
767
891
  throw new Error("Telegram chat context is missing.");
768
892
  }
769
893
  const botId = getBotId();
770
894
  const chatId = String(ctx.chat.id);
895
+ const macro = await memoryService.getMacro(target);
896
+ if (!macro) {
897
+ return `Macro was not found: ${target}\n\n${await memoryService.listMacros()}`;
898
+ }
899
+ await bridge.logSystem(botId, chatId, `Macro executed: ${macro.alias}`);
900
+ await messageBatcher.enqueue({ botToken: token, telegramChatId: ctx.chat.id }, botId, chatId, macro.prompt);
901
+ return undefined;
902
+ };
903
+ const handleMacroCommand = async (ctx, text) => {
904
+ const botId = getBotId();
771
905
  const parsed = parseMacroCommandText(text, botId);
772
906
  if (parsed.kind === "help") {
773
- await reply(ctx, await formatMacroHelp(memoryService));
907
+ const macros = await memoryService.getMacros();
908
+ const rows = macros.slice(0, 20).map((macro) => [
909
+ actionButton(ctx, truncateButtonLabel(macro.alias), { kind: "macro.run", alias: macro.alias }),
910
+ ]);
911
+ await reply(ctx, await formatMacroHelp(memoryService), keyboardOptions(rows));
774
912
  return;
775
913
  }
776
914
  if (parsed.kind === "set") {
@@ -785,25 +923,34 @@ ${bridge.formatStatus(mapping)}`);
785
923
  await reply(ctx, removed ? `Removed macro '${parsed.alias}'.` : `Macro was not found: ${parsed.alias}`);
786
924
  return;
787
925
  }
788
- const macro = await memoryService.getMacro(parsed.target);
789
- if (!macro) {
790
- await reply(ctx, `Macro was not found: ${parsed.target}\n\n${await memoryService.listMacros()}`);
791
- return;
926
+ const result = await runMacro(ctx, parsed.target);
927
+ if (result) {
928
+ await reply(ctx, result);
792
929
  }
793
- await bridge.logSystem(botId, chatId, `Macro executed: ${macro.alias}`);
794
- await messageBatcher.enqueue({ botToken: token, telegramChatId: ctx.chat.id }, botId, chatId, macro.prompt);
795
930
  };
796
931
  bot.command("macro", async (ctx) => {
797
932
  await handleMacroCommand(ctx, ctx.message?.text ?? "/macro");
798
933
  });
799
- bot.command("bots", async (ctx) => {
934
+ const replyBotList = async (ctx) => {
800
935
  await ensureOwnerControlAccess(ctx);
801
936
  const pendingNotice = await botManagement.getPendingOperationNotice();
802
- if (pendingNotice?.pending) {
803
- await reply(ctx, `${pendingNotice.message}\n\n${await botManagement.listBots()}`);
804
- return;
805
- }
806
- await reply(ctx, await botManagement.listBots());
937
+ const [botList, choices] = await Promise.all([
938
+ botManagement.listBots(),
939
+ botManagement.listBotChoices(),
940
+ ]);
941
+ const rows = [];
942
+ for (let index = 0; index < choices.length; index += 2) {
943
+ rows.push(choices.slice(index, index + 2).map((choice) => ({
944
+ text: `@${choice.username}`,
945
+ url: `https://t.me/${choice.username}`,
946
+ })));
947
+ }
948
+ rows.push([actionButton(ctx, "Refresh", { kind: "bots.refresh" })]);
949
+ const message = pendingNotice?.pending ? `${pendingNotice.message}\n\n${botList}` : botList;
950
+ await reply(ctx, message, keyboardOptions(rows));
951
+ };
952
+ bot.command("bots", async (ctx) => {
953
+ await replyBotList(ctx);
807
954
  });
808
955
  bot.command("bot", async (ctx) => {
809
956
  await ensureOwnerControlAccess(ctx);
@@ -882,20 +1029,39 @@ ${bridge.formatStatus(mapping)}`);
882
1029
  return { chunks: flattenChunks([output], 3900) };
883
1030
  });
884
1031
  });
1032
+ const setChatSandbox = async (ctx, sandboxMode) => {
1033
+ if (!ctx.chat) {
1034
+ throw new Error("Telegram chat context is missing.");
1035
+ }
1036
+ const mapping = await bridge.setCodexSandboxMode(getBotId(), String(ctx.chat.id), sandboxMode);
1037
+ return `Set Codex sandbox to ${sandboxMode}.\n\n${bridge.formatStatus(mapping)}`;
1038
+ };
885
1039
  bot.command("sandbox", async (ctx) => {
886
1040
  const botId = getBotId();
887
1041
  const chatId = String(ctx.chat.id);
888
1042
  const { args } = parseCommand(ctx.message?.text, 2);
889
1043
  const provider = args[0]?.toLowerCase();
890
1044
  const sandboxMode = args[1]?.toLowerCase();
1045
+ if (!provider && !sandboxMode) {
1046
+ const mapping = await bridge.status(botId, chatId);
1047
+ if (!mapping?.session.codex) {
1048
+ await reply(ctx, "No Codex session is paired with this chat.");
1049
+ return;
1050
+ }
1051
+ const current = mapping.session.codex.sandboxMode ?? config.codexSandboxMode;
1052
+ const modes = ["read-only", "workspace-write", "danger-full-access"];
1053
+ await reply(ctx, `Codex sandbox\ncurrent: ${current}\n\nChoose a mode:`, keyboardOptions(modes.map((mode) => [
1054
+ actionButton(ctx, `${mode === current ? "✓ " : ""}${mode}`, { kind: "sandbox.set", mode }),
1055
+ ])));
1056
+ return;
1057
+ }
891
1058
  if (provider !== "codex" || !sandboxMode || !isCodexSandboxMode(sandboxMode)) {
892
1059
  await reply(ctx, "Usage: `/sandbox codex <read-only|workspace-write|danger-full-access>`", {
893
1060
  parse_mode: "Markdown",
894
1061
  });
895
1062
  return;
896
1063
  }
897
- const mapping = await bridge.setCodexSandboxMode(botId, chatId, sandboxMode);
898
- await reply(ctx, `Set Codex sandbox to ${sandboxMode}.\n\n${bridge.formatStatus(mapping)}`);
1064
+ await reply(ctx, await setChatSandbox(ctx, sandboxMode));
899
1065
  });
900
1066
  bot.command("reset", async (ctx) => {
901
1067
  const botId = getBotId();
@@ -905,26 +1071,86 @@ ${bridge.formatStatus(mapping)}`);
905
1071
  await reply(ctx, "Cleared all pairings for this chat.");
906
1072
  });
907
1073
  bot.on("callback_query:data", async (ctx) => {
908
- const match = /^remoteagent:queue:(remove:(Q\d+)|del)$/i.exec(ctx.callbackQuery.data);
909
- if (!match) {
910
- return;
911
- }
1074
+ const data = ctx.callbackQuery.data;
912
1075
  const callbackChat = ctx.callbackQuery.message?.chat;
913
1076
  if (!callbackChat) {
914
1077
  await callTelegramApi(token, "answerCallbackQuery", {
915
1078
  callback_query_id: ctx.callbackQuery.id,
916
- text: "This queue action is no longer available.",
1079
+ text: "This action is no longer available.",
917
1080
  });
918
1081
  return;
919
1082
  }
920
1083
  const botId = getBotId();
921
1084
  const chatId = String(callbackChat.id);
922
- const result = await removeQueuedInstruction(botId, chatId, match[2]);
1085
+ const queueMatch = /^remoteagent:queue:(remove:(Q\d+)|del)$/i.exec(data);
1086
+ if (queueMatch) {
1087
+ const result = await removeQueuedInstruction(botId, chatId, queueMatch[2]);
1088
+ await callTelegramApi(token, "answerCallbackQuery", {
1089
+ callback_query_id: ctx.callbackQuery.id,
1090
+ text: result.split("\n", 1)[0],
1091
+ });
1092
+ await sendTelegramMessage(token, callbackChat.id, result);
1093
+ return;
1094
+ }
1095
+ if (!data.startsWith("remoteagent:action:")) {
1096
+ return;
1097
+ }
1098
+ const action = inlineActions.resolve(data, chatId, String(ctx.from.id));
1099
+ if (!action) {
1100
+ await callTelegramApi(token, "answerCallbackQuery", {
1101
+ callback_query_id: ctx.callbackQuery.id,
1102
+ text: "This button expired. Open the command list again.",
1103
+ });
1104
+ return;
1105
+ }
923
1106
  await callTelegramApi(token, "answerCallbackQuery", {
924
1107
  callback_query_id: ctx.callbackQuery.id,
925
- text: result.split("\n", 1)[0],
1108
+ text: action.kind === "macro.run" ? "Macro selected." : "Applying...",
926
1109
  });
927
- await sendTelegramMessage(token, callbackChat.id, result);
1110
+ try {
1111
+ if (action.kind === "session.switch") {
1112
+ await reply(ctx, await switchChatSession(ctx, action.selector));
1113
+ return;
1114
+ }
1115
+ if (action.kind === "session.list") {
1116
+ await replySessionList(ctx, action.showAll);
1117
+ return;
1118
+ }
1119
+ if (action.kind === "model.set") {
1120
+ await reply(ctx, await setChatModel(ctx, action.model));
1121
+ return;
1122
+ }
1123
+ if (action.kind === "macro.run") {
1124
+ const result = await runMacro(ctx, action.alias);
1125
+ if (result) {
1126
+ await reply(ctx, result);
1127
+ }
1128
+ return;
1129
+ }
1130
+ if (action.kind === "option.show") {
1131
+ await ensureOwnerControlAccess(ctx);
1132
+ await reply(ctx, formatRuntimeOptionDetail(action.option), { parse_mode: "Markdown" });
1133
+ return;
1134
+ }
1135
+ if (action.kind === "bots.refresh") {
1136
+ await replyBotList(ctx);
1137
+ return;
1138
+ }
1139
+ if (action.mode === "danger-full-access" && !action.confirmed) {
1140
+ await reply(ctx, "Confirm Codex sandbox change to danger-full-access.", keyboardOptions([[
1141
+ actionButton(ctx, "Confirm danger-full-access", {
1142
+ kind: "sandbox.set",
1143
+ mode: "danger-full-access",
1144
+ confirmed: true,
1145
+ }),
1146
+ ]]));
1147
+ return;
1148
+ }
1149
+ await reply(ctx, await setChatSandbox(ctx, action.mode));
1150
+ }
1151
+ catch (error) {
1152
+ await reply(ctx, error instanceof Error ? error.message : String(error));
1153
+ }
928
1154
  });
929
1155
  bot.on("message", async (ctx) => {
930
1156
  const botId = getBotId();
@@ -2224,12 +2450,15 @@ function formatSecretHelp() {
2224
2450
  "/secret set KEY value",
2225
2451
  "/secret list",
2226
2452
  "/secret remove KEY",
2453
+ "/secret export PASSPHRASE_KEY [KEY ...]",
2227
2454
  "```",
2228
2455
  "",
2229
2456
  "Example:",
2230
2457
  "```text",
2231
2458
  "/secret set GIFTISHOW_AUTH_KEY REAL...",
2232
2459
  "/secret set GIFTISHOW_TOKEN_KEY xNC...",
2460
+ "/secret set REMOTEAGENT_TRANSFER_PASSPHRASE a-long-private-passphrase",
2461
+ "/secret export REMOTEAGENT_TRANSFER_PASSPHRASE GIFTISHOW_AUTH_KEY GIFTISHOW_TOKEN_KEY",
2233
2462
  "```",
2234
2463
  "",
2235
2464
  "Then tell the agent:",
@@ -2267,6 +2496,26 @@ function formatRuntimeOptions() {
2267
2496
  "`command-menu refresh` reapplies Telegram slash-command autocomplete without changing the saved option.",
2268
2497
  ].join("\n");
2269
2498
  }
2499
+ function formatRuntimeOptionDetail(option) {
2500
+ if (option === "retry") {
2501
+ return `Current automatic continuation retry limit: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)}\n\nUsage: \`/option retry <count>\``;
2502
+ }
2503
+ if (option === "intent") {
2504
+ return `Current untagged intent retry limit: ${formatRetryLimit(config.telegramUntaggedIntentRetries)}\n\nUsage: \`/option intent <count>\``;
2505
+ }
2506
+ if (option === "command-menu") {
2507
+ return `Current Telegram command menu: ${config.telegramCommandMenuEnabled ? "on" : "off"}\n\nUsage: \`/option command-menu on\`, \`/option command-menu off\`, or \`/option command-menu refresh\``;
2508
+ }
2509
+ return `Current provider execution timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)}\n\nUsage: \`/option timeout <seconds>\``;
2510
+ }
2511
+ function truncateButtonLabel(value, maxLength = 48) {
2512
+ const normalized = value.replace(/\s+/g, " ").trim();
2513
+ return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 3)}...`;
2514
+ }
2515
+ function workspaceLeaf(workspace) {
2516
+ const normalized = workspace.replace(/[\\/]+$/, "");
2517
+ return normalized.split(/[\\/]/).at(-1) || workspace;
2518
+ }
2270
2519
  function formatRetryLimit(value) {
2271
2520
  return value === 0 ? "unlimited" : `${value}`;
2272
2521
  }
@@ -2802,6 +3051,25 @@ async function sendTelegramDocument(botToken, chatId, document) {
2802
3051
  }
2803
3052
  return payload.result;
2804
3053
  }
3054
+ async function deleteTelegramMessage(botToken, chatId, messageId) {
3055
+ const { stdout, stderr } = await execFileAsync("curl", [
3056
+ "-sS",
3057
+ "--max-time",
3058
+ "20",
3059
+ "-d",
3060
+ `chat_id=${chatId}`,
3061
+ "-d",
3062
+ `message_id=${messageId}`,
3063
+ `https://api.telegram.org/bot${botToken}/deleteMessage`,
3064
+ ]);
3065
+ if (stderr?.trim()) {
3066
+ console.error(`curl stderr for deleteMessage: ${stderr.trim()}`);
3067
+ }
3068
+ const payload = JSON.parse(stdout);
3069
+ if (!payload.ok) {
3070
+ throw new Error(payload.description || "Telegram API deleteMessage failed.");
3071
+ }
3072
+ }
2805
3073
  async function sendTelegramMessage(botToken, chatId, text, extra) {
2806
3074
  const startedAt = Date.now();
2807
3075
  try {