@letrquan/book 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2747 -40
- package/README.md +132 -1311
- package/dist/additional-roots-H7UNAEHS.js +31 -0
- package/dist/{app-MOXXWEOY.js → app-GE4OHJOC.js} +5740 -4045
- package/dist/app-GE4OHJOC.js.map +1 -0
- package/dist/{chunk-5GDZ22YP.js → chunk-224FJZLH.js} +99 -108
- package/dist/chunk-224FJZLH.js.map +1 -0
- package/dist/chunk-2AAOF5B5.js +28 -0
- package/dist/chunk-2AAOF5B5.js.map +1 -0
- package/dist/{chunk-CAGOB2N4.js → chunk-4AS7LKKQ.js} +6 -4
- package/dist/chunk-4AS7LKKQ.js.map +1 -0
- package/dist/chunk-525CAZPY.js +120 -0
- package/dist/chunk-525CAZPY.js.map +1 -0
- package/dist/{chunk-URLQAXTR.js → chunk-56SIEMDX.js} +2 -2
- package/dist/chunk-56SIEMDX.js.map +1 -0
- package/dist/{chunk-XIFFTATU.js → chunk-5YHTKWJN.js} +5420 -4031
- package/dist/chunk-5YHTKWJN.js.map +1 -0
- package/dist/chunk-62QL5QUP.js +283 -0
- package/dist/chunk-62QL5QUP.js.map +1 -0
- package/dist/{chunk-KWHQ2XXL.js → chunk-67UZEWWF.js} +3 -3
- package/dist/chunk-67UZEWWF.js.map +1 -0
- package/dist/chunk-6OQQOMZR.js +785 -0
- package/dist/chunk-6OQQOMZR.js.map +1 -0
- package/dist/{chunk-GTVB4HEM.js → chunk-AQEQX4FF.js} +344 -315
- package/dist/chunk-AQEQX4FF.js.map +1 -0
- package/dist/chunk-C7ADUNPR.js +273 -0
- package/dist/chunk-C7ADUNPR.js.map +1 -0
- package/dist/{chunk-E75IPST5.js → chunk-EFJ2NH6E.js} +12 -58
- package/dist/chunk-EFJ2NH6E.js.map +1 -0
- package/dist/{chunk-5KLVH3PY.js → chunk-HQI7NYGX.js} +5 -3
- package/dist/chunk-HQI7NYGX.js.map +1 -0
- package/dist/chunk-IME7R2UI.js +6541 -0
- package/dist/chunk-IME7R2UI.js.map +1 -0
- package/dist/chunk-J4F2QLM2.js +273 -0
- package/dist/chunk-J4F2QLM2.js.map +1 -0
- package/dist/{chunk-MAQXCUR4.js → chunk-J62VZYOB.js} +444 -112
- package/dist/chunk-J62VZYOB.js.map +1 -0
- package/dist/chunk-JTPOI3KU.js +826 -0
- package/dist/chunk-JTPOI3KU.js.map +1 -0
- package/dist/{chunk-WKBO5R4O.js → chunk-KBQS35OV.js} +2 -2
- package/dist/chunk-KBQS35OV.js.map +1 -0
- package/dist/chunk-KXAYUIKP.js +90 -0
- package/dist/chunk-KXAYUIKP.js.map +1 -0
- package/dist/{chunk-VZQXX3WW.js → chunk-QYBD22H5.js} +1 -1
- package/dist/chunk-QYBD22H5.js.map +1 -0
- package/dist/{chunk-V4BOYR52.js → chunk-RFJ6Q6LE.js} +763 -198
- package/dist/chunk-RFJ6Q6LE.js.map +1 -0
- package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
- package/dist/chunk-RT5SATF7.js.map +1 -0
- package/dist/{chunk-C7D4NJ2D.js → chunk-S4JMID4G.js} +68 -26
- package/dist/chunk-S4JMID4G.js.map +1 -0
- package/dist/{chunk-ANSJPYGI.js → chunk-SOW4WRQP.js} +3 -3
- package/dist/chunk-SOW4WRQP.js.map +1 -0
- package/dist/{chunk-J7MIZ5CA.js → chunk-TLX3H2CL.js} +518 -79
- package/dist/chunk-TLX3H2CL.js.map +1 -0
- package/dist/{chunk-DGMQEI6Y.js → chunk-VPNLCMVR.js} +22 -9
- package/dist/chunk-VPNLCMVR.js.map +1 -0
- package/dist/{chunk-BTZZUCLI.js → chunk-W3XFHEF4.js} +173 -29
- package/dist/chunk-W3XFHEF4.js.map +1 -0
- package/dist/{chunk-574C6MQR.js → chunk-X4NJT436.js} +130 -60
- package/dist/chunk-X4NJT436.js.map +1 -0
- package/dist/chunk-X66I2NQO.js +137 -0
- package/dist/chunk-X66I2NQO.js.map +1 -0
- package/dist/chunk-XHAO3K7I.js +18 -0
- package/dist/chunk-XHAO3K7I.js.map +1 -0
- package/dist/{chunk-3U5IBM24.js → chunk-Y2VHN7TC.js} +2 -2
- package/dist/chunk-Y2VHN7TC.js.map +1 -0
- package/dist/chunk-YC6W4EJH.js +947 -0
- package/dist/chunk-YC6W4EJH.js.map +1 -0
- package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
- package/dist/chunk-ZPSZZUWF.js.map +1 -0
- package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-VBIU6BLM.js} +6 -5
- package/dist/context-CBOBDY5Q.js +38 -0
- package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-ZVT4V7IM.js} +5 -5
- package/dist/index.js +996 -155
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-T4I4SEHU.js} +13 -9
- package/dist/interactive-assets-T4I4SEHU.js.map +1 -0
- package/dist/job-runner.js +159 -111
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.d.ts +2 -0
- package/dist/job-supervisor.js +112 -0
- package/dist/job-supervisor.js.map +1 -0
- package/dist/loader-S77OVTHH.js +18 -0
- package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-HJ2VDYMC.js} +5 -5
- package/dist/{mcp-config-YIJIES2C.js → mcp-config-RNB5LEVE.js} +3 -3
- package/dist/memory-store-DZUT5HUH.js +75 -0
- package/dist/{model-window-store-YFVNXCIP.js → model-window-store-MTSQ5JDJ.js} +8 -4
- package/dist/openai-compatible-2OB54YST.js +14 -0
- package/dist/{permission-approvals-76PVREFD.js → permission-approvals-YLV6UC5W.js} +5 -5
- package/dist/permissions-DHOKF3W2.js +45 -0
- package/dist/sandbox-WRYTZH3D.js +41 -0
- package/dist/{scrollback-LUKLIBVI.js → scrollback-35ME2GFX.js} +32 -22
- package/dist/scrollback-35ME2GFX.js.map +1 -0
- package/dist/sdk.d.ts +2219 -2361
- package/dist/sdk.js +53 -38
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-JUNPGAAM.js +35 -0
- package/dist/settings-loader-JUNPGAAM.js.map +1 -0
- package/dist/{shell-selection-V6QAYXPX.js → shell-selection-CDHGJ4GC.js} +2 -2
- package/dist/shell-selection-CDHGJ4GC.js.map +1 -0
- package/package.json +23 -22
- package/dist/app-MOXXWEOY.js.map +0 -1
- package/dist/chunk-24AXE6SP.js.map +0 -1
- package/dist/chunk-3U5IBM24.js.map +0 -1
- package/dist/chunk-574C6MQR.js.map +0 -1
- package/dist/chunk-5GDZ22YP.js.map +0 -1
- package/dist/chunk-5KLVH3PY.js.map +0 -1
- package/dist/chunk-7N4J557C.js.map +0 -1
- package/dist/chunk-A7JLBK2Y.js +0 -3506
- package/dist/chunk-A7JLBK2Y.js.map +0 -1
- package/dist/chunk-ANSJPYGI.js.map +0 -1
- package/dist/chunk-BTZZUCLI.js.map +0 -1
- package/dist/chunk-C7D4NJ2D.js.map +0 -1
- package/dist/chunk-CAGOB2N4.js.map +0 -1
- package/dist/chunk-DGMQEI6Y.js.map +0 -1
- package/dist/chunk-E75IPST5.js.map +0 -1
- package/dist/chunk-EYGD6X2T.js +0 -180
- package/dist/chunk-EYGD6X2T.js.map +0 -1
- package/dist/chunk-GTVB4HEM.js.map +0 -1
- package/dist/chunk-J7MIZ5CA.js.map +0 -1
- package/dist/chunk-KWHQ2XXL.js.map +0 -1
- package/dist/chunk-L6LK2QIQ.js +0 -657
- package/dist/chunk-L6LK2QIQ.js.map +0 -1
- package/dist/chunk-MAQXCUR4.js.map +0 -1
- package/dist/chunk-S4XL7HOM.js +0 -402
- package/dist/chunk-S4XL7HOM.js.map +0 -1
- package/dist/chunk-URLQAXTR.js.map +0 -1
- package/dist/chunk-V4BOYR52.js.map +0 -1
- package/dist/chunk-VZQXX3WW.js.map +0 -1
- package/dist/chunk-WKBO5R4O.js.map +0 -1
- package/dist/chunk-XIFFTATU.js.map +0 -1
- package/dist/context-ZC5IFEAX.js +0 -25
- package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
- package/dist/loader-4IDS2RIE.js +0 -15
- package/dist/openai-compatible-RHZUBHV6.js +0 -10
- package/dist/permissions-CHCKTFQQ.js +0 -28
- package/dist/sandbox-CNVAQDA7.js +0 -23
- package/dist/scrollback-LUKLIBVI.js.map +0 -1
- package/dist/settings-loader-M2QP5TX2.js +0 -21
- package/patches/ink+6.8.0.patch +0 -13
- package/scripts/apply-ink-patch.mjs +0 -29
- package/scripts/ink-patch.mjs +0 -59
- package/scripts/verify-ink-patch.mjs +0 -11
- /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-H7UNAEHS.js.map} +0 -0
- /package/dist/{context-ZC5IFEAX.js.map → command-approvals-VBIU6BLM.js.map} +0 -0
- /package/dist/{hook-approvals-CTUFKEJT.js.map → context-CBOBDY5Q.js.map} +0 -0
- /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-ZVT4V7IM.js.map} +0 -0
- /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-S77OVTHH.js.map} +0 -0
- /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-HJ2VDYMC.js.map} +0 -0
- /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-RNB5LEVE.js.map} +0 -0
- /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-DZUT5HUH.js.map} +0 -0
- /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-MTSQ5JDJ.js.map} +0 -0
- /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-2OB54YST.js.map} +0 -0
- /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-YLV6UC5W.js.map} +0 -0
- /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-DHOKF3W2.js.map} +0 -0
- /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-WRYTZH3D.js.map} +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
toolFailure
|
|
3
|
+
} from "./chunk-224FJZLH.js";
|
|
4
|
+
|
|
5
|
+
// src/tools/path-utils.ts
|
|
6
|
+
import { existsSync, realpathSync } from "fs";
|
|
7
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
8
|
+
var REAL_PATH_COMPARISON = { relative, isAbsolute, sep };
|
|
9
|
+
function isOutside(root, candidate, path = REAL_PATH_COMPARISON) {
|
|
10
|
+
const rel = path.relative(root, candidate);
|
|
11
|
+
return rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel);
|
|
12
|
+
}
|
|
13
|
+
function nearestExistingPath(inputPath) {
|
|
14
|
+
let candidate = inputPath;
|
|
15
|
+
while (!existsSync(candidate)) {
|
|
16
|
+
const parent = dirname(candidate);
|
|
17
|
+
if (parent === candidate) return null;
|
|
18
|
+
candidate = parent;
|
|
19
|
+
}
|
|
20
|
+
return candidate;
|
|
21
|
+
}
|
|
22
|
+
function resolveWorkspacePath(workspaceRoot, inputPath) {
|
|
23
|
+
const lexicalRoot = resolve(workspaceRoot);
|
|
24
|
+
const filePath = resolve(isAbsolute(inputPath) ? inputPath : resolve(lexicalRoot, inputPath));
|
|
25
|
+
const canonicalRoot = canonicalizePath(lexicalRoot);
|
|
26
|
+
const canonicalPath = canonicalizePath(filePath);
|
|
27
|
+
if (isOutside(canonicalRoot, canonicalPath)) return null;
|
|
28
|
+
const asWrittenInside = !isOutside(lexicalRoot, filePath);
|
|
29
|
+
const rel = asWrittenInside ? relative(lexicalRoot, filePath) : relative(canonicalRoot, canonicalPath);
|
|
30
|
+
return { filePath, canonicalPath, relativePath: rel.replace(/\\/g, "/") };
|
|
31
|
+
}
|
|
32
|
+
function readOnlyEntries(roots) {
|
|
33
|
+
return [...roots.readOnlyRoots ?? [], ...roots.additionalRoots ?? []];
|
|
34
|
+
}
|
|
35
|
+
function resolveReadablePathDetail(context, inputPath) {
|
|
36
|
+
const entries = readOnlyEntries(context);
|
|
37
|
+
const rejected = (match) => isExcludedPath(match.canonicalPath, entries) ? { reason: "excluded" } : { path: match };
|
|
38
|
+
const wsMatch = resolveWorkspacePath(context.workspaceRoot, inputPath);
|
|
39
|
+
if (wsMatch) {
|
|
40
|
+
return rejected({ ...wsMatch, root: context.workspaceRoot, inWorkspace: true });
|
|
41
|
+
}
|
|
42
|
+
if (!isAbsolute(inputPath)) return { reason: "unreachable" };
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const root = typeof entry === "string" ? entry : entry.root;
|
|
45
|
+
const match = resolveWorkspacePath(root, inputPath);
|
|
46
|
+
if (match) return rejected({ ...match, root, inWorkspace: false });
|
|
47
|
+
}
|
|
48
|
+
return { reason: "unreachable" };
|
|
49
|
+
}
|
|
50
|
+
function resolveReadablePath(context, inputPath) {
|
|
51
|
+
const detail = resolveReadablePathDetail(context, inputPath);
|
|
52
|
+
return "path" in detail ? detail.path : null;
|
|
53
|
+
}
|
|
54
|
+
function resolveMutationPath(context, inputPath) {
|
|
55
|
+
const inWorkspace = resolveWorkspacePath(context.workspaceRoot, inputPath);
|
|
56
|
+
if (inWorkspace) return { ...inWorkspace, root: context.workspaceRoot, inWorkspace: true };
|
|
57
|
+
if (!isAbsolute(inputPath)) return null;
|
|
58
|
+
for (const root of context.additionalRoots ?? []) {
|
|
59
|
+
const match = resolveWorkspacePath(root, inputPath);
|
|
60
|
+
if (!match) continue;
|
|
61
|
+
const rel = relative(resolve(context.workspaceRoot), match.filePath);
|
|
62
|
+
return { ...match, relativePath: rel.replace(/\\/g, "/"), root, inWorkspace: false };
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function isExcludedPath(canonicalPath, roots) {
|
|
67
|
+
const fold = (p) => process.platform === "linux" ? p : p.toLowerCase();
|
|
68
|
+
const target = fold(canonicalPath);
|
|
69
|
+
for (const entry of roots ?? []) {
|
|
70
|
+
if (typeof entry === "string" || !entry.exclude?.length) continue;
|
|
71
|
+
const base = fold(canonicalizePath(entry.root));
|
|
72
|
+
for (const sub of entry.exclude) {
|
|
73
|
+
const dir = fold(join(base, sub));
|
|
74
|
+
if (target === dir || target.startsWith(`${dir}${sep}`)) return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
function pathOutsideWorkspaceResult(inputPath) {
|
|
80
|
+
return toolFailure(`Path outside workspace and additionalDirectories: ${inputPath}`, {
|
|
81
|
+
code: "path_outside_workspace",
|
|
82
|
+
remediation: "Add its directory to additionalDirectories, or start Book in a directory that contains it. No permission rule and no permission mode can change which roots a read may cross."
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function realWorkspaceRoot(root) {
|
|
86
|
+
try {
|
|
87
|
+
return realpathSync.native(root);
|
|
88
|
+
} catch {
|
|
89
|
+
return resolve(root);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function canonicalizePath(inputPath) {
|
|
93
|
+
const absolute = resolve(inputPath);
|
|
94
|
+
const existing = nearestExistingPath(absolute);
|
|
95
|
+
if (!existing) return absolute;
|
|
96
|
+
try {
|
|
97
|
+
return resolve(realpathSync.native(existing), relative(existing, absolute));
|
|
98
|
+
} catch {
|
|
99
|
+
return absolute;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function isUnderRoot(canonicalPath, root) {
|
|
103
|
+
return !isOutside(canonicalizePath(root), canonicalPath);
|
|
104
|
+
}
|
|
105
|
+
function hasBookLocalSettingsShape(canonicalPath, options) {
|
|
106
|
+
const parts = canonicalPath.toLowerCase().split(/[\\/]/);
|
|
107
|
+
if (parts[parts.length - 1] === ".book") return options.directory === true;
|
|
108
|
+
return parts[parts.length - 2] === ".book" && parts[parts.length - 1] === "settings.local.json";
|
|
109
|
+
}
|
|
110
|
+
function isBookLocalSettingsPath(path, root, options = {}) {
|
|
111
|
+
if (!hasBookLocalSettingsShape(path, options)) return false;
|
|
112
|
+
const target = canonicalizePath(path).toLowerCase();
|
|
113
|
+
const bookDir = join(canonicalizePath(root), ".book").toLowerCase();
|
|
114
|
+
return target === join(bookDir, "settings.local.json") || options.directory === true && target === bookDir;
|
|
115
|
+
}
|
|
116
|
+
function isBookLocalSettingsUnderRoots(path, roots, options = {}) {
|
|
117
|
+
if (!hasBookLocalSettingsShape(path, options)) return false;
|
|
118
|
+
return [roots.workspaceRoot, ...readOnlyEntries(roots)].some((root) => {
|
|
119
|
+
const entryRoot = typeof root === "string" ? root : root.root;
|
|
120
|
+
return isBookLocalSettingsPath(path, entryRoot, options);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export {
|
|
125
|
+
isOutside,
|
|
126
|
+
resolveWorkspacePath,
|
|
127
|
+
resolveReadablePathDetail,
|
|
128
|
+
resolveReadablePath,
|
|
129
|
+
resolveMutationPath,
|
|
130
|
+
pathOutsideWorkspaceResult,
|
|
131
|
+
realWorkspaceRoot,
|
|
132
|
+
canonicalizePath,
|
|
133
|
+
isUnderRoot,
|
|
134
|
+
isBookLocalSettingsPath,
|
|
135
|
+
isBookLocalSettingsUnderRoots
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=chunk-X66I2NQO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/path-utils.ts"],"sourcesContent":["import { existsSync, realpathSync } from 'fs';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'path';\nimport type { ReadOnlyRoot, ToolResult } from '../types/tools.js';\nimport { toolFailure } from './result.js';\n\n/**\n * The path semantics a containment comparison needs.\n *\n * Injectable so a caller whose paths are spelled by another `path` module — the\n * sandbox builder resolves a *test* workspace through `path.win32` or\n * `path.posix` and must judge containment in the same spelling — compares in the\n * same terms it resolved in. `sep` is part of the contract because `..` is only\n * a parent step in the separator of the module that produced the relative path.\n */\nexport interface PathComparison {\n relative(from: string, to: string): string;\n isAbsolute(path: string): boolean;\n sep: string;\n}\n\nconst REAL_PATH_COMPARISON: PathComparison = { relative, isAbsolute, sep };\n\n/**\n * Whether `candidate` is `root` or lies outside it — the one containment test\n * the rest of the codebase makes.\n *\n * Note what it does *not* accept: a relative path that merely starts with `..`,\n * such as `..meta`. That is a name in the root's own directory, and treating it\n * as a parent step reads a directory called `..meta` as being outside the tree\n * it is in. `relative` can only produce a real parent step as `..` on its own or\n * as `../…` with the separator of the module that produced it, and the third\n * clause covers a relative path answering to two different drives.\n */\nexport function isOutside(\n root: string,\n candidate: string,\n path: PathComparison = REAL_PATH_COMPARISON,\n): boolean {\n const rel = path.relative(root, candidate);\n return rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel);\n}\n\nfunction nearestExistingPath(inputPath: string): string | null {\n let candidate = inputPath;\n while (!existsSync(candidate)) {\n const parent = dirname(candidate);\n if (parent === candidate) return null;\n candidate = parent;\n }\n return candidate;\n}\n\nexport function resolveWorkspacePath(\n workspaceRoot: string,\n inputPath: string,\n): { filePath: string; canonicalPath: string; relativePath: string } | null {\n const lexicalRoot = resolve(workspaceRoot);\n const filePath = resolve(isAbsolute(inputPath) ? inputPath : resolve(lexicalRoot, inputPath));\n // Containment is decided on the canonical pair — both sides after following links, and in one\n // separator — because one directory has more than one spelling. A root the user typed in 8.3\n // short form (`C:\\Users\\RUNNER~1\\…`) is a different string from the long form `realpath`,\n // a glob walk and ripgrep report, and reading \"outside\" between the two spellings of the *same*\n // directory is what let a `.book/settings.local.json` inside the workspace look like a file\n // somewhere else (#264). The canonical path is the one a read opens and a write lands on, so it\n // is the one that decides, and a link pointing out of a root still resolves outside it.\n const canonicalRoot = canonicalizePath(lexicalRoot);\n const canonicalPath = canonicalizePath(filePath);\n if (isOutside(canonicalRoot, canonicalPath)) return null;\n\n // The spelling shown to the model is relative to the root as written whenever the path was\n // written inside it, so a symlinked spelling keeps naming the file the way the caller reached it\n // (`link/notes.txt`) rather than by its real name.\n const asWrittenInside = !isOutside(lexicalRoot, filePath);\n const rel = asWrittenInside\n ? relative(lexicalRoot, filePath)\n : relative(canonicalRoot, canonicalPath);\n return { filePath, canonicalPath, relativePath: rel.replace(/\\\\/g, '/') };\n}\n\nexport interface ResolvedReadablePath {\n filePath: string;\n canonicalPath: string;\n relativePath: string;\n /**\n * The root that served this path, as the caller spelled it — the same string the file tools\n * glob and ripgrep from, and the one a result label should keep using, since it is the spelling\n * `Read` accepts back.\n */\n root: string;\n /**\n * Whether `root` is the workspace, which is what makes `relativePath` the workspace-relative\n * spelling rather than one relative to some other root.\n */\n inWorkspace: boolean;\n}\n\n/** The roots a file tool may serve, in the order it tries them. */\nexport interface PathRoots {\n workspaceRoot: string;\n readOnlyRoots?: readonly (string | ReadOnlyRoot)[];\n /**\n * Honored `additionalDirectories` (#300), as resolved real roots. Tried after the workspace, and\n * only for an absolute path: a relative path stays workspace-anchored.\n *\n * These are *read* roots. A write tool still resolves against the workspace alone\n * ({@link resolveWorkspacePath}), so widening this list never widens what Write, Edit,\n * MultiEdit, NotebookEdit or ApplyPatch may change.\n */\n additionalRoots?: readonly string[];\n}\n\n/** The entries of a read-only root list, which carry exclusions, as a plain array. */\nfunction readOnlyEntries(roots: PathRoots): Array<string | ReadOnlyRoot> {\n return [...(roots.readOnlyRoots ?? []), ...(roots.additionalRoots ?? [])];\n}\n\n/**\n * Why a path no tool may serve it: `excluded` when it fell under a root that hides it (Book's\n * memory inbox, `.book/settings.local.json`'s directory), or `unreachable` when no root contains\n * it at all. Only the second is a refusal whose remedy is to widen the roots; the first has a\n * narrower one, and a prompt is the right answer for it.\n */\nexport type UnreachablePathReason = 'excluded' | 'unreachable';\n\nexport type ReadablePathDetail = { path: ResolvedReadablePath } | { reason: UnreachablePathReason };\n\n/**\n * Which resolved file the read tools may serve, and why not when they may not.\n *\n * The workspace is tried first, then the read-only roots, then the honored `additionalDirectories`\n * — the same order {@link resolveReadablePath} has always used, with the extra roots appended.\n * Only an absolute path reaches the roots outside the workspace, so a relative path never\n * re-anchors to one.\n *\n * The distinction the single `null` used to hide matters now that a call the tool cannot serve is\n * refused instead of prompted for: a path under a hidden subpath keeps its prompt, while a path\n * no root contains is refused outright and the refusal names the remedy.\n */\nexport function resolveReadablePathDetail(\n context: PathRoots,\n inputPath: string,\n): ReadablePathDetail {\n const entries = readOnlyEntries(context);\n const rejected = (match: ResolvedReadablePath): ReadablePathDetail =>\n isExcludedPath(match.canonicalPath, entries) ? { reason: 'excluded' } : { path: match };\n\n // The exclusion applies whichever root matched: a memory directory inside the workspace\n // (running in $HOME) must not expose its inbox through the workspace branch. The root that\n // matched travels with the answer, so a caller labelling or re-searching the file does not have\n // to work out which of the roots it was found under by comparing paths again.\n const wsMatch = resolveWorkspacePath(context.workspaceRoot, inputPath);\n if (wsMatch) {\n return rejected({ ...wsMatch, root: context.workspaceRoot, inWorkspace: true });\n }\n\n if (!isAbsolute(inputPath)) return { reason: 'unreachable' };\n for (const entry of entries) {\n const root = typeof entry === 'string' ? entry : entry.root;\n const match = resolveWorkspacePath(root, inputPath);\n if (match) return rejected({ ...match, root, inWorkspace: false });\n }\n return { reason: 'unreachable' };\n}\n\nexport function resolveReadablePath(\n context: PathRoots,\n inputPath: string,\n): ResolvedReadablePath | null {\n const detail = resolveReadablePathDetail(context, inputPath);\n return 'path' in detail ? detail.path : null;\n}\n\n/**\n * Where a write tool may put a file: the workspace, then the honored directories.\n *\n * A write to an absolute path inside an honored directory is served exactly the way the same write\n * inside the workspace is served — same permission verdict, same `.book/settings.local.json` guard,\n * same fresh-observation requirement — and a relative path stays anchored to the workspace, so a\n * bare `./notes.txt` cannot be re-anchored to a root the caller never named. Read-only roots are\n * deliberately not consulted: they are roots a *read* may cross, not a write target.\n *\n * A match under an honored root reports the **workspace**-relative spelling of its path, not the\n * root-relative one its own resolution produced. That is the spelling the observation ledger is\n * keyed on (`observeFile` derives it from the absolute path, so a file in an honored root and a\n * workspace file of the same name can never collide), and the fresh-observation check looks the\n * write up under it. Reporting the root-relative form instead would make the Read that licensed\n * the write and the write itself consult two different keys, and every honored write would report\n * `file_not_observed`.\n */\nexport function resolveMutationPath(\n context: PathRoots,\n inputPath: string,\n): ResolvedReadablePath | null {\n const inWorkspace = resolveWorkspacePath(context.workspaceRoot, inputPath);\n if (inWorkspace) return { ...inWorkspace, root: context.workspaceRoot, inWorkspace: true };\n if (!isAbsolute(inputPath)) return null;\n for (const root of context.additionalRoots ?? []) {\n const match = resolveWorkspacePath(root, inputPath);\n if (!match) continue;\n const rel = relative(resolve(context.workspaceRoot), match.filePath);\n return { ...match, relativePath: rel.replace(/\\\\/g, '/'), root, inWorkspace: false };\n }\n return null;\n}\n\n/**\n * Whether a resolved path falls under an excluded subpath of a read-only root (the memory\n * inbox). Checked on the canonical path — after symlinks — and case-folded where the file\n * system folds case, so `.INBOX/` or a link into `.inbox` cannot slip past it.\n */\nfunction isExcludedPath(\n canonicalPath: string,\n roots: readonly (string | ReadOnlyRoot)[] | undefined,\n): boolean {\n const fold = (p: string) => (process.platform === 'linux' ? p : p.toLowerCase());\n const target = fold(canonicalPath);\n for (const entry of roots ?? []) {\n if (typeof entry === 'string' || !entry.exclude?.length) continue;\n const base = fold(canonicalizePath(entry.root));\n for (const sub of entry.exclude) {\n const dir = fold(join(base, sub));\n if (target === dir || target.startsWith(`${dir}${sep}`)) return true;\n }\n }\n return false;\n}\n\n/**\n * The refusal a read tool gives a path no root serves.\n *\n * The message names the directory list as well as the workspace: after #300 the workspace is not\n * the only root a read may cross, so \"outside workspace\" alone sent the model looking for a rule\n * that would never work. The code is unchanged, so a caller that classifies refusals by code\n * (the agent loop's streak remedy, the managed-child notice) is unaffected.\n */\nexport function pathOutsideWorkspaceResult(inputPath: unknown): ToolResult {\n return toolFailure(`Path outside workspace and additionalDirectories: ${inputPath}`, {\n code: 'path_outside_workspace',\n remediation:\n 'Add its directory to additionalDirectories, or start Book in a directory that contains it. ' +\n 'No permission rule and no permission mode can change which roots a read may cross.',\n });\n}\n\n/** The workspace root after following links, or the root as given when it cannot be resolved. */\nexport function realWorkspaceRoot(root: string): string {\n try {\n return realpathSync.native(root);\n } catch {\n return resolve(root);\n }\n}\n\n/**\n * A path with its links followed, the way {@link resolveWorkspacePath} canonicalises one: the\n * nearest existing ancestor is resolved and the rest of the path re-appended, so a path that\n * does not exist yet still comes back through a link above it.\n *\n * `realWorkspaceRoot` alone is not enough for a comparison against a *file* path — a read target\n * that has been deleted, or one named before it is written, would fall back to its lexical form\n * and quietly fail to see through the link that reaches it.\n */\nexport function canonicalizePath(inputPath: string): string {\n const absolute = resolve(inputPath);\n const existing = nearestExistingPath(absolute);\n if (!existing) return absolute;\n try {\n return resolve(realpathSync.native(existing), relative(existing, absolute));\n } catch {\n return absolute;\n }\n}\n\n/**\n * Whether a canonical path is a root or under it, both sides in one canonical form.\n *\n * The comparison every \"is this path inside that root\" test outside a resolution makes: which\n * root served a resolved file, whether a Glob label belongs to the workspace, which root a\n * guarded read was served by. The path arrives canonical — from `realpath`, a glob walk or\n * ripgrep, or from {@link resolveWorkspacePath} — so the root has to be put in that form before\n * the two are compared. An as-given root and a canonical path are two spellings of one directory at\n * best, and two unrelated ones when the root was typed in 8.3 short form.\n */\nexport function isUnderRoot(canonicalPath: string, root: string): boolean {\n return !isOutside(canonicalizePath(root), canonicalPath);\n}\n\n/**\n * Whether a path has the *shape* of a root's Book-local settings file or directory, whatever root\n * is being asked about: the last segment is `.book`, or `settings.local.json` under one.\n *\n * The cheap half of {@link isBookLocalSettingsPath}, and the half worth running for every file a\n * Grep lists, because the rest of that answer costs a `realpath` per root. Sound rather than\n * exact — a `.book` somewhere below a root has the shape without being that root's — which is\n * what a prefilter has to be.\n */\nfunction hasBookLocalSettingsShape(\n canonicalPath: string,\n options: { directory?: boolean },\n): boolean {\n const parts = canonicalPath.toLowerCase().split(/[\\\\/]/);\n if (parts[parts.length - 1] === '.book') return options.directory === true;\n return parts[parts.length - 2] === '.book' && parts[parts.length - 1] === 'settings.local.json';\n}\n\n/**\n * Whether a path is Book's project-local settings file, `<root>/.book/settings.local.json`, which\n * can hold an API key; with `directory`, also the `.book` directory that holds it.\n *\n * Both sides are put in one canonical form before they are compared, and compared\n * case-insensitively on every platform: on a case-sensitive file system that only makes the check\n * stricter, while a case-insensitive Linux mount (WSL on /mnt/c) serves any spelling from the same\n * file. The root is canonicalized because it is the side a caller spells freely — 8.3 short form,\n * a lowercase drive letter, forward slashes — and the path because this is the guard that leaked\n * an API key when a root's spelling and a resolved path's spelling disagreed, and a check that\n * silently stops matching when a caller hands it a path in another form is the same failure with\n * a smaller blast radius. Callers that check every file a Grep listed run\n * {@link hasBookLocalSettingsShape} first, which is what keeps this off the per-file path.\n */\nexport function isBookLocalSettingsPath(\n path: string,\n root: string,\n options: { directory?: boolean } = {},\n): boolean {\n if (!hasBookLocalSettingsShape(path, options)) return false;\n const target = canonicalizePath(path).toLowerCase();\n const bookDir = join(canonicalizePath(root), '.book').toLowerCase();\n return (\n target === join(bookDir, 'settings.local.json') ||\n (options.directory === true && target === bookDir)\n );\n}\n\n/**\n * Whether a path is Book's own local settings under *any* root a read may cross.\n *\n * After #300 a read can cross a directory the workspace does not contain, and that directory can\n * carry its own `.book/settings.local.json` with an API key in it — the same file, with the same\n * reason to stay closed, reached by a path the workspace-root check would not recognize. One\n * function so the read tools and the permission scope agree on which paths are guarded. The shape\n * test comes first, so the roots are only put in canonical form for a file that could be one,\n * which is what keeps this free for the per-file call a Grep makes.\n */\nexport function isBookLocalSettingsUnderRoots(\n path: string,\n roots: PathRoots,\n options: { directory?: boolean } = {},\n): boolean {\n if (!hasBookLocalSettingsShape(path, options)) return false;\n return [roots.workspaceRoot, ...readOnlyEntries(roots)].some((root) => {\n const entryRoot = typeof root === 'string' ? root : root.root;\n return isBookLocalSettingsPath(path, entryRoot, options);\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/child-env.ts
|
|
2
|
+
var CHILD_SESSION_DRIVER = "agent";
|
|
3
|
+
var DEFAULTED_NODE_ENV_MARKER = "BOOK_DEFAULTED_NODE_ENV";
|
|
4
|
+
function buildChildEnv(base = process.env, overrides = {}) {
|
|
5
|
+
const env = { ...base, ...overrides };
|
|
6
|
+
delete env[DEFAULTED_NODE_ENV_MARKER];
|
|
7
|
+
env.BOOK_SESSION_DRIVER = CHILD_SESSION_DRIVER;
|
|
8
|
+
const overrideIsAmbient = overrides[DEFAULTED_NODE_ENV_MARKER] !== void 0;
|
|
9
|
+
if (overrideIsAmbient || base[DEFAULTED_NODE_ENV_MARKER] !== void 0 && !overrides.NODE_ENV) {
|
|
10
|
+
delete env.NODE_ENV;
|
|
11
|
+
}
|
|
12
|
+
return env;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
buildChildEnv
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=chunk-XHAO3K7I.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/child-env.ts"],"sourcesContent":["/**\n * The environment every command Book starts is built here.\n *\n * `runtime-env.ts` defaults `NODE_ENV=production` before React loads, because the development\n * renderer is 2-3x slower per pass. That default is a fact about Book's own process: it is why\n * the TUI is fast, and nothing else. Inherited by a child it silently changes what the command\n * does — `npm install` drops devDependencies, a test runner reads a production build flag, a\n * framework refuses to serve a source map — while the user, who set nothing, has no way to see\n * why.\n *\n * So the default is marked (`DEFAULTED_NODE_ENV_MARKER`), and the marker is a full stop: a child\n * never sees it, and a child only loses its NODE_ENV when the marker says Book invented it. A\n * NODE_ENV the user exported before Book started has no marker and always passes through, as\n * does one a caller sets explicitly in `overrides` — a hook's own `env`, an MCP server's `env`,\n * or `ToolContext.env`. The marker is also what covers Book's own detached processes\n * (`job-runner.ts`, `job-supervisor.ts`), which build their environment from `process.env` and\n * would otherwise hand the default on to a persistent job that outlives Book.\n *\n * `ToolContext.env` is `process.env` in the agent, so the default arrives through the override\n * layer as well. The marker is what tells that apart from a request: an override object carrying\n * the marker is a copy of Book's own environment, and its NODE_ENV is the default arriving by a\n * second route. Every other override object is a declaration — even one whose NODE_ENV happens to\n * equal `production`, which is the single value an explicit request and Book's default share, and\n * the one a value-comparison rule would silently delete.\n */\n\n/**\n * Every process Book starts runs under Book, so a `book` among them is driven by Book, not by the\n * user typing (`src/session-driver.ts`): it may keep what it learns, never its delegator's\n * instructions, as a delegated run of any other harness does. Its own `--session-driver` flag\n * still wins.\n */\nexport const CHILD_SESSION_DRIVER = 'agent';\n\n/** Set alongside the default `NODE_ENV`, and stripped from every child. */\nexport const DEFAULTED_NODE_ENV_MARKER = 'BOOK_DEFAULTED_NODE_ENV';\n\n/**\n * Build the environment for one child process.\n *\n * `base` defaults to Book's own environment. The result is a fresh object: a caller that\n * mutates it cannot reach back into `process.env`, which is shared with every other child.\n */\nexport function buildChildEnv(\n base: NodeJS.ProcessEnv = process.env,\n overrides: NodeJS.ProcessEnv = {},\n): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...base, ...overrides };\n delete env[DEFAULTED_NODE_ENV_MARKER];\n env.BOOK_SESSION_DRIVER = CHILD_SESSION_DRIVER;\n // Only Book's own default is removed, and only when nothing asked for it. A NODE_ENV that\n // arrived some other way is the user's and the command's to keep.\n //\n // An override carrying the marker is a copy of Book's own environment rather than a declaration:\n // `ToolContext.env` is `process.env` in the agent, and a persistent job's spec carries the\n // environment its runner was started with. Both carry the default without having asked for it.\n const overrideIsAmbient = overrides[DEFAULTED_NODE_ENV_MARKER] !== undefined;\n if (overrideIsAmbient || (base[DEFAULTED_NODE_ENV_MARKER] !== undefined && !overrides.NODE_ENV)) {\n delete env.NODE_ENV;\n }\n return env;\n}\n"],"mappings":";AAgCO,IAAM,uBAAuB;AAG7B,IAAM,4BAA4B;AAQlC,SAAS,cACd,OAA0B,QAAQ,KAClC,YAA+B,CAAC,GACb;AACnB,QAAM,MAAyB,EAAE,GAAG,MAAM,GAAG,UAAU;AACvD,SAAO,IAAI,yBAAyB;AACpC,MAAI,sBAAsB;AAO1B,QAAM,oBAAoB,UAAU,yBAAyB,MAAM;AACnE,MAAI,qBAAsB,KAAK,yBAAyB,MAAM,UAAa,CAAC,UAAU,UAAW;AAC/F,WAAO,IAAI;AAAA,EACb;AACA,SAAO;AACT;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveBookHome
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZPSZZUWF.js";
|
|
4
4
|
|
|
5
5
|
// src/mcp-config.ts
|
|
6
6
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -210,4 +210,4 @@ export {
|
|
|
210
210
|
loadMcpConfig,
|
|
211
211
|
formatMcpServerCommand
|
|
212
212
|
};
|
|
213
|
-
//# sourceMappingURL=chunk-
|
|
213
|
+
//# sourceMappingURL=chunk-Y2VHN7TC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mcp-config.ts"],"sourcesContent":["import { readFileSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { resolveBookHome } from './book-home.js';\n\nexport type McpTransportType = 'stdio' | 'http' | 'sse';\n\nexport interface McpServerConfig {\n /** Explicit transport. Omitted means stdio when `command` is present. */\n type?: McpTransportType;\n /** Executable for stdio servers. */\n command?: string;\n args?: string[];\n env?: Record<string, string>;\n /** Optional working directory for stdio servers. */\n cwd?: string;\n /** Endpoint for HTTP/SSE servers. */\n url?: string;\n /** Additional request headers for HTTP/SSE servers. Values are never shown in diagnostics. */\n headers?: Record<string, string>;\n}\n\nconst SAFE_SERVER_NAME = /^[A-Za-z0-9_-]+$/;\n\nexport function isValidMcpServerName(name: string): boolean {\n return SAFE_SERVER_NAME.test(name) && !name.includes('__');\n}\n\ninterface McpConfigDocument {\n mcpServers?: Record<string, unknown>;\n}\n\n/** Where a server declaration came from; project declarations require approval. */\nexport type McpServerSource = 'user' | 'project';\n\nexport interface ResolvedMcpServer {\n name: string;\n config: McpServerConfig;\n source: McpServerSource;\n /** Config file that defined (or, on a name collision, last overrode) this server. */\n path: string;\n}\n\nexport interface McpDiagnostic {\n level: 'warn';\n message: string;\n server?: string;\n}\n\nexport type McpDiagnosticSink = (diagnostic: McpDiagnostic) => void;\n\n/** Preserves the historical stderr warning behavior for headless/SDK hosts. */\nexport const consoleMcpDiagnosticSink: McpDiagnosticSink = (diagnostic) => {\n console.warn(`⚠ ${diagnostic.message}`);\n};\n\nexport interface ResolveMcpServersOptions {\n home?: string;\n onDiagnostic?: McpDiagnosticSink;\n /** Environment used for `${VAR}` interpolation; defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n}\n\ninterface ExpandedValue {\n value: string;\n missing: string[];\n}\n\n/**\n * Expand the small interpolation language used by the de-facto mcpServers\n * config format. We deliberately support only `${NAME}` and `${NAME:-default}`\n * here; shell syntax is never evaluated.\n */\nfunction expandValue(raw: string, env: NodeJS.ProcessEnv): ExpandedValue {\n const missing = new Set<string>();\n const value = raw.replace(\n /\\$\\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\\}/g,\n (_match, name: string, fallback?: string) => {\n const configured = env[name];\n if (configured !== undefined) return configured;\n if (fallback !== undefined) return fallback;\n missing.add(name);\n return '';\n },\n );\n return { value, missing: [...missing] };\n}\n\nfunction expandRecord(\n raw: unknown,\n env: NodeJS.ProcessEnv,\n): { value: Record<string, string>; missing: string[]; invalid: string[] } {\n const value: Record<string, string> = {};\n const missing = new Set<string>();\n const invalid: string[] = [];\n if (raw === undefined) return { value, missing: [], invalid };\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n return { value, missing: [], invalid: ['expected an object'] };\n }\n for (const [key, item] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof item !== 'string') {\n invalid.push(`${key} must be a string`);\n continue;\n }\n const expanded = expandValue(item, env);\n value[key] = expanded.value;\n for (const name of expanded.missing) missing.add(name);\n }\n return { value, missing: [...missing], invalid };\n}\n\nfunction normalizeServerConfig(\n name: string,\n raw: unknown,\n env: NodeJS.ProcessEnv,\n): { config?: McpServerConfig; error?: string } {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n return { error: 'expected an object' };\n }\n const source = raw as Record<string, unknown>;\n const requestedType = source.type ?? source.transport;\n const type =\n requestedType === undefined\n ? source.url !== undefined\n ? 'http'\n : 'stdio'\n : String(requestedType).toLowerCase();\n if (!['stdio', 'http', 'sse'].includes(type)) {\n return { error: `unsupported transport type \"${String(requestedType)}\"` };\n }\n\n const commandExpanded =\n typeof source.command === 'string' ? expandValue(source.command, env) : undefined;\n const urlExpanded = typeof source.url === 'string' ? expandValue(source.url, env) : undefined;\n const missing = new Set([...(commandExpanded?.missing ?? []), ...(urlExpanded?.missing ?? [])]);\n const args: string[] = [];\n if (source.args !== undefined) {\n if (!Array.isArray(source.args)) return { error: 'args must be an array' };\n for (const [index, item] of source.args.entries()) {\n if (typeof item !== 'string') return { error: `args[${index}] must be a string` };\n const expanded = expandValue(item, env);\n args.push(expanded.value);\n for (const variable of expanded.missing) missing.add(variable);\n }\n }\n const envValues = expandRecord(source.env, env);\n const headerValues = expandRecord(source.headers, env);\n for (const variable of [...envValues.missing, ...headerValues.missing]) missing.add(variable);\n if (envValues.invalid.length > 0) return { error: `env ${envValues.invalid.join(', ')}` };\n if (headerValues.invalid.length > 0)\n return { error: `headers ${headerValues.invalid.join(', ')}` };\n if (missing.size > 0)\n return { error: `missing environment variable(s): ${[...missing].join(', ')}` };\n\n if (type === 'stdio') {\n if (!commandExpanded?.value.trim())\n return { error: 'stdio server requires a non-empty command' };\n const cwd = typeof source.cwd === 'string' ? expandValue(source.cwd, env) : undefined;\n if (cwd?.missing.length)\n return { error: `missing environment variable(s): ${cwd.missing.join(', ')}` };\n return {\n config: {\n type: 'stdio',\n command: commandExpanded.value,\n args,\n env: envValues.value,\n ...(cwd?.value ? { cwd: cwd.value } : {}),\n },\n };\n }\n\n if (!urlExpanded?.value.trim()) return { error: `${type} server requires a non-empty url` };\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(urlExpanded.value);\n } catch {\n return { error: 'url must be a valid absolute URL' };\n }\n if (!['http:', 'https:'].includes(parsedUrl.protocol)) {\n return { error: 'url must use http:// or https://' };\n }\n if (parsedUrl.username || parsedUrl.password)\n return { error: 'url must not contain credentials' };\n return {\n config: {\n type: type as 'http' | 'sse',\n url: parsedUrl.toString(),\n headers: headerValues.value,\n },\n };\n}\n\n/**\n * Resolve the declared MCP servers with provenance.\n *\n * Sources, later wins on a name collision: <BOOK_HOME>/.book/mcp.json (user),\n * then <workspace>/.mcp.json (project). A project entry that overrides a\n * user-global name keeps `source: 'project'` so it cannot inherit the trust of\n * the name it shadows.\n */\nexport function resolveMcpServerList(\n workspace: string,\n options: ResolveMcpServersOptions = {},\n): ResolvedMcpServer[] {\n const onDiagnostic = options.onDiagnostic ?? consoleMcpDiagnosticSink;\n const env = options.env ?? process.env;\n const bookHome = options.home ? join(options.home, '.book') : resolveBookHome();\n const sources: Array<{ path: string; source: McpServerSource }> = [\n { path: join(bookHome, 'mcp.json'), source: 'user' },\n { path: join(workspace, '.mcp.json'), source: 'project' },\n ];\n const servers = new Map<string, ResolvedMcpServer>();\n\n for (const { path, source } of sources) {\n if (!existsSync(path)) continue;\n let parsed: McpConfigDocument;\n try {\n parsed = JSON.parse(readFileSync(path, 'utf-8')) as McpConfigDocument;\n } catch {\n onDiagnostic({ level: 'warn', message: `Failed to load MCP config: ${path}` });\n continue;\n }\n if (!parsed || typeof parsed !== 'object' || !parsed.mcpServers) continue;\n if (typeof parsed.mcpServers !== 'object' || Array.isArray(parsed.mcpServers)) {\n onDiagnostic({ level: 'warn', message: `Invalid mcpServers object: ${path}` });\n continue;\n }\n for (const [name, rawConfig] of Object.entries(parsed.mcpServers)) {\n if (!isValidMcpServerName(name)) {\n onDiagnostic({\n level: 'warn',\n message: `Ignoring invalid MCP server name: ${name} (${path}); use letters, numbers, hyphens, or underscores, without a double underscore`,\n server: name,\n });\n continue;\n }\n const normalized = normalizeServerConfig(name, rawConfig, env);\n if (!normalized.config) {\n onDiagnostic({\n level: 'warn',\n message: `Ignoring invalid MCP server config: ${name} (${path}): ${normalized.error ?? 'invalid configuration'}`,\n server: name,\n });\n continue;\n }\n servers.set(name, {\n name,\n source,\n path,\n config: normalized.config,\n });\n }\n }\n\n return [...servers.values()];\n}\n\n/** Merged name → config view used by doctor and ambient-run fingerprinting. */\nexport function loadMcpConfig(workspace: string, home?: string): Record<string, McpServerConfig> {\n const servers: Record<string, McpServerConfig> = {};\n for (const server of resolveMcpServerList(workspace, { home })) {\n servers[server.name] = server.config;\n }\n return servers;\n}\n\n/** Render one server as the exact command line a host is being asked to trust. */\nexport function formatMcpServerCommand(config: McpServerConfig): string {\n const sensitiveValues = [\n ...Object.values(config.env ?? {}),\n ...Object.values(config.headers ?? {}),\n ]\n .filter((value) => value.length >= 4)\n .sort((left, right) => right.length - left.length);\n const redact = (value: string): string => {\n let rendered = value;\n for (const sensitive of sensitiveValues)\n rendered = rendered.replaceAll(sensitive, '<redacted>');\n return rendered;\n };\n if (config.type === 'http' || config.type === 'sse' || (!config.command && config.url)) {\n const headers = Object.keys(config.headers ?? {}).sort();\n let endpoint = config.url ?? '';\n let queryConfigured = false;\n try {\n const url = new URL(endpoint);\n queryConfigured = Boolean(url.search);\n url.search = '';\n url.hash = '';\n endpoint = url.toString();\n } catch {\n endpoint = redact(endpoint);\n }\n const metadata = [\n ...(queryConfigured ? ['query configured'] : []),\n ...(headers.length ? [`headers: ${headers.join(', ')}`] : []),\n ];\n return `${config.type === 'sse' ? 'sse' : 'http'} ${endpoint}${metadata.length ? ` (${metadata.join('; ')})` : ''}`;\n }\n return [config.command ?? '', ...(config.args ?? [])].filter(Boolean).map(redact).join(' ');\n}\n"],"mappings":";;;;;AAAA,SAAS,cAAc,kBAAkB;AACzC,SAAS,YAAY;AAoBrB,IAAM,mBAAmB;AAElB,SAAS,qBAAqB,MAAuB;AAC1D,SAAO,iBAAiB,KAAK,IAAI,KAAK,CAAC,KAAK,SAAS,IAAI;AAC3D;AA0BO,IAAM,2BAA8C,CAAC,eAAe;AACzE,UAAQ,KAAK,WAAM,WAAW,OAAO,EAAE;AACzC;AAmBA,SAAS,YAAY,KAAa,KAAuC;AACvE,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAQ,IAAI;AAAA,IAChB;AAAA,IACA,CAAC,QAAQ,MAAc,aAAsB;AAC3C,YAAM,aAAa,IAAI,IAAI;AAC3B,UAAI,eAAe,OAAW,QAAO;AACrC,UAAI,aAAa,OAAW,QAAO;AACnC,cAAQ,IAAI,IAAI;AAChB,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO,EAAE,OAAO,SAAS,CAAC,GAAG,OAAO,EAAE;AACxC;AAEA,SAAS,aACP,KACA,KACyE;AACzE,QAAM,QAAgC,CAAC;AACvC,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,UAAoB,CAAC;AAC3B,MAAI,QAAQ,OAAW,QAAO,EAAE,OAAO,SAAS,CAAC,GAAG,QAAQ;AAC5D,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACzD,WAAO,EAAE,OAAO,SAAS,CAAC,GAAG,SAAS,CAAC,oBAAoB,EAAE;AAAA,EAC/D;AACA,aAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,GAA8B,GAAG;AACxE,QAAI,OAAO,SAAS,UAAU;AAC5B,cAAQ,KAAK,GAAG,GAAG,mBAAmB;AACtC;AAAA,IACF;AACA,UAAM,WAAW,YAAY,MAAM,GAAG;AACtC,UAAM,GAAG,IAAI,SAAS;AACtB,eAAW,QAAQ,SAAS,QAAS,SAAQ,IAAI,IAAI;AAAA,EACvD;AACA,SAAO,EAAE,OAAO,SAAS,CAAC,GAAG,OAAO,GAAG,QAAQ;AACjD;AAEA,SAAS,sBACP,MACA,KACA,KAC8C;AAC9C,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACzD,WAAO,EAAE,OAAO,qBAAqB;AAAA,EACvC;AACA,QAAM,SAAS;AACf,QAAM,gBAAgB,OAAO,QAAQ,OAAO;AAC5C,QAAM,OACJ,kBAAkB,SACd,OAAO,QAAQ,SACb,SACA,UACF,OAAO,aAAa,EAAE,YAAY;AACxC,MAAI,CAAC,CAAC,SAAS,QAAQ,KAAK,EAAE,SAAS,IAAI,GAAG;AAC5C,WAAO,EAAE,OAAO,+BAA+B,OAAO,aAAa,CAAC,IAAI;AAAA,EAC1E;AAEA,QAAM,kBACJ,OAAO,OAAO,YAAY,WAAW,YAAY,OAAO,SAAS,GAAG,IAAI;AAC1E,QAAM,cAAc,OAAO,OAAO,QAAQ,WAAW,YAAY,OAAO,KAAK,GAAG,IAAI;AACpF,QAAM,UAAU,oBAAI,IAAI,CAAC,GAAI,iBAAiB,WAAW,CAAC,GAAI,GAAI,aAAa,WAAW,CAAC,CAAE,CAAC;AAC9F,QAAM,OAAiB,CAAC;AACxB,MAAI,OAAO,SAAS,QAAW;AAC7B,QAAI,CAAC,MAAM,QAAQ,OAAO,IAAI,EAAG,QAAO,EAAE,OAAO,wBAAwB;AACzE,eAAW,CAAC,OAAO,IAAI,KAAK,OAAO,KAAK,QAAQ,GAAG;AACjD,UAAI,OAAO,SAAS,SAAU,QAAO,EAAE,OAAO,QAAQ,KAAK,qBAAqB;AAChF,YAAM,WAAW,YAAY,MAAM,GAAG;AACtC,WAAK,KAAK,SAAS,KAAK;AACxB,iBAAW,YAAY,SAAS,QAAS,SAAQ,IAAI,QAAQ;AAAA,IAC/D;AAAA,EACF;AACA,QAAM,YAAY,aAAa,OAAO,KAAK,GAAG;AAC9C,QAAM,eAAe,aAAa,OAAO,SAAS,GAAG;AACrD,aAAW,YAAY,CAAC,GAAG,UAAU,SAAS,GAAG,aAAa,OAAO,EAAG,SAAQ,IAAI,QAAQ;AAC5F,MAAI,UAAU,QAAQ,SAAS,EAAG,QAAO,EAAE,OAAO,OAAO,UAAU,QAAQ,KAAK,IAAI,CAAC,GAAG;AACxF,MAAI,aAAa,QAAQ,SAAS;AAChC,WAAO,EAAE,OAAO,WAAW,aAAa,QAAQ,KAAK,IAAI,CAAC,GAAG;AAC/D,MAAI,QAAQ,OAAO;AACjB,WAAO,EAAE,OAAO,oCAAoC,CAAC,GAAG,OAAO,EAAE,KAAK,IAAI,CAAC,GAAG;AAEhF,MAAI,SAAS,SAAS;AACpB,QAAI,CAAC,iBAAiB,MAAM,KAAK;AAC/B,aAAO,EAAE,OAAO,4CAA4C;AAC9D,UAAM,MAAM,OAAO,OAAO,QAAQ,WAAW,YAAY,OAAO,KAAK,GAAG,IAAI;AAC5E,QAAI,KAAK,QAAQ;AACf,aAAO,EAAE,OAAO,oCAAoC,IAAI,QAAQ,KAAK,IAAI,CAAC,GAAG;AAC/E,WAAO;AAAA,MACL,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,SAAS,gBAAgB;AAAA,QACzB;AAAA,QACA,KAAK,UAAU;AAAA,QACf,GAAI,KAAK,QAAQ,EAAE,KAAK,IAAI,MAAM,IAAI,CAAC;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,aAAa,MAAM,KAAK,EAAG,QAAO,EAAE,OAAO,GAAG,IAAI,mCAAmC;AAC1F,MAAI;AACJ,MAAI;AACF,gBAAY,IAAI,IAAI,YAAY,KAAK;AAAA,EACvC,QAAQ;AACN,WAAO,EAAE,OAAO,mCAAmC;AAAA,EACrD;AACA,MAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,UAAU,QAAQ,GAAG;AACrD,WAAO,EAAE,OAAO,mCAAmC;AAAA,EACrD;AACA,MAAI,UAAU,YAAY,UAAU;AAClC,WAAO,EAAE,OAAO,mCAAmC;AACrD,SAAO;AAAA,IACL,QAAQ;AAAA,MACN;AAAA,MACA,KAAK,UAAU,SAAS;AAAA,MACxB,SAAS,aAAa;AAAA,IACxB;AAAA,EACF;AACF;AAUO,SAAS,qBACd,WACA,UAAoC,CAAC,GAChB;AACrB,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,MAAM,QAAQ,OAAO,QAAQ;AACnC,QAAM,WAAW,QAAQ,OAAO,KAAK,QAAQ,MAAM,OAAO,IAAI,gBAAgB;AAC9E,QAAM,UAA4D;AAAA,IAChE,EAAE,MAAM,KAAK,UAAU,UAAU,GAAG,QAAQ,OAAO;AAAA,IACnD,EAAE,MAAM,KAAK,WAAW,WAAW,GAAG,QAAQ,UAAU;AAAA,EAC1D;AACA,QAAM,UAAU,oBAAI,IAA+B;AAEnD,aAAW,EAAE,MAAM,OAAO,KAAK,SAAS;AACtC,QAAI,CAAC,WAAW,IAAI,EAAG;AACvB,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,aAAa,MAAM,OAAO,CAAC;AAAA,IACjD,QAAQ;AACN,mBAAa,EAAE,OAAO,QAAQ,SAAS,8BAA8B,IAAI,GAAG,CAAC;AAC7E;AAAA,IACF;AACA,QAAI,CAAC,UAAU,OAAO,WAAW,YAAY,CAAC,OAAO,WAAY;AACjE,QAAI,OAAO,OAAO,eAAe,YAAY,MAAM,QAAQ,OAAO,UAAU,GAAG;AAC7E,mBAAa,EAAE,OAAO,QAAQ,SAAS,8BAA8B,IAAI,GAAG,CAAC;AAC7E;AAAA,IACF;AACA,eAAW,CAAC,MAAM,SAAS,KAAK,OAAO,QAAQ,OAAO,UAAU,GAAG;AACjE,UAAI,CAAC,qBAAqB,IAAI,GAAG;AAC/B,qBAAa;AAAA,UACX,OAAO;AAAA,UACP,SAAS,qCAAqC,IAAI,KAAK,IAAI;AAAA,UAC3D,QAAQ;AAAA,QACV,CAAC;AACD;AAAA,MACF;AACA,YAAM,aAAa,sBAAsB,MAAM,WAAW,GAAG;AAC7D,UAAI,CAAC,WAAW,QAAQ;AACtB,qBAAa;AAAA,UACX,OAAO;AAAA,UACP,SAAS,uCAAuC,IAAI,KAAK,IAAI,MAAM,WAAW,SAAS,uBAAuB;AAAA,UAC9G,QAAQ;AAAA,QACV,CAAC;AACD;AAAA,MACF;AACA,cAAQ,IAAI,MAAM;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ,WAAW;AAAA,MACrB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,CAAC,GAAG,QAAQ,OAAO,CAAC;AAC7B;AAGO,SAAS,cAAc,WAAmB,MAAgD;AAC/F,QAAM,UAA2C,CAAC;AAClD,aAAW,UAAU,qBAAqB,WAAW,EAAE,KAAK,CAAC,GAAG;AAC9D,YAAQ,OAAO,IAAI,IAAI,OAAO;AAAA,EAChC;AACA,SAAO;AACT;AAGO,SAAS,uBAAuB,QAAiC;AACtE,QAAM,kBAAkB;AAAA,IACtB,GAAG,OAAO,OAAO,OAAO,OAAO,CAAC,CAAC;AAAA,IACjC,GAAG,OAAO,OAAO,OAAO,WAAW,CAAC,CAAC;AAAA,EACvC,EACG,OAAO,CAAC,UAAU,MAAM,UAAU,CAAC,EACnC,KAAK,CAAC,MAAM,UAAU,MAAM,SAAS,KAAK,MAAM;AACnD,QAAM,SAAS,CAAC,UAA0B;AACxC,QAAI,WAAW;AACf,eAAW,aAAa;AACtB,iBAAW,SAAS,WAAW,WAAW,YAAY;AACxD,WAAO;AAAA,EACT;AACA,MAAI,OAAO,SAAS,UAAU,OAAO,SAAS,SAAU,CAAC,OAAO,WAAW,OAAO,KAAM;AACtF,UAAM,UAAU,OAAO,KAAK,OAAO,WAAW,CAAC,CAAC,EAAE,KAAK;AACvD,QAAI,WAAW,OAAO,OAAO;AAC7B,QAAI,kBAAkB;AACtB,QAAI;AACF,YAAM,MAAM,IAAI,IAAI,QAAQ;AAC5B,wBAAkB,QAAQ,IAAI,MAAM;AACpC,UAAI,SAAS;AACb,UAAI,OAAO;AACX,iBAAW,IAAI,SAAS;AAAA,IAC1B,QAAQ;AACN,iBAAW,OAAO,QAAQ;AAAA,IAC5B;AACA,UAAM,WAAW;AAAA,MACf,GAAI,kBAAkB,CAAC,kBAAkB,IAAI,CAAC;AAAA,MAC9C,GAAI,QAAQ,SAAS,CAAC,YAAY,QAAQ,KAAK,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,IAC7D;AACA,WAAO,GAAG,OAAO,SAAS,QAAQ,QAAQ,MAAM,IAAI,QAAQ,GAAG,SAAS,SAAS,KAAK,SAAS,KAAK,IAAI,CAAC,MAAM,EAAE;AAAA,EACnH;AACA,SAAO,CAAC,OAAO,WAAW,IAAI,GAAI,OAAO,QAAQ,CAAC,CAAE,EAAE,OAAO,OAAO,EAAE,IAAI,MAAM,EAAE,KAAK,GAAG;AAC5F;","names":[]}
|