@songsid/agend 2.1.4-beta.3 → 2.1.4-beta.31

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.
Files changed (146) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-endpoint.js +1 -1
  4. package/dist/agent-endpoint.js.map +1 -1
  5. package/dist/backend/antigravity.d.ts +9 -5
  6. package/dist/backend/antigravity.js +155 -30
  7. package/dist/backend/antigravity.js.map +1 -1
  8. package/dist/backend/claude-code.d.ts +33 -21
  9. package/dist/backend/claude-code.js +103 -27
  10. package/dist/backend/claude-code.js.map +1 -1
  11. package/dist/backend/codex.d.ts +17 -0
  12. package/dist/backend/codex.js +123 -8
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/gemini-cli.js +2 -2
  15. package/dist/backend/gemini-cli.js.map +1 -1
  16. package/dist/backend/grok.js +2 -2
  17. package/dist/backend/grok.js.map +1 -1
  18. package/dist/backend/kiro.d.ts +1 -0
  19. package/dist/backend/kiro.js +17 -3
  20. package/dist/backend/kiro.js.map +1 -1
  21. package/dist/backend/opencode.d.ts +27 -11
  22. package/dist/backend/opencode.js +50 -39
  23. package/dist/backend/opencode.js.map +1 -1
  24. package/dist/backend/types.d.ts +20 -0
  25. package/dist/backend/types.js +12 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/channel/adapters/discord.js +106 -19
  28. package/dist/channel/adapters/discord.js.map +1 -1
  29. package/dist/channel/adapters/telegram.js +5 -3
  30. package/dist/channel/adapters/telegram.js.map +1 -1
  31. package/dist/channel/agy-mcp-launcher.d.ts +2 -0
  32. package/dist/channel/agy-mcp-launcher.js +28 -0
  33. package/dist/channel/agy-mcp-launcher.js.map +1 -0
  34. package/dist/channel/mcp-server.js +6 -20
  35. package/dist/channel/mcp-server.js.map +1 -1
  36. package/dist/channel/mcp-tools.js +4 -2
  37. package/dist/channel/mcp-tools.js.map +1 -1
  38. package/dist/channel/types.d.ts +1 -1
  39. package/dist/classic-channel-manager.d.ts +40 -3
  40. package/dist/classic-channel-manager.js +218 -31
  41. package/dist/classic-channel-manager.js.map +1 -1
  42. package/dist/cli.js +148 -33
  43. package/dist/cli.js.map +1 -1
  44. package/dist/completion-install.d.ts +70 -0
  45. package/dist/completion-install.js +152 -0
  46. package/dist/completion-install.js.map +1 -0
  47. package/dist/completion.d.ts +7 -1
  48. package/dist/completion.js +14 -4
  49. package/dist/completion.js.map +1 -1
  50. package/dist/config-validator.js +38 -7
  51. package/dist/config-validator.js.map +1 -1
  52. package/dist/config.d.ts +2 -0
  53. package/dist/config.js +10 -1
  54. package/dist/config.js.map +1 -1
  55. package/dist/daemon.d.ts +177 -5
  56. package/dist/daemon.js +771 -99
  57. package/dist/daemon.js.map +1 -1
  58. package/dist/doctor.d.ts +26 -0
  59. package/dist/doctor.js +216 -0
  60. package/dist/doctor.js.map +1 -0
  61. package/dist/fleet-context.d.ts +47 -0
  62. package/dist/fleet-manager.d.ts +251 -10
  63. package/dist/fleet-manager.js +2059 -387
  64. package/dist/fleet-manager.js.map +1 -1
  65. package/dist/fleet-yaml-slim.d.ts +10 -0
  66. package/dist/fleet-yaml-slim.js +59 -0
  67. package/dist/fleet-yaml-slim.js.map +1 -0
  68. package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
  69. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +3 -1
  70. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
  71. package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
  72. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
  73. package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
  74. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
  75. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
  76. package/dist/general-knowledge/skills/model-discovery/SKILL.md +64 -16
  77. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
  78. package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
  79. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -0
  80. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
  81. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
  82. package/dist/instance-lifecycle.d.ts +45 -0
  83. package/dist/instance-lifecycle.js +150 -5
  84. package/dist/instance-lifecycle.js.map +1 -1
  85. package/dist/instructions.d.ts +12 -0
  86. package/dist/instructions.js +35 -1
  87. package/dist/instructions.js.map +1 -1
  88. package/dist/locale.js +750 -288
  89. package/dist/locale.js.map +1 -1
  90. package/dist/login-flows.d.ts +85 -0
  91. package/dist/login-flows.js +159 -0
  92. package/dist/login-flows.js.map +1 -0
  93. package/dist/login-manager.d.ts +63 -0
  94. package/dist/login-manager.js +134 -0
  95. package/dist/login-manager.js.map +1 -0
  96. package/dist/outbound-handlers.d.ts +8 -0
  97. package/dist/outbound-handlers.js +44 -5
  98. package/dist/outbound-handlers.js.map +1 -1
  99. package/dist/outbound-schemas.d.ts +5 -0
  100. package/dist/outbound-schemas.js +7 -0
  101. package/dist/outbound-schemas.js.map +1 -1
  102. package/dist/quickstart.js +25 -0
  103. package/dist/quickstart.js.map +1 -1
  104. package/dist/restart-progress.d.ts +2 -0
  105. package/dist/restart-progress.js +3 -0
  106. package/dist/restart-progress.js.map +1 -1
  107. package/dist/scheduler/db.d.ts +12 -0
  108. package/dist/scheduler/db.js +59 -0
  109. package/dist/scheduler/db.js.map +1 -1
  110. package/dist/service-installer.d.ts +11 -0
  111. package/dist/service-installer.js +84 -18
  112. package/dist/service-installer.js.map +1 -1
  113. package/dist/settings-api.js +1 -1
  114. package/dist/settings-api.js.map +1 -1
  115. package/dist/setup-wizard.js +2 -2
  116. package/dist/setup-wizard.js.map +1 -1
  117. package/dist/tips.d.ts +44 -0
  118. package/dist/tips.js +355 -0
  119. package/dist/tips.js.map +1 -0
  120. package/dist/tmux-manager.d.ts +6 -0
  121. package/dist/tmux-manager.js +12 -0
  122. package/dist/tmux-manager.js.map +1 -1
  123. package/dist/tool-progress.d.ts +40 -0
  124. package/dist/tool-progress.js +289 -0
  125. package/dist/tool-progress.js.map +1 -0
  126. package/dist/topic-commands.d.ts +36 -4
  127. package/dist/topic-commands.js +322 -64
  128. package/dist/topic-commands.js.map +1 -1
  129. package/dist/transcript-monitor.d.ts +15 -2
  130. package/dist/transcript-monitor.js +63 -17
  131. package/dist/transcript-monitor.js.map +1 -1
  132. package/dist/transcript-sources.d.ts +131 -0
  133. package/dist/transcript-sources.js +580 -0
  134. package/dist/transcript-sources.js.map +1 -0
  135. package/dist/types.d.ts +11 -0
  136. package/dist/ui/dashboard.html +55 -32
  137. package/dist/ui/settings.html +120 -51
  138. package/dist/ui/view.html +23 -15
  139. package/dist/usage/providers.d.ts +7 -0
  140. package/dist/usage/providers.js +18 -1
  141. package/dist/usage/providers.js.map +1 -1
  142. package/dist/usage/usage-api.d.ts +8 -2
  143. package/dist/usage/usage-api.js +24 -3
  144. package/dist/usage/usage-api.js.map +1 -1
  145. package/dist/workflow-templates/default.md +2 -1
  146. package/package.json +1 -1
