@hydra-acp/cli 0.1.4 → 0.1.5

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