zidane 1.8.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-CWQ5XOw6.d.ts → agent-D-ZFMbSd.d.ts} +371 -21
- package/dist/{chunk-FFFDQHMA.js → chunk-7JTBBZ2U.js} +21 -0
- package/dist/chunk-BCXXXJ3G.js +779 -0
- package/dist/{chunk-WQBKOZVI.js → chunk-FRNFVKWW.js} +50 -12
- package/dist/{chunk-EC7IRWVS.js → chunk-LN4LLLHA.js} +101 -7
- package/dist/chunk-LVC7NQUZ.js +22 -0
- package/dist/chunk-MYWDHD7C.js +14 -0
- package/dist/{chunk-3RJOWJOJ.js → chunk-OVQ4N64O.js} +1 -1
- package/dist/{chunk-4N5ADW7A.js → chunk-PASFWG7S.js} +343 -32
- package/dist/{chunk-TBC6MSVK.js → chunk-PJUUYBKF.js} +53 -4
- package/dist/{chunk-2IB4XBQE.js → chunk-VG2E6YK3.js} +0 -97
- package/dist/harnesses.d.ts +1 -2
- package/dist/harnesses.js +5 -5
- package/dist/index.d.ts +5 -6
- package/dist/index.js +42 -12
- package/dist/mcp.d.ts +1 -2
- package/dist/mcp.js +3 -1
- package/dist/providers.d.ts +1 -2
- package/dist/providers.js +3 -3
- package/dist/session/sqlite.d.ts +16 -0
- package/dist/session/sqlite.js +98 -0
- package/dist/session.d.ts +1 -2
- package/dist/session.js +3 -5
- package/dist/skills-use-C4KFVla0.d.ts +66 -0
- package/dist/skills.d.ts +215 -30
- package/dist/skills.js +21 -2
- package/dist/{spawn-EEv1Johs.d.ts → spawn-RoqpjYLZ.d.ts} +1 -1
- package/dist/tools.d.ts +3 -4
- package/dist/tools.js +10 -4
- package/dist/types.d.ts +2 -3
- package/dist/types.js +8 -2
- package/package.json +12 -3
- package/dist/chunk-4C6Y56CC.js +0 -390
- package/dist/chunk-CFLC2I7D.js +0 -8
- package/dist/glob-j9gbk6xm.d.ts +0 -5
- package/dist/types-CDI8Kmve.d.ts +0 -64
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgentToolNotAllowedError
|
|
3
|
+
} from "./chunk-7JTBBZ2U.js";
|
|
4
|
+
|
|
5
|
+
// 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))
|
|
21
|
+
return "already-active";
|
|
22
|
+
if (maxActive !== void 0 && byName.size >= maxActive)
|
|
23
|
+
return "cap-reached";
|
|
24
|
+
byName.set(skill.name, {
|
|
25
|
+
skill,
|
|
26
|
+
activatedAt: Date.now(),
|
|
27
|
+
activatedVia: via
|
|
28
|
+
});
|
|
29
|
+
return "ok";
|
|
30
|
+
},
|
|
31
|
+
deactivate(name) {
|
|
32
|
+
const existing = byName.get(name);
|
|
33
|
+
if (!existing)
|
|
34
|
+
return void 0;
|
|
35
|
+
byName.delete(name);
|
|
36
|
+
return existing;
|
|
37
|
+
},
|
|
38
|
+
clear() {
|
|
39
|
+
const snapshot = [...byName.values()];
|
|
40
|
+
byName.clear();
|
|
41
|
+
return snapshot;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/skills/validate.ts
|
|
47
|
+
import { basename } from "path";
|
|
48
|
+
var NAME_MAX = 64;
|
|
49
|
+
var DESCRIPTION_MAX = 1024;
|
|
50
|
+
var COMPATIBILITY_MAX = 500;
|
|
51
|
+
var SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
52
|
+
var CONSECUTIVE_HYPHENS_RE = /--/;
|
|
53
|
+
var ALLOWED_TOOL_PATTERN_RE = /^([\w-]+)(?:\(([^)]*)\))?$/;
|
|
54
|
+
var ABS_WINDOWS_PATH_RE = /^[a-z]:[\\/]/i;
|
|
55
|
+
var PATH_SEPARATOR_RE = /[\\/]/;
|
|
56
|
+
var TRAILING_SLASHES_RE = /\/+$/;
|
|
57
|
+
function validateSkillName(name) {
|
|
58
|
+
if (typeof name !== "string")
|
|
59
|
+
return false;
|
|
60
|
+
if (name.length < 1 || name.length > NAME_MAX)
|
|
61
|
+
return false;
|
|
62
|
+
if (CONSECUTIVE_HYPHENS_RE.test(name))
|
|
63
|
+
return false;
|
|
64
|
+
return SKILL_NAME_RE.test(name);
|
|
65
|
+
}
|
|
66
|
+
function validateSkillForWrite(skill) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
if (!validateSkillName(skill.name)) {
|
|
69
|
+
errors.push({
|
|
70
|
+
code: "invalid-name",
|
|
71
|
+
message: `Skill name "${skill.name}" must be 1-64 chars, lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens.`,
|
|
72
|
+
field: "name"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (skill.location) {
|
|
76
|
+
const dirName = basename(skill.baseDir ?? "");
|
|
77
|
+
if (dirName && dirName !== skill.name) {
|
|
78
|
+
errors.push({
|
|
79
|
+
code: "name-mismatch-directory",
|
|
80
|
+
message: `Skill name "${skill.name}" must match parent directory name "${dirName}".`,
|
|
81
|
+
field: "name"
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (typeof skill.description !== "string" || skill.description.length < 1) {
|
|
86
|
+
errors.push({
|
|
87
|
+
code: "missing-description",
|
|
88
|
+
message: "Skill description is required (non-empty).",
|
|
89
|
+
field: "description"
|
|
90
|
+
});
|
|
91
|
+
} else if (skill.description.length > DESCRIPTION_MAX) {
|
|
92
|
+
errors.push({
|
|
93
|
+
code: "description-too-long",
|
|
94
|
+
message: `Skill description must be at most ${DESCRIPTION_MAX} characters (got ${skill.description.length}).`,
|
|
95
|
+
field: "description"
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (skill.compatibility !== void 0) {
|
|
99
|
+
if (typeof skill.compatibility !== "string" || skill.compatibility.length === 0) {
|
|
100
|
+
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) {
|
|
106
|
+
errors.push({
|
|
107
|
+
code: "compatibility-too-long",
|
|
108
|
+
message: `Compatibility must be at most ${COMPATIBILITY_MAX} characters (got ${skill.compatibility.length}).`,
|
|
109
|
+
field: "compatibility"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (skill.metadata) {
|
|
114
|
+
for (const [key, value] of Object.entries(skill.metadata)) {
|
|
115
|
+
if (typeof value !== "string") {
|
|
116
|
+
errors.push({
|
|
117
|
+
code: "invalid-metadata-value",
|
|
118
|
+
message: `Metadata value for "${key}" must be a string (spec: "A map from string keys to string values").`,
|
|
119
|
+
field: "metadata"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (skill.allowedTools) {
|
|
125
|
+
for (const pattern of skill.allowedTools) {
|
|
126
|
+
if (!ALLOWED_TOOL_PATTERN_RE.test(pattern)) {
|
|
127
|
+
errors.push({
|
|
128
|
+
code: "invalid-allowed-tool-pattern",
|
|
129
|
+
message: `Allowed-tools entry "${pattern}" is not a recognized pattern (expected "ToolName" or "ToolName(arg:*)").`,
|
|
130
|
+
field: "allowed-tools"
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { valid: errors.length === 0, errors };
|
|
136
|
+
}
|
|
137
|
+
function validateResourcePath(relPath, baseDir) {
|
|
138
|
+
if (typeof relPath !== "string" || relPath.length === 0)
|
|
139
|
+
return { valid: false, error: "Resource path must be a non-empty string." };
|
|
140
|
+
if (relPath.includes("\0"))
|
|
141
|
+
return { valid: false, error: "Resource path contains a null byte." };
|
|
142
|
+
if (relPath.startsWith("/") || ABS_WINDOWS_PATH_RE.test(relPath))
|
|
143
|
+
return { valid: false, error: `Absolute paths are not allowed ("${relPath}").` };
|
|
144
|
+
const segments = [];
|
|
145
|
+
for (const segment of relPath.split(PATH_SEPARATOR_RE)) {
|
|
146
|
+
if (segment === "" || segment === ".")
|
|
147
|
+
continue;
|
|
148
|
+
if (segment === "..") {
|
|
149
|
+
if (segments.length === 0) {
|
|
150
|
+
return {
|
|
151
|
+
valid: false,
|
|
152
|
+
error: `Resource path "${relPath}" escapes the skill directory.`
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
segments.pop();
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
segments.push(segment);
|
|
159
|
+
}
|
|
160
|
+
if (segments.length === 0)
|
|
161
|
+
return { valid: false, error: "Resource path resolves to the skill root itself." };
|
|
162
|
+
const absolutePath = `${baseDir.replace(TRAILING_SLASHES_RE, "")}/${segments.join("/")}`;
|
|
163
|
+
return { valid: true, absolutePath };
|
|
164
|
+
}
|
|
165
|
+
function parseAllowedToolPattern(entry) {
|
|
166
|
+
const m = entry.trim().match(ALLOWED_TOOL_PATTERN_RE);
|
|
167
|
+
if (!m)
|
|
168
|
+
return null;
|
|
169
|
+
const tool = m[1];
|
|
170
|
+
const arg = m[2];
|
|
171
|
+
if (!arg)
|
|
172
|
+
return { tool };
|
|
173
|
+
if (arg.endsWith(":*"))
|
|
174
|
+
return { tool, argPrefix: arg.slice(0, -2) };
|
|
175
|
+
return { tool, argPrefix: arg };
|
|
176
|
+
}
|
|
177
|
+
function matchesAllowedTool(displayName, input, pattern) {
|
|
178
|
+
const parsed = parseAllowedToolPattern(pattern);
|
|
179
|
+
if (!parsed)
|
|
180
|
+
return false;
|
|
181
|
+
if (parsed.tool !== displayName)
|
|
182
|
+
return false;
|
|
183
|
+
if (parsed.argPrefix === void 0)
|
|
184
|
+
return true;
|
|
185
|
+
for (const value of Object.values(input)) {
|
|
186
|
+
if (typeof value === "string" && value.startsWith(parsed.argPrefix))
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
function isToolAllowedByUnion(displayName, input, union) {
|
|
192
|
+
if (union.length === 0)
|
|
193
|
+
return true;
|
|
194
|
+
return union.some((pattern) => matchesAllowedTool(displayName, input, pattern));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/skills/allowed-tools.ts
|
|
198
|
+
var IMPLICITLY_ALLOWED_SKILL_TOOLS = [
|
|
199
|
+
"skills_use",
|
|
200
|
+
"skills_read",
|
|
201
|
+
"skills_run_script"
|
|
202
|
+
];
|
|
203
|
+
function installAllowedToolsGate(hooks, state) {
|
|
204
|
+
function effectiveUnion() {
|
|
205
|
+
const active = state.active();
|
|
206
|
+
const declared = [];
|
|
207
|
+
for (const record of active) {
|
|
208
|
+
if (record.skill.allowedTools?.length)
|
|
209
|
+
declared.push(...record.skill.allowedTools);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
union: declared.length > 0 ? [...declared, ...IMPLICITLY_ALLOWED_SKILL_TOOLS] : [],
|
|
213
|
+
active: active.map((a) => a.skill.name)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function gateHandler(ctx) {
|
|
217
|
+
const { union, active } = effectiveUnion();
|
|
218
|
+
if (union.length === 0)
|
|
219
|
+
return;
|
|
220
|
+
if (isToolAllowedByUnion(ctx.displayName, ctx.input, union))
|
|
221
|
+
return;
|
|
222
|
+
const err = new AgentToolNotAllowedError({
|
|
223
|
+
toolName: ctx.name,
|
|
224
|
+
displayName: ctx.displayName,
|
|
225
|
+
allowedUnion: union,
|
|
226
|
+
activeSkills: active
|
|
227
|
+
});
|
|
228
|
+
ctx.block = true;
|
|
229
|
+
ctx.reason = err.message;
|
|
230
|
+
}
|
|
231
|
+
function mcpGateHandler(ctx) {
|
|
232
|
+
const { union, active } = effectiveUnion();
|
|
233
|
+
if (union.length === 0)
|
|
234
|
+
return;
|
|
235
|
+
if (isToolAllowedByUnion(ctx.displayName, ctx.input, union))
|
|
236
|
+
return;
|
|
237
|
+
const err = new AgentToolNotAllowedError({
|
|
238
|
+
toolName: `mcp_${ctx.server}_${ctx.tool}`,
|
|
239
|
+
displayName: ctx.displayName,
|
|
240
|
+
allowedUnion: union,
|
|
241
|
+
activeSkills: active
|
|
242
|
+
});
|
|
243
|
+
ctx.block = true;
|
|
244
|
+
ctx.reason = err.message;
|
|
245
|
+
}
|
|
246
|
+
const unregisterTool = hooks.hook("tool:gate", gateHandler);
|
|
247
|
+
const unregisterMcp = hooks.hook("mcp:tool:gate", mcpGateHandler);
|
|
248
|
+
return function uninstall() {
|
|
249
|
+
unregisterTool();
|
|
250
|
+
unregisterMcp();
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/skills/catalog.ts
|
|
255
|
+
function buildCatalog(skills, optionsOrReadToolName = {}) {
|
|
256
|
+
if (skills.length === 0)
|
|
257
|
+
return "";
|
|
258
|
+
const options = typeof optionsOrReadToolName === "string" ? { skillsToolRegistered: false, readToolName: optionsOrReadToolName } : optionsOrReadToolName;
|
|
259
|
+
const skillsToolRegistered = options.skillsToolRegistered ?? true;
|
|
260
|
+
const readToolName = options.readToolName ?? "read_file";
|
|
261
|
+
const entries = skills.map((skill) => {
|
|
262
|
+
const locationLine = skill.location ? `
|
|
263
|
+
<location>${escapeXml(skill.location)}</location>` : "";
|
|
264
|
+
return ` <skill>
|
|
265
|
+
<name>${escapeXml(skill.name)}</name>
|
|
266
|
+
<description>${escapeXml(skill.description)}</description>${locationLine}
|
|
267
|
+
</skill>`;
|
|
268
|
+
}).join("\n");
|
|
269
|
+
const hasFsSkills = skills.some((s) => s.location);
|
|
270
|
+
const hasInlineSkills = skills.some((s) => !s.location);
|
|
271
|
+
const behavioralParts = [];
|
|
272
|
+
behavioralParts.push(
|
|
273
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
274
|
+
"When a task matches a skill's description, activate the skill to load its full instructions before proceeding."
|
|
275
|
+
);
|
|
276
|
+
if (skillsToolRegistered) {
|
|
277
|
+
behavioralParts.push(
|
|
278
|
+
"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).",
|
|
279
|
+
"Relative paths referenced in a skill's instructions resolve against the skill directory noted in the `skills_use` response."
|
|
280
|
+
);
|
|
281
|
+
} else if (hasFsSkills) {
|
|
282
|
+
behavioralParts.push(
|
|
283
|
+
`For skills with a <location>, use the ${readToolName} tool to read the SKILL.md file at that path.`,
|
|
284
|
+
"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."
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (hasInlineSkills && !skillsToolRegistered) {
|
|
288
|
+
behavioralParts.push(
|
|
289
|
+
"Skills without a <location> have their instructions included directly in <instructions> tags below."
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const parts = [
|
|
293
|
+
behavioralParts.join("\n"),
|
|
294
|
+
"",
|
|
295
|
+
"<available_skills>",
|
|
296
|
+
entries,
|
|
297
|
+
"</available_skills>"
|
|
298
|
+
];
|
|
299
|
+
if (hasInlineSkills && !skillsToolRegistered) {
|
|
300
|
+
parts.push("");
|
|
301
|
+
for (const skill of skills) {
|
|
302
|
+
if (!skill.location && skill.instructions) {
|
|
303
|
+
parts.push(`<skill_instructions name="${escapeXml(skill.name)}">`);
|
|
304
|
+
parts.push(skill.instructions);
|
|
305
|
+
if (skill.resources && skill.resources.length > 0) {
|
|
306
|
+
parts.push("");
|
|
307
|
+
parts.push("<skill_resources>");
|
|
308
|
+
for (const res of skill.resources) {
|
|
309
|
+
parts.push(` <file type="${res.type}">${escapeXml(res.path)}</file>`);
|
|
310
|
+
}
|
|
311
|
+
parts.push("</skill_resources>");
|
|
312
|
+
}
|
|
313
|
+
parts.push("</skill_instructions>");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return parts.join("\n");
|
|
318
|
+
}
|
|
319
|
+
var RE_AMP = /&/g;
|
|
320
|
+
var RE_LT = /</g;
|
|
321
|
+
var RE_GT = />/g;
|
|
322
|
+
var RE_QUOT = /"/g;
|
|
323
|
+
function escapeXml(str) {
|
|
324
|
+
return str.replace(RE_AMP, "&").replace(RE_LT, "<").replace(RE_GT, ">").replace(RE_QUOT, """);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/skills/discovery.ts
|
|
328
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
329
|
+
import { homedir } from "os";
|
|
330
|
+
import { basename as basename2, dirname, join, resolve } from "path";
|
|
331
|
+
var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
332
|
+
var INDENT_RE = /^[ \t]{2,}/;
|
|
333
|
+
var KV_RE = /^([^:]+):(.*)$/;
|
|
334
|
+
var QUOTE_RE = /^(['"])(.*)\1$/;
|
|
335
|
+
var WHITESPACE_SPLIT_RE = /\s+/;
|
|
336
|
+
var PARAGRAPH_SPLIT_RE = /\n\n/;
|
|
337
|
+
var COMMA_OR_SPACE_RE = /[,\s]+/;
|
|
338
|
+
function parseFrontmatter(content) {
|
|
339
|
+
const diagnostics = [];
|
|
340
|
+
const match = content.match(FRONTMATTER_RE);
|
|
341
|
+
if (!match) {
|
|
342
|
+
return { frontmatter: {}, body: content.trim(), diagnostics };
|
|
343
|
+
}
|
|
344
|
+
const yamlBlock = match[1];
|
|
345
|
+
const body = match[2].trim();
|
|
346
|
+
const frontmatter = {};
|
|
347
|
+
let currentKey = null;
|
|
348
|
+
let currentMap = null;
|
|
349
|
+
for (const line of yamlBlock.split("\n")) {
|
|
350
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
351
|
+
continue;
|
|
352
|
+
if (currentKey && currentMap && INDENT_RE.test(line)) {
|
|
353
|
+
const nestedMatch = line.trim().match(KV_RE);
|
|
354
|
+
if (nestedMatch) {
|
|
355
|
+
const val = nestedMatch[2].trim();
|
|
356
|
+
currentMap[nestedMatch[1].trim()] = unquoteYaml(val);
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (currentKey && currentMap) {
|
|
361
|
+
frontmatter[currentKey] = currentMap;
|
|
362
|
+
currentKey = null;
|
|
363
|
+
currentMap = null;
|
|
364
|
+
}
|
|
365
|
+
const kvMatch = matchFirstColon(line);
|
|
366
|
+
if (!kvMatch)
|
|
367
|
+
continue;
|
|
368
|
+
const key = kvMatch.key.trim();
|
|
369
|
+
const rawValue = kvMatch.value.trim();
|
|
370
|
+
if (!rawValue) {
|
|
371
|
+
currentKey = key;
|
|
372
|
+
currentMap = {};
|
|
373
|
+
} else {
|
|
374
|
+
frontmatter[key] = unquoteYaml(rawValue);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (currentKey && currentMap) {
|
|
378
|
+
frontmatter[currentKey] = currentMap;
|
|
379
|
+
}
|
|
380
|
+
return { frontmatter, body, diagnostics };
|
|
381
|
+
}
|
|
382
|
+
function matchFirstColon(line) {
|
|
383
|
+
const idx = line.indexOf(":");
|
|
384
|
+
if (idx < 0)
|
|
385
|
+
return null;
|
|
386
|
+
const key = line.slice(0, idx);
|
|
387
|
+
const value = line.slice(idx + 1);
|
|
388
|
+
if (!KV_RE.test(`${key}:`))
|
|
389
|
+
return null;
|
|
390
|
+
return { key, value };
|
|
391
|
+
}
|
|
392
|
+
function unquoteYaml(val) {
|
|
393
|
+
const m = val.match(QUOTE_RE);
|
|
394
|
+
if (m)
|
|
395
|
+
return m[2];
|
|
396
|
+
return val;
|
|
397
|
+
}
|
|
398
|
+
var RESOURCE_DIRS = {
|
|
399
|
+
scripts: "script",
|
|
400
|
+
references: "reference",
|
|
401
|
+
assets: "asset"
|
|
402
|
+
};
|
|
403
|
+
function enumerateResources(baseDir) {
|
|
404
|
+
const resources = [];
|
|
405
|
+
for (const [dir, type] of Object.entries(RESOURCE_DIRS)) {
|
|
406
|
+
const dirPath = join(baseDir, dir);
|
|
407
|
+
if (!existsSync(dirPath) || !statSync(dirPath).isDirectory())
|
|
408
|
+
continue;
|
|
409
|
+
try {
|
|
410
|
+
const files = readdirSync(dirPath, { recursive: true });
|
|
411
|
+
for (const file of files) {
|
|
412
|
+
const fullPath = join(dirPath, file);
|
|
413
|
+
if (statSync(fullPath).isFile()) {
|
|
414
|
+
resources.push({ path: join(dir, file), type });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
for (const entry of readdirSync(baseDir)) {
|
|
422
|
+
if (entry === "SKILL.md")
|
|
423
|
+
continue;
|
|
424
|
+
const entryPath = join(baseDir, entry);
|
|
425
|
+
if (statSync(entryPath).isFile()) {
|
|
426
|
+
resources.push({ path: entry, type: "other" });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
return resources;
|
|
432
|
+
}
|
|
433
|
+
async function parseSkillFile(filePath, options = {}) {
|
|
434
|
+
const absPath = resolve(filePath);
|
|
435
|
+
if (!existsSync(absPath))
|
|
436
|
+
return null;
|
|
437
|
+
const content = readFileSync(absPath, "utf-8");
|
|
438
|
+
const { frontmatter, body, diagnostics } = parseFrontmatter(content);
|
|
439
|
+
let description = frontmatter.description;
|
|
440
|
+
if (!description && body) {
|
|
441
|
+
const firstParagraph = body.split(PARAGRAPH_SPLIT_RE)[0]?.trim();
|
|
442
|
+
if (firstParagraph)
|
|
443
|
+
description = firstParagraph;
|
|
444
|
+
}
|
|
445
|
+
if (!description)
|
|
446
|
+
return null;
|
|
447
|
+
if (description.length > 1024) {
|
|
448
|
+
diagnostics.push({
|
|
449
|
+
severity: "warning",
|
|
450
|
+
code: "description-too-long",
|
|
451
|
+
message: `Description exceeds spec limit of 1024 characters (got ${description.length}). Loading anyway.`,
|
|
452
|
+
field: "description"
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
const baseDir = dirname(absPath);
|
|
456
|
+
const dirName = basename2(baseDir);
|
|
457
|
+
const name = frontmatter.name || dirName;
|
|
458
|
+
if (frontmatter.name && frontmatter.name !== dirName) {
|
|
459
|
+
diagnostics.push({
|
|
460
|
+
severity: "warning",
|
|
461
|
+
code: "name-mismatch-directory",
|
|
462
|
+
message: `Skill name "${frontmatter.name}" does not match parent directory "${dirName}". Loading anyway.`,
|
|
463
|
+
field: "name"
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (name.length > 64) {
|
|
467
|
+
diagnostics.push({
|
|
468
|
+
severity: "warning",
|
|
469
|
+
code: "name-too-long",
|
|
470
|
+
message: `Skill name "${name}" exceeds spec limit of 64 characters. Loading anyway.`,
|
|
471
|
+
field: "name"
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
if (!validateSkillName(name)) {
|
|
475
|
+
diagnostics.push({
|
|
476
|
+
severity: "warning",
|
|
477
|
+
code: "invalid-name-format",
|
|
478
|
+
message: `Skill name "${name}" does not match spec format (lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens). Loading anyway.`,
|
|
479
|
+
field: "name"
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
const config = {
|
|
483
|
+
name,
|
|
484
|
+
description,
|
|
485
|
+
instructions: body,
|
|
486
|
+
source: options.source ?? "project",
|
|
487
|
+
location: absPath,
|
|
488
|
+
baseDir,
|
|
489
|
+
resources: enumerateResources(baseDir)
|
|
490
|
+
};
|
|
491
|
+
if (frontmatter.license)
|
|
492
|
+
config.license = frontmatter.license;
|
|
493
|
+
if (frontmatter.compatibility) {
|
|
494
|
+
const comp = frontmatter.compatibility;
|
|
495
|
+
if (comp.length > 500) {
|
|
496
|
+
diagnostics.push({
|
|
497
|
+
severity: "warning",
|
|
498
|
+
code: "compatibility-too-long",
|
|
499
|
+
message: `Compatibility exceeds spec limit of 500 characters (got ${comp.length}). Loading anyway.`,
|
|
500
|
+
field: "compatibility"
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
config.compatibility = comp;
|
|
504
|
+
}
|
|
505
|
+
const metadata = {};
|
|
506
|
+
if (frontmatter.metadata && typeof frontmatter.metadata === "object") {
|
|
507
|
+
for (const [k, v] of Object.entries(frontmatter.metadata)) {
|
|
508
|
+
if (typeof v !== "string") {
|
|
509
|
+
diagnostics.push({
|
|
510
|
+
severity: "warning",
|
|
511
|
+
code: "invalid-metadata-value",
|
|
512
|
+
message: `Metadata value for "${k}" is not a string; coerced. (Spec requires string values.)`,
|
|
513
|
+
field: "metadata"
|
|
514
|
+
});
|
|
515
|
+
metadata[k] = String(v);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
metadata[k] = v;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (frontmatter.paths) {
|
|
522
|
+
const raw = frontmatter.paths;
|
|
523
|
+
const normalized = raw.split(COMMA_OR_SPACE_RE).filter(Boolean).join(",");
|
|
524
|
+
metadata["zidane.paths"] = normalized;
|
|
525
|
+
diagnostics.push({
|
|
526
|
+
severity: "warning",
|
|
527
|
+
code: "deprecated-top-level-field",
|
|
528
|
+
message: '`paths` is not a spec field and is deprecated \u2014 moved to `metadata["zidane.paths"]`.',
|
|
529
|
+
field: "paths"
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (frontmatter.model) {
|
|
533
|
+
metadata["zidane.model"] = frontmatter.model;
|
|
534
|
+
diagnostics.push({
|
|
535
|
+
severity: "warning",
|
|
536
|
+
code: "deprecated-top-level-field",
|
|
537
|
+
message: '`model` is not a spec field and is deprecated \u2014 moved to `metadata["zidane.model"]`.',
|
|
538
|
+
field: "model"
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
const legacyThinking = frontmatter.thinking ?? frontmatter.effort;
|
|
542
|
+
if (legacyThinking) {
|
|
543
|
+
metadata["zidane.thinking"] = legacyThinking;
|
|
544
|
+
diagnostics.push({
|
|
545
|
+
severity: "warning",
|
|
546
|
+
code: "deprecated-top-level-field",
|
|
547
|
+
message: `\`${frontmatter.thinking ? "thinking" : "effort"}\` is not a spec field and is deprecated \u2014 moved to \`metadata["zidane.thinking"]\`.`,
|
|
548
|
+
field: frontmatter.thinking ? "thinking" : "effort"
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (Object.keys(metadata).length > 0)
|
|
552
|
+
config.metadata = metadata;
|
|
553
|
+
if (frontmatter["allowed-tools"]) {
|
|
554
|
+
config.allowedTools = frontmatter["allowed-tools"].split(WHITESPACE_SPLIT_RE).filter(Boolean);
|
|
555
|
+
}
|
|
556
|
+
if (diagnostics.length > 0)
|
|
557
|
+
config.diagnostics = diagnostics;
|
|
558
|
+
return config;
|
|
559
|
+
}
|
|
560
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", "dist", "build"]);
|
|
561
|
+
function findSkillDirs(root, maxDepth = 4, _depth = 0) {
|
|
562
|
+
if (_depth > maxDepth)
|
|
563
|
+
return [];
|
|
564
|
+
if (!existsSync(root) || !statSync(root).isDirectory())
|
|
565
|
+
return [];
|
|
566
|
+
const results = [];
|
|
567
|
+
try {
|
|
568
|
+
for (const entry of readdirSync(root)) {
|
|
569
|
+
if (SKIP_DIRS.has(entry))
|
|
570
|
+
continue;
|
|
571
|
+
const entryPath = join(root, entry);
|
|
572
|
+
if (!statSync(entryPath).isDirectory())
|
|
573
|
+
continue;
|
|
574
|
+
const skillFile = join(entryPath, "SKILL.md");
|
|
575
|
+
if (existsSync(skillFile) && statSync(skillFile).isFile()) {
|
|
576
|
+
results.push(skillFile);
|
|
577
|
+
} else {
|
|
578
|
+
results.push(...findSkillDirs(entryPath, maxDepth, _depth + 1));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
return results;
|
|
584
|
+
}
|
|
585
|
+
function getDefaultScanPaths() {
|
|
586
|
+
const home = homedir();
|
|
587
|
+
const cwd = process.cwd();
|
|
588
|
+
return [
|
|
589
|
+
// Project-level (higher priority)
|
|
590
|
+
{ path: join(cwd, ".agents", "skills"), source: "project" },
|
|
591
|
+
{ path: join(cwd, ".zidane", "skills"), source: "project" },
|
|
592
|
+
// User-level (lower priority)
|
|
593
|
+
{ path: join(home, ".agents", "skills"), source: "user" },
|
|
594
|
+
{ path: join(home, ".zidane", "skills"), source: "user" }
|
|
595
|
+
];
|
|
596
|
+
}
|
|
597
|
+
function inferSource(path) {
|
|
598
|
+
return path.startsWith(homedir()) ? "user" : "project";
|
|
599
|
+
}
|
|
600
|
+
async function discoverSkills(paths) {
|
|
601
|
+
const skillsByName = /* @__PURE__ */ new Map();
|
|
602
|
+
for (const { path: scanPath, source } of paths) {
|
|
603
|
+
const skillFiles = findSkillDirs(resolve(scanPath));
|
|
604
|
+
for (const file of skillFiles) {
|
|
605
|
+
const skill = await parseSkillFile(file, { source });
|
|
606
|
+
if (!skill)
|
|
607
|
+
continue;
|
|
608
|
+
if (skillsByName.has(skill.name)) {
|
|
609
|
+
const existing = skillsByName.get(skill.name);
|
|
610
|
+
const diag = {
|
|
611
|
+
severity: "warning",
|
|
612
|
+
code: "name-collision-shadowed",
|
|
613
|
+
message: `A skill with name "${skill.name}" was also found at ${file} (source: ${source}); shadowed by ${existing.location} (source: ${existing.source}).`
|
|
614
|
+
};
|
|
615
|
+
existing.diagnostics = [...existing.diagnostics ?? [], diag];
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
skillsByName.set(skill.name, skill);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return [...skillsByName.values()];
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/skills/writer.ts
|
|
625
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
626
|
+
import { join as join2 } from "path";
|
|
627
|
+
var YAML_RESERVED_RE = /[:#&*!|>%@`]/;
|
|
628
|
+
var YAML_EDGE_OR_QUOTE_RE = /^\s|\s$|["']/;
|
|
629
|
+
var DQUOTE_RE = /"/g;
|
|
630
|
+
function yamlEscape(value) {
|
|
631
|
+
if (YAML_RESERVED_RE.test(value) || YAML_EDGE_OR_QUOTE_RE.test(value) || value === "")
|
|
632
|
+
return `"${value.replace(DQUOTE_RE, '\\"')}"`;
|
|
633
|
+
return value;
|
|
634
|
+
}
|
|
635
|
+
function serializeFrontmatter(skill) {
|
|
636
|
+
const lines = ["---"];
|
|
637
|
+
lines.push(`name: ${yamlEscape(skill.name)}`);
|
|
638
|
+
lines.push(`description: ${yamlEscape(skill.description)}`);
|
|
639
|
+
if (skill.license)
|
|
640
|
+
lines.push(`license: ${yamlEscape(skill.license)}`);
|
|
641
|
+
if (skill.compatibility)
|
|
642
|
+
lines.push(`compatibility: ${yamlEscape(skill.compatibility)}`);
|
|
643
|
+
if (skill.allowedTools?.length)
|
|
644
|
+
lines.push(`allowed-tools: ${skill.allowedTools.join(" ")}`);
|
|
645
|
+
if (skill.metadata && Object.keys(skill.metadata).length > 0) {
|
|
646
|
+
lines.push("metadata:");
|
|
647
|
+
for (const [key, value] of Object.entries(skill.metadata)) {
|
|
648
|
+
lines.push(` ${key}: "${value.replace(DQUOTE_RE, '\\"')}"`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
lines.push("---");
|
|
652
|
+
return lines.join("\n");
|
|
653
|
+
}
|
|
654
|
+
function writeSkillToDisk(skill, targetDir) {
|
|
655
|
+
const result = validateSkillForWrite(skill);
|
|
656
|
+
if (!result.valid) {
|
|
657
|
+
const summary = result.errors.map((e) => ` - [${e.code}] ${e.message}`).join("\n");
|
|
658
|
+
throw new Error(`Cannot write invalid skill "${skill.name}":
|
|
659
|
+
${summary}`);
|
|
660
|
+
}
|
|
661
|
+
const skillDir = join2(targetDir, skill.name);
|
|
662
|
+
mkdirSync(skillDir, { recursive: true });
|
|
663
|
+
const frontmatter = serializeFrontmatter(skill);
|
|
664
|
+
const body = skill.instructions ? `
|
|
665
|
+
${skill.instructions}` : "";
|
|
666
|
+
const content = `${frontmatter}
|
|
667
|
+
${body}
|
|
668
|
+
`;
|
|
669
|
+
const skillPath = join2(skillDir, "SKILL.md");
|
|
670
|
+
writeFileSync(skillPath, content);
|
|
671
|
+
return skillPath;
|
|
672
|
+
}
|
|
673
|
+
function writeSkillsToDisk(skills, targetDir) {
|
|
674
|
+
mkdirSync(targetDir, { recursive: true });
|
|
675
|
+
for (const skill of skills) {
|
|
676
|
+
writeSkillToDisk(skill, targetDir);
|
|
677
|
+
}
|
|
678
|
+
return targetDir;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/skills/resolve.ts
|
|
682
|
+
import { mkdtempSync } from "fs";
|
|
683
|
+
import { tmpdir } from "os";
|
|
684
|
+
import { join as join3 } from "path";
|
|
685
|
+
async function resolveSkills(config) {
|
|
686
|
+
const sourcedPaths = [];
|
|
687
|
+
if (!config.skipDefaultPaths) {
|
|
688
|
+
sourcedPaths.push(...getDefaultScanPaths());
|
|
689
|
+
}
|
|
690
|
+
for (const p of config.scan ?? []) {
|
|
691
|
+
sourcedPaths.push({ path: p, source: inferSource(p) });
|
|
692
|
+
}
|
|
693
|
+
if (config.write?.length) {
|
|
694
|
+
const writeDir = mkdtempSync(join3(tmpdir(), "zidane-skills-"));
|
|
695
|
+
writeSkillsToDisk(config.write, writeDir);
|
|
696
|
+
sourcedPaths.push({ path: writeDir, source: "inline" });
|
|
697
|
+
}
|
|
698
|
+
let skills = await discoverSkills(sourcedPaths);
|
|
699
|
+
if (config.trustProjectSkills === false) {
|
|
700
|
+
skills = skills.filter((s) => s.source !== void 0 && s.source !== "project");
|
|
701
|
+
}
|
|
702
|
+
const exclude = new Set(config.exclude ?? []);
|
|
703
|
+
let filtered = skills.filter((s) => !exclude.has(s.name));
|
|
704
|
+
if (Array.isArray(config.enabled)) {
|
|
705
|
+
const allowlist = new Set(config.enabled);
|
|
706
|
+
filtered = filtered.filter((s) => allowlist.has(s.name));
|
|
707
|
+
}
|
|
708
|
+
return filtered;
|
|
709
|
+
}
|
|
710
|
+
function mergeSkillsConfig(harness, agent) {
|
|
711
|
+
if (!harness && !agent)
|
|
712
|
+
return void 0;
|
|
713
|
+
if (!harness)
|
|
714
|
+
return agent;
|
|
715
|
+
if (!agent)
|
|
716
|
+
return harness;
|
|
717
|
+
return {
|
|
718
|
+
// Agent-level takes precedence when explicitly set
|
|
719
|
+
enabled: agent.enabled !== void 0 ? agent.enabled : harness.enabled,
|
|
720
|
+
scan: [...harness.scan ?? [], ...agent.scan ?? []],
|
|
721
|
+
write: [...harness.write ?? [], ...agent.write ?? []],
|
|
722
|
+
exclude: [.../* @__PURE__ */ new Set([...harness.exclude ?? [], ...agent.exclude ?? []])],
|
|
723
|
+
skipDefaultPaths: agent.skipDefaultPaths ?? harness.skipDefaultPaths,
|
|
724
|
+
tool: agent.tool ?? harness.tool,
|
|
725
|
+
maxActive: agent.maxActive ?? harness.maxActive,
|
|
726
|
+
scriptTimeoutMs: agent.scriptTimeoutMs ?? harness.scriptTimeoutMs,
|
|
727
|
+
trustProjectSkills: agent.trustProjectSkills ?? harness.trustProjectSkills
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/skills/interpolate.ts
|
|
732
|
+
var SHELL_INTERPOLATION_RE = /!`([^`]+)`/g;
|
|
733
|
+
async function interpolateShellCommands(instructions, execution, handle) {
|
|
734
|
+
const matches = [...instructions.matchAll(SHELL_INTERPOLATION_RE)];
|
|
735
|
+
if (matches.length === 0)
|
|
736
|
+
return instructions;
|
|
737
|
+
const replacements = [];
|
|
738
|
+
for (const match of matches) {
|
|
739
|
+
const command = match[1];
|
|
740
|
+
const index = match.index;
|
|
741
|
+
const length = match[0].length;
|
|
742
|
+
try {
|
|
743
|
+
const result2 = await execution.exec(handle, command, { timeout: 30 });
|
|
744
|
+
const output = result2.exitCode === 0 ? result2.stdout.trim() : `[command failed (exit ${result2.exitCode}): ${result2.stderr.trim() || result2.stdout.trim()}]`;
|
|
745
|
+
replacements.push({ index, length, output });
|
|
746
|
+
} catch (err) {
|
|
747
|
+
replacements.push({ index, length, output: `[command error: ${err.message}]` });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
let result = instructions;
|
|
751
|
+
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
752
|
+
const { index, length, output } = replacements[i];
|
|
753
|
+
result = result.slice(0, index) + output + result.slice(index + length);
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export {
|
|
759
|
+
createSkillActivationState,
|
|
760
|
+
validateSkillName,
|
|
761
|
+
validateSkillForWrite,
|
|
762
|
+
validateResourcePath,
|
|
763
|
+
parseAllowedToolPattern,
|
|
764
|
+
matchesAllowedTool,
|
|
765
|
+
isToolAllowedByUnion,
|
|
766
|
+
IMPLICITLY_ALLOWED_SKILL_TOOLS,
|
|
767
|
+
installAllowedToolsGate,
|
|
768
|
+
buildCatalog,
|
|
769
|
+
parseFrontmatter,
|
|
770
|
+
parseSkillFile,
|
|
771
|
+
getDefaultScanPaths,
|
|
772
|
+
inferSource,
|
|
773
|
+
discoverSkills,
|
|
774
|
+
writeSkillToDisk,
|
|
775
|
+
writeSkillsToDisk,
|
|
776
|
+
resolveSkills,
|
|
777
|
+
mergeSkillsConfig,
|
|
778
|
+
interpolateShellCommands
|
|
779
|
+
};
|