@letrquan/book 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +2504 -0
  2. package/LICENSE +123 -0
  3. package/README.md +1383 -0
  4. package/dist/app-MOXXWEOY.js +18945 -0
  5. package/dist/app-MOXXWEOY.js.map +1 -0
  6. package/dist/chunk-24AXE6SP.js +12 -0
  7. package/dist/chunk-24AXE6SP.js.map +1 -0
  8. package/dist/chunk-3U5IBM24.js +213 -0
  9. package/dist/chunk-3U5IBM24.js.map +1 -0
  10. package/dist/chunk-574C6MQR.js +743 -0
  11. package/dist/chunk-574C6MQR.js.map +1 -0
  12. package/dist/chunk-5GDZ22YP.js +449 -0
  13. package/dist/chunk-5GDZ22YP.js.map +1 -0
  14. package/dist/chunk-5KLVH3PY.js +167 -0
  15. package/dist/chunk-5KLVH3PY.js.map +1 -0
  16. package/dist/chunk-5RDRAO4B.js +92 -0
  17. package/dist/chunk-5RDRAO4B.js.map +1 -0
  18. package/dist/chunk-7N4J557C.js +10 -0
  19. package/dist/chunk-7N4J557C.js.map +1 -0
  20. package/dist/chunk-A7JLBK2Y.js +3506 -0
  21. package/dist/chunk-A7JLBK2Y.js.map +1 -0
  22. package/dist/chunk-ANSJPYGI.js +84 -0
  23. package/dist/chunk-ANSJPYGI.js.map +1 -0
  24. package/dist/chunk-BTZZUCLI.js +264 -0
  25. package/dist/chunk-BTZZUCLI.js.map +1 -0
  26. package/dist/chunk-C7D4NJ2D.js +217 -0
  27. package/dist/chunk-C7D4NJ2D.js.map +1 -0
  28. package/dist/chunk-CAGOB2N4.js +208 -0
  29. package/dist/chunk-CAGOB2N4.js.map +1 -0
  30. package/dist/chunk-DGMQEI6Y.js +98 -0
  31. package/dist/chunk-DGMQEI6Y.js.map +1 -0
  32. package/dist/chunk-E75IPST5.js +289 -0
  33. package/dist/chunk-E75IPST5.js.map +1 -0
  34. package/dist/chunk-EYGD6X2T.js +180 -0
  35. package/dist/chunk-EYGD6X2T.js.map +1 -0
  36. package/dist/chunk-GTVB4HEM.js +765 -0
  37. package/dist/chunk-GTVB4HEM.js.map +1 -0
  38. package/dist/chunk-J7MIZ5CA.js +1907 -0
  39. package/dist/chunk-J7MIZ5CA.js.map +1 -0
  40. package/dist/chunk-KWHQ2XXL.js +626 -0
  41. package/dist/chunk-KWHQ2XXL.js.map +1 -0
  42. package/dist/chunk-L6LK2QIQ.js +657 -0
  43. package/dist/chunk-L6LK2QIQ.js.map +1 -0
  44. package/dist/chunk-MAQXCUR4.js +1007 -0
  45. package/dist/chunk-MAQXCUR4.js.map +1 -0
  46. package/dist/chunk-S4XL7HOM.js +402 -0
  47. package/dist/chunk-S4XL7HOM.js.map +1 -0
  48. package/dist/chunk-URLQAXTR.js +60 -0
  49. package/dist/chunk-URLQAXTR.js.map +1 -0
  50. package/dist/chunk-V4BOYR52.js +3504 -0
  51. package/dist/chunk-V4BOYR52.js.map +1 -0
  52. package/dist/chunk-VZQXX3WW.js +210 -0
  53. package/dist/chunk-VZQXX3WW.js.map +1 -0
  54. package/dist/chunk-WKBO5R4O.js +34 -0
  55. package/dist/chunk-WKBO5R4O.js.map +1 -0
  56. package/dist/chunk-XIFFTATU.js +15753 -0
  57. package/dist/chunk-XIFFTATU.js.map +1 -0
  58. package/dist/command-approvals-6Y57VCWZ.js +28 -0
  59. package/dist/command-approvals-6Y57VCWZ.js.map +1 -0
  60. package/dist/context-ZC5IFEAX.js +25 -0
  61. package/dist/context-ZC5IFEAX.js.map +1 -0
  62. package/dist/hook-approvals-CTUFKEJT.js +22 -0
  63. package/dist/hook-approvals-CTUFKEJT.js.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +2582 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/interactive-assets-CCAAWVKH.js +31 -0
  68. package/dist/interactive-assets-CCAAWVKH.js.map +1 -0
  69. package/dist/job-runner.d.ts +2 -0
  70. package/dist/job-runner.js +235 -0
  71. package/dist/job-runner.js.map +1 -0
  72. package/dist/loader-4IDS2RIE.js +15 -0
  73. package/dist/loader-4IDS2RIE.js.map +1 -0
  74. package/dist/mcp-approvals-WNPH65BK.js +18 -0
  75. package/dist/mcp-approvals-WNPH65BK.js.map +1 -0
  76. package/dist/mcp-config-YIJIES2C.js +16 -0
  77. package/dist/mcp-config-YIJIES2C.js.map +1 -0
  78. package/dist/model-window-store-YFVNXCIP.js +27 -0
  79. package/dist/model-window-store-YFVNXCIP.js.map +1 -0
  80. package/dist/openai-compatible-RHZUBHV6.js +10 -0
  81. package/dist/openai-compatible-RHZUBHV6.js.map +1 -0
  82. package/dist/permission-approvals-76PVREFD.js +14 -0
  83. package/dist/permission-approvals-76PVREFD.js.map +1 -0
  84. package/dist/permissions-CHCKTFQQ.js +28 -0
  85. package/dist/permissions-CHCKTFQQ.js.map +1 -0
  86. package/dist/sandbox-CNVAQDA7.js +23 -0
  87. package/dist/sandbox-CNVAQDA7.js.map +1 -0
  88. package/dist/scrollback-LUKLIBVI.js +136 -0
  89. package/dist/scrollback-LUKLIBVI.js.map +1 -0
  90. package/dist/sdk.d.ts +4574 -0
  91. package/dist/sdk.js +665 -0
  92. package/dist/sdk.js.map +1 -0
  93. package/dist/settings-loader-M2QP5TX2.js +21 -0
  94. package/dist/settings-loader-M2QP5TX2.js.map +1 -0
  95. package/dist/settings-removed-JWWPT4WA.js +19 -0
  96. package/dist/settings-removed-JWWPT4WA.js.map +1 -0
  97. package/dist/shell-selection-V6QAYXPX.js +17 -0
  98. package/dist/shell-selection-V6QAYXPX.js.map +1 -0
  99. package/package.json +143 -0
  100. package/patches/ink+6.8.0.patch +13 -0
  101. package/scripts/apply-ink-patch.mjs +29 -0
  102. package/scripts/ink-patch.mjs +59 -0
  103. package/scripts/verify-ink-patch.mjs +11 -0
