@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/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
- agentDir: (id) => path.join(hydraHome(), "agents", id),
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 path6 = paths.config();
113
- const text = await fs.readFile(path6, "utf8");
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(path6, JSON.stringify(raw, null, 2) + "\n", {
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((resolve4, reject) => {
438
+ const response = new Promise((resolve5, reject) => {
412
439
  this.pending.set(id, {
413
- resolve: (result) => resolve4(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: "switch",
538
- name: "/hydra switch",
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 switch can
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 switch so
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 "switch":
1201
- return this.runSwitchCommand(arg);
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 switch's transcript-injection
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
- runSwitchCommand(newAgentId) {
1339
+ runAgentCommand(newAgentId) {
1260
1340
  if (!newAgentId) {
1261
1341
  throw withCode(
1262
- new Error("/hydra switch requires an agent id"),
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 switch path so the agent isn't asked to respond to a user
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 switch's transcript builder filters them out.
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((resolve4, reject) => {
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
- resolve4(result);
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((resolve4, reject) => {
1615
- const run = async () => {
1694
+ return new Promise((resolve5, reject) => {
1695
+ const run2 = async () => {
1616
1696
  try {
1617
1697
  const result = await task();
1618
- resolve4(result);
1698
+ resolve5(result);
1619
1699
  } catch (err) {
1620
1700
  reject(err);
1621
1701
  }
1622
1702
  };
1623
- this.promptQueue.push(run);
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 fs6 } from "fs";
1655
- import * as path3 from "path";
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 fs6.readFile(file, "utf8");
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 fs6.mkdir(path3.dirname(file), { recursive: true });
1780
+ await fs7.mkdir(path4.dirname(file), { recursive: true });
1700
1781
  const lines = history.map((entry) => JSON.stringify(entry));
1701
- await fs6.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
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((resolve4, reject) => {
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
- resolve4();
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 spawn3 } from "child_process";
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 = spawn3(process.execPath, [cliPath, "daemon", "start"], {
1817
- detached: true,
1818
- stdio: "ignore",
1819
- env: process.env
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
- status: (s.status ?? "live").toUpperCase(),
1847
- clients: s.status === "cold" ? "-" : String(s.attachedClients),
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
- status: maxLen(HEADER.status, rows.map((r) => r.status)),
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.status.padEnd(w.status),
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
- status: "STATUS",
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 fs11 from "fs/promises";
1984
- import * as path5 from "path";
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 fs11.mkdir(path5.dirname(path5.resolve(resolved)), { recursive: true });
2104
- await fs11.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
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 fs11.readFile(file, "utf8");
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((resolve4, reject) => {
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
- resolve4(wsToMessageStream(ws));
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((resolve4, reject) => {
2273
- this.pendingRequests.set(id, { resolve: resolve4, reject });
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((resolve4) => {
2302
- this.releaseConnectGate = resolve4;
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((resolve4) => {
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
- resolve4({ kind: "new" });
2983
+ resolve5({ kind: "new" });
2843
2984
  return;
2844
2985
  }
2845
2986
  const session = visible[selectedIdx - 1];
2846
2987
  if (!session) {
2847
- resolve4({ kind: "abort" });
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
- resolve4(result);
2998
+ resolve5(result);
2858
2999
  return;
2859
3000
  }
2860
3001
  case "ESCAPE":
2861
3002
  case "CTRL_C":
2862
3003
  cleanup();
2863
- resolve4({ kind: "abort" });
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 sig = `hdr|${w}|${this.header.agent}|${this.header.cwd}|${sid}|${title ?? ""}|${usage ?? ""}`;
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 + this.header.agent.length + " \xB7 ".length + " \xB7 ".length + sid.length + (title ? " \xB7 ".length : 0) + (usage ? usage.length + 3 : 0);
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 cwdMin = Math.min(this.header.cwd.length, 12);
3948
- const titleCap = Math.max(8, variableRoom - cwdMin);
3949
- titleRoom = Math.min(title.length, titleCap);
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(this.header.agent)(" \xB7 ").dim.noFormat(truncate(this.header.cwd, cwdRoom))(" \xB7 ").yellow(sid);
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 title = readString(u, "title");
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 name = c.name.startsWith("/") ? c.name : `/${c.name}`;
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 title = readString(u, "title") ?? readString(u, "name") ?? readString(u, "label") ?? "tool call";
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 title = readString(u, "title");
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
- { prefix: "\u25A3 ", prefixStyle: "plan", body: "Plan", bodyStyle: "plan" }
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 resolve4 = pendingPermission.resolve;
5629
+ const resolve5 = pendingPermission.resolve;
5460
5630
  pendingPermission = null;
5461
5631
  screen.setPermissionPrompt(null);
5462
- resolve4(result ?? { outcome: { outcome: "cancelled" } });
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: resolve4 } = pendingPermission;
5665
+ const { options, resolve: resolve5 } = pendingPermission;
5496
5666
  pendingPermission = null;
5497
5667
  screen.setPermissionPrompt(null);
5498
5668
  if (optionId === null) {
5499
- resolve4({ outcome: { outcome: "cancelled" } });
5669
+ resolve5({ outcome: { outcome: "cancelled" } });
5500
5670
  return;
5501
5671
  }
5502
- resolve4({ outcome: { outcome: "selected", optionId } });
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 options = Array.isArray(p.options) ? p.options : [];
5508
- const title = p.toolCall?.title ?? p.toolCall?.name ?? "tool";
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((resolve4) => {
5696
+ return new Promise((resolve5) => {
5521
5697
  pendingPermission = {
5522
5698
  title,
5523
5699
  options,
5524
5700
  selectedIndex: 0,
5525
- resolve: resolve4,
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
- ...opts.name ? { _meta: { [HYDRA_META_KEY]: { name: opts.name } } } : {}
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((resolve4) => {
5788
- finishSession = resolve4;
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 resolve4 = pendingPermission.resolve;
6594
+ const resolve5 = pendingPermission.resolve;
6411
6595
  pendingPermission = null;
6412
6596
  screen.setPermissionPrompt(null);
6413
- resolve4({ outcome: { outcome: "cancelled" } });
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 dirname3, resolve as resolve3 } from "path";
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 fsp4 from "fs/promises";
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 fs9 from "fs";
6736
- import * as fsp2 from "fs/promises";
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 fs3 from "fs/promises";
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 json = await response.json();
6843
- const data = RegistryDocument.parse(json);
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
- const raw = await fs3.readFile(paths.registryCache(), "utf8");
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 !== "ENOENT") {
6856
- throw err;
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 fs3.mkdir(paths.home(), { recursive: true });
6863
- await fs3.writeFile(
6864
- paths.registryCache(),
6865
- JSON.stringify(cache, null, 2) + "\n",
6866
- "utf8"
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
- throw new Error(
6892
- `Agent ${agent.id} uses binary distribution; not yet supported in hydra-acp. PRs welcome.`
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 fs7 from "fs/promises";
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((resolve4, reject) => {
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
- resolve4();
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 = spawn(opts.plan.command, opts.plan.args, {
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 fs4 from "fs/promises";
7054
- import * as path2 from "path";
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 fs4.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
7544
+ await fs5.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
7108
7545
  const full = { version: 1, ...record };
7109
- await fs4.writeFile(
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 fs4.readFile(paths.sessionFile(sessionId), "utf8");
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 fs4.unlink(paths.sessionFile(sessionId));
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 fs4.rmdir(paths.sessionDir(sessionId));
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 fs4.readdir(paths.sessionsDir());
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 fs5 from "fs/promises";
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 fs5.mkdir(paths.sessionDir(sessionId), { recursive: true });
7668
+ await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
7231
7669
  const line = JSON.stringify(entry) + "\n";
7232
- await fs5.appendFile(paths.historyFile(sessionId), line, {
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 fs5.mkdir(paths.sessionDir(sessionId), { recursive: true });
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 fs5.writeFile(paths.historyFile(sessionId), body, {
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 fs5.readFile(paths.historyFile(sessionId), "utf8");
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 fs5.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
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 fs5.readFile(paths.historyFile(sessionId), "utf8");
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 fs5.unlink(paths.historyFile(sessionId));
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 fs5.rmdir(paths.sessionDir(sessionId));
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("session/load", {
7469
- sessionId: params.upstreamSessionId,
7470
- cwd: params.cwd,
7471
- mcpServers: []
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
- currentModel: params.currentModel,
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
- currentModel: params.currentModel,
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 switch path so both
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("session/new", {
7565
- cwd: params.cwd,
7566
- mcpServers: params.mcpServers ?? []
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: newResult.sessionId,
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 switch's persistAgentChange (which
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 switch. The on-disk record's
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 fs7.readFile(paths.tuiHistoryFile(sessionId), "utf8");
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 fs7.stat(paths.historyFile(sessionId));
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 spawn2 } from "child_process";
8011
- import * as fs8 from "fs";
8012
- import * as fsp from "fs/promises";
8013
- import * as path4 from "path";
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 fsp.mkdir(paths.extensionsDir(), { recursive: true });
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((resolve4) => {
8607
+ new Promise((resolve5) => {
8061
8608
  if (child.exitCode !== null || child.signalCode !== null) {
8062
- resolve4();
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
- resolve4();
8617
+ resolve5();
8071
8618
  }, STOP_GRACE_MS);
8072
8619
  child.on("exit", () => {
8073
8620
  clearTimeout(timer);
8074
- resolve4();
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((resolve4) => {
8183
- entry.exitWaiters.push(resolve4);
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 fsp.readdir(paths.extensionsDir());
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 = path4.join(paths.extensionsDir(), entry);
8802
+ const pidPath = path5.join(paths.extensionsDir(), entry);
8256
8803
  let pid;
8257
8804
  try {
8258
- const raw = await fsp.readFile(pidPath, "utf8");
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 fsp.unlink(pidPath).catch(() => void 0);
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 = fs8.createWriteStream(paths.extensionLogFile(ext.name), {
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 = spawn2(cmd, args, {
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
- fs8.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
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
- fs8.unlinkSync(paths.extensionPidFile(ext.name));
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 resolve4 of waiters) {
8381
- resolve4();
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 fsp2.readFile(config.daemon.tls.key),
9198
- cert: await fsp2.readFile(config.daemon.tls.cert)
9747
+ key: await fsp3.readFile(config.daemon.tls.key),
9748
+ cert: await fsp3.readFile(config.daemon.tls.cert)
9199
9749
  } : void 0;
9200
- await fsp2.mkdir(paths.home(), { recursive: true });
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 fsp2.mkdir(paths.home(), { recursive: true });
9247
- await fsp2.writeFile(
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
- fs9.unlinkSync(paths.pidFile());
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 fs10 from "fs";
9314
- import * as fsp3 from "fs/promises";
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 fsp3.stat(logPath);
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 = fs10.watch(logPath, () => {
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 fsp3.stat(logPath);
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 fsp3.open(logPath, "r");
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((resolve4) => {
9920
+ await new Promise((resolve5) => {
9366
9921
  const finish = () => {
9367
9922
  watcher.close();
9368
- resolve4();
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 fsp3.open(logPath, "r");
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
- const handle = await startDaemon(config);
9439
- process.stdout.write(
9440
- `hydra-acp daemon listening on ${config.daemon.host}:${config.daemon.port}
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 fsp4.readFile(paths.pidFile(), "utf8");
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 fsp5 from "fs/promises";
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 (err) {
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 fsp5.readFile(paths.config(), "utf8");
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 fsp5.writeFile(
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-id");
11003
+ const agentId = positionalAgentId ?? resolveOption(flags2, "agent");
10433
11004
  if (!agentId) {
10434
11005
  process.stderr.write(
10435
- "Usage: hydra-acp launch <agent-id> [agent-args...]\n"
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
- await runShim({ sessionId: sessionId2, agentId, agentArgs, name: name2 });
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-id");
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 = dirname3(fileURLToPath(import.meta.url));
11207
+ const here = dirname4(fileURLToPath(import.meta.url));
10630
11208
  const pkg = JSON.parse(
10631
- readFileSync(resolve3(here, "../package.json"), "utf8")
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-id> [agent-args...]",
10648
- " Shim mode, force daemon to spawn <agent-id>",
10649
- " from the registry. Args after <agent-id>",
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|stop|restart|status",
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-id <id>] [--cwd <path>] [--name <label>]",
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-id HYDRA_ACP_AGENT_ID",
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
  ""