@songsid/agend 2.1.4-beta.8 → 2.1.4

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 (173) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +2 -2
  4. package/dist/agent-endpoint.js +1 -1
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/backend/antigravity.d.ts +12 -8
  7. package/dist/backend/antigravity.js +189 -42
  8. package/dist/backend/antigravity.js.map +1 -1
  9. package/dist/backend/claude-code.d.ts +87 -23
  10. package/dist/backend/claude-code.js +361 -46
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.d.ts +18 -0
  13. package/dist/backend/codex.js +139 -5
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/gemini-cli.js +2 -2
  16. package/dist/backend/gemini-cli.js.map +1 -1
  17. package/dist/backend/grok.js +2 -2
  18. package/dist/backend/grok.js.map +1 -1
  19. package/dist/backend/kiro.d.ts +52 -2
  20. package/dist/backend/kiro.js +267 -12
  21. package/dist/backend/kiro.js.map +1 -1
  22. package/dist/backend/opencode.js +17 -4
  23. package/dist/backend/opencode.js.map +1 -1
  24. package/dist/backend/types.d.ts +122 -3
  25. package/dist/backend/types.js +5 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/backend-outage.d.ts +61 -0
  28. package/dist/backend-outage.js +71 -0
  29. package/dist/backend-outage.js.map +1 -0
  30. package/dist/channel/adapters/discord.d.ts +44 -2
  31. package/dist/channel/adapters/discord.js +529 -89
  32. package/dist/channel/adapters/discord.js.map +1 -1
  33. package/dist/channel/adapters/telegram.d.ts +30 -0
  34. package/dist/channel/adapters/telegram.js +221 -19
  35. package/dist/channel/adapters/telegram.js.map +1 -1
  36. package/dist/channel/agy-mcp-launcher.d.ts +2 -0
  37. package/dist/channel/agy-mcp-launcher.js +28 -0
  38. package/dist/channel/agy-mcp-launcher.js.map +1 -0
  39. package/dist/channel/ipc-bridge.d.ts +2 -1
  40. package/dist/channel/ipc-bridge.js +33 -4
  41. package/dist/channel/ipc-bridge.js.map +1 -1
  42. package/dist/channel/markdown-chunk.d.ts +42 -0
  43. package/dist/channel/markdown-chunk.js +205 -0
  44. package/dist/channel/markdown-chunk.js.map +1 -0
  45. package/dist/channel/mcp-server.js +34 -5
  46. package/dist/channel/mcp-server.js.map +1 -1
  47. package/dist/channel/mcp-tools.js +5 -3
  48. package/dist/channel/mcp-tools.js.map +1 -1
  49. package/dist/channel/message-queue.js +64 -8
  50. package/dist/channel/message-queue.js.map +1 -1
  51. package/dist/channel/types.d.ts +31 -1
  52. package/dist/classic-channel-manager.d.ts +65 -0
  53. package/dist/classic-channel-manager.js +172 -3
  54. package/dist/classic-channel-manager.js.map +1 -1
  55. package/dist/cli.js +124 -83
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config-validator.js +16 -0
  58. package/dist/config-validator.js.map +1 -1
  59. package/dist/config.d.ts +4 -0
  60. package/dist/config.js +12 -1
  61. package/dist/config.js.map +1 -1
  62. package/dist/cross-instance-envelope.d.ts +6 -0
  63. package/dist/cross-instance-envelope.js +30 -0
  64. package/dist/cross-instance-envelope.js.map +1 -0
  65. package/dist/daemon.d.ts +538 -11
  66. package/dist/daemon.js +2272 -288
  67. package/dist/daemon.js.map +1 -1
  68. package/dist/doctor.d.ts +41 -0
  69. package/dist/doctor.js +267 -0
  70. package/dist/doctor.js.map +1 -0
  71. package/dist/fleet-context.d.ts +63 -0
  72. package/dist/fleet-context.js.map +1 -1
  73. package/dist/fleet-manager.d.ts +424 -11
  74. package/dist/fleet-manager.js +2753 -444
  75. package/dist/fleet-manager.js.map +1 -1
  76. package/dist/fleet-yaml-slim.d.ts +10 -0
  77. package/dist/fleet-yaml-slim.js +59 -0
  78. package/dist/fleet-yaml-slim.js.map +1 -0
  79. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  80. package/dist/general-knowledge/skills/model-discovery/SKILL.md +63 -16
  81. package/dist/general-knowledge/skills/session-management/SKILL.md +171 -8
  82. package/dist/instance-lifecycle.d.ts +114 -1
  83. package/dist/instance-lifecycle.js +404 -14
  84. package/dist/instance-lifecycle.js.map +1 -1
  85. package/dist/instructions.js +42 -4
  86. package/dist/instructions.js.map +1 -1
  87. package/dist/locale.js +1024 -310
  88. package/dist/locale.js.map +1 -1
  89. package/dist/login-flows.d.ts +91 -0
  90. package/dist/login-flows.js +170 -0
  91. package/dist/login-flows.js.map +1 -0
  92. package/dist/login-manager.d.ts +63 -0
  93. package/dist/login-manager.js +134 -0
  94. package/dist/login-manager.js.map +1 -0
  95. package/dist/network-family.d.ts +18 -0
  96. package/dist/network-family.js +20 -0
  97. package/dist/network-family.js.map +1 -0
  98. package/dist/outbound-handlers.d.ts +16 -1
  99. package/dist/outbound-handlers.js +174 -40
  100. package/dist/outbound-handlers.js.map +1 -1
  101. package/dist/outbound-schemas.d.ts +10 -3
  102. package/dist/outbound-schemas.js +10 -0
  103. package/dist/outbound-schemas.js.map +1 -1
  104. package/dist/pane-input-residue.d.ts +52 -0
  105. package/dist/pane-input-residue.js +107 -0
  106. package/dist/pane-input-residue.js.map +1 -0
  107. package/dist/process-memory.d.ts +6 -0
  108. package/dist/process-memory.js +45 -0
  109. package/dist/process-memory.js.map +1 -1
  110. package/dist/reply-dedup.d.ts +7 -8
  111. package/dist/reply-dedup.js +0 -0
  112. package/dist/reply-dedup.js.map +1 -1
  113. package/dist/restart-progress.d.ts +2 -0
  114. package/dist/restart-progress.js +3 -0
  115. package/dist/restart-progress.js.map +1 -1
  116. package/dist/scheduler/db.d.ts +12 -0
  117. package/dist/scheduler/db.js +59 -0
  118. package/dist/scheduler/db.js.map +1 -1
  119. package/dist/service-installer.d.ts +11 -0
  120. package/dist/service-installer.js +84 -18
  121. package/dist/service-installer.js.map +1 -1
  122. package/dist/settings-api.js +1 -1
  123. package/dist/settings-api.js.map +1 -1
  124. package/dist/setup-wizard.js +2 -2
  125. package/dist/setup-wizard.js.map +1 -1
  126. package/dist/spawn-gate.d.ts +30 -0
  127. package/dist/spawn-gate.js +79 -0
  128. package/dist/spawn-gate.js.map +1 -0
  129. package/dist/steer-capability.d.ts +1 -0
  130. package/dist/steer-capability.js +6 -0
  131. package/dist/steer-capability.js.map +1 -0
  132. package/dist/storm-window.d.ts +83 -0
  133. package/dist/storm-window.js +251 -0
  134. package/dist/storm-window.js.map +1 -0
  135. package/dist/tips.d.ts +44 -0
  136. package/dist/tips.js +355 -0
  137. package/dist/tips.js.map +1 -0
  138. package/dist/tmux-manager.d.ts +16 -0
  139. package/dist/tmux-manager.js +110 -25
  140. package/dist/tmux-manager.js.map +1 -1
  141. package/dist/tool-progress.d.ts +40 -0
  142. package/dist/tool-progress.js +289 -0
  143. package/dist/tool-progress.js.map +1 -0
  144. package/dist/topic-commands.d.ts +45 -6
  145. package/dist/topic-commands.js +394 -66
  146. package/dist/topic-commands.js.map +1 -1
  147. package/dist/transcript-monitor.d.ts +15 -2
  148. package/dist/transcript-monitor.js +63 -17
  149. package/dist/transcript-monitor.js.map +1 -1
  150. package/dist/transcript-sources.d.ts +131 -0
  151. package/dist/transcript-sources.js +580 -0
  152. package/dist/transcript-sources.js.map +1 -0
  153. package/dist/types.d.ts +11 -0
  154. package/dist/ui/dashboard.html +55 -32
  155. package/dist/ui/settings.html +200 -60
  156. package/dist/ui/view.html +147 -30
  157. package/dist/usage/format-rich.d.ts +1 -1
  158. package/dist/usage/format-rich.js +28 -24
  159. package/dist/usage/format-rich.js.map +1 -1
  160. package/dist/usage/i18n-keys.d.ts +7 -0
  161. package/dist/usage/i18n-keys.js +34 -0
  162. package/dist/usage/i18n-keys.js.map +1 -0
  163. package/dist/usage/i18n.d.ts +5 -0
  164. package/dist/usage/i18n.js +27 -0
  165. package/dist/usage/i18n.js.map +1 -0
  166. package/dist/usage/providers.d.ts +21 -0
  167. package/dist/usage/providers.js +153 -75
  168. package/dist/usage/providers.js.map +1 -1
  169. package/dist/usage/usage-api.d.ts +11 -3
  170. package/dist/usage/usage-api.js +61 -24
  171. package/dist/usage/usage-api.js.map +1 -1
  172. package/dist/workflow-templates/default.md +2 -1
  173. package/package.json +2 -2
