@hydra-acp/cli 0.1.15 → 0.1.17

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
@@ -360,6 +360,12 @@ function extractHydraMeta(meta) {
360
360
  if (typeof obj.currentMode === "string") {
361
361
  out.currentMode = obj.currentMode;
362
362
  }
363
+ if (obj.currentUsage) {
364
+ const parsed = SessionListUsage.safeParse(obj.currentUsage);
365
+ if (parsed.success) {
366
+ out.currentUsage = parsed.data;
367
+ }
368
+ }
363
369
  if (typeof obj.turnStartedAt === "number" && obj.turnStartedAt > 0) {
364
370
  out.turnStartedAt = obj.turnStartedAt;
365
371
  }
@@ -1204,21 +1210,126 @@ var init_session = __esm({
1204
1210
  }
1205
1211
  this.clients.set(client.clientId, client);
1206
1212
  this.updatedAt = Date.now();
1207
- if (historyPolicy === "none" || historyPolicy === "pending_only") {
1213
+ if (historyPolicy === "none") {
1208
1214
  return Promise.resolve({ entries: [], appliedPolicy: historyPolicy });
1209
1215
  }
1216
+ if (historyPolicy === "pending_only") {
1217
+ return Promise.resolve({
1218
+ entries: this.buildStateSnapshotReplay(),
1219
+ appliedPolicy: historyPolicy
1220
+ });
1221
+ }
1210
1222
  return this.loadReplay(historyPolicy, opts);
1211
1223
  }
1212
1224
  async loadReplay(historyPolicy, opts) {
1213
1225
  const all = await this.getHistorySnapshot();
1226
+ const state = this.buildStateSnapshotReplay();
1214
1227
  if (historyPolicy === "after_message") {
1215
1228
  const cutoff = opts.afterMessageId ? findMessageIdIndex(all, opts.afterMessageId) : -1;
1216
1229
  if (cutoff < 0) {
1217
- return { entries: all, appliedPolicy: "full" };
1230
+ return { entries: [...state, ...all], appliedPolicy: "full" };
1231
+ }
1232
+ return {
1233
+ entries: [...state, ...all.slice(cutoff + 1)],
1234
+ appliedPolicy: "after_message"
1235
+ };
1236
+ }
1237
+ return { entries: [...state, ...all], appliedPolicy: "full" };
1238
+ }
1239
+ // Synthesizes one session/update notification per cached STATE_UPDATE_KIND
1240
+ // so an attaching client receives the current snapshot through the
1241
+ // standard ACP event channel. Without this, third-party clients would
1242
+ // never see current_model/mode/usage/info/commands on resume — they're
1243
+ // filtered out of recorded history (canonical state lives in meta.json
1244
+ // and is *also* surfaced via the attach response _meta, but third-party
1245
+ // clients can't read hydra's namespaced meta).
1246
+ buildStateSnapshotReplay() {
1247
+ const out = [];
1248
+ const sessionId = this.sessionId;
1249
+ const recordedAt = Date.now();
1250
+ if (this.title !== void 0 && this.title.length > 0) {
1251
+ out.push({
1252
+ method: "session/update",
1253
+ params: {
1254
+ sessionId,
1255
+ update: {
1256
+ sessionUpdate: "session_info_update",
1257
+ title: this.title
1258
+ }
1259
+ },
1260
+ recordedAt
1261
+ });
1262
+ }
1263
+ if (this.currentModel !== void 0 && this.currentModel.length > 0) {
1264
+ out.push({
1265
+ method: "session/update",
1266
+ params: {
1267
+ sessionId,
1268
+ update: {
1269
+ sessionUpdate: "current_model_update",
1270
+ currentModel: this.currentModel
1271
+ }
1272
+ },
1273
+ recordedAt
1274
+ });
1275
+ }
1276
+ if (this.currentMode !== void 0 && this.currentMode.length > 0) {
1277
+ out.push({
1278
+ method: "session/update",
1279
+ params: {
1280
+ sessionId,
1281
+ update: {
1282
+ sessionUpdate: "current_mode_update",
1283
+ currentMode: this.currentMode
1284
+ }
1285
+ },
1286
+ recordedAt
1287
+ });
1288
+ }
1289
+ const cmds = this.mergedAvailableCommands();
1290
+ if (cmds.length > 0) {
1291
+ out.push({
1292
+ method: "session/update",
1293
+ params: {
1294
+ sessionId,
1295
+ update: {
1296
+ sessionUpdate: "available_commands_update",
1297
+ availableCommands: cmds
1298
+ }
1299
+ },
1300
+ recordedAt
1301
+ });
1302
+ }
1303
+ if (this.currentUsage !== void 0) {
1304
+ const u = this.currentUsage;
1305
+ const update = {
1306
+ sessionUpdate: "usage_update"
1307
+ };
1308
+ if (typeof u.used === "number") {
1309
+ update.used = u.used;
1310
+ }
1311
+ if (typeof u.size === "number") {
1312
+ update.size = u.size;
1313
+ }
1314
+ if (typeof u.costAmount === "number" || typeof u.costCurrency === "string") {
1315
+ const cost = {};
1316
+ if (typeof u.costAmount === "number") {
1317
+ cost.amount = u.costAmount;
1318
+ }
1319
+ if (typeof u.costCurrency === "string") {
1320
+ cost.currency = u.costCurrency;
1321
+ }
1322
+ update.cost = cost;
1323
+ }
1324
+ if (Object.keys(update).length > 1) {
1325
+ out.push({
1326
+ method: "session/update",
1327
+ params: { sessionId, update },
1328
+ recordedAt
1329
+ });
1218
1330
  }
1219
- return { entries: all.slice(cutoff + 1), appliedPolicy: "after_message" };
1220
1331
  }
1221
- return { entries: all, appliedPolicy: "full" };
1332
+ return out;
1222
1333
  }
1223
1334
  // Dispatch in-flight permission requests to a freshly-attached
1224
1335
  // client. Called by the daemon's WS handler *after* it finishes
@@ -2477,633 +2588,1252 @@ var init_bundle = __esm({
2477
2588
  }
2478
2589
  });
2479
2590
 
2480
- // src/acp/ws-stream.ts
2481
- function wsToMessageStream(ws) {
2482
- const messageHandlers = [];
2483
- const closeHandlers = [];
2484
- let closed = false;
2485
- const emitClose = (err) => {
2486
- if (closed) {
2487
- return;
2488
- }
2489
- closed = true;
2490
- for (const handler of closeHandlers) {
2491
- handler(err);
2492
- }
2493
- };
2494
- ws.on("message", (data, isBinary) => {
2495
- if (isBinary) {
2496
- return;
2497
- }
2498
- const text = data.toString("utf8");
2499
- try {
2500
- const parsed = JSON.parse(text);
2501
- for (const handler of messageHandlers) {
2502
- handler(parsed);
2503
- }
2504
- } catch (err) {
2505
- for (const handler of messageHandlers) {
2506
- handler({
2507
- jsonrpc: "2.0",
2508
- id: 0,
2509
- error: {
2510
- code: JsonRpcErrorCodes.ParseError,
2511
- message: `Failed to parse WS frame: ${err.message}`
2512
- }
2513
- });
2514
- }
2515
- }
2516
- });
2517
- ws.on("close", () => emitClose());
2518
- ws.on("error", (err) => emitClose(err));
2519
- return {
2520
- async send(message) {
2521
- if (closed) {
2522
- throw new Error("ws is closed");
2523
- }
2524
- const text = JSON.stringify(message);
2525
- await new Promise((resolve5, reject) => {
2526
- ws.send(text, (err) => {
2527
- if (err) {
2528
- reject(err);
2529
- return;
2530
- }
2531
- resolve5();
2532
- });
2533
- });
2534
- },
2535
- onMessage(handler) {
2536
- messageHandlers.push(handler);
2537
- },
2538
- onClose(handler) {
2539
- closeHandlers.push(handler);
2540
- },
2541
- async close() {
2542
- if (closed) {
2543
- return;
2544
- }
2545
- ws.close();
2546
- emitClose();
2547
- }
2548
- };
2591
+ // src/core/render-update.ts
2592
+ import stripAnsi from "strip-ansi";
2593
+ function sanitizeWireText(text) {
2594
+ return stripAnsi(text).replace(STRIP_CONTROLS, "");
2549
2595
  }
2550
- var init_ws_stream = __esm({
2551
- "src/acp/ws-stream.ts"() {
2552
- "use strict";
2553
- init_types();
2554
- }
2555
- });
2556
-
2557
- // src/core/daemon-bootstrap.ts
2558
- import { spawn as spawn5 } from "child_process";
2559
- import { setTimeout as sleep } from "timers/promises";
2560
- async function ensureDaemonReachable(config) {
2561
- if (await pingHealth(config)) {
2562
- return;
2563
- }
2564
- process.stderr.write("hydra-acp: daemon not running; starting it...\n");
2565
- spawnDaemonDetached();
2566
- await waitForDaemonReady(config);
2596
+ function sanitizeSingleLine(text) {
2597
+ return sanitizeWireText(text).replace(/[\n\t]+/g, " ").replace(/ +/g, " ").trim();
2567
2598
  }
2568
- async function pingHealth(config) {
2569
- const protocol = config.daemon.tls ? "https" : "http";
2570
- const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/v1/health`;
2571
- try {
2572
- const response = await fetch(url, {
2573
- signal: AbortSignal.timeout(500)
2574
- });
2575
- return response.ok;
2576
- } catch {
2577
- return false;
2599
+ function mapUpdate(update) {
2600
+ if (!update || typeof update !== "object") {
2601
+ return null;
2578
2602
  }
2579
- }
2580
- function spawnDaemonDetached() {
2581
- const cliPath = process.argv[1];
2582
- if (!cliPath) {
2583
- throw new Error("Cannot determine hydra-acp binary path to spawn daemon");
2603
+ const u = update;
2604
+ const tag = u.sessionUpdate ?? u.kind;
2605
+ if (typeof tag !== "string") {
2606
+ return null;
2584
2607
  }
2585
- const child = spawn5(
2586
- process.execPath,
2587
- [cliPath, "daemon", "start", "--foreground"],
2588
- {
2589
- detached: true,
2590
- stdio: "ignore",
2591
- env: process.env
2592
- }
2593
- );
2594
- child.unref();
2595
- }
2596
- async function waitForDaemonReady(config, timeoutMs = 15e3) {
2597
- const deadline = Date.now() + timeoutMs;
2598
- while (Date.now() < deadline) {
2599
- if (await pingHealth(config)) {
2600
- return;
2601
- }
2602
- await sleep(150);
2608
+ switch (tag) {
2609
+ case "agent_message_chunk":
2610
+ return mapAgentText(u);
2611
+ case "agent_thought_chunk":
2612
+ case "agent_thought":
2613
+ return mapAgentThought(u);
2614
+ case "user_message_chunk":
2615
+ return mapUserText(u);
2616
+ case "prompt_received":
2617
+ return mapPromptReceived(u);
2618
+ case "tool_call":
2619
+ return mapToolCall(u);
2620
+ case "tool_call_update":
2621
+ return mapToolCallUpdate(u);
2622
+ case "plan":
2623
+ return mapPlan(u);
2624
+ case "current_mode_update":
2625
+ return mapMode(u);
2626
+ case "current_model_update":
2627
+ return mapModel(u);
2628
+ case "turn_complete":
2629
+ return mapTurnComplete(u);
2630
+ case "usage_update":
2631
+ return mapUsage(u);
2632
+ case "available_commands_update":
2633
+ return mapAvailableCommands(u);
2634
+ case "session_info_update":
2635
+ return mapSessionInfo(u);
2636
+ default:
2637
+ return { kind: "unknown", sessionUpdate: tag, raw: update };
2603
2638
  }
2604
- throw new Error(
2605
- `hydra-acp daemon did not become ready within ${timeoutMs}ms`
2606
- );
2607
2639
  }
2608
- var init_daemon_bootstrap = __esm({
2609
- "src/core/daemon-bootstrap.ts"() {
2610
- "use strict";
2640
+ function mapSessionInfo(u) {
2641
+ const rawTitle = readString(u, "title");
2642
+ const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
2643
+ const meta = u._meta;
2644
+ let agentId;
2645
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
2646
+ const ns = meta["hydra-acp"];
2647
+ if (ns && typeof ns === "object" && !Array.isArray(ns)) {
2648
+ const candidate = ns.agentId;
2649
+ if (typeof candidate === "string") {
2650
+ agentId = candidate;
2651
+ }
2652
+ }
2611
2653
  }
2612
- });
2613
-
2614
- // src/core/agent-display.ts
2615
- function shortenModel(model) {
2616
- if (!model) {
2617
- return void 0;
2654
+ if (title === void 0 && agentId === void 0) {
2655
+ return null;
2618
2656
  }
2619
- const idx = model.lastIndexOf("/");
2620
- if (idx === -1) {
2621
- return model;
2657
+ const event = { kind: "session-info" };
2658
+ if (title !== void 0) {
2659
+ event.title = title;
2622
2660
  }
2623
- return model.slice(idx + 1);
2624
- }
2625
- function formatAgentWithModel(agentId, model) {
2626
- const agent = agentId ?? "?";
2627
- const short = shortenModel(model);
2628
- if (!short) {
2629
- return agent;
2661
+ if (agentId !== void 0) {
2662
+ event.agentId = agentId;
2630
2663
  }
2631
- return `${agent}${AGENT_MODEL_SEP}${short}`;
2664
+ return event;
2632
2665
  }
2633
- function formatAgentCell(agentId, usage) {
2634
- const base = agentId ?? "?";
2635
- if (!usage || typeof usage.costAmount !== "number") {
2636
- return base;
2637
- }
2638
- const compact = formatCostCompact(usage.costAmount, usage.costCurrency);
2639
- if (compact === null) {
2640
- return base;
2666
+ function normalizeAdvertisedCommands(list) {
2667
+ if (!Array.isArray(list)) {
2668
+ return [];
2641
2669
  }
2642
- return `${base} ${compact}`;
2643
- }
2644
- function formatCost(amount, currency) {
2645
- const sign = currency === "USD" || currency === void 0 ? "$" : "";
2646
- const decimals = amount >= 1 ? 2 : 4;
2647
- return `${sign}${amount.toFixed(decimals)}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
2670
+ const out = [];
2671
+ for (const raw of list) {
2672
+ if (!raw || typeof raw !== "object") {
2673
+ continue;
2674
+ }
2675
+ const c = raw;
2676
+ if (typeof c.name !== "string" || c.name.length === 0) {
2677
+ continue;
2678
+ }
2679
+ const rawName = c.name.startsWith("/") ? c.name : `/${c.name}`;
2680
+ const cmd = { name: sanitizeSingleLine(rawName) };
2681
+ if (typeof c.description === "string") {
2682
+ cmd.description = sanitizeSingleLine(c.description);
2683
+ }
2684
+ out.push(cmd);
2685
+ }
2686
+ return out;
2648
2687
  }
2649
- function formatCostCompact(amount, currency) {
2650
- const whole = Math.round(amount);
2651
- if (whole === 0) {
2688
+ function mapAvailableCommands(u) {
2689
+ const list = u.availableCommands ?? u.commands;
2690
+ if (!Array.isArray(list)) {
2652
2691
  return null;
2653
2692
  }
2654
- const sign = currency === "USD" || currency === void 0 ? "$" : "";
2655
- return `${sign}${whole}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
2693
+ return { kind: "available-commands", commands: normalizeAdvertisedCommands(list) };
2656
2694
  }
2657
- var AGENT_MODEL_SEP;
2658
- var init_agent_display = __esm({
2659
- "src/core/agent-display.ts"() {
2660
- "use strict";
2661
- AGENT_MODEL_SEP = "\u2022";
2695
+ function mapUsage(u) {
2696
+ const event = { kind: "usage-update" };
2697
+ if (typeof u.used === "number") {
2698
+ event.used = u.used;
2662
2699
  }
2663
- });
2664
-
2665
- // src/cli/session-row.ts
2666
- function toRow(s, now = Date.now()) {
2667
- return {
2668
- session: stripHydraSessionPrefix(s.sessionId),
2669
- upstream: formatUpstreamCell(s.upstreamSessionId, s.importedFromMachine),
2670
- state: formatState(s.status, s.attachedClients),
2671
- agent: formatAgentCell(s.agentId, s.currentUsage),
2672
- age: formatRelativeAge(s.updatedAt, now),
2673
- title: s.title ?? "-",
2674
- cwd: shortenHomePath(s.cwd)
2675
- };
2676
- }
2677
- function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
2678
- if (upstreamSessionId && upstreamSessionId.length > 0) {
2679
- return upstreamSessionId;
2700
+ if (typeof u.size === "number") {
2701
+ event.size = u.size;
2680
2702
  }
2681
- if (importedFromMachine && importedFromMachine.length > 0) {
2682
- return `\u2190 ${importedFromMachine}`;
2703
+ if (u.cost && typeof u.cost === "object") {
2704
+ const cost = u.cost;
2705
+ if (typeof cost.amount === "number") {
2706
+ event.costAmount = cost.amount;
2707
+ }
2708
+ if (typeof cost.currency === "string") {
2709
+ event.costCurrency = cost.currency;
2710
+ }
2683
2711
  }
2684
- return "-";
2712
+ return event;
2685
2713
  }
2686
- function formatState(status, clients) {
2687
- if (status === "cold") {
2688
- return "COLD";
2714
+ function mapAgentText(u) {
2715
+ const text = extractContentText(u.content);
2716
+ if (text === null) {
2717
+ return null;
2689
2718
  }
2690
- return `LIVE(${clients})`;
2719
+ return { kind: "agent-text", text };
2691
2720
  }
2692
- function computeWidths(rows) {
2693
- return {
2694
- session: maxLen(HEADER.session, rows.map((r) => r.session)),
2695
- upstream: maxLen(HEADER.upstream, rows.map((r) => r.upstream)),
2696
- state: maxLen(HEADER.state, rows.map((r) => r.state)),
2697
- agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
2698
- age: maxLen(HEADER.age, rows.map((r) => r.age)),
2699
- cwd: maxLen(HEADER.cwd, rows.map((r) => r.cwd)),
2700
- title: maxLen(HEADER.title, rows.map((r) => r.title))
2701
- };
2721
+ function mapAgentThought(u) {
2722
+ const text = typeof u.text === "string" ? sanitizeWireText(u.text) : extractContentText(u.content);
2723
+ if (text === null) {
2724
+ return null;
2725
+ }
2726
+ return { kind: "agent-thought", text };
2702
2727
  }
2703
- function formatRelativeAge(iso, now) {
2704
- if (!iso) {
2705
- return "?";
2728
+ function mapUserText(u) {
2729
+ const meta = u._meta;
2730
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
2731
+ const hydra = meta["hydra-acp"];
2732
+ if (hydra && typeof hydra === "object" && !Array.isArray(hydra) && hydra.compatFor === "prompt_received") {
2733
+ return null;
2734
+ }
2706
2735
  }
2707
- const t = Date.parse(iso);
2708
- if (Number.isNaN(t)) {
2709
- return "?";
2736
+ const text = extractContentText(u.content);
2737
+ if (text === null) {
2738
+ return null;
2710
2739
  }
2711
- const diff = Math.max(0, now - t);
2712
- const sec = Math.floor(diff / 1e3);
2713
- if (sec < 60) {
2714
- return "<1m";
2740
+ return { kind: "user-text", text };
2741
+ }
2742
+ function mapPromptReceived(u) {
2743
+ const promptText = extractPromptText2(u.prompt);
2744
+ if (promptText === null) {
2745
+ return null;
2715
2746
  }
2716
- const min = Math.floor(sec / 60);
2717
- if (min < 60) {
2718
- return `${min}m`;
2747
+ return { kind: "user-text", text: promptText };
2748
+ }
2749
+ function mapToolCall(u) {
2750
+ const toolCallId = readString(u, "toolCallId") ?? readString(u, "id");
2751
+ if (!toolCallId) {
2752
+ return null;
2719
2753
  }
2720
- const hr = Math.floor(min / 60);
2721
- if (hr < 24) {
2722
- return `${hr}h`;
2754
+ const rawTitle = readString(u, "title") ?? readString(u, "name") ?? readString(u, "label") ?? "tool call";
2755
+ const title = sanitizeSingleLine(rawTitle);
2756
+ const status = readString(u, "status");
2757
+ const rawKind = readString(u, "kind");
2758
+ const event = { kind: "tool-call", toolCallId, title };
2759
+ if (status !== void 0) {
2760
+ event.status = status;
2723
2761
  }
2724
- const day = Math.floor(hr / 24);
2725
- if (day < 14) {
2726
- return `${day}d`;
2762
+ if (rawKind !== void 0) {
2763
+ event.rawKind = rawKind;
2727
2764
  }
2728
- const week = Math.floor(day / 7);
2729
- if (week < 9) {
2730
- return `${week}w`;
2765
+ return event;
2766
+ }
2767
+ function mapToolCallUpdate(u) {
2768
+ const toolCallId = readString(u, "toolCallId") ?? readString(u, "id");
2769
+ if (!toolCallId) {
2770
+ return null;
2731
2771
  }
2732
- const month = Math.floor(day / 30);
2733
- if (month < 12) {
2734
- return `${month}mo`;
2772
+ const rawTitle = readString(u, "title");
2773
+ const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
2774
+ const status = readString(u, "status");
2775
+ const meaningful = title !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
2776
+ if (!meaningful) {
2777
+ return null;
2735
2778
  }
2736
- const year = Math.floor(day / 365);
2737
- return `${year}y`;
2779
+ const event = { kind: "tool-call-update", toolCallId };
2780
+ if (title !== void 0) {
2781
+ event.title = title;
2782
+ }
2783
+ if (status !== void 0) {
2784
+ event.status = status;
2785
+ }
2786
+ return event;
2738
2787
  }
2739
- function maxLen(headerCell, values) {
2740
- let max = headerCell.length;
2741
- for (const v of values) {
2742
- if (v.length > max) {
2743
- max = v.length;
2788
+ function mapPlan(u) {
2789
+ const entries = u.entries;
2790
+ if (!Array.isArray(entries)) {
2791
+ return null;
2792
+ }
2793
+ const normalized = [];
2794
+ for (const raw of entries) {
2795
+ if (!raw || typeof raw !== "object") {
2796
+ continue;
2797
+ }
2798
+ const e = raw;
2799
+ const content = typeof e.content === "string" ? sanitizeSingleLine(e.content) : void 0;
2800
+ if (!content) {
2801
+ continue;
2802
+ }
2803
+ const entry = { content };
2804
+ if (typeof e.status === "string") {
2805
+ entry.status = e.status;
2806
+ }
2807
+ if (typeof e.priority === "string") {
2808
+ entry.priority = e.priority;
2744
2809
  }
2810
+ normalized.push(entry);
2745
2811
  }
2746
- return max;
2812
+ return { kind: "plan", entries: normalized };
2747
2813
  }
2748
- function formatRow(r, w, maxWidth, cwdMaxWidth = DEFAULT_CWD_MAX_WIDTH) {
2749
- const fixed = [
2750
- r.session.padEnd(w.session),
2751
- r.upstream.padEnd(w.upstream),
2752
- r.state.padEnd(w.state),
2753
- r.agent.padEnd(w.agent),
2754
- r.age.padStart(w.age)
2755
- ].join(SEP);
2756
- if (maxWidth === void 0) {
2757
- return [fixed, r.cwd.padEnd(w.cwd), r.title].join(SEP);
2814
+ function mapMode(u) {
2815
+ const mode = readString(u, "currentMode") ?? readString(u, "mode");
2816
+ if (!mode) {
2817
+ return null;
2758
2818
  }
2759
- const budget = maxWidth - fixed.length - SEP.length;
2760
- if (budget <= 0) {
2761
- return fixed.slice(0, maxWidth);
2819
+ return { kind: "mode-changed", mode: sanitizeSingleLine(mode) };
2820
+ }
2821
+ function mapModel(u) {
2822
+ const model = readString(u, "currentModel") ?? readString(u, "model");
2823
+ if (!model) {
2824
+ return null;
2762
2825
  }
2763
- const cwdCap = Math.min(w.cwd, cwdMaxWidth);
2764
- const cwdAlloc = Math.min(cwdCap, Math.max(0, budget - SEP.length - 1));
2765
- const cwdCell = truncateMiddle(r.cwd, cwdAlloc).padEnd(cwdAlloc);
2766
- const titleBudget = Math.max(0, budget - cwdAlloc - SEP.length);
2767
- const titleCell = truncateRight(r.title, titleBudget);
2768
- return [fixed, cwdCell, titleCell].join(SEP);
2826
+ return { kind: "model-changed", model: sanitizeSingleLine(model) };
2769
2827
  }
2770
- function truncateRight(s, max) {
2771
- if (max <= 0) {
2772
- return "";
2828
+ function mapTurnComplete(u) {
2829
+ const stopReason = readString(u, "stopReason");
2830
+ return stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" };
2831
+ }
2832
+ function extractContentText(content) {
2833
+ if (typeof content === "string") {
2834
+ return sanitizeWireText(content);
2773
2835
  }
2774
- if (s.length <= max) {
2775
- return s;
2836
+ if (!content || typeof content !== "object") {
2837
+ return null;
2776
2838
  }
2777
- if (max === 1) {
2778
- return "\u2026";
2839
+ const c = content;
2840
+ if (c.type === "text" && typeof c.text === "string") {
2841
+ return sanitizeWireText(c.text);
2779
2842
  }
2780
- return s.slice(0, max - 1) + "\u2026";
2781
- }
2782
- function truncateMiddle(s, max) {
2783
- if (max <= 0) {
2784
- return "";
2843
+ if (typeof c.text === "string") {
2844
+ return sanitizeWireText(c.text);
2785
2845
  }
2786
- if (s.length <= max) {
2787
- return s;
2846
+ return null;
2847
+ }
2848
+ function extractPromptText2(prompt) {
2849
+ if (!Array.isArray(prompt)) {
2850
+ return null;
2788
2851
  }
2789
- if (max === 1) {
2790
- return "\u2026";
2852
+ const parts = [];
2853
+ for (const block of prompt) {
2854
+ const text = extractContentText(block);
2855
+ if (text !== null) {
2856
+ parts.push(text);
2857
+ }
2791
2858
  }
2792
- const head = Math.ceil((max - 1) / 2);
2793
- const tail = max - 1 - head;
2794
- return s.slice(0, head) + "\u2026" + s.slice(s.length - tail);
2859
+ if (parts.length === 0) {
2860
+ return null;
2861
+ }
2862
+ return parts.join("");
2795
2863
  }
2796
- var HEADER, SEP, DEFAULT_CWD_MAX_WIDTH;
2797
- var init_session_row = __esm({
2798
- "src/cli/session-row.ts"() {
2864
+ function readString(u, key) {
2865
+ const v = u[key];
2866
+ return typeof v === "string" ? v : void 0;
2867
+ }
2868
+ var STRIP_CONTROLS;
2869
+ var init_render_update = __esm({
2870
+ "src/core/render-update.ts"() {
2799
2871
  "use strict";
2800
- init_agent_display();
2801
- init_paths();
2802
- init_session();
2803
- HEADER = {
2804
- session: "SESSION",
2805
- upstream: "UPSTREAM",
2806
- state: "STATE",
2807
- agent: "AGENT",
2808
- age: "AGE",
2809
- title: "TITLE",
2810
- cwd: "CWD"
2811
- };
2812
- SEP = " ";
2813
- DEFAULT_CWD_MAX_WIDTH = 24;
2872
+ STRIP_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
2814
2873
  }
2815
2874
  });
2816
2875
 
2817
- // src/cli/commands/sessions.ts
2818
- import * as fs13 from "fs/promises";
2819
- import * as path8 from "path";
2820
- async function runSessionsList(opts = {}) {
2821
- const config = await loadConfig();
2822
- const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
2823
- const url = new URL(`${baseUrl}/v1/sessions`);
2824
- const response = await fetch(url.toString(), {
2825
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
2826
- });
2827
- if (!response.ok) {
2828
- process.stderr.write(`Daemon returned HTTP ${response.status}
2829
- `);
2830
- process.exit(1);
2831
- }
2832
- const body = await response.json();
2833
- if (body.sessions.length === 0) {
2834
- process.stdout.write("No active sessions.\n");
2835
- return;
2836
- }
2837
- const sorted = body.sessions.slice().sort((a, b) => {
2838
- const liveDiff = (b.status === "live" ? 1 : 0) - (a.status === "live" ? 1 : 0);
2839
- if (liveDiff !== 0) {
2840
- return liveDiff;
2841
- }
2842
- return String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""));
2843
- });
2844
- let visible = sorted;
2845
- let truncated = 0;
2846
- if (!opts.all) {
2847
- const liveCount = sorted.filter((s) => s.status !== "cold").length;
2848
- const limit = config.sessionListColdLimit;
2849
- const coldSlice = sorted.slice(liveCount, liveCount + limit);
2850
- const hiddenCold = sorted.length - liveCount - coldSlice.length;
2851
- visible = [...sorted.slice(0, liveCount), ...coldSlice];
2852
- truncated = hiddenCold;
2853
- }
2854
- const now = Date.now();
2855
- const rows = visible.map((s) => toRow(s, now));
2856
- const widths = computeWidths(rows);
2857
- const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
2858
- const cwdMax = config.tui.cwdColumnMaxWidth;
2859
- process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdMax) + "\n");
2860
- for (const r of rows) {
2861
- process.stdout.write(formatRow(r, widths, maxWidth, cwdMax) + "\n");
2862
- }
2863
- if (truncated > 0) {
2864
- process.stdout.write(
2865
- `
2866
- ... ${truncated} more cold session${truncated === 1 ? "" : "s"} hidden. Use --all to show.
2867
- `
2868
- );
2876
+ // src/core/transcript.ts
2877
+ function bundleToMarkdown(bundle) {
2878
+ const events = collectEvents(bundle);
2879
+ const toolFinalStates = collectToolFinalStates(events);
2880
+ const out = [];
2881
+ emitHeader(out, bundle);
2882
+ emitBody(out, events, toolFinalStates);
2883
+ let text = out.join("\n");
2884
+ if (!text.endsWith("\n")) {
2885
+ text += "\n";
2869
2886
  }
2887
+ return text;
2870
2888
  }
2871
- async function runSessionsKill(id) {
2872
- if (!id) {
2873
- process.stderr.write("Usage: hydra-acp sessions kill <session-id>\n");
2874
- process.exit(2);
2875
- }
2876
- const config = await loadConfig();
2877
- const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
2878
- const response = await fetch(`${baseUrl}/v1/sessions/${id}/kill`, {
2879
- method: "POST",
2880
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
2881
- });
2882
- if (!response.ok && response.status !== 204) {
2883
- process.stderr.write(`Daemon returned HTTP ${response.status}
2884
- `);
2885
- process.exit(1);
2889
+ function collectEvents(bundle) {
2890
+ const out = [];
2891
+ for (const entry of bundle.history) {
2892
+ if (entry.method !== "session/update") {
2893
+ continue;
2894
+ }
2895
+ const params = entry.params;
2896
+ if (!params || typeof params !== "object") {
2897
+ continue;
2898
+ }
2899
+ const event = mapUpdate(params.update);
2900
+ if (event === null) {
2901
+ continue;
2902
+ }
2903
+ out.push({ event, recordedAt: entry.recordedAt });
2886
2904
  }
2887
- process.stdout.write(`Killed ${id}
2888
- `);
2905
+ return out;
2889
2906
  }
2890
- async function runSessionsRemove(id) {
2891
- if (!id) {
2892
- process.stderr.write("Usage: hydra-acp sessions remove <session-id>\n");
2893
- process.exit(2);
2894
- }
2895
- const config = await loadConfig();
2896
- const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
2897
- const response = await fetch(`${baseUrl}/v1/sessions/${id}`, {
2898
- method: "DELETE",
2899
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
2900
- });
2901
- if (!response.ok && response.status !== 204) {
2902
- process.stderr.write(`Daemon returned HTTP ${response.status}
2903
- `);
2904
- process.exit(1);
2907
+ function collectToolFinalStates(events) {
2908
+ const out = /* @__PURE__ */ new Map();
2909
+ for (const { event } of events) {
2910
+ if (event.kind === "tool-call") {
2911
+ const existing = out.get(event.toolCallId);
2912
+ out.set(event.toolCallId, {
2913
+ title: event.title,
2914
+ status: event.status ?? existing?.status ?? "pending"
2915
+ });
2916
+ continue;
2917
+ }
2918
+ if (event.kind === "tool-call-update") {
2919
+ const existing = out.get(event.toolCallId) ?? {
2920
+ title: "tool call",
2921
+ status: "pending"
2922
+ };
2923
+ out.set(event.toolCallId, {
2924
+ title: event.title ?? existing.title,
2925
+ status: event.status ?? existing.status
2926
+ });
2927
+ }
2905
2928
  }
2906
- process.stdout.write(`Removed ${id}
2907
- `);
2929
+ return out;
2908
2930
  }
2909
- async function runSessionsExport(id, outPath) {
2910
- if (!id) {
2911
- process.stderr.write(
2912
- "Usage: hydra-acp sessions export <session-id> [--out <file>]\n"
2913
- );
2914
- process.exit(2);
2915
- }
2916
- const config = await loadConfig();
2917
- const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
2918
- const response = await fetch(
2919
- `${baseUrl}/v1/sessions/${encodeURIComponent(id)}/export`,
2920
- {
2921
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
2922
- }
2931
+ function emitHeader(out, bundle) {
2932
+ const session = bundle.session;
2933
+ const shortId2 = stripHydraSessionPrefix(session.sessionId);
2934
+ const title = session.title?.trim() || `Hydra session ${shortId2}`;
2935
+ out.push(`# ${escapeInline(title)}`);
2936
+ out.push("");
2937
+ const lines = [];
2938
+ lines.push(`- **Session:** \`${shortId2}\` (lineage \`${session.lineageId}\`)`);
2939
+ const agentBits = [session.agentId];
2940
+ if (session.currentModel) {
2941
+ agentBits.push(`model: ${session.currentModel}`);
2942
+ }
2943
+ if (session.currentMode) {
2944
+ agentBits.push(`mode: ${session.currentMode}`);
2945
+ }
2946
+ lines.push(`- **Agent:** ${agentBits.filter(Boolean).join(" \xB7 ")}`);
2947
+ lines.push(`- **Cwd:** ${session.cwd}`);
2948
+ lines.push(
2949
+ `- **Exported:** ${bundle.exportedAt} from ${bundle.exportedFrom.machine} (hydra ${bundle.exportedFrom.hydraVersion})`
2923
2950
  );
2924
- if (!response.ok) {
2925
- const text = await response.text().catch(() => "");
2926
- process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
2927
- `);
2928
- process.exit(1);
2929
- }
2930
- const body = await response.text();
2931
- if (!outPath) {
2932
- process.stdout.write(body);
2933
- if (!body.endsWith("\n")) {
2934
- process.stdout.write("\n");
2951
+ const usage = session.currentUsage;
2952
+ if (usage && (usage.used !== void 0 || usage.costAmount !== void 0)) {
2953
+ const usageBits = [];
2954
+ if (usage.used !== void 0) {
2955
+ const denom = usage.size !== void 0 ? `${formatNumber(usage.size)}` : void 0;
2956
+ usageBits.push(
2957
+ denom ? `${formatNumber(usage.used)} / ${denom} tokens` : `${formatNumber(usage.used)} tokens`
2958
+ );
2935
2959
  }
2936
- return;
2960
+ if (usage.costAmount !== void 0) {
2961
+ const currency = usage.costCurrency ?? "USD";
2962
+ usageBits.push(`$${usage.costAmount.toFixed(2)} ${currency}`);
2963
+ }
2964
+ lines.push(`- **Usage:** ${usageBits.join(" \xB7 ")}`);
2937
2965
  }
2938
- const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
2939
- await fs13.mkdir(path8.dirname(path8.resolve(resolved)), { recursive: true });
2940
- await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
2941
- process.stdout.write(`Wrote ${resolved}
2942
- `);
2966
+ out.push(lines.join("\n"));
2967
+ out.push("");
2943
2968
  }
2944
- async function runSessionsImport(file, opts = {}) {
2945
- if (!file) {
2946
- process.stderr.write(
2947
- "Usage: hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]\n"
2948
- );
2949
- process.exit(2);
2969
+ function emitBody(out, events, toolFinalStates) {
2970
+ if (!events.some((e) => isVisible(e.event))) {
2971
+ out.push("_No conversation history recorded._");
2972
+ out.push("");
2973
+ return;
2950
2974
  }
2951
- let cwdOverride;
2952
- if (opts.cwd !== void 0) {
2953
- const resolved = path8.resolve(opts.cwd);
2954
- try {
2955
- const stat4 = await fs13.stat(resolved);
2956
- if (!stat4.isDirectory()) {
2957
- process.stderr.write(`--cwd ${resolved} is not a directory
2958
- `);
2959
- process.exit(1);
2975
+ const seenToolIds = /* @__PURE__ */ new Set();
2976
+ let turn = 0;
2977
+ let agentBuffer = "";
2978
+ let inTurn = false;
2979
+ const flushAgent = () => {
2980
+ if (agentBuffer.length === 0) {
2981
+ return;
2982
+ }
2983
+ out.push(agentBuffer.trimEnd());
2984
+ out.push("");
2985
+ agentBuffer = "";
2986
+ };
2987
+ const startTurnIfNeeded = () => {
2988
+ if (inTurn) {
2989
+ return;
2990
+ }
2991
+ turn += 1;
2992
+ out.push("---");
2993
+ out.push("");
2994
+ out.push(`## Turn ${turn}`);
2995
+ out.push("");
2996
+ inTurn = true;
2997
+ };
2998
+ for (const { event } of events) {
2999
+ switch (event.kind) {
3000
+ case "user-text": {
3001
+ flushAgent();
3002
+ turn += 1;
3003
+ out.push("---");
3004
+ out.push("");
3005
+ out.push(`## Turn ${turn}`);
3006
+ out.push("");
3007
+ out.push("**User:**");
3008
+ out.push("");
3009
+ for (const line of event.text.split("\n")) {
3010
+ out.push(`> ${escapeInline(line)}`);
3011
+ }
3012
+ out.push("");
3013
+ out.push("**Assistant:**");
3014
+ out.push("");
3015
+ inTurn = true;
3016
+ break;
2960
3017
  }
2961
- } catch {
2962
- process.stderr.write(`--cwd ${resolved} does not exist
2963
- `);
2964
- process.exit(1);
3018
+ case "agent-text":
3019
+ startTurnIfNeeded();
3020
+ agentBuffer += event.text;
3021
+ break;
3022
+ case "agent-thought": {
3023
+ startTurnIfNeeded();
3024
+ flushAgent();
3025
+ const lines = event.text.split("\n");
3026
+ for (const line of lines) {
3027
+ out.push(`> _${escapeInline(line)}_`);
3028
+ }
3029
+ out.push("");
3030
+ break;
3031
+ }
3032
+ case "tool-call": {
3033
+ startTurnIfNeeded();
3034
+ flushAgent();
3035
+ if (seenToolIds.has(event.toolCallId)) {
3036
+ break;
3037
+ }
3038
+ seenToolIds.add(event.toolCallId);
3039
+ const final = toolFinalStates.get(event.toolCallId) ?? {
3040
+ title: event.title,
3041
+ status: event.status ?? "pending"
3042
+ };
3043
+ out.push(`- ${statusGlyph(final.status)} ${formatToolLine(final)}`);
3044
+ out.push("");
3045
+ break;
3046
+ }
3047
+ case "tool-call-update":
3048
+ break;
3049
+ case "plan": {
3050
+ startTurnIfNeeded();
3051
+ flushAgent();
3052
+ out.push("**Plan:**");
3053
+ out.push("");
3054
+ for (const entry of event.entries) {
3055
+ const checked = entry.status === "completed" ? "[x]" : "[ ]";
3056
+ out.push(`- ${checked} ${escapeInline(entry.content)}`);
3057
+ }
3058
+ out.push("");
3059
+ break;
3060
+ }
3061
+ case "mode-changed":
3062
+ startTurnIfNeeded();
3063
+ flushAgent();
3064
+ out.push(`_mode: ${escapeInline(event.mode)}_`);
3065
+ out.push("");
3066
+ break;
3067
+ case "model-changed":
3068
+ startTurnIfNeeded();
3069
+ flushAgent();
3070
+ out.push(`_model: ${escapeInline(event.model)}_`);
3071
+ out.push("");
3072
+ break;
3073
+ case "turn-complete":
3074
+ flushAgent();
3075
+ break;
3076
+ case "usage-update":
3077
+ case "available-commands":
3078
+ case "session-info":
3079
+ case "unknown":
3080
+ break;
2965
3081
  }
2966
- cwdOverride = resolved;
2967
3082
  }
2968
- let body;
2969
- if (file === "-") {
2970
- body = await readStdin();
2971
- } else {
2972
- body = await fs13.readFile(file, "utf8");
3083
+ flushAgent();
3084
+ }
3085
+ function isVisible(event) {
3086
+ switch (event.kind) {
3087
+ case "usage-update":
3088
+ case "available-commands":
3089
+ case "session-info":
3090
+ case "unknown":
3091
+ case "turn-complete":
3092
+ return false;
3093
+ default:
3094
+ return true;
2973
3095
  }
2974
- let bundle;
2975
- try {
2976
- bundle = JSON.parse(body);
2977
- } catch (err) {
2978
- process.stderr.write(`Failed to parse bundle: ${err.message}
2979
- `);
2980
- process.exit(1);
3096
+ }
3097
+ function formatToolLine(state) {
3098
+ const status = state.status;
3099
+ const suffix = status === "completed" || status === void 0 ? "" : ` _(${status})_`;
3100
+ return `${escapeInline(state.title)}${suffix}`;
3101
+ }
3102
+ function statusGlyph(status) {
3103
+ switch (status) {
3104
+ case "completed":
3105
+ return "\u2713";
3106
+ case "failed":
3107
+ return "\u2717";
3108
+ case "cancelled":
3109
+ case "rejected":
3110
+ return "\u2298";
3111
+ case "in_progress":
3112
+ return "\u21BB";
3113
+ default:
3114
+ return "\xB7";
2981
3115
  }
2982
- if (opts.info === true) {
2983
- const inspectConfig = await loadConfigReadOnly();
2984
- printBundleInfo(bundle, inspectConfig.tui.cwdColumnMaxWidth);
2985
- return;
3116
+ }
3117
+ function escapeInline(text) {
3118
+ return text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
3119
+ }
3120
+ function formatNumber(n) {
3121
+ return n.toLocaleString("en-US");
3122
+ }
3123
+ var init_transcript = __esm({
3124
+ "src/core/transcript.ts"() {
3125
+ "use strict";
3126
+ init_render_update();
3127
+ init_session();
2986
3128
  }
2987
- const config = await loadConfig();
2988
- const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
2989
- const response = await fetch(`${baseUrl}/v1/sessions/import`, {
2990
- method: "POST",
2991
- headers: {
2992
- "Content-Type": "application/json",
2993
- Authorization: `Bearer ${config.daemon.authToken}`
2994
- },
2995
- body: JSON.stringify({
2996
- bundle,
2997
- replace: opts.replace === true,
2998
- ...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
2999
- })
3129
+ });
3130
+
3131
+ // src/acp/ws-stream.ts
3132
+ function wsToMessageStream(ws) {
3133
+ const messageHandlers = [];
3134
+ const closeHandlers = [];
3135
+ let closed = false;
3136
+ const emitClose = (err) => {
3137
+ if (closed) {
3138
+ return;
3139
+ }
3140
+ closed = true;
3141
+ for (const handler of closeHandlers) {
3142
+ handler(err);
3143
+ }
3144
+ };
3145
+ ws.on("message", (data, isBinary) => {
3146
+ if (isBinary) {
3147
+ return;
3148
+ }
3149
+ const text = data.toString("utf8");
3150
+ try {
3151
+ const parsed = JSON.parse(text);
3152
+ for (const handler of messageHandlers) {
3153
+ handler(parsed);
3154
+ }
3155
+ } catch (err) {
3156
+ for (const handler of messageHandlers) {
3157
+ handler({
3158
+ jsonrpc: "2.0",
3159
+ id: 0,
3160
+ error: {
3161
+ code: JsonRpcErrorCodes.ParseError,
3162
+ message: `Failed to parse WS frame: ${err.message}`
3163
+ }
3164
+ });
3165
+ }
3166
+ }
3000
3167
  });
3001
- if (response.status === 409) {
3002
- const detail = await response.json().catch(() => ({}));
3003
- process.stderr.write(
3004
- `Bundle already imported as ${detail.existingSessionId ?? "unknown"}. Use --replace to overwrite.
3005
- `
3006
- );
3007
- process.exit(1);
3008
- }
3009
- if (!response.ok) {
3010
- const text = await response.text().catch(() => "");
3011
- process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
3012
- `);
3013
- process.exit(1);
3014
- }
3015
- const result = await response.json();
3016
- process.stdout.write(
3017
- result.replaced ? `Replaced ${result.sessionId} (from ${result.importedFromSessionId})
3018
- ` : `Imported as ${result.sessionId} (from ${result.importedFromSessionId})
3019
- `
3020
- );
3021
- }
3022
- function bundleToSummary(parsed) {
3168
+ ws.on("close", () => emitClose());
3169
+ ws.on("error", (err) => emitClose(err));
3023
3170
  return {
3024
- sessionId: parsed.session.sessionId,
3025
- cwd: parsed.session.cwd,
3026
- agentId: parsed.session.agentId,
3027
- currentUsage: parsed.session.currentUsage,
3028
- title: parsed.session.title,
3029
- importedFromMachine: parsed.exportedFrom.machine,
3030
- attachedClients: 0,
3031
- updatedAt: parsed.session.updatedAt,
3032
- status: "cold"
3171
+ async send(message) {
3172
+ if (closed) {
3173
+ throw new Error("ws is closed");
3174
+ }
3175
+ const text = JSON.stringify(message);
3176
+ await new Promise((resolve5, reject) => {
3177
+ ws.send(text, (err) => {
3178
+ if (err) {
3179
+ reject(err);
3180
+ return;
3181
+ }
3182
+ resolve5();
3183
+ });
3184
+ });
3185
+ },
3186
+ onMessage(handler) {
3187
+ messageHandlers.push(handler);
3188
+ },
3189
+ onClose(handler) {
3190
+ closeHandlers.push(handler);
3191
+ },
3192
+ async close() {
3193
+ if (closed) {
3194
+ return;
3195
+ }
3196
+ ws.close();
3197
+ emitClose();
3198
+ }
3033
3199
  };
3034
3200
  }
3035
- function printBundleInfo(raw, cwdColumnMaxWidth) {
3036
- let parsed;
3201
+ var init_ws_stream = __esm({
3202
+ "src/acp/ws-stream.ts"() {
3203
+ "use strict";
3204
+ init_types();
3205
+ }
3206
+ });
3207
+
3208
+ // src/core/daemon-bootstrap.ts
3209
+ import { spawn as spawn5 } from "child_process";
3210
+ import { setTimeout as sleep } from "timers/promises";
3211
+ async function ensureDaemonReachable(config) {
3212
+ if (await pingHealth(config)) {
3213
+ return;
3214
+ }
3215
+ process.stderr.write("hydra-acp: daemon not running; starting it...\n");
3216
+ spawnDaemonDetached();
3217
+ await waitForDaemonReady(config);
3218
+ }
3219
+ async function pingHealth(config) {
3220
+ const protocol = config.daemon.tls ? "https" : "http";
3221
+ const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/v1/health`;
3037
3222
  try {
3038
- parsed = decodeBundle(raw);
3039
- } catch (err) {
3040
- process.stderr.write(`Not a valid bundle: ${err.message}
3041
- `);
3042
- process.exit(1);
3223
+ const response = await fetch(url, {
3224
+ signal: AbortSignal.timeout(500)
3225
+ });
3226
+ return response.ok;
3227
+ } catch {
3228
+ return false;
3043
3229
  }
3044
- const summary = bundleToSummary(parsed);
3045
- const row = toRow(summary);
3046
- const widths = computeWidths([row]);
3047
- const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
3048
- process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
3049
- process.stdout.write(formatRow(row, widths, maxWidth, cwdColumnMaxWidth) + "\n");
3050
- const originUpstream = parsed.session.upstreamSessionId ?? "-";
3051
- process.stdout.write(
3052
- `
3053
- lineage: ${parsed.session.lineageId}
3054
- exported: ${parsed.exportedAt} from ${parsed.exportedFrom.machine} (hydra ${parsed.exportedFrom.hydraVersion})
3055
- origin session: ${parsed.session.sessionId}
3056
- origin upstream: ${originUpstream}
3057
- history entries: ${parsed.history.length}` + (parsed.promptHistory ? `, prompt history: ${parsed.promptHistory.length}
3058
- ` : "\n")
3059
- );
3060
3230
  }
3061
- async function readStdin() {
3062
- const chunks = [];
3063
- for await (const chunk of process.stdin) {
3064
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
3231
+ function spawnDaemonDetached() {
3232
+ const cliPath = process.argv[1];
3233
+ if (!cliPath) {
3234
+ throw new Error("Cannot determine hydra-acp binary path to spawn daemon");
3065
3235
  }
3066
- return Buffer.concat(chunks).toString("utf8");
3236
+ const child = spawn5(
3237
+ process.execPath,
3238
+ [cliPath, "daemon", "start", "--foreground"],
3239
+ {
3240
+ detached: true,
3241
+ stdio: "ignore",
3242
+ env: process.env
3243
+ }
3244
+ );
3245
+ child.unref();
3067
3246
  }
3068
- function deriveFilenameFrom(response, id) {
3069
- const cd = response.headers.get("content-disposition");
3070
- if (cd) {
3071
- const match = cd.match(/filename="([^"]+)"/);
3072
- if (match) {
3073
- return match[1];
3247
+ async function waitForDaemonReady(config, timeoutMs = 15e3) {
3248
+ const deadline = Date.now() + timeoutMs;
3249
+ while (Date.now() < deadline) {
3250
+ if (await pingHealth(config)) {
3251
+ return;
3074
3252
  }
3253
+ await sleep(150);
3075
3254
  }
3076
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3077
- return `hydra-${id}-${stamp}.hydra`;
3078
- }
3079
- function httpBase(host, port, tls) {
3080
- const protocol = tls ? "https" : "http";
3081
- return `${protocol}://${host}:${port}`;
3255
+ throw new Error(
3256
+ `hydra-acp daemon did not become ready within ${timeoutMs}ms`
3257
+ );
3082
3258
  }
3083
- var init_sessions = __esm({
3084
- "src/cli/commands/sessions.ts"() {
3259
+ var init_daemon_bootstrap = __esm({
3260
+ "src/core/daemon-bootstrap.ts"() {
3085
3261
  "use strict";
3086
- init_config();
3087
- init_bundle();
3088
- init_session_row();
3089
3262
  }
3090
3263
  });
3091
3264
 
3092
- // src/shim/resilient-ws.ts
3093
- import { setTimeout as sleep3 } from "timers/promises";
3094
- import { WebSocket } from "ws";
3095
- function isResponse(msg) {
3096
- return !("method" in msg) && "id" in msg && msg.id !== void 0;
3265
+ // src/core/agent-display.ts
3266
+ function shortenModel(model) {
3267
+ if (!model) {
3268
+ return void 0;
3269
+ }
3270
+ const idx = model.lastIndexOf("/");
3271
+ if (idx === -1) {
3272
+ return model;
3273
+ }
3274
+ return model.slice(idx + 1);
3097
3275
  }
3098
- async function openWs(url, subprotocols) {
3099
- return new Promise((resolve5, reject) => {
3100
- const ws = new WebSocket(url, subprotocols);
3101
- const onOpen = () => {
3102
- ws.off("error", onError);
3103
- resolve5(wsToMessageStream(ws));
3104
- };
3105
- const onError = (err) => {
3106
- ws.off("open", onOpen);
3276
+ function formatAgentWithModel(agentId, model) {
3277
+ const agent = agentId ?? "?";
3278
+ const short = shortenModel(model);
3279
+ if (!short) {
3280
+ return agent;
3281
+ }
3282
+ return `${agent}${AGENT_MODEL_SEP}${short}`;
3283
+ }
3284
+ function formatAgentCell(agentId, usage) {
3285
+ const base = agentId ?? "?";
3286
+ if (!usage || typeof usage.costAmount !== "number") {
3287
+ return base;
3288
+ }
3289
+ const compact = formatCostCompact(usage.costAmount, usage.costCurrency);
3290
+ if (compact === null) {
3291
+ return base;
3292
+ }
3293
+ return `${base} ${compact}`;
3294
+ }
3295
+ function formatCost(amount, currency) {
3296
+ const sign = currency === "USD" || currency === void 0 ? "$" : "";
3297
+ const decimals = amount >= 1 ? 2 : 4;
3298
+ return `${sign}${amount.toFixed(decimals)}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
3299
+ }
3300
+ function formatCostCompact(amount, currency) {
3301
+ const whole = Math.round(amount);
3302
+ if (whole === 0) {
3303
+ return null;
3304
+ }
3305
+ const sign = currency === "USD" || currency === void 0 ? "$" : "";
3306
+ return `${sign}${whole}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
3307
+ }
3308
+ var AGENT_MODEL_SEP;
3309
+ var init_agent_display = __esm({
3310
+ "src/core/agent-display.ts"() {
3311
+ "use strict";
3312
+ AGENT_MODEL_SEP = "\u2022";
3313
+ }
3314
+ });
3315
+
3316
+ // src/cli/session-row.ts
3317
+ function toRow(s, now = Date.now()) {
3318
+ return {
3319
+ session: stripHydraSessionPrefix(s.sessionId),
3320
+ upstream: formatUpstreamCell(s.upstreamSessionId, s.importedFromMachine),
3321
+ state: formatState(s.status, s.attachedClients),
3322
+ agent: formatAgentCell(s.agentId, s.currentUsage),
3323
+ age: formatRelativeAge(s.updatedAt, now),
3324
+ title: s.title ?? "-",
3325
+ cwd: shortenHomePath(s.cwd)
3326
+ };
3327
+ }
3328
+ function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
3329
+ if (upstreamSessionId && upstreamSessionId.length > 0) {
3330
+ return upstreamSessionId;
3331
+ }
3332
+ if (importedFromMachine && importedFromMachine.length > 0) {
3333
+ return `\u2190 ${importedFromMachine}`;
3334
+ }
3335
+ return "-";
3336
+ }
3337
+ function formatState(status, clients) {
3338
+ if (status === "cold") {
3339
+ return "COLD";
3340
+ }
3341
+ return `LIVE(${clients})`;
3342
+ }
3343
+ function computeWidths(rows) {
3344
+ return {
3345
+ session: maxLen(HEADER.session, rows.map((r) => r.session)),
3346
+ upstream: maxLen(HEADER.upstream, rows.map((r) => r.upstream)),
3347
+ state: maxLen(HEADER.state, rows.map((r) => r.state)),
3348
+ agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
3349
+ age: maxLen(HEADER.age, rows.map((r) => r.age)),
3350
+ cwd: maxLen(HEADER.cwd, rows.map((r) => r.cwd)),
3351
+ title: maxLen(HEADER.title, rows.map((r) => r.title))
3352
+ };
3353
+ }
3354
+ function formatRelativeAge(iso, now) {
3355
+ if (!iso) {
3356
+ return "?";
3357
+ }
3358
+ const t = Date.parse(iso);
3359
+ if (Number.isNaN(t)) {
3360
+ return "?";
3361
+ }
3362
+ const diff = Math.max(0, now - t);
3363
+ const sec = Math.floor(diff / 1e3);
3364
+ if (sec < 60) {
3365
+ return "<1m";
3366
+ }
3367
+ const min = Math.floor(sec / 60);
3368
+ if (min < 60) {
3369
+ return `${min}m`;
3370
+ }
3371
+ const hr = Math.floor(min / 60);
3372
+ if (hr < 24) {
3373
+ return `${hr}h`;
3374
+ }
3375
+ const day = Math.floor(hr / 24);
3376
+ if (day < 14) {
3377
+ return `${day}d`;
3378
+ }
3379
+ const week = Math.floor(day / 7);
3380
+ if (week < 9) {
3381
+ return `${week}w`;
3382
+ }
3383
+ const month = Math.floor(day / 30);
3384
+ if (month < 12) {
3385
+ return `${month}mo`;
3386
+ }
3387
+ const year = Math.floor(day / 365);
3388
+ return `${year}y`;
3389
+ }
3390
+ function maxLen(headerCell, values) {
3391
+ let max = headerCell.length;
3392
+ for (const v of values) {
3393
+ if (v.length > max) {
3394
+ max = v.length;
3395
+ }
3396
+ }
3397
+ return max;
3398
+ }
3399
+ function formatRow(r, w, maxWidth, cwdMaxWidth = DEFAULT_CWD_MAX_WIDTH) {
3400
+ const fixed = [
3401
+ r.session.padEnd(w.session),
3402
+ r.upstream.padEnd(w.upstream),
3403
+ r.state.padEnd(w.state),
3404
+ r.agent.padEnd(w.agent),
3405
+ r.age.padStart(w.age)
3406
+ ].join(SEP);
3407
+ if (maxWidth === void 0) {
3408
+ return [fixed, r.cwd.padEnd(w.cwd), r.title].join(SEP);
3409
+ }
3410
+ const budget = maxWidth - fixed.length - SEP.length;
3411
+ if (budget <= 0) {
3412
+ return fixed.slice(0, maxWidth);
3413
+ }
3414
+ const cwdCap = Math.min(w.cwd, cwdMaxWidth);
3415
+ const cwdAlloc = Math.min(cwdCap, Math.max(0, budget - SEP.length - 1));
3416
+ const cwdCell = truncateMiddle(r.cwd, cwdAlloc).padEnd(cwdAlloc);
3417
+ const titleBudget = Math.max(0, budget - cwdAlloc - SEP.length);
3418
+ const titleCell = truncateRight(r.title, titleBudget);
3419
+ return [fixed, cwdCell, titleCell].join(SEP);
3420
+ }
3421
+ function truncateRight(s, max) {
3422
+ if (max <= 0) {
3423
+ return "";
3424
+ }
3425
+ if (s.length <= max) {
3426
+ return s;
3427
+ }
3428
+ if (max === 1) {
3429
+ return "\u2026";
3430
+ }
3431
+ return s.slice(0, max - 1) + "\u2026";
3432
+ }
3433
+ function truncateMiddle(s, max) {
3434
+ if (max <= 0) {
3435
+ return "";
3436
+ }
3437
+ if (s.length <= max) {
3438
+ return s;
3439
+ }
3440
+ if (max === 1) {
3441
+ return "\u2026";
3442
+ }
3443
+ const head = Math.ceil((max - 1) / 2);
3444
+ const tail = max - 1 - head;
3445
+ return s.slice(0, head) + "\u2026" + s.slice(s.length - tail);
3446
+ }
3447
+ var HEADER, SEP, DEFAULT_CWD_MAX_WIDTH;
3448
+ var init_session_row = __esm({
3449
+ "src/cli/session-row.ts"() {
3450
+ "use strict";
3451
+ init_agent_display();
3452
+ init_paths();
3453
+ init_session();
3454
+ HEADER = {
3455
+ session: "SESSION",
3456
+ upstream: "UPSTREAM",
3457
+ state: "STATE",
3458
+ agent: "AGENT",
3459
+ age: "AGE",
3460
+ title: "TITLE",
3461
+ cwd: "CWD"
3462
+ };
3463
+ SEP = " ";
3464
+ DEFAULT_CWD_MAX_WIDTH = 24;
3465
+ }
3466
+ });
3467
+
3468
+ // src/cli/commands/sessions.ts
3469
+ import * as fs13 from "fs/promises";
3470
+ import * as path8 from "path";
3471
+ async function runSessionsList(opts = {}) {
3472
+ const config = await loadConfig();
3473
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3474
+ const url = new URL(`${baseUrl}/v1/sessions`);
3475
+ const response = await fetch(url.toString(), {
3476
+ headers: { Authorization: `Bearer ${config.daemon.authToken}` }
3477
+ });
3478
+ if (!response.ok) {
3479
+ process.stderr.write(`Daemon returned HTTP ${response.status}
3480
+ `);
3481
+ process.exit(1);
3482
+ }
3483
+ const body = await response.json();
3484
+ if (opts.json) {
3485
+ process.stdout.write(JSON.stringify(body.sessions, null, 2) + "\n");
3486
+ return;
3487
+ }
3488
+ if (body.sessions.length === 0) {
3489
+ process.stdout.write("No active sessions.\n");
3490
+ return;
3491
+ }
3492
+ const sorted = body.sessions.slice().sort((a, b) => {
3493
+ const liveDiff = (b.status === "live" ? 1 : 0) - (a.status === "live" ? 1 : 0);
3494
+ if (liveDiff !== 0) {
3495
+ return liveDiff;
3496
+ }
3497
+ return String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""));
3498
+ });
3499
+ let visible = sorted;
3500
+ let truncated = 0;
3501
+ if (!opts.all) {
3502
+ const liveCount = sorted.filter((s) => s.status !== "cold").length;
3503
+ const limit = config.sessionListColdLimit;
3504
+ const coldSlice = sorted.slice(liveCount, liveCount + limit);
3505
+ const hiddenCold = sorted.length - liveCount - coldSlice.length;
3506
+ visible = [...sorted.slice(0, liveCount), ...coldSlice];
3507
+ truncated = hiddenCold;
3508
+ }
3509
+ const now = Date.now();
3510
+ const rows = visible.map((s) => toRow(s, now));
3511
+ const widths = computeWidths(rows);
3512
+ const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
3513
+ const cwdMax = config.tui.cwdColumnMaxWidth;
3514
+ process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdMax) + "\n");
3515
+ for (const r of rows) {
3516
+ process.stdout.write(formatRow(r, widths, maxWidth, cwdMax) + "\n");
3517
+ }
3518
+ if (truncated > 0) {
3519
+ process.stdout.write(
3520
+ `
3521
+ ... ${truncated} more cold session${truncated === 1 ? "" : "s"} hidden. Use --all to show.
3522
+ `
3523
+ );
3524
+ }
3525
+ }
3526
+ async function runSessionsKill(id) {
3527
+ if (!id) {
3528
+ process.stderr.write("Usage: hydra-acp sessions kill <session-id>\n");
3529
+ process.exit(2);
3530
+ }
3531
+ const config = await loadConfig();
3532
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3533
+ const response = await fetch(`${baseUrl}/v1/sessions/${id}/kill`, {
3534
+ method: "POST",
3535
+ headers: { Authorization: `Bearer ${config.daemon.authToken}` }
3536
+ });
3537
+ if (!response.ok && response.status !== 204) {
3538
+ process.stderr.write(`Daemon returned HTTP ${response.status}
3539
+ `);
3540
+ process.exit(1);
3541
+ }
3542
+ process.stdout.write(`Killed ${id}
3543
+ `);
3544
+ }
3545
+ async function runSessionsRemove(id) {
3546
+ if (!id) {
3547
+ process.stderr.write("Usage: hydra-acp sessions remove <session-id>\n");
3548
+ process.exit(2);
3549
+ }
3550
+ const config = await loadConfig();
3551
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3552
+ const response = await fetch(`${baseUrl}/v1/sessions/${id}`, {
3553
+ method: "DELETE",
3554
+ headers: { Authorization: `Bearer ${config.daemon.authToken}` }
3555
+ });
3556
+ if (!response.ok && response.status !== 204) {
3557
+ process.stderr.write(`Daemon returned HTTP ${response.status}
3558
+ `);
3559
+ process.exit(1);
3560
+ }
3561
+ process.stdout.write(`Removed ${id}
3562
+ `);
3563
+ }
3564
+ async function runSessionsExport(id, outPath) {
3565
+ if (!id) {
3566
+ process.stderr.write(
3567
+ "Usage: hydra-acp sessions export <session-id> [--out <file>]\n"
3568
+ );
3569
+ process.exit(2);
3570
+ }
3571
+ const config = await loadConfig();
3572
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3573
+ const response = await fetch(
3574
+ `${baseUrl}/v1/sessions/${encodeURIComponent(id)}/export`,
3575
+ {
3576
+ headers: { Authorization: `Bearer ${config.daemon.authToken}` }
3577
+ }
3578
+ );
3579
+ if (!response.ok) {
3580
+ const text = await response.text().catch(() => "");
3581
+ process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
3582
+ `);
3583
+ process.exit(1);
3584
+ }
3585
+ const body = await response.text();
3586
+ if (!outPath) {
3587
+ process.stdout.write(body);
3588
+ if (!body.endsWith("\n")) {
3589
+ process.stdout.write("\n");
3590
+ }
3591
+ return;
3592
+ }
3593
+ const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
3594
+ await fs13.mkdir(path8.dirname(path8.resolve(resolved)), { recursive: true });
3595
+ await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
3596
+ process.stdout.write(`Wrote ${resolved}
3597
+ `);
3598
+ }
3599
+ async function runSessionsTranscript(idOrFile, outPath) {
3600
+ if (!idOrFile) {
3601
+ process.stderr.write(
3602
+ "Usage: hydra-acp sessions transcript <session-id>|<file> [--out <file>|.]\n"
3603
+ );
3604
+ process.exit(2);
3605
+ }
3606
+ let body;
3607
+ let defaultName;
3608
+ const localFile = await readBundleFileIfExists(idOrFile);
3609
+ if (localFile !== null) {
3610
+ const bundle = decodeBundleOrExit(localFile.raw);
3611
+ body = bundleToMarkdown(bundle);
3612
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3613
+ defaultName = `${path8.basename(idOrFile, path8.extname(idOrFile))}-${stamp}.md`;
3614
+ } else {
3615
+ const config = await loadConfig();
3616
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3617
+ const response = await fetch(
3618
+ `${baseUrl}/v1/sessions/${encodeURIComponent(idOrFile)}/transcript`,
3619
+ {
3620
+ headers: { Authorization: `Bearer ${config.daemon.authToken}` }
3621
+ }
3622
+ );
3623
+ if (!response.ok) {
3624
+ const text = await response.text().catch(() => "");
3625
+ process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
3626
+ `);
3627
+ process.exit(1);
3628
+ }
3629
+ body = await response.text();
3630
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3631
+ defaultName = `hydra-${idOrFile}-${stamp}.md`;
3632
+ }
3633
+ if (!outPath) {
3634
+ process.stdout.write(body);
3635
+ if (!body.endsWith("\n")) {
3636
+ process.stdout.write("\n");
3637
+ }
3638
+ return;
3639
+ }
3640
+ const resolved = outPath === "." ? defaultName : outPath;
3641
+ await fs13.mkdir(path8.dirname(path8.resolve(resolved)), { recursive: true });
3642
+ await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
3643
+ process.stdout.write(`Wrote ${resolved}
3644
+ `);
3645
+ }
3646
+ async function readBundleFileIfExists(arg) {
3647
+ try {
3648
+ const stat4 = await fs13.stat(arg);
3649
+ if (!stat4.isFile()) {
3650
+ return null;
3651
+ }
3652
+ } catch {
3653
+ return null;
3654
+ }
3655
+ const text = await fs13.readFile(arg, "utf8");
3656
+ try {
3657
+ return { raw: JSON.parse(text) };
3658
+ } catch (err) {
3659
+ process.stderr.write(`Failed to parse bundle file: ${err.message}
3660
+ `);
3661
+ process.exit(1);
3662
+ }
3663
+ }
3664
+ function decodeBundleOrExit(raw) {
3665
+ try {
3666
+ return decodeBundle(raw);
3667
+ } catch (err) {
3668
+ process.stderr.write(`Not a valid bundle: ${err.message}
3669
+ `);
3670
+ process.exit(1);
3671
+ }
3672
+ }
3673
+ async function runSessionsImport(file, opts = {}) {
3674
+ if (!file) {
3675
+ process.stderr.write(
3676
+ "Usage: hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]\n"
3677
+ );
3678
+ process.exit(2);
3679
+ }
3680
+ let cwdOverride;
3681
+ if (opts.cwd !== void 0) {
3682
+ const resolved = path8.resolve(opts.cwd);
3683
+ try {
3684
+ const stat4 = await fs13.stat(resolved);
3685
+ if (!stat4.isDirectory()) {
3686
+ process.stderr.write(`--cwd ${resolved} is not a directory
3687
+ `);
3688
+ process.exit(1);
3689
+ }
3690
+ } catch {
3691
+ process.stderr.write(`--cwd ${resolved} does not exist
3692
+ `);
3693
+ process.exit(1);
3694
+ }
3695
+ cwdOverride = resolved;
3696
+ }
3697
+ let body;
3698
+ if (file === "-") {
3699
+ body = await readStdin();
3700
+ } else {
3701
+ body = await fs13.readFile(file, "utf8");
3702
+ }
3703
+ let bundle;
3704
+ try {
3705
+ bundle = JSON.parse(body);
3706
+ } catch (err) {
3707
+ process.stderr.write(`Failed to parse bundle: ${err.message}
3708
+ `);
3709
+ process.exit(1);
3710
+ }
3711
+ if (opts.info === true) {
3712
+ const inspectConfig = await loadConfigReadOnly();
3713
+ printBundleInfo(bundle, inspectConfig.tui.cwdColumnMaxWidth);
3714
+ return;
3715
+ }
3716
+ const config = await loadConfig();
3717
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3718
+ const response = await fetch(`${baseUrl}/v1/sessions/import`, {
3719
+ method: "POST",
3720
+ headers: {
3721
+ "Content-Type": "application/json",
3722
+ Authorization: `Bearer ${config.daemon.authToken}`
3723
+ },
3724
+ body: JSON.stringify({
3725
+ bundle,
3726
+ replace: opts.replace === true,
3727
+ ...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
3728
+ })
3729
+ });
3730
+ if (response.status === 409) {
3731
+ const detail = await response.json().catch(() => ({}));
3732
+ process.stderr.write(
3733
+ `Bundle already imported as ${detail.existingSessionId ?? "unknown"}. Use --replace to overwrite.
3734
+ `
3735
+ );
3736
+ process.exit(1);
3737
+ }
3738
+ if (!response.ok) {
3739
+ const text = await response.text().catch(() => "");
3740
+ process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
3741
+ `);
3742
+ process.exit(1);
3743
+ }
3744
+ const result = await response.json();
3745
+ process.stdout.write(
3746
+ result.replaced ? `Replaced ${result.sessionId} (from ${result.importedFromSessionId})
3747
+ ` : `Imported as ${result.sessionId} (from ${result.importedFromSessionId})
3748
+ `
3749
+ );
3750
+ }
3751
+ function bundleToSummary(parsed) {
3752
+ return {
3753
+ sessionId: parsed.session.sessionId,
3754
+ cwd: parsed.session.cwd,
3755
+ agentId: parsed.session.agentId,
3756
+ currentUsage: parsed.session.currentUsage,
3757
+ title: parsed.session.title,
3758
+ importedFromMachine: parsed.exportedFrom.machine,
3759
+ attachedClients: 0,
3760
+ updatedAt: parsed.session.updatedAt,
3761
+ status: "cold"
3762
+ };
3763
+ }
3764
+ function printBundleInfo(raw, cwdColumnMaxWidth) {
3765
+ let parsed;
3766
+ try {
3767
+ parsed = decodeBundle(raw);
3768
+ } catch (err) {
3769
+ process.stderr.write(`Not a valid bundle: ${err.message}
3770
+ `);
3771
+ process.exit(1);
3772
+ }
3773
+ const summary = bundleToSummary(parsed);
3774
+ const row = toRow(summary);
3775
+ const widths = computeWidths([row]);
3776
+ const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
3777
+ process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
3778
+ process.stdout.write(formatRow(row, widths, maxWidth, cwdColumnMaxWidth) + "\n");
3779
+ const originUpstream = parsed.session.upstreamSessionId ?? "-";
3780
+ process.stdout.write(
3781
+ `
3782
+ lineage: ${parsed.session.lineageId}
3783
+ exported: ${parsed.exportedAt} from ${parsed.exportedFrom.machine} (hydra ${parsed.exportedFrom.hydraVersion})
3784
+ origin session: ${parsed.session.sessionId}
3785
+ origin upstream: ${originUpstream}
3786
+ history entries: ${parsed.history.length}` + (parsed.promptHistory ? `, prompt history: ${parsed.promptHistory.length}
3787
+ ` : "\n")
3788
+ );
3789
+ }
3790
+ async function readStdin() {
3791
+ const chunks = [];
3792
+ for await (const chunk of process.stdin) {
3793
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
3794
+ }
3795
+ return Buffer.concat(chunks).toString("utf8");
3796
+ }
3797
+ function deriveFilenameFrom(response, id) {
3798
+ const cd = response.headers.get("content-disposition");
3799
+ if (cd) {
3800
+ const match = cd.match(/filename="([^"]+)"/);
3801
+ if (match) {
3802
+ return match[1];
3803
+ }
3804
+ }
3805
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3806
+ return `hydra-${id}-${stamp}.hydra`;
3807
+ }
3808
+ function httpBase(host, port, tls) {
3809
+ const protocol = tls ? "https" : "http";
3810
+ return `${protocol}://${host}:${port}`;
3811
+ }
3812
+ var init_sessions = __esm({
3813
+ "src/cli/commands/sessions.ts"() {
3814
+ "use strict";
3815
+ init_config();
3816
+ init_bundle();
3817
+ init_transcript();
3818
+ init_session_row();
3819
+ }
3820
+ });
3821
+
3822
+ // src/shim/resilient-ws.ts
3823
+ import { setTimeout as sleep3 } from "timers/promises";
3824
+ import { WebSocket } from "ws";
3825
+ function isResponse(msg) {
3826
+ return !("method" in msg) && "id" in msg && msg.id !== void 0;
3827
+ }
3828
+ async function openWs(url, subprotocols) {
3829
+ return new Promise((resolve5, reject) => {
3830
+ const ws = new WebSocket(url, subprotocols);
3831
+ const onOpen = () => {
3832
+ ws.off("error", onError);
3833
+ resolve5(wsToMessageStream(ws));
3834
+ };
3835
+ const onError = (err) => {
3836
+ ws.off("open", onOpen);
3107
3837
  reject(err);
3108
3838
  };
3109
3839
  ws.once("open", onOpen);
@@ -3324,6 +4054,59 @@ var init_resilient_ws = __esm({
3324
4054
  }
3325
4055
  });
3326
4056
 
4057
+ // src/core/update-check.ts
4058
+ function disabled() {
4059
+ if (process.env.NO_UPDATE_NOTIFIER === "1") {
4060
+ return true;
4061
+ }
4062
+ if (process.argv.includes("--no-update-notifier")) {
4063
+ return true;
4064
+ }
4065
+ return false;
4066
+ }
4067
+ async function getPendingUpdate() {
4068
+ if (cached !== void 0) {
4069
+ return cached;
4070
+ }
4071
+ if (disabled()) {
4072
+ cached = null;
4073
+ return cached;
4074
+ }
4075
+ try {
4076
+ const mod = await import("update-notifier");
4077
+ const updateNotifier = mod.default ?? mod;
4078
+ const notifier = updateNotifier({
4079
+ pkg: { name: PKG_NAME, version: HYDRA_VERSION },
4080
+ updateCheckInterval: 1e3 * 60 * 60 * 24
4081
+ });
4082
+ notifier.check();
4083
+ const u = notifier.update;
4084
+ if (u && typeof u.latest === "string" && typeof u.current === "string" && u.latest !== u.current) {
4085
+ cached = {
4086
+ current: u.current,
4087
+ latest: u.latest,
4088
+ type: typeof u.type === "string" ? u.type : "unknown"
4089
+ };
4090
+ } else {
4091
+ cached = null;
4092
+ }
4093
+ } catch {
4094
+ cached = null;
4095
+ }
4096
+ return cached;
4097
+ }
4098
+ function formatUpdateNoticeLine(info) {
4099
+ return `hydra-acp ${info.latest} available (current ${info.current}) \xB7 run: npm update -g ${PKG_NAME}`;
4100
+ }
4101
+ var PKG_NAME, cached;
4102
+ var init_update_check = __esm({
4103
+ "src/core/update-check.ts"() {
4104
+ "use strict";
4105
+ init_hydra_version();
4106
+ PKG_NAME = "@hydra-acp/cli";
4107
+ }
4108
+ });
4109
+
3327
4110
  // src/tui/discovery.ts
3328
4111
  async function listSessions(config, opts = {}, fetchImpl = fetch) {
3329
4112
  const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
@@ -4558,7 +5341,7 @@ function mapKeyName(name) {
4558
5341
  return null;
4559
5342
  }
4560
5343
  }
4561
- var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
5344
+ var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
4562
5345
  var init_screen = __esm({
4563
5346
  "src/tui/screen.ts"() {
4564
5347
  "use strict";
@@ -4566,7 +5349,7 @@ var init_screen = __esm({
4566
5349
  init_paths();
4567
5350
  init_session();
4568
5351
  init_attachments();
4569
- HEADER_ROWS = 2;
5352
+ SESSIONBAR_ROWS = 1;
4570
5353
  BANNER_ROWS = 1;
4571
5354
  SEPARATOR_ROWS = 1;
4572
5355
  MAX_PROMPT_ROWS = 8;
@@ -4664,7 +5447,7 @@ var init_screen = __esm({
4664
5447
  hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
4665
5448
  queued: 0
4666
5449
  };
4667
- header = { agent: "?", cwd: "?", sessionId: "?" };
5450
+ sessionbar = { agent: "?", cwd: "?", sessionId: "?" };
4668
5451
  lastWindowTitle = null;
4669
5452
  resizeHandler;
4670
5453
  keyHandler;
@@ -5015,8 +5798,8 @@ var init_screen = __esm({
5015
5798
  this.trimScrollback();
5016
5799
  this.scheduleRepaint();
5017
5800
  }
5018
- setHeader(header) {
5019
- this.header = { ...this.header, ...header };
5801
+ setSessionbar(sessionbar) {
5802
+ this.sessionbar = { ...this.sessionbar, ...sessionbar };
5020
5803
  this.syncWindowTitle();
5021
5804
  this.repaint();
5022
5805
  }
@@ -5024,8 +5807,8 @@ var init_screen = __esm({
5024
5807
  // the host terminal via OSC 2. Supported by xterm/foot/iTerm2/Alacritty/
5025
5808
  // most modern emulators; ignored harmlessly elsewhere.
5026
5809
  syncWindowTitle() {
5027
- const title = this.header.title?.trim();
5028
- const fallback = shortId(this.header.sessionId) || "hydra";
5810
+ const title = this.sessionbar.title?.trim();
5811
+ const fallback = shortId(this.sessionbar.sessionId) || "hydra";
5029
5812
  const raw = title && title.length > 0 ? title : fallback;
5030
5813
  const clean = raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
5031
5814
  if (clean === this.lastWindowTitle) {
@@ -5547,8 +6330,10 @@ var init_screen = __esm({
5547
6330
  return Math.max(1, this.scrollbackVisibleRows() - 2);
5548
6331
  }
5549
6332
  scrollbackVisibleRows() {
5550
- const top = HEADER_ROWS + SEPARATOR_ROWS;
5551
- const bottom = this.term.height - this.promptRows() - BANNER_ROWS - SEPARATOR_ROWS - this.chipRows() - this.queuedRows() - this.completionRows();
6333
+ const top = 1;
6334
+ const bottom = this.term.height - this.promptRows() - SESSIONBAR_ROWS - SEPARATOR_ROWS - // separator between banner and sessionbar
6335
+ BANNER_ROWS - SEPARATOR_ROWS - // separator above prompt
6336
+ this.chipRows() - this.queuedRows() - this.completionRows();
5552
6337
  return Math.max(0, bottom - top + 1);
5553
6338
  }
5554
6339
  maxScrollOffset() {
@@ -5625,30 +6410,32 @@ var init_screen = __esm({
5625
6410
  this.lastFrameW = w;
5626
6411
  this.lastFrameH = h;
5627
6412
  }
5628
- this.drawHeader();
5629
- this.drawSeparator(HEADER_ROWS);
5630
6413
  this.drawScrollback();
5631
6414
  this.drawCompletionZone();
5632
6415
  this.drawQueuedZone();
5633
6416
  this.drawAttachmentChipZone();
5634
6417
  const promptRows = this.promptRows();
5635
- const separatorRow = h - promptRows - BANNER_ROWS;
5636
- this.drawSeparator(separatorRow);
6418
+ const separatorAbovePromptRow = h - promptRows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
6419
+ this.drawSeparator(separatorAbovePromptRow);
5637
6420
  this.drawPrompt();
5638
6421
  this.drawBanner();
6422
+ this.drawSeparator(h - SESSIONBAR_ROWS);
6423
+ this.drawSessionbar();
5639
6424
  this.placeCursor();
5640
6425
  this.lastPromptRows = promptRows;
5641
6426
  }
5642
- drawHeader() {
6427
+ drawSessionbar() {
5643
6428
  const w = this.term.width;
5644
- const usage = formatUsage(this.header.usage);
5645
- const sid = shortId(this.header.sessionId);
5646
- const title = this.header.title?.trim();
5647
- const agentCell = formatAgentWithModel(this.header.agent, this.header.model);
5648
- const cwdDisplay = shortenHomePath(this.header.cwd);
5649
- const sig = `hdr|${w}|${agentCell}|${cwdDisplay}|${sid}|${title ?? ""}|${usage ?? ""}`;
5650
- this.paintRow(1, sig, () => {
5651
- const fixed = "hydra \xB7 ".length + agentCell.length + " \xB7 ".length + " \xB7 ".length + sid.length + (title ? " \xB7 ".length : 0) + (usage ? usage.length + 3 : 0);
6429
+ const row = this.term.height;
6430
+ const sid = shortId(this.sessionbar.sessionId);
6431
+ const title = this.sessionbar.title?.trim();
6432
+ const agentCell = formatAgentWithModel(this.sessionbar.agent, this.sessionbar.model);
6433
+ const cwdDisplay = shortenHomePath(this.sessionbar.cwd);
6434
+ const usage = formatUsage(this.sessionbar.usage);
6435
+ const sig = `sbar|${w}|${sid}|${agentCell}|${cwdDisplay}|${title ?? ""}|${usage ?? ""}`;
6436
+ this.paintRow(row, sig, () => {
6437
+ const usageReserve = usage ? usage.length + 3 : 0;
6438
+ const fixed = sid.length + " \xB7 ".length + agentCell.length + " \xB7 ".length + (title ? " \xB7 ".length : 0) + usageReserve;
5652
6439
  const variableRoom = Math.max(8, w - fixed);
5653
6440
  let cwdRoom;
5654
6441
  let titleRoom;
@@ -5660,13 +6447,13 @@ var init_screen = __esm({
5660
6447
  titleRoom = 0;
5661
6448
  cwdRoom = variableRoom;
5662
6449
  }
5663
- this.term.bold("hydra")(" \xB7 ").cyan.noFormat(agentCell)(" \xB7 ").dim.noFormat(truncate(cwdDisplay, cwdRoom))(" \xB7 ").yellow(sid);
6450
+ this.term.yellow(sid)(" \xB7 ").cyan.noFormat(agentCell)(" \xB7 ").dim.noFormat(truncate(cwdDisplay, cwdRoom));
5664
6451
  if (title) {
5665
6452
  this.term(" \xB7 ").bold.noFormat(truncate(title, titleRoom));
5666
6453
  }
5667
6454
  if (usage) {
5668
6455
  const col = Math.max(1, w - usage.length + 1);
5669
- this.term.moveTo(col, 1).eraseLineAfter();
6456
+ this.term.moveTo(col, row).eraseLineAfter();
5670
6457
  this.term.dim.noFormat(usage);
5671
6458
  }
5672
6459
  });
@@ -5679,7 +6466,7 @@ var init_screen = __esm({
5679
6466
  }
5680
6467
  drawScrollback() {
5681
6468
  const w = this.term.width;
5682
- const top = HEADER_ROWS + SEPARATOR_ROWS;
6469
+ const top = 1;
5683
6470
  const visibleRows = this.scrollbackVisibleRows();
5684
6471
  if (visibleRows <= 0) {
5685
6472
  return;
@@ -5745,7 +6532,7 @@ var init_screen = __esm({
5745
6532
  }
5746
6533
  const w = this.term.width;
5747
6534
  const promptRows = this.promptRows();
5748
- const separatorRow = this.term.height - promptRows - BANNER_ROWS;
6535
+ const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
5749
6536
  const queuedRows = this.queuedRows();
5750
6537
  const chipRows = this.chipRows();
5751
6538
  const completionBottom = separatorRow - 1 - queuedRows - chipRows;
@@ -5793,7 +6580,7 @@ var init_screen = __esm({
5793
6580
  }
5794
6581
  const w = this.term.width;
5795
6582
  const promptRows = this.promptRows();
5796
- const separatorRow = this.term.height - promptRows - BANNER_ROWS;
6583
+ const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
5797
6584
  const chipBottom = separatorRow - 1;
5798
6585
  const chipTop = chipBottom - rows + 1;
5799
6586
  const iterm = this.isIterm2();
@@ -5840,7 +6627,7 @@ var init_screen = __esm({
5840
6627
  }
5841
6628
  const w = this.term.width;
5842
6629
  const promptRows = this.promptRows();
5843
- const separatorRow = this.term.height - promptRows - BANNER_ROWS;
6630
+ const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
5844
6631
  const chipRows = this.chipRows();
5845
6632
  const queuedBottom = separatorRow - 1 - chipRows;
5846
6633
  const queuedTop = queuedBottom - rows + 1;
@@ -5882,7 +6669,7 @@ var init_screen = __esm({
5882
6669
  const state = this.dispatcher.state();
5883
6670
  const visualRows = computePromptVisualRows(state.buffer, room);
5884
6671
  const layout = computePromptLayout(visualRows, state, MAX_PROMPT_ROWS);
5885
- const top = this.term.height - layout.rendered - BANNER_ROWS + 1;
6672
+ const top = this.term.height - layout.rendered - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
5886
6673
  for (let i = 0; i < layout.rendered; i++) {
5887
6674
  const vr = visualRows[layout.windowStart + i];
5888
6675
  const row = top + i;
@@ -5918,7 +6705,7 @@ var init_screen = __esm({
5918
6705
  return;
5919
6706
  }
5920
6707
  const w = this.term.width;
5921
- const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
6708
+ const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
5922
6709
  this.paintRow(top, `confirm|q|${w}|${spec.question}`, () => {
5923
6710
  this.term.brightYellow(` ? ${truncate(spec.question, w - 4)}`);
5924
6711
  });
@@ -5933,7 +6720,7 @@ var init_screen = __esm({
5933
6720
  }
5934
6721
  const w = this.term.width;
5935
6722
  const rows = this.permissionRows();
5936
- const top = this.term.height - rows - BANNER_ROWS + 1;
6723
+ const top = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
5937
6724
  let row = top;
5938
6725
  const writeRow = (sig, paint) => {
5939
6726
  if (row >= top + rows) {
@@ -5975,7 +6762,7 @@ var init_screen = __esm({
5975
6762
  });
5976
6763
  }
5977
6764
  drawBanner() {
5978
- const row = this.term.height;
6765
+ const row = this.term.height - SESSIONBAR_ROWS - SEPARATOR_ROWS;
5979
6766
  const w = this.term.width;
5980
6767
  const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
5981
6768
  const right = this.bannerRightContent();
@@ -6024,13 +6811,14 @@ var init_screen = __esm({
6024
6811
  placeCursor() {
6025
6812
  if (this.permissionPrompt) {
6026
6813
  const rows = this.permissionRows();
6027
- const top2 = this.term.height - rows - BANNER_ROWS + 1;
6814
+ const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
6028
6815
  const optionRow = top2 + 3 + this.permissionPrompt.selectedIndex;
6029
- this.term.moveTo(2, Math.min(optionRow, this.term.height - BANNER_ROWS));
6816
+ const lastUsableRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
6817
+ this.term.moveTo(2, Math.min(optionRow, lastUsableRow));
6030
6818
  return;
6031
6819
  }
6032
6820
  if (this.confirmPrompt) {
6033
- const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
6821
+ const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
6034
6822
  this.term.moveTo(2, top2);
6035
6823
  return;
6036
6824
  }
@@ -6044,12 +6832,13 @@ var init_screen = __esm({
6044
6832
  const state = this.dispatcher.state();
6045
6833
  const visualRows = computePromptVisualRows(state.buffer, room);
6046
6834
  const layout = computePromptLayout(visualRows, state, MAX_PROMPT_ROWS);
6047
- const top = this.term.height - layout.rendered - BANNER_ROWS + 1;
6835
+ const top = this.term.height - layout.rendered - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
6048
6836
  const row = top + Math.max(0, layout.cursorVisualRow - layout.windowStart);
6049
6837
  const col = layout.cursorVisualCol + 3;
6838
+ const lastPromptRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
6050
6839
  this.term.moveTo(
6051
6840
  Math.min(col, this.term.width),
6052
- Math.min(row, this.term.height - BANNER_ROWS)
6841
+ Math.min(row, lastPromptRow)
6053
6842
  );
6054
6843
  }
6055
6844
  promptRows() {
@@ -6741,746 +7530,461 @@ var init_input = __esm({
6741
7530
  };
6742
7531
  return [];
6743
7532
  }
6744
- const matchIndices = this.findHistoryMatches(query);
6745
- if (matchIndices.length === 0) {
6746
- return [{ type: "escalate-search", query }];
6747
- }
6748
- this.historySearch = {
6749
- query,
6750
- matchIndices,
6751
- cursor: 0,
6752
- savedDraft: {
6753
- buffer: [...this.buffer],
6754
- row: this.row,
6755
- col: this.col
6756
- }
6757
- };
6758
- this.loadEntry(this.history[matchIndices[0]] ?? "");
6759
- return [];
6760
- }
6761
- // ^R advance. At the oldest match with a non-empty query, falls
6762
- // through to scrollback search (same escalate path as a never-
6763
- // matched startHistorySearch). With an empty query at the oldest
6764
- // match (i.e. the user walked all history with no filter), advance
6765
- // is a no-op so the buffer stays on the oldest entry.
6766
- advanceHistorySearch() {
6767
- if (this.historySearch === null) {
6768
- return [];
6769
- }
6770
- const search = this.historySearch;
6771
- const atOldest = search.cursor >= search.matchIndices.length - 1;
6772
- if (atOldest) {
6773
- if (search.query.length === 0) {
6774
- return [];
6775
- }
6776
- const query = search.query;
6777
- const draft = search.savedDraft;
6778
- this.historySearch = null;
6779
- this.buffer = [...draft.buffer];
6780
- this.row = draft.row;
6781
- this.col = draft.col;
6782
- return [{ type: "escalate-search", query }];
6783
- }
6784
- search.cursor += 1;
6785
- const idx = search.matchIndices[search.cursor];
6786
- this.loadEntry(this.history[idx] ?? "");
6787
- return [];
6788
- }
6789
- // ^S retreat — walk toward newer matches. No-op at the newest match
6790
- // (no wrap, mirroring ^R no-wrap at the oldest).
6791
- retreatHistorySearch() {
6792
- if (this.historySearch === null) {
6793
- return;
6794
- }
6795
- if (this.historySearch.cursor === 0) {
6796
- return;
6797
- }
6798
- this.historySearch.cursor -= 1;
6799
- const idx = this.historySearch.matchIndices[this.historySearch.cursor];
6800
- this.loadEntry(this.history[idx] ?? "");
6801
- }
6802
- // Backspace / typing within search mode mutates the query and
6803
- // re-searches. When the new query is empty, restore the saved
6804
- // draft buffer (typically empty) and stay in search mode — the
6805
- // user can keep typing. When the new query has matches, load the
6806
- // top one. When the new query has no matches, escalate to scrollback
6807
- // search so the typed term applies there instead.
6808
- mutateHistorySearchQuery(newQuery) {
6809
- if (this.historySearch === null) {
6810
- return [];
6811
- }
6812
- if (newQuery.length === 0) {
6813
- this.historySearch.query = "";
6814
- this.historySearch.matchIndices = [];
6815
- this.historySearch.cursor = 0;
6816
- const draft = this.historySearch.savedDraft;
6817
- this.buffer = [...draft.buffer];
6818
- this.row = draft.row;
6819
- this.col = draft.col;
6820
- return [];
6821
- }
6822
- const matchIndices = this.findHistoryMatches(newQuery);
6823
- if (matchIndices.length === 0) {
6824
- const draft = this.historySearch.savedDraft;
6825
- this.historySearch = null;
6826
- this.buffer = [...draft.buffer];
6827
- this.row = draft.row;
6828
- this.col = draft.col;
6829
- return [{ type: "escalate-search", query: newQuery }];
6830
- }
6831
- this.historySearch.query = newQuery;
6832
- this.historySearch.matchIndices = matchIndices;
6833
- this.historySearch.cursor = 0;
6834
- this.loadEntry(this.history[matchIndices[0]] ?? "");
6835
- return [];
6836
- }
6837
- findHistoryMatches(query) {
6838
- const out = [];
6839
- for (let i = this.history.length - 1; i >= 0; i--) {
6840
- const entry = this.history[i] ?? "";
6841
- if (query.length === 0 || entry.toLowerCase().includes(query)) {
6842
- out.push(i);
6843
- }
6844
- }
6845
- return out;
6846
- }
6847
- cancelHistorySearch() {
6848
- if (this.historySearch === null) {
6849
- return;
6850
- }
6851
- const draft = this.historySearch.savedDraft;
6852
- this.historySearch = null;
6853
- this.buffer = [...draft.buffer];
6854
- this.row = draft.row;
6855
- this.col = draft.col;
6856
- }
6857
- loadEntry(text) {
6858
- this.buffer = text.split("\n");
6859
- if (this.buffer.length === 0) {
6860
- this.buffer = [""];
6861
- }
6862
- this.row = this.buffer.length - 1;
6863
- this.col = (this.buffer[this.row] ?? "").length;
6864
- }
6865
- send() {
6866
- const text = this.bufferText();
6867
- if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
6868
- const index = this.queueIndex;
6869
- const attachments2 = [...this.attachments];
6870
- this.clearBuffer();
6871
- if (text.trim().length === 0) {
6872
- return [{ type: "queue-remove", index }];
6873
- }
6874
- return [{ type: "queue-edit", index, text, attachments: attachments2 }];
6875
- }
6876
- if (text.trim().length === 0 && this.attachments.length === 0) {
6877
- return [];
6878
- }
6879
- const planMode = this.planMode;
6880
- const attachments = [...this.attachments];
6881
- this.clearBuffer();
6882
- return [{ type: "send", text, planMode, attachments }];
6883
- }
6884
- // Home: jump to the very start of the prompt buffer. If we're already
6885
- // there, fall through to scrolling the scrollback to its top.
6886
- handleHome() {
6887
- if (this.row !== 0 || this.col !== 0) {
6888
- this.row = 0;
6889
- this.col = 0;
6890
- return [];
7533
+ const matchIndices = this.findHistoryMatches(query);
7534
+ if (matchIndices.length === 0) {
7535
+ return [{ type: "escalate-search", query }];
6891
7536
  }
6892
- return [{ type: "scroll-to-top" }];
7537
+ this.historySearch = {
7538
+ query,
7539
+ matchIndices,
7540
+ cursor: 0,
7541
+ savedDraft: {
7542
+ buffer: [...this.buffer],
7543
+ row: this.row,
7544
+ col: this.col
7545
+ }
7546
+ };
7547
+ this.loadEntry(this.history[matchIndices[0]] ?? "");
7548
+ return [];
6893
7549
  }
6894
- // End: jump to the end of the last line of the prompt buffer. Already
6895
- // there scroll the scrollback to the bottom (newest).
6896
- handleEnd() {
6897
- const lastRow = this.buffer.length - 1;
6898
- const lastCol = (this.buffer[lastRow] ?? "").length;
6899
- if (this.row !== lastRow || this.col !== lastCol) {
6900
- this.row = lastRow;
6901
- this.col = lastCol;
7550
+ // ^R advance. At the oldest match with a non-empty query, falls
7551
+ // through to scrollback search (same escalate path as a never-
7552
+ // matched startHistorySearch). With an empty query at the oldest
7553
+ // match (i.e. the user walked all history with no filter), advance
7554
+ // is a no-op so the buffer stays on the oldest entry.
7555
+ advanceHistorySearch() {
7556
+ if (this.historySearch === null) {
6902
7557
  return [];
6903
7558
  }
6904
- return [{ type: "scroll-to-bottom" }];
6905
- }
6906
- handleCtrlC() {
6907
- if (this.queueIndex >= 0) {
6908
- const index = this.queueIndex;
6909
- this.queueIndex = -1;
6910
- this.restoreDraft();
6911
- return [{ type: "queue-remove", index }];
7559
+ const search = this.historySearch;
7560
+ const atOldest = search.cursor >= search.matchIndices.length - 1;
7561
+ if (atOldest) {
7562
+ if (search.query.length === 0) {
7563
+ return [];
7564
+ }
7565
+ const query = search.query;
7566
+ const draft = search.savedDraft;
7567
+ this.historySearch = null;
7568
+ this.buffer = [...draft.buffer];
7569
+ this.row = draft.row;
7570
+ this.col = draft.col;
7571
+ return [{ type: "escalate-search", query }];
6912
7572
  }
6913
- if (!this.bufferIsEmpty() || this.attachments.length > 0) {
6914
- this.buffer = [""];
6915
- this.row = 0;
6916
- this.col = 0;
6917
- this.attachments = [];
6918
- this.historyIndex = -1;
6919
- this.savedDraft = null;
6920
- this.savedAttachments = null;
6921
- return [];
7573
+ search.cursor += 1;
7574
+ const idx = search.matchIndices[search.cursor];
7575
+ this.loadEntry(this.history[idx] ?? "");
7576
+ return [];
7577
+ }
7578
+ // ^S retreat — walk toward newer matches. No-op at the newest match
7579
+ // (no wrap, mirroring ^R no-wrap at the oldest).
7580
+ retreatHistorySearch() {
7581
+ if (this.historySearch === null) {
7582
+ return;
6922
7583
  }
6923
- if (this.turnRunning) {
6924
- return [{ type: "cancel" }];
7584
+ if (this.historySearch.cursor === 0) {
7585
+ return;
6925
7586
  }
6926
- return [{ type: "exit" }];
6927
- }
6928
- };
6929
- }
6930
- });
6931
-
6932
- // src/tui/clipboard.ts
6933
- import { spawn as nodeSpawn } from "child_process";
6934
- import fs14 from "fs/promises";
6935
- import os4 from "os";
6936
- import path10 from "path";
6937
- async function readClipboard(envIn = {}) {
6938
- const env = { ...defaultEnv, ...envIn };
6939
- if (env.platform === "darwin") {
6940
- return readMacOS(env);
6941
- }
6942
- if (env.platform === "linux") {
6943
- return readLinux(env);
6944
- }
6945
- return {
6946
- ok: false,
6947
- reason: `clipboard paste is not supported on ${env.platform}`
6948
- };
6949
- }
6950
- async function readMacOS(env) {
6951
- const tmpPath = path10.join(
6952
- env.tmpdir(),
6953
- `hydra-clipboard-${Date.now()}-${process.pid}.png`
6954
- );
6955
- const script = [
6956
- "set png_data to the clipboard as \xABclass PNGf\xBB",
6957
- `set out_file to (open for access (POSIX file "${tmpPath}") with write permission)`,
6958
- "write png_data to out_file",
6959
- "close access out_file"
6960
- ];
6961
- const args = [];
6962
- for (const line of script) {
6963
- args.push("-e", line);
6964
- }
6965
- try {
6966
- await run2(env.spawn, "osascript", args);
6967
- const img = await readFileAsAttachment(tmpPath, true);
6968
- if (img.ok) {
6969
- return img;
6970
- }
6971
- if (img.reason.startsWith("clipboard image is")) {
6972
- return img;
6973
- }
6974
- } catch {
6975
- await fs14.unlink(tmpPath).catch(() => void 0);
6976
- }
6977
- try {
6978
- const buf = await runCapture(env.spawn, "pbpaste", []);
6979
- if (buf.length === 0) {
6980
- return { ok: false, reason: "clipboard is empty" };
6981
- }
6982
- return { ok: true, kind: "text", text: normalizeText(buf.toString("utf-8")) };
6983
- } catch {
6984
- return { ok: false, reason: "clipboard read failed" };
6985
- }
6986
- }
6987
- async function readLinux(env) {
6988
- const tool = await detectLinuxTool(env);
6989
- if (!tool) {
6990
- return {
6991
- ok: false,
6992
- reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
6993
- };
6994
- }
6995
- try {
6996
- const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
6997
- if (buf.length > 0) {
6998
- if (buf.length > MAX_ATTACHMENT_BYTES) {
6999
- return {
7000
- ok: false,
7001
- reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
7002
- };
7587
+ this.historySearch.cursor -= 1;
7588
+ const idx = this.historySearch.matchIndices[this.historySearch.cursor];
7589
+ this.loadEntry(this.history[idx] ?? "");
7003
7590
  }
7004
- return {
7005
- ok: true,
7006
- kind: "image",
7007
- attachment: {
7008
- mimeType: "image/png",
7009
- data: buf.toString("base64"),
7010
- sizeBytes: buf.length
7591
+ // Backspace / typing within search mode mutates the query and
7592
+ // re-searches. When the new query is empty, restore the saved
7593
+ // draft buffer (typically empty) and stay in search mode — the
7594
+ // user can keep typing. When the new query has matches, load the
7595
+ // top one. When the new query has no matches, escalate to scrollback
7596
+ // search so the typed term applies there instead.
7597
+ mutateHistorySearchQuery(newQuery) {
7598
+ if (this.historySearch === null) {
7599
+ return [];
7011
7600
  }
7012
- };
7013
- }
7014
- } catch {
7015
- }
7016
- try {
7017
- const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
7018
- if (buf.length === 0) {
7019
- return { ok: false, reason: "clipboard is empty" };
7020
- }
7021
- return {
7022
- ok: true,
7023
- kind: "text",
7024
- text: normalizeText(buf.toString("utf-8"))
7025
- };
7026
- } catch {
7027
- return { ok: false, reason: "clipboard read failed" };
7028
- }
7029
- }
7030
- async function detectLinuxTool(env) {
7031
- if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
7032
- return {
7033
- cmd: "wl-paste",
7034
- imageArgs: ["-t", "image/png"],
7035
- // -n: drop trailing newline wl-paste adds by default. We further
7036
- // normalize line endings below, but this avoids a spurious
7037
- // empty trailing row from a single-line clipboard text.
7038
- textArgs: ["-n"]
7039
- };
7040
- }
7041
- if (env.env.DISPLAY && await which(env, "xclip")) {
7042
- return {
7043
- cmd: "xclip",
7044
- imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
7045
- textArgs: ["-selection", "clipboard", "-o"]
7046
- };
7047
- }
7048
- return null;
7049
- }
7050
- function normalizeText(text) {
7051
- return text.replace(/\r\n?/g, "\n");
7052
- }
7053
- async function which(env, cmd) {
7054
- try {
7055
- await run2(env.spawn, "which", [cmd]);
7056
- return true;
7057
- } catch {
7058
- return false;
7059
- }
7060
- }
7061
- async function readFileAsAttachment(p, unlinkAfter) {
7062
- try {
7063
- const buf = await fs14.readFile(p);
7064
- if (unlinkAfter) {
7065
- await fs14.unlink(p).catch(() => void 0);
7066
- }
7067
- if (buf.length === 0) {
7068
- return { ok: false, reason: "no image on clipboard" };
7069
- }
7070
- if (buf.length > MAX_ATTACHMENT_BYTES) {
7071
- return {
7072
- ok: false,
7073
- reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
7074
- };
7075
- }
7076
- const mimeType = mimeFromExtension(p) ?? "image/png";
7077
- return {
7078
- ok: true,
7079
- kind: "image",
7080
- attachment: {
7081
- mimeType,
7082
- data: buf.toString("base64"),
7083
- sizeBytes: buf.length
7601
+ if (newQuery.length === 0) {
7602
+ this.historySearch.query = "";
7603
+ this.historySearch.matchIndices = [];
7604
+ this.historySearch.cursor = 0;
7605
+ const draft = this.historySearch.savedDraft;
7606
+ this.buffer = [...draft.buffer];
7607
+ this.row = draft.row;
7608
+ this.col = draft.col;
7609
+ return [];
7610
+ }
7611
+ const matchIndices = this.findHistoryMatches(newQuery);
7612
+ if (matchIndices.length === 0) {
7613
+ const draft = this.historySearch.savedDraft;
7614
+ this.historySearch = null;
7615
+ this.buffer = [...draft.buffer];
7616
+ this.row = draft.row;
7617
+ this.col = draft.col;
7618
+ return [{ type: "escalate-search", query: newQuery }];
7619
+ }
7620
+ this.historySearch.query = newQuery;
7621
+ this.historySearch.matchIndices = matchIndices;
7622
+ this.historySearch.cursor = 0;
7623
+ this.loadEntry(this.history[matchIndices[0]] ?? "");
7624
+ return [];
7084
7625
  }
7085
- };
7086
- } catch {
7087
- return { ok: false, reason: "failed to read clipboard image" };
7088
- }
7089
- }
7090
- function run2(spawn6, cmd, args) {
7091
- return new Promise((resolve5, reject) => {
7092
- const proc = spawn6(cmd, args);
7093
- proc.stdout?.on("data", () => void 0);
7094
- proc.stderr?.on("data", () => void 0);
7095
- proc.on("error", reject);
7096
- proc.on("close", (code) => {
7097
- if (code === 0) {
7098
- resolve5();
7099
- } else {
7100
- reject(new Error(`${cmd} exited ${code}`));
7626
+ findHistoryMatches(query) {
7627
+ const out = [];
7628
+ for (let i = this.history.length - 1; i >= 0; i--) {
7629
+ const entry = this.history[i] ?? "";
7630
+ if (query.length === 0 || entry.toLowerCase().includes(query)) {
7631
+ out.push(i);
7632
+ }
7633
+ }
7634
+ return out;
7101
7635
  }
7102
- });
7103
- });
7104
- }
7105
- function runCapture(spawn6, cmd, args) {
7106
- return new Promise((resolve5, reject) => {
7107
- const proc = spawn6(cmd, args);
7108
- const chunks = [];
7109
- let stdoutEnded = proc.stdout === null;
7110
- let closedCode = null;
7111
- let settled = false;
7112
- const settle = () => {
7113
- if (settled || !stdoutEnded || closedCode === null) {
7114
- return;
7636
+ cancelHistorySearch() {
7637
+ if (this.historySearch === null) {
7638
+ return;
7639
+ }
7640
+ const draft = this.historySearch.savedDraft;
7641
+ this.historySearch = null;
7642
+ this.buffer = [...draft.buffer];
7643
+ this.row = draft.row;
7644
+ this.col = draft.col;
7115
7645
  }
7116
- settled = true;
7117
- if (closedCode === 0) {
7118
- resolve5(Buffer.concat(chunks));
7119
- } else {
7120
- reject(new Error(`${cmd} exited ${closedCode}`));
7646
+ loadEntry(text) {
7647
+ this.buffer = text.split("\n");
7648
+ if (this.buffer.length === 0) {
7649
+ this.buffer = [""];
7650
+ }
7651
+ this.row = this.buffer.length - 1;
7652
+ this.col = (this.buffer[this.row] ?? "").length;
7121
7653
  }
7122
- };
7123
- proc.stdout?.on("data", (chunk) => {
7124
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
7125
- });
7126
- proc.stdout?.on("end", () => {
7127
- stdoutEnded = true;
7128
- settle();
7129
- });
7130
- proc.stderr?.on("data", () => void 0);
7131
- proc.on("error", (err) => {
7132
- if (settled) {
7133
- return;
7654
+ send() {
7655
+ const text = this.bufferText();
7656
+ if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
7657
+ const index = this.queueIndex;
7658
+ const attachments2 = [...this.attachments];
7659
+ this.clearBuffer();
7660
+ if (text.trim().length === 0) {
7661
+ return [{ type: "queue-remove", index }];
7662
+ }
7663
+ return [{ type: "queue-edit", index, text, attachments: attachments2 }];
7664
+ }
7665
+ if (text.trim().length === 0 && this.attachments.length === 0) {
7666
+ return [];
7667
+ }
7668
+ const planMode = this.planMode;
7669
+ const attachments = [...this.attachments];
7670
+ this.clearBuffer();
7671
+ return [{ type: "send", text, planMode, attachments }];
7134
7672
  }
7135
- settled = true;
7136
- reject(err);
7137
- });
7138
- proc.on("close", (code) => {
7139
- closedCode = code ?? 0;
7140
- settle();
7141
- });
7142
- });
7143
- }
7144
- var defaultEnv;
7145
- var init_clipboard = __esm({
7146
- "src/tui/clipboard.ts"() {
7147
- "use strict";
7148
- init_attachments();
7149
- defaultEnv = {
7150
- platform: process.platform,
7151
- env: process.env,
7152
- spawn: nodeSpawn,
7153
- tmpdir: os4.tmpdir
7154
- };
7155
- }
7156
- });
7157
-
7158
- // src/tui/completion.ts
7159
- function longestCommonPrefix(names) {
7160
- if (names.length === 0) {
7161
- return "";
7162
- }
7163
- let prefix = names[0] ?? "";
7164
- for (let i = 1; i < names.length; i++) {
7165
- const n = names[i] ?? "";
7166
- let j = 0;
7167
- while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
7168
- j += 1;
7169
- }
7170
- prefix = prefix.slice(0, j);
7171
- if (prefix.length === 0) {
7172
- break;
7173
- }
7174
- }
7175
- return prefix;
7176
- }
7177
- function computeTabCompletion(args) {
7178
- const { matches, firstLine: firstLine3 } = args;
7179
- if (matches.length === 0) {
7180
- return null;
7181
- }
7182
- const space = firstLine3.indexOf(" ");
7183
- const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
7184
- const tail = space === -1 ? "" : firstLine3.slice(space);
7185
- if (matches.length === 1) {
7186
- const name = matches[0] ?? "";
7187
- const suffix = tail.startsWith(" ") ? "" : " ";
7188
- return name + suffix + tail;
7189
- }
7190
- const commonPrefix = longestCommonPrefix(matches);
7191
- if (commonPrefix.length <= typedPrefix.length) {
7192
- return null;
7193
- }
7194
- return commonPrefix + tail;
7195
- }
7196
- var init_completion = __esm({
7197
- "src/tui/completion.ts"() {
7198
- "use strict";
7199
- }
7200
- });
7201
-
7202
- // src/tui/render-update.ts
7203
- import stripAnsi from "strip-ansi";
7204
- function sanitizeWireText(text) {
7205
- return stripAnsi(text).replace(STRIP_CONTROLS, "");
7206
- }
7207
- function sanitizeSingleLine(text) {
7208
- return sanitizeWireText(text).replace(/[\n\t]+/g, " ").replace(/ +/g, " ").trim();
7209
- }
7210
- function mapUpdate(update) {
7211
- if (!update || typeof update !== "object") {
7212
- return null;
7213
- }
7214
- const u = update;
7215
- const tag = u.sessionUpdate ?? u.kind;
7216
- if (typeof tag !== "string") {
7217
- return null;
7218
- }
7219
- switch (tag) {
7220
- case "agent_message_chunk":
7221
- return mapAgentText(u);
7222
- case "agent_thought_chunk":
7223
- case "agent_thought":
7224
- return mapAgentThought(u);
7225
- case "user_message_chunk":
7226
- return mapUserText(u);
7227
- case "prompt_received":
7228
- return mapPromptReceived(u);
7229
- case "tool_call":
7230
- return mapToolCall(u);
7231
- case "tool_call_update":
7232
- return mapToolCallUpdate(u);
7233
- case "plan":
7234
- return mapPlan(u);
7235
- case "current_mode_update":
7236
- return mapMode(u);
7237
- case "current_model_update":
7238
- return mapModel(u);
7239
- case "turn_complete":
7240
- return mapTurnComplete(u);
7241
- case "usage_update":
7242
- return mapUsage(u);
7243
- case "available_commands_update":
7244
- return mapAvailableCommands(u);
7245
- case "session_info_update":
7246
- return mapSessionInfo(u);
7247
- default:
7248
- return { kind: "unknown", sessionUpdate: tag, raw: update };
7249
- }
7250
- }
7251
- function mapSessionInfo(u) {
7252
- const rawTitle = readString(u, "title");
7253
- const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
7254
- const meta = u._meta;
7255
- let agentId;
7256
- if (meta && typeof meta === "object" && !Array.isArray(meta)) {
7257
- const ns = meta["hydra-acp"];
7258
- if (ns && typeof ns === "object" && !Array.isArray(ns)) {
7259
- const candidate = ns.agentId;
7260
- if (typeof candidate === "string") {
7261
- agentId = candidate;
7673
+ // Home: jump to the very start of the prompt buffer. If we're already
7674
+ // there, fall through to scrolling the scrollback to its top.
7675
+ handleHome() {
7676
+ if (this.row !== 0 || this.col !== 0) {
7677
+ this.row = 0;
7678
+ this.col = 0;
7679
+ return [];
7680
+ }
7681
+ return [{ type: "scroll-to-top" }];
7682
+ }
7683
+ // End: jump to the end of the last line of the prompt buffer. Already
7684
+ // there → scroll the scrollback to the bottom (newest).
7685
+ handleEnd() {
7686
+ const lastRow = this.buffer.length - 1;
7687
+ const lastCol = (this.buffer[lastRow] ?? "").length;
7688
+ if (this.row !== lastRow || this.col !== lastCol) {
7689
+ this.row = lastRow;
7690
+ this.col = lastCol;
7691
+ return [];
7692
+ }
7693
+ return [{ type: "scroll-to-bottom" }];
7262
7694
  }
7263
- }
7264
- }
7265
- if (title === void 0 && agentId === void 0) {
7266
- return null;
7695
+ handleCtrlC() {
7696
+ if (this.queueIndex >= 0) {
7697
+ const index = this.queueIndex;
7698
+ this.queueIndex = -1;
7699
+ this.restoreDraft();
7700
+ return [{ type: "queue-remove", index }];
7701
+ }
7702
+ if (!this.bufferIsEmpty() || this.attachments.length > 0) {
7703
+ this.buffer = [""];
7704
+ this.row = 0;
7705
+ this.col = 0;
7706
+ this.attachments = [];
7707
+ this.historyIndex = -1;
7708
+ this.savedDraft = null;
7709
+ this.savedAttachments = null;
7710
+ return [];
7711
+ }
7712
+ if (this.turnRunning) {
7713
+ return [{ type: "cancel" }];
7714
+ }
7715
+ return [{ type: "exit" }];
7716
+ }
7717
+ };
7267
7718
  }
7268
- const event = { kind: "session-info" };
7269
- if (title !== void 0) {
7270
- event.title = title;
7719
+ });
7720
+
7721
+ // src/tui/clipboard.ts
7722
+ import { spawn as nodeSpawn } from "child_process";
7723
+ import fs14 from "fs/promises";
7724
+ import os4 from "os";
7725
+ import path10 from "path";
7726
+ async function readClipboard(envIn = {}) {
7727
+ const env = { ...defaultEnv, ...envIn };
7728
+ if (env.platform === "darwin") {
7729
+ return readMacOS(env);
7271
7730
  }
7272
- if (agentId !== void 0) {
7273
- event.agentId = agentId;
7731
+ if (env.platform === "linux") {
7732
+ return readLinux(env);
7274
7733
  }
7275
- return event;
7734
+ return {
7735
+ ok: false,
7736
+ reason: `clipboard paste is not supported on ${env.platform}`
7737
+ };
7276
7738
  }
7277
- function normalizeAdvertisedCommands(list) {
7278
- if (!Array.isArray(list)) {
7279
- return [];
7739
+ async function readMacOS(env) {
7740
+ const tmpPath = path10.join(
7741
+ env.tmpdir(),
7742
+ `hydra-clipboard-${Date.now()}-${process.pid}.png`
7743
+ );
7744
+ const script = [
7745
+ "set png_data to the clipboard as \xABclass PNGf\xBB",
7746
+ `set out_file to (open for access (POSIX file "${tmpPath}") with write permission)`,
7747
+ "write png_data to out_file",
7748
+ "close access out_file"
7749
+ ];
7750
+ const args = [];
7751
+ for (const line of script) {
7752
+ args.push("-e", line);
7280
7753
  }
7281
- const out = [];
7282
- for (const raw of list) {
7283
- if (!raw || typeof raw !== "object") {
7284
- continue;
7285
- }
7286
- const c = raw;
7287
- if (typeof c.name !== "string" || c.name.length === 0) {
7288
- continue;
7754
+ try {
7755
+ await run2(env.spawn, "osascript", args);
7756
+ const img = await readFileAsAttachment(tmpPath, true);
7757
+ if (img.ok) {
7758
+ return img;
7289
7759
  }
7290
- const rawName = c.name.startsWith("/") ? c.name : `/${c.name}`;
7291
- const cmd = { name: sanitizeSingleLine(rawName) };
7292
- if (typeof c.description === "string") {
7293
- cmd.description = sanitizeSingleLine(c.description);
7760
+ if (img.reason.startsWith("clipboard image is")) {
7761
+ return img;
7294
7762
  }
7295
- out.push(cmd);
7296
- }
7297
- return out;
7298
- }
7299
- function mapAvailableCommands(u) {
7300
- const list = u.availableCommands ?? u.commands;
7301
- if (!Array.isArray(list)) {
7302
- return null;
7303
- }
7304
- return { kind: "available-commands", commands: normalizeAdvertisedCommands(list) };
7305
- }
7306
- function mapUsage(u) {
7307
- const event = { kind: "usage-update" };
7308
- if (typeof u.used === "number") {
7309
- event.used = u.used;
7310
- }
7311
- if (typeof u.size === "number") {
7312
- event.size = u.size;
7763
+ } catch {
7764
+ await fs14.unlink(tmpPath).catch(() => void 0);
7313
7765
  }
7314
- if (u.cost && typeof u.cost === "object") {
7315
- const cost = u.cost;
7316
- if (typeof cost.amount === "number") {
7317
- event.costAmount = cost.amount;
7318
- }
7319
- if (typeof cost.currency === "string") {
7320
- event.costCurrency = cost.currency;
7766
+ try {
7767
+ const buf = await runCapture(env.spawn, "pbpaste", []);
7768
+ if (buf.length === 0) {
7769
+ return { ok: false, reason: "clipboard is empty" };
7321
7770
  }
7771
+ return { ok: true, kind: "text", text: normalizeText(buf.toString("utf-8")) };
7772
+ } catch {
7773
+ return { ok: false, reason: "clipboard read failed" };
7322
7774
  }
7323
- return event;
7324
- }
7325
- function mapAgentText(u) {
7326
- const text = extractContentText(u.content);
7327
- if (text === null) {
7328
- return null;
7329
- }
7330
- return { kind: "agent-text", text };
7331
7775
  }
7332
- function mapAgentThought(u) {
7333
- const text = typeof u.text === "string" ? sanitizeWireText(u.text) : extractContentText(u.content);
7334
- if (text === null) {
7335
- return null;
7776
+ async function readLinux(env) {
7777
+ const tool = await detectLinuxTool(env);
7778
+ if (!tool) {
7779
+ return {
7780
+ ok: false,
7781
+ reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
7782
+ };
7336
7783
  }
7337
- return { kind: "agent-thought", text };
7338
- }
7339
- function mapUserText(u) {
7340
- const meta = u._meta;
7341
- if (meta && typeof meta === "object" && !Array.isArray(meta)) {
7342
- const hydra = meta["hydra-acp"];
7343
- if (hydra && typeof hydra === "object" && !Array.isArray(hydra) && hydra.compatFor === "prompt_received") {
7344
- return null;
7784
+ try {
7785
+ const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
7786
+ if (buf.length > 0) {
7787
+ if (buf.length > MAX_ATTACHMENT_BYTES) {
7788
+ return {
7789
+ ok: false,
7790
+ reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
7791
+ };
7792
+ }
7793
+ return {
7794
+ ok: true,
7795
+ kind: "image",
7796
+ attachment: {
7797
+ mimeType: "image/png",
7798
+ data: buf.toString("base64"),
7799
+ sizeBytes: buf.length
7800
+ }
7801
+ };
7345
7802
  }
7803
+ } catch {
7346
7804
  }
7347
- const text = extractContentText(u.content);
7348
- if (text === null) {
7349
- return null;
7350
- }
7351
- return { kind: "user-text", text };
7352
- }
7353
- function mapPromptReceived(u) {
7354
- const promptText = extractPromptText2(u.prompt);
7355
- if (promptText === null) {
7356
- return null;
7805
+ try {
7806
+ const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
7807
+ if (buf.length === 0) {
7808
+ return { ok: false, reason: "clipboard is empty" };
7809
+ }
7810
+ return {
7811
+ ok: true,
7812
+ kind: "text",
7813
+ text: normalizeText(buf.toString("utf-8"))
7814
+ };
7815
+ } catch {
7816
+ return { ok: false, reason: "clipboard read failed" };
7357
7817
  }
7358
- return { kind: "user-text", text: promptText };
7359
7818
  }
7360
- function mapToolCall(u) {
7361
- const toolCallId = readString(u, "toolCallId") ?? readString(u, "id");
7362
- if (!toolCallId) {
7363
- return null;
7364
- }
7365
- const rawTitle = readString(u, "title") ?? readString(u, "name") ?? readString(u, "label") ?? "tool call";
7366
- const title = sanitizeSingleLine(rawTitle);
7367
- const status = readString(u, "status");
7368
- const rawKind = readString(u, "kind");
7369
- const event = { kind: "tool-call", toolCallId, title };
7370
- if (status !== void 0) {
7371
- event.status = status;
7819
+ async function detectLinuxTool(env) {
7820
+ if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
7821
+ return {
7822
+ cmd: "wl-paste",
7823
+ imageArgs: ["-t", "image/png"],
7824
+ // -n: drop trailing newline wl-paste adds by default. We further
7825
+ // normalize line endings below, but this avoids a spurious
7826
+ // empty trailing row from a single-line clipboard text.
7827
+ textArgs: ["-n"]
7828
+ };
7372
7829
  }
7373
- if (rawKind !== void 0) {
7374
- event.rawKind = rawKind;
7830
+ if (env.env.DISPLAY && await which(env, "xclip")) {
7831
+ return {
7832
+ cmd: "xclip",
7833
+ imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
7834
+ textArgs: ["-selection", "clipboard", "-o"]
7835
+ };
7375
7836
  }
7376
- return event;
7837
+ return null;
7377
7838
  }
7378
- function mapToolCallUpdate(u) {
7379
- const toolCallId = readString(u, "toolCallId") ?? readString(u, "id");
7380
- if (!toolCallId) {
7381
- return null;
7382
- }
7383
- const rawTitle = readString(u, "title");
7384
- const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
7385
- const status = readString(u, "status");
7386
- const meaningful = title !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
7387
- if (!meaningful) {
7388
- return null;
7389
- }
7390
- const event = { kind: "tool-call-update", toolCallId };
7391
- if (title !== void 0) {
7392
- event.title = title;
7393
- }
7394
- if (status !== void 0) {
7395
- event.status = status;
7396
- }
7397
- return event;
7839
+ function normalizeText(text) {
7840
+ return text.replace(/\r\n?/g, "\n");
7398
7841
  }
7399
- function mapPlan(u) {
7400
- const entries = u.entries;
7401
- if (!Array.isArray(entries)) {
7402
- return null;
7842
+ async function which(env, cmd) {
7843
+ try {
7844
+ await run2(env.spawn, "which", [cmd]);
7845
+ return true;
7846
+ } catch {
7847
+ return false;
7403
7848
  }
7404
- const normalized = [];
7405
- for (const raw of entries) {
7406
- if (!raw || typeof raw !== "object") {
7407
- continue;
7408
- }
7409
- const e = raw;
7410
- const content = typeof e.content === "string" ? sanitizeSingleLine(e.content) : void 0;
7411
- if (!content) {
7412
- continue;
7849
+ }
7850
+ async function readFileAsAttachment(p, unlinkAfter) {
7851
+ try {
7852
+ const buf = await fs14.readFile(p);
7853
+ if (unlinkAfter) {
7854
+ await fs14.unlink(p).catch(() => void 0);
7413
7855
  }
7414
- const entry = { content };
7415
- if (typeof e.status === "string") {
7416
- entry.status = e.status;
7856
+ if (buf.length === 0) {
7857
+ return { ok: false, reason: "no image on clipboard" };
7417
7858
  }
7418
- if (typeof e.priority === "string") {
7419
- entry.priority = e.priority;
7859
+ if (buf.length > MAX_ATTACHMENT_BYTES) {
7860
+ return {
7861
+ ok: false,
7862
+ reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
7863
+ };
7420
7864
  }
7421
- normalized.push(entry);
7422
- }
7423
- return { kind: "plan", entries: normalized };
7424
- }
7425
- function mapMode(u) {
7426
- const mode = readString(u, "currentMode") ?? readString(u, "mode");
7427
- if (!mode) {
7428
- return null;
7865
+ const mimeType = mimeFromExtension(p) ?? "image/png";
7866
+ return {
7867
+ ok: true,
7868
+ kind: "image",
7869
+ attachment: {
7870
+ mimeType,
7871
+ data: buf.toString("base64"),
7872
+ sizeBytes: buf.length
7873
+ }
7874
+ };
7875
+ } catch {
7876
+ return { ok: false, reason: "failed to read clipboard image" };
7429
7877
  }
7430
- return { kind: "mode-changed", mode: sanitizeSingleLine(mode) };
7431
7878
  }
7432
- function mapModel(u) {
7433
- const model = readString(u, "currentModel") ?? readString(u, "model");
7434
- if (!model) {
7435
- return null;
7436
- }
7437
- return { kind: "model-changed", model: sanitizeSingleLine(model) };
7879
+ function run2(spawn6, cmd, args) {
7880
+ return new Promise((resolve5, reject) => {
7881
+ const proc = spawn6(cmd, args);
7882
+ proc.stdout?.on("data", () => void 0);
7883
+ proc.stderr?.on("data", () => void 0);
7884
+ proc.on("error", reject);
7885
+ proc.on("close", (code) => {
7886
+ if (code === 0) {
7887
+ resolve5();
7888
+ } else {
7889
+ reject(new Error(`${cmd} exited ${code}`));
7890
+ }
7891
+ });
7892
+ });
7438
7893
  }
7439
- function mapTurnComplete(u) {
7440
- const stopReason = readString(u, "stopReason");
7441
- return stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" };
7894
+ function runCapture(spawn6, cmd, args) {
7895
+ return new Promise((resolve5, reject) => {
7896
+ const proc = spawn6(cmd, args);
7897
+ const chunks = [];
7898
+ let stdoutEnded = proc.stdout === null;
7899
+ let closedCode = null;
7900
+ let settled = false;
7901
+ const settle = () => {
7902
+ if (settled || !stdoutEnded || closedCode === null) {
7903
+ return;
7904
+ }
7905
+ settled = true;
7906
+ if (closedCode === 0) {
7907
+ resolve5(Buffer.concat(chunks));
7908
+ } else {
7909
+ reject(new Error(`${cmd} exited ${closedCode}`));
7910
+ }
7911
+ };
7912
+ proc.stdout?.on("data", (chunk) => {
7913
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
7914
+ });
7915
+ proc.stdout?.on("end", () => {
7916
+ stdoutEnded = true;
7917
+ settle();
7918
+ });
7919
+ proc.stderr?.on("data", () => void 0);
7920
+ proc.on("error", (err) => {
7921
+ if (settled) {
7922
+ return;
7923
+ }
7924
+ settled = true;
7925
+ reject(err);
7926
+ });
7927
+ proc.on("close", (code) => {
7928
+ closedCode = code ?? 0;
7929
+ settle();
7930
+ });
7931
+ });
7442
7932
  }
7443
- function extractContentText(content) {
7444
- if (typeof content === "string") {
7445
- return sanitizeWireText(content);
7446
- }
7447
- if (!content || typeof content !== "object") {
7448
- return null;
7933
+ var defaultEnv;
7934
+ var init_clipboard = __esm({
7935
+ "src/tui/clipboard.ts"() {
7936
+ "use strict";
7937
+ init_attachments();
7938
+ defaultEnv = {
7939
+ platform: process.platform,
7940
+ env: process.env,
7941
+ spawn: nodeSpawn,
7942
+ tmpdir: os4.tmpdir
7943
+ };
7449
7944
  }
7450
- const c = content;
7451
- if (c.type === "text" && typeof c.text === "string") {
7452
- return sanitizeWireText(c.text);
7945
+ });
7946
+
7947
+ // src/tui/completion.ts
7948
+ function longestCommonPrefix(names) {
7949
+ if (names.length === 0) {
7950
+ return "";
7453
7951
  }
7454
- if (typeof c.text === "string") {
7455
- return sanitizeWireText(c.text);
7952
+ let prefix = names[0] ?? "";
7953
+ for (let i = 1; i < names.length; i++) {
7954
+ const n = names[i] ?? "";
7955
+ let j = 0;
7956
+ while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
7957
+ j += 1;
7958
+ }
7959
+ prefix = prefix.slice(0, j);
7960
+ if (prefix.length === 0) {
7961
+ break;
7962
+ }
7456
7963
  }
7457
- return null;
7964
+ return prefix;
7458
7965
  }
7459
- function extractPromptText2(prompt) {
7460
- if (!Array.isArray(prompt)) {
7966
+ function computeTabCompletion(args) {
7967
+ const { matches, firstLine: firstLine3 } = args;
7968
+ if (matches.length === 0) {
7461
7969
  return null;
7462
7970
  }
7463
- const parts = [];
7464
- for (const block of prompt) {
7465
- const text = extractContentText(block);
7466
- if (text !== null) {
7467
- parts.push(text);
7468
- }
7971
+ const space = firstLine3.indexOf(" ");
7972
+ const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
7973
+ const tail = space === -1 ? "" : firstLine3.slice(space);
7974
+ if (matches.length === 1) {
7975
+ const name = matches[0] ?? "";
7976
+ const suffix = tail.startsWith(" ") ? "" : " ";
7977
+ return name + suffix + tail;
7469
7978
  }
7470
- if (parts.length === 0) {
7979
+ const commonPrefix = longestCommonPrefix(matches);
7980
+ if (commonPrefix.length <= typedPrefix.length) {
7471
7981
  return null;
7472
7982
  }
7473
- return parts.join("");
7474
- }
7475
- function readString(u, key) {
7476
- const v = u[key];
7477
- return typeof v === "string" ? v : void 0;
7983
+ return commonPrefix + tail;
7478
7984
  }
7479
- var STRIP_CONTROLS;
7480
- var init_render_update = __esm({
7481
- "src/tui/render-update.ts"() {
7985
+ var init_completion = __esm({
7986
+ "src/tui/completion.ts"() {
7482
7987
  "use strict";
7483
- STRIP_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
7484
7988
  }
7485
7989
  });
7486
7990
 
@@ -7693,7 +8197,7 @@ function formatBlock(text, prefix, bodyStyle, prefixStyle, sentBy, fillRow) {
7693
8197
  }
7694
8198
  return out;
7695
8199
  }
7696
- function formatToolLine(state) {
8200
+ function formatToolLine2(state) {
7697
8201
  const initial = state.initialTitle;
7698
8202
  const latest = state.latestTitle;
7699
8203
  const initialLc = initial.toLowerCase();
@@ -7853,6 +8357,11 @@ async function runTuiApp(opts) {
7853
8357
  while (nextOpts !== null) {
7854
8358
  nextOpts = await runSession(term, config, nextOpts, exitHint);
7855
8359
  }
8360
+ const pendingUpdate = await getPendingUpdate();
8361
+ if (pendingUpdate) {
8362
+ process.stderr.write(`\u2728 ${formatUpdateNoticeLine(pendingUpdate)}
8363
+ `);
8364
+ }
7856
8365
  if (exitHint.sessionId) {
7857
8366
  const short = stripHydraSessionPrefix(exitHint.sessionId);
7858
8367
  process.stdout.write(`To resume: hydra-acp tui --resume ${short}
@@ -8096,6 +8605,7 @@ async function runSession(term, config, opts, exitHint) {
8096
8605
  let initialModel;
8097
8606
  let initialMode;
8098
8607
  let initialCommands;
8608
+ let initialUsage;
8099
8609
  let initialTurnStartedAt;
8100
8610
  if (ctx.sessionId === "__new__") {
8101
8611
  const hydraNewMeta = {};
@@ -8125,6 +8635,7 @@ async function runSession(term, config, opts, exitHint) {
8125
8635
  }
8126
8636
  initialModel = hydraMeta.currentModel;
8127
8637
  initialMode = hydraMeta.currentMode;
8638
+ initialUsage = hydraMeta.currentUsage;
8128
8639
  initialTurnStartedAt = hydraMeta.turnStartedAt;
8129
8640
  if (hydraMeta.availableCommands) {
8130
8641
  initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
@@ -8150,6 +8661,7 @@ async function runSession(term, config, opts, exitHint) {
8150
8661
  }
8151
8662
  initialModel = hydraMeta.currentModel;
8152
8663
  initialMode = hydraMeta.currentMode;
8664
+ initialUsage = hydraMeta.currentUsage;
8153
8665
  initialTurnStartedAt = hydraMeta.turnStartedAt;
8154
8666
  if (hydraMeta.availableCommands) {
8155
8667
  initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
@@ -8356,18 +8868,25 @@ async function runSession(term, config, opts, exitHint) {
8356
8868
  }
8357
8869
  return true;
8358
8870
  };
8359
- const headerName = resolvedAgentId || agentInfoName || "?";
8871
+ const sessionbarAgent = resolvedAgentId || agentInfoName || "?";
8872
+ const usage = { ...initialUsage ?? {} };
8360
8873
  screen.start();
8361
- screen.setHeader({
8362
- agent: headerName,
8874
+ screen.setSessionbar({
8875
+ agent: sessionbarAgent,
8363
8876
  cwd: resolvedCwd,
8364
8877
  sessionId: resolvedSessionId,
8365
8878
  title: resolvedTitle,
8366
- model: initialModel
8879
+ model: initialModel,
8880
+ usage: { ...usage }
8367
8881
  });
8368
8882
  if (initialMode) {
8369
8883
  screen.appendLines(formatEvent({ kind: "mode-changed", mode: initialMode }));
8370
8884
  }
8885
+ void getPendingUpdate().then((info) => {
8886
+ if (info) {
8887
+ screen.notify(`\u2728 ${formatUpdateNoticeLine(info)}`, 3e4);
8888
+ }
8889
+ });
8371
8890
  let finishSession = null;
8372
8891
  const sessionDone = new Promise((resolve5) => {
8373
8892
  finishSession = resolve5;
@@ -8929,7 +9448,6 @@ async function runSession(term, config, opts, exitHint) {
8929
9448
  );
8930
9449
  }
8931
9450
  };
8932
- const usage = {};
8933
9451
  const toolStates = /* @__PURE__ */ new Map();
8934
9452
  const toolCallOrder = [];
8935
9453
  let toolsExpanded = false;
@@ -9012,7 +9530,7 @@ async function runSession(term, config, opts, exitHint) {
9012
9530
  for (const id of visibleIds) {
9013
9531
  const state = toolStates.get(id);
9014
9532
  if (state) {
9015
- lines.push(formatToolLine(state));
9533
+ lines.push(formatToolLine2(state));
9016
9534
  }
9017
9535
  }
9018
9536
  screen.upsertLines("tools", lines);
@@ -9058,11 +9576,11 @@ async function runSession(term, config, opts, exitHint) {
9058
9576
  }
9059
9577
  if (event.kind === "session-info") {
9060
9578
  if (event.title !== void 0) {
9061
- screen.setHeader({ title: event.title });
9579
+ screen.setSessionbar({ title: event.title });
9062
9580
  }
9063
9581
  if (event.agentId !== void 0 && event.agentId !== resolvedAgentId) {
9064
9582
  resolvedAgentId = event.agentId;
9065
- screen.setHeader({ agent: event.agentId });
9583
+ screen.setSessionbar({ agent: event.agentId });
9066
9584
  }
9067
9585
  return;
9068
9586
  }
@@ -9085,7 +9603,7 @@ async function runSession(term, config, opts, exitHint) {
9085
9603
  changed = true;
9086
9604
  }
9087
9605
  if (changed) {
9088
- screen.setHeader({ usage: { ...usage } });
9606
+ screen.setSessionbar({ usage: { ...usage } });
9089
9607
  }
9090
9608
  return;
9091
9609
  }
@@ -9136,7 +9654,7 @@ async function runSession(term, config, opts, exitHint) {
9136
9654
  return;
9137
9655
  }
9138
9656
  if (event.kind === "model-changed") {
9139
- screen.setHeader({ model: event.model });
9657
+ screen.setSessionbar({ model: event.model });
9140
9658
  }
9141
9659
  const formatted = formatEvent(event);
9142
9660
  if (formatted.length > 0) {
@@ -9387,6 +9905,7 @@ var init_app = __esm({
9387
9905
  init_session();
9388
9906
  init_paths();
9389
9907
  init_hydra_version();
9908
+ init_update_check();
9390
9909
  init_history();
9391
9910
  init_discovery();
9392
9911
  init_picker();
@@ -9425,6 +9944,7 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
9425
9944
  "foreground",
9426
9945
  "help",
9427
9946
  "info",
9947
+ "json",
9428
9948
  "new",
9429
9949
  "reattach",
9430
9950
  "replace",
@@ -11700,6 +12220,7 @@ function constantTimeEqual(a, b) {
11700
12220
  // src/daemon/routes/sessions.ts
11701
12221
  init_config();
11702
12222
  init_bundle();
12223
+ init_transcript();
11703
12224
  init_types();
11704
12225
  init_hydra_version();
11705
12226
  import * as os3 from "os";
@@ -11782,6 +12303,24 @@ function registerSessionRoutes(app, manager, defaults) {
11782
12303
  );
11783
12304
  reply.code(200).send(bundle);
11784
12305
  });
12306
+ app.get("/v1/sessions/:id/transcript", async (request, reply) => {
12307
+ const raw = request.params.id;
12308
+ const id = await manager.resolveCanonicalId(raw) ?? raw;
12309
+ const exported = await manager.exportBundle(id);
12310
+ if (!exported) {
12311
+ reply.code(404).send({ error: "session not found" });
12312
+ return;
12313
+ }
12314
+ const bundle = encodeBundle({
12315
+ record: exported.record,
12316
+ history: exported.history,
12317
+ promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
12318
+ hydraVersion: HYDRA_VERSION,
12319
+ machine: os3.hostname()
12320
+ });
12321
+ reply.header("Content-Type", "text/markdown; charset=utf-8");
12322
+ reply.code(200).send(bundleToMarkdown(bundle));
12323
+ });
11785
12324
  app.post("/v1/sessions/import", async (request, reply) => {
11786
12325
  const body = request.body ?? {};
11787
12326
  if (body.bundle === void 0) {
@@ -12096,11 +12635,18 @@ function registerAcpWsEndpoint(app, deps) {
12096
12635
  model: hydraMeta.model
12097
12636
  });
12098
12637
  const client = bindClientToSession(connection, session, state);
12099
- await session.attach(client, "full");
12638
+ const { entries: replay } = await session.attach(client, "full");
12100
12639
  state.attached.set(session.sessionId, {
12101
12640
  sessionId: session.sessionId,
12102
12641
  clientId: client.clientId
12103
12642
  });
12643
+ setImmediate(() => {
12644
+ void (async () => {
12645
+ for (const note of replay) {
12646
+ await connection.notify(note.method, note.params).catch(() => void 0);
12647
+ }
12648
+ })();
12649
+ });
12104
12650
  return {
12105
12651
  sessionId: session.sessionId,
12106
12652
  _meta: buildResponseMeta(session)
@@ -12314,6 +12860,9 @@ function buildResponseMeta(session) {
12314
12860
  if (session.currentMode !== void 0) {
12315
12861
  ours.currentMode = session.currentMode;
12316
12862
  }
12863
+ if (session.currentUsage !== void 0) {
12864
+ ours.currentUsage = session.currentUsage;
12865
+ }
12317
12866
  const commands = session.mergedAvailableCommands();
12318
12867
  if (commands.length > 0) {
12319
12868
  ours.availableCommands = commands;
@@ -13714,54 +14263,8 @@ function injectHydraMeta(msg, additions) {
13714
14263
  };
13715
14264
  }
13716
14265
 
13717
- // src/core/update-check.ts
13718
- init_hydra_version();
13719
- var PKG_NAME = "@hydra-acp/cli";
13720
- var cached;
13721
- function disabled() {
13722
- if (process.env.NO_UPDATE_NOTIFIER === "1") {
13723
- return true;
13724
- }
13725
- if (process.argv.includes("--no-update-notifier")) {
13726
- return true;
13727
- }
13728
- return false;
13729
- }
13730
- async function getPendingUpdate() {
13731
- if (cached !== void 0) {
13732
- return cached;
13733
- }
13734
- if (disabled()) {
13735
- cached = null;
13736
- return cached;
13737
- }
13738
- try {
13739
- const mod = await import("update-notifier");
13740
- const updateNotifier = mod.default ?? mod;
13741
- const notifier = updateNotifier({
13742
- pkg: { name: PKG_NAME, version: HYDRA_VERSION },
13743
- updateCheckInterval: 1e3 * 60 * 60 * 24
13744
- });
13745
- const u = notifier.update;
13746
- if (u && typeof u.latest === "string" && typeof u.current === "string" && u.latest !== u.current) {
13747
- cached = {
13748
- current: u.current,
13749
- latest: u.latest,
13750
- type: typeof u.type === "string" ? u.type : "unknown"
13751
- };
13752
- } else {
13753
- cached = null;
13754
- }
13755
- } catch {
13756
- cached = null;
13757
- }
13758
- return cached;
13759
- }
13760
- function formatUpdateNoticeLine(info) {
13761
- return `hydra-acp ${info.latest} available (current ${info.current}) \xB7 run: npm update -g ${PKG_NAME}`;
13762
- }
13763
-
13764
14266
  // src/cli.ts
14267
+ init_update_check();
13765
14268
  var suppressUpdateNotice = false;
13766
14269
  async function main() {
13767
14270
  const argv = process.argv.slice(2);
@@ -13872,7 +14375,10 @@ async function main() {
13872
14375
  case "sessions": {
13873
14376
  const sub = positional[1];
13874
14377
  if (sub === void 0 || sub === "list") {
13875
- await runSessionsList({ all: flags.all === true });
14378
+ await runSessionsList({
14379
+ all: flags.all === true,
14380
+ json: flags.json === true
14381
+ });
13876
14382
  return;
13877
14383
  }
13878
14384
  if (sub === "kill") {
@@ -13888,6 +14394,11 @@ async function main() {
13888
14394
  await runSessionsExport(positional[2], out);
13889
14395
  return;
13890
14396
  }
14397
+ if (sub === "transcript") {
14398
+ const out = resolveOption(flags, "out");
14399
+ await runSessionsTranscript(positional[2], out);
14400
+ return;
14401
+ }
13891
14402
  if (sub === "import") {
13892
14403
  const cwd = resolveOption(flags, "cwd");
13893
14404
  await runSessionsImport(positional[2], {
@@ -14031,11 +14542,14 @@ function printHelp() {
14031
14542
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
14032
14543
  " hydra-acp daemon stop|restart|status",
14033
14544
  " hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
14034
- " hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
14545
+ " hydra-acp sessions [list] [--all] [--json]",
14546
+ " List sessions (live + 20 most-recent cold; --all for everything; --json emits the raw daemon response as JSON for scripts)",
14035
14547
  " hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
14036
14548
  " hydra-acp sessions remove <id> Remove a session entirely (live or cold)",
14037
14549
  " hydra-acp sessions export <id> [--out <file>|.]",
14038
14550
  " Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
14551
+ " hydra-acp sessions transcript <id>|<file> [--out <file>|.]",
14552
+ " Render a session as a markdown transcript. Accepts a session id (renders via the daemon) or a local .hydra bundle file (rendered in-process). Writes to <file>, to a default-named file when --out=., or to stdout",
14039
14553
  " hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]",
14040
14554
  " Import a bundle from <file> or stdin (-); --replace overwrites a lineage match (kills it if live); --cwd overrides the bundle's recorded working directory; --info prints the bundle's meta without importing",
14041
14555
  " hydra-acp extensions list List configured extensions and live state",