@songsid/agend 2.1.2-beta.15 → 2.1.2-beta.16

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 (122) hide show
  1. package/dist/access-path.js +15 -6
  2. package/dist/access-path.js.map +1 -1
  3. package/dist/backend/claude-code.d.ts +37 -0
  4. package/dist/backend/claude-code.js +43 -1
  5. package/dist/backend/claude-code.js.map +1 -1
  6. package/dist/backend/codex.js +22 -0
  7. package/dist/backend/codex.js.map +1 -1
  8. package/dist/backend/grok.d.ts +23 -0
  9. package/dist/backend/grok.js +25 -0
  10. package/dist/backend/grok.js.map +1 -1
  11. package/dist/backend/kiro.d.ts +20 -0
  12. package/dist/backend/kiro.js +59 -0
  13. package/dist/backend/kiro.js.map +1 -1
  14. package/dist/backend/types.d.ts +27 -0
  15. package/dist/backend/types.js +1 -1
  16. package/dist/backend/types.js.map +1 -1
  17. package/dist/channel/adapters/discord.d.ts +15 -0
  18. package/dist/channel/adapters/discord.js +90 -1
  19. package/dist/channel/adapters/discord.js.map +1 -1
  20. package/dist/channel/adapters/telegram.d.ts +21 -0
  21. package/dist/channel/adapters/telegram.js +82 -0
  22. package/dist/channel/adapters/telegram.js.map +1 -1
  23. package/dist/channel/ipc-bridge.d.ts +9 -1
  24. package/dist/channel/ipc-bridge.js +12 -3
  25. package/dist/channel/ipc-bridge.js.map +1 -1
  26. package/dist/channel/ipc-timeouts.d.ts +40 -0
  27. package/dist/channel/ipc-timeouts.js +58 -0
  28. package/dist/channel/ipc-timeouts.js.map +1 -0
  29. package/dist/channel/mcp-server.js +25 -14
  30. package/dist/channel/mcp-server.js.map +1 -1
  31. package/dist/channel/mcp-tools.js +4 -2
  32. package/dist/channel/mcp-tools.js.map +1 -1
  33. package/dist/channel/reconnect-backoff.d.ts +17 -0
  34. package/dist/channel/reconnect-backoff.js +21 -0
  35. package/dist/channel/reconnect-backoff.js.map +1 -0
  36. package/dist/channel/types.d.ts +29 -0
  37. package/dist/classic-channel-manager.js +1 -5
  38. package/dist/classic-channel-manager.js.map +1 -1
  39. package/dist/cli.js +129 -50
  40. package/dist/cli.js.map +1 -1
  41. package/dist/completion.d.ts +27 -0
  42. package/dist/completion.js +121 -0
  43. package/dist/completion.js.map +1 -0
  44. package/dist/config-validator.js +9 -0
  45. package/dist/config-validator.js.map +1 -1
  46. package/dist/config.js +1 -0
  47. package/dist/config.js.map +1 -1
  48. package/dist/cost-guard.d.ts +3 -1
  49. package/dist/cost-guard.js +3 -1
  50. package/dist/cost-guard.js.map +1 -1
  51. package/dist/daemon.d.ts +164 -5
  52. package/dist/daemon.js +711 -290
  53. package/dist/daemon.js.map +1 -1
  54. package/dist/event-log.d.ts +31 -0
  55. package/dist/event-log.js +96 -0
  56. package/dist/event-log.js.map +1 -1
  57. package/dist/fleet-manager.d.ts +188 -1
  58. package/dist/fleet-manager.js +697 -61
  59. package/dist/fleet-manager.js.map +1 -1
  60. package/dist/general-knowledge/skills/model-discovery/SKILL.md +1 -1
  61. package/dist/hang-detector.d.ts +19 -13
  62. package/dist/hang-detector.js +19 -49
  63. package/dist/hang-detector.js.map +1 -1
  64. package/dist/instance-lifecycle.js +25 -0
  65. package/dist/instance-lifecycle.js.map +1 -1
  66. package/dist/locale.js +1 -0
  67. package/dist/locale.js.map +1 -1
  68. package/dist/mcp-liveness.d.ts +21 -0
  69. package/dist/mcp-liveness.js +27 -0
  70. package/dist/mcp-liveness.js.map +1 -0
  71. package/dist/outbound-handlers.d.ts +7 -0
  72. package/dist/outbound-handlers.js +79 -5
  73. package/dist/outbound-handlers.js.map +1 -1
  74. package/dist/outbound-schemas.d.ts +7 -2
  75. package/dist/outbound-schemas.js +13 -2
  76. package/dist/outbound-schemas.js.map +1 -1
  77. package/dist/pane-write-lock.d.ts +48 -0
  78. package/dist/pane-write-lock.js +73 -0
  79. package/dist/pane-write-lock.js.map +1 -0
  80. package/dist/process-memory.d.ts +31 -0
  81. package/dist/process-memory.js +79 -0
  82. package/dist/process-memory.js.map +1 -0
  83. package/dist/quickstart.js +17 -16
  84. package/dist/quickstart.js.map +1 -1
  85. package/dist/scheduler/db.js +3 -0
  86. package/dist/scheduler/db.js.map +1 -1
  87. package/dist/sd-notify.d.ts +27 -0
  88. package/dist/sd-notify.js +33 -1
  89. package/dist/sd-notify.js.map +1 -1
  90. package/dist/secret-file.d.ts +33 -0
  91. package/dist/secret-file.js +36 -0
  92. package/dist/secret-file.js.map +1 -0
  93. package/dist/setup-wizard.js +9 -7
  94. package/dist/setup-wizard.js.map +1 -1
  95. package/dist/tmux-control.d.ts +58 -5
  96. package/dist/tmux-control.js +102 -14
  97. package/dist/tmux-control.js.map +1 -1
  98. package/dist/tmux-manager.d.ts +31 -1
  99. package/dist/tmux-manager.js +40 -11
  100. package/dist/tmux-manager.js.map +1 -1
  101. package/dist/topic-commands.d.ts +22 -2
  102. package/dist/topic-commands.js +102 -19
  103. package/dist/topic-commands.js.map +1 -1
  104. package/dist/types.d.ts +6 -0
  105. package/dist/ui/view.html +123 -2
  106. package/dist/usage/providers.d.ts +50 -0
  107. package/dist/usage/providers.js +611 -0
  108. package/dist/usage/providers.js.map +1 -0
  109. package/dist/usage/usage-api.d.ts +50 -0
  110. package/dist/usage/usage-api.js +111 -0
  111. package/dist/usage/usage-api.js.map +1 -0
  112. package/dist/view-api.js +1 -0
  113. package/dist/view-api.js.map +1 -1
  114. package/dist/web-api.js +5 -2
  115. package/dist/web-api.js.map +1 -1
  116. package/package.json +4 -1
  117. package/dist/channel/tool-tracker.d.ts +0 -13
  118. package/dist/channel/tool-tracker.js +0 -58
  119. package/dist/channel/tool-tracker.js.map +0 -1
  120. package/dist/daemon-entry.d.ts +0 -1
  121. package/dist/daemon-entry.js +0 -30
  122. package/dist/daemon-entry.js.map +0 -1
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ import { getAgendHome, getTmuxSocketName } from "./paths.js";
14
14
  import { readClassicLastActivityAt } from "./classic-channel-manager.js";
