@wrongstack/core 0.277.1 → 0.280.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-bridge-BFJ2ODzI.d.ts → agent-bridge-DXC6QDJ4.d.ts} +1 -1
- package/dist/{agent-subagent-runner-BimKihiC.d.ts → agent-subagent-runner-PoqNKiR4.d.ts} +563 -471
- package/dist/{compactor-D3BGw26y.d.ts → compactor-U3agvUIG.d.ts} +1 -1
- package/dist/{config-DAOjriz9.d.ts → config-Cr3312zc.d.ts} +102 -4
- package/dist/coordination/index.d.ts +1087 -998
- package/dist/coordination/index.js +12235 -12052
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +31 -30
- package/dist/defaults/index.js +403 -189
- package/dist/defaults/index.js.map +1 -1
- package/dist/{brain-CCfuEOdp.d.ts → events-Bs2fmldo.d.ts} +117 -112
- package/dist/execution/index.d.ts +27 -19
- package/dist/execution/index.js +216 -63
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +1 -1
- package/dist/execution/prompt-enhancer.js.map +1 -1
- package/dist/extension/index.d.ts +8 -7
- package/dist/{global-mailbox-Dr4cTKqL.d.ts → global-mailbox-Ct7IorLJ.d.ts} +84 -6
- package/dist/{goal-store-C1uH4srH.d.ts → goal-store-C4F6DjC0.d.ts} +1 -1
- package/dist/hq/index.d.ts +504 -7
- package/dist/hq/index.js +1069 -20
- package/dist/hq/index.js.map +1 -1
- package/dist/{index-DJXj-dcr.d.ts → index-kidebiDh.d.ts} +8 -5
- package/dist/{index-cMEmzCVN.d.ts → index-nP09-oP2.d.ts} +2 -2
- package/dist/index.d.ts +153 -76
- package/dist/index.js +5791 -3163
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +7 -6
- package/dist/kernel/index.d.ts +14 -13
- package/dist/kernel/index.js +31 -15
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mailbox-types-DTl7bRH3.d.ts → mailbox-types-BGZWrYTJ.d.ts} +38 -0
- package/dist/{mcp-servers-CFb60-pH.d.ts → mcp-servers-D910X5_r.d.ts} +3 -3
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-5Ufn7f2m.d.ts → models-registry-CLkoOcHk.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-CcrcncvG.d.ts → multi-agent-coordinator-CieyUoEL.d.ts} +1 -1
- package/dist/{null-fleet-bus-C9KsYyrI.d.ts → null-fleet-bus-DkdmZJ_W.d.ts} +464 -464
- package/dist/observability/index.d.ts +3 -2
- package/dist/{path-resolver-CEeX9I7O.d.ts → path-resolver-XfZ9eLxG.d.ts} +3 -3
- package/dist/{permission-DbsGOA1C.d.ts → permission-Dx6dIqS2.d.ts} +2 -7
- package/dist/{permission-policy-BpEea3r7.d.ts → permission-policy-C8vJcnX5.d.ts} +2 -2
- package/dist/{pipeline-CEjBjzVA.d.ts → pipeline-BwAP21_4.d.ts} +9 -4
- package/dist/{provider-model-resolve-BpfXp3Jj.d.ts → provider-model-resolve-CwQNZWt_.d.ts} +3 -3
- package/dist/{provider-runner-CnOSr5BN.d.ts → provider-runner-CYHFImzV.d.ts} +3 -3
- package/dist/{retry-policy-Git9WF6d.d.ts → retry-policy-D4feSLk3.d.ts} +1 -1
- package/dist/sdd/index.d.ts +11 -10
- package/dist/sdd/index.js +2 -2
- package/dist/sdd/index.js.map +1 -1
- package/dist/secret-scrubber-3MHDDAtm.d.ts +6 -0
- package/dist/{secret-vault-DDSMHqIm.d.ts → secret-vault-CImt2XrR.d.ts} +1 -1
- package/dist/security/index.d.ts +6 -5
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-Cq72C0Oy.d.ts → selector-Dy-MzKp1.d.ts} +1 -1
- package/dist/{session-event-bridge-DG94B3Bk.d.ts → session-event-bridge-CqdiGnfU.d.ts} +1 -1
- package/dist/{session-reader-BzT-iMQT.d.ts → session-reader-Hk0WbNm9.d.ts} +1 -1
- package/dist/{skill-DGIXCtdv.d.ts → skill-DHniprNl.d.ts} +15 -1
- package/dist/skills/index.d.ts +472 -26
- package/dist/skills/index.js +872 -129
- package/dist/skills/index.js.map +1 -1
- package/dist/storage/index.d.ts +27 -14
- package/dist/storage/index.js +264 -85
- package/dist/storage/index.js.map +1 -1
- package/dist/{strategy-compactor-Bt_ZH6R0.d.ts → strategy-compactor-CQwhbErd.d.ts} +32 -17
- package/dist/{todos-checkpoint-CH1pcua9.d.ts → todos-checkpoint-Bk2uP7Ex.d.ts} +6 -6
- package/dist/{context-DPlA6kid.d.ts → tool-BkOgs_KL.d.ts} +306 -286
- package/dist/{tool-executor-SVFq7IOR.d.ts → tool-executor-SiE1wlZo.d.ts} +9 -9
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/types/index.d.ts +22 -21
- package/dist/types/index.js +7 -9
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +30 -4
- package/dist/utils/index.js +50 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/{worktree-manager-C4YIf1Fa.d.ts → worktree-manager-BjOFF6bt.d.ts} +1 -1
- package/dist/{wstack-paths-_NrRovdr.d.ts → wstack-paths-CMl_cYgq.d.ts} +8 -0
- package/package.json +1 -1
- package/skills/mailbox-bridge/SKILL.md +1 -0
- package/skills/plugin-author/SKILL.md +350 -0
- package/skills/sdd/SKILL.md +134 -134
- package/skills/skill-creator/SKILL.md +45 -7
- package/skills/wrongstack-mailbox/SKILL.md +40 -21
package/dist/skills/index.js
CHANGED
|
@@ -1,37 +1,168 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import * as path4 from 'path';
|
|
1
|
+
import * as fs5 from 'fs/promises';
|
|
3
2
|
import * as os from 'os';
|
|
4
|
-
import
|
|
3
|
+
import * as path5 from 'path';
|
|
5
4
|
import { Readable } from 'stream';
|
|
6
5
|
import { pipeline } from 'stream/promises';
|
|
6
|
+
import { createGunzip } from 'zlib';
|
|
7
7
|
import { randomBytes } from 'crypto';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
8
9
|
|
|
9
|
-
// src/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
// src/skills/foreign-sources.ts
|
|
11
|
+
var FOREIGN_SKILL_TOOLS = [
|
|
12
|
+
{ id: "agents", subdir: "skills" },
|
|
13
|
+
// shared store (asm / agentskills.io ecosystem)
|
|
14
|
+
{ id: "codex", subdir: "skills" },
|
|
15
|
+
// OpenAI Codex CLI
|
|
16
|
+
{ id: "gemini", subdir: "skills" },
|
|
17
|
+
// Gemini CLI
|
|
18
|
+
{ id: "cursor", subdir: "skills" },
|
|
19
|
+
// Cursor (standard subdir, aligned with skills.sh ecosystem)
|
|
20
|
+
{ id: "qwen", subdir: "skills" },
|
|
21
|
+
// Qwen Code
|
|
22
|
+
{ id: "trae", subdir: "skills" },
|
|
23
|
+
// Trae
|
|
24
|
+
{ id: "windsurf", subdir: "skills" }
|
|
25
|
+
// Windsurf
|
|
26
|
+
];
|
|
27
|
+
var KNOWN_FOREIGN_IDS = new Set(FOREIGN_SKILL_TOOLS.map((t) => t.id));
|
|
28
|
+
function resolveForeignToolIds(opt) {
|
|
29
|
+
return resolveForeignToolIdsWithWarnings(opt).ids;
|
|
30
|
+
}
|
|
31
|
+
function resolveForeignToolIdsWithWarnings(opt) {
|
|
32
|
+
if (opt === false) return { ids: [], unknownIds: [] };
|
|
33
|
+
if (Array.isArray(opt)) {
|
|
34
|
+
const ids = [];
|
|
35
|
+
const unknownIds = [];
|
|
36
|
+
for (const id of opt) {
|
|
37
|
+
if (KNOWN_FOREIGN_IDS.has(id)) ids.push(id);
|
|
38
|
+
else unknownIds.push(id);
|
|
39
|
+
}
|
|
40
|
+
return { ids, unknownIds };
|
|
15
41
|
}
|
|
16
|
-
return
|
|
42
|
+
return { ids: FOREIGN_SKILL_TOOLS.map((t) => t.id), unknownIds: [] };
|
|
43
|
+
}
|
|
44
|
+
function securityScoreToTier(score) {
|
|
45
|
+
if (score == null || Number.isNaN(score)) return "low";
|
|
46
|
+
if (score < 30) return "low";
|
|
47
|
+
if (score < 70) return "medium";
|
|
48
|
+
return "high";
|
|
17
49
|
}
|
|
18
50
|
|
|
19
|
-
// src/
|
|
20
|
-
|
|
21
|
-
|
|
51
|
+
// src/skills/frontmatter.ts
|
|
52
|
+
var SCALAR_KEYS = /* @__PURE__ */ new Set(["name", "description", "version", "license", "compatibility"]);
|
|
53
|
+
function parseSkillFrontmatter(raw) {
|
|
54
|
+
const text = normalizeLineEndings(raw);
|
|
55
|
+
if (!text.startsWith("---")) return {};
|
|
56
|
+
const end = text.indexOf("\n---", 4);
|
|
57
|
+
if (end === -1) return {};
|
|
58
|
+
return parseFrontmatterBlock(text.slice(4, end));
|
|
59
|
+
}
|
|
60
|
+
function stripFrontmatter(raw) {
|
|
61
|
+
const text = normalizeLineEndings(raw);
|
|
62
|
+
if (!text.startsWith("---")) return text;
|
|
63
|
+
const end = text.indexOf("\n---", 4);
|
|
64
|
+
if (end === -1) return text;
|
|
65
|
+
let body = text.slice(end + 4);
|
|
66
|
+
if (body.startsWith("\n")) body = body.slice(1);
|
|
67
|
+
return body;
|
|
68
|
+
}
|
|
69
|
+
function normalizeLineEndings(s) {
|
|
70
|
+
return s.replace(/\r\n?/g, "\n");
|
|
71
|
+
}
|
|
72
|
+
function parseFrontmatterBlock(block) {
|
|
73
|
+
const out = {};
|
|
74
|
+
const lines = block.split("\n");
|
|
75
|
+
let i = 0;
|
|
76
|
+
while (i < lines.length) {
|
|
77
|
+
const line = lines[i] ?? "";
|
|
78
|
+
const m = /^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)$/.exec(line);
|
|
79
|
+
if (!m) {
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const key = m[1] ?? "";
|
|
84
|
+
const rest = (m[2] ?? "").trim();
|
|
85
|
+
if (key === "metadata") {
|
|
86
|
+
const map = {};
|
|
87
|
+
i++;
|
|
88
|
+
while (i < lines.length) {
|
|
89
|
+
const sub = lines[i] ?? "";
|
|
90
|
+
const sm = /^\s+([a-zA-Z0-9_.-]+):\s*(.*)$/.exec(sub);
|
|
91
|
+
if (!sm) break;
|
|
92
|
+
map[sm[1] ?? ""] = unquote((sm[2] ?? "").trim());
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
if (Object.keys(map).length > 0) out.metadata = map;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (rest === "|" || rest === ">") {
|
|
99
|
+
const collected = [];
|
|
100
|
+
i++;
|
|
101
|
+
while (i < lines.length) {
|
|
102
|
+
const sub = lines[i] ?? "";
|
|
103
|
+
if (sub === "" || sub.startsWith(" ") || sub.startsWith(" ")) {
|
|
104
|
+
collected.push(sub.replace(/^\s+/, ""));
|
|
105
|
+
i++;
|
|
106
|
+
} else break;
|
|
107
|
+
}
|
|
108
|
+
out[normalizeKey(key)] = collected.join("\n").trim();
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (key === "allowed-tools" || key === "allowedTools") {
|
|
112
|
+
out.allowedTools = rest.split(/[\s,]+/).filter(Boolean);
|
|
113
|
+
i++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (SCALAR_KEYS.has(key)) {
|
|
117
|
+
out[key] = unquote(rest);
|
|
118
|
+
i++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function normalizeKey(key) {
|
|
126
|
+
return key === "allowed-tools" ? "allowedTools" : key;
|
|
127
|
+
}
|
|
128
|
+
function unquote(s) {
|
|
129
|
+
if (s.length >= 2 && (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'"))) {
|
|
130
|
+
return s.slice(1, -1);
|
|
131
|
+
}
|
|
132
|
+
return s;
|
|
133
|
+
}
|
|
134
|
+
function isValidSkillNameFormat(name) {
|
|
135
|
+
return name.length >= 1 && name.length <= 64 && /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name);
|
|
136
|
+
}
|
|
137
|
+
function validateSkillName(name, parentDirName) {
|
|
138
|
+
const errors = [];
|
|
139
|
+
if (!name || name.trim().length === 0) {
|
|
140
|
+
errors.push("name is empty");
|
|
141
|
+
return errors;
|
|
142
|
+
}
|
|
143
|
+
if (name.length > 64) errors.push(`name is ${name.length} characters (max 64)`);
|
|
144
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
145
|
+
errors.push(
|
|
146
|
+
"name must be lowercase letters, digits, and single hyphens only (no leading/trailing/consecutive hyphens)"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (parentDirName !== void 0 && name !== parentDirName) {
|
|
150
|
+
errors.push(`name "${name}" must match its parent directory "${parentDirName}"`);
|
|
151
|
+
}
|
|
152
|
+
return errors;
|
|
22
153
|
}
|
|
23
154
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
24
|
-
const dir =
|
|
25
|
-
await
|
|
26
|
-
const tmp =
|
|
155
|
+
const dir = path5.dirname(targetPath);
|
|
156
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
157
|
+
const tmp = path5.join(dir, `.${path5.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
27
158
|
try {
|
|
28
159
|
if (typeof content === "string") {
|
|
29
|
-
await
|
|
160
|
+
await fs5.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
30
161
|
} else {
|
|
31
|
-
await
|
|
162
|
+
await fs5.writeFile(tmp, content, { flag: "wx" });
|
|
32
163
|
}
|
|
33
164
|
try {
|
|
34
|
-
const fh = await
|
|
165
|
+
const fh = await fs5.open(tmp, "r+");
|
|
35
166
|
try {
|
|
36
167
|
await fh.sync();
|
|
37
168
|
} finally {
|
|
@@ -41,24 +172,24 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
41
172
|
}
|
|
42
173
|
let mode;
|
|
43
174
|
try {
|
|
44
|
-
const stat3 = await
|
|
175
|
+
const stat3 = await fs5.stat(targetPath);
|
|
45
176
|
mode = stat3.mode & 511;
|
|
46
177
|
} catch {
|
|
47
178
|
mode = opts.mode;
|
|
48
179
|
}
|
|
49
180
|
if (mode !== void 0) {
|
|
50
|
-
await
|
|
181
|
+
await fs5.chmod(tmp, mode);
|
|
51
182
|
}
|
|
52
183
|
await renameWithRetry(tmp, targetPath);
|
|
53
184
|
if (mode !== void 0 && process.platform === "win32") {
|
|
54
185
|
try {
|
|
55
|
-
await
|
|
186
|
+
await fs5.chmod(targetPath, mode);
|
|
56
187
|
} catch {
|
|
57
188
|
}
|
|
58
189
|
}
|
|
59
190
|
} catch (err) {
|
|
60
191
|
try {
|
|
61
|
-
await
|
|
192
|
+
await fs5.unlink(tmp);
|
|
62
193
|
} catch {
|
|
63
194
|
}
|
|
64
195
|
throw err;
|
|
@@ -67,14 +198,14 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
67
198
|
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
68
199
|
async function renameWithRetry(from, to) {
|
|
69
200
|
if (process.platform !== "win32") {
|
|
70
|
-
await
|
|
201
|
+
await fs5.rename(from, to);
|
|
71
202
|
return;
|
|
72
203
|
}
|
|
73
204
|
const delays = [10, 25, 60, 120, 250];
|
|
74
205
|
let lastErr;
|
|
75
206
|
for (let i = 0; i <= delays.length; i++) {
|
|
76
207
|
try {
|
|
77
|
-
await
|
|
208
|
+
await fs5.rename(from, to);
|
|
78
209
|
return;
|
|
79
210
|
} catch (err) {
|
|
80
211
|
lastErr = err;
|
|
@@ -88,6 +219,21 @@ async function renameWithRetry(from, to) {
|
|
|
88
219
|
throw lastErr;
|
|
89
220
|
}
|
|
90
221
|
|
|
222
|
+
// src/utils/error.ts
|
|
223
|
+
function toErrorMessage(err) {
|
|
224
|
+
return err instanceof Error ? err.message : String(err);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/utils/expect-defined.ts
|
|
228
|
+
function expectDefined(value, label) {
|
|
229
|
+
if (value === null || value === void 0) {
|
|
230
|
+
const err = new Error("Expected value to be defined");
|
|
231
|
+
err.name = "ExpectDefinedError";
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
|
|
91
237
|
// src/types/errors.ts
|
|
92
238
|
var ERROR_CODES = {
|
|
93
239
|
// Provider
|
|
@@ -189,6 +335,118 @@ var FsError = class extends WrongStackError {
|
|
|
189
335
|
this.path = opts.path;
|
|
190
336
|
}
|
|
191
337
|
};
|
|
338
|
+
var FetchError = class extends WrongStackError {
|
|
339
|
+
status;
|
|
340
|
+
constructor(opts) {
|
|
341
|
+
super({
|
|
342
|
+
message: opts.message,
|
|
343
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
344
|
+
subsystem: "general",
|
|
345
|
+
severity: "error",
|
|
346
|
+
recoverable: opts.status === 429 || opts.status >= 500,
|
|
347
|
+
context: { status: opts.status, ...opts.context },
|
|
348
|
+
cause: opts.cause
|
|
349
|
+
});
|
|
350
|
+
this.name = "FetchError";
|
|
351
|
+
this.status = opts.status;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
var ParseError = class extends WrongStackError {
|
|
355
|
+
source;
|
|
356
|
+
constructor(opts) {
|
|
357
|
+
super({
|
|
358
|
+
message: opts.message,
|
|
359
|
+
code: ERROR_CODES.PARSE_FAILED,
|
|
360
|
+
subsystem: "general",
|
|
361
|
+
severity: "error",
|
|
362
|
+
recoverable: false,
|
|
363
|
+
context: { source: opts.source, ...opts.context },
|
|
364
|
+
cause: opts.cause
|
|
365
|
+
});
|
|
366
|
+
this.name = "ParseError";
|
|
367
|
+
this.source = opts.source;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// src/skills/limits.ts
|
|
372
|
+
var SKILL_LIMITS = {
|
|
373
|
+
/**
|
|
374
|
+
* Per-skill body cap when injecting a full skill body into the system prompt
|
|
375
|
+
* (eager mode), and when returning a skill body from the `skill` tool.
|
|
376
|
+
* ~4k tokens. Oversized bodies are truncated at a paragraph boundary.
|
|
377
|
+
*
|
|
378
|
+
* Consumers: `system-prompt-builder.capSkillBody`, `skill` tool body return.
|
|
379
|
+
*/
|
|
380
|
+
MAX_SKILL_BODY_CHARS: 16e3,
|
|
381
|
+
/**
|
|
382
|
+
* Per-resource cap when the `skill` tool loads a bundled file
|
|
383
|
+
* (`scripts/`, `references/`, `assets/`). ~8k tokens.
|
|
384
|
+
*
|
|
385
|
+
* Consumer: `skill` tool `loadResource`.
|
|
386
|
+
*/
|
|
387
|
+
MAX_RESOURCE_CHARS: 32e3,
|
|
388
|
+
/**
|
|
389
|
+
* Maximum number of bundled resource paths the `skill` tool lists in one
|
|
390
|
+
* response. Caps a pathological skill (huge `assets/` tree) from blowing up
|
|
391
|
+
* the tool result.
|
|
392
|
+
*
|
|
393
|
+
* Consumer: `skill` tool resource listing.
|
|
394
|
+
*/
|
|
395
|
+
MAX_LISTED_RESOURCES: 100,
|
|
396
|
+
/**
|
|
397
|
+
* Max size of a single installed skill file (SKILL.md or a bundled resource).
|
|
398
|
+
* Guards against a malicious registry skill shipping a multi-MB blob that
|
|
399
|
+
* then flows into the prompt. 100KB.
|
|
400
|
+
*
|
|
401
|
+
* Consumer: `SkillInstaller.install` / `importFromDir`.
|
|
402
|
+
*/
|
|
403
|
+
MAX_SKILL_FILE_SIZE: 100 * 1024,
|
|
404
|
+
/**
|
|
405
|
+
* Max size of a GitHub tarball downloaded by the skill installer. Guards
|
|
406
|
+
* against a repo accidentally (or maliciously) shipping a giant tarball that
|
|
407
|
+
* would be extracted into a temp dir. 50MB.
|
|
408
|
+
*
|
|
409
|
+
* Consumer: `downloadGitHubTarball`.
|
|
410
|
+
*/
|
|
411
|
+
MAX_TARBALL_SIZE: 50 * 1024 * 1024,
|
|
412
|
+
/**
|
|
413
|
+
* Default total char budget for skill bodies injected in eager mode when
|
|
414
|
+
* `config.skills.eagerMaxChars` is unset. ~6k tokens. Skills are injected
|
|
415
|
+
* highest-priority first; once the budget is exhausted the remaining skills
|
|
416
|
+
* are listed as a manifest the agent loads via the `skill` tool. Set very
|
|
417
|
+
* high to effectively disable budgeting.
|
|
418
|
+
*
|
|
419
|
+
* Consumer: `DefaultSystemPromptBuilder.buildMemoryAndSkills` (default for
|
|
420
|
+
* `skillEagerMaxChars`).
|
|
421
|
+
*/
|
|
422
|
+
EAGER_DEFAULT_MAX_CHARS: 24e3,
|
|
423
|
+
/**
|
|
424
|
+
* Auto-compact body limits (the token-saving fallback used when a skill has
|
|
425
|
+
* no hand-crafted `SKILL.save.md`). The Overview and Rules sections are
|
|
426
|
+
* extracted and trimmed to these char budgets, then the total is capped.
|
|
427
|
+
*
|
|
428
|
+
* Consumer: `DefaultSkillLoader.compactSkillBody`.
|
|
429
|
+
*/
|
|
430
|
+
COMPACT_OVERVIEW_MAX: 200,
|
|
431
|
+
COMPACT_RULES_MAX: 350,
|
|
432
|
+
COMPACT_TOTAL_MAX: 450,
|
|
433
|
+
/**
|
|
434
|
+
* Max length of a skill name (agentskills.io spec). Enforced by the
|
|
435
|
+
* frontmatter validator's format regex plus this length bound.
|
|
436
|
+
*
|
|
437
|
+
* Consumer: `isValidSkillNameFormat`.
|
|
438
|
+
*/
|
|
439
|
+
SKILL_NAME_MAX_LEN: 64,
|
|
440
|
+
/**
|
|
441
|
+
* Soft line limit for a SKILL.md body, per the bundled `skill-creator` rule.
|
|
442
|
+
* Not enforced in code (a skill can exceed it) — surfaced by `/skill-gen
|
|
443
|
+
* validate` and the `skill-creator` skill as guidance to move deep material
|
|
444
|
+
* into `references/`.
|
|
445
|
+
*
|
|
446
|
+
* Consumer: `skill-creator` skill, `/skill-gen validate` advisory.
|
|
447
|
+
*/
|
|
448
|
+
SKILL_BODY_LINE_LIMIT: 500
|
|
449
|
+
};
|
|
192
450
|
|
|
193
451
|
// src/skills/github-fetcher.ts
|
|
194
452
|
function parseSkillRef(input) {
|
|
@@ -214,32 +472,58 @@ function parseSkillRef(input) {
|
|
|
214
472
|
}
|
|
215
473
|
return { owner: expectDefined(parts[0]), repo: expectDefined(parts[1]), ref };
|
|
216
474
|
}
|
|
217
|
-
|
|
475
|
+
function resolveGitHubToken(env = process.env) {
|
|
476
|
+
const raw = env["WRONGSTACK_GITHUB_TOKEN"] ?? env["GITHUB_TOKEN"] ?? env["GH_TOKEN"];
|
|
477
|
+
if (!raw) return void 0;
|
|
478
|
+
const trimmed = raw.trim();
|
|
479
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
480
|
+
}
|
|
218
481
|
async function downloadGitHubTarball(parsed) {
|
|
219
482
|
const url = `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/tarball/${parsed.ref}`;
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
483
|
+
const token = resolveGitHubToken();
|
|
484
|
+
const headers = {
|
|
485
|
+
Accept: "application/vnd.github+json",
|
|
486
|
+
"User-Agent": "wrongstack-skill-installer"
|
|
487
|
+
};
|
|
488
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
489
|
+
let response;
|
|
490
|
+
try {
|
|
491
|
+
response = await fetch(url, {
|
|
492
|
+
signal: AbortSignal.timeout(3e4),
|
|
493
|
+
headers,
|
|
494
|
+
redirect: "follow"
|
|
495
|
+
});
|
|
496
|
+
} catch (err) {
|
|
497
|
+
throw new FetchError({
|
|
498
|
+
message: `Network error fetching ${parsed.owner}/${parsed.repo}: ${err instanceof Error ? err.message : String(err)}`,
|
|
499
|
+
status: 0,
|
|
500
|
+
context: { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, op: "tarball" },
|
|
501
|
+
cause: err
|
|
502
|
+
});
|
|
503
|
+
}
|
|
228
504
|
if (!response.ok) {
|
|
229
505
|
if (response.status === 404) {
|
|
230
506
|
throw new WrongStackError({
|
|
231
|
-
message: `Repository not found: ${parsed.owner}/${parsed.repo}` + (parsed.ref !== "main" ? ` (ref: ${parsed.ref})` : ""),
|
|
507
|
+
message: `Repository not found: ${parsed.owner}/${parsed.repo}` + (parsed.ref !== "main" ? ` (ref: ${parsed.ref})` : "") + (token ? "" : ". If this is a private repo, set GITHUB_TOKEN (or GH_TOKEN)."),
|
|
232
508
|
code: ERROR_CODES.UNKNOWN,
|
|
233
509
|
subsystem: "general",
|
|
234
510
|
context: { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, status: 404 }
|
|
235
511
|
});
|
|
236
512
|
}
|
|
237
513
|
if (response.status === 403) {
|
|
514
|
+
const remaining = response.headers.get("x-ratelimit-remaining");
|
|
515
|
+
const isRateLimited = remaining === "0";
|
|
238
516
|
throw new WrongStackError({
|
|
239
|
-
message: `Access denied: ${parsed.owner}/${parsed.repo}. The repository may be private or rate-limited.`,
|
|
517
|
+
message: isRateLimited ? `GitHub API rate limit exceeded. Set GITHUB_TOKEN (or GH_TOKEN) to use the higher authenticated limit.` : `Access denied: ${parsed.owner}/${parsed.repo}. The repository may be private (set GITHUB_TOKEN to install private repos) or rate-limited.`,
|
|
240
518
|
code: ERROR_CODES.UNKNOWN,
|
|
241
519
|
subsystem: "general",
|
|
242
|
-
context: {
|
|
520
|
+
context: {
|
|
521
|
+
owner: parsed.owner,
|
|
522
|
+
repo: parsed.repo,
|
|
523
|
+
status: 403,
|
|
524
|
+
rateLimited: isRateLimited,
|
|
525
|
+
hasToken: Boolean(token)
|
|
526
|
+
}
|
|
243
527
|
});
|
|
244
528
|
}
|
|
245
529
|
throw new WrongStackError({
|
|
@@ -250,20 +534,20 @@ async function downloadGitHubTarball(parsed) {
|
|
|
250
534
|
});
|
|
251
535
|
}
|
|
252
536
|
const contentLength = response.headers.get("content-length");
|
|
253
|
-
if (contentLength && Number.parseInt(contentLength, 10) > MAX_TARBALL_SIZE) {
|
|
537
|
+
if (contentLength && Number.parseInt(contentLength, 10) > SKILL_LIMITS.MAX_TARBALL_SIZE) {
|
|
254
538
|
throw new WrongStackError({
|
|
255
|
-
message: `Tarball too large (${(Number.parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB). Max: ${MAX_TARBALL_SIZE / 1024 / 1024}MB`,
|
|
539
|
+
message: `Tarball too large (${(Number.parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB). Max: ${SKILL_LIMITS.MAX_TARBALL_SIZE / 1024 / 1024}MB`,
|
|
256
540
|
code: ERROR_CODES.UNKNOWN,
|
|
257
541
|
subsystem: "general",
|
|
258
542
|
context: {
|
|
259
543
|
owner: parsed.owner,
|
|
260
544
|
repo: parsed.repo,
|
|
261
545
|
contentLengthBytes: Number.parseInt(contentLength, 10),
|
|
262
|
-
maxBytes: MAX_TARBALL_SIZE
|
|
546
|
+
maxBytes: SKILL_LIMITS.MAX_TARBALL_SIZE
|
|
263
547
|
}
|
|
264
548
|
});
|
|
265
549
|
}
|
|
266
|
-
const tempDir = await
|
|
550
|
+
const tempDir = await fs5.mkdtemp(path5.join(os.tmpdir(), "wskill-"));
|
|
267
551
|
try {
|
|
268
552
|
if (!response.body) {
|
|
269
553
|
throw new WrongStackError({
|
|
@@ -285,7 +569,7 @@ async function downloadGitHubTarball(parsed) {
|
|
|
285
569
|
await extractTar(tarBuf, tempDir);
|
|
286
570
|
return { tempDir };
|
|
287
571
|
} catch (err) {
|
|
288
|
-
await
|
|
572
|
+
await fs5.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
289
573
|
});
|
|
290
574
|
throw err;
|
|
291
575
|
}
|
|
@@ -302,25 +586,25 @@ async function extractTar(buf, destDir) {
|
|
|
302
586
|
const fullPath = prefix ? `${prefix}/${name}` : name;
|
|
303
587
|
const relPath = stripTopDir(fullPath);
|
|
304
588
|
if (relPath && relPath !== "." && relPath !== "..") {
|
|
305
|
-
const destPath =
|
|
306
|
-
const resolvedDest =
|
|
307
|
-
const resolvedRoot =
|
|
308
|
-
if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot +
|
|
589
|
+
const destPath = path5.join(destDir, relPath);
|
|
590
|
+
const resolvedDest = path5.resolve(destPath);
|
|
591
|
+
const resolvedRoot = path5.resolve(destDir);
|
|
592
|
+
if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot + path5.sep)) {
|
|
309
593
|
offset += 512 + Math.ceil(size / 512) * 512;
|
|
310
594
|
continue;
|
|
311
595
|
}
|
|
312
596
|
if (typeflag === 53 || typeflag === 0) {
|
|
313
597
|
if (relPath.endsWith("/") || typeflag === 53) {
|
|
314
|
-
await
|
|
598
|
+
await fs5.mkdir(destPath, { recursive: true });
|
|
315
599
|
}
|
|
316
600
|
}
|
|
317
601
|
if ((typeflag === 48 || typeflag === 0 || typeflag === 0) && size > 0) {
|
|
318
|
-
const dir =
|
|
319
|
-
await
|
|
602
|
+
const dir = path5.dirname(destPath);
|
|
603
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
320
604
|
const dataStart = offset + 512;
|
|
321
605
|
const dataEnd = dataStart + size;
|
|
322
606
|
if (dataEnd > buf.length) break;
|
|
323
|
-
await
|
|
607
|
+
await fs5.writeFile(destPath, buf.subarray(dataStart, dataEnd));
|
|
324
608
|
}
|
|
325
609
|
}
|
|
326
610
|
offset += 512 + Math.ceil(size / 512) * 512;
|
|
@@ -347,7 +631,7 @@ var SkillManifestStore = class {
|
|
|
347
631
|
async read() {
|
|
348
632
|
if (this.cache) return this.cache;
|
|
349
633
|
try {
|
|
350
|
-
const raw = await
|
|
634
|
+
const raw = await fs5.readFile(this.manifestPath, "utf8");
|
|
351
635
|
const data = JSON.parse(raw);
|
|
352
636
|
if (!Array.isArray(data.skills)) {
|
|
353
637
|
this.cache = { skills: [] };
|
|
@@ -360,25 +644,21 @@ var SkillManifestStore = class {
|
|
|
360
644
|
return this.cache;
|
|
361
645
|
}
|
|
362
646
|
async write(data) {
|
|
363
|
-
const dir =
|
|
364
|
-
await
|
|
647
|
+
const dir = path5.dirname(this.manifestPath);
|
|
648
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
365
649
|
await atomicWrite(this.manifestPath, JSON.stringify(data, null, 2) + "\n");
|
|
366
650
|
this.cache = data;
|
|
367
651
|
}
|
|
368
652
|
async addEntry(entry) {
|
|
369
653
|
const data = await this.read();
|
|
370
|
-
data.skills = data.skills.filter(
|
|
371
|
-
(s) => !(s.name === entry.name && s.scope === entry.scope)
|
|
372
|
-
);
|
|
654
|
+
data.skills = data.skills.filter((s) => !(s.name === entry.name && s.scope === entry.scope));
|
|
373
655
|
data.skills.push(entry);
|
|
374
656
|
await this.write(data);
|
|
375
657
|
}
|
|
376
658
|
async removeEntry(name, scope) {
|
|
377
659
|
const data = await this.read();
|
|
378
660
|
const before = data.skills.length;
|
|
379
|
-
data.skills = data.skills.filter(
|
|
380
|
-
(s) => !(s.name === name && s.scope === scope)
|
|
381
|
-
);
|
|
661
|
+
data.skills = data.skills.filter((s) => !(s.name === name && s.scope === scope));
|
|
382
662
|
if (data.skills.length === before) return false;
|
|
383
663
|
await this.write(data);
|
|
384
664
|
return true;
|
|
@@ -401,25 +681,363 @@ var SkillManifestStore = class {
|
|
|
401
681
|
}
|
|
402
682
|
};
|
|
403
683
|
|
|
404
|
-
// src/skills/
|
|
405
|
-
var
|
|
684
|
+
// src/skills/registry/github-direct-adapter.ts
|
|
685
|
+
var githubDirectAdapter = {
|
|
686
|
+
id: "github",
|
|
687
|
+
displayName: "GitHub (direct)",
|
|
688
|
+
async search(_query, _opts = {}) {
|
|
689
|
+
return { adapterId: "github", results: [], hasMore: false };
|
|
690
|
+
},
|
|
691
|
+
resolveInstallRef(registryId) {
|
|
692
|
+
const trimmed = registryId.trim();
|
|
693
|
+
if (!trimmed.includes("/")) {
|
|
694
|
+
return trimmed;
|
|
695
|
+
}
|
|
696
|
+
return trimmed;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// src/skills/registry/skills-sh-adapter.ts
|
|
701
|
+
var DEFAULT_SKILLS_SH_URL = "https://skills.sh";
|
|
702
|
+
var SEARCH_TIMEOUT_MS = 15e3;
|
|
703
|
+
var MAX_PAGE_SIZE = 50;
|
|
704
|
+
var defaultFetcher = async (url) => {
|
|
705
|
+
let res;
|
|
706
|
+
try {
|
|
707
|
+
res = await fetch(url, {
|
|
708
|
+
signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS),
|
|
709
|
+
headers: {
|
|
710
|
+
Accept: "application/json",
|
|
711
|
+
"User-Agent": "wrongstack-skill-installer"
|
|
712
|
+
},
|
|
713
|
+
redirect: "follow"
|
|
714
|
+
});
|
|
715
|
+
} catch (err) {
|
|
716
|
+
throw new FetchError({
|
|
717
|
+
message: `Network error querying skill registry: ${err instanceof Error ? err.message : String(err)}`,
|
|
718
|
+
status: 0,
|
|
719
|
+
context: { url, op: "skills.sh.search" },
|
|
720
|
+
cause: err
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
if (!res.ok) {
|
|
724
|
+
throw new FetchError({
|
|
725
|
+
message: `Skill registry returned ${res.status} ${res.statusText}`,
|
|
726
|
+
status: res.status,
|
|
727
|
+
context: { url, op: "skills.sh.search" }
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
return await res.json();
|
|
732
|
+
} catch (err) {
|
|
733
|
+
throw new ParseError({
|
|
734
|
+
message: `Skill registry response was not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
735
|
+
source: "skills.sh.search",
|
|
736
|
+
context: { url },
|
|
737
|
+
cause: err
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
function createSkillsShAdapter(opts = {}) {
|
|
742
|
+
const baseUrl = (opts.baseUrl ?? DEFAULT_SKILLS_SH_URL).replace(/\/+$/, "");
|
|
743
|
+
const fetcher = opts.fetcher ?? defaultFetcher;
|
|
744
|
+
const id = "skills.sh";
|
|
745
|
+
return {
|
|
746
|
+
id,
|
|
747
|
+
displayName: "skills.sh",
|
|
748
|
+
async search(query, sopts = {}) {
|
|
749
|
+
const page = Math.max(1, sopts.page ?? 1);
|
|
750
|
+
const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, sopts.pageSize ?? 20));
|
|
751
|
+
const q = query.trim();
|
|
752
|
+
if (!q) return { adapterId: id, results: [], hasMore: false };
|
|
753
|
+
const url = `${baseUrl}/api/skills?query=${encodeURIComponent(q)}&page=${page}&pageSize=${pageSize}&sortBy=installs`;
|
|
754
|
+
const raw = await fetcher(url);
|
|
755
|
+
const results = parseResults(raw, url);
|
|
756
|
+
return {
|
|
757
|
+
adapterId: id,
|
|
758
|
+
results,
|
|
759
|
+
hasMore: results.length === pageSize
|
|
760
|
+
};
|
|
761
|
+
},
|
|
762
|
+
resolveInstallRef(registryId) {
|
|
763
|
+
const trimmed = registryId.trim();
|
|
764
|
+
if (!trimmed) {
|
|
765
|
+
throw new ParseError({
|
|
766
|
+
message: "Empty skills.sh registry id.",
|
|
767
|
+
source: "skills.sh.resolveInstallRef"
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
const atIdx = trimmed.indexOf("@");
|
|
771
|
+
const repoPart = atIdx > 0 ? trimmed.slice(0, atIdx) : trimmed;
|
|
772
|
+
const segs = repoPart.split("/").filter(Boolean);
|
|
773
|
+
if (segs.length !== 2) {
|
|
774
|
+
throw new ParseError({
|
|
775
|
+
message: `Invalid skills.sh id "${registryId}". Expected "<owner>/<repo>" or "<owner>/<repo>@<ref>".`,
|
|
776
|
+
source: "skills.sh.resolveInstallRef",
|
|
777
|
+
context: { registryId }
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return trimmed;
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function parseResults(raw, url) {
|
|
785
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
786
|
+
throw new ParseError({
|
|
787
|
+
message: "Skill registry response was not a JSON object.",
|
|
788
|
+
source: "skills.sh.search",
|
|
789
|
+
context: { url }
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
const results = raw.results;
|
|
793
|
+
if (!Array.isArray(results)) {
|
|
794
|
+
return [];
|
|
795
|
+
}
|
|
796
|
+
const out = [];
|
|
797
|
+
for (let i = 0; i < results.length; i++) {
|
|
798
|
+
const entry = results[i];
|
|
799
|
+
if (!entry || typeof entry !== "object") continue;
|
|
800
|
+
const name = strField(entry, "name") ?? strField(entry, "slug");
|
|
801
|
+
const description = strField(entry, "description") ?? "";
|
|
802
|
+
const owner = strField(entry, "owner") ?? strField(entry, "author");
|
|
803
|
+
const repo = strField(entry, "repo") ?? strField(entry, "repository");
|
|
804
|
+
if (!name || !owner || !repo) continue;
|
|
805
|
+
const ref = strField(entry, "ref") ?? strField(entry, "branch") ?? strField(entry, "tag");
|
|
806
|
+
const installRef = ref ? `${owner}/${repo}@${ref}` : `${owner}/${repo}`;
|
|
807
|
+
out.push({
|
|
808
|
+
id: strField(entry, "id") ?? `${owner}/${repo}`,
|
|
809
|
+
name,
|
|
810
|
+
description,
|
|
811
|
+
author: owner,
|
|
812
|
+
installs: numField(entry, "installs") ?? numField(entry, "installCount"),
|
|
813
|
+
stars: numField(entry, "stars") ?? numField(entry, "starCount"),
|
|
814
|
+
securityScore: numField(entry, "securityScore") ?? numField(entry, "score"),
|
|
815
|
+
updatedAt: strField(entry, "updatedAt") ?? strField(entry, "updated_at"),
|
|
816
|
+
installRef
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
return out;
|
|
820
|
+
}
|
|
821
|
+
function strField(rec, key) {
|
|
822
|
+
const v = rec[key];
|
|
823
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
824
|
+
}
|
|
825
|
+
function numField(rec, key) {
|
|
826
|
+
const v = rec[key];
|
|
827
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
828
|
+
if (typeof v === "string" && /^\d+(\.\d+)?$/.test(v.trim())) {
|
|
829
|
+
const n = Number(v);
|
|
830
|
+
if (Number.isFinite(n)) return n;
|
|
831
|
+
}
|
|
832
|
+
return void 0;
|
|
833
|
+
}
|
|
834
|
+
async function validateSkillNameAvailable(name, loader) {
|
|
835
|
+
const formatViolations = validateSkillName(name);
|
|
836
|
+
const conflicts = loader ? (await loader.listEntries()).filter((e) => e.name === name) : [];
|
|
837
|
+
const formatOk = formatViolations.length === 0;
|
|
838
|
+
const collisionBlocks = conflicts.some((c) => c.source === "project" || c.source === "user");
|
|
839
|
+
return {
|
|
840
|
+
ok: formatOk && !collisionBlocks,
|
|
841
|
+
formatViolations,
|
|
842
|
+
conflicts
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function generateSkillSkeleton(opts) {
|
|
846
|
+
const name = opts.name.trim();
|
|
847
|
+
if (!isValidSkillNameFormat(name)) {
|
|
848
|
+
throw new WrongStackError({
|
|
849
|
+
message: `Cannot generate skeleton: invalid skill name "${name}". Names must be kebab-case (a-z0-9 and hyphens), \u226464 chars.`,
|
|
850
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
851
|
+
subsystem: "general",
|
|
852
|
+
context: { name }
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
const description = opts.description.trim();
|
|
856
|
+
const triggers = (opts.triggerKeywords ?? []).map((k) => k.trim()).filter(Boolean).map((k) => `"${k}"`).join(", ");
|
|
857
|
+
const triggerLine = triggers ? `
|
|
858
|
+
Triggers: user says ${triggers}.` : "";
|
|
859
|
+
const version = opts.version?.trim() || "1.0.0";
|
|
860
|
+
const title = name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
861
|
+
return `---
|
|
862
|
+
name: ${name}
|
|
863
|
+
description: |
|
|
864
|
+
${description || `Use this skill when <trigger situation>.`}${triggerLine}
|
|
865
|
+
version: ${version}
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
# ${title}
|
|
869
|
+
|
|
870
|
+
## Overview
|
|
871
|
+
|
|
872
|
+
${description || "One-line description of what this skill does."}
|
|
873
|
+
|
|
874
|
+
## Rules
|
|
875
|
+
|
|
876
|
+
1. Rule one
|
|
877
|
+
2. Rule two
|
|
878
|
+
|
|
879
|
+
## Patterns
|
|
880
|
+
|
|
881
|
+
### Do
|
|
882
|
+
|
|
883
|
+
\`\`\`ts
|
|
884
|
+
// good example
|
|
885
|
+
\`\`\`
|
|
886
|
+
|
|
887
|
+
### Don't
|
|
888
|
+
|
|
889
|
+
\`\`\`ts
|
|
890
|
+
// bad example
|
|
891
|
+
\`\`\`
|
|
892
|
+
|
|
893
|
+
## Skills in scope
|
|
894
|
+
|
|
895
|
+
- \`other-skill\` \u2014 for delegation when this skill needs help
|
|
896
|
+
`;
|
|
897
|
+
}
|
|
898
|
+
function extractSkillFromPrompt(prompt) {
|
|
899
|
+
const text = prompt.trim();
|
|
900
|
+
if (!text) {
|
|
901
|
+
return { suggestedName: "", description: "", body: "", triggerKeywords: [] };
|
|
902
|
+
}
|
|
903
|
+
const headingMatch = text.match(/^#{1,6}\s+(.+?)\s*$/m);
|
|
904
|
+
const firstLine = text.split("\n").find((l) => l.trim().length > 0)?.trim() ?? text;
|
|
905
|
+
const titleSource = headingMatch?.[1] ?? firstLine.replace(/^#+\s*/, "");
|
|
906
|
+
const suggestedName = toKebab(titleSource).slice(0, SKILL_LIMITS.SKILL_NAME_MAX_LEN);
|
|
907
|
+
const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
908
|
+
const description = paragraphs[0]?.replace(/^#{1,6}\s+/m, "").trim() ?? titleSource;
|
|
909
|
+
const body = paragraphs.slice(1).join("\n\n");
|
|
910
|
+
const quoted = [...text.matchAll(/"([^"]{2,40})"/g)].map((m) => m[1]).filter((q) => typeof q === "string").map((q) => q.toLowerCase());
|
|
911
|
+
const titleWords = titleSource.split(/\W+/).map((w) => w.toLowerCase()).filter((w) => w.length > 3 && !STOPWORDS.has(w));
|
|
912
|
+
const triggerKeywords = dedupe([...quoted, ...titleWords]).slice(0, 8);
|
|
913
|
+
return { suggestedName, description, body, triggerKeywords };
|
|
914
|
+
}
|
|
915
|
+
async function openInEditor(filePath, env = process.env) {
|
|
916
|
+
const editor = env["VISUAL"]?.trim() && env["VISUAL"] || env["EDITOR"]?.trim() && env["EDITOR"] || defaultEditor();
|
|
917
|
+
if (!editor) {
|
|
918
|
+
throw new WrongStackError({
|
|
919
|
+
message: "No editor found. Set the EDITOR or VISUAL environment variable (e.g. `export EDITOR=code` or `export EDITOR=vim`).",
|
|
920
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
921
|
+
subsystem: "general",
|
|
922
|
+
context: { filePath }
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
const shell = process.platform === "win32";
|
|
926
|
+
const parts = editor.split(/\s+/).filter(Boolean);
|
|
927
|
+
if (parts.length === 0) {
|
|
928
|
+
throw new WrongStackError({
|
|
929
|
+
message: "Resolved editor command is empty.",
|
|
930
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
931
|
+
subsystem: "general",
|
|
932
|
+
context: { filePath }
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
const child = spawn(parts[0], [...parts.slice(1), filePath], {
|
|
936
|
+
stdio: "ignore",
|
|
937
|
+
detached: true,
|
|
938
|
+
shell
|
|
939
|
+
});
|
|
940
|
+
child.unref();
|
|
941
|
+
}
|
|
942
|
+
async function writeSkeletonSkill(skillsDir, body, opts = {}) {
|
|
943
|
+
const fm = parseSkillFrontmatter(body);
|
|
944
|
+
const name = fm.name;
|
|
945
|
+
if (!name || !isValidSkillNameFormat(name)) {
|
|
946
|
+
throw new WrongStackError({
|
|
947
|
+
message: "Skeleton body has no valid `name` in its frontmatter.",
|
|
948
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
949
|
+
subsystem: "general"
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
const skillDir = path5.join(skillsDir, name);
|
|
953
|
+
const skillFile = path5.join(skillDir, "SKILL.md");
|
|
954
|
+
if (!opts.overwrite) {
|
|
955
|
+
try {
|
|
956
|
+
await fs5.access(skillFile);
|
|
957
|
+
throw new WrongStackError({
|
|
958
|
+
message: `A skill already exists at ${skillFile}. Use --force to overwrite.`,
|
|
959
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
960
|
+
subsystem: "general",
|
|
961
|
+
context: { skillFile }
|
|
962
|
+
});
|
|
963
|
+
} catch (err) {
|
|
964
|
+
if (err instanceof WrongStackError) throw err;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
await fs5.mkdir(skillDir, { recursive: true });
|
|
968
|
+
await fs5.writeFile(skillFile, body, "utf8");
|
|
969
|
+
return skillFile;
|
|
970
|
+
}
|
|
971
|
+
function bodyLineAdvisory(body) {
|
|
972
|
+
const lines = body.split("\n").length;
|
|
973
|
+
return { lines, over: lines > SKILL_LIMITS.SKILL_BODY_LINE_LIMIT };
|
|
974
|
+
}
|
|
975
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
976
|
+
"the",
|
|
977
|
+
"and",
|
|
978
|
+
"for",
|
|
979
|
+
"with",
|
|
980
|
+
"that",
|
|
981
|
+
"this",
|
|
982
|
+
"from",
|
|
983
|
+
"your",
|
|
984
|
+
"have",
|
|
985
|
+
"will",
|
|
986
|
+
"are",
|
|
987
|
+
"was",
|
|
988
|
+
"were",
|
|
989
|
+
"been",
|
|
990
|
+
"into",
|
|
991
|
+
"when",
|
|
992
|
+
"use",
|
|
993
|
+
"skill",
|
|
994
|
+
"about"
|
|
995
|
+
]);
|
|
996
|
+
function toKebab(s) {
|
|
997
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SKILL_LIMITS.SKILL_NAME_MAX_LEN) || "skill";
|
|
998
|
+
}
|
|
999
|
+
function dedupe(arr) {
|
|
1000
|
+
return [...new Set(arr)];
|
|
1001
|
+
}
|
|
1002
|
+
function defaultEditor() {
|
|
1003
|
+
if (process.platform === "win32") return "notepad";
|
|
1004
|
+
if (process.platform === "darwin") return "open";
|
|
1005
|
+
return void 0;
|
|
1006
|
+
}
|
|
1007
|
+
var MAX_SKILL_FILE_SIZE = SKILL_LIMITS.MAX_SKILL_FILE_SIZE;
|
|
406
1008
|
var SkillInstaller = class {
|
|
407
1009
|
opts;
|
|
408
1010
|
manifest;
|
|
1011
|
+
adapters;
|
|
409
1012
|
constructor(opts) {
|
|
410
1013
|
this.opts = opts;
|
|
411
1014
|
this.manifest = new SkillManifestStore(opts.manifestPath);
|
|
1015
|
+
this.adapters = opts.registryAdapters?.length ? opts.registryAdapters : [githubDirectAdapter];
|
|
412
1016
|
}
|
|
413
1017
|
/**
|
|
414
|
-
* Install skills from a
|
|
415
|
-
*
|
|
1018
|
+
* Install skills from a skill reference.
|
|
1019
|
+
*
|
|
1020
|
+
* Accepts two ref formats:
|
|
1021
|
+
* - `user/repo[@ref]` — direct GitHub install (original path)
|
|
1022
|
+
* - `<adapterId>:<registryId>` — registry-resolved (e.g.
|
|
1023
|
+
* `skills.sh:owner/repo@v1`); the
|
|
1024
|
+
* adapter resolves it to a `user/repo`
|
|
1025
|
+
* ref and the install proceeds as above.
|
|
1026
|
+
*
|
|
1027
|
+
* Supports both single-skill repos (SKILL.md at root) and multi-skill repos
|
|
1028
|
+
* (skills/ subdirectory). The manifest records the GitHub source as
|
|
1029
|
+
* `github:owner/repo` (so `/skill-update` keeps working) plus the originating
|
|
1030
|
+
* registry in `registryFrom` when the install came through a registry.
|
|
416
1031
|
*/
|
|
417
1032
|
async install(refInput, opts) {
|
|
418
|
-
const
|
|
1033
|
+
const resolved = this.resolveRef(refInput);
|
|
1034
|
+
const parsed = parseSkillRef(resolved.installRef);
|
|
419
1035
|
const scope = opts?.global ? "user" : "project";
|
|
420
1036
|
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
421
1037
|
const source = `github:${parsed.owner}/${parsed.repo}`;
|
|
422
|
-
this.opts.log?.(
|
|
1038
|
+
this.opts.log?.(
|
|
1039
|
+
resolved.fromRegistry ? `Resolving ${refInput} \u2192 ${resolved.installRef} (${resolved.adapterId}), downloading...` : `Downloading ${parsed.owner}/${parsed.repo}@${parsed.ref}...`
|
|
1040
|
+
);
|
|
423
1041
|
const { tempDir } = await downloadGitHubTarball(parsed);
|
|
424
1042
|
try {
|
|
425
1043
|
const skills = await this.detectSkills(tempDir);
|
|
@@ -439,14 +1057,14 @@ var SkillInstaller = class {
|
|
|
439
1057
|
this.opts.log?.(`Overwriting existing skill "${skill.name}" (${scope})...`);
|
|
440
1058
|
await this.removeSkillFiles(skill.name, scope);
|
|
441
1059
|
}
|
|
442
|
-
const destDir =
|
|
443
|
-
await
|
|
1060
|
+
const destDir = path5.join(targetDir, skill.name);
|
|
1061
|
+
await fs5.mkdir(destDir, { recursive: true });
|
|
444
1062
|
const copiedFiles = [];
|
|
445
1063
|
for (const file of skill.files) {
|
|
446
|
-
const srcPath =
|
|
447
|
-
const destPath =
|
|
448
|
-
const
|
|
449
|
-
if (!
|
|
1064
|
+
const srcPath = path5.join(skill.baseDir, file);
|
|
1065
|
+
const destPath = path5.join(destDir, file);
|
|
1066
|
+
const resolved2 = path5.resolve(destPath);
|
|
1067
|
+
if (!resolved2.startsWith(path5.resolve(destDir))) {
|
|
450
1068
|
throw new FsError({
|
|
451
1069
|
message: `Path traversal detected in skill file: ${file}`,
|
|
452
1070
|
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
@@ -454,7 +1072,7 @@ var SkillInstaller = class {
|
|
|
454
1072
|
context: { reason: "path_traversal", skillName: skill.name }
|
|
455
1073
|
});
|
|
456
1074
|
}
|
|
457
|
-
const stat3 = await
|
|
1075
|
+
const stat3 = await fs5.stat(srcPath);
|
|
458
1076
|
if (stat3.size > MAX_SKILL_FILE_SIZE) {
|
|
459
1077
|
throw new FsError({
|
|
460
1078
|
message: `Skill file "${file}" is too large (${(stat3.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`,
|
|
@@ -463,8 +1081,8 @@ var SkillInstaller = class {
|
|
|
463
1081
|
context: { skillName: skill.name, fileSize: stat3.size, maxSize: MAX_SKILL_FILE_SIZE }
|
|
464
1082
|
});
|
|
465
1083
|
}
|
|
466
|
-
await
|
|
467
|
-
await
|
|
1084
|
+
await fs5.mkdir(path5.dirname(destPath), { recursive: true });
|
|
1085
|
+
await fs5.copyFile(srcPath, destPath);
|
|
468
1086
|
copiedFiles.push(file);
|
|
469
1087
|
}
|
|
470
1088
|
const entry = {
|
|
@@ -474,7 +1092,12 @@ var SkillInstaller = class {
|
|
|
474
1092
|
scope,
|
|
475
1093
|
projectHash: scope === "project" ? this.opts.projectHash : void 0,
|
|
476
1094
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
477
|
-
files: copiedFiles
|
|
1095
|
+
files: copiedFiles,
|
|
1096
|
+
// When the install came through a registry (e.g. skills.sh), record
|
|
1097
|
+
// which adapter + registry id resolved to this GitHub repo, so the
|
|
1098
|
+
// source is traceable. `source` stays `github:owner/repo` for
|
|
1099
|
+
// backward compat with `/skill-update`.
|
|
1100
|
+
...resolved.fromRegistry ? { registryFrom: { adapterId: resolved.adapterId, registryId: resolved.registryId } } : {}
|
|
478
1101
|
};
|
|
479
1102
|
await this.manifest.addEntry(entry);
|
|
480
1103
|
results.push({
|
|
@@ -489,10 +1112,97 @@ var SkillInstaller = class {
|
|
|
489
1112
|
this.invalidateLoaderCache();
|
|
490
1113
|
return results;
|
|
491
1114
|
} finally {
|
|
492
|
-
await
|
|
1115
|
+
await fs5.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
493
1116
|
});
|
|
494
1117
|
}
|
|
495
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Import skills from a local directory (e.g. `.claude/skills`) into the
|
|
1121
|
+
* project or user skills dir, optionally as symlinks. Used by `/skill-import`
|
|
1122
|
+
* to take ownership of foreign skills so they can be edited/committed.
|
|
1123
|
+
* Each direct subdirectory containing a valid `SKILL.md` is copied verbatim.
|
|
1124
|
+
*/
|
|
1125
|
+
async importFromDir(srcDir, opts) {
|
|
1126
|
+
const scope = opts?.global ? "user" : "project";
|
|
1127
|
+
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
1128
|
+
let entries;
|
|
1129
|
+
try {
|
|
1130
|
+
entries = await fs5.readdir(srcDir, { withFileTypes: true });
|
|
1131
|
+
} catch {
|
|
1132
|
+
throw new WrongStackError({
|
|
1133
|
+
message: `Source directory not found or not readable: ${srcDir}`,
|
|
1134
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
1135
|
+
subsystem: "general",
|
|
1136
|
+
context: { srcDir }
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
const results = [];
|
|
1140
|
+
for (const e of entries) {
|
|
1141
|
+
if (!await entryIsDirectory(srcDir, e)) continue;
|
|
1142
|
+
const skillMdPath = path5.join(srcDir, e.name, "SKILL.md");
|
|
1143
|
+
let content;
|
|
1144
|
+
try {
|
|
1145
|
+
content = await fs5.readFile(skillMdPath, "utf8");
|
|
1146
|
+
} catch {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
const fm = parseSkillFrontmatter(content);
|
|
1150
|
+
if (!fm.name || !fm.description || !isValidSkillNameFormat(fm.name)) continue;
|
|
1151
|
+
const existing = await this.manifest.findByName(fm.name);
|
|
1152
|
+
if (existing.find((x) => x.scope === scope)) {
|
|
1153
|
+
await this.removeSkillFiles(fm.name, scope);
|
|
1154
|
+
}
|
|
1155
|
+
const destDir = path5.join(targetDir, fm.name);
|
|
1156
|
+
await fs5.mkdir(destDir, { recursive: true });
|
|
1157
|
+
const srcSkillDir = path5.join(srcDir, e.name);
|
|
1158
|
+
const files = await collectFiles(srcSkillDir, srcSkillDir);
|
|
1159
|
+
const copiedFiles = [];
|
|
1160
|
+
for (const file of files) {
|
|
1161
|
+
const srcPath = path5.join(srcSkillDir, file);
|
|
1162
|
+
const destPath = path5.join(destDir, file);
|
|
1163
|
+
const resolved = path5.resolve(destPath);
|
|
1164
|
+
if (!resolved.startsWith(path5.resolve(destDir))) {
|
|
1165
|
+
throw new FsError({
|
|
1166
|
+
message: `Path traversal detected in skill file: ${file}`,
|
|
1167
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
1168
|
+
path: destPath,
|
|
1169
|
+
context: { reason: "path_traversal", skillName: fm.name }
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
await fs5.mkdir(path5.dirname(destPath), { recursive: true });
|
|
1173
|
+
if (opts?.link) {
|
|
1174
|
+
try {
|
|
1175
|
+
await fs5.symlink(srcPath, destPath);
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
await fs5.copyFile(srcPath, destPath);
|
|
1178
|
+
this.opts.log?.(`symlink failed for ${file} (${toErrorMessage(err)}); copied instead`);
|
|
1179
|
+
}
|
|
1180
|
+
} else {
|
|
1181
|
+
await fs5.copyFile(srcPath, destPath);
|
|
1182
|
+
}
|
|
1183
|
+
copiedFiles.push(file);
|
|
1184
|
+
}
|
|
1185
|
+
await this.manifest.addEntry({
|
|
1186
|
+
name: fm.name,
|
|
1187
|
+
source: `import:${srcDir}`,
|
|
1188
|
+
ref: "-",
|
|
1189
|
+
scope,
|
|
1190
|
+
projectHash: scope === "project" ? this.opts.projectHash : void 0,
|
|
1191
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1192
|
+
files: copiedFiles
|
|
1193
|
+
});
|
|
1194
|
+
results.push({
|
|
1195
|
+
name: fm.name,
|
|
1196
|
+
path: destDir,
|
|
1197
|
+
scope,
|
|
1198
|
+
source: `import:${srcDir}`,
|
|
1199
|
+
ref: "-",
|
|
1200
|
+
skillCount: 1
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
this.invalidateLoaderCache();
|
|
1204
|
+
return results;
|
|
1205
|
+
}
|
|
496
1206
|
/**
|
|
497
1207
|
* Update installed skills.
|
|
498
1208
|
* - No args: update all
|
|
@@ -593,6 +1303,28 @@ var SkillInstaller = class {
|
|
|
593
1303
|
async listInstalled() {
|
|
594
1304
|
return this.manifest.listAll();
|
|
595
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Search across all configured registry adapters. Results from each adapter
|
|
1308
|
+
* are merged (deduplicated by `installRef`, adapters earlier in the list win
|
|
1309
|
+
* conflicts). Adapters that don't support search (e.g. github-direct)
|
|
1310
|
+
* contribute nothing.
|
|
1311
|
+
*/
|
|
1312
|
+
async search(query, opts) {
|
|
1313
|
+
const settled = await Promise.allSettled(this.adapters.map((a) => a.search(query, opts)));
|
|
1314
|
+
const perAdapter = [];
|
|
1315
|
+
const errors = [];
|
|
1316
|
+
settled.forEach((s, i) => {
|
|
1317
|
+
if (s.status === "fulfilled") perAdapter.push(s.value);
|
|
1318
|
+
else {
|
|
1319
|
+
const id = this.adapters[i]?.id ?? "?";
|
|
1320
|
+
errors.push(`${id}: ${toErrorMessage(s.reason)}`);
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
if (errors.length > 0) {
|
|
1324
|
+
this.opts.log?.(`Some registries failed during search: ${errors.join("; ")}`);
|
|
1325
|
+
}
|
|
1326
|
+
return dedupeSearchResults(perAdapter);
|
|
1327
|
+
}
|
|
596
1328
|
// ── Private helpers ──────────────────────────────────────────────
|
|
597
1329
|
/**
|
|
598
1330
|
* Detect skills in an extracted repository.
|
|
@@ -600,14 +1332,14 @@ var SkillInstaller = class {
|
|
|
600
1332
|
*/
|
|
601
1333
|
async detectSkills(baseDir) {
|
|
602
1334
|
const results = [];
|
|
603
|
-
const rootSkillMd =
|
|
1335
|
+
const rootSkillMd = path5.join(baseDir, "SKILL.md");
|
|
604
1336
|
try {
|
|
605
|
-
await
|
|
606
|
-
const content = await
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
1337
|
+
await fs5.access(rootSkillMd);
|
|
1338
|
+
const content = await fs5.readFile(rootSkillMd, "utf8");
|
|
1339
|
+
const fm = parseSkillFrontmatter(content);
|
|
1340
|
+
if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
|
|
609
1341
|
results.push({
|
|
610
|
-
name:
|
|
1342
|
+
name: fm.name,
|
|
611
1343
|
baseDir,
|
|
612
1344
|
files: ["SKILL.md"]
|
|
613
1345
|
});
|
|
@@ -615,20 +1347,20 @@ var SkillInstaller = class {
|
|
|
615
1347
|
}
|
|
616
1348
|
} catch {
|
|
617
1349
|
}
|
|
618
|
-
const skillsDir =
|
|
1350
|
+
const skillsDir = path5.join(baseDir, "skills");
|
|
619
1351
|
try {
|
|
620
|
-
const entries = await
|
|
1352
|
+
const entries = await fs5.readdir(skillsDir, { withFileTypes: true });
|
|
621
1353
|
for (const entry of entries) {
|
|
622
1354
|
if (!entry.isDirectory()) continue;
|
|
623
|
-
const skillFile =
|
|
1355
|
+
const skillFile = path5.join(skillsDir, entry.name, "SKILL.md");
|
|
624
1356
|
try {
|
|
625
|
-
const content = await
|
|
626
|
-
const
|
|
627
|
-
if (
|
|
628
|
-
const skillDir =
|
|
1357
|
+
const content = await fs5.readFile(skillFile, "utf8");
|
|
1358
|
+
const fm = parseSkillFrontmatter(content);
|
|
1359
|
+
if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
|
|
1360
|
+
const skillDir = path5.join(skillsDir, entry.name);
|
|
629
1361
|
const files = await collectFiles(skillDir, skillDir);
|
|
630
1362
|
results.push({
|
|
631
|
-
name:
|
|
1363
|
+
name: fm.name,
|
|
632
1364
|
baseDir: skillDir,
|
|
633
1365
|
files
|
|
634
1366
|
});
|
|
@@ -645,8 +1377,8 @@ var SkillInstaller = class {
|
|
|
645
1377
|
*/
|
|
646
1378
|
async removeSkillFiles(name, scope) {
|
|
647
1379
|
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
648
|
-
const skillDir =
|
|
649
|
-
await
|
|
1380
|
+
const skillDir = path5.join(targetDir, name);
|
|
1381
|
+
await fs5.rm(skillDir, { recursive: true, force: true });
|
|
650
1382
|
}
|
|
651
1383
|
/**
|
|
652
1384
|
* Invalidate the skill loader's cache so newly installed skills appear.
|
|
@@ -657,49 +1389,43 @@ var SkillInstaller = class {
|
|
|
657
1389
|
loader.invalidateCache();
|
|
658
1390
|
}
|
|
659
1391
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
};
|
|
676
|
-
for (const line of block.split("\n")) {
|
|
677
|
-
const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
|
|
678
|
-
if (m) {
|
|
679
|
-
flush();
|
|
680
|
-
key = m[1] ?? "";
|
|
681
|
-
const pipe = m[2];
|
|
682
|
-
const rest = m[3] ?? "";
|
|
683
|
-
if (pipe === "|") {
|
|
684
|
-
value = [];
|
|
685
|
-
} else if (rest) {
|
|
686
|
-
value = [rest];
|
|
687
|
-
} else {
|
|
688
|
-
value = [];
|
|
1392
|
+
/**
|
|
1393
|
+
* Resolve an install ref, dispatching registry-prefixed refs to the matching
|
|
1394
|
+
* adapter. Returns the concrete `user/repo[@ref]` install ref plus provenance
|
|
1395
|
+
* (which adapter resolved it, if any).
|
|
1396
|
+
*/
|
|
1397
|
+
resolveRef(refInput) {
|
|
1398
|
+
const trimmed = refInput.trim();
|
|
1399
|
+
const colonIdx = trimmed.indexOf(":");
|
|
1400
|
+
if (colonIdx > 0) {
|
|
1401
|
+
const maybeAdapterId = trimmed.slice(0, colonIdx);
|
|
1402
|
+
const adapter = this.adapters.find((a) => a.id === maybeAdapterId);
|
|
1403
|
+
if (adapter) {
|
|
1404
|
+
const registryId = trimmed.slice(colonIdx + 1);
|
|
1405
|
+
const installRef = adapter.resolveInstallRef(registryId);
|
|
1406
|
+
return { installRef, fromRegistry: true, adapterId: adapter.id, registryId };
|
|
689
1407
|
}
|
|
690
|
-
} else if (key) {
|
|
691
|
-
value.push(line.replace(/^\s+/, ""));
|
|
692
1408
|
}
|
|
1409
|
+
return { installRef: trimmed, fromRegistry: false, adapterId: "", registryId: "" };
|
|
693
1410
|
}
|
|
694
|
-
|
|
695
|
-
|
|
1411
|
+
};
|
|
1412
|
+
async function entryIsDirectory(dir, entry) {
|
|
1413
|
+
if (entry.isDirectory()) return true;
|
|
1414
|
+
if (entry.isSymbolicLink()) {
|
|
1415
|
+
try {
|
|
1416
|
+
return (await fs5.stat(path5.join(dir, entry.name))).isDirectory();
|
|
1417
|
+
} catch {
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return false;
|
|
696
1422
|
}
|
|
697
1423
|
async function collectFiles(dir, baseDir) {
|
|
698
1424
|
const results = [];
|
|
699
|
-
const entries = await
|
|
1425
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
700
1426
|
for (const entry of entries) {
|
|
701
|
-
const fullPath =
|
|
702
|
-
const relPath =
|
|
1427
|
+
const fullPath = path5.join(dir, entry.name);
|
|
1428
|
+
const relPath = path5.relative(baseDir, fullPath);
|
|
703
1429
|
if (entry.isDirectory()) {
|
|
704
1430
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
705
1431
|
results.push(...await collectFiles(fullPath, baseDir));
|
|
@@ -709,7 +1435,24 @@ async function collectFiles(dir, baseDir) {
|
|
|
709
1435
|
}
|
|
710
1436
|
return results;
|
|
711
1437
|
}
|
|
1438
|
+
function dedupeSearchResults(perAdapter) {
|
|
1439
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1440
|
+
const out = [];
|
|
1441
|
+
for (const block of perAdapter) {
|
|
1442
|
+
const kept = [];
|
|
1443
|
+
for (const r of block.results) {
|
|
1444
|
+
const key = r.installRef;
|
|
1445
|
+
if (seen.has(key)) continue;
|
|
1446
|
+
seen.add(key);
|
|
1447
|
+
kept.push(r);
|
|
1448
|
+
}
|
|
1449
|
+
if (kept.length > 0 || block.results.length > 0) {
|
|
1450
|
+
out.push({ ...block, results: kept });
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return out;
|
|
1454
|
+
}
|
|
712
1455
|
|
|
713
|
-
export { SkillInstaller, SkillManifestStore, downloadGitHubTarball, parseSkillRef };
|
|
1456
|
+
export { DEFAULT_SKILLS_SH_URL, FOREIGN_SKILL_TOOLS, SKILL_LIMITS, SkillInstaller, SkillManifestStore, bodyLineAdvisory, createSkillsShAdapter, downloadGitHubTarball, extractSkillFromPrompt, generateSkillSkeleton, githubDirectAdapter, isValidSkillNameFormat, openInEditor, parseSkillFrontmatter, parseSkillRef, resolveForeignToolIds, resolveForeignToolIdsWithWarnings, securityScoreToTier, stripFrontmatter, validateSkillName, validateSkillNameAvailable, writeSkeletonSkill };
|
|
714
1457
|
//# sourceMappingURL=index.js.map
|
|
715
1458
|
//# sourceMappingURL=index.js.map
|