@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.
- package/dist/cli.js +2983 -655
- package/dist/index.d.ts +145 -13
- package/dist/index.js +1404 -81
- package/package.json +2 -1
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"),
|
|
@@ -1757,7 +1764,10 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1757
1764
|
// every entry would be re-appended to history.jsonl, doubling the log
|
|
1758
1765
|
// each time the session was woken up.
|
|
1759
1766
|
drainBuffered(method) {
|
|
1767
|
+
const buf = this.bufferedNotifications.get(method);
|
|
1768
|
+
const count = buf?.length ?? 0;
|
|
1760
1769
|
this.bufferedNotifications.delete(method);
|
|
1770
|
+
return count;
|
|
1761
1771
|
}
|
|
1762
1772
|
onClose(handler) {
|
|
1763
1773
|
this.closeHandlers.push(handler);
|
|
@@ -2018,14 +2028,30 @@ import { customAlphabet } from "nanoid";
|
|
|
2018
2028
|
|
|
2019
2029
|
// src/core/stream-buffer.ts
|
|
2020
2030
|
import * as fsp3 from "fs/promises";
|
|
2021
|
-
var DEFAULT_CAPACITY_BYTES =
|
|
2031
|
+
var DEFAULT_CAPACITY_BYTES = 64 * 1024 * 1024;
|
|
2032
|
+
var INITIAL_CAPACITY_BYTES = 1 * 1024 * 1024;
|
|
2022
2033
|
var STREAM_READ_MAX_BYTES = 64 * 1024;
|
|
2023
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
|
+
}
|
|
2024
2043
|
var SessionStreamBuffer = class {
|
|
2025
2044
|
storage;
|
|
2026
|
-
|
|
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;
|
|
2027
2053
|
// Absolute monotonic byte offset of the next byte to be written. Also
|
|
2028
|
-
// the count of bytes ever appended. `writeCursor -
|
|
2054
|
+
// the count of bytes ever appended. `writeCursor - currentCapacityBytes`
|
|
2029
2055
|
// (clamped at 0) is the oldest still-resident byte's cursor.
|
|
2030
2056
|
writeCursor = 0;
|
|
2031
2057
|
closed = false;
|
|
@@ -2040,24 +2066,33 @@ var SessionStreamBuffer = class {
|
|
|
2040
2066
|
// calls don't interleave their writes.
|
|
2041
2067
|
fileWriteChain = Promise.resolve();
|
|
2042
2068
|
constructor(opts = {}) {
|
|
2043
|
-
this.
|
|
2044
|
-
if (this.
|
|
2069
|
+
this.maxCapacityBytes = opts.capacityBytes ?? DEFAULT_CAPACITY_BYTES;
|
|
2070
|
+
if (this.maxCapacityBytes <= 0) {
|
|
2045
2071
|
throw new Error("capacityBytes must be > 0");
|
|
2046
2072
|
}
|
|
2047
|
-
this.
|
|
2073
|
+
this.currentCapacityBytes = Math.min(
|
|
2074
|
+
INITIAL_CAPACITY_BYTES,
|
|
2075
|
+
this.maxCapacityBytes
|
|
2076
|
+
);
|
|
2077
|
+
this.storage = Buffer.alloc(this.currentCapacityBytes);
|
|
2048
2078
|
this.filePath = opts.filePath;
|
|
2049
2079
|
this.fileCapBytes = opts.fileCapBytes ?? Number.POSITIVE_INFINITY;
|
|
2050
2080
|
this.onFileCapReached = opts.onFileCapReached;
|
|
2051
2081
|
this.logWriteError = opts.logWriteError;
|
|
2052
2082
|
}
|
|
2053
2083
|
get capacity() {
|
|
2054
|
-
return this.
|
|
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;
|
|
2055
2090
|
}
|
|
2056
2091
|
get writeCursorPos() {
|
|
2057
2092
|
return this.writeCursor;
|
|
2058
2093
|
}
|
|
2059
2094
|
get oldestAvailable() {
|
|
2060
|
-
return Math.max(0, this.writeCursor - this.
|
|
2095
|
+
return Math.max(0, this.writeCursor - this.currentCapacityBytes);
|
|
2061
2096
|
}
|
|
2062
2097
|
get isClosed() {
|
|
2063
2098
|
return this.closed;
|
|
@@ -2202,6 +2237,136 @@ var SessionStreamBuffer = class {
|
|
|
2202
2237
|
this.waiters.push(waiter);
|
|
2203
2238
|
});
|
|
2204
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
|
+
}
|
|
2205
2370
|
wakeWaiters(outcome) {
|
|
2206
2371
|
if (this.waiters.length === 0) {
|
|
2207
2372
|
return;
|
|
@@ -2212,15 +2377,42 @@ var SessionStreamBuffer = class {
|
|
|
2212
2377
|
w.resolve(outcome);
|
|
2213
2378
|
}
|
|
2214
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
|
+
}
|
|
2215
2406
|
writeRing(chunk) {
|
|
2216
2407
|
const len = chunk.length;
|
|
2217
|
-
|
|
2218
|
-
|
|
2408
|
+
this.growIfNeeded(len);
|
|
2409
|
+
if (len >= this.currentCapacityBytes) {
|
|
2410
|
+
const tailStart = len - this.currentCapacityBytes;
|
|
2219
2411
|
chunk.copy(this.storage, 0, tailStart, len);
|
|
2220
2412
|
return;
|
|
2221
2413
|
}
|
|
2222
|
-
const offset = this.writeCursor % this.
|
|
2223
|
-
const tailRoom = this.
|
|
2414
|
+
const offset = this.writeCursor % this.currentCapacityBytes;
|
|
2415
|
+
const tailRoom = this.currentCapacityBytes - offset;
|
|
2224
2416
|
if (len <= tailRoom) {
|
|
2225
2417
|
chunk.copy(this.storage, offset, 0, len);
|
|
2226
2418
|
} else {
|
|
@@ -2233,8 +2425,8 @@ var SessionStreamBuffer = class {
|
|
|
2233
2425
|
return Buffer.alloc(0);
|
|
2234
2426
|
}
|
|
2235
2427
|
const out = Buffer.alloc(length);
|
|
2236
|
-
const offset = fromCursor % this.
|
|
2237
|
-
const tailLen = Math.min(length, this.
|
|
2428
|
+
const offset = fromCursor % this.currentCapacityBytes;
|
|
2429
|
+
const tailLen = Math.min(length, this.currentCapacityBytes - offset);
|
|
2238
2430
|
this.storage.copy(out, 0, offset, offset + tailLen);
|
|
2239
2431
|
if (tailLen < length) {
|
|
2240
2432
|
this.storage.copy(out, tailLen, 0, length - tailLen);
|
|
@@ -2376,6 +2568,7 @@ var Session = class {
|
|
|
2376
2568
|
agentCapabilities;
|
|
2377
2569
|
agentArgs;
|
|
2378
2570
|
parentSessionId;
|
|
2571
|
+
originatingClient;
|
|
2379
2572
|
title;
|
|
2380
2573
|
// Snapshot state delivered to attaching clients via the attach
|
|
2381
2574
|
// response _meta rather than via history replay (which would be
|
|
@@ -2453,6 +2646,8 @@ var Session = class {
|
|
|
2453
2646
|
listSessions;
|
|
2454
2647
|
logger;
|
|
2455
2648
|
transformChain;
|
|
2649
|
+
extensionCommands;
|
|
2650
|
+
extensionCommandsUnsub;
|
|
2456
2651
|
// Outstanding "processing" claims: token → claim waiting for respondsTo discharge.
|
|
2457
2652
|
pendingClaims = /* @__PURE__ */ new Map();
|
|
2458
2653
|
agentChangeHandlers = [];
|
|
@@ -2528,6 +2723,7 @@ var Session = class {
|
|
|
2528
2723
|
this.agentCapabilities = init.agentCapabilities;
|
|
2529
2724
|
this.agentArgs = init.agentArgs;
|
|
2530
2725
|
this.parentSessionId = init.parentSessionId;
|
|
2726
|
+
this.originatingClient = init.originatingClient;
|
|
2531
2727
|
this.title = init.title;
|
|
2532
2728
|
this.currentModel = init.currentModel;
|
|
2533
2729
|
this.currentMode = init.currentMode;
|
|
@@ -2548,6 +2744,14 @@ var Session = class {
|
|
|
2548
2744
|
this.listSessions = init.listSessions;
|
|
2549
2745
|
this.logger = init.logger;
|
|
2550
2746
|
this.transformChain = init.transformChain ?? [];
|
|
2747
|
+
this.extensionCommands = init.extensionCommands;
|
|
2748
|
+
if (this.extensionCommands) {
|
|
2749
|
+
this.extensionCommandsUnsub = this.extensionCommands.onChange(() => {
|
|
2750
|
+
if (!this.closed) {
|
|
2751
|
+
this.broadcastMergedCommands();
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2551
2755
|
if (init.firstPromptSeeded) {
|
|
2552
2756
|
this.firstPromptSeeded = true;
|
|
2553
2757
|
}
|
|
@@ -2562,18 +2766,11 @@ var Session = class {
|
|
|
2562
2766
|
this.notifyChain("session.opened", {});
|
|
2563
2767
|
}
|
|
2564
2768
|
broadcastMergedCommands() {
|
|
2565
|
-
const merged = [
|
|
2566
|
-
...hydraCommandsAsAdvertised(),
|
|
2567
|
-
{ name: "model <model-id>", description: "Switch model; omit arg to list available models" },
|
|
2568
|
-
{ name: "sessions", description: "List all sessions" },
|
|
2569
|
-
{ name: "help", description: "Show available commands" },
|
|
2570
|
-
...this.agentAdvertisedCommands
|
|
2571
|
-
];
|
|
2572
2769
|
this.recordAndBroadcast("session/update", {
|
|
2573
2770
|
sessionId: this.upstreamSessionId,
|
|
2574
2771
|
update: {
|
|
2575
2772
|
sessionUpdate: "available_commands_update",
|
|
2576
|
-
availableCommands:
|
|
2773
|
+
availableCommands: this.mergedAvailableCommands()
|
|
2577
2774
|
}
|
|
2578
2775
|
});
|
|
2579
2776
|
}
|
|
@@ -3736,6 +3933,9 @@ var Session = class {
|
|
|
3736
3933
|
if (!trimmed || trimmed === this.currentModel) {
|
|
3737
3934
|
return true;
|
|
3738
3935
|
}
|
|
3936
|
+
this.logger?.info(
|
|
3937
|
+
`live current_model_update: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
3938
|
+
);
|
|
3739
3939
|
this.currentModel = trimmed;
|
|
3740
3940
|
for (const handler of this.modelHandlers) {
|
|
3741
3941
|
try {
|
|
@@ -3781,6 +3981,9 @@ var Session = class {
|
|
|
3781
3981
|
if (typeof cv === "string") {
|
|
3782
3982
|
const trimmed = cv.trim();
|
|
3783
3983
|
if (trimmed && trimmed !== this.currentModel) {
|
|
3984
|
+
this.logger?.info(
|
|
3985
|
+
`live config_option_update(model): sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
3986
|
+
);
|
|
3784
3987
|
this.currentModel = trimmed;
|
|
3785
3988
|
for (const handler of this.modelHandlers) {
|
|
3786
3989
|
try {
|
|
@@ -3949,6 +4152,9 @@ var Session = class {
|
|
|
3949
4152
|
this.broadcastAvailableModes();
|
|
3950
4153
|
}
|
|
3951
4154
|
setAgentAdvertisedModels(models) {
|
|
4155
|
+
this.logger?.info(
|
|
4156
|
+
`setAgentAdvertisedModels: sessionId=${this.sessionId} currentModel=${JSON.stringify(this.currentModel)} newList=[${models.map((m) => m.modelId).join(",")}]`
|
|
4157
|
+
);
|
|
3952
4158
|
if (sameAdvertisedModels(this.agentAdvertisedModels, models)) {
|
|
3953
4159
|
this.broadcastAvailableModels();
|
|
3954
4160
|
return;
|
|
@@ -3980,6 +4186,38 @@ var Session = class {
|
|
|
3980
4186
|
onModeChange(handler) {
|
|
3981
4187
|
this.modeHandlers.push(handler);
|
|
3982
4188
|
}
|
|
4189
|
+
// Apply a model change initiated by a client request (session/set_model)
|
|
4190
|
+
// when the agent doesn't emit a current_model_update notification, or
|
|
4191
|
+
// emits a non-spec shape (e.g. config_option_update). Fires modelHandlers
|
|
4192
|
+
// (persistence) and broadcasts a synthetic current_model_update so all
|
|
4193
|
+
// attached clients — including the originator — repaint immediately.
|
|
4194
|
+
applyModelChange(modelId) {
|
|
4195
|
+
const trimmed = modelId.trim();
|
|
4196
|
+
if (!trimmed || trimmed === this.currentModel) {
|
|
4197
|
+
return;
|
|
4198
|
+
}
|
|
4199
|
+
this.logger?.info(
|
|
4200
|
+
`applyModelChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4201
|
+
);
|
|
4202
|
+
this.currentModel = trimmed;
|
|
4203
|
+
for (const handler of this.modelHandlers) {
|
|
4204
|
+
try {
|
|
4205
|
+
handler(trimmed);
|
|
4206
|
+
} catch {
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
const update = {
|
|
4210
|
+
sessionUpdate: "current_model_update",
|
|
4211
|
+
currentModel: trimmed
|
|
4212
|
+
};
|
|
4213
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
4214
|
+
update.availableModels = [...this.agentAdvertisedModels];
|
|
4215
|
+
}
|
|
4216
|
+
this.recordAndBroadcast("session/update", {
|
|
4217
|
+
sessionId: this.upstreamSessionId,
|
|
4218
|
+
update
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
3983
4221
|
// Apply a mode change initiated by a client request (session/set_mode)
|
|
3984
4222
|
// when the agent doesn't emit a current_mode_update notification on its
|
|
3985
4223
|
// own. Fires modeHandlers so the persistence hook and any other listeners
|
|
@@ -4003,11 +4241,31 @@ var Session = class {
|
|
|
4003
4241
|
onUsageChange(handler) {
|
|
4004
4242
|
this.usageHandlers.push(handler);
|
|
4005
4243
|
}
|
|
4006
|
-
// Returns a freshly merged command list (hydra ∪ agent) for
|
|
4007
|
-
// that need a snapshot — notably acp-ws.ts's buildResponseMeta
|
|
4008
|
-
// assembling the attach response.
|
|
4244
|
+
// Returns a freshly merged command list (hydra ∪ extension ∪ agent) for
|
|
4245
|
+
// callers that need a snapshot — notably acp-ws.ts's buildResponseMeta
|
|
4246
|
+
// when assembling the attach response. Order: built-in hydra verbs,
|
|
4247
|
+
// top-level daemon verbs (/model, /sessions, /help), extension-registered
|
|
4248
|
+
// entries, then whatever the agent advertised.
|
|
4009
4249
|
mergedAvailableCommands() {
|
|
4010
|
-
|
|
4250
|
+
const out = [
|
|
4251
|
+
...hydraCommandsAsAdvertised(),
|
|
4252
|
+
{ name: "model <model-id>", description: "Switch model; omit arg to list available models" },
|
|
4253
|
+
{ name: "sessions", description: "List all sessions" },
|
|
4254
|
+
{ name: "help", description: "Show available commands" }
|
|
4255
|
+
];
|
|
4256
|
+
if (this.extensionCommands) {
|
|
4257
|
+
for (const { name, command } of this.extensionCommands.list()) {
|
|
4258
|
+
const head = `hydra ${name} ${command.verb}`;
|
|
4259
|
+
const display = command.argsHint ? `${head} ${command.argsHint}` : head;
|
|
4260
|
+
const entry = { name: display };
|
|
4261
|
+
if (command.description) {
|
|
4262
|
+
entry.description = command.description;
|
|
4263
|
+
}
|
|
4264
|
+
out.push(entry);
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
out.push(...this.agentAdvertisedCommands);
|
|
4268
|
+
return out;
|
|
4011
4269
|
}
|
|
4012
4270
|
// The agent's own advertised commands (not merged with hydra verbs).
|
|
4013
4271
|
// Used by SessionManager to persist into meta.json so cold resurrect
|
|
@@ -4059,39 +4317,118 @@ var Session = class {
|
|
|
4059
4317
|
// caller's promise resolves like a normal turn. To add a verb: append
|
|
4060
4318
|
// an entry to HYDRA_COMMANDS (drives validation + client advertising)
|
|
4061
4319
|
// and a dispatch case in the switch below.
|
|
4320
|
+
//
|
|
4321
|
+
// Extensions/transformers can also bind verbs via the
|
|
4322
|
+
// ExtensionCommandRegistry: "/hydra <process-name> <verb> [args]" routes
|
|
4323
|
+
// to that process's WS connection. Built-in hydra verbs win on name
|
|
4324
|
+
// collision so an extension can never shadow them.
|
|
4062
4325
|
async handleSlashCommand(text) {
|
|
4063
4326
|
const rest = text.slice("/hydra".length).trim();
|
|
4064
4327
|
const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
4065
|
-
const
|
|
4066
|
-
const
|
|
4067
|
-
if (
|
|
4328
|
+
const first = match?.[1] ?? "";
|
|
4329
|
+
const remainder = (match?.[2] ?? "").trim();
|
|
4330
|
+
if (first === "") {
|
|
4068
4331
|
return { stopReason: "end_turn" };
|
|
4069
4332
|
}
|
|
4070
|
-
if (
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4333
|
+
if (HYDRA_COMMANDS.some((c) => c.verb === first)) {
|
|
4334
|
+
switch (first) {
|
|
4335
|
+
case "title":
|
|
4336
|
+
return this.runTitleCommand(remainder);
|
|
4337
|
+
case "agent":
|
|
4338
|
+
return this.runAgentCommand(remainder);
|
|
4339
|
+
case "kill":
|
|
4340
|
+
return this.runKillCommand();
|
|
4341
|
+
case "restart":
|
|
4342
|
+
return this.runRestartCommand();
|
|
4343
|
+
default: {
|
|
4344
|
+
const err2 = new Error(
|
|
4345
|
+
`no dispatcher for /hydra verb ${first}`
|
|
4346
|
+
);
|
|
4347
|
+
err2.code = JsonRpcErrorCodes.InternalError;
|
|
4348
|
+
throw err2;
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4077
4351
|
}
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
`no dispatcher for /hydra verb ${verb}`
|
|
4090
|
-
);
|
|
4091
|
-
err.code = JsonRpcErrorCodes.InternalError;
|
|
4092
|
-
throw err;
|
|
4352
|
+
if (this.extensionCommands?.has(first)) {
|
|
4353
|
+
return this.runExtensionCommand(first, remainder);
|
|
4354
|
+
}
|
|
4355
|
+
const known = HYDRA_COMMANDS.map((c) => c.verb);
|
|
4356
|
+
if (this.extensionCommands) {
|
|
4357
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4358
|
+
for (const { name } of this.extensionCommands.list()) {
|
|
4359
|
+
if (!seen.has(name)) {
|
|
4360
|
+
known.push(name);
|
|
4361
|
+
seen.add(name);
|
|
4362
|
+
}
|
|
4093
4363
|
}
|
|
4094
4364
|
}
|
|
4365
|
+
const err = new Error(
|
|
4366
|
+
`unknown /hydra verb: ${first} (known: ${known.join(", ")})`
|
|
4367
|
+
);
|
|
4368
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
4369
|
+
throw err;
|
|
4370
|
+
}
|
|
4371
|
+
// "/hydra <name> <verb> [args]" — name matches a registered extension
|
|
4372
|
+
// or transformer. We split the remainder into verb + args, validate the
|
|
4373
|
+
// verb against what the process advertised, and forward as a
|
|
4374
|
+
// hydra-acp/extension_command request on the process's WS connection.
|
|
4375
|
+
// The reply's text (if any) is broadcast as a synthetic
|
|
4376
|
+
// agent_message_chunk so it appears in the conversation alongside the
|
|
4377
|
+
// user's invocation.
|
|
4378
|
+
runExtensionCommand(name, remainder) {
|
|
4379
|
+
return this.enqueuePrompt(async () => {
|
|
4380
|
+
const entry = this.extensionCommands?.get(name);
|
|
4381
|
+
if (!entry) {
|
|
4382
|
+
return this.emitExtensionReply(
|
|
4383
|
+
`extension "${name}" is no longer connected`
|
|
4384
|
+
);
|
|
4385
|
+
}
|
|
4386
|
+
const m = remainder.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
4387
|
+
const verb = m?.[1] ?? "";
|
|
4388
|
+
const args = (m?.[2] ?? "").trim();
|
|
4389
|
+
if (verb === "") {
|
|
4390
|
+
const verbs = entry.commands.map((c) => c.verb).join(", ");
|
|
4391
|
+
return this.emitExtensionReply(
|
|
4392
|
+
`/hydra ${name} requires a verb (known: ${verbs || "(none)"})`
|
|
4393
|
+
);
|
|
4394
|
+
}
|
|
4395
|
+
if (!entry.commands.some((c) => c.verb === verb)) {
|
|
4396
|
+
const verbs = entry.commands.map((c) => c.verb).join(", ");
|
|
4397
|
+
return this.emitExtensionReply(
|
|
4398
|
+
`unknown verb "${verb}" for ${name} (known: ${verbs || "(none)"})`
|
|
4399
|
+
);
|
|
4400
|
+
}
|
|
4401
|
+
let reply;
|
|
4402
|
+
try {
|
|
4403
|
+
reply = await entry.connection.request("hydra-acp/extension_command", {
|
|
4404
|
+
sessionId: this.sessionId,
|
|
4405
|
+
verb,
|
|
4406
|
+
args
|
|
4407
|
+
});
|
|
4408
|
+
} catch (err) {
|
|
4409
|
+
return this.emitExtensionReply(
|
|
4410
|
+
`${name} ${verb}: ${err.message}`
|
|
4411
|
+
);
|
|
4412
|
+
}
|
|
4413
|
+
const text = reply && typeof reply === "object" && typeof reply.text === "string" ? reply.text : "";
|
|
4414
|
+
if (text.length > 0) {
|
|
4415
|
+
return this.emitExtensionReply(text);
|
|
4416
|
+
}
|
|
4417
|
+
return { stopReason: "end_turn" };
|
|
4418
|
+
});
|
|
4419
|
+
}
|
|
4420
|
+
emitExtensionReply(text) {
|
|
4421
|
+
this.recordAndBroadcast("session/update", {
|
|
4422
|
+
sessionId: this.upstreamSessionId,
|
|
4423
|
+
update: {
|
|
4424
|
+
sessionUpdate: "agent_message_chunk",
|
|
4425
|
+
content: { type: "text", text: `
|
|
4426
|
+
${text}
|
|
4427
|
+
` },
|
|
4428
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
4429
|
+
}
|
|
4430
|
+
});
|
|
4431
|
+
return { stopReason: "end_turn" };
|
|
4095
4432
|
}
|
|
4096
4433
|
async handleSessionsCommand() {
|
|
4097
4434
|
let text;
|
|
@@ -4154,11 +4491,15 @@ ${text}
|
|
|
4154
4491
|
if (models.length === 0) {
|
|
4155
4492
|
body = current ? `Current model: ${current}` : "_(no models advertised yet)_";
|
|
4156
4493
|
} else {
|
|
4494
|
+
const inList = current ? models.some((m) => m.modelId === current) : true;
|
|
4157
4495
|
const lines = models.map((m) => {
|
|
4158
4496
|
const marker = m.modelId === current ? " \u25C0" : "";
|
|
4159
4497
|
const desc = m.name && m.name !== m.modelId ? ` ${m.name}` : "";
|
|
4160
4498
|
return `${m.modelId}${marker}${desc}`;
|
|
4161
4499
|
});
|
|
4500
|
+
if (!inList && current) {
|
|
4501
|
+
lines.unshift(`${current} \u25C0`);
|
|
4502
|
+
}
|
|
4162
4503
|
body = lines.join("\n");
|
|
4163
4504
|
}
|
|
4164
4505
|
this.recordAndBroadcast("session/update", {
|
|
@@ -4570,6 +4911,43 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
4570
4911
|
}
|
|
4571
4912
|
return out;
|
|
4572
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
|
+
}
|
|
4573
4951
|
requireStreamBuffer() {
|
|
4574
4952
|
if (this.streamBuffer === void 0) {
|
|
4575
4953
|
const err = new Error(
|
|
@@ -4586,6 +4964,10 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
4586
4964
|
}
|
|
4587
4965
|
this.closed = true;
|
|
4588
4966
|
this.cancelIdleTimer();
|
|
4967
|
+
if (this.extensionCommandsUnsub) {
|
|
4968
|
+
this.extensionCommandsUnsub();
|
|
4969
|
+
this.extensionCommandsUnsub = void 0;
|
|
4970
|
+
}
|
|
4589
4971
|
if (this.currentEntry?.kind === "user") {
|
|
4590
4972
|
this.broadcastTurnComplete(
|
|
4591
4973
|
this.currentEntry.clientId,
|
|
@@ -5325,6 +5707,10 @@ var PersistedUsage = z4.object({
|
|
|
5325
5707
|
costCurrency: z4.string().optional(),
|
|
5326
5708
|
cumulativeCost: z4.number().optional()
|
|
5327
5709
|
});
|
|
5710
|
+
var PersistedOriginatingClient = z4.object({
|
|
5711
|
+
name: z4.string(),
|
|
5712
|
+
version: z4.string().optional()
|
|
5713
|
+
});
|
|
5328
5714
|
var SessionRecord = z4.object({
|
|
5329
5715
|
version: z4.literal(1),
|
|
5330
5716
|
sessionId: z4.string(),
|
|
@@ -5375,6 +5761,10 @@ var SessionRecord = z4.object({
|
|
|
5375
5761
|
// Set when this session was spawned as a child by a transformer via
|
|
5376
5762
|
// hydra-acp/spawn_child_session. Points to the spawning session's id.
|
|
5377
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(),
|
|
5378
5768
|
createdAt: z4.string(),
|
|
5379
5769
|
updatedAt: z4.string()
|
|
5380
5770
|
});
|
|
@@ -5497,6 +5887,7 @@ function recordFromMemorySession(args) {
|
|
|
5497
5887
|
agentModels: args.agentModels,
|
|
5498
5888
|
pendingHistorySync: args.pendingHistorySync,
|
|
5499
5889
|
parentSessionId: args.parentSessionId,
|
|
5890
|
+
originatingClient: args.originatingClient,
|
|
5500
5891
|
createdAt: args.createdAt ?? now,
|
|
5501
5892
|
updatedAt: args.updatedAt ?? now
|
|
5502
5893
|
};
|
|
@@ -5711,6 +6102,7 @@ var SessionManager = class {
|
|
|
5711
6102
|
this.defaultTransformers = options.defaultTransformers ?? [];
|
|
5712
6103
|
this.logger = options.logger;
|
|
5713
6104
|
this.npmRegistry = options.npmRegistry;
|
|
6105
|
+
this.extensionCommands = options.extensionCommands;
|
|
5714
6106
|
}
|
|
5715
6107
|
registry;
|
|
5716
6108
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -5729,6 +6121,7 @@ var SessionManager = class {
|
|
|
5729
6121
|
metaWriteQueues = /* @__PURE__ */ new Map();
|
|
5730
6122
|
logger;
|
|
5731
6123
|
npmRegistry;
|
|
6124
|
+
extensionCommands;
|
|
5732
6125
|
async create(params) {
|
|
5733
6126
|
const fresh = await this.bootstrapAgent({
|
|
5734
6127
|
agentId: params.agentId,
|
|
@@ -5782,7 +6175,9 @@ var SessionManager = class {
|
|
|
5782
6175
|
agentModes: fresh.initialModes,
|
|
5783
6176
|
agentModels: fresh.initialModels,
|
|
5784
6177
|
transformChain: params.transformChain,
|
|
5785
|
-
parentSessionId: params.parentSessionId
|
|
6178
|
+
parentSessionId: params.parentSessionId,
|
|
6179
|
+
originatingClient: params.originatingClient,
|
|
6180
|
+
extensionCommands: this.extensionCommands
|
|
5786
6181
|
});
|
|
5787
6182
|
await this.attachManagerHooks(session);
|
|
5788
6183
|
return session;
|
|
@@ -5853,12 +6248,14 @@ var SessionManager = class {
|
|
|
5853
6248
|
}
|
|
5854
6249
|
let loadResult;
|
|
5855
6250
|
try {
|
|
6251
|
+
const loadMeta = buildSessionLoadMeta(params.agentId, params.currentModel);
|
|
5856
6252
|
loadResult = await agent.connection.request(
|
|
5857
6253
|
"session/load",
|
|
5858
6254
|
{
|
|
5859
6255
|
sessionId: params.upstreamSessionId,
|
|
5860
6256
|
cwd: params.cwd,
|
|
5861
|
-
mcpServers: []
|
|
6257
|
+
mcpServers: [],
|
|
6258
|
+
...loadMeta && { _meta: loadMeta }
|
|
5862
6259
|
}
|
|
5863
6260
|
);
|
|
5864
6261
|
} catch (err) {
|
|
@@ -5874,7 +6271,10 @@ var SessionManager = class {
|
|
|
5874
6271
|
() => void 0
|
|
5875
6272
|
);
|
|
5876
6273
|
} else {
|
|
5877
|
-
agent.connection.drainBuffered("session/update");
|
|
6274
|
+
const drain1Count = agent.connection.drainBuffered("session/update");
|
|
6275
|
+
this.logger?.info(
|
|
6276
|
+
`resurrect: drain1 dropped ${drain1Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
|
|
6277
|
+
);
|
|
5878
6278
|
}
|
|
5879
6279
|
const agentReportedMode = extractInitialCurrentMode(loadResult ?? {});
|
|
5880
6280
|
const advertisedModes = params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {}));
|
|
@@ -5892,6 +6292,30 @@ var SessionManager = class {
|
|
|
5892
6292
|
this.logger?.info(
|
|
5893
6293
|
`resurrect: effectiveMode=${JSON.stringify(effectiveMode)} for sessionId=${params.hydraSessionId}`
|
|
5894
6294
|
);
|
|
6295
|
+
const agentReportedModel = extractInitialModel(loadResult ?? {});
|
|
6296
|
+
const advertisedModels = nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})) ?? params.agentModels;
|
|
6297
|
+
this.logger?.info(
|
|
6298
|
+
`resurrect: sessionId=${params.hydraSessionId} persistedModel=${JSON.stringify(params.currentModel)} agentReportedModel=${JSON.stringify(agentReportedModel)} advertisedModels=${JSON.stringify(advertisedModels?.map((m) => m.modelId))}`
|
|
6299
|
+
);
|
|
6300
|
+
if (params.pendingHistorySync !== true) {
|
|
6301
|
+
const drain2Count = agent.connection.drainBuffered("session/update");
|
|
6302
|
+
this.logger?.info(
|
|
6303
|
+
`resurrect: drain2 (post-mode-restore) dropped ${drain2Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
|
|
6304
|
+
);
|
|
6305
|
+
}
|
|
6306
|
+
const effectiveModel = await restoreCurrentModel({
|
|
6307
|
+
agent,
|
|
6308
|
+
upstreamSessionId: params.upstreamSessionId,
|
|
6309
|
+
persistedModel: params.currentModel,
|
|
6310
|
+
agentReportedModel,
|
|
6311
|
+
logger: this.logger
|
|
6312
|
+
});
|
|
6313
|
+
if (params.pendingHistorySync !== true) {
|
|
6314
|
+
const drain3Count = agent.connection.drainBuffered("session/update");
|
|
6315
|
+
this.logger?.info(
|
|
6316
|
+
`resurrect: drain3 (post-model-restore) dropped ${drain3Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
|
|
6317
|
+
);
|
|
6318
|
+
}
|
|
5895
6319
|
const session = new Session({
|
|
5896
6320
|
sessionId: params.hydraSessionId,
|
|
5897
6321
|
cwd: params.cwd,
|
|
@@ -5908,11 +6332,7 @@ var SessionManager = class {
|
|
|
5908
6332
|
listSessions: () => this.list(),
|
|
5909
6333
|
historyStore: this.histories,
|
|
5910
6334
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
5911
|
-
|
|
5912
|
-
// we never captured one (e.g. old opencode sessions on disk before
|
|
5913
|
-
// this fix), fall back to the model the agent ships in its
|
|
5914
|
-
// session/load response body.
|
|
5915
|
-
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
6335
|
+
currentModel: effectiveModel,
|
|
5916
6336
|
currentMode: effectiveMode,
|
|
5917
6337
|
currentUsage: params.currentUsage,
|
|
5918
6338
|
agentCommands: params.agentCommands,
|
|
@@ -5921,13 +6341,15 @@ var SessionManager = class {
|
|
|
5921
6341
|
// snapshot — the proxy's available models can change between daemon
|
|
5922
6342
|
// restarts (quota resets, rollouts), so meta.json is intentionally
|
|
5923
6343
|
// treated as a cold fallback here, not the authoritative source.
|
|
5924
|
-
agentModels:
|
|
6344
|
+
agentModels: advertisedModels,
|
|
5925
6345
|
// Only gate the first-prompt title heuristic when we actually have
|
|
5926
6346
|
// a title to preserve. A title-less session (lost to a write race
|
|
5927
6347
|
// or never seeded) should re-derive from the next prompt rather
|
|
5928
6348
|
// than stay stuck.
|
|
5929
6349
|
firstPromptSeeded: !!params.title,
|
|
5930
|
-
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
6350
|
+
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
6351
|
+
originatingClient: params.originatingClient,
|
|
6352
|
+
extensionCommands: this.extensionCommands
|
|
5931
6353
|
});
|
|
5932
6354
|
await this.attachManagerHooks(session);
|
|
5933
6355
|
return session;
|
|
@@ -5946,7 +6368,11 @@ var SessionManager = class {
|
|
|
5946
6368
|
cwd,
|
|
5947
6369
|
agentArgs: params.agentArgs,
|
|
5948
6370
|
mcpServers: [],
|
|
5949
|
-
onInstallProgress: params.onInstallProgress
|
|
6371
|
+
onInstallProgress: params.onInstallProgress,
|
|
6372
|
+
// Pass the persisted model so bootstrapAgent calls session/set_model
|
|
6373
|
+
// during session/new — the only context where the agent reliably
|
|
6374
|
+
// honours the switch.
|
|
6375
|
+
model: params.currentModel
|
|
5950
6376
|
});
|
|
5951
6377
|
const advertisedModes = params.agentModes ?? fresh.initialModes;
|
|
5952
6378
|
const effectiveMode = await restoreCurrentMode({
|
|
@@ -5957,6 +6383,15 @@ var SessionManager = class {
|
|
|
5957
6383
|
advertisedModes,
|
|
5958
6384
|
logger: this.logger
|
|
5959
6385
|
});
|
|
6386
|
+
const advertisedModels = params.agentModels ?? fresh.initialModels;
|
|
6387
|
+
const effectiveModel = await restoreCurrentModel({
|
|
6388
|
+
agent: fresh.agent,
|
|
6389
|
+
upstreamSessionId: fresh.upstreamSessionId,
|
|
6390
|
+
persistedModel: params.currentModel,
|
|
6391
|
+
agentReportedModel: fresh.initialModel,
|
|
6392
|
+
logger: this.logger
|
|
6393
|
+
});
|
|
6394
|
+
fresh.agent.connection.drainBuffered("session/update");
|
|
5960
6395
|
const session = new Session({
|
|
5961
6396
|
sessionId: params.hydraSessionId,
|
|
5962
6397
|
cwd,
|
|
@@ -5973,16 +6408,16 @@ var SessionManager = class {
|
|
|
5973
6408
|
listSessions: () => this.list(),
|
|
5974
6409
|
historyStore: this.histories,
|
|
5975
6410
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
5976
|
-
|
|
5977
|
-
// fall back to whatever the agent ships in its session/new response.
|
|
5978
|
-
currentModel: params.currentModel ?? fresh.initialModel,
|
|
6411
|
+
currentModel: effectiveModel,
|
|
5979
6412
|
currentMode: effectiveMode,
|
|
5980
6413
|
currentUsage: params.currentUsage,
|
|
5981
6414
|
agentCommands: params.agentCommands,
|
|
5982
6415
|
agentModes: advertisedModes,
|
|
5983
|
-
agentModels:
|
|
6416
|
+
agentModels: advertisedModels,
|
|
5984
6417
|
firstPromptSeeded: !!params.title,
|
|
5985
|
-
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
6418
|
+
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
6419
|
+
originatingClient: params.originatingClient,
|
|
6420
|
+
extensionCommands: this.extensionCommands
|
|
5986
6421
|
});
|
|
5987
6422
|
await this.attachManagerHooks(session);
|
|
5988
6423
|
void session.seedFromImport().catch(() => void 0);
|
|
@@ -6331,7 +6766,8 @@ var SessionManager = class {
|
|
|
6331
6766
|
agentModes: record.agentModes,
|
|
6332
6767
|
agentModels: record.agentModels,
|
|
6333
6768
|
createdAt: record.createdAt,
|
|
6334
|
-
pendingHistorySync: record.pendingHistorySync
|
|
6769
|
+
pendingHistorySync: record.pendingHistorySync,
|
|
6770
|
+
originatingClient: record.originatingClient
|
|
6335
6771
|
};
|
|
6336
6772
|
}
|
|
6337
6773
|
async clearPendingHistorySync(sessionId) {
|
|
@@ -6432,6 +6868,7 @@ var SessionManager = class {
|
|
|
6432
6868
|
currentModel: session.currentModel,
|
|
6433
6869
|
currentUsage: session.currentUsage,
|
|
6434
6870
|
parentSessionId: session.parentSessionId,
|
|
6871
|
+
originatingClient: session.originatingClient,
|
|
6435
6872
|
updatedAt: used,
|
|
6436
6873
|
attachedClients: session.attachedCount,
|
|
6437
6874
|
status: "live",
|
|
@@ -6461,6 +6898,7 @@ var SessionManager = class {
|
|
|
6461
6898
|
importedFromMachine: r.importedFromMachine,
|
|
6462
6899
|
importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
|
|
6463
6900
|
parentSessionId: r.parentSessionId,
|
|
6901
|
+
originatingClient: r.originatingClient,
|
|
6464
6902
|
updatedAt: used,
|
|
6465
6903
|
attachedClients: 0,
|
|
6466
6904
|
status: "cold",
|
|
@@ -6806,6 +7244,7 @@ function mergeForPersistence(session, existing) {
|
|
|
6806
7244
|
agentModes,
|
|
6807
7245
|
agentModels,
|
|
6808
7246
|
parentSessionId: session.parentSessionId ?? existing?.parentSessionId,
|
|
7247
|
+
originatingClient: session.originatingClient ?? existing?.originatingClient,
|
|
6809
7248
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
6810
7249
|
});
|
|
6811
7250
|
}
|
|
@@ -6834,6 +7273,13 @@ function usageSnapshotToPersisted(usage) {
|
|
|
6834
7273
|
function persistedUsageToSnapshot(usage) {
|
|
6835
7274
|
return usage ? { ...usage } : void 0;
|
|
6836
7275
|
}
|
|
7276
|
+
function buildSessionLoadMeta(agentId, model) {
|
|
7277
|
+
if (!model)
|
|
7278
|
+
return void 0;
|
|
7279
|
+
if (agentId === "claude-acp")
|
|
7280
|
+
return { claudeCode: { options: { model } } };
|
|
7281
|
+
return void 0;
|
|
7282
|
+
}
|
|
6837
7283
|
function extractInitialModel(result) {
|
|
6838
7284
|
const direct = asString(result.currentModelId) ?? asString(result.currentModel) ?? asString(result.modelId) ?? asString(result.model);
|
|
6839
7285
|
if (direct) {
|
|
@@ -7013,6 +7459,33 @@ async function restoreCurrentMode(opts) {
|
|
|
7013
7459
|
return agentReportedMode;
|
|
7014
7460
|
}
|
|
7015
7461
|
}
|
|
7462
|
+
async function restoreCurrentModel(opts) {
|
|
7463
|
+
const { agent, upstreamSessionId, persistedModel, agentReportedModel, logger } = opts;
|
|
7464
|
+
if (!persistedModel) {
|
|
7465
|
+
return agentReportedModel;
|
|
7466
|
+
}
|
|
7467
|
+
if (persistedModel === agentReportedModel) {
|
|
7468
|
+
return persistedModel;
|
|
7469
|
+
}
|
|
7470
|
+
try {
|
|
7471
|
+
logger?.info(
|
|
7472
|
+
`resurrect: pushing persisted modelId=${JSON.stringify(persistedModel)} to agent (agentReported=${JSON.stringify(agentReportedModel)})`
|
|
7473
|
+
);
|
|
7474
|
+
await agent.connection.request("session/set_model", {
|
|
7475
|
+
sessionId: upstreamSessionId,
|
|
7476
|
+
modelId: persistedModel
|
|
7477
|
+
});
|
|
7478
|
+
logger?.info(
|
|
7479
|
+
`resurrect: session/set_model accepted, effectiveModel=${JSON.stringify(persistedModel)}`
|
|
7480
|
+
);
|
|
7481
|
+
return persistedModel;
|
|
7482
|
+
} catch (err) {
|
|
7483
|
+
logger?.warn(
|
|
7484
|
+
`resurrect: session/set_model rejected by agent for modelId=${JSON.stringify(persistedModel)} (${err.message}); session will use ${JSON.stringify(agentReportedModel)}`
|
|
7485
|
+
);
|
|
7486
|
+
return agentReportedModel;
|
|
7487
|
+
}
|
|
7488
|
+
}
|
|
7016
7489
|
function parseModesList(list) {
|
|
7017
7490
|
if (!Array.isArray(list)) {
|
|
7018
7491
|
return [];
|
|
@@ -7966,6 +8439,55 @@ function withCode3(err, code) {
|
|
|
7966
8439
|
return err;
|
|
7967
8440
|
}
|
|
7968
8441
|
|
|
8442
|
+
// src/core/extension-commands.ts
|
|
8443
|
+
var ExtensionCommandRegistry = class {
|
|
8444
|
+
entries = /* @__PURE__ */ new Map();
|
|
8445
|
+
changeHandlers = [];
|
|
8446
|
+
register(name, connection, commands) {
|
|
8447
|
+
this.entries.set(name, { connection, commands: [...commands] });
|
|
8448
|
+
this.fireChanged();
|
|
8449
|
+
}
|
|
8450
|
+
clear(name) {
|
|
8451
|
+
if (this.entries.delete(name)) {
|
|
8452
|
+
this.fireChanged();
|
|
8453
|
+
}
|
|
8454
|
+
}
|
|
8455
|
+
get(name) {
|
|
8456
|
+
return this.entries.get(name);
|
|
8457
|
+
}
|
|
8458
|
+
has(name) {
|
|
8459
|
+
return this.entries.has(name);
|
|
8460
|
+
}
|
|
8461
|
+
// Snapshot of every (name, command) pair. Order is stable per-name
|
|
8462
|
+
// (insertion order of the map and the original commands list).
|
|
8463
|
+
list() {
|
|
8464
|
+
const out = [];
|
|
8465
|
+
for (const [name, entry] of this.entries) {
|
|
8466
|
+
for (const command of entry.commands) {
|
|
8467
|
+
out.push({ name, command });
|
|
8468
|
+
}
|
|
8469
|
+
}
|
|
8470
|
+
return out;
|
|
8471
|
+
}
|
|
8472
|
+
onChange(handler) {
|
|
8473
|
+
this.changeHandlers.push(handler);
|
|
8474
|
+
return () => {
|
|
8475
|
+
const i = this.changeHandlers.indexOf(handler);
|
|
8476
|
+
if (i >= 0) {
|
|
8477
|
+
this.changeHandlers.splice(i, 1);
|
|
8478
|
+
}
|
|
8479
|
+
};
|
|
8480
|
+
}
|
|
8481
|
+
fireChanged() {
|
|
8482
|
+
for (const h of this.changeHandlers) {
|
|
8483
|
+
try {
|
|
8484
|
+
h();
|
|
8485
|
+
} catch {
|
|
8486
|
+
}
|
|
8487
|
+
}
|
|
8488
|
+
}
|
|
8489
|
+
};
|
|
8490
|
+
|
|
7969
8491
|
// src/core/agent-prune.ts
|
|
7970
8492
|
import * as fsp7 from "fs/promises";
|
|
7971
8493
|
import * as path9 from "path";
|
|
@@ -9131,6 +9653,376 @@ function isLoopbackHost(host) {
|
|
|
9131
9653
|
return host === "127.0.0.1" || host === "::1" || host === "localhost" || host === "[::1]";
|
|
9132
9654
|
}
|
|
9133
9655
|
|
|
9656
|
+
// src/core/history-search.ts
|
|
9657
|
+
function parseQuery(raw) {
|
|
9658
|
+
const trimmed = raw.trim();
|
|
9659
|
+
if (trimmed.length === 0) {
|
|
9660
|
+
return { operator: "OR", terms: [] };
|
|
9661
|
+
}
|
|
9662
|
+
const tokenRe = /\w+:"[^"]*"|"[^"]*"|\S+/g;
|
|
9663
|
+
const tokens = [];
|
|
9664
|
+
let m;
|
|
9665
|
+
while ((m = tokenRe.exec(trimmed)) !== null) {
|
|
9666
|
+
tokens.push(m[0]);
|
|
9667
|
+
}
|
|
9668
|
+
let operator = "OR";
|
|
9669
|
+
let sawAnd = false;
|
|
9670
|
+
let sawOr = false;
|
|
9671
|
+
const termTokens = [];
|
|
9672
|
+
for (const tok of tokens) {
|
|
9673
|
+
const upper = tok.toUpperCase();
|
|
9674
|
+
if (upper === "AND") {
|
|
9675
|
+
sawAnd = true;
|
|
9676
|
+
} else if (upper === "OR") {
|
|
9677
|
+
sawOr = true;
|
|
9678
|
+
} else {
|
|
9679
|
+
termTokens.push(tok);
|
|
9680
|
+
}
|
|
9681
|
+
}
|
|
9682
|
+
if (sawAnd) {
|
|
9683
|
+
operator = "AND";
|
|
9684
|
+
} else if (sawOr) {
|
|
9685
|
+
operator = "OR";
|
|
9686
|
+
}
|
|
9687
|
+
const terms = termTokens.map((tok) => parseTermToken(tok)).filter((t) => t.term.length > 0);
|
|
9688
|
+
return { operator, terms };
|
|
9689
|
+
}
|
|
9690
|
+
function parseTermToken(tok) {
|
|
9691
|
+
const pq = /^(\w+):"([^"]*)"$/.exec(tok);
|
|
9692
|
+
if (pq) {
|
|
9693
|
+
return { scope: prefixToScope(pq[1]), term: pq[2] };
|
|
9694
|
+
}
|
|
9695
|
+
const q = /^"([^"]*)"$/.exec(tok);
|
|
9696
|
+
if (q) {
|
|
9697
|
+
return { scope: "all", term: q[1] };
|
|
9698
|
+
}
|
|
9699
|
+
const pb = /^(prompt|response|tool):([\s\S]*)$/i.exec(tok);
|
|
9700
|
+
if (pb) {
|
|
9701
|
+
return { scope: prefixToScope(pb[1]), term: pb[2].trim() };
|
|
9702
|
+
}
|
|
9703
|
+
return { scope: "all", term: tok.trim() };
|
|
9704
|
+
}
|
|
9705
|
+
function prefixToScope(prefix) {
|
|
9706
|
+
switch (prefix.toLowerCase()) {
|
|
9707
|
+
case "prompt":
|
|
9708
|
+
return "user";
|
|
9709
|
+
case "response":
|
|
9710
|
+
return "agent";
|
|
9711
|
+
case "tool":
|
|
9712
|
+
return "tool";
|
|
9713
|
+
default:
|
|
9714
|
+
return "all";
|
|
9715
|
+
}
|
|
9716
|
+
}
|
|
9717
|
+
function scopeMatchesKind(scope, kind) {
|
|
9718
|
+
if (scope === "all") {
|
|
9719
|
+
return true;
|
|
9720
|
+
}
|
|
9721
|
+
if (scope === "user") {
|
|
9722
|
+
return kind === "user";
|
|
9723
|
+
}
|
|
9724
|
+
if (scope === "agent") {
|
|
9725
|
+
return kind === "agent" || kind === "thought";
|
|
9726
|
+
}
|
|
9727
|
+
return kind === "tool" || kind === "tool-input";
|
|
9728
|
+
}
|
|
9729
|
+
var DEFAULT_MAX_SNIPPETS_PER_SESSION = 5;
|
|
9730
|
+
var DEFAULT_MAX_SESSIONS = 200;
|
|
9731
|
+
var SNIPPET_SIDE = 30;
|
|
9732
|
+
async function searchHistories(manager, query, opts = {}) {
|
|
9733
|
+
const parsed = parseQuery(query);
|
|
9734
|
+
if (parsed.terms.length === 0) {
|
|
9735
|
+
return { query, truncated: false, results: [] };
|
|
9736
|
+
}
|
|
9737
|
+
const maxPerSession = opts.maxSnippetsPerSession ?? DEFAULT_MAX_SNIPPETS_PER_SESSION;
|
|
9738
|
+
const maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
9739
|
+
const allow = opts.sessionIds ? new Set(opts.sessionIds) : null;
|
|
9740
|
+
const all = await manager.list();
|
|
9741
|
+
const candidates = allow ? all.filter((s) => allow.has(s.sessionId)) : all;
|
|
9742
|
+
const results = [];
|
|
9743
|
+
let truncated = false;
|
|
9744
|
+
for (const candidate of candidates) {
|
|
9745
|
+
if (results.length >= maxSessions) {
|
|
9746
|
+
truncated = true;
|
|
9747
|
+
break;
|
|
9748
|
+
}
|
|
9749
|
+
const entries = await manager.loadHistory(candidate.sessionId).catch(
|
|
9750
|
+
() => []
|
|
9751
|
+
);
|
|
9752
|
+
const found = scanSessionEntries(entries, parsed, maxPerSession);
|
|
9753
|
+
if (found.snippets.length === 0) {
|
|
9754
|
+
continue;
|
|
9755
|
+
}
|
|
9756
|
+
const hit = {
|
|
9757
|
+
sessionId: candidate.sessionId,
|
|
9758
|
+
cwd: candidate.cwd,
|
|
9759
|
+
status: candidate.status,
|
|
9760
|
+
updatedAt: candidate.updatedAt,
|
|
9761
|
+
totalMatches: found.totalMatches,
|
|
9762
|
+
snippets: found.snippets
|
|
9763
|
+
};
|
|
9764
|
+
if (candidate.title !== void 0) {
|
|
9765
|
+
hit.title = candidate.title;
|
|
9766
|
+
}
|
|
9767
|
+
results.push(hit);
|
|
9768
|
+
}
|
|
9769
|
+
return { query, truncated, results };
|
|
9770
|
+
}
|
|
9771
|
+
function scanSessionEntries(entries, query, maxSnippets) {
|
|
9772
|
+
if (query.terms.length === 0) {
|
|
9773
|
+
return { totalMatches: 0, snippets: [] };
|
|
9774
|
+
}
|
|
9775
|
+
let totalMatches = 0;
|
|
9776
|
+
const snippets = [];
|
|
9777
|
+
for (const { scope, term } of query.terms) {
|
|
9778
|
+
const result = scanForTerm(entries, term, scope, maxSnippets - snippets.length);
|
|
9779
|
+
if (query.operator === "AND" && result.totalMatches === 0) {
|
|
9780
|
+
return { totalMatches: 0, snippets: [] };
|
|
9781
|
+
}
|
|
9782
|
+
totalMatches += result.totalMatches;
|
|
9783
|
+
snippets.push(...result.snippets);
|
|
9784
|
+
}
|
|
9785
|
+
return { totalMatches, snippets };
|
|
9786
|
+
}
|
|
9787
|
+
function scanForTerm(entries, term, scope, snippetBudget) {
|
|
9788
|
+
const needle = term.toLowerCase();
|
|
9789
|
+
let totalMatches = 0;
|
|
9790
|
+
const snippets = [];
|
|
9791
|
+
for (const entry of entries) {
|
|
9792
|
+
const fragments = extractSearchableFragments(entry).filter(
|
|
9793
|
+
(f) => scopeMatchesKind(scope, f.kind)
|
|
9794
|
+
);
|
|
9795
|
+
for (const frag of fragments) {
|
|
9796
|
+
const hay = frag.text.toLowerCase();
|
|
9797
|
+
let idx = hay.indexOf(needle);
|
|
9798
|
+
if (idx === -1) {
|
|
9799
|
+
continue;
|
|
9800
|
+
}
|
|
9801
|
+
let occurrences = 0;
|
|
9802
|
+
while (idx !== -1) {
|
|
9803
|
+
occurrences++;
|
|
9804
|
+
idx = hay.indexOf(needle, idx + needle.length);
|
|
9805
|
+
}
|
|
9806
|
+
totalMatches += occurrences;
|
|
9807
|
+
if (snippets.length < snippetBudget) {
|
|
9808
|
+
const first = hay.indexOf(needle);
|
|
9809
|
+
const snippet = {
|
|
9810
|
+
kind: frag.kind,
|
|
9811
|
+
text: buildSnippet(frag.text, first, needle.length),
|
|
9812
|
+
recordedAt: entry.recordedAt
|
|
9813
|
+
};
|
|
9814
|
+
if (frag.toolName !== void 0) {
|
|
9815
|
+
snippet.toolName = frag.toolName;
|
|
9816
|
+
}
|
|
9817
|
+
snippets.push(snippet);
|
|
9818
|
+
}
|
|
9819
|
+
}
|
|
9820
|
+
}
|
|
9821
|
+
return { totalMatches, snippets };
|
|
9822
|
+
}
|
|
9823
|
+
function extractSearchableFragments(entry) {
|
|
9824
|
+
if (entry.method !== "session/update") {
|
|
9825
|
+
return [];
|
|
9826
|
+
}
|
|
9827
|
+
const params = entry.params;
|
|
9828
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
9829
|
+
return [];
|
|
9830
|
+
}
|
|
9831
|
+
const update = params.update;
|
|
9832
|
+
if (!update || typeof update !== "object" || Array.isArray(update)) {
|
|
9833
|
+
return [];
|
|
9834
|
+
}
|
|
9835
|
+
const u = update;
|
|
9836
|
+
const tag = typeof u.sessionUpdate === "string" ? u.sessionUpdate : u.kind;
|
|
9837
|
+
if (typeof tag !== "string") {
|
|
9838
|
+
return [];
|
|
9839
|
+
}
|
|
9840
|
+
switch (tag) {
|
|
9841
|
+
case "agent_message_chunk": {
|
|
9842
|
+
const text = readContentText(u.content);
|
|
9843
|
+
return text ? [{ kind: "agent", text }] : [];
|
|
9844
|
+
}
|
|
9845
|
+
case "agent_thought":
|
|
9846
|
+
case "agent_thought_chunk": {
|
|
9847
|
+
const text = typeof u.text === "string" ? sanitizeWireText(u.text) : readContentText(u.content);
|
|
9848
|
+
return text ? [{ kind: "thought", text }] : [];
|
|
9849
|
+
}
|
|
9850
|
+
case "user_message_chunk": {
|
|
9851
|
+
if (isCompatPromptReceived(u)) {
|
|
9852
|
+
return [];
|
|
9853
|
+
}
|
|
9854
|
+
const text = readContentText(u.content);
|
|
9855
|
+
return text ? [{ kind: "user", text }] : [];
|
|
9856
|
+
}
|
|
9857
|
+
case "prompt_received": {
|
|
9858
|
+
const text = readPromptText(u.prompt);
|
|
9859
|
+
return text ? [{ kind: "user", text }] : [];
|
|
9860
|
+
}
|
|
9861
|
+
case "tool_call":
|
|
9862
|
+
case "tool_call_update": {
|
|
9863
|
+
return extractToolFragments(u);
|
|
9864
|
+
}
|
|
9865
|
+
default:
|
|
9866
|
+
return [];
|
|
9867
|
+
}
|
|
9868
|
+
}
|
|
9869
|
+
function extractToolFragments(u) {
|
|
9870
|
+
const toolName = readString2(u, "name");
|
|
9871
|
+
const title = readString2(u, "title");
|
|
9872
|
+
const out = [];
|
|
9873
|
+
if (title !== void 0) {
|
|
9874
|
+
const sanitized = sanitizeSingleLine(title);
|
|
9875
|
+
if (sanitized.length > 0) {
|
|
9876
|
+
const frag = { kind: "tool", text: sanitized };
|
|
9877
|
+
if (toolName !== void 0) {
|
|
9878
|
+
frag.toolName = toolName;
|
|
9879
|
+
}
|
|
9880
|
+
out.push(frag);
|
|
9881
|
+
}
|
|
9882
|
+
}
|
|
9883
|
+
if (toolName !== void 0 && toolName !== title) {
|
|
9884
|
+
const sanitized = sanitizeSingleLine(toolName);
|
|
9885
|
+
if (sanitized.length > 0) {
|
|
9886
|
+
out.push({ kind: "tool", toolName, text: sanitized });
|
|
9887
|
+
}
|
|
9888
|
+
}
|
|
9889
|
+
const rawInput = u.rawInput;
|
|
9890
|
+
if (rawInput && typeof rawInput === "object") {
|
|
9891
|
+
const serialized = safeStringify(rawInput);
|
|
9892
|
+
if (serialized.length > 0) {
|
|
9893
|
+
const frag = {
|
|
9894
|
+
kind: "tool-input",
|
|
9895
|
+
text: sanitizeSingleLine(serialized)
|
|
9896
|
+
};
|
|
9897
|
+
if (toolName !== void 0) {
|
|
9898
|
+
frag.toolName = toolName;
|
|
9899
|
+
}
|
|
9900
|
+
out.push(frag);
|
|
9901
|
+
}
|
|
9902
|
+
}
|
|
9903
|
+
const locations = u.locations;
|
|
9904
|
+
if (Array.isArray(locations) && locations.length > 0) {
|
|
9905
|
+
const serialized = safeStringify(locations);
|
|
9906
|
+
if (serialized.length > 0) {
|
|
9907
|
+
const frag = {
|
|
9908
|
+
kind: "tool-input",
|
|
9909
|
+
text: sanitizeSingleLine(serialized)
|
|
9910
|
+
};
|
|
9911
|
+
if (toolName !== void 0) {
|
|
9912
|
+
frag.toolName = toolName;
|
|
9913
|
+
}
|
|
9914
|
+
out.push(frag);
|
|
9915
|
+
}
|
|
9916
|
+
}
|
|
9917
|
+
const errorText = extractToolErrorText(u);
|
|
9918
|
+
if (errorText !== null) {
|
|
9919
|
+
const frag = { kind: "tool", text: errorText };
|
|
9920
|
+
if (toolName !== void 0) {
|
|
9921
|
+
frag.toolName = toolName;
|
|
9922
|
+
}
|
|
9923
|
+
out.push(frag);
|
|
9924
|
+
}
|
|
9925
|
+
return out;
|
|
9926
|
+
}
|
|
9927
|
+
function extractToolErrorText(u) {
|
|
9928
|
+
const content = u.content;
|
|
9929
|
+
if (Array.isArray(content)) {
|
|
9930
|
+
for (const block of content) {
|
|
9931
|
+
if (!block || typeof block !== "object") {
|
|
9932
|
+
continue;
|
|
9933
|
+
}
|
|
9934
|
+
const b = block;
|
|
9935
|
+
const inner = b.content;
|
|
9936
|
+
if (!inner || typeof inner !== "object") {
|
|
9937
|
+
continue;
|
|
9938
|
+
}
|
|
9939
|
+
const i = inner;
|
|
9940
|
+
if (i.type === "text" && typeof i.text === "string") {
|
|
9941
|
+
const s = sanitizeSingleLine(i.text);
|
|
9942
|
+
if (s.length > 0) {
|
|
9943
|
+
return s;
|
|
9944
|
+
}
|
|
9945
|
+
}
|
|
9946
|
+
}
|
|
9947
|
+
}
|
|
9948
|
+
const rawOutput = u.rawOutput;
|
|
9949
|
+
if (rawOutput && typeof rawOutput === "object") {
|
|
9950
|
+
const err = rawOutput.error;
|
|
9951
|
+
if (typeof err === "string") {
|
|
9952
|
+
const s = sanitizeSingleLine(err);
|
|
9953
|
+
if (s.length > 0) {
|
|
9954
|
+
return s;
|
|
9955
|
+
}
|
|
9956
|
+
}
|
|
9957
|
+
}
|
|
9958
|
+
return null;
|
|
9959
|
+
}
|
|
9960
|
+
function isCompatPromptReceived(u) {
|
|
9961
|
+
const meta = u._meta;
|
|
9962
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
9963
|
+
return false;
|
|
9964
|
+
}
|
|
9965
|
+
const hydra = meta["hydra-acp"];
|
|
9966
|
+
if (!hydra || typeof hydra !== "object" || Array.isArray(hydra)) {
|
|
9967
|
+
return false;
|
|
9968
|
+
}
|
|
9969
|
+
return hydra.compatFor === "prompt_received";
|
|
9970
|
+
}
|
|
9971
|
+
function readContentText(content) {
|
|
9972
|
+
if (typeof content === "string") {
|
|
9973
|
+
return sanitizeWireText(content);
|
|
9974
|
+
}
|
|
9975
|
+
if (!content || typeof content !== "object" || Array.isArray(content)) {
|
|
9976
|
+
return "";
|
|
9977
|
+
}
|
|
9978
|
+
const c = content;
|
|
9979
|
+
if (typeof c.text === "string") {
|
|
9980
|
+
return sanitizeWireText(c.text);
|
|
9981
|
+
}
|
|
9982
|
+
return "";
|
|
9983
|
+
}
|
|
9984
|
+
function readPromptText(prompt) {
|
|
9985
|
+
if (!Array.isArray(prompt)) {
|
|
9986
|
+
return "";
|
|
9987
|
+
}
|
|
9988
|
+
const parts = [];
|
|
9989
|
+
for (const block of prompt) {
|
|
9990
|
+
const text = readContentText(block);
|
|
9991
|
+
if (text.length > 0) {
|
|
9992
|
+
parts.push(text);
|
|
9993
|
+
}
|
|
9994
|
+
}
|
|
9995
|
+
return parts.join("");
|
|
9996
|
+
}
|
|
9997
|
+
function readString2(u, key) {
|
|
9998
|
+
const v = u[key];
|
|
9999
|
+
return typeof v === "string" ? v : void 0;
|
|
10000
|
+
}
|
|
10001
|
+
function safeStringify(value) {
|
|
10002
|
+
try {
|
|
10003
|
+
return JSON.stringify(value);
|
|
10004
|
+
} catch {
|
|
10005
|
+
return "";
|
|
10006
|
+
}
|
|
10007
|
+
}
|
|
10008
|
+
function buildSnippet(text, matchIdx, matchLen) {
|
|
10009
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
10010
|
+
if (flat.length === 0) {
|
|
10011
|
+
return "";
|
|
10012
|
+
}
|
|
10013
|
+
const flatLower = flat.toLowerCase();
|
|
10014
|
+
const needleSlice = text.slice(matchIdx, matchIdx + matchLen).toLowerCase().replace(/\s+/g, " ").trim();
|
|
10015
|
+
let pos = needleSlice.length > 0 ? flatLower.indexOf(needleSlice) : 0;
|
|
10016
|
+
if (pos === -1) {
|
|
10017
|
+
pos = 0;
|
|
10018
|
+
}
|
|
10019
|
+
const start = Math.max(0, pos - SNIPPET_SIDE);
|
|
10020
|
+
const end = Math.min(flat.length, pos + needleSlice.length + SNIPPET_SIDE);
|
|
10021
|
+
const head = start > 0 ? "\u2026" : "";
|
|
10022
|
+
const tail = end < flat.length ? "\u2026" : "";
|
|
10023
|
+
return `${head}${flat.slice(start, end)}${tail}`;
|
|
10024
|
+
}
|
|
10025
|
+
|
|
9134
10026
|
// src/daemon/routes/sessions.ts
|
|
9135
10027
|
function resolveHydraHost(defaults) {
|
|
9136
10028
|
if (defaults.publicHost && defaults.publicHost.length > 0) {
|
|
@@ -9147,6 +10039,17 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9147
10039
|
const sessions = await manager.list({ cwd: query?.cwd });
|
|
9148
10040
|
return { sessions };
|
|
9149
10041
|
});
|
|
10042
|
+
app.get("/v1/sessions/search", async (request, reply) => {
|
|
10043
|
+
const query = request.query;
|
|
10044
|
+
const q = query?.q ?? "";
|
|
10045
|
+
if (q.trim().length === 0) {
|
|
10046
|
+
reply.code(400).send({ error: "q is required" });
|
|
10047
|
+
return reply;
|
|
10048
|
+
}
|
|
10049
|
+
const ids = query?.sessionIds ? query.sessionIds.split(",").filter((s) => s.length > 0) : void 0;
|
|
10050
|
+
const out = await searchHistories(manager, q, { sessionIds: ids });
|
|
10051
|
+
return out;
|
|
10052
|
+
});
|
|
9150
10053
|
app.post("/v1/sessions", async (request, reply) => {
|
|
9151
10054
|
const body = request.body ?? {};
|
|
9152
10055
|
const cwd = expandHome(body.cwd ?? defaults.cwd);
|
|
@@ -9931,6 +10834,7 @@ function wsToMessageStream(ws) {
|
|
|
9931
10834
|
// src/daemon/acp-ws.ts
|
|
9932
10835
|
import * as os4 from "os";
|
|
9933
10836
|
import * as path12 from "path";
|
|
10837
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
9934
10838
|
function registerAcpWsEndpoint(app, deps) {
|
|
9935
10839
|
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
9936
10840
|
const token = tokenFromUpgradeRequest({
|
|
@@ -9968,6 +10872,12 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
9968
10872
|
};
|
|
9969
10873
|
connection.onRequest("initialize", async (raw) => {
|
|
9970
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
|
+
}
|
|
9971
10881
|
const version = params.clientInfo?.version;
|
|
9972
10882
|
if (version && processIdentity) {
|
|
9973
10883
|
if (processIdentity.kind === "extension") {
|
|
@@ -9978,6 +10888,34 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
9978
10888
|
}
|
|
9979
10889
|
return buildInitializeResult();
|
|
9980
10890
|
});
|
|
10891
|
+
if (processIdentity && deps.extensionCommands) {
|
|
10892
|
+
const registry = deps.extensionCommands;
|
|
10893
|
+
connection.onRequest("hydra-acp/register_commands", async (raw) => {
|
|
10894
|
+
const params = raw ?? {};
|
|
10895
|
+
const commands = Array.isArray(params.commands) ? params.commands.map((c) => {
|
|
10896
|
+
if (!c || typeof c !== "object") {
|
|
10897
|
+
return void 0;
|
|
10898
|
+
}
|
|
10899
|
+
const obj = c;
|
|
10900
|
+
if (typeof obj.verb !== "string" || obj.verb.length === 0) {
|
|
10901
|
+
return void 0;
|
|
10902
|
+
}
|
|
10903
|
+
const spec = { verb: obj.verb };
|
|
10904
|
+
if (typeof obj.argsHint === "string") {
|
|
10905
|
+
spec.argsHint = obj.argsHint;
|
|
10906
|
+
}
|
|
10907
|
+
if (typeof obj.description === "string") {
|
|
10908
|
+
spec.description = obj.description;
|
|
10909
|
+
}
|
|
10910
|
+
return spec;
|
|
10911
|
+
}).filter((s) => s !== void 0) : [];
|
|
10912
|
+
registry.register(processIdentity.name, connection, commands);
|
|
10913
|
+
return { ok: true, registered: commands.length };
|
|
10914
|
+
});
|
|
10915
|
+
connection.onClose(() => {
|
|
10916
|
+
registry.clear(processIdentity.name);
|
|
10917
|
+
});
|
|
10918
|
+
}
|
|
9981
10919
|
if (processIdentity?.kind === "transformer") {
|
|
9982
10920
|
connection.onRequest("transformer/initialize", async (raw) => {
|
|
9983
10921
|
const params = raw ?? {};
|
|
@@ -10119,16 +11057,50 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
10119
11057
|
);
|
|
10120
11058
|
const transformerNames = Array.isArray(hydraMeta.transformers) && hydraMeta.transformers.every((t) => typeof t === "string") ? hydraMeta.transformers : deps.manager.defaultTransformers ?? [];
|
|
10121
11059
|
const transformChain = deps.transformers?.resolveChain(transformerNames) ?? [];
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
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
|
+
}
|
|
10132
11104
|
const client = bindClientToSession(connection, session, state);
|
|
10133
11105
|
const { entries: replay } = await session.attach(client, "full");
|
|
10134
11106
|
state.attached.set(session.sessionId, {
|
|
@@ -10525,7 +11497,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
10525
11497
|
return null;
|
|
10526
11498
|
}
|
|
10527
11499
|
app.log.info(decision.logMessage);
|
|
10528
|
-
|
|
11500
|
+
const { modelId } = rawParams;
|
|
11501
|
+
const result = await decision.session.forwardRequest("session/set_model", rawParams);
|
|
11502
|
+
decision.session.applyModelChange(modelId);
|
|
11503
|
+
return result;
|
|
10529
11504
|
});
|
|
10530
11505
|
connection.onRequest("session/set_mode", async (rawParams) => {
|
|
10531
11506
|
const params = rawParams;
|
|
@@ -10846,6 +11821,336 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
|
|
|
10846
11821
|
};
|
|
10847
11822
|
}
|
|
10848
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
|
+
|
|
10849
12154
|
// src/daemon/server.ts
|
|
10850
12155
|
async function startDaemon(config, serviceToken) {
|
|
10851
12156
|
ensureLoopbackOrTls(config);
|
|
@@ -10916,13 +12221,15 @@ async function startDaemon(config, serviceToken) {
|
|
|
10916
12221
|
stderrTailBytes: config.daemon.agentStderrTailBytes,
|
|
10917
12222
|
logger: agentLogger
|
|
10918
12223
|
});
|
|
12224
|
+
const extensionCommands = new ExtensionCommandRegistry();
|
|
10919
12225
|
const manager = new SessionManager(registry, spawner, void 0, {
|
|
10920
12226
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
10921
12227
|
defaultModels: config.defaultModels,
|
|
10922
12228
|
defaultTransformers: config.defaultTransformers,
|
|
10923
12229
|
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
|
|
10924
12230
|
logger: agentLogger,
|
|
10925
|
-
npmRegistry: config.npmRegistry
|
|
12231
|
+
npmRegistry: config.npmRegistry,
|
|
12232
|
+
extensionCommands
|
|
10926
12233
|
});
|
|
10927
12234
|
const extensions = new ExtensionManager(extensionList(config), void 0, {
|
|
10928
12235
|
tokenRegistry: processRegistry
|
|
@@ -10949,6 +12256,19 @@ async function startDaemon(config, serviceToken) {
|
|
|
10949
12256
|
store: sessionTokenStore,
|
|
10950
12257
|
rateLimiter: authRateLimiter
|
|
10951
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
|
+
};
|
|
10952
12272
|
registerAcpWsEndpoint(app, {
|
|
10953
12273
|
validator,
|
|
10954
12274
|
manager,
|
|
@@ -10956,7 +12276,10 @@ async function startDaemon(config, serviceToken) {
|
|
|
10956
12276
|
processRegistry,
|
|
10957
12277
|
onExtensionVersion: (name, version) => extensions.reportVersion(name, version),
|
|
10958
12278
|
onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
|
|
10959
|
-
transformers
|
|
12279
|
+
transformers,
|
|
12280
|
+
extensionCommands,
|
|
12281
|
+
stdinMcpRegistry,
|
|
12282
|
+
getDaemonOrigin
|
|
10960
12283
|
});
|
|
10961
12284
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
10962
12285
|
const address = app.server.address();
|