@letrquan/book 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +2504 -0
  2. package/LICENSE +123 -0
  3. package/README.md +1383 -0
  4. package/dist/app-MOXXWEOY.js +18945 -0
  5. package/dist/app-MOXXWEOY.js.map +1 -0
  6. package/dist/chunk-24AXE6SP.js +12 -0
  7. package/dist/chunk-24AXE6SP.js.map +1 -0
  8. package/dist/chunk-3U5IBM24.js +213 -0
  9. package/dist/chunk-3U5IBM24.js.map +1 -0
  10. package/dist/chunk-574C6MQR.js +743 -0
  11. package/dist/chunk-574C6MQR.js.map +1 -0
  12. package/dist/chunk-5GDZ22YP.js +449 -0
  13. package/dist/chunk-5GDZ22YP.js.map +1 -0
  14. package/dist/chunk-5KLVH3PY.js +167 -0
  15. package/dist/chunk-5KLVH3PY.js.map +1 -0
  16. package/dist/chunk-5RDRAO4B.js +92 -0
  17. package/dist/chunk-5RDRAO4B.js.map +1 -0
  18. package/dist/chunk-7N4J557C.js +10 -0
  19. package/dist/chunk-7N4J557C.js.map +1 -0
  20. package/dist/chunk-A7JLBK2Y.js +3506 -0
  21. package/dist/chunk-A7JLBK2Y.js.map +1 -0
  22. package/dist/chunk-ANSJPYGI.js +84 -0
  23. package/dist/chunk-ANSJPYGI.js.map +1 -0
  24. package/dist/chunk-BTZZUCLI.js +264 -0
  25. package/dist/chunk-BTZZUCLI.js.map +1 -0
  26. package/dist/chunk-C7D4NJ2D.js +217 -0
  27. package/dist/chunk-C7D4NJ2D.js.map +1 -0
  28. package/dist/chunk-CAGOB2N4.js +208 -0
  29. package/dist/chunk-CAGOB2N4.js.map +1 -0
  30. package/dist/chunk-DGMQEI6Y.js +98 -0
  31. package/dist/chunk-DGMQEI6Y.js.map +1 -0
  32. package/dist/chunk-E75IPST5.js +289 -0
  33. package/dist/chunk-E75IPST5.js.map +1 -0
  34. package/dist/chunk-EYGD6X2T.js +180 -0
  35. package/dist/chunk-EYGD6X2T.js.map +1 -0
  36. package/dist/chunk-GTVB4HEM.js +765 -0
  37. package/dist/chunk-GTVB4HEM.js.map +1 -0
  38. package/dist/chunk-J7MIZ5CA.js +1907 -0
  39. package/dist/chunk-J7MIZ5CA.js.map +1 -0
  40. package/dist/chunk-KWHQ2XXL.js +626 -0
  41. package/dist/chunk-KWHQ2XXL.js.map +1 -0
  42. package/dist/chunk-L6LK2QIQ.js +657 -0
  43. package/dist/chunk-L6LK2QIQ.js.map +1 -0
  44. package/dist/chunk-MAQXCUR4.js +1007 -0
  45. package/dist/chunk-MAQXCUR4.js.map +1 -0
  46. package/dist/chunk-S4XL7HOM.js +402 -0
  47. package/dist/chunk-S4XL7HOM.js.map +1 -0
  48. package/dist/chunk-URLQAXTR.js +60 -0
  49. package/dist/chunk-URLQAXTR.js.map +1 -0
  50. package/dist/chunk-V4BOYR52.js +3504 -0
  51. package/dist/chunk-V4BOYR52.js.map +1 -0
  52. package/dist/chunk-VZQXX3WW.js +210 -0
  53. package/dist/chunk-VZQXX3WW.js.map +1 -0
  54. package/dist/chunk-WKBO5R4O.js +34 -0
  55. package/dist/chunk-WKBO5R4O.js.map +1 -0
  56. package/dist/chunk-XIFFTATU.js +15753 -0
  57. package/dist/chunk-XIFFTATU.js.map +1 -0
  58. package/dist/command-approvals-6Y57VCWZ.js +28 -0
  59. package/dist/command-approvals-6Y57VCWZ.js.map +1 -0
  60. package/dist/context-ZC5IFEAX.js +25 -0
  61. package/dist/context-ZC5IFEAX.js.map +1 -0
  62. package/dist/hook-approvals-CTUFKEJT.js +22 -0
  63. package/dist/hook-approvals-CTUFKEJT.js.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +2582 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/interactive-assets-CCAAWVKH.js +31 -0
  68. package/dist/interactive-assets-CCAAWVKH.js.map +1 -0
  69. package/dist/job-runner.d.ts +2 -0
  70. package/dist/job-runner.js +235 -0
  71. package/dist/job-runner.js.map +1 -0
  72. package/dist/loader-4IDS2RIE.js +15 -0
  73. package/dist/loader-4IDS2RIE.js.map +1 -0
  74. package/dist/mcp-approvals-WNPH65BK.js +18 -0
  75. package/dist/mcp-approvals-WNPH65BK.js.map +1 -0
  76. package/dist/mcp-config-YIJIES2C.js +16 -0
  77. package/dist/mcp-config-YIJIES2C.js.map +1 -0
  78. package/dist/model-window-store-YFVNXCIP.js +27 -0
  79. package/dist/model-window-store-YFVNXCIP.js.map +1 -0
  80. package/dist/openai-compatible-RHZUBHV6.js +10 -0
  81. package/dist/openai-compatible-RHZUBHV6.js.map +1 -0
  82. package/dist/permission-approvals-76PVREFD.js +14 -0
  83. package/dist/permission-approvals-76PVREFD.js.map +1 -0
  84. package/dist/permissions-CHCKTFQQ.js +28 -0
  85. package/dist/permissions-CHCKTFQQ.js.map +1 -0
  86. package/dist/sandbox-CNVAQDA7.js +23 -0
  87. package/dist/sandbox-CNVAQDA7.js.map +1 -0
  88. package/dist/scrollback-LUKLIBVI.js +136 -0
  89. package/dist/scrollback-LUKLIBVI.js.map +1 -0
  90. package/dist/sdk.d.ts +4574 -0
  91. package/dist/sdk.js +665 -0
  92. package/dist/sdk.js.map +1 -0
  93. package/dist/settings-loader-M2QP5TX2.js +21 -0
  94. package/dist/settings-loader-M2QP5TX2.js.map +1 -0
  95. package/dist/settings-removed-JWWPT4WA.js +19 -0
  96. package/dist/settings-removed-JWWPT4WA.js.map +1 -0
  97. package/dist/shell-selection-V6QAYXPX.js +17 -0
  98. package/dist/shell-selection-V6QAYXPX.js.map +1 -0
  99. package/package.json +143 -0
  100. package/patches/ink+6.8.0.patch +13 -0
  101. package/scripts/apply-ink-patch.mjs +29 -0
  102. package/scripts/ink-patch.mjs +59 -0
  103. package/scripts/verify-ink-patch.mjs +11 -0
