@songsid/agend 2.1.4-beta.2 → 2.1.4-beta.20

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 (128) hide show
  1. package/dist/agent-endpoint.js +1 -1
  2. package/dist/agent-endpoint.js.map +1 -1
  3. package/dist/backend/antigravity.d.ts +1 -0
  4. package/dist/backend/antigravity.js +4 -0
  5. package/dist/backend/antigravity.js.map +1 -1
  6. package/dist/backend/claude-code.js +23 -2
  7. package/dist/backend/claude-code.js.map +1 -1
  8. package/dist/backend/codex.d.ts +17 -0
  9. package/dist/backend/codex.js +122 -7
  10. package/dist/backend/codex.js.map +1 -1
  11. package/dist/backend/kiro.d.ts +1 -0
  12. package/dist/backend/kiro.js +14 -0
  13. package/dist/backend/kiro.js.map +1 -1
  14. package/dist/backend/opencode.d.ts +27 -11
  15. package/dist/backend/opencode.js +48 -37
  16. package/dist/backend/opencode.js.map +1 -1
  17. package/dist/backend/types.d.ts +14 -0
  18. package/dist/backend/types.js +7 -0
  19. package/dist/backend/types.js.map +1 -1
  20. package/dist/channel/adapters/discord.js +72 -25
  21. package/dist/channel/adapters/discord.js.map +1 -1
  22. package/dist/channel/adapters/telegram.js +5 -3
  23. package/dist/channel/adapters/telegram.js.map +1 -1
  24. package/dist/channel/mcp-server.js +6 -20
  25. package/dist/channel/mcp-server.js.map +1 -1
  26. package/dist/channel/mcp-tools.js +4 -2
  27. package/dist/channel/mcp-tools.js.map +1 -1
  28. package/dist/channel/types.d.ts +1 -1
  29. package/dist/classic-channel-manager.d.ts +40 -3
  30. package/dist/classic-channel-manager.js +218 -31
  31. package/dist/classic-channel-manager.js.map +1 -1
  32. package/dist/cli.js +159 -36
  33. package/dist/cli.js.map +1 -1
  34. package/dist/completion-install.d.ts +70 -0
  35. package/dist/completion-install.js +152 -0
  36. package/dist/completion-install.js.map +1 -0
  37. package/dist/completion.d.ts +7 -1
  38. package/dist/completion.js +14 -4
  39. package/dist/completion.js.map +1 -1
  40. package/dist/config-validator.js +38 -7
  41. package/dist/config-validator.js.map +1 -1
  42. package/dist/config.d.ts +2 -0
  43. package/dist/config.js +10 -1
  44. package/dist/config.js.map +1 -1
  45. package/dist/daemon.d.ts +171 -5
  46. package/dist/daemon.js +696 -82
  47. package/dist/daemon.js.map +1 -1
  48. package/dist/doctor.d.ts +26 -0
  49. package/dist/doctor.js +216 -0
  50. package/dist/doctor.js.map +1 -0
  51. package/dist/fleet-context.d.ts +14 -0
  52. package/dist/fleet-manager.d.ts +231 -10
  53. package/dist/fleet-manager.js +1798 -493
  54. package/dist/fleet-manager.js.map +1 -1
  55. package/dist/fleet-yaml-slim.d.ts +10 -0
  56. package/dist/fleet-yaml-slim.js +59 -0
  57. package/dist/fleet-yaml-slim.js.map +1 -0
  58. package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
  59. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +3 -1
  60. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
  61. package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
  62. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
  63. package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
  64. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
  65. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
  66. package/dist/general-knowledge/skills/model-discovery/SKILL.md +64 -16
  67. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
  68. package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
  69. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -0
  70. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
  71. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
  72. package/dist/instance-lifecycle.d.ts +8 -0
  73. package/dist/instance-lifecycle.js +46 -25
  74. package/dist/instance-lifecycle.js.map +1 -1
  75. package/dist/instructions.d.ts +12 -0
  76. package/dist/instructions.js +33 -0
  77. package/dist/instructions.js.map +1 -1
  78. package/dist/locale.js +656 -220
  79. package/dist/locale.js.map +1 -1
  80. package/dist/outbound-handlers.d.ts +8 -0
  81. package/dist/outbound-handlers.js +44 -5
  82. package/dist/outbound-handlers.js.map +1 -1
  83. package/dist/outbound-schemas.d.ts +5 -0
  84. package/dist/outbound-schemas.js +7 -0
  85. package/dist/outbound-schemas.js.map +1 -1
  86. package/dist/quickstart.js +25 -0
  87. package/dist/quickstart.js.map +1 -1
  88. package/dist/restart-progress.d.ts +9 -1
  89. package/dist/restart-progress.js +30 -6
  90. package/dist/restart-progress.js.map +1 -1
  91. package/dist/scheduler/db.d.ts +5 -0
  92. package/dist/scheduler/db.js +35 -0
  93. package/dist/scheduler/db.js.map +1 -1
  94. package/dist/service-installer.d.ts +11 -0
  95. package/dist/service-installer.js +84 -18
  96. package/dist/service-installer.js.map +1 -1
  97. package/dist/settings-api.js +1 -1
  98. package/dist/settings-api.js.map +1 -1
  99. package/dist/tips.d.ts +39 -0
  100. package/dist/tips.js +351 -0
  101. package/dist/tips.js.map +1 -0
  102. package/dist/tool-progress.d.ts +40 -0
  103. package/dist/tool-progress.js +289 -0
  104. package/dist/tool-progress.js.map +1 -0
  105. package/dist/topic-commands.d.ts +28 -4
  106. package/dist/topic-commands.js +217 -65
  107. package/dist/topic-commands.js.map +1 -1
  108. package/dist/transcript-monitor.d.ts +15 -2
  109. package/dist/transcript-monitor.js +63 -17
  110. package/dist/transcript-monitor.js.map +1 -1
  111. package/dist/transcript-sources.d.ts +106 -0
  112. package/dist/transcript-sources.js +428 -0
  113. package/dist/transcript-sources.js.map +1 -0
  114. package/dist/types.d.ts +11 -0
  115. package/dist/ui/dashboard.html +55 -32
  116. package/dist/ui/settings.html +120 -51
  117. package/dist/ui/view.html +23 -15
  118. package/dist/update-marker.d.ts +30 -0
  119. package/dist/update-marker.js +71 -20
  120. package/dist/update-marker.js.map +1 -1
  121. package/dist/update-progress.d.ts +6 -0
  122. package/dist/update-progress.js +29 -0
  123. package/dist/update-progress.js.map +1 -0
  124. package/dist/usage/usage-api.d.ts +8 -2
  125. package/dist/usage/usage-api.js +24 -3
  126. package/dist/usage/usage-api.js.map +1 -1
  127. package/dist/workflow-templates/default.md +2 -1
  128. 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,10 @@ 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
