zidane 4.0.2 → 4.1.4

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 (77) hide show
  1. package/README.md +196 -614
  2. package/dist/agent-BoV5Twdl.d.ts +2347 -0
  3. package/dist/agent-BoV5Twdl.d.ts.map +1 -0
  4. package/dist/contexts-3Arvn7yR.js +321 -0
  5. package/dist/contexts-3Arvn7yR.js.map +1 -0
  6. package/dist/contexts.d.ts +2 -25
  7. package/dist/contexts.js +2 -10
  8. package/dist/errors-D1lhd6mX.js +118 -0
  9. package/dist/errors-D1lhd6mX.js.map +1 -0
  10. package/dist/index-28otmfLX.d.ts +400 -0
  11. package/dist/index-28otmfLX.d.ts.map +1 -0
  12. package/dist/index-BfSdALzk.d.ts +113 -0
  13. package/dist/index-BfSdALzk.d.ts.map +1 -0
  14. package/dist/index-DPsd0qwm.d.ts +254 -0
  15. package/dist/index-DPsd0qwm.d.ts.map +1 -0
  16. package/dist/index.d.ts +5 -95
  17. package/dist/index.js +141 -271
  18. package/dist/index.js.map +1 -0
  19. package/dist/interpolate-CukJwP2G.js +887 -0
  20. package/dist/interpolate-CukJwP2G.js.map +1 -0
  21. package/dist/mcp-8wClKY-3.js +771 -0
  22. package/dist/mcp-8wClKY-3.js.map +1 -0
  23. package/dist/mcp.d.ts +2 -4
  24. package/dist/mcp.js +2 -13
  25. package/dist/messages-z5Pq20p7.js +1020 -0
  26. package/dist/messages-z5Pq20p7.js.map +1 -0
  27. package/dist/presets-Cs7_CsMk.js +39 -0
  28. package/dist/presets-Cs7_CsMk.js.map +1 -0
  29. package/dist/presets.d.ts +2 -43
  30. package/dist/presets.js +2 -17
  31. package/dist/providers-CX-R-Oy-.js +969 -0
  32. package/dist/providers-CX-R-Oy-.js.map +1 -0
  33. package/dist/providers.d.ts +2 -4
  34. package/dist/providers.js +3 -23
  35. package/dist/session/sqlite.d.ts +7 -12
  36. package/dist/session/sqlite.d.ts.map +1 -0
  37. package/dist/session/sqlite.js +67 -79
  38. package/dist/session/sqlite.js.map +1 -0
  39. package/dist/session-Cn68UASv.js +440 -0
  40. package/dist/session-Cn68UASv.js.map +1 -0
  41. package/dist/session.d.ts +2 -4
  42. package/dist/session.js +3 -27
  43. package/dist/skills.d.ts +3 -322
  44. package/dist/skills.js +24 -47
  45. package/dist/skills.js.map +1 -0
  46. package/dist/stats-DoKUtF5T.js +58 -0
  47. package/dist/stats-DoKUtF5T.js.map +1 -0
  48. package/dist/tools-DpeWKzP1.js +3941 -0
  49. package/dist/tools-DpeWKzP1.js.map +1 -0
  50. package/dist/tools.d.ts +3 -95
  51. package/dist/tools.js +2 -40
  52. package/dist/tui.d.ts +533 -0
  53. package/dist/tui.d.ts.map +1 -0
  54. package/dist/tui.js +2004 -0
  55. package/dist/tui.js.map +1 -0
  56. package/dist/types-Bx_F8jet.js +39 -0
  57. package/dist/types-Bx_F8jet.js.map +1 -0
  58. package/dist/types.d.ts +4 -55
  59. package/dist/types.js +4 -28
  60. package/package.json +38 -4
  61. package/dist/agent-BAHrGtqu.d.ts +0 -2425
  62. package/dist/chunk-4ILGBQ23.js +0 -803
  63. package/dist/chunk-4LPBN547.js +0 -3540
  64. package/dist/chunk-64LLNY7F.js +0 -28
  65. package/dist/chunk-6STZTA4N.js +0 -830
  66. package/dist/chunk-7GQ7P6DM.js +0 -566
  67. package/dist/chunk-IC7FT4OD.js +0 -37
  68. package/dist/chunk-JCOB6IYO.js +0 -22
  69. package/dist/chunk-JH6IAAFA.js +0 -28
  70. package/dist/chunk-LNN5UTS2.js +0 -97
  71. package/dist/chunk-PMCQOMV4.js +0 -490
  72. package/dist/chunk-UD25QF3H.js +0 -304
  73. package/dist/chunk-W57VY6DJ.js +0 -834
  74. package/dist/sandbox-D7v6Wy62.d.ts +0 -28
  75. package/dist/skills-use-DwZrNmcw.d.ts +0 -80
  76. package/dist/types-Bai5rKpa.d.ts +0 -89
  77. package/dist/validation-Pm--dQEU.d.ts +0 -185