@@ -0,0 +1,626 @@
1
+ import {
2
+ parseFrontmatter
3
+ } from "./chunk-E75IPST5.js";
4
+ import {
5
+ resolveBookHome
6
+ } from "./chunk-24AXE6SP.js";
7
+
8
+ // src/skills.ts
9
+ import {
10
+ closeSync,
11
+ existsSync,
12
+ lstatSync,
13
+ openSync,
14
+ readFileSync,
15
+ readSync,
16
+ readdirSync,
17
+ realpathSync,
18
+ statSync
19
+ } from "fs";
20
+ import { createHash } from "crypto";
21
+ import { homedir } from "os";
22
+ import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "path";
23
+ var SKILL_ENTRY_FILENAME = "SKILL.md";
24
+ var MAX_SKILL_HEADER_BYTES = 64 * 1024;
25
+ var MAX_SKILL_BODY_BYTES = 100 * 1024;
26
+ var MAX_SKILL_RESOURCES = 200;
27
+ var MAX_SKILL_RESOURCE_DEPTH = 4;
28
+ var MAX_SKILL_RESOURCE_BYTES = 2 * 1024 * 1024;
29
+ var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
30
+ var KNOWN_FRONTMATTER_FIELDS = /* @__PURE__ */ new Set([
31
+ "name",
32
+ "description",
33
+ "license",
34
+ "compatibility",
35
+ "version",
36
+ "metadata",
37
+ "allowed-tools",
38
+ "tools",
39
+ "when_to_use",
40
+ "model",
41
+ "lifetime",
42
+ "disabled",
43
+ // Interoperable Claude/Codex skill metadata. Invocation controls are
44
+ // advisory to Book's explicit `$name` UI and do not grant permissions.
45
+ "disable-model-invocation",
46
+ "user-invocable",
47
+ "argument-hint"
48
+ ]);
49
+ function pathKey(path) {
50
+ return process.platform === "win32" ? resolve(path).toLowerCase() : resolve(path);
51
+ }
52
+ function isInside(root, candidate) {
53
+ const rel = relative(root, candidate);
54
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
55
+ }
56
+ function projectRootFor(workspace) {
57
+ let current = resolve(workspace);
58
+ const filesystemRoot = parse(current).root;
59
+ while (true) {
60
+ if (existsSync(join(current, ".git"))) return current;
61
+ if (current === filesystemRoot) return resolve(workspace);
62
+ current = dirname(current);
63
+ }
64
+ }
65
+ function directoriesFromRoot(root, workspace) {
66
+ const normalizedRoot = resolve(root);
67
+ const normalizedWorkspace = resolve(workspace);
68
+ if (!isInside(normalizedRoot, normalizedWorkspace)) return [normalizedWorkspace];
69
+ const directories = [];
70
+ let current = normalizedWorkspace;
71
+ while (true) {
72
+ directories.push(current);
73
+ if (pathKey(current) === pathKey(normalizedRoot)) break;
74
+ current = dirname(current);
75
+ }
76
+ return directories.reverse();
77
+ }
78
+ function skillRoots(workspace, options = {}) {
79
+ const roots = [];
80
+ if (options.includeUser !== false) {
81
+ const home = options.homeDir ?? homedir();
82
+ const bookHome = options.bookHomeDir ?? (options.homeDir ? join(home, ".book") : resolveBookHome());
83
+ roots.push(
84
+ { path: join(home, ".claude", "skills"), source: "user", kind: "claude" },
85
+ { path: join(home, ".agents", "skills"), source: "user", kind: "agents" },
86
+ ...options.includeOpenCode === false ? [] : [
87
+ {
88
+ path: join(home, ".config", "opencode", "skills"),
89
+ source: "user",
90
+ kind: "opencode"
91
+ }
92
+ ],
93
+ { path: join(bookHome, "skills"), source: "user", kind: "book" }
94
+ );
95
+ }
96
+ const projectRoot = options.projectRoot ?? projectRootFor(workspace);
97
+ for (const directory of directoriesFromRoot(projectRoot, workspace)) {
98
+ roots.push(
99
+ { path: join(directory, ".claude", "skills"), source: "project", kind: "claude" },
100
+ { path: join(directory, ".agents", "skills"), source: "project", kind: "agents" },
101
+ ...options.includeOpenCode === false ? [] : [
102
+ {
103
+ path: join(directory, ".opencode", "skills"),
104
+ source: "project",
105
+ kind: "opencode"
106
+ }
107
+ ],
108
+ { path: join(directory, ".book", "skills"), source: "project", kind: "book" }
109
+ );
110
+ }
111
+ const byPath = /* @__PURE__ */ new Map();
112
+ for (const root of roots) byPath.set(pathKey(root.path), root);
113
+ return [...byPath.values()];
114
+ }
115
+ function readFrontmatterPrefix(path) {
116
+ const issues = [];
117
+ const fd = openSync(path, "r");
118
+ const chunks = [];
119
+ let total = 0;
120
+ try {
121
+ while (total < MAX_SKILL_HEADER_BYTES) {
122
+ const chunk = Buffer.alloc(Math.min(4096, MAX_SKILL_HEADER_BYTES - total));
123
+ const count = readSync(fd, chunk, 0, chunk.length, total);
124
+ if (count === 0) break;
125
+ chunks.push(chunk.subarray(0, count));
126
+ total += count;
127
+ const text = Buffer.concat(chunks).toString("utf8");
128
+ if (/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/.test(text)) {
129
+ const match = text.match(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/);
130
+ const headerText = match?.[0] ?? "";
131
+ return { frontmatter: parseFrontmatter(headerText).frontmatter, headerText, issues };
132
+ }
133
+ }
134
+ } finally {
135
+ closeSync(fd);
136
+ }
137
+ issues.push({
138
+ code: "invalid_frontmatter",
139
+ message: `Missing YAML frontmatter or closing delimiter within ${MAX_SKILL_HEADER_BYTES} bytes.`,
140
+ severity: "error"
141
+ });
142
+ return { frontmatter: {}, headerText: "", issues };
143
+ }
144
+ function stringField(frontmatter, name) {
145
+ const value = frontmatter[name];
146
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
147
+ }
148
+ function metadataFromHeader(headerText) {
149
+ const lines = headerText.split(/\r?\n/);
150
+ const metadata = {};
151
+ const start = lines.findIndex((line) => /^metadata\s*:\s*$/.test(line.trim()));
152
+ if (start < 0) return metadata;
153
+ for (const line of lines.slice(start + 1)) {
154
+ if (/^---\s*$/.test(line)) break;
155
+ if (line.trim() && !/^\s+/.test(line)) break;
156
+ const match = line.match(/^\s+([A-Za-z0-9_.-]+)\s*:\s*(.*?)\s*$/);
157
+ if (!match) continue;
158
+ metadata[match[1]] = match[2].replace(/^["'](.*)["']$/, "$1");
159
+ }
160
+ return metadata;
161
+ }
162
+ function allowedToolsFrom(frontmatter) {
163
+ const value = frontmatter["allowed-tools"] ?? frontmatter.tools;
164
+ if (Array.isArray(value)) {
165
+ const tools = value.map(String).map((tool) => tool.trim()).filter(Boolean);
166
+ return tools.length ? tools : void 0;
167
+ }
168
+ if (typeof value === "string") {
169
+ const tools = value.match(/[^\s]+(?:\([^)]*\))?/g)?.map((tool) => tool.trim()) ?? [];
170
+ return tools.length ? tools : void 0;
171
+ }
172
+ return void 0;
173
+ }
174
+ function collectResources(rootPath, entryPath) {
175
+ const resources = [];
176
+ const issues = [];
177
+ let depthLimitReported = false;
178
+ const visit = (directory, depth) => {
179
+ if (depth > MAX_SKILL_RESOURCE_DEPTH) {
180
+ if (!depthLimitReported) {
181
+ depthLimitReported = true;
182
+ issues.push({
183
+ code: "resource_depth_limit",
184
+ message: `Resources deeper than ${MAX_SKILL_RESOURCE_DEPTH} directories are ignored.`,
185
+ severity: "warning"
186
+ });
187
+ }
188
+ return;
189
+ }
190
+ if (resources.length >= MAX_SKILL_RESOURCES) return;
191
+ let entries;
192
+ try {
193
+ entries = readdirSync(directory).sort();
194
+ } catch (error) {
195
+ issues.push({
196
+ code: "resource_unreadable",
197
+ message: `Could not inspect resources in ${directory}: ${error instanceof Error ? error.message : String(error)}`,
198
+ severity: "warning"
199
+ });
200
+ return;
201
+ }
202
+ for (const entry of entries) {
203
+ if (resources.length >= MAX_SKILL_RESOURCES) break;
204
+ const fullPath = join(directory, entry);
205
+ if (pathKey(fullPath) === pathKey(entryPath)) continue;
206
+ let info;
207
+ try {
208
+ info = lstatSync(fullPath);
209
+ } catch {
210
+ continue;
211
+ }
212
+ if (info.isSymbolicLink()) {
213
+ issues.push({
214
+ code: "resource_symlink_ignored",
215
+ message: `Ignored symbolic-link resource: ${relative(rootPath, fullPath)}`,
216
+ severity: "warning"
217
+ });
218
+ continue;
219
+ }
220
+ if (info.isDirectory()) {
221
+ visit(fullPath, depth + 1);
222
+ continue;
223
+ }
224
+ if (!info.isFile()) continue;
225
+ let digest = createHash("sha256").update(`unavailable:${info.size}:${info.mtimeMs}`).digest("hex");
226
+ if (info.size > MAX_SKILL_RESOURCE_BYTES) {
227
+ issues.push({
228
+ code: "resource_too_large",
229
+ message: `Resource exceeds ${MAX_SKILL_RESOURCE_BYTES} bytes: ${relative(rootPath, fullPath)}`,
230
+ severity: "warning"
231
+ });
232
+ }
233
+ try {
234
+ const fd = openSync(fullPath, "r");
235
+ try {
236
+ const prefix = Buffer.alloc(Math.min(info.size, 8192));
237
+ const read = readSync(fd, prefix, 0, prefix.length, 0);
238
+ if (prefix.subarray(0, read).includes(0)) {
239
+ issues.push({
240
+ code: "binary_resource",
241
+ message: `Binary resource cannot be loaded into model context: ${relative(rootPath, fullPath)}`,
242
+ severity: "warning"
243
+ });
244
+ }
245
+ } finally {
246
+ closeSync(fd);
247
+ }
248
+ } catch (error) {
249
+ issues.push({
250
+ code: "resource_unreadable",
251
+ message: `Could not validate resource ${relative(rootPath, fullPath)}: ${error instanceof Error ? error.message : String(error)}`,
252
+ severity: "warning"
253
+ });
254
+ }
255
+ if (info.size <= MAX_SKILL_RESOURCE_BYTES) {
256
+ try {
257
+ digest = createHash("sha256").update(readFileSync(fullPath)).digest("hex");
258
+ } catch {
259
+ }
260
+ }
261
+ resources.push({
262
+ relativePath: relative(rootPath, fullPath).split(sep).join("/"),
263
+ byteSize: info.size,
264
+ mtimeMs: info.mtimeMs,
265
+ digest
266
+ });
267
+ }
268
+ };
269
+ visit(rootPath, 1);
270
+ if (resources.length >= MAX_SKILL_RESOURCES) {
271
+ issues.push({
272
+ code: "resource_limit",
273
+ message: `Only the first ${MAX_SKILL_RESOURCES} resources are available.`,
274
+ severity: "warning"
275
+ });
276
+ }
277
+ return { resources, issues };
278
+ }
279
+ function descriptorFromEntry(entryPath, root, directoryName, flat = false) {
280
+ const { frontmatter, headerText, issues: headerIssues } = readFrontmatterPrefix(entryPath);
281
+ const issues = [...headerIssues];
282
+ const declaredName = stringField(frontmatter, "name");
283
+ const name = declaredName ?? directoryName;
284
+ const description = stringField(frontmatter, "description") ?? "";
285
+ if (!SKILL_NAME_PATTERN.test(name) || name.length > 64) {
286
+ issues.push({
287
+ code: "invalid_name",
288
+ message: "Skill names must be 1-64 lowercase letters, numbers, or single hyphens.",
289
+ severity: "error"
290
+ });
291
+ }
292
+ if (!flat && declaredName !== directoryName) {
293
+ issues.push({
294
+ code: "name_directory_mismatch",
295
+ message: `Declared name "${declaredName ?? "(missing)"}" must match directory "${directoryName}".`,
296
+ severity: "error"
297
+ });
298
+ }
299
+ if (!declaredName && flat) {
300
+ issues.push({
301
+ code: "legacy_flat_skill",
302
+ message: "Flat .skill.md files are supported for compatibility; use <name>/SKILL.md.",
303
+ severity: "warning"
304
+ });
305
+ }
306
+ if (!description || description.length > 1024) {
307
+ issues.push({
308
+ code: "invalid_description",
309
+ message: "Skill description is required and must be at most 1024 characters.",
310
+ severity: "error"
311
+ });
312
+ }
313
+ const compatibility = stringField(frontmatter, "compatibility");
314
+ if (compatibility && compatibility.length > 500) {
315
+ issues.push({
316
+ code: "invalid_compatibility",
317
+ message: "Skill compatibility must be at most 500 characters.",
318
+ severity: "error"
319
+ });
320
+ }
321
+ const whenToUse = stringField(frontmatter, "when_to_use");
322
+ if (whenToUse && whenToUse.length > 1024) {
323
+ issues.push({
324
+ code: "invalid_when_to_use",
325
+ message: "Skill when_to_use must be at most 1024 characters.",
326
+ severity: "error"
327
+ });
328
+ }
329
+ const model = stringField(frontmatter, "model");
330
+ if (model) {
331
+ issues.push({
332
+ code: "unsupported_model_hint",
333
+ message: "The model field is retained for compatibility but does not change Book models.",
334
+ severity: "warning"
335
+ });
336
+ }
337
+ const requestedLifetime = stringField(frontmatter, "lifetime");
338
+ if (requestedLifetime && requestedLifetime !== "run" && requestedLifetime !== "turn") {
339
+ issues.push({
340
+ code: "invalid_lifetime",
341
+ message: "Skill lifetime must be run or turn; Book will use run.",
342
+ severity: "warning"
343
+ });
344
+ }
345
+ const lifetime = requestedLifetime === "turn" ? "turn" : "run";
346
+ for (const field of Object.keys(frontmatter)) {
347
+ if (!KNOWN_FRONTMATTER_FIELDS.has(field)) {
348
+ issues.push({
349
+ code: "unknown_frontmatter_field",
350
+ message: `Unknown frontmatter field: ${field}`,
351
+ severity: "warning"
352
+ });
353
+ }
354
+ }
355
+ let entryByteSize = 0;
356
+ let entryMtimeMs = 0;
357
+ try {
358
+ const entryInfo = statSync(entryPath);
359
+ entryByteSize = entryInfo.size;
360
+ entryMtimeMs = entryInfo.mtimeMs;
361
+ if (entryByteSize > MAX_SKILL_BODY_BYTES + MAX_SKILL_HEADER_BYTES) {
362
+ issues.push({
363
+ code: "body_too_large",
364
+ message: `SKILL.md exceeds the ${MAX_SKILL_BODY_BYTES}-byte body budget.`,
365
+ severity: "error"
366
+ });
367
+ }
368
+ } catch {
369
+ issues.push({
370
+ code: "skill_unreadable",
371
+ message: "Skill entrypoint is unreadable.",
372
+ severity: "error"
373
+ });
374
+ }
375
+ const rootPath = dirname(entryPath);
376
+ const resourceResult = flat ? { resources: [], issues: [] } : collectResources(rootPath, entryPath);
377
+ issues.push(...resourceResult.issues);
378
+ const metadata = metadataFromHeader(headerText);
379
+ const version = stringField(frontmatter, "version") ?? metadata.version ?? "unversioned";
380
+ const resourceDigest = createHash("sha256").update(JSON.stringify(resourceResult.resources)).digest("hex");
381
+ const descriptorDigest = createHash("sha256").update(
382
+ JSON.stringify({
383
+ name,
384
+ description,
385
+ whenToUse,
386
+ version,
387
+ source: root.source,
388
+ rootKind: root.kind,
389
+ path: pathKey(entryPath),
390
+ allowedTools: allowedToolsFrom(frontmatter),
391
+ lifetime,
392
+ resourceDigest
393
+ })
394
+ ).digest("hex");
395
+ return {
396
+ id: `${root.kind}:${pathKey(entryPath)}`,
397
+ version,
398
+ descriptorDigest,
399
+ resourceDigest,
400
+ name,
401
+ description,
402
+ whenToUse,
403
+ license: stringField(frontmatter, "license"),
404
+ compatibility,
405
+ metadata,
406
+ allowedTools: allowedToolsFrom(frontmatter),
407
+ model,
408
+ lifetime,
409
+ path: entryPath,
410
+ rootPath,
411
+ source: root.source,
412
+ rootKind: root.kind,
413
+ activation: String(frontmatter.disabled).toLowerCase() === "true" ? "off" : "manual",
414
+ execution: "inherit",
415
+ invocationCount: 0,
416
+ entryByteSize,
417
+ entryMtimeMs,
418
+ resources: resourceResult.resources,
419
+ issues,
420
+ valid: !issues.some((issue) => issue.severity === "error"),
421
+ shadowed: []
422
+ };
423
+ }
424
+ function skillsUnderRoot(root) {
425
+ if (!existsSync(root.path)) return [];
426
+ let entries;
427
+ try {
428
+ entries = readdirSync(root.path).sort();
429
+ } catch {
430
+ return [];
431
+ }
432
+ const skills = [];
433
+ for (const entry of entries) {
434
+ const fullPath = join(root.path, entry);
435
+ let info;
436
+ try {
437
+ info = lstatSync(fullPath);
438
+ } catch {
439
+ continue;
440
+ }
441
+ let directory = info.isDirectory();
442
+ if (info.isSymbolicLink()) {
443
+ try {
444
+ directory = statSync(fullPath).isDirectory();
445
+ } catch {
446
+ continue;
447
+ }
448
+ }
449
+ if (directory) {
450
+ const entryPath = join(fullPath, SKILL_ENTRY_FILENAME);
451
+ if (existsSync(entryPath) && statSync(entryPath).isFile()) {
452
+ skills.push(descriptorFromEntry(entryPath, root, entry));
453
+ }
454
+ } else if (info.isFile() && entry.endsWith(".skill.md")) {
455
+ skills.push(descriptorFromEntry(fullPath, root, entry.replace(/\.skill\.md$/, ""), true));
456
+ }
457
+ }
458
+ return skills;
459
+ }
460
+ function discoverSkills(workspace, overrides = {}, options = {}) {
461
+ const byName = /* @__PURE__ */ new Map();
462
+ for (const root of skillRoots(workspace, options)) {
463
+ for (const skill of skillsUnderRoot(root)) {
464
+ const previous = byName.get(skill.name);
465
+ if (previous) {
466
+ skill.shadowed = [
467
+ ...previous.shadowed,
468
+ { path: previous.path, source: previous.source, rootKind: previous.rootKind }
469
+ ];
470
+ }
471
+ byName.set(skill.name, skill);
472
+ }
473
+ }
474
+ return applySkillOverrides(
475
+ [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)),
476
+ overrides,
477
+ options.executionOverrides ?? {},
478
+ options.enabled ?? true
479
+ );
480
+ }
481
+ function applySkillOverrides(skills, overrides, executionOverrides = {}, enabled = true) {
482
+ return skills.map((skill) => ({
483
+ ...skill,
484
+ activation: enabled ? overrides[skill.name] ?? skill.activation ?? "manual" : "off",
485
+ execution: executionOverrides[skill.name] ?? skill.execution ?? "inherit"
486
+ }));
487
+ }
488
+ function loadSkillBody(skill) {
489
+ if (!skill.valid) throw new Error(`Skill "${skill.name}" is invalid.`);
490
+ const canonicalRoot = realpathSync(skill.rootPath);
491
+ const canonicalEntry = realpathSync(skill.path);
492
+ if (!isInside(canonicalRoot, canonicalEntry)) {
493
+ throw new Error(`Skill entrypoint escapes its root: ${skill.path}`);
494
+ }
495
+ const current = statSync(canonicalEntry);
496
+ if (current.size !== skill.entryByteSize || current.mtimeMs !== skill.entryMtimeMs) {
497
+ throw new Error(`Skill "${skill.name}" changed after discovery; reload skills before use.`);
498
+ }
499
+ const buffer = readFileSync(canonicalEntry);
500
+ if (buffer.includes(0)) {
501
+ throw new Error(`Skill "${skill.name}" contains binary content.`);
502
+ }
503
+ const raw = buffer.toString("utf8");
504
+ const { body } = parseFrontmatter(raw);
505
+ const byteSize = Buffer.byteLength(body);
506
+ if (byteSize > MAX_SKILL_BODY_BYTES) {
507
+ throw new Error(`Skill body exceeds ${MAX_SKILL_BODY_BYTES} bytes.`);
508
+ }
509
+ return {
510
+ body,
511
+ digest: createHash("sha256").update(body).digest("hex"),
512
+ byteSize
513
+ };
514
+ }
515
+ function skillListingBudgetChars(contextWindow) {
516
+ return Math.min(8e3, Math.max(512, Math.floor(contextWindow * 0.08)));
517
+ }
518
+ function buildSkillListing(skills, budgetChars = 8e3) {
519
+ const visible = skills.filter(
520
+ (skill) => skill.valid && (skill.activation === "auto" || skill.activation === "name-only")
521
+ );
522
+ if (visible.length === 0) {
523
+ return {
524
+ text: "",
525
+ budgetChars,
526
+ visibleCount: 0,
527
+ included: [],
528
+ omitted: [],
529
+ collapsed: [],
530
+ charCount: 0
531
+ };
532
+ }
533
+ const sorted = [...visible].sort(
534
+ (a, b) => b.invocationCount - a.invocationCount || a.name.localeCompare(b.name)
535
+ );
536
+ const lines = [
537
+ "## Available skills",
538
+ "Use InvokeSkill when a request matches a description. A user can force exact use with `$skill-name`."
539
+ ];
540
+ let remainingBudget = budgetChars - lines.join("\n").length;
541
+ const includedNames = [];
542
+ const collapsedNames = /* @__PURE__ */ new Set();
543
+ let included = 0;
544
+ for (const skill of sorted) {
545
+ if (skill.activation === "name-only") {
546
+ const bare = `- **${skill.name}**`;
547
+ if (bare.length > remainingBudget) break;
548
+ lines.push(bare);
549
+ remainingBudget -= bare.length + 1;
550
+ included++;
551
+ includedNames.push(skill.name);
552
+ collapsedNames.add(skill.name);
553
+ continue;
554
+ }
555
+ const entry = `- **${skill.name}**: ${skill.description}`;
556
+ const trigger = skill.whenToUse ? `
557
+ when: ${skill.whenToUse}` : "";
558
+ const full = `${entry}${trigger}`;
559
+ if (full.length > remainingBudget) {
560
+ const bare = `- **${skill.name}**`;
561
+ if (bare.length <= remainingBudget) {
562
+ lines.push(bare);
563
+ included++;
564
+ includedNames.push(skill.name);
565
+ collapsedNames.add(skill.name);
566
+ }
567
+ break;
568
+ }
569
+ lines.push(full);
570
+ remainingBudget -= full.length + 1;
571
+ included++;
572
+ includedNames.push(skill.name);
573
+ }
574
+ let omitted = sorted.length - included;
575
+ if (omitted > 0) {
576
+ let warning = `- ${omitted} additional skill${omitted === 1 ? " was" : "s were"} omitted from this prompt budget. Use /skills to inspect the full catalog.`;
577
+ while (warning.length > remainingBudget && lines.length > 2) {
578
+ const removed = lines.pop();
579
+ if (!removed) break;
580
+ remainingBudget += removed.length + 1;
581
+ included--;
582
+ const removedName = includedNames.pop();
583
+ if (removedName) collapsedNames.delete(removedName);
584
+ omitted = sorted.length - included;
585
+ warning = `- ${omitted} additional skill${omitted === 1 ? " was" : "s were"} omitted from this prompt budget. Use /skills to inspect the full catalog.`;
586
+ }
587
+ if (warning.length <= remainingBudget) lines.push(warning);
588
+ }
589
+ const text = lines.join("\n");
590
+ const includedSet = new Set(includedNames);
591
+ return {
592
+ text,
593
+ budgetChars,
594
+ visibleCount: sorted.length,
595
+ included: includedNames,
596
+ omitted: sorted.filter((skill) => !includedSet.has(skill.name)).map((skill) => skill.name),
597
+ collapsed: includedNames.filter((name) => collapsedNames.has(name)),
598
+ charCount: text.length
599
+ };
600
+ }
601
+ function generateSkillListing(skills, budgetChars = 8e3) {
602
+ return buildSkillListing(skills, budgetChars).text;
603
+ }
604
+ function explicitSkillMentions(prompt, skills) {
605
+ const available = new Set(skills.map((skill) => skill.name));
606
+ const mentions = [];
607
+ const pattern = /(?<![A-Za-z0-9_-])\$([a-z0-9]+(?:-[a-z0-9]+)*)(?![A-Za-z0-9_-])/g;
608
+ for (const match of prompt.matchAll(pattern)) {
609
+ const name = match[1];
610
+ if (available.has(name) && !mentions.includes(name)) mentions.push(name);
611
+ }
612
+ return mentions;
613
+ }
614
+
615
+ export {
616
+ MAX_SKILL_RESOURCE_BYTES,
617
+ skillRoots,
618
+ discoverSkills,
619
+ applySkillOverrides,
620
+ loadSkillBody,
621
+ skillListingBudgetChars,
622
+ buildSkillListing,
623
+ generateSkillListing,
624
+ explicitSkillMentions
625
+ };
626
+ //# sourceMappingURL=chunk-KWHQ2XXL.js.map