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

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 +76 -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 +16 -0
  52. package/dist/fleet-manager.d.ts +233 -10
  53. package/dist/fleet-manager.js +1828 -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 +227 -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);
523
569
  }
524
- return "❌ Instance not connected (IPC unavailable)";
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);
594
+ }
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,50 @@ 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 === "advanced on") {
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
+ await adapter.sendText(msg.chatId, this.ctx.unlockAdvancedTips?.(msg.userId)
722
+ ? t("tips.advanced.unlocked")
723
+ : t("tips.unavailable"), { threadId: msg.threadId });
724
+ return;
725
+ }
726
+ if (mode === "on" || mode === "off") {
727
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
728
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
729
+ return;
730
+ }
731
+ this.ctx.fleetConfig.defaults.tips = mode === "on";
732
+ this.ctx.saveFleetConfig();
733
+ await adapter.sendText(msg.chatId, t(mode === "on" ? "tips.enabled" : "tips.disabled"), {
734
+ threadId: msg.threadId,
735
+ });
736
+ return;
737
+ }
738
+ if (mode) {
739
+ await adapter.sendText(msg.chatId, t("tips.usage"), { threadId: msg.threadId });
740
+ return;
741
+ }
742
+ const result = await this.ctx.promptTip?.(
743
+ // /tips is a General command, so the route target is the current General.
744
+ Object.entries(this.ctx.fleetConfig.instances)
745
+ .find(([, config]) => config.general_topic === true
746
+ && (!config.channel_id || config.channel_id === msg.adapterId))?.[0] ?? "general", adapter, msg.chatId, msg.threadId) ?? "unavailable";
747
+ if (result === "empty") {
748
+ await adapter.sendText(msg.chatId, t("tips.empty"), { threadId: msg.threadId });
749
+ }
750
+ else if (result === "unavailable") {
751
+ await adapter.sendText(msg.chatId, t("tips.unavailable"), { threadId: msg.threadId });
597
752
  }
598
753
  }
