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/README.md +33 -0
- package/bin/remoteagent.js +1 -1
- package/dist/adapters/codex-adapter.js +22 -4
- package/dist/bot.js +321 -53
- package/dist/cli.js +208 -0
- package/dist/services/agent-memory-service.js +6 -4
- package/dist/services/bot-management-service.js +3 -0
- package/dist/services/bridge-service.js +16 -7
- package/dist/services/cli-config-service.js +154 -0
- package/dist/services/secret-transfer-service.js +237 -0
- package/dist/telegram-command-menu.js +1 -1
- package/docs/CLI_BOOTSTRAP_AND_SECRET_MIGRATION.md +98 -0
- package/docs/RELEASING.md +22 -0
- package/package.json +2 -1
- package/scripts/install.sh +9 -0
- package/scripts/selftest-cli.mjs +121 -0
- package/scripts/selftest-codex-stream.mjs +9 -2
- package/scripts/selftest-telegram-update.mjs +120 -0
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
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
789
|
-
if (
|
|
790
|
-
await reply(ctx,
|
|
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
|
-
|
|
934
|
+
const replyBotList = async (ctx) => {
|
|
800
935
|
await ensureOwnerControlAccess(ctx);
|
|
801
936
|
const pendingNotice = await botManagement.getPendingOperationNotice();
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
1108
|
+
text: action.kind === "macro.run" ? "Macro selected." : "Applying...",
|
|
926
1109
|
});
|
|
927
|
-
|
|
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 {
|