@songsid/agend 2.1.4-beta.4 → 2.1.4-beta.40

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 (168) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-endpoint.js +1 -1
  4. package/dist/agent-endpoint.js.map +1 -1
  5. package/dist/backend/antigravity.d.ts +9 -5
  6. package/dist/backend/antigravity.js +156 -31
  7. package/dist/backend/antigravity.js.map +1 -1
  8. package/dist/backend/claude-code.d.ts +62 -23
  9. package/dist/backend/claude-code.js +296 -43
  10. package/dist/backend/claude-code.js.map +1 -1
  11. package/dist/backend/codex.d.ts +18 -0
  12. package/dist/backend/codex.js +148 -8
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/gemini-cli.js +2 -2
  15. package/dist/backend/gemini-cli.js.map +1 -1
  16. package/dist/backend/grok.js +2 -2
  17. package/dist/backend/grok.js.map +1 -1
  18. package/dist/backend/kiro.d.ts +2 -0
  19. package/dist/backend/kiro.js +24 -3
  20. package/dist/backend/kiro.js.map +1 -1
  21. package/dist/backend/opencode.d.ts +27 -11
  22. package/dist/backend/opencode.js +65 -41
  23. package/dist/backend/opencode.js.map +1 -1
  24. package/dist/backend/types.d.ts +45 -3
  25. package/dist/backend/types.js +12 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/channel/adapters/discord.d.ts +44 -2
  28. package/dist/channel/adapters/discord.js +526 -78
  29. package/dist/channel/adapters/discord.js.map +1 -1
  30. package/dist/channel/adapters/telegram.d.ts +30 -0
  31. package/dist/channel/adapters/telegram.js +212 -18
  32. package/dist/channel/adapters/telegram.js.map +1 -1
  33. package/dist/channel/agy-mcp-launcher.d.ts +2 -0
  34. package/dist/channel/agy-mcp-launcher.js +28 -0
  35. package/dist/channel/agy-mcp-launcher.js.map +1 -0
  36. package/dist/channel/ipc-bridge.d.ts +2 -1
  37. package/dist/channel/ipc-bridge.js +33 -4
  38. package/dist/channel/ipc-bridge.js.map +1 -1
  39. package/dist/channel/mcp-server.js +28 -25
  40. package/dist/channel/mcp-server.js.map +1 -1
  41. package/dist/channel/mcp-tools.js +4 -2
  42. package/dist/channel/mcp-tools.js.map +1 -1
  43. package/dist/channel/message-queue.js +62 -1
  44. package/dist/channel/message-queue.js.map +1 -1
  45. package/dist/channel/types.d.ts +31 -1
  46. package/dist/classic-channel-manager.d.ts +40 -3
  47. package/dist/classic-channel-manager.js +218 -31
  48. package/dist/classic-channel-manager.js.map +1 -1
  49. package/dist/cli.js +109 -37
  50. package/dist/cli.js.map +1 -1
  51. package/dist/config-validator.js +48 -7
  52. package/dist/config-validator.js.map +1 -1
  53. package/dist/config.d.ts +4 -0
  54. package/dist/config.js +12 -1
  55. package/dist/config.js.map +1 -1
  56. package/dist/cross-instance-envelope.d.ts +6 -0
  57. package/dist/cross-instance-envelope.js +30 -0
  58. package/dist/cross-instance-envelope.js.map +1 -0
  59. package/dist/daemon.d.ts +319 -9
  60. package/dist/daemon.js +1402 -255
  61. package/dist/daemon.js.map +1 -1
  62. package/dist/doctor.d.ts +41 -0
  63. package/dist/doctor.js +267 -0
  64. package/dist/doctor.js.map +1 -0
  65. package/dist/fleet-context.d.ts +47 -0
  66. package/dist/fleet-manager.d.ts +265 -11
  67. package/dist/fleet-manager.js +2273 -455
  68. package/dist/fleet-manager.js.map +1 -1
  69. package/dist/fleet-yaml-slim.d.ts +10 -0
  70. package/dist/fleet-yaml-slim.js +59 -0
  71. package/dist/fleet-yaml-slim.js.map +1 -0
  72. package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
  73. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +3 -1
  74. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
  75. package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
  76. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
  77. package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
  78. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
  79. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
  80. package/dist/general-knowledge/skills/model-discovery/SKILL.md +64 -16
  81. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
  82. package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
  83. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -0
  84. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
  85. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
  86. package/dist/instance-lifecycle.d.ts +67 -0
  87. package/dist/instance-lifecycle.js +273 -8
  88. package/dist/instance-lifecycle.js.map +1 -1
  89. package/dist/instructions.d.ts +12 -0
  90. package/dist/instructions.js +35 -1
  91. package/dist/instructions.js.map +1 -1
  92. package/dist/locale.js +958 -288
  93. package/dist/locale.js.map +1 -1
  94. package/dist/login-flows.d.ts +91 -0
  95. package/dist/login-flows.js +170 -0
  96. package/dist/login-flows.js.map +1 -0
  97. package/dist/login-manager.d.ts +63 -0
  98. package/dist/login-manager.js +134 -0
  99. package/dist/login-manager.js.map +1 -0
  100. package/dist/network-family.d.ts +18 -0
  101. package/dist/network-family.js +20 -0
  102. package/dist/network-family.js.map +1 -0
  103. package/dist/outbound-handlers.d.ts +8 -0
  104. package/dist/outbound-handlers.js +101 -18
  105. package/dist/outbound-handlers.js.map +1 -1
  106. package/dist/outbound-schemas.d.ts +7 -1
  107. package/dist/outbound-schemas.js +9 -0
  108. package/dist/outbound-schemas.js.map +1 -1
  109. package/dist/reply-dedup.d.ts +7 -8
  110. package/dist/reply-dedup.js +0 -0
  111. package/dist/reply-dedup.js.map +1 -1
  112. package/dist/restart-progress.d.ts +2 -0
  113. package/dist/restart-progress.js +3 -0
  114. package/dist/restart-progress.js.map +1 -1
  115. package/dist/scheduler/db.d.ts +12 -0
  116. package/dist/scheduler/db.js +59 -0
  117. package/dist/scheduler/db.js.map +1 -1
  118. package/dist/service-installer.d.ts +11 -0
  119. package/dist/service-installer.js +84 -18
  120. package/dist/service-installer.js.map +1 -1
  121. package/dist/settings-api.js +1 -1
  122. package/dist/settings-api.js.map +1 -1
  123. package/dist/setup-wizard.js +2 -2
  124. package/dist/setup-wizard.js.map +1 -1
  125. package/dist/spawn-gate.d.ts +30 -0
  126. package/dist/spawn-gate.js +79 -0
  127. package/dist/spawn-gate.js.map +1 -0
  128. package/dist/storm-window.d.ts +83 -0
  129. package/dist/storm-window.js +251 -0
  130. package/dist/storm-window.js.map +1 -0
  131. package/dist/tips.d.ts +44 -0
  132. package/dist/tips.js +355 -0
  133. package/dist/tips.js.map +1 -0
  134. package/dist/tmux-manager.d.ts +16 -0
  135. package/dist/tmux-manager.js +110 -25
  136. package/dist/tmux-manager.js.map +1 -1
  137. package/dist/tool-progress.d.ts +40 -0
  138. package/dist/tool-progress.js +289 -0
  139. package/dist/tool-progress.js.map +1 -0
  140. package/dist/topic-commands.d.ts +42 -4
  141. package/dist/topic-commands.js +354 -66
  142. package/dist/topic-commands.js.map +1 -1
  143. package/dist/transcript-monitor.d.ts +15 -2
  144. package/dist/transcript-monitor.js +63 -17
  145. package/dist/transcript-monitor.js.map +1 -1
  146. package/dist/transcript-sources.d.ts +131 -0
  147. package/dist/transcript-sources.js +580 -0
  148. package/dist/transcript-sources.js.map +1 -0
  149. package/dist/types.d.ts +13 -0
  150. package/dist/ui/dashboard.html +55 -32
  151. package/dist/ui/settings.html +200 -60
  152. package/dist/ui/view.html +147 -30
  153. package/dist/usage/format-rich.js +24 -23
  154. package/dist/usage/format-rich.js.map +1 -1
  155. package/dist/usage/i18n-keys.d.ts +7 -0
  156. package/dist/usage/i18n-keys.js +34 -0
  157. package/dist/usage/i18n-keys.js.map +1 -0
  158. package/dist/usage/i18n.d.ts +5 -0
  159. package/dist/usage/i18n.js +27 -0
  160. package/dist/usage/i18n.js.map +1 -0
  161. package/dist/usage/providers.d.ts +19 -0
  162. package/dist/usage/providers.js +150 -74
  163. package/dist/usage/providers.js.map +1 -1
  164. package/dist/usage/usage-api.d.ts +8 -2
  165. package/dist/usage/usage-api.js +45 -23
  166. package/dist/usage/usage-api.js.map +1 -1
  167. package/dist/workflow-templates/default.md +2 -1
  168. package/package.json +2 -2