599
754
  async handleStatusCommand(msg) {
@@ -622,7 +777,7 @@ export class TopicCommands {
622
777
  /** Get fleet status as markdown text (shared by TG + DC) */
623
778
  async getStatusText() {
624
779
  if (!this.ctx.fleetConfig)
625
- return "No fleet config loaded.";
780
+ return t("status.no_config");
626
781
  const rows = [];
627
782
  let pausedCount = 0;
628
783
  const fleetNames = new Set(Object.keys(this.ctx.fleetConfig.instances));
@@ -659,10 +814,10 @@ export class TopicCommands {
659
814
  icon = "🔴";
660
815
  else
661
816
  icon = "⚪";
662
- const stateLabel = executionState === "idle" ? "🟢 idle"
663
- : executionState === "working" ? "🔵 working"
664
- : executionState === "stuck" ? "🔴 stuck"
665
- : executionState === "paused" ? "⏸ paused"
817
+ const stateLabel = executionState === "idle" ? `🟢 ${t("state.idle")}`
818
+ : executionState === "working" ? `🔵 ${t("state.working")}`
819
+ : executionState === "stuck" ? `🔴 ${t("state.stuck")}`
820
+ : executionState === "paused" ? `⏸ ${t("state.paused")}`
666
821
  : "—";
667
822
  const displayName = this.shortInstanceName(name);
668
823
  // IPC reachability moved here from /sysinfo: it is per-instance health, and
@@ -674,27 +829,27 @@ export class TopicCommands {
674
829
  rows.push(`| ${displayName} | ${backend} | ${contextStr} | ${effort} | ${formatCents(costCents)} | ${icon} | ${stateLabel} | ${ipc} |`);
675
830
  }
676
831
  if (rows.length === 0)
677
- return "No instances configured.";
832
+ return t("status.no_instances");
678
833
  const lines = [
679
- "## Fleet Status",
834
+ `## ${t("status.title")}`,
680
835
  "",
681
- "| Instance | Backend | Ctx | Effort | Cost | Status | State | IPC |",
836
+ t("status.table_header"),
682
837
  "|----------|---------|-----|--------|------|--------|-------|-----|",
683
838
  ...rows,
684
839
  "",
685
- `Paused instances: ${pausedCount}`,
840
+ t("status.paused_count", pausedCount),
686
841
  ];
687
842
  const limitCents = this.ctx.costGuard?.getLimitCents() ?? 0;
688
843
  const totalCents = this.ctx.costGuard?.getFleetTotalCents() ?? 0;
689
844
  if (limitCents > 0) {
690
- lines.push("", `Fleet: ${formatCents(totalCents)} / ${formatCents(limitCents)} daily`);
845
+ lines.push("", t("status.daily_cost", formatCents(totalCents), formatCents(limitCents)));
691
846
  }
692
847
  // Adapter states (only show if any are not connected)
693
848
  const adapterStates = this.ctx.getAdapterStates?.();
694
849
  if (adapterStates && adapterStates.size > 0) {
695
850
  const issues = [...adapterStates.entries()].filter(([, s]) => s.status !== "connected");
696
851
  if (issues.length > 0) {
697
- lines.push("", "**Adapters:**");
852
+ lines.push("", `**${t("status.adapters")}**`);
698
853
  for (const [id, s] of adapterStates) {
699
854
  const icon = s.status === "connected" ? "✅" : s.status === "retrying" ? "🔄" : "❌";
700
855
  lines.push(` ${icon} ${id}: ${s.status}${s.lastError ? ` (${s.lastError.slice(0, 60)})` : ""}`);
@@ -725,17 +880,17 @@ export class TopicCommands {
725
880
  const require = createRequire(import.meta.url);
726
881
  const agendVersion = require("../package.json").version ?? "unknown";
727
882
  return [
728
- "## System Info",
883
+ `## ${t("sysinfo.title")}`,
729
884
  "",
730
- "| Metric | Value |",
885
+ `| ${t("sysinfo.metric")} | ${t("sysinfo.value")} |`,
731
886
  "|--------|-------|",
732
887
  `| AgEnD | v${agendVersion} |`,
733
888
  `| OS | ${process.platform} ${osRelease()} (${process.arch}) |`,
734
889
  `| Node | ${process.version} |`,
735
890
  `| 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 |`,
891
+ `| ${t("sysinfo.uptime")} | ${upHours}h ${upMins}m |`,
892
+ `| ${t("sysinfo.memory")} | ${info.memory_mb.rss} MB RSS |`,
893
+ `| ${t("sysinfo.heap")} | ${info.memory_mb.heapUsed} / ${info.memory_mb.heapTotal} MB |`,
739
894
  ].join("\n");
740
895
  }
741
896
  async handleUpdateCommand(msg) {
@@ -754,11 +909,13 @@ export class TopicCommands {
754
909
  await adapter.sendText(chatId, t("not_authorized"), { threadId });
755
910
  return;
756
911
  }
757
- await adapter.sendText(chatId, t("update.running"), { threadId });
912
+ const sent = await adapter.sendText(chatId, t("update.progress.preparing", 0), { threadId });
913
+ this.ctx.beginUpdateProgress?.(adapter, chatId, threadId, sent.messageId);
758
914
  const currentVersion = createRequire(import.meta.url)("../package.json").version ?? "";
759
915
  const updateCmd = currentVersion.includes("beta") ? "agend update --beta" : "agend update";
760
916
  const { spawn } = await import("node:child_process");
761
917
  const child = spawn("sh", ["-c", `sleep 2 && ${updateCmd}`], { detached: true, stdio: "ignore" });
918
+ child.once("error", err => this.ctx.failUpdateProgress?.(err.message));
762
919
  child.unref();
763
920
  }
764
921
  async handleDoctorCommand(msg) {
@@ -786,11 +943,11 @@ export class TopicCommands {
786
943
  timeout: 30_000,
787
944
  encoding: "utf-8",
788
945
  });
789
- output = stripAnsi(stdout) || "No output";
946
+ output = stripAnsi(stdout) || t("doctor.no_output");
790
947
  }
791
948
  catch (err) {
792
949
  const e = err;
793
- output = stripAnsi(e.stdout ?? e.message ?? "Doctor failed");
950
+ output = stripAnsi(e.stdout ?? e.message ?? t("doctor.failed"));
794
951
  }
795
952
  await adapter.sendText(chatId, output, { threadId });
796
953
  }
@@ -799,7 +956,7 @@ export class TopicCommands {
799
956
  const adapter = this.getReplyAdapter(msg);
800
957
  if (!adapter)
801
958
  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 });
959
+ await adapter.sendText(msg.chatId, t("topic.unbound"), { threadId: msg.threadId });
803
960
  }
804
961
  /** Handle topic deletion — stop daemon and remove from config */
805
962
  async handleTopicDeleted(threadId) {
@@ -912,21 +1069,24 @@ export class TopicCommands {
912
1069
  }
913
1070
  try {
914
1071
  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" },
1072
+ { command: "status", description: "🔒 " + t("slash.status") },
1073
+ { command: "sysinfo", description: t("slash.sysinfo") },
1074
+ { command: "dashboard", description: "🔒 " + t("slash.dashboard") },
1075
+ { command: "ctx", description: t("slash.ctx") },
1076
+ { command: "compact", description: t("slash.compact") },
1077
+ { command: "steer", description: t("slash.steer") },
1078
+ { command: "btw", description: t("slash.btw") },
1079
+ { command: "clear", description: "🔒 " + t("slash.clear") },
1080
+ { command: "model", description: "🔒 " + t("slash.model") },
1081
+ { command: "effort", description: "🔒 " + t("slash.effort") },
1082
+ { command: "pause", description: "🔒 " + t("slash.pause") },
1083
+ { command: "wake", description: "🔒 " + t("slash.wake") },
1084
+ { command: "restart", description: "🔒 " + t("slash.restart") },
1085
+ { command: "collab", description: "🔒 " + t("slash.collab") },
1086
+ { command: "update", description: "🔒 " + t("slash.update") },
1087
+ { command: "doctor", description: "🔒 " + t("slash.doctor") },
1088
+ { command: "usage", description: t("slash.usage") },
1089
+ { command: "tips", description: t("slash.tips") },
930
1090
  ];
931
1091
  const setCommands = async (commands, scope) => {
932
1092
  const response = await fetch(`https://api.telegram.org/bot${botToken}/setMyCommands`, {
@@ -947,15 +1107,17 @@ export class TopicCommands {
947
1107
  }
948
1108
  };
949
1109
  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" },
1110
+ { command: "start", description: "🔒 " + t("slash.start") },
1111
+ { command: "stop", description: "🔒 " + t("slash.stop") },
1112
+ { command: "compact", description: "🔒 " + t("slash.compact") },
1113
+ { command: "steer", description: t("slash.steer") },
1114
+ { command: "btw", description: t("slash.btw") },
1115
+ { command: "clear", description: "🔒 " + t("slash.clear") },
1116
+ { command: "model", description: "🔒 " + t("slash.model") },
1117
+ { command: "effort", description: "🔒 " + t("slash.effort") },
1118
+ { command: "pause", description: "🔒 " + t("slash.pause") },
1119
+ { command: "wake", description: "🔒 " + t("slash.wake") },
1120
+ { command: "ctx", description: t("slash.ctx") },
959
1121
  ];
960
1122
  // A chat_administrators scope has higher precedence than the chat scope.
961
1123
  // Keep both synchronized so a stale admin-only list from BotFather or an