+ }
273
281
  if (text === "/update" || text.startsWith("/update@")) {
274
282
  await this.handleUpdateCommand(msg);
275
283
  return true;
@@ -352,7 +360,7 @@ export class TopicCommands {
352
360
  return true;
353
361
  }
354
362
  if (pauseWake.action === "pause" && isGeneralInstance(this.ctx.fleetConfig, target)) {
355
- await adapter.sendText(msg.chatId, GENERAL_PAUSE_ERROR, { threadId: msg.threadId });
363
+ await adapter.sendText(msg.chatId, t("general.pause_forbidden"), { threadId: msg.threadId });
356
364
  return true;
357
365
  }
358
366
  await adapter.sendText(msg.chatId, await this.runPauseWake(target, pauseWake.action), { threadId: msg.threadId });
@@ -379,7 +387,7 @@ export class TopicCommands {
379
387
  const level = text.replace(/^\/effort(@\S+)?/, "").trim();
380
388
  if (level) {
381
389
  const reply = await this.ctx.applyEffort?.(instanceName, level)
382
- ?? "Effort switching is unavailable.";
390
+ ?? t("effort.unsupported", this.effectiveBackend(instanceName));
383
391
  await adapter.sendText(msg.chatId, reply, { threadId: msg.threadId });
384
392
  }
385
393
  else if (this.ctx.promptEffortMenu) {
@@ -410,7 +418,7 @@ export class TopicCommands {
410
418
  await adapter.sendText(msg.chatId, fallback, { threadId: msg.threadId });
411
419
  }
412
420
  else {
413
- await adapter.sendText(msg.chatId, "Usage: /model <name> — e.g. /model sonnet", { threadId: msg.threadId });
421
+ await adapter.sendText(msg.chatId, t("model.usage"), { threadId: msg.threadId });
414
422
  }
415
423
  return true;
416
424
  }
@@ -422,6 +430,39 @@ export class TopicCommands {
422
430
  await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
423
431
  return true;
424
432
  }
433
+ if (text === "/steer" || text.startsWith("/steer ") || text.startsWith("/steer@")) {
434
+ const adapter = this.getReplyAdapter(msg);
435
+ if (!adapter)
436
+ return false;
437
+ // Deliberately NOT admin-gated: anyone who can speak in this topic can
438
+ // already send the instance a message; /steer only changes WHEN it lands
439
+ // (mid-turn instead of after), and it goes through the full [user:]
440
+ // formatting — unlike /raw, which bypasses it and is gated.
441
+ const content = text.replace(/^\/steer(@\S+)?/, "").trim();
442
+ if (!content) {
443
+ await adapter.sendText(msg.chatId, t("steer.usage"), { threadId: msg.threadId });
444
+ return true;
445
+ }
446
+ const result = this.sendSteer(instanceName, content, msg);
447
+ await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
448
+ return true;
449
+ }
450
+ if (text === "/btw" || text.startsWith("/btw ") || text.startsWith("/btw@")) {
451
+ const adapter = this.getReplyAdapter(msg);
452
+ if (!adapter)
453
+ return false;
454
+ // Like /steer, /btw is not admin-gated: anyone who can send the agent a
455
+ // normal message may ask a side question. Claude owns the isolation from
456
+ // the active turn; AgEnD only submits its native command immediately.
457
+ const content = text.replace(/^\/btw(@\S+)?/, "").trim();
458
+ if (!content) {
459
+ await adapter.sendText(msg.chatId, t("btw.usage"), { threadId: msg.threadId });
460
+ return true;
461
+ }
462
+ const result = this.sendBtw(instanceName, content, msg);
463
+ await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
464
+ return true;
465
+ }
425
466
  if (text === "/clear" || text.startsWith("/clear@")) {
426
467
  const adapter = this.getReplyAdapter(msg);
427
468
  if (!adapter)
@@ -430,8 +471,13 @@ export class TopicCommands {
430
471
  await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
431
472
  return true;
432
473
  }
433
- const result = await this.sendClear(instanceName);
434
- await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
474
+ if (!this.ctx.promptClearConfirmation) {
475
+ await adapter.sendText(msg.chatId, t("clear.prompt_unavailable"), { threadId: msg.threadId });
476
+ return true;
477
+ }
478
+ const fallback = await this.ctx.promptClearConfirmation(instanceName, msg.threadId ?? msg.chatId, adapter, msg.chatId, msg.threadId);
479
+ if (fallback)
480
+ await adapter.sendText(msg.chatId, fallback, { threadId: msg.threadId });
435
481
  return true;
436
482
  }
437
483
  if (text === "/cancel" || text.startsWith("/cancel@")) {
@@ -470,7 +516,7 @@ export class TopicCommands {
470
516
  }
471
517
  async runPauseWake(instanceName, action) {
472
518
  if (action === "pause" && isGeneralInstance(this.ctx.fleetConfig, instanceName)) {
473
- return GENERAL_PAUSE_ERROR;
519
+ return t("general.pause_forbidden");
474
520
  }
475
521
  try {
476
522
  const result = await this.ctx.changeInstancePauseState(instanceName, action);
@@ -519,22 +565,88 @@ export class TopicCommands {
519
565
  ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
520
566
  const cmd = compactCommandForBackend(backend);
521
567
  ipc.send({ type: "raw_paste", content: cmd });
522
- return `🗜️ Compact command sent (\`${cmd}\`).`;
568
+ return t("compact.sent", cmd);
569
+ }
570
+ return t("compact.not_connected");
571
+ }
572
+ /** Whether the instance backend exposes a verified full-reset command. */
573
+ supportsClear(instanceName) {
574
+ return clearCommandForBackend(this.effectiveBackend(instanceName)) !== null;
575
+ }
576
+ /**
577
+ * Steer: interject a message into the instance's CURRENT turn. The daemon
578
+ * pastes it into the busy CLI instead of queueing for idle (falling back to
579
+ * the queue if the TUI swallows busy input — see Daemon.steerMessage).
580
+ */
581
+ /**
582
+ * Backends whose TUI accepts a busy-pane paste as steering input,
583
+ * live-verified: claude-code and codex buffer-then-submit at the turn
584
+ * boundary, grok accepts it in its input line. kiro's legacy TUI swallows
585
+ * the paste outright, and opencode/antigravity are unverified — for those
586
+ * the user gets an honest "not supported" instead of a silent queue
587
+ * fallback that looks like a steer but behaves like a normal message.
588
+ */
589
+ static STEER_SUPPORTED_BACKENDS = new Set(["claude-code", "codex", "grok", "mock"]);
590
+ sendSteer(instanceName, content, msg) {
591
+ const backend = this.effectiveBackend(instanceName);
592
+ if (!TopicCommands.STEER_SUPPORTED_BACKENDS.has(backend)) {
593
+ return t("steer.unsupported", backend);
523
594
  }
524
- return "❌ Instance not connected (IPC unavailable)";
595
+ const ipc = this.ctx.instanceIpcClients.get(instanceName);
596
+ if (!ipc?.connected)
597
+ return t("steer.not_connected");
598
+ ipc.send({
599
+ type: "steer",
600
+ content,
601
+ delivery_epoch: this.ctx.getDeliveryEpoch?.(instanceName) ?? undefined,
602
+ meta: {
603
+ chat_id: msg.chatId,
604
+ message_id: msg.messageId ?? "",
605
+ user: msg.username,
606
+ user_id: msg.userId,
607
+ thread_id: msg.threadId ?? "",
608
+ adapter_id: msg.adapterId ?? "",
609
+ source: msg.source,
610
+ },
611
+ });
612
+ return t("steer.sent", instanceName);
613
+ }
614
+ /** Ask a Claude Code side question without folding it into the active turn. */
615
+ sendBtw(instanceName, content, msg) {
616
+ const backend = this.effectiveBackend(instanceName);
617
+ if (!BTW_SUPPORTED_BACKENDS.has(backend))
618
+ return t("btw.unsupported", backend);
619
+ const ipc = this.ctx.instanceIpcClients.get(instanceName);
620
+ if (!ipc?.connected)
621
+ return t("btw.not_connected");
622
+ ipc.send({
623
+ type: "btw",
624
+ content,
625
+ delivery_epoch: this.ctx.getDeliveryEpoch?.(instanceName) ?? undefined,
626
+ meta: {
627
+ chat_id: msg.chatId,
628
+ message_id: msg.messageId ?? "",
629
+ user: msg.username,
630
+ user_id: msg.userId,
631
+ thread_id: msg.threadId ?? "",
632
+ adapter_id: msg.adapterId ?? "",
633
+ source: msg.source,
634
+ },
635
+ });
636
+ return t("btw.sent", instanceName);
525
637
  }
526
638
  /** Send the backend-appropriate full conversation reset through raw_paste. */
527
639
  async sendClear(instanceName) {
528
640
  const backend = this.effectiveBackend(instanceName);
529
641
  const cmd = clearCommandForBackend(backend);
530
642
  if (!cmd)
531
- return CLEAR_UNSUPPORTED_MSG;
643
+ return t("clear.unsupported");
532
644
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
533
645
  if (ipc?.connected) {
534
646
  ipc.send({ type: "raw_paste", content: cmd });
535
- return `🧹 Clear command sent (\`${cmd}\`).`;
647
+ return t("clear.sent", cmd);
536
648
  }
537
- return "❌ Instance not connected (IPC unavailable)";
649
+ return t("clear.not_connected");
538
650
  }
539
651
  /** Send a backend-appropriate session-save command to a fleet-topic instance. */
540
652
  async sendSave(instanceName, filename) {
@@ -546,13 +658,13 @@ export class TopicCommands {
546
658
  ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
547
659
  const cmd = saveCommandForBackend(backend, filename);
548
660
  if (!cmd)
549
- return SAVE_UNSUPPORTED_MSG;
661
+ return t("save.unsupported");
550
662
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
551
663
  if (ipc?.connected) {
552
664
  ipc.send({ type: "raw_paste", content: cmd });
553
- return `💾 Save command sent (\`${cmd}\`).`;
665
+ return t("save.sent", cmd, instanceName);
554
666
  }
555
- return "❌ Instance not connected (IPC unavailable)";
667
+ return t("save.not_connected");
556
668
  }
557
669
  async handleRestartCommand(msg) {
558
670
  const adapter = this.getReplyAdapter(msg);
@@ -569,8 +681,8 @@ export class TopicCommands {
569
681
  process.kill(process.pid, "SIGUSR2");
570
682
  }
571
683
  /**
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
684
+ * `/usage` — AI subscription usage for CLI backends used by running or paused
685
+ * instances, one compact message. Same permission level as /ctx (none): whoever
574
686
  * can talk to the fleet may see how much headroom it has left. Vendor rate
575
687
  * limits are protected by the shared 5-minute cache, not by gating callers.
576
688
  */
@@ -581,7 +693,7 @@ export class TopicCommands {
581
693
  try {
582
694
  const { getUsageSnapshot } = await import("./usage/usage-api.js");
583
695
  const { renderUsageHtml, renderUsageMarkdown } = await import("./usage/format-rich.js");
584
- const payload = await getUsageSnapshot();
696
+ const payload = await getUsageSnapshot(false, this.ctx.getActiveUsageProviderIds?.());
585
697
  // Each platform gets its native rich format: Telegram needs parse_mode
586
698
  // HTML; Discord renders Markdown in plain content. Anything else falls back
587
699
  // to Markdown, which degrades to readable text.
@@ -593,7 +705,40 @@ export class TopicCommands {
593
705
  }
594
706
  }
595
707
  catch (err) {
596
- await adapter.sendText(msg.chatId, `⚠️ Usage fetch failed: ${err.message}`, { threadId: msg.threadId });
708
+ await adapter.sendText(msg.chatId, t("usage.failed", err.message), { threadId: msg.threadId });
709
+ }
710
+ }
711
+ async handleTipsCommand(msg) {
712
+ const adapter = this.getReplyAdapter(msg);
713
+ if (!adapter || !this.ctx.fleetConfig)
714
+ return;
715
+ const mode = msg.text.trim().replace(/^\/tips(?:@\S+)?/, "").trim().toLowerCase();
716
+ if (mode === "on" || mode === "off") {
717
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
718
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
719
+ return;
720
+ }
721
+ this.ctx.fleetConfig.defaults.tips = mode === "on";
722
+ this.ctx.saveFleetConfig();
723
+ await adapter.sendText(msg.chatId, t(mode === "on" ? "tips.enabled" : "tips.disabled"), {
724
+ threadId: msg.threadId,
725
+ });
726
+ return;
727
+ }
728
+ if (mode) {
729
+ await adapter.sendText(msg.chatId, t("tips.usage"), { threadId: msg.threadId });
730
+ return;
731
+ }
732
+ const result = await this.ctx.promptTip?.(
733
+ // /tips is a General command, so the route target is the current General.
734
+ Object.entries(this.ctx.fleetConfig.instances)
735
+ .find(([, config]) => config.general_topic === true
736
+ && (!config.channel_id || config.channel_id === msg.adapterId))?.[0] ?? "general", adapter, msg.chatId, msg.threadId) ?? "unavailable";
737
+ if (result === "empty") {
738
+ await adapter.sendText(msg.chatId, t("tips.empty"), { threadId: msg.threadId });
739
+ }
740
+ else if (result === "unavailable") {
741
+ await adapter.sendText(msg.chatId, t("tips.unavailable"), { threadId: msg.threadId });
597
742
  }
598
743
  }
599
744
  async handleStatusCommand(msg) {
@@ -622,7 +767,7 @@ export class TopicCommands {
622
767
  /** Get fleet status as markdown text (shared by TG + DC) */
623
768
  async getStatusText() {
624
769
  if (!this.ctx.fleetConfig)
625
- return "No fleet config loaded.";
770
+ return t("status.no_config");
626
771
  const rows = [];
627
772
  let pausedCount = 0;
628
773
  const fleetNames = new Set(Object.keys(this.ctx.fleetConfig.instances));
@@ -659,10 +804,10 @@ export class TopicCommands {
659
804
  icon = "🔴";
660
805
  else
661
806
  icon = "⚪";
662
- const stateLabel = executionState === "idle" ? "🟢 idle"
663
- : executionState === "working" ? "🔵 working"
664
- : executionState === "stuck" ? "🔴 stuck"
665
- : executionState === "paused" ? "⏸ paused"
807
+ const stateLabel = executionState === "idle" ? `🟢 ${t("state.idle")}`
808
+ : executionState === "working" ? `🔵 ${t("state.working")}`
809
+ : executionState === "stuck" ? `🔴 ${t("state.stuck")}`
810
+ : executionState === "paused" ? `⏸ ${t("state.paused")}`
666
811
  : "—";
667
812
  const displayName = this.shortInstanceName(name);
668
813
  // IPC reachability moved here from /sysinfo: it is per-instance health, and
@@ -674,27 +819,27 @@ export class TopicCommands {
674
819
  rows.push(`| ${displayName} | ${backend} | ${contextStr} | ${effort} | ${formatCents(costCents)} | ${icon} | ${stateLabel} | ${ipc} |`);
675
820
  }
676
821
  if (rows.length === 0)
677
- return "No instances configured.";
822
+ return t("status.no_instances");
678
823
  const lines = [
679
- "## Fleet Status",
824
+ `## ${t("status.title")}`,
680
825
  "",
681
- "| Instance | Backend | Ctx | Effort | Cost | Status | State | IPC |",
826
+ t("status.table_header"),
682
827
  "|----------|---------|-----|--------|------|--------|-------|-----|",
683
828
  ...rows,
684
829
  "",
685
- `Paused instances: ${pausedCount}`,
830
+ t("status.paused_count", pausedCount),
686
831
  ];
687
832
  const limitCents = this.ctx.costGuard?.getLimitCents() ?? 0;
688
833
  const totalCents = this.ctx.costGuard?.getFleetTotalCents() ?? 0;
689
834
  if (limitCents > 0) {
690
- lines.push("", `Fleet: ${formatCents(totalCents)} / ${formatCents(limitCents)} daily`);
835
+ lines.push("", t("status.daily_cost", formatCents(totalCents), formatCents(limitCents)));
691
836
  }
692
837
  // Adapter states (only show if any are not connected)
693
838
  const adapterStates = this.ctx.getAdapterStates?.();
694
839
  if (adapterStates && adapterStates.size > 0) {
695
840
  const issues = [...adapterStates.entries()].filter(([, s]) => s.status !== "connected");
696
841
  if (issues.length > 0) {
697
- lines.push("", "**Adapters:**");
842
+ lines.push("", `**${t("status.adapters")}**`);
698
843
  for (const [id, s] of adapterStates) {
699
844
  const icon = s.status === "connected" ? "✅" : s.status === "retrying" ? "🔄" : "❌";
700
845
  lines.push(` ${icon} ${id}: ${s.status}${s.lastError ? ` (${s.lastError.slice(0, 60)})` : ""}`);
@@ -725,17 +870,17 @@ export class TopicCommands {
725
870
  const require = createRequire(import.meta.url);
726
871
  const agendVersion = require("../package.json").version ?? "unknown";
727
872
  return [
728
- "## System Info",
873
+ `## ${t("sysinfo.title")}`,
729
874
  "",
730
- "| Metric | Value |",
875
+ `| ${t("sysinfo.metric")} | ${t("sysinfo.value")} |`,
731
876
  "|--------|-------|",
732
877
  `| AgEnD | v${agendVersion} |`,
733
878
  `| OS | ${process.platform} ${osRelease()} (${process.arch}) |`,
734
879
  `| Node | ${process.version} |`,
735
880
  `| 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 |`,
881
+ `| ${t("sysinfo.uptime")} | ${upHours}h ${upMins}m |`,
882
+ `| ${t("sysinfo.memory")} | ${info.memory_mb.rss} MB RSS |`,
883
+ `| ${t("sysinfo.heap")} | ${info.memory_mb.heapUsed} / ${info.memory_mb.heapTotal} MB |`,
739
884
  ].join("\n");
740
885
  }
741
886
  async handleUpdateCommand(msg) {
@@ -754,11 +899,13 @@ export class TopicCommands {
754
899
  await adapter.sendText(chatId, t("not_authorized"), { threadId });
755
900
  return;
756
901
  }
757
- await adapter.sendText(chatId, t("update.running"), { threadId });
902
+ const sent = await adapter.sendText(chatId, t("update.progress.preparing", 0), { threadId });
903
+ this.ctx.beginUpdateProgress?.(adapter, chatId, threadId, sent.messageId);
758
904
  const currentVersion = createRequire(import.meta.url)("../package.json").version ?? "";
759
905
  const updateCmd = currentVersion.includes("beta") ? "agend update --beta" : "agend update";
760
906
  const { spawn } = await import("node:child_process");
761
907
  const child = spawn("sh", ["-c", `sleep 2 && ${updateCmd}`], { detached: true, stdio: "ignore" });
908
+ child.once("error", err => this.ctx.failUpdateProgress?.(err.message));
762
909
  child.unref();
763
910
  }
764
911
  async handleDoctorCommand(msg) {
@@ -786,11 +933,11 @@ export class TopicCommands {
786
933
  timeout: 30_000,
787
934
  encoding: "utf-8",
788
935
  });
789
- output = stripAnsi(stdout) || "No output";
936
+ output = stripAnsi(stdout) || t("doctor.no_output");
790
937
  }
791
938
  catch (err) {
792
939
  const e = err;
793
- output = stripAnsi(e.stdout ?? e.message ?? "Doctor failed");
940
+ output = stripAnsi(e.stdout ?? e.message ?? t("doctor.failed"));
794
941
  }
795
942
  await adapter.sendText(chatId, output, { threadId });
796
943
  }
@@ -799,7 +946,7 @@ export class TopicCommands {
799
946
  const adapter = this.getReplyAdapter(msg);
800
947
  if (!adapter)
801
948
  return;
802
- 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 });
949
+ await adapter.sendText(msg.chatId, t("topic.unbound"), { threadId: msg.threadId });
803
950
  }
804
951
  /** Handle topic deletion — stop daemon and remove from config */
805
952
  async handleTopicDeleted(threadId) {
@@ -912,21 +1059,24 @@ export class TopicCommands {
912
1059
  }
913
1060
  try {
914
1061
  const fleetCommands = [
915
- { command: "status", description: "🔒 Fleet status, per-instance costs and health" },
916
- { command: "sysinfo", description: "System diagnostics" },
917
- { command: "dashboard", description: "🔒 Get dashboard URLs" },
918
- { command: "ctx", description: "Show context usage" },
919
- { command: "compact", description: "Compact agent context" },
920
- { command: "clear", description: "🔒 Clear agent conversation context" },
921
- { command: "model", description: "🔒 Switch model (admin only)" },
922
- { command: "effort", description: "🔒 Set reasoning effort (admin only)" },
923
- { command: "pause", description: "🔒 Pause an idle instance" },
924
- { command: "wake", description: "🔒 Wake a paused instance" },
925
- { command: "restart", description: "🔒 Graceful restart all instances" },
926
- { command: "collab", description: "🔒 Toggle bot/webhook mode" },
927
- { command: "update", description: "🔒 Update AgEnD to latest" },
928
- { command: "doctor", description: "🔒 Run health diagnostics" },
929
- { command: "usage", description: "AI subscription usage" },
1062
+ { command: "status", description: "🔒 " + t("slash.status") },
1063
+ { command: "sysinfo", description: t("slash.sysinfo") },
1064
+ { command: "dashboard", description: "🔒 " + t("slash.dashboard") },
1065
+ { command: "ctx", description: t("slash.ctx") },
1066
+ { command: "compact", description: t("slash.compact") },
1067
+ { command: "steer", description: t("slash.steer") },
1068
+ { command: "btw", description: t("slash.btw") },
1069
+ { command: "clear", description: "🔒 " + t("slash.clear") },
1070
+ { command: "model", description: "🔒 " + t("slash.model") },
1071
+ { command: "effort", description: "🔒 " + t("slash.effort") },
1072
+ { command: "pause", description: "🔒 " + t("slash.pause") },
1073
+ { command: "wake", description: "🔒 " + t("slash.wake") },
1074
+ { command: "restart", description: "🔒 " + t("slash.restart") },
1075
+ { command: "collab", description: "🔒 " + t("slash.collab") },
1076
+ { command: "update", description: "🔒 " + t("slash.update") },
1077
+ { command: "doctor", description: "🔒 " + t("slash.doctor") },
1078
+ { command: "usage", description: t("slash.usage") },
1079
+ { command: "tips", description: t("slash.tips") },
930
1080
  ];
931
1081
  const setCommands = async (commands, scope) => {
932
1082
  const response = await fetch(`https://api.telegram.org/bot${botToken}/setMyCommands`, {
@@ -947,15 +1097,17 @@ export class TopicCommands {
947
1097
  }
948
1098
  };
949
1099
  const classicCommands = [
950
- { command: "start", description: "🔒 Start an agent in this chat" },
951
- { command: "stop", description: "🔒 Stop the agent" },
952
- { command: "compact", description: "🔒 Compact agent context" },
953
- { command: "clear", description: "🔒 Clear agent conversation context" },
954
- { command: "model", description: "🔒 Switch model (admin only)" },
955
- { command: "effort", description: "🔒 Set reasoning effort (admin only)" },
956
- { command: "pause", description: "🔒 Pause the agent" },
957
- { command: "wake", description: "🔒 Wake the agent" },
958
- { command: "ctx", description: "Show context usage" },
1100
+ { command: "start", description: "🔒 " + t("slash.start") },
1101
+ { command: "stop", description: "🔒 " + t("slash.stop") },
1102
+ { command: "compact", description: "🔒 " + t("slash.compact") },
1103
+ { command: "steer", description: t("slash.steer") },
1104
+ { command: "btw", description: t("slash.btw") },
1105
+ { command: "clear", description: "🔒 " + t("slash.clear") },
1106
+ { command: "model", description: "🔒 " + t("slash.model") },
1107
+ { command: "effort", description: "🔒 " + t("slash.effort") },
1108
+ { command: "pause", description: "🔒 " + t("slash.pause") },
1109
+ { command: "wake", description: "🔒 " + t("slash.wake") },
1110
+ { command: "ctx", description: t("slash.ctx") },
959
1111
  ];
960
1112
  // A chat_administrators scope has higher precedence than the chat scope.
961
1113
  // Keep both synchronized so a stale admin-only list from BotFather or an