@songsid/agend 2.1.2-beta.8 → 2.1.2
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.
- package/dist/access-path.js +15 -6
- package/dist/access-path.js.map +1 -1
- package/dist/agent-cli-instructions.md +1 -0
- package/dist/agent-cli.js +14 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +25 -1
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/backend/antigravity.d.ts +20 -0
- package/dist/backend/antigravity.js +51 -4
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +59 -0
- package/dist/backend/claude-code.js +67 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +26 -0
- package/dist/backend/codex.js +100 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.d.ts +25 -0
- package/dist/backend/grok.js +51 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +23 -0
- package/dist/backend/kiro.js +120 -2
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +58 -0
- package/dist/backend/types.js +13 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +15 -0
- package/dist/channel/adapters/discord.js +120 -8
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +21 -0
- package/dist/channel/adapters/telegram.js +86 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/ipc-bridge.d.ts +9 -1
- package/dist/channel/ipc-bridge.js +12 -3
- package/dist/channel/ipc-bridge.js.map +1 -1
- package/dist/channel/ipc-timeouts.d.ts +46 -0
- package/dist/channel/ipc-timeouts.js +65 -0
- package/dist/channel/ipc-timeouts.js.map +1 -0
- package/dist/channel/mcp-server.js +25 -14
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.js +30 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/reconnect-backoff.d.ts +17 -0
- package/dist/channel/reconnect-backoff.js +21 -0
- package/dist/channel/reconnect-backoff.js.map +1 -0
- package/dist/channel/types.d.ts +29 -0
- package/dist/classic-channel-manager.js +1 -5
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +242 -91
- package/dist/cli.js.map +1 -1
- package/dist/completion.d.ts +27 -0
- package/dist/completion.js +121 -0
- package/dist/completion.js.map +1 -0
- package/dist/config-validator.js +21 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/cost-guard.d.ts +3 -1
- package/dist/cost-guard.js +3 -1
- package/dist/cost-guard.js.map +1 -1
- package/dist/daemon.d.ts +299 -7
- package/dist/daemon.js +1152 -311
- package/dist/daemon.js.map +1 -1
- package/dist/event-log.d.ts +31 -0
- package/dist/event-log.js +96 -0
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-context.d.ts +14 -0
- package/dist/fleet-lock.d.ts +28 -0
- package/dist/fleet-lock.js +130 -0
- package/dist/fleet-lock.js.map +1 -0
- package/dist/fleet-manager.d.ts +348 -6
- package/dist/fleet-manager.js +1860 -262
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-instance.d.ts +9 -0
- package/dist/general-instance.js +11 -0
- package/dist/general-instance.js.map +1 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +22 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +14 -23
- package/dist/general-knowledge/skills/session-management/SKILL.md +15 -34
- package/dist/hang-detector.d.ts +19 -13
- package/dist/hang-detector.js +19 -49
- package/dist/hang-detector.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +58 -2
- package/dist/instance-lifecycle.js +231 -46
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +7 -1
- package/dist/locale.js.map +1 -1
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -1
- package/dist/mcp-liveness.d.ts +21 -0
- package/dist/mcp-liveness.js +27 -0
- package/dist/mcp-liveness.js.map +1 -0
- package/dist/outbound-handlers.d.ts +9 -0
- package/dist/outbound-handlers.js +194 -31
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +11 -3
- package/dist/outbound-schemas.js +16 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-write-lock.d.ts +48 -0
- package/dist/pane-write-lock.js +73 -0
- package/dist/pane-write-lock.js.map +1 -0
- package/dist/process-memory.d.ts +31 -0
- package/dist/process-memory.js +79 -0
- package/dist/process-memory.js.map +1 -0
- package/dist/quickstart.js +17 -16
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-dedup.d.ts +41 -0
- package/dist/reply-dedup.js +0 -0
- package/dist/reply-dedup.js.map +1 -0
- package/dist/restart-progress.d.ts +41 -0
- package/dist/restart-progress.js +97 -0
- package/dist/restart-progress.js.map +1 -0
- package/dist/scheduler/db.js +3 -0
- package/dist/scheduler/db.js.map +1 -1
- package/dist/sd-notify.d.ts +27 -0
- package/dist/sd-notify.js +33 -1
- package/dist/sd-notify.js.map +1 -1
- package/dist/secret-file.d.ts +33 -0
- package/dist/secret-file.js +36 -0
- package/dist/secret-file.js.map +1 -0
- package/dist/service-installer.d.ts +28 -0
- package/dist/service-installer.js +72 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +5 -3
- package/dist/settings-api.js.map +1 -1
- package/dist/setup-wizard.js +9 -7
- package/dist/setup-wizard.js.map +1 -1
- package/dist/tmux-control.d.ts +58 -5
- package/dist/tmux-control.js +102 -14
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +31 -1
- package/dist/tmux-manager.js +40 -11
- package/dist/tmux-manager.js.map +1 -1
- package/dist/topic-commands.d.ts +49 -9
- package/dist/topic-commands.js +270 -109
- package/dist/topic-commands.js.map +1 -1
- package/dist/tui-glyphs.d.ts +5 -1
- package/dist/tui-glyphs.js +6 -2
- package/dist/tui-glyphs.js.map +1 -1
- package/dist/types.d.ts +19 -2
- package/dist/ui/settings.html +1 -1
- package/dist/ui/view.html +123 -2
- package/dist/update-marker.d.ts +15 -0
- package/dist/update-marker.js +69 -0
- package/dist/update-marker.js.map +1 -0
- package/dist/usage/format-rich.d.ts +11 -0
- package/dist/usage/format-rich.js +121 -0
- package/dist/usage/format-rich.js.map +1 -0
- package/dist/usage/provider-alerts.d.ts +33 -0
- package/dist/usage/provider-alerts.js +57 -0
- package/dist/usage/provider-alerts.js.map +1 -0
- package/dist/usage/providers.d.ts +130 -0
- package/dist/usage/providers.js +1063 -0
- package/dist/usage/providers.js.map +1 -0
- package/dist/usage/statusline-usage.d.ts +20 -0
- package/dist/usage/statusline-usage.js +120 -0
- package/dist/usage/statusline-usage.js.map +1 -0
- package/dist/usage/usage-api.d.ts +50 -0
- package/dist/usage/usage-api.js +197 -0
- package/dist/usage/usage-api.js.map +1 -0
- package/dist/view-api.js +8 -3
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.js +7 -3
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +6 -0
- package/dist/web-auth.js +54 -0
- package/dist/web-auth.js.map +1 -0
- package/package.json +4 -1
- package/dist/channel/tool-tracker.d.ts +0 -13
- package/dist/channel/tool-tracker.js +0 -58
- package/dist/channel/tool-tracker.js.map +0 -1
- package/dist/daemon-entry.d.ts +0 -1
- package/dist/daemon-entry.js +0 -30
- package/dist/daemon-entry.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,11 @@ 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";
|
|
19
|
+
import { clearUpdateMarker, markUpdateInProgress } from "./update-marker.js";
|
|
20
|
+
import { acquireFleetLock, releaseProcessFleetLock, setProcessFleetLock } from "./fleet-lock.js";
|
|
21
|
+
import { SYSTEMD_RESTART_TIMEOUT_MS } from "./service-installer.js";
|
|
18
22
|
/** Prefix tmux args with -L when socket isolation is active. */
|
|
19
23
|
function tmuxArgs(args) {
|
|
20
24
|
const socket = getTmuxSocketName();
|
|
@@ -25,6 +29,18 @@ const __dirname = dirname(__filename);
|
|
|
25
29
|
const DATA_DIR = getAgendHome();
|
|
26
30
|
const FLEET_CONFIG_PATH = join(DATA_DIR, "fleet.yaml");
|
|
27
31
|
const program = new Command();
|
|
32
|
+
function claimFleetSingleton() {
|
|
33
|
+
try {
|
|
34
|
+
setProcessFleetLock(acquireFleetLock(DATA_DIR));
|
|
35
|
+
process.once("exit", () => { releaseProcessFleetLock(); });
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(err.message);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
28
44
|
// Read version from package.json at build time
|
|
29
45
|
const pkgVersion = (() => {
|
|
30
46
|
try {
|
|
@@ -68,8 +84,14 @@ fleet
|
|
|
68
84
|
const fleet = loadFleetConfig(FLEET_CONFIG_PATH);
|
|
69
85
|
const port = fleet.health_port ?? 19280;
|
|
70
86
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
// Any HTTP answer means a fleet daemon is listening — including a 503,
|
|
88
|
+
// which /health now returns when the fleet is degraded (no adapter
|
|
89
|
+
// connected, an instance crashed). Checking `.ok` here would treat a
|
|
90
|
+
// degraded fleet as "not running" and start a SECOND fleet on the same
|
|
91
|
+
// data dir, whose startup cleanup kills the first one's tmux windows.
|
|
92
|
+
// Only a connection error means nothing is there (the catch below).
|
|
93
|
+
await fetch(`http://127.0.0.1:${port}/health`);
|
|
94
|
+
{
|
|
73
95
|
try {
|
|
74
96
|
const resp = await fetch(`http://127.0.0.1:${port}/api/instance/${encodeURIComponent(instance)}/start`, { method: "POST" });
|
|
75
97
|
const body = await resp.json();
|
|
@@ -90,8 +112,65 @@ fleet
|
|
|
90
112
|
}
|
|
91
113
|
catch { /* fleet not running, fall through */ }
|
|
92
114
|
}
|
|
115
|
+
// Claim the singleton before constructing FleetManager: its constructor and
|
|
116
|
+
// startAll path touch shared tmux, sockets, state files and adapters.
|
|
117
|
+
if (!claimFleetSingleton())
|
|
118
|
+
return;
|
|
93
119
|
const { FleetManager } = await import("./fleet-manager.js");
|
|
94
120
|
const fm = new FleetManager(DATA_DIR);
|
|
121
|
+
// Register crash/signal handlers BEFORE startAll(). Startup routinely takes
|
|
122
|
+
// minutes (sequential general spawn, staggered instances, warmup waits), and
|
|
123
|
+
// without these a Ctrl-C or `systemctl stop` in that window got Node's default
|
|
124
|
+
// behaviour: instant death with fleet.pid already written, databases unclosed,
|
|
125
|
+
// and every tmux window orphaned.
|
|
126
|
+
let stopping = false;
|
|
127
|
+
const shutdown = async () => {
|
|
128
|
+
if (stopping)
|
|
129
|
+
return; // SIGINT and SIGTERM share this, and crash paths call stopAll too
|
|
130
|
+
stopping = true;
|
|
131
|
+
console.log("\nStopping fleet...");
|
|
132
|
+
await fm.stopAll().catch(err => console.error("Shutdown error:", err));
|
|
133
|
+
process.exit(0);
|
|
134
|
+
};
|
|
135
|
+
process.on("SIGINT", shutdown);
|
|
136
|
+
process.on("SIGTERM", shutdown);
|
|
137
|
+
process.on("uncaughtException", (err) => {
|
|
138
|
+
// An uncaught exception leaves unknown state — still the one case worth
|
|
139
|
+
// tearing down for.
|
|
140
|
+
console.error("Uncaught exception:", err);
|
|
141
|
+
if (stopping)
|
|
142
|
+
return;
|
|
143
|
+
stopping = true;
|
|
144
|
+
fm.stopAll().catch(() => { }).finally(() => process.exit(1));
|
|
145
|
+
});
|
|
146
|
+
process.on("unhandledRejection", (err) => {
|
|
147
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
148
|
+
// 409 = another bot poller exists — the adapter retries, don't crash.
|
|
149
|
+
if (msg.includes("409") && msg.includes("getUpdates")) {
|
|
150
|
+
console.error("Bot polling conflict (409) — retrying...");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Do NOT take the fleet down. A rejected promise in one instance's tmux
|
|
154
|
+
// call, health tick, or model switch used to stop every other working
|
|
155
|
+
// instance and exit(1) — the widest blast radius in the process. The
|
|
156
|
+
// per-instance health check and crash recovery handle a genuinely broken
|
|
157
|
+
// instance on their own.
|
|
158
|
+
//
|
|
159
|
+
// Surviving quietly would be its own failure mode, so this reports to the
|
|
160
|
+
// General topic as well as the log: the operator must not have to read
|
|
161
|
+
// daemon.log to learn the fleet swallowed a fault. notifyFleetError()
|
|
162
|
+
// throttles repeats, since these usually arrive from a loop.
|
|
163
|
+
console.error("Unhandled rejection (fleet continues):", err);
|
|
164
|
+
if (err instanceof Error && err.stack)
|
|
165
|
+
console.error(err.stack);
|
|
166
|
+
try {
|
|
167
|
+
fm.notifyFleetError(`⚠️ Fleet caught an unhandled error and kept running:\n\`${msg}\`\nSee daemon.log for the stack trace.`);
|
|
168
|
+
}
|
|
169
|
+
catch (notifyErr) {
|
|
170
|
+
// Never let the reporting path itself become an unhandled rejection.
|
|
171
|
+
console.error("Failed to report unhandled rejection:", notifyErr);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
95
174
|
if (instance) {
|
|
96
175
|
const config = fm.loadConfig(FLEET_CONFIG_PATH);
|
|
97
176
|
const inst = config.instances[instance];
|
|
@@ -100,7 +179,10 @@ fleet
|
|
|
100
179
|
process.exit(1);
|
|
101
180
|
}
|
|
102
181
|
const topicMode = config.channel?.mode === "topic";
|
|
103
|
-
|
|
182
|
+
// An explicit `agend fleet start <name>` is a resume request when the
|
|
183
|
+
// instance was auto/manual-paused. Fleet startup itself still leaves
|
|
184
|
+
// persisted pauses alone.
|
|
185
|
+
await fm.startInstance(instance, inst, topicMode, "fleet-topic", true);
|
|
104
186
|
}
|
|
105
187
|
else {
|
|
106
188
|
if (process.env.AGEND_HOME) {
|
|
@@ -109,30 +191,6 @@ fleet
|
|
|
109
191
|
await fm.startAll(FLEET_CONFIG_PATH);
|
|
110
192
|
}
|
|
111
193
|
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
194
|
});
|
|
137
195
|
fleet
|
|
138
196
|
.command("stop")
|
|
@@ -319,6 +377,8 @@ fleet
|
|
|
319
377
|
}
|
|
320
378
|
console.log("Old fleet stopped. Starting with new code...");
|
|
321
379
|
// Start new fleet in this process (new Node.js process = new code loaded)
|
|
380
|
+
if (!claimFleetSingleton())
|
|
381
|
+
return;
|
|
322
382
|
const { FleetManager } = await import("./fleet-manager.js");
|
|
323
383
|
const fm = new FleetManager(DATA_DIR);
|
|
324
384
|
await fm.startAll(FLEET_CONFIG_PATH);
|
|
@@ -1106,11 +1166,23 @@ program
|
|
|
1106
1166
|
else {
|
|
1107
1167
|
console.log(`\n Updating AgEnD to ${tag}...\n`);
|
|
1108
1168
|
}
|
|
1169
|
+
// Tell the running fleet that what happens next is planned. `npm install -g`
|
|
1170
|
+
// replaces the package directory underneath the live daemon — MCP servers
|
|
1171
|
+
// die, then the restart stops every instance — and without this marker the
|
|
1172
|
+
// old daemon reports each one as a crash. The new fleet deletes the marker
|
|
1173
|
+
// once it finishes starting; it also expires on its own, so an update that
|
|
1174
|
+
// dies here does not silence real alerts.
|
|
1175
|
+
markUpdateInProgress(DATA_DIR);
|
|
1176
|
+
const failUpdate = (message) => {
|
|
1177
|
+
clearUpdateMarker(DATA_DIR);
|
|
1178
|
+
console.error(message);
|
|
1179
|
+
process.exit(1);
|
|
1180
|
+
};
|
|
1109
1181
|
// Detect and remove stale npm link (local build that shadows global install)
|
|
1110
1182
|
try {
|
|
1111
1183
|
const agendPath = execSync("which agend", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
1112
1184
|
const resolved = execSync(`readlink -f "${agendPath}"`, { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
1113
|
-
if (resolved.includes("/Projects/") || resolved.includes("@suzuke") || resolved.includes("/src/")) {
|
|
1185
|
+
if (resolved.includes("/Projects/") || resolved.includes("@songsid") || resolved.includes("@suzuke") || resolved.includes("/src/")) {
|
|
1114
1186
|
console.log(` ⚠️ Detected local npm link: ${resolved}`);
|
|
1115
1187
|
console.log(" Removing link to allow global install...\n");
|
|
1116
1188
|
try {
|
|
@@ -1147,8 +1219,7 @@ program
|
|
|
1147
1219
|
execSync("curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash", { stdio: "inherit" });
|
|
1148
1220
|
}
|
|
1149
1221
|
catch {
|
|
1150
|
-
|
|
1151
|
-
process.exit(1);
|
|
1222
|
+
failUpdate(" Failed to install nvm. Install manually: https://github.com/nvm-sh/nvm");
|
|
1152
1223
|
}
|
|
1153
1224
|
}
|
|
1154
1225
|
console.log(" Using nvm to install Node 22...");
|
|
@@ -1157,12 +1228,16 @@ program
|
|
|
1157
1228
|
execSync(`bash -c '${nvmPrefix} && npm install -g ${pkg}'`, { stdio: "inherit" });
|
|
1158
1229
|
}
|
|
1159
1230
|
catch {
|
|
1160
|
-
|
|
1161
|
-
process.exit(1);
|
|
1231
|
+
failUpdate(" Failed to install via nvm.");
|
|
1162
1232
|
}
|
|
1163
1233
|
// Try to remove old system binary
|
|
1164
1234
|
console.log(" Note: removing old system install (may require sudo)...");
|
|
1165
|
-
|
|
1235
|
+
// Never wait for a password in an agent/non-interactive terminal. This is
|
|
1236
|
+
// best-effort cleanup; the new nvm install already succeeded.
|
|
1237
|
+
spawnSync("sudo", ["-n", "npm", "uninstall", "-g", "@songsid/agend"], {
|
|
1238
|
+
stdio: "inherit",
|
|
1239
|
+
timeout: 10_000,
|
|
1240
|
+
});
|
|
1166
1241
|
}
|
|
1167
1242
|
else {
|
|
1168
1243
|
// ── Direct install ──
|
|
@@ -1170,8 +1245,7 @@ program
|
|
|
1170
1245
|
execSync(`npm install -g ${pkg}`, { stdio: "inherit" });
|
|
1171
1246
|
}
|
|
1172
1247
|
catch {
|
|
1173
|
-
|
|
1174
|
-
process.exit(1);
|
|
1248
|
+
failUpdate(` Failed to update. Try: npm install -g ${pkg}`);
|
|
1175
1249
|
}
|
|
1176
1250
|
}
|
|
1177
1251
|
// ── Verify installation ──
|
|
@@ -1181,15 +1255,13 @@ program
|
|
|
1181
1255
|
? spawnSync("bash", ["-c", `source ${nvmSh2} && nvm use 22 > /dev/null 2>&1 && which agend`], { encoding: "utf-8" }).stdout?.trim()
|
|
1182
1256
|
: spawnSync("which", ["agend"], { encoding: "utf-8" }).stdout?.trim();
|
|
1183
1257
|
if (!agendPath) {
|
|
1184
|
-
console.error(" ✗ Verification failed: agend not found in PATH after install.");
|
|
1185
1258
|
if (needsSudo)
|
|
1186
1259
|
console.error(" You may need to add nvm to your shell profile and restart.");
|
|
1187
|
-
|
|
1260
|
+
failUpdate(" ✗ Verification failed: agend not found in PATH after install.");
|
|
1188
1261
|
}
|
|
1189
1262
|
const verifyResult = spawnSync(agendPath, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
1190
1263
|
if (verifyResult.status !== 0) {
|
|
1191
|
-
|
|
1192
|
-
process.exit(1);
|
|
1264
|
+
failUpdate(" ✗ Verification failed: agend --version returned error.");
|
|
1193
1265
|
}
|
|
1194
1266
|
const newVersion = (verifyResult.stdout ?? "").trim();
|
|
1195
1267
|
console.log(` ✓ Installed: ${newVersion}`);
|
|
@@ -1216,9 +1288,20 @@ program
|
|
|
1216
1288
|
// restart` (new binary) does the 4-environment service detection (system
|
|
1217
1289
|
// systemd → user systemd → launchd → detached pid).
|
|
1218
1290
|
console.log(" Restarting fleet...");
|
|
1219
|
-
|
|
1220
|
-
|
|
1291
|
+
// `agend restart` may synchronously wait for a Type=notify service to finish
|
|
1292
|
+
// a multi-minute stop/start and send READY=1. Keep this parent wrapper alive
|
|
1293
|
+
// longer than the restart command's own bounded wait.
|
|
1294
|
+
const restartResult = spawnSync(agendPath, ["restart"], {
|
|
1295
|
+
encoding: "utf-8",
|
|
1296
|
+
timeout: SYSTEMD_RESTART_TIMEOUT_MS + 60_000,
|
|
1297
|
+
stdio: "inherit",
|
|
1298
|
+
});
|
|
1299
|
+
if (!reportUpdateRestart(restartResult.status)) {
|
|
1300
|
+
// No new fleet is coming up to clear the marker — do it here, or the next
|
|
1301
|
+
// 15 minutes of genuine crashes would go unreported.
|
|
1302
|
+
clearUpdateMarker(DATA_DIR);
|
|
1221
1303
|
process.exitCode = 1;
|
|
1304
|
+
}
|
|
1222
1305
|
});
|
|
1223
1306
|
program
|
|
1224
1307
|
.command("reload")
|
|
@@ -1331,8 +1414,10 @@ program
|
|
|
1331
1414
|
.command("start")
|
|
1332
1415
|
.description("Start the AgEnD service (must be installed first)")
|
|
1333
1416
|
.action(async () => {
|
|
1334
|
-
const { getServicePath, startService } = await import("./service-installer.js");
|
|
1335
|
-
|
|
1417
|
+
const { getServicePath, getSystemServicePath, startService, startSystemService } = await import("./service-installer.js");
|
|
1418
|
+
const userServicePath = getServicePath();
|
|
1419
|
+
const systemServicePath = getSystemServicePath();
|
|
1420
|
+
if (!userServicePath && !systemServicePath) {
|
|
1336
1421
|
console.log("No service installed. Starting fleet directly...");
|
|
1337
1422
|
const { spawn } = await import("node:child_process");
|
|
1338
1423
|
const child = spawn("sh", ["-c", "agend fleet start"], { detached: true, stdio: "ignore" });
|
|
@@ -1340,16 +1425,16 @@ program
|
|
|
1340
1425
|
console.log("Fleet starting in background.");
|
|
1341
1426
|
return;
|
|
1342
1427
|
}
|
|
1343
|
-
|
|
1428
|
+
const started = systemServicePath ? startSystemService() : startService();
|
|
1429
|
+
if (started) {
|
|
1344
1430
|
console.log("Service started.");
|
|
1345
1431
|
}
|
|
1346
1432
|
else {
|
|
1347
|
-
//
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
console.log("Fleet starting in background.");
|
|
1433
|
+
// A service file means the service manager owns this fleet. Falling back
|
|
1434
|
+
// to a detached process here creates a second fleet when the bus recovers.
|
|
1435
|
+
console.error("Failed to start the installed AgEnD service; refusing to start a detached duplicate.");
|
|
1436
|
+
console.error("Check your service manager, then retry: systemctl --user status com.agend.fleet");
|
|
1437
|
+
process.exitCode = 1;
|
|
1353
1438
|
}
|
|
1354
1439
|
});
|
|
1355
1440
|
program
|
|
@@ -1361,18 +1446,10 @@ program
|
|
|
1361
1446
|
// service's own HOME, which may differ from what this command resolves (sudo,
|
|
1362
1447
|
// AGEND_HOME) — an absent pid file does NOT mean it's stopped. Ask the service
|
|
1363
1448
|
// manager first. This is the canonical restart the `update` flow spawns.
|
|
1364
|
-
const { detectPlatform } = await import("./service-installer.js");
|
|
1449
|
+
const { detectPlatform, getServicePath, getSystemServicePath, getSystemdServiceState, restartSystemdService, } = await import("./service-installer.js");
|
|
1365
1450
|
const { spawn, spawnSync } = await import("node:child_process");
|
|
1366
1451
|
const plat = detectPlatform();
|
|
1367
1452
|
const pidPath = join(DATA_DIR, "fleet.pid");
|
|
1368
|
-
const isActive = (cmd) => {
|
|
1369
|
-
try {
|
|
1370
|
-
return spawnSync("sh", ["-c", cmd], { encoding: "utf-8", timeout: 5000 }).stdout?.trim() === "active";
|
|
1371
|
-
}
|
|
1372
|
-
catch {
|
|
1373
|
-
return false;
|
|
1374
|
-
}
|
|
1375
|
-
};
|
|
1376
1453
|
const run = (cmd) => {
|
|
1377
1454
|
try {
|
|
1378
1455
|
execSync(cmd, { stdio: "inherit", timeout: 15000 });
|
|
@@ -1383,9 +1460,17 @@ program
|
|
|
1383
1460
|
}
|
|
1384
1461
|
};
|
|
1385
1462
|
// 1. System-level systemd service "agend"
|
|
1386
|
-
|
|
1463
|
+
const systemServiceInstalled = plat !== "macos" && getSystemServicePath() !== null;
|
|
1464
|
+
const systemState = plat !== "macos" ? getSystemdServiceState("agend", false) : "stopped";
|
|
1465
|
+
if (systemServiceInstalled && systemState === "unavailable") {
|
|
1466
|
+
console.error("Cannot reach the system service manager; refusing a detached restart that could duplicate the fleet.");
|
|
1467
|
+
console.error("Check: systemctl status agend");
|
|
1468
|
+
process.exitCode = 1;
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if (systemServiceInstalled && (systemState === "running" || systemState === "stopped")) {
|
|
1387
1472
|
run("systemctl daemon-reload");
|
|
1388
|
-
if (
|
|
1473
|
+
if (restartSystemdService("agend", false)) {
|
|
1389
1474
|
console.log("Service restarted (system).");
|
|
1390
1475
|
return;
|
|
1391
1476
|
}
|
|
@@ -1395,13 +1480,23 @@ program
|
|
|
1395
1480
|
return;
|
|
1396
1481
|
}
|
|
1397
1482
|
// 2. User-level systemd service "com.agend.fleet"
|
|
1398
|
-
|
|
1483
|
+
const userServiceInstalled = plat !== "macos" && getServicePath() !== null;
|
|
1484
|
+
const userState = plat !== "macos"
|
|
1485
|
+
? getSystemdServiceState("com.agend.fleet", true)
|
|
1486
|
+
: "stopped";
|
|
1487
|
+
if (userServiceInstalled && userState === "unavailable") {
|
|
1488
|
+
console.error("Cannot reach the user service manager; refusing a detached restart that could duplicate the fleet.");
|
|
1489
|
+
console.error("Check: systemctl --user status com.agend.fleet");
|
|
1490
|
+
process.exitCode = 1;
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (userServiceInstalled && (userState === "running" || userState === "stopped")) {
|
|
1399
1494
|
run("systemctl --user daemon-reload");
|
|
1400
1495
|
try {
|
|
1401
1496
|
execSync("systemctl --user reset-failed com.agend.fleet", { stdio: "pipe", timeout: 5000 });
|
|
1402
1497
|
}
|
|
1403
1498
|
catch { /* best effort */ }
|
|
1404
|
-
if (
|
|
1499
|
+
if (restartSystemdService("com.agend.fleet", true)) {
|
|
1405
1500
|
console.log("Service restarted (user).");
|
|
1406
1501
|
return;
|
|
1407
1502
|
}
|
|
@@ -1835,28 +1930,7 @@ async function fuzzyMatch(query, names) {
|
|
|
1835
1930
|
return null;
|
|
1836
1931
|
}
|
|
1837
1932
|
async function resolveInstance(query, config) {
|
|
1838
|
-
const names =
|
|
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 */ }
|
|
1933
|
+
const names = await listInstanceNames(config);
|
|
1860
1934
|
const match = await fuzzyMatch(query, names);
|
|
1861
1935
|
if (!match) {
|
|
1862
1936
|
console.error(`No instance matching "${query}". Available: ${names.join(", ")}`);
|
|
@@ -1907,6 +1981,15 @@ function getTreeRssKb(pid, depth = 0) {
|
|
|
1907
1981
|
function getInstanceStatusStandalone(name) {
|
|
1908
1982
|
if (hasPausedMarker(join(DATA_DIR, "instances", name)))
|
|
1909
1983
|
return "paused";
|
|
1984
|
+
// daemon.pid is the shared FleetManager PID, so it remains alive when one
|
|
1985
|
+
// CLI has entered the persisted 3-crashes circuit breaker. Consult that
|
|
1986
|
+
// marker before using daemon.pid as a fallback.
|
|
1987
|
+
try {
|
|
1988
|
+
const crash = JSON.parse(readFileSync(join(DATA_DIR, "instances", name, "crash-state.json"), "utf8"));
|
|
1989
|
+
if (Number(crash?.crashesInWindow) >= 3)
|
|
1990
|
+
return "crashed";
|
|
1991
|
+
}
|
|
1992
|
+
catch { /* no valid crash-loop marker */ }
|
|
1910
1993
|
const pidPath = join(DATA_DIR, "instances", name, "daemon.pid");
|
|
1911
1994
|
if (!existsSync(pidPath))
|
|
1912
1995
|
return "stopped";
|
|
@@ -1989,10 +2072,48 @@ const defaultParser = (output) => {
|
|
|
1989
2072
|
};
|
|
1990
2073
|
/** Optional backend-specific overrides; none needed — defaultParser covers all. */
|
|
1991
2074
|
const contextParsers = {};
|
|
2075
|
+
/**
|
|
2076
|
+
* Every instance name the CLI accepts: fleet.yaml instances plus the
|
|
2077
|
+
* dynamically-created ClassicBot ones. Shared by `resolveInstance` (fuzzy
|
|
2078
|
+
* matching for `attach`) and `ls --names-only` (shell completion), so the
|
|
2079
|
+
* candidates offered by tab-completion are exactly the ones `attach` resolves.
|
|
2080
|
+
*/
|
|
2081
|
+
async function listInstanceNames(config) {
|
|
2082
|
+
const names = Object.keys(config.instances);
|
|
2083
|
+
try {
|
|
2084
|
+
const classicPath = join(DATA_DIR, "classicBot.yaml");
|
|
2085
|
+
if (existsSync(classicPath)) {
|
|
2086
|
+
const yamlMod = (await import("js-yaml")).default;
|
|
2087
|
+
const classic = yamlMod.load(readFileSync(classicPath, "utf-8"));
|
|
2088
|
+
if (classic?.channels) {
|
|
2089
|
+
for (const [key, val] of Object.entries(classic.channels)) {
|
|
2090
|
+
// New format persists instanceName; old format keyed by channelId.
|
|
2091
|
+
if (val.instanceName) {
|
|
2092
|
+
names.push(val.instanceName);
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
const channelId = val.channelId ?? key;
|
|
2096
|
+
const chName = (val.name ?? channelId).toLowerCase().replace(/[^\p{L}\d-]/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "project";
|
|
2097
|
+
names.push(`classic-${chName}-${channelId.slice(-4)}`);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
catch { /* ignore */ }
|
|
2103
|
+
return names;
|
|
2104
|
+
}
|
|
1992
2105
|
async function lsAction(opts) {
|
|
1993
2106
|
const yaml = (await import("js-yaml")).default;
|
|
1994
2107
|
const config = yaml.load(readFileSync(FLEET_CONFIG_PATH, "utf-8"));
|
|
1995
2108
|
const names = Object.keys(config.instances);
|
|
2109
|
+
// --names-only feeds shell completion, so it must return before any tmux or
|
|
2110
|
+
// status work (a tab press should not wait on the fleet) and must print
|
|
2111
|
+
// nothing but names — a "no instances" notice would become a candidate.
|
|
2112
|
+
if (opts.namesOnly) {
|
|
2113
|
+
for (const n of await listInstanceNames(config))
|
|
2114
|
+
console.log(n);
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
1996
2117
|
// Load classic channels from classicBot.yaml (keyed by channelId)
|
|
1997
2118
|
const classicPath = join(DATA_DIR, "classicBot.yaml");
|
|
1998
2119
|
let classicConfig = null;
|
|
@@ -2195,6 +2316,10 @@ async function lsAction(opts) {
|
|
|
2195
2316
|
}
|
|
2196
2317
|
// Status icon — prefer tri-state 'state' field, fall back to idle boolean
|
|
2197
2318
|
const statusIcon = (s, idle, state) => {
|
|
2319
|
+
if (s === "crashed")
|
|
2320
|
+
return "\x1b[31m●\x1b[0m";
|
|
2321
|
+
if (s === "stopped")
|
|
2322
|
+
return "\x1b[90m○\x1b[0m";
|
|
2198
2323
|
if (state === "stuck")
|
|
2199
2324
|
return "\x1b[31m●\x1b[0m";
|
|
2200
2325
|
if (state === "paused" || s === "paused")
|
|
@@ -2206,11 +2331,13 @@ async function lsAction(opts) {
|
|
|
2206
2331
|
// Fallback for when API is unreachable
|
|
2207
2332
|
if (s === "running")
|
|
2208
2333
|
return idle === false ? "\x1b[34m●\x1b[0m" : "\x1b[32m●\x1b[0m";
|
|
2209
|
-
if (s === "crashed")
|
|
2210
|
-
return "\x1b[31m●\x1b[0m";
|
|
2211
2334
|
return "\x1b[90m○\x1b[0m";
|
|
2212
2335
|
};
|
|
2213
2336
|
const statusLabel = (s, idle, state) => {
|
|
2337
|
+
if (s === "crashed")
|
|
2338
|
+
return "Crashed";
|
|
2339
|
+
if (s === "stopped")
|
|
2340
|
+
return "Stopped";
|
|
2214
2341
|
if (state === "stuck")
|
|
2215
2342
|
return "Stuck";
|
|
2216
2343
|
if (state === "paused" || s === "paused")
|
|
@@ -2221,8 +2348,6 @@ async function lsAction(opts) {
|
|
|
2221
2348
|
return "Idle";
|
|
2222
2349
|
if (s === "running")
|
|
2223
2350
|
return idle === false ? "Busy" : "Idle";
|
|
2224
|
-
if (s === "crashed")
|
|
2225
|
-
return "Crashed";
|
|
2226
2351
|
return "Stopped";
|
|
2227
2352
|
};
|
|
2228
2353
|
/** Get display width accounting for fullwidth (CJK) characters */
|
|
@@ -2278,12 +2403,38 @@ program
|
|
|
2278
2403
|
.command("ls")
|
|
2279
2404
|
.description("List all instances with status, backend, team, and last activity")
|
|
2280
2405
|
.option("--json", "Output as JSON")
|
|
2406
|
+
.option("--names-only", "Print instance names one per line (for shell completion)")
|
|
2281
2407
|
.action(async (opts) => {
|
|
2282
2408
|
await lsAction(opts);
|
|
2283
2409
|
});
|
|
2284
2410
|
program
|
|
2285
|
-
.command("
|
|
2286
|
-
.description("
|
|
2411
|
+
.command("completion")
|
|
2412
|
+
.description("Print a shell completion script (bash or zsh)")
|
|
2413
|
+
.argument("<shell>", `Shell to generate for (${COMPLETION_SHELLS.join(", ")})`)
|
|
2414
|
+
.addHelpText("after", `
|
|
2415
|
+
Install:
|
|
2416
|
+
bash echo 'eval "$(agend completion bash)"' >> ~/.bashrc
|
|
2417
|
+
zsh echo 'eval "$(agend completion zsh)"' >> ~/.zshrc
|
|
2418
|
+
|
|
2419
|
+
Then reload the shell. Tab-completes instance names for \`agend attach\` and
|
|
2420
|
+
\`agend fleet start|stop|restart\`, and subcommands elsewhere.`)
|
|
2421
|
+
.action((shell) => {
|
|
2422
|
+
if (!COMPLETION_SHELLS.includes(shell)) {
|
|
2423
|
+
console.error(`Unsupported shell: ${shell}. Supported: ${COMPLETION_SHELLS.join(", ")}`);
|
|
2424
|
+
process.exit(2);
|
|
2425
|
+
}
|
|
2426
|
+
// Subcommand names come from commander itself, so the script can't drift
|
|
2427
|
+
// out of sync with the CLI the way a hardcoded list would.
|
|
2428
|
+
const fleetCmd = program.commands.find(c => c.name() === "fleet");
|
|
2429
|
+
console.log(completionScript(shell, {
|
|
2430
|
+
topLevel: program.commands.map(c => c.name()),
|
|
2431
|
+
fleetSub: fleetCmd?.commands.map(c => c.name()) ?? [],
|
|
2432
|
+
instanceCommands: ["attach"],
|
|
2433
|
+
fleetInstanceCommands: ["start", "stop", "restart"],
|
|
2434
|
+
}));
|
|
2435
|
+
});
|
|
2436
|
+
program
|
|
2437
|
+
.command("health").description("Fleet health check — shows problems and diagnostics")
|
|
2287
2438
|
.option("--json", "Output as JSON")
|
|
2288
2439
|
.option("-q, --quiet", "One-line summary only")
|
|
2289
2440
|
.action(async (opts) => {
|