@songsid/agend 2.1.4-beta.9 → 2.1.5-beta.1

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 (186) 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 +523 -90
  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 +13 -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 +491 -14
  66. package/dist/daemon.js +2121 -276
  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 +402 -11
  74. package/dist/fleet-manager.js +2594 -422
  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 -320
  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/topic-commands.d.ts +31 -7
  142. package/dist/topic-commands.js +342 -70
  143. package/dist/topic-commands.js.map +1 -1
  144. package/dist/transcript-sources.d.ts +35 -10
  145. package/dist/transcript-sources.js +190 -38
  146. package/dist/transcript-sources.js.map +1 -1
  147. package/dist/types.d.ts +6 -1
  148. package/dist/ui/dashboard.html +55 -32
  149. package/dist/ui/settings.html +200 -60
  150. package/dist/ui/view.html +147 -30
  151. package/dist/ui/web-terminal/terminal.css +17 -0
  152. package/dist/ui/web-terminal/terminal.html +43 -0
  153. package/dist/ui/web-terminal/terminal.js +148 -0
  154. package/dist/ui/web-terminal/vendor/LICENSE.addon-fit +19 -0
  155. package/dist/ui/web-terminal/vendor/LICENSE.addon-web-links +19 -0
  156. package/dist/ui/web-terminal/vendor/LICENSE.xterm +21 -0
  157. package/dist/ui/web-terminal/vendor/VENDORED.md +22 -0
  158. package/dist/ui/web-terminal/vendor/addon-fit.js +1 -0
  159. package/dist/ui/web-terminal/vendor/addon-web-links.js +1 -0
  160. package/dist/ui/web-terminal/vendor/xterm.css +285 -0
  161. package/dist/ui/web-terminal/vendor/xterm.js +1 -0
  162. package/dist/usage/format-rich.d.ts +1 -1
  163. package/dist/usage/format-rich.js +28 -24
  164. package/dist/usage/format-rich.js.map +1 -1
  165. package/dist/usage/i18n-keys.d.ts +7 -0
  166. package/dist/usage/i18n-keys.js +34 -0
  167. package/dist/usage/i18n-keys.js.map +1 -0
  168. package/dist/usage/i18n.d.ts +5 -0
  169. package/dist/usage/i18n.js +27 -0
  170. package/dist/usage/i18n.js.map +1 -0
  171. package/dist/usage/providers.d.ts +21 -0
  172. package/dist/usage/providers.js +153 -75
  173. package/dist/usage/providers.js.map +1 -1
  174. package/dist/usage/usage-api.d.ts +11 -3
  175. package/dist/usage/usage-api.js +61 -24
  176. package/dist/usage/usage-api.js.map +1 -1
  177. package/dist/web-terminal-http.d.ts +53 -0
  178. package/dist/web-terminal-http.js +396 -0
  179. package/dist/web-terminal-http.js.map +1 -0
  180. package/dist/web-terminal.d.ts +329 -0
  181. package/dist/web-terminal.js +833 -0
  182. package/dist/web-terminal.js.map +1 -0
  183. package/dist/ws-server.d.ts +69 -0
  184. package/dist/ws-server.js +392 -0
  185. package/dist/ws-server.js.map +1 -0
  186. 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
  }
