@hydra-acp/cli 0.1.14 → 0.1.16

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,58 @@ 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
+ const u = notifier.update;
4083
+ if (u && typeof u.latest === "string" && typeof u.current === "string" && u.latest !== u.current) {
4084
+ cached = {
4085
+ current: u.current,
4086
+ latest: u.latest,
4087
+ type: typeof u.type === "string" ? u.type : "unknown"
4088
+ };
4089
+ } else {
4090
+ cached = null;
4091
+ }
4092
+ } catch {
4093
+ cached = null;
4094
+ }
4095
+ return cached;
4096
+ }
4097
+ function formatUpdateNoticeLine(info) {
4098
+ return `hydra-acp ${info.latest} available (current ${info.current}) \xB7 run: npm update -g ${PKG_NAME}`;
4099
+ }
4100
+ var PKG_NAME, cached;
4101
+ var init_update_check = __esm({
4102
+ "src/core/update-check.ts"() {
4103
+ "use strict";
4104
+ init_hydra_version();
4105
+ PKG_NAME = "@hydra-acp/cli";
4106
+ }
4107
+ });
4108
+
3327
4109
  // src/tui/discovery.ts
3328
4110
  async function listSessions(config, opts = {}, fetchImpl = fetch) {
3329
4111
  const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
@@ -4558,7 +5340,7 @@ function mapKeyName(name) {
4558
5340
  return null;
4559
5341
  }
4560
5342
  }
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;
5343
+ 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
5344
  var init_screen = __esm({
4563
5345
  "src/tui/screen.ts"() {
4564
5346
  "use strict";
@@ -4566,7 +5348,7 @@ var init_screen = __esm({
4566
5348
  init_paths();
4567
5349
  init_session();
4568
5350
  init_attachments();
4569
- HEADER_ROWS = 2;
5351
+ SESSIONBAR_ROWS = 1;
4570
5352
  BANNER_ROWS = 1;
4571
5353
  SEPARATOR_ROWS = 1;
4572
5354
  MAX_PROMPT_ROWS = 8;
@@ -4664,7 +5446,7 @@ var init_screen = __esm({
4664
5446
  hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
4665
5447
  queued: 0
4666
5448
  };
4667
- header = { agent: "?", cwd: "?", sessionId: "?" };
5449
+ sessionbar = { agent: "?", cwd: "?", sessionId: "?" };
4668
5450
  lastWindowTitle = null;
4669
5451
  resizeHandler;
4670
5452
  keyHandler;
@@ -5015,8 +5797,8 @@ var init_screen = __esm({
5015
5797
  this.trimScrollback();
5016
5798
  this.scheduleRepaint();
5017
5799
  }
5018
- setHeader(header) {
5019
- this.header = { ...this.header, ...header };
5800
+ setSessionbar(sessionbar) {
5801
+ this.sessionbar = { ...this.sessionbar, ...sessionbar };
5020
5802
  this.syncWindowTitle();
5021
5803
  this.repaint();
5022
5804
  }
@@ -5024,8 +5806,8 @@ var init_screen = __esm({
5024
5806
  // the host terminal via OSC 2. Supported by xterm/foot/iTerm2/Alacritty/
5025
5807
  // most modern emulators; ignored harmlessly elsewhere.
5026
5808
  syncWindowTitle() {
5027
- const title = this.header.title?.trim();
5028
- const fallback = shortId(this.header.sessionId) || "hydra";
5809
+ const title = this.sessionbar.title?.trim();
5810
+ const fallback = shortId(this.sessionbar.sessionId) || "hydra";
5029
5811
  const raw = title && title.length > 0 ? title : fallback;
5030
5812
  const clean = raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
5031
5813
  if (clean === this.lastWindowTitle) {
@@ -5547,8 +6329,10 @@ var init_screen = __esm({
5547
6329
  return Math.max(1, this.scrollbackVisibleRows() - 2);
5548
6330
  }
5549
6331
  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();
6332
+ const top = 1;
6333
+ const bottom = this.term.height - this.promptRows() - SESSIONBAR_ROWS - SEPARATOR_ROWS - // separator between banner and sessionbar
6334
+ BANNER_ROWS - SEPARATOR_ROWS - // separator above prompt
6335
+ this.chipRows() - this.queuedRows() - this.completionRows();
5552
6336
  return Math.max(0, bottom - top + 1);
5553
6337
  }
5554
6338
  maxScrollOffset() {
@@ -5625,30 +6409,32 @@ var init_screen = __esm({
5625
6409
  this.lastFrameW = w;
5626
6410
  this.lastFrameH = h;
5627
6411
  }
5628
- this.drawHeader();
5629
- this.drawSeparator(HEADER_ROWS);
5630
6412
  this.drawScrollback();
5631
6413
  this.drawCompletionZone();
5632
6414
  this.drawQueuedZone();
5633
6415
  this.drawAttachmentChipZone();
5634
6416
  const promptRows = this.promptRows();
5635
- const separatorRow = h - promptRows - BANNER_ROWS;
5636
- this.drawSeparator(separatorRow);
6417
+ const separatorAbovePromptRow = h - promptRows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
6418
+ this.drawSeparator(separatorAbovePromptRow);
5637
6419
  this.drawPrompt();
5638
6420
  this.drawBanner();
6421
+ this.drawSeparator(h - SESSIONBAR_ROWS);
6422
+ this.drawSessionbar();
5639
6423
  this.placeCursor();
5640
6424
  this.lastPromptRows = promptRows;
5641
6425
  }
5642
- drawHeader() {
6426
+ drawSessionbar() {
5643
6427
  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);
6428
+ const row = this.term.height;
6429
+ const sid = shortId(this.sessionbar.sessionId);
6430
+ const title = this.sessionbar.title?.trim();
6431
+ const agentCell = formatAgentWithModel(this.sessionbar.agent, this.sessionbar.model);
6432
+ const cwdDisplay = shortenHomePath(this.sessionbar.cwd);
6433
+ const usage = formatUsage(this.sessionbar.usage);
6434
+ const sig = `sbar|${w}|${sid}|${agentCell}|${cwdDisplay}|${title ?? ""}|${usage ?? ""}`;
6435
+ this.paintRow(row, sig, () => {
6436
+ const usageReserve = usage ? usage.length + 3 : 0;
6437
+ const fixed = sid.length + " \xB7 ".length + agentCell.length + " \xB7 ".length + (title ? " \xB7 ".length : 0) + usageReserve;
5652
6438
  const variableRoom = Math.max(8, w - fixed);
5653
6439
  let cwdRoom;
5654
6440
  let titleRoom;
@@ -5660,13 +6446,13 @@ var init_screen = __esm({
5660
6446
  titleRoom = 0;
5661
6447
  cwdRoom = variableRoom;
5662
6448
  }
5663
- this.term.bold("hydra")(" \xB7 ").cyan.noFormat(agentCell)(" \xB7 ").dim.noFormat(truncate(cwdDisplay, cwdRoom))(" \xB7 ").yellow(sid);
6449
+ this.term.yellow(sid)(" \xB7 ").cyan.noFormat(agentCell)(" \xB7 ").dim.noFormat(truncate(cwdDisplay, cwdRoom));
5664
6450
  if (title) {
5665
6451
  this.term(" \xB7 ").bold.noFormat(truncate(title, titleRoom));
5666
6452
  }
5667
6453
  if (usage) {
5668
6454
  const col = Math.max(1, w - usage.length + 1);
5669
- this.term.moveTo(col, 1).eraseLineAfter();
6455
+ this.term.moveTo(col, row).eraseLineAfter();
5670
6456
  this.term.dim.noFormat(usage);
5671
6457
  }
5672
6458
  });
@@ -5679,7 +6465,7 @@ var init_screen = __esm({
5679
6465
  }
5680
6466
  drawScrollback() {
5681
6467
  const w = this.term.width;
5682
- const top = HEADER_ROWS + SEPARATOR_ROWS;
6468
+ const top = 1;
5683
6469
  const visibleRows = this.scrollbackVisibleRows();
5684
6470
  if (visibleRows <= 0) {
5685
6471
  return;
@@ -5745,7 +6531,7 @@ var init_screen = __esm({
5745
6531
  }
5746
6532
  const w = this.term.width;
5747
6533
  const promptRows = this.promptRows();
5748
- const separatorRow = this.term.height - promptRows - BANNER_ROWS;
6534
+ const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
5749
6535
  const queuedRows = this.queuedRows();
5750
6536
  const chipRows = this.chipRows();
5751
6537
  const completionBottom = separatorRow - 1 - queuedRows - chipRows;
@@ -5793,7 +6579,7 @@ var init_screen = __esm({
5793
6579
  }
5794
6580
  const w = this.term.width;
5795
6581
  const promptRows = this.promptRows();
5796
- const separatorRow = this.term.height - promptRows - BANNER_ROWS;
6582
+ const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
5797
6583
  const chipBottom = separatorRow - 1;
5798
6584
  const chipTop = chipBottom - rows + 1;
5799
6585
  const iterm = this.isIterm2();
@@ -5840,7 +6626,7 @@ var init_screen = __esm({
5840
6626
  }
5841
6627
  const w = this.term.width;
5842
6628
  const promptRows = this.promptRows();
5843
- const separatorRow = this.term.height - promptRows - BANNER_ROWS;
6629
+ const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
5844
6630
  const chipRows = this.chipRows();
5845
6631
  const queuedBottom = separatorRow - 1 - chipRows;
5846
6632
  const queuedTop = queuedBottom - rows + 1;
@@ -5882,7 +6668,7 @@ var init_screen = __esm({
5882
6668
  const state = this.dispatcher.state();
5883
6669
  const visualRows = computePromptVisualRows(state.buffer, room);
5884
6670
  const layout = computePromptLayout(visualRows, state, MAX_PROMPT_ROWS);
5885
- const top = this.term.height - layout.rendered - BANNER_ROWS + 1;
6671
+ const top = this.term.height - layout.rendered - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
5886
6672
  for (let i = 0; i < layout.rendered; i++) {
5887
6673
  const vr = visualRows[layout.windowStart + i];
5888
6674
  const row = top + i;
@@ -5918,7 +6704,7 @@ var init_screen = __esm({
5918
6704
  return;
5919
6705
  }
5920
6706
  const w = this.term.width;
5921
- const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
6707
+ const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
5922
6708
  this.paintRow(top, `confirm|q|${w}|${spec.question}`, () => {
5923
6709
  this.term.brightYellow(` ? ${truncate(spec.question, w - 4)}`);
5924
6710
  });
@@ -5933,7 +6719,7 @@ var init_screen = __esm({
5933
6719
  }
5934
6720
  const w = this.term.width;
5935
6721
  const rows = this.permissionRows();
5936
- const top = this.term.height - rows - BANNER_ROWS + 1;
6722
+ const top = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
5937
6723
  let row = top;
5938
6724
  const writeRow = (sig, paint) => {
5939
6725
  if (row >= top + rows) {
@@ -5975,7 +6761,7 @@ var init_screen = __esm({
5975
6761
  });
5976
6762
  }
5977
6763
  drawBanner() {
5978
- const row = this.term.height;
6764
+ const row = this.term.height - SESSIONBAR_ROWS - SEPARATOR_ROWS;
5979
6765
  const w = this.term.width;
5980
6766
  const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
5981
6767
  const right = this.bannerRightContent();
@@ -6024,13 +6810,14 @@ var init_screen = __esm({
6024
6810
  placeCursor() {
6025
6811
  if (this.permissionPrompt) {
6026
6812
  const rows = this.permissionRows();
6027
- const top2 = this.term.height - rows - BANNER_ROWS + 1;
6813
+ const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
6028
6814
  const optionRow = top2 + 3 + this.permissionPrompt.selectedIndex;
6029
- this.term.moveTo(2, Math.min(optionRow, this.term.height - BANNER_ROWS));
6815
+ const lastUsableRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
6816
+ this.term.moveTo(2, Math.min(optionRow, lastUsableRow));
6030
6817
  return;
6031
6818
  }
6032
6819
  if (this.confirmPrompt) {
6033
- const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
6820
+ const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
6034
6821
  this.term.moveTo(2, top2);
6035
6822
  return;
6036
6823
  }
@@ -6044,12 +6831,13 @@ var init_screen = __esm({
6044
6831
  const state = this.dispatcher.state();
6045
6832
  const visualRows = computePromptVisualRows(state.buffer, room);
6046
6833
  const layout = computePromptLayout(visualRows, state, MAX_PROMPT_ROWS);
6047
- const top = this.term.height - layout.rendered - BANNER_ROWS + 1;
6834
+ const top = this.term.height - layout.rendered - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
6048
6835
  const row = top + Math.max(0, layout.cursorVisualRow - layout.windowStart);
6049
6836
  const col = layout.cursorVisualCol + 3;
6837
+ const lastPromptRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
6050
6838
  this.term.moveTo(
6051
6839
  Math.min(col, this.term.width),
6052
- Math.min(row, this.term.height - BANNER_ROWS)
6840
+ Math.min(row, lastPromptRow)
6053
6841
  );
6054
6842
  }
6055
6843
  promptRows() {
@@ -6118,9 +6906,9 @@ var init_screen = __esm({
6118
6906
  wrapOne(line, width) {
6119
6907
  const id = this.lineIds.get(line);
6120
6908
  if (id !== void 0) {
6121
- const cached = this.wrapCache.get(id);
6122
- if (cached) {
6123
- return cached;
6909
+ const cached2 = this.wrapCache.get(id);
6910
+ if (cached2) {
6911
+ return cached2;
6124
6912
  }
6125
6913
  }
6126
6914
  const prefix = line.prefix ?? "";
@@ -6843,644 +7631,359 @@ var init_input = __esm({
6843
7631
  }
6844
7632
  }
6845
7633
  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 [];
6891
- }
6892
- return [{ type: "scroll-to-top" }];
6893
- }
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;
6902
- return [];
6903
- }
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 }];
6912
- }
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 [];
6922
- }
6923
- if (this.turnRunning) {
6924
- return [{ type: "cancel" }];
6925
- }
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
- };
7003
- }
7004
- return {
7005
- ok: true,
7006
- kind: "image",
7007
- attachment: {
7008
- mimeType: "image/png",
7009
- data: buf.toString("base64"),
7010
- sizeBytes: buf.length
7634
+ }
7635
+ cancelHistorySearch() {
7636
+ if (this.historySearch === null) {
7637
+ return;
7011
7638
  }
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
7639
+ const draft = this.historySearch.savedDraft;
7640
+ this.historySearch = null;
7641
+ this.buffer = [...draft.buffer];
7642
+ this.row = draft.row;
7643
+ this.col = draft.col;
7084
7644
  }
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}`));
7645
+ loadEntry(text) {
7646
+ this.buffer = text.split("\n");
7647
+ if (this.buffer.length === 0) {
7648
+ this.buffer = [""];
7649
+ }
7650
+ this.row = this.buffer.length - 1;
7651
+ this.col = (this.buffer[this.row] ?? "").length;
7101
7652
  }
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;
7653
+ send() {
7654
+ const text = this.bufferText();
7655
+ if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
7656
+ const index = this.queueIndex;
7657
+ const attachments2 = [...this.attachments];
7658
+ this.clearBuffer();
7659
+ if (text.trim().length === 0) {
7660
+ return [{ type: "queue-remove", index }];
7661
+ }
7662
+ return [{ type: "queue-edit", index, text, attachments: attachments2 }];
7663
+ }
7664
+ if (text.trim().length === 0 && this.attachments.length === 0) {
7665
+ return [];
7666
+ }
7667
+ const planMode = this.planMode;
7668
+ const attachments = [...this.attachments];
7669
+ this.clearBuffer();
7670
+ return [{ type: "send", text, planMode, attachments }];
7115
7671
  }
7116
- settled = true;
7117
- if (closedCode === 0) {
7118
- resolve5(Buffer.concat(chunks));
7119
- } else {
7120
- reject(new Error(`${cmd} exited ${closedCode}`));
7672
+ // Home: jump to the very start of the prompt buffer. If we're already
7673
+ // there, fall through to scrolling the scrollback to its top.
7674
+ handleHome() {
7675
+ if (this.row !== 0 || this.col !== 0) {
7676
+ this.row = 0;
7677
+ this.col = 0;
7678
+ return [];
7679
+ }
7680
+ return [{ type: "scroll-to-top" }];
7121
7681
  }
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;
7682
+ // End: jump to the end of the last line of the prompt buffer. Already
7683
+ // there → scroll the scrollback to the bottom (newest).
7684
+ handleEnd() {
7685
+ const lastRow = this.buffer.length - 1;
7686
+ const lastCol = (this.buffer[lastRow] ?? "").length;
7687
+ if (this.row !== lastRow || this.col !== lastCol) {
7688
+ this.row = lastRow;
7689
+ this.col = lastCol;
7690
+ return [];
7691
+ }
7692
+ return [{ type: "scroll-to-bottom" }];
7134
7693
  }
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";
7694
+ handleCtrlC() {
7695
+ if (this.queueIndex >= 0) {
7696
+ const index = this.queueIndex;
7697
+ this.queueIndex = -1;
7698
+ this.restoreDraft();
7699
+ return [{ type: "queue-remove", index }];
7700
+ }
7701
+ if (!this.bufferIsEmpty() || this.attachments.length > 0) {
7702
+ this.buffer = [""];
7703
+ this.row = 0;
7704
+ this.col = 0;
7705
+ this.attachments = [];
7706
+ this.historyIndex = -1;
7707
+ this.savedDraft = null;
7708
+ this.savedAttachments = null;
7709
+ return [];
7710
+ }
7711
+ if (this.turnRunning) {
7712
+ return [{ type: "cancel" }];
7713
+ }
7714
+ return [{ type: "exit" }];
7715
+ }
7716
+ };
7199
7717
  }
7200
7718
  });
7201
7719
 
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;
7262
- }
7263
- }
7264
- }
7265
- if (title === void 0 && agentId === void 0) {
7266
- return null;
7267
- }
7268
- const event = { kind: "session-info" };
7269
- if (title !== void 0) {
7270
- event.title = title;
7720
+ // src/tui/clipboard.ts
7721
+ import { spawn as nodeSpawn } from "child_process";
7722
+ import fs14 from "fs/promises";
7723
+ import os4 from "os";
7724
+ import path10 from "path";
7725
+ async function readClipboard(envIn = {}) {
7726
+ const env = { ...defaultEnv, ...envIn };
7727
+ if (env.platform === "darwin") {
7728
+ return readMacOS(env);
7271
7729
  }
7272
- if (agentId !== void 0) {
7273
- event.agentId = agentId;
7730
+ if (env.platform === "linux") {
7731
+ return readLinux(env);
7274
7732
  }
7275
- return event;
7733
+ return {
7734
+ ok: false,
7735
+ reason: `clipboard paste is not supported on ${env.platform}`
7736
+ };
7276
7737
  }
7277
- function normalizeAdvertisedCommands(list) {
7278
- if (!Array.isArray(list)) {
7279
- return [];
7738
+ async function readMacOS(env) {
7739
+ const tmpPath = path10.join(
7740
+ env.tmpdir(),
7741
+ `hydra-clipboard-${Date.now()}-${process.pid}.png`
7742
+ );
7743
+ const script = [
7744
+ "set png_data to the clipboard as \xABclass PNGf\xBB",
7745
+ `set out_file to (open for access (POSIX file "${tmpPath}") with write permission)`,
7746
+ "write png_data to out_file",
7747
+ "close access out_file"
7748
+ ];
7749
+ const args = [];
7750
+ for (const line of script) {
7751
+ args.push("-e", line);
7280
7752
  }
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;
7753
+ try {
7754
+ await run2(env.spawn, "osascript", args);
7755
+ const img = await readFileAsAttachment(tmpPath, true);
7756
+ if (img.ok) {
7757
+ return img;
7289
7758
  }
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);
7759
+ if (img.reason.startsWith("clipboard image is")) {
7760
+ return img;
7294
7761
  }
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;
7762
+ } catch {
7763
+ await fs14.unlink(tmpPath).catch(() => void 0);
7313
7764
  }
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;
7765
+ try {
7766
+ const buf = await runCapture(env.spawn, "pbpaste", []);
7767
+ if (buf.length === 0) {
7768
+ return { ok: false, reason: "clipboard is empty" };
7321
7769
  }
7770
+ return { ok: true, kind: "text", text: normalizeText(buf.toString("utf-8")) };
7771
+ } catch {
7772
+ return { ok: false, reason: "clipboard read failed" };
7322
7773
  }
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
7774
  }
7332
- function mapAgentThought(u) {
7333
- const text = typeof u.text === "string" ? sanitizeWireText(u.text) : extractContentText(u.content);
7334
- if (text === null) {
7335
- return null;
7775
+ async function readLinux(env) {
7776
+ const tool = await detectLinuxTool(env);
7777
+ if (!tool) {
7778
+ return {
7779
+ ok: false,
7780
+ reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
7781
+ };
7336
7782
  }
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;
7783
+ try {
7784
+ const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
7785
+ if (buf.length > 0) {
7786
+ if (buf.length > MAX_ATTACHMENT_BYTES) {
7787
+ return {
7788
+ ok: false,
7789
+ reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
7790
+ };
7791
+ }
7792
+ return {
7793
+ ok: true,
7794
+ kind: "image",
7795
+ attachment: {
7796
+ mimeType: "image/png",
7797
+ data: buf.toString("base64"),
7798
+ sizeBytes: buf.length
7799
+ }
7800
+ };
7345
7801
  }
7802
+ } catch {
7346
7803
  }
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;
7357
- }
7358
- return { kind: "user-text", text: promptText };
7359
- }
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;
7372
- }
7373
- if (rawKind !== void 0) {
7374
- event.rawKind = rawKind;
7375
- }
7376
- return event;
7377
- }
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;
7804
+ try {
7805
+ const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
7806
+ if (buf.length === 0) {
7807
+ return { ok: false, reason: "clipboard is empty" };
7808
+ }
7809
+ return {
7810
+ ok: true,
7811
+ kind: "text",
7812
+ text: normalizeText(buf.toString("utf-8"))
7813
+ };
7814
+ } catch {
7815
+ return { ok: false, reason: "clipboard read failed" };
7389
7816
  }
7390
- const event = { kind: "tool-call-update", toolCallId };
7391
- if (title !== void 0) {
7392
- event.title = title;
7817
+ }
7818
+ async function detectLinuxTool(env) {
7819
+ if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
7820
+ return {
7821
+ cmd: "wl-paste",
7822
+ imageArgs: ["-t", "image/png"],
7823
+ // -n: drop trailing newline wl-paste adds by default. We further
7824
+ // normalize line endings below, but this avoids a spurious
7825
+ // empty trailing row from a single-line clipboard text.
7826
+ textArgs: ["-n"]
7827
+ };
7393
7828
  }
7394
- if (status !== void 0) {
7395
- event.status = status;
7829
+ if (env.env.DISPLAY && await which(env, "xclip")) {
7830
+ return {
7831
+ cmd: "xclip",
7832
+ imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
7833
+ textArgs: ["-selection", "clipboard", "-o"]
7834
+ };
7396
7835
  }
7397
- return event;
7836
+ return null;
7398
7837
  }
7399
- function mapPlan(u) {
7400
- const entries = u.entries;
7401
- if (!Array.isArray(entries)) {
7402
- return null;
7838
+ function normalizeText(text) {
7839
+ return text.replace(/\r\n?/g, "\n");
7840
+ }
7841
+ async function which(env, cmd) {
7842
+ try {
7843
+ await run2(env.spawn, "which", [cmd]);
7844
+ return true;
7845
+ } catch {
7846
+ return false;
7403
7847
  }
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;
7848
+ }
7849
+ async function readFileAsAttachment(p, unlinkAfter) {
7850
+ try {
7851
+ const buf = await fs14.readFile(p);
7852
+ if (unlinkAfter) {
7853
+ await fs14.unlink(p).catch(() => void 0);
7413
7854
  }
7414
- const entry = { content };
7415
- if (typeof e.status === "string") {
7416
- entry.status = e.status;
7855
+ if (buf.length === 0) {
7856
+ return { ok: false, reason: "no image on clipboard" };
7417
7857
  }
7418
- if (typeof e.priority === "string") {
7419
- entry.priority = e.priority;
7858
+ if (buf.length > MAX_ATTACHMENT_BYTES) {
7859
+ return {
7860
+ ok: false,
7861
+ reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
7862
+ };
7420
7863
  }
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;
7864
+ const mimeType = mimeFromExtension(p) ?? "image/png";
7865
+ return {
7866
+ ok: true,
7867
+ kind: "image",
7868
+ attachment: {
7869
+ mimeType,
7870
+ data: buf.toString("base64"),
7871
+ sizeBytes: buf.length
7872
+ }
7873
+ };
7874
+ } catch {
7875
+ return { ok: false, reason: "failed to read clipboard image" };
7429
7876
  }
7430
- return { kind: "mode-changed", mode: sanitizeSingleLine(mode) };
7431
7877
  }
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) };
7878
+ function run2(spawn6, cmd, args) {
7879
+ return new Promise((resolve5, reject) => {
7880
+ const proc = spawn6(cmd, args);
7881
+ proc.stdout?.on("data", () => void 0);
7882
+ proc.stderr?.on("data", () => void 0);
7883
+ proc.on("error", reject);
7884
+ proc.on("close", (code) => {
7885
+ if (code === 0) {
7886
+ resolve5();
7887
+ } else {
7888
+ reject(new Error(`${cmd} exited ${code}`));
7889
+ }
7890
+ });
7891
+ });
7438
7892
  }
7439
- function mapTurnComplete(u) {
7440
- const stopReason = readString(u, "stopReason");
7441
- return stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" };
7893
+ function runCapture(spawn6, cmd, args) {
7894
+ return new Promise((resolve5, reject) => {
7895
+ const proc = spawn6(cmd, args);
7896
+ const chunks = [];
7897
+ let stdoutEnded = proc.stdout === null;
7898
+ let closedCode = null;
7899
+ let settled = false;
7900
+ const settle = () => {
7901
+ if (settled || !stdoutEnded || closedCode === null) {
7902
+ return;
7903
+ }
7904
+ settled = true;
7905
+ if (closedCode === 0) {
7906
+ resolve5(Buffer.concat(chunks));
7907
+ } else {
7908
+ reject(new Error(`${cmd} exited ${closedCode}`));
7909
+ }
7910
+ };
7911
+ proc.stdout?.on("data", (chunk) => {
7912
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
7913
+ });
7914
+ proc.stdout?.on("end", () => {
7915
+ stdoutEnded = true;
7916
+ settle();
7917
+ });
7918
+ proc.stderr?.on("data", () => void 0);
7919
+ proc.on("error", (err) => {
7920
+ if (settled) {
7921
+ return;
7922
+ }
7923
+ settled = true;
7924
+ reject(err);
7925
+ });
7926
+ proc.on("close", (code) => {
7927
+ closedCode = code ?? 0;
7928
+ settle();
7929
+ });
7930
+ });
7442
7931
  }
7443
- function extractContentText(content) {
7444
- if (typeof content === "string") {
7445
- return sanitizeWireText(content);
7446
- }
7447
- if (!content || typeof content !== "object") {
7448
- return null;
7932
+ var defaultEnv;
7933
+ var init_clipboard = __esm({
7934
+ "src/tui/clipboard.ts"() {
7935
+ "use strict";
7936
+ init_attachments();
7937
+ defaultEnv = {
7938
+ platform: process.platform,
7939
+ env: process.env,
7940
+ spawn: nodeSpawn,
7941
+ tmpdir: os4.tmpdir
7942
+ };
7449
7943
  }
7450
- const c = content;
7451
- if (c.type === "text" && typeof c.text === "string") {
7452
- return sanitizeWireText(c.text);
7944
+ });
7945
+
7946
+ // src/tui/completion.ts
7947
+ function longestCommonPrefix(names) {
7948
+ if (names.length === 0) {
7949
+ return "";
7453
7950
  }
7454
- if (typeof c.text === "string") {
7455
- return sanitizeWireText(c.text);
7951
+ let prefix = names[0] ?? "";
7952
+ for (let i = 1; i < names.length; i++) {
7953
+ const n = names[i] ?? "";
7954
+ let j = 0;
7955
+ while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
7956
+ j += 1;
7957
+ }
7958
+ prefix = prefix.slice(0, j);
7959
+ if (prefix.length === 0) {
7960
+ break;
7961
+ }
7456
7962
  }
7457
- return null;
7963
+ return prefix;
7458
7964
  }
7459
- function extractPromptText2(prompt) {
7460
- if (!Array.isArray(prompt)) {
7965
+ function computeTabCompletion(args) {
7966
+ const { matches, firstLine: firstLine3 } = args;
7967
+ if (matches.length === 0) {
7461
7968
  return null;
7462
7969
  }
7463
- const parts = [];
7464
- for (const block of prompt) {
7465
- const text = extractContentText(block);
7466
- if (text !== null) {
7467
- parts.push(text);
7468
- }
7970
+ const space = firstLine3.indexOf(" ");
7971
+ const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
7972
+ const tail = space === -1 ? "" : firstLine3.slice(space);
7973
+ if (matches.length === 1) {
7974
+ const name = matches[0] ?? "";
7975
+ const suffix = tail.startsWith(" ") ? "" : " ";
7976
+ return name + suffix + tail;
7469
7977
  }
7470
- if (parts.length === 0) {
7978
+ const commonPrefix = longestCommonPrefix(matches);
7979
+ if (commonPrefix.length <= typedPrefix.length) {
7471
7980
  return null;
7472
7981
  }
7473
- return parts.join("");
7474
- }
7475
- function readString(u, key) {
7476
- const v = u[key];
7477
- return typeof v === "string" ? v : void 0;
7982
+ return commonPrefix + tail;
7478
7983
  }
7479
- var STRIP_CONTROLS;
7480
- var init_render_update = __esm({
7481
- "src/tui/render-update.ts"() {
7984
+ var init_completion = __esm({
7985
+ "src/tui/completion.ts"() {
7482
7986
  "use strict";
7483
- STRIP_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
7484
7987
  }
7485
7988
  });
7486
7989
 
@@ -7693,7 +8196,7 @@ function formatBlock(text, prefix, bodyStyle, prefixStyle, sentBy, fillRow) {
7693
8196
  }
7694
8197
  return out;
7695
8198
  }
7696
- function formatToolLine(state) {
8199
+ function formatToolLine2(state) {
7697
8200
  const initial = state.initialTitle;
7698
8201
  const latest = state.latestTitle;
7699
8202
  const initialLc = initial.toLowerCase();
@@ -8096,6 +8599,7 @@ async function runSession(term, config, opts, exitHint) {
8096
8599
  let initialModel;
8097
8600
  let initialMode;
8098
8601
  let initialCommands;
8602
+ let initialUsage;
8099
8603
  let initialTurnStartedAt;
8100
8604
  if (ctx.sessionId === "__new__") {
8101
8605
  const hydraNewMeta = {};
@@ -8125,6 +8629,7 @@ async function runSession(term, config, opts, exitHint) {
8125
8629
  }
8126
8630
  initialModel = hydraMeta.currentModel;
8127
8631
  initialMode = hydraMeta.currentMode;
8632
+ initialUsage = hydraMeta.currentUsage;
8128
8633
  initialTurnStartedAt = hydraMeta.turnStartedAt;
8129
8634
  if (hydraMeta.availableCommands) {
8130
8635
  initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
@@ -8150,6 +8655,7 @@ async function runSession(term, config, opts, exitHint) {
8150
8655
  }
8151
8656
  initialModel = hydraMeta.currentModel;
8152
8657
  initialMode = hydraMeta.currentMode;
8658
+ initialUsage = hydraMeta.currentUsage;
8153
8659
  initialTurnStartedAt = hydraMeta.turnStartedAt;
8154
8660
  if (hydraMeta.availableCommands) {
8155
8661
  initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
@@ -8356,18 +8862,25 @@ async function runSession(term, config, opts, exitHint) {
8356
8862
  }
8357
8863
  return true;
8358
8864
  };
8359
- const headerName = resolvedAgentId || agentInfoName || "?";
8865
+ const sessionbarAgent = resolvedAgentId || agentInfoName || "?";
8866
+ const usage = { ...initialUsage ?? {} };
8360
8867
  screen.start();
8361
- screen.setHeader({
8362
- agent: headerName,
8868
+ screen.setSessionbar({
8869
+ agent: sessionbarAgent,
8363
8870
  cwd: resolvedCwd,
8364
8871
  sessionId: resolvedSessionId,
8365
8872
  title: resolvedTitle,
8366
- model: initialModel
8873
+ model: initialModel,
8874
+ usage: { ...usage }
8367
8875
  });
8368
8876
  if (initialMode) {
8369
8877
  screen.appendLines(formatEvent({ kind: "mode-changed", mode: initialMode }));
8370
8878
  }
8879
+ void getPendingUpdate().then((info) => {
8880
+ if (info) {
8881
+ screen.notify(`\u2728 ${formatUpdateNoticeLine(info)}`, 3e4);
8882
+ }
8883
+ });
8371
8884
  let finishSession = null;
8372
8885
  const sessionDone = new Promise((resolve5) => {
8373
8886
  finishSession = resolve5;
@@ -8929,7 +9442,6 @@ async function runSession(term, config, opts, exitHint) {
8929
9442
  );
8930
9443
  }
8931
9444
  };
8932
- const usage = {};
8933
9445
  const toolStates = /* @__PURE__ */ new Map();
8934
9446
  const toolCallOrder = [];
8935
9447
  let toolsExpanded = false;
@@ -9012,7 +9524,7 @@ async function runSession(term, config, opts, exitHint) {
9012
9524
  for (const id of visibleIds) {
9013
9525
  const state = toolStates.get(id);
9014
9526
  if (state) {
9015
- lines.push(formatToolLine(state));
9527
+ lines.push(formatToolLine2(state));
9016
9528
  }
9017
9529
  }
9018
9530
  screen.upsertLines("tools", lines);
@@ -9058,11 +9570,11 @@ async function runSession(term, config, opts, exitHint) {
9058
9570
  }
9059
9571
  if (event.kind === "session-info") {
9060
9572
  if (event.title !== void 0) {
9061
- screen.setHeader({ title: event.title });
9573
+ screen.setSessionbar({ title: event.title });
9062
9574
  }
9063
9575
  if (event.agentId !== void 0 && event.agentId !== resolvedAgentId) {
9064
9576
  resolvedAgentId = event.agentId;
9065
- screen.setHeader({ agent: event.agentId });
9577
+ screen.setSessionbar({ agent: event.agentId });
9066
9578
  }
9067
9579
  return;
9068
9580
  }
@@ -9085,7 +9597,7 @@ async function runSession(term, config, opts, exitHint) {
9085
9597
  changed = true;
9086
9598
  }
9087
9599
  if (changed) {
9088
- screen.setHeader({ usage: { ...usage } });
9600
+ screen.setSessionbar({ usage: { ...usage } });
9089
9601
  }
9090
9602
  return;
9091
9603
  }
@@ -9136,7 +9648,7 @@ async function runSession(term, config, opts, exitHint) {
9136
9648
  return;
9137
9649
  }
9138
9650
  if (event.kind === "model-changed") {
9139
- screen.setHeader({ model: event.model });
9651
+ screen.setSessionbar({ model: event.model });
9140
9652
  }
9141
9653
  const formatted = formatEvent(event);
9142
9654
  if (formatted.length > 0) {
@@ -9387,6 +9899,7 @@ var init_app = __esm({
9387
9899
  init_session();
9388
9900
  init_paths();
9389
9901
  init_hydra_version();
9902
+ init_update_check();
9390
9903
  init_history();
9391
9904
  init_discovery();
9392
9905
  init_picker();
@@ -9425,6 +9938,7 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
9425
9938
  "foreground",
9426
9939
  "help",
9427
9940
  "info",
9941
+ "json",
9428
9942
  "new",
9429
9943
  "reattach",
9430
9944
  "replace",
@@ -11700,6 +12214,7 @@ function constantTimeEqual(a, b) {
11700
12214
  // src/daemon/routes/sessions.ts
11701
12215
  init_config();
11702
12216
  init_bundle();
12217
+ init_transcript();
11703
12218
  init_types();
11704
12219
  init_hydra_version();
11705
12220
  import * as os3 from "os";
@@ -11782,6 +12297,24 @@ function registerSessionRoutes(app, manager, defaults) {
11782
12297
  );
11783
12298
  reply.code(200).send(bundle);
11784
12299
  });
12300
+ app.get("/v1/sessions/:id/transcript", async (request, reply) => {
12301
+ const raw = request.params.id;
12302
+ const id = await manager.resolveCanonicalId(raw) ?? raw;
12303
+ const exported = await manager.exportBundle(id);
12304
+ if (!exported) {
12305
+ reply.code(404).send({ error: "session not found" });
12306
+ return;
12307
+ }
12308
+ const bundle = encodeBundle({
12309
+ record: exported.record,
12310
+ history: exported.history,
12311
+ promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
12312
+ hydraVersion: HYDRA_VERSION,
12313
+ machine: os3.hostname()
12314
+ });
12315
+ reply.header("Content-Type", "text/markdown; charset=utf-8");
12316
+ reply.code(200).send(bundleToMarkdown(bundle));
12317
+ });
11785
12318
  app.post("/v1/sessions/import", async (request, reply) => {
11786
12319
  const body = request.body ?? {};
11787
12320
  if (body.bundle === void 0) {
@@ -12096,11 +12629,18 @@ function registerAcpWsEndpoint(app, deps) {
12096
12629
  model: hydraMeta.model
12097
12630
  });
12098
12631
  const client = bindClientToSession(connection, session, state);
12099
- await session.attach(client, "full");
12632
+ const { entries: replay } = await session.attach(client, "full");
12100
12633
  state.attached.set(session.sessionId, {
12101
12634
  sessionId: session.sessionId,
12102
12635
  clientId: client.clientId
12103
12636
  });
12637
+ setImmediate(() => {
12638
+ void (async () => {
12639
+ for (const note of replay) {
12640
+ await connection.notify(note.method, note.params).catch(() => void 0);
12641
+ }
12642
+ })();
12643
+ });
12104
12644
  return {
12105
12645
  sessionId: session.sessionId,
12106
12646
  _meta: buildResponseMeta(session)
@@ -12314,6 +12854,9 @@ function buildResponseMeta(session) {
12314
12854
  if (session.currentMode !== void 0) {
12315
12855
  ours.currentMode = session.currentMode;
12316
12856
  }
12857
+ if (session.currentUsage !== void 0) {
12858
+ ours.currentUsage = session.currentUsage;
12859
+ }
12317
12860
  const commands = session.mergedAvailableCommands();
12318
12861
  if (commands.length > 0) {
12319
12862
  ours.availableCommands = commands;
@@ -13715,6 +14258,8 @@ function injectHydraMeta(msg, additions) {
13715
14258
  }
13716
14259
 
13717
14260
  // src/cli.ts
14261
+ init_update_check();
14262
+ var suppressUpdateNotice = false;
13718
14263
  async function main() {
13719
14264
  const argv = process.argv.slice(2);
13720
14265
  const launchIdx = argv.indexOf("launch");
@@ -13746,6 +14291,7 @@ async function main() {
13746
14291
  const sessionId2 = typeof flags2.resume === "string" ? flags2.resume : resolveOption(flags2, "session-id");
13747
14292
  const name2 = resolveOption(flags2, "name");
13748
14293
  const model2 = resolveOption(flags2, "model");
14294
+ suppressUpdateNotice = true;
13749
14295
  await runShim({ sessionId: sessionId2, agentId, agentArgs, name: name2, model: model2 });
13750
14296
  return;
13751
14297
  }
@@ -13770,6 +14316,7 @@ async function main() {
13770
14316
  const model = resolveOption(flags, "model");
13771
14317
  if (!subcommand) {
13772
14318
  if (process.stdout.isTTY) {
14319
+ suppressUpdateNotice = true;
13773
14320
  await dispatchTui(flags, {
13774
14321
  sessionId,
13775
14322
  agentId: agentIdFromFlag,
@@ -13778,11 +14325,13 @@ async function main() {
13778
14325
  });
13779
14326
  return;
13780
14327
  }
14328
+ suppressUpdateNotice = true;
13781
14329
  await runShim({ sessionId, name, agentId: agentIdFromFlag, model });
13782
14330
  return;
13783
14331
  }
13784
14332
  switch (subcommand) {
13785
14333
  case "shim":
14334
+ suppressUpdateNotice = true;
13786
14335
  await runShim({ sessionId, name, agentId: agentIdFromFlag, model });
13787
14336
  return;
13788
14337
  case "init":
@@ -13820,7 +14369,10 @@ async function main() {
13820
14369
  case "sessions": {
13821
14370
  const sub = positional[1];
13822
14371
  if (sub === void 0 || sub === "list") {
13823
- await runSessionsList({ all: flags.all === true });
14372
+ await runSessionsList({
14373
+ all: flags.all === true,
14374
+ json: flags.json === true
14375
+ });
13824
14376
  return;
13825
14377
  }
13826
14378
  if (sub === "kill") {
@@ -13836,6 +14388,11 @@ async function main() {
13836
14388
  await runSessionsExport(positional[2], out);
13837
14389
  return;
13838
14390
  }
14391
+ if (sub === "transcript") {
14392
+ const out = resolveOption(flags, "out");
14393
+ await runSessionsTranscript(positional[2], out);
14394
+ return;
14395
+ }
13839
14396
  if (sub === "import") {
13840
14397
  const cwd = resolveOption(flags, "cwd");
13841
14398
  await runSessionsImport(positional[2], {
@@ -13905,6 +14462,7 @@ async function main() {
13905
14462
  return;
13906
14463
  }
13907
14464
  case "tui":
14465
+ suppressUpdateNotice = true;
13908
14466
  await dispatchTui(flags, {
13909
14467
  sessionId,
13910
14468
  agentId: agentIdFromFlag,
@@ -13978,11 +14536,14 @@ function printHelp() {
13978
14536
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
13979
14537
  " hydra-acp daemon stop|restart|status",
13980
14538
  " hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
13981
- " hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
14539
+ " hydra-acp sessions [list] [--all] [--json]",
14540
+ " List sessions (live + 20 most-recent cold; --all for everything; --json emits the raw daemon response as JSON for scripts)",
13982
14541
  " hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
13983
14542
  " hydra-acp sessions remove <id> Remove a session entirely (live or cold)",
13984
14543
  " hydra-acp sessions export <id> [--out <file>|.]",
13985
14544
  " Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
14545
+ " hydra-acp sessions transcript <id>|<file> [--out <file>|.]",
14546
+ " 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",
13986
14547
  " hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]",
13987
14548
  " 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",
13988
14549
  " hydra-acp extensions list List configured extensions and live state",
@@ -14007,8 +14568,22 @@ function printHelp() {
14007
14568
  ].join("\n")
14008
14569
  );
14009
14570
  }
14010
- main().catch((err) => {
14571
+ async function maybePrintUpdateNotice() {
14572
+ if (suppressUpdateNotice) {
14573
+ return;
14574
+ }
14575
+ try {
14576
+ const info = await getPendingUpdate();
14577
+ if (info) {
14578
+ process.stderr.write(`\u2728 ${formatUpdateNoticeLine(info)}
14579
+ `);
14580
+ }
14581
+ } catch {
14582
+ }
14583
+ }
14584
+ main().then(maybePrintUpdateNotice).catch(async (err) => {
14011
14585
  process.stderr.write(`hydra-acp: ${err.message}
14012
14586
  `);
14587
+ await maybePrintUpdateNotice();
14013
14588
  process.exit(1);
14014
14589
  });