15
15
  import { hasPausedMarker } from "./pause-marker.js";
16
16
  import { PIE_PERCENT_RE } from "./tui-glyphs.js";
17
+ import { COMPLETION_SHELLS, completionScript } from "./completion.js";
17
18
  import { getUpdateSelector, lookupTargetVersion, reportUpdateRestart, shouldSkipUpdate, } from "./update-check.js";
18
19
  /** Prefix tmux args with -L when socket isolation is active. */
19
20
  function tmuxArgs(args) {
@@ -68,8 +69,14 @@ fleet
68
69
  const fleet = loadFleetConfig(FLEET_CONFIG_PATH);
69
70
  const port = fleet.health_port ?? 19280;
70
71
  try {
71
- const healthResp = await fetch(`http://127.0.0.1:${port}/health`);
72
- if (healthResp.ok) {
72
+ // Any HTTP answer means a fleet daemon is listening — including a 503,
73
+ // which /health now returns when the fleet is degraded (no adapter
74
+ // connected, an instance crashed). Checking `.ok` here would treat a
75
+ // degraded fleet as "not running" and start a SECOND fleet on the same
76
+ // data dir, whose startup cleanup kills the first one's tmux windows.
77
+ // Only a connection error means nothing is there (the catch below).
78
+ await fetch(`http://127.0.0.1:${port}/health`);
79
+ {
73
80
  try {
74
81
  const resp = await fetch(`http://127.0.0.1:${port}/api/instance/${encodeURIComponent(instance)}/start`, { method: "POST" });
75
82
  const body = await resp.json();
@@ -92,6 +99,59 @@ fleet
92
99
  }
93
100
  const { FleetManager } = await import("./fleet-manager.js");
94
101
  const fm = new FleetManager(DATA_DIR);
102
+ // Register crash/signal handlers BEFORE startAll(). Startup routinely takes
103
+ // minutes (sequential general spawn, staggered instances, warmup waits), and
104
+ // without these a Ctrl-C or `systemctl stop` in that window got Node's default
105
+ // behaviour: instant death with fleet.pid already written, databases unclosed,
106
+ // and every tmux window orphaned.
107
+ let stopping = false;
108
+ const shutdown = async () => {
109
+ if (stopping)
110
+ return; // SIGINT and SIGTERM share this, and crash paths call stopAll too
111
+ stopping = true;
112
+ console.log("\nStopping fleet...");
113
+ await fm.stopAll().catch(err => console.error("Shutdown error:", err));
114
+ process.exit(0);
115
+ };
116
+ process.on("SIGINT", shutdown);
117
+ process.on("SIGTERM", shutdown);
118
+ process.on("uncaughtException", (err) => {
119
+ // An uncaught exception leaves unknown state — still the one case worth
120
+ // tearing down for.
121
+ console.error("Uncaught exception:", err);
122
+ if (stopping)
123
+ return;
124
+ stopping = true;
125
+ fm.stopAll().catch(() => { }).finally(() => process.exit(1));
126
+ });
127
+ process.on("unhandledRejection", (err) => {
128
+ const msg = err instanceof Error ? err.message : String(err);
129
+ // 409 = another bot poller exists — the adapter retries, don't crash.
130
+ if (msg.includes("409") && msg.includes("getUpdates")) {
131
+ console.error("Bot polling conflict (409) — retrying...");
132
+ return;
133
+ }
134
+ // Do NOT take the fleet down. A rejected promise in one instance's tmux
135
+ // call, health tick, or model switch used to stop every other working
136
+ // instance and exit(1) — the widest blast radius in the process. The
137
+ // per-instance health check and crash recovery handle a genuinely broken
138
+ // instance on their own.
139
+ //
140
+ // Surviving quietly would be its own failure mode, so this reports to the
141
+ // General topic as well as the log: the operator must not have to read
142
+ // daemon.log to learn the fleet swallowed a fault. notifyFleetError()
143
+ // throttles repeats, since these usually arrive from a loop.
144
+ console.error("Unhandled rejection (fleet continues):", err);
145
+ if (err instanceof Error && err.stack)
146
+ console.error(err.stack);
147
+ try {
148
+ fm.notifyFleetError(`⚠️ Fleet caught an unhandled error and kept running:\n\`${msg}\`\nSee daemon.log for the stack trace.`);
149
+ }
150
+ catch (notifyErr) {
151
+ // Never let the reporting path itself become an unhandled rejection.
152
+ console.error("Failed to report unhandled rejection:", notifyErr);
153
+ }
154
+ });
95
155
  if (instance) {
96
156
  const config = fm.loadConfig(FLEET_CONFIG_PATH);
97
157
  const inst = config.instances[instance];
@@ -109,30 +169,6 @@ fleet
109
169
  await fm.startAll(FLEET_CONFIG_PATH);
110
170
  }
111
171
  console.log("Fleet started");
112
- // Keep process alive + clean shutdown on Ctrl+C
113
- const shutdown = async () => {
114
- console.log("\nStopping fleet...");
115
- await fm.stopAll();
116
- process.exit(0);
117
- };
118
- process.on("SIGINT", shutdown);
119
- process.on("SIGTERM", shutdown);
120
- process.on("uncaughtException", async (err) => {
121
- console.error("Uncaught exception:", err);
122
- await fm.stopAll().catch(() => { });
123
- process.exit(1);
124
- });
125
- process.on("unhandledRejection", async (err) => {
126
- const msg = err instanceof Error ? err.message : String(err);
127
- // 409 = another bot poller exists — adapter handles retry, don't crash
128
- if (msg.includes("409") && msg.includes("getUpdates")) {
129
- console.error("Bot polling conflict (409) — retrying...");
130
- return;
131
- }
132
- console.error("Unhandled rejection:", err);
133
- await fm.stopAll().catch(() => { });
134
- process.exit(1);
135
- });
136
172
  });
137
173
  fleet
138
174
  .command("stop")
@@ -1835,28 +1871,7 @@ async function fuzzyMatch(query, names) {
1835
1871
  return null;
1836
1872
  }
1837
1873
  async function resolveInstance(query, config) {
1838
- const names = Object.keys(config.instances);
1839
- // Include classic instances from classicBot.yaml
1840
- try {
1841
- const classicPath = join(DATA_DIR, "classicBot.yaml");
1842
- if (existsSync(classicPath)) {
1843
- const yamlMod = (await import("js-yaml")).default;
1844
- const classic = yamlMod.load(readFileSync(classicPath, "utf-8"));
1845
- if (classic?.channels) {
1846
- for (const [key, val] of Object.entries(classic.channels)) {
1847
- // New format persists instanceName; old format keyed by channelId.
1848
- if (val.instanceName) {
1849
- names.push(val.instanceName);
1850
- continue;
1851
- }
1852
- const channelId = val.channelId ?? key;
1853
- const chName = (val.name ?? channelId).toLowerCase().replace(/[^\p{L}\d-]/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "project";
1854
- names.push(`classic-${chName}-${channelId.slice(-4)}`);
1855
- }
1856
- }
1857
- }
1858
- }
1859
- catch { /* ignore */ }
1874
+ const names = await listInstanceNames(config);
1860
1875
  const match = await fuzzyMatch(query, names);
1861
1876
  if (!match) {
1862
1877
  console.error(`No instance matching "${query}". Available: ${names.join(", ")}`);
@@ -1989,10 +2004,48 @@ const defaultParser = (output) => {
1989
2004
  };
1990
2005
  /** Optional backend-specific overrides; none needed — defaultParser covers all. */
1991
2006
  const contextParsers = {};
2007
+ /**
2008
+ * Every instance name the CLI accepts: fleet.yaml instances plus the
2009
+ * dynamically-created ClassicBot ones. Shared by `resolveInstance` (fuzzy
2010
+ * matching for `attach`) and `ls --names-only` (shell completion), so the
2011
+ * candidates offered by tab-completion are exactly the ones `attach` resolves.
2012
+ */
2013
+ async function listInstanceNames(config) {
2014
+ const names = Object.keys(config.instances);
2015
+ try {
2016
+ const classicPath = join(DATA_DIR, "classicBot.yaml");
2017
+ if (existsSync(classicPath)) {
2018
+ const yamlMod = (await import("js-yaml")).default;
2019
+ const classic = yamlMod.load(readFileSync(classicPath, "utf-8"));
2020
+ if (classic?.channels) {
2021
+ for (const [key, val] of Object.entries(classic.channels)) {
2022
+ // New format persists instanceName; old format keyed by channelId.
2023
+ if (val.instanceName) {
2024
+ names.push(val.instanceName);
2025
+ continue;
2026
+ }
2027
+ const channelId = val.channelId ?? key;
2028
+ const chName = (val.name ?? channelId).toLowerCase().replace(/[^\p{L}\d-]/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "project";
2029
+ names.push(`classic-${chName}-${channelId.slice(-4)}`);
2030
+ }
2031
+ }
2032
+ }
2033
+ }
2034
+ catch { /* ignore */ }
2035
+ return names;
2036
+ }
1992
2037
  async function lsAction(opts) {
1993
2038
  const yaml = (await import("js-yaml")).default;
1994
2039
  const config = yaml.load(readFileSync(FLEET_CONFIG_PATH, "utf-8"));
1995
2040
  const names = Object.keys(config.instances);
2041
+ // --names-only feeds shell completion, so it must return before any tmux or
2042
+ // status work (a tab press should not wait on the fleet) and must print
2043
+ // nothing but names — a "no instances" notice would become a candidate.
2044
+ if (opts.namesOnly) {
2045
+ for (const n of await listInstanceNames(config))
2046
+ console.log(n);
2047
+ return;
2048
+ }
1996
2049
  // Load classic channels from classicBot.yaml (keyed by channelId)
1997
2050
  const classicPath = join(DATA_DIR, "classicBot.yaml");
1998
2051
  let classicConfig = null;
@@ -2278,12 +2331,38 @@ program
2278
2331
  .command("ls")
2279
2332
  .description("List all instances with status, backend, team, and last activity")
2280
2333
  .option("--json", "Output as JSON")
2334
+ .option("--names-only", "Print instance names one per line (for shell completion)")
2281
2335
  .action(async (opts) => {
2282
2336
  await lsAction(opts);
2283
2337
  });
2284
2338
  program
2285
- .command("health")
2286
- .description("Fleet health check shows problems and diagnostics")
2339
+ .command("completion")
2340
+ .description("Print a shell completion script (bash or zsh)")
2341
+ .argument("<shell>", `Shell to generate for (${COMPLETION_SHELLS.join(", ")})`)
2342
+ .addHelpText("after", `
2343
+ Install:
2344
+ bash echo 'eval "$(agend completion bash)"' >> ~/.bashrc
2345
+ zsh echo 'eval "$(agend completion zsh)"' >> ~/.zshrc
2346
+
2347
+ Then reload the shell. Tab-completes instance names for \`agend attach\` and
2348
+ \`agend fleet start|stop|restart\`, and subcommands elsewhere.`)
2349
+ .action((shell) => {
2350
+ if (!COMPLETION_SHELLS.includes(shell)) {
2351
+ console.error(`Unsupported shell: ${shell}. Supported: ${COMPLETION_SHELLS.join(", ")}`);
2352
+ process.exit(2);
2353
+ }
2354
+ // Subcommand names come from commander itself, so the script can't drift
2355
+ // out of sync with the CLI the way a hardcoded list would.
2356
+ const fleetCmd = program.commands.find(c => c.name() === "fleet");
2357
+ console.log(completionScript(shell, {
2358
+ topLevel: program.commands.map(c => c.name()),
2359
+ fleetSub: fleetCmd?.commands.map(c => c.name()) ?? [],
2360
+ instanceCommands: ["attach"],
2361
+ fleetInstanceCommands: ["start", "stop", "restart"],
2362
+ }));
2363
+ });
2364
+ program
2365
+ .command("health").description("Fleet health check — shows problems and diagnostics")
2287
2366
  .option("--json", "Output as JSON")
2288
2367
  .option("-q, --quiet", "One-line summary only")
2289
2368
  .action(async (opts) => {