@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.
- package/CHANGELOG.md +2504 -0
- package/LICENSE +123 -0
- package/README.md +1383 -0
- package/dist/app-MOXXWEOY.js +18945 -0
- package/dist/app-MOXXWEOY.js.map +1 -0
- package/dist/chunk-24AXE6SP.js +12 -0
- package/dist/chunk-24AXE6SP.js.map +1 -0
- package/dist/chunk-3U5IBM24.js +213 -0
- package/dist/chunk-3U5IBM24.js.map +1 -0
- package/dist/chunk-574C6MQR.js +743 -0
- package/dist/chunk-574C6MQR.js.map +1 -0
- package/dist/chunk-5GDZ22YP.js +449 -0
- package/dist/chunk-5GDZ22YP.js.map +1 -0
- package/dist/chunk-5KLVH3PY.js +167 -0
- package/dist/chunk-5KLVH3PY.js.map +1 -0
- package/dist/chunk-5RDRAO4B.js +92 -0
- package/dist/chunk-5RDRAO4B.js.map +1 -0
- package/dist/chunk-7N4J557C.js +10 -0
- package/dist/chunk-7N4J557C.js.map +1 -0
- package/dist/chunk-A7JLBK2Y.js +3506 -0
- package/dist/chunk-A7JLBK2Y.js.map +1 -0
- package/dist/chunk-ANSJPYGI.js +84 -0
- package/dist/chunk-ANSJPYGI.js.map +1 -0
- package/dist/chunk-BTZZUCLI.js +264 -0
- package/dist/chunk-BTZZUCLI.js.map +1 -0
- package/dist/chunk-C7D4NJ2D.js +217 -0
- package/dist/chunk-C7D4NJ2D.js.map +1 -0
- package/dist/chunk-CAGOB2N4.js +208 -0
- package/dist/chunk-CAGOB2N4.js.map +1 -0
- package/dist/chunk-DGMQEI6Y.js +98 -0
- package/dist/chunk-DGMQEI6Y.js.map +1 -0
- package/dist/chunk-E75IPST5.js +289 -0
- package/dist/chunk-E75IPST5.js.map +1 -0
- package/dist/chunk-EYGD6X2T.js +180 -0
- package/dist/chunk-EYGD6X2T.js.map +1 -0
- package/dist/chunk-GTVB4HEM.js +765 -0
- package/dist/chunk-GTVB4HEM.js.map +1 -0
- package/dist/chunk-J7MIZ5CA.js +1907 -0
- package/dist/chunk-J7MIZ5CA.js.map +1 -0
- package/dist/chunk-KWHQ2XXL.js +626 -0
- package/dist/chunk-KWHQ2XXL.js.map +1 -0
- package/dist/chunk-L6LK2QIQ.js +657 -0
- package/dist/chunk-L6LK2QIQ.js.map +1 -0
- package/dist/chunk-MAQXCUR4.js +1007 -0
- package/dist/chunk-MAQXCUR4.js.map +1 -0
- package/dist/chunk-S4XL7HOM.js +402 -0
- package/dist/chunk-S4XL7HOM.js.map +1 -0
- package/dist/chunk-URLQAXTR.js +60 -0
- package/dist/chunk-URLQAXTR.js.map +1 -0
- package/dist/chunk-V4BOYR52.js +3504 -0
- package/dist/chunk-V4BOYR52.js.map +1 -0
- package/dist/chunk-VZQXX3WW.js +210 -0
- package/dist/chunk-VZQXX3WW.js.map +1 -0
- package/dist/chunk-WKBO5R4O.js +34 -0
- package/dist/chunk-WKBO5R4O.js.map +1 -0
- package/dist/chunk-XIFFTATU.js +15753 -0
- package/dist/chunk-XIFFTATU.js.map +1 -0
- package/dist/command-approvals-6Y57VCWZ.js +28 -0
- package/dist/command-approvals-6Y57VCWZ.js.map +1 -0
- package/dist/context-ZC5IFEAX.js +25 -0
- package/dist/context-ZC5IFEAX.js.map +1 -0
- package/dist/hook-approvals-CTUFKEJT.js +22 -0
- package/dist/hook-approvals-CTUFKEJT.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2582 -0
- package/dist/index.js.map +1 -0
- package/dist/interactive-assets-CCAAWVKH.js +31 -0
- package/dist/interactive-assets-CCAAWVKH.js.map +1 -0
- package/dist/job-runner.d.ts +2 -0
- package/dist/job-runner.js +235 -0
- package/dist/job-runner.js.map +1 -0
- package/dist/loader-4IDS2RIE.js +15 -0
- package/dist/loader-4IDS2RIE.js.map +1 -0
- package/dist/mcp-approvals-WNPH65BK.js +18 -0
- package/dist/mcp-approvals-WNPH65BK.js.map +1 -0
- package/dist/mcp-config-YIJIES2C.js +16 -0
- package/dist/mcp-config-YIJIES2C.js.map +1 -0
- package/dist/model-window-store-YFVNXCIP.js +27 -0
- package/dist/model-window-store-YFVNXCIP.js.map +1 -0
- package/dist/openai-compatible-RHZUBHV6.js +10 -0
- package/dist/openai-compatible-RHZUBHV6.js.map +1 -0
- package/dist/permission-approvals-76PVREFD.js +14 -0
- package/dist/permission-approvals-76PVREFD.js.map +1 -0
- package/dist/permissions-CHCKTFQQ.js +28 -0
- package/dist/permissions-CHCKTFQQ.js.map +1 -0
- package/dist/sandbox-CNVAQDA7.js +23 -0
- package/dist/sandbox-CNVAQDA7.js.map +1 -0
- package/dist/scrollback-LUKLIBVI.js +136 -0
- package/dist/scrollback-LUKLIBVI.js.map +1 -0
- package/dist/sdk.d.ts +4574 -0
- package/dist/sdk.js +665 -0
- package/dist/sdk.js.map +1 -0
- package/dist/settings-loader-M2QP5TX2.js +21 -0
- package/dist/settings-loader-M2QP5TX2.js.map +1 -0
- package/dist/settings-removed-JWWPT4WA.js +19 -0
- package/dist/settings-removed-JWWPT4WA.js.map +1 -0
- package/dist/shell-selection-V6QAYXPX.js +17 -0
- package/dist/shell-selection-V6QAYXPX.js.map +1 -0
- package/package.json +143 -0
- package/patches/ink+6.8.0.patch +13 -0
- package/scripts/apply-ink-patch.mjs +29 -0
- package/scripts/ink-patch.mjs +59 -0
- 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
|