@hydra-acp/cli 0.1.48 → 0.1.50

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.
Files changed (4) hide show
  1. package/dist/cli.js +2983 -655
  2. package/dist/index.d.ts +145 -13
  3. package/dist/index.js +1404 -81
  4. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -930,6 +930,9 @@ function extractHydraMeta(meta) {
930
930
  if (typeof obj.promptUpdating === "boolean") {
931
931
  out.promptUpdating = obj.promptUpdating;
932
932
  }
933
+ if (typeof obj.mcpStdin === "boolean") {
934
+ out.mcpStdin = obj.mcpStdin;
935
+ }
933
936
  if (typeof obj.promptAmending === "boolean") {
934
937
  out.promptAmending = obj.promptAmending;
935
938
  }
@@ -1163,6 +1166,10 @@ var init_types = __esm({
1163
1166
  importedFromUpstreamSessionId: z3.string().optional(),
1164
1167
  // Set when this session was spawned as a child by a transformer.
1165
1168
  parentSessionId: z3.string().optional(),
1169
+ // clientInfo from the process that issued session/new. Lets list views
1170
+ // hide cat-style ancillary sessions by default while letting an
1171
+ // override flag surface them.
1172
+ originatingClient: z3.object({ name: z3.string(), version: z3.string().optional() }).optional(),
1166
1173
  updatedAt: z3.string(),
1167
1174
  attachedClients: z3.number().int().nonnegative(),
1168
1175
  status: z3.enum(["live", "cold"]).default("live"),
@@ -1397,7 +1404,10 @@ var init_connection = __esm({
1397
1404
  // every entry would be re-appended to history.jsonl, doubling the log
1398
1405
  // each time the session was woken up.
1399
1406
  drainBuffered(method) {
1407
+ const buf = this.bufferedNotifications.get(method);
1408
+ const count = buf?.length ?? 0;
1400
1409
  this.bufferedNotifications.delete(method);
1410
+ return count;
1401
1411
  }
1402
1412
  onClose(handler) {
1403
1413
  this.closeHandlers.push(handler);
@@ -1544,18 +1554,34 @@ var init_connection = __esm({
1544
1554
 
1545
1555
  // src/core/stream-buffer.ts
1546
1556
  import * as fsp3 from "fs/promises";
1547
- var DEFAULT_CAPACITY_BYTES, STREAM_READ_MAX_BYTES, STREAM_WAIT_MAX_MS, SessionStreamBuffer;
1557
+ function escapeRegex(s) {
1558
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1559
+ }
1560
+ var DEFAULT_CAPACITY_BYTES, INITIAL_CAPACITY_BYTES, STREAM_READ_MAX_BYTES, STREAM_WAIT_MAX_MS, STREAM_GREP_DEFAULT_MATCHES, STREAM_GREP_MAX_MATCHES, STREAM_GREP_DEFAULT_BYTES, STREAM_GREP_MAX_BYTES, STREAM_GREP_MAX_CONTEXT, SessionStreamBuffer;
1548
1561
  var init_stream_buffer = __esm({
1549
1562
  "src/core/stream-buffer.ts"() {
1550
1563
  "use strict";
1551
- DEFAULT_CAPACITY_BYTES = 16 * 1024 * 1024;
1564
+ DEFAULT_CAPACITY_BYTES = 64 * 1024 * 1024;
1565
+ INITIAL_CAPACITY_BYTES = 1 * 1024 * 1024;
1552
1566
  STREAM_READ_MAX_BYTES = 64 * 1024;
1553
1567
  STREAM_WAIT_MAX_MS = 6e4;
1568
+ STREAM_GREP_DEFAULT_MATCHES = 100;
1569
+ STREAM_GREP_MAX_MATCHES = 1e3;
1570
+ STREAM_GREP_DEFAULT_BYTES = 64 * 1024;
1571
+ STREAM_GREP_MAX_BYTES = 256 * 1024;
1572
+ STREAM_GREP_MAX_CONTEXT = 20;
1554
1573
  SessionStreamBuffer = class {
1555
1574
  storage;
1556
- capacityBytes;
1575
+ // The configured cap. Eviction begins once writeCursor exceeds this.
1576
+ maxCapacityBytes;
1577
+ // The size of the currently-allocated `storage`. Starts at
1578
+ // INITIAL_CAPACITY_BYTES (clamped to maxCapacityBytes) and doubles on
1579
+ // demand. Once it reaches maxCapacityBytes the ring behaves like a
1580
+ // fixed-size buffer; before then, writeCursor < currentCapacityBytes
1581
+ // always, so no wrap-around math is in play.
1582
+ currentCapacityBytes;
1557
1583
  // Absolute monotonic byte offset of the next byte to be written. Also
1558
- // the count of bytes ever appended. `writeCursor - capacityBytes`
1584
+ // the count of bytes ever appended. `writeCursor - currentCapacityBytes`
1559
1585
  // (clamped at 0) is the oldest still-resident byte's cursor.
1560
1586
  writeCursor = 0;
1561
1587
  closed = false;
@@ -1570,24 +1596,33 @@ var init_stream_buffer = __esm({
1570
1596
  // calls don't interleave their writes.
1571
1597
  fileWriteChain = Promise.resolve();
1572
1598
  constructor(opts = {}) {
1573
- this.capacityBytes = opts.capacityBytes ?? DEFAULT_CAPACITY_BYTES;
1574
- if (this.capacityBytes <= 0) {
1599
+ this.maxCapacityBytes = opts.capacityBytes ?? DEFAULT_CAPACITY_BYTES;
1600
+ if (this.maxCapacityBytes <= 0) {
1575
1601
  throw new Error("capacityBytes must be > 0");
1576
1602
  }
1577
- this.storage = Buffer.alloc(this.capacityBytes);
1603
+ this.currentCapacityBytes = Math.min(
1604
+ INITIAL_CAPACITY_BYTES,
1605
+ this.maxCapacityBytes
1606
+ );
1607
+ this.storage = Buffer.alloc(this.currentCapacityBytes);
1578
1608
  this.filePath = opts.filePath;
1579
1609
  this.fileCapBytes = opts.fileCapBytes ?? Number.POSITIVE_INFINITY;
1580
1610
  this.onFileCapReached = opts.onFileCapReached;
1581
1611
  this.logWriteError = opts.logWriteError;
1582
1612
  }
1583
1613
  get capacity() {
1584
- return this.capacityBytes;
1614
+ return this.maxCapacityBytes;
1615
+ }
1616
+ // Currently-allocated storage size (for observability / tests). May be
1617
+ // anywhere between INITIAL_CAPACITY_BYTES and capacity.
1618
+ get allocatedBytes() {
1619
+ return this.currentCapacityBytes;
1585
1620
  }
1586
1621
  get writeCursorPos() {
1587
1622
  return this.writeCursor;
1588
1623
  }
1589
1624
  get oldestAvailable() {
1590
- return Math.max(0, this.writeCursor - this.capacityBytes);
1625
+ return Math.max(0, this.writeCursor - this.currentCapacityBytes);
1591
1626
  }
1592
1627
  get isClosed() {
1593
1628
  return this.closed;
@@ -1732,6 +1767,136 @@ var init_stream_buffer = __esm({
1732
1767
  this.waiters.push(waiter);
1733
1768
  });
1734
1769
  }
1770
+ // Scan the resident region line-by-line, returning lines that match
1771
+ // `pattern`. Server-side filtering so the agent doesn't have to pull
1772
+ // and decode 64 KiB base64 windows just to grep a multi-MB log.
1773
+ //
1774
+ // Lines are split on `\n` (LF). A trailing partial line (no LF) is
1775
+ // skipped when the buffer is still open — its bytes might be the
1776
+ // start of a longer line that's still being written — but is treated
1777
+ // as a final full line once the buffer is closed.
1778
+ //
1779
+ // Caps: max 1000 matches and 256 KiB of output bytes per call. The
1780
+ // agent should re-call with `cursor = nextCursor` to resume when
1781
+ // `truncated:true`.
1782
+ grep(opts) {
1783
+ const oldest = this.oldestAvailable;
1784
+ const requested = opts.cursor;
1785
+ let start = requested ?? oldest;
1786
+ let gap = 0;
1787
+ if (requested !== void 0 && requested < oldest) {
1788
+ gap = oldest - requested;
1789
+ start = oldest;
1790
+ }
1791
+ if (start > this.writeCursor) {
1792
+ start = this.writeCursor;
1793
+ }
1794
+ const slice = this.sliceFromRing(start, this.writeCursor - start);
1795
+ const useRegex = opts.regex ?? true;
1796
+ const flags = opts.caseInsensitive === true ? "i" : "";
1797
+ const re = useRegex ? new RegExp(opts.pattern, flags) : new RegExp(escapeRegex(opts.pattern), flags);
1798
+ const invert = opts.invert ?? false;
1799
+ const maxMatches = Math.max(
1800
+ 1,
1801
+ Math.min(
1802
+ opts.maxMatches ?? STREAM_GREP_DEFAULT_MATCHES,
1803
+ STREAM_GREP_MAX_MATCHES
1804
+ )
1805
+ );
1806
+ const maxBytes = Math.max(
1807
+ 1,
1808
+ Math.min(opts.maxBytes ?? STREAM_GREP_DEFAULT_BYTES, STREAM_GREP_MAX_BYTES)
1809
+ );
1810
+ const contextBefore = Math.max(
1811
+ 0,
1812
+ Math.min(opts.contextBefore ?? 0, STREAM_GREP_MAX_CONTEXT)
1813
+ );
1814
+ const contextAfter = Math.max(
1815
+ 0,
1816
+ Math.min(opts.contextAfter ?? 0, STREAM_GREP_MAX_CONTEXT)
1817
+ );
1818
+ const matches = [];
1819
+ const beforeRing = [];
1820
+ const pendingAfter = [];
1821
+ let bytesUsed = 0;
1822
+ let truncated = false;
1823
+ let lineStartByte = 0;
1824
+ let resumeFromLineStart = 0;
1825
+ const processLine = (lineCursor, lineText) => {
1826
+ for (const pa of pendingAfter) {
1827
+ if (pa.remaining > 0) {
1828
+ if (pa.match.after === void 0) {
1829
+ pa.match.after = [];
1830
+ }
1831
+ pa.match.after.push({ cursor: lineCursor, line: lineText });
1832
+ pa.remaining--;
1833
+ bytesUsed += lineText.length;
1834
+ }
1835
+ }
1836
+ while (pendingAfter.length > 0 && pendingAfter[0].remaining === 0) {
1837
+ pendingAfter.shift();
1838
+ }
1839
+ const matched = re.test(lineText) !== invert;
1840
+ if (matched && matches.length < maxMatches) {
1841
+ const m = { cursor: lineCursor, line: lineText };
1842
+ if (contextBefore > 0 && beforeRing.length > 0) {
1843
+ m.before = beforeRing.slice();
1844
+ for (const b of m.before) {
1845
+ bytesUsed += b.line.length;
1846
+ }
1847
+ }
1848
+ bytesUsed += lineText.length;
1849
+ matches.push(m);
1850
+ if (contextAfter > 0) {
1851
+ pendingAfter.push({ match: m, remaining: contextAfter });
1852
+ }
1853
+ }
1854
+ if (contextBefore > 0) {
1855
+ beforeRing.push({ cursor: lineCursor, line: lineText });
1856
+ while (beforeRing.length > contextBefore) {
1857
+ beforeRing.shift();
1858
+ }
1859
+ }
1860
+ const hitMaxMatches = matches.length >= maxMatches && pendingAfter.length === 0;
1861
+ const hitMaxBytes = bytesUsed >= maxBytes;
1862
+ return hitMaxMatches || hitMaxBytes;
1863
+ };
1864
+ for (let i = 0; i < slice.length; i++) {
1865
+ if (slice[i] !== 10) {
1866
+ continue;
1867
+ }
1868
+ const lineText = slice.subarray(lineStartByte, i).toString("utf8");
1869
+ const lineCursor = start + lineStartByte;
1870
+ lineStartByte = i + 1;
1871
+ resumeFromLineStart = lineStartByte;
1872
+ if (processLine(lineCursor, lineText)) {
1873
+ truncated = true;
1874
+ break;
1875
+ }
1876
+ }
1877
+ if (!truncated && lineStartByte < slice.length && this.closed) {
1878
+ const lineText = slice.subarray(lineStartByte).toString("utf8");
1879
+ const lineCursor = start + lineStartByte;
1880
+ if (processLine(lineCursor, lineText)) {
1881
+ truncated = true;
1882
+ }
1883
+ resumeFromLineStart = slice.length;
1884
+ }
1885
+ const nextCursor = Math.min(start + resumeFromLineStart, this.writeCursor);
1886
+ const result = {
1887
+ matches,
1888
+ truncated,
1889
+ nextCursor,
1890
+ scannedBytes: resumeFromLineStart
1891
+ };
1892
+ if (gap > 0) {
1893
+ result.gap = gap;
1894
+ }
1895
+ if (this.closed && nextCursor >= this.writeCursor) {
1896
+ result.eof = true;
1897
+ }
1898
+ return result;
1899
+ }
1735
1900
  wakeWaiters(outcome) {
1736
1901
  if (this.waiters.length === 0) {
1737
1902
  return;
@@ -1742,15 +1907,42 @@ var init_stream_buffer = __esm({
1742
1907
  w.resolve(outcome);
1743
1908
  }
1744
1909
  }
1910
+ // Grow `storage` if needed to fit `additionalBytes` more bytes without
1911
+ // wrapping. Caps at maxCapacityBytes; once we're at the cap, callers
1912
+ // fall back to ring-wrap behavior. Doubles each grow so we amortize.
1913
+ // Only called before we've ever wrapped (writeCursor < currentCapacity
1914
+ // always holds while we're growing), so the existing bytes live at
1915
+ // storage[0..writeCursor] and we can just copy them flat.
1916
+ growIfNeeded(additionalBytes) {
1917
+ if (this.currentCapacityBytes >= this.maxCapacityBytes) {
1918
+ return;
1919
+ }
1920
+ const needed = this.writeCursor + additionalBytes;
1921
+ if (needed <= this.currentCapacityBytes) {
1922
+ return;
1923
+ }
1924
+ let next = this.currentCapacityBytes;
1925
+ while (next < needed && next < this.maxCapacityBytes) {
1926
+ next = Math.min(this.maxCapacityBytes, next * 2);
1927
+ }
1928
+ if (next === this.currentCapacityBytes) {
1929
+ return;
1930
+ }
1931
+ const newStorage = Buffer.alloc(next);
1932
+ this.storage.copy(newStorage, 0, 0, this.writeCursor);
1933
+ this.storage = newStorage;
1934
+ this.currentCapacityBytes = next;
1935
+ }
1745
1936
  writeRing(chunk) {
1746
1937
  const len = chunk.length;
1747
- if (len >= this.capacityBytes) {
1748
- const tailStart = len - this.capacityBytes;
1938
+ this.growIfNeeded(len);
1939
+ if (len >= this.currentCapacityBytes) {
1940
+ const tailStart = len - this.currentCapacityBytes;
1749
1941
  chunk.copy(this.storage, 0, tailStart, len);
1750
1942
  return;
1751
1943
  }
1752
- const offset = this.writeCursor % this.capacityBytes;
1753
- const tailRoom = this.capacityBytes - offset;
1944
+ const offset = this.writeCursor % this.currentCapacityBytes;
1945
+ const tailRoom = this.currentCapacityBytes - offset;
1754
1946
  if (len <= tailRoom) {
1755
1947
  chunk.copy(this.storage, offset, 0, len);
1756
1948
  } else {
@@ -1763,8 +1955,8 @@ var init_stream_buffer = __esm({
1763
1955
  return Buffer.alloc(0);
1764
1956
  }
1765
1957
  const out = Buffer.alloc(length);
1766
- const offset = fromCursor % this.capacityBytes;
1767
- const tailLen = Math.min(length, this.capacityBytes - offset);
1958
+ const offset = fromCursor % this.currentCapacityBytes;
1959
+ const tailLen = Math.min(length, this.currentCapacityBytes - offset);
1768
1960
  this.storage.copy(out, 0, offset, offset + tailLen);
1769
1961
  if (tailLen < length) {
1770
1962
  this.storage.copy(out, tailLen, 0, length - tailLen);
@@ -2188,6 +2380,7 @@ var init_session = __esm({
2188
2380
  agentCapabilities;
2189
2381
  agentArgs;
2190
2382
  parentSessionId;
2383
+ originatingClient;
2191
2384
  title;
2192
2385
  // Snapshot state delivered to attaching clients via the attach
2193
2386
  // response _meta rather than via history replay (which would be
@@ -2265,6 +2458,8 @@ var init_session = __esm({
2265
2458
  listSessions;
2266
2459
  logger;
2267
2460
  transformChain;
2461
+ extensionCommands;
2462
+ extensionCommandsUnsub;
2268
2463
  // Outstanding "processing" claims: token → claim waiting for respondsTo discharge.
2269
2464
  pendingClaims = /* @__PURE__ */ new Map();
2270
2465
  agentChangeHandlers = [];
@@ -2340,6 +2535,7 @@ var init_session = __esm({
2340
2535
  this.agentCapabilities = init.agentCapabilities;
2341
2536
  this.agentArgs = init.agentArgs;
2342
2537
  this.parentSessionId = init.parentSessionId;
2538
+ this.originatingClient = init.originatingClient;
2343
2539
  this.title = init.title;
2344
2540
  this.currentModel = init.currentModel;
2345
2541
  this.currentMode = init.currentMode;
@@ -2360,6 +2556,14 @@ var init_session = __esm({
2360
2556
  this.listSessions = init.listSessions;
2361
2557
  this.logger = init.logger;
2362
2558
  this.transformChain = init.transformChain ?? [];
2559
+ this.extensionCommands = init.extensionCommands;
2560
+ if (this.extensionCommands) {
2561
+ this.extensionCommandsUnsub = this.extensionCommands.onChange(() => {
2562
+ if (!this.closed) {
2563
+ this.broadcastMergedCommands();
2564
+ }
2565
+ });
2566
+ }
2363
2567
  if (init.firstPromptSeeded) {
2364
2568
  this.firstPromptSeeded = true;
2365
2569
  }
@@ -2374,18 +2578,11 @@ var init_session = __esm({
2374
2578
  this.notifyChain("session.opened", {});
2375
2579
  }
2376
2580
  broadcastMergedCommands() {
2377
- const merged = [
2378
- ...hydraCommandsAsAdvertised(),
2379
- { name: "model <model-id>", description: "Switch model; omit arg to list available models" },
2380
- { name: "sessions", description: "List all sessions" },
2381
- { name: "help", description: "Show available commands" },
2382
- ...this.agentAdvertisedCommands
2383
- ];
2384
2581
  this.recordAndBroadcast("session/update", {
2385
2582
  sessionId: this.upstreamSessionId,
2386
2583
  update: {
2387
2584
  sessionUpdate: "available_commands_update",
2388
- availableCommands: merged
2585
+ availableCommands: this.mergedAvailableCommands()
2389
2586
  }
2390
2587
  });
2391
2588
  }
@@ -3548,6 +3745,9 @@ var init_session = __esm({
3548
3745
  if (!trimmed || trimmed === this.currentModel) {
3549
3746
  return true;
3550
3747
  }
3748
+ this.logger?.info(
3749
+ `live current_model_update: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
3750
+ );
3551
3751
  this.currentModel = trimmed;
3552
3752
  for (const handler of this.modelHandlers) {
3553
3753
  try {
@@ -3593,6 +3793,9 @@ var init_session = __esm({
3593
3793
  if (typeof cv === "string") {
3594
3794
  const trimmed = cv.trim();
3595
3795
  if (trimmed && trimmed !== this.currentModel) {
3796
+ this.logger?.info(
3797
+ `live config_option_update(model): sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
3798
+ );
3596
3799
  this.currentModel = trimmed;
3597
3800
  for (const handler of this.modelHandlers) {
3598
3801
  try {
@@ -3761,6 +3964,9 @@ var init_session = __esm({
3761
3964
  this.broadcastAvailableModes();
3762
3965
  }
3763
3966
  setAgentAdvertisedModels(models) {
3967
+ this.logger?.info(
3968
+ `setAgentAdvertisedModels: sessionId=${this.sessionId} currentModel=${JSON.stringify(this.currentModel)} newList=[${models.map((m) => m.modelId).join(",")}]`
3969
+ );
3764
3970
  if (sameAdvertisedModels(this.agentAdvertisedModels, models)) {
3765
3971
  this.broadcastAvailableModels();
3766
3972
  return;
@@ -3792,6 +3998,38 @@ var init_session = __esm({
3792
3998
  onModeChange(handler) {
3793
3999
  this.modeHandlers.push(handler);
3794
4000
  }
4001
+ // Apply a model change initiated by a client request (session/set_model)
4002
+ // when the agent doesn't emit a current_model_update notification, or
4003
+ // emits a non-spec shape (e.g. config_option_update). Fires modelHandlers
4004
+ // (persistence) and broadcasts a synthetic current_model_update so all
4005
+ // attached clients — including the originator — repaint immediately.
4006
+ applyModelChange(modelId) {
4007
+ const trimmed = modelId.trim();
4008
+ if (!trimmed || trimmed === this.currentModel) {
4009
+ return;
4010
+ }
4011
+ this.logger?.info(
4012
+ `applyModelChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
4013
+ );
4014
+ this.currentModel = trimmed;
4015
+ for (const handler of this.modelHandlers) {
4016
+ try {
4017
+ handler(trimmed);
4018
+ } catch {
4019
+ }
4020
+ }
4021
+ const update = {
4022
+ sessionUpdate: "current_model_update",
4023
+ currentModel: trimmed
4024
+ };
4025
+ if (this.agentAdvertisedModels.length > 0) {
4026
+ update.availableModels = [...this.agentAdvertisedModels];
4027
+ }
4028
+ this.recordAndBroadcast("session/update", {
4029
+ sessionId: this.upstreamSessionId,
4030
+ update
4031
+ });
4032
+ }
3795
4033
  // Apply a mode change initiated by a client request (session/set_mode)
3796
4034
  // when the agent doesn't emit a current_mode_update notification on its
3797
4035
  // own. Fires modeHandlers so the persistence hook and any other listeners
@@ -3815,11 +4053,31 @@ var init_session = __esm({
3815
4053
  onUsageChange(handler) {
3816
4054
  this.usageHandlers.push(handler);
3817
4055
  }
3818
- // Returns a freshly merged command list (hydra ∪ agent) for callers
3819
- // that need a snapshot — notably acp-ws.ts's buildResponseMeta when
3820
- // assembling the attach response.
4056
+ // Returns a freshly merged command list (hydra ∪ extension ∪ agent) for
4057
+ // callers that need a snapshot — notably acp-ws.ts's buildResponseMeta
4058
+ // when assembling the attach response. Order: built-in hydra verbs,
4059
+ // top-level daemon verbs (/model, /sessions, /help), extension-registered
4060
+ // entries, then whatever the agent advertised.
3821
4061
  mergedAvailableCommands() {
3822
- return [...hydraCommandsAsAdvertised(), ...this.agentAdvertisedCommands];
4062
+ const out = [
4063
+ ...hydraCommandsAsAdvertised(),
4064
+ { name: "model <model-id>", description: "Switch model; omit arg to list available models" },
4065
+ { name: "sessions", description: "List all sessions" },
4066
+ { name: "help", description: "Show available commands" }
4067
+ ];
4068
+ if (this.extensionCommands) {
4069
+ for (const { name, command } of this.extensionCommands.list()) {
4070
+ const head = `hydra ${name} ${command.verb}`;
4071
+ const display = command.argsHint ? `${head} ${command.argsHint}` : head;
4072
+ const entry = { name: display };
4073
+ if (command.description) {
4074
+ entry.description = command.description;
4075
+ }
4076
+ out.push(entry);
4077
+ }
4078
+ }
4079
+ out.push(...this.agentAdvertisedCommands);
4080
+ return out;
3823
4081
  }
3824
4082
  // The agent's own advertised commands (not merged with hydra verbs).
3825
4083
  // Used by SessionManager to persist into meta.json so cold resurrect
@@ -3871,39 +4129,118 @@ var init_session = __esm({
3871
4129
  // caller's promise resolves like a normal turn. To add a verb: append
3872
4130
  // an entry to HYDRA_COMMANDS (drives validation + client advertising)
3873
4131
  // and a dispatch case in the switch below.
4132
+ //
4133
+ // Extensions/transformers can also bind verbs via the
4134
+ // ExtensionCommandRegistry: "/hydra <process-name> <verb> [args]" routes
4135
+ // to that process's WS connection. Built-in hydra verbs win on name
4136
+ // collision so an extension can never shadow them.
3874
4137
  async handleSlashCommand(text) {
3875
4138
  const rest = text.slice("/hydra".length).trim();
3876
4139
  const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
3877
- const verb = match?.[1] ?? "";
3878
- const arg = (match?.[2] ?? "").trim();
3879
- if (verb === "") {
4140
+ const first = match?.[1] ?? "";
4141
+ const remainder = (match?.[2] ?? "").trim();
4142
+ if (first === "") {
3880
4143
  return { stopReason: "end_turn" };
3881
4144
  }
3882
- if (!HYDRA_COMMANDS.some((c) => c.verb === verb)) {
3883
- const known = HYDRA_COMMANDS.map((c) => c.verb).join(", ");
3884
- const err = new Error(
3885
- `unknown /hydra verb: ${verb} (known: ${known})`
3886
- );
3887
- err.code = JsonRpcErrorCodes.InvalidParams;
3888
- throw err;
4145
+ if (HYDRA_COMMANDS.some((c) => c.verb === first)) {
4146
+ switch (first) {
4147
+ case "title":
4148
+ return this.runTitleCommand(remainder);
4149
+ case "agent":
4150
+ return this.runAgentCommand(remainder);
4151
+ case "kill":
4152
+ return this.runKillCommand();
4153
+ case "restart":
4154
+ return this.runRestartCommand();
4155
+ default: {
4156
+ const err2 = new Error(
4157
+ `no dispatcher for /hydra verb ${first}`
4158
+ );
4159
+ err2.code = JsonRpcErrorCodes.InternalError;
4160
+ throw err2;
4161
+ }
4162
+ }
3889
4163
  }
3890
- switch (verb) {
3891
- case "title":
3892
- return this.runTitleCommand(arg);
3893
- case "agent":
3894
- return this.runAgentCommand(arg);
3895
- case "kill":
3896
- return this.runKillCommand();
3897
- case "restart":
3898
- return this.runRestartCommand();
3899
- default: {
3900
- const err = new Error(
3901
- `no dispatcher for /hydra verb ${verb}`
3902
- );
3903
- err.code = JsonRpcErrorCodes.InternalError;
3904
- throw err;
4164
+ if (this.extensionCommands?.has(first)) {
4165
+ return this.runExtensionCommand(first, remainder);
4166
+ }
4167
+ const known = HYDRA_COMMANDS.map((c) => c.verb);
4168
+ if (this.extensionCommands) {
4169
+ const seen = /* @__PURE__ */ new Set();
4170
+ for (const { name } of this.extensionCommands.list()) {
4171
+ if (!seen.has(name)) {
4172
+ known.push(name);
4173
+ seen.add(name);
4174
+ }
3905
4175
  }
3906
4176
  }
4177
+ const err = new Error(
4178
+ `unknown /hydra verb: ${first} (known: ${known.join(", ")})`
4179
+ );
4180
+ err.code = JsonRpcErrorCodes.InvalidParams;
4181
+ throw err;
4182
+ }
4183
+ // "/hydra <name> <verb> [args]" — name matches a registered extension
4184
+ // or transformer. We split the remainder into verb + args, validate the
4185
+ // verb against what the process advertised, and forward as a
4186
+ // hydra-acp/extension_command request on the process's WS connection.
4187
+ // The reply's text (if any) is broadcast as a synthetic
4188
+ // agent_message_chunk so it appears in the conversation alongside the
4189
+ // user's invocation.
4190
+ runExtensionCommand(name, remainder) {
4191
+ return this.enqueuePrompt(async () => {
4192
+ const entry = this.extensionCommands?.get(name);
4193
+ if (!entry) {
4194
+ return this.emitExtensionReply(
4195
+ `extension "${name}" is no longer connected`
4196
+ );
4197
+ }
4198
+ const m = remainder.match(/^(\S+)(?:\s+([\s\S]*))?$/);
4199
+ const verb = m?.[1] ?? "";
4200
+ const args = (m?.[2] ?? "").trim();
4201
+ if (verb === "") {
4202
+ const verbs = entry.commands.map((c) => c.verb).join(", ");
4203
+ return this.emitExtensionReply(
4204
+ `/hydra ${name} requires a verb (known: ${verbs || "(none)"})`
4205
+ );
4206
+ }
4207
+ if (!entry.commands.some((c) => c.verb === verb)) {
4208
+ const verbs = entry.commands.map((c) => c.verb).join(", ");
4209
+ return this.emitExtensionReply(
4210
+ `unknown verb "${verb}" for ${name} (known: ${verbs || "(none)"})`
4211
+ );
4212
+ }
4213
+ let reply;
4214
+ try {
4215
+ reply = await entry.connection.request("hydra-acp/extension_command", {
4216
+ sessionId: this.sessionId,
4217
+ verb,
4218
+ args
4219
+ });
4220
+ } catch (err) {
4221
+ return this.emitExtensionReply(
4222
+ `${name} ${verb}: ${err.message}`
4223
+ );
4224
+ }
4225
+ const text = reply && typeof reply === "object" && typeof reply.text === "string" ? reply.text : "";
4226
+ if (text.length > 0) {
4227
+ return this.emitExtensionReply(text);
4228
+ }
4229
+ return { stopReason: "end_turn" };
4230
+ });
4231
+ }
4232
+ emitExtensionReply(text) {
4233
+ this.recordAndBroadcast("session/update", {
4234
+ sessionId: this.upstreamSessionId,
4235
+ update: {
4236
+ sessionUpdate: "agent_message_chunk",
4237
+ content: { type: "text", text: `
4238
+ ${text}
4239
+ ` },
4240
+ _meta: { "hydra-acp": { synthetic: true } }
4241
+ }
4242
+ });
4243
+ return { stopReason: "end_turn" };
3907
4244
  }
3908
4245
  async handleSessionsCommand() {
3909
4246
  let text;
@@ -3966,11 +4303,15 @@ ${text}
3966
4303
  if (models.length === 0) {
3967
4304
  body = current ? `Current model: ${current}` : "_(no models advertised yet)_";
3968
4305
  } else {
4306
+ const inList = current ? models.some((m) => m.modelId === current) : true;
3969
4307
  const lines = models.map((m) => {
3970
4308
  const marker = m.modelId === current ? " \u25C0" : "";
3971
4309
  const desc = m.name && m.name !== m.modelId ? ` ${m.name}` : "";
3972
4310
  return `${m.modelId}${marker}${desc}`;
3973
4311
  });
4312
+ if (!inList && current) {
4313
+ lines.unshift(`${current} \u25C0`);
4314
+ }
3974
4315
  body = lines.join("\n");
3975
4316
  }
3976
4317
  this.recordAndBroadcast("session/update", {
@@ -4382,6 +4723,43 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
4382
4723
  }
4383
4724
  return out;
4384
4725
  }
4726
+ streamTail(bytes) {
4727
+ const buf = this.requireStreamBuffer();
4728
+ const r = buf.tail(bytes);
4729
+ return {
4730
+ bytes: r.bytes.toString("base64"),
4731
+ startCursor: r.startCursor,
4732
+ endCursor: r.endCursor,
4733
+ truncated: r.truncated
4734
+ };
4735
+ }
4736
+ streamHead(bytes) {
4737
+ const buf = this.requireStreamBuffer();
4738
+ const r = buf.head(bytes);
4739
+ return {
4740
+ bytes: r.bytes.toString("base64"),
4741
+ startCursor: r.startCursor,
4742
+ endCursor: r.endCursor,
4743
+ truncated: r.truncated
4744
+ };
4745
+ }
4746
+ async streamWaitFor(cursor, timeoutMs) {
4747
+ const buf = this.requireStreamBuffer();
4748
+ return buf.waitForData(cursor, timeoutMs);
4749
+ }
4750
+ streamGrep(opts) {
4751
+ const buf = this.requireStreamBuffer();
4752
+ return buf.grep(opts);
4753
+ }
4754
+ streamInfo() {
4755
+ const buf = this.requireStreamBuffer();
4756
+ return {
4757
+ writeCursor: buf.writeCursorPos,
4758
+ oldestAvailable: buf.oldestAvailable,
4759
+ capacity: buf.capacity,
4760
+ closed: buf.isClosed
4761
+ };
4762
+ }
4385
4763
  requireStreamBuffer() {
4386
4764
  if (this.streamBuffer === void 0) {
4387
4765
  const err = new Error(
@@ -4398,6 +4776,10 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
4398
4776
  }
4399
4777
  this.closed = true;
4400
4778
  this.cancelIdleTimer();
4779
+ if (this.extensionCommandsUnsub) {
4780
+ this.extensionCommandsUnsub();
4781
+ this.extensionCommandsUnsub = void 0;
4782
+ }
4401
4783
  if (this.currentEntry?.kind === "user") {
4402
4784
  this.broadcastTurnComplete(
4403
4785
  this.currentEntry.clientId,
@@ -4968,11 +5350,12 @@ function resolveVersion() {
4968
5350
  }
4969
5351
  return "0.0.0";
4970
5352
  }
4971
- var HYDRA_VERSION;
5353
+ var HYDRA_VERSION, HYDRA_CAT_CLIENT_NAME;
4972
5354
  var init_hydra_version = __esm({
4973
5355
  "src/core/hydra-version.ts"() {
4974
5356
  "use strict";
4975
5357
  HYDRA_VERSION = resolveVersion();
5358
+ HYDRA_CAT_CLIENT_NAME = "hydra-acp-cat";
4976
5359
  }
4977
5360
  });
4978
5361
 
@@ -5560,7 +5943,8 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
5560
5943
  title: s.title,
5561
5944
  importedFromMachine: s.importedFromMachine,
5562
5945
  importedFromUpstreamSessionId: s.importedFromUpstreamSessionId,
5563
- busy: s.busy
5946
+ busy: s.busy,
5947
+ originatingClient: s.originatingClient
5564
5948
  }));
5565
5949
  }
5566
5950
  async function killSession(target, id, fetchImpl = fetch) {
@@ -5598,6 +5982,20 @@ async function regenSessionTitle(target, id, fetchImpl = fetch) {
5598
5982
  throw new Error(`daemon returned HTTP ${response.status}`);
5599
5983
  }
5600
5984
  }
5985
+ async function searchSessions(target, query, opts = {}, fetchImpl = fetch) {
5986
+ const url = new URL(`${target.baseUrl}/v1/sessions/search`);
5987
+ url.searchParams.set("q", query);
5988
+ if (opts.sessionIds && opts.sessionIds.length > 0) {
5989
+ url.searchParams.set("sessionIds", opts.sessionIds.join(","));
5990
+ }
5991
+ const response = await fetchImpl(url.toString(), {
5992
+ headers: { Authorization: `Bearer ${target.token}` }
5993
+ });
5994
+ if (!response.ok) {
5995
+ throw new Error(`daemon returned HTTP ${response.status}`);
5996
+ }
5997
+ return await response.json();
5998
+ }
5601
5999
  async function deleteSession(target, id, fetchImpl = fetch) {
5602
6000
  const response = await fetchImpl(`${target.baseUrl}/v1/sessions/${id}`, {
5603
6001
  method: "DELETE",
@@ -6066,6 +6464,47 @@ var init_resilient_ws = __esm({
6066
6464
  }
6067
6465
  });
6068
6466
 
6467
+ // src/acp/permission-pick.ts
6468
+ function pickPermissionOptionId(params, preferredKinds) {
6469
+ const options = params && typeof params === "object" ? params.options : void 0;
6470
+ if (Array.isArray(options)) {
6471
+ for (const kind of preferredKinds) {
6472
+ const match = options.find(
6473
+ (o) => typeof o === "object" && o !== null && o.kind === kind && typeof o.optionId === "string"
6474
+ );
6475
+ if (match?.optionId !== void 0) {
6476
+ return match.optionId;
6477
+ }
6478
+ }
6479
+ const fallback = options.find(
6480
+ (o) => typeof o === "object" && o !== null && typeof o.optionId === "string"
6481
+ );
6482
+ if (fallback?.optionId !== void 0) {
6483
+ return fallback.optionId;
6484
+ }
6485
+ }
6486
+ return preferredKinds[0] ?? "allow";
6487
+ }
6488
+ function buildApproveResponse(params) {
6489
+ const optionId = pickPermissionOptionId(params, [
6490
+ "allow_once",
6491
+ "allow_always"
6492
+ ]);
6493
+ return { outcome: { outcome: "selected", optionId } };
6494
+ }
6495
+ function buildRejectResponse(params) {
6496
+ const optionId = pickPermissionOptionId(params, [
6497
+ "reject_once",
6498
+ "reject_always"
6499
+ ]);
6500
+ return { outcome: { outcome: "selected", optionId } };
6501
+ }
6502
+ var init_permission_pick = __esm({
6503
+ "src/acp/permission-pick.ts"() {
6504
+ "use strict";
6505
+ }
6506
+ });
6507
+
6069
6508
  // src/core/update-check.ts
6070
6509
  function disabled() {
6071
6510
  if (process.env.NO_UPDATE_NOTIFIER === "1") {
@@ -7277,7 +7716,7 @@ function writeStyled(term, text, style) {
7277
7716
  term(text);
7278
7717
  return;
7279
7718
  case "thought":
7280
- term.brightBlack.noFormat(text);
7719
+ term.brightBlack(text);
7281
7720
  return;
7282
7721
  case "tool":
7283
7722
  term.brightBlue.noFormat(text);
@@ -9982,11 +10421,8 @@ uncaught: ${err.stack ?? err.message}
9982
10421
  }
9983
10422
  });
9984
10423
 
9985
- // src/tui/picker.ts
9986
- function createPickerPrefs() {
9987
- return { filters: { cwdOnly: false, hostFilter: "__local" } };
9988
- }
9989
- async function pickSession(term, opts) {
10424
+ // src/tui/prompt-utils.ts
10425
+ function resetTerminalModes() {
9990
10426
  process.stdout.write("\x1B[<u");
9991
10427
  process.stdout.write("\x1B[?2004l");
9992
10428
  process.stdout.write("\x1B[>4;0m");
@@ -9996,36 +10432,476 @@ async function pickSession(term, opts) {
9996
10432
  process.stdout.write("\x1B[?1006l");
9997
10433
  process.stdout.write("\x1B[?1l");
9998
10434
  process.stdout.write("\x1B>");
9999
- const sortSessions = (sessions) => {
10000
- const score = (s) => {
10001
- if (s.status !== "live") {
10002
- return 0;
10003
- }
10004
- return s.cwd === opts.cwd ? 2 : 1;
10005
- };
10006
- return [...sessions].sort((a, b) => {
10007
- const tier = score(b) - score(a);
10008
- if (tier !== 0) {
10009
- return tier;
10010
- }
10011
- return b.updatedAt.slice(0, 16).localeCompare(a.updatedAt.slice(0, 16));
10012
- });
10435
+ }
10436
+ function readTermWidth(term) {
10437
+ return term.width ?? 80;
10438
+ }
10439
+ function readTermHeight(term) {
10440
+ return term.height ?? 24;
10441
+ }
10442
+ function drawBox(term, opts) {
10443
+ const termW = readTermWidth(term);
10444
+ const termH = readTermHeight(term);
10445
+ const desiredContentW = opts.contentWidth ?? MAX_BOX_WIDTH;
10446
+ const maxContentW = Math.max(10, Math.min(MAX_BOX_WIDTH, termW - 4));
10447
+ const contentW = Math.min(desiredContentW, maxContentW);
10448
+ const w = contentW + 2;
10449
+ const contentH = Math.max(1, Math.min(opts.contentHeight, termH - 4));
10450
+ const h = contentH + 2;
10451
+ const x = Math.max(1, Math.floor((termW - w) / 2) + 1);
10452
+ const y = Math.max(1, Math.floor((termH - h) / 2) + 1);
10453
+ term.moveTo(1, 1).eraseDisplayBelow();
10454
+ const topInner = HORIZ.repeat(w - 2);
10455
+ const top = renderTitleStrip(topInner, opts.title);
10456
+ term.moveTo(x, y);
10457
+ term.dim.noFormat(TL);
10458
+ paintTopStrip(term, top);
10459
+ term.dim.noFormat(TR);
10460
+ for (let row = 1; row <= contentH; row++) {
10461
+ term.moveTo(x, y + row);
10462
+ term.dim.noFormat(VERT);
10463
+ term.moveTo(x + w - 1, y + row);
10464
+ term.dim.noFormat(VERT);
10465
+ }
10466
+ term.moveTo(x, y + h - 1);
10467
+ term.dim.noFormat(BL + HORIZ.repeat(w - 2) + BR);
10468
+ return {
10469
+ x,
10470
+ y,
10471
+ w,
10472
+ h,
10473
+ contentX: x + 1,
10474
+ contentY: y + 1,
10475
+ contentW,
10476
+ contentH
10013
10477
  };
10014
- const prefs = opts.prefs ?? createPickerPrefs();
10015
- if (opts.prefs === void 0 && opts.currentSessionId !== void 0) {
10016
- const current = opts.sessions.find(
10017
- (s) => s.sessionId === opts.currentSessionId
10018
- );
10019
- if (current?.importedFromMachine) {
10020
- prefs.filters.hostFilter = "__all";
10021
- }
10478
+ }
10479
+ function renderTitleStrip(innerDashes, title) {
10480
+ if (!title) {
10481
+ return { dashes: innerDashes };
10022
10482
  }
10023
- let allSessions = sortSessions(opts.sessions);
10024
- const applyPrefsFilters = (sessions) => {
10483
+ const chip = ` ${title} `;
10484
+ if (chip.length + 4 > innerDashes.length) {
10485
+ return { dashes: innerDashes };
10486
+ }
10487
+ const offset = 2;
10488
+ const dashes = innerDashes.slice(0, offset) + " ".repeat(chip.length) + innerDashes.slice(offset + chip.length);
10489
+ return { dashes, title: { offset, text: chip } };
10490
+ }
10491
+ function paintTopStrip(term, strip) {
10492
+ if (!strip.title) {
10493
+ term.dim.noFormat(strip.dashes);
10494
+ return;
10495
+ }
10496
+ term.dim.noFormat(strip.dashes.slice(0, strip.title.offset));
10497
+ term.brightCyan.noFormat(strip.title.text);
10498
+ term.dim.noFormat(strip.dashes.slice(strip.title.offset + strip.title.text.length));
10499
+ }
10500
+ var MAX_BOX_WIDTH, HORIZ, VERT, TL, TR, BL, BR;
10501
+ var init_prompt_utils = __esm({
10502
+ "src/tui/prompt-utils.ts"() {
10503
+ "use strict";
10504
+ MAX_BOX_WIDTH = 64;
10505
+ HORIZ = "\u2500";
10506
+ VERT = "\u2502";
10507
+ TL = "\u250C";
10508
+ TR = "\u2510";
10509
+ BL = "\u2514";
10510
+ BR = "\u2518";
10511
+ }
10512
+ });
10513
+
10514
+ // src/tui/import-action-prompt.ts
10515
+ function actionPromptStep(selected, key, choices = ACTION_CHOICES) {
10516
+ if (key.kind === "cancel") {
10517
+ return { kind: "cancel" };
10518
+ }
10519
+ if (key.kind === "back") {
10520
+ return { kind: "back" };
10521
+ }
10522
+ if (key.kind === "enter") {
10523
+ const choice = choices[selected];
10524
+ if (!choice) {
10525
+ return { kind: "back" };
10526
+ }
10527
+ return { kind: "resolve", action: choice.key };
10528
+ }
10529
+ if (key.kind === "up") {
10530
+ return {
10531
+ kind: "continue",
10532
+ selected: Math.max(0, selected - 1)
10533
+ };
10534
+ }
10535
+ if (key.kind === "down") {
10536
+ return {
10537
+ kind: "continue",
10538
+ selected: Math.min(choices.length - 1, selected + 1)
10539
+ };
10540
+ }
10541
+ if (key.kind === "char") {
10542
+ const lower = key.ch.toLowerCase();
10543
+ if (lower === "n") {
10544
+ return {
10545
+ kind: "continue",
10546
+ selected: Math.min(choices.length - 1, selected + 1)
10547
+ };
10548
+ }
10549
+ if (lower === "p") {
10550
+ return {
10551
+ kind: "continue",
10552
+ selected: Math.max(0, selected - 1)
10553
+ };
10554
+ }
10555
+ const idx = choices.findIndex((c) => c.hotkey.toLowerCase() === lower);
10556
+ if (idx >= 0) {
10557
+ const choice = choices[idx];
10558
+ if (choice) {
10559
+ return { kind: "resolve", action: choice.key };
10560
+ }
10561
+ }
10562
+ }
10563
+ return { kind: "continue", selected };
10564
+ }
10565
+ async function promptForImportAction(term, session) {
10566
+ resetTerminalModes();
10567
+ const shortId2 = stripHydraSessionPrefix(session.sessionId);
10568
+ const fromMachine = session.importedFromMachine ?? "another machine";
10569
+ const originalCwd = shortenHomePath(session.cwd);
10570
+ let selected = ACTION_CHOICES.findIndex((c) => c.key === "view");
10571
+ if (selected < 0) {
10572
+ selected = 0;
10573
+ }
10574
+ const render = () => {
10575
+ const choiceRows = ACTION_CHOICES.length * 2;
10576
+ const contentHeight = 7 + choiceRows + 2;
10577
+ const layout = drawBox(term, {
10578
+ contentHeight,
10579
+ title: "Imported session"
10580
+ });
10581
+ const innerW = layout.contentW;
10582
+ const headerRows = [
10583
+ { label: "session: ", value: shortId2 },
10584
+ { label: "from: ", value: fromMachine },
10585
+ { label: "cwd: ", value: originalCwd }
10586
+ ];
10587
+ let row = 0;
10588
+ for (const hr of headerRows) {
10589
+ term.moveTo(layout.contentX, layout.contentY + row);
10590
+ term.dim.noFormat(` ${hr.label}`);
10591
+ term.noFormat(truncate2(hr.value, innerW - hr.label.length - 2));
10592
+ row++;
10593
+ }
10594
+ row++;
10595
+ term.moveTo(layout.contentX, layout.contentY + row);
10596
+ term.noFormat(" What do you want to do?");
10597
+ row += 2;
10598
+ for (let i = 0; i < ACTION_CHOICES.length; i++) {
10599
+ const choice = ACTION_CHOICES[i];
10600
+ if (!choice) {
10601
+ continue;
10602
+ }
10603
+ const pointer = i === selected ? "\u276F" : " ";
10604
+ const label = ` ${pointer} ${choice.label}`;
10605
+ term.moveTo(layout.contentX, layout.contentY + row);
10606
+ if (i === selected) {
10607
+ term.brightWhite.bgBlue.noFormat(padRight(label, innerW));
10608
+ } else {
10609
+ term.noFormat(label);
10610
+ }
10611
+ row++;
10612
+ term.moveTo(layout.contentX, layout.contentY + row);
10613
+ term.dim.noFormat(` ${choice.description}`);
10614
+ row++;
10615
+ }
10616
+ row++;
10617
+ term.moveTo(layout.contentX, layout.contentY + row);
10618
+ term.dim.noFormat(" \u2191/\u2193 navigate \xB7 Enter select \xB7 f/v jump \xB7 Esc back");
10619
+ return layout;
10620
+ };
10621
+ render();
10622
+ term.hideCursor();
10623
+ return await new Promise((resolve6) => {
10624
+ let resolved = false;
10625
+ const cleanup = () => {
10626
+ if (resolved) {
10627
+ return;
10628
+ }
10629
+ resolved = true;
10630
+ term.off("key", onKey);
10631
+ term.off("resize", onResize);
10632
+ term.grabInput(false);
10633
+ term.hideCursor(false);
10634
+ term.moveTo(1, 1).eraseDisplayBelow();
10635
+ };
10636
+ const finish = (value) => {
10637
+ cleanup();
10638
+ resolve6(value);
10639
+ };
10640
+ const onResize = () => {
10641
+ if (resolved) {
10642
+ return;
10643
+ }
10644
+ render();
10645
+ };
10646
+ const onKey = (name, _matches, data) => {
10647
+ const input = mapKey(name, data);
10648
+ if (!input) {
10649
+ return;
10650
+ }
10651
+ const step = actionPromptStep(selected, input);
10652
+ if (step.kind === "cancel") {
10653
+ finish("cancel");
10654
+ return;
10655
+ }
10656
+ if (step.kind === "back") {
10657
+ finish("back");
10658
+ return;
10659
+ }
10660
+ if (step.kind === "resolve") {
10661
+ finish(step.action);
10662
+ return;
10663
+ }
10664
+ if (step.selected !== selected) {
10665
+ selected = step.selected;
10666
+ render();
10667
+ }
10668
+ };
10669
+ term.grabInput({});
10670
+ term.on("key", onKey);
10671
+ term.on("resize", onResize);
10672
+ });
10673
+ }
10674
+ function mapKey(name, data) {
10675
+ if (name === "UP") {
10676
+ return { kind: "up" };
10677
+ }
10678
+ if (name === "DOWN") {
10679
+ return { kind: "down" };
10680
+ }
10681
+ if (name === "ENTER" || name === "KP_ENTER") {
10682
+ return { kind: "enter" };
10683
+ }
10684
+ if (name === "ESCAPE") {
10685
+ return { kind: "back" };
10686
+ }
10687
+ if (name === "CTRL_C" || name === "CTRL_D") {
10688
+ return { kind: "cancel" };
10689
+ }
10690
+ if (data?.isCharacter) {
10691
+ return { kind: "char", ch: name };
10692
+ }
10693
+ return null;
10694
+ }
10695
+ async function promptForLaunchOrView(term, session, focus) {
10696
+ const shortId2 = stripHydraSessionPrefix(session.sessionId);
10697
+ const titleOrCwd = session.title ?? shortenHomePath(session.cwd);
10698
+ let selected = 1;
10699
+ const CHOICES = [
10700
+ { label: "Launch", hotkey: "l", description: "start a new agent session" },
10701
+ { label: "View transcript", hotkey: "v", description: "open read-only, no agent spawn" }
10702
+ ];
10703
+ const render = () => {
10704
+ const layout = drawBox(term, { contentHeight: 11, title: "Open session" });
10705
+ const innerW = layout.contentW;
10706
+ let row = 0;
10707
+ term.moveTo(layout.contentX, layout.contentY + row);
10708
+ term.dim.noFormat(" session: ");
10709
+ term.noFormat(truncate2(shortId2, innerW - 10));
10710
+ row++;
10711
+ term.moveTo(layout.contentX, layout.contentY + row);
10712
+ term.noFormat(" " + truncate2(titleOrCwd, innerW - 2));
10713
+ row++;
10714
+ row++;
10715
+ term.moveTo(layout.contentX, layout.contentY + row);
10716
+ term.noFormat(" What do you want to do?");
10717
+ row += 2;
10718
+ for (let i = 0; i < CHOICES.length; i++) {
10719
+ const choice = CHOICES[i];
10720
+ if (!choice) {
10721
+ continue;
10722
+ }
10723
+ const pointer = i === selected ? "\u276F" : " ";
10724
+ const label = ` ${pointer} ${choice.label}`;
10725
+ term.moveTo(layout.contentX, layout.contentY + row);
10726
+ if (i === selected) {
10727
+ term.brightWhite.bgBlue.noFormat(padRight(label, innerW));
10728
+ } else {
10729
+ term.noFormat(label);
10730
+ }
10731
+ row++;
10732
+ term.moveTo(layout.contentX, layout.contentY + row);
10733
+ term.dim.noFormat(` ${choice.description}`);
10734
+ row++;
10735
+ }
10736
+ row++;
10737
+ term.moveTo(layout.contentX, layout.contentY + row);
10738
+ term.dim.noFormat(" \u2191/\u2193 navigate \xB7 Enter select \xB7 l/v jump \xB7 Esc back");
10739
+ };
10740
+ render();
10741
+ term.hideCursor();
10742
+ return await new Promise((resolve6) => {
10743
+ let resolved = false;
10744
+ const cleanup = () => {
10745
+ resolved = true;
10746
+ };
10747
+ const finish = (value) => {
10748
+ cleanup();
10749
+ focus.pop();
10750
+ resolve6(value);
10751
+ };
10752
+ const onKey = (name, _m, data) => {
10753
+ if (name === "CTRL_C" || name === "CTRL_D") {
10754
+ finish("cancel");
10755
+ return;
10756
+ }
10757
+ if (name === "ESCAPE") {
10758
+ finish("back");
10759
+ return;
10760
+ }
10761
+ if (name === "ENTER" || name === "KP_ENTER") {
10762
+ finish(selected === 0 ? "launch" : "view");
10763
+ return;
10764
+ }
10765
+ if (name === "UP" || name === "SHIFT_TAB") {
10766
+ if (selected > 0) {
10767
+ selected--;
10768
+ render();
10769
+ }
10770
+ return;
10771
+ }
10772
+ if (name === "DOWN" || name === "TAB") {
10773
+ if (selected < CHOICES.length - 1) {
10774
+ selected++;
10775
+ render();
10776
+ }
10777
+ return;
10778
+ }
10779
+ if (data?.isCharacter) {
10780
+ const lower = name.toLowerCase();
10781
+ if (lower === "l") {
10782
+ finish("launch");
10783
+ return;
10784
+ }
10785
+ if (lower === "v") {
10786
+ finish("view");
10787
+ return;
10788
+ }
10789
+ if (lower === "n") {
10790
+ if (selected < CHOICES.length - 1) {
10791
+ selected++;
10792
+ render();
10793
+ }
10794
+ return;
10795
+ }
10796
+ if (lower === "p") {
10797
+ if (selected > 0) {
10798
+ selected--;
10799
+ render();
10800
+ }
10801
+ return;
10802
+ }
10803
+ }
10804
+ };
10805
+ focus.push({
10806
+ onKey: (name, _m, data) => {
10807
+ if (!resolved) onKey(name, _m, data);
10808
+ },
10809
+ onResize: () => {
10810
+ if (!resolved) render();
10811
+ }
10812
+ });
10813
+ });
10814
+ }
10815
+ function truncate2(s, max) {
10816
+ if (max <= 1) {
10817
+ return "";
10818
+ }
10819
+ if (s.length <= max) {
10820
+ return s;
10821
+ }
10822
+ return s.slice(0, Math.max(0, max - 1)) + "\u2026";
10823
+ }
10824
+ function padRight(s, w) {
10825
+ if (s.length >= w) {
10826
+ return s.slice(0, w);
10827
+ }
10828
+ return s + " ".repeat(w - s.length);
10829
+ }
10830
+ var ACTION_CHOICES;
10831
+ var init_import_action_prompt = __esm({
10832
+ "src/tui/import-action-prompt.ts"() {
10833
+ "use strict";
10834
+ init_paths();
10835
+ init_session();
10836
+ init_prompt_utils();
10837
+ ACTION_CHOICES = [
10838
+ {
10839
+ key: "fork-local",
10840
+ label: "Fork locally",
10841
+ hotkey: "f",
10842
+ description: "spawn a local fork \u2014 original imported copy stays as-is"
10843
+ },
10844
+ {
10845
+ key: "view",
10846
+ label: "View transcript",
10847
+ hotkey: "v",
10848
+ description: "open read-only, no agent spawn"
10849
+ }
10850
+ ];
10851
+ }
10852
+ });
10853
+
10854
+ // src/tui/picker.ts
10855
+ function createPickerPrefs() {
10856
+ return {
10857
+ filters: { cwdOnly: false, hostFilter: "__local", showCat: false }
10858
+ };
10859
+ }
10860
+ async function pickSession(term, opts) {
10861
+ process.stdout.write("\x1B[<u");
10862
+ process.stdout.write("\x1B[?2004l");
10863
+ process.stdout.write("\x1B[>4;0m");
10864
+ process.stdout.write("\x1B[>5;0m");
10865
+ process.stdout.write("\x1B[?1000l");
10866
+ process.stdout.write("\x1B[?1002l");
10867
+ process.stdout.write("\x1B[?1006l");
10868
+ process.stdout.write("\x1B[?1l");
10869
+ process.stdout.write("\x1B>");
10870
+ const sortSessions = (sessions) => {
10871
+ const score = (s) => {
10872
+ if (s.status !== "live") {
10873
+ return 0;
10874
+ }
10875
+ return s.cwd === opts.cwd ? 2 : 1;
10876
+ };
10877
+ return [...sessions].sort((a, b) => {
10878
+ const tier = score(b) - score(a);
10879
+ if (tier !== 0) {
10880
+ return tier;
10881
+ }
10882
+ return b.updatedAt.slice(0, 16).localeCompare(a.updatedAt.slice(0, 16));
10883
+ });
10884
+ };
10885
+ const prefs = opts.prefs ?? createPickerPrefs();
10886
+ if (opts.prefs === void 0 && opts.currentSessionId !== void 0) {
10887
+ const current = opts.sessions.find(
10888
+ (s) => s.sessionId === opts.currentSessionId
10889
+ );
10890
+ if (current?.importedFromMachine) {
10891
+ prefs.filters.hostFilter = "__all";
10892
+ }
10893
+ }
10894
+ let allSessions = sortSessions(opts.sessions);
10895
+ const applyPrefsFilters = (sessions) => {
10025
10896
  let base = sessions;
10026
10897
  if (prefs.filters.cwdOnly) {
10027
10898
  base = base.filter((s) => s.cwd === opts.cwd);
10028
10899
  }
10900
+ if (!prefs.filters.showCat) {
10901
+ base = base.filter(
10902
+ (s) => s.originatingClient?.name !== HYDRA_CAT_CLIENT_NAME
10903
+ );
10904
+ }
10029
10905
  base = filterByHost(base, prefs.filters.hostFilter);
10030
10906
  return base;
10031
10907
  };
@@ -10045,11 +10921,19 @@ async function pickSession(term, opts) {
10045
10921
  let searchTerm = "";
10046
10922
  let mode = "normal";
10047
10923
  let pendingAction = null;
10924
+ let findSubMode = "input";
10925
+ let findComposer = new InputDispatcher({ history: [] });
10926
+ let findResults = [];
10927
+ let findTruncated = false;
10928
+ let findSelectedIdx = 0;
10929
+ let findSnippetIdx = 0;
10930
+ let findError = null;
10931
+ let findInFlight = false;
10048
10932
  let renameBuffer = "";
10049
10933
  let transientStatus = null;
10050
10934
  const composer = new InputDispatcher({ history: [] });
10051
- let termHeight = readTermHeight(term);
10052
- let termWidth = readTermWidth(term);
10935
+ let termHeight = readTermHeight2(term);
10936
+ let termWidth = readTermWidth2(term);
10053
10937
  let viewportSize = 0;
10054
10938
  let composerTitle = "";
10055
10939
  let composerRoom = 0;
@@ -10061,10 +10945,16 @@ async function pickSession(term, opts) {
10061
10945
  let headerLine = "";
10062
10946
  let sessionLines = [];
10063
10947
  let startRow = 1;
10948
+ let findRoom = 0;
10949
+ let findVisualRows = [];
10950
+ let findBoxRows = 1;
10951
+ let findBoxWindowStart = 0;
10952
+ let findBoxCursorVisualRow = 0;
10953
+ let findBoxCursorVisualCol = 0;
10064
10954
  const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
10065
10955
  const computeLayout = () => {
10066
- termHeight = readTermHeight(term);
10067
- termWidth = readTermWidth(term);
10956
+ termHeight = readTermHeight2(term);
10957
+ termWidth = readTermWidth2(term);
10068
10958
  const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
10069
10959
  composerRoom = Math.max(10, termWidth - BOX_HORIZONTAL_PAD);
10070
10960
  const titleBudget = Math.max(10, termWidth - 8);
@@ -10115,6 +11005,19 @@ async function pickSession(term, opts) {
10115
11005
  }
10116
11006
  adjustScroll();
10117
11007
  };
11008
+ const restoreCursorAfterFilter = (keepId) => {
11009
+ if (keepId !== void 0) {
11010
+ const idx = visible.findIndex((s) => s.sessionId === keepId);
11011
+ if (idx >= 0) {
11012
+ selectedIdx = idx + 1;
11013
+ adjustScroll();
11014
+ return;
11015
+ }
11016
+ }
11017
+ selectedIdx = visible.length > 0 ? 1 : 0;
11018
+ scrollOffset = 0;
11019
+ adjustScroll();
11020
+ };
10118
11021
  const adjustScroll = () => {
10119
11022
  if (selectedIdx === 0) {
10120
11023
  return;
@@ -10191,6 +11094,9 @@ async function pickSession(term, opts) {
10191
11094
  prefs.filters.hostFilter === "__local" ? "host: local" : `host: ${prefs.filters.hostFilter}`
10192
11095
  );
10193
11096
  }
11097
+ if (prefs.filters.showCat) {
11098
+ parts.push("+cat");
11099
+ }
10194
11100
  if (above > 0) {
10195
11101
  parts.push(`\u2191 ${above} above`);
10196
11102
  }
@@ -10240,59 +11146,370 @@ async function pickSession(term, opts) {
10240
11146
  if (visualOffset < 0 || visualOffset >= composerRows) {
10241
11147
  return;
10242
11148
  }
10243
- const col = 3 + composerCursorCol;
10244
- term.moveTo(col, composerBodyRow(visualOffset));
11149
+ const col = 3 + composerCursorCol;
11150
+ term.moveTo(col, composerBodyRow(visualOffset));
11151
+ };
11152
+ const renderFromScratch = () => {
11153
+ withSync(() => {
11154
+ term.hideCursor();
11155
+ computeLayout();
11156
+ adjustScroll();
11157
+ startRow = 1;
11158
+ term.moveTo(1, 1).eraseDisplayBelow();
11159
+ paintComposerTopBorder();
11160
+ term("\n");
11161
+ for (let v = 0; v < composerRows; v++) {
11162
+ paintComposerBodyRow(composerWindowStart + v);
11163
+ term("\n");
11164
+ }
11165
+ paintComposerBottomBorder();
11166
+ term("\n\n");
11167
+ term.dim.noFormat(` ${headerLine}`)("\n");
11168
+ for (let v = 0; v < viewportSize; v++) {
11169
+ paintSessionRow(scrollOffset + v);
11170
+ term("\n");
11171
+ }
11172
+ paintIndicator();
11173
+ term("\n");
11174
+ if (selectedIdx === 0) {
11175
+ placeComposerCursor();
11176
+ term.hideCursor(false);
11177
+ }
11178
+ });
11179
+ };
11180
+ const renderHelp = () => {
11181
+ withSync(() => {
11182
+ term.hideCursor();
11183
+ term.moveTo(1, 1).eraseDisplayBelow();
11184
+ term.brightWhite.bold.noFormat(" Picker hotkeys")("\n\n");
11185
+ for (const entry of HELP_ENTRIES) {
11186
+ if (entry === null) {
11187
+ term("\n");
11188
+ continue;
11189
+ }
11190
+ const [keys, desc] = entry;
11191
+ term.brightCyan.noFormat(` ${keys.padEnd(HELP_KEYS_WIDTH)}`);
11192
+ term.noFormat(desc)("\n");
11193
+ }
11194
+ term("\n");
11195
+ term.dim.noFormat(" press any key to dismiss")("\n");
11196
+ });
11197
+ };
11198
+ const findResultsStartRow = () => findBoxRows + 4;
11199
+ const FIND_FOOTER_ROWS = 2;
11200
+ let findScrollOffset = 0;
11201
+ const findViewportSize = () => {
11202
+ termHeight = readTermHeight2(term);
11203
+ const avail = Math.max(2, termHeight - (findBoxRows + 3) - FIND_FOOTER_ROWS);
11204
+ return Math.max(1, Math.floor(avail / 2));
11205
+ };
11206
+ const adjustFindScroll = () => {
11207
+ const v = findViewportSize();
11208
+ if (findSelectedIdx < findScrollOffset) {
11209
+ findScrollOffset = findSelectedIdx;
11210
+ } else if (findSelectedIdx >= findScrollOffset + v) {
11211
+ findScrollOffset = findSelectedIdx - v + 1;
11212
+ }
11213
+ if (findScrollOffset + v > findResults.length) {
11214
+ findScrollOffset = Math.max(0, findResults.length - v);
11215
+ }
11216
+ if (findScrollOffset < 0) {
11217
+ findScrollOffset = 0;
11218
+ }
11219
+ };
11220
+ const paintFindBoxTopBorder = (focused) => {
11221
+ termWidth = readTermWidth2(term);
11222
+ const inner = Math.max(2, termWidth - 2);
11223
+ const title = "\u2500 Find sessions ";
11224
+ const dashes = "\u2500".repeat(Math.max(1, inner - title.length));
11225
+ if (focused) {
11226
+ term.brightBlue.noFormat(`\u256D${title}${dashes}\u256E`);
11227
+ } else {
11228
+ term.dim.noFormat(`\u256D${title}${dashes}\u256E`);
11229
+ }
11230
+ term.styleReset();
11231
+ };
11232
+ const computeFindBoxLayout = () => {
11233
+ termWidth = readTermWidth2(term);
11234
+ findRoom = Math.max(10, termWidth - BOX_HORIZONTAL_PAD);
11235
+ const state = findComposer.state();
11236
+ findVisualRows = computePromptVisualRows(state.buffer, findRoom);
11237
+ const layout = computePromptLayout(findVisualRows, state, FIND_BOX_MAX_ROWS);
11238
+ findBoxRows = layout.rendered;
11239
+ findBoxWindowStart = layout.windowStart;
11240
+ findBoxCursorVisualRow = layout.cursorVisualRow;
11241
+ findBoxCursorVisualCol = layout.cursorVisualCol;
11242
+ };
11243
+ const paintFindBoxBodyRow = (visualIdx, focused) => {
11244
+ termWidth = readTermWidth2(term);
11245
+ const inner = Math.max(2, termWidth - 2);
11246
+ const vr = findVisualRows[visualIdx];
11247
+ let slice = "";
11248
+ if (vr) {
11249
+ slice = (findComposer.state().buffer[vr.bufferIdx] ?? "").slice(
11250
+ vr.startCol,
11251
+ vr.endCol
11252
+ );
11253
+ }
11254
+ const padWidth = Math.max(0, inner - 1 - slice.length);
11255
+ const pad = " ".repeat(padWidth);
11256
+ if (focused) {
11257
+ term.brightBlue.noFormat("\u2502");
11258
+ term.noFormat(` ${slice}${pad}`);
11259
+ term.brightBlue.noFormat("\u2502");
11260
+ } else {
11261
+ term.dim.noFormat("\u2502");
11262
+ term.noFormat(` ${slice}${pad}`);
11263
+ term.dim.noFormat("\u2502");
11264
+ }
11265
+ term.styleReset();
11266
+ };
11267
+ const paintFindBoxBottomBorder = (focused) => {
11268
+ termWidth = readTermWidth2(term);
11269
+ const inner = Math.max(2, termWidth - 2);
11270
+ const dashes = "\u2500".repeat(inner);
11271
+ if (focused) {
11272
+ term.brightBlue.noFormat(`\u2570${dashes}\u256F`);
11273
+ } else {
11274
+ term.dim.noFormat(`\u2570${dashes}\u256F`);
11275
+ }
11276
+ term.styleReset();
11277
+ };
11278
+ const findBoxCursorCol = () => 3 + findBoxCursorVisualCol;
11279
+ const findBoxCursorScreenRow = () => 2 + (findBoxCursorVisualRow - findBoxWindowStart);
11280
+ const repaintFindBoxChrome = () => {
11281
+ const focused = findSubMode === "input";
11282
+ withSync(() => {
11283
+ if (focused) {
11284
+ term.hideCursor();
11285
+ }
11286
+ term.moveTo(1, 1);
11287
+ paintFindBoxTopBorder(focused);
11288
+ for (let v = 0; v < findBoxRows; v++) {
11289
+ term.moveTo(1, 2 + v);
11290
+ paintFindBoxBodyRow(findBoxWindowStart + v, focused);
11291
+ }
11292
+ term.moveTo(1, 2 + findBoxRows);
11293
+ paintFindBoxBottomBorder(focused);
11294
+ if (focused) {
11295
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
11296
+ term.hideCursor(false);
11297
+ }
11298
+ });
11299
+ };
11300
+ const repaintFindBoxBodyRows = () => {
11301
+ withSync(() => {
11302
+ term.hideCursor();
11303
+ for (let v = 0; v < findBoxRows; v++) {
11304
+ term.moveTo(1, 2 + v);
11305
+ paintFindBoxBodyRow(findBoxWindowStart + v, true);
11306
+ }
11307
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
11308
+ term.hideCursor(false);
11309
+ });
11310
+ };
11311
+ const SNIPPET_KIND_GLYPH = {
11312
+ user: "user",
11313
+ agent: "agent",
11314
+ thought: "thought",
11315
+ tool: "tool",
11316
+ "tool-input": "tool-input"
11317
+ };
11318
+ const findResultData = (idx, focused) => {
11319
+ const hit = findResults[idx];
11320
+ if (!hit) {
11321
+ return { rowBudget: 20, line1: "", line2: "", focusedRow: false };
11322
+ }
11323
+ const w = readTermWidth2(term);
11324
+ const rowBudget = Math.max(20, w - ROW_PREFIX_WIDTH);
11325
+ const shortId3 = stripHydraSessionPrefix(hit.sessionId);
11326
+ const title = hit.title ?? shortenHomePath(hit.cwd);
11327
+ const counterText = focused && hit.snippets.length > 1 ? ` [${findSnippetIdx + 1}/${hit.snippets.length}]` : focused && hit.totalMatches > hit.snippets.length ? ` [${hit.snippets.length} of ${hit.totalMatches}]` : "";
11328
+ const head = `${shortId3} ${hit.status === "live" ? "live" : "cold"}`;
11329
+ const titleBudget = Math.max(5, rowBudget - head.length - counterText.length - 2);
11330
+ const titleSlice = truncateMiddle(title, titleBudget);
11331
+ const line1 = `${head} ${titleSlice}${counterText}`.padEnd(rowBudget);
11332
+ const snippet = hit.snippets[focused ? findSnippetIdx : 0];
11333
+ const kind = snippet ? SNIPPET_KIND_GLYPH[snippet.kind] ?? snippet.kind : "";
11334
+ const prefix = snippet?.toolName ? `${kind} \xB7 ${snippet.toolName}` : kind;
11335
+ const snippetBudget = Math.max(10, rowBudget - prefix.length - 6);
11336
+ const text = snippet ? truncateMiddle(snippet.text, snippetBudget) : "";
11337
+ const line2 = snippet ? ` ${prefix} ${text}` : " (no snippet)";
11338
+ return { rowBudget, line1, line2: line2.padEnd(rowBudget + ROW_PREFIX_WIDTH), focusedRow: focused };
11339
+ };
11340
+ const paintFindResultA = (idx, focused) => {
11341
+ const { line1, focusedRow } = findResultData(idx, focused);
11342
+ if (focusedRow) {
11343
+ term.brightWhite.bgBlue.noFormat(`\u276F ${line1}`);
11344
+ } else {
11345
+ term.noFormat(` ${line1}`);
11346
+ }
11347
+ term.styleReset();
11348
+ };
11349
+ const paintFindResultB = (idx, focused) => {
11350
+ const { line2 } = findResultData(idx, focused);
11351
+ term.dim.noFormat(line2);
11352
+ term.styleReset();
11353
+ };
11354
+ const paintFindIndicator = () => {
11355
+ if (findInFlight) {
11356
+ term.dim.noFormat(" searching\u2026");
11357
+ term.styleReset();
11358
+ term.eraseLineAfter();
11359
+ } else if (findError !== null) {
11360
+ term.brightRed.noFormat(` ${findError}`);
11361
+ term.styleReset();
11362
+ term.eraseLineAfter();
11363
+ } else if (findSubMode === "input") {
11364
+ if (findResults.length > 0) {
11365
+ term.dim.noFormat(" Enter to search \xB7 \u2193 browse results \xB7 Esc cancel");
11366
+ } else {
11367
+ term.dim.noFormat(" Enter to search \xB7 Esc cancel");
11368
+ }
11369
+ term.styleReset();
11370
+ term.eraseLineAfter();
11371
+ } else {
11372
+ const sCount = findResults.length;
11373
+ const truncSuffix = findTruncated ? " \xB7 truncated" : "";
11374
+ const countPart = sCount > 0 ? ` ${sCount} ${sCount === 1 ? "session" : "sessions"} match${truncSuffix} \xB7 ` : " ";
11375
+ term.dim.noFormat(
11376
+ `${countPart}\u2191 edit query \xB7 Up/Down sessions \xB7 n/p snippets \xB7 Enter open \xB7 Esc back`
11377
+ );
11378
+ term.styleReset();
11379
+ term.eraseLineAfter();
11380
+ }
10245
11381
  };
10246
- const renderFromScratch = () => {
10247
- if (mode === "help") {
10248
- renderHelp();
10249
- return;
10250
- }
11382
+ const renderFind = () => {
11383
+ computeFindBoxLayout();
11384
+ const focused = findSubMode === "input";
11385
+ const queryText = findComposer.state().buffer.join("\n");
10251
11386
  withSync(() => {
10252
11387
  term.hideCursor();
10253
- computeLayout();
10254
- adjustScroll();
10255
- startRow = 1;
10256
11388
  term.moveTo(1, 1).eraseDisplayBelow();
10257
- paintComposerTopBorder();
10258
- term("\n");
10259
- for (let v = 0; v < composerRows; v++) {
10260
- paintComposerBodyRow(composerWindowStart + v);
10261
- term("\n");
10262
- }
10263
- paintComposerBottomBorder();
10264
- term("\n\n");
10265
- term.dim.noFormat(` ${headerLine}`)("\n");
10266
- for (let v = 0; v < viewportSize; v++) {
10267
- paintSessionRow(scrollOffset + v);
10268
- term("\n");
11389
+ paintFindBoxTopBorder(focused);
11390
+ for (let v = 0; v < findBoxRows; v++) {
11391
+ term.moveTo(1, 2 + v);
11392
+ paintFindBoxBodyRow(findBoxWindowStart + v, focused);
11393
+ }
11394
+ term.moveTo(1, 2 + findBoxRows);
11395
+ paintFindBoxBottomBorder(focused);
11396
+ const sCount = findResults.length;
11397
+ if (sCount === 0) {
11398
+ term.moveTo(1, findResultsStartRow());
11399
+ if (findInFlight) {
11400
+ } else if (findError === null && queryText.trim().length === 0) {
11401
+ term.dim.noFormat(" type a query in the box above, then press Enter");
11402
+ term.eraseLineAfter();
11403
+ } else if (findError === null) {
11404
+ term.dim.noFormat(" no matches");
11405
+ term.eraseLineAfter();
11406
+ }
11407
+ term.moveTo(1, findResultsStartRow() + 1);
11408
+ paintFindIndicator();
11409
+ } else {
11410
+ adjustFindScroll();
11411
+ const v = findViewportSize();
11412
+ const listFocused = findSubMode !== "input";
11413
+ for (let i = 0; i < v; i++) {
11414
+ const idx = findScrollOffset + i;
11415
+ term.moveTo(1, findResultsStartRow() + i * 2);
11416
+ if (idx < sCount) {
11417
+ paintFindResultA(idx, listFocused && idx === findSelectedIdx);
11418
+ } else {
11419
+ term.eraseLineAfter();
11420
+ }
11421
+ term.moveTo(1, findResultsStartRow() + i * 2 + 1);
11422
+ if (idx < sCount) {
11423
+ paintFindResultB(idx, listFocused && idx === findSelectedIdx);
11424
+ } else {
11425
+ term.eraseLineAfter();
11426
+ }
11427
+ }
11428
+ term.moveTo(1, findResultsStartRow() + v * 2);
11429
+ paintFindIndicator();
10269
11430
  }
10270
- paintIndicator();
10271
- term("\n");
10272
- if (selectedIdx === 0) {
10273
- placeComposerCursor();
11431
+ if (focused) {
11432
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
10274
11433
  term.hideCursor(false);
10275
11434
  }
10276
11435
  });
10277
11436
  };
10278
- const renderHelp = () => {
11437
+ const repaintFindResult = (idx, focused) => {
11438
+ const viewportIdx = idx - findScrollOffset;
11439
+ if (viewportIdx < 0 || viewportIdx >= findViewportSize()) {
11440
+ return;
11441
+ }
10279
11442
  withSync(() => {
10280
- term.hideCursor();
10281
- term.moveTo(1, 1).eraseDisplayBelow();
10282
- term.brightWhite.bold.noFormat(" Picker hotkeys")("\n\n");
10283
- for (const entry of HELP_ENTRIES) {
10284
- if (entry === null) {
10285
- term("\n");
10286
- continue;
11443
+ term.moveTo(1, findResultsStartRow() + viewportIdx * 2);
11444
+ paintFindResultA(idx, focused);
11445
+ term.moveTo(1, findResultsStartRow() + viewportIdx * 2 + 1);
11446
+ paintFindResultB(idx, focused);
11447
+ });
11448
+ };
11449
+ const repaintFindIndicatorRow = () => {
11450
+ withSync(() => {
11451
+ term.moveTo(1, findResultsStartRow() + findViewportSize() * 2);
11452
+ paintFindIndicator();
11453
+ });
11454
+ };
11455
+ const repaintFindViewport = () => {
11456
+ withSync(() => {
11457
+ const v = findViewportSize();
11458
+ const sCount = findResults.length;
11459
+ const listFocused = findSubMode !== "input";
11460
+ for (let i = 0; i < v; i++) {
11461
+ const idx = findScrollOffset + i;
11462
+ term.moveTo(1, findResultsStartRow() + i * 2);
11463
+ if (idx < sCount) {
11464
+ paintFindResultA(idx, listFocused && idx === findSelectedIdx);
11465
+ } else {
11466
+ term.eraseLineAfter();
11467
+ }
11468
+ term.moveTo(1, findResultsStartRow() + i * 2 + 1);
11469
+ if (idx < sCount) {
11470
+ paintFindResultB(idx, listFocused && idx === findSelectedIdx);
11471
+ } else {
11472
+ term.eraseLineAfter();
10287
11473
  }
10288
- const [keys, desc] = entry;
10289
- term.brightCyan.noFormat(` ${keys.padEnd(HELP_KEYS_WIDTH)}`);
10290
- term.noFormat(desc)("\n");
10291
11474
  }
10292
- term("\n");
10293
- term.dim.noFormat(" press any key to dismiss")("\n");
11475
+ term.moveTo(1, findResultsStartRow() + v * 2);
11476
+ paintFindIndicator();
10294
11477
  });
10295
11478
  };
11479
+ const findQueryText = () => findComposer.state().buffer.join("\n");
11480
+ const runFind = async () => {
11481
+ const query = findQueryText().trim();
11482
+ if (query.length === 0) {
11483
+ return;
11484
+ }
11485
+ if (visible.length === 0) {
11486
+ findError = "no sessions in view to search";
11487
+ renderFind();
11488
+ return;
11489
+ }
11490
+ findInFlight = true;
11491
+ findError = null;
11492
+ renderFind();
11493
+ try {
11494
+ const out = await searchSessions(opts.target, query, {
11495
+ sessionIds: visible.map((s) => s.sessionId)
11496
+ });
11497
+ findResults = out.results;
11498
+ findTruncated = out.truncated;
11499
+ findSelectedIdx = 0;
11500
+ findSnippetIdx = 0;
11501
+ findScrollOffset = 0;
11502
+ findSubMode = out.results.length > 0 ? "results" : "input";
11503
+ computeFindBoxLayout();
11504
+ } catch (err) {
11505
+ findError = `search failed: ${err.message}`;
11506
+ } finally {
11507
+ findInFlight = false;
11508
+ renderFind();
11509
+ }
11510
+ };
11511
+ let exitFind = () => {
11512
+ };
10296
11513
  const repaintComposerChrome = () => {
10297
11514
  withSync(() => {
10298
11515
  const showCursor = selectedIdx === 0;
@@ -10441,23 +11658,48 @@ async function pickSession(term, opts) {
10441
11658
  let resolved = false;
10442
11659
  let autoRefreshTimer = null;
10443
11660
  let autoRefreshInFlight = false;
10444
- const onResize = () => {
10445
- if (resolved) {
10446
- return;
11661
+ const focusStack = [];
11662
+ const pushLayer = (layer) => {
11663
+ focusStack.push(layer);
11664
+ };
11665
+ const popLayer = () => {
11666
+ focusStack.pop();
11667
+ if (!resolved) {
11668
+ focusStack[focusStack.length - 1]?.onResize();
10447
11669
  }
10448
- renderFromScratch();
11670
+ };
11671
+ const focus = { push: pushLayer, pop: popLayer };
11672
+ exitFind = () => {
11673
+ findComposer = new InputDispatcher({ history: [] });
11674
+ findResults = [];
11675
+ findTruncated = false;
11676
+ findSelectedIdx = 0;
11677
+ findSnippetIdx = 0;
11678
+ findScrollOffset = 0;
11679
+ findError = null;
11680
+ findInFlight = false;
11681
+ findSubMode = "input";
11682
+ popLayer();
11683
+ };
11684
+ const dispatch = (name, _matches, data) => {
11685
+ focusStack[focusStack.length - 1]?.onKey(name, _matches, data);
11686
+ };
11687
+ const dispatchResize = () => {
11688
+ if (resolved) return;
11689
+ focusStack[focusStack.length - 1]?.onResize();
10449
11690
  };
10450
11691
  const cleanup = () => {
10451
11692
  if (resolved) {
10452
11693
  return;
10453
11694
  }
10454
11695
  resolved = true;
11696
+ focusStack.length = 0;
10455
11697
  if (autoRefreshTimer) {
10456
11698
  clearInterval(autoRefreshTimer);
10457
11699
  autoRefreshTimer = null;
10458
11700
  }
10459
- term.off("key", onKey);
10460
- term.off("resize", onResize);
11701
+ term.off("key", dispatch);
11702
+ term.off("resize", dispatchResize);
10461
11703
  process.stdout.write("\x1B[?2004l");
10462
11704
  const tClean = term;
10463
11705
  if (tClean.stdin && tkStdinHandler) {
@@ -10613,18 +11855,231 @@ ${cells}`;
10613
11855
  paintIndicator();
10614
11856
  return true;
10615
11857
  };
10616
- const onKey = (name, _matches, data) => {
10617
- if (mode === "busy") {
11858
+ const openHelpLayer = () => {
11859
+ renderHelp();
11860
+ pushLayer({
11861
+ onKey: (name) => {
11862
+ if (name === "CTRL_C") {
11863
+ cleanup();
11864
+ resolve6({ kind: "abort" });
11865
+ return;
11866
+ }
11867
+ popLayer();
11868
+ },
11869
+ onResize: () => renderHelp()
11870
+ });
11871
+ };
11872
+ const openFindLayer = () => {
11873
+ if (visible.length === 0) {
11874
+ transientStatus = "no sessions to search";
11875
+ paintIndicator();
10618
11876
  return;
10619
11877
  }
10620
- if (mode === "help") {
10621
- if (name === "CTRL_C") {
10622
- cleanup();
10623
- resolve6({ kind: "abort" });
11878
+ findComposer = new InputDispatcher({ history: [] });
11879
+ findResults = [];
11880
+ findTruncated = false;
11881
+ findSelectedIdx = 0;
11882
+ findSnippetIdx = 0;
11883
+ findScrollOffset = 0;
11884
+ findError = null;
11885
+ findInFlight = false;
11886
+ findSubMode = "input";
11887
+ computeFindBoxLayout();
11888
+ renderFind();
11889
+ const findOnKey = (name, _matches, data) => {
11890
+ if (findSubMode === "input") {
11891
+ if (findInFlight) {
11892
+ return;
11893
+ }
11894
+ if (name === "ESCAPE" || name === "CTRL_C") {
11895
+ exitFind();
11896
+ return;
11897
+ }
11898
+ if (name === "ENTER" || name === "KP_ENTER") {
11899
+ if (findQueryText().trim().length === 0) {
11900
+ return;
11901
+ }
11902
+ void runFind();
11903
+ return;
11904
+ }
11905
+ if ((name === "DOWN" || name === "TAB" || name === "CTRL_N") && findResults.length > 0) {
11906
+ findSubMode = "results";
11907
+ findSelectedIdx = 0;
11908
+ findSnippetIdx = 0;
11909
+ withSync(() => {
11910
+ repaintFindBoxChrome();
11911
+ repaintFindResult(0, true);
11912
+ repaintFindIndicatorRow();
11913
+ term.hideCursor();
11914
+ });
11915
+ return;
11916
+ }
11917
+ const before = findComposer.state();
11918
+ let event = null;
11919
+ if (data?.isCharacter) {
11920
+ event = { type: "char", ch: name };
11921
+ } else {
11922
+ const mapped = mapKeyName(name);
11923
+ if (mapped !== null)
11924
+ event = { type: "key", name: mapped };
11925
+ }
11926
+ if (event === null) {
11927
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
11928
+ return;
11929
+ }
11930
+ findComposer.feed(event);
11931
+ const after = findComposer.state();
11932
+ const unchanged = before.buffer.length === after.buffer.length && before.buffer.every((l, i) => l === after.buffer[i]) && before.row === after.row && before.col === after.col;
11933
+ if (unchanged) {
11934
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
11935
+ return;
11936
+ }
11937
+ const prevRows = findBoxRows;
11938
+ computeFindBoxLayout();
11939
+ if (findBoxRows !== prevRows) {
11940
+ renderFind();
11941
+ } else {
11942
+ repaintFindBoxBodyRows();
11943
+ }
10624
11944
  return;
10625
11945
  }
10626
- mode = "normal";
10627
- renderFromScratch();
11946
+ if (findSubMode === "results") {
11947
+ if (name === "ESCAPE" || name === "CTRL_C") {
11948
+ exitFind();
11949
+ return;
11950
+ }
11951
+ if (name === "CTRL_F") {
11952
+ findSubMode = "input";
11953
+ repaintFindViewport();
11954
+ repaintFindIndicatorRow();
11955
+ repaintFindBoxChrome();
11956
+ return;
11957
+ }
11958
+ if (name === "ENTER" || name === "KP_ENTER") {
11959
+ const hit = findResults[findSelectedIdx];
11960
+ if (!hit) {
11961
+ return;
11962
+ }
11963
+ const session = visible.find((s) => s.sessionId === hit.sessionId);
11964
+ const isImportedPassive = !!session?.importedFromMachine && !session.upstreamSessionId;
11965
+ if (isImportedPassive) {
11966
+ cleanup();
11967
+ const result = {
11968
+ kind: "attach",
11969
+ sessionId: hit.sessionId
11970
+ };
11971
+ if (session.agentId !== void 0) {
11972
+ result.agentId = session.agentId;
11973
+ }
11974
+ resolve6(result);
11975
+ return;
11976
+ }
11977
+ void (async () => {
11978
+ const action = await promptForLaunchOrView(term, {
11979
+ sessionId: hit.sessionId,
11980
+ title: hit.title,
11981
+ cwd: hit.cwd
11982
+ }, focus);
11983
+ if (action === "cancel") {
11984
+ cleanup();
11985
+ resolve6({ kind: "abort" });
11986
+ return;
11987
+ }
11988
+ if (action === "back") return;
11989
+ cleanup();
11990
+ const result = {
11991
+ kind: "attach",
11992
+ sessionId: hit.sessionId,
11993
+ readonly: action === "view"
11994
+ };
11995
+ if (session?.agentId !== void 0) {
11996
+ result.agentId = session.agentId;
11997
+ }
11998
+ resolve6(result);
11999
+ })();
12000
+ return;
12001
+ }
12002
+ if (data?.isCharacter && (name === "n" || name === "N")) {
12003
+ const hit = findResults[findSelectedIdx];
12004
+ if (!hit || hit.snippets.length <= 1) {
12005
+ return;
12006
+ }
12007
+ findSnippetIdx = (findSnippetIdx + 1) % hit.snippets.length;
12008
+ repaintFindResult(findSelectedIdx, true);
12009
+ return;
12010
+ }
12011
+ if (data?.isCharacter && (name === "p" || name === "P")) {
12012
+ const hit = findResults[findSelectedIdx];
12013
+ if (!hit || hit.snippets.length <= 1) {
12014
+ return;
12015
+ }
12016
+ findSnippetIdx = (findSnippetIdx - 1 + hit.snippets.length) % hit.snippets.length;
12017
+ repaintFindResult(findSelectedIdx, true);
12018
+ return;
12019
+ }
12020
+ const moveDeep = (delta) => {
12021
+ if (delta < 0 && findSelectedIdx === 0) {
12022
+ findSubMode = "input";
12023
+ withSync(() => {
12024
+ repaintFindResult(0, false);
12025
+ repaintFindIndicatorRow();
12026
+ repaintFindBoxChrome();
12027
+ });
12028
+ return;
12029
+ }
12030
+ const next = Math.min(
12031
+ findResults.length - 1,
12032
+ Math.max(0, findSelectedIdx + delta)
12033
+ );
12034
+ if (next === findSelectedIdx) {
12035
+ return;
12036
+ }
12037
+ const oldIdx = findSelectedIdx;
12038
+ const oldScroll = findScrollOffset;
12039
+ findSelectedIdx = next;
12040
+ findSnippetIdx = 0;
12041
+ adjustFindScroll();
12042
+ if (findScrollOffset !== oldScroll) {
12043
+ repaintFindViewport();
12044
+ } else {
12045
+ withSync(() => {
12046
+ repaintFindResult(oldIdx, false);
12047
+ repaintFindResult(findSelectedIdx, true);
12048
+ });
12049
+ repaintFindIndicatorRow();
12050
+ }
12051
+ };
12052
+ switch (name) {
12053
+ case "UP":
12054
+ case "SHIFT_TAB":
12055
+ case "CTRL_P":
12056
+ moveDeep(-1);
12057
+ return;
12058
+ case "DOWN":
12059
+ case "TAB":
12060
+ case "CTRL_N":
12061
+ moveDeep(1);
12062
+ return;
12063
+ case "PAGE_UP":
12064
+ moveDeep(-findViewportSize());
12065
+ return;
12066
+ case "PAGE_DOWN":
12067
+ moveDeep(findViewportSize());
12068
+ return;
12069
+ case "HOME":
12070
+ moveDeep(-findSelectedIdx);
12071
+ return;
12072
+ case "END":
12073
+ moveDeep(findResults.length);
12074
+ return;
12075
+ }
12076
+ return;
12077
+ }
12078
+ };
12079
+ pushLayer({ onKey: findOnKey, onResize: () => renderFind() });
12080
+ };
12081
+ const onKey = (name, _matches, data) => {
12082
+ if (mode === "busy") {
10628
12083
  return;
10629
12084
  }
10630
12085
  if (mode === "rename") {
@@ -10688,6 +12143,10 @@ ${cells}`;
10688
12143
  return;
10689
12144
  }
10690
12145
  clearTransient();
12146
+ if (name === "CTRL_F") {
12147
+ openFindLayer();
12148
+ return;
12149
+ }
10691
12150
  if (selectedIdx === 0 && !searchActive) {
10692
12151
  if (name === "ESCAPE" || name === "CTRL_C" || name === "CTRL_D") {
10693
12152
  cleanup();
@@ -10759,8 +12218,7 @@ ${cells}`;
10759
12218
  return;
10760
12219
  }
10761
12220
  if (!searchActive && data?.isCharacter && name === "?") {
10762
- mode = "help";
10763
- renderHelp();
12221
+ openHelpLayer();
10764
12222
  return;
10765
12223
  }
10766
12224
  if (searchActive) {
@@ -10820,13 +12278,7 @@ ${cells}`;
10820
12278
  const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
10821
12279
  prefs.filters.cwdOnly = !prefs.filters.cwdOnly;
10822
12280
  applyFilter();
10823
- if (keepId !== void 0) {
10824
- const idx = visible.findIndex((s) => s.sessionId === keepId);
10825
- if (idx >= 0) {
10826
- selectedIdx = idx + 1;
10827
- adjustScroll();
10828
- }
10829
- }
12281
+ restoreCursorAfterFilter(keepId);
10830
12282
  renderFromScratch();
10831
12283
  return;
10832
12284
  }
@@ -10837,13 +12289,15 @@ ${cells}`;
10837
12289
  allSessions
10838
12290
  );
10839
12291
  applyFilter();
10840
- if (keepId !== void 0) {
10841
- const idx = visible.findIndex((s) => s.sessionId === keepId);
10842
- if (idx >= 0) {
10843
- selectedIdx = idx + 1;
10844
- adjustScroll();
10845
- }
10846
- }
12292
+ restoreCursorAfterFilter(keepId);
12293
+ renderFromScratch();
12294
+ return;
12295
+ }
12296
+ if (name === "i" || name === "I") {
12297
+ const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
12298
+ prefs.filters.showCat = !prefs.filters.showCat;
12299
+ applyFilter();
12300
+ restoreCursorAfterFilter(keepId);
10847
12301
  renderFromScratch();
10848
12302
  return;
10849
12303
  }
@@ -10980,6 +12434,12 @@ ${cells}`;
10980
12434
  return;
10981
12435
  }
10982
12436
  };
12437
+ pushLayer({
12438
+ onKey: (name, _matches, data) => onKey(name, _matches, data),
12439
+ onResize: () => {
12440
+ if (!resolved) renderFromScratch();
12441
+ }
12442
+ });
10983
12443
  term.grabInput({});
10984
12444
  const tSetup = term;
10985
12445
  if (tSetup.stdin && typeof tSetup.onStdin === "function") {
@@ -10988,10 +12448,10 @@ ${cells}`;
10988
12448
  tSetup.stdin.on("data", rawStdinHandler);
10989
12449
  process.stdout.write("\x1B[?2004h");
10990
12450
  }
10991
- term.on("key", onKey);
10992
- term.on("resize", onResize);
12451
+ term.on("key", dispatch);
12452
+ term.on("resize", dispatchResize);
10993
12453
  autoRefreshTimer = setInterval(() => {
10994
- if (resolved || mode !== "normal" || searchActive || autoRefreshInFlight) {
12454
+ if (resolved || focusStack.length > 1 || mode !== "normal" || searchActive || autoRefreshInFlight) {
10995
12455
  return;
10996
12456
  }
10997
12457
  const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
@@ -11002,10 +12462,10 @@ ${cells}`;
11002
12462
  }, 3e3);
11003
12463
  });
11004
12464
  }
11005
- function readTermHeight(term) {
12465
+ function readTermHeight2(term) {
11006
12466
  return term.height ?? 24;
11007
12467
  }
11008
- function readTermWidth(term) {
12468
+ function readTermWidth2(term) {
11009
12469
  return term.width ?? 80;
11010
12470
  }
11011
12471
  function formatComposerTitle(cwd, maxWidth) {
@@ -11060,19 +12520,22 @@ function matchesSearch(s, term) {
11060
12520
  }
11061
12521
  return false;
11062
12522
  }
11063
- var ROW_PREFIX_WIDTH, PICKER_COMPOSER_MAX_ROWS, BOX_HORIZONTAL_PAD, HELP_KEYS_WIDTH, HELP_ENTRIES;
12523
+ var ROW_PREFIX_WIDTH, PICKER_COMPOSER_MAX_ROWS, FIND_BOX_MAX_ROWS, BOX_HORIZONTAL_PAD, HELP_KEYS_WIDTH, HELP_ENTRIES;
11064
12524
  var init_picker = __esm({
11065
12525
  "src/tui/picker.ts"() {
11066
12526
  "use strict";
11067
12527
  init_session_row();
11068
12528
  init_paths();
11069
12529
  init_session();
12530
+ init_hydra_version();
11070
12531
  init_discovery();
11071
12532
  init_input();
11072
12533
  init_screen();
12534
+ init_import_action_prompt();
11073
12535
  init_sync();
11074
12536
  ROW_PREFIX_WIDTH = 2;
11075
12537
  PICKER_COMPOSER_MAX_ROWS = 4;
12538
+ FIND_BOX_MAX_ROWS = 4;
11076
12539
  BOX_HORIZONTAL_PAD = 4;
11077
12540
  HELP_KEYS_WIDTH = 20;
11078
12541
  HELP_ENTRIES = [
@@ -11085,9 +12548,11 @@ var init_picker = __esm({
11085
12548
  ["Enter", "open selected session"],
11086
12549
  ["v", "view-only (open transcript without spawning the agent)"],
11087
12550
  null,
11088
- ["/", "search sessions"],
12551
+ ["/", "search sessions (metadata)"],
12552
+ ["^f", "find in session history (content + tool inputs)"],
11089
12553
  ["o", "toggle cwd-only filter"],
11090
12554
  ["h", "cycle host filter (local / <peer> / all)"],
12555
+ ["i", "toggle include-cat filter"],
11091
12556
  ["r", "refresh from daemon"],
11092
12557
  null,
11093
12558
  ["k", "kill the selected live session"],
@@ -11116,105 +12581,15 @@ async function validateLocalCwd(input) {
11116
12581
  } catch {
11117
12582
  return { ok: false, reason: `${resolved} does not exist` };
11118
12583
  }
11119
- if (!stat5.isDirectory()) {
11120
- return { ok: false, reason: `${resolved} is not a directory` };
11121
- }
11122
- return { ok: true, path: resolved };
11123
- }
11124
- var init_cwd = __esm({
11125
- "src/core/cwd.ts"() {
11126
- "use strict";
11127
- init_config();
11128
- }
11129
- });
11130
-
11131
- // src/tui/prompt-utils.ts
11132
- function resetTerminalModes() {
11133
- process.stdout.write("\x1B[<u");
11134
- process.stdout.write("\x1B[?2004l");
11135
- process.stdout.write("\x1B[>4;0m");
11136
- process.stdout.write("\x1B[>5;0m");
11137
- process.stdout.write("\x1B[?1000l");
11138
- process.stdout.write("\x1B[?1002l");
11139
- process.stdout.write("\x1B[?1006l");
11140
- process.stdout.write("\x1B[?1l");
11141
- process.stdout.write("\x1B>");
11142
- }
11143
- function readTermWidth2(term) {
11144
- return term.width ?? 80;
11145
- }
11146
- function readTermHeight2(term) {
11147
- return term.height ?? 24;
11148
- }
11149
- function drawBox(term, opts) {
11150
- const termW = readTermWidth2(term);
11151
- const termH = readTermHeight2(term);
11152
- const desiredContentW = opts.contentWidth ?? MAX_BOX_WIDTH;
11153
- const maxContentW = Math.max(10, Math.min(MAX_BOX_WIDTH, termW - 4));
11154
- const contentW = Math.min(desiredContentW, maxContentW);
11155
- const w = contentW + 2;
11156
- const contentH = Math.max(1, Math.min(opts.contentHeight, termH - 4));
11157
- const h = contentH + 2;
11158
- const x = Math.max(1, Math.floor((termW - w) / 2) + 1);
11159
- const y = Math.max(1, Math.floor((termH - h) / 2) + 1);
11160
- term.moveTo(1, 1).eraseDisplayBelow();
11161
- const topInner = HORIZ.repeat(w - 2);
11162
- const top = renderTitleStrip(topInner, opts.title);
11163
- term.moveTo(x, y);
11164
- term.dim.noFormat(TL);
11165
- paintTopStrip(term, top);
11166
- term.dim.noFormat(TR);
11167
- for (let row = 1; row <= contentH; row++) {
11168
- term.moveTo(x, y + row);
11169
- term.dim.noFormat(VERT);
11170
- term.moveTo(x + w - 1, y + row);
11171
- term.dim.noFormat(VERT);
11172
- }
11173
- term.moveTo(x, y + h - 1);
11174
- term.dim.noFormat(BL + HORIZ.repeat(w - 2) + BR);
11175
- return {
11176
- x,
11177
- y,
11178
- w,
11179
- h,
11180
- contentX: x + 1,
11181
- contentY: y + 1,
11182
- contentW,
11183
- contentH
11184
- };
11185
- }
11186
- function renderTitleStrip(innerDashes, title) {
11187
- if (!title) {
11188
- return { dashes: innerDashes };
11189
- }
11190
- const chip = ` ${title} `;
11191
- if (chip.length + 4 > innerDashes.length) {
11192
- return { dashes: innerDashes };
11193
- }
11194
- const offset = 2;
11195
- const dashes = innerDashes.slice(0, offset) + " ".repeat(chip.length) + innerDashes.slice(offset + chip.length);
11196
- return { dashes, title: { offset, text: chip } };
11197
- }
11198
- function paintTopStrip(term, strip) {
11199
- if (!strip.title) {
11200
- term.dim.noFormat(strip.dashes);
11201
- return;
12584
+ if (!stat5.isDirectory()) {
12585
+ return { ok: false, reason: `${resolved} is not a directory` };
11202
12586
  }
11203
- term.dim.noFormat(strip.dashes.slice(0, strip.title.offset));
11204
- term.brightCyan.noFormat(strip.title.text);
11205
- term.dim.noFormat(strip.dashes.slice(strip.title.offset + strip.title.text.length));
12587
+ return { ok: true, path: resolved };
11206
12588
  }
11207
- var MAX_BOX_WIDTH, HORIZ, VERT, TL, TR, BL, BR;
11208
- var init_prompt_utils = __esm({
11209
- "src/tui/prompt-utils.ts"() {
12589
+ var init_cwd = __esm({
12590
+ "src/core/cwd.ts"() {
11210
12591
  "use strict";
11211
- MAX_BOX_WIDTH = 64;
11212
- HORIZ = "\u2500";
11213
- VERT = "\u2502";
11214
- TL = "\u250C";
11215
- TR = "\u2510";
11216
- BL = "\u2514";
11217
- BR = "\u2518";
12592
+ init_config();
11218
12593
  }
11219
12594
  });
11220
12595
 
@@ -11246,7 +12621,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11246
12621
  for (const hr of headerRows) {
11247
12622
  term.moveTo(layout.contentX, layout.contentY + row);
11248
12623
  term.dim.noFormat(` ${hr.label}`);
11249
- term.noFormat(truncate2(hr.value, innerW - hr.label.length - 2));
12624
+ term.noFormat(truncate3(hr.value, innerW - hr.label.length - 2));
11250
12625
  row++;
11251
12626
  }
11252
12627
  row++;
@@ -11257,7 +12632,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11257
12632
  row += 2;
11258
12633
  if (errorLine !== null) {
11259
12634
  term.moveTo(layout.contentX, layout.contentY + row);
11260
- term.red.noFormat(` ${truncate2(errorLine, innerW - 2)}`);
12635
+ term.red.noFormat(` ${truncate3(errorLine, innerW - 2)}`);
11261
12636
  } else {
11262
12637
  term.moveTo(layout.contentX, layout.contentY + row);
11263
12638
  term.dim.noFormat(
@@ -11293,7 +12668,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11293
12668
  term.dim.noFormat("\u2502");
11294
12669
  term.moveTo(layout.contentX, layout.contentY + errRow);
11295
12670
  if (errorLine !== null) {
11296
- term.red.noFormat(` ${truncate2(errorLine, layout.contentW - 2)}`);
12671
+ term.red.noFormat(` ${truncate3(errorLine, layout.contentW - 2)}`);
11297
12672
  } else {
11298
12673
  term.dim.noFormat(
11299
12674
  " Enter accept \xB7 Esc back \xB7 ^U clear \xB7 ^W kill word"
@@ -11389,7 +12764,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11389
12764
  term.on("resize", onResize);
11390
12765
  });
11391
12766
  }
11392
- function truncate2(s, max) {
12767
+ function truncate3(s, max) {
11393
12768
  if (max <= 1) {
11394
12769
  return "";
11395
12770
  }
@@ -11417,226 +12792,6 @@ var init_import_cwd_prompt = __esm({
11417
12792
  }
11418
12793
  });
11419
12794
 
11420
- // src/tui/import-action-prompt.ts
11421
- function actionPromptStep(selected, key, choices = ACTION_CHOICES) {
11422
- if (key.kind === "cancel") {
11423
- return { kind: "cancel" };
11424
- }
11425
- if (key.kind === "back") {
11426
- return { kind: "back" };
11427
- }
11428
- if (key.kind === "enter") {
11429
- const choice = choices[selected];
11430
- if (!choice) {
11431
- return { kind: "back" };
11432
- }
11433
- return { kind: "resolve", action: choice.key };
11434
- }
11435
- if (key.kind === "up") {
11436
- return {
11437
- kind: "continue",
11438
- selected: Math.max(0, selected - 1)
11439
- };
11440
- }
11441
- if (key.kind === "down") {
11442
- return {
11443
- kind: "continue",
11444
- selected: Math.min(choices.length - 1, selected + 1)
11445
- };
11446
- }
11447
- if (key.kind === "char") {
11448
- const lower = key.ch.toLowerCase();
11449
- if (lower === "n") {
11450
- return {
11451
- kind: "continue",
11452
- selected: Math.min(choices.length - 1, selected + 1)
11453
- };
11454
- }
11455
- if (lower === "p") {
11456
- return {
11457
- kind: "continue",
11458
- selected: Math.max(0, selected - 1)
11459
- };
11460
- }
11461
- const idx = choices.findIndex((c) => c.hotkey.toLowerCase() === lower);
11462
- if (idx >= 0) {
11463
- const choice = choices[idx];
11464
- if (choice) {
11465
- return { kind: "resolve", action: choice.key };
11466
- }
11467
- }
11468
- }
11469
- return { kind: "continue", selected };
11470
- }
11471
- async function promptForImportAction(term, session) {
11472
- resetTerminalModes();
11473
- const shortId2 = stripHydraSessionPrefix(session.sessionId);
11474
- const fromMachine = session.importedFromMachine ?? "another machine";
11475
- const originalCwd = shortenHomePath(session.cwd);
11476
- let selected = ACTION_CHOICES.findIndex((c) => c.key === "view");
11477
- if (selected < 0) {
11478
- selected = 0;
11479
- }
11480
- const render = () => {
11481
- const choiceRows = ACTION_CHOICES.length * 2;
11482
- const contentHeight = 7 + choiceRows + 2;
11483
- const layout = drawBox(term, {
11484
- contentHeight,
11485
- title: "Imported session"
11486
- });
11487
- const innerW = layout.contentW;
11488
- const headerRows = [
11489
- { label: "session: ", value: shortId2 },
11490
- { label: "from: ", value: fromMachine },
11491
- { label: "cwd: ", value: originalCwd }
11492
- ];
11493
- let row = 0;
11494
- for (const hr of headerRows) {
11495
- term.moveTo(layout.contentX, layout.contentY + row);
11496
- term.dim.noFormat(` ${hr.label}`);
11497
- term.noFormat(truncate3(hr.value, innerW - hr.label.length - 2));
11498
- row++;
11499
- }
11500
- row++;
11501
- term.moveTo(layout.contentX, layout.contentY + row);
11502
- term.noFormat(" What do you want to do?");
11503
- row += 2;
11504
- for (let i = 0; i < ACTION_CHOICES.length; i++) {
11505
- const choice = ACTION_CHOICES[i];
11506
- if (!choice) {
11507
- continue;
11508
- }
11509
- const pointer = i === selected ? "\u276F" : " ";
11510
- const label = ` ${pointer} ${choice.label}`;
11511
- term.moveTo(layout.contentX, layout.contentY + row);
11512
- if (i === selected) {
11513
- term.brightWhite.bgBlue.noFormat(padRight(label, innerW));
11514
- } else {
11515
- term.noFormat(label);
11516
- }
11517
- row++;
11518
- term.moveTo(layout.contentX, layout.contentY + row);
11519
- term.dim.noFormat(` ${choice.description}`);
11520
- row++;
11521
- }
11522
- row++;
11523
- term.moveTo(layout.contentX, layout.contentY + row);
11524
- term.dim.noFormat(" \u2191/\u2193 navigate \xB7 Enter select \xB7 f/v jump \xB7 Esc back");
11525
- return layout;
11526
- };
11527
- render();
11528
- term.hideCursor();
11529
- return await new Promise((resolve6) => {
11530
- let resolved = false;
11531
- const cleanup = () => {
11532
- if (resolved) {
11533
- return;
11534
- }
11535
- resolved = true;
11536
- term.off("key", onKey);
11537
- term.off("resize", onResize);
11538
- term.grabInput(false);
11539
- term.hideCursor(false);
11540
- term.moveTo(1, 1).eraseDisplayBelow();
11541
- };
11542
- const finish = (value) => {
11543
- cleanup();
11544
- resolve6(value);
11545
- };
11546
- const onResize = () => {
11547
- if (resolved) {
11548
- return;
11549
- }
11550
- render();
11551
- };
11552
- const onKey = (name, _matches, data) => {
11553
- const input = mapKey(name, data);
11554
- if (!input) {
11555
- return;
11556
- }
11557
- const step = actionPromptStep(selected, input);
11558
- if (step.kind === "cancel") {
11559
- finish("cancel");
11560
- return;
11561
- }
11562
- if (step.kind === "back") {
11563
- finish("back");
11564
- return;
11565
- }
11566
- if (step.kind === "resolve") {
11567
- finish(step.action);
11568
- return;
11569
- }
11570
- if (step.selected !== selected) {
11571
- selected = step.selected;
11572
- render();
11573
- }
11574
- };
11575
- term.grabInput({});
11576
- term.on("key", onKey);
11577
- term.on("resize", onResize);
11578
- });
11579
- }
11580
- function mapKey(name, data) {
11581
- if (name === "UP") {
11582
- return { kind: "up" };
11583
- }
11584
- if (name === "DOWN") {
11585
- return { kind: "down" };
11586
- }
11587
- if (name === "ENTER" || name === "KP_ENTER") {
11588
- return { kind: "enter" };
11589
- }
11590
- if (name === "ESCAPE") {
11591
- return { kind: "back" };
11592
- }
11593
- if (name === "CTRL_C" || name === "CTRL_D") {
11594
- return { kind: "cancel" };
11595
- }
11596
- if (data?.isCharacter) {
11597
- return { kind: "char", ch: name };
11598
- }
11599
- return null;
11600
- }
11601
- function truncate3(s, max) {
11602
- if (max <= 1) {
11603
- return "";
11604
- }
11605
- if (s.length <= max) {
11606
- return s;
11607
- }
11608
- return s.slice(0, Math.max(0, max - 1)) + "\u2026";
11609
- }
11610
- function padRight(s, w) {
11611
- if (s.length >= w) {
11612
- return s.slice(0, w);
11613
- }
11614
- return s + " ".repeat(w - s.length);
11615
- }
11616
- var ACTION_CHOICES;
11617
- var init_import_action_prompt = __esm({
11618
- "src/tui/import-action-prompt.ts"() {
11619
- "use strict";
11620
- init_paths();
11621
- init_session();
11622
- init_prompt_utils();
11623
- ACTION_CHOICES = [
11624
- {
11625
- key: "fork-local",
11626
- label: "Fork locally",
11627
- hotkey: "f",
11628
- description: "spawn a local fork \u2014 original imported copy stays as-is"
11629
- },
11630
- {
11631
- key: "view",
11632
- label: "View transcript",
11633
- hotkey: "v",
11634
- description: "open read-only, no agent spawn"
11635
- }
11636
- ];
11637
- }
11638
- });
11639
-
11640
12795
  // src/tui/clipboard.ts
11641
12796
  import { spawn as nodeSpawn } from "child_process";
11642
12797
  import fs21 from "fs/promises";
@@ -12035,41 +13190,69 @@ function formatEvent(event) {
12035
13190
  return [];
12036
13191
  }
12037
13192
  }
12038
- function applyInlineMarkup(text) {
13193
+ function applyInlineMarkup(text, opts) {
13194
+ const codeOpen = opts?.codeOpen ?? "^C";
13195
+ const boldReset = opts?.boldReset ?? "^:";
13196
+ const codeReset = opts?.codeReset ?? "^:";
12039
13197
  let s = text.replace(/\^/g, "^^");
12040
- s = s.replace(/\*\*(.+?)\*\*/g, "^+$1^:");
12041
- s = s.replace(/`([^`]+)`/g, "^C$1^:");
13198
+ s = s.replace(/\*\*(.+?)\*\*/g, `^+$1${boldReset}`);
13199
+ s = s.replace(/`([^`]+)`/g, `${codeOpen}$1${codeReset}`);
12042
13200
  return s;
12043
13201
  }
12044
- function parseAgentMarkdown(text) {
13202
+ function parseMarkdown(text, opts) {
13203
+ const {
13204
+ proseStyle,
13205
+ highlightCode,
13206
+ prefixStyle,
13207
+ firstPrefix = " ",
13208
+ inlineOpts
13209
+ } = opts;
12045
13210
  const out = [];
12046
13211
  const lines = text.split("\n");
12047
13212
  let inCode = false;
12048
13213
  let codeLang = "";
12049
13214
  let codeBuffer = [];
13215
+ let firstNonBlank = firstPrefix !== " ";
13216
+ const line = (body, bodyStyle, prefix = " ") => {
13217
+ const entry = { prefix, body, bodyStyle };
13218
+ if (prefixStyle !== void 0)
13219
+ entry.prefixStyle = prefixStyle;
13220
+ out.push(entry);
13221
+ };
13222
+ const nextPrefix = () => {
13223
+ if (!firstNonBlank)
13224
+ return " ";
13225
+ firstNonBlank = false;
13226
+ return firstPrefix;
13227
+ };
12050
13228
  const flushCode = () => {
12051
- if (codeBuffer.length === 0) {
13229
+ if (codeBuffer.length === 0)
12052
13230
  return;
12053
- }
12054
- const highlighted = highlightFencedBlock(codeLang, codeBuffer);
12055
- for (const piece of highlighted) {
12056
- const entry = {
12057
- prefix: " ",
12058
- body: piece.body,
12059
- bodyStyle: "code",
12060
- fillRow: true
12061
- };
12062
- if (piece.ansi) {
12063
- entry.ansi = true;
13231
+ if (highlightCode) {
13232
+ const highlighted = highlightFencedBlock(codeLang, codeBuffer);
13233
+ for (const piece of highlighted) {
13234
+ const entry = {
13235
+ prefix: " ",
13236
+ body: piece.body,
13237
+ bodyStyle: "code",
13238
+ fillRow: true
13239
+ };
13240
+ if (prefixStyle !== void 0)
13241
+ entry.prefixStyle = prefixStyle;
13242
+ if (piece.ansi)
13243
+ entry.ansi = true;
13244
+ out.push(entry);
12064
13245
  }
12065
- out.push(entry);
13246
+ } else {
13247
+ for (const cl of codeBuffer)
13248
+ line(cl.replace(/\^/g, "^^"), proseStyle);
12066
13249
  }
12067
13250
  codeBuffer = [];
12068
13251
  codeLang = "";
12069
13252
  };
12070
13253
  for (let i = 0; i < lines.length; i++) {
12071
- const line = lines[i];
12072
- const fence = line.match(/^\s*```\s*(\w*)\s*$/);
13254
+ const l = lines[i];
13255
+ const fence = l.match(/^\s*```\s*(\w*)\s*$/);
12073
13256
  if (fence) {
12074
13257
  if (!inCode) {
12075
13258
  inCode = true;
@@ -12081,68 +13264,81 @@ function parseAgentMarkdown(text) {
12081
13264
  continue;
12082
13265
  }
12083
13266
  if (inCode) {
12084
- codeBuffer.push(line);
13267
+ codeBuffer.push(l);
12085
13268
  continue;
12086
13269
  }
12087
- const heading = line.match(/^(#{1,6})\s+(.*)$/);
13270
+ const heading = l.match(/^(#{1,6})\s+(.*)$/);
12088
13271
  if (heading) {
12089
13272
  const level = heading[1].length;
12090
- const text2 = heading[2] ?? "";
12091
- const style = level === 1 ? "heading-1" : level === 2 ? "heading-2" : "heading-3";
12092
- out.push({
12093
- prefix: " ",
12094
- body: text2,
12095
- bodyStyle: style
12096
- });
13273
+ const headingText = heading[2] ?? "";
13274
+ const headingStyle = highlightCode ? level === 1 ? "heading-1" : level === 2 ? "heading-2" : "heading-3" : proseStyle;
13275
+ line(headingText, headingStyle, nextPrefix());
12097
13276
  continue;
12098
13277
  }
12099
13278
  const next = lines[i + 1];
12100
- if (line.includes("|") && next !== void 0 && isTableSeparatorLine(next) && parseTableRow(line).length === parseTableRow(next).length) {
12101
- const header = parseTableRow(line);
13279
+ if (l.includes("|") && next !== void 0 && isTableSeparatorLine(next) && parseTableRow(l).length === parseTableRow(next).length) {
13280
+ const header = parseTableRow(l);
12102
13281
  const body = [];
12103
13282
  let j = i + 2;
12104
13283
  while (j < lines.length && lines[j].includes("|")) {
12105
13284
  body.push(parseTableRow(lines[j]));
12106
13285
  j++;
12107
13286
  }
12108
- out.push(...formatTable(header, body));
13287
+ const tableLines = formatTable(header, body);
13288
+ for (const tl of tableLines) {
13289
+ if (prefixStyle !== void 0)
13290
+ tl.prefixStyle = prefixStyle;
13291
+ out.push(tl);
13292
+ }
12109
13293
  i = j - 1;
12110
13294
  continue;
12111
13295
  }
12112
- const bullet = line.match(/^(\s*)[-*+]\s+(.*)$/);
13296
+ const bullet = l.match(/^(\s*)[-*+]\s+(.*)$/);
12113
13297
  if (bullet) {
12114
13298
  const indent = bullet[1] ?? "";
12115
13299
  const item = bullet[2] ?? "";
12116
- out.push({
12117
- prefix: " ",
12118
- body: `${indent}\u2022 ${applyInlineMarkup(item)}`,
12119
- bodyStyle: "agent"
12120
- });
13300
+ line(
13301
+ `${indent}\u2022 ${applyInlineMarkup(item, inlineOpts)}`,
13302
+ proseStyle,
13303
+ nextPrefix()
13304
+ );
12121
13305
  continue;
12122
13306
  }
12123
- const ordered = line.match(/^(\s*)(\d+)\.\s+(.*)$/);
13307
+ const ordered = l.match(/^(\s*)(\d+)\.\s+(.*)$/);
12124
13308
  if (ordered) {
12125
13309
  const indent = ordered[1] ?? "";
12126
13310
  const num = ordered[2] ?? "";
12127
13311
  const item = ordered[3] ?? "";
12128
- out.push({
12129
- prefix: " ",
12130
- body: `${indent}${num}. ${applyInlineMarkup(item)}`,
12131
- bodyStyle: "agent"
12132
- });
13312
+ line(
13313
+ `${indent}${num}. ${applyInlineMarkup(item, inlineOpts)}`,
13314
+ proseStyle,
13315
+ nextPrefix()
13316
+ );
12133
13317
  continue;
12134
13318
  }
12135
- out.push({
12136
- prefix: " ",
12137
- body: applyInlineMarkup(line),
12138
- bodyStyle: "agent"
12139
- });
13319
+ const isBlank = l.trim() === "";
13320
+ line(
13321
+ applyInlineMarkup(l, inlineOpts),
13322
+ proseStyle,
13323
+ isBlank ? " " : nextPrefix()
13324
+ );
12140
13325
  }
12141
- if (inCode) {
13326
+ if (inCode)
12142
13327
  flushCode();
12143
- }
12144
13328
  return out;
12145
13329
  }
13330
+ function parseAgentMarkdown(text) {
13331
+ return parseMarkdown(text, { proseStyle: "agent", highlightCode: true });
13332
+ }
13333
+ function parseThoughtMarkdown(text) {
13334
+ return parseMarkdown(text, {
13335
+ proseStyle: "thought",
13336
+ highlightCode: false,
13337
+ prefixStyle: "thought",
13338
+ firstPrefix: "\xB7 ",
13339
+ inlineOpts: { codeOpen: "^c", boldReset: "^-", codeReset: "^K" }
13340
+ });
13341
+ }
12146
13342
  function parseTableRow(line) {
12147
13343
  let s = line.trim();
12148
13344
  if (s.startsWith("|")) {
@@ -12893,6 +14089,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
12893
14089
  if (teardownStarted) {
12894
14090
  return { outcome: { outcome: "cancelled" } };
12895
14091
  }
14092
+ if (opts.dangerouslySkipPermissions) {
14093
+ return buildApproveResponse(params);
14094
+ }
12896
14095
  const p = params ?? {};
12897
14096
  const rawOptions = Array.isArray(p.options) ? p.options : [];
12898
14097
  const options = rawOptions.map((o) => ({
@@ -14169,6 +15368,33 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14169
15368
  agentKey = null;
14170
15369
  agentBuffer = "";
14171
15370
  };
15371
+ let thoughtBuffer = "";
15372
+ let thoughtKey = null;
15373
+ let thoughtSeq = 0;
15374
+ const renderThoughtBlock = () => {
15375
+ if (thoughtKey === null)
15376
+ return;
15377
+ const lines = parseThoughtMarkdown(thoughtBuffer);
15378
+ if (lines.length === 0)
15379
+ return;
15380
+ screen.upsertLines(thoughtKey, lines);
15381
+ };
15382
+ const appendThought = (text) => {
15383
+ if (text.length === 0)
15384
+ return;
15385
+ if (thoughtKey === null) {
15386
+ screen.ensureSeparator();
15387
+ thoughtKey = `thought:${thoughtSeq}`;
15388
+ thoughtSeq += 1;
15389
+ thoughtBuffer = "";
15390
+ }
15391
+ thoughtBuffer += text;
15392
+ renderThoughtBlock();
15393
+ };
15394
+ const closeThought = () => {
15395
+ thoughtKey = null;
15396
+ thoughtBuffer = "";
15397
+ };
14172
15398
  const renderToolsBlock = () => {
14173
15399
  if (toolsBlockStartedAt === null) {
14174
15400
  return;
@@ -14310,6 +15536,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14310
15536
  recordHistoryEntry(event.text);
14311
15537
  }
14312
15538
  closeAgentText();
15539
+ closeThought();
14313
15540
  if (toolsBlockStartedAt !== null) {
14314
15541
  toolsBlockEndedAt = Date.now();
14315
15542
  renderToolsBlock();
@@ -14333,16 +15560,18 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14333
15560
  return;
14334
15561
  }
14335
15562
  if (event.kind === "agent-text") {
15563
+ closeThought();
14336
15564
  appendAgentText(event.text);
14337
15565
  return;
14338
15566
  }
14339
15567
  if (event.kind === "agent-thought") {
14340
15568
  closeAgentText();
14341
- screen.appendStreaming(event.text, "\xB7 ", "thought", "thought");
15569
+ appendThought(event.text);
14342
15570
  return;
14343
15571
  }
14344
15572
  if (event.kind === "exit-plan-mode") {
14345
15573
  closeAgentText();
15574
+ closeThought();
14346
15575
  const existing = exitPlanStates.get(event.toolCallId);
14347
15576
  const merged = {
14348
15577
  plan: event.plan ?? existing?.plan ?? "",
@@ -14360,12 +15589,14 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14360
15589
  }
14361
15590
  if (event.kind === "tool-call") {
14362
15591
  closeAgentText();
15592
+ closeThought();
14363
15593
  recordToolCall(event.toolCallId, event.title, event.status, void 0);
14364
15594
  renderToolsBlock();
14365
15595
  return;
14366
15596
  }
14367
15597
  if (event.kind === "plan") {
14368
15598
  closeAgentText();
15599
+ closeThought();
14369
15600
  lastPlanEvent = event;
14370
15601
  const lines = formatEvent(event);
14371
15602
  if (lines.length > 0) {
@@ -14375,6 +15606,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14375
15606
  }
14376
15607
  if (event.kind === "tool-call-update") {
14377
15608
  closeAgentText();
15609
+ closeThought();
14378
15610
  recordToolCall(
14379
15611
  event.toolCallId,
14380
15612
  event.title,
@@ -14397,6 +15629,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14397
15629
  if (event.kind === "turn-complete") {
14398
15630
  currentHeadMessageId = void 0;
14399
15631
  closeAgentText();
15632
+ closeThought();
14400
15633
  let effectiveStopReason = event.amended ? "amended" : event.stopReason;
14401
15634
  if (!event.amended && upstreamInterruptedSeen && (effectiveStopReason === void 0 || effectiveStopReason === "end_turn")) {
14402
15635
  effectiveStopReason = "error";
@@ -14491,6 +15724,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14491
15724
  resolve6({ outcome: { outcome: "cancelled" } });
14492
15725
  }
14493
15726
  closeAgentText();
15727
+ closeThought();
14494
15728
  };
14495
15729
  const markToolsBlockRecoveryFailed = () => {
14496
15730
  if (toolsBlockStartedAt === null) {
@@ -14870,6 +16104,7 @@ var init_app = __esm({
14870
16104
  init_session();
14871
16105
  init_paths();
14872
16106
  init_hydra_version();
16107
+ init_permission_pick();
14873
16108
  init_update_check();
14874
16109
  init_history();
14875
16110
  init_discovery();
@@ -14938,9 +16173,14 @@ import { dirname as dirname6, resolve as resolve5 } from "path";
14938
16173
  // src/cli/parse-args.ts
14939
16174
  var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
14940
16175
  "all",
16176
+ "dangerously-skip-permissions",
14941
16177
  "detach",
16178
+ "disabled",
16179
+ "follow",
16180
+ "force",
14942
16181
  "foreground",
14943
16182
  "help",
16183
+ "include-cat",
14944
16184
  "info",
14945
16185
  "json",
14946
16186
  "new",
@@ -14948,9 +16188,32 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
14948
16188
  "readonly",
14949
16189
  "replace",
14950
16190
  "rotate-token",
14951
- "stream",
14952
16191
  "version"
14953
16192
  ]);
16193
+ var KNOWN_VALUE_FLAGS = /* @__PURE__ */ new Set([
16194
+ "agent",
16195
+ "args",
16196
+ "command",
16197
+ "cwd",
16198
+ "env",
16199
+ "host",
16200
+ "model",
16201
+ "name",
16202
+ "out",
16203
+ "prompt",
16204
+ "session",
16205
+ "stream-bytes",
16206
+ "stream-threshold",
16207
+ "tail"
16208
+ ]);
16209
+ function validateKnownFlags(flags) {
16210
+ for (const key of Object.keys(flags)) {
16211
+ if (!KNOWN_BOOLEAN_FLAGS.has(key) && !KNOWN_VALUE_FLAGS.has(key)) {
16212
+ return key;
16213
+ }
16214
+ }
16215
+ return void 0;
16216
+ }
14954
16217
  function parseArgs(argv) {
14955
16218
  const positional = [];
14956
16219
  const flags = {};
@@ -16097,6 +17360,10 @@ var PersistedUsage = z4.object({
16097
17360
  costCurrency: z4.string().optional(),
16098
17361
  cumulativeCost: z4.number().optional()
16099
17362
  });
17363
+ var PersistedOriginatingClient = z4.object({
17364
+ name: z4.string(),
17365
+ version: z4.string().optional()
17366
+ });
16100
17367
  var SessionRecord = z4.object({
16101
17368
  version: z4.literal(1),
16102
17369
  sessionId: z4.string(),
@@ -16147,6 +17414,10 @@ var SessionRecord = z4.object({
16147
17414
  // Set when this session was spawned as a child by a transformer via
16148
17415
  // hydra-acp/spawn_child_session. Points to the spawning session's id.
16149
17416
  parentSessionId: z4.string().optional(),
17417
+ // clientInfo from the process that issued session/new. Picker and
17418
+ // `sessions list` use this to hide cat-style ancillary sessions by
17419
+ // default; carried in meta.json so cold sessions filter the same way.
17420
+ originatingClient: PersistedOriginatingClient.optional(),
16150
17421
  createdAt: z4.string(),
16151
17422
  updatedAt: z4.string()
16152
17423
  });
@@ -16269,6 +17540,7 @@ function recordFromMemorySession(args) {
16269
17540
  agentModels: args.agentModels,
16270
17541
  pendingHistorySync: args.pendingHistorySync,
16271
17542
  parentSessionId: args.parentSessionId,
17543
+ originatingClient: args.originatingClient,
16272
17544
  createdAt: args.createdAt ?? now,
16273
17545
  updatedAt: args.updatedAt ?? now
16274
17546
  };
@@ -16453,6 +17725,7 @@ var SessionManager = class {
16453
17725
  this.defaultTransformers = options.defaultTransformers ?? [];
16454
17726
  this.logger = options.logger;
16455
17727
  this.npmRegistry = options.npmRegistry;
17728
+ this.extensionCommands = options.extensionCommands;
16456
17729
  }
16457
17730
  registry;
16458
17731
  sessions = /* @__PURE__ */ new Map();
@@ -16471,6 +17744,7 @@ var SessionManager = class {
16471
17744
  metaWriteQueues = /* @__PURE__ */ new Map();
16472
17745
  logger;
16473
17746
  npmRegistry;
17747
+ extensionCommands;
16474
17748
  async create(params) {
16475
17749
  const fresh = await this.bootstrapAgent({
16476
17750
  agentId: params.agentId,
@@ -16524,7 +17798,9 @@ var SessionManager = class {
16524
17798
  agentModes: fresh.initialModes,
16525
17799
  agentModels: fresh.initialModels,
16526
17800
  transformChain: params.transformChain,
16527
- parentSessionId: params.parentSessionId
17801
+ parentSessionId: params.parentSessionId,
17802
+ originatingClient: params.originatingClient,
17803
+ extensionCommands: this.extensionCommands
16528
17804
  });
16529
17805
  await this.attachManagerHooks(session);
16530
17806
  return session;
@@ -16595,12 +17871,14 @@ var SessionManager = class {
16595
17871
  }
16596
17872
  let loadResult;
16597
17873
  try {
17874
+ const loadMeta = buildSessionLoadMeta(params.agentId, params.currentModel);
16598
17875
  loadResult = await agent.connection.request(
16599
17876
  "session/load",
16600
17877
  {
16601
17878
  sessionId: params.upstreamSessionId,
16602
17879
  cwd: params.cwd,
16603
- mcpServers: []
17880
+ mcpServers: [],
17881
+ ...loadMeta && { _meta: loadMeta }
16604
17882
  }
16605
17883
  );
16606
17884
  } catch (err) {
@@ -16616,7 +17894,10 @@ var SessionManager = class {
16616
17894
  () => void 0
16617
17895
  );
16618
17896
  } else {
16619
- agent.connection.drainBuffered("session/update");
17897
+ const drain1Count = agent.connection.drainBuffered("session/update");
17898
+ this.logger?.info(
17899
+ `resurrect: drain1 dropped ${drain1Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
17900
+ );
16620
17901
  }
16621
17902
  const agentReportedMode = extractInitialCurrentMode(loadResult ?? {});
16622
17903
  const advertisedModes = params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {}));
@@ -16634,6 +17915,30 @@ var SessionManager = class {
16634
17915
  this.logger?.info(
16635
17916
  `resurrect: effectiveMode=${JSON.stringify(effectiveMode)} for sessionId=${params.hydraSessionId}`
16636
17917
  );
17918
+ const agentReportedModel = extractInitialModel(loadResult ?? {});
17919
+ const advertisedModels = nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})) ?? params.agentModels;
17920
+ this.logger?.info(
17921
+ `resurrect: sessionId=${params.hydraSessionId} persistedModel=${JSON.stringify(params.currentModel)} agentReportedModel=${JSON.stringify(agentReportedModel)} advertisedModels=${JSON.stringify(advertisedModels?.map((m) => m.modelId))}`
17922
+ );
17923
+ if (params.pendingHistorySync !== true) {
17924
+ const drain2Count = agent.connection.drainBuffered("session/update");
17925
+ this.logger?.info(
17926
+ `resurrect: drain2 (post-mode-restore) dropped ${drain2Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
17927
+ );
17928
+ }
17929
+ const effectiveModel = await restoreCurrentModel({
17930
+ agent,
17931
+ upstreamSessionId: params.upstreamSessionId,
17932
+ persistedModel: params.currentModel,
17933
+ agentReportedModel,
17934
+ logger: this.logger
17935
+ });
17936
+ if (params.pendingHistorySync !== true) {
17937
+ const drain3Count = agent.connection.drainBuffered("session/update");
17938
+ this.logger?.info(
17939
+ `resurrect: drain3 (post-model-restore) dropped ${drain3Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
17940
+ );
17941
+ }
16637
17942
  const session = new Session({
16638
17943
  sessionId: params.hydraSessionId,
16639
17944
  cwd: params.cwd,
@@ -16650,11 +17955,7 @@ var SessionManager = class {
16650
17955
  listSessions: () => this.list(),
16651
17956
  historyStore: this.histories,
16652
17957
  historyMaxEntries: this.sessionHistoryMaxEntries,
16653
- // Prefer what we previously stored from a current_model_update; if
16654
- // we never captured one (e.g. old opencode sessions on disk before
16655
- // this fix), fall back to the model the agent ships in its
16656
- // session/load response body.
16657
- currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
17958
+ currentModel: effectiveModel,
16658
17959
  currentMode: effectiveMode,
16659
17960
  currentUsage: params.currentUsage,
16660
17961
  agentCommands: params.agentCommands,
@@ -16663,13 +17964,15 @@ var SessionManager = class {
16663
17964
  // snapshot — the proxy's available models can change between daemon
16664
17965
  // restarts (quota resets, rollouts), so meta.json is intentionally
16665
17966
  // treated as a cold fallback here, not the authoritative source.
16666
- agentModels: nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})) ?? params.agentModels,
17967
+ agentModels: advertisedModels,
16667
17968
  // Only gate the first-prompt title heuristic when we actually have
16668
17969
  // a title to preserve. A title-less session (lost to a write race
16669
17970
  // or never seeded) should re-derive from the next prompt rather
16670
17971
  // than stay stuck.
16671
17972
  firstPromptSeeded: !!params.title,
16672
- createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
17973
+ createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
17974
+ originatingClient: params.originatingClient,
17975
+ extensionCommands: this.extensionCommands
16673
17976
  });
16674
17977
  await this.attachManagerHooks(session);
16675
17978
  return session;
@@ -16688,7 +17991,11 @@ var SessionManager = class {
16688
17991
  cwd,
16689
17992
  agentArgs: params.agentArgs,
16690
17993
  mcpServers: [],
16691
- onInstallProgress: params.onInstallProgress
17994
+ onInstallProgress: params.onInstallProgress,
17995
+ // Pass the persisted model so bootstrapAgent calls session/set_model
17996
+ // during session/new — the only context where the agent reliably
17997
+ // honours the switch.
17998
+ model: params.currentModel
16692
17999
  });
16693
18000
  const advertisedModes = params.agentModes ?? fresh.initialModes;
16694
18001
  const effectiveMode = await restoreCurrentMode({
@@ -16699,6 +18006,15 @@ var SessionManager = class {
16699
18006
  advertisedModes,
16700
18007
  logger: this.logger
16701
18008
  });
18009
+ const advertisedModels = params.agentModels ?? fresh.initialModels;
18010
+ const effectiveModel = await restoreCurrentModel({
18011
+ agent: fresh.agent,
18012
+ upstreamSessionId: fresh.upstreamSessionId,
18013
+ persistedModel: params.currentModel,
18014
+ agentReportedModel: fresh.initialModel,
18015
+ logger: this.logger
18016
+ });
18017
+ fresh.agent.connection.drainBuffered("session/update");
16702
18018
  const session = new Session({
16703
18019
  sessionId: params.hydraSessionId,
16704
18020
  cwd,
@@ -16715,16 +18031,16 @@ var SessionManager = class {
16715
18031
  listSessions: () => this.list(),
16716
18032
  historyStore: this.histories,
16717
18033
  historyMaxEntries: this.sessionHistoryMaxEntries,
16718
- // Prefer the stored value (set by a previous current_model_update);
16719
- // fall back to whatever the agent ships in its session/new response.
16720
- currentModel: params.currentModel ?? fresh.initialModel,
18034
+ currentModel: effectiveModel,
16721
18035
  currentMode: effectiveMode,
16722
18036
  currentUsage: params.currentUsage,
16723
18037
  agentCommands: params.agentCommands,
16724
18038
  agentModes: advertisedModes,
16725
- agentModels: params.agentModels ?? fresh.initialModels,
18039
+ agentModels: advertisedModels,
16726
18040
  firstPromptSeeded: !!params.title,
16727
- createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
18041
+ createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
18042
+ originatingClient: params.originatingClient,
18043
+ extensionCommands: this.extensionCommands
16728
18044
  });
16729
18045
  await this.attachManagerHooks(session);
16730
18046
  void session.seedFromImport().catch(() => void 0);
@@ -17073,7 +18389,8 @@ var SessionManager = class {
17073
18389
  agentModes: record.agentModes,
17074
18390
  agentModels: record.agentModels,
17075
18391
  createdAt: record.createdAt,
17076
- pendingHistorySync: record.pendingHistorySync
18392
+ pendingHistorySync: record.pendingHistorySync,
18393
+ originatingClient: record.originatingClient
17077
18394
  };
17078
18395
  }
17079
18396
  async clearPendingHistorySync(sessionId) {
@@ -17174,6 +18491,7 @@ var SessionManager = class {
17174
18491
  currentModel: session.currentModel,
17175
18492
  currentUsage: session.currentUsage,
17176
18493
  parentSessionId: session.parentSessionId,
18494
+ originatingClient: session.originatingClient,
17177
18495
  updatedAt: used,
17178
18496
  attachedClients: session.attachedCount,
17179
18497
  status: "live",
@@ -17203,6 +18521,7 @@ var SessionManager = class {
17203
18521
  importedFromMachine: r.importedFromMachine,
17204
18522
  importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
17205
18523
  parentSessionId: r.parentSessionId,
18524
+ originatingClient: r.originatingClient,
17206
18525
  updatedAt: used,
17207
18526
  attachedClients: 0,
17208
18527
  status: "cold",
@@ -17548,6 +18867,7 @@ function mergeForPersistence(session, existing) {
17548
18867
  agentModes,
17549
18868
  agentModels,
17550
18869
  parentSessionId: session.parentSessionId ?? existing?.parentSessionId,
18870
+ originatingClient: session.originatingClient ?? existing?.originatingClient,
17551
18871
  createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
17552
18872
  });
17553
18873
  }
@@ -17576,6 +18896,13 @@ function usageSnapshotToPersisted(usage) {
17576
18896
  function persistedUsageToSnapshot(usage) {
17577
18897
  return usage ? { ...usage } : void 0;
17578
18898
  }
18899
+ function buildSessionLoadMeta(agentId, model) {
18900
+ if (!model)
18901
+ return void 0;
18902
+ if (agentId === "claude-acp")
18903
+ return { claudeCode: { options: { model } } };
18904
+ return void 0;
18905
+ }
17579
18906
  function extractInitialModel(result) {
17580
18907
  const direct = asString(result.currentModelId) ?? asString(result.currentModel) ?? asString(result.modelId) ?? asString(result.model);
17581
18908
  if (direct) {
@@ -17755,6 +19082,33 @@ async function restoreCurrentMode(opts) {
17755
19082
  return agentReportedMode;
17756
19083
  }
17757
19084
  }
19085
+ async function restoreCurrentModel(opts) {
19086
+ const { agent, upstreamSessionId, persistedModel, agentReportedModel, logger } = opts;
19087
+ if (!persistedModel) {
19088
+ return agentReportedModel;
19089
+ }
19090
+ if (persistedModel === agentReportedModel) {
19091
+ return persistedModel;
19092
+ }
19093
+ try {
19094
+ logger?.info(
19095
+ `resurrect: pushing persisted modelId=${JSON.stringify(persistedModel)} to agent (agentReported=${JSON.stringify(agentReportedModel)})`
19096
+ );
19097
+ await agent.connection.request("session/set_model", {
19098
+ sessionId: upstreamSessionId,
19099
+ modelId: persistedModel
19100
+ });
19101
+ logger?.info(
19102
+ `resurrect: session/set_model accepted, effectiveModel=${JSON.stringify(persistedModel)}`
19103
+ );
19104
+ return persistedModel;
19105
+ } catch (err) {
19106
+ logger?.warn(
19107
+ `resurrect: session/set_model rejected by agent for modelId=${JSON.stringify(persistedModel)} (${err.message}); session will use ${JSON.stringify(agentReportedModel)}`
19108
+ );
19109
+ return agentReportedModel;
19110
+ }
19111
+ }
17758
19112
  function parseModesList(list) {
17759
19113
  if (!Array.isArray(list)) {
17760
19114
  return [];
@@ -18710,6 +20064,55 @@ function withCode3(err, code) {
18710
20064
  return err;
18711
20065
  }
18712
20066
 
20067
+ // src/core/extension-commands.ts
20068
+ var ExtensionCommandRegistry = class {
20069
+ entries = /* @__PURE__ */ new Map();
20070
+ changeHandlers = [];
20071
+ register(name, connection, commands) {
20072
+ this.entries.set(name, { connection, commands: [...commands] });
20073
+ this.fireChanged();
20074
+ }
20075
+ clear(name) {
20076
+ if (this.entries.delete(name)) {
20077
+ this.fireChanged();
20078
+ }
20079
+ }
20080
+ get(name) {
20081
+ return this.entries.get(name);
20082
+ }
20083
+ has(name) {
20084
+ return this.entries.has(name);
20085
+ }
20086
+ // Snapshot of every (name, command) pair. Order is stable per-name
20087
+ // (insertion order of the map and the original commands list).
20088
+ list() {
20089
+ const out = [];
20090
+ for (const [name, entry] of this.entries) {
20091
+ for (const command of entry.commands) {
20092
+ out.push({ name, command });
20093
+ }
20094
+ }
20095
+ return out;
20096
+ }
20097
+ onChange(handler) {
20098
+ this.changeHandlers.push(handler);
20099
+ return () => {
20100
+ const i = this.changeHandlers.indexOf(handler);
20101
+ if (i >= 0) {
20102
+ this.changeHandlers.splice(i, 1);
20103
+ }
20104
+ };
20105
+ }
20106
+ fireChanged() {
20107
+ for (const h of this.changeHandlers) {
20108
+ try {
20109
+ h();
20110
+ } catch {
20111
+ }
20112
+ }
20113
+ }
20114
+ };
20115
+
18713
20116
  // src/daemon/server.ts
18714
20117
  init_paths();
18715
20118
 
@@ -19443,37 +20846,410 @@ function isVisible(event) {
19443
20846
  return true;
19444
20847
  }
19445
20848
  }
19446
- function formatToolLine(state) {
19447
- const status = state.status;
19448
- const suffix = status === "completed" || status === void 0 ? "" : ` _(${status})_`;
19449
- return `${escapeInline(state.title)}${suffix}`;
20849
+ function formatToolLine(state) {
20850
+ const status = state.status;
20851
+ const suffix = status === "completed" || status === void 0 ? "" : ` _(${status})_`;
20852
+ return `${escapeInline(state.title)}${suffix}`;
20853
+ }
20854
+ function statusGlyph(status) {
20855
+ switch (status) {
20856
+ case "completed":
20857
+ return "\u2713";
20858
+ case "failed":
20859
+ return "\u2717";
20860
+ case "cancelled":
20861
+ case "rejected":
20862
+ return "\u2298";
20863
+ case "in_progress":
20864
+ return "\u21BB";
20865
+ default:
20866
+ return "\xB7";
20867
+ }
20868
+ }
20869
+ function escapeInline(text) {
20870
+ return text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
20871
+ }
20872
+ function formatNumber(n) {
20873
+ return n.toLocaleString("en-US");
20874
+ }
20875
+
20876
+ // src/daemon/routes/sessions.ts
20877
+ init_types();
20878
+ init_hydra_version();
20879
+ init_remote_url();
20880
+
20881
+ // src/core/history-search.ts
20882
+ init_render_update();
20883
+ function parseQuery(raw) {
20884
+ const trimmed = raw.trim();
20885
+ if (trimmed.length === 0) {
20886
+ return { operator: "OR", terms: [] };
20887
+ }
20888
+ const tokenRe = /\w+:"[^"]*"|"[^"]*"|\S+/g;
20889
+ const tokens = [];
20890
+ let m;
20891
+ while ((m = tokenRe.exec(trimmed)) !== null) {
20892
+ tokens.push(m[0]);
20893
+ }
20894
+ let operator = "OR";
20895
+ let sawAnd = false;
20896
+ let sawOr = false;
20897
+ const termTokens = [];
20898
+ for (const tok of tokens) {
20899
+ const upper = tok.toUpperCase();
20900
+ if (upper === "AND") {
20901
+ sawAnd = true;
20902
+ } else if (upper === "OR") {
20903
+ sawOr = true;
20904
+ } else {
20905
+ termTokens.push(tok);
20906
+ }
20907
+ }
20908
+ if (sawAnd) {
20909
+ operator = "AND";
20910
+ } else if (sawOr) {
20911
+ operator = "OR";
20912
+ }
20913
+ const terms = termTokens.map((tok) => parseTermToken(tok)).filter((t) => t.term.length > 0);
20914
+ return { operator, terms };
20915
+ }
20916
+ function parseTermToken(tok) {
20917
+ const pq = /^(\w+):"([^"]*)"$/.exec(tok);
20918
+ if (pq) {
20919
+ return { scope: prefixToScope(pq[1]), term: pq[2] };
20920
+ }
20921
+ const q = /^"([^"]*)"$/.exec(tok);
20922
+ if (q) {
20923
+ return { scope: "all", term: q[1] };
20924
+ }
20925
+ const pb = /^(prompt|response|tool):([\s\S]*)$/i.exec(tok);
20926
+ if (pb) {
20927
+ return { scope: prefixToScope(pb[1]), term: pb[2].trim() };
20928
+ }
20929
+ return { scope: "all", term: tok.trim() };
20930
+ }
20931
+ function prefixToScope(prefix) {
20932
+ switch (prefix.toLowerCase()) {
20933
+ case "prompt":
20934
+ return "user";
20935
+ case "response":
20936
+ return "agent";
20937
+ case "tool":
20938
+ return "tool";
20939
+ default:
20940
+ return "all";
20941
+ }
20942
+ }
20943
+ function scopeMatchesKind(scope, kind) {
20944
+ if (scope === "all") {
20945
+ return true;
20946
+ }
20947
+ if (scope === "user") {
20948
+ return kind === "user";
20949
+ }
20950
+ if (scope === "agent") {
20951
+ return kind === "agent" || kind === "thought";
20952
+ }
20953
+ return kind === "tool" || kind === "tool-input";
20954
+ }
20955
+ var DEFAULT_MAX_SNIPPETS_PER_SESSION = 5;
20956
+ var DEFAULT_MAX_SESSIONS = 200;
20957
+ var SNIPPET_SIDE = 30;
20958
+ async function searchHistories(manager, query, opts = {}) {
20959
+ const parsed = parseQuery(query);
20960
+ if (parsed.terms.length === 0) {
20961
+ return { query, truncated: false, results: [] };
20962
+ }
20963
+ const maxPerSession = opts.maxSnippetsPerSession ?? DEFAULT_MAX_SNIPPETS_PER_SESSION;
20964
+ const maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
20965
+ const allow = opts.sessionIds ? new Set(opts.sessionIds) : null;
20966
+ const all = await manager.list();
20967
+ const candidates = allow ? all.filter((s) => allow.has(s.sessionId)) : all;
20968
+ const results = [];
20969
+ let truncated = false;
20970
+ for (const candidate of candidates) {
20971
+ if (results.length >= maxSessions) {
20972
+ truncated = true;
20973
+ break;
20974
+ }
20975
+ const entries = await manager.loadHistory(candidate.sessionId).catch(
20976
+ () => []
20977
+ );
20978
+ const found = scanSessionEntries(entries, parsed, maxPerSession);
20979
+ if (found.snippets.length === 0) {
20980
+ continue;
20981
+ }
20982
+ const hit = {
20983
+ sessionId: candidate.sessionId,
20984
+ cwd: candidate.cwd,
20985
+ status: candidate.status,
20986
+ updatedAt: candidate.updatedAt,
20987
+ totalMatches: found.totalMatches,
20988
+ snippets: found.snippets
20989
+ };
20990
+ if (candidate.title !== void 0) {
20991
+ hit.title = candidate.title;
20992
+ }
20993
+ results.push(hit);
20994
+ }
20995
+ return { query, truncated, results };
20996
+ }
20997
+ function scanSessionEntries(entries, query, maxSnippets) {
20998
+ if (query.terms.length === 0) {
20999
+ return { totalMatches: 0, snippets: [] };
21000
+ }
21001
+ let totalMatches = 0;
21002
+ const snippets = [];
21003
+ for (const { scope, term } of query.terms) {
21004
+ const result = scanForTerm(entries, term, scope, maxSnippets - snippets.length);
21005
+ if (query.operator === "AND" && result.totalMatches === 0) {
21006
+ return { totalMatches: 0, snippets: [] };
21007
+ }
21008
+ totalMatches += result.totalMatches;
21009
+ snippets.push(...result.snippets);
21010
+ }
21011
+ return { totalMatches, snippets };
21012
+ }
21013
+ function scanForTerm(entries, term, scope, snippetBudget) {
21014
+ const needle = term.toLowerCase();
21015
+ let totalMatches = 0;
21016
+ const snippets = [];
21017
+ for (const entry of entries) {
21018
+ const fragments = extractSearchableFragments(entry).filter(
21019
+ (f) => scopeMatchesKind(scope, f.kind)
21020
+ );
21021
+ for (const frag of fragments) {
21022
+ const hay = frag.text.toLowerCase();
21023
+ let idx = hay.indexOf(needle);
21024
+ if (idx === -1) {
21025
+ continue;
21026
+ }
21027
+ let occurrences = 0;
21028
+ while (idx !== -1) {
21029
+ occurrences++;
21030
+ idx = hay.indexOf(needle, idx + needle.length);
21031
+ }
21032
+ totalMatches += occurrences;
21033
+ if (snippets.length < snippetBudget) {
21034
+ const first = hay.indexOf(needle);
21035
+ const snippet = {
21036
+ kind: frag.kind,
21037
+ text: buildSnippet(frag.text, first, needle.length),
21038
+ recordedAt: entry.recordedAt
21039
+ };
21040
+ if (frag.toolName !== void 0) {
21041
+ snippet.toolName = frag.toolName;
21042
+ }
21043
+ snippets.push(snippet);
21044
+ }
21045
+ }
21046
+ }
21047
+ return { totalMatches, snippets };
21048
+ }
21049
+ function extractSearchableFragments(entry) {
21050
+ if (entry.method !== "session/update") {
21051
+ return [];
21052
+ }
21053
+ const params = entry.params;
21054
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
21055
+ return [];
21056
+ }
21057
+ const update = params.update;
21058
+ if (!update || typeof update !== "object" || Array.isArray(update)) {
21059
+ return [];
21060
+ }
21061
+ const u = update;
21062
+ const tag = typeof u.sessionUpdate === "string" ? u.sessionUpdate : u.kind;
21063
+ if (typeof tag !== "string") {
21064
+ return [];
21065
+ }
21066
+ switch (tag) {
21067
+ case "agent_message_chunk": {
21068
+ const text = readContentText(u.content);
21069
+ return text ? [{ kind: "agent", text }] : [];
21070
+ }
21071
+ case "agent_thought":
21072
+ case "agent_thought_chunk": {
21073
+ const text = typeof u.text === "string" ? sanitizeWireText(u.text) : readContentText(u.content);
21074
+ return text ? [{ kind: "thought", text }] : [];
21075
+ }
21076
+ case "user_message_chunk": {
21077
+ if (isCompatPromptReceived(u)) {
21078
+ return [];
21079
+ }
21080
+ const text = readContentText(u.content);
21081
+ return text ? [{ kind: "user", text }] : [];
21082
+ }
21083
+ case "prompt_received": {
21084
+ const text = readPromptText(u.prompt);
21085
+ return text ? [{ kind: "user", text }] : [];
21086
+ }
21087
+ case "tool_call":
21088
+ case "tool_call_update": {
21089
+ return extractToolFragments(u);
21090
+ }
21091
+ default:
21092
+ return [];
21093
+ }
21094
+ }
21095
+ function extractToolFragments(u) {
21096
+ const toolName = readString2(u, "name");
21097
+ const title = readString2(u, "title");
21098
+ const out = [];
21099
+ if (title !== void 0) {
21100
+ const sanitized = sanitizeSingleLine(title);
21101
+ if (sanitized.length > 0) {
21102
+ const frag = { kind: "tool", text: sanitized };
21103
+ if (toolName !== void 0) {
21104
+ frag.toolName = toolName;
21105
+ }
21106
+ out.push(frag);
21107
+ }
21108
+ }
21109
+ if (toolName !== void 0 && toolName !== title) {
21110
+ const sanitized = sanitizeSingleLine(toolName);
21111
+ if (sanitized.length > 0) {
21112
+ out.push({ kind: "tool", toolName, text: sanitized });
21113
+ }
21114
+ }
21115
+ const rawInput = u.rawInput;
21116
+ if (rawInput && typeof rawInput === "object") {
21117
+ const serialized = safeStringify(rawInput);
21118
+ if (serialized.length > 0) {
21119
+ const frag = {
21120
+ kind: "tool-input",
21121
+ text: sanitizeSingleLine(serialized)
21122
+ };
21123
+ if (toolName !== void 0) {
21124
+ frag.toolName = toolName;
21125
+ }
21126
+ out.push(frag);
21127
+ }
21128
+ }
21129
+ const locations = u.locations;
21130
+ if (Array.isArray(locations) && locations.length > 0) {
21131
+ const serialized = safeStringify(locations);
21132
+ if (serialized.length > 0) {
21133
+ const frag = {
21134
+ kind: "tool-input",
21135
+ text: sanitizeSingleLine(serialized)
21136
+ };
21137
+ if (toolName !== void 0) {
21138
+ frag.toolName = toolName;
21139
+ }
21140
+ out.push(frag);
21141
+ }
21142
+ }
21143
+ const errorText = extractToolErrorText(u);
21144
+ if (errorText !== null) {
21145
+ const frag = { kind: "tool", text: errorText };
21146
+ if (toolName !== void 0) {
21147
+ frag.toolName = toolName;
21148
+ }
21149
+ out.push(frag);
21150
+ }
21151
+ return out;
21152
+ }
21153
+ function extractToolErrorText(u) {
21154
+ const content = u.content;
21155
+ if (Array.isArray(content)) {
21156
+ for (const block of content) {
21157
+ if (!block || typeof block !== "object") {
21158
+ continue;
21159
+ }
21160
+ const b = block;
21161
+ const inner = b.content;
21162
+ if (!inner || typeof inner !== "object") {
21163
+ continue;
21164
+ }
21165
+ const i = inner;
21166
+ if (i.type === "text" && typeof i.text === "string") {
21167
+ const s = sanitizeSingleLine(i.text);
21168
+ if (s.length > 0) {
21169
+ return s;
21170
+ }
21171
+ }
21172
+ }
21173
+ }
21174
+ const rawOutput = u.rawOutput;
21175
+ if (rawOutput && typeof rawOutput === "object") {
21176
+ const err = rawOutput.error;
21177
+ if (typeof err === "string") {
21178
+ const s = sanitizeSingleLine(err);
21179
+ if (s.length > 0) {
21180
+ return s;
21181
+ }
21182
+ }
21183
+ }
21184
+ return null;
21185
+ }
21186
+ function isCompatPromptReceived(u) {
21187
+ const meta = u._meta;
21188
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
21189
+ return false;
21190
+ }
21191
+ const hydra = meta["hydra-acp"];
21192
+ if (!hydra || typeof hydra !== "object" || Array.isArray(hydra)) {
21193
+ return false;
21194
+ }
21195
+ return hydra.compatFor === "prompt_received";
21196
+ }
21197
+ function readContentText(content) {
21198
+ if (typeof content === "string") {
21199
+ return sanitizeWireText(content);
21200
+ }
21201
+ if (!content || typeof content !== "object" || Array.isArray(content)) {
21202
+ return "";
21203
+ }
21204
+ const c = content;
21205
+ if (typeof c.text === "string") {
21206
+ return sanitizeWireText(c.text);
21207
+ }
21208
+ return "";
21209
+ }
21210
+ function readPromptText(prompt) {
21211
+ if (!Array.isArray(prompt)) {
21212
+ return "";
21213
+ }
21214
+ const parts = [];
21215
+ for (const block of prompt) {
21216
+ const text = readContentText(block);
21217
+ if (text.length > 0) {
21218
+ parts.push(text);
21219
+ }
21220
+ }
21221
+ return parts.join("");
21222
+ }
21223
+ function readString2(u, key) {
21224
+ const v = u[key];
21225
+ return typeof v === "string" ? v : void 0;
19450
21226
  }
19451
- function statusGlyph(status) {
19452
- switch (status) {
19453
- case "completed":
19454
- return "\u2713";
19455
- case "failed":
19456
- return "\u2717";
19457
- case "cancelled":
19458
- case "rejected":
19459
- return "\u2298";
19460
- case "in_progress":
19461
- return "\u21BB";
19462
- default:
19463
- return "\xB7";
21227
+ function safeStringify(value) {
21228
+ try {
21229
+ return JSON.stringify(value);
21230
+ } catch {
21231
+ return "";
19464
21232
  }
19465
21233
  }
19466
- function escapeInline(text) {
19467
- return text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
19468
- }
19469
- function formatNumber(n) {
19470
- return n.toLocaleString("en-US");
21234
+ function buildSnippet(text, matchIdx, matchLen) {
21235
+ const flat = text.replace(/\s+/g, " ").trim();
21236
+ if (flat.length === 0) {
21237
+ return "";
21238
+ }
21239
+ const flatLower = flat.toLowerCase();
21240
+ const needleSlice = text.slice(matchIdx, matchIdx + matchLen).toLowerCase().replace(/\s+/g, " ").trim();
21241
+ let pos = needleSlice.length > 0 ? flatLower.indexOf(needleSlice) : 0;
21242
+ if (pos === -1) {
21243
+ pos = 0;
21244
+ }
21245
+ const start = Math.max(0, pos - SNIPPET_SIDE);
21246
+ const end = Math.min(flat.length, pos + needleSlice.length + SNIPPET_SIDE);
21247
+ const head = start > 0 ? "\u2026" : "";
21248
+ const tail = end < flat.length ? "\u2026" : "";
21249
+ return `${head}${flat.slice(start, end)}${tail}`;
19471
21250
  }
19472
21251
 
19473
21252
  // src/daemon/routes/sessions.ts
19474
- init_types();
19475
- init_hydra_version();
19476
- init_remote_url();
19477
21253
  function resolveHydraHost(defaults) {
19478
21254
  if (defaults.publicHost && defaults.publicHost.length > 0) {
19479
21255
  return defaults.publicHost;
@@ -19489,6 +21265,17 @@ function registerSessionRoutes(app, manager, defaults) {
19489
21265
  const sessions = await manager.list({ cwd: query?.cwd });
19490
21266
  return { sessions };
19491
21267
  });
21268
+ app.get("/v1/sessions/search", async (request, reply) => {
21269
+ const query = request.query;
21270
+ const q = query?.q ?? "";
21271
+ if (q.trim().length === 0) {
21272
+ reply.code(400).send({ error: "q is required" });
21273
+ return reply;
21274
+ }
21275
+ const ids = query?.sessionIds ? query.sessionIds.split(",").filter((s) => s.length > 0) : void 0;
21276
+ const out = await searchHistories(manager, q, { sessionIds: ids });
21277
+ return out;
21278
+ });
19492
21279
  app.post("/v1/sessions", async (request, reply) => {
19493
21280
  const body = request.body ?? {};
19494
21281
  const cwd = expandHome(body.cwd ?? defaults.cwd);
@@ -20229,6 +22016,7 @@ import { nanoid as nanoid2 } from "nanoid";
20229
22016
  import * as os5 from "os";
20230
22017
  import * as path13 from "path";
20231
22018
  init_hydra_version();
22019
+ import { randomBytes as randomBytes3 } from "crypto";
20232
22020
  function registerAcpWsEndpoint(app, deps) {
20233
22021
  app.get("/acp", { websocket: true }, async (socket, request) => {
20234
22022
  const token = tokenFromUpgradeRequest({
@@ -20266,6 +22054,12 @@ function registerAcpWsEndpoint(app, deps) {
20266
22054
  };
20267
22055
  connection.onRequest("initialize", async (raw) => {
20268
22056
  const params = InitializeParams.parse(raw ?? {});
22057
+ if (params.clientInfo?.name) {
22058
+ state.clientInfo = {
22059
+ name: params.clientInfo.name,
22060
+ ...params.clientInfo.version !== void 0 ? { version: params.clientInfo.version } : {}
22061
+ };
22062
+ }
20269
22063
  const version = params.clientInfo?.version;
20270
22064
  if (version && processIdentity) {
20271
22065
  if (processIdentity.kind === "extension") {
@@ -20276,6 +22070,34 @@ function registerAcpWsEndpoint(app, deps) {
20276
22070
  }
20277
22071
  return buildInitializeResult();
20278
22072
  });
22073
+ if (processIdentity && deps.extensionCommands) {
22074
+ const registry = deps.extensionCommands;
22075
+ connection.onRequest("hydra-acp/register_commands", async (raw) => {
22076
+ const params = raw ?? {};
22077
+ const commands = Array.isArray(params.commands) ? params.commands.map((c) => {
22078
+ if (!c || typeof c !== "object") {
22079
+ return void 0;
22080
+ }
22081
+ const obj = c;
22082
+ if (typeof obj.verb !== "string" || obj.verb.length === 0) {
22083
+ return void 0;
22084
+ }
22085
+ const spec = { verb: obj.verb };
22086
+ if (typeof obj.argsHint === "string") {
22087
+ spec.argsHint = obj.argsHint;
22088
+ }
22089
+ if (typeof obj.description === "string") {
22090
+ spec.description = obj.description;
22091
+ }
22092
+ return spec;
22093
+ }).filter((s) => s !== void 0) : [];
22094
+ registry.register(processIdentity.name, connection, commands);
22095
+ return { ok: true, registered: commands.length };
22096
+ });
22097
+ connection.onClose(() => {
22098
+ registry.clear(processIdentity.name);
22099
+ });
22100
+ }
20279
22101
  if (processIdentity?.kind === "transformer") {
20280
22102
  connection.onRequest("transformer/initialize", async (raw) => {
20281
22103
  const params = raw ?? {};
@@ -20417,16 +22239,50 @@ function registerAcpWsEndpoint(app, deps) {
20417
22239
  );
20418
22240
  const transformerNames = Array.isArray(hydraMeta.transformers) && hydraMeta.transformers.every((t) => typeof t === "string") ? hydraMeta.transformers : deps.manager.defaultTransformers ?? [];
20419
22241
  const transformChain = deps.transformers?.resolveChain(transformerNames) ?? [];
20420
- const session = await deps.manager.create({
20421
- cwd: params.cwd,
20422
- agentId: params.agentId ?? deps.defaultAgent,
20423
- mcpServers: params.mcpServers,
20424
- title: hydraMeta.name,
20425
- agentArgs: hydraMeta.agentArgs,
20426
- model: hydraMeta.model,
20427
- onInstallProgress: makeInstallProgressForwarder(connection),
20428
- transformChain
20429
- });
22242
+ let stdinToken;
22243
+ let stdinReservation;
22244
+ let augmentedMcpServers = params.mcpServers;
22245
+ if (hydraMeta.mcpStdin === true && deps.stdinMcpRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
22246
+ stdinToken = randomBytes3(32).toString("hex");
22247
+ stdinReservation = deps.stdinMcpRegistry.reserve(stdinToken);
22248
+ const url = `${deps.getDaemonOrigin()}/mcp/stdin`;
22249
+ const descriptor = {
22250
+ name: "hydra_stdin",
22251
+ type: "http",
22252
+ url,
22253
+ headers: [
22254
+ { name: "Authorization", value: `Bearer ${stdinToken}` }
22255
+ ]
22256
+ };
22257
+ augmentedMcpServers = [...params.mcpServers ?? [], descriptor];
22258
+ }
22259
+ let session;
22260
+ try {
22261
+ session = await deps.manager.create({
22262
+ cwd: params.cwd,
22263
+ agentId: params.agentId ?? deps.defaultAgent,
22264
+ mcpServers: augmentedMcpServers,
22265
+ title: hydraMeta.name,
22266
+ agentArgs: hydraMeta.agentArgs,
22267
+ model: hydraMeta.model,
22268
+ onInstallProgress: makeInstallProgressForwarder(connection),
22269
+ transformChain,
22270
+ originatingClient: state.clientInfo
22271
+ });
22272
+ } catch (err) {
22273
+ if (stdinReservation !== void 0) {
22274
+ stdinReservation.abandon(err instanceof Error ? err : void 0);
22275
+ }
22276
+ throw err;
22277
+ }
22278
+ if (stdinToken !== void 0 && stdinReservation !== void 0 && deps.stdinMcpRegistry !== void 0) {
22279
+ const token2 = stdinToken;
22280
+ const registry = deps.stdinMcpRegistry;
22281
+ stdinReservation.complete(session);
22282
+ session.onClose(() => {
22283
+ void registry.unbind(token2);
22284
+ });
22285
+ }
20430
22286
  const client = bindClientToSession(connection, session, state);
20431
22287
  const { entries: replay } = await session.attach(client, "full");
20432
22288
  state.attached.set(session.sessionId, {
@@ -20823,7 +22679,10 @@ function registerAcpWsEndpoint(app, deps) {
20823
22679
  return null;
20824
22680
  }
20825
22681
  app.log.info(decision.logMessage);
20826
- return decision.session.forwardRequest("session/set_model", rawParams);
22682
+ const { modelId } = rawParams;
22683
+ const result = await decision.session.forwardRequest("session/set_model", rawParams);
22684
+ decision.session.applyModelChange(modelId);
22685
+ return result;
20827
22686
  });
20828
22687
  connection.onRequest("session/set_mode", async (rawParams) => {
20829
22688
  const params = rawParams;
@@ -21144,6 +23003,336 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
21144
23003
  };
21145
23004
  }
21146
23005
 
23006
+ // src/daemon/mcp/stdin-registry.ts
23007
+ var StdinMcpRegistry = class {
23008
+ byToken = /* @__PURE__ */ new Map();
23009
+ // Reserve a token slot before the session exists. Used by acp-ws when
23010
+ // we need to inject the bearer into the agent's mcpServers BEFORE
23011
+ // manager.create() returns — claude-acp connects to /mcp/stdin during
23012
+ // session/new initialization (eagerly), so the route handler must be
23013
+ // able to find the token by the time the agent's first request lands.
23014
+ reserve(token) {
23015
+ if (this.byToken.has(token)) {
23016
+ throw new Error(`stdin MCP token already bound`);
23017
+ }
23018
+ let resolveSession2;
23019
+ let rejectSession;
23020
+ const sessionReady = new Promise((resolve6, reject) => {
23021
+ resolveSession2 = resolve6;
23022
+ rejectSession = reject;
23023
+ });
23024
+ sessionReady.catch(() => void 0);
23025
+ const entry = { session: void 0, sessionReady };
23026
+ this.byToken.set(token, entry);
23027
+ return {
23028
+ complete: (session) => {
23029
+ entry.session = session;
23030
+ resolveSession2(session);
23031
+ },
23032
+ abandon: (reason) => {
23033
+ this.byToken.delete(token);
23034
+ rejectSession(reason ?? new Error("stdin MCP reservation abandoned"));
23035
+ }
23036
+ };
23037
+ }
23038
+ // Convenience for callers that already have the session in hand (and
23039
+ // for tests). Equivalent to reserve() + complete() back-to-back.
23040
+ bind(token, session) {
23041
+ const { complete } = this.reserve(token);
23042
+ complete(session);
23043
+ }
23044
+ lookup(token) {
23045
+ return this.byToken.get(token);
23046
+ }
23047
+ attachTransport(token, server, transport) {
23048
+ const ep = this.byToken.get(token);
23049
+ if (!ep) {
23050
+ return;
23051
+ }
23052
+ ep.server = server;
23053
+ ep.transport = transport;
23054
+ }
23055
+ async unbind(token) {
23056
+ const ep = this.byToken.get(token);
23057
+ if (!ep) {
23058
+ return;
23059
+ }
23060
+ this.byToken.delete(token);
23061
+ if (ep.transport) {
23062
+ try {
23063
+ await ep.transport.close();
23064
+ } catch {
23065
+ }
23066
+ }
23067
+ if (ep.server) {
23068
+ try {
23069
+ await ep.server.close();
23070
+ } catch {
23071
+ }
23072
+ }
23073
+ }
23074
+ size() {
23075
+ return this.byToken.size;
23076
+ }
23077
+ };
23078
+
23079
+ // src/daemon/mcp/stdin-server.ts
23080
+ import { randomUUID } from "crypto";
23081
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23082
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23083
+ import { z as z7 } from "zod";
23084
+ var BEARER_PREFIX2 = "Bearer ";
23085
+ function extractBearer(req) {
23086
+ const header = req.headers.authorization;
23087
+ if (typeof header !== "string") {
23088
+ return void 0;
23089
+ }
23090
+ if (!header.startsWith(BEARER_PREFIX2)) {
23091
+ return void 0;
23092
+ }
23093
+ const token = header.slice(BEARER_PREFIX2.length).trim();
23094
+ return token.length > 0 ? token : void 0;
23095
+ }
23096
+ function buildMcpServer(session) {
23097
+ const server = new McpServer(
23098
+ { name: "hydra-stdin", version: "1.0.0" },
23099
+ {
23100
+ instructions: "Piped input from `hydra cat --stream` is exposed here as a byte stream. Use `tail_stdin` for the latest N bytes (good for finding the end of a log), `head_stdin` for the first N bytes (good for headers/preamble), `read_stdin` for windowed reads against an absolute byte cursor, `wait_for_more` to block until new bytes arrive past a cursor, and `stdin_info` for the current cursors/capacity/closed status. Byte payloads come back base64-encoded."
23101
+ }
23102
+ );
23103
+ server.registerTool(
23104
+ "tail_stdin",
23105
+ {
23106
+ description: "Return the most recent `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means older bytes existed but have been evicted from the ring.",
23107
+ inputSchema: {
23108
+ bytes: z7.number().int().min(1).describe("How many trailing bytes to return.")
23109
+ }
23110
+ },
23111
+ async ({ bytes }) => {
23112
+ const r = session.streamTail(bytes);
23113
+ return {
23114
+ content: [
23115
+ {
23116
+ type: "text",
23117
+ text: JSON.stringify(r)
23118
+ }
23119
+ ],
23120
+ structuredContent: r
23121
+ };
23122
+ }
23123
+ );
23124
+ server.registerTool(
23125
+ "head_stdin",
23126
+ {
23127
+ description: "Return the first `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means the head has already been evicted from the ring and the returned bytes start at the oldest still-resident cursor.",
23128
+ inputSchema: {
23129
+ bytes: z7.number().int().min(1).describe("How many leading bytes to return.")
23130
+ }
23131
+ },
23132
+ async ({ bytes }) => {
23133
+ const r = session.streamHead(bytes);
23134
+ return {
23135
+ content: [
23136
+ {
23137
+ type: "text",
23138
+ text: JSON.stringify(r)
23139
+ }
23140
+ ],
23141
+ structuredContent: r
23142
+ };
23143
+ }
23144
+ );
23145
+ server.registerTool(
23146
+ "read_stdin",
23147
+ {
23148
+ description: "Read up to `max_bytes` bytes starting at absolute byte `cursor`. Returns `{bytes, nextCursor, gap?, eof?}` \u2014 `gap` is the number of bytes silently skipped because the ring had evicted them; `eof:true` means the producer closed and there is nothing left to read.",
23149
+ inputSchema: {
23150
+ cursor: z7.number().int().min(0).describe(
23151
+ "Absolute byte offset to start reading from. Use 0 to read from the very beginning (may produce a gap if old bytes have been evicted)."
23152
+ ),
23153
+ max_bytes: z7.number().int().min(1).optional().describe(
23154
+ "Optional cap on how many bytes to return. Server caps at 64 KiB regardless."
23155
+ ),
23156
+ wait_ms: z7.number().int().min(0).optional().describe(
23157
+ "If no bytes are available, block up to this many ms for more (capped server-side at 60_000)."
23158
+ )
23159
+ }
23160
+ },
23161
+ async ({ cursor, max_bytes, wait_ms }) => {
23162
+ const r = await session.streamRead(cursor, max_bytes, wait_ms);
23163
+ return {
23164
+ content: [
23165
+ {
23166
+ type: "text",
23167
+ text: JSON.stringify(r)
23168
+ }
23169
+ ],
23170
+ structuredContent: r
23171
+ };
23172
+ }
23173
+ );
23174
+ server.registerTool(
23175
+ "wait_for_more",
23176
+ {
23177
+ description: "Block until bytes are available past `cursor`, the stream closes, or `timeout_ms` elapses. Returns one of {data, eof, timeout} plus the current `writeCursor`. Use this when you've consumed everything up to a cursor and want to wait for more without busy-polling.",
23178
+ inputSchema: {
23179
+ cursor: z7.number().int().min(0).describe("The cursor you've already consumed up to."),
23180
+ timeout_ms: z7.number().int().min(0).describe("Maximum ms to block (server caps at 60_000).")
23181
+ }
23182
+ },
23183
+ async ({ cursor, timeout_ms }) => {
23184
+ const outcome = await session.streamWaitFor(cursor, timeout_ms);
23185
+ const info = session.streamInfo();
23186
+ const payload = { outcome, writeCursor: info.writeCursor, closed: info.closed };
23187
+ return {
23188
+ content: [
23189
+ {
23190
+ type: "text",
23191
+ text: JSON.stringify(payload)
23192
+ }
23193
+ ],
23194
+ structuredContent: payload
23195
+ };
23196
+ }
23197
+ );
23198
+ server.registerTool(
23199
+ "grep_stdin",
23200
+ {
23201
+ description: "Scan piped stdin line-by-line and return lines matching `pattern`. Prefer this over `read_stdin` when the question is 'find lines that mention X' \u2014 it filters server-side so you don't pull and decode 64 KiB base64 windows. Returns `{matches: [{cursor, line, before?, after?}], truncated, nextCursor, gap?, scannedBytes, eof?}`. Lines come back as decoded UTF-8 strings (not base64). When `truncated:true`, re-call with `cursor: nextCursor` to resume.",
23202
+ inputSchema: {
23203
+ pattern: z7.string().min(1).describe(
23204
+ "Search pattern. Treated as a JavaScript regular expression by default (set `regex:false` for a literal substring match)."
23205
+ ),
23206
+ regex: z7.boolean().optional().describe("Default true. Pass false to treat `pattern` as a literal substring."),
23207
+ case_insensitive: z7.boolean().optional().describe("Default false. Pass true for case-insensitive matching."),
23208
+ invert: z7.boolean().optional().describe("Default false. Pass true to return lines that do NOT match the pattern."),
23209
+ max_matches: z7.number().int().min(1).optional().describe("Default 100. Capped server-side at 1000."),
23210
+ max_bytes: z7.number().int().min(1).optional().describe("Default 64 KiB output. Capped server-side at 256 KiB."),
23211
+ context_before: z7.number().int().min(0).optional().describe("Default 0. Number of lines before each match to include (capped at 20)."),
23212
+ context_after: z7.number().int().min(0).optional().describe("Default 0. Number of lines after each match to include (capped at 20)."),
23213
+ cursor: z7.number().int().min(0).optional().describe(
23214
+ "Optional absolute byte offset to start scanning from. Omit to scan from the oldest still-resident byte. Pass the `nextCursor` from a previous truncated call to resume."
23215
+ )
23216
+ }
23217
+ },
23218
+ async (args) => {
23219
+ const opts = { pattern: args.pattern };
23220
+ if (args.regex !== void 0) {
23221
+ opts.regex = args.regex;
23222
+ }
23223
+ if (args.case_insensitive !== void 0) {
23224
+ opts.caseInsensitive = args.case_insensitive;
23225
+ }
23226
+ if (args.invert !== void 0) {
23227
+ opts.invert = args.invert;
23228
+ }
23229
+ if (args.max_matches !== void 0) {
23230
+ opts.maxMatches = args.max_matches;
23231
+ }
23232
+ if (args.max_bytes !== void 0) {
23233
+ opts.maxBytes = args.max_bytes;
23234
+ }
23235
+ if (args.context_before !== void 0) {
23236
+ opts.contextBefore = args.context_before;
23237
+ }
23238
+ if (args.context_after !== void 0) {
23239
+ opts.contextAfter = args.context_after;
23240
+ }
23241
+ if (args.cursor !== void 0) {
23242
+ opts.cursor = args.cursor;
23243
+ }
23244
+ const r = session.streamGrep(opts);
23245
+ const payload = r;
23246
+ return {
23247
+ content: [{ type: "text", text: JSON.stringify(r) }],
23248
+ structuredContent: payload
23249
+ };
23250
+ }
23251
+ );
23252
+ server.registerTool(
23253
+ "stdin_info",
23254
+ {
23255
+ description: "Report cursor / capacity / closed state of the stdin ring. Cheap; safe to call repeatedly.",
23256
+ inputSchema: {}
23257
+ },
23258
+ async () => {
23259
+ const r = session.streamInfo();
23260
+ return {
23261
+ content: [
23262
+ {
23263
+ type: "text",
23264
+ text: JSON.stringify(r)
23265
+ }
23266
+ ],
23267
+ structuredContent: r
23268
+ };
23269
+ }
23270
+ );
23271
+ return server;
23272
+ }
23273
+ async function ensureTransport(token, session, registry) {
23274
+ const existing = registry.lookup(token);
23275
+ if (existing?.transport !== void 0) {
23276
+ return existing.transport;
23277
+ }
23278
+ const server = buildMcpServer(session);
23279
+ const transport = new StreamableHTTPServerTransport({
23280
+ sessionIdGenerator: () => randomUUID()
23281
+ });
23282
+ await server.connect(transport);
23283
+ registry.attachTransport(token, server, transport);
23284
+ return transport;
23285
+ }
23286
+ var SESSION_READY_TIMEOUT_MS = 1e4;
23287
+ async function handle(req, reply, registry) {
23288
+ const token = extractBearer(req);
23289
+ if (token === void 0) {
23290
+ reply.code(401).send({ error: "missing bearer token" });
23291
+ return;
23292
+ }
23293
+ const ep = registry.lookup(token);
23294
+ if (ep === void 0) {
23295
+ reply.code(404).send({ error: "unknown stdin token" });
23296
+ return;
23297
+ }
23298
+ let session;
23299
+ if (ep.session !== void 0) {
23300
+ session = ep.session;
23301
+ } else {
23302
+ let timer;
23303
+ const timeout = new Promise((resolve6) => {
23304
+ timer = setTimeout(() => resolve6(void 0), SESSION_READY_TIMEOUT_MS);
23305
+ });
23306
+ const resolved = await Promise.race([
23307
+ ep.sessionReady.catch(() => void 0),
23308
+ timeout
23309
+ ]);
23310
+ if (timer !== void 0) {
23311
+ clearTimeout(timer);
23312
+ }
23313
+ if (resolved === void 0) {
23314
+ reply.code(503).send({ error: "session not ready" });
23315
+ return;
23316
+ }
23317
+ session = resolved;
23318
+ }
23319
+ const transport = await ensureTransport(token, session, registry);
23320
+ reply.hijack();
23321
+ await transport.handleRequest(req.raw, reply.raw, req.body);
23322
+ }
23323
+ function registerStdinMcpRoutes(app, registry) {
23324
+ const opts = { config: { skipAuth: true } };
23325
+ app.post("/mcp/stdin", opts, async (req, reply) => {
23326
+ await handle(req, reply, registry);
23327
+ });
23328
+ app.get("/mcp/stdin", opts, async (req, reply) => {
23329
+ await handle(req, reply, registry);
23330
+ });
23331
+ app.delete("/mcp/stdin", opts, async (req, reply) => {
23332
+ await handle(req, reply, registry);
23333
+ });
23334
+ }
23335
+
21147
23336
  // src/daemon/server.ts
21148
23337
  async function startDaemon(config, serviceToken) {
21149
23338
  ensureLoopbackOrTls(config);
@@ -21214,13 +23403,15 @@ async function startDaemon(config, serviceToken) {
21214
23403
  stderrTailBytes: config.daemon.agentStderrTailBytes,
21215
23404
  logger: agentLogger
21216
23405
  });
23406
+ const extensionCommands = new ExtensionCommandRegistry();
21217
23407
  const manager = new SessionManager(registry, spawner, void 0, {
21218
23408
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
21219
23409
  defaultModels: config.defaultModels,
21220
23410
  defaultTransformers: config.defaultTransformers,
21221
23411
  sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
21222
23412
  logger: agentLogger,
21223
- npmRegistry: config.npmRegistry
23413
+ npmRegistry: config.npmRegistry,
23414
+ extensionCommands
21224
23415
  });
21225
23416
  const extensions = new ExtensionManager(extensionList(config), void 0, {
21226
23417
  tokenRegistry: processRegistry
@@ -21247,6 +23438,19 @@ async function startDaemon(config, serviceToken) {
21247
23438
  store: sessionTokenStore,
21248
23439
  rateLimiter: authRateLimiter
21249
23440
  });
23441
+ const stdinMcpRegistry = new StdinMcpRegistry();
23442
+ registerStdinMcpRoutes(app, stdinMcpRegistry);
23443
+ let daemonOriginCached;
23444
+ const getDaemonOrigin = () => {
23445
+ if (daemonOriginCached !== void 0) {
23446
+ return daemonOriginCached;
23447
+ }
23448
+ const addr = app.server.address();
23449
+ const port = addr && typeof addr === "object" ? addr.port : config.daemon.port;
23450
+ const scheme2 = config.daemon.tls ? "https" : "http";
23451
+ daemonOriginCached = `${scheme2}://${config.daemon.host}:${port}`;
23452
+ return daemonOriginCached;
23453
+ };
21250
23454
  registerAcpWsEndpoint(app, {
21251
23455
  validator,
21252
23456
  manager,
@@ -21254,7 +23458,10 @@ async function startDaemon(config, serviceToken) {
21254
23458
  processRegistry,
21255
23459
  onExtensionVersion: (name, version) => extensions.reportVersion(name, version),
21256
23460
  onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
21257
- transformers
23461
+ transformers,
23462
+ extensionCommands,
23463
+ stdinMcpRegistry,
23464
+ getDaemonOrigin
21258
23465
  });
21259
23466
  await app.listen({ host: config.daemon.host, port: config.daemon.port });
21260
23467
  const address = app.server.address();
@@ -21510,14 +23717,14 @@ async function runDaemonStart(flags = {}) {
21510
23717
  }
21511
23718
  if (flagBool(flags, "foreground")) {
21512
23719
  process.title = "hydra-daemon";
21513
- const handle = await startDaemon(config, serviceToken);
23720
+ const handle2 = await startDaemon(config, serviceToken);
21514
23721
  process.stdout.write(
21515
23722
  `hydra-acp daemon listening on ${config.daemon.host}:${config.daemon.port}
21516
23723
  `
21517
23724
  );
21518
23725
  const shutdown = async () => {
21519
23726
  process.stdout.write("Shutting down...\n");
21520
- await handle.shutdown();
23727
+ await handle2.shutdown();
21521
23728
  process.exit(0);
21522
23729
  };
21523
23730
  process.on("SIGINT", () => void shutdown());
@@ -21687,6 +23894,7 @@ init_remote_target();
21687
23894
  init_remote_url();
21688
23895
  init_session();
21689
23896
  init_discovery();
23897
+ init_hydra_version();
21690
23898
  import * as fs19 from "fs/promises";
21691
23899
  import * as path14 from "path";
21692
23900
  init_session_row();
@@ -21704,10 +23912,13 @@ async function runSessionsList(opts = {}) {
21704
23912
  process.exit(1);
21705
23913
  }
21706
23914
  const body = await response.json();
23915
+ const sessionsAfterCatFilter = opts.includeCat ? body.sessions : body.sessions.filter(
23916
+ (s) => s.originatingClient?.name !== HYDRA_CAT_CLIENT_NAME
23917
+ );
21707
23918
  const host = opts.host ?? "local";
21708
- const hostFiltered = host === "all" ? body.sessions : host === "local" ? body.sessions.filter(
23919
+ const hostFiltered = host === "all" ? sessionsAfterCatFilter : host === "local" ? sessionsAfterCatFilter.filter(
21709
23920
  (s) => !s.importedFromMachine || !!s.upstreamSessionId
21710
- ) : body.sessions.filter(
23921
+ ) : sessionsAfterCatFilter.filter(
21711
23922
  (s) => s.importedFromMachine === host && !s.upstreamSessionId
21712
23923
  );
21713
23924
  if (opts.json) {
@@ -23558,6 +25769,9 @@ function isResponse2(msg) {
23558
25769
  return !("method" in msg) && "id" in msg;
23559
25770
  }
23560
25771
 
25772
+ // src/shim/proxy.ts
25773
+ init_permission_pick();
25774
+
23561
25775
  // src/core/process-title.ts
23562
25776
  init_bin_name();
23563
25777
  import { writeFileSync as writeFileSync3 } from "fs";
@@ -23642,6 +25856,14 @@ function wireShim({
23642
25856
  }) {
23643
25857
  upstream.onMessage((msg) => {
23644
25858
  tracker.observeFromServer(msg);
25859
+ if (opts.dangerouslySkipPermissions === true && isPermissionRequest(msg)) {
25860
+ void upstream.send({
25861
+ jsonrpc: "2.0",
25862
+ id: msg.id,
25863
+ result: buildApproveResponse(msg.params)
25864
+ });
25865
+ return;
25866
+ }
23645
25867
  maybeReplyToResolvedPermission(msg, tracker, downstream);
23646
25868
  void downstream.send(msg);
23647
25869
  });
@@ -23791,6 +26013,9 @@ async function replayAttach(stream, ctx, afterMessageId) {
23791
26013
  function isSessionNewRequest(msg) {
23792
26014
  return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "session/new";
23793
26015
  }
26016
+ function isPermissionRequest(msg) {
26017
+ return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "session/request_permission";
26018
+ }
23794
26019
  function buildAttachFromNew(msg, sessionId) {
23795
26020
  return {
23796
26021
  jsonrpc: "2.0",
@@ -23837,6 +26062,10 @@ init_daemon_bootstrap();
23837
26062
  init_render_update();
23838
26063
  init_types();
23839
26064
  init_hydra_version();
26065
+ init_permission_pick();
26066
+ import { mkdtempSync, rmSync } from "fs";
26067
+ import { tmpdir as tmpdir2 } from "os";
26068
+ import { join as join11 } from "path";
23840
26069
  import { WebSocket as WebSocket2 } from "ws";
23841
26070
 
23842
26071
  // src/cli/commands/cat-chunker.ts
@@ -23891,8 +26120,26 @@ function createChunker(opts) {
23891
26120
  }
23892
26121
 
23893
26122
  // src/cli/commands/cat.ts
23894
- var DEFAULT_STREAM_THRESHOLD = 32 * 1024;
23895
- var DEFAULT_STREAM_FILE_CAP = 64 * 1024 * 1024;
26123
+ var DEFAULT_STREAM_THRESHOLD = 1 * 1024 * 1024;
26124
+ var HYDRA_STDIN_TOOL_PREFIX = "mcp__hydra_stdin__";
26125
+ function isHydraStdinPermissionRequest(params) {
26126
+ if (!params || typeof params !== "object") {
26127
+ return false;
26128
+ }
26129
+ const toolCall = params.toolCall;
26130
+ if (!toolCall || typeof toolCall !== "object") {
26131
+ return false;
26132
+ }
26133
+ const title = toolCall.title;
26134
+ if (typeof title === "string" && title.startsWith(HYDRA_STDIN_TOOL_PREFIX)) {
26135
+ return true;
26136
+ }
26137
+ const toolName = toolCall.toolName;
26138
+ if (typeof toolName === "string" && toolName.startsWith(HYDRA_STDIN_TOOL_PREFIX)) {
26139
+ return true;
26140
+ }
26141
+ return false;
26142
+ }
23896
26143
  async function runCat(opts) {
23897
26144
  setHydraProcessTitle(buildTitleFromArgv(process.argv.slice(2)));
23898
26145
  if (process.stdin.isTTY && !opts.prompt && !opts.sessionId) {
@@ -23902,6 +26149,18 @@ async function runCat(opts) {
23902
26149
  process.exit(2);
23903
26150
  return;
23904
26151
  }
26152
+ if (!opts.sessionId && opts.cwd === void 0 && process.stdin.isTTY !== true) {
26153
+ const sandbox = mkdtempSync(join11(tmpdir2(), "hydra-cat-"));
26154
+ opts.cwd = sandbox;
26155
+ if (!opts.detach) {
26156
+ process.on("exit", () => {
26157
+ try {
26158
+ rmSync(sandbox, { recursive: true, force: true });
26159
+ } catch {
26160
+ }
26161
+ });
26162
+ }
26163
+ }
23905
26164
  const config = await loadConfig();
23906
26165
  const target = opts.target ?? await resolveLocalTarget(config);
23907
26166
  if (target.isLocal && !opts.target) {
@@ -23925,9 +26184,19 @@ async function runCat(opts) {
23925
26184
  }
23926
26185
  async function runCatLoop(args) {
23927
26186
  const { conn, opts, stdin, stdinIsTty, stdout, stderr } = args;
26187
+ const useAutoStream = !stdinIsTty && opts.sessionId === void 0 && opts.follow !== true;
23928
26188
  conn.setDefaultHandler(async () => {
23929
26189
  return { error: { code: -32601, message: "method not implemented" } };
23930
26190
  });
26191
+ conn.onRequest("session/request_permission", async (params) => {
26192
+ if (opts.dangerouslySkipPermissions) {
26193
+ return buildApproveResponse(params);
26194
+ }
26195
+ if (!isHydraStdinPermissionRequest(params)) {
26196
+ return buildRejectResponse(params);
26197
+ }
26198
+ return buildApproveResponse(params);
26199
+ });
23931
26200
  try {
23932
26201
  await conn.request("initialize", {
23933
26202
  protocolVersion: ACP_PROTOCOL_VERSION,
@@ -23935,11 +26204,11 @@ async function runCatLoop(args) {
23935
26204
  fs: { readTextFile: false, writeTextFile: false },
23936
26205
  terminal: false
23937
26206
  },
23938
- clientInfo: { name: "hydra-acp-cat", version: HYDRA_VERSION }
26207
+ clientInfo: { name: HYDRA_CAT_CLIENT_NAME, version: HYDRA_VERSION }
23939
26208
  });
23940
26209
  } catch {
23941
26210
  }
23942
- const sessionId = await openOrAttachSession(conn, opts);
26211
+ const sessionId = await openOrAttachSession(conn, opts, useAutoStream);
23943
26212
  let turnHadOutput = false;
23944
26213
  let lastCharWasNewline = true;
23945
26214
  const writeStdout = (text) => {
@@ -23968,9 +26237,10 @@ async function runCatLoop(args) {
23968
26237
  finalizeTurn();
23969
26238
  }
23970
26239
  });
26240
+ let firstChunkSent = false;
23971
26241
  const sendChunk = async (text) => {
23972
26242
  const promptBlocks = [];
23973
- if (opts.prompt) {
26243
+ if (opts.prompt && !firstChunkSent) {
23974
26244
  promptBlocks.push({ type: "text", text: opts.prompt });
23975
26245
  }
23976
26246
  if (text.length > 0) {
@@ -23984,6 +26254,7 @@ async function runCatLoop(args) {
23984
26254
  sessionId,
23985
26255
  prompt: promptBlocks
23986
26256
  });
26257
+ firstChunkSent = true;
23987
26258
  } catch (err) {
23988
26259
  stderr(`hydra-acp cat: prompt failed: ${err.message}
23989
26260
  `);
@@ -24042,22 +26313,6 @@ async function runCatLoop(args) {
24042
26313
  }
24043
26314
  }
24044
26315
  };
24045
- const chunker = createChunker({
24046
- // setImmediate fires in the libuv "check" phase, after pending
24047
- // I/O has been polled and any back-to-back "data" events have
24048
- // been emitted. That makes it the natural hook for "the writer
24049
- // has paused, time to flush": if more bytes were sitting in the
24050
- // pipe buffer, Node would have emitted another "data" event
24051
- // before this fires, and the chunker would detect that and defer.
24052
- scheduleFlushCheck: (cb) => {
24053
- const h = setImmediate(cb);
24054
- return () => clearImmediate(h);
24055
- },
24056
- onChunk: (text) => {
24057
- chunkQueue.push(text);
24058
- void drainQueue();
24059
- }
24060
- });
24061
26316
  if (stdinIsTty && !opts.sessionId) {
24062
26317
  if (opts.prompt) {
24063
26318
  await sendChunk("");
@@ -24065,7 +26320,7 @@ async function runCatLoop(args) {
24065
26320
  await settle(0);
24066
26321
  return done;
24067
26322
  }
24068
- if (opts.stream && !stdinIsTty) {
26323
+ if (useAutoStream) {
24069
26324
  if (typeof stdin.setEncoding === "function") {
24070
26325
  stdin.setEncoding("utf8");
24071
26326
  }
@@ -24106,15 +26361,49 @@ async function runCatLoop(args) {
24106
26361
  if (typeof stdin.setEncoding === "function") {
24107
26362
  stdin.setEncoding("utf8");
24108
26363
  }
26364
+ const useFollow = opts.follow === true || stdinIsTty && Boolean(opts.sessionId);
26365
+ if (useFollow) {
26366
+ const chunker = createChunker({
26367
+ scheduleFlushCheck: (cb) => {
26368
+ const h = setImmediate(cb);
26369
+ return () => clearImmediate(h);
26370
+ },
26371
+ onChunk: (text) => {
26372
+ chunkQueue.push(text);
26373
+ void drainQueue();
26374
+ }
26375
+ });
26376
+ stdin.on("data", (data) => {
26377
+ chunker.feed(typeof data === "string" ? data : data.toString("utf8"));
26378
+ });
26379
+ stdin.on("end", () => {
26380
+ chunker.eof();
26381
+ stdinEnded = true;
26382
+ if (!draining && chunkQueue.length === 0) {
26383
+ void settle(exitCode);
26384
+ }
26385
+ });
26386
+ stdin.on("error", (err) => {
26387
+ stderr(`hydra-acp cat: stdin error: ${err.message}
26388
+ `);
26389
+ exitCode = 1;
26390
+ stdinEnded = true;
26391
+ if (!draining && chunkQueue.length === 0) {
26392
+ void settle(exitCode);
26393
+ }
26394
+ });
26395
+ return done;
26396
+ }
26397
+ let oneShotBuffer = "";
24109
26398
  stdin.on("data", (data) => {
24110
- chunker.feed(typeof data === "string" ? data : data.toString("utf8"));
26399
+ oneShotBuffer += typeof data === "string" ? data : data.toString("utf8");
24111
26400
  });
24112
26401
  stdin.on("end", () => {
24113
- chunker.eof();
24114
26402
  stdinEnded = true;
24115
- if (!draining && chunkQueue.length === 0) {
24116
- void settle(exitCode);
26403
+ if (oneShotBuffer.length > 0) {
26404
+ chunkQueue.push(oneShotBuffer);
24117
26405
  }
26406
+ void drainQueue();
24118
26407
  });
24119
26408
  stdin.on("error", (err) => {
24120
26409
  stderr(`hydra-acp cat: stdin error: ${err.message}
@@ -24167,12 +26456,11 @@ function runStreamingPath(args) {
24167
26456
  try {
24168
26457
  const openParams = {
24169
26458
  sessionId,
24170
- mode: "file"
26459
+ mode: "memory"
24171
26460
  };
24172
26461
  if (opts.streamBufferBytes !== void 0) {
24173
26462
  openParams.capacityBytes = opts.streamBufferBytes;
24174
26463
  }
24175
- openParams.fileCapBytes = opts.streamFileCapBytes ?? DEFAULT_STREAM_FILE_CAP;
24176
26464
  open2 = await conn.request("hydra-acp/stream_open", openParams);
24177
26465
  } catch (err) {
24178
26466
  args.onPromptFailed(
@@ -24180,23 +26468,12 @@ function runStreamingPath(args) {
24180
26468
  );
24181
26469
  return;
24182
26470
  }
24183
- const filePath = open2.filePath;
24184
- if (filePath === void 0) {
24185
- args.onPromptFailed(
24186
- new Error("daemon did not return a filePath for stream mode")
24187
- );
24188
- return;
24189
- }
24190
26471
  if (headBuffer.length > 0) {
24191
26472
  writeToStream(headBuffer, false);
24192
26473
  headBuffer = Buffer.alloc(0);
24193
26474
  }
24194
26475
  await writeChain.catch(() => void 0);
24195
- const promptText = buildStreamPromptText(
24196
- opts.prompt,
24197
- filePath,
24198
- opts.streamFileCapBytes ?? DEFAULT_STREAM_FILE_CAP
24199
- );
26476
+ const promptText = buildStreamPromptText(opts.prompt, open2.capacityBytes);
24200
26477
  const promptDone = conn.request("session/prompt", {
24201
26478
  sessionId,
24202
26479
  prompt: [{ type: "text", text: promptText }]
@@ -24239,22 +26516,35 @@ function runStreamingPath(args) {
24239
26516
  });
24240
26517
  stdin.on("error", args.onError);
24241
26518
  }
24242
- function buildStreamPromptText(standing, filePath, fileCapBytes) {
24243
- const capHuman = fileCapBytes >= 1024 * 1024 ? `${(fileCapBytes / (1024 * 1024)).toFixed(0)} MB` : `${(fileCapBytes / 1024).toFixed(0)} KB`;
24244
- const note = `Stdin is being streamed to ${filePath}. The file is being appended to live; use whatever shell tools fit (\`tail -f\`, \`head\`, \`grep\`, \`wc -l\`, etc.). Soft cap ${capHuman} \u2014 if more is written past that, older bytes are dropped.`;
26519
+ function buildStreamPromptText(standing, ringCapacityBytes) {
26520
+ const capHuman = ringCapacityBytes >= 1024 * 1024 ? `${(ringCapacityBytes / (1024 * 1024)).toFixed(0)} MB` : `${(ringCapacityBytes / 1024).toFixed(0)} KB`;
26521
+ const toolNote = `The user has piped data into this session. The bytes are NOT in your prompt; they live in the \`hydra_stdin\` MCP server and you read them via its tools:
26522
+ - \`stdin_info()\` \u2014 current writeCursor / oldestAvailable / capacity / closed. Cheap; call first to see how much data is there.
26523
+ - \`grep_stdin({pattern, regex?, case_insensitive?, context_before?, context_after?, cursor?})\` \u2014 server-side line filter; returns matching lines as decoded strings (not base64). Prefer this for "find lines that mention X" questions on multi-MB inputs.
26524
+ - \`head_stdin({bytes})\` \u2014 first N bytes (good for headers / preamble / file signatures).
26525
+ - \`tail_stdin({bytes})\` \u2014 most recent N bytes (good for log endings / recent errors).
26526
+ - \`read_stdin({cursor, max_bytes, wait_ms})\` \u2014 windowed read at an absolute byte cursor; iterate to sweep the whole stream.
26527
+ - \`wait_for_more({cursor, timeout_ms})\` \u2014 block for new bytes past a cursor (only useful for live tails).
26528
+
26529
+ Byte payloads (head/tail/read) come back base64-encoded \u2014 decode before reading them as text. \`grep_stdin\` returns plain strings. The ring holds the most recent ~${capHuman}; older bytes are evicted, and the byte tools report the gap when that happens. Per-call cap is 64 KiB for byte tools; loop \`read_stdin\` (advancing the cursor by \`nextCursor\`) when you need more.`;
24245
26530
  if (standing && standing.length > 0) {
24246
- return `${standing}
26531
+ return `${toolNote}
24247
26532
 
24248
- ${note}`;
26533
+ Use those tools NOW to answer the user's question \u2014 do not ask whether to check stdin; just check it. Pick the right tool for the question (grep_stdin for finding specific lines, head for preamble / file type, tail for recent events, read_stdin + cursor sweep for whole-stream scans), then answer.
26534
+
26535
+ User's question:
26536
+ ${standing}`;
24249
26537
  }
24250
- return note;
26538
+ return `${toolNote}
26539
+
26540
+ Use those tools to inspect the piped input and report what's there. Start with \`stdin_info()\` to see the size, then \`head_stdin\` and/or \`tail_stdin\` to look at the bytes.`;
24251
26541
  }
24252
- async function openOrAttachSession(conn, opts) {
26542
+ async function openOrAttachSession(conn, opts, useAutoStream) {
24253
26543
  if (opts.sessionId) {
24254
26544
  const attached = await conn.request("session/attach", {
24255
26545
  sessionId: opts.sessionId,
24256
26546
  historyPolicy: "pending_only",
24257
- clientInfo: { name: "hydra-acp-cat", version: HYDRA_VERSION }
26547
+ clientInfo: { name: HYDRA_CAT_CLIENT_NAME, version: HYDRA_VERSION }
24258
26548
  });
24259
26549
  return attached.sessionId;
24260
26550
  }
@@ -24265,6 +26555,9 @@ async function openOrAttachSession(conn, opts) {
24265
26555
  if (opts.model) {
24266
26556
  hydraMeta.model = opts.model;
24267
26557
  }
26558
+ if (useAutoStream) {
26559
+ hydraMeta.mcpStdin = true;
26560
+ }
24268
26561
  const cwd = opts.cwd ?? process.cwd();
24269
26562
  const params = { cwd };
24270
26563
  if (opts.agentId) {
@@ -24296,6 +26589,16 @@ async function openWs2(url, subprotocols) {
24296
26589
  // src/cli.ts
24297
26590
  init_update_check();
24298
26591
  var suppressUpdateNotice = false;
26592
+ var dangerousNoticePrinted = false;
26593
+ function warnIfDangerouslySkipping(active) {
26594
+ if (!active || dangerousNoticePrinted) {
26595
+ return;
26596
+ }
26597
+ dangerousNoticePrinted = true;
26598
+ process.stderr.write(
26599
+ "hydra-acp: --dangerously-skip-permissions is set \u2014 all tool permission requests will be auto-approved.\n"
26600
+ );
26601
+ }
24299
26602
  async function main() {
24300
26603
  const argv = process.argv.slice(2);
24301
26604
  const launchIdx = argv.indexOf("launch");
@@ -24305,6 +26608,7 @@ async function main() {
24305
26608
  const positionalAgentId = afterLaunch[0];
24306
26609
  const agentArgs = afterLaunch.slice(1);
24307
26610
  const { flags: flags2 } = parseArgs(beforeLaunch);
26611
+ rejectUnknownFlags(flags2);
24308
26612
  if (flags2.reattach === true) {
24309
26613
  process.stderr.write(
24310
26614
  "hydra-acp launch: --reattach is not valid here. Pass --session <id-or-url> to attach to a specific session.\n"
@@ -24327,11 +26631,14 @@ async function main() {
24327
26631
  const name2 = resolveOption(flags2, "name");
24328
26632
  const model2 = resolveOption(flags2, "model");
24329
26633
  suppressUpdateNotice = true;
26634
+ const dangerous = flags2["dangerously-skip-permissions"] === true;
26635
+ warnIfDangerouslySkipping(dangerous);
24330
26636
  const shimOpts = {
24331
26637
  agentId,
24332
26638
  agentArgs,
24333
26639
  name: name2,
24334
- model: model2
26640
+ model: model2,
26641
+ dangerouslySkipPermissions: dangerous
24335
26642
  };
24336
26643
  if (resolved2?.sessionId !== void 0) {
24337
26644
  shimOpts.sessionId = resolved2.sessionId;
@@ -24343,6 +26650,7 @@ async function main() {
24343
26650
  return;
24344
26651
  }
24345
26652
  const { positional, flags } = parseArgs(argv);
26653
+ rejectUnknownFlags(flags);
24346
26654
  if (flags.version === true || positional[0] === "--version") {
24347
26655
  process.stdout.write(`hydra-acp ${readVersion()}
24348
26656
  `);
@@ -24356,6 +26664,8 @@ async function main() {
24356
26664
  const name = resolveOption(flags, "name");
24357
26665
  const agentIdFromFlag = resolveOption(flags, "agent");
24358
26666
  const model = resolveOption(flags, "model");
26667
+ const dangerouslySkipPermissions = flags["dangerously-skip-permissions"] === true;
26668
+ warnIfDangerouslySkipping(dangerouslySkipPermissions);
24359
26669
  const sessionInput = readSessionInput(flags);
24360
26670
  const interactive = subcommand === "tui" || subcommand === void 0 && process.stdout.isTTY;
24361
26671
  const resolved = await resolveSessionFlagOrExit(sessionInput, {
@@ -24377,7 +26687,8 @@ async function main() {
24377
26687
  agentId: agentIdFromFlag,
24378
26688
  name,
24379
26689
  model,
24380
- target: sessionTarget
26690
+ target: sessionTarget,
26691
+ dangerouslySkipPermissions
24381
26692
  });
24382
26693
  return;
24383
26694
  }
@@ -24385,7 +26696,8 @@ async function main() {
24385
26696
  const shimOpts = {
24386
26697
  name,
24387
26698
  model,
24388
- agentId: agentIdFromFlag
26699
+ agentId: agentIdFromFlag,
26700
+ dangerouslySkipPermissions
24389
26701
  };
24390
26702
  if (sessionId !== void 0) {
24391
26703
  shimOpts.sessionId = sessionId;
@@ -24402,7 +26714,8 @@ async function main() {
24402
26714
  const shimOpts = {
24403
26715
  name,
24404
26716
  model,
24405
- agentId: agentIdFromFlag
26717
+ agentId: agentIdFromFlag,
26718
+ dangerouslySkipPermissions
24406
26719
  };
24407
26720
  if (sessionId !== void 0) {
24408
26721
  shimOpts.sessionId = sessionId;
@@ -24425,7 +26738,8 @@ async function main() {
24425
26738
  model,
24426
26739
  agentId: agentIdFromFlag,
24427
26740
  detach: flags.detach === true,
24428
- stream: flags.stream === true
26741
+ follow: flags.follow === true,
26742
+ dangerouslySkipPermissions
24429
26743
  };
24430
26744
  if (cwd !== void 0) {
24431
26745
  catOpts.cwd = cwd;
@@ -24441,10 +26755,6 @@ async function main() {
24441
26755
  if (streamBufferBytes !== void 0) {
24442
26756
  catOpts.streamBufferBytes = streamBufferBytes;
24443
26757
  }
24444
- const streamFileCap = parseNumericFlag(flags, "stream-file-cap");
24445
- if (streamFileCap !== void 0) {
24446
- catOpts.streamFileCapBytes = streamFileCap;
24447
- }
24448
26758
  suppressUpdateNotice = true;
24449
26759
  await runCat(catOpts);
24450
26760
  return;
@@ -24488,7 +26798,8 @@ async function main() {
24488
26798
  await runSessionsList({
24489
26799
  all: flags.all === true,
24490
26800
  json: flags.json === true,
24491
- host: typeof flags.host === "string" ? flags.host : void 0
26801
+ host: typeof flags.host === "string" ? flags.host : void 0,
26802
+ includeCat: flags["include-cat"] === true
24492
26803
  });
24493
26804
  return;
24494
26805
  }
@@ -24670,7 +26981,8 @@ async function main() {
24670
26981
  agentId: agentIdFromFlag,
24671
26982
  name,
24672
26983
  model,
24673
- target: sessionTarget
26984
+ target: sessionTarget,
26985
+ dangerouslySkipPermissions
24674
26986
  });
24675
26987
  return;
24676
26988
  default:
@@ -24712,6 +27024,9 @@ async function dispatchTui(flags, base) {
24712
27024
  if (base.target !== void 0) {
24713
27025
  tuiOpts.target = base.target;
24714
27026
  }
27027
+ if (base.dangerouslySkipPermissions === true) {
27028
+ tuiOpts.dangerouslySkipPermissions = true;
27029
+ }
24715
27030
  await runTui(tuiOpts);
24716
27031
  }
24717
27032
  function parseNumericFlag(flags, name) {
@@ -24730,6 +27045,17 @@ function parseNumericFlag(flags, name) {
24730
27045
  }
24731
27046
  return void 0;
24732
27047
  }
27048
+ function rejectUnknownFlags(flags) {
27049
+ const unknown = validateKnownFlags(flags);
27050
+ if (unknown === void 0) {
27051
+ return;
27052
+ }
27053
+ process.stderr.write(`hydra-acp: unknown flag: --${unknown}
27054
+
27055
+ `);
27056
+ printHelp();
27057
+ process.exit(2);
27058
+ }
24733
27059
  function readShortPrompt(argv) {
24734
27060
  for (let i = 0; i < argv.length; i += 1) {
24735
27061
  const tok = argv[i];
@@ -24802,15 +27128,17 @@ function printHelp() {
24802
27128
  " --reattach Pick the most-recent session for the current cwd.",
24803
27129
  " --new Force a fresh session.",
24804
27130
  " --readonly Open a session as a transcript viewer (requires --session).",
27131
+ " --dangerously-skip-permissions Auto-approve every tool permission request (tui / shim / launch / cat).",
24805
27132
  " HYDRA_ACP_SESSION Env var equivalent of --session (flag wins).",
24806
27133
  " hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
24807
27134
  " hydra-acp daemon [status] Show daemon pid/version (default when no subcommand)",
24808
27135
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
24809
27136
  " hydra-acp daemon stop|restart",
24810
27137
  " hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
24811
- " hydra-acp session [list] [--all] [--json] [--host=<host>]",
27138
+ " hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-cat]",
24812
27139
  " List sessions (live + 20 most-recent cold; --all for everything; --json emits JSON for scripts).",
24813
27140
  " --host filters by origin machine: 'local' (default) shows only sessions created here, 'all' shows everything, or pass a hostname (e.g. machine-b) to show only imports from that peer.",
27141
+ " --include-cat surfaces sessions spawned by `hydra cat` (hidden by default).",
24814
27142
  " hydra-acp session kill <id> Demote a live session to cold (keeps the on-disk record)",
24815
27143
  " hydra-acp session remove <id> Remove a session entirely (live or cold)",
24816
27144
  " hydra-acp session export <id> [--out <file>|.]",