@hydra-acp/cli 0.1.49 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1304,6 +1304,9 @@ function extractHydraMeta(meta) {
1304
1304
  if (typeof obj.promptUpdating === "boolean") {
1305
1305
  out.promptUpdating = obj.promptUpdating;
1306
1306
  }
1307
+ if (typeof obj.mcpStdin === "boolean") {
1308
+ out.mcpStdin = obj.mcpStdin;
1309
+ }
1307
1310
  if (typeof obj.promptAmending === "boolean") {
1308
1311
  out.promptAmending = obj.promptAmending;
1309
1312
  }
@@ -1419,6 +1422,10 @@ var SessionListEntry = z3.object({
1419
1422
  importedFromUpstreamSessionId: z3.string().optional(),
1420
1423
  // Set when this session was spawned as a child by a transformer.
1421
1424
  parentSessionId: z3.string().optional(),
1425
+ // clientInfo from the process that issued session/new. Lets list views
1426
+ // hide cat-style ancillary sessions by default while letting an
1427
+ // override flag surface them.
1428
+ originatingClient: z3.object({ name: z3.string(), version: z3.string().optional() }).optional(),
1422
1429
  updatedAt: z3.string(),
1423
1430
  attachedClients: z3.number().int().nonnegative(),
1424
1431
  status: z3.enum(["live", "cold"]).default("live"),
@@ -2021,14 +2028,30 @@ import { customAlphabet } from "nanoid";
2021
2028
 
2022
2029
  // src/core/stream-buffer.ts
2023
2030
  import * as fsp3 from "fs/promises";
2024
- var DEFAULT_CAPACITY_BYTES = 16 * 1024 * 1024;
2031
+ var DEFAULT_CAPACITY_BYTES = 64 * 1024 * 1024;
2032
+ var INITIAL_CAPACITY_BYTES = 1 * 1024 * 1024;
2025
2033
  var STREAM_READ_MAX_BYTES = 64 * 1024;
2026
2034
  var STREAM_WAIT_MAX_MS = 6e4;
2035
+ var STREAM_GREP_DEFAULT_MATCHES = 100;
2036
+ var STREAM_GREP_MAX_MATCHES = 1e3;
2037
+ var STREAM_GREP_DEFAULT_BYTES = 64 * 1024;
2038
+ var STREAM_GREP_MAX_BYTES = 256 * 1024;
2039
+ var STREAM_GREP_MAX_CONTEXT = 20;
2040
+ function escapeRegex(s) {
2041
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2042
+ }
2027
2043
  var SessionStreamBuffer = class {
2028
2044
  storage;
2029
- capacityBytes;
2045
+ // The configured cap. Eviction begins once writeCursor exceeds this.
2046
+ maxCapacityBytes;
2047
+ // The size of the currently-allocated `storage`. Starts at
2048
+ // INITIAL_CAPACITY_BYTES (clamped to maxCapacityBytes) and doubles on
2049
+ // demand. Once it reaches maxCapacityBytes the ring behaves like a
2050
+ // fixed-size buffer; before then, writeCursor < currentCapacityBytes
2051
+ // always, so no wrap-around math is in play.
2052
+ currentCapacityBytes;
2030
2053
  // Absolute monotonic byte offset of the next byte to be written. Also
2031
- // the count of bytes ever appended. `writeCursor - capacityBytes`
2054
+ // the count of bytes ever appended. `writeCursor - currentCapacityBytes`
2032
2055
  // (clamped at 0) is the oldest still-resident byte's cursor.
2033
2056
  writeCursor = 0;
2034
2057
  closed = false;
@@ -2043,24 +2066,33 @@ var SessionStreamBuffer = class {
2043
2066
  // calls don't interleave their writes.
2044
2067
  fileWriteChain = Promise.resolve();
2045
2068
  constructor(opts = {}) {
2046
- this.capacityBytes = opts.capacityBytes ?? DEFAULT_CAPACITY_BYTES;
2047
- if (this.capacityBytes <= 0) {
2069
+ this.maxCapacityBytes = opts.capacityBytes ?? DEFAULT_CAPACITY_BYTES;
2070
+ if (this.maxCapacityBytes <= 0) {
2048
2071
  throw new Error("capacityBytes must be > 0");
2049
2072
  }
2050
- this.storage = Buffer.alloc(this.capacityBytes);
2073
+ this.currentCapacityBytes = Math.min(
2074
+ INITIAL_CAPACITY_BYTES,
2075
+ this.maxCapacityBytes
2076
+ );
2077
+ this.storage = Buffer.alloc(this.currentCapacityBytes);
2051
2078
  this.filePath = opts.filePath;
2052
2079
  this.fileCapBytes = opts.fileCapBytes ?? Number.POSITIVE_INFINITY;
2053
2080
  this.onFileCapReached = opts.onFileCapReached;
2054
2081
  this.logWriteError = opts.logWriteError;
2055
2082
  }
2056
2083
  get capacity() {
2057
- return this.capacityBytes;
2084
+ return this.maxCapacityBytes;
2085
+ }
2086
+ // Currently-allocated storage size (for observability / tests). May be
2087
+ // anywhere between INITIAL_CAPACITY_BYTES and capacity.
2088
+ get allocatedBytes() {
2089
+ return this.currentCapacityBytes;
2058
2090
  }
2059
2091
  get writeCursorPos() {
2060
2092
  return this.writeCursor;
2061
2093
  }
2062
2094
  get oldestAvailable() {
2063
- return Math.max(0, this.writeCursor - this.capacityBytes);
2095
+ return Math.max(0, this.writeCursor - this.currentCapacityBytes);
2064
2096
  }
2065
2097
  get isClosed() {
2066
2098
  return this.closed;
@@ -2205,6 +2237,136 @@ var SessionStreamBuffer = class {
2205
2237
  this.waiters.push(waiter);
2206
2238
  });
2207
2239
  }
2240
+ // Scan the resident region line-by-line, returning lines that match
2241
+ // `pattern`. Server-side filtering so the agent doesn't have to pull
2242
+ // and decode 64 KiB base64 windows just to grep a multi-MB log.
2243
+ //
2244
+ // Lines are split on `\n` (LF). A trailing partial line (no LF) is
2245
+ // skipped when the buffer is still open — its bytes might be the
2246
+ // start of a longer line that's still being written — but is treated
2247
+ // as a final full line once the buffer is closed.
2248
+ //
2249
+ // Caps: max 1000 matches and 256 KiB of output bytes per call. The
2250
+ // agent should re-call with `cursor = nextCursor` to resume when
2251
+ // `truncated:true`.
2252
+ grep(opts) {
2253
+ const oldest = this.oldestAvailable;
2254
+ const requested = opts.cursor;
2255
+ let start = requested ?? oldest;
2256
+ let gap = 0;
2257
+ if (requested !== void 0 && requested < oldest) {
2258
+ gap = oldest - requested;
2259
+ start = oldest;
2260
+ }
2261
+ if (start > this.writeCursor) {
2262
+ start = this.writeCursor;
2263
+ }
2264
+ const slice = this.sliceFromRing(start, this.writeCursor - start);
2265
+ const useRegex = opts.regex ?? true;
2266
+ const flags = opts.caseInsensitive === true ? "i" : "";
2267
+ const re = useRegex ? new RegExp(opts.pattern, flags) : new RegExp(escapeRegex(opts.pattern), flags);
2268
+ const invert = opts.invert ?? false;
2269
+ const maxMatches = Math.max(
2270
+ 1,
2271
+ Math.min(
2272
+ opts.maxMatches ?? STREAM_GREP_DEFAULT_MATCHES,
2273
+ STREAM_GREP_MAX_MATCHES
2274
+ )
2275
+ );
2276
+ const maxBytes = Math.max(
2277
+ 1,
2278
+ Math.min(opts.maxBytes ?? STREAM_GREP_DEFAULT_BYTES, STREAM_GREP_MAX_BYTES)
2279
+ );
2280
+ const contextBefore = Math.max(
2281
+ 0,
2282
+ Math.min(opts.contextBefore ?? 0, STREAM_GREP_MAX_CONTEXT)
2283
+ );
2284
+ const contextAfter = Math.max(
2285
+ 0,
2286
+ Math.min(opts.contextAfter ?? 0, STREAM_GREP_MAX_CONTEXT)
2287
+ );
2288
+ const matches = [];
2289
+ const beforeRing = [];
2290
+ const pendingAfter = [];
2291
+ let bytesUsed = 0;
2292
+ let truncated = false;
2293
+ let lineStartByte = 0;
2294
+ let resumeFromLineStart = 0;
2295
+ const processLine = (lineCursor, lineText) => {
2296
+ for (const pa of pendingAfter) {
2297
+ if (pa.remaining > 0) {
2298
+ if (pa.match.after === void 0) {
2299
+ pa.match.after = [];
2300
+ }
2301
+ pa.match.after.push({ cursor: lineCursor, line: lineText });
2302
+ pa.remaining--;
2303
+ bytesUsed += lineText.length;
2304
+ }
2305
+ }
2306
+ while (pendingAfter.length > 0 && pendingAfter[0].remaining === 0) {
2307
+ pendingAfter.shift();
2308
+ }
2309
+ const matched = re.test(lineText) !== invert;
2310
+ if (matched && matches.length < maxMatches) {
2311
+ const m = { cursor: lineCursor, line: lineText };
2312
+ if (contextBefore > 0 && beforeRing.length > 0) {
2313
+ m.before = beforeRing.slice();
2314
+ for (const b of m.before) {
2315
+ bytesUsed += b.line.length;
2316
+ }
2317
+ }
2318
+ bytesUsed += lineText.length;
2319
+ matches.push(m);
2320
+ if (contextAfter > 0) {
2321
+ pendingAfter.push({ match: m, remaining: contextAfter });
2322
+ }
2323
+ }
2324
+ if (contextBefore > 0) {
2325
+ beforeRing.push({ cursor: lineCursor, line: lineText });
2326
+ while (beforeRing.length > contextBefore) {
2327
+ beforeRing.shift();
2328
+ }
2329
+ }
2330
+ const hitMaxMatches = matches.length >= maxMatches && pendingAfter.length === 0;
2331
+ const hitMaxBytes = bytesUsed >= maxBytes;
2332
+ return hitMaxMatches || hitMaxBytes;
2333
+ };
2334
+ for (let i = 0; i < slice.length; i++) {
2335
+ if (slice[i] !== 10) {
2336
+ continue;
2337
+ }
2338
+ const lineText = slice.subarray(lineStartByte, i).toString("utf8");
2339
+ const lineCursor = start + lineStartByte;
2340
+ lineStartByte = i + 1;
2341
+ resumeFromLineStart = lineStartByte;
2342
+ if (processLine(lineCursor, lineText)) {
2343
+ truncated = true;
2344
+ break;
2345
+ }
2346
+ }
2347
+ if (!truncated && lineStartByte < slice.length && this.closed) {
2348
+ const lineText = slice.subarray(lineStartByte).toString("utf8");
2349
+ const lineCursor = start + lineStartByte;
2350
+ if (processLine(lineCursor, lineText)) {
2351
+ truncated = true;
2352
+ }
2353
+ resumeFromLineStart = slice.length;
2354
+ }
2355
+ const nextCursor = Math.min(start + resumeFromLineStart, this.writeCursor);
2356
+ const result = {
2357
+ matches,
2358
+ truncated,
2359
+ nextCursor,
2360
+ scannedBytes: resumeFromLineStart
2361
+ };
2362
+ if (gap > 0) {
2363
+ result.gap = gap;
2364
+ }
2365
+ if (this.closed && nextCursor >= this.writeCursor) {
2366
+ result.eof = true;
2367
+ }
2368
+ return result;
2369
+ }
2208
2370
  wakeWaiters(outcome) {
2209
2371
  if (this.waiters.length === 0) {
2210
2372
  return;
@@ -2215,15 +2377,42 @@ var SessionStreamBuffer = class {
2215
2377
  w.resolve(outcome);
2216
2378
  }
2217
2379
  }
2380
+ // Grow `storage` if needed to fit `additionalBytes` more bytes without
2381
+ // wrapping. Caps at maxCapacityBytes; once we're at the cap, callers
2382
+ // fall back to ring-wrap behavior. Doubles each grow so we amortize.
2383
+ // Only called before we've ever wrapped (writeCursor < currentCapacity
2384
+ // always holds while we're growing), so the existing bytes live at
2385
+ // storage[0..writeCursor] and we can just copy them flat.
2386
+ growIfNeeded(additionalBytes) {
2387
+ if (this.currentCapacityBytes >= this.maxCapacityBytes) {
2388
+ return;
2389
+ }
2390
+ const needed = this.writeCursor + additionalBytes;
2391
+ if (needed <= this.currentCapacityBytes) {
2392
+ return;
2393
+ }
2394
+ let next = this.currentCapacityBytes;
2395
+ while (next < needed && next < this.maxCapacityBytes) {
2396
+ next = Math.min(this.maxCapacityBytes, next * 2);
2397
+ }
2398
+ if (next === this.currentCapacityBytes) {
2399
+ return;
2400
+ }
2401
+ const newStorage = Buffer.alloc(next);
2402
+ this.storage.copy(newStorage, 0, 0, this.writeCursor);
2403
+ this.storage = newStorage;
2404
+ this.currentCapacityBytes = next;
2405
+ }
2218
2406
  writeRing(chunk) {
2219
2407
  const len = chunk.length;
2220
- if (len >= this.capacityBytes) {
2221
- const tailStart = len - this.capacityBytes;
2408
+ this.growIfNeeded(len);
2409
+ if (len >= this.currentCapacityBytes) {
2410
+ const tailStart = len - this.currentCapacityBytes;
2222
2411
  chunk.copy(this.storage, 0, tailStart, len);
2223
2412
  return;
2224
2413
  }
2225
- const offset = this.writeCursor % this.capacityBytes;
2226
- const tailRoom = this.capacityBytes - offset;
2414
+ const offset = this.writeCursor % this.currentCapacityBytes;
2415
+ const tailRoom = this.currentCapacityBytes - offset;
2227
2416
  if (len <= tailRoom) {
2228
2417
  chunk.copy(this.storage, offset, 0, len);
2229
2418
  } else {
@@ -2236,8 +2425,8 @@ var SessionStreamBuffer = class {
2236
2425
  return Buffer.alloc(0);
2237
2426
  }
2238
2427
  const out = Buffer.alloc(length);
2239
- const offset = fromCursor % this.capacityBytes;
2240
- const tailLen = Math.min(length, this.capacityBytes - offset);
2428
+ const offset = fromCursor % this.currentCapacityBytes;
2429
+ const tailLen = Math.min(length, this.currentCapacityBytes - offset);
2241
2430
  this.storage.copy(out, 0, offset, offset + tailLen);
2242
2431
  if (tailLen < length) {
2243
2432
  this.storage.copy(out, tailLen, 0, length - tailLen);
@@ -2379,6 +2568,7 @@ var Session = class {
2379
2568
  agentCapabilities;
2380
2569
  agentArgs;
2381
2570
  parentSessionId;
2571
+ originatingClient;
2382
2572
  title;
2383
2573
  // Snapshot state delivered to attaching clients via the attach
2384
2574
  // response _meta rather than via history replay (which would be
@@ -2533,6 +2723,7 @@ var Session = class {
2533
2723
  this.agentCapabilities = init.agentCapabilities;
2534
2724
  this.agentArgs = init.agentArgs;
2535
2725
  this.parentSessionId = init.parentSessionId;
2726
+ this.originatingClient = init.originatingClient;
2536
2727
  this.title = init.title;
2537
2728
  this.currentModel = init.currentModel;
2538
2729
  this.currentMode = init.currentMode;
@@ -4720,6 +4911,43 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
4720
4911
  }
4721
4912
  return out;
4722
4913
  }
4914
+ streamTail(bytes) {
4915
+ const buf = this.requireStreamBuffer();
4916
+ const r = buf.tail(bytes);
4917
+ return {
4918
+ bytes: r.bytes.toString("base64"),
4919
+ startCursor: r.startCursor,
4920
+ endCursor: r.endCursor,
4921
+ truncated: r.truncated
4922
+ };
4923
+ }
4924
+ streamHead(bytes) {
4925
+ const buf = this.requireStreamBuffer();
4926
+ const r = buf.head(bytes);
4927
+ return {
4928
+ bytes: r.bytes.toString("base64"),
4929
+ startCursor: r.startCursor,
4930
+ endCursor: r.endCursor,
4931
+ truncated: r.truncated
4932
+ };
4933
+ }
4934
+ async streamWaitFor(cursor, timeoutMs) {
4935
+ const buf = this.requireStreamBuffer();
4936
+ return buf.waitForData(cursor, timeoutMs);
4937
+ }
4938
+ streamGrep(opts) {
4939
+ const buf = this.requireStreamBuffer();
4940
+ return buf.grep(opts);
4941
+ }
4942
+ streamInfo() {
4943
+ const buf = this.requireStreamBuffer();
4944
+ return {
4945
+ writeCursor: buf.writeCursorPos,
4946
+ oldestAvailable: buf.oldestAvailable,
4947
+ capacity: buf.capacity,
4948
+ closed: buf.isClosed
4949
+ };
4950
+ }
4723
4951
  requireStreamBuffer() {
4724
4952
  if (this.streamBuffer === void 0) {
4725
4953
  const err = new Error(
@@ -5479,6 +5707,10 @@ var PersistedUsage = z4.object({
5479
5707
  costCurrency: z4.string().optional(),
5480
5708
  cumulativeCost: z4.number().optional()
5481
5709
  });
5710
+ var PersistedOriginatingClient = z4.object({
5711
+ name: z4.string(),
5712
+ version: z4.string().optional()
5713
+ });
5482
5714
  var SessionRecord = z4.object({
5483
5715
  version: z4.literal(1),
5484
5716
  sessionId: z4.string(),
@@ -5529,6 +5761,10 @@ var SessionRecord = z4.object({
5529
5761
  // Set when this session was spawned as a child by a transformer via
5530
5762
  // hydra-acp/spawn_child_session. Points to the spawning session's id.
5531
5763
  parentSessionId: z4.string().optional(),
5764
+ // clientInfo from the process that issued session/new. Picker and
5765
+ // `sessions list` use this to hide cat-style ancillary sessions by
5766
+ // default; carried in meta.json so cold sessions filter the same way.
5767
+ originatingClient: PersistedOriginatingClient.optional(),
5532
5768
  createdAt: z4.string(),
5533
5769
  updatedAt: z4.string()
5534
5770
  });
@@ -5651,6 +5887,7 @@ function recordFromMemorySession(args) {
5651
5887
  agentModels: args.agentModels,
5652
5888
  pendingHistorySync: args.pendingHistorySync,
5653
5889
  parentSessionId: args.parentSessionId,
5890
+ originatingClient: args.originatingClient,
5654
5891
  createdAt: args.createdAt ?? now,
5655
5892
  updatedAt: args.updatedAt ?? now
5656
5893
  };
@@ -5939,6 +6176,7 @@ var SessionManager = class {
5939
6176
  agentModels: fresh.initialModels,
5940
6177
  transformChain: params.transformChain,
5941
6178
  parentSessionId: params.parentSessionId,
6179
+ originatingClient: params.originatingClient,
5942
6180
  extensionCommands: this.extensionCommands
5943
6181
  });
5944
6182
  await this.attachManagerHooks(session);
@@ -6110,6 +6348,7 @@ var SessionManager = class {
6110
6348
  // than stay stuck.
6111
6349
  firstPromptSeeded: !!params.title,
6112
6350
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
6351
+ originatingClient: params.originatingClient,
6113
6352
  extensionCommands: this.extensionCommands
6114
6353
  });
6115
6354
  await this.attachManagerHooks(session);
@@ -6177,6 +6416,7 @@ var SessionManager = class {
6177
6416
  agentModels: advertisedModels,
6178
6417
  firstPromptSeeded: !!params.title,
6179
6418
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
6419
+ originatingClient: params.originatingClient,
6180
6420
  extensionCommands: this.extensionCommands
6181
6421
  });
6182
6422
  await this.attachManagerHooks(session);
@@ -6526,7 +6766,8 @@ var SessionManager = class {
6526
6766
  agentModes: record.agentModes,
6527
6767
  agentModels: record.agentModels,
6528
6768
  createdAt: record.createdAt,
6529
- pendingHistorySync: record.pendingHistorySync
6769
+ pendingHistorySync: record.pendingHistorySync,
6770
+ originatingClient: record.originatingClient
6530
6771
  };
6531
6772
  }
6532
6773
  async clearPendingHistorySync(sessionId) {
@@ -6627,6 +6868,7 @@ var SessionManager = class {
6627
6868
  currentModel: session.currentModel,
6628
6869
  currentUsage: session.currentUsage,
6629
6870
  parentSessionId: session.parentSessionId,
6871
+ originatingClient: session.originatingClient,
6630
6872
  updatedAt: used,
6631
6873
  attachedClients: session.attachedCount,
6632
6874
  status: "live",
@@ -6656,6 +6898,7 @@ var SessionManager = class {
6656
6898
  importedFromMachine: r.importedFromMachine,
6657
6899
  importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
6658
6900
  parentSessionId: r.parentSessionId,
6901
+ originatingClient: r.originatingClient,
6659
6902
  updatedAt: used,
6660
6903
  attachedClients: 0,
6661
6904
  status: "cold",
@@ -7001,6 +7244,7 @@ function mergeForPersistence(session, existing) {
7001
7244
  agentModes,
7002
7245
  agentModels,
7003
7246
  parentSessionId: session.parentSessionId ?? existing?.parentSessionId,
7247
+ originatingClient: session.originatingClient ?? existing?.originatingClient,
7004
7248
  createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
7005
7249
  });
7006
7250
  }
@@ -10590,6 +10834,7 @@ function wsToMessageStream(ws) {
10590
10834
  // src/daemon/acp-ws.ts
10591
10835
  import * as os4 from "os";
10592
10836
  import * as path12 from "path";
10837
+ import { randomBytes as randomBytes3 } from "crypto";
10593
10838
  function registerAcpWsEndpoint(app, deps) {
10594
10839
  app.get("/acp", { websocket: true }, async (socket, request) => {
10595
10840
  const token = tokenFromUpgradeRequest({
@@ -10627,6 +10872,12 @@ function registerAcpWsEndpoint(app, deps) {
10627
10872
  };
10628
10873
  connection.onRequest("initialize", async (raw) => {
10629
10874
  const params = InitializeParams.parse(raw ?? {});
10875
+ if (params.clientInfo?.name) {
10876
+ state.clientInfo = {
10877
+ name: params.clientInfo.name,
10878
+ ...params.clientInfo.version !== void 0 ? { version: params.clientInfo.version } : {}
10879
+ };
10880
+ }
10630
10881
  const version = params.clientInfo?.version;
10631
10882
  if (version && processIdentity) {
10632
10883
  if (processIdentity.kind === "extension") {
@@ -10806,16 +11057,50 @@ function registerAcpWsEndpoint(app, deps) {
10806
11057
  );
10807
11058
  const transformerNames = Array.isArray(hydraMeta.transformers) && hydraMeta.transformers.every((t) => typeof t === "string") ? hydraMeta.transformers : deps.manager.defaultTransformers ?? [];
10808
11059
  const transformChain = deps.transformers?.resolveChain(transformerNames) ?? [];
10809
- const session = await deps.manager.create({
10810
- cwd: params.cwd,
10811
- agentId: params.agentId ?? deps.defaultAgent,
10812
- mcpServers: params.mcpServers,
10813
- title: hydraMeta.name,
10814
- agentArgs: hydraMeta.agentArgs,
10815
- model: hydraMeta.model,
10816
- onInstallProgress: makeInstallProgressForwarder(connection),
10817
- transformChain
10818
- });
11060
+ let stdinToken;
11061
+ let stdinReservation;
11062
+ let augmentedMcpServers = params.mcpServers;
11063
+ if (hydraMeta.mcpStdin === true && deps.stdinMcpRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
11064
+ stdinToken = randomBytes3(32).toString("hex");
11065
+ stdinReservation = deps.stdinMcpRegistry.reserve(stdinToken);
11066
+ const url = `${deps.getDaemonOrigin()}/mcp/stdin`;
11067
+ const descriptor = {
11068
+ name: "hydra_stdin",
11069
+ type: "http",
11070
+ url,
11071
+ headers: [
11072
+ { name: "Authorization", value: `Bearer ${stdinToken}` }
11073
+ ]
11074
+ };
11075
+ augmentedMcpServers = [...params.mcpServers ?? [], descriptor];
11076
+ }
11077
+ let session;
11078
+ try {
11079
+ session = await deps.manager.create({
11080
+ cwd: params.cwd,
11081
+ agentId: params.agentId ?? deps.defaultAgent,
11082
+ mcpServers: augmentedMcpServers,
11083
+ title: hydraMeta.name,
11084
+ agentArgs: hydraMeta.agentArgs,
11085
+ model: hydraMeta.model,
11086
+ onInstallProgress: makeInstallProgressForwarder(connection),
11087
+ transformChain,
11088
+ originatingClient: state.clientInfo
11089
+ });
11090
+ } catch (err) {
11091
+ if (stdinReservation !== void 0) {
11092
+ stdinReservation.abandon(err instanceof Error ? err : void 0);
11093
+ }
11094
+ throw err;
11095
+ }
11096
+ if (stdinToken !== void 0 && stdinReservation !== void 0 && deps.stdinMcpRegistry !== void 0) {
11097
+ const token2 = stdinToken;
11098
+ const registry = deps.stdinMcpRegistry;
11099
+ stdinReservation.complete(session);
11100
+ session.onClose(() => {
11101
+ void registry.unbind(token2);
11102
+ });
11103
+ }
10819
11104
  const client = bindClientToSession(connection, session, state);
10820
11105
  const { entries: replay } = await session.attach(client, "full");
10821
11106
  state.attached.set(session.sessionId, {
@@ -11536,6 +11821,336 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
11536
11821
  };
11537
11822
  }
11538
11823
 
11824
+ // src/daemon/mcp/stdin-registry.ts
11825
+ var StdinMcpRegistry = class {
11826
+ byToken = /* @__PURE__ */ new Map();
11827
+ // Reserve a token slot before the session exists. Used by acp-ws when
11828
+ // we need to inject the bearer into the agent's mcpServers BEFORE
11829
+ // manager.create() returns — claude-acp connects to /mcp/stdin during
11830
+ // session/new initialization (eagerly), so the route handler must be
11831
+ // able to find the token by the time the agent's first request lands.
11832
+ reserve(token) {
11833
+ if (this.byToken.has(token)) {
11834
+ throw new Error(`stdin MCP token already bound`);
11835
+ }
11836
+ let resolveSession;
11837
+ let rejectSession;
11838
+ const sessionReady = new Promise((resolve3, reject) => {
11839
+ resolveSession = resolve3;
11840
+ rejectSession = reject;
11841
+ });
11842
+ sessionReady.catch(() => void 0);
11843
+ const entry = { session: void 0, sessionReady };
11844
+ this.byToken.set(token, entry);
11845
+ return {
11846
+ complete: (session) => {
11847
+ entry.session = session;
11848
+ resolveSession(session);
11849
+ },
11850
+ abandon: (reason) => {
11851
+ this.byToken.delete(token);
11852
+ rejectSession(reason ?? new Error("stdin MCP reservation abandoned"));
11853
+ }
11854
+ };
11855
+ }
11856
+ // Convenience for callers that already have the session in hand (and
11857
+ // for tests). Equivalent to reserve() + complete() back-to-back.
11858
+ bind(token, session) {
11859
+ const { complete } = this.reserve(token);
11860
+ complete(session);
11861
+ }
11862
+ lookup(token) {
11863
+ return this.byToken.get(token);
11864
+ }
11865
+ attachTransport(token, server, transport) {
11866
+ const ep = this.byToken.get(token);
11867
+ if (!ep) {
11868
+ return;
11869
+ }
11870
+ ep.server = server;
11871
+ ep.transport = transport;
11872
+ }
11873
+ async unbind(token) {
11874
+ const ep = this.byToken.get(token);
11875
+ if (!ep) {
11876
+ return;
11877
+ }
11878
+ this.byToken.delete(token);
11879
+ if (ep.transport) {
11880
+ try {
11881
+ await ep.transport.close();
11882
+ } catch {
11883
+ }
11884
+ }
11885
+ if (ep.server) {
11886
+ try {
11887
+ await ep.server.close();
11888
+ } catch {
11889
+ }
11890
+ }
11891
+ }
11892
+ size() {
11893
+ return this.byToken.size;
11894
+ }
11895
+ };
11896
+
11897
+ // src/daemon/mcp/stdin-server.ts
11898
+ import { randomUUID } from "crypto";
11899
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11900
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11901
+ import { z as z7 } from "zod";
11902
+ var BEARER_PREFIX2 = "Bearer ";
11903
+ function extractBearer(req) {
11904
+ const header = req.headers.authorization;
11905
+ if (typeof header !== "string") {
11906
+ return void 0;
11907
+ }
11908
+ if (!header.startsWith(BEARER_PREFIX2)) {
11909
+ return void 0;
11910
+ }
11911
+ const token = header.slice(BEARER_PREFIX2.length).trim();
11912
+ return token.length > 0 ? token : void 0;
11913
+ }
11914
+ function buildMcpServer(session) {
11915
+ const server = new McpServer(
11916
+ { name: "hydra-stdin", version: "1.0.0" },
11917
+ {
11918
+ 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."
11919
+ }
11920
+ );
11921
+ server.registerTool(
11922
+ "tail_stdin",
11923
+ {
11924
+ 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.",
11925
+ inputSchema: {
11926
+ bytes: z7.number().int().min(1).describe("How many trailing bytes to return.")
11927
+ }
11928
+ },
11929
+ async ({ bytes }) => {
11930
+ const r = session.streamTail(bytes);
11931
+ return {
11932
+ content: [
11933
+ {
11934
+ type: "text",
11935
+ text: JSON.stringify(r)
11936
+ }
11937
+ ],
11938
+ structuredContent: r
11939
+ };
11940
+ }
11941
+ );
11942
+ server.registerTool(
11943
+ "head_stdin",
11944
+ {
11945
+ 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.",
11946
+ inputSchema: {
11947
+ bytes: z7.number().int().min(1).describe("How many leading bytes to return.")
11948
+ }
11949
+ },
11950
+ async ({ bytes }) => {
11951
+ const r = session.streamHead(bytes);
11952
+ return {
11953
+ content: [
11954
+ {
11955
+ type: "text",
11956
+ text: JSON.stringify(r)
11957
+ }
11958
+ ],
11959
+ structuredContent: r
11960
+ };
11961
+ }
11962
+ );
11963
+ server.registerTool(
11964
+ "read_stdin",
11965
+ {
11966
+ 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.",
11967
+ inputSchema: {
11968
+ cursor: z7.number().int().min(0).describe(
11969
+ "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)."
11970
+ ),
11971
+ max_bytes: z7.number().int().min(1).optional().describe(
11972
+ "Optional cap on how many bytes to return. Server caps at 64 KiB regardless."
11973
+ ),
11974
+ wait_ms: z7.number().int().min(0).optional().describe(
11975
+ "If no bytes are available, block up to this many ms for more (capped server-side at 60_000)."
11976
+ )
11977
+ }
11978
+ },
11979
+ async ({ cursor, max_bytes, wait_ms }) => {
11980
+ const r = await session.streamRead(cursor, max_bytes, wait_ms);
11981
+ return {
11982
+ content: [
11983
+ {
11984
+ type: "text",
11985
+ text: JSON.stringify(r)
11986
+ }
11987
+ ],
11988
+ structuredContent: r
11989
+ };
11990
+ }
11991
+ );
11992
+ server.registerTool(
11993
+ "wait_for_more",
11994
+ {
11995
+ 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.",
11996
+ inputSchema: {
11997
+ cursor: z7.number().int().min(0).describe("The cursor you've already consumed up to."),
11998
+ timeout_ms: z7.number().int().min(0).describe("Maximum ms to block (server caps at 60_000).")
11999
+ }
12000
+ },
12001
+ async ({ cursor, timeout_ms }) => {
12002
+ const outcome = await session.streamWaitFor(cursor, timeout_ms);
12003
+ const info = session.streamInfo();
12004
+ const payload = { outcome, writeCursor: info.writeCursor, closed: info.closed };
12005
+ return {
12006
+ content: [
12007
+ {
12008
+ type: "text",
12009
+ text: JSON.stringify(payload)
12010
+ }
12011
+ ],
12012
+ structuredContent: payload
12013
+ };
12014
+ }
12015
+ );
12016
+ server.registerTool(
12017
+ "grep_stdin",
12018
+ {
12019
+ 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.",
12020
+ inputSchema: {
12021
+ pattern: z7.string().min(1).describe(
12022
+ "Search pattern. Treated as a JavaScript regular expression by default (set `regex:false` for a literal substring match)."
12023
+ ),
12024
+ regex: z7.boolean().optional().describe("Default true. Pass false to treat `pattern` as a literal substring."),
12025
+ case_insensitive: z7.boolean().optional().describe("Default false. Pass true for case-insensitive matching."),
12026
+ invert: z7.boolean().optional().describe("Default false. Pass true to return lines that do NOT match the pattern."),
12027
+ max_matches: z7.number().int().min(1).optional().describe("Default 100. Capped server-side at 1000."),
12028
+ max_bytes: z7.number().int().min(1).optional().describe("Default 64 KiB output. Capped server-side at 256 KiB."),
12029
+ context_before: z7.number().int().min(0).optional().describe("Default 0. Number of lines before each match to include (capped at 20)."),
12030
+ context_after: z7.number().int().min(0).optional().describe("Default 0. Number of lines after each match to include (capped at 20)."),
12031
+ cursor: z7.number().int().min(0).optional().describe(
12032
+ "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."
12033
+ )
12034
+ }
12035
+ },
12036
+ async (args) => {
12037
+ const opts = { pattern: args.pattern };
12038
+ if (args.regex !== void 0) {
12039
+ opts.regex = args.regex;
12040
+ }
12041
+ if (args.case_insensitive !== void 0) {
12042
+ opts.caseInsensitive = args.case_insensitive;
12043
+ }
12044
+ if (args.invert !== void 0) {
12045
+ opts.invert = args.invert;
12046
+ }
12047
+ if (args.max_matches !== void 0) {
12048
+ opts.maxMatches = args.max_matches;
12049
+ }
12050
+ if (args.max_bytes !== void 0) {
12051
+ opts.maxBytes = args.max_bytes;
12052
+ }
12053
+ if (args.context_before !== void 0) {
12054
+ opts.contextBefore = args.context_before;
12055
+ }
12056
+ if (args.context_after !== void 0) {
12057
+ opts.contextAfter = args.context_after;
12058
+ }
12059
+ if (args.cursor !== void 0) {
12060
+ opts.cursor = args.cursor;
12061
+ }
12062
+ const r = session.streamGrep(opts);
12063
+ const payload = r;
12064
+ return {
12065
+ content: [{ type: "text", text: JSON.stringify(r) }],
12066
+ structuredContent: payload
12067
+ };
12068
+ }
12069
+ );
12070
+ server.registerTool(
12071
+ "stdin_info",
12072
+ {
12073
+ description: "Report cursor / capacity / closed state of the stdin ring. Cheap; safe to call repeatedly.",
12074
+ inputSchema: {}
12075
+ },
12076
+ async () => {
12077
+ const r = session.streamInfo();
12078
+ return {
12079
+ content: [
12080
+ {
12081
+ type: "text",
12082
+ text: JSON.stringify(r)
12083
+ }
12084
+ ],
12085
+ structuredContent: r
12086
+ };
12087
+ }
12088
+ );
12089
+ return server;
12090
+ }
12091
+ async function ensureTransport(token, session, registry) {
12092
+ const existing = registry.lookup(token);
12093
+ if (existing?.transport !== void 0) {
12094
+ return existing.transport;
12095
+ }
12096
+ const server = buildMcpServer(session);
12097
+ const transport = new StreamableHTTPServerTransport({
12098
+ sessionIdGenerator: () => randomUUID()
12099
+ });
12100
+ await server.connect(transport);
12101
+ registry.attachTransport(token, server, transport);
12102
+ return transport;
12103
+ }
12104
+ var SESSION_READY_TIMEOUT_MS = 1e4;
12105
+ async function handle(req, reply, registry) {
12106
+ const token = extractBearer(req);
12107
+ if (token === void 0) {
12108
+ reply.code(401).send({ error: "missing bearer token" });
12109
+ return;
12110
+ }
12111
+ const ep = registry.lookup(token);
12112
+ if (ep === void 0) {
12113
+ reply.code(404).send({ error: "unknown stdin token" });
12114
+ return;
12115
+ }
12116
+ let session;
12117
+ if (ep.session !== void 0) {
12118
+ session = ep.session;
12119
+ } else {
12120
+ let timer;
12121
+ const timeout = new Promise((resolve3) => {
12122
+ timer = setTimeout(() => resolve3(void 0), SESSION_READY_TIMEOUT_MS);
12123
+ });
12124
+ const resolved = await Promise.race([
12125
+ ep.sessionReady.catch(() => void 0),
12126
+ timeout
12127
+ ]);
12128
+ if (timer !== void 0) {
12129
+ clearTimeout(timer);
12130
+ }
12131
+ if (resolved === void 0) {
12132
+ reply.code(503).send({ error: "session not ready" });
12133
+ return;
12134
+ }
12135
+ session = resolved;
12136
+ }
12137
+ const transport = await ensureTransport(token, session, registry);
12138
+ reply.hijack();
12139
+ await transport.handleRequest(req.raw, reply.raw, req.body);
12140
+ }
12141
+ function registerStdinMcpRoutes(app, registry) {
12142
+ const opts = { config: { skipAuth: true } };
12143
+ app.post("/mcp/stdin", opts, async (req, reply) => {
12144
+ await handle(req, reply, registry);
12145
+ });
12146
+ app.get("/mcp/stdin", opts, async (req, reply) => {
12147
+ await handle(req, reply, registry);
12148
+ });
12149
+ app.delete("/mcp/stdin", opts, async (req, reply) => {
12150
+ await handle(req, reply, registry);
12151
+ });
12152
+ }
12153
+
11539
12154
  // src/daemon/server.ts
11540
12155
  async function startDaemon(config, serviceToken) {
11541
12156
  ensureLoopbackOrTls(config);
@@ -11641,6 +12256,19 @@ async function startDaemon(config, serviceToken) {
11641
12256
  store: sessionTokenStore,
11642
12257
  rateLimiter: authRateLimiter
11643
12258
  });
12259
+ const stdinMcpRegistry = new StdinMcpRegistry();
12260
+ registerStdinMcpRoutes(app, stdinMcpRegistry);
12261
+ let daemonOriginCached;
12262
+ const getDaemonOrigin = () => {
12263
+ if (daemonOriginCached !== void 0) {
12264
+ return daemonOriginCached;
12265
+ }
12266
+ const addr = app.server.address();
12267
+ const port = addr && typeof addr === "object" ? addr.port : config.daemon.port;
12268
+ const scheme2 = config.daemon.tls ? "https" : "http";
12269
+ daemonOriginCached = `${scheme2}://${config.daemon.host}:${port}`;
12270
+ return daemonOriginCached;
12271
+ };
11644
12272
  registerAcpWsEndpoint(app, {
11645
12273
  validator,
11646
12274
  manager,
@@ -11649,7 +12277,9 @@ async function startDaemon(config, serviceToken) {
11649
12277
  onExtensionVersion: (name, version) => extensions.reportVersion(name, version),
11650
12278
  onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
11651
12279
  transformers,
11652
- extensionCommands
12280
+ extensionCommands,
12281
+ stdinMcpRegistry,
12282
+ getDaemonOrigin
11653
12283
  });
11654
12284
  await app.listen({ host: config.daemon.host, port: config.daemon.port });
11655
12285
  const address = app.server.address();