@hydra-acp/cli 0.1.4 → 0.1.6
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 +16 -13
- package/dist/cli.js +834 -254
- package/dist/index.d.ts +433 -31
- package/dist/index.js +577 -126
- 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.
|
|
@@ -236,6 +248,9 @@ function extractHydraMeta(meta) {
|
|
|
236
248
|
out.resume = parsed.data;
|
|
237
249
|
}
|
|
238
250
|
}
|
|
251
|
+
if (typeof obj.model === "string") {
|
|
252
|
+
out.model = obj.model;
|
|
253
|
+
}
|
|
239
254
|
if (typeof obj.currentModel === "string") {
|
|
240
255
|
out.currentModel = obj.currentModel;
|
|
241
256
|
}
|
|
@@ -270,7 +285,7 @@ function extractHydraMeta(meta) {
|
|
|
270
285
|
function mergeMeta(passthrough, ours) {
|
|
271
286
|
return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
|
|
272
287
|
}
|
|
273
|
-
var JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
288
|
+
var JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
274
289
|
var init_types = __esm({
|
|
275
290
|
"src/acp/types.ts"() {
|
|
276
291
|
"use strict";
|
|
@@ -325,12 +340,24 @@ var init_types = __esm({
|
|
|
325
340
|
cursor: z3.string().optional(),
|
|
326
341
|
limit: z3.number().int().positive().max(200).optional()
|
|
327
342
|
});
|
|
343
|
+
SessionListUsage = z3.object({
|
|
344
|
+
used: z3.number().optional(),
|
|
345
|
+
size: z3.number().optional(),
|
|
346
|
+
costAmount: z3.number().optional(),
|
|
347
|
+
costCurrency: z3.string().optional()
|
|
348
|
+
});
|
|
328
349
|
SessionListEntry = z3.object({
|
|
329
350
|
sessionId: z3.string(),
|
|
330
351
|
upstreamSessionId: z3.string().optional(),
|
|
331
352
|
cwd: z3.string(),
|
|
332
353
|
title: z3.string().optional(),
|
|
333
354
|
agentId: z3.string().optional(),
|
|
355
|
+
// Last-known model id, so list views can render `<agent>(<model>)`
|
|
356
|
+
// without resurrecting cold sessions to look it up.
|
|
357
|
+
currentModel: z3.string().optional(),
|
|
358
|
+
// Last-known usage snapshot so list views can show per-session cost
|
|
359
|
+
// (and tokens, in callers that care) without resurrecting cold sessions.
|
|
360
|
+
currentUsage: SessionListUsage.optional(),
|
|
334
361
|
updatedAt: z3.string(),
|
|
335
362
|
attachedClients: z3.number().int().nonnegative(),
|
|
336
363
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
@@ -408,9 +435,9 @@ var init_connection = __esm({
|
|
|
408
435
|
}
|
|
409
436
|
const id = nanoid();
|
|
410
437
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
411
|
-
const response = new Promise((
|
|
438
|
+
const response = new Promise((resolve5, reject) => {
|
|
412
439
|
this.pending.set(id, {
|
|
413
|
-
resolve: (result) =>
|
|
440
|
+
resolve: (result) => resolve5(result),
|
|
414
441
|
reject
|
|
415
442
|
});
|
|
416
443
|
this.stream.send(message).catch((err) => {
|
|
@@ -534,8 +561,8 @@ var init_hydra_commands = __esm({
|
|
|
534
561
|
description: "Regenerate the session title via the agent (or set manually with an arg)"
|
|
535
562
|
},
|
|
536
563
|
{
|
|
537
|
-
verb: "
|
|
538
|
-
name: "/hydra
|
|
564
|
+
verb: "agent",
|
|
565
|
+
name: "/hydra agent",
|
|
539
566
|
argsHint: "<agent>",
|
|
540
567
|
description: "Swap the agent backing this session, preserving context"
|
|
541
568
|
}
|
|
@@ -648,7 +675,7 @@ var init_session = __esm({
|
|
|
648
675
|
Session = class {
|
|
649
676
|
sessionId;
|
|
650
677
|
cwd;
|
|
651
|
-
// agent / agentId / upstreamSessionId are mutable so /hydra
|
|
678
|
+
// agent / agentId / upstreamSessionId are mutable so /hydra agent can
|
|
652
679
|
// replace the underlying agent process while keeping the same Session
|
|
653
680
|
// record. agentMeta is the metadata returned by the agent at session/new
|
|
654
681
|
// time; it gets refreshed on switch too.
|
|
@@ -663,6 +690,7 @@ var init_session = __esm({
|
|
|
663
690
|
// stale-prone for snapshot-shaped events).
|
|
664
691
|
currentModel;
|
|
665
692
|
currentMode;
|
|
693
|
+
currentUsage;
|
|
666
694
|
updatedAt;
|
|
667
695
|
createdAt;
|
|
668
696
|
clients = /* @__PURE__ */ new Map();
|
|
@@ -724,6 +752,7 @@ var init_session = __esm({
|
|
|
724
752
|
agentCommandsHandlers = [];
|
|
725
753
|
modelHandlers = [];
|
|
726
754
|
modeHandlers = [];
|
|
755
|
+
usageHandlers = [];
|
|
727
756
|
constructor(init) {
|
|
728
757
|
this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
|
|
729
758
|
this.cwd = init.cwd;
|
|
@@ -735,6 +764,7 @@ var init_session = __esm({
|
|
|
735
764
|
this.title = init.title;
|
|
736
765
|
this.currentModel = init.currentModel;
|
|
737
766
|
this.currentMode = init.currentMode;
|
|
767
|
+
this.currentUsage = init.currentUsage;
|
|
738
768
|
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
739
769
|
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
740
770
|
}
|
|
@@ -764,7 +794,7 @@ var init_session = __esm({
|
|
|
764
794
|
});
|
|
765
795
|
}
|
|
766
796
|
// Register session/update, session/request_permission, and onExit
|
|
767
|
-
// handlers on an agent connection. Re-run on every /hydra
|
|
797
|
+
// handlers on an agent connection. Re-run on every /hydra agent so
|
|
768
798
|
// the new agent is plumbed identically. The exit handler's identity
|
|
769
799
|
// check is what makes switching safe: when the *old* agent exits as
|
|
770
800
|
// part of a swap, this.agent has already been replaced, so we no-op
|
|
@@ -788,6 +818,10 @@ var init_session = __esm({
|
|
|
788
818
|
this.recordAndBroadcast("session/update", params);
|
|
789
819
|
return;
|
|
790
820
|
}
|
|
821
|
+
if (this.maybeApplyAgentUsage(params)) {
|
|
822
|
+
this.recordAndBroadcast("session/update", params);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
791
825
|
this.maybeApplyAgentSessionInfo(params);
|
|
792
826
|
this.recordAndBroadcast("session/update", params);
|
|
793
827
|
});
|
|
@@ -1105,6 +1139,49 @@ var init_session = __esm({
|
|
|
1105
1139
|
}
|
|
1106
1140
|
return true;
|
|
1107
1141
|
}
|
|
1142
|
+
// usage_update carries any subset of {used, size, cost.amount,
|
|
1143
|
+
// cost.currency}. Merge non-undefined fields onto currentUsage so a
|
|
1144
|
+
// sparse update preserves prior values, and fire usage handlers only
|
|
1145
|
+
// if something actually changed.
|
|
1146
|
+
maybeApplyAgentUsage(params) {
|
|
1147
|
+
const obj = params ?? {};
|
|
1148
|
+
const update = obj.update ?? {};
|
|
1149
|
+
if (update.sessionUpdate !== "usage_update") {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
const next = { ...this.currentUsage ?? {} };
|
|
1153
|
+
let changed = false;
|
|
1154
|
+
if (typeof update.used === "number" && next.used !== update.used) {
|
|
1155
|
+
next.used = update.used;
|
|
1156
|
+
changed = true;
|
|
1157
|
+
}
|
|
1158
|
+
if (typeof update.size === "number" && next.size !== update.size) {
|
|
1159
|
+
next.size = update.size;
|
|
1160
|
+
changed = true;
|
|
1161
|
+
}
|
|
1162
|
+
if (update.cost && typeof update.cost === "object") {
|
|
1163
|
+
const cost = update.cost;
|
|
1164
|
+
if (typeof cost.amount === "number" && next.costAmount !== cost.amount) {
|
|
1165
|
+
next.costAmount = cost.amount;
|
|
1166
|
+
changed = true;
|
|
1167
|
+
}
|
|
1168
|
+
if (typeof cost.currency === "string" && next.costCurrency !== cost.currency) {
|
|
1169
|
+
next.costCurrency = cost.currency;
|
|
1170
|
+
changed = true;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (!changed) {
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
this.currentUsage = next;
|
|
1177
|
+
for (const handler of this.usageHandlers) {
|
|
1178
|
+
try {
|
|
1179
|
+
handler(next);
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1108
1185
|
// Update the cached agent command list, fire persist handlers, and
|
|
1109
1186
|
// broadcast the merged list to attached clients. Idempotent on a
|
|
1110
1187
|
// structurally identical list so we don't churn meta.json on noisy
|
|
@@ -1135,6 +1212,9 @@ var init_session = __esm({
|
|
|
1135
1212
|
onModeChange(handler) {
|
|
1136
1213
|
this.modeHandlers.push(handler);
|
|
1137
1214
|
}
|
|
1215
|
+
onUsageChange(handler) {
|
|
1216
|
+
this.usageHandlers.push(handler);
|
|
1217
|
+
}
|
|
1138
1218
|
// Returns a freshly merged command list (hydra ∪ agent) for callers
|
|
1139
1219
|
// that need a snapshot — notably acp-ws.ts's buildResponseMeta when
|
|
1140
1220
|
// assembling the attach response.
|
|
@@ -1197,8 +1277,8 @@ var init_session = __esm({
|
|
|
1197
1277
|
switch (verb) {
|
|
1198
1278
|
case "title":
|
|
1199
1279
|
return this.runTitleCommand(arg);
|
|
1200
|
-
case "
|
|
1201
|
-
return this.
|
|
1280
|
+
case "agent":
|
|
1281
|
+
return this.runAgentCommand(arg);
|
|
1202
1282
|
default: {
|
|
1203
1283
|
const err = new Error(
|
|
1204
1284
|
`no dispatcher for /hydra verb ${verb}`
|
|
@@ -1234,7 +1314,7 @@ var init_session = __esm({
|
|
|
1234
1314
|
}
|
|
1235
1315
|
// Send a prompt to the underlying agent and capture its reply chunks
|
|
1236
1316
|
// privately (no fan-out to clients, no recording into history). Used
|
|
1237
|
-
// by /hydra title's regen path and /hydra
|
|
1317
|
+
// by /hydra title's regen path and /hydra agent's transcript-injection
|
|
1238
1318
|
// path. Returns the joined agent_message_chunk text.
|
|
1239
1319
|
async runInternalPrompt(text) {
|
|
1240
1320
|
if (this.internalPromptCapture) {
|
|
@@ -1256,10 +1336,10 @@ var init_session = __esm({
|
|
|
1256
1336
|
// record. Spawns the new agent first so a failure leaves the old one
|
|
1257
1337
|
// intact; then injects a synthesized transcript so the new agent has
|
|
1258
1338
|
// context for the next turn.
|
|
1259
|
-
|
|
1339
|
+
runAgentCommand(newAgentId) {
|
|
1260
1340
|
if (!newAgentId) {
|
|
1261
1341
|
throw withCode(
|
|
1262
|
-
new Error("/hydra
|
|
1342
|
+
new Error("/hydra agent requires an agent id"),
|
|
1263
1343
|
JsonRpcErrorCodes.InvalidParams
|
|
1264
1344
|
);
|
|
1265
1345
|
}
|
|
@@ -1393,7 +1473,7 @@ var init_session = __esm({
|
|
|
1393
1473
|
// on the first wake-up of a session whose meta.json has an empty
|
|
1394
1474
|
// upstreamSessionId (the import marker). Wrapped in enqueuePrompt so
|
|
1395
1475
|
// any user prompts arriving mid-seed queue behind it (mirrors the
|
|
1396
|
-
// /hydra
|
|
1476
|
+
// /hydra agent path so the agent isn't asked to respond to a user
|
|
1397
1477
|
// turn before it has absorbed the imported transcript). Best-effort:
|
|
1398
1478
|
// if the agent fails to absorb the transcript we still leave the
|
|
1399
1479
|
// session usable — the user just continues without context.
|
|
@@ -1417,7 +1497,7 @@ var init_session = __esm({
|
|
|
1417
1497
|
// ones read it and relabel) and (b) drop a visible banner into the
|
|
1418
1498
|
// transcript so users see the switch rather than just suddenly getting
|
|
1419
1499
|
// answers from a different agent. Both updates carry synthetic=true
|
|
1420
|
-
// so a future /hydra
|
|
1500
|
+
// so a future /hydra agent's transcript builder filters them out.
|
|
1421
1501
|
broadcastAgentSwitch(oldAgentId, newAgentId) {
|
|
1422
1502
|
this.recordAndBroadcast("session/update", {
|
|
1423
1503
|
sessionId: this.sessionId,
|
|
@@ -1564,7 +1644,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1564
1644
|
);
|
|
1565
1645
|
}
|
|
1566
1646
|
const clientParams = this.rewriteForClient(params);
|
|
1567
|
-
return new Promise((
|
|
1647
|
+
return new Promise((resolve5, reject) => {
|
|
1568
1648
|
let settled = false;
|
|
1569
1649
|
const outbound = [];
|
|
1570
1650
|
const entry = { addClient: sendTo };
|
|
@@ -1599,7 +1679,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1599
1679
|
result
|
|
1600
1680
|
}).catch(() => void 0);
|
|
1601
1681
|
}
|
|
1602
|
-
|
|
1682
|
+
resolve5(result);
|
|
1603
1683
|
});
|
|
1604
1684
|
}).catch((err) => {
|
|
1605
1685
|
settle(() => reject(err));
|
|
@@ -1611,16 +1691,16 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1611
1691
|
});
|
|
1612
1692
|
}
|
|
1613
1693
|
async enqueuePrompt(task) {
|
|
1614
|
-
return new Promise((
|
|
1615
|
-
const
|
|
1694
|
+
return new Promise((resolve5, reject) => {
|
|
1695
|
+
const run2 = async () => {
|
|
1616
1696
|
try {
|
|
1617
1697
|
const result = await task();
|
|
1618
|
-
|
|
1698
|
+
resolve5(result);
|
|
1619
1699
|
} catch (err) {
|
|
1620
1700
|
reject(err);
|
|
1621
1701
|
}
|
|
1622
1702
|
};
|
|
1623
|
-
this.promptQueue.push(
|
|
1703
|
+
this.promptQueue.push(run2);
|
|
1624
1704
|
void this.drainQueue();
|
|
1625
1705
|
});
|
|
1626
1706
|
}
|
|
@@ -1645,18 +1725,19 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1645
1725
|
"session_info_update",
|
|
1646
1726
|
"current_model_update",
|
|
1647
1727
|
"current_mode_update",
|
|
1648
|
-
"available_commands_update"
|
|
1728
|
+
"available_commands_update",
|
|
1729
|
+
"usage_update"
|
|
1649
1730
|
]);
|
|
1650
1731
|
}
|
|
1651
1732
|
});
|
|
1652
1733
|
|
|
1653
1734
|
// src/tui/history.ts
|
|
1654
|
-
import { promises as
|
|
1655
|
-
import * as
|
|
1735
|
+
import { promises as fs7 } from "fs";
|
|
1736
|
+
import * as path4 from "path";
|
|
1656
1737
|
async function loadHistory(file) {
|
|
1657
1738
|
let text;
|
|
1658
1739
|
try {
|
|
1659
|
-
text = await
|
|
1740
|
+
text = await fs7.readFile(file, "utf8");
|
|
1660
1741
|
} catch (err) {
|
|
1661
1742
|
if (err.code === "ENOENT") {
|
|
1662
1743
|
return [];
|
|
@@ -1696,9 +1777,9 @@ function appendEntry(history, entry) {
|
|
|
1696
1777
|
return out;
|
|
1697
1778
|
}
|
|
1698
1779
|
async function saveHistory(file, history) {
|
|
1699
|
-
await
|
|
1780
|
+
await fs7.mkdir(path4.dirname(file), { recursive: true });
|
|
1700
1781
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
1701
|
-
await
|
|
1782
|
+
await fs7.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
1702
1783
|
}
|
|
1703
1784
|
var HISTORY_CAP;
|
|
1704
1785
|
var init_history = __esm({
|
|
@@ -1753,13 +1834,13 @@ function wsToMessageStream(ws) {
|
|
|
1753
1834
|
throw new Error("ws is closed");
|
|
1754
1835
|
}
|
|
1755
1836
|
const text = JSON.stringify(message);
|
|
1756
|
-
await new Promise((
|
|
1837
|
+
await new Promise((resolve5, reject) => {
|
|
1757
1838
|
ws.send(text, (err) => {
|
|
1758
1839
|
if (err) {
|
|
1759
1840
|
reject(err);
|
|
1760
1841
|
return;
|
|
1761
1842
|
}
|
|
1762
|
-
|
|
1843
|
+
resolve5();
|
|
1763
1844
|
});
|
|
1764
1845
|
});
|
|
1765
1846
|
},
|
|
@@ -1786,7 +1867,7 @@ var init_ws_stream = __esm({
|
|
|
1786
1867
|
});
|
|
1787
1868
|
|
|
1788
1869
|
// src/core/daemon-bootstrap.ts
|
|
1789
|
-
import { spawn as
|
|
1870
|
+
import { spawn as spawn4 } from "child_process";
|
|
1790
1871
|
import { setTimeout as sleep } from "timers/promises";
|
|
1791
1872
|
async function ensureDaemonReachable(config) {
|
|
1792
1873
|
if (await pingHealth(config)) {
|
|
@@ -1813,11 +1894,15 @@ function spawnDaemonDetached() {
|
|
|
1813
1894
|
if (!cliPath) {
|
|
1814
1895
|
throw new Error("Cannot determine hydra-acp binary path to spawn daemon");
|
|
1815
1896
|
}
|
|
1816
|
-
const child =
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1897
|
+
const child = spawn4(
|
|
1898
|
+
process.execPath,
|
|
1899
|
+
[cliPath, "daemon", "start", "--foreground"],
|
|
1900
|
+
{
|
|
1901
|
+
detached: true,
|
|
1902
|
+
stdio: "ignore",
|
|
1903
|
+
env: process.env
|
|
1904
|
+
}
|
|
1905
|
+
);
|
|
1821
1906
|
child.unref();
|
|
1822
1907
|
}
|
|
1823
1908
|
async function waitForDaemonReady(config, timeoutMs = 15e3) {
|
|
@@ -1838,25 +1923,80 @@ var init_daemon_bootstrap = __esm({
|
|
|
1838
1923
|
}
|
|
1839
1924
|
});
|
|
1840
1925
|
|
|
1926
|
+
// src/core/agent-display.ts
|
|
1927
|
+
function shortenModel(model) {
|
|
1928
|
+
if (!model) {
|
|
1929
|
+
return void 0;
|
|
1930
|
+
}
|
|
1931
|
+
const idx = model.lastIndexOf("/");
|
|
1932
|
+
if (idx === -1) {
|
|
1933
|
+
return model;
|
|
1934
|
+
}
|
|
1935
|
+
return model.slice(idx + 1);
|
|
1936
|
+
}
|
|
1937
|
+
function formatAgentWithModel(agentId, model) {
|
|
1938
|
+
const agent = agentId ?? "?";
|
|
1939
|
+
const short = shortenModel(model);
|
|
1940
|
+
if (!short) {
|
|
1941
|
+
return agent;
|
|
1942
|
+
}
|
|
1943
|
+
return `${agent}${AGENT_MODEL_SEP}${short}`;
|
|
1944
|
+
}
|
|
1945
|
+
function formatAgentCell(agentId, model, usage) {
|
|
1946
|
+
const base = formatAgentWithModel(agentId, model);
|
|
1947
|
+
if (!usage || typeof usage.costAmount !== "number") {
|
|
1948
|
+
return base;
|
|
1949
|
+
}
|
|
1950
|
+
const compact = formatCostCompact(usage.costAmount, usage.costCurrency);
|
|
1951
|
+
if (compact === null) {
|
|
1952
|
+
return base;
|
|
1953
|
+
}
|
|
1954
|
+
return `${base} ${compact}`;
|
|
1955
|
+
}
|
|
1956
|
+
function formatCost(amount, currency) {
|
|
1957
|
+
const sign = currency === "USD" || currency === void 0 ? "$" : "";
|
|
1958
|
+
const decimals = amount >= 1 ? 2 : 4;
|
|
1959
|
+
return `${sign}${amount.toFixed(decimals)}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
|
|
1960
|
+
}
|
|
1961
|
+
function formatCostCompact(amount, currency) {
|
|
1962
|
+
const whole = Math.round(amount);
|
|
1963
|
+
if (whole === 0) {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
const sign = currency === "USD" || currency === void 0 ? "$" : "";
|
|
1967
|
+
return `${sign}${whole}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
|
|
1968
|
+
}
|
|
1969
|
+
var AGENT_MODEL_SEP;
|
|
1970
|
+
var init_agent_display = __esm({
|
|
1971
|
+
"src/core/agent-display.ts"() {
|
|
1972
|
+
"use strict";
|
|
1973
|
+
AGENT_MODEL_SEP = "\u2022";
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1841
1977
|
// src/cli/session-row.ts
|
|
1842
1978
|
function toRow(s, now = Date.now()) {
|
|
1843
1979
|
return {
|
|
1844
1980
|
session: stripHydraSessionPrefix(s.sessionId),
|
|
1845
1981
|
upstream: s.upstreamSessionId ?? "-",
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
agent: s.agentId ?? "?",
|
|
1982
|
+
state: formatState(s.status, s.attachedClients),
|
|
1983
|
+
agent: formatAgentCell(s.agentId, s.currentModel, s.currentUsage),
|
|
1849
1984
|
age: formatRelativeAge(s.updatedAt, now),
|
|
1850
1985
|
title: s.title ?? "-",
|
|
1851
1986
|
cwd: s.cwd
|
|
1852
1987
|
};
|
|
1853
1988
|
}
|
|
1989
|
+
function formatState(status, clients) {
|
|
1990
|
+
if (status === "cold") {
|
|
1991
|
+
return "COLD";
|
|
1992
|
+
}
|
|
1993
|
+
return `LIVE(${clients})`;
|
|
1994
|
+
}
|
|
1854
1995
|
function computeWidths(rows) {
|
|
1855
1996
|
return {
|
|
1856
1997
|
session: maxLen(HEADER.session, rows.map((r) => r.session)),
|
|
1857
1998
|
upstream: maxLen(HEADER.upstream, rows.map((r) => r.upstream)),
|
|
1858
|
-
|
|
1859
|
-
clients: maxLen(HEADER.clients, rows.map((r) => r.clients)),
|
|
1999
|
+
state: maxLen(HEADER.state, rows.map((r) => r.state)),
|
|
1860
2000
|
agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
|
|
1861
2001
|
age: maxLen(HEADER.age, rows.map((r) => r.age)),
|
|
1862
2002
|
title: maxLen(HEADER.title, rows.map((r) => r.title))
|
|
@@ -1911,8 +2051,7 @@ function formatRow(r, w, maxWidth) {
|
|
|
1911
2051
|
const fixed = [
|
|
1912
2052
|
r.session.padEnd(w.session),
|
|
1913
2053
|
r.upstream.padEnd(w.upstream),
|
|
1914
|
-
r.
|
|
1915
|
-
r.clients.padStart(w.clients),
|
|
2054
|
+
r.state.padEnd(w.state),
|
|
1916
2055
|
r.agent.padEnd(w.agent),
|
|
1917
2056
|
r.age.padStart(w.age)
|
|
1918
2057
|
].join(SEP);
|
|
@@ -1962,12 +2101,12 @@ var HEADER, SEP, MIN_CWD, TITLE_MAX_WIDTH;
|
|
|
1962
2101
|
var init_session_row = __esm({
|
|
1963
2102
|
"src/cli/session-row.ts"() {
|
|
1964
2103
|
"use strict";
|
|
2104
|
+
init_agent_display();
|
|
1965
2105
|
init_session();
|
|
1966
2106
|
HEADER = {
|
|
1967
2107
|
session: "SESSION",
|
|
1968
2108
|
upstream: "UPSTREAM",
|
|
1969
|
-
|
|
1970
|
-
clients: "CLIENTS",
|
|
2109
|
+
state: "STATE",
|
|
1971
2110
|
agent: "AGENT",
|
|
1972
2111
|
age: "AGE",
|
|
1973
2112
|
title: "TITLE",
|
|
@@ -1980,8 +2119,8 @@ var init_session_row = __esm({
|
|
|
1980
2119
|
});
|
|
1981
2120
|
|
|
1982
2121
|
// src/cli/commands/sessions.ts
|
|
1983
|
-
import * as
|
|
1984
|
-
import * as
|
|
2122
|
+
import * as fs12 from "fs/promises";
|
|
2123
|
+
import * as path6 from "path";
|
|
1985
2124
|
async function runSessionsList(opts = {}) {
|
|
1986
2125
|
const config = await loadConfig();
|
|
1987
2126
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -2100,8 +2239,8 @@ async function runSessionsExport(id, outPath) {
|
|
|
2100
2239
|
return;
|
|
2101
2240
|
}
|
|
2102
2241
|
const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
|
|
2103
|
-
await
|
|
2104
|
-
await
|
|
2242
|
+
await fs12.mkdir(path6.dirname(path6.resolve(resolved)), { recursive: true });
|
|
2243
|
+
await fs12.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
2105
2244
|
process.stdout.write(`Wrote ${resolved}
|
|
2106
2245
|
`);
|
|
2107
2246
|
}
|
|
@@ -2116,7 +2255,7 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2116
2255
|
if (file === "-") {
|
|
2117
2256
|
body = await readStdin();
|
|
2118
2257
|
} else {
|
|
2119
|
-
body = await
|
|
2258
|
+
body = await fs12.readFile(file, "utf8");
|
|
2120
2259
|
}
|
|
2121
2260
|
let bundle;
|
|
2122
2261
|
try {
|
|
@@ -2194,11 +2333,11 @@ function isResponse(msg) {
|
|
|
2194
2333
|
return !("method" in msg) && "id" in msg && msg.id !== void 0;
|
|
2195
2334
|
}
|
|
2196
2335
|
async function openWs(url, subprotocols) {
|
|
2197
|
-
return new Promise((
|
|
2336
|
+
return new Promise((resolve5, reject) => {
|
|
2198
2337
|
const ws = new WebSocket(url, subprotocols);
|
|
2199
2338
|
const onOpen = () => {
|
|
2200
2339
|
ws.off("error", onError);
|
|
2201
|
-
|
|
2340
|
+
resolve5(wsToMessageStream(ws));
|
|
2202
2341
|
};
|
|
2203
2342
|
const onError = (err) => {
|
|
2204
2343
|
ws.off("open", onOpen);
|
|
@@ -2269,8 +2408,8 @@ var init_resilient_ws = __esm({
|
|
|
2269
2408
|
throw new Error("resilient ws stream not connected");
|
|
2270
2409
|
}
|
|
2271
2410
|
const id = message.id;
|
|
2272
|
-
const promise = new Promise((
|
|
2273
|
-
this.pendingRequests.set(id, { resolve:
|
|
2411
|
+
const promise = new Promise((resolve5, reject) => {
|
|
2412
|
+
this.pendingRequests.set(id, { resolve: resolve5, reject });
|
|
2274
2413
|
});
|
|
2275
2414
|
try {
|
|
2276
2415
|
await this.current.send(message);
|
|
@@ -2298,8 +2437,8 @@ var init_resilient_ws = __esm({
|
|
|
2298
2437
|
this.bindStream(stream);
|
|
2299
2438
|
const wasFirst = this.firstConnect;
|
|
2300
2439
|
this.firstConnect = false;
|
|
2301
|
-
this.connectGate = new Promise((
|
|
2302
|
-
this.releaseConnectGate =
|
|
2440
|
+
this.connectGate = new Promise((resolve5) => {
|
|
2441
|
+
this.releaseConnectGate = resolve5;
|
|
2303
2442
|
});
|
|
2304
2443
|
try {
|
|
2305
2444
|
if (this.opts.onConnect) {
|
|
@@ -2450,6 +2589,8 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
|
2450
2589
|
status: s.status ?? "live",
|
|
2451
2590
|
upstreamSessionId: s.upstreamSessionId,
|
|
2452
2591
|
agentId: s.agentId,
|
|
2592
|
+
currentModel: s.currentModel,
|
|
2593
|
+
currentUsage: s.currentUsage,
|
|
2453
2594
|
title: s.title
|
|
2454
2595
|
}));
|
|
2455
2596
|
}
|
|
@@ -2652,7 +2793,7 @@ async function pickSession(term, opts) {
|
|
|
2652
2793
|
};
|
|
2653
2794
|
renderFromScratch();
|
|
2654
2795
|
term.hideCursor();
|
|
2655
|
-
return await new Promise((
|
|
2796
|
+
return await new Promise((resolve5) => {
|
|
2656
2797
|
let resolved = false;
|
|
2657
2798
|
const onResize = () => {
|
|
2658
2799
|
if (resolved) {
|
|
@@ -2839,12 +2980,12 @@ async function pickSession(term, opts) {
|
|
|
2839
2980
|
case "KP_ENTER": {
|
|
2840
2981
|
cleanup();
|
|
2841
2982
|
if (selectedIdx === 0) {
|
|
2842
|
-
|
|
2983
|
+
resolve5({ kind: "new" });
|
|
2843
2984
|
return;
|
|
2844
2985
|
}
|
|
2845
2986
|
const session = visible[selectedIdx - 1];
|
|
2846
2987
|
if (!session) {
|
|
2847
|
-
|
|
2988
|
+
resolve5({ kind: "abort" });
|
|
2848
2989
|
return;
|
|
2849
2990
|
}
|
|
2850
2991
|
const result = {
|
|
@@ -2854,13 +2995,13 @@ async function pickSession(term, opts) {
|
|
|
2854
2995
|
if (session.agentId !== void 0) {
|
|
2855
2996
|
result.agentId = session.agentId;
|
|
2856
2997
|
}
|
|
2857
|
-
|
|
2998
|
+
resolve5(result);
|
|
2858
2999
|
return;
|
|
2859
3000
|
}
|
|
2860
3001
|
case "ESCAPE":
|
|
2861
3002
|
case "CTRL_C":
|
|
2862
3003
|
cleanup();
|
|
2863
|
-
|
|
3004
|
+
resolve5({ kind: "abort" });
|
|
2864
3005
|
return;
|
|
2865
3006
|
}
|
|
2866
3007
|
};
|
|
@@ -2892,6 +3033,7 @@ var init_picker = __esm({
|
|
|
2892
3033
|
});
|
|
2893
3034
|
|
|
2894
3035
|
// src/tui/screen.ts
|
|
3036
|
+
import os3 from "os";
|
|
2895
3037
|
import stringWidth from "string-width";
|
|
2896
3038
|
import wrapAnsi from "wrap-ansi";
|
|
2897
3039
|
function formattedLineSig(zone, width, line) {
|
|
@@ -3111,6 +3253,19 @@ function wrapVisible(text, width) {
|
|
|
3111
3253
|
}
|
|
3112
3254
|
return out;
|
|
3113
3255
|
}
|
|
3256
|
+
function shortenHomePath(p) {
|
|
3257
|
+
const home = os3.homedir();
|
|
3258
|
+
if (!home) {
|
|
3259
|
+
return p;
|
|
3260
|
+
}
|
|
3261
|
+
if (p === home) {
|
|
3262
|
+
return "~";
|
|
3263
|
+
}
|
|
3264
|
+
if (p.startsWith(home + "/")) {
|
|
3265
|
+
return "~" + p.slice(home.length);
|
|
3266
|
+
}
|
|
3267
|
+
return p;
|
|
3268
|
+
}
|
|
3114
3269
|
function truncate(text, max) {
|
|
3115
3270
|
if (max <= 0) {
|
|
3116
3271
|
return "";
|
|
@@ -3189,11 +3344,6 @@ function formatTokens(n) {
|
|
|
3189
3344
|
}
|
|
3190
3345
|
return `${n}`;
|
|
3191
3346
|
}
|
|
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
3347
|
function mapKeyName(name) {
|
|
3198
3348
|
switch (name) {
|
|
3199
3349
|
case "ENTER":
|
|
@@ -3260,6 +3410,7 @@ var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS,
|
|
|
3260
3410
|
var init_screen = __esm({
|
|
3261
3411
|
"src/tui/screen.ts"() {
|
|
3262
3412
|
"use strict";
|
|
3413
|
+
init_agent_display();
|
|
3263
3414
|
init_session();
|
|
3264
3415
|
HEADER_ROWS = 2;
|
|
3265
3416
|
BANNER_ROWS = 1;
|
|
@@ -3937,22 +4088,23 @@ var init_screen = __esm({
|
|
|
3937
4088
|
const usage = formatUsage(this.header.usage);
|
|
3938
4089
|
const sid = shortId(this.header.sessionId);
|
|
3939
4090
|
const title = this.header.title?.trim();
|
|
3940
|
-
const
|
|
4091
|
+
const agentCell = formatAgentWithModel(this.header.agent, this.header.model);
|
|
4092
|
+
const cwdDisplay = shortenHomePath(this.header.cwd);
|
|
4093
|
+
const sig = `hdr|${w}|${agentCell}|${cwdDisplay}|${sid}|${title ?? ""}|${usage ?? ""}`;
|
|
3941
4094
|
this.paintRow(1, sig, () => {
|
|
3942
|
-
const fixed = "hydra \xB7 ".length +
|
|
4095
|
+
const fixed = "hydra \xB7 ".length + agentCell.length + " \xB7 ".length + " \xB7 ".length + sid.length + (title ? " \xB7 ".length : 0) + (usage ? usage.length + 3 : 0);
|
|
3943
4096
|
const variableRoom = Math.max(8, w - fixed);
|
|
3944
4097
|
let cwdRoom;
|
|
3945
4098
|
let titleRoom;
|
|
3946
4099
|
if (title) {
|
|
3947
|
-
const
|
|
3948
|
-
|
|
3949
|
-
titleRoom = Math.
|
|
3950
|
-
cwdRoom = Math.max(8, variableRoom - titleRoom);
|
|
4100
|
+
const titleMin = Math.min(title.length, 8);
|
|
4101
|
+
cwdRoom = Math.min(cwdDisplay.length, Math.max(8, variableRoom - titleMin));
|
|
4102
|
+
titleRoom = Math.max(0, variableRoom - cwdRoom);
|
|
3951
4103
|
} else {
|
|
3952
4104
|
titleRoom = 0;
|
|
3953
4105
|
cwdRoom = variableRoom;
|
|
3954
4106
|
}
|
|
3955
|
-
this.term.bold("hydra")(" \xB7 ").cyan.noFormat(
|
|
4107
|
+
this.term.bold("hydra")(" \xB7 ").cyan.noFormat(agentCell)(" \xB7 ").dim.noFormat(truncate(cwdDisplay, cwdRoom))(" \xB7 ").yellow(sid);
|
|
3956
4108
|
if (title) {
|
|
3957
4109
|
this.term(" \xB7 ").bold.noFormat(truncate(title, titleRoom));
|
|
3958
4110
|
}
|
|
@@ -4740,6 +4892,10 @@ var init_input = __esm({
|
|
|
4740
4892
|
});
|
|
4741
4893
|
|
|
4742
4894
|
// src/tui/render-update.ts
|
|
4895
|
+
import stripAnsi from "strip-ansi";
|
|
4896
|
+
function sanitizeWireText(text) {
|
|
4897
|
+
return stripAnsi(text).replace(STRIP_CONTROLS, "");
|
|
4898
|
+
}
|
|
4743
4899
|
function mapUpdate(update) {
|
|
4744
4900
|
if (!update || typeof update !== "object") {
|
|
4745
4901
|
return null;
|
|
@@ -4782,7 +4938,8 @@ function mapUpdate(update) {
|
|
|
4782
4938
|
}
|
|
4783
4939
|
}
|
|
4784
4940
|
function mapSessionInfo(u) {
|
|
4785
|
-
const
|
|
4941
|
+
const rawTitle = readString(u, "title");
|
|
4942
|
+
const title = rawTitle !== void 0 ? sanitizeWireText(rawTitle) : void 0;
|
|
4786
4943
|
const meta = u._meta;
|
|
4787
4944
|
let agentId;
|
|
4788
4945
|
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
@@ -4820,10 +4977,10 @@ function mapAvailableCommands(u) {
|
|
|
4820
4977
|
if (typeof c.name !== "string" || c.name.length === 0) {
|
|
4821
4978
|
continue;
|
|
4822
4979
|
}
|
|
4823
|
-
const
|
|
4824
|
-
const cmd = { name };
|
|
4980
|
+
const rawName = c.name.startsWith("/") ? c.name : `/${c.name}`;
|
|
4981
|
+
const cmd = { name: sanitizeWireText(rawName) };
|
|
4825
4982
|
if (typeof c.description === "string") {
|
|
4826
|
-
cmd.description = c.description;
|
|
4983
|
+
cmd.description = sanitizeWireText(c.description);
|
|
4827
4984
|
}
|
|
4828
4985
|
out.push(cmd);
|
|
4829
4986
|
}
|
|
@@ -4856,7 +5013,7 @@ function mapAgentText(u) {
|
|
|
4856
5013
|
return { kind: "agent-text", text };
|
|
4857
5014
|
}
|
|
4858
5015
|
function mapAgentThought(u) {
|
|
4859
|
-
const text = typeof u.text === "string" ? u.text : extractContentText(u.content);
|
|
5016
|
+
const text = typeof u.text === "string" ? sanitizeWireText(u.text) : extractContentText(u.content);
|
|
4860
5017
|
if (text === null) {
|
|
4861
5018
|
return null;
|
|
4862
5019
|
}
|
|
@@ -4888,7 +5045,8 @@ function mapToolCall(u) {
|
|
|
4888
5045
|
if (!toolCallId) {
|
|
4889
5046
|
return null;
|
|
4890
5047
|
}
|
|
4891
|
-
const
|
|
5048
|
+
const rawTitle = readString(u, "title") ?? readString(u, "name") ?? readString(u, "label") ?? "tool call";
|
|
5049
|
+
const title = sanitizeWireText(rawTitle);
|
|
4892
5050
|
const status = readString(u, "status");
|
|
4893
5051
|
const rawKind = readString(u, "kind");
|
|
4894
5052
|
const event = { kind: "tool-call", toolCallId, title };
|
|
@@ -4905,7 +5063,8 @@ function mapToolCallUpdate(u) {
|
|
|
4905
5063
|
if (!toolCallId) {
|
|
4906
5064
|
return null;
|
|
4907
5065
|
}
|
|
4908
|
-
const
|
|
5066
|
+
const rawTitle = readString(u, "title");
|
|
5067
|
+
const title = rawTitle !== void 0 ? sanitizeWireText(rawTitle) : void 0;
|
|
4909
5068
|
const status = readString(u, "status");
|
|
4910
5069
|
const meaningful = title !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
|
|
4911
5070
|
if (!meaningful) {
|
|
@@ -4931,7 +5090,7 @@ function mapPlan(u) {
|
|
|
4931
5090
|
continue;
|
|
4932
5091
|
}
|
|
4933
5092
|
const e = raw;
|
|
4934
|
-
const content = typeof e.content === "string" ? e.content : void 0;
|
|
5093
|
+
const content = typeof e.content === "string" ? sanitizeWireText(e.content) : void 0;
|
|
4935
5094
|
if (!content) {
|
|
4936
5095
|
continue;
|
|
4937
5096
|
}
|
|
@@ -4951,14 +5110,14 @@ function mapMode(u) {
|
|
|
4951
5110
|
if (!mode) {
|
|
4952
5111
|
return null;
|
|
4953
5112
|
}
|
|
4954
|
-
return { kind: "mode-changed", mode };
|
|
5113
|
+
return { kind: "mode-changed", mode: sanitizeWireText(mode) };
|
|
4955
5114
|
}
|
|
4956
5115
|
function mapModel(u) {
|
|
4957
5116
|
const model = readString(u, "currentModel") ?? readString(u, "model");
|
|
4958
5117
|
if (!model) {
|
|
4959
5118
|
return null;
|
|
4960
5119
|
}
|
|
4961
|
-
return { kind: "model-changed", model };
|
|
5120
|
+
return { kind: "model-changed", model: sanitizeWireText(model) };
|
|
4962
5121
|
}
|
|
4963
5122
|
function mapTurnComplete(u) {
|
|
4964
5123
|
const stopReason = readString(u, "stopReason");
|
|
@@ -4966,17 +5125,17 @@ function mapTurnComplete(u) {
|
|
|
4966
5125
|
}
|
|
4967
5126
|
function extractContentText(content) {
|
|
4968
5127
|
if (typeof content === "string") {
|
|
4969
|
-
return content;
|
|
5128
|
+
return sanitizeWireText(content);
|
|
4970
5129
|
}
|
|
4971
5130
|
if (!content || typeof content !== "object") {
|
|
4972
5131
|
return null;
|
|
4973
5132
|
}
|
|
4974
5133
|
const c = content;
|
|
4975
5134
|
if (c.type === "text" && typeof c.text === "string") {
|
|
4976
|
-
return c.text;
|
|
5135
|
+
return sanitizeWireText(c.text);
|
|
4977
5136
|
}
|
|
4978
5137
|
if (typeof c.text === "string") {
|
|
4979
|
-
return c.text;
|
|
5138
|
+
return sanitizeWireText(c.text);
|
|
4980
5139
|
}
|
|
4981
5140
|
return null;
|
|
4982
5141
|
}
|
|
@@ -5000,9 +5159,11 @@ function readString(u, key) {
|
|
|
5000
5159
|
const v = u[key];
|
|
5001
5160
|
return typeof v === "string" ? v : void 0;
|
|
5002
5161
|
}
|
|
5162
|
+
var STRIP_CONTROLS;
|
|
5003
5163
|
var init_render_update = __esm({
|
|
5004
5164
|
"src/tui/render-update.ts"() {
|
|
5005
5165
|
"use strict";
|
|
5166
|
+
STRIP_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
5006
5167
|
}
|
|
5007
5168
|
});
|
|
5008
5169
|
|
|
@@ -5262,8 +5423,17 @@ function formatPlan(event) {
|
|
|
5262
5423
|
}
|
|
5263
5424
|
];
|
|
5264
5425
|
}
|
|
5426
|
+
const allComplete = event.entries.every(
|
|
5427
|
+
(e) => (e.status ?? "pending") === "completed"
|
|
5428
|
+
);
|
|
5429
|
+
const headerStyle = allComplete ? "plan-done" : "plan";
|
|
5265
5430
|
const lines = [
|
|
5266
|
-
{
|
|
5431
|
+
{
|
|
5432
|
+
prefix: "\u25A3 ",
|
|
5433
|
+
prefixStyle: headerStyle,
|
|
5434
|
+
body: "Plan",
|
|
5435
|
+
bodyStyle: headerStyle
|
|
5436
|
+
}
|
|
5267
5437
|
];
|
|
5268
5438
|
for (const entry of event.entries) {
|
|
5269
5439
|
const status = entry.status ?? "pending";
|
|
@@ -5456,10 +5626,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5456
5626
|
if (pendingPermission.toolCallId && toolCallId && pendingPermission.toolCallId !== toolCallId) {
|
|
5457
5627
|
return;
|
|
5458
5628
|
}
|
|
5459
|
-
const
|
|
5629
|
+
const resolve5 = pendingPermission.resolve;
|
|
5460
5630
|
pendingPermission = null;
|
|
5461
5631
|
screen.setPermissionPrompt(null);
|
|
5462
|
-
|
|
5632
|
+
resolve5(result ?? { outcome: { outcome: "cancelled" } });
|
|
5463
5633
|
};
|
|
5464
5634
|
const maybeDismissPermissionByToolUpdate = (update) => {
|
|
5465
5635
|
if (!pendingPermission?.toolCallId) {
|
|
@@ -5492,20 +5662,26 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5492
5662
|
if (!pendingPermission) {
|
|
5493
5663
|
return;
|
|
5494
5664
|
}
|
|
5495
|
-
const { options, resolve:
|
|
5665
|
+
const { options, resolve: resolve5 } = pendingPermission;
|
|
5496
5666
|
pendingPermission = null;
|
|
5497
5667
|
screen.setPermissionPrompt(null);
|
|
5498
5668
|
if (optionId === null) {
|
|
5499
|
-
|
|
5669
|
+
resolve5({ outcome: { outcome: "cancelled" } });
|
|
5500
5670
|
return;
|
|
5501
5671
|
}
|
|
5502
|
-
|
|
5672
|
+
resolve5({ outcome: { outcome: "selected", optionId } });
|
|
5503
5673
|
void options;
|
|
5504
5674
|
};
|
|
5505
5675
|
conn.onRequest("session/request_permission", async (params) => {
|
|
5506
5676
|
const p = params ?? {};
|
|
5507
|
-
const
|
|
5508
|
-
const
|
|
5677
|
+
const rawOptions = Array.isArray(p.options) ? p.options : [];
|
|
5678
|
+
const options = rawOptions.map((o) => ({
|
|
5679
|
+
optionId: o.optionId,
|
|
5680
|
+
name: sanitizeWireText(o.name ?? ""),
|
|
5681
|
+
...o.kind !== void 0 ? { kind: o.kind } : {}
|
|
5682
|
+
}));
|
|
5683
|
+
const rawTitle = p.toolCall?.title ?? p.toolCall?.name ?? "tool";
|
|
5684
|
+
const title = sanitizeWireText(rawTitle);
|
|
5509
5685
|
const toolCallId = p.toolCall?.toolCallId;
|
|
5510
5686
|
if (options.length === 0) {
|
|
5511
5687
|
screen.appendLines([
|
|
@@ -5517,12 +5693,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5517
5693
|
]);
|
|
5518
5694
|
return { outcome: { outcome: "cancelled" } };
|
|
5519
5695
|
}
|
|
5520
|
-
return new Promise((
|
|
5696
|
+
return new Promise((resolve5) => {
|
|
5521
5697
|
pendingPermission = {
|
|
5522
5698
|
title,
|
|
5523
5699
|
options,
|
|
5524
5700
|
selectedIndex: 0,
|
|
5525
|
-
resolve:
|
|
5701
|
+
resolve: resolve5,
|
|
5526
5702
|
toolCallId
|
|
5527
5703
|
};
|
|
5528
5704
|
refreshPermissionPrompt();
|
|
@@ -5554,10 +5730,17 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5554
5730
|
let initialCommands;
|
|
5555
5731
|
let initialTurnStartedAt;
|
|
5556
5732
|
if (ctx.sessionId === "__new__") {
|
|
5733
|
+
const hydraNewMeta = {};
|
|
5734
|
+
if (opts.name) {
|
|
5735
|
+
hydraNewMeta.name = opts.name;
|
|
5736
|
+
}
|
|
5737
|
+
if (opts.model) {
|
|
5738
|
+
hydraNewMeta.model = opts.model;
|
|
5739
|
+
}
|
|
5557
5740
|
const created = await conn.request("session/new", {
|
|
5558
5741
|
cwd: ctx.cwd,
|
|
5559
5742
|
...opts.agentId ? { agentId: opts.agentId } : {},
|
|
5560
|
-
...
|
|
5743
|
+
...Object.keys(hydraNewMeta).length > 0 ? { _meta: { [HYDRA_META_KEY]: hydraNewMeta } } : {}
|
|
5561
5744
|
});
|
|
5562
5745
|
resolvedSessionId = created.sessionId;
|
|
5563
5746
|
exitHint.sessionId = resolvedSessionId;
|
|
@@ -5775,17 +5958,15 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5775
5958
|
agent: headerName,
|
|
5776
5959
|
cwd: resolvedCwd,
|
|
5777
5960
|
sessionId: resolvedSessionId,
|
|
5778
|
-
title: resolvedTitle
|
|
5961
|
+
title: resolvedTitle,
|
|
5962
|
+
model: initialModel
|
|
5779
5963
|
});
|
|
5780
5964
|
if (initialMode) {
|
|
5781
5965
|
screen.appendLines(formatEvent({ kind: "mode-changed", mode: initialMode }));
|
|
5782
5966
|
}
|
|
5783
|
-
if (initialModel) {
|
|
5784
|
-
screen.appendLines(formatEvent({ kind: "model-changed", model: initialModel }));
|
|
5785
|
-
}
|
|
5786
5967
|
let finishSession = null;
|
|
5787
|
-
const sessionDone = new Promise((
|
|
5788
|
-
finishSession =
|
|
5968
|
+
const sessionDone = new Promise((resolve5) => {
|
|
5969
|
+
finishSession = resolve5;
|
|
5789
5970
|
});
|
|
5790
5971
|
const cancelRemoteTurn = () => {
|
|
5791
5972
|
conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
|
|
@@ -6358,6 +6539,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6358
6539
|
renderToolsBlock();
|
|
6359
6540
|
return;
|
|
6360
6541
|
}
|
|
6542
|
+
if (event.kind === "model-changed") {
|
|
6543
|
+
screen.setHeader({ model: event.model });
|
|
6544
|
+
}
|
|
6361
6545
|
const formatted = formatEvent(event);
|
|
6362
6546
|
if (formatted.length > 0) {
|
|
6363
6547
|
screen.appendLines(formatted);
|
|
@@ -6407,10 +6591,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6407
6591
|
}
|
|
6408
6592
|
const resetInFlightUiState = () => {
|
|
6409
6593
|
if (pendingPermission) {
|
|
6410
|
-
const
|
|
6594
|
+
const resolve5 = pendingPermission.resolve;
|
|
6411
6595
|
pendingPermission = null;
|
|
6412
6596
|
screen.setPermissionPrompt(null);
|
|
6413
|
-
|
|
6597
|
+
resolve5({ outcome: { outcome: "cancelled" } });
|
|
6414
6598
|
}
|
|
6415
6599
|
closeAgentText();
|
|
6416
6600
|
if (toolsBlockStartedAt !== null) {
|
|
@@ -6627,7 +6811,7 @@ var init_tui = __esm({
|
|
|
6627
6811
|
// src/cli.ts
|
|
6628
6812
|
import { readFileSync } from "fs";
|
|
6629
6813
|
import { fileURLToPath } from "url";
|
|
6630
|
-
import { dirname as
|
|
6814
|
+
import { dirname as dirname4, resolve as resolve4 } from "path";
|
|
6631
6815
|
|
|
6632
6816
|
// src/cli/parse-args.ts
|
|
6633
6817
|
function parseArgs(argv) {
|
|
@@ -6727,13 +6911,13 @@ New token: ${newToken}
|
|
|
6727
6911
|
// src/cli/commands/daemon.ts
|
|
6728
6912
|
init_paths();
|
|
6729
6913
|
init_config();
|
|
6730
|
-
import * as
|
|
6914
|
+
import * as fsp5 from "fs/promises";
|
|
6731
6915
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
6732
6916
|
|
|
6733
6917
|
// src/daemon/server.ts
|
|
6734
6918
|
init_config();
|
|
6735
|
-
import * as
|
|
6736
|
-
import * as
|
|
6919
|
+
import * as fs10 from "fs";
|
|
6920
|
+
import * as fsp3 from "fs/promises";
|
|
6737
6921
|
import Fastify from "fastify";
|
|
6738
6922
|
import websocketPlugin from "@fastify/websocket";
|
|
6739
6923
|
import pino from "pino";
|
|
@@ -6741,8 +6925,214 @@ import createPinoRoll from "pino-roll";
|
|
|
6741
6925
|
|
|
6742
6926
|
// src/core/registry.ts
|
|
6743
6927
|
init_paths();
|
|
6744
|
-
import * as
|
|
6928
|
+
import * as fs4 from "fs/promises";
|
|
6745
6929
|
import { z as z2 } from "zod";
|
|
6930
|
+
|
|
6931
|
+
// src/core/binary-install.ts
|
|
6932
|
+
init_paths();
|
|
6933
|
+
import * as fs3 from "fs";
|
|
6934
|
+
import * as fsp from "fs/promises";
|
|
6935
|
+
import * as path2 from "path";
|
|
6936
|
+
import { spawn } from "child_process";
|
|
6937
|
+
import { Readable } from "stream";
|
|
6938
|
+
function currentPlatformKey() {
|
|
6939
|
+
const osPart = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "windows" : void 0;
|
|
6940
|
+
const archPart = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : void 0;
|
|
6941
|
+
if (!osPart || !archPart) {
|
|
6942
|
+
return void 0;
|
|
6943
|
+
}
|
|
6944
|
+
return `${osPart}-${archPart}`;
|
|
6945
|
+
}
|
|
6946
|
+
function pickBinaryTarget(distribution, platformKey = currentPlatformKey()) {
|
|
6947
|
+
if (!platformKey) {
|
|
6948
|
+
return void 0;
|
|
6949
|
+
}
|
|
6950
|
+
return distribution[platformKey];
|
|
6951
|
+
}
|
|
6952
|
+
var logSink = (msg) => {
|
|
6953
|
+
process.stderr.write(msg + "\n");
|
|
6954
|
+
};
|
|
6955
|
+
function setBinaryInstallLogger(log) {
|
|
6956
|
+
logSink = log ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
6957
|
+
}
|
|
6958
|
+
async function ensureBinary(args) {
|
|
6959
|
+
if (!args.target.archive) {
|
|
6960
|
+
throw new Error(
|
|
6961
|
+
`Agent ${args.agentId} has no archive URL for ${currentPlatformKey() ?? "this platform"}`
|
|
6962
|
+
);
|
|
6963
|
+
}
|
|
6964
|
+
if (!args.target.cmd) {
|
|
6965
|
+
throw new Error(`Agent ${args.agentId} has no cmd in its binary target`);
|
|
6966
|
+
}
|
|
6967
|
+
const platformKey = currentPlatformKey();
|
|
6968
|
+
if (!platformKey) {
|
|
6969
|
+
throw new Error(
|
|
6970
|
+
`Agent ${args.agentId}: cannot determine platform key for ${process.platform}/${process.arch}`
|
|
6971
|
+
);
|
|
6972
|
+
}
|
|
6973
|
+
const installDir = paths.agentInstallDir(
|
|
6974
|
+
args.agentId,
|
|
6975
|
+
platformKey,
|
|
6976
|
+
args.version
|
|
6977
|
+
);
|
|
6978
|
+
const cmdPath = path2.resolve(installDir, args.target.cmd);
|
|
6979
|
+
if (await fileExists(cmdPath)) {
|
|
6980
|
+
return cmdPath;
|
|
6981
|
+
}
|
|
6982
|
+
await downloadAndExtract({
|
|
6983
|
+
agentId: args.agentId,
|
|
6984
|
+
archiveUrl: args.target.archive,
|
|
6985
|
+
installDir
|
|
6986
|
+
});
|
|
6987
|
+
if (!await fileExists(cmdPath)) {
|
|
6988
|
+
throw new Error(
|
|
6989
|
+
`Agent ${args.agentId}: extracted archive did not contain ${args.target.cmd} (looked in ${installDir})`
|
|
6990
|
+
);
|
|
6991
|
+
}
|
|
6992
|
+
if (process.platform !== "win32") {
|
|
6993
|
+
await fsp.chmod(cmdPath, 493).catch(() => void 0);
|
|
6994
|
+
}
|
|
6995
|
+
return cmdPath;
|
|
6996
|
+
}
|
|
6997
|
+
async function downloadAndExtract(args) {
|
|
6998
|
+
await fsp.mkdir(path2.dirname(args.installDir), { recursive: true });
|
|
6999
|
+
const tempDir = await fsp.mkdtemp(`${args.installDir}.partial-`);
|
|
7000
|
+
try {
|
|
7001
|
+
logSink(`hydra-acp: downloading ${args.agentId} from ${args.archiveUrl}`);
|
|
7002
|
+
const archivePath = await downloadTo({
|
|
7003
|
+
url: args.archiveUrl,
|
|
7004
|
+
dir: tempDir,
|
|
7005
|
+
agentId: args.agentId
|
|
7006
|
+
});
|
|
7007
|
+
logSink(`hydra-acp: extracting ${args.agentId}`);
|
|
7008
|
+
await extract(archivePath, tempDir);
|
|
7009
|
+
await fsp.unlink(archivePath).catch(() => void 0);
|
|
7010
|
+
try {
|
|
7011
|
+
await fsp.rename(tempDir, args.installDir);
|
|
7012
|
+
} catch (err) {
|
|
7013
|
+
const e = err;
|
|
7014
|
+
if ((e.code === "EEXIST" || e.code === "ENOTEMPTY") && await fileExists(args.installDir)) {
|
|
7015
|
+
await fsp.rm(tempDir, { recursive: true, force: true }).catch(
|
|
7016
|
+
() => void 0
|
|
7017
|
+
);
|
|
7018
|
+
return;
|
|
7019
|
+
}
|
|
7020
|
+
throw err;
|
|
7021
|
+
}
|
|
7022
|
+
logSink(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
7023
|
+
} catch (err) {
|
|
7024
|
+
await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
7025
|
+
throw err;
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
7028
|
+
async function downloadTo(args) {
|
|
7029
|
+
const filename = inferArchiveName(args.url);
|
|
7030
|
+
const dest = path2.join(args.dir, filename);
|
|
7031
|
+
const response = await fetch(args.url, { redirect: "follow" });
|
|
7032
|
+
if (!response.ok || !response.body) {
|
|
7033
|
+
throw new Error(
|
|
7034
|
+
`Failed to download ${args.url}: HTTP ${response.status} ${response.statusText}`
|
|
7035
|
+
);
|
|
7036
|
+
}
|
|
7037
|
+
const total = Number(response.headers.get("content-length") ?? "0");
|
|
7038
|
+
const out = fs3.createWriteStream(dest);
|
|
7039
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
7040
|
+
let received = 0;
|
|
7041
|
+
let lastEmit = Date.now();
|
|
7042
|
+
const EMIT_INTERVAL_MS = 2e3;
|
|
7043
|
+
nodeStream.on("data", (chunk) => {
|
|
7044
|
+
received += chunk.length;
|
|
7045
|
+
const now = Date.now();
|
|
7046
|
+
if (now - lastEmit < EMIT_INTERVAL_MS) {
|
|
7047
|
+
return;
|
|
7048
|
+
}
|
|
7049
|
+
lastEmit = now;
|
|
7050
|
+
logSink(formatProgress(args.agentId, received, total));
|
|
7051
|
+
});
|
|
7052
|
+
await new Promise((resolve5, reject) => {
|
|
7053
|
+
nodeStream.on("error", reject);
|
|
7054
|
+
out.on("error", reject);
|
|
7055
|
+
out.on("finish", () => resolve5());
|
|
7056
|
+
nodeStream.pipe(out);
|
|
7057
|
+
});
|
|
7058
|
+
logSink(formatProgress(
|
|
7059
|
+
args.agentId,
|
|
7060
|
+
received,
|
|
7061
|
+
total,
|
|
7062
|
+
/* done */
|
|
7063
|
+
true
|
|
7064
|
+
));
|
|
7065
|
+
return dest;
|
|
7066
|
+
}
|
|
7067
|
+
function formatProgress(agentId, received, total, done = false) {
|
|
7068
|
+
const rxMb = (received / 1e6).toFixed(1);
|
|
7069
|
+
if (total > 0) {
|
|
7070
|
+
const totalMb = (total / 1e6).toFixed(1);
|
|
7071
|
+
const pct = Math.min(100, Math.floor(received / total * 100));
|
|
7072
|
+
const tag2 = done ? "downloaded" : "downloading";
|
|
7073
|
+
return `hydra-acp: ${tag2} ${agentId} ${rxMb}/${totalMb} MB (${pct}%)`;
|
|
7074
|
+
}
|
|
7075
|
+
const tag = done ? "downloaded" : "downloading";
|
|
7076
|
+
return `hydra-acp: ${tag} ${agentId} ${rxMb} MB`;
|
|
7077
|
+
}
|
|
7078
|
+
function inferArchiveName(url) {
|
|
7079
|
+
const u = new URL(url);
|
|
7080
|
+
const base = path2.posix.basename(u.pathname);
|
|
7081
|
+
return base || "archive";
|
|
7082
|
+
}
|
|
7083
|
+
async function extract(archivePath, dest) {
|
|
7084
|
+
const lower = archivePath.toLowerCase();
|
|
7085
|
+
if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz") || lower.endsWith(".tar")) {
|
|
7086
|
+
await run("tar", ["-xf", archivePath, "-C", dest]);
|
|
7087
|
+
return;
|
|
7088
|
+
}
|
|
7089
|
+
if (lower.endsWith(".zip")) {
|
|
7090
|
+
if (await hasCommand("unzip")) {
|
|
7091
|
+
await run("unzip", ["-q", archivePath, "-d", dest]);
|
|
7092
|
+
return;
|
|
7093
|
+
}
|
|
7094
|
+
await run("tar", ["-xf", archivePath, "-C", dest]);
|
|
7095
|
+
return;
|
|
7096
|
+
}
|
|
7097
|
+
throw new Error(`Unsupported archive format: ${archivePath}`);
|
|
7098
|
+
}
|
|
7099
|
+
function run(cmd, args) {
|
|
7100
|
+
return new Promise((resolve5, reject) => {
|
|
7101
|
+
const child = spawn(cmd, args, {
|
|
7102
|
+
stdio: ["ignore", "ignore", "inherit"]
|
|
7103
|
+
});
|
|
7104
|
+
child.on("error", reject);
|
|
7105
|
+
child.on("exit", (code, signal) => {
|
|
7106
|
+
if (code === 0) {
|
|
7107
|
+
resolve5();
|
|
7108
|
+
return;
|
|
7109
|
+
}
|
|
7110
|
+
reject(
|
|
7111
|
+
new Error(
|
|
7112
|
+
`${cmd} ${args.join(" ")} exited with ${code !== null ? `code ${code}` : `signal ${signal}`}`
|
|
7113
|
+
)
|
|
7114
|
+
);
|
|
7115
|
+
});
|
|
7116
|
+
});
|
|
7117
|
+
}
|
|
7118
|
+
async function hasCommand(name) {
|
|
7119
|
+
return new Promise((resolve5) => {
|
|
7120
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
7121
|
+
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
7122
|
+
child.on("error", () => resolve5(false));
|
|
7123
|
+
child.on("exit", (code) => resolve5(code === 0));
|
|
7124
|
+
});
|
|
7125
|
+
}
|
|
7126
|
+
async function fileExists(p) {
|
|
7127
|
+
try {
|
|
7128
|
+
await fsp.access(p);
|
|
7129
|
+
return true;
|
|
7130
|
+
} catch {
|
|
7131
|
+
return false;
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7134
|
+
|
|
7135
|
+
// src/core/registry.ts
|
|
6746
7136
|
var NpxDistribution = z2.object({
|
|
6747
7137
|
package: z2.string(),
|
|
6748
7138
|
args: z2.array(z2.string()).optional(),
|
|
@@ -6750,7 +7140,9 @@ var NpxDistribution = z2.object({
|
|
|
6750
7140
|
});
|
|
6751
7141
|
var BinaryTarget = z2.object({
|
|
6752
7142
|
archive: z2.string().url().optional(),
|
|
6753
|
-
cmd: z2.string().optional()
|
|
7143
|
+
cmd: z2.string().optional(),
|
|
7144
|
+
args: z2.array(z2.string()).optional(),
|
|
7145
|
+
env: z2.record(z2.string()).optional()
|
|
6754
7146
|
});
|
|
6755
7147
|
var BinaryDistribution = z2.object({
|
|
6756
7148
|
"darwin-aarch64": BinaryTarget.optional(),
|
|
@@ -6839,34 +7231,59 @@ var Registry = class {
|
|
|
6839
7231
|
if (!response.ok) {
|
|
6840
7232
|
throw new Error(`Registry fetch failed: HTTP ${response.status}`);
|
|
6841
7233
|
}
|
|
6842
|
-
const
|
|
6843
|
-
const data = RegistryDocument.parse(
|
|
6844
|
-
return { fetchedAt: Date.now(), data };
|
|
7234
|
+
const raw = await response.json();
|
|
7235
|
+
const data = RegistryDocument.parse(raw);
|
|
7236
|
+
return { fetchedAt: Date.now(), raw, data };
|
|
6845
7237
|
}
|
|
6846
7238
|
async readDiskCache() {
|
|
7239
|
+
let text;
|
|
6847
7240
|
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
|
-
}
|
|
7241
|
+
text = await fs4.readFile(paths.registryCache(), "utf8");
|
|
6853
7242
|
} catch (err) {
|
|
6854
7243
|
const e = err;
|
|
6855
|
-
if (e.code
|
|
6856
|
-
|
|
7244
|
+
if (e.code === "ENOENT") {
|
|
7245
|
+
return void 0;
|
|
6857
7246
|
}
|
|
7247
|
+
throw err;
|
|
7248
|
+
}
|
|
7249
|
+
try {
|
|
7250
|
+
const parsed = JSON.parse(text);
|
|
7251
|
+
if (typeof parsed.fetchedAt !== "number" || parsed.data === void 0) {
|
|
7252
|
+
return void 0;
|
|
7253
|
+
}
|
|
7254
|
+
const data = RegistryDocument.parse(parsed.data);
|
|
7255
|
+
return { fetchedAt: parsed.fetchedAt, raw: parsed.data, data };
|
|
7256
|
+
} catch {
|
|
7257
|
+
return void 0;
|
|
6858
7258
|
}
|
|
6859
|
-
return void 0;
|
|
6860
7259
|
}
|
|
7260
|
+
// Atomic write: dump to a sibling temp path, then rename onto the
|
|
7261
|
+
// target. POSIX rename is atomic within a filesystem, so readers
|
|
7262
|
+
// either see the old file or the fully-written new file — never a
|
|
7263
|
+
// truncated middle. This also makes simultaneous writers safe
|
|
7264
|
+
// without a lock file: the loser of the rename race just gets its
|
|
7265
|
+
// version replaced by the winner's.
|
|
6861
7266
|
async writeDiskCache(cache) {
|
|
6862
|
-
await
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
7267
|
+
await fs4.mkdir(paths.home(), { recursive: true });
|
|
7268
|
+
const final = paths.registryCache();
|
|
7269
|
+
const tmp = `${final}.tmp-${process.pid}-${randSuffix()}`;
|
|
7270
|
+
const body = JSON.stringify(
|
|
7271
|
+
{ fetchedAt: cache.fetchedAt, data: cache.raw },
|
|
7272
|
+
null,
|
|
7273
|
+
2
|
|
7274
|
+
) + "\n";
|
|
7275
|
+
try {
|
|
7276
|
+
await fs4.writeFile(tmp, body, "utf8");
|
|
7277
|
+
await fs4.rename(tmp, final);
|
|
7278
|
+
} catch (err) {
|
|
7279
|
+
await fs4.unlink(tmp).catch(() => void 0);
|
|
7280
|
+
throw err;
|
|
7281
|
+
}
|
|
6868
7282
|
}
|
|
6869
7283
|
};
|
|
7284
|
+
function randSuffix() {
|
|
7285
|
+
return Math.random().toString(36).slice(2, 10);
|
|
7286
|
+
}
|
|
6870
7287
|
function npxPackageBasename(agent) {
|
|
6871
7288
|
const pkg = agent.distribution.npx?.package;
|
|
6872
7289
|
if (!pkg) {
|
|
@@ -6877,7 +7294,7 @@ function npxPackageBasename(agent) {
|
|
|
6877
7294
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
6878
7295
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
6879
7296
|
}
|
|
6880
|
-
function planSpawn(agent, extraArgs = []) {
|
|
7297
|
+
async function planSpawn(agent, extraArgs = []) {
|
|
6881
7298
|
if (agent.distribution.npx) {
|
|
6882
7299
|
const npx = agent.distribution.npx;
|
|
6883
7300
|
const args = ["-y", npx.package, ...npx.args ?? [], ...extraArgs];
|
|
@@ -6888,9 +7305,22 @@ function planSpawn(agent, extraArgs = []) {
|
|
|
6888
7305
|
};
|
|
6889
7306
|
}
|
|
6890
7307
|
if (agent.distribution.binary) {
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
7308
|
+
const target = pickBinaryTarget(agent.distribution.binary);
|
|
7309
|
+
if (!target) {
|
|
7310
|
+
throw new Error(
|
|
7311
|
+
`Agent ${agent.id} has no binary distribution for ${currentPlatformKey() ?? "this platform"}.`
|
|
7312
|
+
);
|
|
7313
|
+
}
|
|
7314
|
+
const cmdPath = await ensureBinary({
|
|
7315
|
+
agentId: agent.id,
|
|
7316
|
+
version: agent.version ?? "current",
|
|
7317
|
+
target
|
|
7318
|
+
});
|
|
7319
|
+
return {
|
|
7320
|
+
command: cmdPath,
|
|
7321
|
+
args: [...target.args ?? [], ...extraArgs],
|
|
7322
|
+
env: target.env ?? {}
|
|
7323
|
+
};
|
|
6894
7324
|
}
|
|
6895
7325
|
if (agent.distribution.uvx) {
|
|
6896
7326
|
const uvx = agent.distribution.uvx;
|
|
@@ -6905,11 +7335,11 @@ function planSpawn(agent, extraArgs = []) {
|
|
|
6905
7335
|
}
|
|
6906
7336
|
|
|
6907
7337
|
// src/core/session-manager.ts
|
|
6908
|
-
import * as
|
|
7338
|
+
import * as fs8 from "fs/promises";
|
|
6909
7339
|
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
6910
7340
|
|
|
6911
7341
|
// src/core/agent-instance.ts
|
|
6912
|
-
import { spawn } from "child_process";
|
|
7342
|
+
import { spawn as spawn2 } from "child_process";
|
|
6913
7343
|
|
|
6914
7344
|
// src/acp/framing.ts
|
|
6915
7345
|
init_types();
|
|
@@ -6965,13 +7395,13 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
6965
7395
|
throw new Error("stream is closed");
|
|
6966
7396
|
}
|
|
6967
7397
|
const line = JSON.stringify(message) + "\n";
|
|
6968
|
-
await new Promise((
|
|
7398
|
+
await new Promise((resolve5, reject) => {
|
|
6969
7399
|
stdin.write(line, (err) => {
|
|
6970
7400
|
if (err) {
|
|
6971
7401
|
reject(err);
|
|
6972
7402
|
return;
|
|
6973
7403
|
}
|
|
6974
|
-
|
|
7404
|
+
resolve5();
|
|
6975
7405
|
});
|
|
6976
7406
|
});
|
|
6977
7407
|
},
|
|
@@ -7023,7 +7453,7 @@ var AgentInstance = class _AgentInstance {
|
|
|
7023
7453
|
...opts.plan.env,
|
|
7024
7454
|
...opts.extraEnv ?? {}
|
|
7025
7455
|
};
|
|
7026
|
-
const child =
|
|
7456
|
+
const child = spawn2(opts.plan.command, opts.plan.args, {
|
|
7027
7457
|
cwd: opts.cwd,
|
|
7028
7458
|
env,
|
|
7029
7459
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7050,8 +7480,8 @@ init_session();
|
|
|
7050
7480
|
|
|
7051
7481
|
// src/core/session-store.ts
|
|
7052
7482
|
init_paths();
|
|
7053
|
-
import * as
|
|
7054
|
-
import * as
|
|
7483
|
+
import * as fs5 from "fs/promises";
|
|
7484
|
+
import * as path3 from "path";
|
|
7055
7485
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
7056
7486
|
import { z as z4 } from "zod";
|
|
7057
7487
|
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -7064,6 +7494,12 @@ var PersistedAgentCommand = z4.object({
|
|
|
7064
7494
|
name: z4.string(),
|
|
7065
7495
|
description: z4.string().optional()
|
|
7066
7496
|
});
|
|
7497
|
+
var PersistedUsage = z4.object({
|
|
7498
|
+
used: z4.number().optional(),
|
|
7499
|
+
size: z4.number().optional(),
|
|
7500
|
+
costAmount: z4.number().optional(),
|
|
7501
|
+
costCurrency: z4.string().optional()
|
|
7502
|
+
});
|
|
7067
7503
|
var SessionRecord = z4.object({
|
|
7068
7504
|
version: z4.literal(1),
|
|
7069
7505
|
sessionId: z4.string(),
|
|
@@ -7091,6 +7527,7 @@ var SessionRecord = z4.object({
|
|
|
7091
7527
|
// replay of a snapshot-shaped notification.
|
|
7092
7528
|
currentModel: z4.string().optional(),
|
|
7093
7529
|
currentMode: z4.string().optional(),
|
|
7530
|
+
currentUsage: PersistedUsage.optional(),
|
|
7094
7531
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
7095
7532
|
createdAt: z4.string(),
|
|
7096
7533
|
updatedAt: z4.string()
|
|
@@ -7104,9 +7541,9 @@ function assertSafeId(id) {
|
|
|
7104
7541
|
var SessionStore = class {
|
|
7105
7542
|
async write(record) {
|
|
7106
7543
|
assertSafeId(record.sessionId);
|
|
7107
|
-
await
|
|
7544
|
+
await fs5.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
7108
7545
|
const full = { version: 1, ...record };
|
|
7109
|
-
await
|
|
7546
|
+
await fs5.writeFile(
|
|
7110
7547
|
paths.sessionFile(record.sessionId),
|
|
7111
7548
|
JSON.stringify(full, null, 2) + "\n",
|
|
7112
7549
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -7118,7 +7555,7 @@ var SessionStore = class {
|
|
|
7118
7555
|
}
|
|
7119
7556
|
let raw;
|
|
7120
7557
|
try {
|
|
7121
|
-
raw = await
|
|
7558
|
+
raw = await fs5.readFile(paths.sessionFile(sessionId), "utf8");
|
|
7122
7559
|
} catch (err) {
|
|
7123
7560
|
const e = err;
|
|
7124
7561
|
if (e.code === "ENOENT") {
|
|
@@ -7137,7 +7574,7 @@ var SessionStore = class {
|
|
|
7137
7574
|
return;
|
|
7138
7575
|
}
|
|
7139
7576
|
try {
|
|
7140
|
-
await
|
|
7577
|
+
await fs5.unlink(paths.sessionFile(sessionId));
|
|
7141
7578
|
} catch (err) {
|
|
7142
7579
|
const e = err;
|
|
7143
7580
|
if (e.code !== "ENOENT") {
|
|
@@ -7145,7 +7582,7 @@ var SessionStore = class {
|
|
|
7145
7582
|
}
|
|
7146
7583
|
}
|
|
7147
7584
|
try {
|
|
7148
|
-
await
|
|
7585
|
+
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
7149
7586
|
} catch (err) {
|
|
7150
7587
|
const e = err;
|
|
7151
7588
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -7175,7 +7612,7 @@ var SessionStore = class {
|
|
|
7175
7612
|
async list() {
|
|
7176
7613
|
let entries;
|
|
7177
7614
|
try {
|
|
7178
|
-
entries = await
|
|
7615
|
+
entries = await fs5.readdir(paths.sessionsDir());
|
|
7179
7616
|
} catch (err) {
|
|
7180
7617
|
const e = err;
|
|
7181
7618
|
if (e.code === "ENOENT") {
|
|
@@ -7206,6 +7643,7 @@ function recordFromMemorySession(args) {
|
|
|
7206
7643
|
agentArgs: args.agentArgs,
|
|
7207
7644
|
currentModel: args.currentModel,
|
|
7208
7645
|
currentMode: args.currentMode,
|
|
7646
|
+
currentUsage: args.currentUsage,
|
|
7209
7647
|
agentCommands: args.agentCommands,
|
|
7210
7648
|
createdAt: args.createdAt ?? now,
|
|
7211
7649
|
updatedAt: args.updatedAt ?? now
|
|
@@ -7214,7 +7652,7 @@ function recordFromMemorySession(args) {
|
|
|
7214
7652
|
|
|
7215
7653
|
// src/core/history-store.ts
|
|
7216
7654
|
init_paths();
|
|
7217
|
-
import * as
|
|
7655
|
+
import * as fs6 from "fs/promises";
|
|
7218
7656
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
7219
7657
|
var MAX_ENTRIES = 1e3;
|
|
7220
7658
|
var HistoryStore = class {
|
|
@@ -7227,9 +7665,9 @@ var HistoryStore = class {
|
|
|
7227
7665
|
return;
|
|
7228
7666
|
}
|
|
7229
7667
|
return this.enqueue(sessionId, async () => {
|
|
7230
|
-
await
|
|
7668
|
+
await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
7231
7669
|
const line = JSON.stringify(entry) + "\n";
|
|
7232
|
-
await
|
|
7670
|
+
await fs6.appendFile(paths.historyFile(sessionId), line, {
|
|
7233
7671
|
encoding: "utf8",
|
|
7234
7672
|
mode: 384
|
|
7235
7673
|
});
|
|
@@ -7240,9 +7678,9 @@ var HistoryStore = class {
|
|
|
7240
7678
|
return;
|
|
7241
7679
|
}
|
|
7242
7680
|
return this.enqueue(sessionId, async () => {
|
|
7243
|
-
await
|
|
7681
|
+
await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
7244
7682
|
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
7245
|
-
await
|
|
7683
|
+
await fs6.writeFile(paths.historyFile(sessionId), body, {
|
|
7246
7684
|
encoding: "utf8",
|
|
7247
7685
|
mode: 384
|
|
7248
7686
|
});
|
|
@@ -7259,7 +7697,7 @@ var HistoryStore = class {
|
|
|
7259
7697
|
return this.enqueue(sessionId, async () => {
|
|
7260
7698
|
let raw;
|
|
7261
7699
|
try {
|
|
7262
|
-
raw = await
|
|
7700
|
+
raw = await fs6.readFile(paths.historyFile(sessionId), "utf8");
|
|
7263
7701
|
} catch (err) {
|
|
7264
7702
|
const e = err;
|
|
7265
7703
|
if (e.code === "ENOENT") {
|
|
@@ -7272,7 +7710,7 @@ var HistoryStore = class {
|
|
|
7272
7710
|
return;
|
|
7273
7711
|
}
|
|
7274
7712
|
const trimmed = lines.slice(-maxEntries);
|
|
7275
|
-
await
|
|
7713
|
+
await fs6.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
7276
7714
|
encoding: "utf8",
|
|
7277
7715
|
mode: 384
|
|
7278
7716
|
});
|
|
@@ -7288,7 +7726,7 @@ var HistoryStore = class {
|
|
|
7288
7726
|
}
|
|
7289
7727
|
let raw;
|
|
7290
7728
|
try {
|
|
7291
|
-
raw = await
|
|
7729
|
+
raw = await fs6.readFile(paths.historyFile(sessionId), "utf8");
|
|
7292
7730
|
} catch (err) {
|
|
7293
7731
|
const e = err;
|
|
7294
7732
|
if (e.code === "ENOENT") {
|
|
@@ -7334,7 +7772,7 @@ var HistoryStore = class {
|
|
|
7334
7772
|
}
|
|
7335
7773
|
return this.enqueue(sessionId, async () => {
|
|
7336
7774
|
try {
|
|
7337
|
-
await
|
|
7775
|
+
await fs6.unlink(paths.historyFile(sessionId));
|
|
7338
7776
|
} catch (err) {
|
|
7339
7777
|
const e = err;
|
|
7340
7778
|
if (e.code !== "ENOENT") {
|
|
@@ -7342,7 +7780,7 @@ var HistoryStore = class {
|
|
|
7342
7780
|
}
|
|
7343
7781
|
}
|
|
7344
7782
|
try {
|
|
7345
|
-
await
|
|
7783
|
+
await fs6.rmdir(paths.sessionDir(sessionId));
|
|
7346
7784
|
} catch (err) {
|
|
7347
7785
|
const e = err;
|
|
7348
7786
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -7378,6 +7816,7 @@ var SessionManager = class {
|
|
|
7378
7816
|
this.store = store ?? new SessionStore();
|
|
7379
7817
|
this.histories = new HistoryStore();
|
|
7380
7818
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
7819
|
+
this.defaultModels = options.defaultModels ?? {};
|
|
7381
7820
|
}
|
|
7382
7821
|
registry;
|
|
7383
7822
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -7386,6 +7825,7 @@ var SessionManager = class {
|
|
|
7386
7825
|
store;
|
|
7387
7826
|
histories;
|
|
7388
7827
|
idleTimeoutMs;
|
|
7828
|
+
defaultModels;
|
|
7389
7829
|
// Serialize meta.json read-modify-write operations per session id so
|
|
7390
7830
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
7391
7831
|
// back-to-back) don't lose writes via interleaved reads.
|
|
@@ -7395,7 +7835,8 @@ var SessionManager = class {
|
|
|
7395
7835
|
agentId: params.agentId,
|
|
7396
7836
|
cwd: params.cwd,
|
|
7397
7837
|
agentArgs: params.agentArgs,
|
|
7398
|
-
mcpServers: params.mcpServers
|
|
7838
|
+
mcpServers: params.mcpServers,
|
|
7839
|
+
model: params.model
|
|
7399
7840
|
});
|
|
7400
7841
|
const session = new Session({
|
|
7401
7842
|
cwd: params.cwd,
|
|
@@ -7407,7 +7848,8 @@ var SessionManager = class {
|
|
|
7407
7848
|
agentArgs: params.agentArgs,
|
|
7408
7849
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
7409
7850
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7410
|
-
historyStore: this.histories
|
|
7851
|
+
historyStore: this.histories,
|
|
7852
|
+
currentModel: fresh.initialModel
|
|
7411
7853
|
});
|
|
7412
7854
|
await this.attachManagerHooks(session);
|
|
7413
7855
|
return session;
|
|
@@ -7452,7 +7894,7 @@ var SessionManager = class {
|
|
|
7452
7894
|
if (params.upstreamSessionId === "") {
|
|
7453
7895
|
return this.doResurrectFromImport(params);
|
|
7454
7896
|
}
|
|
7455
|
-
const plan = planSpawn(agentDef, params.agentArgs ?? []);
|
|
7897
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
7456
7898
|
const agent = this.spawner({
|
|
7457
7899
|
agentId: params.agentId,
|
|
7458
7900
|
cwd: params.cwd,
|
|
@@ -7465,11 +7907,14 @@ var SessionManager = class {
|
|
|
7465
7907
|
});
|
|
7466
7908
|
let loadResult;
|
|
7467
7909
|
try {
|
|
7468
|
-
loadResult = await agent.connection.request(
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7910
|
+
loadResult = await agent.connection.request(
|
|
7911
|
+
"session/load",
|
|
7912
|
+
{
|
|
7913
|
+
sessionId: params.upstreamSessionId,
|
|
7914
|
+
cwd: params.cwd,
|
|
7915
|
+
mcpServers: []
|
|
7916
|
+
}
|
|
7917
|
+
);
|
|
7473
7918
|
} catch (err) {
|
|
7474
7919
|
await agent.kill().catch(() => void 0);
|
|
7475
7920
|
throw new Error(
|
|
@@ -7488,8 +7933,13 @@ var SessionManager = class {
|
|
|
7488
7933
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
7489
7934
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7490
7935
|
historyStore: this.histories,
|
|
7491
|
-
|
|
7936
|
+
// Prefer what we previously stored from a current_model_update; if
|
|
7937
|
+
// we never captured one (e.g. old opencode sessions on disk before
|
|
7938
|
+
// this fix), fall back to the model the agent ships in its
|
|
7939
|
+
// session/load response body.
|
|
7940
|
+
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
7492
7941
|
currentMode: params.currentMode,
|
|
7942
|
+
currentUsage: params.currentUsage,
|
|
7493
7943
|
agentCommands: params.agentCommands,
|
|
7494
7944
|
// Only gate the first-prompt title heuristic when we actually have
|
|
7495
7945
|
// a title to preserve. A title-less session (lost to a write race
|
|
@@ -7527,8 +7977,11 @@ var SessionManager = class {
|
|
|
7527
7977
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
7528
7978
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7529
7979
|
historyStore: this.histories,
|
|
7530
|
-
|
|
7980
|
+
// Prefer the stored value (set by a previous current_model_update);
|
|
7981
|
+
// fall back to whatever the agent ships in its session/new response.
|
|
7982
|
+
currentModel: params.currentModel ?? fresh.initialModel,
|
|
7531
7983
|
currentMode: params.currentMode,
|
|
7984
|
+
currentUsage: params.currentUsage,
|
|
7532
7985
|
agentCommands: params.agentCommands,
|
|
7533
7986
|
firstPromptSeeded: !!params.title,
|
|
7534
7987
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
@@ -7538,7 +7991,7 @@ var SessionManager = class {
|
|
|
7538
7991
|
return session;
|
|
7539
7992
|
}
|
|
7540
7993
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
7541
|
-
// → session/new. Shared by create() and the /hydra
|
|
7994
|
+
// → session/new. Shared by create() and the /hydra agent path so both
|
|
7542
7995
|
// go through the same env / capabilities / error-handling.
|
|
7543
7996
|
async bootstrapAgent(params) {
|
|
7544
7997
|
const agentDef = await this.registry.getAgent(params.agentId);
|
|
@@ -7549,7 +8002,7 @@ var SessionManager = class {
|
|
|
7549
8002
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
7550
8003
|
throw err;
|
|
7551
8004
|
}
|
|
7552
|
-
const plan = planSpawn(agentDef, params.agentArgs ?? []);
|
|
8005
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
7553
8006
|
const agent = this.spawner({
|
|
7554
8007
|
agentId: params.agentId,
|
|
7555
8008
|
cwd: params.cwd,
|
|
@@ -7561,14 +8014,36 @@ var SessionManager = class {
|
|
|
7561
8014
|
clientCapabilities: {},
|
|
7562
8015
|
clientInfo: { name: "hydra", version: "0.1.0" }
|
|
7563
8016
|
});
|
|
7564
|
-
const newResult = await agent.connection.request(
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
8017
|
+
const newResult = await agent.connection.request(
|
|
8018
|
+
"session/new",
|
|
8019
|
+
{
|
|
8020
|
+
cwd: params.cwd,
|
|
8021
|
+
mcpServers: params.mcpServers ?? []
|
|
8022
|
+
}
|
|
8023
|
+
);
|
|
8024
|
+
const sessionIdRaw = newResult.sessionId;
|
|
8025
|
+
if (typeof sessionIdRaw !== "string") {
|
|
8026
|
+
throw new Error(
|
|
8027
|
+
`agent ${params.agentId} returned a non-string sessionId from session/new`
|
|
8028
|
+
);
|
|
8029
|
+
}
|
|
8030
|
+
let initialModel = extractInitialModel(newResult);
|
|
8031
|
+
const desired = params.model ?? this.defaultModels[params.agentId];
|
|
8032
|
+
if (desired && desired !== initialModel) {
|
|
8033
|
+
try {
|
|
8034
|
+
await agent.connection.request("session/set_model", {
|
|
8035
|
+
sessionId: sessionIdRaw,
|
|
8036
|
+
modelId: desired
|
|
8037
|
+
});
|
|
8038
|
+
initialModel = desired;
|
|
8039
|
+
} catch {
|
|
8040
|
+
}
|
|
8041
|
+
}
|
|
7568
8042
|
return {
|
|
7569
8043
|
agent,
|
|
7570
|
-
upstreamSessionId:
|
|
7571
|
-
agentMeta: newResult._meta
|
|
8044
|
+
upstreamSessionId: sessionIdRaw,
|
|
8045
|
+
agentMeta: newResult._meta,
|
|
8046
|
+
initialModel
|
|
7572
8047
|
};
|
|
7573
8048
|
} catch (err) {
|
|
7574
8049
|
await agent.kill().catch(() => void 0);
|
|
@@ -7579,7 +8054,7 @@ var SessionManager = class {
|
|
|
7579
8054
|
// bookkeeping. Called from both create() and resurrect() so the same
|
|
7580
8055
|
// session record + lifecycle handlers are wired regardless of origin.
|
|
7581
8056
|
// Returns once the initial disk record is written — callers should
|
|
7582
|
-
// await so a subsequent /hydra
|
|
8057
|
+
// await so a subsequent /hydra agent's persistAgentChange (which
|
|
7583
8058
|
// does read-then-write) finds the file in place.
|
|
7584
8059
|
async attachManagerHooks(session) {
|
|
7585
8060
|
session.onClose(({ deleteRecord }) => {
|
|
@@ -7607,6 +8082,11 @@ var SessionManager = class {
|
|
|
7607
8082
|
() => void 0
|
|
7608
8083
|
);
|
|
7609
8084
|
});
|
|
8085
|
+
session.onUsageChange((usage) => {
|
|
8086
|
+
void this.persistSnapshot(session.sessionId, {
|
|
8087
|
+
currentUsage: usageSnapshotToPersisted(usage)
|
|
8088
|
+
}).catch(() => void 0);
|
|
8089
|
+
});
|
|
7610
8090
|
session.onAgentCommandsChange((commands) => {
|
|
7611
8091
|
void this.persistSnapshot(session.sessionId, {
|
|
7612
8092
|
agentCommands: commands.map((c) => ({
|
|
@@ -7655,6 +8135,7 @@ var SessionManager = class {
|
|
|
7655
8135
|
agentArgs: record.agentArgs,
|
|
7656
8136
|
currentModel: record.currentModel,
|
|
7657
8137
|
currentMode: record.currentMode,
|
|
8138
|
+
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
7658
8139
|
agentCommands: record.agentCommands,
|
|
7659
8140
|
createdAt: record.createdAt
|
|
7660
8141
|
};
|
|
@@ -7723,6 +8204,8 @@ var SessionManager = class {
|
|
|
7723
8204
|
cwd: session.cwd,
|
|
7724
8205
|
title: session.title,
|
|
7725
8206
|
agentId: session.agentId,
|
|
8207
|
+
currentModel: session.currentModel,
|
|
8208
|
+
currentUsage: session.currentUsage,
|
|
7726
8209
|
updatedAt: used,
|
|
7727
8210
|
attachedClients: session.attachedCount,
|
|
7728
8211
|
status: "live"
|
|
@@ -7743,6 +8226,8 @@ var SessionManager = class {
|
|
|
7743
8226
|
cwd: r.cwd,
|
|
7744
8227
|
title: r.title,
|
|
7745
8228
|
agentId: r.agentId,
|
|
8229
|
+
currentModel: r.currentModel,
|
|
8230
|
+
currentUsage: r.currentUsage,
|
|
7746
8231
|
updatedAt: used,
|
|
7747
8232
|
attachedClients: 0,
|
|
7748
8233
|
status: "cold"
|
|
@@ -7850,6 +8335,7 @@ var SessionManager = class {
|
|
|
7850
8335
|
title: args.bundle.session.title,
|
|
7851
8336
|
currentModel: args.bundle.session.currentModel,
|
|
7852
8337
|
currentMode: args.bundle.session.currentMode,
|
|
8338
|
+
currentUsage: args.bundle.session.currentUsage,
|
|
7853
8339
|
agentCommands: args.bundle.session.agentCommands,
|
|
7854
8340
|
createdAt: args.preservedCreatedAt ?? now,
|
|
7855
8341
|
updatedAt: now
|
|
@@ -7885,7 +8371,7 @@ var SessionManager = class {
|
|
|
7885
8371
|
});
|
|
7886
8372
|
});
|
|
7887
8373
|
}
|
|
7888
|
-
// Persist an agent swap from /hydra
|
|
8374
|
+
// Persist an agent swap from /hydra agent. The on-disk record's
|
|
7889
8375
|
// agentId + upstreamSessionId both rotate so a daemon restart (and
|
|
7890
8376
|
// later resurrect) brings the session back up on the agent the user
|
|
7891
8377
|
// most recently switched to, not the one it was originally created on.
|
|
@@ -7917,6 +8403,7 @@ var SessionManager = class {
|
|
|
7917
8403
|
...record,
|
|
7918
8404
|
...update.currentModel !== void 0 ? { currentModel: update.currentModel } : {},
|
|
7919
8405
|
...update.currentMode !== void 0 ? { currentMode: update.currentMode } : {},
|
|
8406
|
+
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
7920
8407
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
7921
8408
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7922
8409
|
});
|
|
@@ -7971,13 +8458,73 @@ function mergeForPersistence(session, existing) {
|
|
|
7971
8458
|
agentArgs: session.agentArgs,
|
|
7972
8459
|
currentModel: session.currentModel ?? existing?.currentModel,
|
|
7973
8460
|
currentMode: session.currentMode ?? existing?.currentMode,
|
|
8461
|
+
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
7974
8462
|
agentCommands,
|
|
7975
8463
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
7976
8464
|
});
|
|
7977
8465
|
}
|
|
8466
|
+
function usageSnapshotToPersisted(usage) {
|
|
8467
|
+
if (!usage) {
|
|
8468
|
+
return void 0;
|
|
8469
|
+
}
|
|
8470
|
+
const out = {};
|
|
8471
|
+
if (usage.used !== void 0) {
|
|
8472
|
+
out.used = usage.used;
|
|
8473
|
+
}
|
|
8474
|
+
if (usage.size !== void 0) {
|
|
8475
|
+
out.size = usage.size;
|
|
8476
|
+
}
|
|
8477
|
+
if (usage.costAmount !== void 0) {
|
|
8478
|
+
out.costAmount = usage.costAmount;
|
|
8479
|
+
}
|
|
8480
|
+
if (usage.costCurrency !== void 0) {
|
|
8481
|
+
out.costCurrency = usage.costCurrency;
|
|
8482
|
+
}
|
|
8483
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
8484
|
+
}
|
|
8485
|
+
function persistedUsageToSnapshot(usage) {
|
|
8486
|
+
return usage ? { ...usage } : void 0;
|
|
8487
|
+
}
|
|
8488
|
+
function extractInitialModel(result) {
|
|
8489
|
+
const direct = asString(result.currentModelId) ?? asString(result.currentModel) ?? asString(result.modelId) ?? asString(result.model);
|
|
8490
|
+
if (direct) {
|
|
8491
|
+
return direct;
|
|
8492
|
+
}
|
|
8493
|
+
const models = result.models;
|
|
8494
|
+
if (models && typeof models === "object" && !Array.isArray(models)) {
|
|
8495
|
+
const m = asString(models.currentModelId) ?? asString(models.currentModel);
|
|
8496
|
+
if (m) {
|
|
8497
|
+
return m;
|
|
8498
|
+
}
|
|
8499
|
+
}
|
|
8500
|
+
const meta = result._meta;
|
|
8501
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
8502
|
+
for (const [key, value] of Object.entries(
|
|
8503
|
+
meta
|
|
8504
|
+
)) {
|
|
8505
|
+
if (key === "hydra-acp") {
|
|
8506
|
+
continue;
|
|
8507
|
+
}
|
|
8508
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
8509
|
+
const m = asString(value.modelId) ?? asString(value.model) ?? asString(value.currentModelId);
|
|
8510
|
+
if (m) {
|
|
8511
|
+
return m;
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
return void 0;
|
|
8517
|
+
}
|
|
8518
|
+
function asString(value) {
|
|
8519
|
+
if (typeof value !== "string") {
|
|
8520
|
+
return void 0;
|
|
8521
|
+
}
|
|
8522
|
+
const trimmed = value.trim();
|
|
8523
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
8524
|
+
}
|
|
7978
8525
|
async function loadPromptHistorySafely(sessionId) {
|
|
7979
8526
|
try {
|
|
7980
|
-
const raw = await
|
|
8527
|
+
const raw = await fs8.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
7981
8528
|
const out = [];
|
|
7982
8529
|
for (const line of raw.split("\n")) {
|
|
7983
8530
|
if (line.length === 0) {
|
|
@@ -7998,7 +8545,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
7998
8545
|
}
|
|
7999
8546
|
async function historyMtimeIso(sessionId) {
|
|
8000
8547
|
try {
|
|
8001
|
-
const st = await
|
|
8548
|
+
const st = await fs8.stat(paths.historyFile(sessionId));
|
|
8002
8549
|
return new Date(st.mtimeMs).toISOString();
|
|
8003
8550
|
} catch {
|
|
8004
8551
|
return void 0;
|
|
@@ -8007,10 +8554,10 @@ async function historyMtimeIso(sessionId) {
|
|
|
8007
8554
|
|
|
8008
8555
|
// src/core/extensions.ts
|
|
8009
8556
|
init_paths();
|
|
8010
|
-
import { spawn as
|
|
8011
|
-
import * as
|
|
8012
|
-
import * as
|
|
8013
|
-
import * as
|
|
8557
|
+
import { spawn as spawn3 } from "child_process";
|
|
8558
|
+
import * as fs9 from "fs";
|
|
8559
|
+
import * as fsp2 from "fs/promises";
|
|
8560
|
+
import * as path5 from "path";
|
|
8014
8561
|
var RESTART_BASE_MS = 1e3;
|
|
8015
8562
|
var RESTART_CAP_MS = 6e4;
|
|
8016
8563
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -8031,7 +8578,7 @@ var ExtensionManager = class {
|
|
|
8031
8578
|
if (!this.context) {
|
|
8032
8579
|
throw new Error("ExtensionManager: setContext must be called before start");
|
|
8033
8580
|
}
|
|
8034
|
-
await
|
|
8581
|
+
await fsp2.mkdir(paths.extensionsDir(), { recursive: true });
|
|
8035
8582
|
await this.reapOrphans();
|
|
8036
8583
|
for (const entry of this.entries.values()) {
|
|
8037
8584
|
if (!entry.config.enabled) {
|
|
@@ -8057,9 +8604,9 @@ var ExtensionManager = class {
|
|
|
8057
8604
|
} catch {
|
|
8058
8605
|
}
|
|
8059
8606
|
tasks.push(
|
|
8060
|
-
new Promise((
|
|
8607
|
+
new Promise((resolve5) => {
|
|
8061
8608
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
8062
|
-
|
|
8609
|
+
resolve5();
|
|
8063
8610
|
return;
|
|
8064
8611
|
}
|
|
8065
8612
|
const timer = setTimeout(() => {
|
|
@@ -8067,11 +8614,11 @@ var ExtensionManager = class {
|
|
|
8067
8614
|
child.kill("SIGKILL");
|
|
8068
8615
|
} catch {
|
|
8069
8616
|
}
|
|
8070
|
-
|
|
8617
|
+
resolve5();
|
|
8071
8618
|
}, STOP_GRACE_MS);
|
|
8072
8619
|
child.on("exit", () => {
|
|
8073
8620
|
clearTimeout(timer);
|
|
8074
|
-
|
|
8621
|
+
resolve5();
|
|
8075
8622
|
});
|
|
8076
8623
|
})
|
|
8077
8624
|
);
|
|
@@ -8179,8 +8726,8 @@ var ExtensionManager = class {
|
|
|
8179
8726
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
8180
8727
|
return;
|
|
8181
8728
|
}
|
|
8182
|
-
const exited = new Promise((
|
|
8183
|
-
entry.exitWaiters.push(
|
|
8729
|
+
const exited = new Promise((resolve5) => {
|
|
8730
|
+
entry.exitWaiters.push(resolve5);
|
|
8184
8731
|
});
|
|
8185
8732
|
try {
|
|
8186
8733
|
child.kill("SIGTERM");
|
|
@@ -8240,7 +8787,7 @@ var ExtensionManager = class {
|
|
|
8240
8787
|
async reapOrphans() {
|
|
8241
8788
|
let entries;
|
|
8242
8789
|
try {
|
|
8243
|
-
entries = await
|
|
8790
|
+
entries = await fsp2.readdir(paths.extensionsDir());
|
|
8244
8791
|
} catch (err) {
|
|
8245
8792
|
const e = err;
|
|
8246
8793
|
if (e.code === "ENOENT") {
|
|
@@ -8252,10 +8799,10 @@ var ExtensionManager = class {
|
|
|
8252
8799
|
if (!entry.endsWith(".pid")) {
|
|
8253
8800
|
continue;
|
|
8254
8801
|
}
|
|
8255
|
-
const pidPath =
|
|
8802
|
+
const pidPath = path5.join(paths.extensionsDir(), entry);
|
|
8256
8803
|
let pid;
|
|
8257
8804
|
try {
|
|
8258
|
-
const raw = await
|
|
8805
|
+
const raw = await fsp2.readFile(pidPath, "utf8");
|
|
8259
8806
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
8260
8807
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
8261
8808
|
pid = parsed;
|
|
@@ -8278,7 +8825,7 @@ var ExtensionManager = class {
|
|
|
8278
8825
|
}
|
|
8279
8826
|
}
|
|
8280
8827
|
}
|
|
8281
|
-
await
|
|
8828
|
+
await fsp2.unlink(pidPath).catch(() => void 0);
|
|
8282
8829
|
}
|
|
8283
8830
|
}
|
|
8284
8831
|
spawn(entry, attempt) {
|
|
@@ -8291,7 +8838,7 @@ var ExtensionManager = class {
|
|
|
8291
8838
|
}
|
|
8292
8839
|
const ext = entry.config;
|
|
8293
8840
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
8294
|
-
const logStream =
|
|
8841
|
+
const logStream = fs9.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
8295
8842
|
flags: "a"
|
|
8296
8843
|
});
|
|
8297
8844
|
logStream.write(
|
|
@@ -8319,7 +8866,7 @@ var ExtensionManager = class {
|
|
|
8319
8866
|
const args = [...baseArgs, ...ext.args];
|
|
8320
8867
|
let child;
|
|
8321
8868
|
try {
|
|
8322
|
-
child =
|
|
8869
|
+
child = spawn3(cmd, args, {
|
|
8323
8870
|
env,
|
|
8324
8871
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8325
8872
|
detached: false
|
|
@@ -8341,7 +8888,7 @@ var ExtensionManager = class {
|
|
|
8341
8888
|
}
|
|
8342
8889
|
if (typeof child.pid === "number") {
|
|
8343
8890
|
try {
|
|
8344
|
-
|
|
8891
|
+
fs9.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
8345
8892
|
`, {
|
|
8346
8893
|
encoding: "utf8",
|
|
8347
8894
|
mode: 384
|
|
@@ -8366,7 +8913,7 @@ var ExtensionManager = class {
|
|
|
8366
8913
|
});
|
|
8367
8914
|
child.on("exit", (code, signal) => {
|
|
8368
8915
|
try {
|
|
8369
|
-
|
|
8916
|
+
fs9.unlinkSync(paths.extensionPidFile(ext.name));
|
|
8370
8917
|
} catch {
|
|
8371
8918
|
}
|
|
8372
8919
|
logStream.write(
|
|
@@ -8377,8 +8924,8 @@ var ExtensionManager = class {
|
|
|
8377
8924
|
entry.pid = void 0;
|
|
8378
8925
|
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
8379
8926
|
const waiters = entry.exitWaiters.splice(0);
|
|
8380
|
-
for (const
|
|
8381
|
-
|
|
8927
|
+
for (const resolve5 of waiters) {
|
|
8928
|
+
resolve5();
|
|
8382
8929
|
}
|
|
8383
8930
|
if (this.stopping || entry.manuallyStopped) {
|
|
8384
8931
|
try {
|
|
@@ -8500,6 +9047,7 @@ var BundleSession = z5.object({
|
|
|
8500
9047
|
title: z5.string().optional(),
|
|
8501
9048
|
currentModel: z5.string().optional(),
|
|
8502
9049
|
currentMode: z5.string().optional(),
|
|
9050
|
+
currentUsage: PersistedUsage.optional(),
|
|
8503
9051
|
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
8504
9052
|
createdAt: z5.string(),
|
|
8505
9053
|
updatedAt: z5.string()
|
|
@@ -8531,6 +9079,7 @@ function encodeBundle(params) {
|
|
|
8531
9079
|
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
8532
9080
|
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
8533
9081
|
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
9082
|
+
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
8534
9083
|
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
8535
9084
|
createdAt: params.record.createdAt,
|
|
8536
9085
|
updatedAt: params.record.updatedAt
|
|
@@ -8930,7 +9479,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8930
9479
|
agentId: params.agentId ?? deps.defaultAgent,
|
|
8931
9480
|
mcpServers: params.mcpServers,
|
|
8932
9481
|
title: hydraMeta.name,
|
|
8933
|
-
agentArgs: hydraMeta.agentArgs
|
|
9482
|
+
agentArgs: hydraMeta.agentArgs,
|
|
9483
|
+
model: hydraMeta.model
|
|
8934
9484
|
});
|
|
8935
9485
|
const client = bindClientToSession(connection, session, state);
|
|
8936
9486
|
await session.attach(client, "full");
|
|
@@ -9194,10 +9744,10 @@ var HYDRA_VERSION3 = "0.1.0";
|
|
|
9194
9744
|
async function startDaemon(config) {
|
|
9195
9745
|
ensureLoopbackOrTls(config);
|
|
9196
9746
|
const httpsOptions = config.daemon.tls ? {
|
|
9197
|
-
key: await
|
|
9198
|
-
cert: await
|
|
9747
|
+
key: await fsp3.readFile(config.daemon.tls.key),
|
|
9748
|
+
cert: await fsp3.readFile(config.daemon.tls.cert)
|
|
9199
9749
|
} : void 0;
|
|
9200
|
-
await
|
|
9750
|
+
await fsp3.mkdir(paths.home(), { recursive: true });
|
|
9201
9751
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
9202
9752
|
config.daemon.logLevel
|
|
9203
9753
|
);
|
|
@@ -9209,6 +9759,9 @@ async function startDaemon(config) {
|
|
|
9209
9759
|
https: httpsOptions ?? null
|
|
9210
9760
|
});
|
|
9211
9761
|
await app.register(websocketPlugin);
|
|
9762
|
+
setBinaryInstallLogger((msg) => {
|
|
9763
|
+
app.log.info(msg);
|
|
9764
|
+
});
|
|
9212
9765
|
const auth = bearerAuth({ config });
|
|
9213
9766
|
app.addHook("onRequest", async (request, reply) => {
|
|
9214
9767
|
if (request.routeOptions.config?.skipAuth) {
|
|
@@ -9221,7 +9774,8 @@ async function startDaemon(config) {
|
|
|
9221
9774
|
});
|
|
9222
9775
|
const registry = new Registry(config);
|
|
9223
9776
|
const manager = new SessionManager(registry, void 0, void 0, {
|
|
9224
|
-
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3
|
|
9777
|
+
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
9778
|
+
defaultModels: config.defaultModels
|
|
9225
9779
|
});
|
|
9226
9780
|
const extensions = new ExtensionManager(extensionList(config));
|
|
9227
9781
|
registerHealthRoutes(app, HYDRA_VERSION3);
|
|
@@ -9243,8 +9797,8 @@ async function startDaemon(config) {
|
|
|
9243
9797
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
9244
9798
|
const address = app.server.address();
|
|
9245
9799
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
9246
|
-
await
|
|
9247
|
-
await
|
|
9800
|
+
await fsp3.mkdir(paths.home(), { recursive: true });
|
|
9801
|
+
await fsp3.writeFile(
|
|
9248
9802
|
paths.pidFile(),
|
|
9249
9803
|
JSON.stringify({
|
|
9250
9804
|
pid: process.pid,
|
|
@@ -9269,9 +9823,10 @@ async function startDaemon(config) {
|
|
|
9269
9823
|
await extensions.stop();
|
|
9270
9824
|
await manager.closeAll();
|
|
9271
9825
|
await manager.flushMetaWrites();
|
|
9826
|
+
setBinaryInstallLogger(null);
|
|
9272
9827
|
await app.close();
|
|
9273
9828
|
try {
|
|
9274
|
-
|
|
9829
|
+
fs10.unlinkSync(paths.pidFile());
|
|
9275
9830
|
} catch {
|
|
9276
9831
|
}
|
|
9277
9832
|
try {
|
|
@@ -9310,13 +9865,13 @@ function ensureLoopbackOrTls(config) {
|
|
|
9310
9865
|
init_daemon_bootstrap();
|
|
9311
9866
|
|
|
9312
9867
|
// src/cli/commands/log-tail.ts
|
|
9313
|
-
import * as
|
|
9314
|
-
import * as
|
|
9868
|
+
import * as fs11 from "fs";
|
|
9869
|
+
import * as fsp4 from "fs/promises";
|
|
9315
9870
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
9316
9871
|
const opts = parseLogTailFlags(argv);
|
|
9317
9872
|
let stat3;
|
|
9318
9873
|
try {
|
|
9319
|
-
stat3 = await
|
|
9874
|
+
stat3 = await fsp4.stat(logPath);
|
|
9320
9875
|
} catch (err) {
|
|
9321
9876
|
const e = err;
|
|
9322
9877
|
if (e.code === "ENOENT") {
|
|
@@ -9334,7 +9889,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
9334
9889
|
process.stdout.write(`-- following ${logPath} --
|
|
9335
9890
|
`);
|
|
9336
9891
|
let pending = false;
|
|
9337
|
-
const watcher =
|
|
9892
|
+
const watcher = fs11.watch(logPath, () => {
|
|
9338
9893
|
if (pending) {
|
|
9339
9894
|
return;
|
|
9340
9895
|
}
|
|
@@ -9342,14 +9897,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
9342
9897
|
setImmediate(async () => {
|
|
9343
9898
|
pending = false;
|
|
9344
9899
|
try {
|
|
9345
|
-
const s = await
|
|
9900
|
+
const s = await fsp4.stat(logPath);
|
|
9346
9901
|
if (s.size <= position) {
|
|
9347
9902
|
if (s.size < position) {
|
|
9348
9903
|
position = s.size;
|
|
9349
9904
|
}
|
|
9350
9905
|
return;
|
|
9351
9906
|
}
|
|
9352
|
-
const fd = await
|
|
9907
|
+
const fd = await fsp4.open(logPath, "r");
|
|
9353
9908
|
try {
|
|
9354
9909
|
const buf = Buffer.alloc(s.size - position);
|
|
9355
9910
|
await fd.read(buf, 0, buf.length, position);
|
|
@@ -9362,10 +9917,10 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
9362
9917
|
}
|
|
9363
9918
|
});
|
|
9364
9919
|
});
|
|
9365
|
-
await new Promise((
|
|
9920
|
+
await new Promise((resolve5) => {
|
|
9366
9921
|
const finish = () => {
|
|
9367
9922
|
watcher.close();
|
|
9368
|
-
|
|
9923
|
+
resolve5();
|
|
9369
9924
|
};
|
|
9370
9925
|
process.once("SIGINT", finish);
|
|
9371
9926
|
process.once("SIGTERM", finish);
|
|
@@ -9376,7 +9931,7 @@ async function printTail(logPath, fileSize, lines) {
|
|
|
9376
9931
|
return fileSize;
|
|
9377
9932
|
}
|
|
9378
9933
|
const CHUNK = 64 * 1024;
|
|
9379
|
-
const fd = await
|
|
9934
|
+
const fd = await fsp4.open(logPath, "r");
|
|
9380
9935
|
try {
|
|
9381
9936
|
let position = fileSize;
|
|
9382
9937
|
let collected = "";
|
|
@@ -9433,20 +9988,37 @@ function parseLogTailFlags(argv) {
|
|
|
9433
9988
|
}
|
|
9434
9989
|
|
|
9435
9990
|
// src/cli/commands/daemon.ts
|
|
9436
|
-
async function runDaemonStart() {
|
|
9991
|
+
async function runDaemonStart(flags = {}) {
|
|
9437
9992
|
const config = await ensureConfig();
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9993
|
+
if (await pingHealth(config)) {
|
|
9994
|
+
const info2 = await readPidFile();
|
|
9995
|
+
process.stdout.write(
|
|
9996
|
+
`Daemon already running${info2 ? ` (pid ${info2.pid})` : ""}. Run \`hydra-acp daemon restart\` to restart it.
|
|
9997
|
+
`
|
|
9998
|
+
);
|
|
9999
|
+
return;
|
|
10000
|
+
}
|
|
10001
|
+
if (flagBool(flags, "foreground")) {
|
|
10002
|
+
const handle = await startDaemon(config);
|
|
10003
|
+
process.stdout.write(
|
|
10004
|
+
`hydra-acp daemon listening on ${config.daemon.host}:${config.daemon.port}
|
|
9441
10005
|
`
|
|
10006
|
+
);
|
|
10007
|
+
const shutdown = async () => {
|
|
10008
|
+
process.stdout.write("Shutting down...\n");
|
|
10009
|
+
await handle.shutdown();
|
|
10010
|
+
process.exit(0);
|
|
10011
|
+
};
|
|
10012
|
+
process.on("SIGINT", () => void shutdown());
|
|
10013
|
+
process.on("SIGTERM", () => void shutdown());
|
|
10014
|
+
return;
|
|
10015
|
+
}
|
|
10016
|
+
spawnDaemonDetached();
|
|
10017
|
+
await waitForDaemonReady(config);
|
|
10018
|
+
const info = await readPidFile();
|
|
10019
|
+
process.stdout.write(
|
|
10020
|
+
`Daemon started on ${config.daemon.host}:${config.daemon.port}` + (info ? ` pid=${info.pid}` : "") + "\n"
|
|
9442
10021
|
);
|
|
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
10022
|
}
|
|
9451
10023
|
async function runDaemonStop() {
|
|
9452
10024
|
const info = await readPidFile();
|
|
@@ -9528,7 +10100,7 @@ async function runDaemonStatus() {
|
|
|
9528
10100
|
}
|
|
9529
10101
|
async function readPidFile() {
|
|
9530
10102
|
try {
|
|
9531
|
-
const raw = await
|
|
10103
|
+
const raw = await fsp5.readFile(paths.pidFile(), "utf8");
|
|
9532
10104
|
return JSON.parse(raw);
|
|
9533
10105
|
} catch (err) {
|
|
9534
10106
|
const e = err;
|
|
@@ -9553,7 +10125,7 @@ init_sessions();
|
|
|
9553
10125
|
// src/cli/commands/extensions.ts
|
|
9554
10126
|
init_config();
|
|
9555
10127
|
init_paths();
|
|
9556
|
-
import * as
|
|
10128
|
+
import * as fsp6 from "fs/promises";
|
|
9557
10129
|
init_sessions();
|
|
9558
10130
|
async function runExtensionsList() {
|
|
9559
10131
|
const config = await loadConfig();
|
|
@@ -9694,11 +10266,7 @@ async function runExtensionsAdd(name, argv) {
|
|
|
9694
10266
|
`Daemon refused to register ${name} (HTTP ${r.status}${detail}). Restart the daemon to apply.
|
|
9695
10267
|
`
|
|
9696
10268
|
);
|
|
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
|
-
);
|
|
10269
|
+
} catch {
|
|
9702
10270
|
}
|
|
9703
10271
|
}
|
|
9704
10272
|
async function runExtensionsRemove(name) {
|
|
@@ -9753,11 +10321,11 @@ async function runExtensionsRemove(name) {
|
|
|
9753
10321
|
}
|
|
9754
10322
|
}
|
|
9755
10323
|
async function readRawConfig() {
|
|
9756
|
-
const raw = await
|
|
10324
|
+
const raw = await fsp6.readFile(paths.config(), "utf8");
|
|
9757
10325
|
return JSON.parse(raw);
|
|
9758
10326
|
}
|
|
9759
10327
|
async function writeRawConfig(raw) {
|
|
9760
|
-
await
|
|
10328
|
+
await fsp6.writeFile(
|
|
9761
10329
|
paths.config(),
|
|
9762
10330
|
JSON.stringify(raw, null, 2) + "\n",
|
|
9763
10331
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -10287,6 +10855,9 @@ function wireShim({
|
|
|
10287
10855
|
outgoing = injectHydraMeta(outgoing, { name: namingState.name });
|
|
10288
10856
|
namingState.used = true;
|
|
10289
10857
|
}
|
|
10858
|
+
if (opts.model) {
|
|
10859
|
+
outgoing = injectHydraMeta(outgoing, { model: opts.model });
|
|
10860
|
+
}
|
|
10290
10861
|
void upstream.send(outgoing);
|
|
10291
10862
|
return;
|
|
10292
10863
|
}
|
|
@@ -10429,10 +11000,10 @@ async function main() {
|
|
|
10429
11000
|
const positionalAgentId = afterLaunch[0];
|
|
10430
11001
|
const agentArgs = afterLaunch.slice(1);
|
|
10431
11002
|
const { flags: flags2 } = parseArgs(beforeLaunch);
|
|
10432
|
-
const agentId = positionalAgentId ?? resolveOption(flags2, "agent
|
|
11003
|
+
const agentId = positionalAgentId ?? resolveOption(flags2, "agent");
|
|
10433
11004
|
if (!agentId) {
|
|
10434
11005
|
process.stderr.write(
|
|
10435
|
-
"Usage: hydra-acp launch <agent
|
|
11006
|
+
"Usage: hydra-acp launch <agent> [agent-args...]\n"
|
|
10436
11007
|
);
|
|
10437
11008
|
process.exit(2);
|
|
10438
11009
|
return;
|
|
@@ -10440,7 +11011,8 @@ async function main() {
|
|
|
10440
11011
|
const launchResume = flags2.resume;
|
|
10441
11012
|
const sessionId2 = typeof launchResume === "string" ? launchResume : resolveOption(flags2, "session-id");
|
|
10442
11013
|
const name2 = resolveOption(flags2, "name");
|
|
10443
|
-
|
|
11014
|
+
const model2 = resolveOption(flags2, "model");
|
|
11015
|
+
await runShim({ sessionId: sessionId2, agentId, agentArgs, name: name2, model: model2 });
|
|
10444
11016
|
return;
|
|
10445
11017
|
}
|
|
10446
11018
|
const { positional, flags } = parseArgs(argv);
|
|
@@ -10457,22 +11029,24 @@ async function main() {
|
|
|
10457
11029
|
const resumeFlag = flags.resume;
|
|
10458
11030
|
const sessionId = typeof resumeFlag === "string" ? resumeFlag : resolveOption(flags, "session-id");
|
|
10459
11031
|
const name = resolveOption(flags, "name");
|
|
10460
|
-
const agentIdFromFlag = resolveOption(flags, "agent
|
|
11032
|
+
const agentIdFromFlag = resolveOption(flags, "agent");
|
|
11033
|
+
const model = resolveOption(flags, "model");
|
|
10461
11034
|
if (!subcommand) {
|
|
10462
11035
|
if (process.stdout.isTTY) {
|
|
10463
11036
|
await dispatchTui(flags, {
|
|
10464
11037
|
sessionId,
|
|
10465
11038
|
agentId: agentIdFromFlag,
|
|
10466
|
-
name
|
|
11039
|
+
name,
|
|
11040
|
+
model
|
|
10467
11041
|
});
|
|
10468
11042
|
return;
|
|
10469
11043
|
}
|
|
10470
|
-
await runShim({ sessionId, name, agentId: agentIdFromFlag });
|
|
11044
|
+
await runShim({ sessionId, name, agentId: agentIdFromFlag, model });
|
|
10471
11045
|
return;
|
|
10472
11046
|
}
|
|
10473
11047
|
switch (subcommand) {
|
|
10474
11048
|
case "shim":
|
|
10475
|
-
await runShim({ sessionId, name, agentId: agentIdFromFlag });
|
|
11049
|
+
await runShim({ sessionId, name, agentId: agentIdFromFlag, model });
|
|
10476
11050
|
return;
|
|
10477
11051
|
case "init":
|
|
10478
11052
|
await runInit(flags);
|
|
@@ -10482,7 +11056,7 @@ async function main() {
|
|
|
10482
11056
|
const tail = argv.slice(daemonIdx + 1);
|
|
10483
11057
|
const sub = tail[0];
|
|
10484
11058
|
if (sub === "start" || sub === void 0) {
|
|
10485
|
-
await runDaemonStart();
|
|
11059
|
+
await runDaemonStart(flags);
|
|
10486
11060
|
return;
|
|
10487
11061
|
}
|
|
10488
11062
|
if (sub === "stop") {
|
|
@@ -10594,7 +11168,8 @@ async function main() {
|
|
|
10594
11168
|
await dispatchTui(flags, {
|
|
10595
11169
|
sessionId,
|
|
10596
11170
|
agentId: agentIdFromFlag,
|
|
10597
|
-
name
|
|
11171
|
+
name,
|
|
11172
|
+
model
|
|
10598
11173
|
});
|
|
10599
11174
|
return;
|
|
10600
11175
|
default:
|
|
@@ -10622,13 +11197,16 @@ async function dispatchTui(flags, base) {
|
|
|
10622
11197
|
if (base.name !== void 0) {
|
|
10623
11198
|
tuiOpts.name = base.name;
|
|
10624
11199
|
}
|
|
11200
|
+
if (base.model !== void 0) {
|
|
11201
|
+
tuiOpts.model = base.model;
|
|
11202
|
+
}
|
|
10625
11203
|
await runTui(tuiOpts);
|
|
10626
11204
|
}
|
|
10627
11205
|
function readVersion() {
|
|
10628
11206
|
try {
|
|
10629
|
-
const here =
|
|
11207
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
10630
11208
|
const pkg = JSON.parse(
|
|
10631
|
-
readFileSync(
|
|
11209
|
+
readFileSync(resolve4(here, "../package.json"), "utf8")
|
|
10632
11210
|
);
|
|
10633
11211
|
return pkg.version ?? "unknown";
|
|
10634
11212
|
} catch {
|
|
@@ -10644,13 +11222,14 @@ function printHelp() {
|
|
|
10644
11222
|
" hydra-acp Auto: TUI when stdout is a TTY, shim otherwise (the editor-spawned case)",
|
|
10645
11223
|
" hydra-acp shim Run as ACP shim explicitly (forces shim mode regardless of TTY)",
|
|
10646
11224
|
" hydra-acp tui [opts] Run the terminal UI explicitly (see below for opts)",
|
|
10647
|
-
" hydra-acp launch <agent
|
|
10648
|
-
" Shim mode, force daemon to spawn <agent
|
|
10649
|
-
" from the registry. Args after <agent
|
|
11225
|
+
" hydra-acp launch <agent> [agent-args...]",
|
|
11226
|
+
" Shim mode, force daemon to spawn <agent>",
|
|
11227
|
+
" from the registry. Args after <agent>",
|
|
10650
11228
|
" are forwarded to the agent's command.",
|
|
10651
11229
|
" hydra-acp --resume <id> Attach to an existing session (TUI when in a terminal, shim otherwise)",
|
|
10652
11230
|
" hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
|
|
10653
|
-
" hydra-acp daemon start
|
|
11231
|
+
" hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
|
|
11232
|
+
" hydra-acp daemon stop|restart|status",
|
|
10654
11233
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
10655
11234
|
" hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
|
|
10656
11235
|
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
@@ -10666,14 +11245,15 @@ function printHelp() {
|
|
|
10666
11245
|
" hydra-acp extensions logs <name> [-f] [-n N]Tail or follow an extension's log",
|
|
10667
11246
|
" hydra-acp agents [list] List agents in the cached registry",
|
|
10668
11247
|
" hydra-acp agents refresh Force a registry re-fetch",
|
|
10669
|
-
" hydra-acp tui flags: [--resume [<id>]] [--new] [--agent
|
|
11248
|
+
" hydra-acp tui flags: [--resume [<id>]] [--new] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
|
|
10670
11249
|
" --resume <id> attaches to a specific session; bare --resume picks the most-recent",
|
|
10671
11250
|
" in cwd. Smart default (no flags): picks if any live sessions exist, else new.",
|
|
10672
11251
|
" hydra-acp --version Print version",
|
|
10673
11252
|
" hydra-acp --help Show this help",
|
|
10674
11253
|
"",
|
|
10675
11254
|
"Config knob flags accept env-var equivalents (flag wins):",
|
|
10676
|
-
" --agent
|
|
11255
|
+
" --agent HYDRA_ACP_AGENT",
|
|
11256
|
+
" --model HYDRA_ACP_MODEL (one-shot at session/new; ignored on --resume)",
|
|
10677
11257
|
" --resume / --session-id HYDRA_ACP_SESSION_ID",
|
|
10678
11258
|
" --name HYDRA_ACP_NAME",
|
|
10679
11259
|
""
|