appback-remoteagent 0.13.5 → 0.13.7

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
@@ -9,6 +9,7 @@ import { config } from "./config.js";
9
9
  import { ProviderSetupService } from "./services/provider-setup-service.js";
10
10
  import { RemoteShellService } from "./services/remote-shell-service.js";
11
11
  import { AgentMemoryService } from "./services/agent-memory-service.js";
12
+ import { deleteTelegramCommandMenu, setTelegramCommandMenu } from "./telegram-command-menu.js";
12
13
  const execFileAsync = promisify(execFile);
13
14
  const HELP_TEXT = [
14
15
  "Commands:",
@@ -24,7 +25,7 @@ const HELP_TEXT = [
24
25
  "/stop",
25
26
  "/sandbox codex <read-only|workspace-write|danger-full-access>",
26
27
  "/status",
27
- "/option [retry <count>|timeout <seconds>|intent <count>]",
28
+ "/option [retry <count>|timeout <seconds>|intent <count>|command-menu <on|off|refresh>]",
28
29
  "/state [clear|note <text>]",
29
30
  "/artifacts list|cleanup <days>",
30
31
  "/secret set|list|remove",
@@ -484,8 +485,8 @@ ${bridge.formatStatus(mapping)}`);
484
485
  await reply(ctx, formatRuntimeOptions());
485
486
  return;
486
487
  }
487
- if (option !== "retry" && option !== "timeout" && option !== "intent") {
488
- await reply(ctx, "Usage: `/option retry <count>`, `/option timeout <seconds>`, or `/option intent <count>`\n\n`retry` controls automatic continuation turns. `timeout` controls one provider execution limit. `intent` controls retries for untagged intent-only provider replies.", {
488
+ if (option !== "retry" && option !== "timeout" && option !== "intent" && option !== "command-menu") {
489
+ await reply(ctx, "Usage: `/option retry <count>`, `/option timeout <seconds>`, `/option intent <count>`, or `/option command-menu <on|off|refresh>`\n\n`retry` controls automatic continuation turns. `timeout` controls one provider execution limit. `intent` controls retries for untagged intent-only provider replies. `command-menu` controls Telegram slash-command autocomplete for all configured bots.", {
489
490
  parse_mode: "Markdown",
490
491
  });
491
492
  return;
@@ -495,12 +496,39 @@ ${bridge.formatStatus(mapping)}`);
495
496
  ? `Current automatic continuation retry limit: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)}\n\nUsage: \`/option retry <count>\``
496
497
  : option === "intent"
497
498
  ? `Current untagged intent retry limit: ${formatRetryLimit(config.telegramUntaggedIntentRetries)}\n\nUsage: \`/option intent <count>\``
498
- : `Current provider execution timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)}\n\nUsage: \`/option timeout <seconds>\``;
499
+ : option === "command-menu"
500
+ ? `Current Telegram command menu: ${config.telegramCommandMenuEnabled ? "on" : "off"}\n\nUsage: \`/option command-menu on\`, \`/option command-menu off\`, or \`/option command-menu refresh\``
501
+ : `Current provider execution timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)}\n\nUsage: \`/option timeout <seconds>\``;
499
502
  await reply(ctx, current, {
500
503
  parse_mode: "Markdown",
501
504
  });
502
505
  return;
503
506
  }
507
+ if (option === "command-menu") {
508
+ const action = value.toLowerCase();
509
+ if (!["on", "off", "refresh"].includes(action)) {
510
+ await reply(ctx, "Invalid command-menu option. Use `/option command-menu on`, `/option command-menu off`, or `/option command-menu refresh`.", {
511
+ parse_mode: "Markdown",
512
+ });
513
+ return;
514
+ }
515
+ const enabled = action === "refresh" ? config.telegramCommandMenuEnabled : action === "on";
516
+ const result = await applyTelegramCommandMenuOption(enabled);
517
+ if (action !== "refresh") {
518
+ config.telegramCommandMenuEnabled = enabled;
519
+ await upsertInstalledEnvValue("TELEGRAM_COMMAND_MENU_ENABLED", String(enabled));
520
+ }
521
+ await bridge.logSystem(botId, chatId, `Runtime option TELEGRAM_COMMAND_MENU_ENABLED ${action === "refresh" ? "refreshed" : `set to ${enabled}`}. ${result.summary}`);
522
+ await reply(ctx, [
523
+ action === "refresh"
524
+ ? `Refreshed Telegram command menu for configured bots.`
525
+ : `Set Telegram command menu to ${enabled ? "on" : "off"}.`,
526
+ "",
527
+ result.summary,
528
+ action === "refresh" ? "" : `Saved: TELEGRAM_COMMAND_MENU_ENABLED=${enabled}`,
529
+ ].filter(Boolean).join("\n"));
530
+ return;
531
+ }
504
532
  const parsed = Number.parseInt(value, 10);
