@hydra-acp/cli 0.1.4 → 0.1.5
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/README.md +3 -3
- package/dist/cli.js +785 -237
- package/dist/index.d.ts +432 -31
- package/dist/index.js +570 -124
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -37,7 +37,11 @@ var init_paths = __esm({
|
|
|
37
37
|
currentLogFile: () => path.join(hydraHome(), "current.log"),
|
|
38
38
|
registryCache: () => path.join(hydraHome(), "registry.json"),
|
|
39
39
|
agentsDir: () => path.join(hydraHome(), "agents"),
|
|
40
|
-
|
|
40
|
+
// <platformKey>/<agentId>/<version>/ — platform at the top so a Hydra
|
|
41
|
+
// home shared between machines (NFS, rsync'd dotfiles) keeps each
|
|
42
|
+
// machine's binaries cleanly separated. `ls agents/` immediately
|
|
43
|
+
// shows which platforms have ever installed anything.
|
|
44
|
+
agentInstallDir: (id, platformKey, version) => path.join(hydraHome(), "agents", platformKey, id, version),
|
|
41
45
|
sessionsDir: () => path.join(hydraHome(), "sessions"),
|
|
42
46
|
// One directory per session id under sessions/. Co-locates the
|
|
43
47
|
// session record, its transcript, and any future per-session state
|
|
@@ -109,12 +113,12 @@ async function writeMinimalInitConfig(authToken) {
|
|
|
109
113
|
return HydraConfig.parse(minimal);
|
|
110
114
|
}
|
|
111
115
|
async function updateConfigField(mutate) {
|
|
112
|
-
const
|
|
113
|
-
const text = await fs.readFile(
|
|
116
|
+
const path7 = paths.config();
|
|
117
|
+
const text = await fs.readFile(path7, "utf8");
|
|
114
118
|
const raw = JSON.parse(text);
|
|
115
119
|
mutate(raw);
|
|
116
120
|
HydraConfig.parse(raw);
|
|
117
|
-
await fs.writeFile(
|
|
121
|
+
await fs.writeFile(path7, JSON.stringify(raw, null, 2) + "\n", {
|
|
118
122
|
encoding: "utf8",
|
|
119
123
|
mode: 384
|
|
120
124
|
});
|
|
@@ -189,6 +193,14 @@ var init_config = __esm({
|
|
|
189
193
|
daemon: DaemonConfig,
|
|
190
194
|
registry: RegistryConfig.default({ url: REGISTRY_URL_DEFAULT, ttlHours: 24 }),
|
|
191
195
|
defaultAgent: z.string().default("claude-acp"),
|
|
196
|
+
// Optional per-agent default model id. When a brand-new agent process
|
|
197
|
+
// is spawned (session/new path), hydra issues session/set_model with
|
|
198
|
+
// the matching entry so the user lands on their preferred model from
|
|
199
|
+
// the first prompt. Not applied on resurrect — those sessions keep
|
|
200
|
+
// whatever the user last selected. Keys are agent ids; values are the
|
|
201
|
+
// raw model id strings the agent expects (claude-acp: "claude-opus-4-7",
|
|
202
|
+
// opencode: "openai/gpt-5-codex" or "ncp-anthropic/claude-opus-4-7", …).
|
|
203
|
+
defaultModels: z.record(z.string(), z.string()).default({}),
|
|
192
204
|
// Where new sessions land when POST /v1/sessions omits cwd. Stored as
|
|
193
205
|
// a literal string ("~", "~/dev", "$HOME/work") so the config file is
|
|
194
206
|
// portable across machines; expanded via expandHome at use time.
|
|
@@ -270,7 +282,7 @@ function extractHydraMeta(meta) {
|
|
|
270
282
|
function mergeMeta(passthrough, ours) {
|
|
271
283
|
return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
|
|
272
284
|
}
|
|
273
|
-
var JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
285
|
+
var JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
274
286
|
var init_types = __esm({
|
|
275
287
|
"src/acp/types.ts"() {
|
|
276
288
|
"use strict";
|
|
@@ -325,12 +337,24 @@ var init_types = __esm({
|
|
|
325
337
|
cursor: z3.string().optional(),
|
|
326
338
|
limit: z3.number().int().positive().max(200).optional()
|
|
327
339
|
});
|
|
340
|
+
SessionListUsage = z3.object({
|
|
341
|
+
used: z3.number().optional(),
|
|
342
|
+
size: z3.number().optional(),
|
|
343
|
+
costAmount: z3.number().optional(),
|
|
344
|
+
costCurrency: z3.string().optional()
|
|
345
|
+
});
|
|
328
346
|
SessionListEntry = z3.object({
|
|
329
347
|
sessionId: z3.string(),
|
|
330
348
|
upstreamSessionId: z3.string().optional(),
|
|
331
349
|
cwd: z3.string(),
|
|
332
350
|
title: z3.string().optional(),
|
|
333
351
|
agentId: z3.string().optional(),
|
|
352
|
+
// Last-known model id, so list views can render `<agent>(<model>)`
|
|
353
|
+
// without resurrecting cold sessions to look it up.
|
|
354
|
+
currentModel: z3.string().optional(),
|
|
355
|
+
// Last-known usage snapshot so list views can show per-session cost
|
|
356
|
+
// (and tokens, in callers that care) without resurrecting cold sessions.
|
|
357
|
+
currentUsage: SessionListUsage.optional(),
|
|
334
358
|
updatedAt: z3.string(),
|
|
335
359
|
attachedClients: z3.number().int().nonnegative(),
|
|
336
360
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
@@ -408,9 +432,9 @@ var init_connection = __esm({
|
|
|
408
432
|
}
|
|
409
433
|
const id = nanoid();
|
|
410
434
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
411
|
-
const response = new Promise((
|
|
435
|
+
const response = new Promise((resolve5, reject) => {
|
|
412
436
|
this.pending.set(id, {
|
|
413
|
-
resolve: (result) =>
|
|
437
|
+
resolve: (result) => resolve5(result),
|
|
414
438
|
reject
|
|
415
439
|
});
|
|
416
440
|
this.stream.send(message).catch((err) => {
|
|
@@ -534,8 +558,8 @@ var init_hydra_commands = __esm({
|
|
|
534
558
|
description: "Regenerate the session title via the agent (or set manually with an arg)"
|
|
535
559
|
},
|
|
536
560
|
{
|
|
537
|
-
verb: "
|
|
538
|
-
name: "/hydra
|
|
561
|
+
verb: "agent",
|
|
562
|
+
name: "/hydra agent",
|
|
539
563
|
argsHint: "<agent>",
|
|
540
564
|
description: "Swap the agent backing this session, preserving context"
|
|
541
565
|
}
|
|
@@ -648,7 +672,7 @@ var init_session = __esm({
|
|
|
648
672
|
Session = class {
|
|
649
673
|
sessionId;
|
|
650
674
|
cwd;
|
|
651
|
-
// agent / agentId / upstreamSessionId are mutable so /hydra
|
|
675
|
+
// agent / agentId / upstreamSessionId are mutable so /hydra agent can
|
|
652
676
|
// replace the underlying agent process while keeping the same Session
|
|
653
677
|
// record. agentMeta is the metadata returned by the agent at session/new
|
|
654
678
|
// time; it gets refreshed on switch too.
|
|
@@ -663,6 +687,7 @@ var init_session = __esm({
|
|
|
663
687
|
// stale-prone for snapshot-shaped events).
|
|
664
688
|
currentModel;
|
|
665
689
|
currentMode;
|
|
690
|
+
currentUsage;
|
|
666
691
|
updatedAt;
|
|
667
692
|
createdAt;
|
|
668
693
|
clients = /* @__PURE__ */ new Map();
|
|
@@ -724,6 +749,7 @@ var init_session = __esm({
|
|
|
724
749
|
agentCommandsHandlers = [];
|
|
725
750
|
modelHandlers = [];
|
|
726
751
|
modeHandlers = [];
|
|
752
|
+
usageHandlers = [];
|
|
727
753
|
constructor(init) {
|
|
728
754
|
this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
|
|
729
755
|
this.cwd = init.cwd;
|
|
@@ -735,6 +761,7 @@ var init_session = __esm({
|
|
|
735
761
|
this.title = init.title;
|
|
736
762
|
this.currentModel = init.currentModel;
|
|
737
763
|
this.currentMode = init.currentMode;
|
|
764
|
+
this.currentUsage = init.currentUsage;
|
|
738
765
|
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
739
766
|
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
740
767
|
}
|
|
@@ -764,7 +791,7 @@ var init_session = __esm({
|
|
|
764
791
|
});
|
|
765
792
|
}
|
|
766
793
|
// Register session/update, session/request_permission, and onExit
|
|
767
|
-
// handlers on an agent connection. Re-run on every /hydra
|
|
794
|
+
// handlers on an agent connection. Re-run on every /hydra agent so
|
|
768
795
|
// the new agent is plumbed identically. The exit handler's identity
|
|
769
796
|
// check is what makes switching safe: when the *old* agent exits as
|
|
770
797
|
// part of a swap, this.agent has already been replaced, so we no-op
|
|
@@ -788,6 +815,10 @@ var init_session = __esm({
|
|
|
788
815
|
this.recordAndBroadcast("session/update", params);
|
|
789
816
|
return;
|
|
790
817
|
}
|
|
818
|
+
if (this.maybeApplyAgentUsage(params)) {
|
|
819
|
+
this.recordAndBroadcast("session/update", params);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
791
822
|
this.maybeApplyAgentSessionInfo(params);
|
|
792
823
|
this.recordAndBroadcast("session/update", params);
|
|
793
824
|
});
|
|
@@ -1105,6 +1136,49 @@ var init_session = __esm({
|
|
|
1105
1136
|
}
|
|
1106
1137
|
return true;
|
|
1107
1138
|
}
|
|
1139
|
+
// usage_update carries any subset of {used, size, cost.amount,
|
|
1140
|
+
// cost.currency}. Merge non-undefined fields onto currentUsage so a
|
|
1141
|
+
// sparse update preserves prior values, and fire usage handlers only
|
|
1142
|
+
// if something actually changed.
|
|
1143
|
+
maybeApplyAgentUsage(params) {
|
|
1144
|
+
const obj = params ?? {};
|
|
1145
|
+
const update = obj.update ?? {};
|
|
1146
|
+
if (update.sessionUpdate !== "usage_update") {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
const next = { ...this.currentUsage ?? {} };
|
|
1150
|
+
let changed = false;
|
|
1151
|
+
if (typeof update.used === "number" && next.used !== update.used) {
|
|
1152
|
+
next.used = update.used;
|
|
1153
|
+
changed = true;
|
|
1154
|
+
}
|
|
1155
|
+
if (typeof update.size === "number" && next.size !== update.size) {
|
|
1156
|
+
next.size = update.size;
|
|
1157
|
+
changed = true;
|
|
1158
|
+
}
|
|
1159
|
+
if (update.cost && typeof update.cost === "object") {
|
|
1160
|
+
const cost = update.cost;
|
|
1161
|
+
if (typeof cost.amount === "number" && next.costAmount !== cost.amount) {
|
|
1162
|
+
next.costAmount = cost.amount;
|
|
1163
|
+
changed = true;
|
|
1164
|
+
}
|
|
1165
|
+
if (typeof cost.currency === "string" && next.costCurrency !== cost.currency) {
|
|
1166
|
+
next.costCurrency = cost.currency;
|
|
1167
|
+
changed = true;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (!changed) {
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
this.currentUsage = next;
|
|
1174
|
+
for (const handler of this.usageHandlers) {
|
|
1175
|
+
try {
|
|
1176
|
+
handler(next);
|
|
1177
|
+
} catch {
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1108
1182
|
// Update the cached agent command list, fire persist handlers, and
|
|
1109
1183
|
// broadcast the merged list to attached clients. Idempotent on a
|
|
1110
1184
|
// structurally identical list so we don't churn meta.json on noisy
|
|
@@ -1135,6 +1209,9 @@ var init_session = __esm({
|
|
|
1135
1209
|
onModeChange(handler) {
|
|
1136
1210
|
this.modeHandlers.push(handler);
|
|
1137
1211
|
}
|
|
1212
|
+
onUsageChange(handler) {
|
|
1213
|
+
this.usageHandlers.push(handler);
|
|
1214
|
+
}
|
|
1138
1215
|
// Returns a freshly merged command list (hydra ∪ agent) for callers
|
|
1139
1216
|
// that need a snapshot — notably acp-ws.ts's buildResponseMeta when
|
|
1140
1217
|
// assembling the attach response.
|
|
@@ -1197,8 +1274,8 @@ var init_session = __esm({
|
|
|
1197
1274
|
switch (verb) {
|
|
1198
1275
|
case "title":
|
|
1199
1276
|
return this.runTitleCommand(arg);
|
|
1200
|
-
case "
|
|
1201
|
-
return this.
|
|
1277
|
+
case "agent":
|
|
1278
|
+
return this.runAgentCommand(arg);
|
|
1202
1279
|
default: {
|
|
1203
1280
|
const err = new Error(
|
|
1204
1281
|
`no dispatcher for /hydra verb ${verb}`
|
|
@@ -1234,7 +1311,7 @@ var init_session = __esm({
|
|
|
1234
1311
|
}
|
|
1235
1312
|
// Send a prompt to the underlying agent and capture its reply chunks
|
|
1236
1313
|
// privately (no fan-out to clients, no recording into history). Used
|
|
1237
|
-
// by /hydra title's regen path and /hydra
|
|
1314
|
+
// by /hydra title's regen path and /hydra agent's transcript-injection
|
|
1238
1315
|
// path. Returns the joined agent_message_chunk text.
|
|
1239
1316
|
async runInternalPrompt(text) {
|
|
1240
1317
|
if (this.internalPromptCapture) {
|
|
@@ -1256,10 +1333,10 @@ var init_session = __esm({
|
|
|
1256
1333
|
// record. Spawns the new agent first so a failure leaves the old one
|
|
1257
1334
|
// intact; then injects a synthesized transcript so the new agent has
|
|
1258
1335
|
// context for the next turn.
|
|
1259
|
-
|
|
1336
|
+
runAgentCommand(newAgentId) {
|
|
1260
1337
|
if (!newAgentId) {
|
|
1261
1338
|
throw withCode(
|
|
1262
|
-
new Error("/hydra
|
|
1339
|
+
new Error("/hydra agent requires an agent id"),
|
|
1263
1340
|
JsonRpcErrorCodes.InvalidParams
|
|
1264
1341
|
);
|
|
1265
1342
|
}
|
|
@@ -1393,7 +1470,7 @@ var init_session = __esm({
|
|
|
1393
1470
|
// on the first wake-up of a session whose meta.json has an empty
|
|
1394
1471
|
// upstreamSessionId (the import marker). Wrapped in enqueuePrompt so
|
|
1395
1472
|
// any user prompts arriving mid-seed queue behind it (mirrors the
|
|
1396
|
-
// /hydra
|
|
1473
|
+
// /hydra agent path so the agent isn't asked to respond to a user
|
|
1397
1474
|
// turn before it has absorbed the imported transcript). Best-effort:
|
|
1398
1475
|
// if the agent fails to absorb the transcript we still leave the
|
|
1399
1476
|
// session usable — the user just continues without context.
|
|
@@ -1417,7 +1494,7 @@ var init_session = __esm({
|
|
|
1417
1494
|
// ones read it and relabel) and (b) drop a visible banner into the
|
|
1418
1495
|
// transcript so users see the switch rather than just suddenly getting
|
|
1419
1496
|
// answers from a different agent. Both updates carry synthetic=true
|
|
1420
|
-
// so a future /hydra
|
|
1497
|
+
// so a future /hydra agent's transcript builder filters them out.
|
|
1421
1498
|
broadcastAgentSwitch(oldAgentId, newAgentId) {
|
|
1422
1499
|
this.recordAndBroadcast("session/update", {
|
|
1423
1500
|
sessionId: this.sessionId,
|
|
@@ -1564,7 +1641,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1564
1641
|
);
|
|
1565
1642
|
}
|
|
1566
1643
|
const clientParams = this.rewriteForClient(params);
|
|
1567
|
-
return new Promise((
|
|
1644
|
+
return new Promise((resolve5, reject) => {
|
|
1568
1645
|
let settled = false;
|
|
1569
1646
|
const outbound = [];
|
|
1570
1647
|
const entry = { addClient: sendTo };
|
|
@@ -1599,7 +1676,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1599
1676
|
result
|
|
1600
1677
|
}).catch(() => void 0);
|
|
1601
1678
|
}
|
|
1602
|
-
|
|
1679
|
+
resolve5(result);
|
|
1603
1680
|
});
|
|
1604
1681
|
}).catch((err) => {
|
|
1605
1682
|
settle(() => reject(err));
|
|
@@ -1611,16 +1688,16 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1611
1688
|
});
|
|
1612
1689
|
}
|
|
1613
1690
|
async enqueuePrompt(task) {
|
|
1614
|
-
return new Promise((
|
|
1615
|
-
const
|
|
1691
|
+
return new Promise((resolve5, reject) => {
|
|
1692
|
+
const run2 = async () => {
|
|
1616
1693
|
try {
|
|
1617
1694
|
const result = await task();
|
|
1618
|
-
|
|
1695
|
+
resolve5(result);
|
|
1619
1696
|
} catch (err) {
|
|
1620
1697
|
reject(err);
|
|
1621
1698
|
}
|
|
1622
1699
|
};
|
|
1623
|
-
this.promptQueue.push(
|
|
1700
|
+
this.promptQueue.push(run2);
|
|
1624
1701
|
void this.drainQueue();
|
|
1625
1702
|
});
|
|
1626
1703
|
}
|
|
@@ -1645,18 +1722,19 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1645
1722
|
"session_info_update",
|
|
1646
1723
|
"current_model_update",
|
|
1647
1724
|
"current_mode_update",
|
|
1648
|
-
"available_commands_update"
|
|
1725
|
+
"available_commands_update",
|
|
1726
|
+
"usage_update"
|
|
1649
1727
|
]);
|
|
1650
1728
|
}
|
|
1651
1729
|
});
|
|
1652
1730
|
|
|
1653
1731
|
// src/tui/history.ts
|
|
1654
|
-
import { promises as
|
|
1655
|
-
import * as
|
|
1732
|
+
import { promises as fs7 } from "fs";
|
|
1733
|
+
import * as path4 from "path";
|
|
1656
1734
|
async function loadHistory(file) {
|
|
1657
1735
|
let text;
|
|
1658
1736
|
try {
|
|
1659
|
-
text = await
|
|
1737
|
+
text = await fs7.readFile(file, "utf8");
|
|
1660
1738
|
} catch (err) {
|
|
1661
1739
|
if (err.code === "ENOENT") {
|
|
1662
1740
|
return [];
|
|
@@ -1696,9 +1774,9 @@ function appendEntry(history, entry) {
|
|
|
1696
1774
|
return out;
|
|
1697
1775
|
}
|
|
1698
1776
|
async function saveHistory(file, history) {
|
|
1699
|
-
await
|
|
1777
|
+
await fs7.mkdir(path4.dirname(file), { recursive: true });
|
|
1700
1778
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
1701
|
-
await
|
|
1779
|
+
await fs7.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
1702
1780
|
}
|
|
1703
1781
|
var HISTORY_CAP;
|
|
1704
1782
|
var init_history = __esm({
|
|
@@ -1753,13 +1831,13 @@ function wsToMessageStream(ws) {
|
|
|
1753
1831
|
throw new Error("ws is closed");
|
|
1754
1832
|
}
|
|
1755
1833
|
const text = JSON.stringify(message);
|
|
1756
|
-
await new Promise((
|
|
1834
|
+
await new Promise((resolve5, reject) => {
|
|
1757
1835
|
ws.send(text, (err) => {
|
|
1758
1836
|
if (err) {
|
|
1759
1837
|
reject(err);
|
|
1760
1838
|
return;
|
|
1761
1839
|
}
|
|
1762
|
-
|
|
1840
|
+
resolve5();
|
|
1763
1841
|
});
|
|
1764
1842
|
});
|
|
1765
1843
|
},
|
|
@@ -1786,7 +1864,7 @@ var init_ws_stream = __esm({
|
|
|
1786
1864
|
});
|
|
1787
1865
|
|
|
1788
1866
|
// src/core/daemon-bootstrap.ts
|
|
1789
|
-
import { spawn as
|
|
1867
|
+
import { spawn as spawn4 } from "child_process";
|
|
1790
1868
|
import { setTimeout as sleep } from "timers/promises";
|
|
1791
1869
|
async function ensureDaemonReachable(config) {
|
|
1792
1870
|
if (await pingHealth(config)) {
|
|
@@ -1813,11 +1891,15 @@ function spawnDaemonDetached() {
|
|
|
1813
1891
|
if (!cliPath) {
|
|
1814
1892
|
throw new Error("Cannot determine hydra-acp binary path to spawn daemon");
|
|
1815
1893
|
}
|
|
1816
|
-
const child =
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1894
|
+
const child = spawn4(
|
|
1895
|
+
process.execPath,
|
|
1896
|
+
[cliPath, "daemon", "start", "--foreground"],
|
|
1897
|
+
{
|
|
1898
|
+
detached: true,
|
|
1899
|
+
stdio: "ignore",
|
|
1900
|
+
env: process.env
|
|
1901
|
+
}
|
|
1902
|
+
);
|
|
1821
1903
|
child.unref();
|
|
1822
1904
|
}
|
|
1823
1905
|
async function waitForDaemonReady(config, timeoutMs = 15e3) {
|
|
@@ -1838,25 +1920,80 @@ var init_daemon_bootstrap = __esm({
|
|
|
1838
1920
|
}
|
|
1839
1921
|
});
|
|
1840
1922
|
|
|
1923
|
+
// src/core/agent-display.ts
|
|
1924
|
+
function shortenModel(model) {
|
|
1925
|
+
if (!model) {
|
|
1926
|
+
return void 0;
|
|
1927
|
+
}
|
|
1928
|
+
const idx = model.lastIndexOf("/");
|
|
1929
|
+
if (idx === -1) {
|
|
1930
|
+
return model;
|
|
1931
|
+
}
|
|
1932
|
+
return model.slice(idx + 1);
|
|
1933
|
+
}
|
|
1934
|
+
function formatAgentWithModel(agentId, model) {
|
|
1935
|
+
const agent = agentId ?? "?";
|
|
1936
|
+
const short = shortenModel(model);
|
|
1937
|
+
if (!short) {
|
|
1938
|
+
return agent;
|
|
1939
|
+
}
|
|
1940
|
+
return `${agent}${AGENT_MODEL_SEP}${short}`;
|
|
1941
|
+
}
|
|
1942
|
+
function formatAgentCell(agentId, model, usage) {
|
|
1943
|
+
const base = formatAgentWithModel(agentId, model);
|
|
1944
|
+
if (!usage || typeof usage.costAmount !== "number") {
|
|
1945
|
+
return base;
|
|
1946
|
+
}
|
|
1947
|
+
const compact = formatCostCompact(usage.costAmount, usage.costCurrency);
|
|
1948
|
+
if (compact === null) {
|
|
1949
|
+
return base;
|
|
1950
|
+
}
|
|
1951
|
+
return `${base} ${compact}`;
|
|
1952
|
+
}
|
|
1953
|
+
function formatCost(amount, currency) {
|
|
1954
|
+
const sign = currency === "USD" || currency === void 0 ? "$" : "";
|
|
1955
|
+
const decimals = amount >= 1 ? 2 : 4;
|
|
1956
|
+
return `${sign}${amount.toFixed(decimals)}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
|
|
1957
|
+
}
|
|
1958
|
+
function formatCostCompact(amount, currency) {
|
|
1959
|
+
const whole = Math.round(amount);
|
|
1960
|
+
if (whole === 0) {
|
|
1961
|
+
return null;
|
|
1962
|
+
}
|
|
1963
|
+
const sign = currency === "USD" || currency === void 0 ? "$" : "";
|
|
1964
|
+
return `${sign}${whole}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
|
|
1965
|
+
}
|
|
1966
|
+
var AGENT_MODEL_SEP;
|
|
1967
|
+
var init_agent_display = __esm({
|
|
1968
|
+
"src/core/agent-display.ts"() {
|
|
1969
|
+
"use strict";
|
|
1970
|
+
AGENT_MODEL_SEP = "\u2022";
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1841
1974
|
// src/cli/session-row.ts
|
|
1842
1975
|
function toRow(s, now = Date.now()) {
|
|
1843
1976
|
return {
|
|
1844
1977
|
session: stripHydraSessionPrefix(s.sessionId),
|
|
1845
1978
|
upstream: s.upstreamSessionId ?? "-",
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
agent: s.agentId ?? "?",
|
|
1979
|
+
state: formatState(s.status, s.attachedClients),
|
|
1980
|
+
agent: formatAgentCell(s.agentId, s.currentModel, s.currentUsage),
|
|
1849
1981
|
age: formatRelativeAge(s.updatedAt, now),
|
|
1850
1982
|
title: s.title ?? "-",
|
|
1851
1983
|
cwd: s.cwd
|
|
1852
1984
|
};
|
|
1853
1985
|
}
|
|
1986
|
+
function formatState(status, clients) {
|
|
1987
|
+
if (status === "cold") {
|
|
1988
|
+
return "COLD";
|
|
1989
|
+
}
|
|
1990
|
+
return `LIVE(${clients})`;
|
|
1991
|
+
}
|
|
1854
1992
|
function computeWidths(rows) {
|
|
1855
1993
|
return {
|
|
1856
1994
|
session: maxLen(HEADER.session, rows.map((r) => r.session)),
|
|
1857
1995
|
upstream: maxLen(HEADER.upstream, rows.map((r) => r.upstream)),
|
|
1858
|
-
|
|
1859
|
-
clients: maxLen(HEADER.clients, rows.map((r) => r.clients)),
|
|
1996
|
+
state: maxLen(HEADER.state, rows.map((r) => r.state)),
|
|
1860
1997
|
agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
|
|
1861
1998
|
age: maxLen(HEADER.age, rows.map((r) => r.age)),
|
|
1862
1999
|
title: maxLen(HEADER.title, rows.map((r) => r.title))
|
|
@@ -1911,8 +2048,7 @@ function formatRow(r, w, maxWidth) {
|
|
|
1911
2048
|
const fixed = [
|
|
1912
2049
|
r.session.padEnd(w.session),
|
|
1913
2050
|
r.upstream.padEnd(w.upstream),
|
|
1914
|
-
r.
|
|
1915
|
-
r.clients.padStart(w.clients),
|
|
2051
|
+
r.state.padEnd(w.state),
|
|
1916
2052
|
r.agent.padEnd(w.agent),
|
|
1917
2053
|
r.age.padStart(w.age)
|
|
1918
2054
|
].join(SEP);
|
|
@@ -1962,12 +2098,12 @@ var HEADER, SEP, MIN_CWD, TITLE_MAX_WIDTH;
|
|
|
1962
2098
|
var init_session_row = __esm({
|
|
1963
2099
|
"src/cli/session-row.ts"() {
|
|
1964
2100
|
"use strict";
|
|
2101
|
+
init_agent_display();
|
|
1965
2102
|
init_session();
|
|
1966
2103
|
HEADER = {
|
|
1967
2104
|
session: "SESSION",
|
|
1968
2105
|
upstream: "UPSTREAM",
|
|
1969
|
-
|
|
1970
|
-
clients: "CLIENTS",
|
|
2106
|
+
state: "STATE",
|
|
1971
2107
|
agent: "AGENT",
|
|
1972
2108
|
age: "AGE",
|
|
1973
2109
|
title: "TITLE",
|
|
@@ -1980,8 +2116,8 @@ var init_session_row = __esm({
|
|
|
1980
2116
|
});
|
|
1981
2117
|
|
|
1982
2118
|
// src/cli/commands/sessions.ts
|
|
1983
|
-
import * as
|
|
1984
|
-
import * as
|
|
2119
|
+
import * as fs12 from "fs/promises";
|
|
2120
|
+
import * as path6 from "path";
|
|
1985
2121
|
async function runSessionsList(opts = {}) {
|
|
1986
2122
|
const config = await loadConfig();
|
|
1987
2123
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -2100,8 +2236,8 @@ async function runSessionsExport(id, outPath) {
|
|
|
2100
2236
|
return;
|
|
2101
2237
|
}
|
|
2102
2238
|
const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
|
|
2103
|
-
await
|
|
2104
|
-
await
|
|
2239
|
+
await fs12.mkdir(path6.dirname(path6.resolve(resolved)), { recursive: true });
|
|
2240
|
+
await fs12.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
2105
2241
|
process.stdout.write(`Wrote ${resolved}
|
|
2106
2242
|
`);
|
|
2107
2243
|
}
|
|
@@ -2116,7 +2252,7 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2116
2252
|
if (file === "-") {
|
|
2117
2253
|
body = await readStdin();
|
|
2118
2254
|
} else {
|
|
2119
|
-
body = await
|
|
2255
|
+
body = await fs12.readFile(file, "utf8");
|
|
2120
2256
|
}
|
|
2121
2257
|
let bundle;
|
|
2122
2258
|
try {
|
|
@@ -2194,11 +2330,11 @@ function isResponse(msg) {
|
|
|
2194
2330
|
return !("method" in msg) && "id" in msg && msg.id !== void 0;
|
|
2195
2331
|
}
|
|
2196
2332
|
async function openWs(url, subprotocols) {
|
|
2197
|
-
return new Promise((
|
|
2333
|
+
return new Promise((resolve5, reject) => {
|
|
2198
2334
|
const ws = new WebSocket(url, subprotocols);
|
|
2199
2335
|
const onOpen = () => {
|
|
2200
2336
|
ws.off("error", onError);
|
|
2201
|
-
|
|
2337
|
+
resolve5(wsToMessageStream(ws));
|
|
2202
2338
|
};
|
|
2203
2339
|
const onError = (err) => {
|
|
2204
2340
|
ws.off("open", onOpen);
|
|
@@ -2269,8 +2405,8 @@ var init_resilient_ws = __esm({
|
|
|
2269
2405
|
throw new Error("resilient ws stream not connected");
|
|
2270
2406
|
}
|
|
2271
2407
|
const id = message.id;
|
|
2272
|
-
const promise = new Promise((
|
|
2273
|
-
this.pendingRequests.set(id, { resolve:
|
|
2408
|
+
const promise = new Promise((resolve5, reject) => {
|
|
2409
|
+
this.pendingRequests.set(id, { resolve: resolve5, reject });
|
|
2274
2410
|
});
|
|
2275
2411
|
try {
|
|
2276
2412
|
await this.current.send(message);
|
|
@@ -2298,8 +2434,8 @@ var init_resilient_ws = __esm({
|
|
|
2298
2434
|
this.bindStream(stream);
|
|
2299
2435
|
const wasFirst = this.firstConnect;
|
|
2300
2436
|
this.firstConnect = false;
|
|
2301
|
-
this.connectGate = new Promise((
|
|
2302
|
-
this.releaseConnectGate =
|
|
2437
|
+
this.connectGate = new Promise((resolve5) => {
|
|
2438
|
+
this.releaseConnectGate = resolve5;
|
|
2303
2439
|
});
|
|
2304
2440
|
try {
|
|
2305
2441
|
if (this.opts.onConnect) {
|
|
@@ -2450,6 +2586,8 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
|
2450
2586
|
status: s.status ?? "live",
|
|
2451
2587
|
upstreamSessionId: s.upstreamSessionId,
|
|
2452
2588
|
agentId: s.agentId,
|
|
2589
|
+
currentModel: s.currentModel,
|
|
2590
|
+
currentUsage: s.currentUsage,
|
|
2453
2591
|
title: s.title
|
|
2454
2592
|
}));
|
|
2455
2593
|
}
|
|
@@ -2652,7 +2790,7 @@ async function pickSession(term, opts) {
|
|
|
2652
2790
|
};
|
|
2653
2791
|
renderFromScratch();
|
|
2654
2792
|
term.hideCursor();
|
|
2655
|
-
return await new Promise((
|
|
2793
|
+
return await new Promise((resolve5) => {
|
|
2656
2794
|
let resolved = false;
|
|
2657
2795
|
const onResize = () => {
|
|
2658
2796
|
if (resolved) {
|
|
@@ -2839,12 +2977,12 @@ async function pickSession(term, opts) {
|
|
|
2839
2977
|
case "KP_ENTER": {
|
|
2840
2978
|
cleanup();
|
|
2841
2979
|
if (selectedIdx === 0) {
|
|
2842
|
-
|
|
2980
|
+
resolve5({ kind: "new" });
|
|
2843
2981
|
return;
|
|
2844
2982
|
}
|
|
2845
2983
|
const session = visible[selectedIdx - 1];
|
|
2846
2984
|
if (!session) {
|
|
2847
|
-
|
|
2985
|
+
resolve5({ kind: "abort" });
|
|
2848
2986
|
return;
|
|
2849
2987
|
}
|
|
2850
2988
|
const result = {
|
|
@@ -2854,13 +2992,13 @@ async function pickSession(term, opts) {
|
|
|
2854
2992
|
if (session.agentId !== void 0) {
|
|
2855
2993
|
result.agentId = session.agentId;
|
|
2856
2994
|
}
|
|
2857
|
-
|
|
2995
|
+
resolve5(result);
|
|
2858
2996
|
return;
|
|
2859
2997
|
}
|
|
2860
2998
|
case "ESCAPE":
|
|
2861
2999
|
case "CTRL_C":
|
|
2862
3000
|
cleanup();
|
|
2863
|
-
|
|
3001
|
+
resolve5({ kind: "abort" });
|
|
2864
3002
|
return;
|
|
2865
3003
|
}
|
|
2866
3004
|
};
|
|
@@ -2892,6 +3030,7 @@ var init_picker = __esm({
|
|
|
2892
3030
|
});
|
|
2893
3031
|
|
|
2894
3032
|
// src/tui/screen.ts
|
|
3033
|
+
import os3 from "os";
|
|
2895
3034
|
import stringWidth from "string-width";
|
|
2896
3035
|
import wrapAnsi from "wrap-ansi";
|
|
2897
3036
|
function formattedLineSig(zone, width, line) {
|
|
@@ -3111,6 +3250,19 @@ function wrapVisible(text, width) {
|
|
|
3111
3250
|
}
|
|
3112
3251
|
return out;
|
|
3113
3252
|
}
|
|
3253
|
+
function shortenHomePath(p) {
|
|
3254
|
+
const home = os3.homedir();
|
|
3255
|
+
if (!home) {
|
|
3256
|
+
return p;
|
|
3257
|
+
}
|
|
3258
|
+
if (p === home) {
|
|
3259
|
+
return "~";
|
|
3260
|
+
}
|
|
3261
|
+
if (p.startsWith(home + "/")) {
|
|
3262
|
+
return "~" + p.slice(home.length);
|
|
3263
|
+
}
|
|
3264
|
+
return p;
|
|
3265
|
+
}
|
|
3114
3266
|
function truncate(text, max) {
|
|
3115
3267
|
if (max <= 0) {
|
|
3116
3268
|
return "";
|
|
@@ -3189,11 +3341,6 @@ function formatTokens(n) {
|
|
|
3189
3341
|
}
|
|
3190
3342
|
return `${n}`;
|
|
3191
3343
|
}
|
|
3192
|
-
function formatCost(amount, currency) {
|
|
3193
|
-
const sign = currency === "USD" || currency === void 0 ? "$" : "";
|
|
3194
|
-
const decimals = amount >= 1 ? 2 : 4;
|
|
3195
|
-
return `${sign}${amount.toFixed(decimals)}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
|
|
3196
|
-
}
|
|
3197
3344
|
function mapKeyName(name) {
|
|
3198
3345
|
switch (name) {
|
|
3199
3346
|
case "ENTER":
|
|
@@ -3260,6 +3407,7 @@ var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS,
|
|
|
3260
3407
|
var init_screen = __esm({
|
|
3261
3408
|
"src/tui/screen.ts"() {
|
|
3262
3409
|
"use strict";
|
|
3410
|
+
init_agent_display();
|
|
3263
3411
|
init_session();
|
|
3264
3412
|
HEADER_ROWS = 2;
|
|
3265
3413
|
BANNER_ROWS = 1;
|
|
@@ -3937,22 +4085,23 @@ var init_screen = __esm({
|
|
|
3937
4085
|
const usage = formatUsage(this.header.usage);
|
|
3938
4086
|
const sid = shortId(this.header.sessionId);
|
|
3939
4087
|
const title = this.header.title?.trim();
|
|
3940
|
-
const
|
|
4088
|
+
const agentCell = formatAgentWithModel(this.header.agent, this.header.model);
|
|
4089
|
+
const cwdDisplay = shortenHomePath(this.header.cwd);
|
|
4090
|
+
const sig = `hdr|${w}|${agentCell}|${cwdDisplay}|${sid}|${title ?? ""}|${usage ?? ""}`;
|
|
3941
4091
|
this.paintRow(1, sig, () => {
|
|
3942
|
-
const fixed = "hydra \xB7 ".length +
|
|
4092
|
+
const fixed = "hydra \xB7 ".length + agentCell.length + " \xB7 ".length + " \xB7 ".length + sid.length + (title ? " \xB7 ".length : 0) + (usage ? usage.length + 3 : 0);
|
|
3943
4093
|
const variableRoom = Math.max(8, w - fixed);
|
|
3944
4094
|
let cwdRoom;
|
|
3945
4095
|
let titleRoom;
|
|
3946
4096
|
if (title) {
|
|
3947
|
-
const
|
|
3948
|
-
|
|
3949
|
-
titleRoom = Math.
|
|
3950
|
-
cwdRoom = Math.max(8, variableRoom - titleRoom);
|
|
4097
|
+
const titleMin = Math.min(title.length, 8);
|
|
4098
|
+
cwdRoom = Math.min(cwdDisplay.length, Math.max(8, variableRoom - titleMin));
|
|
4099
|
+
titleRoom = Math.max(0, variableRoom - cwdRoom);
|
|
3951
4100
|
} else {
|
|
3952
4101
|
titleRoom = 0;
|
|
3953
4102
|
cwdRoom = variableRoom;
|
|
3954
4103
|
}
|
|
3955
|
-
this.term.bold("hydra")(" \xB7 ").cyan.noFormat(
|
|
4104
|
+
this.term.bold("hydra")(" \xB7 ").cyan.noFormat(agentCell)(" \xB7 ").dim.noFormat(truncate(cwdDisplay, cwdRoom))(" \xB7 ").yellow(sid);
|
|
3956
4105
|
if (title) {
|
|
3957
4106
|
this.term(" \xB7 ").bold.noFormat(truncate(title, titleRoom));
|
|
3958
4107
|
}
|
|
@@ -4740,6 +4889,10 @@ var init_input = __esm({
|
|
|
4740
4889
|
});
|
|
4741
4890
|
|
|
4742
4891
|
// src/tui/render-update.ts
|
|
4892
|
+
import stripAnsi from "strip-ansi";
|
|
4893
|
+
function sanitizeWireText(text) {
|
|
4894
|
+
return stripAnsi(text).replace(STRIP_CONTROLS, "");
|
|
4895
|
+
}
|
|
4743
4896
|
function mapUpdate(update) {
|
|
4744
4897
|
if (!update || typeof update !== "object") {
|
|
4745
4898
|
return null;
|
|
@@ -4782,7 +4935,8 @@ function mapUpdate(update) {
|
|
|
4782
4935
|
}
|
|
4783
4936
|
}
|
|
4784
4937
|
function mapSessionInfo(u) {
|
|
4785
|
-
const
|
|
4938
|
+
const rawTitle = readString(u, "title");
|
|
4939
|
+
const title = rawTitle !== void 0 ? sanitizeWireText(rawTitle) : void 0;
|
|
4786
4940
|
const meta = u._meta;
|
|
4787
4941
|
let agentId;
|
|
4788
4942
|
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
@@ -4820,10 +4974,10 @@ function mapAvailableCommands(u) {
|
|
|
4820
4974
|
if (typeof c.name !== "string" || c.name.length === 0) {
|
|
4821
4975
|
continue;
|
|
4822
4976
|
}
|
|
4823
|
-
const
|
|
4824
|
-
const cmd = { name };
|
|
4977
|
+
const rawName = c.name.startsWith("/") ? c.name : `/${c.name}`;
|
|
4978
|
+
const cmd = { name: sanitizeWireText(rawName) };
|
|
4825
4979
|
if (typeof c.description === "string") {
|
|
4826
|
-
cmd.description = c.description;
|
|
4980
|
+
cmd.description = sanitizeWireText(c.description);
|
|
4827
4981
|
}
|
|
4828
4982
|
out.push(cmd);
|
|
4829
4983
|
}
|
|
@@ -4856,7 +5010,7 @@ function mapAgentText(u) {
|
|
|
4856
5010
|
return { kind: "agent-text", text };
|
|
4857
5011
|
}
|
|
4858
5012
|
function mapAgentThought(u) {
|
|
4859
|
-
const text = typeof u.text === "string" ? u.text : extractContentText(u.content);
|
|
5013
|
+
const text = typeof u.text === "string" ? sanitizeWireText(u.text) : extractContentText(u.content);
|
|
4860
5014
|
if (text === null) {
|
|
4861
5015
|
return null;
|
|
4862
5016
|
}
|
|
@@ -4888,7 +5042,8 @@ function mapToolCall(u) {
|
|
|
4888
5042
|
if (!toolCallId) {
|
|
4889
5043
|
return null;
|
|
4890
5044
|
}
|
|
4891
|
-
const
|
|
5045
|
+
const rawTitle = readString(u, "title") ?? readString(u, "name") ?? readString(u, "label") ?? "tool call";
|
|
5046
|
+
const title = sanitizeWireText(rawTitle);
|
|
4892
5047
|
const status = readString(u, "status");
|
|
4893
5048
|
const rawKind = readString(u, "kind");
|
|
4894
5049
|
const event = { kind: "tool-call", toolCallId, title };
|
|
@@ -4905,7 +5060,8 @@ function mapToolCallUpdate(u) {
|
|
|
4905
5060
|
if (!toolCallId) {
|
|
4906
5061
|
return null;
|
|
4907
5062
|
}
|
|
4908
|
-
const
|
|
5063
|
+
const rawTitle = readString(u, "title");
|
|
5064
|
+
const title = rawTitle !== void 0 ? sanitizeWireText(rawTitle) : void 0;
|
|
4909
5065
|
const status = readString(u, "status");
|
|
4910
5066
|
const meaningful = title !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
|
|
4911
5067
|
if (!meaningful) {
|
|
@@ -4931,7 +5087,7 @@ function mapPlan(u) {
|
|
|
4931
5087
|
continue;
|
|
4932
5088
|
}
|
|
4933
5089
|
const e = raw;
|
|
4934
|
-
const content = typeof e.content === "string" ? e.content : void 0;
|
|
5090
|
+
const content = typeof e.content === "string" ? sanitizeWireText(e.content) : void 0;
|
|
4935
5091
|
if (!content) {
|
|
4936
5092
|
continue;
|
|
4937
5093
|
}
|
|
@@ -4951,14 +5107,14 @@ function mapMode(u) {
|
|
|
4951
5107
|
if (!mode) {
|
|
4952
5108
|
return null;
|
|
4953
5109
|
}
|
|
4954
|
-
return { kind: "mode-changed", mode };
|
|
5110
|
+
return { kind: "mode-changed", mode: sanitizeWireText(mode) };
|
|
4955
5111
|
}
|
|
4956
5112
|
function mapModel(u) {
|
|
4957
5113
|
const model = readString(u, "currentModel") ?? readString(u, "model");
|
|
4958
5114
|
if (!model) {
|
|
4959
5115
|
return null;
|
|
4960
5116
|
}
|
|
4961
|
-
return { kind: "model-changed", model };
|
|
5117
|
+
return { kind: "model-changed", model: sanitizeWireText(model) };
|
|
4962
5118
|
}
|
|
4963
5119
|
function mapTurnComplete(u) {
|
|
4964
5120
|
const stopReason = readString(u, "stopReason");
|
|
@@ -4966,17 +5122,17 @@ function mapTurnComplete(u) {
|
|
|
4966
5122
|
}
|
|
4967
5123
|
function extractContentText(content) {
|
|
4968
5124
|
if (typeof content === "string") {
|
|
4969
|
-
return content;
|
|
5125
|
+
return sanitizeWireText(content);
|
|
4970
5126
|
}
|
|
4971
5127
|
if (!content || typeof content !== "object") {
|
|
4972
5128
|
return null;
|
|
4973
5129
|
}
|
|
4974
5130
|
const c = content;
|
|
4975
5131
|
if (c.type === "text" && typeof c.text === "string") {
|
|
4976
|
-
return c.text;
|
|
5132
|
+
return sanitizeWireText(c.text);
|
|
4977
5133
|
}
|
|
4978
5134
|
if (typeof c.text === "string") {
|
|
4979
|
-
return c.text;
|
|
5135
|
+
return sanitizeWireText(c.text);
|
|
4980
5136
|
}
|
|
4981
5137
|
return null;
|
|
4982
5138
|
}
|
|
@@ -5000,9 +5156,11 @@ function readString(u, key) {
|
|
|
5000
5156
|
const v = u[key];
|
|
5001
5157
|
return typeof v === "string" ? v : void 0;
|
|
5002
5158
|
}
|
|
5159
|
+
var STRIP_CONTROLS;
|
|
5003
5160
|
var init_render_update = __esm({
|
|
5004
5161
|
"src/tui/render-update.ts"() {
|
|
5005
5162
|
"use strict";
|
|
5163
|
+
STRIP_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
5006
5164
|
}
|
|
5007
5165
|
});
|
|
5008
5166
|
|
|
@@ -5456,10 +5614,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5456
5614
|
if (pendingPermission.toolCallId && toolCallId && pendingPermission.toolCallId !== toolCallId) {
|
|
5457
5615
|
return;
|
|
5458
5616
|
}
|
|
5459
|
-
const
|
|
5617
|
+
const resolve5 = pendingPermission.resolve;
|
|
5460
5618
|
pendingPermission = null;
|
|
5461
5619
|
screen.setPermissionPrompt(null);
|
|
5462
|
-
|
|
5620
|
+
resolve5(result ?? { outcome: { outcome: "cancelled" } });
|
|
5463
5621
|
};
|
|
5464
5622
|
const maybeDismissPermissionByToolUpdate = (update) => {
|
|
5465
5623
|
if (!pendingPermission?.toolCallId) {
|
|
@@ -5492,20 +5650,26 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5492
5650
|
if (!pendingPermission) {
|
|
5493
5651
|
return;
|
|
5494
5652
|
}
|
|
5495
|
-
const { options, resolve:
|
|
5653
|
+
const { options, resolve: resolve5 } = pendingPermission;
|
|
5496
5654
|
pendingPermission = null;
|
|
5497
5655
|
screen.setPermissionPrompt(null);
|
|
5498
5656
|
if (optionId === null) {
|
|
5499
|
-
|
|
5657
|
+
resolve5({ outcome: { outcome: "cancelled" } });
|
|
5500
5658
|
return;
|
|
5501
5659
|
}
|
|
5502
|
-
|
|
5660
|
+
resolve5({ outcome: { outcome: "selected", optionId } });
|
|
5503
5661
|
void options;
|
|
5504
5662
|
};
|
|
5505
5663
|
conn.onRequest("session/request_permission", async (params) => {
|
|
5506
5664
|
const p = params ?? {};
|
|
5507
|
-
const
|
|
5508
|
-
const
|
|
5665
|
+
const rawOptions = Array.isArray(p.options) ? p.options : [];
|
|
5666
|
+
const options = rawOptions.map((o) => ({
|
|
5667
|
+
optionId: o.optionId,
|
|
5668
|
+
name: sanitizeWireText(o.name ?? ""),
|
|
5669
|
+
...o.kind !== void 0 ? { kind: o.kind } : {}
|
|
5670
|
+
}));
|
|
5671
|
+
const rawTitle = p.toolCall?.title ?? p.toolCall?.name ?? "tool";
|
|
5672
|
+
const title = sanitizeWireText(rawTitle);
|
|
5509
5673
|
const toolCallId = p.toolCall?.toolCallId;
|
|
5510
5674
|
if (options.length === 0) {
|
|
5511
5675
|
screen.appendLines([
|
|
@@ -5517,12 +5681,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5517
5681
|
]);
|
|
5518
5682
|
return { outcome: { outcome: "cancelled" } };
|
|
5519
5683
|
}
|
|
5520
|
-
return new Promise((
|
|
5684
|
+
return new Promise((resolve5) => {
|
|
5521
5685
|
pendingPermission = {
|
|
5522
5686
|
title,
|
|
5523
5687
|
options,
|
|
5524
5688
|
selectedIndex: 0,
|
|
5525
|
-
resolve:
|
|
5689
|
+
resolve: resolve5,
|
|
5526
5690
|
toolCallId
|
|
5527
5691
|
};
|
|
5528
5692
|
refreshPermissionPrompt();
|
|
@@ -5775,17 +5939,15 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5775
5939
|
agent: headerName,
|
|
5776
5940
|
cwd: resolvedCwd,
|
|
5777
5941
|
sessionId: resolvedSessionId,
|
|
5778
|
-
title: resolvedTitle
|
|
5942
|
+
title: resolvedTitle,
|
|
5943
|
+
model: initialModel
|
|
5779
5944
|
});
|
|
5780
5945
|
if (initialMode) {
|
|
5781
5946
|
screen.appendLines(formatEvent({ kind: "mode-changed", mode: initialMode }));
|
|
5782
5947
|
}
|
|
5783
|
-
if (initialModel) {
|
|
5784
|
-
screen.appendLines(formatEvent({ kind: "model-changed", model: initialModel }));
|
|
5785
|
-
}
|
|
5786
5948
|
let finishSession = null;
|
|
5787
|
-
const sessionDone = new Promise((
|
|
5788
|
-
finishSession =
|
|
5949
|
+
const sessionDone = new Promise((resolve5) => {
|
|
5950
|
+
finishSession = resolve5;
|
|
5789
5951
|
});
|
|
5790
5952
|
const cancelRemoteTurn = () => {
|
|
5791
5953
|
conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
|
|
@@ -6358,6 +6520,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6358
6520
|
renderToolsBlock();
|
|
6359
6521
|
return;
|
|
6360
6522
|
}
|
|
6523
|
+
if (event.kind === "model-changed") {
|
|
6524
|
+
screen.setHeader({ model: event.model });
|
|
6525
|
+
}
|
|
6361
6526
|
const formatted = formatEvent(event);
|
|
6362
6527
|
if (formatted.length > 0) {
|
|
6363
6528
|
screen.appendLines(formatted);
|
|
@@ -6407,10 +6572,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6407
6572
|
}
|
|
6408
6573
|
const resetInFlightUiState = () => {
|
|
6409
6574
|
if (pendingPermission) {
|
|
6410
|
-
const
|
|
6575
|
+
const resolve5 = pendingPermission.resolve;
|
|
6411
6576
|
pendingPermission = null;
|
|
6412
6577
|
screen.setPermissionPrompt(null);
|
|
6413
|
-
|
|
6578
|
+
resolve5({ outcome: { outcome: "cancelled" } });
|
|
6414
6579
|
}
|
|
6415
6580
|
closeAgentText();
|
|
6416
6581
|
if (toolsBlockStartedAt !== null) {
|
|
@@ -6627,7 +6792,7 @@ var init_tui = __esm({
|
|
|
6627
6792
|
// src/cli.ts
|
|
6628
6793
|
import { readFileSync } from "fs";
|
|
6629
6794
|
import { fileURLToPath } from "url";
|
|
6630
|
-
import { dirname as
|
|
6795
|
+
import { dirname as dirname4, resolve as resolve4 } from "path";
|
|
6631
6796
|
|
|
6632
6797
|
// src/cli/parse-args.ts
|
|
6633
6798
|
function parseArgs(argv) {
|
|
@@ -6727,13 +6892,13 @@ New token: ${newToken}
|
|
|
6727
6892
|
// src/cli/commands/daemon.ts
|
|
6728
6893
|
init_paths();
|
|
6729
6894
|
init_config();
|
|
6730
|
-
import * as
|
|
6895
|
+
import * as fsp5 from "fs/promises";
|
|
6731
6896
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
6732
6897
|
|
|
6733
6898
|
// src/daemon/server.ts
|
|
6734
6899
|
init_config();
|
|
6735
|
-
import * as
|
|
6736
|
-
import * as
|
|
6900
|
+
import * as fs10 from "fs";
|
|
6901
|
+
import * as fsp3 from "fs/promises";
|
|
6737
6902
|
import Fastify from "fastify";
|
|
6738
6903
|
import websocketPlugin from "@fastify/websocket";
|
|
6739
6904
|
import pino from "pino";
|
|
@@ -6741,8 +6906,214 @@ import createPinoRoll from "pino-roll";
|
|
|
6741
6906
|
|
|
6742
6907
|
// src/core/registry.ts
|
|
6743
6908
|
init_paths();
|
|
6744
|
-
import * as
|
|
6909
|
+
import * as fs4 from "fs/promises";
|
|
6745
6910
|
import { z as z2 } from "zod";
|
|
6911
|
+
|
|
6912
|
+
// src/core/binary-install.ts
|
|
6913
|
+
init_paths();
|
|
6914
|
+
import * as fs3 from "fs";
|
|
6915
|
+
import * as fsp from "fs/promises";
|
|
6916
|
+
import * as path2 from "path";
|
|
6917
|
+
import { spawn } from "child_process";
|
|
6918
|
+
import { Readable } from "stream";
|
|
6919
|
+
function currentPlatformKey() {
|
|
6920
|
+
const osPart = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "windows" : void 0;
|
|
6921
|
+
const archPart = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : void 0;
|
|
6922
|
+
if (!osPart || !archPart) {
|
|
6923
|
+
return void 0;
|
|
6924
|
+
}
|
|
6925
|
+
return `${osPart}-${archPart}`;
|
|
6926
|
+
}
|
|
6927
|
+
function pickBinaryTarget(distribution, platformKey = currentPlatformKey()) {
|
|
6928
|
+
if (!platformKey) {
|
|
6929
|
+
return void 0;
|
|
6930
|
+
}
|
|
6931
|
+
return distribution[platformKey];
|
|
6932
|
+
}
|
|
6933
|
+
var logSink = (msg) => {
|
|
6934
|
+
process.stderr.write(msg + "\n");
|
|
6935
|
+
};
|
|
6936
|
+
function setBinaryInstallLogger(log) {
|
|
6937
|
+
logSink = log ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
6938
|
+
}
|
|
6939
|
+
async function ensureBinary(args) {
|
|
6940
|
+
if (!args.target.archive) {
|
|
6941
|
+
throw new Error(
|
|
6942
|
+
`Agent ${args.agentId} has no archive URL for ${currentPlatformKey() ?? "this platform"}`
|
|
6943
|
+
);
|
|
6944
|
+
}
|
|
6945
|
+
if (!args.target.cmd) {
|
|
6946
|
+
throw new Error(`Agent ${args.agentId} has no cmd in its binary target`);
|
|
6947
|
+
}
|
|
6948
|
+
const platformKey = currentPlatformKey();
|
|
6949
|
+
if (!platformKey) {
|
|
6950
|
+
throw new Error(
|
|
6951
|
+
`Agent ${args.agentId}: cannot determine platform key for ${process.platform}/${process.arch}`
|
|
6952
|
+
);
|
|
6953
|
+
}
|
|
6954
|
+
const installDir = paths.agentInstallDir(
|
|
6955
|
+
args.agentId,
|
|
6956
|
+
platformKey,
|
|
6957
|
+
args.version
|
|
6958
|
+
);
|
|
6959
|
+
const cmdPath = path2.resolve(installDir, args.target.cmd);
|
|
6960
|
+
if (await fileExists(cmdPath)) {
|
|
6961
|
+
return cmdPath;
|
|
6962
|
+
}
|
|
6963
|
+
await downloadAndExtract({
|
|
6964
|
+
agentId: args.agentId,
|
|
6965
|
+
archiveUrl: args.target.archive,
|
|
6966
|
+
installDir
|
|
6967
|
+
});
|
|
6968
|
+
if (!await fileExists(cmdPath)) {
|
|
6969
|
+
throw new Error(
|
|
6970
|
+
`Agent ${args.agentId}: extracted archive did not contain ${args.target.cmd} (looked in ${installDir})`
|
|
6971
|
+
);
|
|
6972
|
+
}
|
|
6973
|
+
if (process.platform !== "win32") {
|
|
6974
|
+
await fsp.chmod(cmdPath, 493).catch(() => void 0);
|
|
6975
|
+
}
|
|
6976
|
+
return cmdPath;
|
|
6977
|
+
}
|
|
6978
|
+
async function downloadAndExtract(args) {
|
|
6979
|
+
await fsp.mkdir(path2.dirname(args.installDir), { recursive: true });
|
|
6980
|
+
const tempDir = await fsp.mkdtemp(`${args.installDir}.partial-`);
|
|
6981
|
+
try {
|
|
6982
|
+
logSink(`hydra-acp: downloading ${args.agentId} from ${args.archiveUrl}`);
|
|
6983
|
+
const archivePath = await downloadTo({
|
|
6984
|
+
url: args.archiveUrl,
|
|
6985
|
+
dir: tempDir,
|
|
6986
|
+
agentId: args.agentId
|
|
6987
|
+
});
|
|
6988
|
+
logSink(`hydra-acp: extracting ${args.agentId}`);
|
|
6989
|
+
await extract(archivePath, tempDir);
|
|
6990
|
+
await fsp.unlink(archivePath).catch(() => void 0);
|
|
6991
|
+
try {
|
|
6992
|
+
await fsp.rename(tempDir, args.installDir);
|
|
6993
|
+
} catch (err) {
|
|
6994
|
+
const e = err;
|
|
6995
|
+
if ((e.code === "EEXIST" || e.code === "ENOTEMPTY") && await fileExists(args.installDir)) {
|
|
6996
|
+
await fsp.rm(tempDir, { recursive: true, force: true }).catch(
|
|
6997
|
+
() => void 0
|
|
6998
|
+
);
|
|
6999
|
+
return;
|
|
7000
|
+
}
|
|
7001
|
+
throw err;
|
|
7002
|
+
}
|
|
7003
|
+
logSink(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
7004
|
+
} catch (err) {
|
|
7005
|
+
await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
7006
|
+
throw err;
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
async function downloadTo(args) {
|
|
7010
|
+
const filename = inferArchiveName(args.url);
|
|
7011
|
+
const dest = path2.join(args.dir, filename);
|
|
7012
|
+
const response = await fetch(args.url, { redirect: "follow" });
|
|
7013
|
+
if (!response.ok || !response.body) {
|
|
7014
|
+
throw new Error(
|
|
7015
|
+
`Failed to download ${args.url}: HTTP ${response.status} ${response.statusText}`
|
|
7016
|
+
);
|
|
7017
|
+
}
|
|
7018
|
+
const total = Number(response.headers.get("content-length") ?? "0");
|
|
7019
|
+
const out = fs3.createWriteStream(dest);
|
|
7020
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
7021
|
+
let received = 0;
|
|
7022
|
+
let lastEmit = Date.now();
|
|
7023
|
+
const EMIT_INTERVAL_MS = 2e3;
|
|
7024
|
+
nodeStream.on("data", (chunk) => {
|
|
7025
|
+
received += chunk.length;
|
|
7026
|
+
const now = Date.now();
|
|
7027
|
+
if (now - lastEmit < EMIT_INTERVAL_MS) {
|
|
7028
|
+
return;
|
|
7029
|
+
}
|
|
7030
|
+
lastEmit = now;
|
|
7031
|
+
logSink(formatProgress(args.agentId, received, total));
|
|
7032
|
+
});
|
|
7033
|
+
await new Promise((resolve5, reject) => {
|
|
7034
|
+
nodeStream.on("error", reject);
|
|
7035
|
+
out.on("error", reject);
|
|
7036
|
+
out.on("finish", () => resolve5());
|
|
7037
|
+
nodeStream.pipe(out);
|
|
7038
|
+
});
|
|
7039
|
+
logSink(formatProgress(
|
|
7040
|
+
args.agentId,
|
|
7041
|
+
received,
|
|
7042
|
+
total,
|
|
7043
|
+
/* done */
|
|
7044
|
+
true
|
|
7045
|
+
));
|
|
7046
|
+
return dest;
|
|
7047
|
+
}
|
|
7048
|
+
function formatProgress(agentId, received, total, done = false) {
|
|
7049
|
+
const rxMb = (received / 1e6).toFixed(1);
|
|
7050
|
+
if (total > 0) {
|
|
7051
|
+
const totalMb = (total / 1e6).toFixed(1);
|
|
7052
|
+
const pct = Math.min(100, Math.floor(received / total * 100));
|
|
7053
|
+
const tag2 = done ? "downloaded" : "downloading";
|
|
7054
|
+
return `hydra-acp: ${tag2} ${agentId} ${rxMb}/${totalMb} MB (${pct}%)`;
|
|
7055
|
+
}
|
|
7056
|
+
const tag = done ? "downloaded" : "downloading";
|
|
7057
|
+
return `hydra-acp: ${tag} ${agentId} ${rxMb} MB`;
|
|
7058
|
+
}
|
|
7059
|
+
function inferArchiveName(url) {
|
|
7060
|
+
const u = new URL(url);
|
|
7061
|
+
const base = path2.posix.basename(u.pathname);
|
|
7062
|
+
return base || "archive";
|
|
7063
|
+
}
|
|
7064
|
+
async function extract(archivePath, dest) {
|
|
7065
|
+
const lower = archivePath.toLowerCase();
|
|
7066
|
+
if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz") || lower.endsWith(".tar")) {
|
|
7067
|
+
await run("tar", ["-xf", archivePath, "-C", dest]);
|
|
7068
|
+
return;
|
|
7069
|
+
}
|
|
7070
|
+
if (lower.endsWith(".zip")) {
|
|
7071
|
+
if (await hasCommand("unzip")) {
|
|
7072
|
+
await run("unzip", ["-q", archivePath, "-d", dest]);
|
|
7073
|
+
return;
|
|
7074
|
+
}
|
|
7075
|
+
await run("tar", ["-xf", archivePath, "-C", dest]);
|
|
7076
|
+
return;
|
|
7077
|
+
}
|
|
7078
|
+
throw new Error(`Unsupported archive format: ${archivePath}`);
|
|
7079
|
+
}
|
|
7080
|
+
function run(cmd, args) {
|
|
7081
|
+
return new Promise((resolve5, reject) => {
|
|
7082
|
+
const child = spawn(cmd, args, {
|
|
7083
|
+
stdio: ["ignore", "ignore", "inherit"]
|
|
7084
|
+
});
|
|
7085
|
+
child.on("error", reject);
|
|
7086
|
+
child.on("exit", (code, signal) => {
|
|
7087
|
+
if (code === 0) {
|
|
7088
|
+
resolve5();
|
|
7089
|
+
return;
|
|
7090
|
+
}
|
|
7091
|
+
reject(
|
|
7092
|
+
new Error(
|
|
7093
|
+
`${cmd} ${args.join(" ")} exited with ${code !== null ? `code ${code}` : `signal ${signal}`}`
|
|
7094
|
+
)
|
|
7095
|
+
);
|
|
7096
|
+
});
|
|
7097
|
+
});
|
|
7098
|
+
}
|
|
7099
|
+
async function hasCommand(name) {
|
|
7100
|
+
return new Promise((resolve5) => {
|
|
7101
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
7102
|
+
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
7103
|
+
child.on("error", () => resolve5(false));
|
|
7104
|
+
child.on("exit", (code) => resolve5(code === 0));
|
|
7105
|
+
});
|
|
7106
|
+
}
|
|
7107
|
+
async function fileExists(p) {
|
|
7108
|
+
try {
|
|
7109
|
+
await fsp.access(p);
|
|
7110
|
+
return true;
|
|
7111
|
+
} catch {
|
|
7112
|
+
return false;
|
|
7113
|
+
}
|
|
7114
|
+
}
|
|
7115
|
+
|
|
7116
|
+
// src/core/registry.ts
|
|
6746
7117
|
var NpxDistribution = z2.object({
|
|
6747
7118
|
package: z2.string(),
|
|
6748
7119
|
args: z2.array(z2.string()).optional(),
|
|
@@ -6750,7 +7121,9 @@ var NpxDistribution = z2.object({
|
|
|
6750
7121
|
});
|
|
6751
7122
|
var BinaryTarget = z2.object({
|
|
6752
7123
|
archive: z2.string().url().optional(),
|
|
6753
|
-
cmd: z2.string().optional()
|
|
7124
|
+
cmd: z2.string().optional(),
|
|
7125
|
+
args: z2.array(z2.string()).optional(),
|
|
7126
|
+
env: z2.record(z2.string()).optional()
|
|
6754
7127
|
});
|
|
6755
7128
|
var BinaryDistribution = z2.object({
|
|
6756
7129
|
"darwin-aarch64": BinaryTarget.optional(),
|
|
@@ -6839,34 +7212,59 @@ var Registry = class {
|
|
|
6839
7212
|
if (!response.ok) {
|
|
6840
7213
|
throw new Error(`Registry fetch failed: HTTP ${response.status}`);
|
|
6841
7214
|
}
|
|
6842
|
-
const
|
|
6843
|
-
const data = RegistryDocument.parse(
|
|
6844
|
-
return { fetchedAt: Date.now(), data };
|
|
7215
|
+
const raw = await response.json();
|
|
7216
|
+
const data = RegistryDocument.parse(raw);
|
|
7217
|
+
return { fetchedAt: Date.now(), raw, data };
|
|
6845
7218
|
}
|
|
6846
7219
|
async readDiskCache() {
|
|
7220
|
+
let text;
|
|
6847
7221
|
try {
|
|
6848
|
-
|
|
6849
|
-
const parsed = JSON.parse(raw);
|
|
6850
|
-
if (typeof parsed.fetchedAt === "number" && parsed.data && Array.isArray(parsed.data.agents)) {
|
|
6851
|
-
return parsed;
|
|
6852
|
-
}
|
|
7222
|
+
text = await fs4.readFile(paths.registryCache(), "utf8");
|
|
6853
7223
|
} catch (err) {
|
|
6854
7224
|
const e = err;
|
|
6855
|
-
if (e.code
|
|
6856
|
-
|
|
7225
|
+
if (e.code === "ENOENT") {
|
|
7226
|
+
return void 0;
|
|
6857
7227
|
}
|
|
7228
|
+
throw err;
|
|
7229
|
+
}
|
|
7230
|
+
try {
|
|
7231
|
+
const parsed = JSON.parse(text);
|
|
7232
|
+
if (typeof parsed.fetchedAt !== "number" || parsed.data === void 0) {
|
|
7233
|
+
return void 0;
|
|
7234
|
+
}
|
|
7235
|
+
const data = RegistryDocument.parse(parsed.data);
|
|
7236
|
+
return { fetchedAt: parsed.fetchedAt, raw: parsed.data, data };
|
|
7237
|
+
} catch {
|
|
7238
|
+
return void 0;
|
|
6858
7239
|
}
|
|
6859
|
-
return void 0;
|
|
6860
7240
|
}
|
|
7241
|
+
// Atomic write: dump to a sibling temp path, then rename onto the
|
|
7242
|
+
// target. POSIX rename is atomic within a filesystem, so readers
|
|
7243
|
+
// either see the old file or the fully-written new file — never a
|
|
7244
|
+
// truncated middle. This also makes simultaneous writers safe
|
|
7245
|
+
// without a lock file: the loser of the rename race just gets its
|
|
7246
|
+
// version replaced by the winner's.
|
|
6861
7247
|
async writeDiskCache(cache) {
|
|
6862
|
-
await
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
7248
|
+
await fs4.mkdir(paths.home(), { recursive: true });
|
|
7249
|
+
const final = paths.registryCache();
|
|
7250
|
+
const tmp = `${final}.tmp-${process.pid}-${randSuffix()}`;
|
|
7251
|
+
const body = JSON.stringify(
|
|
7252
|
+
{ fetchedAt: cache.fetchedAt, data: cache.raw },
|
|
7253
|
+
null,
|
|
7254
|
+
2
|
|
7255
|
+
) + "\n";
|
|
7256
|
+
try {
|
|
7257
|
+
await fs4.writeFile(tmp, body, "utf8");
|
|
7258
|
+
await fs4.rename(tmp, final);
|
|
7259
|
+
} catch (err) {
|
|
7260
|
+
await fs4.unlink(tmp).catch(() => void 0);
|
|
7261
|
+
throw err;
|
|
7262
|
+
}
|
|
6868
7263
|
}
|
|
6869
7264
|
};
|
|
7265
|
+
function randSuffix() {
|
|
7266
|
+
return Math.random().toString(36).slice(2, 10);
|
|
7267
|
+
}
|
|
6870
7268
|
function npxPackageBasename(agent) {
|
|
6871
7269
|
const pkg = agent.distribution.npx?.package;
|
|
6872
7270
|
if (!pkg) {
|
|
@@ -6877,7 +7275,7 @@ function npxPackageBasename(agent) {
|
|
|
6877
7275
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
6878
7276
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
6879
7277
|
}
|
|
6880
|
-
function planSpawn(agent, extraArgs = []) {
|
|
7278
|
+
async function planSpawn(agent, extraArgs = []) {
|
|
6881
7279
|
if (agent.distribution.npx) {
|
|
6882
7280
|
const npx = agent.distribution.npx;
|
|
6883
7281
|
const args = ["-y", npx.package, ...npx.args ?? [], ...extraArgs];
|
|
@@ -6888,9 +7286,22 @@ function planSpawn(agent, extraArgs = []) {
|
|
|
6888
7286
|
};
|
|
6889
7287
|
}
|
|
6890
7288
|
if (agent.distribution.binary) {
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
7289
|
+
const target = pickBinaryTarget(agent.distribution.binary);
|
|
7290
|
+
if (!target) {
|
|
7291
|
+
throw new Error(
|
|
7292
|
+
`Agent ${agent.id} has no binary distribution for ${currentPlatformKey() ?? "this platform"}.`
|
|
7293
|
+
);
|
|
7294
|
+
}
|
|
7295
|
+
const cmdPath = await ensureBinary({
|
|
7296
|
+
agentId: agent.id,
|
|
7297
|
+
version: agent.version ?? "current",
|
|
7298
|
+
target
|
|
7299
|
+
});
|
|
7300
|
+
return {
|
|
7301
|
+
command: cmdPath,
|
|
7302
|
+
args: [...target.args ?? [], ...extraArgs],
|
|
7303
|
+
env: target.env ?? {}
|
|
7304
|
+
};
|
|
6894
7305
|
}
|
|
6895
7306
|
if (agent.distribution.uvx) {
|
|
6896
7307
|
const uvx = agent.distribution.uvx;
|
|
@@ -6905,11 +7316,11 @@ function planSpawn(agent, extraArgs = []) {
|
|
|
6905
7316
|
}
|
|
6906
7317
|
|
|
6907
7318
|
// src/core/session-manager.ts
|
|
6908
|
-
import * as
|
|
7319
|
+
import * as fs8 from "fs/promises";
|
|
6909
7320
|
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
6910
7321
|
|
|
6911
7322
|
// src/core/agent-instance.ts
|
|
6912
|
-
import { spawn } from "child_process";
|
|
7323
|
+
import { spawn as spawn2 } from "child_process";
|
|
6913
7324
|
|
|
6914
7325
|
// src/acp/framing.ts
|
|
6915
7326
|
init_types();
|
|
@@ -6965,13 +7376,13 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
6965
7376
|
throw new Error("stream is closed");
|
|
6966
7377
|
}
|
|
6967
7378
|
const line = JSON.stringify(message) + "\n";
|
|
6968
|
-
await new Promise((
|
|
7379
|
+
await new Promise((resolve5, reject) => {
|
|
6969
7380
|
stdin.write(line, (err) => {
|
|
6970
7381
|
if (err) {
|
|
6971
7382
|
reject(err);
|
|
6972
7383
|
return;
|
|
6973
7384
|
}
|
|
6974
|
-
|
|
7385
|
+
resolve5();
|
|
6975
7386
|
});
|
|
6976
7387
|
});
|
|
6977
7388
|
},
|
|
@@ -7023,7 +7434,7 @@ var AgentInstance = class _AgentInstance {
|
|
|
7023
7434
|
...opts.plan.env,
|
|
7024
7435
|
...opts.extraEnv ?? {}
|
|
7025
7436
|
};
|
|
7026
|
-
const child =
|
|
7437
|
+
const child = spawn2(opts.plan.command, opts.plan.args, {
|
|
7027
7438
|
cwd: opts.cwd,
|
|
7028
7439
|
env,
|
|
7029
7440
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7050,8 +7461,8 @@ init_session();
|
|
|
7050
7461
|
|
|
7051
7462
|
// src/core/session-store.ts
|
|
7052
7463
|
init_paths();
|
|
7053
|
-
import * as
|
|
7054
|
-
import * as
|
|
7464
|
+
import * as fs5 from "fs/promises";
|
|
7465
|
+
import * as path3 from "path";
|
|
7055
7466
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
7056
7467
|
import { z as z4 } from "zod";
|
|
7057
7468
|
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -7064,6 +7475,12 @@ var PersistedAgentCommand = z4.object({
|
|
|
7064
7475
|
name: z4.string(),
|
|
7065
7476
|
description: z4.string().optional()
|
|
7066
7477
|
});
|
|
7478
|
+
var PersistedUsage = z4.object({
|
|
7479
|
+
used: z4.number().optional(),
|
|
7480
|
+
size: z4.number().optional(),
|
|
7481
|
+
costAmount: z4.number().optional(),
|
|
7482
|
+
costCurrency: z4.string().optional()
|
|
7483
|
+
});
|
|
7067
7484
|
var SessionRecord = z4.object({
|
|
7068
7485
|
version: z4.literal(1),
|
|
7069
7486
|
sessionId: z4.string(),
|
|
@@ -7091,6 +7508,7 @@ var SessionRecord = z4.object({
|
|
|
7091
7508
|
// replay of a snapshot-shaped notification.
|
|
7092
7509
|
currentModel: z4.string().optional(),
|
|
7093
7510
|
currentMode: z4.string().optional(),
|
|
7511
|
+
currentUsage: PersistedUsage.optional(),
|
|
7094
7512
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
7095
7513
|
createdAt: z4.string(),
|
|
7096
7514
|
updatedAt: z4.string()
|
|
@@ -7104,9 +7522,9 @@ function assertSafeId(id) {
|
|
|
7104
7522
|
var SessionStore = class {
|
|
7105
7523
|
async write(record) {
|
|
7106
7524
|
assertSafeId(record.sessionId);
|
|
7107
|
-
await
|
|
7525
|
+
await fs5.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
7108
7526
|
const full = { version: 1, ...record };
|
|
7109
|
-
await
|
|
7527
|
+
await fs5.writeFile(
|
|
7110
7528
|
paths.sessionFile(record.sessionId),
|
|
7111
7529
|
JSON.stringify(full, null, 2) + "\n",
|
|
7112
7530
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -7118,7 +7536,7 @@ var SessionStore = class {
|
|
|
7118
7536
|
}
|
|
7119
7537
|
let raw;
|
|
7120
7538
|
try {
|
|
7121
|
-
raw = await
|
|
7539
|
+
raw = await fs5.readFile(paths.sessionFile(sessionId), "utf8");
|
|
7122
7540
|
} catch (err) {
|
|
7123
7541
|
const e = err;
|
|
7124
7542
|
if (e.code === "ENOENT") {
|
|
@@ -7137,7 +7555,7 @@ var SessionStore = class {
|
|
|
7137
7555
|
return;
|
|
7138
7556
|
}
|
|
7139
7557
|
try {
|
|
7140
|
-
await
|
|
7558
|
+
await fs5.unlink(paths.sessionFile(sessionId));
|
|
7141
7559
|
} catch (err) {
|
|
7142
7560
|
const e = err;
|
|
7143
7561
|
if (e.code !== "ENOENT") {
|
|
@@ -7145,7 +7563,7 @@ var SessionStore = class {
|
|
|
7145
7563
|
}
|
|
7146
7564
|
}
|
|
7147
7565
|
try {
|
|
7148
|
-
await
|
|
7566
|
+
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
7149
7567
|
} catch (err) {
|
|
7150
7568
|
const e = err;
|
|
7151
7569
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -7175,7 +7593,7 @@ var SessionStore = class {
|
|
|
7175
7593
|
async list() {
|
|
7176
7594
|
let entries;
|
|
7177
7595
|
try {
|
|
7178
|
-
entries = await
|
|
7596
|
+
entries = await fs5.readdir(paths.sessionsDir());
|
|
7179
7597
|
} catch (err) {
|
|
7180
7598
|
const e = err;
|
|
7181
7599
|
if (e.code === "ENOENT") {
|
|
@@ -7206,6 +7624,7 @@ function recordFromMemorySession(args) {
|
|
|
7206
7624
|
agentArgs: args.agentArgs,
|
|
7207
7625
|
currentModel: args.currentModel,
|
|
7208
7626
|
currentMode: args.currentMode,
|
|
7627
|
+
currentUsage: args.currentUsage,
|
|
7209
7628
|
agentCommands: args.agentCommands,
|
|
7210
7629
|
createdAt: args.createdAt ?? now,
|
|
7211
7630
|
updatedAt: args.updatedAt ?? now
|
|
@@ -7214,7 +7633,7 @@ function recordFromMemorySession(args) {
|
|
|
7214
7633
|
|
|
7215
7634
|
// src/core/history-store.ts
|
|
7216
7635
|
init_paths();
|
|
7217
|
-
import * as
|
|
7636
|
+
import * as fs6 from "fs/promises";
|
|
7218
7637
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
7219
7638
|
var MAX_ENTRIES = 1e3;
|
|
7220
7639
|
var HistoryStore = class {
|
|
@@ -7227,9 +7646,9 @@ var HistoryStore = class {
|
|
|
7227
7646
|
return;
|
|
7228
7647
|
}
|
|
7229
7648
|
return this.enqueue(sessionId, async () => {
|
|
7230
|
-
await
|
|
7649
|
+
await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
7231
7650
|
const line = JSON.stringify(entry) + "\n";
|
|
7232
|
-
await
|
|
7651
|
+
await fs6.appendFile(paths.historyFile(sessionId), line, {
|
|
7233
7652
|
encoding: "utf8",
|
|
7234
7653
|
mode: 384
|
|
7235
7654
|
});
|
|
@@ -7240,9 +7659,9 @@ var HistoryStore = class {
|
|
|
7240
7659
|
return;
|
|
7241
7660
|
}
|
|
7242
7661
|
return this.enqueue(sessionId, async () => {
|
|
7243
|
-
await
|
|
7662
|
+
await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
7244
7663
|
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
7245
|
-
await
|
|
7664
|
+
await fs6.writeFile(paths.historyFile(sessionId), body, {
|
|
7246
7665
|
encoding: "utf8",
|
|
7247
7666
|
mode: 384
|
|
7248
7667
|
});
|
|
@@ -7259,7 +7678,7 @@ var HistoryStore = class {
|
|
|
7259
7678
|
return this.enqueue(sessionId, async () => {
|
|
7260
7679
|
let raw;
|
|
7261
7680
|
try {
|
|
7262
|
-
raw = await
|
|
7681
|
+
raw = await fs6.readFile(paths.historyFile(sessionId), "utf8");
|
|
7263
7682
|
} catch (err) {
|
|
7264
7683
|
const e = err;
|
|
7265
7684
|
if (e.code === "ENOENT") {
|
|
@@ -7272,7 +7691,7 @@ var HistoryStore = class {
|
|
|
7272
7691
|
return;
|
|
7273
7692
|
}
|
|
7274
7693
|
const trimmed = lines.slice(-maxEntries);
|
|
7275
|
-
await
|
|
7694
|
+
await fs6.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
7276
7695
|
encoding: "utf8",
|
|
7277
7696
|
mode: 384
|
|
7278
7697
|
});
|
|
@@ -7288,7 +7707,7 @@ var HistoryStore = class {
|
|
|
7288
7707
|
}
|
|
7289
7708
|
let raw;
|
|
7290
7709
|
try {
|
|
7291
|
-
raw = await
|
|
7710
|
+
raw = await fs6.readFile(paths.historyFile(sessionId), "utf8");
|
|
7292
7711
|
} catch (err) {
|
|
7293
7712
|
const e = err;
|
|
7294
7713
|
if (e.code === "ENOENT") {
|
|
@@ -7334,7 +7753,7 @@ var HistoryStore = class {
|
|
|
7334
7753
|
}
|
|
7335
7754
|
return this.enqueue(sessionId, async () => {
|
|
7336
7755
|
try {
|
|
7337
|
-
await
|
|
7756
|
+
await fs6.unlink(paths.historyFile(sessionId));
|
|
7338
7757
|
} catch (err) {
|
|
7339
7758
|
const e = err;
|
|
7340
7759
|
if (e.code !== "ENOENT") {
|
|
@@ -7342,7 +7761,7 @@ var HistoryStore = class {
|
|
|
7342
7761
|
}
|
|
7343
7762
|
}
|
|
7344
7763
|
try {
|
|
7345
|
-
await
|
|
7764
|
+
await fs6.rmdir(paths.sessionDir(sessionId));
|
|
7346
7765
|
} catch (err) {
|
|
7347
7766
|
const e = err;
|
|
7348
7767
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -7378,6 +7797,7 @@ var SessionManager = class {
|
|
|
7378
7797
|
this.store = store ?? new SessionStore();
|
|
7379
7798
|
this.histories = new HistoryStore();
|
|
7380
7799
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
7800
|
+
this.defaultModels = options.defaultModels ?? {};
|
|
7381
7801
|
}
|
|
7382
7802
|
registry;
|
|
7383
7803
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -7386,6 +7806,7 @@ var SessionManager = class {
|
|
|
7386
7806
|
store;
|
|
7387
7807
|
histories;
|
|
7388
7808
|
idleTimeoutMs;
|
|
7809
|
+
defaultModels;
|
|
7389
7810
|
// Serialize meta.json read-modify-write operations per session id so
|
|
7390
7811
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
7391
7812
|
// back-to-back) don't lose writes via interleaved reads.
|
|
@@ -7407,7 +7828,8 @@ var SessionManager = class {
|
|
|
7407
7828
|
agentArgs: params.agentArgs,
|
|
7408
7829
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
7409
7830
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7410
|
-
historyStore: this.histories
|
|
7831
|
+
historyStore: this.histories,
|
|
7832
|
+
currentModel: fresh.initialModel
|
|
7411
7833
|
});
|
|
7412
7834
|
await this.attachManagerHooks(session);
|
|
7413
7835
|
return session;
|
|
@@ -7452,7 +7874,7 @@ var SessionManager = class {
|
|
|
7452
7874
|
if (params.upstreamSessionId === "") {
|
|
7453
7875
|
return this.doResurrectFromImport(params);
|
|
7454
7876
|
}
|
|
7455
|
-
const plan = planSpawn(agentDef, params.agentArgs ?? []);
|
|
7877
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
7456
7878
|
const agent = this.spawner({
|
|
7457
7879
|
agentId: params.agentId,
|
|
7458
7880
|
cwd: params.cwd,
|
|
@@ -7465,11 +7887,14 @@ var SessionManager = class {
|
|
|
7465
7887
|
});
|
|
7466
7888
|
let loadResult;
|
|
7467
7889
|
try {
|
|
7468
|
-
loadResult = await agent.connection.request(
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7890
|
+
loadResult = await agent.connection.request(
|
|
7891
|
+
"session/load",
|
|
7892
|
+
{
|
|
7893
|
+
sessionId: params.upstreamSessionId,
|
|
7894
|
+
cwd: params.cwd,
|
|
7895
|
+
mcpServers: []
|
|
7896
|
+
}
|
|
7897
|
+
);
|
|
7473
7898
|
} catch (err) {
|
|
7474
7899
|
await agent.kill().catch(() => void 0);
|
|
7475
7900
|
throw new Error(
|
|
@@ -7488,8 +7913,13 @@ var SessionManager = class {
|
|
|
7488
7913
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
7489
7914
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7490
7915
|
historyStore: this.histories,
|
|
7491
|
-
|
|
7916
|
+
// Prefer what we previously stored from a current_model_update; if
|
|
7917
|
+
// we never captured one (e.g. old opencode sessions on disk before
|
|
7918
|
+
// this fix), fall back to the model the agent ships in its
|
|
7919
|
+
// session/load response body.
|
|
7920
|
+
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
7492
7921
|
currentMode: params.currentMode,
|
|
7922
|
+
currentUsage: params.currentUsage,
|
|
7493
7923
|
agentCommands: params.agentCommands,
|
|
7494
7924
|
// Only gate the first-prompt title heuristic when we actually have
|
|
7495
7925
|
// a title to preserve. A title-less session (lost to a write race
|
|
@@ -7527,8 +7957,11 @@ var SessionManager = class {
|
|
|
7527
7957
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
7528
7958
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7529
7959
|
historyStore: this.histories,
|
|
7530
|
-
|
|
7960
|
+
// Prefer the stored value (set by a previous current_model_update);
|
|
7961
|
+
// fall back to whatever the agent ships in its session/new response.
|
|
7962
|
+
currentModel: params.currentModel ?? fresh.initialModel,
|
|
7531
7963
|
currentMode: params.currentMode,
|
|
7964
|
+
currentUsage: params.currentUsage,
|
|
7532
7965
|
agentCommands: params.agentCommands,
|
|
7533
7966
|
firstPromptSeeded: !!params.title,
|
|
7534
7967
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
@@ -7538,7 +7971,7 @@ var SessionManager = class {
|
|
|
7538
7971
|
return session;
|
|
7539
7972
|
}
|
|
7540
7973
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
7541
|
-
// → session/new. Shared by create() and the /hydra
|
|
7974
|
+
// → session/new. Shared by create() and the /hydra agent path so both
|
|
7542
7975
|
// go through the same env / capabilities / error-handling.
|
|
7543
7976
|
async bootstrapAgent(params) {
|
|
7544
7977
|
const agentDef = await this.registry.getAgent(params.agentId);
|
|
@@ -7549,7 +7982,7 @@ var SessionManager = class {
|
|
|
7549
7982
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
7550
7983
|
throw err;
|
|
7551
7984
|
}
|
|
7552
|
-
const plan = planSpawn(agentDef, params.agentArgs ?? []);
|
|
7985
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
7553
7986
|
const agent = this.spawner({
|
|
7554
7987
|
agentId: params.agentId,
|
|
7555
7988
|
cwd: params.cwd,
|
|
@@ -7561,14 +7994,36 @@ var SessionManager = class {
|
|
|
7561
7994
|
clientCapabilities: {},
|
|
7562
7995
|
clientInfo: { name: "hydra", version: "0.1.0" }
|
|
7563
7996
|
});
|
|
7564
|
-
const newResult = await agent.connection.request(
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7997
|
+
const newResult = await agent.connection.request(
|
|
7998
|
+
"session/new",
|
|
7999
|
+
{
|
|
8000
|
+
cwd: params.cwd,
|
|
8001
|
+
mcpServers: params.mcpServers ?? []
|
|
8002
|
+
}
|
|
8003
|
+
);
|
|
8004
|
+
const sessionIdRaw = newResult.sessionId;
|
|
8005
|
+
if (typeof sessionIdRaw !== "string") {
|
|
8006
|
+
throw new Error(
|
|
8007
|
+
`agent ${params.agentId} returned a non-string sessionId from session/new`
|
|
8008
|
+
);
|
|
8009
|
+
}
|
|
8010
|
+
let initialModel = extractInitialModel(newResult);
|
|
8011
|
+
const desired = this.defaultModels[params.agentId];
|
|
8012
|
+
if (desired && desired !== initialModel) {
|
|
8013
|
+
try {
|
|
8014
|
+
await agent.connection.request("session/set_model", {
|
|
8015
|
+
sessionId: sessionIdRaw,
|
|
8016
|
+
modelId: desired
|
|
8017
|
+
});
|
|
8018
|
+
initialModel = desired;
|
|
8019
|
+
} catch {
|
|
8020
|
+
}
|
|
8021
|
+
}
|
|
7568
8022
|
return {
|
|
7569
8023
|
agent,
|
|
7570
|
-
upstreamSessionId:
|
|
7571
|
-
agentMeta: newResult._meta
|
|
8024
|
+
upstreamSessionId: sessionIdRaw,
|
|
8025
|
+
agentMeta: newResult._meta,
|
|
8026
|
+
initialModel
|
|
7572
8027
|
};
|
|
7573
8028
|
} catch (err) {
|
|
7574
8029
|
await agent.kill().catch(() => void 0);
|
|
@@ -7579,7 +8034,7 @@ var SessionManager = class {
|
|
|
7579
8034
|
// bookkeeping. Called from both create() and resurrect() so the same
|
|
7580
8035
|
// session record + lifecycle handlers are wired regardless of origin.
|
|
7581
8036
|
// Returns once the initial disk record is written — callers should
|
|
7582
|
-
// await so a subsequent /hydra
|
|
8037
|
+
// await so a subsequent /hydra agent's persistAgentChange (which
|
|
7583
8038
|
// does read-then-write) finds the file in place.
|
|
7584
8039
|
async attachManagerHooks(session) {
|
|
7585
8040
|
session.onClose(({ deleteRecord }) => {
|
|
@@ -7607,6 +8062,11 @@ var SessionManager = class {
|
|
|
7607
8062
|
() => void 0
|
|
7608
8063
|
);
|
|
7609
8064
|
});
|
|
8065
|
+
session.onUsageChange((usage) => {
|
|
8066
|
+
void this.persistSnapshot(session.sessionId, {
|
|
8067
|
+
currentUsage: usageSnapshotToPersisted(usage)
|
|
8068
|
+
}).catch(() => void 0);
|
|
8069
|
+
});
|
|
7610
8070
|
session.onAgentCommandsChange((commands) => {
|
|
7611
8071
|
void this.persistSnapshot(session.sessionId, {
|
|
7612
8072
|
agentCommands: commands.map((c) => ({
|
|
@@ -7655,6 +8115,7 @@ var SessionManager = class {
|
|
|
7655
8115
|
agentArgs: record.agentArgs,
|
|
7656
8116
|
currentModel: record.currentModel,
|
|
7657
8117
|
currentMode: record.currentMode,
|
|
8118
|
+
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
7658
8119
|
agentCommands: record.agentCommands,
|
|
7659
8120
|
createdAt: record.createdAt
|
|
7660
8121
|
};
|
|
@@ -7723,6 +8184,8 @@ var SessionManager = class {
|
|
|
7723
8184
|
cwd: session.cwd,
|
|
7724
8185
|
title: session.title,
|
|
7725
8186
|
agentId: session.agentId,
|
|
8187
|
+
currentModel: session.currentModel,
|
|
8188
|
+
currentUsage: session.currentUsage,
|
|
7726
8189
|
updatedAt: used,
|
|
7727
8190
|
attachedClients: session.attachedCount,
|
|
7728
8191
|
status: "live"
|
|
@@ -7743,6 +8206,8 @@ var SessionManager = class {
|
|
|
7743
8206
|
cwd: r.cwd,
|
|
7744
8207
|
title: r.title,
|
|
7745
8208
|
agentId: r.agentId,
|
|
8209
|
+
currentModel: r.currentModel,
|
|
8210
|
+
currentUsage: r.currentUsage,
|
|
7746
8211
|
updatedAt: used,
|
|
7747
8212
|
attachedClients: 0,
|
|
7748
8213
|
status: "cold"
|
|
@@ -7850,6 +8315,7 @@ var SessionManager = class {
|
|
|
7850
8315
|
title: args.bundle.session.title,
|
|
7851
8316
|
currentModel: args.bundle.session.currentModel,
|
|
7852
8317
|
currentMode: args.bundle.session.currentMode,
|
|
8318
|
+
currentUsage: args.bundle.session.currentUsage,
|
|
7853
8319
|
agentCommands: args.bundle.session.agentCommands,
|
|
7854
8320
|
createdAt: args.preservedCreatedAt ?? now,
|
|
7855
8321
|
updatedAt: now
|
|
@@ -7885,7 +8351,7 @@ var SessionManager = class {
|
|
|
7885
8351
|
});
|
|
7886
8352
|
});
|
|
7887
8353
|
}
|
|
7888
|
-
// Persist an agent swap from /hydra
|
|
8354
|
+
// Persist an agent swap from /hydra agent. The on-disk record's
|
|
7889
8355
|
// agentId + upstreamSessionId both rotate so a daemon restart (and
|
|
7890
8356
|
// later resurrect) brings the session back up on the agent the user
|
|
7891
8357
|
// most recently switched to, not the one it was originally created on.
|
|
@@ -7917,6 +8383,7 @@ var SessionManager = class {
|
|
|
7917
8383
|
...record,
|
|
7918
8384
|
...update.currentModel !== void 0 ? { currentModel: update.currentModel } : {},
|
|
7919
8385
|
...update.currentMode !== void 0 ? { currentMode: update.currentMode } : {},
|
|
8386
|
+
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
7920
8387
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
7921
8388
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7922
8389
|
});
|
|
@@ -7971,13 +8438,73 @@ function mergeForPersistence(session, existing) {
|
|
|
7971
8438
|
agentArgs: session.agentArgs,
|
|
7972
8439
|
currentModel: session.currentModel ?? existing?.currentModel,
|
|
7973
8440
|
currentMode: session.currentMode ?? existing?.currentMode,
|
|
8441
|
+
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
7974
8442
|
agentCommands,
|
|
7975
8443
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
7976
8444
|
});
|
|
7977
8445
|
}
|
|
8446
|
+
function usageSnapshotToPersisted(usage) {
|
|
8447
|
+
if (!usage) {
|
|
8448
|
+
return void 0;
|
|
8449
|
+
}
|
|
8450
|
+
const out = {};
|
|
8451
|
+
if (usage.used !== void 0) {
|
|
8452
|
+
out.used = usage.used;
|
|
8453
|
+
}
|
|
8454
|
+
if (usage.size !== void 0) {
|
|
8455
|
+
out.size = usage.size;
|
|
8456
|
+
}
|
|
8457
|
+
if (usage.costAmount !== void 0) {
|
|
8458
|
+
out.costAmount = usage.costAmount;
|
|
8459
|
+
}
|
|
8460
|
+
if (usage.costCurrency !== void 0) {
|
|
8461
|
+
out.costCurrency = usage.costCurrency;
|
|
8462
|
+
}
|
|
8463
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
8464
|
+
}
|
|
8465
|
+
function persistedUsageToSnapshot(usage) {
|
|
8466
|
+
return usage ? { ...usage } : void 0;
|
|
8467
|
+
}
|
|
8468
|
+
function extractInitialModel(result) {
|
|
8469
|
+
const direct = asString(result.currentModelId) ?? asString(result.currentModel) ?? asString(result.modelId) ?? asString(result.model);
|
|
8470
|
+
if (direct) {
|
|
8471
|
+
return direct;
|
|
8472
|
+
}
|
|
8473
|
+
const models = result.models;
|
|
8474
|
+
if (models && typeof models === "object" && !Array.isArray(models)) {
|
|
8475
|
+
const m = asString(models.currentModelId) ?? asString(models.currentModel);
|
|
8476
|
+
if (m) {
|
|
8477
|
+
return m;
|
|
8478
|
+
}
|
|
8479
|
+
}
|
|
8480
|
+
const meta = result._meta;
|
|
8481
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
8482
|
+
for (const [key, value] of Object.entries(
|
|
8483
|
+
meta
|
|
8484
|
+
)) {
|
|
8485
|
+
if (key === "hydra-acp") {
|
|
8486
|
+
continue;
|
|
8487
|
+
}
|
|
8488
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
8489
|
+
const m = asString(value.modelId) ?? asString(value.model) ?? asString(value.currentModelId);
|
|
8490
|
+
if (m) {
|
|
8491
|
+
return m;
|
|
8492
|
+
}
|
|
8493
|
+
}
|
|
8494
|
+
}
|
|
8495
|
+
}
|
|
8496
|
+
return void 0;
|
|
8497
|
+
}
|
|
8498
|
+
function asString(value) {
|
|
8499
|
+
if (typeof value !== "string") {
|
|
8500
|
+
return void 0;
|
|
8501
|
+
}
|
|
8502
|
+
const trimmed = value.trim();
|
|
8503
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
8504
|
+
}
|
|
7978
8505
|
async function loadPromptHistorySafely(sessionId) {
|
|
7979
8506
|
try {
|
|
7980
|
-
const raw = await
|
|
8507
|
+
const raw = await fs8.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
7981
8508
|
const out = [];
|
|
7982
8509
|
for (const line of raw.split("\n")) {
|
|
7983
8510
|
if (line.length === 0) {
|
|
@@ -7998,7 +8525,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
7998
8525
|
}
|
|
7999
8526
|
async function historyMtimeIso(sessionId) {
|
|
8000
8527
|
try {
|
|
8001
|
-
const st = await
|
|
8528
|
+
const st = await fs8.stat(paths.historyFile(sessionId));
|
|
8002
8529
|
return new Date(st.mtimeMs).toISOString();
|
|
8003
8530
|
} catch {
|
|
8004
8531
|
return void 0;
|
|
@@ -8007,10 +8534,10 @@ async function historyMtimeIso(sessionId) {
|
|
|
8007
8534
|
|
|
8008
8535
|
// src/core/extensions.ts
|
|
8009
8536
|
init_paths();
|
|
8010
|
-
import { spawn as
|
|
8011
|
-
import * as
|
|
8012
|
-
import * as
|
|
8013
|
-
import * as
|
|
8537
|
+
import { spawn as spawn3 } from "child_process";
|
|
8538
|
+
import * as fs9 from "fs";
|
|
8539
|
+
import * as fsp2 from "fs/promises";
|
|
8540
|
+
import * as path5 from "path";
|
|
8014
8541
|
var RESTART_BASE_MS = 1e3;
|
|
8015
8542
|
var RESTART_CAP_MS = 6e4;
|
|
8016
8543
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -8031,7 +8558,7 @@ var ExtensionManager = class {
|
|
|
8031
8558
|
if (!this.context) {
|
|
8032
8559
|
throw new Error("ExtensionManager: setContext must be called before start");
|
|
8033
8560
|
}
|
|
8034
|
-
await
|
|
8561
|
+
await fsp2.mkdir(paths.extensionsDir(), { recursive: true });
|
|
8035
8562
|
await this.reapOrphans();
|
|
8036
8563
|
for (const entry of this.entries.values()) {
|
|
8037
8564
|
if (!entry.config.enabled) {
|
|
@@ -8057,9 +8584,9 @@ var ExtensionManager = class {
|
|
|
8057
8584
|
} catch {
|
|
8058
8585
|
}
|
|
8059
8586
|
tasks.push(
|
|
8060
|
-
new Promise((
|
|
8587
|
+
new Promise((resolve5) => {
|
|
8061
8588
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
8062
|
-
|
|
8589
|
+
resolve5();
|
|
8063
8590
|
return;
|
|
8064
8591
|
}
|
|
8065
8592
|
const timer = setTimeout(() => {
|
|
@@ -8067,11 +8594,11 @@ var ExtensionManager = class {
|
|
|
8067
8594
|
child.kill("SIGKILL");
|
|
8068
8595
|
} catch {
|
|
8069
8596
|
}
|
|
8070
|
-
|
|
8597
|
+
resolve5();
|
|
8071
8598
|
}, STOP_GRACE_MS);
|
|
8072
8599
|
child.on("exit", () => {
|
|
8073
8600
|
clearTimeout(timer);
|
|
8074
|
-
|
|
8601
|
+
resolve5();
|
|
8075
8602
|
});
|
|
8076
8603
|
})
|
|
8077
8604
|
);
|
|
@@ -8179,8 +8706,8 @@ var ExtensionManager = class {
|
|
|
8179
8706
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
8180
8707
|
return;
|
|
8181
8708
|
}
|
|
8182
|
-
const exited = new Promise((
|
|
8183
|
-
entry.exitWaiters.push(
|
|
8709
|
+
const exited = new Promise((resolve5) => {
|
|
8710
|
+
entry.exitWaiters.push(resolve5);
|
|
8184
8711
|
});
|
|
8185
8712
|
try {
|
|
8186
8713
|
child.kill("SIGTERM");
|
|
@@ -8240,7 +8767,7 @@ var ExtensionManager = class {
|
|
|
8240
8767
|
async reapOrphans() {
|
|
8241
8768
|
let entries;
|
|
8242
8769
|
try {
|
|
8243
|
-
entries = await
|
|
8770
|
+
entries = await fsp2.readdir(paths.extensionsDir());
|
|
8244
8771
|
} catch (err) {
|
|
8245
8772
|
const e = err;
|
|
8246
8773
|
if (e.code === "ENOENT") {
|
|
@@ -8252,10 +8779,10 @@ var ExtensionManager = class {
|
|
|
8252
8779
|
if (!entry.endsWith(".pid")) {
|
|
8253
8780
|
continue;
|
|
8254
8781
|
}
|
|
8255
|
-
const pidPath =
|
|
8782
|
+
const pidPath = path5.join(paths.extensionsDir(), entry);
|
|
8256
8783
|
let pid;
|
|
8257
8784
|
try {
|
|
8258
|
-
const raw = await
|
|
8785
|
+
const raw = await fsp2.readFile(pidPath, "utf8");
|
|
8259
8786
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
8260
8787
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
8261
8788
|
pid = parsed;
|
|
@@ -8278,7 +8805,7 @@ var ExtensionManager = class {
|
|
|
8278
8805
|
}
|
|
8279
8806
|
}
|
|
8280
8807
|
}
|
|
8281
|
-
await
|
|
8808
|
+
await fsp2.unlink(pidPath).catch(() => void 0);
|
|
8282
8809
|
}
|
|
8283
8810
|
}
|
|
8284
8811
|
spawn(entry, attempt) {
|
|
@@ -8291,7 +8818,7 @@ var ExtensionManager = class {
|
|
|
8291
8818
|
}
|
|
8292
8819
|
const ext = entry.config;
|
|
8293
8820
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
8294
|
-
const logStream =
|
|
8821
|
+
const logStream = fs9.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
8295
8822
|
flags: "a"
|
|
8296
8823
|
});
|
|
8297
8824
|
logStream.write(
|
|
@@ -8319,7 +8846,7 @@ var ExtensionManager = class {
|
|
|
8319
8846
|
const args = [...baseArgs, ...ext.args];
|
|
8320
8847
|
let child;
|
|
8321
8848
|
try {
|
|
8322
|
-
child =
|
|
8849
|
+
child = spawn3(cmd, args, {
|
|
8323
8850
|
env,
|
|
8324
8851
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8325
8852
|
detached: false
|
|
@@ -8341,7 +8868,7 @@ var ExtensionManager = class {
|
|
|
8341
8868
|
}
|
|
8342
8869
|
if (typeof child.pid === "number") {
|
|
8343
8870
|
try {
|
|
8344
|
-
|
|
8871
|
+
fs9.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
8345
8872
|
`, {
|
|
8346
8873
|
encoding: "utf8",
|
|
8347
8874
|
mode: 384
|
|
@@ -8366,7 +8893,7 @@ var ExtensionManager = class {
|
|
|
8366
8893
|
});
|
|
8367
8894
|
child.on("exit", (code, signal) => {
|
|
8368
8895
|
try {
|
|
8369
|
-
|
|
8896
|
+
fs9.unlinkSync(paths.extensionPidFile(ext.name));
|
|
8370
8897
|
} catch {
|
|
8371
8898
|
}
|
|
8372
8899
|
logStream.write(
|
|
@@ -8377,8 +8904,8 @@ var ExtensionManager = class {
|
|
|
8377
8904
|
entry.pid = void 0;
|
|
8378
8905
|
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
8379
8906
|
const waiters = entry.exitWaiters.splice(0);
|
|
8380
|
-
for (const
|
|
8381
|
-
|
|
8907
|
+
for (const resolve5 of waiters) {
|
|
8908
|
+
resolve5();
|
|
8382
8909
|
}
|
|
8383
8910
|
if (this.stopping || entry.manuallyStopped) {
|
|
8384
8911
|
try {
|
|
@@ -8500,6 +9027,7 @@ var BundleSession = z5.object({
|
|
|
8500
9027
|
title: z5.string().optional(),
|
|
8501
9028
|
currentModel: z5.string().optional(),
|
|
8502
9029
|
currentMode: z5.string().optional(),
|
|
9030
|
+
currentUsage: PersistedUsage.optional(),
|
|
8503
9031
|
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
8504
9032
|
createdAt: z5.string(),
|
|
8505
9033
|
updatedAt: z5.string()
|
|
@@ -8531,6 +9059,7 @@ function encodeBundle(params) {
|
|
|
8531
9059
|
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
8532
9060
|
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
8533
9061
|
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
9062
|
+
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
8534
9063
|
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
8535
9064
|
createdAt: params.record.createdAt,
|
|
8536
9065
|
updatedAt: params.record.updatedAt
|
|
@@ -9194,10 +9723,10 @@ var HYDRA_VERSION3 = "0.1.0";
|
|
|
9194
9723
|
async function startDaemon(config) {
|
|
9195
9724
|
ensureLoopbackOrTls(config);
|
|
9196
9725
|
const httpsOptions = config.daemon.tls ? {
|
|
9197
|
-
key: await
|
|
9198
|
-
cert: await
|
|
9726
|
+
key: await fsp3.readFile(config.daemon.tls.key),
|
|
9727
|
+
cert: await fsp3.readFile(config.daemon.tls.cert)
|
|
9199
9728
|
} : void 0;
|
|
9200
|
-
await
|
|
9729
|
+
await fsp3.mkdir(paths.home(), { recursive: true });
|
|
9201
9730
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
9202
9731
|
config.daemon.logLevel
|
|
9203
9732
|
);
|
|
@@ -9209,6 +9738,9 @@ async function startDaemon(config) {
|
|
|
9209
9738
|
https: httpsOptions ?? null
|
|
9210
9739
|
});
|
|
9211
9740
|
await app.register(websocketPlugin);
|
|
9741
|
+
setBinaryInstallLogger((msg) => {
|
|
9742
|
+
app.log.info(msg);
|
|
9743
|
+
});
|
|
9212
9744
|
const auth = bearerAuth({ config });
|
|
9213
9745
|
app.addHook("onRequest", async (request, reply) => {
|
|
9214
9746
|
if (request.routeOptions.config?.skipAuth) {
|
|
@@ -9221,7 +9753,8 @@ async function startDaemon(config) {
|
|
|
9221
9753
|
});
|
|
9222
9754
|
const registry = new Registry(config);
|
|
9223
9755
|
const manager = new SessionManager(registry, void 0, void 0, {
|
|
9224
|
-
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3
|
|
9756
|
+
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
9757
|
+
defaultModels: config.defaultModels
|
|
9225
9758
|
});
|
|
9226
9759
|
const extensions = new ExtensionManager(extensionList(config));
|
|
9227
9760
|
registerHealthRoutes(app, HYDRA_VERSION3);
|
|
@@ -9243,8 +9776,8 @@ async function startDaemon(config) {
|
|
|
9243
9776
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
9244
9777
|
const address = app.server.address();
|
|
9245
9778
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
9246
|
-
await
|
|
9247
|
-
await
|
|
9779
|
+
await fsp3.mkdir(paths.home(), { recursive: true });
|
|
9780
|
+
await fsp3.writeFile(
|
|
9248
9781
|
paths.pidFile(),
|
|
9249
9782
|
JSON.stringify({
|
|
9250
9783
|
pid: process.pid,
|
|
@@ -9269,9 +9802,10 @@ async function startDaemon(config) {
|
|
|
9269
9802
|
await extensions.stop();
|
|
9270
9803
|
await manager.closeAll();
|
|
9271
9804
|
await manager.flushMetaWrites();
|
|
9805
|
+
setBinaryInstallLogger(null);
|
|
9272
9806
|
await app.close();
|
|
9273
9807
|
try {
|
|
9274
|
-
|
|
9808
|
+
fs10.unlinkSync(paths.pidFile());
|
|
9275
9809
|
} catch {
|
|
9276
9810
|
}
|
|
9277
9811
|
try {
|
|
@@ -9310,13 +9844,13 @@ function ensureLoopbackOrTls(config) {
|
|
|
9310
9844
|
init_daemon_bootstrap();
|
|
9311
9845
|
|
|
9312
9846
|
// src/cli/commands/log-tail.ts
|
|
9313
|
-
import * as
|
|
9314
|
-
import * as
|
|
9847
|
+
import * as fs11 from "fs";
|
|
9848
|
+
import * as fsp4 from "fs/promises";
|
|
9315
9849
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
9316
9850
|
const opts = parseLogTailFlags(argv);
|
|
9317
9851
|
let stat3;
|
|
9318
9852
|
try {
|
|
9319
|
-
stat3 = await
|
|
9853
|
+
stat3 = await fsp4.stat(logPath);
|
|
9320
9854
|
} catch (err) {
|
|
9321
9855
|
const e = err;
|
|
9322
9856
|
if (e.code === "ENOENT") {
|
|
@@ -9334,7 +9868,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
9334
9868
|
process.stdout.write(`-- following ${logPath} --
|
|
9335
9869
|
`);
|
|
9336
9870
|
let pending = false;
|
|
9337
|
-
const watcher =
|
|
9871
|
+
const watcher = fs11.watch(logPath, () => {
|
|
9338
9872
|
if (pending) {
|
|
9339
9873
|
return;
|
|
9340
9874
|
}
|
|
@@ -9342,14 +9876,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
9342
9876
|
setImmediate(async () => {
|
|
9343
9877
|
pending = false;
|
|
9344
9878
|
try {
|
|
9345
|
-
const s = await
|
|
9879
|
+
const s = await fsp4.stat(logPath);
|
|
9346
9880
|
if (s.size <= position) {
|
|
9347
9881
|
if (s.size < position) {
|
|
9348
9882
|
position = s.size;
|
|
9349
9883
|
}
|
|
9350
9884
|
return;
|
|
9351
9885
|
}
|
|
9352
|
-
const fd = await
|
|
9886
|
+
const fd = await fsp4.open(logPath, "r");
|
|
9353
9887
|
try {
|
|
9354
9888
|
const buf = Buffer.alloc(s.size - position);
|
|
9355
9889
|
await fd.read(buf, 0, buf.length, position);
|
|
@@ -9362,10 +9896,10 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
9362
9896
|
}
|
|
9363
9897
|
});
|
|
9364
9898
|
});
|
|
9365
|
-
await new Promise((
|
|
9899
|
+
await new Promise((resolve5) => {
|
|
9366
9900
|
const finish = () => {
|
|
9367
9901
|
watcher.close();
|
|
9368
|
-
|
|
9902
|
+
resolve5();
|
|
9369
9903
|
};
|
|
9370
9904
|
process.once("SIGINT", finish);
|
|
9371
9905
|
process.once("SIGTERM", finish);
|
|
@@ -9376,7 +9910,7 @@ async function printTail(logPath, fileSize, lines) {
|
|
|
9376
9910
|
return fileSize;
|
|
9377
9911
|
}
|
|
9378
9912
|
const CHUNK = 64 * 1024;
|
|
9379
|
-
const fd = await
|
|
9913
|
+
const fd = await fsp4.open(logPath, "r");
|
|
9380
9914
|
try {
|
|
9381
9915
|
let position = fileSize;
|
|
9382
9916
|
let collected = "";
|
|
@@ -9433,20 +9967,37 @@ function parseLogTailFlags(argv) {
|
|
|
9433
9967
|
}
|
|
9434
9968
|
|
|
9435
9969
|
// src/cli/commands/daemon.ts
|
|
9436
|
-
async function runDaemonStart() {
|
|
9970
|
+
async function runDaemonStart(flags = {}) {
|
|
9437
9971
|
const config = await ensureConfig();
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9972
|
+
if (await pingHealth(config)) {
|
|
9973
|
+
const info2 = await readPidFile();
|
|
9974
|
+
process.stdout.write(
|
|
9975
|
+
`Daemon already running${info2 ? ` (pid ${info2.pid})` : ""}. Run \`hydra-acp daemon restart\` to restart it.
|
|
9976
|
+
`
|
|
9977
|
+
);
|
|
9978
|
+
return;
|
|
9979
|
+
}
|
|
9980
|
+
if (flagBool(flags, "foreground")) {
|
|
9981
|
+
const handle = await startDaemon(config);
|
|
9982
|
+
process.stdout.write(
|
|
9983
|
+
`hydra-acp daemon listening on ${config.daemon.host}:${config.daemon.port}
|
|
9441
9984
|
`
|
|
9985
|
+
);
|
|
9986
|
+
const shutdown = async () => {
|
|
9987
|
+
process.stdout.write("Shutting down...\n");
|
|
9988
|
+
await handle.shutdown();
|
|
9989
|
+
process.exit(0);
|
|
9990
|
+
};
|
|
9991
|
+
process.on("SIGINT", () => void shutdown());
|
|
9992
|
+
process.on("SIGTERM", () => void shutdown());
|
|
9993
|
+
return;
|
|
9994
|
+
}
|
|
9995
|
+
spawnDaemonDetached();
|
|
9996
|
+
await waitForDaemonReady(config);
|
|
9997
|
+
const info = await readPidFile();
|
|
9998
|
+
process.stdout.write(
|
|
9999
|
+
`Daemon started on ${config.daemon.host}:${config.daemon.port}` + (info ? ` pid=${info.pid}` : "") + "\n"
|
|
9442
10000
|
);
|
|
9443
|
-
const shutdown = async () => {
|
|
9444
|
-
process.stdout.write("Shutting down...\n");
|
|
9445
|
-
await handle.shutdown();
|
|
9446
|
-
process.exit(0);
|
|
9447
|
-
};
|
|
9448
|
-
process.on("SIGINT", () => void shutdown());
|
|
9449
|
-
process.on("SIGTERM", () => void shutdown());
|
|
9450
10001
|
}
|
|
9451
10002
|
async function runDaemonStop() {
|
|
9452
10003
|
const info = await readPidFile();
|
|
@@ -9528,7 +10079,7 @@ async function runDaemonStatus() {
|
|
|
9528
10079
|
}
|
|
9529
10080
|
async function readPidFile() {
|
|
9530
10081
|
try {
|
|
9531
|
-
const raw = await
|
|
10082
|
+
const raw = await fsp5.readFile(paths.pidFile(), "utf8");
|
|
9532
10083
|
return JSON.parse(raw);
|
|
9533
10084
|
} catch (err) {
|
|
9534
10085
|
const e = err;
|
|
@@ -9553,7 +10104,7 @@ init_sessions();
|
|
|
9553
10104
|
// src/cli/commands/extensions.ts
|
|
9554
10105
|
init_config();
|
|
9555
10106
|
init_paths();
|
|
9556
|
-
import * as
|
|
10107
|
+
import * as fsp6 from "fs/promises";
|
|
9557
10108
|
init_sessions();
|
|
9558
10109
|
async function runExtensionsList() {
|
|
9559
10110
|
const config = await loadConfig();
|
|
@@ -9694,11 +10245,7 @@ async function runExtensionsAdd(name, argv) {
|
|
|
9694
10245
|
`Daemon refused to register ${name} (HTTP ${r.status}${detail}). Restart the daemon to apply.
|
|
9695
10246
|
`
|
|
9696
10247
|
);
|
|
9697
|
-
} catch
|
|
9698
|
-
process.stderr.write(
|
|
9699
|
-
`Daemon not reachable (${err.message}). Config saved; the new extension will start on next daemon launch.
|
|
9700
|
-
`
|
|
9701
|
-
);
|
|
10248
|
+
} catch {
|
|
9702
10249
|
}
|
|
9703
10250
|
}
|
|
9704
10251
|
async function runExtensionsRemove(name) {
|
|
@@ -9753,11 +10300,11 @@ async function runExtensionsRemove(name) {
|
|
|
9753
10300
|
}
|
|
9754
10301
|
}
|
|
9755
10302
|
async function readRawConfig() {
|
|
9756
|
-
const raw = await
|
|
10303
|
+
const raw = await fsp6.readFile(paths.config(), "utf8");
|
|
9757
10304
|
return JSON.parse(raw);
|
|
9758
10305
|
}
|
|
9759
10306
|
async function writeRawConfig(raw) {
|
|
9760
|
-
await
|
|
10307
|
+
await fsp6.writeFile(
|
|
9761
10308
|
paths.config(),
|
|
9762
10309
|
JSON.stringify(raw, null, 2) + "\n",
|
|
9763
10310
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -10482,7 +11029,7 @@ async function main() {
|
|
|
10482
11029
|
const tail = argv.slice(daemonIdx + 1);
|
|
10483
11030
|
const sub = tail[0];
|
|
10484
11031
|
if (sub === "start" || sub === void 0) {
|
|
10485
|
-
await runDaemonStart();
|
|
11032
|
+
await runDaemonStart(flags);
|
|
10486
11033
|
return;
|
|
10487
11034
|
}
|
|
10488
11035
|
if (sub === "stop") {
|
|
@@ -10626,9 +11173,9 @@ async function dispatchTui(flags, base) {
|
|
|
10626
11173
|
}
|
|
10627
11174
|
function readVersion() {
|
|
10628
11175
|
try {
|
|
10629
|
-
const here =
|
|
11176
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
10630
11177
|
const pkg = JSON.parse(
|
|
10631
|
-
readFileSync(
|
|
11178
|
+
readFileSync(resolve4(here, "../package.json"), "utf8")
|
|
10632
11179
|
);
|
|
10633
11180
|
return pkg.version ?? "unknown";
|
|
10634
11181
|
} catch {
|
|
@@ -10650,7 +11197,8 @@ function printHelp() {
|
|
|
10650
11197
|
" are forwarded to the agent's command.",
|
|
10651
11198
|
" hydra-acp --resume <id> Attach to an existing session (TUI when in a terminal, shim otherwise)",
|
|
10652
11199
|
" hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
|
|
10653
|
-
" hydra-acp daemon start
|
|
11200
|
+
" hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
|
|
11201
|
+
" hydra-acp daemon stop|restart|status",
|
|
10654
11202
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
10655
11203
|
" hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
|
|
10656
11204
|
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|