@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
package/dist/index.js ADDED
@@ -0,0 +1,2582 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ installFrameCapture,
4
+ installInkScrollRenderer
5
+ } from "./chunk-C7D4NJ2D.js";
6
+ import {
7
+ buildMcpToolDefinitions,
8
+ collectWithheldProjectNotices,
9
+ connectMcpServers,
10
+ disconnectMcpServers,
11
+ readRunStatus,
12
+ runHeadless
13
+ } from "./chunk-J7MIZ5CA.js";
14
+ import {
15
+ SessionStore,
16
+ resolveSessionBootstrap
17
+ } from "./chunk-V4BOYR52.js";
18
+ import {
19
+ aggregateToolUse,
20
+ createDefaultRegistry,
21
+ formatToolStatsReport,
22
+ freezeAgentConfig,
23
+ loadConfig,
24
+ pruneToolUseRecords,
25
+ readToolUseRecords
26
+ } from "./chunk-XIFFTATU.js";
27
+ import {
28
+ EFFORT_LEVELS,
29
+ applySettingWrite,
30
+ describeSettingShadow,
31
+ discoverAgents,
32
+ estimateUsageCost,
33
+ getPackageVersion,
34
+ isEffortLevel,
35
+ parseEffortLevel,
36
+ readScopeDocument,
37
+ resolvePermissionMode,
38
+ settingsScopeLabel,
39
+ withBuiltInAgents
40
+ } from "./chunk-A7JLBK2Y.js";
41
+ import "./chunk-KWHQ2XXL.js";
42
+ import "./chunk-VZQXX3WW.js";
43
+ import {
44
+ discoverCommands
45
+ } from "./chunk-E75IPST5.js";
46
+ import {
47
+ describeProjectCommandShell,
48
+ displayableCommandName,
49
+ partitionProjectCommands,
50
+ persistProjectCommandChoice,
51
+ projectCommandFingerprint
52
+ } from "./chunk-5KLVH3PY.js";
53
+ import {
54
+ formatMcpServerCommand,
55
+ isValidMcpServerName,
56
+ resolveMcpServerList
57
+ } from "./chunk-3U5IBM24.js";
58
+ import {
59
+ evaluateMcpServerApproval,
60
+ mcpServerFingerprint,
61
+ mcpServersToRecord,
62
+ partitionMcpServersByApproval,
63
+ persistMcpProjectServerChoice
64
+ } from "./chunk-URLQAXTR.js";
65
+ import "./chunk-MAQXCUR4.js";
66
+ import {
67
+ canonicalToolName
68
+ } from "./chunk-5GDZ22YP.js";
69
+ import "./chunk-EYGD6X2T.js";
70
+ import {
71
+ DEFAULT_LOCAL_DATA_RETENTION_DAYS,
72
+ cleanupDebugLogs,
73
+ createDebugLogger,
74
+ getDebugLogPath
75
+ } from "./chunk-GTVB4HEM.js";
76
+ import "./chunk-S4XL7HOM.js";
77
+ import "./chunk-7N4J557C.js";
78
+ import {
79
+ loadSettingsFile,
80
+ resolveSettings
81
+ } from "./chunk-BTZZUCLI.js";
82
+ import {
83
+ partitionProjectAllowRules,
84
+ persistProjectAllowRuleChoice
85
+ } from "./chunk-WKBO5R4O.js";
86
+ import {
87
+ collectDeclaredHooks,
88
+ describeDeclaredHook,
89
+ hookFingerprint,
90
+ partitionProjectHooks,
91
+ persistProjectHookChoice
92
+ } from "./chunk-ANSJPYGI.js";
93
+ import "./chunk-DGMQEI6Y.js";
94
+ import "./chunk-5RDRAO4B.js";
95
+ import "./chunk-CAGOB2N4.js";
96
+ import {
97
+ DEFAULT_SETTINGS,
98
+ SettingsRepository,
99
+ formatSettingsDiagnostics,
100
+ formatSettingsKeyHelp,
101
+ getNestedValue,
102
+ parseNumericFlag,
103
+ redactSettingValue,
104
+ redactSettingsForDisplay,
105
+ writeFileAtomic
106
+ } from "./chunk-574C6MQR.js";
107
+ import {
108
+ resolveBookHome
109
+ } from "./chunk-24AXE6SP.js";
110
+
111
+ // src/runtime-env.ts
112
+ if (!process.env.NODE_ENV) {
113
+ process.env.NODE_ENV = "production";
114
+ }
115
+
116
+ // src/index.ts
117
+ import { InvalidArgumentError, program } from "commander";
118
+
119
+ // src/cli/status-cmd.ts
120
+ import { join } from "path";
121
+ function isProcessAlive(pid) {
122
+ if (!Number.isInteger(pid) || pid <= 0) return false;
123
+ try {
124
+ process.kill(pid, 0);
125
+ return true;
126
+ } catch (error) {
127
+ return error.code === "EPERM";
128
+ }
129
+ }
130
+ function buildRunStatus(record) {
131
+ const processAlive = isProcessAlive(record.pid);
132
+ const liveness = record.terminal ? "completed" : record.crash ? "crashed" : processAlive ? "running" : "died";
133
+ return {
134
+ liveness,
135
+ pid: record.pid,
136
+ processAlive,
137
+ runId: record.runId,
138
+ turn: record.turn,
139
+ elapsedMs: record.elapsedMs,
140
+ updatedAt: record.updatedAt,
141
+ lastTool: record.lastTool,
142
+ currentTodo: record.currentTodo,
143
+ openTodos: record.openTodos,
144
+ costUsd: record.costUsd,
145
+ budgetUsd: record.budgetUsd,
146
+ freeDiskBytes: record.freeDiskBytes,
147
+ terminal: record.terminal ? { ...record.terminal } : void 0,
148
+ crash: record.crash ? { ...record.crash } : void 0
149
+ };
150
+ }
151
+ function firstUserTurn(transcript) {
152
+ for (const message of transcript) {
153
+ if (message.role !== "user") continue;
154
+ if (message.kind && message.kind !== "conversation") continue;
155
+ if (message.content.startsWith("[continuation]")) continue;
156
+ if (message.content.startsWith("[work-state]")) continue;
157
+ if (message.agentNotifications?.length) continue;
158
+ return message.content;
159
+ }
160
+ return void 0;
161
+ }
162
+ function buildSessionStatus(meta, loaded, runRecord) {
163
+ const usage = loaded.carriedUsage;
164
+ const models = loaded.carriedModels ?? [];
165
+ let costUsd = models.length > 0 && usage ? 0 : null;
166
+ if (usage) {
167
+ for (const model of models) {
168
+ const quote = estimateUsageCost(model, {
169
+ promptTokens: usage.promptTokens,
170
+ completionTokens: usage.completionTokens,
171
+ totalTokens: usage.totalTokens,
172
+ cacheReadInputTokens: usage.cacheReadInputTokens,
173
+ cacheCreationInputTokens: usage.cacheCreationInputTokens
174
+ });
175
+ if (quote.status !== "known") {
176
+ costUsd = null;
177
+ break;
178
+ }
179
+ costUsd = Math.max(costUsd ?? 0, quote.costUsd);
180
+ }
181
+ }
182
+ return {
183
+ sessionId: meta.id,
184
+ sessionName: meta.name,
185
+ workspace: meta.cwd,
186
+ createdAt: meta.createdAt,
187
+ updatedAt: meta.updatedAt,
188
+ objective: firstUserTurn(loaded.transcript),
189
+ transcriptMessages: loaded.transcript.length,
190
+ contextMessages: loaded.contextHistory.length,
191
+ compactions: loaded.compactBoundaries.length,
192
+ generation: loaded.compactBoundaries.at(-1)?.generation ?? 0,
193
+ usage: usage ? {
194
+ promptTokens: usage.promptTokens,
195
+ completionTokens: usage.completionTokens,
196
+ cacheReadInputTokens: usage.cacheReadInputTokens ?? 0,
197
+ cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0
198
+ } : void 0,
199
+ costUsd,
200
+ costModels: models,
201
+ plan: loaded.plan ? {
202
+ todos: (loaded.plan.todos ?? []).map((todo) => ({
203
+ content: todo.content,
204
+ status: todo.status
205
+ })),
206
+ tasks: (loaded.plan.tasks ?? []).map((task) => ({
207
+ id: task.id,
208
+ subject: task.subject,
209
+ status: task.status,
210
+ blockedBy: task.blockedBy
211
+ }))
212
+ } : void 0,
213
+ run: runRecord ? buildRunStatus(runRecord) : void 0
214
+ };
215
+ }
216
+ function truncate(text, max) {
217
+ const single = text.replace(/\s+/g, " ").trim();
218
+ return single.length <= max ? single : `${single.slice(0, max - 1)}\u2026`;
219
+ }
220
+ function since(timestamp, now) {
221
+ const minutes = Math.max(0, Math.round((now - timestamp) / 6e4));
222
+ if (minutes < 60) return `${minutes}m ago`;
223
+ const hours = Math.round(minutes / 60);
224
+ return hours < 48 ? `${hours}h ago` : `${Math.round(hours / 24)}d ago`;
225
+ }
226
+ function duration(ms) {
227
+ const seconds = Math.max(0, Math.round(ms / 1e3));
228
+ if (seconds < 60) return `${seconds}s`;
229
+ const minutes = Math.floor(seconds / 60);
230
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
231
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
232
+ }
233
+ function gibibytes(bytes) {
234
+ return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
235
+ }
236
+ function renderRunStatus(run, now) {
237
+ const lines = [];
238
+ const headline = {
239
+ running: `running (pid ${run.pid})`,
240
+ completed: `finished \u2014 ${run.terminal?.status ?? "unknown"} (${run.terminal?.reason ?? "no reason recorded"})`,
241
+ crashed: "crashed without a terminal outcome",
242
+ died: `no longer running, and recorded no outcome (pid ${run.pid} is gone)`
243
+ };
244
+ lines.push(`Run: ${headline[run.liveness]}`);
245
+ if (run.terminal?.message) lines.push(` ${run.terminal.message}`);
246
+ if (run.crash) lines.push(` ${run.crash.message}`);
247
+ lines.push(` turn ${run.turn} \u2022 elapsed ${duration(run.elapsedMs)}`);
248
+ lines.push(` last update: ${since(run.updatedAt, now)}`);
249
+ if (run.liveness === "running" && now - run.updatedAt > STALE_RUN_MS) {
250
+ lines.push(
251
+ ` \u26A0 the process is alive but has not written a turn boundary in ${duration(now - run.updatedAt)} \u2014 it may be wedged on a long tool call or a permission prompt.`
252
+ );
253
+ }
254
+ if (run.lastTool) lines.push(` last tool: ${run.lastTool}`);
255
+ if (run.currentTodo) lines.push(` in progress: ${truncate(run.currentTodo, 120)}`);
256
+ if (run.openTodos > 0) lines.push(` open todos: ${run.openTodos}`);
257
+ if (run.costUsd !== null) {
258
+ lines.push(
259
+ ` spend: ~$${run.costUsd.toFixed(4)}` + (run.budgetUsd !== void 0 ? ` of $${run.budgetUsd.toFixed(2)} budget` : "")
260
+ );
261
+ }
262
+ if (run.freeDiskBytes !== void 0) lines.push(` free disk: ${gibibytes(run.freeDiskBytes)}`);
263
+ return lines;
264
+ }
265
+ var STALE_RUN_MS = 15 * 6e4;
266
+ function renderSessionStatus(status, now = Date.now()) {
267
+ const lines = [];
268
+ lines.push(`Session: ${status.sessionName ?? status.sessionId}`);
269
+ if (status.sessionName) lines.push(` id: ${status.sessionId}`);
270
+ lines.push(` workspace: ${status.workspace}`);
271
+ lines.push(` last activity: ${since(status.updatedAt, now)}`);
272
+ lines.push("");
273
+ if (status.run) {
274
+ lines.push(...renderRunStatus(status.run, now));
275
+ lines.push("");
276
+ }
277
+ lines.push(
278
+ status.objective ? `Objective: ${truncate(status.objective, 300)}` : "Objective: (no user turn recorded)"
279
+ );
280
+ lines.push("");
281
+ lines.push(
282
+ `History: ${status.transcriptMessages} messages, ${status.contextMessages} in context` + (status.compactions > 0 ? ` \u2022 ${status.compactions} compaction${status.compactions === 1 ? "" : "s"} (generation ${status.generation})` : " \u2022 never compacted")
283
+ );
284
+ if (status.usage) {
285
+ const { promptTokens, completionTokens, cacheReadInputTokens, cacheCreationInputTokens } = status.usage;
286
+ lines.push(
287
+ `Tokens: prompt ${promptTokens.toLocaleString()} \u2022 completion ${completionTokens.toLocaleString()} \u2022 cache read ${cacheReadInputTokens.toLocaleString()} \u2022 cache write ${cacheCreationInputTokens.toLocaleString()}`
288
+ );
289
+ lines.push(
290
+ status.costUsd !== null ? `Spend: ~$${status.costUsd.toFixed(4)} (upper bound across ${status.costModels.join(", ") || "unknown model"})` : "Spend: (pricing unknown for at least one model used)"
291
+ );
292
+ } else {
293
+ lines.push("Tokens: (none recorded \u2014 this session predates usage records, or never ran)");
294
+ }
295
+ if (status.plan) {
296
+ const open = status.plan.todos.filter((todo) => todo.status !== "completed");
297
+ const openTasks = status.plan.tasks.filter(
298
+ (task) => task.status !== "completed" && task.status !== "deleted"
299
+ );
300
+ lines.push("");
301
+ lines.push(`Plan: ${open.length} open todo(s), ${openTasks.length} open task(s)`);
302
+ for (const todo of open.slice(0, 10)) lines.push(` [${todo.status}] ${todo.content}`);
303
+ for (const task of openTasks.slice(0, 10)) {
304
+ const blockers = task.blockedBy.length ? ` (blocked by ${task.blockedBy.join(", ")})` : "";
305
+ lines.push(` [${task.status}] ${task.subject}${blockers}`);
306
+ }
307
+ } else {
308
+ lines.push("");
309
+ lines.push("Plan: (none persisted)");
310
+ }
311
+ return lines.join("\n");
312
+ }
313
+ function runStatusCommand(options) {
314
+ const stdout = options.stdout ?? process.stdout;
315
+ const home = options.home ?? resolveBookHome();
316
+ const store = options.store ?? new SessionStore(join(home, "sessions"));
317
+ const meta = options.session ? store.findById(options.session) ?? store.findByName(options.session) : store.mostRecentInCwd(options.workspace);
318
+ if (!meta) {
319
+ stdout.write(
320
+ options.session ? `No session found matching "${options.session}".
321
+ ` : `No session has run in ${options.workspace} yet.
322
+ `
323
+ );
324
+ return 1;
325
+ }
326
+ const status = buildSessionStatus(meta, store.load(meta.id), readRunStatus(meta.id, home));
327
+ stdout.write(
328
+ options.json ? `${JSON.stringify(status, null, 2)}
329
+ ` : `${renderSessionStatus(status)}
330
+ `
331
+ );
332
+ return 0;
333
+ }
334
+
335
+ // src/cli/exit.ts
336
+ var exitFn = (code) => {
337
+ process.exit(code);
338
+ };
339
+ var exiting = false;
340
+ function exit(code = 0) {
341
+ exiting = true;
342
+ exitFn(code);
343
+ }
344
+ function isExiting() {
345
+ return exiting;
346
+ }
347
+
348
+ // src/cli/doctor.ts
349
+ import { resolve } from "path";
350
+
351
+ // src/agents/diagnostics.ts
352
+ var MUTATION_TOOLS = /* @__PURE__ */ new Set([
353
+ "ApplyPatch",
354
+ "Write",
355
+ "Edit",
356
+ "MultiEdit",
357
+ "NotebookEdit",
358
+ "Bash",
359
+ "Check"
360
+ ]);
361
+ function validateModel(profile, model, config) {
362
+ if (!model || model === "inherit") return void 0;
363
+ const slash = model.indexOf("/");
364
+ if (slash === 0 || slash === model.length - 1) {
365
+ return {
366
+ code: "invalid-model",
367
+ message: `${profile} uses invalid model "${model}"; expected a model or provider/model reference.`
368
+ };
369
+ }
370
+ if (slash > 0) {
371
+ const providerId = model.slice(0, slash);
372
+ const modelId = model.slice(slash + 1);
373
+ const provider = config.settings.provider[providerId];
374
+ if (provider && Object.keys(provider.models).length > 0 && !provider.models[modelId]) {
375
+ return {
376
+ code: "invalid-model",
377
+ message: `${profile} references ${model}, which is not in provider ${providerId}'s model catalog.`
378
+ };
379
+ }
380
+ }
381
+ return void 0;
382
+ }
383
+ function collectAgentDiagnostics(config, profiles) {
384
+ const diagnostics = [];
385
+ const counts = /* @__PURE__ */ new Map();
386
+ for (const profile of profiles) counts.set(profile.name, (counts.get(profile.name) ?? 0) + 1);
387
+ for (const [name, count] of counts) {
388
+ if (count > 1) {
389
+ diagnostics.push({
390
+ code: "duplicate-profile",
391
+ message: `${name} is defined ${count} times; keep one profile definition per name.`
392
+ });
393
+ }
394
+ }
395
+ const profileNames = new Set(profiles.map((profile) => profile.name));
396
+ for (const [name, override] of Object.entries(config.settings.agents.profiles)) {
397
+ if (!profileNames.has(name)) {
398
+ diagnostics.push({
399
+ code: "missing-profile",
400
+ message: `agents.profiles.${name} has settings but no matching agent definition.`
401
+ });
402
+ }
403
+ const modelDiagnostic = validateModel(name, override.model, config);
404
+ if (modelDiagnostic) diagnostics.push(modelDiagnostic);
405
+ }
406
+ for (const profile of profiles) {
407
+ if (profile.suppressedOverride) {
408
+ diagnostics.push({
409
+ code: "reserved-profile",
410
+ message: `${profile.name} is a reserved built-in agent, so the ${profile.suppressedOverride} definition of the same name is ignored. Rename it, or tune model/effort through agents.profiles.${profile.name}.`
411
+ });
412
+ }
413
+ for (const tool of profile.unknownTools ?? []) {
414
+ diagnostics.push({
415
+ code: "unknown-tool",
416
+ message: `${profile.name} declares unsupported tool ${tool}; import or edit the definition.`
417
+ });
418
+ }
419
+ if (profile.isolation === "workspace-readonly") {
420
+ const mutations = profile.allowedTools.filter(
421
+ (tool) => MUTATION_TOOLS.has(canonicalToolName(tool.split("(")[0].trim()))
422
+ );
423
+ if (mutations.length > 0) {
424
+ diagnostics.push({
425
+ code: "readonly-mutation",
426
+ message: `${profile.name} is read-only but declares mutation tools: ${mutations.join(", ")}. They will be hard-filtered.`
427
+ });
428
+ }
429
+ }
430
+ if (!config.settings.agents.profiles[profile.name]?.model) {
431
+ const modelDiagnostic = validateModel(profile.name, profile.model, config);
432
+ if (modelDiagnostic) diagnostics.push(modelDiagnostic);
433
+ }
434
+ }
435
+ return diagnostics;
436
+ }
437
+
438
+ // src/cli/doctor.ts
439
+ var SHELL_SAFE_BARE = /^[A-Za-z0-9_@%+=:,./\\-]+$/;
440
+ function shellArgument(value) {
441
+ if (SHELL_SAFE_BARE.test(value)) return value;
442
+ if (/["`$\n\r]/.test(value) || value.endsWith("\\")) return null;
443
+ return `"${value}"`;
444
+ }
445
+ function trustCommandLine(workspace, rest) {
446
+ if (resolve(workspace) === resolve(process.cwd())) return `book trust ${rest}`;
447
+ const quoted = shellArgument(workspace);
448
+ return quoted === null ? null : `book trust ${rest} --workspace ${quoted}`;
449
+ }
450
+ function approvalHint(label, workspace, rest) {
451
+ const command = trustCommandLine(workspace, rest);
452
+ return command === null ? `${label} book trust ${rest} (run it from ${workspace})` : `${label} ${command}`;
453
+ }
454
+ function formatRelativeTime(timestamp, now = Date.now()) {
455
+ const elapsedMs = Math.max(0, now - timestamp);
456
+ const minutes = Math.floor(elapsedMs / 6e4);
457
+ if (minutes < 1) return "just now";
458
+ if (minutes < 60) return `${minutes}m ago`;
459
+ const hours = Math.floor(minutes / 60);
460
+ if (hours < 24) return `${hours}h ago`;
461
+ const days = Math.floor(hours / 24);
462
+ return `${days}d ago`;
463
+ }
464
+ function describeCredentials(config) {
465
+ return config.apiKey ? "API key resolved" : "not resolved - set BOOK_API_KEY or set provider.<id>.apiKey in settings";
466
+ }
467
+ var ABSENT_LAYER = "\0absent-settings-layer";
468
+ async function settingsLayers(workspace) {
469
+ const { join: join8, resolve: resolve4 } = await import("path");
470
+ const root = resolve4(workspace);
471
+ return [
472
+ ["User", join8(resolveBookHome(), "settings.json")],
473
+ ["Project", join8(root, ".book", "settings.json")],
474
+ ["Local", join8(root, ".book", "settings.local.json")]
475
+ ];
476
+ }
477
+ async function attributeFailingLayer(workspace) {
478
+ const layers = await settingsLayers(workspace);
479
+ for (let included = 0; included <= layers.length; included++) {
480
+ const [userSettingsPath, projectSettingsPath, localSettingsPath] = layers.map(
481
+ ([, path], index) => index < included ? path : ABSENT_LAYER
482
+ );
483
+ try {
484
+ loadConfig(workspace, {
485
+ allowMissingApiKey: true,
486
+ // A probe must not write: migrations are the real load's business.
487
+ runMigrations: false,
488
+ settingsPaths: { userSettingsPath, projectSettingsPath, localSettingsPath }
489
+ });
490
+ } catch {
491
+ return included === 0 ? void 0 : layers[included - 1][0];
492
+ }
493
+ }
494
+ return void 0;
495
+ }
496
+ async function reportUnloadableConfig(workspace, error) {
497
+ const { existsSync: existsSync4 } = await import("fs");
498
+ const { resolve: resolve4 } = await import("path");
499
+ console.log("Book Doctor");
500
+ console.log("===========");
501
+ console.log();
502
+ console.log("Version:", getPackageVersion());
503
+ console.log("Node:", process.version);
504
+ console.log("Platform:", process.platform, process.arch);
505
+ console.log("Workspace:", resolve4(workspace));
506
+ console.log();
507
+ console.log("Configuration: FAILED TO LOAD");
508
+ console.log(" " + (error instanceof Error ? error.message : String(error)));
509
+ console.log();
510
+ console.log(" No other check can run until the configuration loads.");
511
+ const culprit = await attributeFailingLayer(workspace);
512
+ console.log(" Settings layers, in the order they are applied:");
513
+ for (const [label, path] of await settingsLayers(workspace)) {
514
+ const marker = existsSync4(path) ? "[x]" : "[ ]";
515
+ const blame = label === culprit ? " <- the failure appears with this layer" : "";
516
+ console.log(" " + marker + " " + label + ": " + path + blame);
517
+ }
518
+ console.log();
519
+ if (culprit) {
520
+ console.log(` The ${culprit} layer is where the configuration stops loading.`);
521
+ } else {
522
+ console.log(" No single layer accounts for it: check the environment and .bookrc.json.");
523
+ }
524
+ console.log(" Run `book doctor --no-settings` to report the rest with every layer skipped.");
525
+ }
526
+ async function runDoctorCommand(workspace, options = {}) {
527
+ let config;
528
+ try {
529
+ config = loadConfig(workspace, {
530
+ // --no-settings is the way past a layer doctor cannot otherwise get around;
531
+ // migrations would rewrite settings this run has been told to ignore.
532
+ noSettings: options.noSettings,
533
+ runMigrations: !options.noSettings,
534
+ allowMissingApiKey: true
535
+ });
536
+ } catch (error) {
537
+ await reportUnloadableConfig(workspace, error);
538
+ return;
539
+ }
540
+ const settings = config.settings;
541
+ console.log("Book Doctor");
542
+ console.log("===========");
543
+ console.log();
544
+ console.log("Version:", getPackageVersion());
545
+ console.log("Node:", process.version);
546
+ console.log("Platform:", process.platform, process.arch);
547
+ console.log("Workspace:", config.workspace);
548
+ console.log("Model:", config.model, "(" + config.baseUrl + ")");
549
+ if (config.modelProviderWarning) console.log(" \u26A0 " + config.modelProviderWarning);
550
+ console.log("Credentials:", describeCredentials(config));
551
+ const {
552
+ collectRemovedSettingNotices,
553
+ collectRemovedEnvNotices,
554
+ removedAuthStoreNotice,
555
+ readSettingsDocumentForNotices
556
+ } = await import("./settings-removed-JWWPT4WA.js");
557
+ const { settingsLayerPaths } = await import("./settings-loader-M2QP5TX2.js");
558
+ const { join: joinPath } = await import("path");
559
+ const { existsSync: fileExists } = await import("fs");
560
+ const removedNotices = [
561
+ ...settingsLayerPaths(workspace, void 0, {}).flatMap(
562
+ (path) => collectRemovedSettingNotices(readSettingsDocumentForNotices(path)).map(
563
+ (notice) => `${notice.key} in ${path}: ${notice.message}`
564
+ )
565
+ ),
566
+ ...collectRemovedEnvNotices(process.env).map(
567
+ (notice) => `${notice.key} is set: ${notice.message}`
568
+ )
569
+ ];
570
+ const authStorePath = joinPath(resolveBookHome(), "auth.json");
571
+ if (fileExists(authStorePath)) removedNotices.push(removedAuthStoreNotice(authStorePath));
572
+ if (removedNotices.length > 0) {
573
+ console.log();
574
+ console.log("Removed features still configured on this machine:");
575
+ for (const notice of removedNotices) console.log(` [!] ${notice}`);
576
+ }
577
+ console.log();
578
+ console.log("Settings layers:");
579
+ const { existsSync: existsSync4 } = await import("fs");
580
+ const { join: join8 } = await import("path");
581
+ for (const [label, path] of await settingsLayers(config.workspace)) {
582
+ const marker = options.noSettings ? "[-]" : existsSync4(path) ? "[x]" : "[ ]";
583
+ console.log(" " + marker + " " + label + ": " + path);
584
+ }
585
+ if (options.noSettings) {
586
+ console.log(" (--no-settings: every layer skipped, defaults reported below)");
587
+ }
588
+ console.log();
589
+ const perms = settings.permissions;
590
+ console.log("Permissions:");
591
+ console.log(" Allow rules: " + perms.allow.length);
592
+ console.log(" Ask rules: " + perms.ask.length);
593
+ console.log(" Deny rules: " + perms.deny.length);
594
+ const { loadSettingsFile: loadSettingsFile2 } = await import("./settings-loader-M2QP5TX2.js");
595
+ const { partitionProjectAllowRules: partitionProjectAllowRules2 } = await import("./permission-approvals-76PVREFD.js");
596
+ const projectSettings = loadSettingsFile2(join8(config.workspace, ".book", "settings.json"));
597
+ const projectAllow = partitionProjectAllowRules2(
598
+ projectSettings?.permissions?.allow ?? [],
599
+ perms.projectAllowRules
600
+ );
601
+ if (projectAllow.approved.length + projectAllow.pending.length + projectAllow.rejected.length > 0) {
602
+ console.log(" Project-declared allow rules (require approval):");
603
+ for (const rule of projectAllow.approved) console.log(" [x] " + rule);
604
+ for (const rule of projectAllow.rejected) console.log(" [-] " + rule + " (rejected)");
605
+ for (const rule of projectAllow.pending) console.log(" [!] " + rule + " (not in effect)");
606
+ if (projectAllow.pending.length > 0) {
607
+ console.log(approvalHint(" Approve one:", config.workspace, "rule <rule>"));
608
+ console.log(
609
+ approvalHint(" Approve all pending:", config.workspace, "rule --all-pending") + " (add --reject to refuse)"
610
+ );
611
+ }
612
+ }
613
+ console.log();
614
+ const { discoverCommands: discoverCommands2 } = await import("./loader-4IDS2RIE.js");
615
+ const { partitionProjectCommands: partitionProjectCommands2, describeProjectCommandShell: describeProjectCommandShell2, displayableCommandName: displayableCommandName2 } = await import("./command-approvals-6Y57VCWZ.js");
616
+ const gatedCommands = partitionProjectCommands2(discoverCommands2(config.workspace), settings);
617
+ if (gatedCommands.approved.length + gatedCommands.pending.length + gatedCommands.rejected.length > 0) {
618
+ console.log("Project commands that run shell (require approval):");
619
+ for (const command of gatedCommands.approved)
620
+ console.log(" [x] /" + displayableCommandName2(command.name));
621
+ for (const command of gatedCommands.rejected)
622
+ console.log(" [-] /" + displayableCommandName2(command.name) + " (rejected)");
623
+ for (const command of gatedCommands.pending) {
624
+ console.log(" [!] /" + displayableCommandName2(command.name) + " (refused until approved)");
625
+ for (const line of describeProjectCommandShell2(command)) console.log(" " + line);
626
+ }
627
+ if (gatedCommands.pending.length > 0) {
628
+ console.log(approvalHint(" Approve one:", config.workspace, "command <name>"));
629
+ console.log(
630
+ approvalHint(" Approve all pending:", config.workspace, "command --all-pending") + " (add --reject to refuse)"
631
+ );
632
+ }
633
+ console.log();
634
+ }
635
+ const hooks = settings.hooks;
636
+ const hookLists = Object.entries(hooks).filter(
637
+ (pair) => Array.isArray(pair[1])
638
+ );
639
+ let hookTotal = 0;
640
+ for (const [, entries] of hookLists) hookTotal += entries.length;
641
+ console.log("Hooks:");
642
+ for (const [event, entries] of hookLists) {
643
+ if (entries.length > 0) console.log(" " + event + ": " + entries.length);
644
+ }
645
+ if (hookTotal === 0) console.log(" (none)");
646
+ const { collectDeclaredHooks: collectDeclaredHooks2, describeDeclaredHook: describeDeclaredHook2, partitionProjectHooks: partitionProjectHooks2 } = await import("./hook-approvals-CTUFKEJT.js");
647
+ const declaredProjectHooks = collectDeclaredHooks2(projectSettings);
648
+ if (declaredProjectHooks.length > 0) {
649
+ const hookPartition = partitionProjectHooks2(
650
+ declaredProjectHooks,
651
+ settings.hooks.projectEntries
652
+ );
653
+ console.log(" Project-declared hooks (require approval):");
654
+ const groups = [
655
+ { mark: "[x]", suffix: "", hooks: hookPartition.approved },
656
+ { mark: "[-]", suffix: " (rejected)", hooks: hookPartition.rejected },
657
+ { mark: "[!]", suffix: " (not in effect)", hooks: hookPartition.pending }
658
+ ];
659
+ for (const group of groups) {
660
+ for (const hook of group.hooks) {
661
+ const described = describeDeclaredHook2(hook);
662
+ console.log(" " + group.mark + " " + described.headline + group.suffix);
663
+ for (const detail of described.details) console.log(" " + detail);
664
+ }
665
+ }
666
+ if (hookPartition.pending.length > 0) {
667
+ console.log(approvalHint(" Approve one:", config.workspace, "hook <fingerprint>"));
668
+ console.log(
669
+ approvalHint(" Approve all pending:", config.workspace, "hook --all-pending") + " (add --reject to refuse)"
670
+ );
671
+ }
672
+ }
673
+ console.log();
674
+ const { formatMcpServerCommand: formatMcpServerCommand2, resolveMcpServerList: resolveMcpServerList2 } = await import("./mcp-config-YIJIES2C.js");
675
+ const { evaluateMcpServerApproval: evaluateMcpServerApproval2 } = await import("./mcp-approvals-WNPH65BK.js");
676
+ const mcpServers = resolveMcpServerList2(config.workspace);
677
+ console.log("MCP Servers:");
678
+ for (const server of mcpServers) {
679
+ const trust = server.source === "user" ? "user" : `project: ${evaluateMcpServerApproval2(settings, server)}`;
680
+ console.log(" " + server.name + ": " + formatMcpServerCommand2(server.config) + ` [${trust}]`);
681
+ }
682
+ if (mcpServers.length === 0) console.log(" (none)");
683
+ console.log();
684
+ const { createSandbox, sandboxPolicySummary } = await import("./sandbox-CNVAQDA7.js");
685
+ const { hasAdjudicationPolicy } = await import("./permissions-CHCKTFQQ.js");
686
+ const sandbox = createSandbox(settings.sandbox);
687
+ const policy = sandboxPolicySummary(settings.sandbox, {
688
+ sandboxActive: sandbox !== null,
689
+ adjudicationConfigured: hasAdjudicationPolicy(settings)
690
+ });
691
+ console.log("Sandbox:");
692
+ console.log(" Enabled: " + settings.sandbox.enabled);
693
+ console.log(" Available: " + (sandbox !== null));
694
+ if (sandbox) console.log(" Enforcing: " + sandbox.describe());
695
+ console.log(" Excluded commands: " + settings.sandbox.excludedCommands.length);
696
+ console.log(" Unsandboxed commands: " + policy.unsandboxedCommands);
697
+ console.log(" Auto-allow Bash: " + policy.autoAllowBash);
698
+ console.log();
699
+ const { describeShell, resolveShell } = await import("./shell-selection-V6QAYXPX.js");
700
+ const shell = config.shell ?? resolveShell({ requested: settings.shell });
701
+ console.log("Shell:");
702
+ console.log(" Bash tool runs: " + describeShell(shell));
703
+ console.log(" Selected by: " + shell.source);
704
+ if (shell.warning) console.log(` [!] ${shell.warning}`);
705
+ console.log();
706
+ console.log("Managed agents:");
707
+ const agentDiagnostics = collectAgentDiagnostics(
708
+ config,
709
+ withBuiltInAgents(discoverAgents(config.workspace))
710
+ );
711
+ if (agentDiagnostics.length === 0) {
712
+ console.log(" [x] Profiles and overrides look valid");
713
+ } else {
714
+ for (const diagnostic of agentDiagnostics) {
715
+ console.log(` [!] ${diagnostic.message}`);
716
+ }
717
+ }
718
+ for (const directory of [
719
+ join8(config.workspace, ".claude", "agents"),
720
+ join8(config.workspace, ".agents", "agents")
721
+ ]) {
722
+ if (existsSync4(directory)) {
723
+ console.log(
724
+ ` [!] Third-party definitions found at ${directory}; preview them with /agents import ${directory}.`
725
+ );
726
+ }
727
+ }
728
+ console.log();
729
+ const { createModelWindowStore } = await import("./model-window-store-YFVNXCIP.js");
730
+ const windowStore = createModelWindowStore();
731
+ const allLearned = windowStore.all();
732
+ const learnedModels = Object.keys(allLearned);
733
+ console.log("Learned context windows:");
734
+ if (learnedModels.length === 0) {
735
+ console.log(" (none)");
736
+ } else {
737
+ for (const modelName of learnedModels) {
738
+ const entry = windowStore.getEntry(modelName) ?? allLearned[modelName];
739
+ console.log(
740
+ ` ${modelName}: ${entry.contextWindow.toLocaleString()} tokens (${formatRelativeTime(entry.learnedAt)})`
741
+ );
742
+ }
743
+ }
744
+ console.log();
745
+ console.log("Environment:");
746
+ for (const key of ["BOOK_API_KEY", "BOOK_BASE_URL", "BOOK_MODEL", "BOOK_WORKSPACE"]) {
747
+ const val = process.env[key];
748
+ if (key === "BOOK_API_KEY" && val) {
749
+ console.log(" " + key + ": *** (set)");
750
+ } else {
751
+ console.log(" " + key + ": " + (val || "(not set)"));
752
+ }
753
+ }
754
+ }
755
+
756
+ // src/cli/tool-stats.ts
757
+ var DAY_MS = 864e5;
758
+ async function runToolStatsCommand(options) {
759
+ const settings = resolveSettings(options.workspace);
760
+ const retentionDays = settings.observability.toolTelemetryRetentionDays;
761
+ let windowDays;
762
+ if (options.all) {
763
+ windowDays = void 0;
764
+ } else if (options.since !== void 0) {
765
+ const parsed = Number(options.since);
766
+ windowDays = Number.isFinite(parsed) ? Math.max(0, parsed) : retentionDays;
767
+ } else {
768
+ windowDays = retentionDays;
769
+ }
770
+ const cutoffTs = windowDays === void 0 ? 0 : Date.now() - windowDays * DAY_MS;
771
+ if (options.prune) {
772
+ const result = await pruneToolUseRecords(cutoffTs);
773
+ console.error(
774
+ `Pruned ${result.dropped} record${result.dropped === 1 ? "" : "s"} older than ${windowDays ?? 0} days (${result.kept} kept).`
775
+ );
776
+ }
777
+ const all = await readToolUseRecords();
778
+ const records = all.filter((record) => (record.ts ?? 0) >= cutoffTs);
779
+ const aggregate = aggregateToolUse(records);
780
+ if (options.json) {
781
+ console.log(JSON.stringify(aggregate, null, 2));
782
+ return;
783
+ }
784
+ console.log(formatToolStatsReport(aggregate));
785
+ if (windowDays !== void 0 && aggregate.totalCalls > 0) {
786
+ console.log(
787
+ `
788
+ (Window: last ${windowDays} days. Use --all for full history, --since <days> to change.)`
789
+ );
790
+ }
791
+ }
792
+
793
+ // src/cli/config-cmd.ts
794
+ async function runConfigCommand(workspace, action, key, value, settingsOptions = {}) {
795
+ const resolveMergedSettings = () => settingsOptions.noSettings ? structuredClone(DEFAULT_SETTINGS) : resolveSettings(workspace, settingsOptions.settingsOverridePath);
796
+ const scopeRead = settingsOptions.scope ? readScopeDocument(settingsOptions.scope, workspace) : void 0;
797
+ if (!action || action === "list") {
798
+ if (scopeRead) {
799
+ console.log(`${settingsScopeLabel(scopeRead.scope)} settings (${scopeRead.path}):`);
800
+ if (scopeRead.status === "unreadable") {
801
+ console.error(`Could not read it: ${scopeRead.error ?? "unknown error"}`);
802
+ exit(1);
803
+ }
804
+ console.log(
805
+ scopeRead.status === "present" ? JSON.stringify(redactSettingsForDisplay(scopeRead.document), null, 2) : "No settings file at this scope yet."
806
+ );
807
+ return;
808
+ }
809
+ console.log("Resolved settings:");
810
+ console.log(JSON.stringify(redactSettingsForDisplay(resolveMergedSettings()), null, 2));
811
+ return;
812
+ }
813
+ if (action === "get") {
814
+ if (!key) {
815
+ console.error("Usage: book config get <key>");
816
+ exit(1);
817
+ }
818
+ if (scopeRead?.status === "unreadable") {
819
+ console.error(
820
+ `Could not read ${settingsScopeLabel(scopeRead.scope)} settings (${scopeRead.path}): ${scopeRead.error ?? "unknown error"}`
821
+ );
822
+ exit(1);
823
+ }
824
+ const source = scopeRead ? scopeRead.document : resolveMergedSettings();
825
+ const val = getNestedValue(source, key);
826
+ if (val === void 0) {
827
+ console.log(
828
+ scopeRead ? `Key ${key} is not set in ${settingsScopeLabel(scopeRead.scope)} settings (${scopeRead.path}).` : "Key " + key + " is not set (no value)."
829
+ );
830
+ } else {
831
+ console.log(JSON.stringify(redactSettingValue(key, val), null, 2));
832
+ }
833
+ return;
834
+ }
835
+ if (action === "set") {
836
+ if (!key || value === void 0) {
837
+ console.error("Usage: book config set <key> <value>");
838
+ exit(1);
839
+ }
840
+ let parsedValue;
841
+ try {
842
+ parsedValue = JSON.parse(value);
843
+ } catch {
844
+ parsedValue = value;
845
+ }
846
+ const scope = settingsOptions.scope ?? "user";
847
+ const result = applySettingWrite({
848
+ workspace,
849
+ key,
850
+ value: parsedValue,
851
+ scope,
852
+ settingsOverridePath: settingsOptions.settingsOverridePath,
853
+ noSettings: settingsOptions.noSettings
854
+ });
855
+ if (!result.ok) {
856
+ console.error(result.error);
857
+ exit(1);
858
+ return;
859
+ }
860
+ console.log(
861
+ "Set " + key + " = " + JSON.stringify(redactSettingValue(key, result.value)) + " in " + settingsScopeLabel(result.scope) + " settings (" + result.path + ")"
862
+ );
863
+ for (const shadow of result.shadowedBy) {
864
+ console.warn(`\u26A0 ${describeSettingShadow(shadow, key)}`);
865
+ }
866
+ return;
867
+ }
868
+ if (action === "unset") {
869
+ if (!key) {
870
+ console.error("Usage: book config unset <key>");
871
+ exit(1);
872
+ }
873
+ const scope = settingsOptions.scope ?? "user";
874
+ const layer = readScopeDocument(scope, workspace);
875
+ if (layer.status === "unreadable") {
876
+ console.error(
877
+ `Could not read ${settingsScopeLabel(scope)} settings (${layer.path}): ${layer.error ?? "unknown error"}`
878
+ );
879
+ exit(1);
880
+ }
881
+ if (layer.status === "absent" || getNestedValue(layer.document, key) === void 0) {
882
+ console.log(
883
+ `${key} is not set in ${settingsScopeLabel(scope)} settings (${layer.path}); nothing to remove.`
884
+ );
885
+ return;
886
+ }
887
+ const result = new SettingsRepository(layer.path).remove([key]);
888
+ if (!result.ok) {
889
+ console.error(formatSettingsDiagnostics(result.diagnostics));
890
+ exit(1);
891
+ }
892
+ console.log(`Removed ${key} from ${settingsScopeLabel(scope)} settings (${result.path})`);
893
+ return;
894
+ }
895
+ console.error(
896
+ "Unknown action: " + action + ". Use: get <key>, set <key> <value>, unset <key>, or list"
897
+ );
898
+ exit(1);
899
+ }
900
+
901
+ // src/cli/trust-cmd.ts
902
+ import { join as join2 } from "path";
903
+ function fail(message) {
904
+ console.error(message);
905
+ return exit(1);
906
+ }
907
+ async function runTrustCommand(kind, target, options) {
908
+ const workspace = options.workspace;
909
+ const choice = options.reject ? "rejected" : "approved";
910
+ const verb = options.reject ? "Rejected" : "Approved";
911
+ if (options.allPending && target !== void 0) {
912
+ fail(`book trust ${kind}: pass either a target or --all-pending, not both`);
913
+ }
914
+ if (!options.allPending && target === void 0) {
915
+ fail(
916
+ `book trust ${kind}: name what to trust, or pass --all-pending. Run \`book doctor\` to list what is withheld.`
917
+ );
918
+ }
919
+ let settings;
920
+ try {
921
+ settings = resolveSettings(workspace);
922
+ } catch (error) {
923
+ return fail(
924
+ `book trust ${kind}: cannot read settings for ${workspace}: ${error instanceof Error ? error.message : String(error)}`
925
+ );
926
+ }
927
+ const projectSettings = loadSettingsFile(join2(workspace, ".book", "settings.json"));
928
+ const decisions = [];
929
+ if (kind === "command") {
930
+ const partition = partitionProjectCommands(discoverCommands(workspace), settings);
931
+ const push = (command) => {
932
+ if (!options.reject) {
933
+ console.log(`/${displayableCommandName(command.name)} runs:`);
934
+ for (const line of describeProjectCommandShell(command)) console.log(` ${line}`);
935
+ }
936
+ decisions.push({
937
+ key: command.name,
938
+ label: `/${displayableCommandName(command.name)}`,
939
+ fingerprint: projectCommandFingerprint(command.body)
940
+ });
941
+ };
942
+ if (options.allPending) {
943
+ if (partition.pending.length === 0) {
944
+ console.log(`No project commands are awaiting a decision in ${workspace}.`);
945
+ return;
946
+ }
947
+ for (const command of partition.pending) push(command);
948
+ } else {
949
+ const name = target.replace(/^\//, "");
950
+ const decidable = [...partition.pending, ...partition.approved, ...partition.rejected];
951
+ const command = decidable.find((candidate) => candidate.name === name);
952
+ if (!command) {
953
+ const known = partition.pending.map((pending) => ` /${displayableCommandName(pending.name)}`).join("\n");
954
+ return fail(
955
+ `book trust command: ${workspace} has no project command "${name}" that runs shell.` + (known ? `
956
+ Awaiting a decision:
957
+ ${known}` : "\nNothing is awaiting a decision.")
958
+ );
959
+ }
960
+ push(command);
961
+ }
962
+ } else if (kind === "hook") {
963
+ const declared = collectDeclaredHooks(projectSettings);
964
+ const byFingerprint = new Map(
965
+ declared.map((hook) => [hookFingerprint(hook.event, hook.entry), hook])
966
+ );
967
+ const partition = partitionProjectHooks(declared, settings.hooks.projectEntries);
968
+ if (options.allPending) {
969
+ if (partition.pending.length === 0) {
970
+ console.log(`No project-declared hooks are awaiting a decision in ${workspace}.`);
971
+ return;
972
+ }
973
+ for (const hook of partition.pending) {
974
+ const fingerprint = hookFingerprint(hook.event, hook.entry);
975
+ decisions.push({ key: fingerprint, label: describeDeclaredHook(hook).headline });
976
+ }
977
+ } else {
978
+ const hook = byFingerprint.get(target);
979
+ if (!hook) {
980
+ const known = partition.pending.map(
981
+ (pending) => ` ${hookFingerprint(pending.event, pending.entry)} ${describeDeclaredHook(pending).headline}`
982
+ ).join("\n");
983
+ return fail(
984
+ `book trust hook: no project-declared hook in ${workspace} has fingerprint "${target}".` + (known ? `
985
+ Awaiting a decision:
986
+ ${known}` : "\nNothing is awaiting a decision.")
987
+ );
988
+ }
989
+ decisions.push({ key: target, label: describeDeclaredHook(hook).headline });
990
+ }
991
+ } else {
992
+ const declared = projectSettings?.permissions?.allow ?? [];
993
+ const partition = partitionProjectAllowRules(declared, settings.permissions.projectAllowRules);
994
+ if (options.allPending) {
995
+ if (partition.pending.length === 0) {
996
+ console.log(`No project-declared allow rules are awaiting a decision in ${workspace}.`);
997
+ return;
998
+ }
999
+ for (const rule of partition.pending) decisions.push({ key: rule, label: rule });
1000
+ } else {
1001
+ if (!declared.includes(target)) {
1002
+ const known = partition.pending.map((rule) => ` ${rule}`).join("\n");
1003
+ return fail(
1004
+ `book trust rule: ${workspace} declares no project allow rule "${target}".` + (known ? `
1005
+ Awaiting a decision:
1006
+ ${known}` : "\nNothing is awaiting a decision.")
1007
+ );
1008
+ }
1009
+ decisions.push({ key: target, label: target });
1010
+ }
1011
+ }
1012
+ const failures = [];
1013
+ for (const decision of decisions) {
1014
+ const result = kind === "command" ? persistProjectCommandChoice(workspace, decision.key, decision.fingerprint, choice) : kind === "hook" ? persistProjectHookChoice(workspace, decision.key, choice) : persistProjectAllowRuleChoice(workspace, decision.key, choice);
1015
+ if (result.ok) console.log(`${verb} ${decision.label}`);
1016
+ else failures.push(`${decision.label}: ${result.error ?? "unknown error"}`);
1017
+ }
1018
+ if (failures.length > 0) {
1019
+ fail(
1020
+ `Failed to record ${failures.length} decision(s):
1021
+ ${failures.map((f) => ` ${f}`).join("\n")}`
1022
+ );
1023
+ }
1024
+ }
1025
+
1026
+ // src/cli/run.ts
1027
+ import { render } from "ink";
1028
+ import { createElement } from "react";
1029
+
1030
+ // src/mcp-host.ts
1031
+ var log = createDebugLogger("mcp");
1032
+ var MAX_EVENTS = 50;
1033
+ var McpSessionHost = class {
1034
+ constructor(workspace, settings, options = {}) {
1035
+ this.workspace = workspace;
1036
+ this.settings = settings;
1037
+ this.options = options;
1038
+ this.persistChoice = options.persistChoice ?? persistMcpProjectServerChoice;
1039
+ }
1040
+ workspace;
1041
+ settings;
1042
+ options;
1043
+ states = /* @__PURE__ */ new Map();
1044
+ connections = [];
1045
+ listeners = /* @__PURE__ */ new Set();
1046
+ events = [];
1047
+ abortController = new AbortController();
1048
+ persistChoice;
1049
+ elicitationHandler;
1050
+ nextEventId = 1;
1051
+ snapshot = { servers: [], pendingApprovals: [], events: [] };
1052
+ started = false;
1053
+ disposed = false;
1054
+ /** Resolve declared servers and begin connecting every already-trusted one. */
1055
+ start() {
1056
+ if (this.started || this.disposed) return;
1057
+ this.started = true;
1058
+ const servers = resolveMcpServerList(this.workspace, {
1059
+ home: this.options.home,
1060
+ onDiagnostic: (diagnostic) => log.debug(diagnostic.message)
1061
+ });
1062
+ for (const server of servers) {
1063
+ const approval = evaluateMcpServerApproval(this.settings, server);
1064
+ const recorded = this.settings.mcp?.projectServers?.[server.name];
1065
+ const fingerprint = mcpServerFingerprint(server.config);
1066
+ this.states.set(server.name, {
1067
+ server,
1068
+ fingerprint,
1069
+ status: approval === "approved" ? "connecting" : approval === "rejected" ? "rejected" : "pending-approval",
1070
+ configChangedSinceApproval: approval === "unknown" && recorded !== void 0 && recorded.fingerprint !== fingerprint
1071
+ });
1072
+ }
1073
+ this.rebuildSnapshot();
1074
+ for (const state of this.states.values()) {
1075
+ if (state.status === "connecting") void this.connect(state);
1076
+ }
1077
+ }
1078
+ /** Persist approval for a pending project server and connect it. */
1079
+ approve(name) {
1080
+ const state = this.states.get(name);
1081
+ if (!state || state.status !== "pending-approval") {
1082
+ return { ok: false, error: `No pending approval for MCP server "${name}".` };
1083
+ }
1084
+ const persisted = this.persistChoice(this.workspace, name, state.fingerprint, "approved");
1085
+ if (!persisted.ok) return persisted;
1086
+ state.status = "connecting";
1087
+ state.configChangedSinceApproval = false;
1088
+ this.rebuildSnapshot();
1089
+ void this.connect(state);
1090
+ return { ok: true };
1091
+ }
1092
+ /** Persist rejection for a pending project server. */
1093
+ reject(name) {
1094
+ const state = this.states.get(name);
1095
+ if (!state || state.status !== "pending-approval") {
1096
+ return { ok: false, error: `No pending approval for MCP server "${name}".` };
1097
+ }
1098
+ const persisted = this.persistChoice(this.workspace, name, state.fingerprint, "rejected");
1099
+ if (!persisted.ok) return persisted;
1100
+ state.status = "rejected";
1101
+ this.rebuildSnapshot();
1102
+ return { ok: true };
1103
+ }
1104
+ /** Leave a pending server undecided for this session (re-prompts next session). */
1105
+ defer(name) {
1106
+ const state = this.states.get(name);
1107
+ if (!state || state.status !== "pending-approval") return;
1108
+ state.status = "deferred";
1109
+ this.rebuildSnapshot();
1110
+ }
1111
+ /** Live tool definitions; hosts call this per send so list changes surface. */
1112
+ getToolDefinitions() {
1113
+ return buildMcpToolDefinitions(this.connections, this.options.requestTimeoutMs);
1114
+ }
1115
+ getSnapshot() {
1116
+ return this.snapshot;
1117
+ }
1118
+ subscribe(listener) {
1119
+ this.listeners.add(listener);
1120
+ return () => this.listeners.delete(listener);
1121
+ }
1122
+ /** Install the host prompt used to answer server elicitation requests. */
1123
+ setElicitationHandler(handler) {
1124
+ this.elicitationHandler = handler;
1125
+ }
1126
+ async dispose() {
1127
+ if (this.disposed) return;
1128
+ this.disposed = true;
1129
+ this.abortController.abort(new Error("MCP host disposed"));
1130
+ await disconnectMcpServers(this.connections);
1131
+ this.listeners.clear();
1132
+ }
1133
+ async connect(state) {
1134
+ const { name } = state.server;
1135
+ try {
1136
+ const result = await (this.options.connectServers ?? connectMcpServers)(this.workspace, {
1137
+ servers: { [name]: state.server.config },
1138
+ home: this.options.home,
1139
+ signal: this.abortController.signal,
1140
+ initializationTimeoutMs: this.options.initializationTimeoutMs,
1141
+ requestTimeoutMs: this.options.requestTimeoutMs,
1142
+ onProcessSpawn: this.options.onProcessSpawn,
1143
+ onDiagnostic: (diagnostic) => log.debug(diagnostic.message),
1144
+ onToolListChanged: (serverName) => this.handleToolListChanged(serverName),
1145
+ onConnectionClosed: (serverName) => this.handleConnectionClosed(serverName),
1146
+ // Stable indirection: servers connect in the background before the UI
1147
+ // can install its prompt, so the capability is declared up front and
1148
+ // the handler is resolved per request.
1149
+ onElicit: (request, context) => this.elicitationHandler ? this.elicitationHandler(request, context) : Promise.resolve({ action: "decline" })
1150
+ });
1151
+ const connection = result.connections[0];
1152
+ if (this.disposed) {
1153
+ if (connection) await disconnectMcpServers([connection]);
1154
+ return;
1155
+ }
1156
+ if (!connection) {
1157
+ state.status = "failed";
1158
+ state.error = "connection failed (see debug log)";
1159
+ this.pushEvent({ type: "failed", server: name, error: state.error });
1160
+ return;
1161
+ }
1162
+ if (connection.closed) {
1163
+ state.status = "failed";
1164
+ state.error = "connection closed during startup";
1165
+ this.pushEvent({ type: "failed", server: name, error: state.error });
1166
+ return;
1167
+ }
1168
+ this.connections.push(connection);
1169
+ state.status = "connected";
1170
+ state.toolCount = connection.tools.length;
1171
+ state.error = void 0;
1172
+ this.pushEvent({ type: "connected", server: name, toolCount: connection.tools.length });
1173
+ } catch (error) {
1174
+ state.status = "failed";
1175
+ state.error = error instanceof Error ? error.message : String(error);
1176
+ log.debug(`MCP host connect error for "${name}": ${state.error}`);
1177
+ this.pushEvent({ type: "failed", server: name, error: state.error });
1178
+ }
1179
+ }
1180
+ handleToolListChanged(serverName) {
1181
+ const state = this.states.get(serverName);
1182
+ const connection = this.connections.find((conn) => conn.name === serverName);
1183
+ if (!state || !connection) return;
1184
+ state.toolCount = connection.tools.length;
1185
+ this.pushEvent({ type: "tools-changed", server: serverName, toolCount: state.toolCount });
1186
+ }
1187
+ handleConnectionClosed(serverName) {
1188
+ if (this.disposed) return;
1189
+ const state = this.states.get(serverName);
1190
+ if (!state || state.status !== "connected") return;
1191
+ state.status = "disconnected";
1192
+ state.error = "connection closed";
1193
+ state.toolCount = 0;
1194
+ this.pushEvent({ type: "disconnected", server: serverName });
1195
+ }
1196
+ pushEvent(event) {
1197
+ this.events.push({ ...event, id: this.nextEventId++ });
1198
+ if (this.events.length > MAX_EVENTS) this.events.splice(0, this.events.length - MAX_EVENTS);
1199
+ this.rebuildSnapshot();
1200
+ }
1201
+ rebuildSnapshot() {
1202
+ const servers = [...this.states.values()].map((state) => ({
1203
+ name: state.server.name,
1204
+ source: state.server.source,
1205
+ path: state.server.path,
1206
+ target: formatMcpServerCommand(state.server.config),
1207
+ envKeys: Object.keys(state.server.config.env ?? {}).sort(),
1208
+ headerKeys: Object.keys(state.server.config.headers ?? {}).sort(),
1209
+ fingerprint: state.fingerprint,
1210
+ status: state.status,
1211
+ configChangedSinceApproval: state.configChangedSinceApproval,
1212
+ toolCount: state.toolCount,
1213
+ error: state.error
1214
+ }));
1215
+ this.snapshot = {
1216
+ servers,
1217
+ pendingApprovals: servers.filter((server) => server.status === "pending-approval"),
1218
+ events: [...this.events]
1219
+ };
1220
+ for (const listener of this.listeners) listener();
1221
+ }
1222
+ };
1223
+
1224
+ // src/cli/run.ts
1225
+ import { join as join6 } from "path";
1226
+
1227
+ // src/rewind/snapshot-store.ts
1228
+ import {
1229
+ chmodSync,
1230
+ existsSync,
1231
+ lstatSync,
1232
+ mkdirSync,
1233
+ readFileSync,
1234
+ readdirSync,
1235
+ readlinkSync,
1236
+ renameSync,
1237
+ rmSync,
1238
+ symlinkSync,
1239
+ unlinkSync,
1240
+ writeFileSync
1241
+ } from "fs";
1242
+ import { lstat, mkdir, readFile, readdir, readlink, rename, rm, writeFile } from "fs/promises";
1243
+ import { createHash, randomUUID } from "crypto";
1244
+ import { dirname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
1245
+ import ignore from "ignore";
1246
+ var REWIND_MAX_ENTRIES = 2e4;
1247
+ var REWIND_MAX_FILE_BYTES = 16 * 1024 * 1024;
1248
+ var REWIND_MAX_LOGICAL_BYTES = 256 * 1024 * 1024;
1249
+ var DEFAULT_IGNORE_PATTERNS = [
1250
+ ".book/",
1251
+ "node_modules/",
1252
+ ".pnpm-store/",
1253
+ ".npm/",
1254
+ ".yarn/",
1255
+ ".venv/",
1256
+ "venv/",
1257
+ "env/",
1258
+ "dist/",
1259
+ "build/",
1260
+ "target/",
1261
+ ".next/",
1262
+ ".cache/",
1263
+ ".turbo/",
1264
+ ".parcel-cache/",
1265
+ ".gradle/",
1266
+ "coverage/",
1267
+ "__pycache__/",
1268
+ ".pytest_cache/",
1269
+ ".mypy_cache/",
1270
+ ".ruff_cache/",
1271
+ ".tox/",
1272
+ "*.pyc"
1273
+ ];
1274
+ function sha256(value) {
1275
+ return createHash("sha256").update(value).digest("hex");
1276
+ }
1277
+ function pathExists(path) {
1278
+ try {
1279
+ lstatSync(path);
1280
+ return true;
1281
+ } catch {
1282
+ return false;
1283
+ }
1284
+ }
1285
+ function normalizeWorkspace(workspace) {
1286
+ const normalized = resolve2(workspace);
1287
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
1288
+ }
1289
+ function rewindWorkspaceHash(workspace) {
1290
+ return sha256(normalizeWorkspace(workspace));
1291
+ }
1292
+ function persistentRewindRoot(workspace) {
1293
+ return join3(resolveBookHome(), "rewind", rewindWorkspaceHash(workspace));
1294
+ }
1295
+ var UnavailableRewindSnapshotStore = class {
1296
+ constructor(reason) {
1297
+ this.reason = reason;
1298
+ }
1299
+ reason;
1300
+ capture() {
1301
+ return { ok: false, reason: this.reason };
1302
+ }
1303
+ async captureAsync() {
1304
+ return this.capture();
1305
+ }
1306
+ getCurrentGitHead() {
1307
+ return void 0;
1308
+ }
1309
+ getManifest() {
1310
+ return void 0;
1311
+ }
1312
+ getAvailability() {
1313
+ return { available: false, reason: this.reason };
1314
+ }
1315
+ restore() {
1316
+ return { ok: false, error: this.reason };
1317
+ }
1318
+ rollback() {
1319
+ return { ok: false, error: this.reason };
1320
+ }
1321
+ discardManifest() {
1322
+ }
1323
+ cleanup() {
1324
+ return { manifests: 0, blobs: 0 };
1325
+ }
1326
+ };
1327
+ function createUnavailableRewindSnapshotStore(reason) {
1328
+ return new UnavailableRewindSnapshotStore(reason);
1329
+ }
1330
+ function createRewindSnapshotStore(workspace, root) {
1331
+ try {
1332
+ return new RewindSnapshotStore(workspace, root);
1333
+ } catch (error) {
1334
+ const detail = error instanceof Error ? error.message : String(error);
1335
+ return createUnavailableRewindSnapshotStore(
1336
+ `Code rewind unavailable: snapshot storage could not be initialized (${detail}).`
1337
+ );
1338
+ }
1339
+ }
1340
+ function readCustomIgnorePatterns(workspace) {
1341
+ const path = join3(workspace, ".book", "rewindignore");
1342
+ if (!existsSync(path)) return [];
1343
+ return readFileSync(path, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
1344
+ }
1345
+ async function readCustomIgnorePatternsAsync(workspace) {
1346
+ const path = join3(workspace, ".book", "rewindignore");
1347
+ try {
1348
+ return (await readFile(path, "utf-8")).split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
1349
+ } catch (error) {
1350
+ if (error.code === "ENOENT") return [];
1351
+ throw error;
1352
+ }
1353
+ }
1354
+ function expandNegatedParents(patterns) {
1355
+ const expanded = [];
1356
+ for (const pattern of patterns) {
1357
+ if (pattern.startsWith("!") && !pattern.slice(1).startsWith("*")) {
1358
+ const parts = pattern.slice(1).split("/").filter(Boolean);
1359
+ for (let index = 1; index < parts.length; index++) {
1360
+ expanded.push(`!${parts.slice(0, index).join("/")}/`);
1361
+ }
1362
+ }
1363
+ expanded.push(pattern);
1364
+ }
1365
+ return expanded;
1366
+ }
1367
+ function readOptionalText(path) {
1368
+ try {
1369
+ return readFileSync(path, "utf-8").trim();
1370
+ } catch {
1371
+ return void 0;
1372
+ }
1373
+ }
1374
+ function resolveGitDirectory(workspace) {
1375
+ const dotGit = join3(workspace, ".git");
1376
+ try {
1377
+ if (lstatSync(dotGit).isDirectory()) return dotGit;
1378
+ } catch {
1379
+ return void 0;
1380
+ }
1381
+ const match = /^gitdir:\s*(.+)$/i.exec(readOptionalText(dotGit) ?? "");
1382
+ return match ? resolve2(dirname(dotGit), match[1]) : void 0;
1383
+ }
1384
+ function resolveCommonGitDirectory(gitDirectory) {
1385
+ const commonDirectory = readOptionalText(join3(gitDirectory, "commondir"));
1386
+ return commonDirectory ? resolve2(gitDirectory, commonDirectory) : gitDirectory;
1387
+ }
1388
+ function parseGitHash(value) {
1389
+ const hash = value?.trim();
1390
+ return hash && /^[0-9a-f]{40,64}$/i.test(hash) ? hash : void 0;
1391
+ }
1392
+ function readPackedRef(directory, ref) {
1393
+ const packedRefs = readOptionalText(join3(directory, "packed-refs"));
1394
+ if (!packedRefs) return void 0;
1395
+ for (const line of packedRefs.split(/\r?\n/)) {
1396
+ if (!line || line.startsWith("#") || line.startsWith("^")) continue;
1397
+ const separator = line.indexOf(" ");
1398
+ if (separator > 0 && line.slice(separator + 1) === ref) {
1399
+ return parseGitHash(line.slice(0, separator));
1400
+ }
1401
+ }
1402
+ return void 0;
1403
+ }
1404
+ function resolveGitRef(gitDirectory, commonDirectory, ref, depth = 0) {
1405
+ if (depth > 4 || !ref.startsWith("refs/") || ref.includes("..") || ref.includes("\\") || ref.includes("\0")) {
1406
+ return void 0;
1407
+ }
1408
+ for (const directory of /* @__PURE__ */ new Set([gitDirectory, commonDirectory])) {
1409
+ const value = readOptionalText(join3(directory, ...ref.split("/")));
1410
+ const hash = parseGitHash(value);
1411
+ if (hash) return hash;
1412
+ if (value?.startsWith("ref:")) {
1413
+ const resolved = resolveGitRef(
1414
+ gitDirectory,
1415
+ commonDirectory,
1416
+ value.slice("ref:".length).trim(),
1417
+ depth + 1
1418
+ );
1419
+ if (resolved) return resolved;
1420
+ }
1421
+ }
1422
+ return readPackedRef(gitDirectory, ref) ?? (commonDirectory === gitDirectory ? void 0 : readPackedRef(commonDirectory, ref));
1423
+ }
1424
+ function gitHead(workspace) {
1425
+ const gitDirectory = resolveGitDirectory(workspace);
1426
+ if (!gitDirectory) return void 0;
1427
+ const head = readOptionalText(join3(gitDirectory, "HEAD"));
1428
+ const directHash = parseGitHash(head);
1429
+ if (directHash) return directHash;
1430
+ if (!head?.startsWith("ref:")) return void 0;
1431
+ return resolveGitRef(
1432
+ gitDirectory,
1433
+ resolveCommonGitDirectory(gitDirectory),
1434
+ head.slice("ref:".length).trim()
1435
+ );
1436
+ }
1437
+ async function gitHeadAsync(workspace) {
1438
+ return gitHead(workspace);
1439
+ }
1440
+ function assertSafeRelativePath(path) {
1441
+ if (!path || path.includes("\0") || isAbsolute(path) || path.includes("\\")) {
1442
+ throw new Error(`Unsafe rewind path: ${JSON.stringify(path)}`);
1443
+ }
1444
+ const segments = path.split("/");
1445
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
1446
+ throw new Error(`Unsafe rewind path: ${JSON.stringify(path)}`);
1447
+ }
1448
+ if (segments.includes(".git")) throw new Error("Rewind snapshots may not contain .git paths.");
1449
+ }
1450
+ function resolveEntryPath(workspace, path) {
1451
+ assertSafeRelativePath(path);
1452
+ const target = resolve2(workspace, ...path.split("/"));
1453
+ const rel = relative(workspace, target);
1454
+ if (rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel)) {
1455
+ throw new Error(`Rewind path escapes the workspace: ${path}`);
1456
+ }
1457
+ return target;
1458
+ }
1459
+ function walkIncludedEntries(workspace, patterns, maxEntries = Number.POSITIVE_INFINITY) {
1460
+ const matcher = ignore().add(patterns);
1461
+ const hasNegations = patterns.some((pattern) => pattern.startsWith("!"));
1462
+ const entries = [];
1463
+ const visit = (directory, prefix) => {
1464
+ const children = readdirSync(directory, { withFileTypes: true }).sort(
1465
+ (a, b) => a.name.localeCompare(b.name)
1466
+ );
1467
+ for (const child of children) {
1468
+ if (child.name === ".git") continue;
1469
+ const path = prefix ? `${prefix}/${child.name}` : child.name;
1470
+ const absolutePath = join3(directory, child.name);
1471
+ if (child.isDirectory()) {
1472
+ if (!matcher.ignores(`${path}/`) || hasNegations) visit(absolutePath, path);
1473
+ continue;
1474
+ }
1475
+ if (matcher.ignores(path)) continue;
1476
+ const stat = lstatSync(absolutePath);
1477
+ if (!stat.isFile() && !stat.isSymbolicLink()) continue;
1478
+ entries.push({
1479
+ path,
1480
+ absolutePath,
1481
+ kind: stat.isSymbolicLink() ? "symlink" : "file",
1482
+ byteSize: stat.isSymbolicLink() ? Buffer.byteLength(readlinkSync(absolutePath)) : stat.size,
1483
+ mode: stat.mode,
1484
+ mtimeMs: stat.mtimeMs
1485
+ });
1486
+ if (entries.length > maxEntries) {
1487
+ throw new Error(`Code rewind unavailable: checkpoint exceeds ${maxEntries} entries.`);
1488
+ }
1489
+ }
1490
+ };
1491
+ visit(workspace, "");
1492
+ return entries;
1493
+ }
1494
+ async function walkIncludedEntriesAsync(workspace, patterns, maxEntries = Number.POSITIVE_INFINITY) {
1495
+ const matcher = ignore().add(patterns);
1496
+ const hasNegations = patterns.some((pattern) => pattern.startsWith("!"));
1497
+ const entries = [];
1498
+ const visit = async (directory, prefix) => {
1499
+ const children = (await readdir(directory, { withFileTypes: true })).sort(
1500
+ (a, b) => a.name.localeCompare(b.name)
1501
+ );
1502
+ const files = [];
1503
+ for (const child of children) {
1504
+ if (child.name === ".git") continue;
1505
+ const path = prefix ? `${prefix}/${child.name}` : child.name;
1506
+ const absolutePath = join3(directory, child.name);
1507
+ if (child.isDirectory()) {
1508
+ if (!matcher.ignores(`${path}/`) || hasNegations) await visit(absolutePath, path);
1509
+ continue;
1510
+ }
1511
+ if (matcher.ignores(path)) continue;
1512
+ files.push({ child, path, absolutePath });
1513
+ }
1514
+ const stats = await Promise.all(files.map(({ absolutePath }) => lstat(absolutePath)));
1515
+ for (let index = 0; index < files.length; index++) {
1516
+ const { path, absolutePath } = files[index];
1517
+ const stat = stats[index];
1518
+ if (!stat.isFile() && !stat.isSymbolicLink()) continue;
1519
+ entries.push({
1520
+ path,
1521
+ absolutePath,
1522
+ kind: stat.isSymbolicLink() ? "symlink" : "file",
1523
+ byteSize: stat.isSymbolicLink() ? Buffer.byteLength(await readlink(absolutePath, { encoding: "utf-8" })) : stat.size,
1524
+ mode: stat.mode,
1525
+ mtimeMs: stat.mtimeMs
1526
+ });
1527
+ if (entries.length > maxEntries) {
1528
+ throw new Error(`Code rewind unavailable: checkpoint exceeds ${maxEntries} entries.`);
1529
+ }
1530
+ }
1531
+ };
1532
+ await visit(workspace, "");
1533
+ return entries;
1534
+ }
1535
+ function writeAtomic(path, contents) {
1536
+ mkdirSync(dirname(path), { recursive: true });
1537
+ const temporary = join3(dirname(path), `.book-rewind-${randomUUID()}.tmp`);
1538
+ try {
1539
+ writeFileSync(temporary, contents);
1540
+ renameSync(temporary, path);
1541
+ } finally {
1542
+ if (pathExists(temporary)) unlinkSync(temporary);
1543
+ }
1544
+ }
1545
+ async function writeAtomicAsync(path, contents) {
1546
+ await mkdir(dirname(path), { recursive: true });
1547
+ const temporary = join3(dirname(path), `.book-rewind-${randomUUID()}.tmp`);
1548
+ try {
1549
+ await writeFile(temporary, contents);
1550
+ await rename(temporary, path);
1551
+ } finally {
1552
+ await rm(temporary, { force: true });
1553
+ }
1554
+ }
1555
+ async function pathExistsAsync(path) {
1556
+ try {
1557
+ await lstat(path);
1558
+ return true;
1559
+ } catch {
1560
+ return false;
1561
+ }
1562
+ }
1563
+ var RewindSnapshotStore = class {
1564
+ constructor(workspace, root = persistentRewindRoot(workspace), limits = {}) {
1565
+ this.root = root;
1566
+ this.workspace = resolve2(workspace);
1567
+ this.limits = {
1568
+ maxEntries: limits.maxEntries ?? REWIND_MAX_ENTRIES,
1569
+ maxFileBytes: limits.maxFileBytes ?? REWIND_MAX_FILE_BYTES,
1570
+ maxLogicalBytes: limits.maxLogicalBytes ?? REWIND_MAX_LOGICAL_BYTES
1571
+ };
1572
+ this.manifestsRoot = join3(root, "manifests");
1573
+ this.entrySetsRoot = join3(root, "entry-sets");
1574
+ this.blobsRoot = join3(root, "blobs");
1575
+ mkdirSync(this.manifestsRoot, { recursive: true });
1576
+ mkdirSync(this.entrySetsRoot, { recursive: true });
1577
+ mkdirSync(this.blobsRoot, { recursive: true });
1578
+ }
1579
+ root;
1580
+ manifestsRoot;
1581
+ entrySetsRoot;
1582
+ blobsRoot;
1583
+ workspace;
1584
+ limits;
1585
+ fileCache = /* @__PURE__ */ new Map();
1586
+ capture(ignorePatterns) {
1587
+ const head = gitHead(this.workspace);
1588
+ const patterns = ignorePatterns ?? expandNegatedParents([
1589
+ ...DEFAULT_IGNORE_PATTERNS,
1590
+ ...readCustomIgnorePatterns(this.workspace)
1591
+ ]);
1592
+ try {
1593
+ const walked = walkIncludedEntries(this.workspace, patterns, this.limits.maxEntries);
1594
+ let logicalBytes = 0;
1595
+ const entries = [];
1596
+ const nextCache = /* @__PURE__ */ new Map();
1597
+ for (const entry of walked) {
1598
+ if (entry.byteSize > this.limits.maxFileBytes) {
1599
+ throw new Error(
1600
+ `Code rewind unavailable: ${entry.path} exceeds the ${this.limits.maxFileBytes}-byte per-file limit.`
1601
+ );
1602
+ }
1603
+ const cached = this.fileCache.get(entry.path);
1604
+ if (cached && cached.kind === entry.kind && cached.byteSize === entry.byteSize && cached.mode === entry.mode && cached.mtimeMs === entry.mtimeMs) {
1605
+ logicalBytes += cached.snapshotEntry.byteSize;
1606
+ if (logicalBytes > this.limits.maxLogicalBytes) {
1607
+ throw new Error(
1608
+ `Code rewind unavailable: checkpoint exceeds ${this.limits.maxLogicalBytes} bytes of file content.`
1609
+ );
1610
+ }
1611
+ entries.push(cached.snapshotEntry);
1612
+ nextCache.set(entry.path, cached);
1613
+ continue;
1614
+ }
1615
+ const contents = entry.kind === "symlink" ? Buffer.from(readlinkSync(entry.absolutePath), "utf-8") : readFileSync(entry.absolutePath);
1616
+ if (contents.byteLength > this.limits.maxFileBytes) {
1617
+ throw new Error(
1618
+ `Code rewind unavailable: ${entry.path} exceeds the ${this.limits.maxFileBytes}-byte per-file limit.`
1619
+ );
1620
+ }
1621
+ logicalBytes += contents.byteLength;
1622
+ if (logicalBytes > this.limits.maxLogicalBytes) {
1623
+ throw new Error(
1624
+ `Code rewind unavailable: checkpoint exceeds ${this.limits.maxLogicalBytes} bytes of file content.`
1625
+ );
1626
+ }
1627
+ const blobHash = sha256(contents);
1628
+ const blobPath = join3(this.blobsRoot, blobHash);
1629
+ if (!existsSync(blobPath)) writeAtomic(blobPath, contents);
1630
+ const snapshotEntry = {
1631
+ path: entry.path.replaceAll("\\", "/"),
1632
+ kind: entry.kind,
1633
+ blobHash,
1634
+ byteSize: contents.byteLength,
1635
+ mode: entry.mode
1636
+ };
1637
+ entries.push(snapshotEntry);
1638
+ nextCache.set(entry.path, {
1639
+ kind: entry.kind,
1640
+ byteSize: entry.byteSize,
1641
+ mode: entry.mode,
1642
+ mtimeMs: entry.mtimeMs,
1643
+ snapshotEntry
1644
+ });
1645
+ }
1646
+ const manifest = {
1647
+ version: 1,
1648
+ id: randomUUID(),
1649
+ workspace: normalizeWorkspace(this.workspace),
1650
+ createdAt: Date.now(),
1651
+ gitHead: head,
1652
+ ignorePatterns: patterns,
1653
+ entries,
1654
+ logicalBytes
1655
+ };
1656
+ this.writeManifest(manifest);
1657
+ this.fileCache = nextCache;
1658
+ return { ok: true, manifest };
1659
+ } catch (error) {
1660
+ return {
1661
+ ok: false,
1662
+ reason: error instanceof Error ? error.message : String(error),
1663
+ gitHead: head
1664
+ };
1665
+ }
1666
+ }
1667
+ async captureAsync(ignorePatterns) {
1668
+ let head;
1669
+ try {
1670
+ const [resolvedHead, customIgnorePatterns] = await Promise.all([
1671
+ gitHeadAsync(this.workspace),
1672
+ ignorePatterns ? Promise.resolve([]) : readCustomIgnorePatternsAsync(this.workspace)
1673
+ ]);
1674
+ head = resolvedHead;
1675
+ const patterns = ignorePatterns ?? expandNegatedParents([...DEFAULT_IGNORE_PATTERNS, ...customIgnorePatterns]);
1676
+ const walked = await walkIncludedEntriesAsync(
1677
+ this.workspace,
1678
+ patterns,
1679
+ this.limits.maxEntries
1680
+ );
1681
+ let logicalBytes = 0;
1682
+ const entries = [];
1683
+ const nextCache = /* @__PURE__ */ new Map();
1684
+ for (const entry of walked) {
1685
+ if (entry.byteSize > this.limits.maxFileBytes) {
1686
+ throw new Error(
1687
+ `Code rewind unavailable: ${entry.path} exceeds the ${this.limits.maxFileBytes}-byte per-file limit.`
1688
+ );
1689
+ }
1690
+ const cached = this.fileCache.get(entry.path);
1691
+ if (cached && cached.kind === entry.kind && cached.byteSize === entry.byteSize && cached.mode === entry.mode && cached.mtimeMs === entry.mtimeMs) {
1692
+ logicalBytes += cached.snapshotEntry.byteSize;
1693
+ if (logicalBytes > this.limits.maxLogicalBytes) {
1694
+ throw new Error(
1695
+ `Code rewind unavailable: checkpoint exceeds ${this.limits.maxLogicalBytes} bytes of file content.`
1696
+ );
1697
+ }
1698
+ entries.push(cached.snapshotEntry);
1699
+ nextCache.set(entry.path, cached);
1700
+ continue;
1701
+ }
1702
+ const contents = entry.kind === "symlink" ? Buffer.from(await readlink(entry.absolutePath, { encoding: "utf-8" }), "utf-8") : await readFile(entry.absolutePath);
1703
+ if (contents.byteLength > this.limits.maxFileBytes) {
1704
+ throw new Error(
1705
+ `Code rewind unavailable: ${entry.path} exceeds the ${this.limits.maxFileBytes}-byte per-file limit.`
1706
+ );
1707
+ }
1708
+ logicalBytes += contents.byteLength;
1709
+ if (logicalBytes > this.limits.maxLogicalBytes) {
1710
+ throw new Error(
1711
+ `Code rewind unavailable: checkpoint exceeds ${this.limits.maxLogicalBytes} bytes of file content.`
1712
+ );
1713
+ }
1714
+ const blobHash = sha256(contents);
1715
+ const blobPath = join3(this.blobsRoot, blobHash);
1716
+ if (!await pathExistsAsync(blobPath)) await writeAtomicAsync(blobPath, contents);
1717
+ const snapshotEntry = {
1718
+ path: entry.path.replaceAll("\\", "/"),
1719
+ kind: entry.kind,
1720
+ blobHash,
1721
+ byteSize: contents.byteLength,
1722
+ mode: entry.mode
1723
+ };
1724
+ entries.push(snapshotEntry);
1725
+ nextCache.set(entry.path, {
1726
+ kind: entry.kind,
1727
+ byteSize: entry.byteSize,
1728
+ mode: entry.mode,
1729
+ mtimeMs: entry.mtimeMs,
1730
+ snapshotEntry
1731
+ });
1732
+ }
1733
+ const manifest = {
1734
+ version: 1,
1735
+ id: randomUUID(),
1736
+ workspace: normalizeWorkspace(this.workspace),
1737
+ createdAt: Date.now(),
1738
+ gitHead: head,
1739
+ ignorePatterns: patterns,
1740
+ entries,
1741
+ logicalBytes
1742
+ };
1743
+ await this.writeManifestAsync(manifest);
1744
+ this.fileCache = nextCache;
1745
+ return { ok: true, manifest };
1746
+ } catch (error) {
1747
+ return {
1748
+ ok: false,
1749
+ reason: error instanceof Error ? error.message : String(error),
1750
+ gitHead: head
1751
+ };
1752
+ }
1753
+ }
1754
+ getManifest(id) {
1755
+ if (!/^[a-zA-Z0-9-]+$/.test(id)) return void 0;
1756
+ const path = join3(this.manifestsRoot, `${id}.json`);
1757
+ if (!existsSync(path)) return void 0;
1758
+ try {
1759
+ const stored = JSON.parse(readFileSync(path, "utf-8"));
1760
+ let manifest = stored;
1761
+ if (!Array.isArray(stored.entries) && stored.entrySetHash) {
1762
+ if (!/^[a-f0-9]{64}$/.test(stored.entrySetHash)) return void 0;
1763
+ const entriesPath = join3(this.entrySetsRoot, `${stored.entrySetHash}.json`);
1764
+ if (!existsSync(entriesPath)) return void 0;
1765
+ const serializedEntries = readFileSync(entriesPath, "utf-8");
1766
+ if (sha256(serializedEntries) !== stored.entrySetHash) return void 0;
1767
+ manifest = {
1768
+ ...stored,
1769
+ entries: JSON.parse(serializedEntries)
1770
+ };
1771
+ }
1772
+ if (manifest.version !== 1 || manifest.id !== id || normalizeWorkspace(manifest.workspace) !== normalizeWorkspace(this.workspace) || !Array.isArray(manifest.entries)) {
1773
+ return void 0;
1774
+ }
1775
+ return manifest;
1776
+ } catch {
1777
+ return void 0;
1778
+ }
1779
+ }
1780
+ getCurrentGitHead() {
1781
+ return gitHead(this.workspace);
1782
+ }
1783
+ getAvailability(id, expectedGitHead) {
1784
+ if (!id) return { available: false, reason: "No filesystem checkpoint was captured." };
1785
+ if (!this.getManifest(id)) {
1786
+ return { available: false, reason: "The filesystem checkpoint is missing or corrupt." };
1787
+ }
1788
+ if (this.getCurrentGitHead() !== expectedGitHead) {
1789
+ return {
1790
+ available: false,
1791
+ reason: "Git HEAD changed since this prompt; rewind never moves HEAD or the index."
1792
+ };
1793
+ }
1794
+ return { available: true };
1795
+ }
1796
+ restore(id) {
1797
+ const manifest = this.getManifest(id);
1798
+ if (!manifest) return { ok: false, error: "The filesystem checkpoint is missing or corrupt." };
1799
+ const availability = this.getAvailability(id, manifest.gitHead);
1800
+ if (!availability.available) return { ok: false, error: availability.reason ?? "Unavailable." };
1801
+ const safety = this.capture(manifest.ignorePatterns);
1802
+ if (!safety.ok) {
1803
+ return { ok: false, error: `Could not create a safety snapshot: ${safety.reason}` };
1804
+ }
1805
+ if (this.getCurrentGitHead() !== manifest.gitHead) {
1806
+ this.discardManifest(safety.manifest.id);
1807
+ return {
1808
+ ok: false,
1809
+ error: "Git HEAD changed while preparing rewind; no files were restored."
1810
+ };
1811
+ }
1812
+ try {
1813
+ this.applyManifest(manifest);
1814
+ return { ok: true, safetySnapshotId: safety.manifest.id };
1815
+ } catch (error) {
1816
+ const rollback = this.applyManifestSafely(safety.manifest);
1817
+ this.discardManifest(safety.manifest.id);
1818
+ return {
1819
+ ok: false,
1820
+ error: error instanceof Error ? error.message : String(error),
1821
+ ...rollback.ok ? {} : { rollbackError: rollback.error }
1822
+ };
1823
+ }
1824
+ }
1825
+ rollback(safetySnapshotId) {
1826
+ const manifest = this.getManifest(safetySnapshotId);
1827
+ if (!manifest) return { ok: false, error: "The safety snapshot is missing or corrupt." };
1828
+ const result = this.applyManifestSafely(manifest);
1829
+ if (result.ok) this.discardManifest(safetySnapshotId);
1830
+ return result;
1831
+ }
1832
+ discardManifest(id) {
1833
+ if (!/^[a-zA-Z0-9-]+$/.test(id)) return;
1834
+ const path = join3(this.manifestsRoot, `${id}.json`);
1835
+ if (existsSync(path)) unlinkSync(path);
1836
+ }
1837
+ writeManifest(manifest) {
1838
+ const entriesHash = sha256(JSON.stringify(manifest.entries));
1839
+ const entriesPath = join3(this.entrySetsRoot, `${entriesHash}.json`);
1840
+ if (!existsSync(entriesPath)) writeAtomic(entriesPath, JSON.stringify(manifest.entries));
1841
+ const metadata = { ...manifest };
1842
+ delete metadata.entries;
1843
+ writeAtomic(
1844
+ join3(this.manifestsRoot, `${manifest.id}.json`),
1845
+ JSON.stringify({ ...metadata, entrySetHash: entriesHash })
1846
+ );
1847
+ }
1848
+ async writeManifestAsync(manifest) {
1849
+ const entriesHash = sha256(JSON.stringify(manifest.entries));
1850
+ const entriesPath = join3(this.entrySetsRoot, `${entriesHash}.json`);
1851
+ if (!await pathExistsAsync(entriesPath)) {
1852
+ await writeAtomicAsync(entriesPath, JSON.stringify(manifest.entries));
1853
+ }
1854
+ const metadata = { ...manifest };
1855
+ delete metadata.entries;
1856
+ await writeAtomicAsync(
1857
+ join3(this.manifestsRoot, `${manifest.id}.json`),
1858
+ JSON.stringify({ ...metadata, entrySetHash: entriesHash })
1859
+ );
1860
+ }
1861
+ cleanup(referencedSnapshotIds, days) {
1862
+ const cutoff = Date.now() - days * 864e5;
1863
+ let manifests = 0;
1864
+ let blobs = 0;
1865
+ for (const file of readdirSync(this.manifestsRoot)) {
1866
+ if (!file.endsWith(".json")) continue;
1867
+ const id = file.slice(0, -5);
1868
+ const manifest = this.getManifest(id);
1869
+ if (referencedSnapshotIds.has(id)) continue;
1870
+ if (!manifest || manifest.createdAt < cutoff) {
1871
+ unlinkSync(join3(this.manifestsRoot, file));
1872
+ manifests++;
1873
+ }
1874
+ }
1875
+ const referencedBlobs = /* @__PURE__ */ new Set();
1876
+ const referencedEntrySets = /* @__PURE__ */ new Set();
1877
+ for (const file of readdirSync(this.manifestsRoot)) {
1878
+ if (!file.endsWith(".json")) continue;
1879
+ const id = file.slice(0, -5);
1880
+ const manifest = this.getManifest(id);
1881
+ try {
1882
+ const stored = JSON.parse(readFileSync(join3(this.manifestsRoot, file), "utf-8"));
1883
+ if (stored.entrySetHash) referencedEntrySets.add(stored.entrySetHash);
1884
+ } catch {
1885
+ }
1886
+ for (const entry of manifest?.entries ?? []) referencedBlobs.add(entry.blobHash);
1887
+ }
1888
+ for (const file of readdirSync(this.entrySetsRoot)) {
1889
+ if (!file.endsWith(".json") || referencedEntrySets.has(file.slice(0, -5))) continue;
1890
+ rmSync(join3(this.entrySetsRoot, file), { force: true });
1891
+ }
1892
+ for (const file of readdirSync(this.blobsRoot)) {
1893
+ if (referencedBlobs.has(file)) continue;
1894
+ rmSync(join3(this.blobsRoot, file), { force: true });
1895
+ blobs++;
1896
+ }
1897
+ return { manifests, blobs };
1898
+ }
1899
+ applyManifestSafely(manifest) {
1900
+ try {
1901
+ this.applyManifest(manifest);
1902
+ return { ok: true };
1903
+ } catch (error) {
1904
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
1905
+ }
1906
+ }
1907
+ applyManifest(manifest) {
1908
+ if (manifest.entries.length > this.limits.maxEntries) {
1909
+ throw new Error(`Rewind manifest exceeds ${this.limits.maxEntries} entries.`);
1910
+ }
1911
+ const targetPaths = /* @__PURE__ */ new Set();
1912
+ let logicalBytes = 0;
1913
+ for (const entry of manifest.entries) {
1914
+ resolveEntryPath(this.workspace, entry.path);
1915
+ if (!["file", "symlink"].includes(entry.kind) || !Number.isInteger(entry.mode)) {
1916
+ throw new Error(`Invalid rewind entry for ${entry.path}.`);
1917
+ }
1918
+ if (targetPaths.has(entry.path)) throw new Error(`Duplicate rewind path: ${entry.path}.`);
1919
+ if (!/^[a-f0-9]{64}$/.test(entry.blobHash)) {
1920
+ throw new Error(`Invalid blob hash for ${entry.path}.`);
1921
+ }
1922
+ const blobPath = join3(this.blobsRoot, entry.blobHash);
1923
+ if (!existsSync(blobPath)) throw new Error(`Missing rewind blob for ${entry.path}.`);
1924
+ const contents = readFileSync(blobPath);
1925
+ if (sha256(contents) !== entry.blobHash)
1926
+ throw new Error(`Corrupt rewind blob for ${entry.path}.`);
1927
+ if (contents.byteLength !== entry.byteSize || contents.byteLength > this.limits.maxFileBytes) {
1928
+ throw new Error(`Invalid rewind size for ${entry.path}.`);
1929
+ }
1930
+ logicalBytes += contents.byteLength;
1931
+ if (logicalBytes > this.limits.maxLogicalBytes) {
1932
+ throw new Error(`Rewind manifest exceeds ${this.limits.maxLogicalBytes} bytes.`);
1933
+ }
1934
+ targetPaths.add(entry.path);
1935
+ }
1936
+ const current = walkIncludedEntries(
1937
+ this.workspace,
1938
+ manifest.ignorePatterns,
1939
+ this.limits.maxEntries
1940
+ );
1941
+ const removals = current.filter((entry) => !targetPaths.has(entry.path)).sort((a, b) => b.path.length - a.path.length);
1942
+ for (const entry of removals) unlinkSync(entry.absolutePath);
1943
+ for (const entry of manifest.entries) {
1944
+ const target = resolveEntryPath(this.workspace, entry.path);
1945
+ mkdirSync(dirname(target), { recursive: true });
1946
+ if (pathExists(target) && lstatSync(target).isDirectory()) {
1947
+ throw new Error(`Cannot restore ${entry.path} because a directory exists at that path.`);
1948
+ }
1949
+ const contents = readFileSync(join3(this.blobsRoot, entry.blobHash));
1950
+ const temporary = join3(dirname(target), `.book-rewind-${randomUUID()}.tmp`);
1951
+ try {
1952
+ if (entry.kind === "symlink") {
1953
+ symlinkSync(contents.toString("utf-8"), temporary);
1954
+ } else {
1955
+ writeFileSync(temporary, contents, { mode: entry.mode });
1956
+ chmodSync(temporary, entry.mode);
1957
+ }
1958
+ renameSync(temporary, target);
1959
+ } finally {
1960
+ if (pathExists(temporary)) unlinkSync(temporary);
1961
+ }
1962
+ }
1963
+ }
1964
+ };
1965
+
1966
+ // src/rewind/environment.ts
1967
+ import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2 } from "fs";
1968
+ import { tmpdir } from "os";
1969
+ import { join as join4, resolve as resolve3, sep as sep2 } from "path";
1970
+ function createEphemeralRewindEnvironment(workspace, temporaryRoot = tmpdir()) {
1971
+ const resolvedTemporaryRoot = resolve3(temporaryRoot);
1972
+ const root = mkdtempSync(join4(resolvedTemporaryRoot, "book-rewind-"));
1973
+ let disposed = false;
1974
+ const dispose = () => {
1975
+ if (disposed) return;
1976
+ disposed = true;
1977
+ const resolvedRoot = resolve3(root);
1978
+ if (resolvedRoot.startsWith(`${resolvedTemporaryRoot}${sep2}`) && resolvedRoot.split(sep2).at(-1)?.startsWith("book-rewind-") && existsSync2(resolvedRoot)) {
1979
+ rmSync2(resolvedRoot, { recursive: true, force: true });
1980
+ }
1981
+ process.off("exit", dispose);
1982
+ };
1983
+ process.once("exit", dispose);
1984
+ return {
1985
+ root,
1986
+ timelineStore: new SessionStore(join4(root, "sessions")),
1987
+ snapshotStore: createRewindSnapshotStore(workspace, join4(root, "snapshots")),
1988
+ dispose
1989
+ };
1990
+ }
1991
+
1992
+ // src/cli/ink-patch.ts
1993
+ import { readFileSync as readFileSync2 } from "fs";
1994
+ import { createRequire } from "module";
1995
+ import { dirname as dirname2, join as join5 } from "path";
1996
+ var INK_PATCH_MARKER = "ansiEscapes.cursorUp(previousLines.length - 1)";
1997
+ function isInkIncrementalRendererPatched() {
1998
+ try {
1999
+ const require2 = createRequire(import.meta.url);
2000
+ const logUpdatePath = join5(dirname2(require2.resolve("ink")), "log-update.js");
2001
+ return readFileSync2(logUpdatePath, "utf8").includes(INK_PATCH_MARKER);
2002
+ } catch {
2003
+ return false;
2004
+ }
2005
+ }
2006
+
2007
+ // src/cli/tui-renderer-mode.ts
2008
+ function resolveTuiRendererMode(value = process.env.BOOK_TUI_RENDERER, context = {}) {
2009
+ if (context.isTTY === false || context.screenReader) return "safe";
2010
+ if (context.incrementalRendererPatched === false) return "safe";
2011
+ if (value === "incremental") return "incremental";
2012
+ if (value === "experimental-scroll") return "experimental-scroll";
2013
+ if (value === void 0) return context.platform === "win32" ? "safe" : "incremental";
2014
+ return "safe";
2015
+ }
2016
+
2017
+ // src/cli/run.ts
2018
+ import { spawn } from "child_process";
2019
+ var SESSION_ROOT = join6(resolveBookHome(), "sessions");
2020
+ var ENTER_ALT_SCREEN = "\x1B[?1049h";
2021
+ var EXIT_ALT_SCREEN = "\x1B[?1049l";
2022
+ var DISABLE_MOUSE_REPORTING = "\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1015l";
2023
+ var ENABLE_BUTTON_EVENT_TRACKING = "\x1B[?1002h";
2024
+ var ENABLE_SGR_MOUSE = "\x1B[?1006h";
2025
+ var DISABLE_ALTERNATE_SCROLL = "\x1B[?1007l";
2026
+ var RESTORE_ALTERNATE_SCROLL = "\x1B[?1007h";
2027
+ var WSL_TERMINAL_BRIDGE_SCRIPT = `
2028
+ for process in /proc/[0-9]*; do
2029
+ [ -r "$process/environ" ] || continue
2030
+ /usr/bin/grep -Fzqx "WT_SESSION=$2" "$process/environ" 2>/dev/null || continue
2031
+ target=$(/usr/bin/readlink "$process/fd/1" 2>/dev/null) || continue
2032
+ case "$target" in
2033
+ /dev/pts/*)
2034
+ /usr/bin/printf '%s' "$1" > "$process/fd/1"
2035
+ exit $?
2036
+ ;;
2037
+ esac
2038
+ done
2039
+ exit 1
2040
+ `.trim();
2041
+ function shouldBridgeWslTerminal(platform = process.platform, env = process.env) {
2042
+ const sharedVariables = (env.WSLENV ?? "").split(":").map((entry) => entry.split("/")[0]);
2043
+ return platform === "win32" && Boolean(env.WT_SESSION) && sharedVariables.includes("WT_SESSION");
2044
+ }
2045
+ function writeTerminalControl(stdout, sequence) {
2046
+ stdout.write(sequence);
2047
+ if (stdout !== process.stdout || !shouldBridgeWslTerminal()) return;
2048
+ try {
2049
+ const bridge = spawn(
2050
+ "wsl.exe",
2051
+ [
2052
+ "-e",
2053
+ "sh",
2054
+ "-c",
2055
+ WSL_TERMINAL_BRIDGE_SCRIPT,
2056
+ "book-terminal-bridge",
2057
+ sequence,
2058
+ process.env.WT_SESSION
2059
+ ],
2060
+ {
2061
+ detached: true,
2062
+ stdio: "ignore",
2063
+ windowsHide: true
2064
+ }
2065
+ );
2066
+ bridge.on("error", () => {
2067
+ });
2068
+ bridge.unref();
2069
+ } catch {
2070
+ }
2071
+ }
2072
+ function collectSnapshotReferences(store, cwd) {
2073
+ return store.listSnapshotReferences(cwd);
2074
+ }
2075
+ function enterInteractiveScreen(stdout = process.stdout, bridgeWslTerminal = shouldBridgeWslTerminal()) {
2076
+ if (!stdout.isTTY && !bridgeWslTerminal) return () => {
2077
+ };
2078
+ let restored = false;
2079
+ const restore = () => {
2080
+ if (restored) return;
2081
+ restored = true;
2082
+ writeTerminalControl(
2083
+ stdout,
2084
+ DISABLE_MOUSE_REPORTING + RESTORE_ALTERNATE_SCROLL + EXIT_ALT_SCREEN
2085
+ );
2086
+ process.off("exit", restore);
2087
+ };
2088
+ writeTerminalControl(
2089
+ stdout,
2090
+ ENTER_ALT_SCREEN + DISABLE_MOUSE_REPORTING + DISABLE_ALTERNATE_SCROLL + ENABLE_BUTTON_EVENT_TRACKING + ENABLE_SGR_MOUSE
2091
+ );
2092
+ process.once("exit", restore);
2093
+ return restore;
2094
+ }
2095
+ async function runMainAction(options) {
2096
+ try {
2097
+ const requestedWorkspace = options.workspace;
2098
+ const effortOverride = options.effort ? parseEffortLevel(String(options.effort), "--effort") : void 0;
2099
+ const config = loadConfig(requestedWorkspace, {
2100
+ settingsOverridePath: options.settings,
2101
+ noSettings: options.settings === false,
2102
+ runMigrations: options.settings !== false,
2103
+ modelOverride: options.model,
2104
+ effortOverride,
2105
+ allowMissingApiKey: options.print === void 0 && !options.scrollback
2106
+ });
2107
+ if (config.modelProviderWarning) console.warn("\u26A0 " + config.modelProviderWarning);
2108
+ const interactiveMaxTurns = parseNumericFlag(options.maxTurns, "--max-turns", {
2109
+ integer: true
2110
+ });
2111
+ if (interactiveMaxTurns !== void 0) config.maxTurns = interactiveMaxTurns;
2112
+ if (options.agents) {
2113
+ const agentsMode = String(options.agents);
2114
+ if (!["adaptive", "manual", "off"].includes(agentsMode)) {
2115
+ throw new Error("--agents must be adaptive, manual, or off");
2116
+ }
2117
+ config.settings.agents.mode = agentsMode;
2118
+ }
2119
+ if (options.provider) {
2120
+ const VALID_PROVIDERS = /* @__PURE__ */ new Set(["anthropic", "openai", "auto"]);
2121
+ const raw = String(options.provider).trim().toLowerCase();
2122
+ if (VALID_PROVIDERS.has(raw)) {
2123
+ config.provider = raw;
2124
+ }
2125
+ }
2126
+ freezeAgentConfig(config);
2127
+ cleanupDebugLogs(DEFAULT_LOCAL_DATA_RETENTION_DAYS, getDebugLogPath());
2128
+ if (options.print !== void 0) {
2129
+ for (const notice of collectWithheldProjectNotices({
2130
+ workspace: config.workspace,
2131
+ settings: config.settings,
2132
+ settingsEnabled: options.settings !== false
2133
+ })) {
2134
+ console.warn(notice);
2135
+ }
2136
+ const declaredMcpServers = resolveMcpServerList(config.workspace);
2137
+ const mcpPartition = partitionMcpServersByApproval(declaredMcpServers, config.settings);
2138
+ for (const server of mcpPartition.pending) {
2139
+ console.warn(
2140
+ `\u26A0 Skipping MCP server "${server.name}" (${server.path}): project-declared servers require one-time approval. Approve it in an interactive session first.`
2141
+ );
2142
+ }
2143
+ const mcp = await connectMcpServers(config.workspace, {
2144
+ servers: mcpServersToRecord(mcpPartition.allowed)
2145
+ });
2146
+ let result;
2147
+ try {
2148
+ const registry = createDefaultRegistry({ agents: config.settings.agents.mode !== "off" });
2149
+ if (mcp.tools.length > 0) {
2150
+ registry.registerAll(mcp.tools);
2151
+ }
2152
+ const mode2 = resolvePermissionMode(config.settings, options.permissionMode);
2153
+ const sessionStore2 = options.sessionPersistence ? new SessionStore(SESSION_ROOT) : void 0;
2154
+ const bootstrap2 = resolveSessionBootstrap(sessionStore2, {
2155
+ cwd: config.workspace,
2156
+ resume: options.resume,
2157
+ continue: options.continue,
2158
+ sessionId: options.sessionId,
2159
+ sessionName: options.name,
2160
+ forkSession: options.forkSession
2161
+ });
2162
+ sessionStore2?.cleanup(DEFAULT_LOCAL_DATA_RETENTION_DAYS, /* @__PURE__ */ new Set([bootstrap2.sessionId]));
2163
+ result = await runHeadless(config, registry, {
2164
+ prompt: typeof options.print === "string" ? options.print : void 0,
2165
+ inputFormat: options.inputFormat,
2166
+ outputFormat: options.outputFormat,
2167
+ history: bootstrap2.history,
2168
+ transcript: bootstrap2.transcript,
2169
+ compactBoundaries: bootstrap2.compactBoundaries,
2170
+ plan: bootstrap2.plan,
2171
+ carriedUsage: bootstrap2.carriedUsage,
2172
+ carriedModels: bootstrap2.carriedModels,
2173
+ mode: mode2,
2174
+ signal: options.signal,
2175
+ maxTurns: parseNumericFlag(options.maxTurns, "--max-turns", { integer: true }),
2176
+ maxBudgetUsd: parseNumericFlag(options.maxBudgetUsd, "--max-budget-usd"),
2177
+ verbose: options.verbose,
2178
+ jsonSchema: options.jsonSchema ? JSON.parse(options.jsonSchema) : void 0,
2179
+ sessionStore: sessionStore2,
2180
+ sessionId: bootstrap2.sessionId,
2181
+ sessionName: bootstrap2.sessionName,
2182
+ forkSession: false,
2183
+ sessionCreated: bootstrap2.created,
2184
+ persistSession: options.sessionPersistence,
2185
+ includeHookEvents: options.includeHookEvents,
2186
+ includePartialMessages: options.includePartialMessages,
2187
+ promptSuggestions: options.promptSuggestions
2188
+ });
2189
+ } finally {
2190
+ await disconnectMcpServers(mcp.connections);
2191
+ }
2192
+ if (result?.outcome.status === "failed") {
2193
+ exit(1);
2194
+ }
2195
+ return;
2196
+ }
2197
+ const mode = resolvePermissionMode(config.settings, options.permissionMode);
2198
+ if (options.scrollback) {
2199
+ const { runScrollbackSession } = await import("./scrollback-LUKLIBVI.js");
2200
+ await runScrollbackSession(config, { mode });
2201
+ return;
2202
+ }
2203
+ const sessionStore = options.sessionPersistence ? new SessionStore(SESSION_ROOT) : void 0;
2204
+ const ephemeralRewind = sessionStore ? void 0 : createEphemeralRewindEnvironment(config.workspace);
2205
+ const timelineStore = sessionStore ?? ephemeralRewind.timelineStore;
2206
+ let snapshotStore = sessionStore ? createRewindSnapshotStore(config.workspace) : ephemeralRewind.snapshotStore;
2207
+ if (sessionStore) {
2208
+ try {
2209
+ snapshotStore.cleanup(collectSnapshotReferences(sessionStore, config.workspace), 30);
2210
+ } catch (error) {
2211
+ snapshotStore = createUnavailableRewindSnapshotStore(
2212
+ `Code rewind unavailable: snapshot cleanup failed (${error instanceof Error ? error.message : String(error)}).`
2213
+ );
2214
+ }
2215
+ }
2216
+ const bootstrap = resolveSessionBootstrap(sessionStore, {
2217
+ cwd: config.workspace,
2218
+ resume: options.resume,
2219
+ continue: options.continue,
2220
+ sessionId: options.sessionId,
2221
+ sessionName: options.name,
2222
+ forkSession: options.forkSession
2223
+ });
2224
+ sessionStore?.cleanup(DEFAULT_LOCAL_DATA_RETENTION_DAYS, /* @__PURE__ */ new Set([bootstrap.sessionId]));
2225
+ if (!sessionStore) {
2226
+ timelineStore.create({
2227
+ id: bootstrap.sessionId,
2228
+ cwd: config.workspace,
2229
+ name: bootstrap.sessionName
2230
+ });
2231
+ }
2232
+ const [{ App }, { loadInteractiveAssets }] = await Promise.all([
2233
+ import("./app-MOXXWEOY.js"),
2234
+ import("./interactive-assets-CCAAWVKH.js")
2235
+ ]);
2236
+ const interactiveAssets = loadInteractiveAssets(config);
2237
+ const mcpHost = new McpSessionHost(config.workspace, config.settings);
2238
+ mcpHost.start();
2239
+ let app;
2240
+ const redrawViewport = () => {
2241
+ app?.clear();
2242
+ process.stdout.write("\x1B[H\x1B[2J");
2243
+ };
2244
+ const restoreScreen = config.accessibility.screenReader ? () => {
2245
+ } : enterInteractiveScreen(process.stdout);
2246
+ try {
2247
+ const rendererMode = resolveTuiRendererMode(process.env.BOOK_TUI_RENDERER, {
2248
+ isTTY: process.stdout.isTTY === true,
2249
+ screenReader: config.accessibility.screenReader,
2250
+ incrementalRendererPatched: isInkIncrementalRendererPatched(),
2251
+ platform: process.platform
2252
+ });
2253
+ await installInkScrollRenderer(rendererMode === "experimental-scroll");
2254
+ await installFrameCapture();
2255
+ app = render(
2256
+ createElement(App, {
2257
+ config,
2258
+ permissionMode: mode,
2259
+ interactiveAssets,
2260
+ redrawViewport,
2261
+ mcp: mcpHost,
2262
+ session: { ...bootstrap, store: sessionStore, timelineStore, snapshotStore }
2263
+ }),
2264
+ {
2265
+ exitOnCtrlC: false,
2266
+ isScreenReaderEnabled: config.accessibility.screenReader,
2267
+ incrementalRendering: rendererMode !== "safe",
2268
+ maxFps: 60
2269
+ }
2270
+ );
2271
+ await app.waitUntilExit();
2272
+ } finally {
2273
+ app?.cleanup();
2274
+ restoreScreen();
2275
+ await mcpHost.dispose();
2276
+ ephemeralRewind?.dispose();
2277
+ }
2278
+ } catch (e) {
2279
+ if (isExiting()) throw e;
2280
+ console.error(e instanceof Error ? e.message : String(e));
2281
+ exit(1);
2282
+ }
2283
+ }
2284
+
2285
+ // src/cli/mcp.ts
2286
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2287
+ import { join as join7 } from "path";
2288
+ function configPath(workspace, scope, home) {
2289
+ return scope === "project" ? join7(workspace, ".mcp.json") : home ? join7(home, ".book", "mcp.json") : join7(resolveBookHome(), "mcp.json");
2290
+ }
2291
+ function validScope(scope) {
2292
+ const normalized = scope ?? "user";
2293
+ if (normalized !== "user" && normalized !== "project") {
2294
+ throw new Error('MCP scope must be "user" or "project".');
2295
+ }
2296
+ return normalized;
2297
+ }
2298
+ function readConfigDocument(path) {
2299
+ if (!existsSync3(path)) return {};
2300
+ let parsed;
2301
+ try {
2302
+ parsed = JSON.parse(readFileSync3(path, "utf8"));
2303
+ } catch (error) {
2304
+ throw new Error(
2305
+ `Cannot update ${path}: ${error instanceof Error ? error.message : String(error)}`
2306
+ );
2307
+ }
2308
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2309
+ throw new Error(`Cannot update ${path}: expected a JSON object.`);
2310
+ }
2311
+ const document = parsed;
2312
+ if (document.mcpServers !== void 0 && (!document.mcpServers || typeof document.mcpServers !== "object" || Array.isArray(document.mcpServers))) {
2313
+ throw new Error(`Cannot update ${path}: mcpServers must be an object.`);
2314
+ }
2315
+ return document;
2316
+ }
2317
+ function parseEntries(kind, entries = []) {
2318
+ const parsed = {};
2319
+ for (const entry of entries) {
2320
+ const separator = entry.indexOf("=");
2321
+ if (separator <= 0) throw new Error(`Invalid --${kind} value "${entry}"; expected KEY=VALUE.`);
2322
+ const key = entry.slice(0, separator).trim();
2323
+ if (!key) throw new Error(`Invalid --${kind} value "${entry}"; the key is empty.`);
2324
+ parsed[key] = entry.slice(separator + 1);
2325
+ }
2326
+ return parsed;
2327
+ }
2328
+ function validateRemoteUrl(raw) {
2329
+ let url;
2330
+ try {
2331
+ url = new URL(raw);
2332
+ } catch {
2333
+ throw new Error("Remote MCP URL must be an absolute http:// or https:// URL.");
2334
+ }
2335
+ if (!["http:", "https:"].includes(url.protocol)) {
2336
+ throw new Error("Remote MCP URL must use http:// or https://.");
2337
+ }
2338
+ if (url.username || url.password) throw new Error("Remote MCP URL must not contain credentials.");
2339
+ return raw;
2340
+ }
2341
+ function inferredTransport(target, requested) {
2342
+ if (requested) {
2343
+ if (!["stdio", "http", "sse"].includes(requested)) {
2344
+ throw new Error('MCP transport must be "stdio", "http", or "sse".');
2345
+ }
2346
+ return requested;
2347
+ }
2348
+ return /^https?:\/\//i.test(target) ? "http" : "stdio";
2349
+ }
2350
+ function createServerConfig(target, serverArgs, options) {
2351
+ const transport = inferredTransport(target, options.transport);
2352
+ if (transport === "stdio") {
2353
+ if (!target.trim()) throw new Error("Stdio MCP command must not be empty.");
2354
+ if ((options.header?.length ?? 0) > 0) {
2355
+ throw new Error("--header is available only for HTTP/SSE servers.");
2356
+ }
2357
+ return {
2358
+ type: "stdio",
2359
+ command: target,
2360
+ args: serverArgs,
2361
+ env: parseEntries("env", options.env),
2362
+ ...options.cwd ? { cwd: options.cwd } : {}
2363
+ };
2364
+ }
2365
+ if (serverArgs.length > 0)
2366
+ throw new Error("HTTP/SSE MCP servers do not accept command arguments.");
2367
+ if ((options.env?.length ?? 0) > 0 || options.cwd) {
2368
+ throw new Error("--env and --cwd are available only for stdio servers.");
2369
+ }
2370
+ return {
2371
+ type: transport,
2372
+ url: validateRemoteUrl(target),
2373
+ headers: parseEntries("header", options.header)
2374
+ };
2375
+ }
2376
+ function writeServer(workspace, scope, name, config, force, home) {
2377
+ const path = configPath(workspace, scope, home);
2378
+ const document = readConfigDocument(path);
2379
+ const servers = { ...document.mcpServers ?? {} };
2380
+ if (Object.prototype.hasOwnProperty.call(servers, name) && !force) {
2381
+ throw new Error(`MCP server "${name}" already exists in ${path}; pass --force to replace it.`);
2382
+ }
2383
+ servers[name] = config;
2384
+ writeFileAtomic(path, JSON.stringify({ ...document, mcpServers: servers }, null, 2) + "\n");
2385
+ return path;
2386
+ }
2387
+ function removeServer(workspace, scope, name, home) {
2388
+ const path = configPath(workspace, scope, home);
2389
+ const document = readConfigDocument(path);
2390
+ const servers = { ...document.mcpServers ?? {} };
2391
+ if (!Object.prototype.hasOwnProperty.call(servers, name)) {
2392
+ throw new Error(`MCP server "${name}" is not declared in ${path}.`);
2393
+ }
2394
+ delete servers[name];
2395
+ writeFileAtomic(path, JSON.stringify({ ...document, mcpServers: servers }, null, 2) + "\n");
2396
+ return path;
2397
+ }
2398
+ function serverRecords(workspace, home) {
2399
+ const settings = resolveSettings(workspace);
2400
+ return resolveMcpServerList(workspace, { home }).map((server) => ({
2401
+ name: server.name,
2402
+ source: server.source,
2403
+ path: server.path,
2404
+ transport: server.config.type ?? (server.config.url ? "http" : "stdio"),
2405
+ target: formatMcpServerCommand(server.config),
2406
+ approval: server.source === "user" ? "user" : evaluateMcpServerApproval(settings, server),
2407
+ envKeys: Object.keys(server.config.env ?? {}).sort(),
2408
+ headerKeys: Object.keys(server.config.headers ?? {}).sort()
2409
+ }));
2410
+ }
2411
+ function printRecords(records, json = false) {
2412
+ if (json) {
2413
+ console.log(JSON.stringify(records, null, 2));
2414
+ return;
2415
+ }
2416
+ if (records.length === 0) {
2417
+ console.log("No MCP servers configured.");
2418
+ return;
2419
+ }
2420
+ for (const server of records) {
2421
+ console.log(
2422
+ `${server.name}: ${server.target} [${server.source}${server.source === "project" ? `: ${server.approval}` : ""}]`
2423
+ );
2424
+ if (server.envKeys.length > 0) console.log(` env: ${server.envKeys.join(", ")}`);
2425
+ if (server.headerKeys.length > 0) {
2426
+ console.log(` headers: ${server.headerKeys.join(", ")} (values hidden)`);
2427
+ }
2428
+ console.log(` config: ${server.path}`);
2429
+ }
2430
+ }
2431
+ function runMcpListCommand(options) {
2432
+ printRecords(serverRecords(options.workspace, options.home), options.json);
2433
+ }
2434
+ function runMcpGetCommand(name, options) {
2435
+ const server = serverRecords(options.workspace, options.home).find(
2436
+ (candidate) => candidate.name === name
2437
+ );
2438
+ if (!server) throw new Error(`MCP server "${name}" is not configured.`);
2439
+ printRecords([server], options.json);
2440
+ }
2441
+ function runMcpAddCommand(name, target, serverArgs, options) {
2442
+ if (!isValidMcpServerName(name)) {
2443
+ throw new Error(
2444
+ "MCP server names may contain letters, numbers, hyphens, or underscores, but not a double underscore."
2445
+ );
2446
+ }
2447
+ const scope = validScope(options.scope);
2448
+ const config = createServerConfig(target, serverArgs, options);
2449
+ const path = writeServer(
2450
+ options.workspace,
2451
+ scope,
2452
+ name,
2453
+ config,
2454
+ options.force ?? false,
2455
+ options.home
2456
+ );
2457
+ console.log(`Added MCP server "${name}" to ${path}.`);
2458
+ if (scope === "project") {
2459
+ console.log("Project servers require one-time approval in an interactive Book session.");
2460
+ }
2461
+ }
2462
+ function runMcpRemoveCommand(name, options) {
2463
+ const scope = options.scope ? validScope(options.scope) : serverRecords(options.workspace, options.home).find((candidate) => candidate.name === name)?.source;
2464
+ if (!scope) throw new Error(`MCP server "${name}" is not configured.`);
2465
+ const path = removeServer(options.workspace, scope, name, options.home);
2466
+ console.log(`Removed MCP server "${name}" from ${path}.`);
2467
+ }
2468
+
2469
+ // src/index.ts
2470
+ function collectOption(value, previous) {
2471
+ return [...previous, value];
2472
+ }
2473
+ function parseEffortOption(value) {
2474
+ const normalized = value.trim().toLowerCase();
2475
+ if (!isEffortLevel(normalized)) {
2476
+ throw new InvalidArgumentError(`Valid levels: ${EFFORT_LEVELS.join(", ")}.`);
2477
+ }
2478
+ return normalized;
2479
+ }
2480
+ function resolveWorkspace(workspace) {
2481
+ return workspace ?? program.opts().workspace;
2482
+ }
2483
+ program.name("book").description("AI coding agent with rich TUI").enablePositionalOptions().version(getPackageVersion()).option("-w, --workspace <path>", "Workspace root directory", process.cwd()).option("-m, --model <model>", "Model to use").option(
2484
+ "-p, --print [prompt]",
2485
+ "Print mode (non-interactive). Reads prompt from the flag or stdin."
2486
+ ).option("--output-format <format>", "text | json | stream-json (print mode)", "text").option("--input-format <format>", "text | stream-json (print mode)", "text").option("--max-turns <n>", "Max agent turns (print mode; omit for unlimited)").option("--max-budget-usd <amount>", "Max USD spend (print mode)").option(
2487
+ "--permission-mode <mode>",
2488
+ "default | acceptEdits | plan | auto | dontAsk | bypassPermissions (overrides settings.defaultMode)"
2489
+ ).option("--verbose", "Full turn-by-turn output").option("--json-schema <schema>", "Return validated JSON matching a JSON Schema (print mode)").option("-r, --resume <id|name>", "Resume a session by id or name").option("-c, --continue", "Resume the most recent session in this directory").option("--session-id <uuid>", "Use a specific session id").option("-n, --name <name>", "Set a display name for the session").option("--no-session-persistence", "Do not save the session to disk").option("--fork-session", "On resume, create a new session id instead of reusing").option("--include-hook-events", "Emit hook lifecycle events in stream-json output").option("--include-partial-messages", "Emit partial assistant text deltas in stream-json output").option("--prompt-suggestions", "Ask model for follow-up prompt suggestions after completion").option("--agents <mode>", "Managed agents: adaptive, manual, off").option("--scrollback", "Use terminal-native scrollback instead of the full-screen TUI").option("--settings <path>", "Path to an ad-hoc settings file (overrides all scopes)").option("--no-settings", "Skip all settings.json layers (use defaults + legacy .bookrc.json)").option(
2490
+ "--effort <level>",
2491
+ "Thinking effort: low, medium, high, xhigh, max (default: high)",
2492
+ parseEffortOption
2493
+ ).option("--provider <type>", "Provider: anthropic, openai, auto (default: auto-detect)");
2494
+ program.command("doctor").description("Diagnose configuration and environment").option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--no-settings", "Skip all settings.json layers (use defaults + legacy .bookrc.json)").action(async (options) => {
2495
+ await runDoctorCommand(resolveWorkspace(options.workspace), {
2496
+ noSettings: options.settings === false
2497
+ });
2498
+ });
2499
+ program.command("status").description("Report what a session is working on and what it has spent (no credentials needed)").argument("[session]", "Session id or name (defaults to the most recent in this workspace)").option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--json", "Emit the status as JSON").action((session, options) => {
2500
+ const code = runStatusCommand({
2501
+ session,
2502
+ workspace: resolveWorkspace(options.workspace),
2503
+ json: options.json
2504
+ });
2505
+ if (code !== 0) exit(code);
2506
+ });
2507
+ program.command("tool-stats").description(
2508
+ "Inspect and measure tool use recorded across sessions (fail counts, rates, durations)"
2509
+ ).option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--json", "Emit the aggregate as JSON").option("--since <days>", "Only include records from the last N days").option("--all", "Include the full history, ignoring the retention window").option("--prune", "Drop records older than the window from disk before reporting").action(
2510
+ async (options) => {
2511
+ await runToolStatsCommand({ ...options, workspace: resolveWorkspace(options.workspace) });
2512
+ }
2513
+ );
2514
+ var mcpCommand = program.command("mcp").description("Manage MCP server configurations");
2515
+ mcpCommand.command("list").description("List resolved MCP servers without revealing secret values").option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--json", "Emit JSON").action(
2516
+ (options) => runMcpListCommand({ ...options, workspace: resolveWorkspace(options.workspace) })
2517
+ );
2518
+ mcpCommand.command("get").description("Show one resolved MCP server without revealing secret values").argument("<name>", "Server name").option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--json", "Emit JSON").action(
2519
+ (name, options) => runMcpGetCommand(name, { ...options, workspace: resolveWorkspace(options.workspace) })
2520
+ );
2521
+ mcpCommand.command("add").description("Add an MCP server (use -- before stdio arguments that start with a dash)").argument("<name>", "Server name").argument("<command-or-url>", "Stdio executable or HTTP/SSE URL").argument("[server-args...]", "Arguments for a stdio server").option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--scope <scope>", "user | project", "user").option("--transport <type>", "stdio | http | sse").option("-e, --env <KEY=VALUE>", "Stdio environment entry (repeatable)", collectOption, []).option("-H, --header <KEY=VALUE>", "HTTP header (repeatable)", collectOption, []).option("--cwd <path>", "Working directory for a stdio server").option("--force", "Replace an existing same-scope server").action(
2522
+ (name, target, serverArgs, options) => runMcpAddCommand(name, target, serverArgs, {
2523
+ ...options,
2524
+ workspace: resolveWorkspace(options.workspace)
2525
+ })
2526
+ );
2527
+ mcpCommand.command("remove").alias("rm").description("Remove an MCP server from its effective scope").argument("<name>", "Server name").option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--scope <scope>", "user | project").action(
2528
+ (name, options) => runMcpRemoveCommand(name, { ...options, workspace: resolveWorkspace(options.workspace) })
2529
+ );
2530
+ var trustCommand = program.command("trust").description("Record decisions about configuration a repository declared");
2531
+ trustCommand.command("hook").description("Approve or reject a project-declared hook (see `book doctor`)").argument("[fingerprint]", "Hook fingerprint reported by `book doctor`").option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--all-pending", "Apply to every hook currently awaiting a decision").option("--reject", "Record a refusal instead of an approval").action(
2532
+ async (fingerprint, options) => {
2533
+ await runTrustCommand("hook", fingerprint, {
2534
+ ...options,
2535
+ workspace: resolveWorkspace(options.workspace)
2536
+ });
2537
+ }
2538
+ );
2539
+ trustCommand.command("command").description("Approve or reject a project command that substitutes shell into its prompt").argument("[name]", "Command name, with or without the leading slash").option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--all-pending", "Apply to every command currently awaiting a decision").option("--reject", "Record a refusal instead of an approval").action(
2540
+ async (name, options) => {
2541
+ await runTrustCommand("command", name, {
2542
+ ...options,
2543
+ workspace: resolveWorkspace(options.workspace)
2544
+ });
2545
+ }
2546
+ );
2547
+ trustCommand.command("rule").description("Approve or reject a project-declared permissions.allow rule").argument("[rule]", "Rule text exactly as the project declared it").option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--all-pending", "Apply to every rule currently awaiting a decision").option("--reject", "Record a refusal instead of an approval").action(
2548
+ async (rule, options) => {
2549
+ await runTrustCommand("rule", rule, {
2550
+ ...options,
2551
+ workspace: resolveWorkspace(options.workspace)
2552
+ });
2553
+ }
2554
+ );
2555
+ program.command("config").description("Read and write settings").option("-w, --workspace <path>", "Workspace root directory (defaults to the root -w, then cwd)").option("--settings <path>", "Path to an ad-hoc settings file (overrides all scopes)").option("--no-settings", "Skip all settings.json layers (use defaults + legacy .bookrc.json)").option("-g, --global", "Act on user-global settings (<BOOK_HOME>/settings.json) [default]").option("--project", "Act on checked-in project settings (.book/settings.json)").option("--local", "Act on gitignored project-local settings (.book/settings.local.json)").argument("[action]", "get <key>, set <key> <value>, unset <key>, or list").argument("[key]", "Dot-separated key path (e.g. permissions.deny)").argument("[value]", "Value to set (JSON-parsed)").addHelpText("after", `
2556
+ ${formatSettingsKeyHelp()}`).action(
2557
+ async (action, key, value, options) => {
2558
+ const rootSettings = program.opts().settings;
2559
+ const settings = options.settings ?? rootSettings;
2560
+ const selected = [
2561
+ ["user", options.global],
2562
+ ["project", options.project],
2563
+ ["local", options.local]
2564
+ ].filter(([, on]) => on === true);
2565
+ if (selected.length > 1) {
2566
+ console.error(
2567
+ "Pass at most one of --global, --project, --local. Writes default to --global; reads without one report the resolved merge."
2568
+ );
2569
+ exit(1);
2570
+ }
2571
+ await runConfigCommand(resolveWorkspace(options.workspace), action, key, value, {
2572
+ settingsOverridePath: typeof settings === "string" ? settings : void 0,
2573
+ noSettings: settings === false,
2574
+ scope: selected[0]?.[0]
2575
+ });
2576
+ }
2577
+ );
2578
+ program.action(async (options) => {
2579
+ await runMainAction(options);
2580
+ });
2581
+ program.parse();
2582
+ //# sourceMappingURL=index.js.map