@@ -8,9 +8,10 @@ const execAsync = promisify(exec);
8
8
  import { DEFAULT_INSTANCE_CONFIG } from "./config.js";
9
9
  import { formatCents } from "./cost-guard.js";
10
10
  import { getTmuxSocketName, getTmuxSessionName } from "./paths.js";
11
- import { t } from "./locale.js";
11
+ import { t, getLocale } 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
+ import { backendSupportsSteer } from "./steer-capability.js";
14
15
  export { parseContextPercent, parseTokenContextRatio } from "./context-percent.js";
15
16
  /** Sanitize a directory name into a valid instance name. Keeps Unicode letters (incl. CJK). */
16
17
  export function sanitizeInstanceName(name) {
@@ -19,6 +20,8 @@ export function sanitizeInstanceName(name) {
19
20
  }
20
21
  /** Allowed filename for /save and /load (no path separators, no shell/inject chars). */
21
22
  export const SAVE_FILENAME_RE = /^[\w.-]+$/;
23
+ /** Backends with a native side-question command that does not steer the active turn. */
24
+ export const BTW_SUPPORTED_BACKENDS = new Set(["claude-code"]);
22
25
  /**
23
26
  * Build the backend-specific session-save command, or null if the backend has no
24
27
  * /save equivalent. kiro-cli → `/chat save <name>`; claude-code → `/export <name>.md`.
@@ -36,7 +39,8 @@ export function parseSaveFilename(text) {
36
39
  return (m?.[1] ?? "").trim();
37
40
  }
38
41
  /** 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)";
42
+ /** @deprecated Prefer t("save.unsupported") at the user-facing call site. */
43
+ export const SAVE_UNSUPPORTED_MSG = "⚠️ /save is not supported for this backend (only Kiro CLI and Claude Code).";
40
44
  export function parsePauseWakeCommand(text) {
41
45
  const match = text.match(/^\/(pause|wake)(?:@\S+)?(?:\s+(\S+))?$/);
42
46
  if (!match)
@@ -103,7 +107,8 @@ export function clearCommandForBackend(backend) {
103
107
  return null;
104
108
  }
105
109
  }
106
- export const CLEAR_UNSUPPORTED_MSG = "⚠️ Clear not supported for this backend";
110
+ /** @deprecated Prefer t("clear.unsupported") at the user-facing call site. */
111
+ export const CLEAR_UNSUPPORTED_MSG = "⚠️ Clear is not supported for this backend.";
107
112
  /**
108
113
  * Extract context-usage % from a captured CLI pane. Scans bottom-up so the
109
114
  * MOST RECENT prompt wins (a captured scrollback may hold several). Covers the
@@ -137,6 +142,29 @@ export function readStatuslineContextPct(dataDir, instanceName) {
137
142
  return null;
138
143
  }
139
144
  }
145
+ /**
146
+ * Claude Code's statusline payload reports the model that actually served the
147
+ * current session. This can differ from fleet.yaml after the CLI rejects a
148
+ * plan-gated model and keeps using its previous/default model.
149
+ */
150
+ export function readStatuslineModel(dataDir, instanceName) {
151
+ try {
152
+ const statusFile = join(dataDir, "instances", instanceName, "statusline.json");
153
+ if (!existsSync(statusFile))
154
+ return null;
155
+ const data = JSON.parse(readFileSync(statusFile, "utf-8"));
156
+ const id = typeof data.model?.id === "string" ? data.model.id.trim() : "";
157
+ const displayName = typeof data.model?.display_name === "string"
158
+ ? data.model.display_name.trim()
159
+ : "";
160
+ if (displayName && id && displayName !== id)
161
+ return `${displayName} (${id})`;
162
+ return id || displayName || null;
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ }
140
168
  /** Live-pane scrape used by /ctx, /status Ctx, and /view sidebar. */
141
169
  export function scrapePaneContext(instanceName, backend) {
142
170
  try {
@@ -270,6 +298,20 @@ export class TopicCommands {
270
298
  await this.handleUsageCommand(msg);
271
299
  return true;
272
300
  }
301
+ if (text === "/tips" || text.startsWith("/tips ") || text.startsWith("/tips@")) {
302
+ await this.handleTipsCommand(msg);
303
+ return true;
304
+ }
305
+ if (text === "/login" || text.startsWith("/login ") || text.startsWith("/login@")) {
306
+ await this.handleLoginCommand(msg);
307
+ return true;
308
+ }
309
+ // Telegram command names cannot contain "-", so the menu registers
310
+ // /install_cli; accept both spellings as typed text.
311
+ if (/^\/install[-_]cli(?:@\S+)?(?:\s|$)/.test(text)) {
312
+ await this.handleInstallCliCommand(msg);
313
+ return true;
314
+ }
273
315
  if (text === "/update" || text.startsWith("/update@")) {
274
316
  await this.handleUpdateCommand(msg);
275
317
  return true;
@@ -332,6 +374,13 @@ export class TopicCommands {
332
374
  const text = msg.text?.trim();
333
375
  if (!text)
334
376
  return false;
377
+ // Tips are informational and should appear where requested, including a
378
+ // worker topic. This also keeps Telegram text commands aligned with
379
+ // Discord's channel-local slash-command behavior.
380
+ if (text === "/tips" || text.startsWith("/tips ") || text.startsWith("/tips@")) {
381
+ await this.handleTipsCommand(msg);
382
+ return true;
383
+ }
335
384
  const pauseWake = parsePauseWakeCommand(text);
336
385
  if (pauseWake) {
337
386
  const adapter = this.getReplyAdapter(msg);
@@ -352,7 +401,7 @@ export class TopicCommands {
352
401
  return true;
353
402
  }
354
403
  if (pauseWake.action === "pause" && isGeneralInstance(this.ctx.fleetConfig, target)) {
355
- await adapter.sendText(msg.chatId, GENERAL_PAUSE_ERROR, { threadId: msg.threadId });
404
+ await adapter.sendText(msg.chatId, t("general.pause_forbidden"), { threadId: msg.threadId });
356
405
  return true;
357
406
  }
358
407
  await adapter.sendText(msg.chatId, await this.runPauseWake(target, pauseWake.action), { threadId: msg.threadId });
@@ -379,7 +428,7 @@ export class TopicCommands {
379
428
  const level = text.replace(/^\/effort(@\S+)?/, "").trim();
380
429
  if (level) {
381
430
  const reply = await this.ctx.applyEffort?.(instanceName, level)
382
- ?? "Effort switching is unavailable.";
431
+ ?? t("effort.unsupported", this.effectiveBackend(instanceName));
383
432
  await adapter.sendText(msg.chatId, reply, { threadId: msg.threadId });
384
433
  }
385
434
  else if (this.ctx.promptEffortMenu) {
@@ -410,7 +459,7 @@ export class TopicCommands {
410
459
  await adapter.sendText(msg.chatId, fallback, { threadId: msg.threadId });
411
460
  }
412
461
  else {
413
- await adapter.sendText(msg.chatId, "Usage: /model <name> — e.g. /model sonnet", { threadId: msg.threadId });
462
+ await adapter.sendText(msg.chatId, t("model.usage"), { threadId: msg.threadId });
414
463
  }
415
464
  return true;
416
465
  }
@@ -422,6 +471,39 @@ export class TopicCommands {
422
471
  await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
423
472
  return true;
424
473
  }
474
+ if (text === "/steer" || text.startsWith("/steer ") || text.startsWith("/steer@")) {
475
+ const adapter = this.getReplyAdapter(msg);
476
+ if (!adapter)
477
+ return false;
478
+ // Deliberately NOT admin-gated: anyone who can speak in this topic can
479
+ // already send the instance a message; /steer only changes WHEN it lands
480
+ // (mid-turn instead of after), and it goes through the full [user:]
481
+ // formatting — unlike /raw, which bypasses it and is gated.
482
+ const content = text.replace(/^\/steer(@\S+)?/, "").trim();
483
+ if (!content) {
484
+ await adapter.sendText(msg.chatId, t("steer.usage"), { threadId: msg.threadId });
485
+ return true;
486
+ }
487
+ const result = this.sendSteer(instanceName, content, msg);
488
+ await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
489
+ return true;
490
+ }
491
+ if (text === "/btw" || text.startsWith("/btw ") || text.startsWith("/btw@")) {
492
+ const adapter = this.getReplyAdapter(msg);
493
+ if (!adapter)
494
+ return false;
495
+ // Like /steer, /btw is not admin-gated: anyone who can send the agent a
496
+ // normal message may ask a side question. Claude owns the isolation from
497
+ // the active turn; AgEnD only submits its native command immediately.
498
+ const content = text.replace(/^\/btw(@\S+)?/, "").trim();
499
+ if (!content) {
500
+ await adapter.sendText(msg.chatId, t("btw.usage"), { threadId: msg.threadId });
501
+ return true;
502
+ }
503
+ const result = this.sendBtw(instanceName, content, msg);
504
+ await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
505
+ return true;
506
+ }
425
507
  if (text === "/clear" || text.startsWith("/clear@")) {
426
508
  const adapter = this.getReplyAdapter(msg);
427
509
  if (!adapter)
@@ -475,7 +557,7 @@ export class TopicCommands {
475
557
  }
476
558
  async runPauseWake(instanceName, action) {
477
559
  if (action === "pause" && isGeneralInstance(this.ctx.fleetConfig, instanceName)) {
478
- return GENERAL_PAUSE_ERROR;
560
+ return t("general.pause_forbidden");
479
561
  }
480
562
  try {
481
563
  const result = await this.ctx.changeInstancePauseState(instanceName, action);
@@ -506,7 +588,10 @@ export class TopicCommands {
506
588
  const contextLine = context == null ? null : formatContextUsageLine(context, tokenRatio);
507
589
  // Effective model (resolves per-instance → fleet default → classic channel →
508
590
  // the CLI's own default) via the shared resolver, so /ctx and /model agree.
509
- const modelDisplay = this.ctx.modelDisplayForInstance?.(instanceName);
591
+ const modelDisplay = backend === "claude-code"
592
+ ? readStatuslineModel(this.ctx.dataDir, instanceName)
593
+ ?? this.ctx.modelDisplayForInstance?.(instanceName)
594
+ : this.ctx.modelDisplayForInstance?.(instanceName);
510
595
  const modelLine = modelDisplay ? `\n${t("ctx.model", modelDisplay)}` : "";
511
596
  return context != null
512
597
  ? `${contextLine}\n${t("ctx.backend", backend)}${modelLine}\n${t("ctx.instance", instanceName)}`
@@ -524,23 +609,88 @@ export class TopicCommands {
524
609
  ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
525
610
  const cmd = compactCommandForBackend(backend);
526
611
  ipc.send({ type: "raw_paste", content: cmd });
527
- return `🗜️ Compact command sent (\`${cmd}\`).`;
612
+ return t("compact.sent", cmd);
528
613
  }
529
- return "❌ Instance not connected (IPC unavailable)";
614
+ return t("compact.not_connected");
530
615
  }
531
616
  /** Whether the instance backend exposes a verified full-reset command. */
532
617
  supportsClear(instanceName) {
533
618
  return clearCommandForBackend(this.effectiveBackend(instanceName)) !== null;
534
619
  }
620
+ /**
621
+ * Steer: interject a message into the instance's CURRENT turn. The daemon
622
+ * pastes it into the busy CLI instead of queueing for idle (falling back to
623
+ * the queue if the TUI swallows busy input — see Daemon.steerMessage).
624
+ */
625
+ /**
626
+ * Backends whose TUI accepts a busy-pane paste as steering input,
627
+ * live-verified: claude-code and codex buffer-then-submit at the turn
628
+ * boundary, grok accepts it in its input line. kiro's legacy TUI swallows
629
+ * the paste outright, and opencode/antigravity are unverified — for those
630
+ * the user gets an honest "not supported" instead of a silent queue
631
+ * fallback that looks like a steer but behaves like a normal message.
632
+ */
633
+ sendSteer(instanceName, content, msg) {
634
+ const backend = this.effectiveBackend(instanceName);
635
+ if (!backendSupportsSteer(backend)) {
636
+ return t("steer.unsupported", backend);
637
+ }
638
+ const ipc = this.ctx.instanceIpcClients.get(instanceName);
639
+ if (!ipc?.connected)
640
+ return t("steer.not_connected");
641
+ ipc.send({
642
+ type: "steer",
643
+ content,
644
+ delivery_epoch: this.ctx.getDeliveryEpoch?.(instanceName) ?? undefined,
645
+ meta: {
646
+ chat_id: msg.chatId,
647
+ message_id: msg.messageId ?? "",
648
+ user: msg.username,
649
+ user_id: msg.userId,
650
+ thread_id: msg.threadId ?? "",
651
+ adapter_id: msg.adapterId ?? "",
652
+ source: msg.source,
653
+ },
654
+ });
655
+ return t("steer.sent", instanceName);
656
+ }
657
+ /** Ask a Claude Code side question without folding it into the active turn. */
658
+ sendBtw(instanceName, content, msg) {
659
+ const backend = this.effectiveBackend(instanceName);
660
+ if (!BTW_SUPPORTED_BACKENDS.has(backend))
661
+ return t("btw.unsupported", backend);
662
+ const ipc = this.ctx.instanceIpcClients.get(instanceName);
663
+ if (!ipc?.connected)
664
+ return t("btw.not_connected");
665
+ ipc.send({
666
+ type: "btw",
667
+ content,
668
+ delivery_epoch: this.ctx.getDeliveryEpoch?.(instanceName) ?? undefined,
669
+ meta: {
670
+ chat_id: msg.chatId,
671
+ message_id: msg.messageId ?? "",
672
+ user: msg.username,
673
+ user_id: msg.userId,
674
+ thread_id: msg.threadId ?? "",
675
+ adapter_id: msg.adapterId ?? "",
676
+ source: msg.source,
677
+ },
678
+ });
679
+ return t("btw.sent", instanceName);
680
+ }
535
681
  /** Send the backend-appropriate full conversation reset through raw_paste. */
536
682
  async sendClear(instanceName) {
537
683
  const backend = this.effectiveBackend(instanceName);
538
684
  const cmd = clearCommandForBackend(backend);
539
685
  if (!cmd)
540
- return CLEAR_UNSUPPORTED_MSG;
686
+ return t("clear.unsupported");
541
687
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
542
688
  if (ipc?.connected) {
543
- ipc.send({ type: "raw_paste", content: cmd });
689
+ ipc.send({
690
+ type: "raw_paste",
691
+ content: cmd,
692
+ ...(backend === "kiro-cli" ? { confirm_clear: true } : {}),
693
+ });
544
694
  return t("clear.sent", cmd);
545
695
  }
546
696
  return t("clear.not_connected");
@@ -555,13 +705,13 @@ export class TopicCommands {
555
705
  ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
556
706
  const cmd = saveCommandForBackend(backend, filename);
557
707
  if (!cmd)
558
- return SAVE_UNSUPPORTED_MSG;
708
+ return t("save.unsupported");
559
709
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
560
710
  if (ipc?.connected) {
561
711
  ipc.send({ type: "raw_paste", content: cmd });
562
- return `💾 Save command sent (\`${cmd}\`).`;
712
+ return t("save.sent", cmd, instanceName);
563
713
  }
564
- return "❌ Instance not connected (IPC unavailable)";
714
+ return t("save.not_connected");
565
715
  }
566
716
  async handleRestartCommand(msg) {
567
717
  const adapter = this.getReplyAdapter(msg);
@@ -578,8 +728,8 @@ export class TopicCommands {
578
728
  process.kill(process.pid, "SIGUSR2");
579
729
  }
580
730
  /**
581
- * `/usage` — AI subscription usage for every CLI backend logged in on this
582
- * machine, one compact message. Same permission level as /ctx (none): whoever
731
+ * `/usage` — AI subscription usage for CLI backends used by running or paused
732
+ * instances, one compact message. Same permission level as /ctx (none): whoever
583
733
  * can talk to the fleet may see how much headroom it has left. Vendor rate
584
734
  * limits are protected by the shared 5-minute cache, not by gating callers.
585
735
  */
@@ -590,7 +740,7 @@ export class TopicCommands {
590
740
  try {
591
741
  const { getUsageSnapshot } = await import("./usage/usage-api.js");
592
742
  const { renderUsageHtml, renderUsageMarkdown } = await import("./usage/format-rich.js");
593
- const payload = await getUsageSnapshot();
743
+ const payload = await getUsageSnapshot(false, this.ctx.getActiveUsageProviderIds?.());
594
744
  // Each platform gets its native rich format: Telegram needs parse_mode
595
745
  // HTML; Discord renders Markdown in plain content. Anything else falls back
596
746
  // to Markdown, which degrades to readable text.
@@ -602,7 +752,140 @@ export class TopicCommands {
602
752
  }
603
753
  }
604
754
  catch (err) {
605
- await adapter.sendText(msg.chatId, `⚠️ Usage fetch failed: ${err.message}`, { threadId: msg.threadId });
755
+ await adapter.sendText(msg.chatId, t("usage.failed", err.message), { threadId: msg.threadId });
756
+ }
757
+ }
758
+ /**
759
+ * `/login [backend] | code <text> | cancel` — remote CLI re-login.
760
+ * Fleet-admin only: the flow emits live authorization URLs/codes, and anyone
761
+ * completing one binds their own account to this fleet's CLI.
762
+ */
763
+ async handleLoginCommand(msg) {
764
+ const adapter = this.getReplyAdapter(msg);
765
+ if (!adapter)
766
+ return;
767
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
768
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
769
+ return;
770
+ }
771
+ if (!this.ctx.startLoginSession || !this.ctx.promptLoginBackends
772
+ || !this.ctx.loginSubmitInput || !this.ctx.cancelLoginSession) {
773
+ await adapter.sendText(msg.chatId, t("login.no_session"), { threadId: msg.threadId });
774
+ return;
775
+ }
776
+ const arg = msg.text.trim().replace(/^\/login(?:@\S+)?/, "").trim();
777
+ const chat = {
778
+ adapter,
779
+ adapterId: msg.adapterId ?? adapter.id,
780
+ chatId: msg.chatId,
781
+ threadId: msg.threadId,
782
+ };
783
+ if (!arg) {
784
+ await this.ctx.promptLoginBackends(chat);
785
+ return;
786
+ }
787
+ if (arg === "cancel") {
788
+ await adapter.sendText(msg.chatId, await this.ctx.cancelLoginSession(), { threadId: msg.threadId });
789
+ return;
790
+ }
791
+ if (arg.startsWith("code ")) {
792
+ const input = arg.slice(5).trim();
793
+ const reply = input ? await this.ctx.loginSubmitInput(input) : t("login.usage");
794
+ await adapter.sendText(msg.chatId, reply, { threadId: msg.threadId });
795
+ return;
796
+ }
797
+ if (/\s/.test(arg)) {
798
+ await adapter.sendText(msg.chatId, t("login.usage"), { threadId: msg.threadId });
799
+ return;
800
+ }
801
+ const started = await this.ctx.startLoginSession(arg, chat);
802
+ // null → the still-valid-auth confirmation prompt was posted instead.
803
+ if (started)
804
+ await adapter.sendText(msg.chatId, started, { threadId: msg.threadId });
805
+ }
806
+ /** `/install-cli <backend> | cancel` — remote CLI install. Fleet-admin only. */
807
+ async handleInstallCliCommand(msg) {
808
+ const adapter = this.getReplyAdapter(msg);
809
+ if (!adapter)
810
+ return;
811
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
812
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
813
+ return;
814
+ }
815
+ if (!this.ctx.startInstallSession || !this.ctx.cancelInstallSession) {
816
+ await adapter.sendText(msg.chatId, t("install.no_session"), { threadId: msg.threadId });
817
+ return;
818
+ }
819
+ const arg = msg.text.trim().replace(/^\/install[-_]cli(?:@\S+)?/, "").trim();
820
+ // Bare call → offer the backends, matching bare `/login`. A multi-word arg
821
+ // is still a usage error: it means they typed something, just not a backend.
822
+ if (!arg && this.ctx.promptInstallBackends) {
823
+ await this.ctx.promptInstallBackends({
824
+ adapter,
825
+ adapterId: msg.adapterId ?? adapter.id,
826
+ chatId: msg.chatId,
827
+ threadId: msg.threadId,
828
+ });
829
+ return;
830
+ }
831
+ if (!arg || /\s/.test(arg)) {
832
+ await adapter.sendText(msg.chatId, t("install.usage"), { threadId: msg.threadId });
833
+ return;
834
+ }
835
+ if (arg === "cancel") {
836
+ await adapter.sendText(msg.chatId, await this.ctx.cancelInstallSession(), { threadId: msg.threadId });
837
+ return;
838
+ }
839
+ const chat = {
840
+ adapter,
841
+ adapterId: msg.adapterId ?? adapter.id,
842
+ chatId: msg.chatId,
843
+ threadId: msg.threadId,
844
+ };
845
+ await adapter.sendText(msg.chatId, await this.ctx.startInstallSession(arg, chat), { threadId: msg.threadId });
846
+ }
847
+ async handleTipsCommand(msg) {
848
+ const adapter = this.getReplyAdapter(msg);
849
+ if (!adapter || !this.ctx.fleetConfig)
850
+ return;
851
+ const mode = msg.text.trim().replace(/^\/tips(?:@\S+)?/, "").trim().toLowerCase();
852
+ if (mode === "advanced on") {
853
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
854
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
855
+ return;
856
+ }
857
+ await adapter.sendText(msg.chatId, this.ctx.unlockAdvancedTips?.(msg.userId)
858
+ ? t("tips.advanced.unlocked")
859
+ : t("tips.unavailable"), { threadId: msg.threadId });
860
+ return;
861
+ }
862
+ if (mode === "on" || mode === "off") {
863
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
864
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
865
+ return;
866
+ }
867
+ this.ctx.fleetConfig.defaults.tips = mode === "on";
868
+ this.ctx.saveFleetConfig();
869
+ await adapter.sendText(msg.chatId, t(mode === "on" ? "tips.enabled" : "tips.disabled"), {
870
+ threadId: msg.threadId,
871
+ });
872
+ return;
873
+ }
874
+ if (mode) {
875
+ await adapter.sendText(msg.chatId, t("tips.usage"), { threadId: msg.threadId });
876
+ return;
877
+ }
878
+ const result = await this.ctx.promptTip?.(
879
+ // The instance name scopes prompt cleanup; the Tip is posted at the
880
+ // caller's current chat/thread below.
881
+ Object.entries(this.ctx.fleetConfig.instances)
882
+ .find(([, config]) => config.general_topic === true
883
+ && (!config.channel_id || config.channel_id === msg.adapterId))?.[0] ?? "general", adapter, msg.chatId, msg.threadId) ?? "unavailable";
884
+ if (result === "empty") {
885
+ await adapter.sendText(msg.chatId, t("tips.empty"), { threadId: msg.threadId });
886
+ }
887
+ else if (result === "unavailable") {
888
+ await adapter.sendText(msg.chatId, t("tips.unavailable"), { threadId: msg.threadId });
606
889
  }
607
890
  }
608
891
  async handleStatusCommand(msg) {
@@ -631,7 +914,7 @@ export class TopicCommands {
631
914
  /** Get fleet status as markdown text (shared by TG + DC) */
632
915
  async getStatusText() {
633
916
  if (!this.ctx.fleetConfig)
634
- return "No fleet config loaded.";
917
+ return t("status.no_config");
635
918
  const rows = [];
636
919
  let pausedCount = 0;
637
920
  const fleetNames = new Set(Object.keys(this.ctx.fleetConfig.instances));
@@ -668,10 +951,10 @@ export class TopicCommands {
668
951
  icon = "🔴";
669
952
  else
670
953
  icon = "⚪";
671
- const stateLabel = executionState === "idle" ? "🟢 idle"
672
- : executionState === "working" ? "🔵 working"
673
- : executionState === "stuck" ? "🔴 stuck"
674
- : executionState === "paused" ? "⏸ paused"
954
+ const stateLabel = executionState === "idle" ? `🟢 ${t("state.idle")}`
955
+ : executionState === "working" ? `🔵 ${t("state.working")}`
956
+ : executionState === "stuck" ? `🔴 ${t("state.stuck")}`
957
+ : executionState === "paused" ? `⏸ ${t("state.paused")}`
675
958
  : "—";
676
959
  const displayName = this.shortInstanceName(name);
677
960
  // IPC reachability moved here from /sysinfo: it is per-instance health, and
@@ -683,27 +966,27 @@ export class TopicCommands {
683
966
  rows.push(`| ${displayName} | ${backend} | ${contextStr} | ${effort} | ${formatCents(costCents)} | ${icon} | ${stateLabel} | ${ipc} |`);
684
967
  }
685
968
  if (rows.length === 0)
686
- return "No instances configured.";
969
+ return t("status.no_instances");
687
970
  const lines = [
688
- "## Fleet Status",
971
+ `## ${t("status.title")}`,
689
972
  "",
690
- "| Instance | Backend | Ctx | Effort | Cost | Status | State | IPC |",
973
+ t("status.table_header"),
691
974
  "|----------|---------|-----|--------|------|--------|-------|-----|",
692
975
  ...rows,
693
976
  "",
694
- `Paused instances: ${pausedCount}`,
977
+ t("status.paused_count", pausedCount),
695
978
  ];
696
979
  const limitCents = this.ctx.costGuard?.getLimitCents() ?? 0;
697
980
  const totalCents = this.ctx.costGuard?.getFleetTotalCents() ?? 0;
698
981
  if (limitCents > 0) {
699
- lines.push("", `Fleet: ${formatCents(totalCents)} / ${formatCents(limitCents)} daily`);
982
+ lines.push("", t("status.daily_cost", formatCents(totalCents), formatCents(limitCents)));
700
983
  }
701
984
  // Adapter states (only show if any are not connected)
702
985
  const adapterStates = this.ctx.getAdapterStates?.();
703
986
  if (adapterStates && adapterStates.size > 0) {
704
987
  const issues = [...adapterStates.entries()].filter(([, s]) => s.status !== "connected");
705
988
  if (issues.length > 0) {
706
- lines.push("", "**Adapters:**");
989
+ lines.push("", `**${t("status.adapters")}**`);
707
990
  for (const [id, s] of adapterStates) {
708
991
  const icon = s.status === "connected" ? "✅" : s.status === "retrying" ? "🔄" : "❌";
709
992
  lines.push(` ${icon} ${id}: ${s.status}${s.lastError ? ` (${s.lastError.slice(0, 60)})` : ""}`);
@@ -716,35 +999,73 @@ export class TopicCommands {
716
999
  const adapter = this.getReplyAdapter(msg);
717
1000
  if (!adapter)
718
1001
  return;
719
- const text = this.getSysInfoText();
1002
+ const platform = adapter.type === "discord" ? "discord" : "telegram";
1003
+ const text = this.getSysInfoText({ platform });
720
1004
  await adapter.sendText(msg.chatId, text, { threadId: msg.threadId });
721
1005
  }
722
1006
  /**
723
- * Get system info as markdown text (shared by TG + DC).
1007
+ * Get system info as formatted text.
724
1008
  *
725
1009
  * System-level only. The per-instance table (state/IPC/cost) moved to /status,
726
1010
  * which was already the fleet-per-instance view — the two tables overlapped on
727
1011
  * everything but the IPC column, and each command answered half of "is the
728
1012
  * machine fine and are the instances fine".
1013
+ *
1014
+ * @param opts.platform - "telegram" uses markdown table, "discord" uses plain lines
729
1015
  */
730
- getSysInfoText() {
1016
+ getSysInfoText(opts) {
731
1017
  const info = this.ctx.getSysInfo();
732
1018
  const upHours = Math.floor(info.uptime_seconds / 3600);
733
1019
  const upMins = Math.floor((info.uptime_seconds % 3600) / 60);
734
1020
  const require = createRequire(import.meta.url);
735
1021
  const agendVersion = require("../package.json").version ?? "unknown";
1022
+ const tipsLang = getLocale() === "zh-TW" ? "zh" : "en";
1023
+ // Fleet summary lines (multi-line for mobile readability)
1024
+ const summaryLines = [
1025
+ `${t("sysinfo.instances")}: ${info.running_count} ${t("sysinfo.running")}, ${info.paused_count} ${t("sysinfo.paused")}`,
1026
+ ...(info.fleet_mem_mb !== null ? [`${t("sysinfo.fleet_mem")}: ${(info.fleet_mem_mb / 1024).toFixed(1)} GB`] : []),
1027
+ `${t("sysinfo.system_mem")}: ${info.system_mem_gb.used} / ${info.system_mem_gb.total} GB`,
1028
+ ];
1029
+ const resources = [
1030
+ `📚 **${t("sysinfo.resources")}**`,
1031
+ `- ${t("sysinfo.docs")}: https://songsid.github.io/AgEnD`,
1032
+ `- ${t("sysinfo.tips")}: https://songsid.github.io/AgEnD/tips-${tipsLang}.html`,
1033
+ `- ${t("sysinfo.github")}: https://github.com/songsid/AgEnD`,
1034
+ ];
1035
+ // Discord: plain key-value lines (Discord doesn't render markdown tables)
1036
+ if (opts?.platform === "discord") {
1037
+ return [
1038
+ `## ${t("sysinfo.title")}`,
1039
+ `AgEnD: v${agendVersion}`,
1040
+ `OS: ${process.platform} ${osRelease()} (${process.arch})`,
1041
+ `Node: ${process.version}`,
1042
+ `tmux: ${tmuxVersion()}`,
1043
+ `${t("sysinfo.uptime")}: ${upHours}h ${upMins}m`,
1044
+ `${t("sysinfo.memory")}: ${info.memory_mb.rss} MB RSS`,
1045
+ `${t("sysinfo.heap")}: ${info.memory_mb.heapUsed} / ${info.memory_mb.heapTotal} MB`,
1046
+ "",
1047
+ ...summaryLines,
1048
+ "",
1049
+ ...resources,
1050
+ ].join("\n");
1051
+ }
1052
+ // Telegram (default): markdown table
736
1053
  return [
737
- "## System Info",
1054
+ `## ${t("sysinfo.title")}`,
738
1055
  "",
739
- "| Metric | Value |",
1056
+ `| ${t("sysinfo.metric")} | ${t("sysinfo.value")} |`,
740
1057
  "|--------|-------|",
741
1058
  `| AgEnD | v${agendVersion} |`,
742
1059
  `| OS | ${process.platform} ${osRelease()} (${process.arch}) |`,
743
1060
  `| Node | ${process.version} |`,
744
1061
  `| tmux | ${tmuxVersion()} |`,
745
- `| Uptime | ${upHours}h ${upMins}m |`,
746
- `| Memory | ${info.memory_mb.rss} MB RSS |`,
747
- `| Heap | ${info.memory_mb.heapUsed} / ${info.memory_mb.heapTotal} MB |`,
1062
+ `| ${t("sysinfo.uptime")} | ${upHours}h ${upMins}m |`,
1063
+ `| ${t("sysinfo.memory")} | ${info.memory_mb.rss} MB RSS |`,
1064
+ `| ${t("sysinfo.heap")} | ${info.memory_mb.heapUsed} / ${info.memory_mb.heapTotal} MB |`,
1065
+ "",
1066
+ ...summaryLines,
1067
+ "",
1068
+ ...resources,
748
1069
  ].join("\n");
749
1070
  }
750
1071
  async handleUpdateCommand(msg) {
@@ -797,11 +1118,11 @@ export class TopicCommands {
797
1118
  timeout: 30_000,
798
1119
  encoding: "utf-8",
799
1120
  });
800
- output = stripAnsi(stdout) || "No output";
1121
+ output = stripAnsi(stdout) || t("doctor.no_output");
801
1122
  }
802
1123
  catch (err) {
803
1124
  const e = err;
804
- output = stripAnsi(e.stdout ?? e.message ?? "Doctor failed");
1125
+ output = stripAnsi(e.stdout ?? e.message ?? t("doctor.failed"));
805
1126
  }
806
1127
  await adapter.sendText(chatId, output, { threadId });
807
1128
  }
@@ -810,7 +1131,7 @@ export class TopicCommands {
810
1131
  const adapter = this.getReplyAdapter(msg);
811
1132
  if (!adapter)
812
1133
  return;
813
- 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 });
1134
+ await adapter.sendText(msg.chatId, t("topic.unbound"), { threadId: msg.threadId });
814
1135
  }
815
1136
  /** Handle topic deletion — stop daemon and remove from config */
816
1137
  async handleTopicDeleted(threadId) {
@@ -923,21 +1244,26 @@ export class TopicCommands {
923
1244
  }
924
1245
  try {
925
1246
  const fleetCommands = [
926
- { command: "status", description: "🔒 Fleet status, per-instance costs and health" },
927
- { command: "sysinfo", description: "System diagnostics" },
928
- { command: "dashboard", description: "🔒 Get dashboard URLs" },
929
- { command: "ctx", description: "Show context usage" },
930
- { command: "compact", description: "Compact agent context" },
931
- { command: "clear", description: "🔒 Clear agent conversation context" },
932
- { command: "model", description: "🔒 Switch model (admin only)" },
933
- { command: "effort", description: "🔒 Set reasoning effort (admin only)" },
934
- { command: "pause", description: "🔒 Pause an idle instance" },
935
- { command: "wake", description: "🔒 Wake a paused instance" },
936
- { command: "restart", description: "🔒 Graceful restart all instances" },
937
- { command: "collab", description: "🔒 Toggle bot/webhook mode" },
938
- { command: "update", description: "🔒 Update AgEnD to latest" },
939
- { command: "doctor", description: "🔒 Run health diagnostics" },
940
- { command: "usage", description: "AI subscription usage" },
1247
+ { command: "status", description: "🔒 " + t("slash.status") },
1248
+ { command: "sysinfo", description: t("slash.sysinfo") },
1249
+ { command: "dashboard", description: "🔒 " + t("slash.dashboard") },
1250
+ { command: "ctx", description: t("slash.ctx") },
1251
+ { command: "compact", description: t("slash.compact") },
1252
+ { command: "steer", description: t("slash.steer") },
1253
+ { command: "btw", description: t("slash.btw") },
1254
+ { command: "clear", description: "🔒 " + t("slash.clear") },
1255
+ { command: "model", description: "🔒 " + t("slash.model") },
1256
+ { command: "effort", description: "🔒 " + t("slash.effort") },
1257
+ { command: "pause", description: "🔒 " + t("slash.pause") },
1258
+ { command: "wake", description: "🔒 " + t("slash.wake") },
1259
+ { command: "restart", description: "🔒 " + t("slash.restart") },
1260
+ { command: "collab", description: "🔒 " + t("slash.collab") },
1261
+ { command: "update", description: "🔒 " + t("slash.update") },
1262
+ { command: "doctor", description: "🔒 " + t("slash.doctor") },
1263
+ { command: "login", description: "🔒 " + t("slash.login") },
1264
+ { command: "install_cli", description: "🔒 " + t("slash.install_cli") },
1265
+ { command: "usage", description: t("slash.usage") },
1266
+ { command: "tips", description: t("slash.tips") },
941
1267
  ];
942
1268
  const setCommands = async (commands, scope) => {
943
1269
  const response = await fetch(`https://api.telegram.org/bot${botToken}/setMyCommands`, {
@@ -958,15 +1284,17 @@ export class TopicCommands {
958
1284
  }
959
1285
  };
960
1286
  const classicCommands = [
961
- { command: "start", description: "🔒 Start an agent in this chat" },
962
- { command: "stop", description: "🔒 Stop the agent" },
963
- { command: "compact", description: "🔒 Compact agent context" },
964
- { command: "clear", description: "🔒 Clear agent conversation context" },
965
- { command: "model", description: "🔒 Switch model (admin only)" },
966
- { command: "effort", description: "🔒 Set reasoning effort (admin only)" },
967
- { command: "pause", description: "🔒 Pause the agent" },
968
- { command: "wake", description: "🔒 Wake the agent" },
969
- { command: "ctx", description: "Show context usage" },
1287
+ { command: "start", description: "🔒 " + t("slash.start") },
1288
+ { command: "stop", description: "🔒 " + t("slash.stop") },
1289
+ { command: "compact", description: "🔒 " + t("slash.compact") },
1290
+ { command: "steer", description: t("slash.steer") },
1291
+ { command: "btw", description: t("slash.btw") },
1292
+ { command: "clear", description: "🔒 " + t("slash.clear") },
1293
+ { command: "model", description: "🔒 " + t("slash.model") },
1294
+ { command: "effort", description: "🔒 " + t("slash.effort") },
1295
+ { command: "pause", description: "🔒 " + t("slash.pause") },
1296
+ { command: "wake", description: "🔒 " + t("slash.wake") },
1297
+ { command: "ctx", description: t("slash.ctx") },
970
1298
  ];
971
1299
  // A chat_administrators scope has higher precedence than the chat scope.
972
1300
  // Keep both synchronized so a stale admin-only list from BotFather or an