@@ -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
@@ -137,6 +141,29 @@ export function readStatuslineContextPct(dataDir, instanceName) {
137
141
  return null;
138
142
  }
139
143
  }
144
+ /**
145
+ * Claude Code's statusline payload reports the model that actually served the
146
+ * current session. This can differ from fleet.yaml after the CLI rejects a
147
+ * plan-gated model and keeps using its previous/default model.
148
+ */
149
+ export function readStatuslineModel(dataDir, instanceName) {
150
+ try {
151
+ const statusFile = join(dataDir, "instances", instanceName, "statusline.json");
152
+ if (!existsSync(statusFile))
153
+ return null;
154
+ const data = JSON.parse(readFileSync(statusFile, "utf-8"));
155
+ const id = typeof data.model?.id === "string" ? data.model.id.trim() : "";
156
+ const displayName = typeof data.model?.display_name === "string"
157
+ ? data.model.display_name.trim()
158
+ : "";
159
+ if (displayName && id && displayName !== id)
160
+ return `${displayName} (${id})`;
161
+ return id || displayName || null;
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ }
140
167
  /** Live-pane scrape used by /ctx, /status Ctx, and /view sidebar. */
141
168
  export function scrapePaneContext(instanceName, backend) {
142
169
  try {
@@ -270,6 +297,20 @@ export class TopicCommands {
270
297
  await this.handleUsageCommand(msg);
271
298
  return true;
272
299
  }
300
+ if (text === "/tips" || text.startsWith("/tips ") || text.startsWith("/tips@")) {
301
+ await this.handleTipsCommand(msg);
302
+ return true;
303
+ }
304
+ if (text === "/login" || text.startsWith("/login ") || text.startsWith("/login@")) {
305
+ await this.handleLoginCommand(msg);
306
+ return true;
307
+ }
308
+ // Telegram command names cannot contain "-", so the menu registers
309
+ // /install_cli; accept both spellings as typed text.
310
+ if (/^\/install[-_]cli(?:@\S+)?(?:\s|$)/.test(text)) {
311
+ await this.handleInstallCliCommand(msg);
312
+ return true;
313
+ }
273
314
  if (text === "/update" || text.startsWith("/update@")) {
274
315
  await this.handleUpdateCommand(msg);
275
316
  return true;
@@ -332,6 +373,13 @@ export class TopicCommands {
332
373
  const text = msg.text?.trim();
333
374
  if (!text)
334
375
  return false;
376
+ // Tips are informational and should appear where requested, including a
377
+ // worker topic. This also keeps Telegram text commands aligned with
378
+ // Discord's channel-local slash-command behavior.
379
+ if (text === "/tips" || text.startsWith("/tips ") || text.startsWith("/tips@")) {
380
+ await this.handleTipsCommand(msg);
381
+ return true;
382
+ }
335
383
  const pauseWake = parsePauseWakeCommand(text);
336
384
  if (pauseWake) {
337
385
  const adapter = this.getReplyAdapter(msg);
@@ -352,7 +400,7 @@ export class TopicCommands {
352
400
  return true;
353
401
  }
354
402
  if (pauseWake.action === "pause" && isGeneralInstance(this.ctx.fleetConfig, target)) {
355
- await adapter.sendText(msg.chatId, GENERAL_PAUSE_ERROR, { threadId: msg.threadId });
403
+ await adapter.sendText(msg.chatId, t("general.pause_forbidden"), { threadId: msg.threadId });
356
404
  return true;
357
405
  }
358
406
  await adapter.sendText(msg.chatId, await this.runPauseWake(target, pauseWake.action), { threadId: msg.threadId });
@@ -379,7 +427,7 @@ export class TopicCommands {
379
427
  const level = text.replace(/^\/effort(@\S+)?/, "").trim();
380
428
  if (level) {
381
429
  const reply = await this.ctx.applyEffort?.(instanceName, level)
382
- ?? "Effort switching is unavailable.";
430
+ ?? t("effort.unsupported", this.effectiveBackend(instanceName));
383
431
  await adapter.sendText(msg.chatId, reply, { threadId: msg.threadId });
384
432
  }
385
433
  else if (this.ctx.promptEffortMenu) {
@@ -410,7 +458,7 @@ export class TopicCommands {
410
458
  await adapter.sendText(msg.chatId, fallback, { threadId: msg.threadId });
411
459
  }
412
460
  else {
413
- await adapter.sendText(msg.chatId, "Usage: /model <name> — e.g. /model sonnet", { threadId: msg.threadId });
461
+ await adapter.sendText(msg.chatId, t("model.usage"), { threadId: msg.threadId });
414
462
  }
415
463
  return true;
416
464
  }
@@ -422,6 +470,39 @@ export class TopicCommands {
422
470
  await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
423
471
  return true;
424
472
  }
473
+ if (text === "/steer" || text.startsWith("/steer ") || text.startsWith("/steer@")) {
474
+ const adapter = this.getReplyAdapter(msg);
475
+ if (!adapter)
476
+ return false;
477
+ // Deliberately NOT admin-gated: anyone who can speak in this topic can
478
+ // already send the instance a message; /steer only changes WHEN it lands
479
+ // (mid-turn instead of after), and it goes through the full [user:]
480
+ // formatting — unlike /raw, which bypasses it and is gated.
481
+ const content = text.replace(/^\/steer(@\S+)?/, "").trim();
482
+ if (!content) {
483
+ await adapter.sendText(msg.chatId, t("steer.usage"), { threadId: msg.threadId });
484
+ return true;
485
+ }
486
+ const result = this.sendSteer(instanceName, content, msg);
487
+ await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
488
+ return true;
489
+ }
490
+ if (text === "/btw" || text.startsWith("/btw ") || text.startsWith("/btw@")) {
491
+ const adapter = this.getReplyAdapter(msg);
492
+ if (!adapter)
493
+ return false;
494
+ // Like /steer, /btw is not admin-gated: anyone who can send the agent a
495
+ // normal message may ask a side question. Claude owns the isolation from
496
+ // the active turn; AgEnD only submits its native command immediately.
497
+ const content = text.replace(/^\/btw(@\S+)?/, "").trim();
498
+ if (!content) {
499
+ await adapter.sendText(msg.chatId, t("btw.usage"), { threadId: msg.threadId });
500
+ return true;
501
+ }
502
+ const result = this.sendBtw(instanceName, content, msg);
503
+ await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
504
+ return true;
505
+ }
425
506
  if (text === "/clear" || text.startsWith("/clear@")) {
426
507
  const adapter = this.getReplyAdapter(msg);
427
508
  if (!adapter)
@@ -430,8 +511,13 @@ export class TopicCommands {
430
511
  await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
431
512
  return true;
432
513
  }
433
- const result = await this.sendClear(instanceName);
434
- await adapter.sendText(msg.chatId, result, { threadId: msg.threadId });
514
+ if (!this.ctx.promptClearConfirmation) {
515
+ await adapter.sendText(msg.chatId, t("clear.prompt_unavailable"), { threadId: msg.threadId });
516
+ return true;
517
+ }
518
+ const fallback = await this.ctx.promptClearConfirmation(instanceName, msg.threadId ?? msg.chatId, adapter, msg.chatId, msg.threadId);
519
+ if (fallback)
520
+ await adapter.sendText(msg.chatId, fallback, { threadId: msg.threadId });
435
521
  return true;
436
522
  }
437
523
  if (text === "/cancel" || text.startsWith("/cancel@")) {
@@ -470,7 +556,7 @@ export class TopicCommands {
470
556
  }
471
557
  async runPauseWake(instanceName, action) {
472
558
  if (action === "pause" && isGeneralInstance(this.ctx.fleetConfig, instanceName)) {
473
- return GENERAL_PAUSE_ERROR;
559
+ return t("general.pause_forbidden");
474
560
  }
475
561
  try {
476
562
  const result = await this.ctx.changeInstancePauseState(instanceName, action);
@@ -501,7 +587,10 @@ export class TopicCommands {
501
587
  const contextLine = context == null ? null : formatContextUsageLine(context, tokenRatio);
502
588
  // Effective model (resolves per-instance → fleet default → classic channel →
503
589
  // the CLI's own default) via the shared resolver, so /ctx and /model agree.
504
- const modelDisplay = this.ctx.modelDisplayForInstance?.(instanceName);
590
+ const modelDisplay = backend === "claude-code"
591
+ ? readStatuslineModel(this.ctx.dataDir, instanceName)
592
+ ?? this.ctx.modelDisplayForInstance?.(instanceName)
593
+ : this.ctx.modelDisplayForInstance?.(instanceName);
505
594
  const modelLine = modelDisplay ? `\n${t("ctx.model", modelDisplay)}` : "";
506
595
  return context != null
507
596
  ? `${contextLine}\n${t("ctx.backend", backend)}${modelLine}\n${t("ctx.instance", instanceName)}`
@@ -519,22 +608,92 @@ export class TopicCommands {
519
608
  ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
520
609
  const cmd = compactCommandForBackend(backend);
521
610
  ipc.send({ type: "raw_paste", content: cmd });
522
- return `🗜️ Compact command sent (\`${cmd}\`).`;
611
+ return t("compact.sent", cmd);
523
612
  }
524
- return "❌ Instance not connected (IPC unavailable)";
613
+ return t("compact.not_connected");
614
+ }
615
+ /** Whether the instance backend exposes a verified full-reset command. */
616
+ supportsClear(instanceName) {
617
+ return clearCommandForBackend(this.effectiveBackend(instanceName)) !== null;
618
+ }
619
+ /**
620
+ * Steer: interject a message into the instance's CURRENT turn. The daemon
621
+ * pastes it into the busy CLI instead of queueing for idle (falling back to
622
+ * the queue if the TUI swallows busy input — see Daemon.steerMessage).
623
+ */
624
+ /**
625
+ * Backends whose TUI accepts a busy-pane paste as steering input,
626
+ * live-verified: claude-code and codex buffer-then-submit at the turn
627
+ * boundary, grok accepts it in its input line. kiro's legacy TUI swallows
628
+ * the paste outright, and opencode/antigravity are unverified — for those
629
+ * the user gets an honest "not supported" instead of a silent queue
630
+ * fallback that looks like a steer but behaves like a normal message.
631
+ */
632
+ static STEER_SUPPORTED_BACKENDS = new Set(["claude-code", "codex", "grok", "mock"]);
633
+ sendSteer(instanceName, content, msg) {
634
+ const backend = this.effectiveBackend(instanceName);
635
+ if (!TopicCommands.STEER_SUPPORTED_BACKENDS.has(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);
525
680
  }
526
681
  /** Send the backend-appropriate full conversation reset through raw_paste. */
527
682
  async sendClear(instanceName) {
528
683
  const backend = this.effectiveBackend(instanceName);
529
684
  const cmd = clearCommandForBackend(backend);
530
685
  if (!cmd)
531
- return CLEAR_UNSUPPORTED_MSG;
686
+ return t("clear.unsupported");
532
687
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
533
688
  if (ipc?.connected) {
534
- ipc.send({ type: "raw_paste", content: cmd });
535
- return `🧹 Clear command sent (\`${cmd}\`).`;
689
+ ipc.send({
690
+ type: "raw_paste",
691
+ content: cmd,
692
+ ...(backend === "kiro-cli" ? { confirm_clear: true } : {}),
693
+ });
694
+ return t("clear.sent", cmd);
536
695
  }
537
- return "❌ Instance not connected (IPC unavailable)";
696
+ return t("clear.not_connected");
538
697
  }
539
698
  /** Send a backend-appropriate session-save command to a fleet-topic instance. */
540
699
  async sendSave(instanceName, filename) {
@@ -546,13 +705,13 @@ export class TopicCommands {
546
705
  ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
547
706
  const cmd = saveCommandForBackend(backend, filename);
548
707
  if (!cmd)
549
- return SAVE_UNSUPPORTED_MSG;
708
+ return t("save.unsupported");
550
709
  const ipc = this.ctx.instanceIpcClients.get(instanceName);
551
710
  if (ipc?.connected) {
552
711
  ipc.send({ type: "raw_paste", content: cmd });
553
- return `💾 Save command sent (\`${cmd}\`).`;
712
+ return t("save.sent", cmd, instanceName);
554
713
  }
555
- return "❌ Instance not connected (IPC unavailable)";
714
+ return t("save.not_connected");
556
715
  }
557
716
  async handleRestartCommand(msg) {
558
717
  const adapter = this.getReplyAdapter(msg);
@@ -569,8 +728,8 @@ export class TopicCommands {
569
728
  process.kill(process.pid, "SIGUSR2");
570
729
  }
571
730
  /**
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
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
574
733
  * can talk to the fleet may see how much headroom it has left. Vendor rate
575
734
  * limits are protected by the shared 5-minute cache, not by gating callers.
576
735
  */
@@ -581,7 +740,7 @@ export class TopicCommands {
581
740
  try {
582
741
  const { getUsageSnapshot } = await import("./usage/usage-api.js");
583
742
  const { renderUsageHtml, renderUsageMarkdown } = await import("./usage/format-rich.js");
584
- const payload = await getUsageSnapshot();
743
+ const payload = await getUsageSnapshot(false, this.ctx.getActiveUsageProviderIds?.());
585
744
  // Each platform gets its native rich format: Telegram needs parse_mode
586
745
  // HTML; Discord renders Markdown in plain content. Anything else falls back
587
746
  // to Markdown, which degrades to readable text.
@@ -593,7 +752,129 @@ export class TopicCommands {
593
752
  }
594
753
  }
595
754
  catch (err) {
596
- 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
+ if (!arg || /\s/.test(arg)) {
821
+ await adapter.sendText(msg.chatId, t("install.usage"), { threadId: msg.threadId });
822
+ return;
823
+ }
824
+ if (arg === "cancel") {
825
+ await adapter.sendText(msg.chatId, await this.ctx.cancelInstallSession(), { threadId: msg.threadId });
826
+ return;
827
+ }
828
+ const chat = {
829
+ adapter,
830
+ adapterId: msg.adapterId ?? adapter.id,
831
+ chatId: msg.chatId,
832
+ threadId: msg.threadId,
833
+ };
834
+ await adapter.sendText(msg.chatId, await this.ctx.startInstallSession(arg, chat), { threadId: msg.threadId });
835
+ }
836
+ async handleTipsCommand(msg) {
837
+ const adapter = this.getReplyAdapter(msg);
838
+ if (!adapter || !this.ctx.fleetConfig)
839
+ return;
840
+ const mode = msg.text.trim().replace(/^\/tips(?:@\S+)?/, "").trim().toLowerCase();
841
+ if (mode === "advanced on") {
842
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
843
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
844
+ return;
845
+ }
846
+ await adapter.sendText(msg.chatId, this.ctx.unlockAdvancedTips?.(msg.userId)
847
+ ? t("tips.advanced.unlocked")
848
+ : t("tips.unavailable"), { threadId: msg.threadId });
849
+ return;
850
+ }
851
+ if (mode === "on" || mode === "off") {
852
+ if (!this.ctx.isFleetAdmin(msg.userId, msg.adapterId)) {
853
+ await adapter.sendText(msg.chatId, t("permission.denied"), { threadId: msg.threadId });
854
+ return;
855
+ }
856
+ this.ctx.fleetConfig.defaults.tips = mode === "on";
857
+ this.ctx.saveFleetConfig();
858
+ await adapter.sendText(msg.chatId, t(mode === "on" ? "tips.enabled" : "tips.disabled"), {
859
+ threadId: msg.threadId,
860
+ });
861
+ return;
862
+ }
863
+ if (mode) {
864
+ await adapter.sendText(msg.chatId, t("tips.usage"), { threadId: msg.threadId });
865
+ return;
866
+ }
867
+ const result = await this.ctx.promptTip?.(
868
+ // The instance name scopes prompt cleanup; the Tip is posted at the
869
+ // caller's current chat/thread below.
870
+ Object.entries(this.ctx.fleetConfig.instances)
871
+ .find(([, config]) => config.general_topic === true
872
+ && (!config.channel_id || config.channel_id === msg.adapterId))?.[0] ?? "general", adapter, msg.chatId, msg.threadId) ?? "unavailable";
873
+ if (result === "empty") {
874
+ await adapter.sendText(msg.chatId, t("tips.empty"), { threadId: msg.threadId });
875
+ }
876
+ else if (result === "unavailable") {
877
+ await adapter.sendText(msg.chatId, t("tips.unavailable"), { threadId: msg.threadId });
597
878
  }
598
879
  }
599
880
  async handleStatusCommand(msg) {
@@ -622,7 +903,7 @@ export class TopicCommands {
622
903
  /** Get fleet status as markdown text (shared by TG + DC) */
623
904
  async getStatusText() {
624
905
  if (!this.ctx.fleetConfig)
625
- return "No fleet config loaded.";
906
+ return t("status.no_config");
626
907
  const rows = [];
627
908
  let pausedCount = 0;
628
909
  const fleetNames = new Set(Object.keys(this.ctx.fleetConfig.instances));
@@ -659,10 +940,10 @@ export class TopicCommands {
659
940
  icon = "🔴";
660
941
  else
661
942
  icon = "⚪";
662
- const stateLabel = executionState === "idle" ? "🟢 idle"
663
- : executionState === "working" ? "🔵 working"
664
- : executionState === "stuck" ? "🔴 stuck"
665
- : executionState === "paused" ? "⏸ paused"
943
+ const stateLabel = executionState === "idle" ? `🟢 ${t("state.idle")}`
944
+ : executionState === "working" ? `🔵 ${t("state.working")}`
945
+ : executionState === "stuck" ? `🔴 ${t("state.stuck")}`
946
+ : executionState === "paused" ? `⏸ ${t("state.paused")}`
666
947
  : "—";
667
948
  const displayName = this.shortInstanceName(name);
668
949
  // IPC reachability moved here from /sysinfo: it is per-instance health, and
@@ -674,27 +955,27 @@ export class TopicCommands {
674
955
  rows.push(`| ${displayName} | ${backend} | ${contextStr} | ${effort} | ${formatCents(costCents)} | ${icon} | ${stateLabel} | ${ipc} |`);
675
956
  }
676
957
  if (rows.length === 0)
677
- return "No instances configured.";
958
+ return t("status.no_instances");
678
959
  const lines = [
679
- "## Fleet Status",
960
+ `## ${t("status.title")}`,
680
961
  "",
681
- "| Instance | Backend | Ctx | Effort | Cost | Status | State | IPC |",
962
+ t("status.table_header"),
682
963
  "|----------|---------|-----|--------|------|--------|-------|-----|",
683
964
  ...rows,
684
965
  "",
685
- `Paused instances: ${pausedCount}`,
966
+ t("status.paused_count", pausedCount),
686
967
  ];
687
968
  const limitCents = this.ctx.costGuard?.getLimitCents() ?? 0;
688
969
  const totalCents = this.ctx.costGuard?.getFleetTotalCents() ?? 0;
689
970
  if (limitCents > 0) {
690
- lines.push("", `Fleet: ${formatCents(totalCents)} / ${formatCents(limitCents)} daily`);
971
+ lines.push("", t("status.daily_cost", formatCents(totalCents), formatCents(limitCents)));
691
972
  }
692
973
  // Adapter states (only show if any are not connected)
693
974
  const adapterStates = this.ctx.getAdapterStates?.();
694
975
  if (adapterStates && adapterStates.size > 0) {
695
976
  const issues = [...adapterStates.entries()].filter(([, s]) => s.status !== "connected");
696
977
  if (issues.length > 0) {
697
- lines.push("", "**Adapters:**");
978
+ lines.push("", `**${t("status.adapters")}**`);
698
979
  for (const [id, s] of adapterStates) {
699
980
  const icon = s.status === "connected" ? "✅" : s.status === "retrying" ? "🔄" : "❌";
700
981
  lines.push(` ${icon} ${id}: ${s.status}${s.lastError ? ` (${s.lastError.slice(0, 60)})` : ""}`);
@@ -725,17 +1006,17 @@ export class TopicCommands {
725
1006
  const require = createRequire(import.meta.url);
726
1007
  const agendVersion = require("../package.json").version ?? "unknown";
727
1008
  return [
728
- "## System Info",
1009
+ `## ${t("sysinfo.title")}`,
729
1010
  "",
730
- "| Metric | Value |",
1011
+ `| ${t("sysinfo.metric")} | ${t("sysinfo.value")} |`,
731
1012
  "|--------|-------|",
732
1013
  `| AgEnD | v${agendVersion} |`,
733
1014
  `| OS | ${process.platform} ${osRelease()} (${process.arch}) |`,
734
1015
  `| Node | ${process.version} |`,
735
1016
  `| 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 |`,
1017
+ `| ${t("sysinfo.uptime")} | ${upHours}h ${upMins}m |`,
1018
+ `| ${t("sysinfo.memory")} | ${info.memory_mb.rss} MB RSS |`,
1019
+ `| ${t("sysinfo.heap")} | ${info.memory_mb.heapUsed} / ${info.memory_mb.heapTotal} MB |`,
739
1020
  ].join("\n");
740
1021
  }
741
1022
  async handleUpdateCommand(msg) {
@@ -788,11 +1069,11 @@ export class TopicCommands {
788
1069
  timeout: 30_000,
789
1070
  encoding: "utf-8",
790
1071
  });
791
- output = stripAnsi(stdout) || "No output";
1072
+ output = stripAnsi(stdout) || t("doctor.no_output");
792
1073
  }
793
1074
  catch (err) {
794
1075
  const e = err;
795
- output = stripAnsi(e.stdout ?? e.message ?? "Doctor failed");
1076
+ output = stripAnsi(e.stdout ?? e.message ?? t("doctor.failed"));
796
1077
  }
797
1078
  await adapter.sendText(chatId, output, { threadId });
798
1079
  }
@@ -801,7 +1082,7 @@ export class TopicCommands {
801
1082
  const adapter = this.getReplyAdapter(msg);
802
1083
  if (!adapter)
803
1084
  return;
804
- await adapter.sendText(msg.chatId, "This topic is not bound to an instance. Ask the General assistant to create one with create_instance.", { threadId: msg.threadId });
1085
+ await adapter.sendText(msg.chatId, t("topic.unbound"), { threadId: msg.threadId });
805
1086
  }
806
1087
  /** Handle topic deletion — stop daemon and remove from config */
807
1088
  async handleTopicDeleted(threadId) {
@@ -914,21 +1195,26 @@ export class TopicCommands {
914
1195
  }
915
1196
  try {
916
1197
  const fleetCommands = [
917
- { command: "status", description: "🔒 Fleet status, per-instance costs and health" },
918
- { command: "sysinfo", description: "System diagnostics" },
919
- { command: "dashboard", description: "🔒 Get dashboard URLs" },
920
- { command: "ctx", description: "Show context usage" },
921
- { command: "compact", description: "Compact agent context" },
922
- { command: "clear", description: "🔒 Clear agent conversation context" },
923
- { command: "model", description: "🔒 Switch model (admin only)" },
924
- { command: "effort", description: "🔒 Set reasoning effort (admin only)" },
925
- { command: "pause", description: "🔒 Pause an idle instance" },
926
- { command: "wake", description: "🔒 Wake a paused instance" },
927
- { command: "restart", description: "🔒 Graceful restart all instances" },
928
- { command: "collab", description: "🔒 Toggle bot/webhook mode" },
929
- { command: "update", description: "🔒 Update AgEnD to latest" },
930
- { command: "doctor", description: "🔒 Run health diagnostics" },
931
- { command: "usage", description: "AI subscription usage" },
1198
+ { command: "status", description: "🔒 " + t("slash.status") },
1199
+ { command: "sysinfo", description: t("slash.sysinfo") },
1200
+ { command: "dashboard", description: "🔒 " + t("slash.dashboard") },
1201
+ { command: "ctx", description: t("slash.ctx") },
1202
+ { command: "compact", description: t("slash.compact") },
1203
+ { command: "steer", description: t("slash.steer") },
1204
+ { command: "btw", description: t("slash.btw") },
1205
+ { command: "clear", description: "🔒 " + t("slash.clear") },
1206
+ { command: "model", description: "🔒 " + t("slash.model") },
1207
+ { command: "effort", description: "🔒 " + t("slash.effort") },
1208
+ { command: "pause", description: "🔒 " + t("slash.pause") },
1209
+ { command: "wake", description: "🔒 " + t("slash.wake") },
1210
+ { command: "restart", description: "🔒 " + t("slash.restart") },
1211
+ { command: "collab", description: "🔒 " + t("slash.collab") },
1212
+ { command: "update", description: "🔒 " + t("slash.update") },
1213
+ { command: "doctor", description: "🔒 " + t("slash.doctor") },
1214
+ { command: "login", description: "🔒 " + t("slash.login") },
1215
+ { command: "install_cli", description: "🔒 " + t("slash.install_cli") },
1216
+ { command: "usage", description: t("slash.usage") },
1217
+ { command: "tips", description: t("slash.tips") },
932
1218
  ];
933
1219
  const setCommands = async (commands, scope) => {
934
1220
  const response = await fetch(`https://api.telegram.org/bot${botToken}/setMyCommands`, {
@@ -949,15 +1235,17 @@ export class TopicCommands {
949
1235
  }
950
1236
  };
951
1237
  const classicCommands = [
952
- { command: "start", description: "🔒 Start an agent in this chat" },
953
- { command: "stop", description: "🔒 Stop the agent" },
954
- { command: "compact", description: "🔒 Compact agent context" },
955
- { command: "clear", description: "🔒 Clear agent conversation context" },
956
- { command: "model", description: "🔒 Switch model (admin only)" },
957
- { command: "effort", description: "🔒 Set reasoning effort (admin only)" },
958
- { command: "pause", description: "🔒 Pause the agent" },
959
- { command: "wake", description: "🔒 Wake the agent" },
960
- { command: "ctx", description: "Show context usage" },
1238
+ { command: "start", description: "🔒 " + t("slash.start") },
1239
+ { command: "stop", description: "🔒 " + t("slash.stop") },
1240
+ { command: "compact", description: "🔒 " + t("slash.compact") },
1241
+ { command: "steer", description: t("slash.steer") },
1242
+ { command: "btw", description: t("slash.btw") },
1243
+ { command: "clear", description: "🔒 " + t("slash.clear") },
1244
+ { command: "model", description: "🔒 " + t("slash.model") },
1245
+ { command: "effort", description: "🔒 " + t("slash.effort") },
1246
+ { command: "pause", description: "🔒 " + t("slash.pause") },
1247
+ { command: "wake", description: "🔒 " + t("slash.wake") },
1248
+ { command: "ctx", description: t("slash.ctx") },
961
1249
  ];
962
1250
  // A chat_administrators scope has higher precedence than the chat scope.
963
1251
  // Keep both synchronized so a stale admin-only list from BotFather or an