@@ -10,7 +10,7 @@ import { formatCents } from "./cost-guard.js";
10
10
  import { getTmuxSocketName, getTmuxSessionName } from "./paths.js";
11
11
  import { t } from "./locale.js";
12
12
  import { clampContextPercent, parseContextPercent, parseTokenContextRatio, } from "./context-percent.js";
13
- import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
13
+ import { isGeneralInstance } from "./general-instance.js";
14
14
  export { parseContextPercent, parseTokenContextRatio } from "./context-percent.js";
15
15
  /** Sanitize a directory name into a valid instance name. Keeps Unicode letters (incl. CJK). */
16
16
  export function sanitizeInstanceName(name) {
@@ -19,6 +19,8 @@ export function sanitizeInstanceName(name) {
19
19
  }
20
20
  /** Allowed filename for /save and /load (no path separators, no shell/inject chars). */
21
21
  export const SAVE_FILENAME_RE = /^[\w.-]+$/;
22
+ /** Backends with a native side-question command that does not steer the active turn. */
23
+ export const BTW_SUPPORTED_BACKENDS = new Set(["claude-code"]);
22
24
  /**
23
25
  * Build the backend-specific session-save command, or null if the backend has no
24
26
  * /save equivalent. kiro-cli → `/chat save <name>`; claude-code → `/export <name>.md`.
@@ -36,7 +38,8 @@ export function parseSaveFilename(text) {
36
38
  return (m?.[1] ?? "").trim();
37
39
  }
38
40
  /** Shared message when a backend doesn't support /save. */
39
- export const SAVE_UNSUPPORTED_MSG = "⚠️ /save is not supported for this backend (only kiro-cli and claude-code)";
41
+ /** @deprecated Prefer t("save.unsupported") at the user-facing call site. */
42
+ export const SAVE_UNSUPPORTED_MSG = "⚠️ /save is not supported for this backend (only Kiro CLI and Claude Code).";
40
43
  export function parsePauseWakeCommand(text) {
41
44
  const match = text.match(/^\/(pause|wake)(?:@\S+)?(?:\s+(\S+))?$/);
42
45
  if (!match)
@@ -103,7 +106,8 @@ export function clearCommandForBackend(backend) {
103
106
  return null;
104
107
  }
105
108
  }
106
- export const CLEAR_UNSUPPORTED_MSG = "⚠️ Clear not supported for this backend";
109
+ /** @deprecated Prefer t("clear.unsupported") at the user-facing call site. */
110
+ export const CLEAR_UNSUPPORTED_MSG = "⚠️ Clear is not supported for this backend.";
107
111
  /**
108
112
  * Extract context-usage % from a captured CLI pane. Scans bottom-up so the
109
113
  * MOST RECENT prompt wins (a captured scrollback may hold several). Covers the
@@ -270,6 +274,20 @@ export class TopicCommands {
270
274
  await this.handleUsageCommand(msg);
271
275
  return true;
272
276
  }
277
+ if (text === "/tips" || text.startsWith("/tips ") || text.startsWith("/tips@")) {
278
+ await this.handleTipsCommand(msg);
279
+ return true;
280
+ }
281
+ if (text === "/login" || text.startsWith("/login ") || text.startsWith("/login@")) {
282
+ await this.handleLoginCommand(msg);
283
+ return true;
284
+ }
285
+ // Telegram command names cannot contain "-", so the menu registers
286
+ // /install_cli; accept both spellings as typed text.
287
+ if (/^\/install[-_]cli(?:@\S+)?(?:\s|$)/.test(text)) {
288
+ await this.handleInstallCliCommand(msg);
289
+ return true;
290
+ }
273
291
  if (text === "/update" || text.startsWith("/update@")) {
274
292
  await this.handleUpdateCommand(msg);
275
293
  return true;
@@ -332,6 +350,13 @@ export class TopicCommands {
332
350
  const text = msg.text?.trim();
333
351
  if (!text)
334
352
  return false;
353
+ // Tips are informational and should appear where requested, including a
354
+ // worker topic. This also keeps Telegram text commands aligned with
355
+ // Discord's channel-local slash-command behavior.
356
+ if (text === "/tips" || text.startsWith("/tips ") || text.startsWith("/tips@")) {
357
+ await this.handleTipsCommand(msg);
358
+ return true;
359
+ }
335
360
  const pauseWake = parsePauseWakeCommand(text);
336
361
  if (pauseWake) {
337
362
  const adapter = this.getReplyAdapter(msg);
@@ -352,7 +377,7 @@ export class TopicCommands {
352
377
  return true;
353
378
  }
354
379
  if (pauseWake.action === "pause" && isGeneralInstance(this.ctx.fleetConfig, target)) {
355
- await adapter.sendText(msg.chatId, GENERAL_PAUSE_ERROR, { threadId: msg.threadId });
380
+ await adapter.sendText(msg.chatId, t("general.pause_forbidden"), { threadId: msg.threadId });
356
381
  return true;
357
382
  }
358
383
  await adapter.sendText(msg.chatId, await this.runPauseWake(target, pauseWake.action), { threadId: msg.threadId });
@@ -379,7 +404,7 @@ export class TopicCommands {
379
404
  const level = text.replace(/^\/effort(@\S+)?/, "").trim();
380
405
  if (level) {
381
406
  const reply = await this.ctx.applyEffort?.(instanceName, level)
382
- ?? "Effort switching is unavailable.";
407
+ ?? t("effort.unsupported", this.effectiveBackend(instanceName));
383
408
  await adapter.sendText(msg.chatId, reply, { threadId: msg.threadId });
384
409
  }
385
410
  else if (this.ctx.promptEffortMenu) {
@@ -410,7 +435,7 @@ export class TopicCommands {
410
435
  await adapter.sendText(msg.chatId, fallback, { threadId: msg.threadId });
411
436
  }
412
437
  else {
413
- await adapter.sendText(msg.chatId, "Usage: /model <name> — e.g. /model sonnet", { threadId: msg.threadId });
438
+ await adapter.sendText(msg.chatId, t("model.usage"), { threadId: msg.threadId });
414
439
  }
415
440
  return true;
416
441
  }
@@ -422,6 +447,39 @@ export class TopicCommands {
422
447
  await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
423
448
  return true;
424
449
  }
450
+ if (text === "/steer" || text.startsWith("/steer ") || text.startsWith("/steer@")) {
451
+ const adapter = this.getReplyAdapter(msg);
452
+ if (!adapter)
453
+ return false;
454
+ // Deliberately NOT admin-gated: anyone who can speak in this topic can
455
+ // already send the instance a message; /steer only changes WHEN it lands
456
+ // (mid-turn instead of after), and it goes through the full [user:]
457
+ // formatting — unlike /raw, which bypasses it and is gated.
458
+ const content = text.replace(/^\/steer(@\S+)?/, "").trim();
459
+ if (!content) {
460
+ await adapter.sendText(msg.chatId, t("steer.usage"), { threadId: msg.threadId });
461
+ return true;
462
+ }
463
+ const result = this.sendSteer(instanceName, content, msg);
464
+ await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
465
+ return true;
466
+ }
467
+ if (text === "/btw" || text.startsWith("/btw ") || text.startsWith("/btw@")) {
468
+ const adapter = this.getReplyAdapter(msg);
469
+ if (!adapter)
470
+ return false;
471
+ // Like /steer, /btw is not admin-gated: anyone who can send the agent a
472
+ // normal message may ask a side question. Claude owns the isolation from
473
+ // the active turn; AgEnD only submits its native command immediately.
474
+ const content = text.replace(/^\/btw(@\S+)?/, "").trim();
475
+ if (!content) {
476
+ await adapter.sendText(msg.chatId, t("btw.usage"), { threadId: msg.threadId });
477
+ return true;
478
+ }
479
+ const result = this.sendBtw(instanceName, content, msg);
480
+ await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
481
+ return true;
482
+ }
425
483
  if (text === "/clear" || text.startsWith("/clear@")) {
426
484
  const adapter = this.getReplyAdapter(msg);
427
485
  if (!adapter)
@@ -430,8 +488,13 @@ export class TopicCommands {
430
488
  await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
431
489
  return true;
432
490
  }
433
- const result = await this.sendClear(instanceName);
434
- await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
491
+ if (!this.ctx.promptClearConfirmation) {
492
+ await adapter.sendText(msg.chatId, t("clear.prompt_unavailable"), { threadId: msg.threadId });
493
+ return true;
494
+ }
495
+ const fallback = await this.ctx.promptClearConfirmation(instanceName, msg.threadId ?? msg.chatId, adapter, msg.chatId, msg.threadId);
496
+ if (fallback)
497
+ await adapter.sendText(msg.chatId, fallback, { threadId: msg.threadId });
435
498
  return true;
436
499
  }
437
500
  if (text === "/cancel" || text.startsWith("/cancel@")) {
@@ -470,7 +533,7 @@ export class TopicCommands {
470
533
  }
471
534
  async runPauseWake(instanceName, action) {
472
535
  if (action === "pause" && isGeneralInstance(this.ctx.fleetConfig, instanceName)) {
473
- return GENERAL_PAUSE_ERROR;
536
+ return t("general.pause_forbidden");
474
537
  }
475
538
  try {
476
539
  const result = await this.ctx.changeInstancePauseState(instanceName, action);
@@ -519,22 +582,88 @@ export class TopicCommands {
519
582
  ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
520
583
  const cmd = compactCommandForBackend(backend);
521
584
  ipc.send({ type: "raw_paste", content: cmd });
522
- return `🗜️ Compact command sent (\`${cmd}\`).`;
585
+ return t("compact.sent", cmd);
586
+ }
587
+ return t("compact.not_connected");
588
+ }
589
+ /** Whether the instance backend exposes a verified full-reset command. */
590
+ supportsClear(instanceName) {
591
+ return clearCommandForBackend(this.effectiveBackend(instanceName)) !== null;
592
+ }
593
+ /**
594
+ * Steer: interject a message into the instance's CURRENT turn. The daemon
595
+ * pastes it into the busy CLI instead of queueing for idle (falling back to
596
+ * the queue if the TUI swallows busy input — see Daemon.steerMessage).
597
+ */
598
+ /**
599
+ * Backends whose TUI accepts a busy-pane paste as steering input,
600
+ * live-verified: claude-code and codex buffer-then-submit at the turn
601
+ * boundary, grok accepts it in its input line. kiro's legacy TUI swallows
602
+ * the paste outright, and opencode/antigravity are unverified — for those
603
+ * the user gets an honest "not supported" instead of a silent queue
604
+ * fallback that looks like a steer but behaves like a normal message.
605
+ */
606
+ static STEER_SUPPORTED_BACKENDS = new Set(["claude-code", "codex", "grok", "mock"]);
607
+ sendSteer(instanceName, content, msg) {
608
+ const backend = this.effectiveBackend(instanceName);
609
+ if (!TopicCommands.STEER_SUPPORTED_BACKENDS.has(backend)) {
610
+ return t("steer.unsupported", backend);
523
611
  }
524
- return "❌ Instance not connected (IPC unavailable)";
612
+ const ipc = this.ctx.instanceIpcClients.get(instanceName);
613
+ if (!ipc?.connected)
614
+ return t("steer.not_connected");
615
+ ipc.send({
616
+ type: "steer",
617
+ content,
618
+ delivery_epoch: this.ctx.getDeliveryEpoch?.(instanceName) ?? undefined,
619
+ meta: {
620
+ chat_id: msg.chatId,
621
+ message_id: msg.messageId ?? "",
622
+ user: msg.username,
623
+ user_id: msg.userId,
624
+ thread_id: msg.threadId ?? "",
625
+ adapter_id: msg.adapterId ?? "",
626
+ source: msg.source,
627
+ },
628
+ });
629
+ return t("steer.sent", instanceName);
630
+ }
631
+ /** Ask a Claude Code side question without folding it into the active turn. */
632
+ sendBtw(instanceName, content, msg) {
633
+ const backend = this.effectiveBackend(instanceName);
634
+ if (!BTW_SUPPORTED_BACKENDS.has(backend))
635
+ return t("btw.unsupported", backend);
636
+ const ipc = this.ctx.instanceIpcClients.get(instanceName);
637
+ if (!ipc?.connected)
638
+ return t("btw.not_connected");
639
+ ipc.send({
640
+ type: "btw",
641
+ content,
642
+ delivery_epoch: this.ctx.getDeliveryEpoch?.(instanceName) ?? undefined,
643
+ meta: {
644
+ chat_id: msg.chatId,
645
+ message_id: msg.messageId ?? "",
646
+ user: msg.username,
647
+ user_id: msg.userId,
648
+ thread_id: msg.threadId ?? "",
649
+ adapter_id: msg.adapterId ?? "",
650
+ source: msg.source,
651
+ },
652
+ });
653
+ return t("btw.sent", instanceName);
525
654
  }
526
655
  /** Send the backend-appropriate full conversation reset through raw_paste. */
527
656
  async sendClear(instanceName) {
528
657
  const backend = this.effectiveBackend(instanceName);
529
658
  const cmd = clearCommandForBackend(backend);
530
659
  if (!cmd)
531
- return CLEAR_UNSUPPORTED_MSG;
660
+ return t("clear.unsupported");
532
661
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
533
662
  if (ipc?.connected) {
534
663
  ipc.send({ type: "raw_paste", content: cmd });
535
- return `🧹 Clear command sent (\`${cmd}\`).`;
664
+ return t("clear.sent", cmd);
536
665
  }
537
- return "❌ Instance not connected (IPC unavailable)";
666
+ return t("clear.not_connected");
538
667
  }
539
668
  /** Send a backend-appropriate session-save command to a fleet-topic instance. */
540
669
  async sendSave(instanceName, filename) {
@@ -546,13 +675,13 @@ export class TopicCommands {
546
675
  ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
547
676
  const cmd = saveCommandForBackend(backend, filename);
548
677
  if (!cmd)
549
- return SAVE_UNSUPPORTED_MSG;
678
+ return t("save.unsupported");
550
679
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
551
680
  if (ipc?.connected) {
552
681
  ipc.send({ type: "raw_paste", content: cmd });
553
- return `💾 Save command sent (\`${cmd}\`).`;
682
+ return t("save.sent", cmd, instanceName);
554
683
  }
555
- return "❌ Instance not connected (IPC unavailable)";
684
+ return t("save.not_connected");
556
685
  }
557
686
  async handleRestartCommand(msg) {
558
687
  const adapter = this.getReplyAdapter(msg);
@@ -569,8 +698,8 @@ export class TopicCommands {
569
698
  process.kill(process.pid, "SIGUSR2");
570
699
  }
571
700
  /**
572
- * `/usage` — AI subscription usage for every CLI backend logged in on this
573
- * machine, one compact message. Same permission level as /ctx (none): whoever
701
+ * `/usage` — AI subscription usage for CLI backends used by running or paused
702
+ * instances, one compact message. Same permission level as /ctx (none): whoever
574
703
  * can talk to the fleet may see how much headroom it has left. Vendor rate
575
704
  * limits are protected by the shared 5-minute cache, not by gating callers.
576
705
  */
@@ -581,7 +710,7 @@ export class TopicCommands {
581
710
  try {
582
711
  const { getUsageSnapshot } = await import("./usage/usage-api.js");
583
712
  const { renderUsageHtml, renderUsageMarkdown } = await import("./usage/format-rich.js");
584
- const payload = await getUsageSnapshot();
713
+ const payload = await getUsageSnapshot(false, this.ctx.getActiveUsageProviderIds?.());
585
714
  // Each platform gets its native rich format: Telegram needs parse_mode
586
715
  // HTML; Discord renders Markdown in plain content. Anything else falls back
587
716
  // to Markdown, which degrades to readable text.
@@ -593,7 +722,129 @@ export class TopicCommands {
593
722
  }
594
723
  }
595
724
  catch (err) {
596
- await adapter.sendText(msg.chatId, `⚠️ Usage fetch failed: ${err.message}`, { threadId: msg.threadId });
725
+ await adapter.sendText(msg.chatId, t("usage.failed", err.message), { threadId: msg.threadId });
726
+ }
727
+ }
728
+ /**
729
+ * `/login [backend] | code <text> | cancel` — remote CLI re-login.
730
+ * Fleet-admin only: the flow emits live authorization URLs/codes, and anyone
731
+ * completing one binds their own account to this fleet's CLI.
732
+ */
733
+ async handleLoginCommand(msg) {
734
+ const adapter = this.getReplyAdapter(msg);
735
+ if (!adapter)
736
+ return;
737
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
738
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
739
+ return;
740
+ }
741
+ if (!this.ctx.startLoginSession || !this.ctx.promptLoginBackends
742
+ || !this.ctx.loginSubmitInput || !this.ctx.cancelLoginSession) {
743
+ await adapter.sendText(msg.chatId, t("login.no_session"), { threadId: msg.threadId });
744
+ return;
745
+ }
746
+ const arg = msg.text.trim().replace(/^\/login(?:@\S+)?/, "").trim();
747
+ const chat = {
748
+ adapter,
749
+ adapterId: msg.adapterId ?? adapter.id,
750
+ chatId: msg.chatId,
751
+ threadId: msg.threadId,
752
+ };
753
+ if (!arg) {
754
+ await this.ctx.promptLoginBackends(chat);
755
+ return;
756
+ }
757
+ if (arg === "cancel") {
758
+ await adapter.sendText(msg.chatId, await this.ctx.cancelLoginSession(), { threadId: msg.threadId });
759
+ return;
760
+ }
761
+ if (arg.startsWith("code ")) {
762
+ const input = arg.slice(5).trim();
763
+ const reply = input ? await this.ctx.loginSubmitInput(input) : t("login.usage");
764
+ await adapter.sendText(msg.chatId, reply, { threadId: msg.threadId });
765
+ return;
766
+ }
767
+ if (/\s/.test(arg)) {
768
+ await adapter.sendText(msg.chatId, t("login.usage"), { threadId: msg.threadId });
769
+ return;
770
+ }
771
+ const started = await this.ctx.startLoginSession(arg, chat);
772
+ // null → the still-valid-auth confirmation prompt was posted instead.
773
+ if (started)
774
+ await adapter.sendText(msg.chatId, started, { threadId: msg.threadId });
775
+ }
776
+ /** `/install-cli <backend> | cancel` — remote CLI install. Fleet-admin only. */
777
+ async handleInstallCliCommand(msg) {
778
+ const adapter = this.getReplyAdapter(msg);
779
+ if (!adapter)
780
+ return;
781
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
782
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
783
+ return;
784
+ }
785
+ if (!this.ctx.startInstallSession || !this.ctx.cancelInstallSession) {
786
+ await adapter.sendText(msg.chatId, t("install.no_session"), { threadId: msg.threadId });
787
+ return;
788
+ }
789
+ const arg = msg.text.trim().replace(/^\/install[-_]cli(?:@\S+)?/, "").trim();
790
+ if (!arg || /\s/.test(arg)) {
791
+ await adapter.sendText(msg.chatId, t("install.usage"), { threadId: msg.threadId });
792
+ return;
793
+ }
794
+ if (arg === "cancel") {
795
+ await adapter.sendText(msg.chatId, await this.ctx.cancelInstallSession(), { threadId: msg.threadId });
796
+ return;
797
+ }
798
+ const chat = {
799
+ adapter,
800
+ adapterId: msg.adapterId ?? adapter.id,
801
+ chatId: msg.chatId,
802
+ threadId: msg.threadId,
803
+ };
804
+ await adapter.sendText(msg.chatId, await this.ctx.startInstallSession(arg, chat), { threadId: msg.threadId });
805
+ }
806
+ async handleTipsCommand(msg) {
807
+ const adapter = this.getReplyAdapter(msg);
808
+ if (!adapter || !this.ctx.fleetConfig)
809
+ return;
810
+ const mode = msg.text.trim().replace(/^\/tips(?:@\S+)?/, "").trim().toLowerCase();
811
+ if (mode === "advanced on") {
812
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
813
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
814
+ return;
815
+ }
816
+ await adapter.sendText(msg.chatId, this.ctx.unlockAdvancedTips?.(msg.userId)
817
+ ? t("tips.advanced.unlocked")
818
+ : t("tips.unavailable"), { threadId: msg.threadId });
819
+ return;
820
+ }
821
+ if (mode === "on" || mode === "off") {
822
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
823
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
824
+ return;
825
+ }
826
+ this.ctx.fleetConfig.defaults.tips = mode === "on";
827
+ this.ctx.saveFleetConfig();
828
+ await adapter.sendText(msg.chatId, t(mode === "on" ? "tips.enabled" : "tips.disabled"), {
829
+ threadId: msg.threadId,
830
+ });
831
+ return;
832
+ }
833
+ if (mode) {
834
+ await adapter.sendText(msg.chatId, t("tips.usage"), { threadId: msg.threadId });
835
+ return;
836
+ }
837
+ const result = await this.ctx.promptTip?.(
838
+ // The instance name scopes prompt cleanup; the Tip is posted at the
839
+ // caller's current chat/thread below.
840
+ Object.entries(this.ctx.fleetConfig.instances)
841
+ .find(([, config]) => config.general_topic === true
842
+ && (!config.channel_id || config.channel_id === msg.adapterId))?.[0] ?? "general", adapter, msg.chatId, msg.threadId) ?? "unavailable";
843
+ if (result === "empty") {
844
+ await adapter.sendText(msg.chatId, t("tips.empty"), { threadId: msg.threadId });
845
+ }
846
+ else if (result === "unavailable") {
847
+ await adapter.sendText(msg.chatId, t("tips.unavailable"), { threadId: msg.threadId });
597
848
  }