@@ -439,6 +488,22 @@ export class TopicCommands {
439
488
  await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
440
489
  return true;
441
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
+ }
442
507
  if (text === "/clear" || text.startsWith("/clear@")) {
443
508
  const adapter = this.getReplyAdapter(msg);
444
509
  if (!adapter)
@@ -492,7 +557,7 @@ export class TopicCommands {
492
557
  }
493
558
  async runPauseWake(instanceName, action) {
494
559
  if (action === "pause" && isGeneralInstance(this.ctx.fleetConfig, instanceName)) {
495
- return GENERAL_PAUSE_ERROR;
560
+ return t("general.pause_forbidden");
496
561
  }
497
562
  try {
498
563
  const result = await this.ctx.changeInstancePauseState(instanceName, action);
@@ -523,7 +588,10 @@ export class TopicCommands {
523
588
  const contextLine = context == null ? null : formatContextUsageLine(context, tokenRatio);
524
589
  // Effective model (resolves per-instance → fleet default → classic channel →
525
590
  // the CLI's own default) via the shared resolver, so /ctx and /model agree.
526
- 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);
527
595
  const modelLine = modelDisplay ? `\n${t("ctx.model", modelDisplay)}` : "";
528
596
  return context != null
529
597
  ? `${contextLine}\n${t("ctx.backend", backend)}${modelLine}\n${t("ctx.instance", instanceName)}`
@@ -541,9 +609,9 @@ export class TopicCommands {
541
609
  ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
542
610
  const cmd = compactCommandForBackend(backend);
543
611
  ipc.send({ type: "raw_paste", content: cmd });
544
- return `🗜️ Compact command sent (\`${cmd}\`).`;
612
+ return t("compact.sent", cmd);
545
613
  }
546
- return "❌ Instance not connected (IPC unavailable)";
614
+ return t("compact.not_connected");
547
615
  }
548
616
  /** Whether the instance backend exposes a verified full-reset command. */
549
617
  supportsClear(instanceName) {
@@ -562,10 +630,9 @@ export class TopicCommands {
562
630
  * the user gets an honest "not supported" instead of a silent queue
563
631
  * fallback that looks like a steer but behaves like a normal message.
564
632
  */
565
- static STEER_SUPPORTED_BACKENDS = new Set(["claude-code", "codex", "grok", "mock"]);
566
633
  sendSteer(instanceName, content, msg) {
567
634
  const backend = this.effectiveBackend(instanceName);
568
- if (!TopicCommands.STEER_SUPPORTED_BACKENDS.has(backend)) {
635
+ if (!backendSupportsSteer(backend)) {
569
636
  return t("steer.unsupported", backend);
570
637
  }
571
638
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
@@ -574,6 +641,7 @@ export class TopicCommands {
574
641
  ipc.send({
575
642
  type: "steer",
576
643
  content,
644
+ delivery_epoch: this.ctx.getDeliveryEpoch?.(instanceName) ?? undefined,
577
645
  meta: {
578
646
  chat_id: msg.chatId,
579
647
  message_id: msg.messageId ?? "",
@@ -586,15 +654,43 @@ export class TopicCommands {
586
654
  });
587
655
  return t("steer.sent", instanceName);
588
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
+ }
589
681
  /** Send the backend-appropriate full conversation reset through raw_paste. */
590
682
  async sendClear(instanceName) {
591
683
  const backend = this.effectiveBackend(instanceName);
592
684
  const cmd = clearCommandForBackend(backend);
593
685
  if (!cmd)
594
- return CLEAR_UNSUPPORTED_MSG;
686
+ return t("clear.unsupported");
595
687
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
596
688
  if (ipc?.connected) {
597
- 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
+ });
598
694
  return t("clear.sent", cmd);
599
695
  }
600
696
  return t("clear.not_connected");
@@ -609,13 +705,13 @@ export class TopicCommands {
609
705
  ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
610
706
  const cmd = saveCommandForBackend(backend, filename);
611
707
  if (!cmd)
612
- return SAVE_UNSUPPORTED_MSG;
708
+ return t("save.unsupported");
613
709
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
614
710
  if (ipc?.connected) {
615
711
  ipc.send({ type: "raw_paste", content: cmd });
616
- return `💾 Save command sent (\`${cmd}\`).`;
712
+ return t("save.sent", cmd, instanceName);
617
713
  }
618
- return "❌ Instance not connected (IPC unavailable)";
714
+ return t("save.not_connected");
619
715
  }
620
716
  async handleRestartCommand(msg) {
621
717
  const adapter = this.getReplyAdapter(msg);
@@ -632,8 +728,8 @@ export class TopicCommands {
632
728
  process.kill(process.pid, "SIGUSR2");
633
729
  }
634
730
  /**
635
- * `/usage` — AI subscription usage for every CLI backend logged in on this
636
- * 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
637
733
  * can talk to the fleet may see how much headroom it has left. Vendor rate
638
734
  * limits are protected by the shared 5-minute cache, not by gating callers.
639
735
  */
@@ -644,7 +740,7 @@ export class TopicCommands {
644
740
  try {
645
741
  const { getUsageSnapshot } = await import("./usage/usage-api.js");
646
742
  const { renderUsageHtml, renderUsageMarkdown } = await import("./usage/format-rich.js");
647
- const payload = await getUsageSnapshot();
743
+ const payload = await getUsageSnapshot(false, this.ctx.getActiveUsageProviderIds?.());
648
744
  // Each platform gets its native rich format: Telegram needs parse_mode
649
745
  // HTML; Discord renders Markdown in plain content. Anything else falls back
650
746
  // to Markdown, which degrades to readable text.
@@ -656,7 +752,140 @@ export class TopicCommands {
656
752
  }
657
753
  }
658
754
  catch (err) {
659
- 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 });
660
889
  }
661
890
  }
662
891
  async handleStatusCommand(msg) {
@@ -685,7 +914,7 @@ export class TopicCommands {
685
914
  /** Get fleet status as markdown text (shared by TG + DC) */
686
915
  async getStatusText() {
687
916
  if (!this.ctx.fleetConfig)
688
- return "No fleet config loaded.";
917
+ return t("status.no_config");
689
918
  const rows = [];
690
919
  let pausedCount = 0;
691
920
  const fleetNames = new Set(Object.keys(this.ctx.fleetConfig.instances));
@@ -722,10 +951,10 @@ export class TopicCommands {
722
951
  icon = "🔴";
723
952
  else
724
953
  icon = "⚪";
725
- const stateLabel = executionState === "idle" ? "🟢 idle"
726
- : executionState === "working" ? "🔵 working"
727
- : executionState === "stuck" ? "🔴 stuck"
728
- : 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")}`
729
958
  : "—";
730
959
  const displayName = this.shortInstanceName(name);
731
960
  // IPC reachability moved here from /sysinfo: it is per-instance health, and
@@ -737,27 +966,27 @@ export class TopicCommands {
737
966
  rows.push(`| ${displayName} | ${backend} | ${contextStr} | ${effort} | ${formatCents(costCents)} | ${icon} | ${stateLabel} | ${ipc} |`);
738
967
  }
739
968
  if (rows.length === 0)
740
- return "No instances configured.";
969
+ return t("status.no_instances");
741
970
  const lines = [
742
- "## Fleet Status",
971
+ `## ${t("status.title")}`,
743
972
  "",
744
- "| Instance | Backend | Ctx | Effort | Cost | Status | State | IPC |",
973
+ t("status.table_header"),
745
974
  "|----------|---------|-----|--------|------|--------|-------|-----|",
746
975
  ...rows,
747
976
  "",
748
- `Paused instances: ${pausedCount}`,
977
+ t("status.paused_count", pausedCount),
749
978
  ];
750
979
  const limitCents = this.ctx.costGuard?.getLimitCents() ?? 0;
751
980
  const totalCents = this.ctx.costGuard?.getFleetTotalCents() ?? 0;
752
981
  if (limitCents > 0) {
753
- lines.push("", `Fleet: ${formatCents(totalCents)} / ${formatCents(limitCents)} daily`);
982
+ lines.push("", t("status.daily_cost", formatCents(totalCents), formatCents(limitCents)));
754
983
  }
755
984
  // Adapter states (only show if any are not connected)
756
985
  const adapterStates = this.ctx.getAdapterStates?.();
757
986
  if (adapterStates && adapterStates.size > 0) {
758
987
  const issues = [...adapterStates.entries()].filter(([, s]) => s.status !== "connected");
759
988
  if (issues.length > 0) {
760
- lines.push("", "**Adapters:**");
989
+ lines.push("", `**${t("status.adapters")}**`);
761
990
  for (const [id, s] of adapterStates) {
762
991
  const icon = s.status === "connected" ? "✅" : s.status === "retrying" ? "🔄" : "❌";
763
992
  lines.push(` ${icon} ${id}: ${s.status}${s.lastError ? ` (${s.lastError.slice(0, 60)})` : ""}`);
@@ -770,35 +999,73 @@ export class TopicCommands {
770
999
  const adapter = this.getReplyAdapter(msg);
771
1000
  if (!adapter)
772
1001
  return;
773
- const text = this.getSysInfoText();
1002
+ const platform = adapter.type === "discord" ? "discord" : "telegram";
1003
+ const text = this.getSysInfoText({ platform });
774
1004
  await adapter.sendText(msg.chatId, text, { threadId: msg.threadId });
775
1005
  }
776
1006
  /**
777
- * Get system info as markdown text (shared by TG + DC).
1007
+ * Get system info as formatted text.
778
1008
  *
779
1009
  * System-level only. The per-instance table (state/IPC/cost) moved to /status,
780
1010
  * which was already the fleet-per-instance view — the two tables overlapped on
781
1011
  * everything but the IPC column, and each command answered half of "is the
782
1012
  * machine fine and are the instances fine".
1013
+ *
1014
+ * @param opts.platform - "telegram" uses markdown table, "discord" uses plain lines
783
1015
  */
784
- getSysInfoText() {
1016
+ getSysInfoText(opts) {
785
1017
  const info = this.ctx.getSysInfo();
786
1018
  const upHours = Math.floor(info.uptime_seconds / 3600);
787
1019
  const upMins = Math.floor((info.uptime_seconds % 3600) / 60);
788
1020
  const require = createRequire(import.meta.url);
789
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
790
1053
  return [
791
- "## System Info",
1054
+ `## ${t("sysinfo.title")}`,
792
1055
  "",
793
- "| Metric | Value |",
1056
+ `| ${t("sysinfo.metric")} | ${t("sysinfo.value")} |`,
794
1057
  "|--------|-------|",
795
1058
  `| AgEnD | v${agendVersion} |`,
796
1059
  `| OS | ${process.platform} ${osRelease()} (${process.arch}) |`,
797
1060
  `| Node | ${process.version} |`,
798
1061
  `| tmux | ${tmuxVersion()} |`,
799
- `| Uptime | ${upHours}h ${upMins}m |`,
800
- `| Memory | ${info.memory_mb.rss} MB RSS |`,
801
- `| 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,
802
1069
  ].join("\n");
803
1070
  }
804
1071
  async handleUpdateCommand(msg) {
@@ -851,11 +1118,11 @@ export class TopicCommands {
851
1118
  timeout: 30_000,
852
1119
  encoding: "utf-8",
853
1120
  });
854
- output = stripAnsi(stdout) || "No output";
1121
+ output = stripAnsi(stdout) || t("doctor.no_output");
855
1122
  }
856
1123
  catch (err) {
857
1124
  const e = err;
858
- output = stripAnsi(e.stdout ?? e.message ?? "Doctor failed");
1125
+ output = stripAnsi(e.stdout ?? e.message ?? t("doctor.failed"));
859
1126
  }
860
1127
  await adapter.sendText(chatId, output, { threadId });
861
1128
  }
@@ -864,7 +1131,7 @@ export class TopicCommands {
864
1131
  const adapter = this.getReplyAdapter(msg);
865
1132
  if (!adapter)
866
1133
  return;
867
- 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 });
868
1135
  }
869
1136
  /** Handle topic deletion — stop daemon and remove from config */
870
1137
  async handleTopicDeleted(threadId) {
@@ -977,22 +1244,26 @@ export class TopicCommands {
977
1244
  }
978
1245
  try {
979
1246
  const fleetCommands = [
980
- { command: "status", description: "🔒 Fleet status, per-instance costs and health" },
981
- { command: "sysinfo", description: "System diagnostics" },
982
- { command: "dashboard", description: "🔒 Get dashboard URLs" },
983
- { command: "ctx", description: "Show context usage" },
984
- { command: "compact", description: "Compact agent context" },
985
- { command: "steer", description: "Interject into the current task" },
986
- { command: "clear", description: "🔒 Clear agent conversation context" },
987
- { command: "model", description: "🔒 Switch model (admin only)" },
988
- { command: "effort", description: "🔒 Set reasoning effort (admin only)" },
989
- { command: "pause", description: "🔒 Pause an idle instance" },
990
- { command: "wake", description: "🔒 Wake a paused instance" },
991
- { command: "restart", description: "🔒 Graceful restart all instances" },
992
- { command: "collab", description: "🔒 Toggle bot/webhook mode" },
993
- { command: "update", description: "🔒 Update AgEnD to latest" },
994
- { command: "doctor", description: "🔒 Run health diagnostics" },
995
- { 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") },
996
1267
  ];
997
1268
  const setCommands = async (commands, scope) => {
998
1269
  const response = await fetch(`https://api.telegram.org/bot${botToken}/setMyCommands`, {
@@ -1013,16 +1284,17 @@ export class TopicCommands {
1013
1284
  }
1014
1285
  };
1015
1286
  const classicCommands = [
1016
- { command: "start", description: "🔒 Start an agent in this chat" },
1017
- { command: "stop", description: "🔒 Stop the agent" },
1018
- { command: "compact", description: "🔒 Compact agent context" },
1019
- { command: "steer", description: "Interject into the current task" },
1020
- { command: "clear", description: "🔒 Clear agent conversation context" },
1021
- { command: "model", description: "🔒 Switch model (admin only)" },
1022
- { command: "effort", description: "🔒 Set reasoning effort (admin only)" },
1023
- { command: "pause", description: "🔒 Pause the agent" },
1024
- { command: "wake", description: "🔒 Wake the agent" },
1025
- { 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") },
1026
1298
  ];
1027
1299
  // A chat_administrators scope has higher precedence than the chat scope.
1028
1300
  // Keep both synchronized so a stale admin-only list from BotFather or an