appback-remoteagent 0.13.4 → 0.13.6

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"
@@ -1571,6 +1599,10 @@ function formatSecretHelp() {
1571
1599
  "```text",
1572
1600
  "node \"$REMOTEAGENT_SECRET_BIN\" get <KEY>",
1573
1601
  "```",
1602
+ "If an agent obtains a new secret value such as an OAuth refresh token, it can delegate storage without printing the value:",
1603
+ "```text",
1604
+ "printf '%s' \"$VALUE\" | node \"$REMOTEAGENT_SECRET_BIN\" set <KEY>",
1605
+ "```",
1574
1606
  ].join("\n");
1575
1607
  }
1576
1608
  function formatRuntimeOptions() {
@@ -1579,14 +1611,17 @@ function formatRuntimeOptions() {
1579
1611
  `- retry: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)} (TELEGRAM_AUTO_PROGRESS_MAX_TURNS)`,
1580
1612
  `- timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)} (COMMAND_TIMEOUT_MS)`,
1581
1613
  `- intent: ${formatRetryLimit(config.telegramUntaggedIntentRetries)} (TELEGRAM_UNTAGGED_INTENT_RETRIES)`,
1614
+ `- command-menu: ${config.telegramCommandMenuEnabled ? "on" : "off"} (TELEGRAM_COMMAND_MENU_ENABLED)`,
1582
1615
  "",
1583
1616
  "Usage:",
1584
1617
  "/option retry <count>",
1585
1618
  "/option timeout <seconds>",
1586
1619
  "/option intent <count>",
1620
+ "/option command-menu <on|off|refresh>",
1587
1621
  "",
1588
1622
  "`retry 0` disables the automatic continuation limit.",
1589
1623
  "`intent 0` disables untagged intent-only response retries.",
1624
+ "`command-menu refresh` reapplies Telegram slash-command autocomplete without changing the saved option.",
1590
1625
  ].join("\n");
1591
1626
  }
1592
1627
  function formatRetryLimit(value) {
@@ -1616,6 +1651,34 @@ async function upsertInstalledEnvValue(key, value) {
1616
1651
  }
1617
1652
  await fs.writeFile(envPath, `${next.join("\n").replace(/\n+$/u, "")}\n`, "utf8");
1618
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
+ }
1619
1682
  function chunkMessage(text, size) {
1620
1683
  if (text.length <= size) {
1621
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();
@@ -2,17 +2,28 @@
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
- import { readSecretValue } from "./services/agent-memory-service.js";
6
- function main() {
5
+ import { readSecretValue, writeSecretValue } from "./services/agent-memory-service.js";
6
+ async function main() {
7
7
  const [command, key] = process.argv.slice(2);
8
- if (command !== "get" || !key) {
9
- process.stderr.write("Usage: secret-helper get <KEY>\n");
8
+ if (!["get", "set"].includes(command ?? "") || !key) {
9
+ process.stderr.write("Usage: secret-helper get <KEY>\n secret-helper set <KEY> # reads value from stdin\n");
10
10
  process.exitCode = 2;
11
11
  return;
12
12
  }
13
13
  const dataDir = process.env.REMOTEAGENT_DATA_DIR?.trim()
14
14
  || process.env.DATA_DIR?.trim()
15
15
  || path.join(os.homedir(), ".remoteagent");
16
+ if (command === "set") {
17
+ const value = await readStdin();
18
+ if (!value) {
19
+ process.stderr.write("Secret value was empty.\n");
20
+ process.exitCode = 2;
21
+ return;
22
+ }
23
+ writeSecretValue(dataDir, key.trim(), value);
24
+ process.stdout.write(`Stored secret: ${key.trim()}\n`);
25
+ return;
26
+ }
16
27
  const value = readSecretValue(dataDir, key.trim());
17
28
  if (!value) {
18
29
  process.stderr.write(`Secret was not found: ${key}\n`);
@@ -21,4 +32,11 @@ function main() {
21
32
  }
22
33
  process.stdout.write(value);
23
34
  }
24
- main();
35
+ async function readStdin() {
36
+ const chunks = [];
37
+ for await (const chunk of process.stdin) {
38
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
39
+ }
40
+ return Buffer.concat(chunks).toString("utf8").trim();
41
+ }
42
+ void main();
@@ -338,8 +338,16 @@ export class AgentMemoryService {
338
338
  ? ["Document index:", ...docs.map((doc) => `- ${doc.keyword}: ${doc.targetPath}${doc.note ? ` (${doc.note})` : ""}`)].join("\n")
339
339
  : undefined,
340
340
  secrets.length > 0
341
- ? ["Secret keys available through `node \"$REMOTEAGENT_SECRET_BIN\" get <KEY>`:", ...secrets.map((key) => `- ${key}`)].join("\n")
342
- : undefined,
341
+ ? [
342
+ "Secret keys available through `node \"$REMOTEAGENT_SECRET_BIN\" get <KEY>`:",
343
+ ...secrets.map((key) => `- ${key}`),
344
+ "If you generate a new secret value such as an OAuth refresh token, store it without printing it:",
345
+ "`printf '%s' \"$VALUE\" | node \"$REMOTEAGENT_SECRET_BIN\" set <KEY>`",
346
+ ].join("\n")
347
+ : [
348
+ "Secrets can be stored without printing values:",
349
+ "`printf '%s' \"$VALUE\" | node \"$REMOTEAGENT_SECRET_BIN\" set <KEY>`",
350
+ ].join("\n"),
343
351
  artifacts.length > 0
344
352
  ? ["Recent session artifacts:", ...artifacts.map((artifact) => `- ${artifact.id} ${artifact.kind}: ${artifact.path}`)].join("\n")
345
353
  : undefined,
@@ -735,3 +743,32 @@ export function readSecretValue(dataDir, key) {
735
743
  return undefined;
736
744
  }
737
745
  }
746
+ export function writeSecretValue(dataDir, key, value) {
747
+ if (!/^[A-Z0-9_.-]{1,80}$/.test(key)) {
748
+ throw new Error("Secret key must be 1-80 chars using A-Z, 0-9, dot, underscore, or dash.");
749
+ }
750
+ const filePath = path.join(dataDir, "managed", "secrets.json");
751
+ fsSync.mkdirSync(path.dirname(filePath), { recursive: true });
752
+ let secrets = {};
753
+ try {
754
+ const raw = fsSync.readFileSync(filePath, "utf8");
755
+ secrets = JSON.parse(raw);
756
+ }
757
+ catch {
758
+ secrets = {};
759
+ }
760
+ const now = new Date().toISOString();
761
+ secrets[key] = {
762
+ key,
763
+ value,
764
+ createdAt: secrets[key]?.createdAt ?? now,
765
+ updatedAt: now,
766
+ };
767
+ fsSync.writeFileSync(filePath, `${JSON.stringify(secrets, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
768
+ try {
769
+ fsSync.chmodSync(filePath, 0o600);
770
+ }
771
+ catch {
772
+ // Best effort only; some platforms do not support chmod.
773
+ }
774
+ }
@@ -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.4",
3
+ "version": "0.13.6",
4
4
  "description": "Personal installable session server for continuing local AI work across PC and Telegram",
5
5
  "license": "MIT",
6
6
  "type": "module",