598
849
  }
599
850
  async handleStatusCommand(msg) {
@@ -622,7 +873,7 @@ export class TopicCommands {
622
873
  /** Get fleet status as markdown text (shared by TG + DC) */
623
874
  async getStatusText() {
624
875
  if (!this.ctx.fleetConfig)
625
- return "No fleet config loaded.";
876
+ return t("status.no_config");
626
877
  const rows = [];
627
878
  let pausedCount = 0;
628
879
  const fleetNames = new Set(Object.keys(this.ctx.fleetConfig.instances));
@@ -659,10 +910,10 @@ export class TopicCommands {
659
910
  icon = "🔴";
660
911
  else
661
912
  icon = "⚪";
662
- const stateLabel = executionState === "idle" ? "🟢 idle"
663
- : executionState === "working" ? "🔵 working"
664
- : executionState === "stuck" ? "🔴 stuck"
665
- : executionState === "paused" ? "⏸ paused"
913
+ const stateLabel = executionState === "idle" ? `🟢 ${t("state.idle")}`
914
+ : executionState === "working" ? `🔵 ${t("state.working")}`
915
+ : executionState === "stuck" ? `🔴 ${t("state.stuck")}`
916
+ : executionState === "paused" ? `⏸ ${t("state.paused")}`
666
917
  : "—";
667
918
  const displayName = this.shortInstanceName(name);
668
919
  // IPC reachability moved here from /sysinfo: it is per-instance health, and
@@ -674,27 +925,27 @@ export class TopicCommands {
674
925
  rows.push(`| ${displayName} | ${backend} | ${contextStr} | ${effort} | ${formatCents(costCents)} | ${icon} | ${stateLabel} | ${ipc} |`);
675
926
  }
676
927
  if (rows.length === 0)
677
- return "No instances configured.";
928
+ return t("status.no_instances");
678
929
  const lines = [
679
- "## Fleet Status",
930
+ `## ${t("status.title")}`,
680
931
  "",
681
- "| Instance | Backend | Ctx | Effort | Cost | Status | State | IPC |",
932
+ t("status.table_header"),
682
933
  "|----------|---------|-----|--------|------|--------|-------|-----|",
683
934
  ...rows,
684
935
  "",
685
- `Paused instances: ${pausedCount}`,
936
+ t("status.paused_count", pausedCount),
686
937
  ];
687
938
  const limitCents = this.ctx.costGuard?.getLimitCents() ?? 0;
688
939
  const totalCents = this.ctx.costGuard?.getFleetTotalCents() ?? 0;
689
940
  if (limitCents > 0) {
690
- lines.push("", `Fleet: ${formatCents(totalCents)} / ${formatCents(limitCents)} daily`);
941
+ lines.push("", t("status.daily_cost", formatCents(totalCents), formatCents(limitCents)));
691
942
  }
692
943
  // Adapter states (only show if any are not connected)
693
944
  const adapterStates = this.ctx.getAdapterStates?.();
694
945
  if (adapterStates && adapterStates.size > 0) {
695
946
  const issues = [...adapterStates.entries()].filter(([, s]) => s.status !== "connected");
696
947
  if (issues.length > 0) {
697
- lines.push("", "**Adapters:**");
948
+ lines.push("", `**${t("status.adapters")}**`);
698
949
  for (const [id, s] of adapterStates) {
699
950
  const icon = s.status === "connected" ? "✅" : s.status === "retrying" ? "🔄" : "❌";
700
951
  lines.push(` ${icon} ${id}: ${s.status}${s.lastError ? ` (${s.lastError.slice(0, 60)})` : ""}`);
@@ -725,17 +976,17 @@ export class TopicCommands {
725
976
  const require = createRequire(import.meta.url);
726
977
  const agendVersion = require("../package.json").version ?? "unknown";
727
978
  return [
728
- "## System Info",
979
+ `## ${t("sysinfo.title")}`,
729
980
  "",
730
- "| Metric | Value |",
981
+ `| ${t("sysinfo.metric")} | ${t("sysinfo.value")} |`,
731
982
  "|--------|-------|",
732
983
  `| AgEnD | v${agendVersion} |`,
733
984
  `| OS | ${process.platform} ${osRelease()} (${process.arch}) |`,
734
985
  `| Node | ${process.version} |`,
735
986
  `| tmux | ${tmuxVersion()} |`,
736
- `| Uptime | ${upHours}h ${upMins}m |`,
737
- `| Memory | ${info.memory_mb.rss} MB RSS |`,
738
- `| Heap | ${info.memory_mb.heapUsed} / ${info.memory_mb.heapTotal} MB |`,
987
+ `| ${t("sysinfo.uptime")} | ${upHours}h ${upMins}m |`,
988
+ `| ${t("sysinfo.memory")} | ${info.memory_mb.rss} MB RSS |`,
989
+ `| ${t("sysinfo.heap")} | ${info.memory_mb.heapUsed} / ${info.memory_mb.heapTotal} MB |`,
739
990
  ].join("\n");
740
991
  }
741
992
  async handleUpdateCommand(msg) {
@@ -788,11 +1039,11 @@ export class TopicCommands {
788
1039
  timeout: 30_000,
789
1040
  encoding: "utf-8",
790
1041
  });
791
- output = stripAnsi(stdout) || "No output";
1042
+ output = stripAnsi(stdout) || t("doctor.no_output");
792
1043
  }
793
1044
  catch (err) {
794
1045
  const e = err;
795
- output = stripAnsi(e.stdout ?? e.message ?? "Doctor failed");
1046
+ output = stripAnsi(e.stdout ?? e.message ?? t("doctor.failed"));
796
1047
  }
797
1048
  await adapter.sendText(chatId, output, { threadId });
798
1049
  }
@@ -801,7 +1052,7 @@ export class TopicCommands {
801
1052
  const adapter = this.getReplyAdapter(msg);
802
1053
  if (!adapter)
803
1054
  return;
804
- await adapter.sendText(msg.chatId, "This topic is not bound to an instance. Ask the General assistant to create one with create_instance.", { threadId: msg.threadId });
1055
+ await adapter.sendText(msg.chatId, t("topic.unbound"), { threadId: msg.threadId });
805
1056
  }
806
1057
  /** Handle topic deletion — stop daemon and remove from config */
807
1058
  async handleTopicDeleted(threadId) {
@@ -914,21 +1165,26 @@ export class TopicCommands {
914
1165
  }
915
1166
  try {
916
1167
  const fleetCommands = [
917
- { command: "status", description: "🔒 Fleet status, per-instance costs and health" },
918
- { command: "sysinfo", description: "System diagnostics" },
919
- { command: "dashboard", description: "🔒 Get dashboard URLs" },
920
- { command: "ctx", description: "Show context usage" },
921
- { command: "compact", description: "Compact agent context" },
922
- { command: "clear", description: "🔒 Clear agent conversation context" },
923
- { command: "model", description: "🔒 Switch model (admin only)" },
924
- { command: "effort", description: "🔒 Set reasoning effort (admin only)" },
925
- { command: "pause", description: "🔒 Pause an idle instance" },
926
- { command: "wake", description: "🔒 Wake a paused instance" },
927
- { command: "restart", description: "🔒 Graceful restart all instances" },
928
- { command: "collab", description: "🔒 Toggle bot/webhook mode" },
929
- { command: "update", description: "🔒 Update AgEnD to latest" },
930
- { command: "doctor", description: "🔒 Run health diagnostics" },
931
- { command: "usage", description: "AI subscription usage" },
1168
+ { command: "status", description: "🔒 " + t("slash.status") },
1169
+ { command: "sysinfo", description: t("slash.sysinfo") },
1170
+ { command: "dashboard", description: "🔒 " + t("slash.dashboard") },
1171
+ { command: "ctx", description: t("slash.ctx") },
1172
+ { command: "compact", description: t("slash.compact") },
1173
+ { command: "steer", description: t("slash.steer") },
1174
+ { command: "btw", description: t("slash.btw") },
1175
+ { command: "clear", description: "🔒 " + t("slash.clear") },
1176
+ { command: "model", description: "🔒 " + t("slash.model") },
1177
+ { command: "effort", description: "🔒 " + t("slash.effort") },
1178
+ { command: "pause", description: "🔒 " + t("slash.pause") },
1179
+ { command: "wake", description: "🔒 " + t("slash.wake") },
1180
+ { command: "restart", description: "🔒 " + t("slash.restart") },
1181
+ { command: "collab", description: "🔒 " + t("slash.collab") },
1182
+ { command: "update", description: "🔒 " + t("slash.update") },
1183
+ { command: "doctor", description: "🔒 " + t("slash.doctor") },
1184
+ { command: "login", description: "🔒 " + t("slash.login") },
1185
+ { command: "install_cli", description: "🔒 " + t("slash.install_cli") },
1186
+ { command: "usage", description: t("slash.usage") },
1187
+ { command: "tips", description: t("slash.tips") },
932
1188
  ];
933
1189
  const setCommands = async (commands, scope) => {
934
1190
  const response = await fetch(`https://api.telegram.org/bot${botToken}/setMyCommands`, {
@@ -949,15 +1205,17 @@ export class TopicCommands {
949
1205
  }
950
1206
  };
951
1207
  const classicCommands = [
952
- { command: "start", description: "🔒 Start an agent in this chat" },
953
- { command: "stop", description: "🔒 Stop the agent" },
954
- { command: "compact", description: "🔒 Compact agent context" },
955
- { command: "clear", description: "🔒 Clear agent conversation context" },
956
- { command: "model", description: "🔒 Switch model (admin only)" },
957
- { command: "effort", description: "🔒 Set reasoning effort (admin only)" },
958
- { command: "pause", description: "🔒 Pause the agent" },
959
- { command: "wake", description: "🔒 Wake the agent" },
960
- { command: "ctx", description: "Show context usage" },
1208
+ { command: "start", description: "🔒 " + t("slash.start") },
1209
+ { command: "stop", description: "🔒 " + t("slash.stop") },
1210
+ { command: "compact", description: "🔒 " + t("slash.compact") },
1211
+ { command: "steer", description: t("slash.steer") },
1212
+ { command: "btw", description: t("slash.btw") },
1213
+ { command: "clear", description: "🔒 " + t("slash.clear") },
1214
+ { command: "model", description: "🔒 " + t("slash.model") },
1215
+ { command: "effort", description: "🔒 " + t("slash.effort") },
1216
+ { command: "pause", description: "🔒 " + t("slash.pause") },
1217
+ { command: "wake", description: "🔒 " + t("slash.wake") },
1218
+ { command: "ctx", description: t("slash.ctx") },
961
1219
  ];
962
1220
  // A chat_administrators scope has higher precedence than the chat scope.
963
1221
  // Keep both synchronized so a stale admin-only list from BotFather or an