@@ -0,0 +1,1907 @@
1
+ import {
2
+ AgentSession,
3
+ reviewReportJson,
4
+ reviewRunnerFor,
5
+ runHostReview,
6
+ toElicitationFields,
7
+ validateElicitationContent
8
+ } from "./chunk-V4BOYR52.js";
9
+ import {
10
+ buildAgentCompletionMessage,
11
+ buildPlanApprovalQuestion,
12
+ createAgentRunContext,
13
+ createSessionHistoryTools,
14
+ createTerminalOutcome,
15
+ getOrCreateAgentManager,
16
+ normalizePersistedTodos,
17
+ planApprovalFromUserQuestionResponse,
18
+ planStopDecision,
19
+ shouldCompact,
20
+ takeAgentCompletionBatch,
21
+ usagePressureTokens
22
+ } from "./chunk-XIFFTATU.js";
23
+ import {
24
+ createBuiltinCommandRegistry,
25
+ estimateUsageCost,
26
+ getPackageVersion,
27
+ planWasLost,
28
+ resolveContextLimit,
29
+ resolvePermissionMode
30
+ } from "./chunk-A7JLBK2Y.js";
31
+ import {
32
+ commandEnforcementContext,
33
+ discoverCommands,
34
+ parseSlashInput,
35
+ resolveCommandBody
36
+ } from "./chunk-E75IPST5.js";
37
+ import {
38
+ consoleMcpDiagnosticSink,
39
+ resolveMcpServerList
40
+ } from "./chunk-3U5IBM24.js";
41
+ import {
42
+ observationKey,
43
+ toolFailure,
44
+ toolSuccess
45
+ } from "./chunk-5GDZ22YP.js";
46
+ import {
47
+ loadSettingsFile
48
+ } from "./chunk-BTZZUCLI.js";
49
+ import {
50
+ partitionProjectAllowRules
51
+ } from "./chunk-WKBO5R4O.js";
52
+ import {
53
+ collectDeclaredHooks,
54
+ partitionProjectHooks
55
+ } from "./chunk-ANSJPYGI.js";
56
+ import {
57
+ resolveBookHome
58
+ } from "./chunk-24AXE6SP.js";
59
+
60
+ // src/run-status.ts
61
+ import {
62
+ closeSync,
63
+ mkdirSync,
64
+ openSync,
65
+ readFileSync,
66
+ renameSync,
67
+ rmSync,
68
+ statfsSync,
69
+ writeSync
70
+ } from "fs";
71
+ import { join } from "path";
72
+ var RUN_STATUS_VERSION = 1;
73
+ function runStatusDir(home = resolveBookHome()) {
74
+ return join(home, "runs");
75
+ }
76
+ function runStatusPath(sessionId, home = resolveBookHome()) {
77
+ const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "_");
78
+ return join(runStatusDir(home), `${safe}.json`);
79
+ }
80
+ function freeDiskBytes(path) {
81
+ try {
82
+ const stats = statfsSync(path);
83
+ const free = Number(stats.bavail) * Number(stats.bsize);
84
+ return Number.isFinite(free) && free >= 0 ? free : void 0;
85
+ } catch {
86
+ return void 0;
87
+ }
88
+ }
89
+ function writeRunStatus(record, home = resolveBookHome()) {
90
+ const target = runStatusPath(record.sessionId, home);
91
+ const temp = `${target}.${process.pid}.tmp`;
92
+ try {
93
+ mkdirSync(runStatusDir(home), { recursive: true });
94
+ const fd = openSync(temp, "w");
95
+ try {
96
+ writeSync(fd, JSON.stringify(record, null, 2));
97
+ } finally {
98
+ closeSync(fd);
99
+ }
100
+ renameSync(temp, target);
101
+ } catch {
102
+ try {
103
+ rmSync(temp, { force: true });
104
+ } catch {
105
+ }
106
+ }
107
+ }
108
+ function readRunStatus(sessionId, home = resolveBookHome()) {
109
+ try {
110
+ const parsed = JSON.parse(readFileSync(runStatusPath(sessionId, home), "utf8"));
111
+ if (!parsed || typeof parsed !== "object") return void 0;
112
+ const record = parsed;
113
+ return record.version === RUN_STATUS_VERSION ? record : void 0;
114
+ } catch {
115
+ return void 0;
116
+ }
117
+ }
118
+ var RunStatusWriter = class {
119
+ seed;
120
+ last;
121
+ crashHandlerInstalled = false;
122
+ constructor(seed) {
123
+ this.seed = seed;
124
+ this.last = {
125
+ version: RUN_STATUS_VERSION,
126
+ sessionId: seed.sessionId,
127
+ runId: seed.runId,
128
+ pid: process.pid,
129
+ startedAt: seed.startedAt,
130
+ updatedAt: seed.startedAt,
131
+ turn: 0,
132
+ elapsedMs: 0,
133
+ openTodos: 0,
134
+ costUsd: null,
135
+ budgetUsd: seed.budgetUsd,
136
+ freeDiskBytes: freeDiskBytes(seed.workspace)
137
+ };
138
+ writeRunStatus(this.last, seed.home);
139
+ }
140
+ update(update, now = Date.now()) {
141
+ this.last = {
142
+ ...this.last,
143
+ updatedAt: now,
144
+ elapsedMs: now - this.seed.startedAt,
145
+ turn: update.turn,
146
+ lastTool: update.lastTool ?? this.last.lastTool,
147
+ currentTodo: update.currentTodo,
148
+ openTodos: update.openTodos,
149
+ costUsd: update.costUsd,
150
+ freeDiskBytes: freeDiskBytes(this.seed.workspace),
151
+ terminal: update.terminal ?? this.last.terminal
152
+ };
153
+ writeRunStatus(this.last, this.seed.home);
154
+ }
155
+ /**
156
+ * Record that the process is dying without a terminal outcome.
157
+ *
158
+ * Synchronous by necessity: this runs from `process.on('exit')`, where the event
159
+ * loop is already closed and no promise will ever settle.
160
+ */
161
+ recordCrash(message, now = Date.now()) {
162
+ if (this.last.terminal) return;
163
+ if (this.last.crash) return;
164
+ this.last = { ...this.last, updatedAt: now, crash: { message, at: now } };
165
+ writeRunStatus(this.last, this.seed.home);
166
+ }
167
+ /**
168
+ * Install last-resort handlers so an unhandled fault leaves a record.
169
+ *
170
+ * There is no `uncaughtException` or `unhandledRejection` handler anywhere in the
171
+ * codebase, and `index.ts` ends in a bare `program.parse()` whose returned promise
172
+ * nothing awaits — so a rejection escaping the action handler becomes an unhandled
173
+ * rejection and the operator gets a stack trace on a stderr they may have
174
+ * redirected days ago. Returns a disposer.
175
+ */
176
+ installCrashHandlers() {
177
+ if (this.crashHandlerInstalled) return () => {
178
+ };
179
+ this.crashHandlerInstalled = true;
180
+ const onExit = () => {
181
+ this.recordCrash("The process exited before the run reached a terminal outcome.");
182
+ };
183
+ process.on("exit", onExit);
184
+ return () => {
185
+ process.off("exit", onExit);
186
+ this.crashHandlerInstalled = false;
187
+ };
188
+ }
189
+ snapshot() {
190
+ return this.last;
191
+ }
192
+ };
193
+
194
+ // src/commands/print-dispatch.ts
195
+ var PrintEffectRegistry = class {
196
+ handlers = /* @__PURE__ */ new Map();
197
+ register(type, handler) {
198
+ this.handlers.set(type, handler);
199
+ return this;
200
+ }
201
+ has(type) {
202
+ return this.handlers.has(type);
203
+ }
204
+ handledTypes() {
205
+ return [...this.handlers.keys()];
206
+ }
207
+ handle(effect, env, command) {
208
+ const handler = this.handlers.get(effect.type);
209
+ return handler ? Promise.resolve(handler(effect, env, command)) : void 0;
210
+ }
211
+ };
212
+ var UnsupportedPrintCommandError = class extends Error {
213
+ command;
214
+ constructor(command, message) {
215
+ super(message);
216
+ this.name = "UnsupportedPrintCommandError";
217
+ this.command = command;
218
+ }
219
+ };
220
+ var PrintCommandUsageError = class extends Error {
221
+ command;
222
+ constructor(command, message) {
223
+ super(message);
224
+ this.name = "PrintCommandUsageError";
225
+ this.command = command;
226
+ }
227
+ };
228
+ var FIX_REFUSAL = [
229
+ "\u2715 /review --fix needs an interactive session.",
230
+ "",
231
+ "--fix spawns patcher agents that edit files and commit verified patches. A non-interactive",
232
+ "host cannot approve those tool calls, so the fix pass would either be denied outright or",
233
+ "commit unreviewed changes with no one watching.",
234
+ "",
235
+ 'Run `book` interactively and use /review --fix there, or run `book -p "/review --deep"`',
236
+ "and apply the reported fixes yourself."
237
+ ].join("\n");
238
+ async function performPrintReview(effect, env, command) {
239
+ if (effect.scope.fix) throw new UnsupportedPrintCommandError(command, FIX_REFUSAL);
240
+ if (!env.agents) {
241
+ throw new UnsupportedPrintCommandError(
242
+ command,
243
+ `\u2715 /${command} needs the managed-agent runtime, and this host did not provide one.`
244
+ );
245
+ }
246
+ const result = await runHostReview({
247
+ scope: effect.scope,
248
+ workspace: env.config.workspace,
249
+ // Every reviewer, lens and verifier agent is spawned under the host's run,
250
+ // so the whole pipeline is billed and budgeted as one root.
251
+ runner: reviewRunnerFor(env.agents.manager(), {
252
+ rootRunId: env.agents.runContext.rootRunId,
253
+ parentRunId: env.agents.runContext.runId
254
+ })
255
+ });
256
+ if (result.error) throw new Error(result.segments.join("\n\n"));
257
+ return {
258
+ kind: "handled",
259
+ command,
260
+ output: result.segments.join("\n\n"),
261
+ data: reviewReportJson(result)
262
+ };
263
+ }
264
+ function createPrintEffectRegistry() {
265
+ return new PrintEffectRegistry().register("send-prompt", (effect) => ({
266
+ kind: "prompt",
267
+ prompt: effect.prompt,
268
+ commandContext: effect.context
269
+ })).register("local-message", (effect, _env, command) => {
270
+ if (effect.isError) throw new PrintCommandUsageError(command, effect.content);
271
+ return { kind: "handled", command, output: effect.content };
272
+ }).register("review", performPrintReview);
273
+ }
274
+ function printBuiltinContext(env, commands) {
275
+ return {
276
+ workspace: env.config.workspace,
277
+ sessionId: env.sessionId,
278
+ model: env.config.model,
279
+ provider: env.config.provider,
280
+ currentTurn: 0,
281
+ messages: [],
282
+ lastError: null,
283
+ runtimeConfig: env.config,
284
+ mode: env.mode,
285
+ usage: null,
286
+ turnDurationMs: 0,
287
+ contextHistory: [],
288
+ compactBoundaries: [],
289
+ commandCount: commands.length,
290
+ skillCount: 0,
291
+ resolveAmbientContext: () => ({
292
+ subagentCount: 0,
293
+ hasMemoryIndex: false,
294
+ hasClaudeMdLoader: false
295
+ })
296
+ };
297
+ }
298
+ function supportedCommandHelp(builtins, commands) {
299
+ const lines = ["Commands supported in print mode:"];
300
+ for (const definition of builtins.getDefinitions()) {
301
+ if (definition.nonInteractive) lines.push(` /${definition.name} \u2014 ${definition.description}`);
302
+ }
303
+ const custom = commands.map((command) => `/${command.name}`);
304
+ lines.push(
305
+ custom.length > 0 ? ` custom commands from .book/commands: ${custom.slice(0, 12).join(" ")}${custom.length > 12 ? " \u2026" : ""}` : " plus any custom command in .book/commands/ or ~/.book/commands/"
306
+ );
307
+ return lines;
308
+ }
309
+ function unsupported(command, reason, builtins, commands) {
310
+ return new UnsupportedPrintCommandError(
311
+ command,
312
+ [
313
+ `\u2715 /${command} ${reason}`,
314
+ "",
315
+ ...supportedCommandHelp(builtins, commands),
316
+ "",
317
+ `Run book without --print (interactive mode) to use /${command}.`
318
+ ].join("\n")
319
+ );
320
+ }
321
+ async function resolvePrintCommand(input, env, deps = {}) {
322
+ const parsed = parseSlashInput(input);
323
+ if (!parsed || !parsed.name) return { kind: "passthrough", prompt: input };
324
+ const builtins = deps.builtins ?? createBuiltinCommandRegistry();
325
+ const effects = deps.effects ?? createPrintEffectRegistry();
326
+ const commands = deps.commands ?? discoverCommands(env.config.workspace);
327
+ const context = printBuiltinContext(env, commands);
328
+ const resolved = builtins.resolve(parsed.name, context);
329
+ if (resolved) {
330
+ if (!resolved.definition.nonInteractive) {
331
+ throw unsupported(
332
+ parsed.name,
333
+ "needs an interactive session: it changes host state instead of producing a prompt.",
334
+ builtins,
335
+ commands
336
+ );
337
+ }
338
+ if (!resolved.available) {
339
+ throw unsupported(
340
+ parsed.name,
341
+ `is unavailable: ${resolved.unavailableReason ?? "no reason given"}`,
342
+ builtins,
343
+ commands
344
+ );
345
+ }
346
+ const effect = resolved.definition.execute(
347
+ { name: resolved.invokedName, rawArguments: parsed.rawArguments },
348
+ context
349
+ );
350
+ const dispatch = await effects.handle(effect, env, resolved.invokedName);
351
+ if (!dispatch) {
352
+ throw unsupported(
353
+ parsed.name,
354
+ `resolved to the '${effect.type}' effect, which a non-interactive host cannot perform.`,
355
+ builtins,
356
+ commands
357
+ );
358
+ }
359
+ return dispatch.kind === "handled" ? { ...dispatch, command: parsed.name } : dispatch;
360
+ }
361
+ const custom = commands.find((command) => command.name === parsed.name);
362
+ if (!custom) return { kind: "passthrough", prompt: input };
363
+ const { resolved: body, shellErrors } = await resolveCommandBody(
364
+ custom,
365
+ parsed.rawArguments,
366
+ {
367
+ sessionId: env.sessionId,
368
+ workspace: env.config.workspace,
369
+ model: env.config.model,
370
+ projectCommands: env.config.settings.commands.projectCommands
371
+ },
372
+ env.signal
373
+ );
374
+ return {
375
+ kind: "prompt",
376
+ prompt: body,
377
+ commandContext: commandEnforcementContext(custom, body),
378
+ shellErrors
379
+ };
380
+ }
381
+
382
+ // src/stream-json.ts
383
+ var EVENT_TYPES = /* @__PURE__ */ new Set([
384
+ "user",
385
+ "system",
386
+ "session",
387
+ "run_start",
388
+ "assistant",
389
+ "reasoning",
390
+ "tool_use",
391
+ "tool_result",
392
+ "user_question",
393
+ "user_question_result",
394
+ "agent_start",
395
+ "background_job_start",
396
+ "background_job_update",
397
+ "background_job_output",
398
+ "background_job_result",
399
+ "background_job_dismiss",
400
+ "agent_update",
401
+ "agent_result",
402
+ "agent_question",
403
+ "agent_status",
404
+ "agent_activity",
405
+ "agent_text_delta",
406
+ "agent_message",
407
+ "agent_completion",
408
+ "agent_permission",
409
+ "evidence_update",
410
+ "agent_apply",
411
+ "hook_event",
412
+ "mode_change",
413
+ "plan_approval",
414
+ "prompt_suggestions",
415
+ "error",
416
+ "result",
417
+ "done"
418
+ ]);
419
+ var DEFAULT_MAX_BUFFERED_LINE_BYTES = 1024 * 1024;
420
+ function parseStreamLineDetailed(line) {
421
+ const trimmed = line.trim();
422
+ if (!trimmed) return {};
423
+ let parsed;
424
+ try {
425
+ parsed = JSON.parse(trimmed);
426
+ } catch (error) {
427
+ return {
428
+ diagnostic: {
429
+ code: "invalid-json",
430
+ message: `Invalid stream-json record: ${error instanceof Error ? error.message : String(error)}`,
431
+ line: trimmed
432
+ }
433
+ };
434
+ }
435
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || !("type" in parsed) || typeof parsed.type !== "string" || !EVENT_TYPES.has(parsed.type) || parsed.type === "user" && typeof parsed.content !== "string") {
436
+ return {
437
+ diagnostic: {
438
+ code: "invalid-shape",
439
+ message: "Stream-json records must be objects with a supported type and valid fields.",
440
+ line: trimmed
441
+ }
442
+ };
443
+ }
444
+ return { event: parsed };
445
+ }
446
+ function createStreamParser(onEvent, options = {}) {
447
+ const maxBufferedLineBytes = options.maxBufferedLineBytes ?? DEFAULT_MAX_BUFFERED_LINE_BYTES;
448
+ if (!Number.isSafeInteger(maxBufferedLineBytes) || maxBufferedLineBytes < 1) {
449
+ throw new Error("maxBufferedLineBytes must be a positive safe integer");
450
+ }
451
+ let buffer = "";
452
+ let discardingOversizedLine = false;
453
+ const emitLine = (line) => {
454
+ const { event, diagnostic } = parseStreamLineDetailed(line.replace(/\r$/, ""));
455
+ if (event) onEvent(event);
456
+ if (diagnostic) options.onDiagnostic?.(diagnostic);
457
+ };
458
+ const append = (segment, terminated) => {
459
+ if (discardingOversizedLine) {
460
+ if (terminated) discardingOversizedLine = false;
461
+ return;
462
+ }
463
+ if (Buffer.byteLength(buffer) + Buffer.byteLength(segment) > maxBufferedLineBytes) {
464
+ buffer = "";
465
+ options.onDiagnostic?.({
466
+ code: "oversized-line",
467
+ message: `Stream-json record exceeds the ${maxBufferedLineBytes}-byte limit.`
468
+ });
469
+ discardingOversizedLine = !terminated;
470
+ return;
471
+ }
472
+ buffer += segment;
473
+ if (terminated) {
474
+ emitLine(buffer);
475
+ buffer = "";
476
+ }
477
+ };
478
+ return {
479
+ feed(chunk) {
480
+ const segments = chunk.toString().split("\n");
481
+ for (let index = 0; index < segments.length; index++) {
482
+ append(segments[index] ?? "", index < segments.length - 1);
483
+ }
484
+ },
485
+ flush() {
486
+ if (!discardingOversizedLine && buffer.length > 0) emitLine(buffer);
487
+ buffer = "";
488
+ discardingOversizedLine = false;
489
+ }
490
+ };
491
+ }
492
+
493
+ // src/headless.ts
494
+ function carriedCostUsd(usage, models) {
495
+ let worst = null;
496
+ for (const model of models) {
497
+ const quote = estimateUsageCost(model, usage);
498
+ if (quote.status !== "known") return null;
499
+ worst = worst === null ? quote.costUsd : Math.max(worst, quote.costUsd);
500
+ }
501
+ return worst;
502
+ }
503
+ async function readPipedPrompt(stream) {
504
+ if (stream.isTTY) return void 0;
505
+ const chunks = [];
506
+ for await (const chunk of stream) {
507
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
508
+ }
509
+ const prompt = chunks.join("").trim();
510
+ return prompt || void 0;
511
+ }
512
+ async function runHeadless(config, registry, opts) {
513
+ const mode = resolvePermissionMode(config.settings, opts.mode);
514
+ const stdout = opts.stdout ?? process.stdout;
515
+ const emit = (obj) => {
516
+ if (obj && typeof obj === "object" && "type" in obj) {
517
+ opts.onEvent?.(obj);
518
+ }
519
+ stdout.write(JSON.stringify(obj) + "\n");
520
+ };
521
+ let disposeCrashHandlers;
522
+ const agentSession = new AgentSession();
523
+ const runtime = agentSession.getRuntime();
524
+ runtime.todos.push(...normalizePersistedTodos(opts.plan?.todos));
525
+ if (opts.plan?.tasks?.length) runtime.tasks.push(...opts.plan.tasks);
526
+ runtime.planUnrestored = planWasLost({
527
+ planRecordExisted: opts.plan !== void 0,
528
+ priorMessages: opts.history.length,
529
+ todos: runtime.todos.length,
530
+ tasks: runtime.tasks.length
531
+ });
532
+ try {
533
+ if (opts.outputFormat === "stream-json") {
534
+ emit({ type: "system", model: config.model, cwd: config.workspace });
535
+ }
536
+ const store = opts.persistSession === false ? void 0 : opts.sessionStore;
537
+ let sessionId = opts.sessionId;
538
+ if (store && !sessionId && opts.sessionName) {
539
+ sessionId = store.findByName(opts.sessionName)?.id;
540
+ }
541
+ if (store && sessionId && opts.forkSession) {
542
+ sessionId = void 0;
543
+ }
544
+ let sessionName = opts.sessionName;
545
+ let sessionCreated = opts.sessionCreated === true;
546
+ if (store && !sessionId) {
547
+ sessionId = store.create({ cwd: config.workspace, name: sessionName });
548
+ sessionCreated = true;
549
+ }
550
+ if (store && sessionId && !sessionName) {
551
+ sessionName = store.findById(sessionId)?.name;
552
+ }
553
+ if (sessionCreated && sessionId && opts.outputFormat === "stream-json") {
554
+ emit({ type: "session", session_id: sessionId });
555
+ }
556
+ if (store && sessionId) {
557
+ registry.registerAll(
558
+ createSessionHistoryTools({ store, sessionId: () => sessionId })
559
+ );
560
+ }
561
+ if (sessionId) {
562
+ await agentSession.startLifecycle(
563
+ config,
564
+ sessionId,
565
+ opts.history.length > 0 ? "resume" : "startup",
566
+ {
567
+ onHookEvent: createHookEventHandler(opts, emit)
568
+ }
569
+ );
570
+ }
571
+ const runtimeSessionId = sessionId ?? crypto.randomUUID();
572
+ const statusWriter = new RunStatusWriter({
573
+ sessionId: runtimeSessionId,
574
+ runId: runtimeSessionId,
575
+ workspace: config.workspace,
576
+ budgetUsd: opts.maxBudgetUsd,
577
+ startedAt: Date.now()
578
+ });
579
+ disposeCrashHandlers = statusWriter.installCrashHandlers();
580
+ const permissionRequired = async () => "deny";
581
+ const userQuestionStatus = opts.onUserQuestionRequired ? "pending" : "unavailable";
582
+ const askUser = async (request, context) => {
583
+ try {
584
+ return opts.onUserQuestionRequired ? await opts.onUserQuestionRequired(request, context) : {
585
+ action: "decline",
586
+ message: "User input is unavailable in non-interactive mode."
587
+ };
588
+ } catch (error) {
589
+ return {
590
+ action: "cancel",
591
+ message: `User question handler failed: ${error instanceof Error ? error.message : String(error)}`
592
+ };
593
+ }
594
+ };
595
+ const decidePlanApproval = async (plan) => {
596
+ if (mode === "bypassPermissions") return "approve";
597
+ if (!opts.onUserQuestionRequired) return planStopDecision("approval_unavailable");
598
+ const request = buildPlanApprovalQuestion(plan, crypto.randomUUID());
599
+ emitAgentEvent({ type: "user_question", request, status: userQuestionStatus }, opts, emit);
600
+ const response = await askUser(request, { signal: opts.signal });
601
+ emitAgentEvent({ type: "user_question_result", requestId: request.id, response }, opts, emit);
602
+ return planApprovalFromUserQuestionResponse(request, response);
603
+ };
604
+ const planNotApplied = {};
605
+ let lastUsage = null;
606
+ let lastOutcome = null;
607
+ const runResults = [];
608
+ const recordRunResult = (result2) => {
609
+ const existing = runResults.findIndex(
610
+ (candidate) => candidate.context.runId === result2.context.runId
611
+ );
612
+ if (existing >= 0) runResults[existing] = result2;
613
+ else runResults.push(result2);
614
+ };
615
+ const recordManagedRunResult = (agent) => {
616
+ if (!agent.runId) return;
617
+ const context = createAgentRunContext({
618
+ sessionId: agent.parentSessionId ?? runtimeSessionId,
619
+ runId: agent.runId,
620
+ rootRunId: agent.rootRunId ?? agent.runId,
621
+ parentRunId: agent.parentRunId,
622
+ source: "internal",
623
+ startedAt: agent.runStartedAt ?? agent.startedAt ?? agent.createdAt
624
+ });
625
+ const outcome2 = agent.runOutcome ?? (agent.status === "completed" ? createTerminalOutcome("completed", "normal_completion", { partialOutput: false }) : createTerminalOutcome("failed", "runtime_error", {
626
+ partialOutput: Boolean(agent.result),
627
+ message: agent.error ?? `Managed agent ended with status ${agent.status}.`
628
+ }));
629
+ recordRunResult({
630
+ context,
631
+ outcome: outcome2,
632
+ usage: agent.runUsage ?? null,
633
+ accounting: runtime.runAccounting.snapshotRun(agent.runId),
634
+ ambient: runtime.snapshotRunAmbient(agent.runId)
635
+ });
636
+ };
637
+ const recordManagedCompletionResult = (notification) => {
638
+ if (!notification.runId || runResults.some((candidate) => candidate.context.runId === notification.runId))
639
+ return;
640
+ const completion = notification.completion;
641
+ const outcome2 = notification.outcome ?? (completion.status === "completed" ? createTerminalOutcome("completed", "normal_completion", { partialOutput: false }) : completion.status === "stopped" ? createTerminalOutcome("cancelled", "user_cancelled", {
642
+ partialOutput: Boolean(completion.summary),
643
+ message: completion.error
644
+ }) : completion.status === "interrupted" ? createTerminalOutcome("interrupted", "transport_interrupted", {
645
+ partialOutput: Boolean(completion.summary),
646
+ message: completion.error
647
+ }) : createTerminalOutcome("failed", "runtime_error", {
648
+ partialOutput: Boolean(completion.summary),
649
+ message: completion.error
650
+ }));
651
+ recordRunResult({
652
+ context: createAgentRunContext({
653
+ sessionId: notification.parentSessionId ?? runtimeSessionId,
654
+ runId: notification.runId,
655
+ rootRunId: notification.rootRunId ?? notification.runId,
656
+ parentRunId: notification.parentRunId,
657
+ source: "internal",
658
+ startedAt: completion.startedAt ?? completion.createdAt
659
+ }),
660
+ outcome: outcome2,
661
+ usage: completion.usage ?? null,
662
+ accounting: runtime.runAccounting.snapshotRun(notification.runId),
663
+ ambient: runtime.snapshotRunAmbient(notification.runId)
664
+ });
665
+ };
666
+ const rootContexts = /* @__PURE__ */ new Map();
667
+ let lastHostCompactAttemptKey = null;
668
+ const contextHistory = [...opts.history];
669
+ const transcript = [...opts.transcript ?? opts.history];
670
+ const compactBoundaries = [...opts.compactBoundaries ?? []];
671
+ for (const message of transcript) {
672
+ for (const observation of message.fileObservations ?? []) {
673
+ const key = observationKey(observation.workspaceId, observation.path);
674
+ const current = runtime.fileObservationLedger.get(key);
675
+ if (!current || current.timestamp <= observation.timestamp) {
676
+ runtime.fileObservationLedger.set(key, observation);
677
+ }
678
+ }
679
+ }
680
+ const loopConfig = {
681
+ ...config,
682
+ maxTurns: opts.maxTurns ?? config.maxTurns
683
+ };
684
+ const createRunCallbacks = (runContext, onOutcome) => ({
685
+ onEvent: (event) => {
686
+ if (event.type === "agent_result") recordManagedRunResult(event.agent);
687
+ if (event.type === "terminal") {
688
+ lastOutcome = event.outcome;
689
+ onOutcome(event.outcome);
690
+ }
691
+ emitAgentEvent(event, opts, emit);
692
+ },
693
+ // No header rewrite at turn boundaries: load() derives updatedAt from
694
+ // the last appended record, so the on-disk header is never authoritative.
695
+ onTurnStart: () => {
696
+ },
697
+ onDone: () => {
698
+ },
699
+ onPermissionRequired: permissionRequired,
700
+ onModeChange: (newMode) => {
701
+ if (opts.outputFormat === "stream-json") {
702
+ emit({ type: "mode_change", mode: newMode });
703
+ }
704
+ },
705
+ onPlanApprovalRequired: async (plan) => {
706
+ const decision = await decidePlanApproval(plan);
707
+ if (typeof decision === "object" && decision.decision === "stop") {
708
+ planNotApplied.outcome = {
709
+ status: "not_applied",
710
+ reason: decision.reason,
711
+ plan,
712
+ message: decision.message
713
+ };
714
+ }
715
+ if (opts.outputFormat === "stream-json") {
716
+ emit({ type: "plan_approval", status: planApprovalStatus(decision) });
717
+ }
718
+ return decision;
719
+ },
720
+ onUserQuestionRequired: askUser,
721
+ userQuestionStatus,
722
+ onHookEvent: createHookEventHandler(opts, emit),
723
+ onUsage: (nextUsage) => {
724
+ lastUsage = nextUsage;
725
+ lastHostCompactAttemptKey = null;
726
+ },
727
+ onCompact: async (history, usage, hints) => {
728
+ const outcome2 = await agentSession.compact({
729
+ config,
730
+ history,
731
+ compactBoundaries,
732
+ sessionId,
733
+ transcriptOrdinal: transcript.length,
734
+ runContext,
735
+ runtime,
736
+ timelineStore: store,
737
+ onCommitted: (_result, boundary) => compactBoundaries.push(boundary),
738
+ options: {
739
+ ...hints,
740
+ trigger: "auto",
741
+ preContextTokens: usage ? usagePressureTokens(usage) : void 0,
742
+ signal: opts.signal,
743
+ onHookEvent: createHookEventHandler(opts, emit)
744
+ }
745
+ });
746
+ if (outcome2.result.status === "compacted") {
747
+ emitCompactBoundary(opts.outputFormat, emit, outcome2.result);
748
+ lastUsage = null;
749
+ }
750
+ return outcome2.result;
751
+ },
752
+ onAssistantMessageComplete: (message) => {
753
+ if (!transcript.some((item) => item.id === message.id)) transcript.push(message);
754
+ if (opts.outputFormat === "stream-json" && message.content) {
755
+ emit({ type: "assistant", text: message.content, complete: true });
756
+ }
757
+ }
758
+ });
759
+ const runParentTurn = async (prompt, displayMessage, userMessage, runContext, commandContext) => {
760
+ let runOutcome;
761
+ lastUsage = null;
762
+ let updated;
763
+ try {
764
+ updated = await agentSession.run({
765
+ config: loopConfig,
766
+ registry,
767
+ prompt,
768
+ history: contextHistory,
769
+ compactBoundaries,
770
+ mode,
771
+ sessionId: runtimeSessionId,
772
+ timelineStore: store,
773
+ signal: opts.signal,
774
+ callbacks: createRunCallbacks(runContext, (outcome2) => {
775
+ runOutcome = outcome2;
776
+ }),
777
+ runContext,
778
+ maxBudgetUsd: opts.maxBudgetUsd,
779
+ options: {
780
+ displayMessage,
781
+ userMessageId: userMessage.id,
782
+ userMessageTimestamp: userMessage.timestamp,
783
+ userFileObservations: userMessage.fileObservations,
784
+ userAttachments: userMessage.attachments,
785
+ userMessageKind: userMessage.kind,
786
+ manageSessionHooks: sessionId ? false : void 0,
787
+ // Completion messages are host-generated notifications, not user
788
+ // prompts. Keep UserPromptSubmit hooks from rewriting or blocking them.
789
+ skipUserPromptHooks: userMessage.kind === "agent-notification",
790
+ parentSessionId: runtimeSessionId,
791
+ runContext,
792
+ runtime,
793
+ // Command frontmatter enforcement, exactly as the TUI passes it.
794
+ allowedTools: commandContext?.allowedTools,
795
+ modelOverride: commandContext?.modelOverride,
796
+ commands: commandContext ? [commandContext.command] : void 0,
797
+ statusWriter,
798
+ // No human can resolve a permission prompt here: `permissionRequired`
799
+ // answers `deny` unconditionally.
800
+ unattended: true
801
+ }
802
+ });
803
+ } catch (error) {
804
+ const failedOutcome = runOutcome ?? createTerminalOutcome("interrupted", "missing_terminal", { partialOutput: false });
805
+ recordRunResult({
806
+ context: runContext,
807
+ outcome: failedOutcome,
808
+ usage: runtime.runAccounting.snapshotRun(runContext.runId)?.directUsage ?? lastUsage,
809
+ accounting: runtime.runAccounting.snapshotRun(runContext.runId),
810
+ ambient: runtime.snapshotRunAmbient(runContext.runId)
811
+ });
812
+ throw error;
813
+ }
814
+ const priorIds = new Set(transcript.map((message) => message.id));
815
+ transcript.push(
816
+ ...updated.filter((message) => !priorIds.has(message.id) && message.role === "assistant")
817
+ );
818
+ contextHistory.length = 0;
819
+ contextHistory.push(...updated);
820
+ const turnOutcome = runOutcome ?? createTerminalOutcome("interrupted", "missing_terminal", {
821
+ partialOutput: updated.some((message) => message.role === "assistant")
822
+ });
823
+ recordRunResult({
824
+ context: runContext,
825
+ outcome: turnOutcome,
826
+ usage: runtime.runAccounting.snapshotRun(runContext.runId)?.directUsage ?? lastUsage,
827
+ accounting: runtime.runAccounting.snapshotRun(runContext.runId),
828
+ ambient: runtime.snapshotRunAmbient(runContext.runId)
829
+ });
830
+ };
831
+ const dispatchSubmittedPrompt = (submitted, runContext) => opts.expandSlashCommands === false ? Promise.resolve({ kind: "passthrough", prompt: submitted }) : resolvePrintCommand(submitted, {
832
+ config,
833
+ mode,
834
+ sessionId: runtimeSessionId,
835
+ signal: opts.signal,
836
+ agents: {
837
+ // Built only when an agent-backed command (`/review`) asks for it,
838
+ // and attached to this session's runtime so it is disposed with the
839
+ // run. Its events join the same stream-json output as the main loop,
840
+ // and its finished runs land in `runs` like any other execution.
841
+ manager: () => getOrCreateAgentManager(config, registry.getDefinitions(), {
842
+ runtime,
843
+ permissionMode: mode,
844
+ eventSink: (event) => {
845
+ if (event.type === "agent_result") recordManagedRunResult(event.agent);
846
+ emitAgentEvent(event, opts, emit);
847
+ },
848
+ hookEventSink: createHookEventHandler(opts, emit)
849
+ }),
850
+ runContext
851
+ }
852
+ });
853
+ const commandResults = [];
854
+ let commandUsage = null;
855
+ const prompts = [];
856
+ if (opts.inputFormat === "text") {
857
+ const prompt = opts.prompt ?? await readPipedPrompt(opts.stdin ?? process.stdin);
858
+ if (!prompt) {
859
+ throw new Error(
860
+ "print mode requires a prompt: pass one to -p, pipe one on stdin, or use --input-format stream-json"
861
+ );
862
+ }
863
+ prompts.push(prompt);
864
+ } else {
865
+ const stream = opts.stdin ?? process.stdin;
866
+ const diagnostics = [];
867
+ const parser = createStreamParser(
868
+ (event) => {
869
+ if (event.type === "user") prompts.push(event.content);
870
+ },
871
+ {
872
+ maxBufferedLineBytes: opts.maxInputLineBytes,
873
+ onDiagnostic: (diagnostic) => diagnostics.push(diagnostic)
874
+ }
875
+ );
876
+ for await (const chunk of stream) {
877
+ parser.feed(chunk);
878
+ }
879
+ parser.flush();
880
+ if (diagnostics.length > 0) throw new Error(diagnostics[0]?.message);
881
+ if (opts.prompt) prompts.unshift(opts.prompt);
882
+ }
883
+ let carriedSpend = opts.carriedUsage ? {
884
+ usage: opts.carriedUsage,
885
+ costUsd: carriedCostUsd(
886
+ opts.carriedUsage,
887
+ opts.carriedModels?.length ? opts.carriedModels : [config.model]
888
+ )
889
+ } : void 0;
890
+ for (const submitted of prompts) {
891
+ const runId = crypto.randomUUID();
892
+ const runContext = createAgentRunContext({
893
+ sessionId: runtimeSessionId,
894
+ runId,
895
+ source: opts.runSource ?? "headless",
896
+ resumedFromRunId: runResults.length === 0 ? opts.resumedFromRunId ?? (opts.history.length > 0 ? [...opts.history].reverse().find((message) => message.role === "user")?.id : void 0) : void 0
897
+ });
898
+ runtime.runAccounting.startRoot(runContext, opts.maxBudgetUsd);
899
+ if (carriedSpend) {
900
+ runtime.runAccounting.seedRoot(runContext.rootRunId, carriedSpend);
901
+ }
902
+ const dispatch = await dispatchSubmittedPrompt(submitted, runContext);
903
+ if (dispatch.kind === "prompt" && dispatch.shellErrors?.length) {
904
+ for (const shellError of dispatch.shellErrors) {
905
+ process.stderr.write(`warning: ${shellError}
906
+ `);
907
+ }
908
+ }
909
+ if (dispatch.kind === "handled") {
910
+ const handled = {
911
+ command: dispatch.command,
912
+ output: dispatch.output,
913
+ data: dispatch.data
914
+ };
915
+ commandResults.push(handled);
916
+ const accounting = runtime.runAccounting.snapshotRoot(runContext.rootRunId);
917
+ commandUsage = accounting.inclusiveUsage ?? commandUsage;
918
+ recordRunResult({
919
+ context: runContext,
920
+ outcome: createTerminalOutcome("completed", "normal_completion", {
921
+ partialOutput: false
922
+ }),
923
+ // The host itself made no provider call; everything spent here was
924
+ // spent by the agents this command ran, which is the inclusive figure.
925
+ usage: accounting.inclusiveUsage,
926
+ accounting,
927
+ ambient: runtime.snapshotRunAmbient(runContext.runId)
928
+ });
929
+ if (opts.outputFormat === "text") {
930
+ if (dispatch.output) stdout.write(`${dispatch.output}
931
+ `);
932
+ } else if (opts.outputFormat === "stream-json" && (dispatch.output !== void 0 || dispatch.data !== void 0)) {
933
+ emit({ type: "command_result", ...handled });
934
+ }
935
+ continue;
936
+ }
937
+ const prompt = dispatch.prompt;
938
+ const commandContext = dispatch.kind === "prompt" ? dispatch.commandContext : void 0;
939
+ const userMessage = {
940
+ id: runId,
941
+ role: "user",
942
+ content: prompt,
943
+ // With a command context, `prompt` is a resolved command body, not typed
944
+ // input -- repository-authored for a project command. Marking it keeps
945
+ // print and SDK runs on the same footing as the TUI.
946
+ derivedContent: commandContext ? true : void 0,
947
+ includeInContext: true,
948
+ kind: "conversation",
949
+ timestamp: Date.now()
950
+ };
951
+ const contextLimit = resolveContextLimit(config);
952
+ const hostCompactAttemptKey = `${usagePressureTokens(lastUsage)}:${contextHistory.length}`;
953
+ if (config.autoCompactEnabled !== false && contextLimit != null && shouldCompact(lastUsage, contextLimit) && lastHostCompactAttemptKey !== hostCompactAttemptKey) {
954
+ lastHostCompactAttemptKey = hostCompactAttemptKey;
955
+ try {
956
+ const outcome2 = await agentSession.compact({
957
+ config,
958
+ history: contextHistory,
959
+ compactBoundaries,
960
+ sessionId,
961
+ transcriptOrdinal: transcript.length,
962
+ runContext,
963
+ runtime,
964
+ timelineStore: store,
965
+ onCommitted: (compactResult, boundary) => {
966
+ contextHistory.length = 0;
967
+ contextHistory.push(...compactResult.replacementHistory);
968
+ compactBoundaries.push(boundary);
969
+ lastUsage = null;
970
+ },
971
+ options: {
972
+ trigger: "auto",
973
+ preContextTokens: usagePressureTokens(lastUsage),
974
+ signal: opts.signal,
975
+ upcomingUserIntent: prompt,
976
+ onHookEvent: createHookEventHandler(opts, emit)
977
+ }
978
+ });
979
+ if (outcome2.result.status === "compacted") {
980
+ emitCompactBoundary(opts.outputFormat, emit, outcome2.result);
981
+ }
982
+ } catch {
983
+ }
984
+ }
985
+ const recorded = await agentSession.recordUserMessage({
986
+ config,
987
+ sessionId: runtimeSessionId,
988
+ displayMessage: prompt,
989
+ userMessage,
990
+ sessionName,
991
+ timelineStore: store && sessionId ? store : void 0,
992
+ expandShellInput: false,
993
+ runtime,
994
+ signal: opts.signal
995
+ });
996
+ sessionName = recorded.sessionName;
997
+ const contextMessage = recorded.contextMessage;
998
+ transcript.push(userMessage);
999
+ rootContexts.set(runContext.rootRunId, runContext);
1000
+ await runParentTurn(contextMessage, prompt, userMessage, runContext, commandContext);
1001
+ const spentSoFar = runtime.runAccounting.snapshotRoot(runContext.rootRunId);
1002
+ carriedSpend = {
1003
+ usage: spentSoFar.inclusiveUsage,
1004
+ costUsd: spentSoFar.inclusiveCostUsd
1005
+ };
1006
+ if (planNotApplied.outcome) break;
1007
+ }
1008
+ const managedAgentManager = planNotApplied.outcome ? void 0 : runtime.agentManager;
1009
+ if (managedAgentManager) {
1010
+ const deliveredCompletionIds = new Set(
1011
+ transcript.flatMap(
1012
+ (message) => (message.agentNotifications ?? []).map((notification) => notification.deliveryId).filter((id) => Boolean(id))
1013
+ )
1014
+ );
1015
+ while (true) {
1016
+ await managedAgentManager.waitForIdle();
1017
+ const pending = (await managedAgentManager.listPendingCompletions()).filter(
1018
+ (notification) => notification.parentSessionId === runtimeSessionId
1019
+ );
1020
+ if (pending.length === 0) break;
1021
+ const alreadyDelivered = pending.filter(
1022
+ (notification) => deliveredCompletionIds.has(notification.deliveryId)
1023
+ );
1024
+ for (const notification of alreadyDelivered) {
1025
+ await managedAgentManager.acknowledgeCompletion(notification.deliveryId);
1026
+ }
1027
+ const fresh = pending.filter(
1028
+ (notification) => !deliveredCompletionIds.has(notification.deliveryId)
1029
+ );
1030
+ if (fresh.length === 0) continue;
1031
+ const firstRootRunId = fresh[0]?.rootRunId;
1032
+ const sameRoot = firstRootRunId ? fresh.filter((notification) => notification.rootRunId === firstRootRunId) : fresh;
1033
+ const batch = takeAgentCompletionBatch(sameRoot);
1034
+ for (const notification of batch) recordManagedCompletionResult(notification);
1035
+ const built = batch.map(
1036
+ (notification) => buildAgentCompletionMessage(notification)
1037
+ );
1038
+ const displayMessage = built.map((item) => item.displayMessage).join("\n");
1039
+ const userMessage = {
1040
+ id: crypto.randomUUID(),
1041
+ role: "user",
1042
+ content: displayMessage,
1043
+ contextContent: built.map((item) => item.contextMessage).join("\n"),
1044
+ includeInContext: true,
1045
+ kind: "agent-notification",
1046
+ agentNotifications: built.map((item) => item.display),
1047
+ timestamp: Date.now()
1048
+ };
1049
+ const recorded = await agentSession.recordUserMessage({
1050
+ config,
1051
+ sessionId: runtimeSessionId,
1052
+ displayMessage,
1053
+ contextMessage: userMessage.contextContent,
1054
+ userMessage,
1055
+ timelineStore: store && sessionId ? store : void 0,
1056
+ expandShellInput: false,
1057
+ runtime,
1058
+ signal: opts.signal
1059
+ });
1060
+ transcript.push(userMessage);
1061
+ const parentContext = batch[0]?.rootRunId ? rootContexts.get(batch[0].rootRunId) : void 0;
1062
+ const runContext = createAgentRunContext({
1063
+ sessionId: runtimeSessionId,
1064
+ source: opts.runSource ?? "headless",
1065
+ rootRunId: parentContext?.rootRunId ?? batch[0]?.rootRunId,
1066
+ parentRunId: batch[0]?.runId ?? parentContext?.runId
1067
+ });
1068
+ if (parentContext) rootContexts.set(runContext.rootRunId, parentContext);
1069
+ else rootContexts.set(runContext.rootRunId, runContext);
1070
+ await runParentTurn(recorded.contextMessage, displayMessage, userMessage, runContext);
1071
+ for (const notification of batch) {
1072
+ deliveredCompletionIds.add(notification.deliveryId);
1073
+ await managedAgentManager.acknowledgeCompletion(notification.deliveryId);
1074
+ }
1075
+ }
1076
+ }
1077
+ const outcome = lastOutcome ?? (commandResults.length > 0 ? createTerminalOutcome("completed", "normal_completion", { partialOutput: false }) : createTerminalOutcome("interrupted", "missing_terminal", {
1078
+ partialOutput: transcript.some((message) => message.role === "assistant")
1079
+ }));
1080
+ runResults.sort((left, right) => left.context.startedAt - right.context.startedAt);
1081
+ const reportedUsage = lastUsage ?? commandUsage;
1082
+ const result = {
1083
+ messages: contextHistory,
1084
+ transcript,
1085
+ compactBoundaries,
1086
+ usage: reportedUsage,
1087
+ accounting: runtime.runAccounting.snapshotAll(),
1088
+ outcome,
1089
+ runs: runResults,
1090
+ sessionId,
1091
+ plan: planNotApplied.outcome,
1092
+ commandResults
1093
+ };
1094
+ if (opts.jsonSchema) {
1095
+ const text = lastAssistantText(contextHistory);
1096
+ try {
1097
+ result.structured = JSON.parse(text);
1098
+ } catch {
1099
+ result.structuredError = "Failed to parse JSON from assistant output";
1100
+ }
1101
+ }
1102
+ if (opts.promptSuggestions && opts.outputFormat === "stream-json") {
1103
+ try {
1104
+ const suggestions = await generatePromptSuggestions(
1105
+ config,
1106
+ registry,
1107
+ contextHistory,
1108
+ opts.signal,
1109
+ store?.readImageAttachment ? (attachment) => store.readImageAttachment(runtimeSessionId, attachment) : void 0
1110
+ );
1111
+ if (suggestions.length > 0) {
1112
+ emit({ type: "prompt_suggestions", suggestions });
1113
+ }
1114
+ } catch {
1115
+ }
1116
+ }
1117
+ if (opts.outputFormat === "text") {
1118
+ const stopped = planNotApplied.outcome;
1119
+ if (stopped) {
1120
+ stdout.write(`${stopped.plan}
1121
+
1122
+ ${stopped.message}
1123
+ `);
1124
+ } else {
1125
+ const last = lastAssistantText(contextHistory);
1126
+ if (last) stdout.write(last + "\n");
1127
+ }
1128
+ } else if (opts.outputFormat === "json") {
1129
+ emit({
1130
+ result: {
1131
+ messages: contextHistory,
1132
+ usage: reportedUsage,
1133
+ accounting: result.accounting,
1134
+ outcome,
1135
+ stopReason: outcome.reason,
1136
+ runs: runResults,
1137
+ structured: result.structured,
1138
+ structuredError: result.structuredError,
1139
+ plan: result.plan,
1140
+ commandResults
1141
+ }
1142
+ });
1143
+ } else if (opts.outputFormat === "stream-json") {
1144
+ emit({
1145
+ type: "result",
1146
+ stopReason: outcome.reason,
1147
+ result: {
1148
+ messages: contextHistory,
1149
+ usage: reportedUsage,
1150
+ accounting: result.accounting,
1151
+ outcome,
1152
+ stopReason: outcome.reason,
1153
+ runs: runResults,
1154
+ structured: result.structured,
1155
+ structuredError: result.structuredError,
1156
+ plan: result.plan,
1157
+ commandResults
1158
+ }
1159
+ });
1160
+ }
1161
+ if (sessionId) {
1162
+ await agentSession.endLifecycle(config, sessionId, "completion", {
1163
+ onHookEvent: createHookEventHandler(opts, emit)
1164
+ });
1165
+ }
1166
+ return result;
1167
+ } finally {
1168
+ disposeCrashHandlers?.();
1169
+ agentSession.dispose("headless_complete");
1170
+ }
1171
+ }
1172
+ function lastAssistantText(history) {
1173
+ for (let i = history.length - 1; i >= 0; i--) {
1174
+ const m = history[i];
1175
+ if (m.role === "assistant" && m.content) return m.content;
1176
+ }
1177
+ return "";
1178
+ }
1179
+ function planApprovalStatus(decision) {
1180
+ return typeof decision === "string" ? decision : decision.decision;
1181
+ }
1182
+ function createHookEventHandler(opts, emit) {
1183
+ if (!opts.includeHookEvents || opts.outputFormat !== "stream-json") return void 0;
1184
+ return (event, payload) => emit({ type: "hook_event", event, ...payload });
1185
+ }
1186
+ function emitAgentEvent(event, opts, emit) {
1187
+ if (event.type === "terminal") return;
1188
+ if (event.type === "agent_text_delta" && opts.forwardSubagentText !== true) return;
1189
+ opts.onAgentEvent?.(event);
1190
+ if (event.type === "error") {
1191
+ if (opts.outputFormat === "stream-json") emit({ type: "error", error: event.error });
1192
+ else process.stderr.write(`error: ${event.error}
1193
+ `);
1194
+ return;
1195
+ }
1196
+ if (opts.outputFormat !== "stream-json") return;
1197
+ switch (event.type) {
1198
+ case "run_started":
1199
+ emit({ type: "run_start", run: event.context, ambient: event.ambient });
1200
+ break;
1201
+ case "text":
1202
+ if (opts.includePartialMessages === true)
1203
+ emit({ type: "assistant", text: event.content, complete: false });
1204
+ break;
1205
+ case "reasoning":
1206
+ if (opts.includePartialMessages === true) emit({ type: "reasoning", text: event.content });
1207
+ break;
1208
+ case "tool_use":
1209
+ emit({ type: "tool_use", tool_call: event.toolCall });
1210
+ break;
1211
+ case "tool_result":
1212
+ emit({ type: "tool_result", tool_result: event.toolResult });
1213
+ break;
1214
+ case "user_question":
1215
+ emit({ type: "user_question", request: event.request, status: event.status });
1216
+ break;
1217
+ case "user_question_result":
1218
+ emit({
1219
+ type: "user_question_result",
1220
+ request_id: event.requestId,
1221
+ response: event.response
1222
+ });
1223
+ break;
1224
+ case "agent_start":
1225
+ case "background_job_start":
1226
+ case "background_job_update":
1227
+ case "background_job_output":
1228
+ case "background_job_result":
1229
+ case "background_job_dismiss":
1230
+ case "agent_update":
1231
+ case "agent_result":
1232
+ case "agent_question":
1233
+ case "agent_status":
1234
+ case "agent_activity":
1235
+ case "agent_text_delta":
1236
+ case "agent_message":
1237
+ case "agent_completion":
1238
+ case "agent_permission":
1239
+ case "agent_apply":
1240
+ case "evidence_update":
1241
+ case "skill_lifecycle":
1242
+ case "attempt_discarded":
1243
+ emit(event);
1244
+ break;
1245
+ case "system":
1246
+ case "session":
1247
+ case "result":
1248
+ case "done":
1249
+ break;
1250
+ }
1251
+ }
1252
+ function emitCompactBoundary(outputFormat, emit, result) {
1253
+ if (outputFormat !== "stream-json") return;
1254
+ emit({
1255
+ type: "system",
1256
+ subtype: "compact_boundary",
1257
+ trigger: result.trigger,
1258
+ pre_tokens: result.preContextTokens,
1259
+ compact_id: result.compactId,
1260
+ generation: result.generation,
1261
+ pre_messages: result.preMessageCount,
1262
+ post_messages: result.replacementHistory.length,
1263
+ post_tokens: result.postContextTokens,
1264
+ checkpoint_version: 2,
1265
+ strategy: result.strategy,
1266
+ model_calls: result.modelCalls,
1267
+ degraded: result.degraded,
1268
+ coverage_status: result.checkpoint.coverage?.status ?? "complete",
1269
+ // This generation's status above; the conversation's accumulated one here.
1270
+ coverage_lifetime_status: result.checkpoint.coverage?.lifetime?.status ?? "complete",
1271
+ coverage: result.checkpoint.coverage,
1272
+ warning: result.warning
1273
+ });
1274
+ }
1275
+ async function generatePromptSuggestions(config, _registry, history, signal, resolveAttachment) {
1276
+ const { chatCompletionStream } = await import("./openai-compatible-RHZUBHV6.js");
1277
+ const { buildMessages } = await import("./context-ZC5IFEAX.js");
1278
+ const suggestionMessages = await buildMessages(
1279
+ config,
1280
+ [
1281
+ ...history,
1282
+ {
1283
+ id: crypto.randomUUID(),
1284
+ role: "user",
1285
+ content: "Based on the conversation above, suggest 1-3 follow-up prompts the user might want to ask next. Keep each suggestion under 80 characters. Return ONLY a JSON array of strings, no other text.",
1286
+ includeInContext: true,
1287
+ timestamp: Date.now()
1288
+ }
1289
+ ],
1290
+ void 0,
1291
+ void 0,
1292
+ signal,
1293
+ void 0,
1294
+ void 0,
1295
+ resolveAttachment
1296
+ );
1297
+ const stream = chatCompletionStream(config, suggestionMessages, [], { signal });
1298
+ let content = "";
1299
+ for await (const event of stream) {
1300
+ if (event.type === "text" && event.content) {
1301
+ content += event.content;
1302
+ }
1303
+ }
1304
+ try {
1305
+ const jsonMatch = content.match(/\[[\s\S]*?\]/);
1306
+ if (jsonMatch) {
1307
+ const parsed = JSON.parse(jsonMatch[0]);
1308
+ if (Array.isArray(parsed)) {
1309
+ return parsed.filter((s) => typeof s === "string" && s.length > 0).slice(0, 3);
1310
+ }
1311
+ }
1312
+ } catch {
1313
+ const quoted = content.match(/"([^"]+)"/g);
1314
+ if (quoted) {
1315
+ return quoted.map((s) => s.replace(/^"|"$/g, "")).slice(0, 3);
1316
+ }
1317
+ }
1318
+ return [];
1319
+ }
1320
+
1321
+ // src/mcp.ts
1322
+ import spawn from "cross-spawn";
1323
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1324
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
1325
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1326
+ import { ReadBuffer, serializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
1327
+ import {
1328
+ ElicitRequestSchema,
1329
+ ErrorCode,
1330
+ McpError,
1331
+ ToolListChangedNotificationSchema
1332
+ } from "@modelcontextprotocol/sdk/types.js";
1333
+ var DEFAULT_INITIALIZATION_TIMEOUT_MS = 1e4;
1334
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
1335
+ var DEFAULT_MAX_STDERR_BYTES = 8192;
1336
+ function waitForProcessExit(child, timeoutMs) {
1337
+ if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
1338
+ return new Promise((resolve) => {
1339
+ let settled = false;
1340
+ const finish = (exited) => {
1341
+ if (settled) return;
1342
+ settled = true;
1343
+ clearTimeout(timer);
1344
+ child.removeListener("exit", onExit);
1345
+ resolve(exited);
1346
+ };
1347
+ const onExit = () => finish(true);
1348
+ const timer = setTimeout(() => finish(false), timeoutMs);
1349
+ timer.unref();
1350
+ child.once("exit", onExit);
1351
+ });
1352
+ }
1353
+ async function terminateProcess(child) {
1354
+ child.stdin?.end();
1355
+ if (child.exitCode === null && child.signalCode === null) child.kill();
1356
+ if (!await waitForProcessExit(child, 250)) {
1357
+ child.kill("SIGKILL");
1358
+ await waitForProcessExit(child, 750);
1359
+ }
1360
+ child.stdin?.destroy();
1361
+ child.stdout?.destroy();
1362
+ child.stderr?.destroy();
1363
+ child.unref();
1364
+ }
1365
+ var StdioProcessTransport = class {
1366
+ constructor(name, config, options) {
1367
+ this.name = name;
1368
+ this.config = config;
1369
+ this.options = options;
1370
+ }
1371
+ name;
1372
+ config;
1373
+ options;
1374
+ onclose;
1375
+ onerror;
1376
+ onmessage;
1377
+ child;
1378
+ readBuffer = new ReadBuffer();
1379
+ stderrTail = "";
1380
+ closed = false;
1381
+ cleanupListeners = () => {
1382
+ };
1383
+ protocolVersion;
1384
+ get process() {
1385
+ return this.child;
1386
+ }
1387
+ get stderr() {
1388
+ return this.stderrTail;
1389
+ }
1390
+ setProtocolVersion(version) {
1391
+ this.protocolVersion = version;
1392
+ }
1393
+ start() {
1394
+ if (this.child) {
1395
+ return Promise.reject(new Error(`MCP server ${this.name} transport already started`));
1396
+ }
1397
+ if (!this.config.command) {
1398
+ return Promise.reject(new Error(`MCP server ${this.name} is not a stdio server`));
1399
+ }
1400
+ const child = spawn(this.config.command, this.config.args ?? [], {
1401
+ env: { ...process.env, ...this.config.env },
1402
+ cwd: this.config.cwd,
1403
+ stdio: ["pipe", "pipe", "pipe"]
1404
+ });
1405
+ this.child = child;
1406
+ this.options.onSpawn?.(child);
1407
+ const onStdoutData = (data) => {
1408
+ this.readBuffer.append(data);
1409
+ while (true) {
1410
+ let message;
1411
+ try {
1412
+ message = this.readBuffer.readMessage();
1413
+ } catch (error) {
1414
+ this.onerror?.(error instanceof Error ? error : new Error(String(error)));
1415
+ continue;
1416
+ }
1417
+ if (!message) break;
1418
+ this.onmessage?.(message);
1419
+ }
1420
+ };
1421
+ const onStderrData = (data) => {
1422
+ this.stderrTail = (this.stderrTail + data.toString()).slice(-this.options.maxStderrBytes);
1423
+ };
1424
+ const onChildError = (error) => {
1425
+ this.onerror?.(error);
1426
+ this.teardown();
1427
+ };
1428
+ const onStdinError = () => {
1429
+ };
1430
+ const onExit = () => this.teardown();
1431
+ const onClose = () => this.teardown();
1432
+ this.cleanupListeners = () => {
1433
+ child.stdout?.removeListener("data", onStdoutData);
1434
+ child.stderr?.removeListener("data", onStderrData);
1435
+ child.removeListener("error", onChildError);
1436
+ child.stdin?.removeListener("error", onStdinError);
1437
+ child.removeListener("exit", onExit);
1438
+ child.removeListener("close", onClose);
1439
+ this.cleanupListeners = () => {
1440
+ };
1441
+ };
1442
+ child.stdout?.on("data", onStdoutData);
1443
+ child.stderr?.on("data", onStderrData);
1444
+ child.once("error", onChildError);
1445
+ child.stdin?.on("error", onStdinError);
1446
+ child.once("exit", onExit);
1447
+ child.once("close", onClose);
1448
+ return Promise.resolve();
1449
+ }
1450
+ send(message) {
1451
+ return new Promise((resolve, reject) => {
1452
+ const stdin = this.child?.stdin;
1453
+ if (this.closed || !stdin || stdin.destroyed) {
1454
+ reject(new Error(`MCP server ${this.name} stdin is closed`));
1455
+ return;
1456
+ }
1457
+ try {
1458
+ stdin.write(serializeMessage(message), (error) => {
1459
+ if (error) reject(error);
1460
+ else resolve();
1461
+ });
1462
+ } catch (error) {
1463
+ reject(error instanceof Error ? error : new Error(String(error)));
1464
+ }
1465
+ });
1466
+ }
1467
+ /** Child died or streams closed underneath us: settle pending work, then reap. */
1468
+ teardown() {
1469
+ if (this.closed) return;
1470
+ this.closed = true;
1471
+ this.cleanupListeners();
1472
+ this.onclose?.();
1473
+ if (this.child) void terminateProcess(this.child).catch(() => {
1474
+ });
1475
+ }
1476
+ async close() {
1477
+ if (this.closed) return;
1478
+ this.closed = true;
1479
+ this.cleanupListeners();
1480
+ this.onclose?.();
1481
+ if (this.child) await terminateProcess(this.child).catch(() => {
1482
+ });
1483
+ }
1484
+ };
1485
+ function isMcpErrorWithCode(error, code) {
1486
+ return error instanceof McpError && error.code === code;
1487
+ }
1488
+ function withStderrContext(message, transport) {
1489
+ const tail = transport.stderr.trim();
1490
+ return tail ? `${message} (stderr: ${tail})` : message;
1491
+ }
1492
+ function createSecretRedactor(config) {
1493
+ let queryValues = [];
1494
+ try {
1495
+ queryValues = config.url ? [...new URL(config.url).searchParams.values()] : [];
1496
+ } catch {
1497
+ }
1498
+ const secrets = [
1499
+ ...Object.values(config.env ?? {}),
1500
+ ...Object.values(config.headers ?? {}),
1501
+ ...queryValues
1502
+ ].filter((value) => value.length > 0).sort((left, right) => right.length - left.length);
1503
+ return (message) => {
1504
+ let redacted = message;
1505
+ for (const secret of secrets) redacted = redacted.replaceAll(secret, "<redacted>");
1506
+ return redacted;
1507
+ };
1508
+ }
1509
+ function renderMcpContent(result) {
1510
+ const blocks = Array.isArray(result.content) ? result.content : [];
1511
+ const parts = [];
1512
+ for (const block of blocks) {
1513
+ if (!block || typeof block !== "object") continue;
1514
+ switch (block.type) {
1515
+ case "text":
1516
+ if (typeof block.text === "string") parts.push(block.text);
1517
+ break;
1518
+ case "image":
1519
+ case "audio":
1520
+ parts.push(`[${block.type}${block.mimeType ? `: ${block.mimeType}` : ""}]`);
1521
+ break;
1522
+ case "resource_link":
1523
+ parts.push(`[resource_link: ${block.uri ?? "unknown"}]`);
1524
+ break;
1525
+ case "resource": {
1526
+ const resource = block.resource;
1527
+ if (resource && typeof resource.text === "string") parts.push(resource.text);
1528
+ else if (resource?.uri) {
1529
+ parts.push(
1530
+ `[resource: ${resource.uri}${resource.mimeType ? ` (${resource.mimeType})` : ""}]`
1531
+ );
1532
+ }
1533
+ break;
1534
+ }
1535
+ default:
1536
+ break;
1537
+ }
1538
+ }
1539
+ if (parts.length === 0 && result.structuredContent !== void 0) {
1540
+ parts.push(JSON.stringify(result.structuredContent, null, 2));
1541
+ }
1542
+ return { text: parts.join("\n"), isError: result.isError === true };
1543
+ }
1544
+ async function listAllTools(client, requestTimeoutMs, signal) {
1545
+ const tools = [];
1546
+ let cursor;
1547
+ do {
1548
+ const page = await client.listTools(cursor ? { cursor } : void 0, {
1549
+ timeout: requestTimeoutMs,
1550
+ signal
1551
+ });
1552
+ for (const tool of page.tools) {
1553
+ tools.push({
1554
+ name: tool.name,
1555
+ description: tool.description ?? "",
1556
+ inputSchema: tool.inputSchema ?? { type: "object", properties: {} }
1557
+ });
1558
+ }
1559
+ cursor = page.nextCursor;
1560
+ } while (cursor);
1561
+ return tools;
1562
+ }
1563
+ function isRemoteConfig(config) {
1564
+ return config.type === "http" || config.type === "sse" || Boolean(config.url && !config.command);
1565
+ }
1566
+ function createTransport(name, cfg, options) {
1567
+ if (isRemoteConfig(cfg)) {
1568
+ if (!cfg.url) throw new Error(`MCP server ${name} has no URL`);
1569
+ const url = new URL(cfg.url);
1570
+ const headers = { ...cfg.headers ?? {} };
1571
+ if (cfg.type === "sse") {
1572
+ let protocolVersion;
1573
+ const transport3 = new SSEClientTransport(url, {
1574
+ // The SDK derives the SSE GET headers from requestInit too. Supplying
1575
+ // them here therefore covers both the initial stream and later POSTs.
1576
+ requestInit: { headers }
1577
+ });
1578
+ const sdkSetProtocolVersion = transport3.setProtocolVersion.bind(transport3);
1579
+ transport3.setProtocolVersion = (version) => {
1580
+ protocolVersion = version;
1581
+ sdkSetProtocolVersion(version);
1582
+ };
1583
+ return {
1584
+ transport: transport3,
1585
+ getStderr: () => "",
1586
+ getProtocolVersion: () => protocolVersion
1587
+ };
1588
+ }
1589
+ const transport2 = new StreamableHTTPClientTransport(url, {
1590
+ requestInit: { headers }
1591
+ });
1592
+ return {
1593
+ transport: transport2,
1594
+ getStderr: () => "",
1595
+ getProtocolVersion: () => transport2.protocolVersion
1596
+ };
1597
+ }
1598
+ const transport = new StdioProcessTransport(name, cfg, {
1599
+ maxStderrBytes: options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES,
1600
+ onSpawn: (child) => options.onProcessSpawn?.(name, child)
1601
+ });
1602
+ return {
1603
+ transport,
1604
+ getStderr: () => transport.stderr,
1605
+ getProtocolVersion: () => transport.protocolVersion
1606
+ };
1607
+ }
1608
+ function registerElicitationHandler(client, name, options) {
1609
+ const onElicit = options.onElicit;
1610
+ if (!onElicit) return;
1611
+ let counter = 0;
1612
+ client.setRequestHandler(
1613
+ ElicitRequestSchema,
1614
+ async (request, extra) => {
1615
+ const params = request.params;
1616
+ if (params.mode === "url") {
1617
+ options.onDiagnostic({
1618
+ level: "warn",
1619
+ message: `MCP server "${name}" requested URL elicitation, which Book does not support`,
1620
+ server: name
1621
+ });
1622
+ return { action: "decline" };
1623
+ }
1624
+ const converted = toElicitationFields(params.requestedSchema);
1625
+ if ("error" in converted) {
1626
+ options.onDiagnostic({
1627
+ level: "warn",
1628
+ message: `Declined an elicitation from MCP server "${name}": ${converted.error}`,
1629
+ server: name
1630
+ });
1631
+ return { action: "decline" };
1632
+ }
1633
+ const response = await onElicit(
1634
+ {
1635
+ id: `${name}:${++counter}`,
1636
+ server: name,
1637
+ message: params.message,
1638
+ fields: converted.fields
1639
+ },
1640
+ { signal: extra?.signal }
1641
+ );
1642
+ if (response.action !== "accept") return { action: response.action };
1643
+ const validated = validateElicitationContent(converted.fields, response.content);
1644
+ if ("error" in validated) {
1645
+ options.onDiagnostic({
1646
+ level: "warn",
1647
+ message: `Discarded an elicitation answer for MCP server "${name}": ${validated.error}`,
1648
+ server: name
1649
+ });
1650
+ return { action: "cancel" };
1651
+ }
1652
+ return { action: "accept", content: validated.content };
1653
+ }
1654
+ );
1655
+ }
1656
+ async function connectMcpServer(name, cfg, options) {
1657
+ let handle;
1658
+ let liveConnection;
1659
+ let transportClosed = false;
1660
+ const redact = createSecretRedactor(cfg);
1661
+ try {
1662
+ handle = createTransport(name, cfg, options);
1663
+ const { transport, getStderr, getProtocolVersion } = handle;
1664
+ transport.onclose = () => {
1665
+ transportClosed = true;
1666
+ if (!liveConnection || liveConnection.closed) return;
1667
+ liveConnection.closed = true;
1668
+ options.onConnectionClosed?.(name);
1669
+ };
1670
+ transport.onerror = (error) => {
1671
+ options.onDiagnostic({
1672
+ level: "warn",
1673
+ message: redact(`MCP server "${name}" transport error: ${error.message}`),
1674
+ server: name
1675
+ });
1676
+ };
1677
+ const client = new Client(
1678
+ { name: "book", version: getPackageVersion() },
1679
+ { capabilities: options.onElicit ? { elicitation: { form: {} } } : {} }
1680
+ );
1681
+ registerElicitationHandler(client, name, options);
1682
+ await client.connect(transport, {
1683
+ timeout: options.initializationTimeoutMs,
1684
+ signal: options.signal
1685
+ });
1686
+ const supportsTools = client.getServerCapabilities()?.tools !== void 0;
1687
+ const tools = supportsTools ? await listAllTools(client, options.requestTimeoutMs, options.signal) : [];
1688
+ if (transportClosed) throw new Error("connection closed during initialization");
1689
+ const conn = {
1690
+ name,
1691
+ client,
1692
+ process: transport.process,
1693
+ tools,
1694
+ pending: /* @__PURE__ */ new Map(),
1695
+ closed: false,
1696
+ requestCounter: 0,
1697
+ redact,
1698
+ get stderr() {
1699
+ return redact(getStderr());
1700
+ },
1701
+ protocolVersion: getProtocolVersion(),
1702
+ serverInfo: client.getServerVersion(),
1703
+ capabilities: client.getServerCapabilities(),
1704
+ async close() {
1705
+ try {
1706
+ await transport.terminateSession?.();
1707
+ } catch (error) {
1708
+ options.onDiagnostic({
1709
+ level: "warn",
1710
+ message: redact(
1711
+ `Failed to terminate MCP server "${name}" session: ${error instanceof Error ? error.message : String(error)}`
1712
+ ),
1713
+ server: name
1714
+ });
1715
+ } finally {
1716
+ await client.close();
1717
+ }
1718
+ }
1719
+ };
1720
+ liveConnection = conn;
1721
+ if (supportsTools) {
1722
+ let refreshing = false;
1723
+ client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
1724
+ if (refreshing || conn.closed) return;
1725
+ refreshing = true;
1726
+ try {
1727
+ conn.tools = await listAllTools(client, options.requestTimeoutMs);
1728
+ options.onToolListChanged?.(name);
1729
+ } catch (error) {
1730
+ options.onDiagnostic({
1731
+ level: "warn",
1732
+ message: redact(
1733
+ `Failed to refresh tools for MCP server "${name}": ${error instanceof Error ? error.message : String(error)}`
1734
+ ),
1735
+ server: name
1736
+ });
1737
+ } finally {
1738
+ refreshing = false;
1739
+ }
1740
+ });
1741
+ }
1742
+ return conn;
1743
+ } catch (error) {
1744
+ const message = redact(error instanceof Error ? error.message : String(error));
1745
+ options.onDiagnostic({
1746
+ level: "warn",
1747
+ message: withStderrContext(`Failed to connect to MCP server "${name}": ${message}`, {
1748
+ stderr: redact(handle?.getStderr() ?? "")
1749
+ }),
1750
+ server: name
1751
+ });
1752
+ if (handle?.transport.terminateSession) {
1753
+ await handle.transport.terminateSession().catch(() => {
1754
+ });
1755
+ }
1756
+ await handle?.transport.close().catch(() => {
1757
+ });
1758
+ return null;
1759
+ }
1760
+ }
1761
+ function buildToolDefinition(conn, tool, requestTimeoutMs) {
1762
+ const prefixedName = `mcp__${conn.name}__${tool.name}`;
1763
+ const live = conn;
1764
+ return {
1765
+ name: prefixedName,
1766
+ description: `[MCP:${conn.name}] ${tool.description || tool.name}`,
1767
+ parameters: tool.inputSchema || { type: "object", properties: {} },
1768
+ execute: async (args, ctx) => {
1769
+ const requestId = ++live.requestCounter;
1770
+ conn.pending.set(requestId, { method: tool.name });
1771
+ try {
1772
+ const result = await conn.client.callTool({ name: tool.name, arguments: args }, void 0, {
1773
+ timeout: requestTimeoutMs,
1774
+ resetTimeoutOnProgress: true,
1775
+ signal: ctx.signal
1776
+ });
1777
+ const rendered = renderMcpContent(result);
1778
+ if (rendered.isError) {
1779
+ return toolFailure(
1780
+ `MCP tool ${prefixedName} failed: ${rendered.text || "the server reported an error"}`,
1781
+ { code: "mcp_tool_error", content: rendered.text }
1782
+ );
1783
+ }
1784
+ return toolSuccess(rendered.text, { data: result });
1785
+ } catch (error) {
1786
+ let message;
1787
+ if (ctx.signal?.aborted) {
1788
+ message = `MCP request aborted: ${tool.name}`;
1789
+ } else if (conn.closed && isMcpErrorWithCode(error, ErrorCode.ConnectionClosed)) {
1790
+ message = `MCP server ${conn.name} disconnected`;
1791
+ } else {
1792
+ message = live.redact(error instanceof Error ? error.message : String(error));
1793
+ }
1794
+ const tail = conn.stderr.trim();
1795
+ return toolFailure(
1796
+ `MCP tool ${prefixedName} failed: ${message}${tail ? ` (stderr: ${tail})` : ""}`,
1797
+ { code: "mcp_tool_failed" }
1798
+ );
1799
+ } finally {
1800
+ conn.pending.delete(requestId);
1801
+ }
1802
+ }
1803
+ };
1804
+ }
1805
+ function buildMcpToolDefinitions(connections, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1806
+ const tools = [];
1807
+ for (const conn of connections) {
1808
+ if (conn.closed) continue;
1809
+ for (const tool of conn.tools) tools.push(buildToolDefinition(conn, tool, requestTimeoutMs));
1810
+ }
1811
+ return tools;
1812
+ }
1813
+ async function connectMcpServers(workspace, options = {}) {
1814
+ const resolved = {
1815
+ ...options,
1816
+ initializationTimeoutMs: options.initializationTimeoutMs ?? DEFAULT_INITIALIZATION_TIMEOUT_MS,
1817
+ requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
1818
+ onDiagnostic: options.onDiagnostic ?? consoleMcpDiagnosticSink
1819
+ };
1820
+ let servers;
1821
+ if (options.servers) {
1822
+ servers = Object.entries(options.servers).map(([name, config]) => ({ name, config }));
1823
+ } else {
1824
+ const declared = resolveMcpServerList(workspace, {
1825
+ home: options.home,
1826
+ onDiagnostic: resolved.onDiagnostic
1827
+ });
1828
+ for (const server of declared) {
1829
+ if (server.source === "user") continue;
1830
+ resolved.onDiagnostic({
1831
+ level: "warn",
1832
+ message: `Skipping project-declared MCP server "${server.name}" (${server.path}): it requires approval, and this caller supplied no approved server list.`,
1833
+ server: server.name
1834
+ });
1835
+ }
1836
+ servers = declared.filter((server) => server.source === "user");
1837
+ }
1838
+ const results = await Promise.allSettled(
1839
+ servers.map(({ name, config }) => connectMcpServer(name, config, resolved))
1840
+ );
1841
+ const connections = [];
1842
+ for (const [index, result] of results.entries()) {
1843
+ if (result.status === "rejected") {
1844
+ const server = servers[index];
1845
+ resolved.onDiagnostic({
1846
+ level: "warn",
1847
+ message: `Failed to connect to MCP server "${server.name}": ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`,
1848
+ server: server.name
1849
+ });
1850
+ continue;
1851
+ }
1852
+ if (!result.value) continue;
1853
+ connections.push(result.value);
1854
+ }
1855
+ return { connections, tools: buildMcpToolDefinitions(connections, resolved.requestTimeoutMs) };
1856
+ }
1857
+ async function disconnectMcpServers(connections) {
1858
+ const closing = connections.map(async (conn) => {
1859
+ if (conn.closed) return;
1860
+ conn.closed = true;
1861
+ await conn.close().catch(() => {
1862
+ });
1863
+ });
1864
+ await Promise.all(closing);
1865
+ }
1866
+
1867
+ // src/project-approval-notices.ts
1868
+ import { join as join2 } from "path";
1869
+ function collectWithheldProjectNotices(input) {
1870
+ if (!input.settingsEnabled) return [];
1871
+ const projectSettings = loadSettingsFile(join2(input.workspace, ".book", "settings.json"));
1872
+ const notices = [];
1873
+ const allow = partitionProjectAllowRules(
1874
+ projectSettings?.permissions?.allow ?? [],
1875
+ input.settings.permissions.projectAllowRules
1876
+ );
1877
+ for (const rule of allow.pending) {
1878
+ notices.push(
1879
+ `\u26A0 Ignoring project-declared permission rule "${rule}": it requires approval. Run \`book doctor\` to see how to grant it.`
1880
+ );
1881
+ }
1882
+ const hooks = partitionProjectHooks(
1883
+ collectDeclaredHooks(projectSettings),
1884
+ input.settings.hooks.projectEntries
1885
+ );
1886
+ if (hooks.pending.length > 0) {
1887
+ const byEvent = /* @__PURE__ */ new Map();
1888
+ for (const hook of hooks.pending) {
1889
+ byEvent.set(hook.event, (byEvent.get(hook.event) ?? 0) + 1);
1890
+ }
1891
+ const summary = [...byEvent].map(([event, count]) => `${event} x${count}`).join(", ");
1892
+ notices.push(
1893
+ `\u26A0 Ignoring ${hooks.pending.length} project-declared hook(s) (${summary}): hooks require approval. Run \`book doctor\` to approve them.`
1894
+ );
1895
+ }
1896
+ return notices;
1897
+ }
1898
+
1899
+ export {
1900
+ readRunStatus,
1901
+ runHeadless,
1902
+ buildMcpToolDefinitions,
1903
+ connectMcpServers,
1904
+ disconnectMcpServers,
1905
+ collectWithheldProjectNotices
1906
+ };
1907
+ //# sourceMappingURL=chunk-J7MIZ5CA.js.map