505
533
  if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== value.trim()) {
506
534
  await reply(ctx, option === "retry"
@@ -1583,14 +1611,17 @@ function formatRuntimeOptions() {
1583
1611
  `- retry: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)} (TELEGRAM_AUTO_PROGRESS_MAX_TURNS)`,
1584
1612
  `- timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)} (COMMAND_TIMEOUT_MS)`,
1585
1613
  `- intent: ${formatRetryLimit(config.telegramUntaggedIntentRetries)} (TELEGRAM_UNTAGGED_INTENT_RETRIES)`,
1614
+ `- command-menu: ${config.telegramCommandMenuEnabled ? "on" : "off"} (TELEGRAM_COMMAND_MENU_ENABLED)`,
1586
1615
  "",
1587
1616
  "Usage:",
1588
1617
  "/option retry <count>",
1589
1618
  "/option timeout <seconds>",
1590
1619
  "/option intent <count>",
1620
+ "/option command-menu <on|off|refresh>",
1591
1621
  "",
1592
1622
  "`retry 0` disables the automatic continuation limit.",
1593
1623
  "`intent 0` disables untagged intent-only response retries.",
1624
+ "`command-menu refresh` reapplies Telegram slash-command autocomplete without changing the saved option.",
1594
1625
  ].join("\n");
1595
1626
  }
1596
1627
  function formatRetryLimit(value) {
@@ -1620,6 +1651,34 @@ async function upsertInstalledEnvValue(key, value) {
1620
1651
  }
1621
1652
  await fs.writeFile(envPath, `${next.join("\n").replace(/\n+$/u, "")}\n`, "utf8");
1622
1653
  }
1654
+ async function applyTelegramCommandMenuOption(enabled) {
1655
+ let applied = 0;
1656
+ const failures = [];
1657
+ for (const token of config.telegramBotTokens) {
1658
+ const botLabel = await resolveTelegramBotLabel(token).catch(() => "unknown-bot");
1659
+ try {
1660
+ if (enabled) {
1661
+ await setTelegramCommandMenu(token);
1662
+ }
1663
+ else {
1664
+ await deleteTelegramCommandMenu(token);
1665
+ }
1666
+ applied += 1;
1667
+ }
1668
+ catch (error) {
1669
+ failures.push(`${botLabel}: ${error instanceof Error ? error.message : String(error)}`);
1670
+ }
1671
+ }
1672
+ const parts = [`applied=${applied}/${config.telegramBotTokens.length}`];
1673
+ if (failures.length > 0) {
1674
+ parts.push(`failed=${failures.length}`, ...failures.map((failure) => `- ${failure}`));
1675
+ }
1676
+ return { summary: parts.join("\n") };
1677
+ }
1678
+ async function resolveTelegramBotLabel(token) {
1679
+ const payload = await callTelegramApi(token, "getMe", {});
1680
+ return payload.username ? `@${payload.username}` : String(payload.id);
1681
+ }
1623
1682
  function chunkMessage(text, size) {
1624
1683
  if (text.length <= size) {
1625
1684
  return [text];
package/dist/config.js CHANGED
@@ -130,7 +130,7 @@ const telegramBotUsernames = readTelegramBotUsernames();
130
130
  export const config = {
131
131
  telegramBotTokens,
132
132
  telegramBotUsernames,
133
- telegramCommandMenuEnabled: readBoolean("TELEGRAM_COMMAND_MENU_ENABLED", false),
133
+ telegramCommandMenuEnabled: readBoolean("TELEGRAM_COMMAND_MENU_ENABLED", true),
134
134
  telegramPollingBackoffMinMs: readTimeout("TELEGRAM_POLLING_BACKOFF_MIN_MS", 60_000),
135
135
  telegramPollingBackoffMaxMs: readTimeout("TELEGRAM_POLLING_BACKOFF_MAX_MS", 900_000),
136
136
  telegramPollingMaxConcurrency: readTimeout("TELEGRAM_POLLING_MAX_CONCURRENCY", 3),
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { BotManagementService } from "./services/bot-management-service.js";
15
15
  import { LocalUiService } from "./services/local-ui-service.js";
16
16
  import { AgentMemoryService } from "./services/agent-memory-service.js";
17
17
  import { terminateAllSpawnedExecutions } from "./adapters/windows-shell.js";
18
+ import { setTelegramCommandMenu } from "./telegram-command-menu.js";
18
19
  const execFileAsync = promisify(execFile);
19
20
  const TELEGRAM_GET_UPDATES_HTTP_TIMEOUT_SECONDS = 30;
20
21
  const TELEGRAM_GET_UPDATES_CURL_TIMEOUT_SECONDS = 60;
@@ -107,23 +108,6 @@ function startArtifactCleanupSchedule(memoryService) {
107
108
  interval.unref();
108
109
  }
109
110
  async function configureTelegramCommandMenu(bot) {
110
- const commands = [
111
- { command: "start", description: "Start a new Codex or Claude session" },
112
- { command: "list", description: "List sessions" },
113
- { command: "switch", description: "Switch to a session" },
114
- { command: "status", description: "Show current session status" },
115
- { command: "state", description: "Show or edit session state notes" },
116
- { command: "option", description: "Show or change runtime options" },
117
- { command: "model", description: "Show or change provider model" },
118
- { command: "stop", description: "Stop active work and clear queued messages" },
119
- { command: "batch", description: "Collect and send a multi-message batch" },
120
- { command: "bots", description: "List configured Telegram bots" },
121
- { command: "bot", description: "Manage Telegram bots" },
122
- { command: "install", description: "Install or update Codex or Claude" },
123
- { command: "login", description: "Run provider login flow" },
124
- { command: "reset", description: "Clear this chat binding" },
125
- { command: "help", description: "Show command help" },
126
- ];
127
111
  const token = bot.token;
128
112
  if (!token) {
129
113
  throw new Error("Telegram bot token is unavailable for command menu registration.");
@@ -131,7 +115,7 @@ async function configureTelegramCommandMenu(bot) {
131
115
  let lastError;
132
116
  for (let attempt = 1; attempt <= 3; attempt += 1) {
133
117
  try {
134
- await setTelegramCommandsViaCurl(token, commands);
118
+ await setTelegramCommandMenu(token);
135
119
  return;
136
120
  }
137
121
  catch (error) {
@@ -143,51 +127,6 @@ async function configureTelegramCommandMenu(bot) {
143
127
  }
144
128
  throw lastError;
145
129
  }
146
- async function setTelegramCommandsViaCurl(token, commands) {
147
- const url = `https://api.telegram.org/bot${token}/setMyCommands`;
148
- const payload = JSON.stringify({ commands });
149
- let stdout;
150
- let stderr;
151
- try {
152
- const result = await execFileAsync("curl", [
153
- "-sS",
154
- "-4",
155
- "--max-time",
156
- "20",
157
- "-H",
158
- "Content-Type: application/json",
159
- "-d",
160
- payload,
161
- url,
162
- ]);
163
- stdout = result.stdout;
164
- stderr = result.stderr;
165
- }
166
- catch (error) {
167
- throw new Error(`Telegram setMyCommands curl failed: ${formatCurlError(error)}`);
168
- }
169
- if (stderr?.trim()) {
170
- console.error(`curl stderr for setMyCommands: ${stderr.trim()}`);
171
- }
172
- const parsed = JSON.parse(stdout);
173
- if (!parsed.ok) {
174
- throw new Error(parsed.description || "Telegram setMyCommands failed.");
175
- }
176
- }
177
- function formatCurlError(error) {
178
- if (!(error instanceof Error)) {
179
- return String(error);
180
- }
181
- const code = typeof error === "object" && error !== null && "code" in error
182
- ? String(error.code ?? "")
183
- : "";
184
- const stderr = typeof error === "object" && error !== null && "stderr" in error
185
- ? String(error.stderr ?? "").trim()
186
- : "";
187
- return [code ? `code=${code}` : undefined, stderr || error.message]
188
- .filter(Boolean)
189
- .join(" ");
190
- }
191
130
  main().catch((error) => {
192
131
  console.error("RemoteAgent fatal error:", error);
193
132
  releaseProcessLockSync();
@@ -404,7 +404,7 @@ export class FileStore {
404
404
  }
405
405
  async writeJsonFileAtomic(filePath, value) {
406
406
  await fs.mkdir(path.dirname(filePath), { recursive: true });
407
- const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
407
+ const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
408
408
  await fs.writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
409
409
  await fs.rename(temporaryPath, filePath);
410
410
  }
@@ -0,0 +1,71 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ export const TELEGRAM_COMMAND_MENU = [
5
+ { command: "start", description: "Start a new Codex or Claude session" },
6
+ { command: "list", description: "List sessions" },
7
+ { command: "switch", description: "Switch to a session" },
8
+ { command: "status", description: "Show current session status" },
9
+ { command: "state", description: "Show or edit session state notes" },
10
+ { command: "option", description: "Show or change runtime options" },
11
+ { command: "model", description: "Show or change provider model" },
12
+ { command: "stop", description: "Stop active work and clear queued messages" },
13
+ { command: "batch", description: "Collect and send a multi-message batch" },
14
+ { command: "bots", description: "List configured Telegram bots" },
15
+ { command: "bot", description: "Manage Telegram bots" },
16
+ { command: "install", description: "Install or update Codex or Claude" },
17
+ { command: "login", description: "Run provider login flow" },
18
+ { command: "reset", description: "Clear this chat binding" },
19
+ { command: "help", description: "Show command help" },
20
+ ];
21
+ export async function setTelegramCommandMenu(token) {
22
+ await callTelegramCommandApi(token, "setMyCommands", {
23
+ commands: TELEGRAM_COMMAND_MENU,
24
+ });
25
+ }
26
+ export async function deleteTelegramCommandMenu(token) {
27
+ await callTelegramCommandApi(token, "deleteMyCommands", {});
28
+ }
29
+ async function callTelegramCommandApi(token, method, payload) {
30
+ let stdout;
31
+ let stderr;
32
+ try {
33
+ const result = await execFileAsync("curl", [
34
+ "-sS",
35
+ "-4",
36
+ "--max-time",
37
+ "20",
38
+ "-H",
39
+ "Content-Type: application/json",
40
+ "-d",
41
+ JSON.stringify(payload),
42
+ `https://api.telegram.org/bot${token}/${method}`,
43
+ ]);
44
+ stdout = result.stdout;
45
+ stderr = result.stderr;
46
+ }
47
+ catch (error) {
48
+ throw new Error(`Telegram ${method} curl failed: ${formatCurlError(error)}`);
49
+ }
50
+ if (stderr?.trim()) {
51
+ console.error(`curl stderr for ${method}: ${stderr.trim()}`);
52
+ }
53
+ const parsed = JSON.parse(stdout);
54
+ if (!parsed.ok) {
55
+ throw new Error(parsed.description || `Telegram ${method} failed.`);
56
+ }
57
+ }
58
+ function formatCurlError(error) {
59
+ if (!(error instanceof Error)) {
60
+ return String(error);
61
+ }
62
+ const code = typeof error === "object" && error !== null && "code" in error
63
+ ? String(error.code ?? "")
64
+ : "";
65
+ const stderr = typeof error === "object" && error !== null && "stderr" in error
66
+ ? String(error.stderr ?? "").trim()
67
+ : "";
68
+ return [code ? `code=${code}` : undefined, stderr || error.message]
69
+ .filter(Boolean)
70
+ .join(" ");
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appback-remoteagent",
3
- "version": "0.13.5",
3
+ "version": "0.13.7",
4
4
  "description": "Personal installable session server for continuing local AI work across PC and Telegram",
5
5
  "license": "MIT",
6
6
  "type": "module",