@@ -0,0 +1,887 @@
1
+ import { i as AgentToolNotAllowedError } from "./errors-D1lhd6mX.js";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { homedir, tmpdir } from "node:os";
5
+ //#region src/skills/activation.ts
6
+ function createSkillActivationState(options = {}) {
7
+ const byName = /* @__PURE__ */ new Map();
8
+ const maxActive = typeof options.maxActive === "number" && options.maxActive > 0 ? options.maxActive : void 0;
9
+ return {
10
+ active() {
11
+ return [...byName.values()];
12
+ },
13
+ isActive(name) {
14
+ return byName.has(name);
15
+ },
16
+ get(name) {
17
+ return byName.get(name);
18
+ },
19
+ activate(skill, via) {
20
+ if (byName.has(skill.name)) return "already-active";
21
+ if (maxActive !== void 0 && byName.size >= maxActive) return "cap-reached";
22
+ byName.set(skill.name, {
23
+ skill,
24
+ activatedAt: Date.now(),
25
+ activatedVia: via
26
+ });
27
+ return "ok";
28
+ },
29
+ deactivate(name) {
30
+ const existing = byName.get(name);
31
+ if (!existing) return void 0;
32
+ byName.delete(name);
33
+ return existing;
34
+ },
35
+ clear() {
36
+ const snapshot = [...byName.values()];
37
+ byName.clear();
38
+ return snapshot;
39
+ }
40
+ };
41
+ }
42
+ //#endregion
43
+ //#region src/skills/validate.ts
44
+ const NAME_MAX = 64;
45
+ const DESCRIPTION_MAX = 1024;
46
+ const COMPATIBILITY_MAX = 500;
47
+ const SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
48
+ const CONSECUTIVE_HYPHENS_RE = /--/;
49
+ const ALLOWED_TOOL_PATTERN_RE = /^([\w-]+)(?:\(([^)]*)\))?$/;
50
+ const ABS_WINDOWS_PATH_RE = /^[a-z]:[\\/]/i;
51
+ const PATH_SEPARATOR_RE = /[\\/]/;
52
+ const TRAILING_SLASHES_RE = /\/+$/;
53
+ /**
54
+ * Validate a skill name per the spec:
55
+ * - 1–64 characters
56
+ * - Lowercase alphanumeric + hyphens only
57
+ * - Must not start or end with a hyphen
58
+ * - Must not contain consecutive hyphens
59
+ *
60
+ * The parent-directory match is validated separately (it requires knowing the
61
+ * skill's `location`, which this function does not).
62
+ */
63
+ function validateSkillName(name) {
64
+ if (typeof name !== "string") return false;
65
+ if (name.length < 1 || name.length > NAME_MAX) return false;
66
+ if (CONSECUTIVE_HYPHENS_RE.test(name)) return false;
67
+ return SKILL_NAME_RE.test(name);
68
+ }
69
+ /**
70
+ * Strict validation for a skill that is about to be authored / written to disk.
71
+ * Rejects anything that would violate the spec. Use the lenient parser path
72
+ * (`parseSkillFile`) for loading third-party skills.
73
+ */
74
+ function validateSkillForWrite(skill) {
75
+ const errors = [];
76
+ if (!validateSkillName(skill.name)) errors.push({
77
+ code: "invalid-name",
78
+ message: `Skill name "${skill.name}" must be 1-64 chars, lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens.`,
79
+ field: "name"
80
+ });
81
+ if (skill.location) {
82
+ const dirName = basename(skill.baseDir ?? "");
83
+ if (dirName && dirName !== skill.name) errors.push({
84
+ code: "name-mismatch-directory",
85
+ message: `Skill name "${skill.name}" must match parent directory name "${dirName}".`,
86
+ field: "name"
87
+ });
88
+ }
89
+ if (typeof skill.description !== "string" || skill.description.length < 1) errors.push({
90
+ code: "missing-description",
91
+ message: "Skill description is required (non-empty).",
92
+ field: "description"
93
+ });
94
+ else if (skill.description.length > DESCRIPTION_MAX) errors.push({
95
+ code: "description-too-long",
96
+ message: `Skill description must be at most ${DESCRIPTION_MAX} characters (got ${skill.description.length}).`,
97
+ field: "description"
98
+ });
99
+ if (skill.compatibility !== void 0) {
100
+ if (typeof skill.compatibility !== "string" || skill.compatibility.length === 0) errors.push({
101
+ code: "invalid-compatibility",
102
+ message: "Compatibility must be a non-empty string when provided.",
103
+ field: "compatibility"
104
+ });
105
+ else if (skill.compatibility.length > COMPATIBILITY_MAX) errors.push({
106
+ code: "compatibility-too-long",
107
+ message: `Compatibility must be at most ${COMPATIBILITY_MAX} characters (got ${skill.compatibility.length}).`,
108
+ field: "compatibility"
109
+ });
110
+ }
111
+ if (skill.metadata) {
112
+ for (const [key, value] of Object.entries(skill.metadata)) if (typeof value !== "string") errors.push({
113
+ code: "invalid-metadata-value",
114
+ message: `Metadata value for "${key}" must be a string (spec: "A map from string keys to string values").`,
115
+ field: "metadata"
116
+ });
117
+ }
118
+ if (skill.allowedTools) {
119
+ for (const pattern of skill.allowedTools) if (!ALLOWED_TOOL_PATTERN_RE.test(pattern)) errors.push({
120
+ code: "invalid-allowed-tool-pattern",
121
+ message: `Allowed-tools entry "${pattern}" is not a recognized pattern (expected "ToolName" or "ToolName(arg:*)").`,
122
+ field: "allowed-tools"
123
+ });
124
+ }
125
+ return {
126
+ valid: errors.length === 0,
127
+ errors
128
+ };
129
+ }
130
+ /**
131
+ * Validate that `relPath` stays inside `baseDir` when resolved.
132
+ *
133
+ * Rejects:
134
+ * - Absolute paths (`/etc/passwd`, `C:\…`)
135
+ * - Parent traversal (`..` segments that escape baseDir)
136
+ * - Null-byte tricks
137
+ *
138
+ * Returns `{ valid: true, absolutePath }` on success or `{ valid: false, error }`
139
+ * with a human-readable reason.
140
+ */
141
+ function validateResourcePath(relPath, baseDir) {
142
+ if (typeof relPath !== "string" || relPath.length === 0) return {
143
+ valid: false,
144
+ error: "Resource path must be a non-empty string."
145
+ };
146
+ if (relPath.includes("\0")) return {
147
+ valid: false,
148
+ error: "Resource path contains a null byte."
149
+ };
150
+ if (relPath.startsWith("/") || ABS_WINDOWS_PATH_RE.test(relPath)) return {
151
+ valid: false,
152
+ error: `Absolute paths are not allowed ("${relPath}").`
153
+ };
154
+ const segments = [];
155
+ for (const segment of relPath.split(PATH_SEPARATOR_RE)) {
156
+ if (segment === "" || segment === ".") continue;
157
+ if (segment === "..") {
158
+ if (segments.length === 0) return {
159
+ valid: false,
160
+ error: `Resource path "${relPath}" escapes the skill directory.`
161
+ };
162
+ segments.pop();
163
+ continue;
164
+ }
165
+ segments.push(segment);
166
+ }
167
+ if (segments.length === 0) return {
168
+ valid: false,
169
+ error: "Resource path resolves to the skill root itself."
170
+ };
171
+ return {
172
+ valid: true,
173
+ absolutePath: `${baseDir.replace(TRAILING_SLASHES_RE, "")}/${segments.join("/")}`
174
+ };
175
+ }
176
+ /**
177
+ * Parse a single `allowed-tools` entry into its tool name + optional argument pattern.
178
+ *
179
+ * Examples:
180
+ * - `Read` → `{ tool: 'Read' }`
181
+ * - `Bash(git:*)` → `{ tool: 'Bash', argPrefix: 'git' }`
182
+ */
183
+ function parseAllowedToolPattern(entry) {
184
+ const m = entry.trim().match(ALLOWED_TOOL_PATTERN_RE);
185
+ if (!m) return null;
186
+ const tool = m[1];
187
+ const arg = m[2];
188
+ if (!arg) return { tool };
189
+ if (arg.endsWith(":*")) return {
190
+ tool,
191
+ argPrefix: arg.slice(0, -2)
192
+ };
193
+ return {
194
+ tool,
195
+ argPrefix: arg
196
+ };
197
+ }
198
+ /**
199
+ * Check whether a tool call (identified by its wire/displayName and argument input)
200
+ * matches a skill's allow-list entry.
201
+ *
202
+ * Matching rules (Zidane's interpretation of the spec's unspecified syntax):
203
+ * - Exact match: displayName === pattern (no parens).
204
+ * - Prefix match: for `Tool(arg:*)`, displayName === 'Tool' AND **any** string
205
+ * value in the input object starts with `arg`. This is intentionally
206
+ * permissive: it doesn't depend on a convention about which property carries
207
+ * the "command" (schemas vary across tools), and for the common
208
+ * `shell({ command: 'git …' })` shape it behaves identically to a "primary
209
+ * string" rule.
210
+ * - For tools whose inputs carry no string values, the arg-match returns false.
211
+ */
212
+ function matchesAllowedTool(displayName, input, pattern) {
213
+ const parsed = parseAllowedToolPattern(pattern);
214
+ if (!parsed) return false;
215
+ if (parsed.tool !== displayName) return false;
216
+ if (parsed.argPrefix === void 0) return true;
217
+ for (const value of Object.values(input)) if (typeof value === "string" && value.startsWith(parsed.argPrefix)) return true;
218
+ return false;
219
+ }
220
+ /**
221
+ * Test whether a tool call is allowed by the union of `allowedTools` across a set
222
+ * of active skills. Returns `true` when the union is empty (permissive default)
223
+ * OR when any entry matches.
224
+ */
225
+ function isToolAllowedByUnion(displayName, input, union) {
226
+ if (union.length === 0) return true;
227
+ return union.some((pattern) => matchesAllowedTool(displayName, input, pattern));
228
+ }
229
+ //#endregion
230
+ //#region src/skills/allowed-tools.ts
231
+ /** Tools that are always allowed regardless of the active skills' allow-list. */
232
+ const IMPLICITLY_ALLOWED_SKILL_TOOLS = [
233
+ "skills_use",
234
+ "skills_read",
235
+ "skills_run_script"
236
+ ];
237
+ /**
238
+ * Register `tool:gate` / `mcp:tool:gate` handlers that enforce the union of
239
+ * `allowedTools` across active skills.
240
+ *
241
+ * No-op when no active skill declares an allow-list (permissive default —
242
+ * matches the spec's "experimental" note for `allowed-tools`).
243
+ *
244
+ * Returns an `uninstall` fn. The agent calls this at run end to detach the
245
+ * handlers and prevent cross-run hook leaks.
246
+ */
247
+ function installAllowedToolsGate(hooks, state) {
248
+ function effectiveUnion() {
249
+ const active = state.active();
250
+ const declared = [];
251
+ for (const record of active) if (record.skill.allowedTools?.length) declared.push(...record.skill.allowedTools);
252
+ return {
253
+ union: declared.length > 0 ? [...declared, ...IMPLICITLY_ALLOWED_SKILL_TOOLS] : [],
254
+ active: active.map((a) => a.skill.name)
255
+ };
256
+ }
257
+ function gateHandler(ctx) {
258
+ const { union, active } = effectiveUnion();
259
+ if (union.length === 0) return;
260
+ if (isToolAllowedByUnion(ctx.displayName, ctx.input, union)) return;
261
+ const err = new AgentToolNotAllowedError({
262
+ toolName: ctx.name,
263
+ displayName: ctx.displayName,
264
+ allowedUnion: union,
265
+ activeSkills: active
266
+ });
267
+ ctx.block = true;
268
+ ctx.reason = err.message;
269
+ }
270
+ function mcpGateHandler(ctx) {
271
+ const { union, active } = effectiveUnion();
272
+ if (union.length === 0) return;
273
+ if (isToolAllowedByUnion(ctx.displayName, ctx.input, union)) return;
274
+ const err = new AgentToolNotAllowedError({
275
+ toolName: `mcp_${ctx.server}_${ctx.tool}`,
276
+ displayName: ctx.displayName,
277
+ allowedUnion: union,
278
+ activeSkills: active
279
+ });
280
+ ctx.block = true;
281
+ ctx.reason = err.message;
282
+ }
283
+ const unregisterTool = hooks.hook("tool:gate", gateHandler);
284
+ const unregisterMcp = hooks.hook("mcp:tool:gate", mcpGateHandler);
285
+ return function uninstall() {
286
+ unregisterTool();
287
+ unregisterMcp();
288
+ };
289
+ }
290
+ //#endregion
291
+ //#region src/xml.ts
292
+ /**
293
+ * Minimal XML helpers used by catalog/result builders that emit
294
+ * model-facing pseudo-XML (skills catalog, searchable_tools, tool_search
295
+ * results, skill_content wrappers).
296
+ *
297
+ * Scope: attribute / text-node escaping only — we do NOT emit a full XML
298
+ * document, the model sees these strings as free-form text. Centralised
299
+ * here so the four near-identical copies in `agent.ts`, `skills/catalog.ts`,
300
+ * `tools/skills-use.ts`, and `tools/tool-search.ts` don't drift.
301
+ */
302
+ const RE_AMP = /&/g;
303
+ const RE_LT = /</g;
304
+ const RE_GT = />/g;
305
+ const RE_QUOT = /"/g;
306
+ function escapeXml(str) {
307
+ return str.replace(RE_AMP, "&amp;").replace(RE_LT, "&lt;").replace(RE_GT, "&gt;").replace(RE_QUOT, "&quot;");
308
+ }
309
+ //#endregion
310
+ //#region src/skills/catalog.ts
311
+ /**
312
+ * Build the skill catalog XML and behavioral instructions for the system prompt.
313
+ */
314
+ function buildCatalog(skills, options = {}) {
315
+ if (skills.length === 0) return "";
316
+ const skillsToolRegistered = options.skillsToolRegistered ?? true;
317
+ const readToolName = options.readToolName ?? "read_file";
318
+ const entries = skills.map((skill) => {
319
+ const locationLine = skill.location ? `\n <location>${escapeXml(skill.location)}</location>` : "";
320
+ return ` <skill>
321
+ <name>${escapeXml(skill.name)}</name>
322
+ <description>${escapeXml(skill.description)}</description>${locationLine}
323
+ </skill>`;
324
+ }).join("\n");
325
+ const hasFsSkills = skills.some((s) => s.location);
326
+ const hasInlineSkills = skills.some((s) => !s.location);
327
+ const behavioralParts = [];
328
+ behavioralParts.push("The following skills provide specialized instructions for specific tasks.", "When a task matches a skill's description, activate the skill to load its full instructions before proceeding.");
329
+ if (skillsToolRegistered) behavioralParts.push("To activate a skill, call the `skills_use` tool with the skill's name. The response contains the full instructions and any bundled resources you can then load via `skills_read` (reference files) or execute via `skills_run_script` (scripts/ directory).", "Relative paths referenced in a skill's instructions resolve against the skill directory noted in the `skills_use` response.");
330
+ else if (hasFsSkills) behavioralParts.push(`For skills with a <location>, use the ${readToolName} tool to read the SKILL.md file at that path.`, "When a skill references relative paths, resolve them against the skill's directory (the parent of SKILL.md) and use absolute paths in tool calls.");
331
+ if (hasInlineSkills && !skillsToolRegistered) behavioralParts.push("Skills without a <location> have their instructions included directly in <instructions> tags below.");
332
+ const parts = [
333
+ behavioralParts.join("\n"),
334
+ "",
335
+ "<available_skills>",
336
+ entries,
337
+ "</available_skills>"
338
+ ];
339
+ if (hasInlineSkills && !skillsToolRegistered) {
340
+ parts.push("");
341
+ for (const skill of skills) if (!skill.location && skill.instructions) {
342
+ parts.push(`<skill_instructions name="${escapeXml(skill.name)}">`);
343
+ parts.push(skill.instructions);
344
+ if (skill.resources && skill.resources.length > 0) {
345
+ parts.push("");
346
+ parts.push("<skill_resources>");
347
+ for (const res of skill.resources) parts.push(` <file type="${res.type}">${escapeXml(res.path)}</file>`);
348
+ parts.push("</skill_resources>");
349
+ }
350
+ parts.push("</skill_instructions>");
351
+ }
352
+ }
353
+ return parts.join("\n");
354
+ }
355
+ //#endregion
356
+ //#region src/skills/discovery.ts
357
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
358
+ const INDENT_RE = /^[ \t]{2,}/;
359
+ const KV_RE = /^([^:]+):(.*)$/;
360
+ const DOUBLE_QUOTED_RE = /^"((?:\\.|[^"\\])*)"$/;
361
+ const SINGLE_QUOTED_RE = /^'((?:''|[^'])*)'$/;
362
+ const DQ_ESCAPE_RE = /\\(["\\/bfnrt])/g;
363
+ const WHITESPACE_SPLIT_RE = /\s+/;
364
+ const PARAGRAPH_SPLIT_RE = /\n\n/;
365
+ const COMMA_OR_SPACE_RE = /[,\s]+/;
366
+ /**
367
+ * Parse a SKILL.md file into frontmatter + body.
368
+ *
369
+ * Uses a simple regex-based YAML extractor that handles:
370
+ * - Flat key: value pairs
371
+ * - Quoted values
372
+ * - One-level nested maps (for `metadata:`)
373
+ * - Lenient recovery from unquoted-colon values (e.g.
374
+ * `description: Use when: the user asks …`) via a quote-wrap retry.
375
+ */
376
+ function parseFrontmatter(content) {
377
+ const diagnostics = [];
378
+ const match = content.match(FRONTMATTER_RE);
379
+ if (!match) return {
380
+ frontmatter: {},
381
+ body: content.trim(),
382
+ diagnostics
383
+ };
384
+ const yamlBlock = match[1];
385
+ const body = match[2].trim();
386
+ const frontmatter = {};
387
+ let currentKey = null;
388
+ let currentMap = null;
389
+ for (const line of yamlBlock.split("\n")) {
390
+ if (!line.trim() || line.trim().startsWith("#")) continue;
391
+ if (currentKey && currentMap && INDENT_RE.test(line)) {
392
+ const nestedMatch = line.trim().match(KV_RE);
393
+ if (nestedMatch) {
394
+ const val = nestedMatch[2].trim();
395
+ currentMap[nestedMatch[1].trim()] = unquoteYaml(val);
396
+ }
397
+ continue;
398
+ }
399
+ if (currentKey && currentMap) {
400
+ frontmatter[currentKey] = currentMap;
401
+ currentKey = null;
402
+ currentMap = null;
403
+ }
404
+ const kvMatch = matchFirstColon(line);
405
+ if (!kvMatch) continue;
406
+ const key = kvMatch.key.trim();
407
+ const rawValue = kvMatch.value.trim();
408
+ if (!rawValue) {
409
+ currentKey = key;
410
+ currentMap = {};
411
+ } else frontmatter[key] = unquoteYaml(rawValue);
412
+ }
413
+ if (currentKey && currentMap) frontmatter[currentKey] = currentMap;
414
+ return {
415
+ frontmatter,
416
+ body,
417
+ diagnostics
418
+ };
419
+ }
420
+ function matchFirstColon(line) {
421
+ const idx = line.indexOf(":");
422
+ if (idx < 0) return null;
423
+ const key = line.slice(0, idx);
424
+ const value = line.slice(idx + 1);
425
+ if (!KV_RE.test(`${key}:`)) return null;
426
+ return {
427
+ key,
428
+ value
429
+ };
430
+ }
431
+ /**
432
+ * Strip outer quotes and decode YAML-style escapes.
433
+ *
434
+ * - Double-quoted values honor `\"`, `\\`, `\n`, `\r`, `\t`, `\b`, `\f`, `\/`.
435
+ * - Single-quoted values honor `''` (the YAML single-quote escape for a literal `'`).
436
+ * - Trailing inline comments on unquoted values (` # comment`) are dropped.
437
+ * - Unquoted values are returned as-is (trimmed by caller).
438
+ *
439
+ * Skill frontmatter in the wild uses a narrow YAML subset — this is
440
+ * sufficient for every field in the current spec and the common authoring
441
+ * tools. Block scalars (`|`, `>`) and flow sequences are intentionally left
442
+ * unsupported: they'd only appear in a bespoke workflow, and silently
443
+ * half-supporting them is worse than rejecting them obviously.
444
+ */
445
+ function unquoteYaml(val) {
446
+ const dq = val.match(DOUBLE_QUOTED_RE);
447
+ if (dq) return dq[1].replace(DQ_ESCAPE_RE, (_, ch) => {
448
+ switch (ch) {
449
+ case "\"": return "\"";
450
+ case "\\": return "\\";
451
+ case "/": return "/";
452
+ case "b": return "\b";
453
+ case "f": return "\f";
454
+ case "n": return "\n";
455
+ case "r": return "\r";
456
+ case "t": return " ";
457
+ default: return ch;
458
+ }
459
+ });
460
+ const sq = val.match(SINGLE_QUOTED_RE);
461
+ if (sq) return sq[1].replace(/''/g, "'");
462
+ const hashIdx = val.indexOf(" #");
463
+ if (hashIdx >= 0) return val.slice(0, hashIdx).trimEnd();
464
+ return val;
465
+ }
466
+ /**
467
+ * Narrow a frontmatter field to a string. Produces a diagnostic when present
468
+ * but not a string, so malformed YAML (e.g. `name: 123`) surfaces as a
469
+ * warning instead of silently coerced downstream via `as string`.
470
+ */
471
+ function takeString(frontmatter, key, diagnostics) {
472
+ const raw = frontmatter[key];
473
+ if (raw === void 0 || raw === null) return void 0;
474
+ if (typeof raw === "string") return raw;
475
+ diagnostics.push({
476
+ severity: "warning",
477
+ code: "invalid-field-type",
478
+ message: `Frontmatter field "${key}" expected string, got ${typeof raw}. Coerced.`,
479
+ field: key
480
+ });
481
+ return String(raw);
482
+ }
483
+ const RESOURCE_DIRS = {
484
+ scripts: "script",
485
+ references: "reference",
486
+ assets: "asset"
487
+ };
488
+ function enumerateResources(baseDir) {
489
+ const resources = [];
490
+ for (const [dir, type] of Object.entries(RESOURCE_DIRS)) {
491
+ const dirPath = join(baseDir, dir);
492
+ if (!existsSync(dirPath) || !statSync(dirPath).isDirectory()) continue;
493
+ try {
494
+ const files = readdirSync(dirPath, { recursive: true });
495
+ for (const file of files) {
496
+ const rel = typeof file === "string" ? file : file.toString("utf-8");
497
+ if (statSync(join(dirPath, rel)).isFile()) resources.push({
498
+ path: join(dir, rel),
499
+ type
500
+ });
501
+ }
502
+ } catch {}
503
+ }
504
+ try {
505
+ for (const entry of readdirSync(baseDir)) {
506
+ if (entry === "SKILL.md") continue;
507
+ if (statSync(join(baseDir, entry)).isFile()) resources.push({
508
+ path: entry,
509
+ type: "other"
510
+ });
511
+ }
512
+ } catch {}
513
+ return resources;
514
+ }
515
+ /**
516
+ * Parse a SKILL.md file into a SkillConfig (lenient).
517
+ *
518
+ * Returns `null` only when the skill is fundamentally unusable:
519
+ * - The file is missing
520
+ * - The description is absent (required by spec for disclosure)
521
+ *
522
+ * All other issues are surfaced as `SkillConfig.diagnostics` with severity
523
+ * `warning`. Deprecated top-level fields (`paths`, `model`, `thinking`) are
524
+ * auto-migrated into `metadata['zidane.*']`.
525
+ */
526
+ async function parseSkillFile(filePath, options = {}) {
527
+ const absPath = resolve(filePath);
528
+ if (!existsSync(absPath)) return null;
529
+ const { frontmatter, body, diagnostics } = parseFrontmatter(readFileSync(absPath, "utf-8"));
530
+ let description = takeString(frontmatter, "description", diagnostics);
531
+ if (!description && body) {
532
+ const firstParagraph = body.split(PARAGRAPH_SPLIT_RE)[0]?.trim();
533
+ if (firstParagraph) description = firstParagraph;
534
+ }
535
+ if (!description) return null;
536
+ if (description.length > 1024) diagnostics.push({
537
+ severity: "warning",
538
+ code: "description-too-long",
539
+ message: `Description exceeds spec limit of 1024 characters (got ${description.length}). Loading anyway.`,
540
+ field: "description"
541
+ });
542
+ const baseDir = dirname(absPath);
543
+ const dirName = basename(baseDir);
544
+ const frontmatterName = takeString(frontmatter, "name", diagnostics);
545
+ const name = frontmatterName || dirName;
546
+ if (frontmatterName && frontmatterName !== dirName) diagnostics.push({
547
+ severity: "warning",
548
+ code: "name-mismatch-directory",
549
+ message: `Skill name "${frontmatterName}" does not match parent directory "${dirName}". Loading anyway.`,
550
+ field: "name"
551
+ });
552
+ if (name.length > 64) diagnostics.push({
553
+ severity: "warning",
554
+ code: "name-too-long",
555
+ message: `Skill name "${name}" exceeds spec limit of 64 characters. Loading anyway.`,
556
+ field: "name"
557
+ });
558
+ if (!validateSkillName(name)) diagnostics.push({
559
+ severity: "warning",
560
+ code: "invalid-name-format",
561
+ message: `Skill name "${name}" does not match spec format (lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens). Loading anyway.`,
562
+ field: "name"
563
+ });
564
+ const config = {
565
+ name,
566
+ description,
567
+ instructions: body,
568
+ source: options.source ?? "project",
569
+ location: absPath,
570
+ baseDir,
571
+ resources: enumerateResources(baseDir)
572
+ };
573
+ const license = takeString(frontmatter, "license", diagnostics);
574
+ if (license) config.license = license;
575
+ const compatibility = takeString(frontmatter, "compatibility", diagnostics);
576
+ if (compatibility) {
577
+ if (compatibility.length > 500) diagnostics.push({
578
+ severity: "warning",
579
+ code: "compatibility-too-long",
580
+ message: `Compatibility exceeds spec limit of 500 characters (got ${compatibility.length}). Loading anyway.`,
581
+ field: "compatibility"
582
+ });
583
+ config.compatibility = compatibility;
584
+ }
585
+ const metadata = {};
586
+ const rawMetadata = frontmatter.metadata;
587
+ if (rawMetadata && typeof rawMetadata === "object" && !Array.isArray(rawMetadata)) for (const [k, v] of Object.entries(rawMetadata)) {
588
+ if (typeof v !== "string") {
589
+ diagnostics.push({
590
+ severity: "warning",
591
+ code: "invalid-metadata-value",
592
+ message: `Metadata value for "${k}" is not a string; coerced. (Spec requires string values.)`,
593
+ field: "metadata"
594
+ });
595
+ metadata[k] = String(v);
596
+ continue;
597
+ }
598
+ metadata[k] = v;
599
+ }
600
+ else if (rawMetadata !== void 0) diagnostics.push({
601
+ severity: "warning",
602
+ code: "invalid-metadata-shape",
603
+ message: `Frontmatter "metadata" expected a map, got ${Array.isArray(rawMetadata) ? "array" : typeof rawMetadata}. Ignored.`,
604
+ field: "metadata"
605
+ });
606
+ const pathsField = takeString(frontmatter, "paths", diagnostics);
607
+ if (pathsField) {
608
+ metadata["zidane.paths"] = pathsField.split(COMMA_OR_SPACE_RE).filter(Boolean).join(",");
609
+ diagnostics.push({
610
+ severity: "warning",
611
+ code: "deprecated-top-level-field",
612
+ message: "`paths` is not a spec field and is deprecated — moved to `metadata[\"zidane.paths\"]`.",
613
+ field: "paths"
614
+ });
615
+ }
616
+ const modelField = takeString(frontmatter, "model", diagnostics);
617
+ if (modelField) {
618
+ metadata["zidane.model"] = modelField;
619
+ diagnostics.push({
620
+ severity: "warning",
621
+ code: "deprecated-top-level-field",
622
+ message: "`model` is not a spec field and is deprecated — moved to `metadata[\"zidane.model\"]`.",
623
+ field: "model"
624
+ });
625
+ }
626
+ const thinkingField = takeString(frontmatter, "thinking", diagnostics);
627
+ const effortField = thinkingField ? void 0 : takeString(frontmatter, "effort", diagnostics);
628
+ const legacyThinking = thinkingField ?? effortField;
629
+ if (legacyThinking) {
630
+ metadata["zidane.thinking"] = legacyThinking;
631
+ diagnostics.push({
632
+ severity: "warning",
633
+ code: "deprecated-top-level-field",
634
+ message: `\`${thinkingField ? "thinking" : "effort"}\` is not a spec field and is deprecated — moved to \`metadata["zidane.thinking"]\`.`,
635
+ field: thinkingField ? "thinking" : "effort"
636
+ });
637
+ }
638
+ if (Object.keys(metadata).length > 0) config.metadata = metadata;
639
+ const allowedTools = takeString(frontmatter, "allowed-tools", diagnostics);
640
+ if (allowedTools) config.allowedTools = allowedTools.split(WHITESPACE_SPLIT_RE).filter(Boolean);
641
+ if (diagnostics.length > 0) config.diagnostics = diagnostics;
642
+ return config;
643
+ }
644
+ const SKIP_DIRS = new Set([
645
+ ".git",
646
+ "node_modules",
647
+ ".DS_Store",
648
+ "dist",
649
+ "build"
650
+ ]);
651
+ function findSkillDirs(root, maxDepth = 4, _depth = 0) {
652
+ if (_depth > maxDepth) return [];
653
+ if (!existsSync(root) || !statSync(root).isDirectory()) return [];
654
+ const results = [];
655
+ try {
656
+ for (const entry of readdirSync(root)) {
657
+ if (SKIP_DIRS.has(entry)) continue;
658
+ const entryPath = join(root, entry);
659
+ if (!statSync(entryPath).isDirectory()) continue;
660
+ const skillFile = join(entryPath, "SKILL.md");
661
+ if (existsSync(skillFile) && statSync(skillFile).isFile()) results.push(skillFile);
662
+ else results.push(...findSkillDirs(entryPath, maxDepth, _depth + 1));
663
+ }
664
+ } catch {}
665
+ return results;
666
+ }
667
+ /**
668
+ * Return the default scan paths tagged by source. Project-scope paths come
669
+ * first; their skills therefore win on name collisions against user-scope
670
+ * skills (first-found wins in discovery).
671
+ */
672
+ function getDefaultScanPaths() {
673
+ const home = homedir();
674
+ const cwd = process.cwd();
675
+ return [
676
+ {
677
+ path: join(cwd, ".agents", "skills"),
678
+ source: "project"
679
+ },
680
+ {
681
+ path: join(cwd, ".zidane", "skills"),
682
+ source: "project"
683
+ },
684
+ {
685
+ path: join(home, ".agents", "skills"),
686
+ source: "user"
687
+ },
688
+ {
689
+ path: join(home, ".zidane", "skills"),
690
+ source: "user"
691
+ }
692
+ ];
693
+ }
694
+ /**
695
+ * Infer a source tag for a user-provided scan path.
696
+ * Paths under `$HOME` are treated as 'user'; everything else as 'project'.
697
+ */
698
+ function inferSource(path) {
699
+ return path.startsWith(homedir()) ? "user" : "project";
700
+ }
701
+ /**
702
+ * Discover skills from sourced filesystem paths.
703
+ * Each path is scanned for subdirectories containing SKILL.md.
704
+ * Earlier paths have higher priority (first-found wins on name collision).
705
+ */
706
+ async function discoverSkills(paths) {
707
+ const skillsByName = /* @__PURE__ */ new Map();
708
+ for (const { path: scanPath, source } of paths) {
709
+ const skillFiles = findSkillDirs(resolve(scanPath));
710
+ for (const file of skillFiles) {
711
+ const skill = await parseSkillFile(file, { source });
712
+ if (!skill) continue;
713
+ if (skillsByName.has(skill.name)) {
714
+ const existing = skillsByName.get(skill.name);
715
+ const diag = {
716
+ severity: "warning",
717
+ code: "name-collision-shadowed",
718
+ message: `A skill with name "${skill.name}" was also found at ${file} (source: ${source}); shadowed by ${existing.location} (source: ${existing.source}).`
719
+ };
720
+ existing.diagnostics = [...existing.diagnostics ?? [], diag];
721
+ continue;
722
+ }
723
+ skillsByName.set(skill.name, skill);
724
+ }
725
+ }
726
+ return [...skillsByName.values()];
727
+ }
728
+ //#endregion
729
+ //#region src/skills/writer.ts
730
+ const YAML_RESERVED_RE = /[:#&*!|>%@`]/;
731
+ const YAML_EDGE_OR_QUOTE_RE = /^\s|\s$|["']/;
732
+ const DQUOTE_RE = /"/g;
733
+ const LEADING_NEWLINES_RE = /^\n+/;
734
+ function yamlEscape(value) {
735
+ if (YAML_RESERVED_RE.test(value) || YAML_EDGE_OR_QUOTE_RE.test(value) || value === "") return `"${value.replace(DQUOTE_RE, "\\\"")}"`;
736
+ return value;
737
+ }
738
+ function serializeFrontmatter(skill) {
739
+ const lines = ["---"];
740
+ lines.push(`name: ${yamlEscape(skill.name)}`);
741
+ lines.push(`description: ${yamlEscape(skill.description)}`);
742
+ if (skill.license) lines.push(`license: ${yamlEscape(skill.license)}`);
743
+ if (skill.compatibility) lines.push(`compatibility: ${yamlEscape(skill.compatibility)}`);
744
+ if (skill.allowedTools?.length) lines.push(`allowed-tools: ${skill.allowedTools.join(" ")}`);
745
+ if (skill.metadata && Object.keys(skill.metadata).length > 0) {
746
+ lines.push("metadata:");
747
+ for (const [key, value] of Object.entries(skill.metadata)) lines.push(` ${key}: "${value.replace(DQUOTE_RE, "\\\"")}"`);
748
+ }
749
+ lines.push("---");
750
+ return lines.join("\n");
751
+ }
752
+ /**
753
+ * Write a `SkillConfig` to disk as a proper skill directory with SKILL.md.
754
+ * Returns the path to the written SKILL.md file.
755
+ *
756
+ * Throws if the skill fails `validateSkillForWrite` — the authoring path is
757
+ * strict on purpose. For loading third-party skills, use `parseSkillFile`
758
+ * directly (lenient).
759
+ */
760
+ function writeSkillToDisk(skill, targetDir) {
761
+ const result = validateSkillForWrite(skill);
762
+ if (!result.valid) {
763
+ const summary = result.errors.map((e) => ` - [${e.code}] ${e.message}`).join("\n");
764
+ throw new Error(`Cannot write invalid skill "${skill.name}":\n${summary}`);
765
+ }
766
+ const skillDir = join(targetDir, skill.name);
767
+ mkdirSync(skillDir, { recursive: true });
768
+ const frontmatter = serializeFrontmatter(skill);
769
+ const bodyTrimmed = skill.instructions ? skill.instructions.replace(LEADING_NEWLINES_RE, "") : "";
770
+ const content = bodyTrimmed ? `${frontmatter}\n\n${bodyTrimmed}\n` : `${frontmatter}\n`;
771
+ const skillPath = join(skillDir, "SKILL.md");
772
+ writeFileSync(skillPath, content);
773
+ return skillPath;
774
+ }
775
+ /**
776
+ * Write multiple `SkillConfig` objects to a target directory.
777
+ * Each skill gets its own subdirectory with a SKILL.md file.
778
+ * Returns the target directory path (for use as a scan path).
779
+ */
780
+ function writeSkillsToDisk(skills, targetDir) {
781
+ mkdirSync(targetDir, { recursive: true });
782
+ for (const skill of skills) writeSkillToDisk(skill, targetDir);
783
+ return targetDir;
784
+ }
785
+ //#endregion
786
+ //#region src/skills/resolve.ts
787
+ /**
788
+ * Resolve all skills from a SkillsConfig:
789
+ *
790
+ * 1. Materialize `config.write` entries to a temp directory, tagged as `inline`
791
+ * 2. Combine with default + user-provided scan paths (project-first, user-next)
792
+ * 3. Run lenient discovery
793
+ * 4. Apply filters: `exclude`, `enabled` allowlist, optional project-skill trust gate
794
+ *
795
+ * Returns `{ skills, cleanup }` — call `cleanup()` on agent destroy to remove
796
+ * the temp directory created for inline `config.write` skills. The agent
797
+ * factory wires this automatically; standalone callers must invoke it
798
+ * themselves to avoid leaking OS temp.
799
+ */
800
+ async function resolveSkills(config) {
801
+ const sourcedPaths = [];
802
+ let writeDir;
803
+ if (!config.skipDefaultPaths) sourcedPaths.push(...getDefaultScanPaths());
804
+ for (const p of config.scan ?? []) sourcedPaths.push({
805
+ path: p,
806
+ source: inferSource(p)
807
+ });
808
+ if (config.write?.length) {
809
+ writeDir = mkdtempSync(join(tmpdir(), "zidane-skills-"));
810
+ writeSkillsToDisk(config.write, writeDir);
811
+ sourcedPaths.push({
812
+ path: writeDir,
813
+ source: "inline"
814
+ });
815
+ }
816
+ let skills = await discoverSkills(sourcedPaths);
817
+ if (config.trustProjectSkills === false) skills = skills.filter((s) => s.source !== void 0 && s.source !== "project");
818
+ const exclude = new Set(config.exclude ?? []);
819
+ let filtered = skills.filter((s) => !exclude.has(s.name));
820
+ if (Array.isArray(config.enabled)) {
821
+ const allowlist = new Set(config.enabled);
822
+ filtered = filtered.filter((s) => allowlist.has(s.name));
823
+ }
824
+ return {
825
+ skills: filtered,
826
+ cleanup: writeDir ? () => {
827
+ try {
828
+ rmSync(writeDir, {
829
+ recursive: true,
830
+ force: true
831
+ });
832
+ } catch {}
833
+ } : () => {}
834
+ };
835
+ }
836
+ //#endregion
837
+ //#region src/skills/interpolate.ts
838
+ /** Regex to match !`command` patterns in skill instructions */
839
+ const SHELL_INTERPOLATION_RE = /!`([^`]+)`/g;
840
+ /**
841
+ * Interpolate shell commands in skill instructions.
842
+ *
843
+ * Runs each !\`command\` through the execution context and replaces
844
+ * the placeholder with the command's stdout. If a command fails,
845
+ * the placeholder is replaced with an error message.
846
+ *
847
+ * @param instructions - Raw skill instructions with potential !\`command\` patterns
848
+ * @param execution - The execution context to run commands in
849
+ * @param handle - The active execution handle
850
+ * @returns Instructions with all !\`command\` patterns replaced by output
851
+ */
852
+ async function interpolateShellCommands(instructions, execution, handle) {
853
+ const matches = [...instructions.matchAll(SHELL_INTERPOLATION_RE)];
854
+ if (matches.length === 0) return instructions;
855
+ const replacements = [];
856
+ for (const match of matches) {
857
+ const command = match[1];
858
+ const index = match.index;
859
+ const length = match[0].length;
860
+ try {
861
+ const result = await execution.exec(handle, command, { timeout: 30 });
862
+ const output = result.exitCode === 0 ? result.stdout.trim() : `[command failed (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}]`;
863
+ replacements.push({
864
+ index,
865
+ length,
866
+ output
867
+ });
868
+ } catch (err) {
869
+ const message = err instanceof Error ? err.message : String(err);
870
+ replacements.push({
871
+ index,
872
+ length,
873
+ output: `[command error: ${message}]`
874
+ });
875
+ }
876
+ }
877
+ let result = instructions;
878
+ for (let i = replacements.length - 1; i >= 0; i--) {
879
+ const { index, length, output } = replacements[i];
880
+ result = result.slice(0, index) + output + result.slice(index + length);
881
+ }
882
+ return result;
883
+ }
884
+ //#endregion
885
+ export { validateResourcePath as _, discoverSkills as a, createSkillActivationState as b, parseFrontmatter as c, escapeXml as d, IMPLICITLY_ALLOWED_SKILL_TOOLS as f, parseAllowedToolPattern as g, matchesAllowedTool as h, writeSkillsToDisk as i, parseSkillFile as l, isToolAllowedByUnion as m, resolveSkills as n, getDefaultScanPaths as o, installAllowedToolsGate as p, writeSkillToDisk as r, inferSource as s, interpolateShellCommands as t, buildCatalog as u, validateSkillForWrite as v, validateSkillName as y };
886
+
887
+ //# sourceMappingURL=interpolate-CukJwP2G.js.map