skillmds 1.1.2 → 1.2.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/CHANGELOG.md +84 -0
- package/README.md +89 -14
- package/bin/skillmd-mcp.mjs +2 -1020
- package/dist/chunk-4BFQJ3YZ.js +851 -0
- package/dist/cli.js +1083 -666
- package/dist/mcp.js +579 -0
- package/package.json +2 -3
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
// src/sources.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
var UNSUPPORTED = (what) => new Error(`${what} sources are not supported yet \u2014 clone it locally and run \`skillmd add ./path/to/skill\`.`);
|
|
6
|
+
function assertSubpath(sub) {
|
|
7
|
+
if (!sub) return void 0;
|
|
8
|
+
const clean = sub.replace(/^\/+|\/+$/g, "");
|
|
9
|
+
if (!clean) return void 0;
|
|
10
|
+
if (clean.split("/").some((seg) => seg === ".." || seg === "." || seg === "")) {
|
|
11
|
+
throw new Error(`invalid subpath "${sub}": ".." and empty segments are not allowed`);
|
|
12
|
+
}
|
|
13
|
+
return clean;
|
|
14
|
+
}
|
|
15
|
+
function withRef(spec, override) {
|
|
16
|
+
return override ? { ...spec, ref: override } : spec;
|
|
17
|
+
}
|
|
18
|
+
function parseSource(arg, opts = {}) {
|
|
19
|
+
const exists = opts.exists ?? existsSync;
|
|
20
|
+
const a = arg.trim();
|
|
21
|
+
if (!a) throw new Error("a source is required: owner/name, a GitHub URL, or a local path");
|
|
22
|
+
if (a === "~" || a.startsWith("~/") || a.startsWith("~\\")) {
|
|
23
|
+
return { kind: "local", path: join(homedir(), a.slice(1)), display: a };
|
|
24
|
+
}
|
|
25
|
+
if (a === "." || a.startsWith("./") || a.startsWith("../") || a.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(a) || a.startsWith("~") || exists(a)) {
|
|
26
|
+
return { kind: "local", path: a, display: a };
|
|
27
|
+
}
|
|
28
|
+
if (/^git@|^ssh:\/\//.test(a)) throw UNSUPPORTED("SSH git");
|
|
29
|
+
if (/gitlab\.com\//i.test(a) || a.startsWith("gitlab:")) throw UNSUPPORTED("GitLab");
|
|
30
|
+
const gist = a.match(/^https?:\/\/gist\.github\.com\/([^/]+)\/([0-9a-f]+)\/?$/i);
|
|
31
|
+
if (gist) return { kind: "gist", user: gist[1], id: gist[2], display: a };
|
|
32
|
+
const gh = a.match(/^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/#?]+?)(?:\.git)?(?:\/(tree|blob)\/([^/]+)(?:\/(.*?))?)?\/?(?:[#?].*)?$/i);
|
|
33
|
+
if (gh) {
|
|
34
|
+
const [, owner2, repo2, mode, ref2, rest] = gh;
|
|
35
|
+
let sub = rest;
|
|
36
|
+
if (mode === "blob" && sub) sub = sub.replace(/\/?SKILL\.md$/i, "");
|
|
37
|
+
return withRef({ kind: "github", owner: owner2, repo: repo2, ref: ref2 || void 0, subpath: assertSubpath(sub), display: a }, opts.ref);
|
|
38
|
+
}
|
|
39
|
+
if (/^https?:\/\//.test(a)) throw new Error(`"${a}" is not a supported source (registry slug, GitHub repo/tree/blob URL, gist, or local path)`);
|
|
40
|
+
const body = a.replace(/^(github|gh):/, "");
|
|
41
|
+
const prefixed = body !== a;
|
|
42
|
+
const m = body.match(/^([^/#@\s]+)\/([^/#@\s]+)((?:\/[^#@\s]+)*)?(?:#([^@\s]+))?(?:@([^/\s]+))?$/);
|
|
43
|
+
if (!m) throw new Error(`"${a}" is not a valid source \u2014 expected owner/name, owner/repo[/sub/path][#ref][@skill], a GitHub URL, or a local path`);
|
|
44
|
+
const [, owner, repoRaw, subRaw, ref, skill] = m;
|
|
45
|
+
const repo = repoRaw.replace(/\.git$/, "");
|
|
46
|
+
const subpath = assertSubpath(subRaw);
|
|
47
|
+
if (!prefixed && !subpath && !ref && !skill && !opts.ref) return { kind: "slug", owner, name: repo, display: a };
|
|
48
|
+
return withRef({ kind: "github", owner, repo, ref: ref || void 0, subpath, skill: skill || void 0, display: a }, opts.ref);
|
|
49
|
+
}
|
|
50
|
+
function gigetInput(spec) {
|
|
51
|
+
return `github:${spec.owner}/${spec.repo}${spec.subpath ? `/${spec.subpath}` : ""}${spec.ref ? `#${spec.ref}` : ""}`;
|
|
52
|
+
}
|
|
53
|
+
function sourceId(spec) {
|
|
54
|
+
switch (spec.kind) {
|
|
55
|
+
case "slug":
|
|
56
|
+
return `registry:${spec.owner}/${spec.name}`;
|
|
57
|
+
case "github":
|
|
58
|
+
return `github:${spec.owner}/${spec.repo}${spec.subpath ? `/${spec.subpath}` : ""}${spec.ref ? `#${spec.ref}` : ""}${spec.skill ? `@${spec.skill}` : ""}`;
|
|
59
|
+
case "gist":
|
|
60
|
+
return `gist:${spec.user}/${spec.id}`;
|
|
61
|
+
case "local":
|
|
62
|
+
return `local:${spec.path}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/config.ts
|
|
67
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
68
|
+
import { homedir as homedir2 } from "os";
|
|
69
|
+
import { join as join2 } from "path";
|
|
70
|
+
var DEFAULT_API = "https://api.skillmd.com";
|
|
71
|
+
function configPath(home = homedir2()) {
|
|
72
|
+
return join2(home, ".skillmd", "config.json");
|
|
73
|
+
}
|
|
74
|
+
function readConfig(home) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(readFileSync(configPath(home), "utf8"));
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function writeConfig(cfg, home = homedir2()) {
|
|
82
|
+
mkdirSync(join2(home, ".skillmd"), { recursive: true });
|
|
83
|
+
writeFileSync(configPath(home), JSON.stringify(cfg, null, 2), { encoding: "utf8", mode: 384 });
|
|
84
|
+
}
|
|
85
|
+
function hostOf(url) {
|
|
86
|
+
try {
|
|
87
|
+
return new URL(url).host;
|
|
88
|
+
} catch {
|
|
89
|
+
return url;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
var realSources = () => ({ env: process.env, config: readConfig() });
|
|
93
|
+
function resolveApi(flags = {}, src = realSources()) {
|
|
94
|
+
const base = (flags.api || src.env.SKILLMD_API || src.config.api || DEFAULT_API).replace(/\/+$/, "");
|
|
95
|
+
if (/^http:\/\//i.test(base) && !flags.insecureHttp) {
|
|
96
|
+
throw new Error(`refusing to use an http:// API base (${base}) \u2014 your token would travel unencrypted. Use an https:// base, or pass --insecure-http for local development.`);
|
|
97
|
+
}
|
|
98
|
+
if (!/^https?:\/\//i.test(base)) throw new Error(`API base must be an http(s) URL, got "${base}"`);
|
|
99
|
+
return base;
|
|
100
|
+
}
|
|
101
|
+
function resolveToken(flags = {}, src = realSources()) {
|
|
102
|
+
if (flags.token) return flags.token;
|
|
103
|
+
if (src.env.SKILLMD_TOKEN) return src.env.SKILLMD_TOKEN;
|
|
104
|
+
const stored = src.config.token;
|
|
105
|
+
if (!stored) return void 0;
|
|
106
|
+
const boundTo = src.config.tokenHost ?? hostOf(DEFAULT_API);
|
|
107
|
+
const target = hostOf(resolveApi(flags, src));
|
|
108
|
+
return boundTo === target ? stored : void 0;
|
|
109
|
+
}
|
|
110
|
+
function telemetryDisabled(env = process.env) {
|
|
111
|
+
const on = (v) => Boolean(v) && v !== "0" && v !== "false";
|
|
112
|
+
return on(env.SKILLMD_NO_TELEMETRY) || on(env.DO_NOT_TRACK);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/api.ts
|
|
116
|
+
import { createHash } from "crypto";
|
|
117
|
+
import { isAllowedSourceUrl } from "@skillmds/core";
|
|
118
|
+
|
|
119
|
+
// src/limits.ts
|
|
120
|
+
var MAX_PACK_FILES = 200;
|
|
121
|
+
var MAX_PACK_BYTES = 20 * 1024 * 1024;
|
|
122
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
123
|
+
var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i;
|
|
124
|
+
|
|
125
|
+
// src/api.ts
|
|
126
|
+
import { isAllowedSourceUrl as isAllowedSourceUrl2, reconstructSkillMd, skillMdFor } from "@skillmds/core";
|
|
127
|
+
var IntegrityError = class extends Error {
|
|
128
|
+
integrity = true;
|
|
129
|
+
constructor(message) {
|
|
130
|
+
super(message);
|
|
131
|
+
this.name = "IntegrityError";
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var RegistryError = class extends Error {
|
|
135
|
+
constructor(message, status) {
|
|
136
|
+
super(message);
|
|
137
|
+
this.status = status;
|
|
138
|
+
this.name = "RegistryError";
|
|
139
|
+
}
|
|
140
|
+
status;
|
|
141
|
+
};
|
|
142
|
+
function timedFetch(f, timeoutMs) {
|
|
143
|
+
return (input, init = {}) => f(input, { ...init, signal: init.signal ?? AbortSignal.timeout(timeoutMs) });
|
|
144
|
+
}
|
|
145
|
+
function createClient(flags = {}, opts = {}) {
|
|
146
|
+
const src = opts.sources ?? realSources();
|
|
147
|
+
const base = resolveApi(flags, src);
|
|
148
|
+
const token = resolveToken(flags, src);
|
|
149
|
+
const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
150
|
+
const doFetch = timedFetch(opts.fetch ?? fetch, timeoutMs);
|
|
151
|
+
const host = hostOf(base);
|
|
152
|
+
async function api(path, init = {}) {
|
|
153
|
+
const headers = { accept: "application/json", ...init.headers };
|
|
154
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
155
|
+
let res;
|
|
156
|
+
try {
|
|
157
|
+
res = await doFetch(base + path, { ...init, headers });
|
|
158
|
+
} catch (e) {
|
|
159
|
+
const cause = e instanceof Error ? e.cause?.message ?? e.message : String(e);
|
|
160
|
+
const isTimeout = e instanceof Error && (e.name === "TimeoutError" || e.cause instanceof Error && e.cause.name === "TimeoutError");
|
|
161
|
+
const why = isTimeout ? `timed out after ${Math.round(timeoutMs / 1e3)}s` : cause;
|
|
162
|
+
throw new RegistryError(`could not reach ${base}: ${why}`);
|
|
163
|
+
}
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const detail = await res.text().catch(() => "");
|
|
166
|
+
throw new RegistryError(`SkillMD API ${res.status} on ${path}: ${detail}`, res.status);
|
|
167
|
+
}
|
|
168
|
+
return await res.json();
|
|
169
|
+
}
|
|
170
|
+
return { api, base, host, isDefaultHost: host === hostOf(DEFAULT_API), token, hasToken: Boolean(token), fetch: doFetch };
|
|
171
|
+
}
|
|
172
|
+
async function fetchBundle(base, slug, token, opts = {}) {
|
|
173
|
+
const doFetch = timedFetch(opts.fetch ?? fetch, opts.timeoutMs ?? FETCH_TIMEOUT_MS);
|
|
174
|
+
const headers = { accept: "application/json" };
|
|
175
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
176
|
+
const res = await doFetch(`${base}/api/skills/${slug}/bundle?format=json`, { headers }).catch(() => null);
|
|
177
|
+
if (!res || !res.ok) return null;
|
|
178
|
+
const data = await res.json().catch(() => null);
|
|
179
|
+
if (!data?.files) return null;
|
|
180
|
+
if (data.files.length > MAX_PACK_FILES) throw new Error(`pack too large: > ${MAX_PACK_FILES} files`);
|
|
181
|
+
const out = [];
|
|
182
|
+
let bytes = 0;
|
|
183
|
+
for (const f of data.files) {
|
|
184
|
+
let contents;
|
|
185
|
+
let fromUrl = false;
|
|
186
|
+
if (f.content_base64 != null) {
|
|
187
|
+
contents = Buffer.from(f.content_base64, "base64");
|
|
188
|
+
} else if (f.source_url && isAllowedSourceUrl(f.source_url)) {
|
|
189
|
+
const r = await doFetch(f.source_url).catch(() => null);
|
|
190
|
+
if (!r || !r.ok) throw new Error(`could not fetch companion file "${f.path}" from ${hostOf(f.source_url)}`);
|
|
191
|
+
contents = Buffer.from(await r.arrayBuffer());
|
|
192
|
+
fromUrl = true;
|
|
193
|
+
} else {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
bytes += contents.length;
|
|
197
|
+
if (bytes > MAX_PACK_BYTES) throw new Error(`pack too large: > ${Math.round(MAX_PACK_BYTES / (1024 * 1024))}MB`);
|
|
198
|
+
if (f.sha256) {
|
|
199
|
+
const got = createHash("sha256").update(contents).digest("hex");
|
|
200
|
+
if (got !== f.sha256) throw new IntegrityError(`integrity check failed for "${f.path}" (expected ${f.sha256.slice(0, 12)}\u2026, got ${got.slice(0, 12)}\u2026)`);
|
|
201
|
+
} else if (!fromUrl && f.path !== "SKILL.md") {
|
|
202
|
+
throw new IntegrityError(`"${f.path}" has no sha256 in the registry bundle \u2014 refusing to install unverifiable content`);
|
|
203
|
+
}
|
|
204
|
+
out.push({ path: f.path, contents, ...fromUrl && !f.sha256 ? { unverified: true } : {} });
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/source.ts
|
|
210
|
+
import { existsSync as existsSync2, statSync, lstatSync, readFileSync as readFileSync2, readdirSync, mkdtempSync, rmSync } from "fs";
|
|
211
|
+
import { tmpdir } from "os";
|
|
212
|
+
import { join as join3, basename, dirname, relative, sep } from "path";
|
|
213
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
214
|
+
// build/dependency dirs
|
|
215
|
+
"node_modules",
|
|
216
|
+
".git",
|
|
217
|
+
"dist",
|
|
218
|
+
"build",
|
|
219
|
+
"out",
|
|
220
|
+
".next",
|
|
221
|
+
".wrangler",
|
|
222
|
+
".cache",
|
|
223
|
+
".npm",
|
|
224
|
+
".pnpm-store",
|
|
225
|
+
".venv",
|
|
226
|
+
"venv",
|
|
227
|
+
"__pycache__",
|
|
228
|
+
"target",
|
|
229
|
+
"vendor",
|
|
230
|
+
// large OS / user dirs (mostly relevant when run from a home directory)
|
|
231
|
+
"AppData",
|
|
232
|
+
"Library",
|
|
233
|
+
"$Recycle.Bin",
|
|
234
|
+
"System Volume Information",
|
|
235
|
+
"Windows",
|
|
236
|
+
"Program Files"
|
|
237
|
+
]);
|
|
238
|
+
var MAX_DEPTH = 12;
|
|
239
|
+
function findSkillFiles(dir, depth = 0) {
|
|
240
|
+
const out = [];
|
|
241
|
+
if (depth > MAX_DEPTH) return out;
|
|
242
|
+
let entries;
|
|
243
|
+
try {
|
|
244
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
245
|
+
} catch {
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
if (entry.isDirectory()) {
|
|
250
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
251
|
+
out.push(...findSkillFiles(join3(dir, entry.name), depth + 1));
|
|
252
|
+
} else if (entry.name === "SKILL.md") {
|
|
253
|
+
out.push(join3(dir, entry.name));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
function loadLocal(file) {
|
|
259
|
+
return { path: file, raw: readFileSync2(file, "utf8"), slug: basename(dirname(file)), file };
|
|
260
|
+
}
|
|
261
|
+
async function precheckTreeSize(spec, opts = {}) {
|
|
262
|
+
const f = timedFetch(opts.fetch ?? fetch, FETCH_TIMEOUT_MS);
|
|
263
|
+
const ref = spec.ref ?? "HEAD";
|
|
264
|
+
const res = await f(`https://api.github.com/repos/${spec.owner}/${spec.repo}/git/trees/${encodeURIComponent(ref)}?recursive=1`, {
|
|
265
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "skillmd-cli" }
|
|
266
|
+
}).catch(() => null);
|
|
267
|
+
if (!res || !res.ok) return;
|
|
268
|
+
const data = await res.json().catch(() => null);
|
|
269
|
+
if (!data?.tree || data.truncated) return;
|
|
270
|
+
const prefix = spec.subpath ? `${spec.subpath}/` : "";
|
|
271
|
+
const blobs = data.tree.filter((t) => t.type === "blob" && (!prefix || t.path.startsWith(prefix)));
|
|
272
|
+
const bytes = blobs.reduce((n, t) => n + (t.size ?? 0), 0);
|
|
273
|
+
if (blobs.length > MAX_PACK_FILES) throw new Error(`pack too large: ${blobs.length} files (limit ${MAX_PACK_FILES}) \u2014 point at the skill's own directory (pre-download estimate from the GitHub tree API)`);
|
|
274
|
+
if (bytes > MAX_PACK_BYTES) throw new Error(`pack too large: ${Math.round(bytes / (1024 * 1024))}MB (limit ${Math.round(MAX_PACK_BYTES / (1024 * 1024))}MB) (pre-download estimate from the GitHub tree API)`);
|
|
275
|
+
}
|
|
276
|
+
function asGithub(spec, ref, display) {
|
|
277
|
+
if (spec.kind === "github") return spec;
|
|
278
|
+
return { kind: "github", owner: spec.owner, repo: spec.name, ref, display: display ?? spec.display };
|
|
279
|
+
}
|
|
280
|
+
async function downloadGithub(spec, prefix, opts, precheck) {
|
|
281
|
+
if (precheck) await precheckTreeSize(spec, opts);
|
|
282
|
+
const { downloadTemplate } = await import("giget");
|
|
283
|
+
const tmp = mkdtempSync(join3(tmpdir(), prefix));
|
|
284
|
+
const cleanup = () => rmSync(tmp, { recursive: true, force: true });
|
|
285
|
+
try {
|
|
286
|
+
const { dir } = await downloadTemplate(gigetInput(spec), { dir: tmp, forceClean: true });
|
|
287
|
+
return { dir, cleanup };
|
|
288
|
+
} catch (e) {
|
|
289
|
+
cleanup();
|
|
290
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
291
|
+
throw new Error(`could not download ${spec.display}: ${msg}${/404|Not Found/i.test(msg) ? " (repo, ref or subpath not found \u2014 is it private?)" : ""}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function resolveSource(arg, opts = {}) {
|
|
295
|
+
const spec = parseSource(arg);
|
|
296
|
+
if (spec.kind === "local") {
|
|
297
|
+
if (!existsSync2(spec.path)) throw new Error(`Path not found: ${spec.path}`);
|
|
298
|
+
const st = statSync(spec.path);
|
|
299
|
+
if (st.isFile()) return [loadLocal(spec.path)];
|
|
300
|
+
const files = findSkillFiles(spec.path);
|
|
301
|
+
if (files.length === 0) {
|
|
302
|
+
throw new Error(`No SKILL.md found under ${spec.path}. Point at a skill directory, e.g. "skillmd lint ./my-skill", or run "skillmd init" to create one.`);
|
|
303
|
+
}
|
|
304
|
+
return files.map(loadLocal);
|
|
305
|
+
}
|
|
306
|
+
if (spec.kind === "gist") {
|
|
307
|
+
const f = timedFetch(opts.fetch ?? fetch, FETCH_TIMEOUT_MS);
|
|
308
|
+
const res = await f(`https://gist.githubusercontent.com/${spec.user}/${spec.id}/raw/SKILL.md`);
|
|
309
|
+
if (!res.ok) throw new Error(`gist ${spec.id} has no SKILL.md (HTTP ${res.status})`);
|
|
310
|
+
const tooBig = (n) => new Error(`gist ${spec.id} is too large: ${Math.round(n / (1024 * 1024))}MB (limit ${Math.round(MAX_PACK_BYTES / (1024 * 1024))}MB)`);
|
|
311
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
312
|
+
if (declared > MAX_PACK_BYTES) throw tooBig(declared);
|
|
313
|
+
const raw = await res.text();
|
|
314
|
+
if (raw.length > MAX_PACK_BYTES) throw tooBig(raw.length);
|
|
315
|
+
return [{ path: spec.display, raw, slug: `gist-${spec.id.slice(0, 8)}` }];
|
|
316
|
+
}
|
|
317
|
+
const gh = asGithub(spec);
|
|
318
|
+
const { dir, cleanup } = await downloadGithub(gh, "skillmd-src-", opts, Boolean(gh.subpath));
|
|
319
|
+
try {
|
|
320
|
+
let files = findSkillFiles(dir);
|
|
321
|
+
if (gh.skill) files = files.filter((f) => basename(dirname(f)) === gh.skill);
|
|
322
|
+
if (files.length === 0) throw new Error(`No SKILL.md found in ${gh.display}${gh.skill ? ` for skill "${gh.skill}"` : ""}`);
|
|
323
|
+
return files.map((f) => ({ ...loadLocal(f), path: relative(dir, f).split(sep).join("/"), file: void 0 }));
|
|
324
|
+
} finally {
|
|
325
|
+
cleanup();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function collectFiles(root, dir, limits = {}) {
|
|
329
|
+
const maxFiles = limits.maxFiles ?? MAX_PACK_FILES;
|
|
330
|
+
const maxBytes = limits.maxBytes ?? MAX_PACK_BYTES;
|
|
331
|
+
const out = [];
|
|
332
|
+
let bytes = 0;
|
|
333
|
+
const walk = (d, depth) => {
|
|
334
|
+
if (depth > MAX_DEPTH) return;
|
|
335
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
336
|
+
const fp = join3(d, entry.name);
|
|
337
|
+
const st = lstatSync(fp);
|
|
338
|
+
if (st.isSymbolicLink()) continue;
|
|
339
|
+
if (st.isDirectory()) {
|
|
340
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
341
|
+
walk(fp, depth + 1);
|
|
342
|
+
} else if (st.isFile()) {
|
|
343
|
+
if (out.length >= maxFiles) throw new Error(`pack too large: > ${maxFiles} files`);
|
|
344
|
+
bytes += st.size;
|
|
345
|
+
if (bytes > maxBytes) throw new Error(`pack too large: > ${Math.round(maxBytes / (1024 * 1024))}MB`);
|
|
346
|
+
out.push({ path: relative(root, fp).split(sep).join("/"), contents: readFileSync2(fp) });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
walk(dir, 0);
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
async function resolveTree(sourceUrl, ref, opts = {}) {
|
|
354
|
+
const spec = parseSource(sourceUrl, { ref });
|
|
355
|
+
if (spec.kind !== "github" && spec.kind !== "slug") throw new Error(`cannot fetch a tree from ${sourceUrl}`);
|
|
356
|
+
const gh = asGithub(spec, ref, sourceUrl);
|
|
357
|
+
const { dir, cleanup } = await downloadGithub(gh, "skillmd-pack-", opts, true);
|
|
358
|
+
try {
|
|
359
|
+
return collectFiles(dir, dir);
|
|
360
|
+
} finally {
|
|
361
|
+
cleanup();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function resolvePackFiles(skill, fetchTree = resolveTree) {
|
|
365
|
+
if (!skill.source_repo) return null;
|
|
366
|
+
const attempts = [{ ref: skill.commit_sha ?? void 0, pinMiss: false }];
|
|
367
|
+
if (skill.commit_sha) attempts.push({ ref: void 0, pinMiss: true });
|
|
368
|
+
for (const attempt of attempts) {
|
|
369
|
+
try {
|
|
370
|
+
const files = await fetchTree(skill.source_repo, attempt.ref);
|
|
371
|
+
const sk = files.find((f) => f.path === "SKILL.md");
|
|
372
|
+
if (sk) return { raw: sk.contents.toString("utf8"), files, pinMiss: attempt.pinMiss };
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/sanitize.ts
|
|
380
|
+
var CSI = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
381
|
+
var OSC = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
382
|
+
var ESC_OTHER = /\x1b[@-Z\\-_]/g;
|
|
383
|
+
var C0 = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
384
|
+
var C1 = /[\x80-\x9f]/g;
|
|
385
|
+
function stripTerminalEscapes(input) {
|
|
386
|
+
return input.replace(OSC, "").replace(CSI, "").replace(ESC_OTHER, "").replace(C0, "").replace(C1, "");
|
|
387
|
+
}
|
|
388
|
+
function safeText(input, max = 400) {
|
|
389
|
+
if (typeof input !== "string") return "";
|
|
390
|
+
const flat = stripTerminalEscapes(input).replace(/\s*\n\s*/g, " ").trim();
|
|
391
|
+
if (flat.length <= max) return flat;
|
|
392
|
+
return `${flat.slice(0, max - 1).trimEnd()}\u2026`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/agents.ts
|
|
396
|
+
import { statSync as statSync2 } from "fs";
|
|
397
|
+
import { homedir as homedir3 } from "os";
|
|
398
|
+
import { join as join4, resolve, isAbsolute } from "path";
|
|
399
|
+
var CANONICAL = [".agents", "skills"];
|
|
400
|
+
var AGENTS = [
|
|
401
|
+
{ id: "claude-code", project: [".claude", "skills"], global: [".claude", "skills"], globalEnv: "CLAUDE_CONFIG_DIR", detect: [[".claude"]], projectRoots: [".claude"] },
|
|
402
|
+
{ id: "cursor", project: [".cursor", "skills"], global: [".cursor", "skills"], detect: [[".cursor"]], projectRoots: [".cursor"] },
|
|
403
|
+
{ id: "codex", project: [".agents", "skills"], global: [".codex", "skills"], globalEnv: "CODEX_HOME", detect: [[".codex"]], projectRoots: [".agents", ".codex"] },
|
|
404
|
+
{ id: "windsurf", project: [".windsurf", "skills"], global: [".codeium", "windsurf", "skills"], detect: [[".codeium", "windsurf"], [".windsurf"]], projectRoots: [".windsurf"] },
|
|
405
|
+
{ id: "opencode", project: [".opencode", "skills"], global: [".config", "opencode", "skills"], detect: [[".config", "opencode"], [".opencode"]], projectRoots: [".opencode"] },
|
|
406
|
+
{ id: "gemini-cli", project: [".gemini", "skills"], global: [".gemini", "skills"], detect: [[".gemini"]], projectRoots: [".gemini", ".agents"] },
|
|
407
|
+
{ id: "antigravity", project: [".agent", "skills"], global: [".gemini", "antigravity", "skills"], detect: [[".gemini", "antigravity"], [".antigravity"]], projectRoots: [".agent", ".agents"] },
|
|
408
|
+
{ id: "kiro", project: [".kiro", "skills"], global: [".kiro", "skills"], detect: [[".kiro"]], projectRoots: [".kiro"] },
|
|
409
|
+
{ id: "warp", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".warp"]], projectRoots: [".agents"] },
|
|
410
|
+
{ id: "github-copilot", project: [".claude", "skills"], global: [".copilot", "skills"], detect: [[".copilot"]], projectRoots: [".claude", ".github", ".agents"] },
|
|
411
|
+
{ id: "cline", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".cline"]], projectRoots: [".agents"] },
|
|
412
|
+
{ id: "roo", project: [".roo", "skills"], global: [".roo", "skills"], detect: [[".roo"]], projectRoots: [".roo"] },
|
|
413
|
+
{ id: "goose", project: [".goose", "skills"], global: [".config", "goose", "skills"], detect: [[".config", "goose"]], projectRoots: [".goose"] },
|
|
414
|
+
{ id: "trae", project: [".trae", "skills"], global: [".trae", "skills"], detect: [[".trae"]], projectRoots: [".trae"] },
|
|
415
|
+
{ id: "qwen-code", project: [".qwen", "skills"], global: [".qwen", "skills"], detect: [[".qwen"]], projectRoots: [".qwen"] },
|
|
416
|
+
{ id: "amp", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".config", "amp"]], projectRoots: [".agents"] },
|
|
417
|
+
// — extended registry —
|
|
418
|
+
{ id: "openclaw", project: ["skills"], global: [".openclaw", "skills"], detect: [[".openclaw"], [".clawdbot"], [".moltbot"]], projectRoots: [".openclaw"] },
|
|
419
|
+
{ id: "kilo", project: [".claude", "skills"], global: [".kilocode", "skills"], detect: [[".kilocode"]], projectRoots: [".claude", ".kilocode"] },
|
|
420
|
+
{ id: "hermes-agent", project: [".hermes", "skills"], global: [".hermes", "skills"], detect: [[".hermes"]], projectRoots: [".hermes"] },
|
|
421
|
+
{ id: "continue", project: [".continue", "skills"], global: [".continue", "skills"], detect: [[".continue"]], projectRoots: [".continue"] },
|
|
422
|
+
{ id: "devin", project: [".devin", "skills"], global: [".config", "devin", "skills"], detect: [[".config", "devin"]], projectRoots: [".devin"] },
|
|
423
|
+
{ id: "openhands", project: [".openhands", "skills"], global: [".openhands", "skills"], detect: [[".openhands"]], projectRoots: [".openhands"] },
|
|
424
|
+
{ id: "replit", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".replit"]], projectRoots: [".agents"] },
|
|
425
|
+
{ id: "mistral-vibe", project: [".vibe", "skills"], global: [".vibe", "skills"], detect: [[".vibe"]], projectRoots: [".vibe"] },
|
|
426
|
+
{ id: "zed", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".config", "zed"]], projectRoots: [".agents"] },
|
|
427
|
+
{ id: "adal", project: [".adal", "skills"], global: [".adal", "skills"], detect: [[".adal"]], projectRoots: [".adal"] },
|
|
428
|
+
{ id: "aider-desk", project: [".aider-desk", "skills"], global: [".aider-desk", "skills"], detect: [[".aider-desk"]], projectRoots: [".aider-desk"] },
|
|
429
|
+
{ id: "antigravity-cli", project: [".agents", "skills"], global: [".gemini", "antigravity-cli", "skills"], detect: [[".gemini", "antigravity-cli"]], projectRoots: [".agents"] },
|
|
430
|
+
{ id: "augment", project: [".augment", "skills"], global: [".augment", "skills"], detect: [[".augment"]], projectRoots: [".augment"] },
|
|
431
|
+
{ id: "autohand-code", project: [".autohand", "skills"], global: [".autohand", "skills"], detect: [[".autohand"]], projectRoots: [".autohand"] },
|
|
432
|
+
{ id: "bob", project: [".bob", "skills"], global: [".bob", "skills"], detect: [[".bob"]], projectRoots: [".bob"] },
|
|
433
|
+
{ id: "codearts-agent", project: [".codeartsdoer", "skills"], global: [".codeartsdoer", "skills"], detect: [[".codeartsdoer"]], projectRoots: [".codeartsdoer"] },
|
|
434
|
+
{ id: "codebuddy", project: [".codebuddy", "skills"], global: [".codebuddy", "skills"], detect: [[".codebuddy"]], projectRoots: [".codebuddy"] },
|
|
435
|
+
{ id: "codemaker", project: [".codemaker", "skills"], global: [".codemaker", "skills"], detect: [[".codemaker"]], projectRoots: [".codemaker"] },
|
|
436
|
+
{ id: "codestudio", project: [".codestudio", "skills"], global: [".codestudio", "skills"], detect: [[".codestudio"]], projectRoots: [".codestudio"] },
|
|
437
|
+
{ id: "command-code", project: [".commandcode", "skills"], global: [".commandcode", "skills"], detect: [[".commandcode"]], projectRoots: [".commandcode"] },
|
|
438
|
+
{ id: "cortex", project: [".cortex", "skills"], global: [".snowflake", "cortex", "skills"], detect: [[".snowflake", "cortex"]], projectRoots: [".cortex"] },
|
|
439
|
+
{ id: "crush", project: [".crush", "skills"], global: [".config", "crush", "skills"], detect: [[".config", "crush"]], projectRoots: [".crush"] },
|
|
440
|
+
{ id: "deepagents", project: [".agents", "skills"], global: [".deepagents", "agent", "skills"], detect: [[".deepagents"]], projectRoots: [".agents"] },
|
|
441
|
+
{ id: "dexto", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".dexto"]], projectRoots: [".agents"] },
|
|
442
|
+
{ id: "droid", project: [".factory", "skills"], global: [".factory", "skills"], detect: [[".factory"]], projectRoots: [".factory", ".agents"] },
|
|
443
|
+
{ id: "firebender", project: [".agents", "skills"], global: [".firebender", "skills"], detect: [[".firebender"]], projectRoots: [".agents"] },
|
|
444
|
+
{ id: "forgecode", project: [".forge", "skills"], global: [".forge", "skills"], detect: [[".forge"]], projectRoots: [".forge"] },
|
|
445
|
+
{ id: "iflow-cli", project: [".iflow", "skills"], global: [".iflow", "skills"], detect: [[".iflow"]], projectRoots: [".iflow"] },
|
|
446
|
+
{ id: "inference-sh", project: [".inferencesh", "skills"], global: [".inferencesh", "skills"], detect: [[".inferencesh"]], projectRoots: [".inferencesh"] },
|
|
447
|
+
{ id: "jazz", project: [".jazz", "skills"], global: [".jazz", "skills"], detect: [[".jazz"]], projectRoots: [".jazz"] },
|
|
448
|
+
{ id: "junie", project: [".junie", "skills"], global: [".junie", "skills"], detect: [[".junie"]], projectRoots: [".junie"] },
|
|
449
|
+
{ id: "kimi-code-cli", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".kimi-code"], [".kimi"]], projectRoots: [".agents"] },
|
|
450
|
+
{ id: "kode", project: [".kode", "skills"], global: [".kode", "skills"], detect: [[".kode"]], projectRoots: [".kode"] },
|
|
451
|
+
{ id: "lingma", project: [".lingma", "skills"], global: [".lingma", "skills"], detect: [[".lingma"]], projectRoots: [".lingma"] },
|
|
452
|
+
{ id: "loaf", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".loaf"]], projectRoots: [".agents"] },
|
|
453
|
+
{ id: "mcpjam", project: [".mcpjam", "skills"], global: [".mcpjam", "skills"], detect: [[".mcpjam"]], projectRoots: [".mcpjam"] },
|
|
454
|
+
{ id: "moxby", project: [".moxby", "skills"], global: [".moxby", "skills"], detect: [[".moxby"]], projectRoots: [".moxby"] },
|
|
455
|
+
{ id: "mux", project: [".mux", "skills"], global: [".mux", "skills"], detect: [[".mux"]], projectRoots: [".mux"] },
|
|
456
|
+
{ id: "neovate", project: [".neovate", "skills"], global: [".neovate", "skills"], detect: [[".neovate"]], projectRoots: [".neovate"] },
|
|
457
|
+
{ id: "ona", project: [".ona", "skills"], global: [".ona", "skills"], detect: [[".ona"]], projectRoots: [".ona"] },
|
|
458
|
+
{ id: "pi", project: [".pi", "skills"], global: [".pi", "agent", "skills"], detect: [[".pi", "agent"]], projectRoots: [".pi"] },
|
|
459
|
+
{ id: "pochi", project: [".pochi", "skills"], global: [".pochi", "skills"], detect: [[".pochi"]], projectRoots: [".pochi"] },
|
|
460
|
+
{ id: "qoder", project: [".qoder", "skills"], global: [".qoder", "skills"], detect: [[".qoder"]], projectRoots: [".qoder"] },
|
|
461
|
+
{ id: "qoder-cn", project: [".qoder", "skills"], global: [".qoder-cn", "skills"], detect: [[".qoder-cn"]], projectRoots: [".qoder"] },
|
|
462
|
+
{ id: "reasonix", project: [".reasonix", "skills"], global: [".reasonix", "skills"], detect: [[".reasonix"]], projectRoots: [".reasonix"] },
|
|
463
|
+
{ id: "rovodev", project: [".rovodev", "skills"], global: [".rovodev", "skills"], detect: [[".rovodev"]], projectRoots: [".rovodev"] },
|
|
464
|
+
{ id: "tabnine-cli", project: [".tabnine", "agent", "skills"], global: [".tabnine", "agent", "skills"], detect: [[".tabnine"]], projectRoots: [".tabnine"] },
|
|
465
|
+
{ id: "terramind", project: [".terramind", "skills"], global: [".terramind", "skills"], detect: [[".terramind"]], projectRoots: [".terramind"] },
|
|
466
|
+
{ id: "tinycloud", project: [".tinycloud", "skills"], global: [".tinycloud", "skills"], detect: [[".tinycloud"]], projectRoots: [".tinycloud"] },
|
|
467
|
+
{ id: "trae-cn", project: [".trae", "skills"], global: [".trae-cn", "skills"], detect: [[".trae-cn"]], projectRoots: [".trae"] },
|
|
468
|
+
{ id: "zcode", project: [".zcode", "skills"], global: [".zcode", "skills"], detect: [[".zcode"]], projectRoots: [".zcode"] },
|
|
469
|
+
{ id: "zencoder", project: [".zencoder", "skills"], global: [".zencoder", "skills"], detect: [[".zencoder"]], projectRoots: [".zencoder"] },
|
|
470
|
+
// — project-only agents (no documented global skills dir) —
|
|
471
|
+
{ id: "eve", project: ["agent", "skills"], global: null, detect: [[".eve"]], projectRoots: ["agent"] },
|
|
472
|
+
{ id: "promptscript", project: [".agents", "skills"], global: null, detect: [[".promptscript"]], projectRoots: [".agents"] }
|
|
473
|
+
];
|
|
474
|
+
function canonicalDir(opts = {}) {
|
|
475
|
+
return opts.global ? join4(opts.home ?? homedir3(), ...CANONICAL) : join4(opts.cwd ?? process.cwd(), ...CANONICAL);
|
|
476
|
+
}
|
|
477
|
+
function agentById(id) {
|
|
478
|
+
const agent = AGENTS.find((a) => a.id === id);
|
|
479
|
+
if (!agent) throw new Error(`Unknown agent: ${id}. Known: ${AGENTS.map((a) => a.id).join(", ")}`);
|
|
480
|
+
return agent;
|
|
481
|
+
}
|
|
482
|
+
function agentSupportsGlobal(id) {
|
|
483
|
+
return agentById(id).global !== null;
|
|
484
|
+
}
|
|
485
|
+
function agentDir(agentId, opts = {}) {
|
|
486
|
+
const agent = agentById(agentId);
|
|
487
|
+
if (!opts.global) return join4(opts.cwd ?? process.cwd(), ...agent.project);
|
|
488
|
+
if (agent.global === null) throw new Error(`${agent.id} does not support global installs (project-only agent)`);
|
|
489
|
+
const env = opts.env ?? process.env;
|
|
490
|
+
const override = agent.globalEnv ? env[agent.globalEnv] : void 0;
|
|
491
|
+
if (override) return join4(isAbsolute(override) ? override : resolve(override), ...agent.global.slice(1));
|
|
492
|
+
return join4(opts.home ?? homedir3(), ...agent.global);
|
|
493
|
+
}
|
|
494
|
+
function agentRootExists(agentId, cwd) {
|
|
495
|
+
return agentById(agentId).projectRoots.some((r) => r !== CANONICAL[0] && isDir(join4(cwd, r)));
|
|
496
|
+
}
|
|
497
|
+
function isDir(p) {
|
|
498
|
+
try {
|
|
499
|
+
return statSync2(p).isDirectory();
|
|
500
|
+
} catch {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function detectAgents(opts = {}) {
|
|
505
|
+
const home = opts.home ?? homedir3();
|
|
506
|
+
const env = opts.env ?? process.env;
|
|
507
|
+
return AGENTS.filter((a) => {
|
|
508
|
+
if (a.detect.some((d) => isDir(join4(home, ...d)))) return true;
|
|
509
|
+
const override = a.globalEnv ? env[a.globalEnv] : void 0;
|
|
510
|
+
return !!override && isDir(isAbsolute(override) ? override : resolve(override));
|
|
511
|
+
}).map((a) => a.id);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/project.ts
|
|
515
|
+
import { existsSync as existsSync3 } from "fs";
|
|
516
|
+
import { join as join5 } from "path";
|
|
517
|
+
var PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "AGENTS.md", "CLAUDE.md", "skills-lock.json"];
|
|
518
|
+
var AGENT_PROJECT_ROOTS = [...new Set(AGENTS.flatMap((a) => a.projectRoots))].filter((r) => r.startsWith("."));
|
|
519
|
+
function looksLikeProject(dir) {
|
|
520
|
+
if (PROJECT_MARKERS.some((m) => existsSync3(join5(dir, m)))) return true;
|
|
521
|
+
return AGENT_PROJECT_ROOTS.some((seg) => existsSync3(join5(dir, seg)));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/lock.ts
|
|
525
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, copyFileSync, renameSync, rmSync as rmSync2 } from "fs";
|
|
526
|
+
import { homedir as homedir4 } from "os";
|
|
527
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
528
|
+
function lockPath(scope) {
|
|
529
|
+
return scope.global ? join6(scope.home ?? homedir4(), ".skillmd", "lock.json") : join6(scope.cwd ?? process.cwd(), "skills-lock.json");
|
|
530
|
+
}
|
|
531
|
+
var EMPTY = () => ({ version: 1, skills: {} });
|
|
532
|
+
var corruptSeen = /* @__PURE__ */ new Set();
|
|
533
|
+
function readLock(scope, opts = {}) {
|
|
534
|
+
const file = lockPath(scope);
|
|
535
|
+
if (!existsSync4(file)) return EMPTY();
|
|
536
|
+
try {
|
|
537
|
+
const parsed = JSON.parse(readFileSync3(file, "utf8"));
|
|
538
|
+
if (!parsed || typeof parsed !== "object") throw new Error("unexpected shape");
|
|
539
|
+
if (parsed.version !== 1) throw new Error(`unsupported lock version ${String(parsed.version)}`);
|
|
540
|
+
if (typeof parsed.skills !== "object" || parsed.skills === null || Array.isArray(parsed.skills)) throw new Error("unexpected shape");
|
|
541
|
+
return { version: 1, skills: parsed.skills };
|
|
542
|
+
} catch (e) {
|
|
543
|
+
corruptSeen.add(file);
|
|
544
|
+
opts.warn?.(`could not parse ${file} (${e instanceof Error ? e.message : String(e)}) \u2014 treating it as empty; a backup is written before it is next rewritten`);
|
|
545
|
+
return EMPTY();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function stripTimestamps(e) {
|
|
549
|
+
const rest = { ...e };
|
|
550
|
+
delete rest.installedAt;
|
|
551
|
+
delete rest.updatedAt;
|
|
552
|
+
return rest;
|
|
553
|
+
}
|
|
554
|
+
function writeLock(scope, lock) {
|
|
555
|
+
const file = lockPath(scope);
|
|
556
|
+
mkdirSync2(dirname2(file), { recursive: true });
|
|
557
|
+
if (corruptSeen.has(file) && existsSync4(file)) {
|
|
558
|
+
copyFileSync(file, `${file}.bak`);
|
|
559
|
+
corruptSeen.delete(file);
|
|
560
|
+
}
|
|
561
|
+
const names = Object.keys(lock.skills).sort();
|
|
562
|
+
const skills = {};
|
|
563
|
+
for (const n of names) skills[n] = scope.global ? lock.skills[n] : stripTimestamps(lock.skills[n]);
|
|
564
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
565
|
+
writeFileSync2(tmp, `${JSON.stringify({ version: 1, skills }, null, 2)}
|
|
566
|
+
`, "utf8");
|
|
567
|
+
try {
|
|
568
|
+
try {
|
|
569
|
+
renameSync(tmp, file);
|
|
570
|
+
} catch {
|
|
571
|
+
rmSync2(file, { force: true });
|
|
572
|
+
renameSync(tmp, file);
|
|
573
|
+
}
|
|
574
|
+
} catch (e) {
|
|
575
|
+
rmSync2(tmp, { force: true });
|
|
576
|
+
throw e;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
var defaultWarnOpts = () => ({ warn: (m) => console.warn(m) });
|
|
580
|
+
function upsertEntry(scope, name, entry, opts = defaultWarnOpts()) {
|
|
581
|
+
const lock = readLock(scope, opts);
|
|
582
|
+
const prev = lock.skills[name];
|
|
583
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
584
|
+
lock.skills[name] = { ...entry, installedAt: prev?.installedAt ?? entry.installedAt ?? now, updatedAt: prev ? now : entry.updatedAt ?? now };
|
|
585
|
+
writeLock(scope, lock);
|
|
586
|
+
}
|
|
587
|
+
function removeEntry(scope, name, opts = defaultWarnOpts()) {
|
|
588
|
+
const lock = readLock(scope, opts);
|
|
589
|
+
if (!(name in lock.skills)) return false;
|
|
590
|
+
delete lock.skills[name];
|
|
591
|
+
writeLock(scope, lock);
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/installer.ts
|
|
596
|
+
import { createHash as createHash2 } from "crypto";
|
|
597
|
+
import { cpSync, existsSync as existsSync5, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2, renameSync as renameSync2, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
598
|
+
import { platform } from "os";
|
|
599
|
+
import { dirname as dirname3, join as join7, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
600
|
+
var NAME_OK = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
601
|
+
function assertSafeName(name) {
|
|
602
|
+
if (!NAME_OK.test(name) || name.includes("..")) throw new Error(`refused unsafe skill name: "${name}"`);
|
|
603
|
+
if (WINDOWS_RESERVED.test(name)) throw new Error(`refused reserved name "${name}" (not writable on Windows)`);
|
|
604
|
+
}
|
|
605
|
+
function digestOf(files) {
|
|
606
|
+
const h = createHash2("sha256");
|
|
607
|
+
const norm = files.map((f) => ({ path: f.path.replace(/\\/g, "/"), contents: f.contents }));
|
|
608
|
+
norm.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
609
|
+
for (const f of norm) {
|
|
610
|
+
h.update(f.path).update("\0").update(typeof f.contents === "string" ? Buffer.from(f.contents) : f.contents).update("\0");
|
|
611
|
+
}
|
|
612
|
+
return `sha256:${h.digest("hex")}`;
|
|
613
|
+
}
|
|
614
|
+
function writeTree(dest, files) {
|
|
615
|
+
if (files.length === 0) throw new Error("skill has no files");
|
|
616
|
+
mkdirSync3(dest, { recursive: true });
|
|
617
|
+
const root = resolve2(dest);
|
|
618
|
+
for (const f of files) {
|
|
619
|
+
const fp = resolve2(dest, f.path);
|
|
620
|
+
if (fp !== root && !fp.startsWith(root + sep2)) throw new Error(`refused unsafe path in skill: ${f.path}`);
|
|
621
|
+
const parts = relative2(root, fp).split(sep2);
|
|
622
|
+
if (parts.some((p) => WINDOWS_RESERVED.test(p))) throw new Error(`refused reserved file name in skill: ${f.path}`);
|
|
623
|
+
if (parts.some((p) => /[. ]$/.test(p))) throw new Error(`refused invalid file name in skill: ${f.path}`);
|
|
624
|
+
mkdirSync3(dirname3(fp), { recursive: true });
|
|
625
|
+
writeFileSync3(fp, f.contents);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function defaultLink(target, linkPath) {
|
|
629
|
+
mkdirSync3(dirname3(linkPath), { recursive: true });
|
|
630
|
+
if (platform() === "win32") {
|
|
631
|
+
symlinkSync(resolve2(target), linkPath, "junction");
|
|
632
|
+
return "junction";
|
|
633
|
+
}
|
|
634
|
+
symlinkSync(relative2(dirname3(linkPath), target) || ".", linkPath, "dir");
|
|
635
|
+
return "symlink";
|
|
636
|
+
}
|
|
637
|
+
function isSkillDirOrLink(p) {
|
|
638
|
+
try {
|
|
639
|
+
const st = lstatSync2(p);
|
|
640
|
+
return st.isSymbolicLink() || st.isDirectory() && existsSync5(join7(p, "SKILL.md"));
|
|
641
|
+
} catch {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function pathExists(p) {
|
|
646
|
+
try {
|
|
647
|
+
lstatSync2(p);
|
|
648
|
+
return true;
|
|
649
|
+
} catch {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function removeSkillPath(p) {
|
|
654
|
+
if (!isSkillDirOrLink(p)) return false;
|
|
655
|
+
rmSync3(p, { recursive: true, force: true });
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
async function installSkill(req) {
|
|
659
|
+
assertSafeName(req.name);
|
|
660
|
+
const scope = req.scope;
|
|
661
|
+
const cwd = resolve2(scope.cwd ?? process.cwd());
|
|
662
|
+
const canonRoot = canonicalDir(scope);
|
|
663
|
+
const canonical = join7(canonRoot, req.name);
|
|
664
|
+
const lockScope = { global: Boolean(scope.global), cwd: scope.cwd, home: scope.home };
|
|
665
|
+
const lock = readLock(lockScope);
|
|
666
|
+
const prev = lock.skills[req.name];
|
|
667
|
+
let replaced;
|
|
668
|
+
if (prev && prev.source !== req.source) {
|
|
669
|
+
if (!req.force) throw new Error(`"${req.name}" is already installed from ${prev.source}; pass --force to replace it with ${req.source}`);
|
|
670
|
+
replaced = prev.source;
|
|
671
|
+
} else if (!prev && existsSync5(canonical)) {
|
|
672
|
+
if (!req.force) throw new Error(`"${req.name}" exists at ${canonical} but is not tracked by skillmd; pass --force to replace it`);
|
|
673
|
+
replaced = "untracked";
|
|
674
|
+
}
|
|
675
|
+
if (!prev) {
|
|
676
|
+
for (const agentId of req.agents) {
|
|
677
|
+
if (scope.global && !agentSupportsGlobal(agentId)) continue;
|
|
678
|
+
const dir = agentDir(agentId, scope);
|
|
679
|
+
if (resolve2(dir) === resolve2(canonRoot)) continue;
|
|
680
|
+
if (!scope.global && !req.explicitAgents && !agentRootExists(agentId, cwd)) continue;
|
|
681
|
+
const p = join7(dir, req.name);
|
|
682
|
+
if (!pathExists(p) || lstatSync2(p).isSymbolicLink()) continue;
|
|
683
|
+
if (!req.force) throw new Error(`"${req.name}" exists at ${p} but is not tracked by skillmd; pass --force to replace it`);
|
|
684
|
+
replaced = "untracked";
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const staging = `${canonical}.tmp-${process.pid}`;
|
|
688
|
+
rmSync3(staging, { recursive: true, force: true });
|
|
689
|
+
try {
|
|
690
|
+
writeTree(staging, req.files);
|
|
691
|
+
} catch (e) {
|
|
692
|
+
rmSync3(staging, { recursive: true, force: true });
|
|
693
|
+
throw e;
|
|
694
|
+
}
|
|
695
|
+
mkdirSync3(canonRoot, { recursive: true });
|
|
696
|
+
const old = `${canonical}.old-${process.pid}`;
|
|
697
|
+
rmSync3(old, { recursive: true, force: true });
|
|
698
|
+
let movedAside = false;
|
|
699
|
+
if (pathExists(canonical)) {
|
|
700
|
+
renameSync2(canonical, old);
|
|
701
|
+
movedAside = true;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
renameSync2(staging, canonical);
|
|
705
|
+
} catch (e) {
|
|
706
|
+
if (movedAside && pathExists(old)) renameSync2(old, canonical);
|
|
707
|
+
rmSync3(staging, { recursive: true, force: true });
|
|
708
|
+
throw e;
|
|
709
|
+
}
|
|
710
|
+
if (movedAside) rmSync3(old, { recursive: true, force: true });
|
|
711
|
+
const link = req.link ?? defaultLink;
|
|
712
|
+
const targets = [];
|
|
713
|
+
const skipped = [];
|
|
714
|
+
const modes = {};
|
|
715
|
+
const seenDirs = /* @__PURE__ */ new Map();
|
|
716
|
+
for (const agentId of req.agents) {
|
|
717
|
+
if (scope.global && !agentSupportsGlobal(agentId)) {
|
|
718
|
+
skipped.push({ agent: agentId, reason: "project-only agent" });
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const dir = agentDir(agentId, scope);
|
|
722
|
+
if (resolve2(dir) === resolve2(canonRoot)) {
|
|
723
|
+
targets.push({ agent: agentId, path: canonical, mode: "canonical" });
|
|
724
|
+
modes[agentId] = "canonical";
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
const first = seenDirs.get(resolve2(dir));
|
|
728
|
+
if (first) {
|
|
729
|
+
targets.push({ agent: agentId, path: first.path, mode: first.mode });
|
|
730
|
+
modes[agentId] = first.mode;
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (!scope.global && !req.explicitAgents && !agentRootExists(agentId, cwd)) {
|
|
734
|
+
const a = agentById(agentId);
|
|
735
|
+
skipped.push({ agent: agentId, reason: `not present in this project (no ${a.projectRoots[0] ?? a.project[0]}/ dir) \u2014 pass -a ${agentId} to add it` });
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const linkPath = join7(dir, req.name);
|
|
739
|
+
removeSkillPath(linkPath);
|
|
740
|
+
let mode;
|
|
741
|
+
if (req.mode === "copy") {
|
|
742
|
+
mkdirSync3(dir, { recursive: true });
|
|
743
|
+
cpSync(canonical, linkPath, { recursive: true });
|
|
744
|
+
mode = "copy";
|
|
745
|
+
} else {
|
|
746
|
+
try {
|
|
747
|
+
mode = link(canonical, linkPath);
|
|
748
|
+
} catch (e) {
|
|
749
|
+
if (pathExists(linkPath)) {
|
|
750
|
+
if (!lstatSync2(linkPath).isSymbolicLink()) throw e;
|
|
751
|
+
rmSync3(linkPath, { recursive: true, force: true });
|
|
752
|
+
}
|
|
753
|
+
mkdirSync3(dir, { recursive: true });
|
|
754
|
+
cpSync(canonical, linkPath, { recursive: true });
|
|
755
|
+
mode = "copy";
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const t = { agent: agentId, path: linkPath, mode };
|
|
759
|
+
targets.push(t);
|
|
760
|
+
seenDirs.set(resolve2(dir), t);
|
|
761
|
+
modes[agentId] = mode;
|
|
762
|
+
}
|
|
763
|
+
const digest = digestOf(req.files);
|
|
764
|
+
upsertEntry(lockScope, req.name, {
|
|
765
|
+
source: req.source,
|
|
766
|
+
commit_sha: req.commit_sha,
|
|
767
|
+
digest,
|
|
768
|
+
agents: targets.map((t) => t.agent),
|
|
769
|
+
mode: modes
|
|
770
|
+
});
|
|
771
|
+
return { canonical, targets, skipped, replaced, digest };
|
|
772
|
+
}
|
|
773
|
+
function uninstallSkill(name, scope) {
|
|
774
|
+
assertSafeName(name);
|
|
775
|
+
const removed = [];
|
|
776
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
777
|
+
for (const a of AGENTS) {
|
|
778
|
+
if (scope.global && a.global === null) continue;
|
|
779
|
+
dirs.add(agentDir(a.id, scope));
|
|
780
|
+
}
|
|
781
|
+
for (const d of dirs) {
|
|
782
|
+
const p = join7(d, name);
|
|
783
|
+
if (removeSkillPath(p)) removed.push(p);
|
|
784
|
+
}
|
|
785
|
+
const canonical = join7(canonicalDir(scope), name);
|
|
786
|
+
if (removeSkillPath(canonical) && !removed.includes(canonical)) removed.push(canonical);
|
|
787
|
+
const hadEntry = removeEntry({ global: Boolean(scope.global), cwd: scope.cwd, home: scope.home }, name);
|
|
788
|
+
return { removed, untracked: !hadEntry };
|
|
789
|
+
}
|
|
790
|
+
function listInstalled(scope) {
|
|
791
|
+
const lock = readLock({ global: Boolean(scope.global), cwd: scope.cwd, home: scope.home });
|
|
792
|
+
const out = /* @__PURE__ */ new Map();
|
|
793
|
+
const kind = scope.global ? "global" : "project";
|
|
794
|
+
for (const [name, e] of Object.entries(lock.skills)) {
|
|
795
|
+
out.set(name, { name, scope: kind, path: join7(canonicalDir(scope), name), agents: [...e.agents], source: e.source, mode: e.mode, tracked: true });
|
|
796
|
+
}
|
|
797
|
+
const seen = /* @__PURE__ */ new Set();
|
|
798
|
+
for (const a of AGENTS) {
|
|
799
|
+
if (scope.global && a.global === null) continue;
|
|
800
|
+
const dir = agentDir(a.id, scope);
|
|
801
|
+
if (seen.has(dir) || !existsSync5(dir)) continue;
|
|
802
|
+
seen.add(dir);
|
|
803
|
+
for (const entry of readdirSync2(dir)) {
|
|
804
|
+
const p = join7(dir, entry);
|
|
805
|
+
if (!existsSync5(join7(p, "SKILL.md"))) continue;
|
|
806
|
+
const known = out.get(entry);
|
|
807
|
+
if (known) {
|
|
808
|
+
if (!known.agents.includes(a.id) && lstatSync2(p).isSymbolicLink()) known.agents.push(a.id);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
out.set(entry, { name: entry, scope: kind, path: p, agents: [a.id], mode: { [a.id]: "copy" }, tracked: false });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return [...out.values()].sort((x, y) => x.name.localeCompare(y.name));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export {
|
|
818
|
+
parseSource,
|
|
819
|
+
gigetInput,
|
|
820
|
+
sourceId,
|
|
821
|
+
configPath,
|
|
822
|
+
readConfig,
|
|
823
|
+
writeConfig,
|
|
824
|
+
hostOf,
|
|
825
|
+
resolveApi,
|
|
826
|
+
telemetryDisabled,
|
|
827
|
+
IntegrityError,
|
|
828
|
+
RegistryError,
|
|
829
|
+
createClient,
|
|
830
|
+
fetchBundle,
|
|
831
|
+
skillMdFor,
|
|
832
|
+
resolveSource,
|
|
833
|
+
collectFiles,
|
|
834
|
+
resolveTree,
|
|
835
|
+
resolvePackFiles,
|
|
836
|
+
stripTerminalEscapes,
|
|
837
|
+
safeText,
|
|
838
|
+
AGENTS,
|
|
839
|
+
canonicalDir,
|
|
840
|
+
agentSupportsGlobal,
|
|
841
|
+
agentDir,
|
|
842
|
+
detectAgents,
|
|
843
|
+
looksLikeProject,
|
|
844
|
+
readLock,
|
|
845
|
+
upsertEntry,
|
|
846
|
+
digestOf,
|
|
847
|
+
isSkillDirOrLink,
|
|
848
|
+
installSkill,
|
|
849
|
+
uninstallSkill,
|
|
850
|
+
listInstalled
|
|
851
|
+
};
|