@safeskill/cli 0.2.3 → 0.3.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/CHANGELOG.md +10 -0
- package/README.md +99 -55
- package/dist/_chunks/libs/common.mjs +138 -8
- package/dist/_chunks/rolldown-runtime.mjs +10 -1
- package/dist/cli.mjs +2536 -738
- package/package.json +32 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { a as
|
|
4
|
-
import {
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { i as __toESM, n as __exportAll } from "./_chunks/rolldown-runtime.mjs";
|
|
3
|
+
import { _ as require_picocolors, a as esm_default, c as Me, d as be, f as fe, g as pD, h as ye, i as require_yauzl, l as Se, m as xe, n as xdgConfig, o as Ie, p as ve, r as require_gray_matter, s as M, t as require_dist, u as Y } from "./_chunks/libs/common.mjs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
6
5
|
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
7
7
|
import { homedir, platform, tmpdir } from "os";
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
9
8
|
import * as readline from "readline";
|
|
10
9
|
import { Writable } from "stream";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
import { execFile, execSync, spawn, spawnSync } from "child_process";
|
|
11
12
|
import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
|
|
12
13
|
import { gunzipSync } from "zlib";
|
|
13
14
|
import { createHash } from "crypto";
|
|
15
|
+
import { createHash as createHash$1 } from "node:crypto";
|
|
14
16
|
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
15
17
|
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
16
18
|
let version$2;
|
|
@@ -37,6 +39,14 @@ function getOwnerRepo(parsed) {
|
|
|
37
39
|
if (path.includes("/")) return path;
|
|
38
40
|
return null;
|
|
39
41
|
}
|
|
42
|
+
if (parsed.url.startsWith("ssh://")) try {
|
|
43
|
+
let path = new URL(parsed.url).pathname.slice(1);
|
|
44
|
+
path = path.replace(/\.git$/, "");
|
|
45
|
+
if (path.includes("/")) return path;
|
|
46
|
+
return null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
40
50
|
if (!parsed.url.startsWith("http://") && !parsed.url.startsWith("https://")) return null;
|
|
41
51
|
try {
|
|
42
52
|
let path = new URL(parsed.url).pathname.slice(1);
|
|
@@ -71,17 +81,57 @@ function isLocalPath(input) {
|
|
|
71
81
|
return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || /^[a-zA-Z]:[/\\]/.test(input);
|
|
72
82
|
}
|
|
73
83
|
const SOURCE_ALIASES = { "coinbase/agentWallet": "coinbase/agentic-wallet-skills" };
|
|
84
|
+
function decodeFragmentValue(value) {
|
|
85
|
+
try {
|
|
86
|
+
return decodeURIComponent(value);
|
|
87
|
+
} catch {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function looksLikeGitSource(input) {
|
|
92
|
+
if (input.startsWith("github:") || input.startsWith("gitlab:") || input.startsWith("git@")) return true;
|
|
93
|
+
if (input.startsWith("ssh://")) return true;
|
|
94
|
+
if (input.startsWith("http://") || input.startsWith("https://")) try {
|
|
95
|
+
const parsed = new URL(input);
|
|
96
|
+
const pathname = parsed.pathname;
|
|
97
|
+
if (parsed.hostname === "github.com") return /^\/[^/]+\/[^/]+(?:\.git)?(?:\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
|
|
98
|
+
if (parsed.hostname === "gitlab.com") return /^\/.+?\/[^/]+(?:\.git)?(?:\/-\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
|
|
99
|
+
} catch {}
|
|
100
|
+
if (/^https?:\/\/.+\.git(?:$|[/?])/i.test(input)) return true;
|
|
101
|
+
return !input.includes(":") && !input.startsWith(".") && !input.startsWith("/") && /^([^/]+)\/([^/]+)(?:\/(.+)|@(.+))?$/.test(input);
|
|
102
|
+
}
|
|
103
|
+
function parseFragmentRef(input) {
|
|
104
|
+
const hashIndex = input.indexOf("#");
|
|
105
|
+
if (hashIndex < 0) return { inputWithoutFragment: input };
|
|
106
|
+
let inputWithoutFragment = input.slice(0, hashIndex);
|
|
107
|
+
const fragment = input.slice(hashIndex + 1);
|
|
108
|
+
if (!looksLikeGitSource(inputWithoutFragment) && inputWithoutFragment.endsWith("/")) {
|
|
109
|
+
const inputWithoutTrailingSlash = inputWithoutFragment.slice(0, -1);
|
|
110
|
+
if (looksLikeGitSource(inputWithoutTrailingSlash)) inputWithoutFragment = inputWithoutTrailingSlash;
|
|
111
|
+
}
|
|
112
|
+
if (!fragment || !looksLikeGitSource(inputWithoutFragment)) return { inputWithoutFragment: input };
|
|
113
|
+
const atIndex = fragment.indexOf("@");
|
|
114
|
+
if (atIndex === -1) return {
|
|
115
|
+
inputWithoutFragment,
|
|
116
|
+
ref: decodeFragmentValue(fragment)
|
|
117
|
+
};
|
|
118
|
+
const ref = fragment.slice(0, atIndex);
|
|
119
|
+
const skillFilter = fragment.slice(atIndex + 1);
|
|
120
|
+
return {
|
|
121
|
+
inputWithoutFragment,
|
|
122
|
+
ref: ref ? decodeFragmentValue(ref) : void 0,
|
|
123
|
+
skillFilter: skillFilter ? decodeFragmentValue(skillFilter) : void 0
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function appendFragmentRef(input, ref, skillFilter) {
|
|
127
|
+
if (!ref) return input;
|
|
128
|
+
return `${input}#${ref}${skillFilter ? `@${skillFilter}` : ""}`;
|
|
129
|
+
}
|
|
74
130
|
function parseSource(input) {
|
|
75
|
-
const alias = SOURCE_ALIASES[input];
|
|
76
|
-
if (alias) input = alias;
|
|
77
131
|
if (input.startsWith("safeskill://")) return {
|
|
78
132
|
type: "hub",
|
|
79
133
|
url: input
|
|
80
134
|
};
|
|
81
|
-
const githubPrefixMatch = input.match(/^github:(.+)$/);
|
|
82
|
-
if (githubPrefixMatch) return parseSource(githubPrefixMatch[1]);
|
|
83
|
-
const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
|
|
84
|
-
if (gitlabPrefixMatch) return parseSource(`https://gitlab.com/${gitlabPrefixMatch[1]}`);
|
|
85
135
|
if (isLocalPath(input)) {
|
|
86
136
|
const resolvedPath = resolve(input);
|
|
87
137
|
return {
|
|
@@ -90,14 +140,29 @@ function parseSource(input) {
|
|
|
90
140
|
localPath: resolvedPath
|
|
91
141
|
};
|
|
92
142
|
}
|
|
143
|
+
const { inputWithoutFragment, ref: fragmentRef, skillFilter: fragmentSkillFilter } = parseFragmentRef(input);
|
|
144
|
+
input = inputWithoutFragment;
|
|
145
|
+
const alias = SOURCE_ALIASES[input];
|
|
146
|
+
if (alias) input = alias;
|
|
147
|
+
const githubPrefixMatch = input.match(/^github:(.+)$/);
|
|
148
|
+
if (githubPrefixMatch) return parseSource(appendFragmentRef(githubPrefixMatch[1], fragmentRef, fragmentSkillFilter));
|
|
149
|
+
const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
|
|
150
|
+
if (gitlabPrefixMatch) return parseSource(appendFragmentRef(`https://gitlab.com/${gitlabPrefixMatch[1]}`, fragmentRef, fragmentSkillFilter));
|
|
151
|
+
if (input.startsWith("ssh://")) return {
|
|
152
|
+
type: "git",
|
|
153
|
+
url: input,
|
|
154
|
+
...fragmentRef ? { ref: fragmentRef } : {},
|
|
155
|
+
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
156
|
+
};
|
|
93
157
|
const githubTreeWithPathMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
|
|
94
158
|
if (githubTreeWithPathMatch) {
|
|
95
159
|
const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
|
|
96
160
|
return {
|
|
97
161
|
type: "github",
|
|
98
162
|
url: `https://github.com/${owner}/${repo}.git`,
|
|
99
|
-
ref,
|
|
100
|
-
subpath: subpath ? sanitizeSubpath(subpath) : subpath
|
|
163
|
+
ref: ref || fragmentRef,
|
|
164
|
+
subpath: subpath ? sanitizeSubpath(subpath) : subpath,
|
|
165
|
+
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
101
166
|
};
|
|
102
167
|
}
|
|
103
168
|
const githubTreeMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
|
|
@@ -106,7 +171,8 @@ function parseSource(input) {
|
|
|
106
171
|
return {
|
|
107
172
|
type: "github",
|
|
108
173
|
url: `https://github.com/${owner}/${repo}.git`,
|
|
109
|
-
ref
|
|
174
|
+
ref: ref || fragmentRef,
|
|
175
|
+
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
110
176
|
};
|
|
111
177
|
}
|
|
112
178
|
const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
@@ -114,7 +180,9 @@ function parseSource(input) {
|
|
|
114
180
|
const [, owner, repo] = githubRepoMatch;
|
|
115
181
|
return {
|
|
116
182
|
type: "github",
|
|
117
|
-
url: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git
|
|
183
|
+
url: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git`,
|
|
184
|
+
...fragmentRef ? { ref: fragmentRef } : {},
|
|
185
|
+
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
118
186
|
};
|
|
119
187
|
}
|
|
120
188
|
const gitlabTreeWithPathMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)\/(.+)/);
|
|
@@ -123,8 +191,9 @@ function parseSource(input) {
|
|
|
123
191
|
if (hostname !== "github.com" && repoPath) return {
|
|
124
192
|
type: "gitlab",
|
|
125
193
|
url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`,
|
|
126
|
-
ref,
|
|
127
|
-
subpath: subpath ? sanitizeSubpath(subpath) : subpath
|
|
194
|
+
ref: ref || fragmentRef,
|
|
195
|
+
subpath: subpath ? sanitizeSubpath(subpath) : subpath,
|
|
196
|
+
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
128
197
|
};
|
|
129
198
|
}
|
|
130
199
|
const gitlabTreeMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)$/);
|
|
@@ -133,7 +202,8 @@ function parseSource(input) {
|
|
|
133
202
|
if (hostname !== "github.com" && repoPath) return {
|
|
134
203
|
type: "gitlab",
|
|
135
204
|
url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`,
|
|
136
|
-
ref
|
|
205
|
+
ref: ref || fragmentRef,
|
|
206
|
+
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
137
207
|
};
|
|
138
208
|
}
|
|
139
209
|
const gitlabRepoMatch = input.match(/gitlab\.com\/(.+?)(?:\.git)?\/?$/);
|
|
@@ -141,7 +211,9 @@ function parseSource(input) {
|
|
|
141
211
|
const repoPath = gitlabRepoMatch[1];
|
|
142
212
|
if (repoPath.includes("/")) return {
|
|
143
213
|
type: "gitlab",
|
|
144
|
-
url: `https://gitlab.com/${repoPath}.git
|
|
214
|
+
url: `https://gitlab.com/${repoPath}.git`,
|
|
215
|
+
...fragmentRef ? { ref: fragmentRef } : {},
|
|
216
|
+
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
145
217
|
};
|
|
146
218
|
}
|
|
147
219
|
const atSkillMatch = input.match(/^([^/]+)\/([^/@]+)@(.+)$/);
|
|
@@ -150,16 +222,19 @@ function parseSource(input) {
|
|
|
150
222
|
return {
|
|
151
223
|
type: "github",
|
|
152
224
|
url: `https://github.com/${owner}/${repo}.git`,
|
|
153
|
-
|
|
225
|
+
...fragmentRef ? { ref: fragmentRef } : {},
|
|
226
|
+
skillFilter: fragmentSkillFilter || skillFilter
|
|
154
227
|
};
|
|
155
228
|
}
|
|
156
|
-
const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(
|
|
229
|
+
const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+?))?\/?$/);
|
|
157
230
|
if (shorthandMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
|
|
158
231
|
const [, owner, repo, subpath] = shorthandMatch;
|
|
159
232
|
return {
|
|
160
233
|
type: "github",
|
|
161
234
|
url: `https://github.com/${owner}/${repo}.git`,
|
|
162
|
-
|
|
235
|
+
...fragmentRef ? { ref: fragmentRef } : {},
|
|
236
|
+
subpath: subpath ? sanitizeSubpath(subpath) : subpath,
|
|
237
|
+
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
163
238
|
};
|
|
164
239
|
}
|
|
165
240
|
if (isWellKnownUrl(input)) return {
|
|
@@ -168,7 +243,9 @@ function parseSource(input) {
|
|
|
168
243
|
};
|
|
169
244
|
return {
|
|
170
245
|
type: "git",
|
|
171
|
-
url: input
|
|
246
|
+
url: input,
|
|
247
|
+
...fragmentRef ? { ref: fragmentRef } : {},
|
|
248
|
+
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
172
249
|
};
|
|
173
250
|
}
|
|
174
251
|
function isWellKnownUrl(input) {
|
|
@@ -349,45 +426,187 @@ async function searchMultiselect(options) {
|
|
|
349
426
|
render();
|
|
350
427
|
});
|
|
351
428
|
}
|
|
352
|
-
const
|
|
429
|
+
const DEFAULT_CLONE_TIMEOUT_MS = 3e5;
|
|
430
|
+
const CLONE_TIMEOUT_MS = (() => {
|
|
431
|
+
const raw = process.env.SKILLS_CLONE_TIMEOUT_MS;
|
|
432
|
+
if (!raw) return DEFAULT_CLONE_TIMEOUT_MS;
|
|
433
|
+
const parsed = Number.parseInt(raw, 10);
|
|
434
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
|
|
435
|
+
})();
|
|
436
|
+
const execFileAsync = promisify(execFile);
|
|
437
|
+
const LFS_DISABLED_GIT_CONFIG = [
|
|
438
|
+
"filter.lfs.required=false",
|
|
439
|
+
"filter.lfs.smudge=",
|
|
440
|
+
"filter.lfs.clean=",
|
|
441
|
+
"filter.lfs.process="
|
|
442
|
+
];
|
|
353
443
|
var GitCloneError = class extends Error {
|
|
354
444
|
url;
|
|
355
445
|
isTimeout;
|
|
446
|
+
isAuth;
|
|
356
447
|
isAuthError;
|
|
357
448
|
constructor(message, url, isTimeout = false, isAuthError = false) {
|
|
358
449
|
super(message);
|
|
359
450
|
this.name = "GitCloneError";
|
|
360
451
|
this.url = url;
|
|
361
452
|
this.isTimeout = isTimeout;
|
|
453
|
+
this.isAuth = isAuthError;
|
|
362
454
|
this.isAuthError = isAuthError;
|
|
363
455
|
}
|
|
364
456
|
};
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
457
|
+
function parseGitHubRepoUrl(url) {
|
|
458
|
+
const sshMatch = url.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
|
|
459
|
+
if (sshMatch) {
|
|
460
|
+
const owner = sshMatch[1];
|
|
461
|
+
const repo = sshMatch[2];
|
|
462
|
+
return {
|
|
463
|
+
owner,
|
|
464
|
+
repo,
|
|
465
|
+
slug: `${owner}/${repo}`,
|
|
466
|
+
sshUrl: `git@github.com:${owner}/${repo}.git`
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const parsed = new URL(url);
|
|
471
|
+
if (parsed.hostname !== "github.com") return null;
|
|
472
|
+
const match = parsed.pathname.match(/^\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
473
|
+
if (!match) return null;
|
|
474
|
+
const owner = match[1];
|
|
475
|
+
const repo = match[2];
|
|
476
|
+
return {
|
|
477
|
+
owner,
|
|
478
|
+
repo,
|
|
479
|
+
slug: `${owner}/${repo}`,
|
|
480
|
+
sshUrl: `git@github.com:${owner}/${repo}.git`
|
|
481
|
+
};
|
|
482
|
+
} catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function isGitHubHttpsCloneUrl(url) {
|
|
487
|
+
try {
|
|
488
|
+
const parsed = new URL(url);
|
|
489
|
+
return parsed.protocol === "https:" && parsed.hostname === "github.com";
|
|
490
|
+
} catch {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function isGitHubSsoAuthError(message) {
|
|
495
|
+
const lower = message.toLowerCase();
|
|
496
|
+
return lower.includes("saml sso") || lower.includes("enforced sso") || lower.includes("enabled or enforced saml") || lower.includes("re-authorize the oauth application");
|
|
497
|
+
}
|
|
498
|
+
function isAuthFailure(message) {
|
|
499
|
+
return message.includes("Authentication failed") || message.includes("could not read Username") || message.includes("Permission denied") || message.includes("Repository not found") || message.includes("requested URL returned error: 403") || isGitHubSsoAuthError(message);
|
|
500
|
+
}
|
|
501
|
+
function redactCloneUrl(url) {
|
|
502
|
+
return url.replace(/(https?:\/\/)[^/\s?#'")<>]+@/gi, "$1[redacted]@").replace(/([a-z][a-z0-9+.-]*:\/\/)[^/\s?#'")<>@:]+:[^/\s?#'")<>@]*@/gi, "$1[redacted]@");
|
|
503
|
+
}
|
|
504
|
+
function buildGitCloneEnv(extraEnv) {
|
|
505
|
+
return {
|
|
368
506
|
...process.env,
|
|
369
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
507
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
508
|
+
GIT_LFS_SKIP_SMUDGE: "1",
|
|
509
|
+
...extraEnv
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function gitLfsDisabledCloneArgs() {
|
|
513
|
+
return LFS_DISABLED_GIT_CONFIG.flatMap((config) => ["--config", config]);
|
|
514
|
+
}
|
|
515
|
+
function createGitClient(extraEnv) {
|
|
516
|
+
return esm_default({
|
|
517
|
+
timeout: { block: CLONE_TIMEOUT_MS },
|
|
518
|
+
config: LFS_DISABLED_GIT_CONFIG
|
|
519
|
+
}).env(buildGitCloneEnv(extraEnv));
|
|
520
|
+
}
|
|
521
|
+
async function resetTempDir(dir) {
|
|
522
|
+
await rm(dir, {
|
|
523
|
+
recursive: true,
|
|
524
|
+
force: true
|
|
525
|
+
}).catch(() => {});
|
|
526
|
+
await mkdir(dir, { recursive: true });
|
|
527
|
+
}
|
|
528
|
+
async function tryGhClone(repo, tempDir, ref) {
|
|
529
|
+
let cloneTarget = repo.slug;
|
|
530
|
+
try {
|
|
531
|
+
const { stdout, stderr } = await execFileAsync("gh", [
|
|
532
|
+
"auth",
|
|
533
|
+
"status",
|
|
534
|
+
"-h",
|
|
535
|
+
"github.com"
|
|
536
|
+
], {
|
|
537
|
+
timeout: 5e3,
|
|
538
|
+
env: buildGitCloneEnv()
|
|
539
|
+
});
|
|
540
|
+
const statusOutput = `${stdout}${stderr}`;
|
|
541
|
+
if (/Git operations protocol:\s+ssh/i.test(statusOutput)) cloneTarget = repo.sshUrl;
|
|
542
|
+
} catch {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
const gitFlags = ref ? [
|
|
546
|
+
...gitLfsDisabledCloneArgs(),
|
|
547
|
+
"--depth=1",
|
|
548
|
+
"--branch",
|
|
549
|
+
ref
|
|
550
|
+
] : [...gitLfsDisabledCloneArgs(), "--depth=1"];
|
|
551
|
+
await execFileAsync("gh", [
|
|
552
|
+
"repo",
|
|
553
|
+
"clone",
|
|
554
|
+
cloneTarget,
|
|
555
|
+
tempDir,
|
|
556
|
+
"--",
|
|
557
|
+
...gitFlags
|
|
558
|
+
], {
|
|
559
|
+
timeout: CLONE_TIMEOUT_MS,
|
|
560
|
+
env: buildGitCloneEnv()
|
|
370
561
|
});
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
function buildGitHubAuthError(url, repo, message) {
|
|
565
|
+
const displayUrl = redactCloneUrl(url);
|
|
566
|
+
if (repo && isGitHubSsoAuthError(message)) return `GitHub blocked HTTPS access to ${displayUrl} because the organization enforces SAML SSO.\n skills tried your existing git credentials and available fallbacks, but none succeeded.\n - Re-authorize your GitHub credentials/app for that org's SSO policy\n - Or rerun with SSH: npx skills add ${repo.sshUrl}\n - Verify access with: gh auth status -h github.com or ssh -T git@github.com`;
|
|
567
|
+
if (repo) return `Authentication failed for ${displayUrl}.\n - For private repos, ensure you have access\n - Retry with SSH: npx skills add ${repo.sshUrl}\n - Check access with: gh auth status -h github.com or ssh -T git@github.com`;
|
|
568
|
+
return `Authentication failed for ${displayUrl}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`;
|
|
569
|
+
}
|
|
570
|
+
async function cloneRepo(url, ref) {
|
|
571
|
+
const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
|
|
371
572
|
const cloneOptions = ref ? [
|
|
372
573
|
"--depth",
|
|
373
574
|
"1",
|
|
374
575
|
"--branch",
|
|
375
576
|
ref
|
|
376
577
|
] : ["--depth", "1"];
|
|
578
|
+
const repo = parseGitHubRepoUrl(url);
|
|
377
579
|
try {
|
|
378
|
-
await
|
|
580
|
+
await createGitClient().clone(url, tempDir, cloneOptions);
|
|
379
581
|
return tempDir;
|
|
380
582
|
} catch (error) {
|
|
583
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
584
|
+
const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
|
|
585
|
+
const isAuthError = isAuthFailure(errorMessage);
|
|
586
|
+
if (isTimeout) {
|
|
587
|
+
await rm(tempDir, {
|
|
588
|
+
recursive: true,
|
|
589
|
+
force: true
|
|
590
|
+
}).catch(() => {});
|
|
591
|
+
throw new GitCloneError(`Clone timed out after ${Math.round(CLONE_TIMEOUT_MS / 1e3)}s. Common causes:\n - Large repository: raise the timeout with SKILLS_CLONE_TIMEOUT_MS=600000 (10m)\n - Slow network: retry, or clone manually and pass the local path to 'skills add'\n - Private repo without credentials: ensure auth is configured\n - For SSH: ssh-add -l (to check loaded keys)\n - For HTTPS: gh auth status (if using GitHub CLI)`, url, true, false);
|
|
592
|
+
}
|
|
593
|
+
if (isAuthError && repo && isGitHubHttpsCloneUrl(url)) {
|
|
594
|
+
try {
|
|
595
|
+
await resetTempDir(tempDir);
|
|
596
|
+
if (await tryGhClone(repo, tempDir, ref)) return tempDir;
|
|
597
|
+
} catch {}
|
|
598
|
+
try {
|
|
599
|
+
await resetTempDir(tempDir);
|
|
600
|
+
await createGitClient({ GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes" }).clone(repo.sshUrl, tempDir, cloneOptions);
|
|
601
|
+
return tempDir;
|
|
602
|
+
} catch {}
|
|
603
|
+
}
|
|
381
604
|
await rm(tempDir, {
|
|
382
605
|
recursive: true,
|
|
383
606
|
force: true
|
|
384
607
|
}).catch(() => {});
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const isAuthError = errorMessage.includes("Authentication failed") || errorMessage.includes("could not read Username") || errorMessage.includes("Permission denied") || errorMessage.includes("Repository not found");
|
|
388
|
-
if (isTimeout) throw new GitCloneError("Clone timed out after 60s. This often happens with private repos that require authentication.\n Ensure you have access and your SSH keys or credentials are configured:\n - For SSH: ssh-add -l (to check loaded keys)\n - For HTTPS: gh auth status (if using GitHub CLI)", url, true, false);
|
|
389
|
-
if (isAuthError) throw new GitCloneError(`Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`, url, false, true);
|
|
390
|
-
throw new GitCloneError(`Failed to clone ${url}: ${errorMessage}`, url, false, false);
|
|
608
|
+
if (isAuthError) throw new GitCloneError(buildGitHubAuthError(url, repo, errorMessage), url, false, true);
|
|
609
|
+
throw new GitCloneError(`Failed to clone ${redactCloneUrl(url)}: ${redactCloneUrl(errorMessage)}`, url, false, false);
|
|
391
610
|
}
|
|
392
611
|
}
|
|
393
612
|
async function cleanupTempDir(dir) {
|
|
@@ -831,6 +1050,18 @@ async function checkHubUpdates(items) {
|
|
|
831
1050
|
return readEnvelope(response);
|
|
832
1051
|
}
|
|
833
1052
|
var import_gray_matter = /* @__PURE__ */ __toESM(require_gray_matter(), 1);
|
|
1053
|
+
function parseFrontmatter(content) {
|
|
1054
|
+
const match = content.match(/^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?([\s\S]*)$/);
|
|
1055
|
+
if (!match) return {
|
|
1056
|
+
data: {},
|
|
1057
|
+
content
|
|
1058
|
+
};
|
|
1059
|
+
const parsed = (0, import_gray_matter.default)(`---\n${match[1] ?? ""}\n---\n`);
|
|
1060
|
+
return {
|
|
1061
|
+
data: parsed.data && typeof parsed.data === "object" && !Array.isArray(parsed.data) ? parsed.data : {},
|
|
1062
|
+
content: match[2] ?? ""
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
834
1065
|
function isContainedIn(targetPath, basePath) {
|
|
835
1066
|
const normalizedBase = normalize(resolve(basePath));
|
|
836
1067
|
const normalizedTarget = normalize(resolve(targetPath));
|
|
@@ -896,6 +1127,85 @@ async function getPluginGroupings(basePath) {
|
|
|
896
1127
|
} catch {}
|
|
897
1128
|
return groupings;
|
|
898
1129
|
}
|
|
1130
|
+
const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
|
|
1131
|
+
const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
|
|
1132
|
+
const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
|
|
1133
|
+
const SIMPLE_ESC_RE = /\x1b[\x20-\x2f]*[\x30-\x7e]/g;
|
|
1134
|
+
const C1_RE = /[\x80-\x9f]/g;
|
|
1135
|
+
const CONTROL_RE = /[\x00-\x08\x0b\x0c\x0d-\x1f\x7f]/g;
|
|
1136
|
+
function stripTerminalEscapes(input) {
|
|
1137
|
+
return input.replace(OSC_RE, "").replace(DCS_PM_APC_RE, "").replace(CSI_RE, "").replace(SIMPLE_ESC_RE, "").replace(C1_RE, "").replace(CONTROL_RE, "");
|
|
1138
|
+
}
|
|
1139
|
+
function sanitizeMetadata(input) {
|
|
1140
|
+
if (typeof input !== "string") return "";
|
|
1141
|
+
return stripTerminalEscapes(input).replace(/[\r\n]+/g, " ").trim();
|
|
1142
|
+
}
|
|
1143
|
+
const LOCAL_LOCK_FILE = "skills-lock.json";
|
|
1144
|
+
const CURRENT_VERSION$1 = 1;
|
|
1145
|
+
function getLocalLockPath(cwd) {
|
|
1146
|
+
return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
|
|
1147
|
+
}
|
|
1148
|
+
async function readLocalLock(cwd) {
|
|
1149
|
+
const lockPath = getLocalLockPath(cwd);
|
|
1150
|
+
try {
|
|
1151
|
+
const content = await readFile(lockPath, "utf-8");
|
|
1152
|
+
const parsed = JSON.parse(content);
|
|
1153
|
+
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
|
|
1154
|
+
if (parsed.version < CURRENT_VERSION$1) return createEmptyLocalLock();
|
|
1155
|
+
return parsed;
|
|
1156
|
+
} catch {
|
|
1157
|
+
return createEmptyLocalLock();
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async function writeLocalLock(lock, cwd) {
|
|
1161
|
+
const lockPath = getLocalLockPath(cwd);
|
|
1162
|
+
const sortedSkills = {};
|
|
1163
|
+
for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
|
|
1164
|
+
const sorted = {
|
|
1165
|
+
version: lock.version,
|
|
1166
|
+
skills: sortedSkills
|
|
1167
|
+
};
|
|
1168
|
+
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
1169
|
+
}
|
|
1170
|
+
async function computeSkillFolderHash(skillDir) {
|
|
1171
|
+
const files = [];
|
|
1172
|
+
await collectFiles(skillDir, skillDir, files);
|
|
1173
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
1174
|
+
const hash = createHash("sha256");
|
|
1175
|
+
for (const file of files) {
|
|
1176
|
+
hash.update(file.relativePath);
|
|
1177
|
+
hash.update(file.content);
|
|
1178
|
+
}
|
|
1179
|
+
return hash.digest("hex");
|
|
1180
|
+
}
|
|
1181
|
+
async function collectFiles(baseDir, currentDir, results) {
|
|
1182
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
1183
|
+
await Promise.all(entries.map(async (entry) => {
|
|
1184
|
+
const fullPath = join(currentDir, entry.name);
|
|
1185
|
+
if (entry.isDirectory()) {
|
|
1186
|
+
if (entry.name === ".git" || entry.name === "node_modules") return;
|
|
1187
|
+
await collectFiles(baseDir, fullPath, results);
|
|
1188
|
+
} else if (entry.isFile()) {
|
|
1189
|
+
const content = await readFile(fullPath);
|
|
1190
|
+
const relativePath = relative(baseDir, fullPath).split("\\").join("/");
|
|
1191
|
+
results.push({
|
|
1192
|
+
relativePath,
|
|
1193
|
+
content
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}));
|
|
1197
|
+
}
|
|
1198
|
+
async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
1199
|
+
const lock = await readLocalLock(cwd);
|
|
1200
|
+
lock.skills[skillName] = entry;
|
|
1201
|
+
await writeLocalLock(lock, cwd);
|
|
1202
|
+
}
|
|
1203
|
+
function createEmptyLocalLock() {
|
|
1204
|
+
return {
|
|
1205
|
+
version: CURRENT_VERSION$1,
|
|
1206
|
+
skills: {}
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
899
1209
|
const SKIP_DIRS = [
|
|
900
1210
|
"node_modules",
|
|
901
1211
|
".git",
|
|
@@ -903,6 +1213,61 @@ const SKIP_DIRS = [
|
|
|
903
1213
|
"build",
|
|
904
1214
|
"__pycache__"
|
|
905
1215
|
];
|
|
1216
|
+
const AGENT_PROJECT_SKILL_DIRS = [
|
|
1217
|
+
".aider-desk/skills",
|
|
1218
|
+
".agents/skills",
|
|
1219
|
+
"agent/skills",
|
|
1220
|
+
"data/skills",
|
|
1221
|
+
".autohand/skills",
|
|
1222
|
+
".adal/skills",
|
|
1223
|
+
".bob/skills",
|
|
1224
|
+
".claude/skills",
|
|
1225
|
+
".cline/skills",
|
|
1226
|
+
".codeartsdoer/skills",
|
|
1227
|
+
".codebuddy/skills",
|
|
1228
|
+
".codemaker/skills",
|
|
1229
|
+
".codestudio/skills",
|
|
1230
|
+
".codex/skills",
|
|
1231
|
+
".commandcode/skills",
|
|
1232
|
+
".continue/skills",
|
|
1233
|
+
".github/skills",
|
|
1234
|
+
".goose/skills",
|
|
1235
|
+
".devin/skills",
|
|
1236
|
+
".flocks/plugins/skills",
|
|
1237
|
+
".forge/skills",
|
|
1238
|
+
".hermes/skills",
|
|
1239
|
+
".iflow/skills",
|
|
1240
|
+
".inferencesh/skills",
|
|
1241
|
+
".jazz/skills",
|
|
1242
|
+
".junie/skills",
|
|
1243
|
+
".kilocode/skills",
|
|
1244
|
+
".kiro/skills",
|
|
1245
|
+
".mux/skills",
|
|
1246
|
+
".lingma/skills",
|
|
1247
|
+
".moxby/skills",
|
|
1248
|
+
".neovate/skills",
|
|
1249
|
+
".ona/skills",
|
|
1250
|
+
".opencode/skills",
|
|
1251
|
+
".openhands/skills",
|
|
1252
|
+
".pi/skills",
|
|
1253
|
+
".pochi/skills",
|
|
1254
|
+
".qoder/skills",
|
|
1255
|
+
".reasonix/skills",
|
|
1256
|
+
".rovodev/skills",
|
|
1257
|
+
".roo/skills",
|
|
1258
|
+
".tabnine/agent/skills",
|
|
1259
|
+
".terramind/skills",
|
|
1260
|
+
".tinycloud/skills",
|
|
1261
|
+
".trae/skills",
|
|
1262
|
+
".windsurf/skills",
|
|
1263
|
+
".zencoder/skills"
|
|
1264
|
+
];
|
|
1265
|
+
function normalizeSkillName(name) {
|
|
1266
|
+
return name.toLowerCase().replace(/[\s_]+/g, "-");
|
|
1267
|
+
}
|
|
1268
|
+
function normalizeRelativePath(path) {
|
|
1269
|
+
return path.split(sep).join("/").replace(/\/+/g, "/");
|
|
1270
|
+
}
|
|
906
1271
|
function shouldInstallInternalSkills() {
|
|
907
1272
|
const envValue = process.env.INSTALL_INTERNAL_SKILLS;
|
|
908
1273
|
return envValue === "1" || envValue === "true";
|
|
@@ -917,7 +1282,7 @@ async function hasSkillMd(dir) {
|
|
|
917
1282
|
async function parseSkillMd(skillMdPath, options) {
|
|
918
1283
|
try {
|
|
919
1284
|
const content = await readFile(skillMdPath, "utf-8");
|
|
920
|
-
const { data } = (
|
|
1285
|
+
const { data } = parseFrontmatter(content);
|
|
921
1286
|
let skillData = data;
|
|
922
1287
|
if (!skillData.name || !skillData.description) try {
|
|
923
1288
|
const metaContent = await readFile(join(dirname(skillMdPath), "_meta.json"), "utf-8");
|
|
@@ -940,9 +1305,14 @@ async function parseSkillMd(skillMdPath, options) {
|
|
|
940
1305
|
if (!skillData.name || !skillData.description) return null;
|
|
941
1306
|
if (typeof skillData.name !== "string" || typeof skillData.description !== "string") return null;
|
|
942
1307
|
if (skillData.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
|
|
1308
|
+
let name = sanitizeMetadata(skillData.name);
|
|
1309
|
+
let description = sanitizeMetadata(skillData.description);
|
|
1310
|
+
if (!name && options?.fallbackName) name = sanitizeMetadata(options.fallbackName);
|
|
1311
|
+
if (!description && options?.fallbackDescription) description = sanitizeMetadata(options.fallbackDescription);
|
|
1312
|
+
if (!name || !description) return null;
|
|
943
1313
|
return {
|
|
944
|
-
name
|
|
945
|
-
description
|
|
1314
|
+
name,
|
|
1315
|
+
description,
|
|
946
1316
|
path: dirname(skillMdPath),
|
|
947
1317
|
rawContent: content,
|
|
948
1318
|
metadata: skillData.metadata,
|
|
@@ -972,6 +1342,8 @@ function isSubpathSafe(basePath, subpath) {
|
|
|
972
1342
|
async function discoverSkills(basePath, subpath, options) {
|
|
973
1343
|
const skills = [];
|
|
974
1344
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1345
|
+
const localLock = await readLocalLock(basePath);
|
|
1346
|
+
const lockedSkillNames = new Set(Object.keys(localLock.skills).map(normalizeSkillName));
|
|
975
1347
|
if (subpath && !isSubpathSafe(basePath, subpath)) throw new Error(`Invalid subpath: "${subpath}" resolves outside the repository directory. Subpath must not contain ".." segments that escape the base path.`);
|
|
976
1348
|
const searchPath = subpath ? join(basePath, subpath) : basePath;
|
|
977
1349
|
const pluginGroupings = await getPluginGroupings(searchPath);
|
|
@@ -980,13 +1352,23 @@ async function discoverSkills(basePath, subpath, options) {
|
|
|
980
1352
|
if (pluginGroupings.has(resolvedPath)) skill.pluginName = pluginGroupings.get(resolvedPath);
|
|
981
1353
|
return skill;
|
|
982
1354
|
};
|
|
1355
|
+
const isInstalledProjectSkill = (skill) => {
|
|
1356
|
+
if (lockedSkillNames.size === 0) return false;
|
|
1357
|
+
const relativeDir = normalizeRelativePath(relative(basePath, skill.path));
|
|
1358
|
+
if (!AGENT_PROJECT_SKILL_DIRS.some((dir) => relativeDir === dir || relativeDir.startsWith(`${dir}/`))) return false;
|
|
1359
|
+
const skillName = normalizeSkillName(skill.name);
|
|
1360
|
+
const directoryName = normalizeSkillName(basename(skill.path));
|
|
1361
|
+
return lockedSkillNames.has(skillName) || lockedSkillNames.has(directoryName);
|
|
1362
|
+
};
|
|
983
1363
|
if (await hasSkillMd(searchPath)) {
|
|
984
1364
|
let skill = await parseSkillMd(join(searchPath, "SKILL.md"), options);
|
|
985
1365
|
if (skill) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1366
|
+
if (!isInstalledProjectSkill(skill)) {
|
|
1367
|
+
skill = enhanceSkill(skill);
|
|
1368
|
+
skills.push(skill);
|
|
1369
|
+
seenNames.add(skill.name);
|
|
1370
|
+
if (!options?.fullDepth) return skills;
|
|
1371
|
+
}
|
|
990
1372
|
}
|
|
991
1373
|
}
|
|
992
1374
|
const prioritySearchDirs = [
|
|
@@ -995,50 +1377,44 @@ async function discoverSkills(basePath, subpath, options) {
|
|
|
995
1377
|
join(searchPath, "skills/.curated"),
|
|
996
1378
|
join(searchPath, "skills/.experimental"),
|
|
997
1379
|
join(searchPath, "skills/.system"),
|
|
998
|
-
join(searchPath,
|
|
999
|
-
join(searchPath, ".claude/skills"),
|
|
1000
|
-
join(searchPath, ".cline/skills"),
|
|
1001
|
-
join(searchPath, ".codebuddy/skills"),
|
|
1002
|
-
join(searchPath, ".codex/skills"),
|
|
1003
|
-
join(searchPath, ".commandcode/skills"),
|
|
1004
|
-
join(searchPath, ".continue/skills"),
|
|
1005
|
-
join(searchPath, ".github/skills"),
|
|
1006
|
-
join(searchPath, ".goose/skills"),
|
|
1007
|
-
join(searchPath, ".iflow/skills"),
|
|
1008
|
-
join(searchPath, ".junie/skills"),
|
|
1009
|
-
join(searchPath, ".kilocode/skills"),
|
|
1010
|
-
join(searchPath, ".kiro/skills"),
|
|
1011
|
-
join(searchPath, ".mux/skills"),
|
|
1012
|
-
join(searchPath, ".neovate/skills"),
|
|
1013
|
-
join(searchPath, ".opencode/skills"),
|
|
1014
|
-
join(searchPath, ".openhands/skills"),
|
|
1015
|
-
join(searchPath, ".pi/skills"),
|
|
1016
|
-
join(searchPath, ".qoder/skills"),
|
|
1017
|
-
join(searchPath, ".roo/skills"),
|
|
1018
|
-
join(searchPath, ".trae/skills"),
|
|
1019
|
-
join(searchPath, ".windsurf/skills"),
|
|
1020
|
-
join(searchPath, ".zencoder/skills")
|
|
1380
|
+
...AGENT_PROJECT_SKILL_DIRS.map((dir) => join(searchPath, dir))
|
|
1021
1381
|
];
|
|
1382
|
+
const deepContainerDirs = new Set(prioritySearchDirs.slice(1));
|
|
1022
1383
|
prioritySearchDirs.push(...await getPluginSkillPaths(searchPath));
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1384
|
+
const tryAddSkillAt = async (skillDir) => {
|
|
1385
|
+
if (!await hasSkillMd(skillDir)) return false;
|
|
1386
|
+
let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
|
|
1387
|
+
if (!skill || seenNames.has(skill.name)) return true;
|
|
1388
|
+
if (isInstalledProjectSkill(skill)) return true;
|
|
1389
|
+
skill = enhanceSkill(skill);
|
|
1390
|
+
skills.push(skill);
|
|
1391
|
+
seenNames.add(skill.name);
|
|
1392
|
+
return true;
|
|
1393
|
+
};
|
|
1394
|
+
for (const dir of prioritySearchDirs) {
|
|
1395
|
+
const walkDeep = deepContainerDirs.has(dir);
|
|
1396
|
+
try {
|
|
1397
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1398
|
+
for (const entry of entries) {
|
|
1399
|
+
if (!entry.isDirectory()) continue;
|
|
1400
|
+
const childDir = join(dir, entry.name);
|
|
1401
|
+
if (await tryAddSkillAt(childDir) || !walkDeep) continue;
|
|
1402
|
+
if (SKIP_DIRS.includes(entry.name)) continue;
|
|
1403
|
+
try {
|
|
1404
|
+
const grandEntries = await readdir(childDir, { withFileTypes: true });
|
|
1405
|
+
for (const grand of grandEntries) {
|
|
1406
|
+
if (!grand.isDirectory() || SKIP_DIRS.includes(grand.name)) continue;
|
|
1407
|
+
await tryAddSkillAt(join(childDir, grand.name));
|
|
1408
|
+
}
|
|
1409
|
+
} catch {}
|
|
1034
1410
|
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1411
|
+
} catch {}
|
|
1412
|
+
}
|
|
1037
1413
|
if (skills.length === 0 || options?.fullDepth) {
|
|
1038
1414
|
const allSkillDirs = await findSkillDirs(searchPath);
|
|
1039
1415
|
for (const skillDir of allSkillDirs) {
|
|
1040
1416
|
let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
|
|
1041
|
-
if (skill && !seenNames.has(skill.name)) {
|
|
1417
|
+
if (skill && !seenNames.has(skill.name) && !isInstalledProjectSkill(skill)) {
|
|
1042
1418
|
skill = enhanceSkill(skill);
|
|
1043
1419
|
skills.push(skill);
|
|
1044
1420
|
seenNames.add(skill.name);
|
|
@@ -1062,6 +1438,19 @@ const home = homedir();
|
|
|
1062
1438
|
const configHome = xdgConfig ?? join(home, ".config");
|
|
1063
1439
|
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
1064
1440
|
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
1441
|
+
const vibeHome = process.env.VIBE_HOME?.trim() || join(home, ".vibe");
|
|
1442
|
+
const hermesHome = process.env.HERMES_HOME?.trim() || join(home, ".hermes");
|
|
1443
|
+
const autohandHome = process.env.AUTOHAND_HOME?.trim() || join(home, ".autohand");
|
|
1444
|
+
const zedAppDataHome = process.env.APPDATA?.trim();
|
|
1445
|
+
const zedFlatpakConfigHome = process.env.FLATPAK_XDG_CONFIG_HOME?.trim();
|
|
1446
|
+
function packageJsonHasDependency(packageJsonPath, dependencyName) {
|
|
1447
|
+
try {
|
|
1448
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1449
|
+
return !!(packageJson.dependencies?.[dependencyName] || packageJson.devDependencies?.[dependencyName]);
|
|
1450
|
+
} catch {
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1065
1454
|
function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
|
|
1066
1455
|
if (pathExists(join(homeDir, ".openclaw"))) return join(homeDir, ".openclaw/skills");
|
|
1067
1456
|
if (pathExists(join(homeDir, ".clawdbot"))) return join(homeDir, ".clawdbot/skills");
|
|
@@ -1069,6 +1458,13 @@ function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
|
|
|
1069
1458
|
return join(homeDir, ".openclaw/skills");
|
|
1070
1459
|
}
|
|
1071
1460
|
const agents = {
|
|
1461
|
+
"aider-desk": {
|
|
1462
|
+
name: "aider-desk",
|
|
1463
|
+
displayName: "AiderDesk",
|
|
1464
|
+
skillsDir: ".aider-desk/skills",
|
|
1465
|
+
globalSkillsDir: join(home, ".aider-desk/skills"),
|
|
1466
|
+
detectInstalled: async () => existsSync(join(home, ".aider-desk"))
|
|
1467
|
+
},
|
|
1072
1468
|
amp: {
|
|
1073
1469
|
name: "amp",
|
|
1074
1470
|
displayName: "Amp",
|
|
@@ -1087,6 +1483,27 @@ const agents = {
|
|
|
1087
1483
|
return existsSync(join(home, ".gemini/antigravity"));
|
|
1088
1484
|
}
|
|
1089
1485
|
},
|
|
1486
|
+
"antigravity-cli": {
|
|
1487
|
+
name: "antigravity-cli",
|
|
1488
|
+
displayName: "Antigravity CLI",
|
|
1489
|
+
skillsDir: ".agents/skills",
|
|
1490
|
+
globalSkillsDir: join(home, ".gemini/antigravity-cli/skills"),
|
|
1491
|
+
detectInstalled: async () => existsSync(join(home, ".gemini/antigravity-cli"))
|
|
1492
|
+
},
|
|
1493
|
+
astrbot: {
|
|
1494
|
+
name: "astrbot",
|
|
1495
|
+
displayName: "AstrBot",
|
|
1496
|
+
skillsDir: "data/skills",
|
|
1497
|
+
globalSkillsDir: join(home, ".astrbot/data/skills"),
|
|
1498
|
+
detectInstalled: async () => existsSync(join(process.cwd(), "data/skills")) || existsSync(join(home, ".astrbot"))
|
|
1499
|
+
},
|
|
1500
|
+
"autohand-code": {
|
|
1501
|
+
name: "autohand-code",
|
|
1502
|
+
displayName: "Autohand Code CLI",
|
|
1503
|
+
skillsDir: ".autohand/skills",
|
|
1504
|
+
globalSkillsDir: join(autohandHome, "skills"),
|
|
1505
|
+
detectInstalled: async () => existsSync(autohandHome)
|
|
1506
|
+
},
|
|
1090
1507
|
augment: {
|
|
1091
1508
|
name: "augment",
|
|
1092
1509
|
displayName: "Augment",
|
|
@@ -1096,6 +1513,13 @@ const agents = {
|
|
|
1096
1513
|
return existsSync(join(home, ".augment"));
|
|
1097
1514
|
}
|
|
1098
1515
|
},
|
|
1516
|
+
bob: {
|
|
1517
|
+
name: "bob",
|
|
1518
|
+
displayName: "IBM Bob",
|
|
1519
|
+
skillsDir: ".bob/skills",
|
|
1520
|
+
globalSkillsDir: join(home, ".bob/skills"),
|
|
1521
|
+
detectInstalled: async () => existsSync(join(home, ".bob"))
|
|
1522
|
+
},
|
|
1099
1523
|
"claude-code": {
|
|
1100
1524
|
name: "claude-code",
|
|
1101
1525
|
displayName: "Claude Code",
|
|
@@ -1123,6 +1547,13 @@ const agents = {
|
|
|
1123
1547
|
return existsSync(join(home, ".cline"));
|
|
1124
1548
|
}
|
|
1125
1549
|
},
|
|
1550
|
+
"codearts-agent": {
|
|
1551
|
+
name: "codearts-agent",
|
|
1552
|
+
displayName: "CodeArts Agent",
|
|
1553
|
+
skillsDir: ".codeartsdoer/skills",
|
|
1554
|
+
globalSkillsDir: join(home, ".codeartsdoer/skills"),
|
|
1555
|
+
detectInstalled: async () => existsSync(join(home, ".codeartsdoer"))
|
|
1556
|
+
},
|
|
1126
1557
|
codebuddy: {
|
|
1127
1558
|
name: "codebuddy",
|
|
1128
1559
|
displayName: "CodeBuddy",
|
|
@@ -1132,6 +1563,20 @@ const agents = {
|
|
|
1132
1563
|
return existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home, ".codebuddy"));
|
|
1133
1564
|
}
|
|
1134
1565
|
},
|
|
1566
|
+
codemaker: {
|
|
1567
|
+
name: "codemaker",
|
|
1568
|
+
displayName: "Codemaker",
|
|
1569
|
+
skillsDir: ".codemaker/skills",
|
|
1570
|
+
globalSkillsDir: join(home, ".codemaker/skills"),
|
|
1571
|
+
detectInstalled: async () => existsSync(join(home, ".codemaker"))
|
|
1572
|
+
},
|
|
1573
|
+
codestudio: {
|
|
1574
|
+
name: "codestudio",
|
|
1575
|
+
displayName: "Code Studio",
|
|
1576
|
+
skillsDir: ".codestudio/skills",
|
|
1577
|
+
globalSkillsDir: join(home, ".codestudio/skills"),
|
|
1578
|
+
detectInstalled: async () => existsSync(join(home, ".codestudio"))
|
|
1579
|
+
},
|
|
1135
1580
|
codex: {
|
|
1136
1581
|
name: "codex",
|
|
1137
1582
|
displayName: "Codex",
|
|
@@ -1195,6 +1640,21 @@ const agents = {
|
|
|
1195
1640
|
return existsSync(join(home, ".deepagents"));
|
|
1196
1641
|
}
|
|
1197
1642
|
},
|
|
1643
|
+
devin: {
|
|
1644
|
+
name: "devin",
|
|
1645
|
+
displayName: "Devin for Terminal",
|
|
1646
|
+
skillsDir: ".devin/skills",
|
|
1647
|
+
globalSkillsDir: join(configHome, "devin/skills"),
|
|
1648
|
+
detectInstalled: async () => existsSync(join(configHome, "devin"))
|
|
1649
|
+
},
|
|
1650
|
+
dexto: {
|
|
1651
|
+
name: "dexto",
|
|
1652
|
+
displayName: "Dexto",
|
|
1653
|
+
skillsDir: ".agents/skills",
|
|
1654
|
+
globalSkillsDir: join(home, ".agents/skills"),
|
|
1655
|
+
showInUniversalPrompt: false,
|
|
1656
|
+
detectInstalled: async () => existsSync(join(home, ".dexto"))
|
|
1657
|
+
},
|
|
1198
1658
|
droid: {
|
|
1199
1659
|
name: "droid",
|
|
1200
1660
|
displayName: "Droid",
|
|
@@ -1204,15 +1664,33 @@ const agents = {
|
|
|
1204
1664
|
return existsSync(join(home, ".factory"));
|
|
1205
1665
|
}
|
|
1206
1666
|
},
|
|
1667
|
+
eve: {
|
|
1668
|
+
name: "eve",
|
|
1669
|
+
displayName: "Eve",
|
|
1670
|
+
skillsDir: "agent/skills",
|
|
1671
|
+
globalSkillsDir: void 0,
|
|
1672
|
+
detectInstalled: async () => {
|
|
1673
|
+
const cwd = process.cwd();
|
|
1674
|
+
return existsSync(join(cwd, "agent")) && packageJsonHasDependency(join(cwd, "package.json"), "eve");
|
|
1675
|
+
}
|
|
1676
|
+
},
|
|
1207
1677
|
firebender: {
|
|
1208
1678
|
name: "firebender",
|
|
1209
1679
|
displayName: "Firebender",
|
|
1210
1680
|
skillsDir: ".agents/skills",
|
|
1211
1681
|
globalSkillsDir: join(home, ".firebender/skills"),
|
|
1682
|
+
showInUniversalPrompt: false,
|
|
1212
1683
|
detectInstalled: async () => {
|
|
1213
1684
|
return existsSync(join(home, ".firebender"));
|
|
1214
1685
|
}
|
|
1215
1686
|
},
|
|
1687
|
+
forgecode: {
|
|
1688
|
+
name: "forgecode",
|
|
1689
|
+
displayName: "ForgeCode",
|
|
1690
|
+
skillsDir: ".forge/skills",
|
|
1691
|
+
globalSkillsDir: join(home, ".forge/skills"),
|
|
1692
|
+
detectInstalled: async () => existsSync(join(home, ".forge"))
|
|
1693
|
+
},
|
|
1216
1694
|
flocks: {
|
|
1217
1695
|
name: "flocks",
|
|
1218
1696
|
displayName: "Flocks",
|
|
@@ -1249,6 +1727,27 @@ const agents = {
|
|
|
1249
1727
|
return existsSync(join(configHome, "goose"));
|
|
1250
1728
|
}
|
|
1251
1729
|
},
|
|
1730
|
+
"hermes-agent": {
|
|
1731
|
+
name: "hermes-agent",
|
|
1732
|
+
displayName: "Hermes Agent",
|
|
1733
|
+
skillsDir: ".hermes/skills",
|
|
1734
|
+
globalSkillsDir: join(hermesHome, "skills"),
|
|
1735
|
+
detectInstalled: async () => existsSync(hermesHome)
|
|
1736
|
+
},
|
|
1737
|
+
"inference-sh": {
|
|
1738
|
+
name: "inference-sh",
|
|
1739
|
+
displayName: "inference.sh",
|
|
1740
|
+
skillsDir: ".inferencesh/skills",
|
|
1741
|
+
globalSkillsDir: join(home, ".inferencesh/skills"),
|
|
1742
|
+
detectInstalled: async () => existsSync(join(home, ".inferencesh"))
|
|
1743
|
+
},
|
|
1744
|
+
jazz: {
|
|
1745
|
+
name: "jazz",
|
|
1746
|
+
displayName: "Jazz",
|
|
1747
|
+
skillsDir: ".jazz/skills",
|
|
1748
|
+
globalSkillsDir: join(home, ".jazz/skills"),
|
|
1749
|
+
detectInstalled: async () => existsSync(join(home, ".jazz")) || existsSync(join(process.cwd(), ".jazz"))
|
|
1750
|
+
},
|
|
1252
1751
|
junie: {
|
|
1253
1752
|
name: "junie",
|
|
1254
1753
|
displayName: "Junie",
|
|
@@ -1278,13 +1777,22 @@ const agents = {
|
|
|
1278
1777
|
},
|
|
1279
1778
|
"kimi-cli": {
|
|
1280
1779
|
name: "kimi-cli",
|
|
1281
|
-
displayName: "Kimi
|
|
1780
|
+
displayName: "Kimi CLI",
|
|
1282
1781
|
skillsDir: ".agents/skills",
|
|
1283
1782
|
globalSkillsDir: join(home, ".config/agents/skills"),
|
|
1284
1783
|
detectInstalled: async () => {
|
|
1285
1784
|
return existsSync(join(home, ".kimi"));
|
|
1286
1785
|
}
|
|
1287
1786
|
},
|
|
1787
|
+
"kimi-code-cli": {
|
|
1788
|
+
name: "kimi-code-cli",
|
|
1789
|
+
displayName: "Kimi Code CLI",
|
|
1790
|
+
skillsDir: ".agents/skills",
|
|
1791
|
+
globalSkillsDir: join(home, ".agents/skills"),
|
|
1792
|
+
detectInstalled: async () => {
|
|
1793
|
+
return existsSync(join(home, ".kimi-code")) || existsSync(join(home, ".kimi"));
|
|
1794
|
+
}
|
|
1795
|
+
},
|
|
1288
1796
|
"kiro-cli": {
|
|
1289
1797
|
name: "kiro-cli",
|
|
1290
1798
|
displayName: "Kiro CLI",
|
|
@@ -1303,6 +1811,21 @@ const agents = {
|
|
|
1303
1811
|
return existsSync(join(home, ".kode"));
|
|
1304
1812
|
}
|
|
1305
1813
|
},
|
|
1814
|
+
lingma: {
|
|
1815
|
+
name: "lingma",
|
|
1816
|
+
displayName: "Lingma",
|
|
1817
|
+
skillsDir: ".lingma/skills",
|
|
1818
|
+
globalSkillsDir: join(home, ".lingma/skills"),
|
|
1819
|
+
detectInstalled: async () => existsSync(join(home, ".lingma"))
|
|
1820
|
+
},
|
|
1821
|
+
loaf: {
|
|
1822
|
+
name: "loaf",
|
|
1823
|
+
displayName: "Loaf",
|
|
1824
|
+
skillsDir: ".agents/skills",
|
|
1825
|
+
globalSkillsDir: join(home, ".agents/skills"),
|
|
1826
|
+
showInUniversalPrompt: false,
|
|
1827
|
+
detectInstalled: async () => existsSync(join(home, ".loaf"))
|
|
1828
|
+
},
|
|
1306
1829
|
mcpjam: {
|
|
1307
1830
|
name: "mcpjam",
|
|
1308
1831
|
displayName: "MCPJam",
|
|
@@ -1316,11 +1839,18 @@ const agents = {
|
|
|
1316
1839
|
name: "mistral-vibe",
|
|
1317
1840
|
displayName: "Mistral Vibe",
|
|
1318
1841
|
skillsDir: ".vibe/skills",
|
|
1319
|
-
globalSkillsDir: join(
|
|
1842
|
+
globalSkillsDir: join(vibeHome, "skills"),
|
|
1320
1843
|
detectInstalled: async () => {
|
|
1321
|
-
return existsSync(
|
|
1844
|
+
return existsSync(vibeHome);
|
|
1322
1845
|
}
|
|
1323
1846
|
},
|
|
1847
|
+
moxby: {
|
|
1848
|
+
name: "moxby",
|
|
1849
|
+
displayName: "Moxby",
|
|
1850
|
+
skillsDir: ".moxby/skills",
|
|
1851
|
+
globalSkillsDir: join(home, ".moxby/skills"),
|
|
1852
|
+
detectInstalled: async () => existsSync(join(home, ".moxby"))
|
|
1853
|
+
},
|
|
1324
1854
|
mux: {
|
|
1325
1855
|
name: "mux",
|
|
1326
1856
|
displayName: "Mux",
|
|
@@ -1348,6 +1878,13 @@ const agents = {
|
|
|
1348
1878
|
return existsSync(join(home, ".openhands"));
|
|
1349
1879
|
}
|
|
1350
1880
|
},
|
|
1881
|
+
ona: {
|
|
1882
|
+
name: "ona",
|
|
1883
|
+
displayName: "Ona",
|
|
1884
|
+
skillsDir: ".ona/skills",
|
|
1885
|
+
globalSkillsDir: join(home, ".ona/skills"),
|
|
1886
|
+
detectInstalled: async () => existsSync(join(home, ".ona"))
|
|
1887
|
+
},
|
|
1351
1888
|
pi: {
|
|
1352
1889
|
name: "pi",
|
|
1353
1890
|
displayName: "Pi",
|
|
@@ -1366,6 +1903,13 @@ const agents = {
|
|
|
1366
1903
|
return existsSync(join(home, ".qoder"));
|
|
1367
1904
|
}
|
|
1368
1905
|
},
|
|
1906
|
+
"qoder-cn": {
|
|
1907
|
+
name: "qoder-cn",
|
|
1908
|
+
displayName: "Qoder CN",
|
|
1909
|
+
skillsDir: ".qoder/skills",
|
|
1910
|
+
globalSkillsDir: join(home, ".qoder-cn/skills"),
|
|
1911
|
+
detectInstalled: async () => existsSync(join(home, ".qoder-cn"))
|
|
1912
|
+
},
|
|
1369
1913
|
"qwen-code": {
|
|
1370
1914
|
name: "qwen-code",
|
|
1371
1915
|
displayName: "Qwen Code",
|
|
@@ -1385,6 +1929,20 @@ const agents = {
|
|
|
1385
1929
|
return existsSync(join(process.cwd(), ".replit"));
|
|
1386
1930
|
}
|
|
1387
1931
|
},
|
|
1932
|
+
reasonix: {
|
|
1933
|
+
name: "reasonix",
|
|
1934
|
+
displayName: "Reasonix",
|
|
1935
|
+
skillsDir: ".reasonix/skills",
|
|
1936
|
+
globalSkillsDir: join(home, ".reasonix/skills"),
|
|
1937
|
+
detectInstalled: async () => existsSync(join(home, ".reasonix"))
|
|
1938
|
+
},
|
|
1939
|
+
rovodev: {
|
|
1940
|
+
name: "rovodev",
|
|
1941
|
+
displayName: "Rovo Dev",
|
|
1942
|
+
skillsDir: ".rovodev/skills",
|
|
1943
|
+
globalSkillsDir: join(home, ".rovodev/skills"),
|
|
1944
|
+
detectInstalled: async () => existsSync(join(home, ".rovodev"))
|
|
1945
|
+
},
|
|
1388
1946
|
roo: {
|
|
1389
1947
|
name: "roo",
|
|
1390
1948
|
displayName: "Roo Code",
|
|
@@ -1394,6 +1952,27 @@ const agents = {
|
|
|
1394
1952
|
return existsSync(join(home, ".roo"));
|
|
1395
1953
|
}
|
|
1396
1954
|
},
|
|
1955
|
+
"tabnine-cli": {
|
|
1956
|
+
name: "tabnine-cli",
|
|
1957
|
+
displayName: "Tabnine CLI",
|
|
1958
|
+
skillsDir: ".tabnine/agent/skills",
|
|
1959
|
+
globalSkillsDir: join(home, ".tabnine/agent/skills"),
|
|
1960
|
+
detectInstalled: async () => existsSync(join(home, ".tabnine"))
|
|
1961
|
+
},
|
|
1962
|
+
terramind: {
|
|
1963
|
+
name: "terramind",
|
|
1964
|
+
displayName: "Terramind",
|
|
1965
|
+
skillsDir: ".terramind/skills",
|
|
1966
|
+
globalSkillsDir: join(home, ".terramind/skills"),
|
|
1967
|
+
detectInstalled: async () => existsSync(join(home, ".terramind"))
|
|
1968
|
+
},
|
|
1969
|
+
tinycloud: {
|
|
1970
|
+
name: "tinycloud",
|
|
1971
|
+
displayName: "Tinycloud",
|
|
1972
|
+
skillsDir: ".tinycloud/skills",
|
|
1973
|
+
globalSkillsDir: join(home, ".tinycloud/skills"),
|
|
1974
|
+
detectInstalled: async () => existsSync(join(home, ".tinycloud"))
|
|
1975
|
+
},
|
|
1397
1976
|
trae: {
|
|
1398
1977
|
name: "trae",
|
|
1399
1978
|
displayName: "Trae",
|
|
@@ -1430,6 +2009,15 @@ const agents = {
|
|
|
1430
2009
|
return existsSync(join(home, ".codeium/windsurf"));
|
|
1431
2010
|
}
|
|
1432
2011
|
},
|
|
2012
|
+
zed: {
|
|
2013
|
+
name: "zed",
|
|
2014
|
+
displayName: "Zed",
|
|
2015
|
+
skillsDir: ".agents/skills",
|
|
2016
|
+
globalSkillsDir: join(home, ".agents/skills"),
|
|
2017
|
+
detectInstalled: async () => {
|
|
2018
|
+
return existsSync(join(configHome, "zed")) || !!zedAppDataHome && existsSync(join(zedAppDataHome, "Zed")) || !!zedFlatpakConfigHome && existsSync(join(zedFlatpakConfigHome, "zed"));
|
|
2019
|
+
}
|
|
2020
|
+
},
|
|
1433
2021
|
zencoder: {
|
|
1434
2022
|
name: "zencoder",
|
|
1435
2023
|
displayName: "Zencoder",
|
|
@@ -1439,6 +2027,13 @@ const agents = {
|
|
|
1439
2027
|
return existsSync(join(home, ".zencoder"));
|
|
1440
2028
|
}
|
|
1441
2029
|
},
|
|
2030
|
+
zenflow: {
|
|
2031
|
+
name: "zenflow",
|
|
2032
|
+
displayName: "Zenflow",
|
|
2033
|
+
skillsDir: ".zencoder/skills",
|
|
2034
|
+
globalSkillsDir: join(home, ".zencoder/skills"),
|
|
2035
|
+
detectInstalled: async () => existsSync(join(home, ".zencoder"))
|
|
2036
|
+
},
|
|
1442
2037
|
neovate: {
|
|
1443
2038
|
name: "neovate",
|
|
1444
2039
|
displayName: "Neovate",
|
|
@@ -1457,6 +2052,16 @@ const agents = {
|
|
|
1457
2052
|
return existsSync(join(home, ".pochi"));
|
|
1458
2053
|
}
|
|
1459
2054
|
},
|
|
2055
|
+
promptscript: {
|
|
2056
|
+
name: "promptscript",
|
|
2057
|
+
displayName: "PromptScript",
|
|
2058
|
+
skillsDir: ".agents/skills",
|
|
2059
|
+
globalSkillsDir: void 0,
|
|
2060
|
+
showInUniversalPrompt: false,
|
|
2061
|
+
detectInstalled: async () => {
|
|
2062
|
+
return existsSync(join(process.cwd(), ".promptscript")) || existsSync(join(process.cwd(), "promptscript.yaml"));
|
|
2063
|
+
}
|
|
2064
|
+
},
|
|
1460
2065
|
adal: {
|
|
1461
2066
|
name: "adal",
|
|
1462
2067
|
displayName: "AdaL",
|
|
@@ -1481,6 +2086,16 @@ async function detectInstalledAgents() {
|
|
|
1481
2086
|
installed: await config.detectInstalled()
|
|
1482
2087
|
})))).filter((r) => r.installed).map((r) => r.type);
|
|
1483
2088
|
}
|
|
2089
|
+
const EVE_SUBAGENTS_DIR = join("agent", "subagents");
|
|
2090
|
+
function getEveSubagents(cwd = process.cwd()) {
|
|
2091
|
+
const dir = join(cwd, EVE_SUBAGENTS_DIR);
|
|
2092
|
+
if (!existsSync(dir)) return [];
|
|
2093
|
+
try {
|
|
2094
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
2095
|
+
} catch {
|
|
2096
|
+
return [];
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
1484
2099
|
function getUniversalAgents() {
|
|
1485
2100
|
return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type);
|
|
1486
2101
|
}
|
|
@@ -1490,7 +2105,7 @@ function getNonUniversalAgents() {
|
|
|
1490
2105
|
function isUniversalAgent(type) {
|
|
1491
2106
|
return agents[type].skillsDir === ".agents/skills";
|
|
1492
2107
|
}
|
|
1493
|
-
const AGENTS_DIR$
|
|
2108
|
+
const AGENTS_DIR$1 = ".agents";
|
|
1494
2109
|
const SKILLS_SUBDIR = "skills";
|
|
1495
2110
|
function sanitizeName(name) {
|
|
1496
2111
|
const asciiName = name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "");
|
|
@@ -1509,15 +2124,19 @@ function sanitizeName(name) {
|
|
|
1509
2124
|
function sanitizeSkillName(skill) {
|
|
1510
2125
|
return sanitizeName(skill.installName || skill.slug || skill.name);
|
|
1511
2126
|
}
|
|
1512
|
-
function isPathSafe(basePath, targetPath) {
|
|
2127
|
+
function isPathSafe$1(basePath, targetPath) {
|
|
1513
2128
|
const normalizedBase = normalize(resolve(basePath));
|
|
1514
2129
|
const normalizedTarget = normalize(resolve(targetPath));
|
|
1515
2130
|
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
1516
2131
|
}
|
|
1517
2132
|
function getCanonicalSkillsDir(global, cwd) {
|
|
1518
|
-
return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$
|
|
2133
|
+
return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
|
|
2134
|
+
}
|
|
2135
|
+
function getEveSubagentSkillsDir(subagent, cwd) {
|
|
2136
|
+
return join(cwd || process.cwd(), EVE_SUBAGENTS_DIR, sanitizeName(subagent), "skills");
|
|
1519
2137
|
}
|
|
1520
|
-
function getAgentBaseDir(agentType, global, cwd) {
|
|
2138
|
+
function getAgentBaseDir(agentType, global, cwd, eveSubagent) {
|
|
2139
|
+
if (agentType === "eve" && eveSubagent) return getEveSubagentSkillsDir(eveSubagent, cwd);
|
|
1521
2140
|
if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
|
|
1522
2141
|
const agent = agents[agentType];
|
|
1523
2142
|
const baseDir = global ? homedir() : cwd || process.cwd();
|
|
@@ -1585,18 +2204,18 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1585
2204
|
error: `${agent.displayName} does not support global skill installation`
|
|
1586
2205
|
};
|
|
1587
2206
|
const skillName = sanitizeSkillName(skill);
|
|
1588
|
-
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
2207
|
+
const canonicalBase = agentType === "eve" ? getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
|
|
1589
2208
|
const canonicalDir = join(canonicalBase, skillName);
|
|
1590
|
-
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
|
|
2209
|
+
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent);
|
|
1591
2210
|
const agentDir = join(agentBase, skillName);
|
|
1592
2211
|
const installMode = options.mode ?? "symlink";
|
|
1593
|
-
if (!isPathSafe(canonicalBase, canonicalDir)) return {
|
|
2212
|
+
if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
|
|
1594
2213
|
success: false,
|
|
1595
2214
|
path: agentDir,
|
|
1596
2215
|
mode: installMode,
|
|
1597
2216
|
error: "Invalid skill name: potential path traversal detected"
|
|
1598
2217
|
};
|
|
1599
|
-
if (!isPathSafe(agentBase, agentDir)) return {
|
|
2218
|
+
if (!isPathSafe$1(agentBase, agentDir)) return {
|
|
1600
2219
|
success: false,
|
|
1601
2220
|
path: agentDir,
|
|
1602
2221
|
mode: installMode,
|
|
@@ -1605,7 +2224,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1605
2224
|
try {
|
|
1606
2225
|
if (installMode === "copy") {
|
|
1607
2226
|
await cleanAndCreateDirectory(agentDir);
|
|
1608
|
-
await copyDirectory(skill.path, agentDir);
|
|
2227
|
+
await copyDirectory(skill.path, agentDir, agentType);
|
|
1609
2228
|
return {
|
|
1610
2229
|
success: true,
|
|
1611
2230
|
path: agentDir,
|
|
@@ -1613,7 +2232,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1613
2232
|
};
|
|
1614
2233
|
}
|
|
1615
2234
|
await cleanAndCreateDirectory(canonicalDir);
|
|
1616
|
-
await copyDirectory(skill.path, canonicalDir);
|
|
2235
|
+
await copyDirectory(skill.path, canonicalDir, agentType);
|
|
1617
2236
|
if (isGlobal && isUniversalAgent(agentType)) return {
|
|
1618
2237
|
success: true,
|
|
1619
2238
|
path: canonicalDir,
|
|
@@ -1646,26 +2265,44 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1646
2265
|
};
|
|
1647
2266
|
}
|
|
1648
2267
|
}
|
|
1649
|
-
const EXCLUDE_FILES = new Set(["metadata.json"]);
|
|
1650
|
-
const EXCLUDE_DIRS = new Set([
|
|
2268
|
+
const EXCLUDE_FILES$1 = new Set(["metadata.json"]);
|
|
2269
|
+
const EXCLUDE_DIRS$1 = new Set([
|
|
1651
2270
|
".git",
|
|
1652
2271
|
"__pycache__",
|
|
1653
2272
|
"__pypackages__"
|
|
1654
2273
|
]);
|
|
1655
|
-
const isExcluded = (name, isDirectory = false) => {
|
|
1656
|
-
if (EXCLUDE_FILES.has(name)) return true;
|
|
2274
|
+
const isExcluded$1 = (name, isDirectory = false) => {
|
|
2275
|
+
if (EXCLUDE_FILES$1.has(name)) return true;
|
|
1657
2276
|
if (name.startsWith(".")) return true;
|
|
1658
|
-
if (isDirectory && EXCLUDE_DIRS.has(name)) return true;
|
|
2277
|
+
if (isDirectory && EXCLUDE_DIRS$1.has(name)) return true;
|
|
1659
2278
|
return false;
|
|
1660
2279
|
};
|
|
1661
|
-
|
|
2280
|
+
function stripIgnoredEveFrontmatter(raw) {
|
|
2281
|
+
const { data, content } = parseFrontmatter(raw);
|
|
2282
|
+
const eveData = {};
|
|
2283
|
+
if (typeof data.description === "string") eveData.description = data.description;
|
|
2284
|
+
if (typeof data.license === "string") eveData.license = data.license;
|
|
2285
|
+
if (data.metadata && typeof data.metadata === "object") {
|
|
2286
|
+
const metadata = { ...data.metadata };
|
|
2287
|
+
delete metadata.internal;
|
|
2288
|
+
if (Object.keys(metadata).length > 0) eveData.metadata = metadata;
|
|
2289
|
+
}
|
|
2290
|
+
const keys = Object.keys(eveData);
|
|
2291
|
+
if (keys.length === 0) return content.trimStart();
|
|
2292
|
+
return `---\n${keys.map((key) => `${key}: ${JSON.stringify(eveData[key])}`).join("\n")}\n---\n${content}`;
|
|
2293
|
+
}
|
|
2294
|
+
async function copyDirectory(src, dest, agentType) {
|
|
1662
2295
|
await mkdir(dest, { recursive: true });
|
|
1663
2296
|
const entries = await readdir(src, { withFileTypes: true });
|
|
1664
|
-
await Promise.all(entries.filter((entry) => !isExcluded(entry.name, entry.isDirectory())).map(async (entry) => {
|
|
2297
|
+
await Promise.all(entries.filter((entry) => !isExcluded$1(entry.name, entry.isDirectory())).map(async (entry) => {
|
|
1665
2298
|
const srcPath = join(src, entry.name);
|
|
1666
2299
|
const destPath = join(dest, entry.name);
|
|
1667
|
-
if (entry.isDirectory()) await copyDirectory(srcPath, destPath);
|
|
2300
|
+
if (entry.isDirectory()) await copyDirectory(srcPath, destPath, agentType);
|
|
1668
2301
|
else try {
|
|
2302
|
+
if (agentType === "eve" && entry.name.toLowerCase() === "skill.md") {
|
|
2303
|
+
await writeFile(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
1669
2306
|
await cp(srcPath, destPath, {
|
|
1670
2307
|
dereference: true,
|
|
1671
2308
|
recursive: true
|
|
@@ -1680,9 +2317,9 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
|
|
|
1680
2317
|
const agent = agents[agentType];
|
|
1681
2318
|
const sanitized = sanitizeName(skillName);
|
|
1682
2319
|
if (options.global && agent.globalSkillsDir === void 0) return false;
|
|
1683
|
-
const targetBase = options.global
|
|
2320
|
+
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd, options.eveSubagent);
|
|
1684
2321
|
const skillDir = join(targetBase, sanitized);
|
|
1685
|
-
if (!isPathSafe(targetBase, skillDir)) return false;
|
|
2322
|
+
if (!isPathSafe$1(targetBase, skillDir)) return false;
|
|
1686
2323
|
try {
|
|
1687
2324
|
await access(skillDir);
|
|
1688
2325
|
return true;
|
|
@@ -1694,16 +2331,16 @@ function getInstallPath(skillName, agentType, options = {}) {
|
|
|
1694
2331
|
agents[agentType];
|
|
1695
2332
|
options.cwd || process.cwd();
|
|
1696
2333
|
const sanitized = sanitizeName(skillName);
|
|
1697
|
-
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd);
|
|
2334
|
+
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd, options.eveSubagent);
|
|
1698
2335
|
const installPath = join(targetBase, sanitized);
|
|
1699
|
-
if (!isPathSafe(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
2336
|
+
if (!isPathSafe$1(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
1700
2337
|
return installPath;
|
|
1701
2338
|
}
|
|
1702
2339
|
function getCanonicalPath(skillName, options = {}) {
|
|
1703
2340
|
const sanitized = sanitizeName(skillName);
|
|
1704
2341
|
const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
|
|
1705
2342
|
const canonicalPath = join(canonicalBase, sanitized);
|
|
1706
|
-
if (!isPathSafe(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
2343
|
+
if (!isPathSafe$1(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
1707
2344
|
return canonicalPath;
|
|
1708
2345
|
}
|
|
1709
2346
|
async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
@@ -1718,17 +2355,17 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
|
1718
2355
|
error: `${agent.displayName} does not support global skill installation`
|
|
1719
2356
|
};
|
|
1720
2357
|
const skillName = sanitizeSkillName(skill);
|
|
1721
|
-
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
2358
|
+
const canonicalBase = agentType === "eve" ? getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
|
|
1722
2359
|
const canonicalDir = join(canonicalBase, skillName);
|
|
1723
|
-
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
|
|
2360
|
+
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent);
|
|
1724
2361
|
const agentDir = join(agentBase, skillName);
|
|
1725
|
-
if (!isPathSafe(canonicalBase, canonicalDir)) return {
|
|
2362
|
+
if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
|
|
1726
2363
|
success: false,
|
|
1727
2364
|
path: agentDir,
|
|
1728
2365
|
mode: installMode,
|
|
1729
2366
|
error: "Invalid skill name: potential path traversal detected"
|
|
1730
2367
|
};
|
|
1731
|
-
if (!isPathSafe(agentBase, agentDir)) return {
|
|
2368
|
+
if (!isPathSafe$1(agentBase, agentDir)) return {
|
|
1732
2369
|
success: false,
|
|
1733
2370
|
path: agentDir,
|
|
1734
2371
|
mode: installMode,
|
|
@@ -1737,10 +2374,10 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
|
1737
2374
|
async function writeSkillFiles(targetDir) {
|
|
1738
2375
|
for (const [filePath, content] of skill.files) {
|
|
1739
2376
|
const fullPath = join(targetDir, filePath);
|
|
1740
|
-
if (!isPathSafe(targetDir, fullPath)) continue;
|
|
2377
|
+
if (!isPathSafe$1(targetDir, fullPath)) continue;
|
|
1741
2378
|
const parentDir = dirname(fullPath);
|
|
1742
2379
|
if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
|
|
1743
|
-
await writeFile(fullPath, content, "utf-8");
|
|
2380
|
+
await writeFile(fullPath, agentType === "eve" && basename(filePath).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(content) : content, "utf-8");
|
|
1744
2381
|
}
|
|
1745
2382
|
}
|
|
1746
2383
|
try {
|
|
@@ -1811,6 +2448,14 @@ async function listInstalledSkills(options = {}) {
|
|
|
1811
2448
|
path: agentDir,
|
|
1812
2449
|
agentType
|
|
1813
2450
|
});
|
|
2451
|
+
if (agentType === "eve" && !isGlobal) for (const subagent of getEveSubagents(cwd)) {
|
|
2452
|
+
const subagentDir = getEveSubagentSkillsDir(subagent, cwd);
|
|
2453
|
+
if (!scopes.some((s) => s.path === subagentDir && s.global === isGlobal)) scopes.push({
|
|
2454
|
+
global: isGlobal,
|
|
2455
|
+
path: subagentDir,
|
|
2456
|
+
agentType
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
1814
2459
|
}
|
|
1815
2460
|
const allAgentTypes = Object.keys(agents);
|
|
1816
2461
|
for (const agentType of allAgentTypes) {
|
|
@@ -1824,6 +2469,14 @@ async function listInstalledSkills(options = {}) {
|
|
|
1824
2469
|
path: agentDir,
|
|
1825
2470
|
agentType
|
|
1826
2471
|
});
|
|
2472
|
+
if (agentType === "eve" && !isGlobal) for (const subagent of getEveSubagents(cwd)) {
|
|
2473
|
+
const subagentDir = getEveSubagentSkillsDir(subagent, cwd);
|
|
2474
|
+
if (!scopes.some((s) => s.path === subagentDir && s.global === isGlobal)) scopes.push({
|
|
2475
|
+
global: isGlobal,
|
|
2476
|
+
path: subagentDir,
|
|
2477
|
+
agentType
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
1827
2480
|
}
|
|
1828
2481
|
}
|
|
1829
2482
|
for (const scope of scopes) try {
|
|
@@ -1869,7 +2522,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
1869
2522
|
]));
|
|
1870
2523
|
for (const possibleName of possibleNames) {
|
|
1871
2524
|
const agentSkillDir = join(agentBase, possibleName);
|
|
1872
|
-
if (!isPathSafe(agentBase, agentSkillDir)) continue;
|
|
2525
|
+
if (!isPathSafe$1(agentBase, agentSkillDir)) continue;
|
|
1873
2526
|
try {
|
|
1874
2527
|
await access(agentSkillDir);
|
|
1875
2528
|
found = true;
|
|
@@ -1881,7 +2534,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
1881
2534
|
for (const agentEntry of agentEntries) {
|
|
1882
2535
|
if (!agentEntry.isDirectory()) continue;
|
|
1883
2536
|
const candidateDir = join(agentBase, agentEntry.name);
|
|
1884
|
-
if (!isPathSafe(agentBase, candidateDir)) continue;
|
|
2537
|
+
if (!isPathSafe$1(agentBase, candidateDir)) continue;
|
|
1885
2538
|
try {
|
|
1886
2539
|
const candidateSkillMd = join(candidateDir, "SKILL.md");
|
|
1887
2540
|
await stat(candidateSkillMd);
|
|
@@ -1912,6 +2565,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
1912
2565
|
}
|
|
1913
2566
|
const AUDIT_URL = "https://add-skill.vercel.sh/audit";
|
|
1914
2567
|
let cliVersion = null;
|
|
2568
|
+
let detectedAgentName = null;
|
|
1915
2569
|
function isCI() {
|
|
1916
2570
|
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
|
|
1917
2571
|
}
|
|
@@ -1921,6 +2575,9 @@ function isEnabled() {
|
|
|
1921
2575
|
function setVersion(version) {
|
|
1922
2576
|
cliVersion = version;
|
|
1923
2577
|
}
|
|
2578
|
+
function setDetectedAgent(agentName) {
|
|
2579
|
+
detectedAgentName = agentName;
|
|
2580
|
+
}
|
|
1924
2581
|
async function fetchAuditData(source, skillSlugs, timeoutMs = 3e3) {
|
|
1925
2582
|
if (skillSlugs.length === 0) return null;
|
|
1926
2583
|
try {
|
|
@@ -1945,6 +2602,7 @@ function track(data) {
|
|
|
1945
2602
|
const params = new URLSearchParams();
|
|
1946
2603
|
if (cliVersion) params.set("v", cliVersion);
|
|
1947
2604
|
if (isCI()) params.set("ci", "1");
|
|
2605
|
+
if (detectedAgentName) params.set("agent", detectedAgentName);
|
|
1948
2606
|
for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
|
|
1949
2607
|
const telemetryUrl = await getSafeSkillTelemetryUrl();
|
|
1950
2608
|
fetch(`${telemetryUrl}?${params.toString()}`).catch(() => {});
|
|
@@ -1989,6 +2647,15 @@ var WellKnownProvider = class {
|
|
|
1989
2647
|
}
|
|
1990
2648
|
}
|
|
1991
2649
|
async fetchIndex(baseUrl) {
|
|
2650
|
+
const result = await this.fetchIndexForUse(baseUrl);
|
|
2651
|
+
if (!result) return null;
|
|
2652
|
+
return {
|
|
2653
|
+
index: result.index,
|
|
2654
|
+
resolvedBaseUrl: result.resolvedBaseUrl,
|
|
2655
|
+
resolvedWellKnownPath: result.resolvedWellKnownPath
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
async fetchIndexForUse(baseUrl) {
|
|
1992
2659
|
try {
|
|
1993
2660
|
const parsed = new URL(baseUrl);
|
|
1994
2661
|
const basePath = parsed.pathname.replace(/\/$/, "");
|
|
@@ -2011,12 +2678,21 @@ var WellKnownProvider = class {
|
|
|
2011
2678
|
const index = await response.json();
|
|
2012
2679
|
if (!index.skills || !Array.isArray(index.skills)) continue;
|
|
2013
2680
|
let allValid = true;
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2681
|
+
const originalEntries = [];
|
|
2682
|
+
const sanitizedEntries = [];
|
|
2683
|
+
for (const entry of index.skills) {
|
|
2684
|
+
if (!this.isValidSkillEntry(entry)) {
|
|
2685
|
+
allValid = false;
|
|
2686
|
+
break;
|
|
2687
|
+
}
|
|
2688
|
+
const sanitizedEntry = this.sanitizeSkillEntry(entry);
|
|
2689
|
+
if (!sanitizedEntry) continue;
|
|
2690
|
+
originalEntries.push(entry);
|
|
2691
|
+
sanitizedEntries.push(sanitizedEntry);
|
|
2017
2692
|
}
|
|
2018
|
-
if (allValid) return {
|
|
2019
|
-
index,
|
|
2693
|
+
if (allValid && sanitizedEntries.length > 0) return {
|
|
2694
|
+
index: { skills: sanitizedEntries },
|
|
2695
|
+
originalEntries,
|
|
2020
2696
|
resolvedBaseUrl: resolvedBase,
|
|
2021
2697
|
resolvedWellKnownPath: wellKnownPath
|
|
2022
2698
|
};
|
|
@@ -2034,28 +2710,41 @@ var WellKnownProvider = class {
|
|
|
2034
2710
|
if (typeof e.name !== "string" || !e.name) return false;
|
|
2035
2711
|
if (typeof e.description !== "string" || !e.description) return false;
|
|
2036
2712
|
if (!Array.isArray(e.files) || e.files.length === 0) return false;
|
|
2037
|
-
if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(e.name)
|
|
2038
|
-
if (e.name.length === 1 && !/^[a-z0-9]$/.test(e.name)) return false;
|
|
2039
|
-
}
|
|
2713
|
+
if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(e.name)) return false;
|
|
2040
2714
|
for (const file of e.files) {
|
|
2041
2715
|
if (typeof file !== "string") return false;
|
|
2042
2716
|
if (file.startsWith("/") || file.startsWith("\\") || file.includes("..")) return false;
|
|
2717
|
+
if (!this.isSafeWellKnownFilePath(file)) return false;
|
|
2043
2718
|
}
|
|
2044
2719
|
if (!e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md")) return false;
|
|
2045
2720
|
return true;
|
|
2046
2721
|
}
|
|
2047
|
-
|
|
2722
|
+
isSafeWellKnownFilePath(file) {
|
|
2723
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(file)) return false;
|
|
2724
|
+
return stripTerminalEscapes(file) === file;
|
|
2725
|
+
}
|
|
2726
|
+
sanitizeSkillEntry(entry) {
|
|
2727
|
+
const name = sanitizeMetadata(entry.name);
|
|
2728
|
+
const description = sanitizeMetadata(entry.description);
|
|
2729
|
+
if (!name || !description) return null;
|
|
2730
|
+
return {
|
|
2731
|
+
...entry,
|
|
2732
|
+
name,
|
|
2733
|
+
description
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
async fetchSkill(url) {
|
|
2048
2737
|
try {
|
|
2049
2738
|
const parsed = new URL(url);
|
|
2050
|
-
const result = await this.
|
|
2739
|
+
const result = await this.fetchIndexForUse(url);
|
|
2051
2740
|
if (!result) return null;
|
|
2052
|
-
const { index, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2741
|
+
const { index, originalEntries, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2053
2742
|
let skillName = null;
|
|
2054
2743
|
const pathMatch = parsed.pathname.match(/\/.well-known\/(?:agent-skills|skills)\/([^/]+)\/?$/);
|
|
2055
2744
|
if (pathMatch && pathMatch[1] && pathMatch[1] !== "index.json") skillName = pathMatch[1];
|
|
2056
|
-
else if (index.skills.length === 1) skillName =
|
|
2745
|
+
else if (index.skills.length === 1) skillName = originalEntries[0].name;
|
|
2057
2746
|
if (!skillName) return null;
|
|
2058
|
-
const skillEntry =
|
|
2747
|
+
const skillEntry = originalEntries.find((s) => s.name === skillName);
|
|
2059
2748
|
if (!skillEntry) return null;
|
|
2060
2749
|
return this.fetchSkillByEntry(resolvedBaseUrl, skillEntry, resolvedWellKnownPath);
|
|
2061
2750
|
} catch {
|
|
@@ -2065,13 +2754,16 @@ var WellKnownProvider = class {
|
|
|
2065
2754
|
async fetchSkillByEntry(baseUrl, entry, wellKnownPath) {
|
|
2066
2755
|
try {
|
|
2067
2756
|
const resolvedPath = wellKnownPath ?? this.WELL_KNOWN_PATHS[0];
|
|
2757
|
+
if (!this.isValidSkillEntry(entry)) return null;
|
|
2758
|
+
const sanitizedEntry = this.sanitizeSkillEntry(entry);
|
|
2759
|
+
if (!sanitizedEntry) return null;
|
|
2068
2760
|
const skillBaseUrl = `${baseUrl.replace(/\/$/, "")}/${resolvedPath}/${entry.name}`;
|
|
2069
2761
|
const skillMdUrl = `${skillBaseUrl}/SKILL.md`;
|
|
2070
2762
|
const response = await fetch(skillMdUrl, { headers: getHeaders() });
|
|
2071
2763
|
if (!response.ok) return null;
|
|
2072
2764
|
const content = await response.text();
|
|
2073
|
-
const { data } = (
|
|
2074
|
-
if (!data.name || !data.description) return null;
|
|
2765
|
+
const { data } = parseFrontmatter(content);
|
|
2766
|
+
if (typeof data.name !== "string" || typeof data.description !== "string" || !data.name || !data.description) return null;
|
|
2075
2767
|
const files = /* @__PURE__ */ new Map();
|
|
2076
2768
|
files.set("SKILL.md", content);
|
|
2077
2769
|
const filePromises = entry.files.filter((f) => f.toLowerCase() !== "skill.md").map(async (filePath) => {
|
|
@@ -2087,15 +2779,18 @@ var WellKnownProvider = class {
|
|
|
2087
2779
|
});
|
|
2088
2780
|
const fileResults = await Promise.all(filePromises);
|
|
2089
2781
|
for (const result of fileResults) if (result) files.set(result.path, result.content);
|
|
2782
|
+
const name = sanitizeMetadata(data.name);
|
|
2783
|
+
const description = sanitizeMetadata(data.description);
|
|
2784
|
+
if (!name || !description) return null;
|
|
2090
2785
|
return {
|
|
2091
|
-
name
|
|
2092
|
-
description
|
|
2786
|
+
name,
|
|
2787
|
+
description,
|
|
2093
2788
|
content,
|
|
2094
2789
|
installName: entry.name,
|
|
2095
2790
|
sourceUrl: skillMdUrl,
|
|
2096
2791
|
metadata: data.metadata,
|
|
2097
2792
|
files,
|
|
2098
|
-
indexEntry:
|
|
2793
|
+
indexEntry: sanitizedEntry
|
|
2099
2794
|
};
|
|
2100
2795
|
} catch {
|
|
2101
2796
|
return null;
|
|
@@ -2103,10 +2798,10 @@ var WellKnownProvider = class {
|
|
|
2103
2798
|
}
|
|
2104
2799
|
async fetchAllSkills(url) {
|
|
2105
2800
|
try {
|
|
2106
|
-
const result = await this.
|
|
2801
|
+
const result = await this.fetchIndexForUse(url);
|
|
2107
2802
|
if (!result) return [];
|
|
2108
|
-
const {
|
|
2109
|
-
const skillPromises =
|
|
2803
|
+
const { originalEntries, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2804
|
+
const skillPromises = originalEntries.map((entry) => this.fetchSkillByEntry(resolvedBaseUrl, entry, resolvedWellKnownPath));
|
|
2110
2805
|
return (await Promise.all(skillPromises)).filter((s) => s !== null);
|
|
2111
2806
|
} catch {
|
|
2112
2807
|
return [];
|
|
@@ -2140,28 +2835,28 @@ var WellKnownProvider = class {
|
|
|
2140
2835
|
}
|
|
2141
2836
|
};
|
|
2142
2837
|
const wellKnownProvider = new WellKnownProvider();
|
|
2143
|
-
const AGENTS_DIR
|
|
2144
|
-
const LOCK_FILE
|
|
2145
|
-
const CURRENT_VERSION
|
|
2146
|
-
function getSkillLockPath
|
|
2838
|
+
const AGENTS_DIR = ".agents";
|
|
2839
|
+
const LOCK_FILE = ".skill-lock.json";
|
|
2840
|
+
const CURRENT_VERSION = 4;
|
|
2841
|
+
function getSkillLockPath() {
|
|
2147
2842
|
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
2148
|
-
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE
|
|
2149
|
-
return join(homedir(), AGENTS_DIR
|
|
2843
|
+
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
|
|
2844
|
+
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
2150
2845
|
}
|
|
2151
|
-
async function readSkillLock
|
|
2152
|
-
const lockPath = getSkillLockPath
|
|
2846
|
+
async function readSkillLock() {
|
|
2847
|
+
const lockPath = getSkillLockPath();
|
|
2153
2848
|
try {
|
|
2154
2849
|
const content = await readFile(lockPath, "utf-8");
|
|
2155
2850
|
const parsed = JSON.parse(content);
|
|
2156
2851
|
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLockFile();
|
|
2157
|
-
if (parsed.version < CURRENT_VERSION
|
|
2852
|
+
if (parsed.version < CURRENT_VERSION) return createEmptyLockFile();
|
|
2158
2853
|
return parsed;
|
|
2159
2854
|
} catch (error) {
|
|
2160
2855
|
return createEmptyLockFile();
|
|
2161
2856
|
}
|
|
2162
2857
|
}
|
|
2163
2858
|
async function writeSkillLock(lock) {
|
|
2164
|
-
const lockPath = getSkillLockPath
|
|
2859
|
+
const lockPath = getSkillLockPath();
|
|
2165
2860
|
await mkdir(dirname(lockPath), { recursive: true });
|
|
2166
2861
|
await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
2167
2862
|
}
|
|
@@ -2181,28 +2876,14 @@ function getGitHubToken() {
|
|
|
2181
2876
|
} catch {}
|
|
2182
2877
|
return null;
|
|
2183
2878
|
}
|
|
2184
|
-
async function fetchSkillFolderHash(ownerRepo, skillPath,
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
for (const branch of ["main", "master"]) try {
|
|
2190
|
-
const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`;
|
|
2191
|
-
const headers = getHeaders({ Accept: "application/vnd.github.v3+json" });
|
|
2192
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
2193
|
-
const response = await fetch(url, { headers });
|
|
2194
|
-
if (!response.ok) continue;
|
|
2195
|
-
const data = await response.json();
|
|
2196
|
-
if (!folderPath) return data.sha;
|
|
2197
|
-
const folderEntry = data.tree.find((entry) => entry.type === "tree" && entry.path === folderPath);
|
|
2198
|
-
if (folderEntry) return folderEntry.sha;
|
|
2199
|
-
} catch {
|
|
2200
|
-
continue;
|
|
2201
|
-
}
|
|
2202
|
-
return null;
|
|
2879
|
+
async function fetchSkillFolderHash(ownerRepo, skillPath, getToken, ref) {
|
|
2880
|
+
const { fetchRepoTree, getSkillFolderHashFromTree } = await Promise.resolve().then(() => blob_exports);
|
|
2881
|
+
const tree = await fetchRepoTree(ownerRepo, ref, getToken ?? void 0);
|
|
2882
|
+
if (!tree) return null;
|
|
2883
|
+
return getSkillFolderHashFromTree(tree, skillPath);
|
|
2203
2884
|
}
|
|
2204
2885
|
async function addSkillToLock(skillName, entry) {
|
|
2205
|
-
const lock = await readSkillLock
|
|
2886
|
+
const lock = await readSkillLock();
|
|
2206
2887
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2207
2888
|
const existingEntry = lock.skills[skillName];
|
|
2208
2889
|
lock.skills[skillName] = {
|
|
@@ -2213,109 +2894,43 @@ async function addSkillToLock(skillName, entry) {
|
|
|
2213
2894
|
await writeSkillLock(lock);
|
|
2214
2895
|
}
|
|
2215
2896
|
async function removeSkillFromLock(skillName) {
|
|
2216
|
-
const lock = await readSkillLock
|
|
2897
|
+
const lock = await readSkillLock();
|
|
2217
2898
|
if (!(skillName in lock.skills)) return false;
|
|
2218
2899
|
delete lock.skills[skillName];
|
|
2219
2900
|
await writeSkillLock(lock);
|
|
2220
2901
|
return true;
|
|
2221
2902
|
}
|
|
2222
2903
|
async function getSkillFromLock(skillName) {
|
|
2223
|
-
return (await readSkillLock
|
|
2904
|
+
return (await readSkillLock()).skills[skillName] ?? null;
|
|
2224
2905
|
}
|
|
2225
2906
|
async function getAllLockedSkills() {
|
|
2226
|
-
return (await readSkillLock
|
|
2907
|
+
return (await readSkillLock()).skills;
|
|
2227
2908
|
}
|
|
2228
2909
|
function createEmptyLockFile() {
|
|
2229
2910
|
return {
|
|
2230
|
-
version: CURRENT_VERSION
|
|
2911
|
+
version: CURRENT_VERSION,
|
|
2231
2912
|
skills: {},
|
|
2232
2913
|
dismissed: {}
|
|
2233
2914
|
};
|
|
2234
2915
|
}
|
|
2235
2916
|
async function isPromptDismissed(promptKey) {
|
|
2236
|
-
return (await readSkillLock
|
|
2917
|
+
return (await readSkillLock()).dismissed?.[promptKey] === true;
|
|
2237
2918
|
}
|
|
2238
2919
|
async function dismissPrompt(promptKey) {
|
|
2239
|
-
const lock = await readSkillLock
|
|
2920
|
+
const lock = await readSkillLock();
|
|
2240
2921
|
if (!lock.dismissed) lock.dismissed = {};
|
|
2241
2922
|
lock.dismissed[promptKey] = true;
|
|
2242
2923
|
await writeSkillLock(lock);
|
|
2243
2924
|
}
|
|
2244
2925
|
async function getLastSelectedAgents() {
|
|
2245
|
-
return (await readSkillLock
|
|
2926
|
+
return (await readSkillLock()).lastSelectedAgents;
|
|
2246
2927
|
}
|
|
2247
2928
|
async function saveSelectedAgents(agents) {
|
|
2248
|
-
const lock = await readSkillLock
|
|
2929
|
+
const lock = await readSkillLock();
|
|
2249
2930
|
lock.lastSelectedAgents = agents;
|
|
2250
2931
|
await writeSkillLock(lock);
|
|
2251
2932
|
}
|
|
2252
|
-
|
|
2253
|
-
const CURRENT_VERSION = 1;
|
|
2254
|
-
function getLocalLockPath(cwd) {
|
|
2255
|
-
return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
|
|
2256
|
-
}
|
|
2257
|
-
async function readLocalLock(cwd) {
|
|
2258
|
-
const lockPath = getLocalLockPath(cwd);
|
|
2259
|
-
try {
|
|
2260
|
-
const content = await readFile(lockPath, "utf-8");
|
|
2261
|
-
const parsed = JSON.parse(content);
|
|
2262
|
-
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
|
|
2263
|
-
if (parsed.version < CURRENT_VERSION) return createEmptyLocalLock();
|
|
2264
|
-
return parsed;
|
|
2265
|
-
} catch {
|
|
2266
|
-
return createEmptyLocalLock();
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
async function writeLocalLock(lock, cwd) {
|
|
2270
|
-
const lockPath = getLocalLockPath(cwd);
|
|
2271
|
-
const sortedSkills = {};
|
|
2272
|
-
for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
|
|
2273
|
-
const sorted = {
|
|
2274
|
-
version: lock.version,
|
|
2275
|
-
skills: sortedSkills
|
|
2276
|
-
};
|
|
2277
|
-
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
2278
|
-
}
|
|
2279
|
-
async function computeSkillFolderHash(skillDir) {
|
|
2280
|
-
const files = [];
|
|
2281
|
-
await collectFiles(skillDir, skillDir, files);
|
|
2282
|
-
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
2283
|
-
const hash = createHash("sha256");
|
|
2284
|
-
for (const file of files) {
|
|
2285
|
-
hash.update(file.relativePath);
|
|
2286
|
-
hash.update(file.content);
|
|
2287
|
-
}
|
|
2288
|
-
return hash.digest("hex");
|
|
2289
|
-
}
|
|
2290
|
-
async function collectFiles(baseDir, currentDir, results) {
|
|
2291
|
-
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
2292
|
-
await Promise.all(entries.map(async (entry) => {
|
|
2293
|
-
const fullPath = join(currentDir, entry.name);
|
|
2294
|
-
if (entry.isDirectory()) {
|
|
2295
|
-
if (entry.name === ".git" || entry.name === "node_modules") return;
|
|
2296
|
-
await collectFiles(baseDir, fullPath, results);
|
|
2297
|
-
} else if (entry.isFile()) {
|
|
2298
|
-
const content = await readFile(fullPath);
|
|
2299
|
-
const relativePath = relative(baseDir, fullPath).split("\\").join("/");
|
|
2300
|
-
results.push({
|
|
2301
|
-
relativePath,
|
|
2302
|
-
content
|
|
2303
|
-
});
|
|
2304
|
-
}
|
|
2305
|
-
}));
|
|
2306
|
-
}
|
|
2307
|
-
async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
2308
|
-
const lock = await readLocalLock(cwd);
|
|
2309
|
-
lock.skills[skillName] = entry;
|
|
2310
|
-
await writeLocalLock(lock, cwd);
|
|
2311
|
-
}
|
|
2312
|
-
function createEmptyLocalLock() {
|
|
2313
|
-
return {
|
|
2314
|
-
version: CURRENT_VERSION,
|
|
2315
|
-
skills: {}
|
|
2316
|
-
};
|
|
2317
|
-
}
|
|
2318
|
-
var version$1 = "0.2.3";
|
|
2933
|
+
var version$1 = "0.3.0";
|
|
2319
2934
|
const isCancelled$1 = (value) => typeof value === "symbol";
|
|
2320
2935
|
async function isSourcePrivate(source) {
|
|
2321
2936
|
const ownerRepo = parseOwnerRepo(source);
|
|
@@ -2325,6 +2940,12 @@ async function isSourcePrivate(source) {
|
|
|
2325
2940
|
function initTelemetry(version) {
|
|
2326
2941
|
setVersion(version);
|
|
2327
2942
|
}
|
|
2943
|
+
function getLockSourceForParsedSource(parsed) {
|
|
2944
|
+
const normalizedSource = getOwnerRepo(parsed);
|
|
2945
|
+
if (parsed.type === "github") return normalizedSource;
|
|
2946
|
+
if (parsed.type === "gitlab" || parsed.type === "git") return parsed.url;
|
|
2947
|
+
return normalizedSource;
|
|
2948
|
+
}
|
|
2328
2949
|
function riskLabel(risk) {
|
|
2329
2950
|
switch (risk) {
|
|
2330
2951
|
case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical Risk"));
|
|
@@ -3141,7 +3762,7 @@ async function runAdd(args, options = {}) {
|
|
|
3141
3762
|
skillFiles[skill.name] = relativePath;
|
|
3142
3763
|
}
|
|
3143
3764
|
const normalizedSource = getOwnerRepo(parsed);
|
|
3144
|
-
const lockSource = parsed
|
|
3765
|
+
const lockSource = getLockSourceForParsedSource(parsed);
|
|
3145
3766
|
if (normalizedSource) {
|
|
3146
3767
|
const ownerRepo = parseOwnerRepo(normalizedSource);
|
|
3147
3768
|
if (ownerRepo) {
|
|
@@ -3170,13 +3791,14 @@ async function runAdd(args, options = {}) {
|
|
|
3170
3791
|
let skillFolderHash = "";
|
|
3171
3792
|
const skillPathValue = skillFiles[skill.name];
|
|
3172
3793
|
if (parsed.type === "github" && normalizedSource && skillPathValue) {
|
|
3173
|
-
const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken
|
|
3794
|
+
const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken, parsed.ref);
|
|
3174
3795
|
if (hash) skillFolderHash = hash;
|
|
3175
3796
|
}
|
|
3176
3797
|
await addSkillToLock(skill.name, {
|
|
3177
3798
|
source: lockSource || normalizedSource || parsed.url,
|
|
3178
3799
|
sourceType: parsed.type,
|
|
3179
3800
|
sourceUrl: parsed.url,
|
|
3801
|
+
ref: parsed.ref,
|
|
3180
3802
|
skillPath: skillPathValue,
|
|
3181
3803
|
skillFolderHash,
|
|
3182
3804
|
pluginName: skill.pluginName,
|
|
@@ -3192,10 +3814,15 @@ async function runAdd(args, options = {}) {
|
|
|
3192
3814
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
3193
3815
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
3194
3816
|
const computedHash = await computeSkillFolderHash(skill.path);
|
|
3817
|
+
const skillPathValue = skillFiles[skill.name];
|
|
3195
3818
|
await addSkillToLocalLock(skill.name, {
|
|
3196
3819
|
source: lockSource || parsed.url,
|
|
3820
|
+
ref: parsed.ref,
|
|
3197
3821
|
sourceType: parsed.type,
|
|
3198
|
-
|
|
3822
|
+
skillPath: skillPathValue,
|
|
3823
|
+
computedHash,
|
|
3824
|
+
resolvedVersion: parsed.type === "hub" ? hubResolved?.version : void 0,
|
|
3825
|
+
artifactDigest: parsed.type === "hub" ? hubResolved?.artifact_digest : void 0
|
|
3199
3826
|
}, cwd);
|
|
3200
3827
|
} catch {}
|
|
3201
3828
|
}
|
|
@@ -3361,10 +3988,21 @@ function parseAddOptions(args) {
|
|
|
3361
3988
|
options
|
|
3362
3989
|
};
|
|
3363
3990
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3991
|
+
var import_dist = require_dist();
|
|
3992
|
+
let cachedResult = null;
|
|
3993
|
+
async function detectAgent() {
|
|
3994
|
+
if (cachedResult) return cachedResult;
|
|
3995
|
+
cachedResult = await (0, import_dist.determineAgent)();
|
|
3996
|
+
if (cachedResult.isAgent) setDetectedAgent(cachedResult.agent.name);
|
|
3997
|
+
return cachedResult;
|
|
3998
|
+
}
|
|
3999
|
+
async function isRunningInAgent() {
|
|
4000
|
+
return (await detectAgent()).isAgent;
|
|
4001
|
+
}
|
|
4002
|
+
const RESET$3 = "\x1B[0m";
|
|
4003
|
+
const BOLD$3 = "\x1B[1m";
|
|
4004
|
+
const DIM$3 = "\x1B[38;5;102m";
|
|
4005
|
+
const TEXT$2 = "\x1B[38;5;145m";
|
|
3368
4006
|
const CYAN$1 = "\x1B[36m";
|
|
3369
4007
|
const GREEN = "\x1B[32m";
|
|
3370
4008
|
const YELLOW$1 = "\x1B[33m";
|
|
@@ -3376,7 +4014,7 @@ function formatCount(count) {
|
|
|
3376
4014
|
}
|
|
3377
4015
|
function formatTrustScore(score) {
|
|
3378
4016
|
if (score === void 0 || score === null) return "";
|
|
3379
|
-
return `${score >= 80 ? GREEN : score >= 60 ? YELLOW$1 : RED}TrustScore:${score}${RESET$
|
|
4017
|
+
return `${score >= 80 ? GREEN : score >= 60 ? YELLOW$1 : RED}TrustScore:${score}${RESET$3}`;
|
|
3380
4018
|
}
|
|
3381
4019
|
function formatRelativeDate(ts) {
|
|
3382
4020
|
if (!ts) return "";
|
|
@@ -3391,8 +4029,8 @@ function formatRelativeDate(ts) {
|
|
|
3391
4029
|
}
|
|
3392
4030
|
function formatStatsBadges(installs, stars) {
|
|
3393
4031
|
const parts = [];
|
|
3394
|
-
if (installs > 0) parts.push(`${CYAN$1}↓ ${formatCount(installs)}${RESET$
|
|
3395
|
-
if (stars > 0) parts.push(`${YELLOW$1}★ ${formatCount(stars)}${RESET$
|
|
4032
|
+
if (installs > 0) parts.push(`${CYAN$1}↓ ${formatCount(installs)}${RESET$3}`);
|
|
4033
|
+
if (stars > 0) parts.push(`${YELLOW$1}★ ${formatCount(stars)}${RESET$3}`);
|
|
3396
4034
|
return parts.join(" ");
|
|
3397
4035
|
}
|
|
3398
4036
|
async function searchSkillsAPI(query) {
|
|
@@ -3418,29 +4056,29 @@ async function runSearchPrompt(initialQuery = "") {
|
|
|
3418
4056
|
if (lastRenderedLines > 0) process.stdout.write(MOVE_UP(lastRenderedLines) + MOVE_TO_COL(1));
|
|
3419
4057
|
process.stdout.write(CLEAR_DOWN);
|
|
3420
4058
|
const lines = [];
|
|
3421
|
-
const cursor = `${BOLD$
|
|
3422
|
-
lines.push(`${TEXT$
|
|
4059
|
+
const cursor = `${BOLD$3}_${RESET$3}`;
|
|
4060
|
+
lines.push(`${TEXT$2}Search skills:${RESET$3} ${query}${cursor}`);
|
|
3423
4061
|
lines.push("");
|
|
3424
|
-
if (!query || query.length < 2) lines.push(`${DIM$
|
|
3425
|
-
else if (results.length === 0 && loading) lines.push(`${DIM$
|
|
3426
|
-
else if (results.length === 0) lines.push(`${DIM$
|
|
4062
|
+
if (!query || query.length < 2) lines.push(`${DIM$3}Start typing to search (min 2 chars)${RESET$3}`);
|
|
4063
|
+
else if (results.length === 0 && loading) lines.push(`${DIM$3}Searching...${RESET$3}`);
|
|
4064
|
+
else if (results.length === 0) lines.push(`${DIM$3}No SafeSkill found${RESET$3}`);
|
|
3427
4065
|
else {
|
|
3428
4066
|
const visible = results.slice(0, 8);
|
|
3429
4067
|
for (let i = 0; i < visible.length; i++) {
|
|
3430
4068
|
const skill = visible[i];
|
|
3431
4069
|
const isSelected = i === selectedIndex;
|
|
3432
|
-
const arrow = isSelected ? `${BOLD$
|
|
4070
|
+
const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
|
|
3433
4071
|
const displayName = skill.label || skill.name;
|
|
3434
|
-
const name = isSelected ? `${BOLD$
|
|
3435
|
-
const source = skill.source ? ` ${DIM$
|
|
4072
|
+
const name = isSelected ? `${BOLD$3}${displayName}${RESET$3}` : `${TEXT$2}${displayName}${RESET$3}`;
|
|
4073
|
+
const source = skill.source ? ` ${DIM$3}${skill.source}${RESET$3}` : "";
|
|
3436
4074
|
const statsBadge = formatStatsBadges(skill.installs, skill.totalStars);
|
|
3437
|
-
const trustBadge = skill.trustScore !== void 0 ? ` ${skill.trustScore >= 80 ? GREEN : skill.trustScore >= 60 ? YELLOW$1 : RED}TrustScore:${skill.trustScore}${RESET$
|
|
3438
|
-
const loadingIndicator = loading && i === 0 ? ` ${DIM$
|
|
4075
|
+
const trustBadge = skill.trustScore !== void 0 ? ` ${skill.trustScore >= 80 ? GREEN : skill.trustScore >= 60 ? YELLOW$1 : RED}TrustScore:${skill.trustScore}${RESET$3}` : "";
|
|
4076
|
+
const loadingIndicator = loading && i === 0 ? ` ${DIM$3}...${RESET$3}` : "";
|
|
3439
4077
|
lines.push(` ${arrow}${trustBadge} ${name}${source}${statsBadge ? ` ${statsBadge}` : ""}${loadingIndicator}`);
|
|
3440
4078
|
}
|
|
3441
4079
|
}
|
|
3442
4080
|
lines.push("");
|
|
3443
|
-
lines.push(`${DIM$
|
|
4081
|
+
lines.push(`${DIM$3}up/down navigate | enter select | esc cancel${RESET$3}`);
|
|
3444
4082
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
3445
4083
|
lastRenderedLines = lines.length;
|
|
3446
4084
|
}
|
|
@@ -3535,10 +4173,10 @@ async function isRepoPublic(owner, repo) {
|
|
|
3535
4173
|
}
|
|
3536
4174
|
async function runFind(args) {
|
|
3537
4175
|
const query = args.join(" ");
|
|
3538
|
-
const isNonInteractive = !process.stdin.isTTY;
|
|
3539
|
-
const agentTip = `${DIM$
|
|
3540
|
-
${DIM$
|
|
3541
|
-
${DIM$
|
|
4176
|
+
const isNonInteractive = !process.stdin.isTTY || await isRunningInAgent();
|
|
4177
|
+
const agentTip = `${DIM$3}Tip: if running in a coding agent, follow these steps:${RESET$3}
|
|
4178
|
+
${DIM$3} 1) npx safeskill find [query]${RESET$3}
|
|
4179
|
+
${DIM$3} 2) npx safeskill add <source>${RESET$3}`;
|
|
3542
4180
|
if (query) {
|
|
3543
4181
|
const results = await searchSkillsAPI(query);
|
|
3544
4182
|
track({
|
|
@@ -3547,26 +4185,26 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3547
4185
|
resultCount: String(results.length)
|
|
3548
4186
|
});
|
|
3549
4187
|
if (results.length === 0) {
|
|
3550
|
-
console.log(`${DIM$
|
|
4188
|
+
console.log(`${DIM$3}No skills found for "${query}"${RESET$3}`);
|
|
3551
4189
|
return;
|
|
3552
4190
|
}
|
|
3553
|
-
console.log(`${DIM$
|
|
4191
|
+
console.log(`${DIM$3}Install with${RESET$3} npx safeskill add <owner/repo@skill>`);
|
|
3554
4192
|
console.log();
|
|
3555
4193
|
for (const skill of results.slice(0, 6)) {
|
|
3556
4194
|
const pkg = skill.source || skill.slug;
|
|
3557
4195
|
const installRef = pkg.startsWith("safeskill://") ? pkg : `${pkg}@${skill.name}`;
|
|
3558
|
-
console.log(`${TEXT$
|
|
4196
|
+
console.log(`${TEXT$2}${installRef}${RESET$3}`);
|
|
3559
4197
|
const statsBadge = formatStatsBadges(skill.installs, skill.totalStars);
|
|
3560
4198
|
const badges = [
|
|
3561
4199
|
formatTrustScore(skill.trustScore),
|
|
3562
4200
|
statsBadge,
|
|
3563
|
-
skill.updatedAt ? `${DIM$
|
|
4201
|
+
skill.updatedAt ? `${DIM$3}${formatRelativeDate(skill.updatedAt)}${RESET$3}` : ""
|
|
3564
4202
|
].filter(Boolean).join(` `);
|
|
3565
4203
|
if (badges) console.log(` ${badges}`);
|
|
3566
4204
|
const displayName = skill.label || skill.name;
|
|
3567
4205
|
const desc = skill.description || (displayName !== skill.name ? displayName : "");
|
|
3568
|
-
if (desc) console.log(`${DIM$
|
|
3569
|
-
if (skill.detailUrl) console.log(`${DIM$
|
|
4206
|
+
if (desc) console.log(`${DIM$3} ${desc}${RESET$3}`);
|
|
4207
|
+
if (skill.detailUrl) console.log(`${DIM$3}└ ${skill.detailUrl}${RESET$3}`);
|
|
3570
4208
|
console.log();
|
|
3571
4209
|
}
|
|
3572
4210
|
return;
|
|
@@ -3574,6 +4212,7 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3574
4212
|
if (isNonInteractive) {
|
|
3575
4213
|
console.log(agentTip);
|
|
3576
4214
|
console.log();
|
|
4215
|
+
return;
|
|
3577
4216
|
}
|
|
3578
4217
|
const selected = await runSearchPrompt();
|
|
3579
4218
|
track({
|
|
@@ -3583,14 +4222,14 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3583
4222
|
interactive: "1"
|
|
3584
4223
|
});
|
|
3585
4224
|
if (!selected) {
|
|
3586
|
-
console.log(`${DIM$
|
|
4225
|
+
console.log(`${DIM$3}Search cancelled${RESET$3}`);
|
|
3587
4226
|
console.log();
|
|
3588
4227
|
return;
|
|
3589
4228
|
}
|
|
3590
4229
|
const pkg = selected.source || selected.slug;
|
|
3591
4230
|
const skillName = selected.name;
|
|
3592
4231
|
console.log();
|
|
3593
|
-
console.log(`${TEXT$
|
|
4232
|
+
console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} from ${DIM$3}${pkg}${RESET$3}...`);
|
|
3594
4233
|
console.log();
|
|
3595
4234
|
const { source, options } = parseAddOptions([
|
|
3596
4235
|
pkg,
|
|
@@ -3600,9 +4239,9 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3600
4239
|
await runAdd(source, options);
|
|
3601
4240
|
console.log();
|
|
3602
4241
|
const info = getOwnerRepoFromString(pkg);
|
|
3603
|
-
if (pkg.startsWith("safeskill://")) console.log(`${DIM$
|
|
3604
|
-
else if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$
|
|
3605
|
-
else console.log(`${DIM$
|
|
4242
|
+
if (pkg.startsWith("safeskill://")) console.log(`${DIM$3}Installed from safeskill source${RESET$3} ${TEXT$2}${pkg}${RESET$3}`);
|
|
4243
|
+
else if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$3}View the skill at${RESET$3} ${TEXT$2}https://skills.sh/${selected.slug}${RESET$3}`);
|
|
4244
|
+
else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
|
|
3606
4245
|
console.log();
|
|
3607
4246
|
}
|
|
3608
4247
|
const isCancelled = (value) => typeof value === "symbol";
|
|
@@ -3939,9 +4578,9 @@ async function runInstallFromLock(args) {
|
|
|
3939
4578
|
}
|
|
3940
4579
|
}
|
|
3941
4580
|
}
|
|
3942
|
-
const RESET$
|
|
3943
|
-
const BOLD$
|
|
3944
|
-
const DIM$
|
|
4581
|
+
const RESET$2 = "\x1B[0m";
|
|
4582
|
+
const BOLD$2 = "\x1B[1m";
|
|
4583
|
+
const DIM$2 = "\x1B[38;5;102m";
|
|
3945
4584
|
const CYAN = "\x1B[36m";
|
|
3946
4585
|
const YELLOW = "\x1B[33m";
|
|
3947
4586
|
function shortenPath(fullPath, cwd) {
|
|
@@ -3977,8 +4616,8 @@ async function runList(args) {
|
|
|
3977
4616
|
const validAgents = Object.keys(agents);
|
|
3978
4617
|
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
3979
4618
|
if (invalidAgents.length > 0) {
|
|
3980
|
-
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$
|
|
3981
|
-
console.log(`${DIM$
|
|
4619
|
+
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$2}`);
|
|
4620
|
+
console.log(`${DIM$2}Valid agents: ${validAgents.join(", ")}${RESET$2}`);
|
|
3982
4621
|
process.exit(1);
|
|
3983
4622
|
}
|
|
3984
4623
|
agentFilter = options.agent;
|
|
@@ -4005,20 +4644,20 @@ async function runList(args) {
|
|
|
4005
4644
|
console.log("[]");
|
|
4006
4645
|
return;
|
|
4007
4646
|
}
|
|
4008
|
-
console.log(`${DIM$
|
|
4009
|
-
if (scope) console.log(`${DIM$
|
|
4010
|
-
else console.log(`${DIM$
|
|
4647
|
+
console.log(`${DIM$2}No ${scopeLabel.toLowerCase()} skills found.${RESET$2}`);
|
|
4648
|
+
if (scope) console.log(`${DIM$2}Try listing project skills without -g${RESET$2}`);
|
|
4649
|
+
else console.log(`${DIM$2}Try listing global skills with -g${RESET$2}`);
|
|
4011
4650
|
return;
|
|
4012
4651
|
}
|
|
4013
4652
|
function printSkill(skill, indent = false) {
|
|
4014
4653
|
const prefix = indent ? " " : "";
|
|
4015
4654
|
const shortPath = shortenPath(skill.canonicalPath, cwd);
|
|
4016
4655
|
const agentNames = skill.agents.map((a) => agents[a].displayName);
|
|
4017
|
-
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$
|
|
4018
|
-
console.log(`${prefix}${CYAN}${skill.name}${RESET$
|
|
4019
|
-
console.log(`${prefix} ${DIM$
|
|
4656
|
+
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$2}`;
|
|
4657
|
+
console.log(`${prefix}${CYAN}${skill.name}${RESET$2} ${DIM$2}${shortPath}${RESET$2}`);
|
|
4658
|
+
console.log(`${prefix} ${DIM$2}Agents:${RESET$2} ${agentInfo}`);
|
|
4020
4659
|
}
|
|
4021
|
-
console.log(`${BOLD$
|
|
4660
|
+
console.log(`${BOLD$2}${scopeLabel} Skills${RESET$2}`);
|
|
4022
4661
|
console.log();
|
|
4023
4662
|
const groupedSkills = {};
|
|
4024
4663
|
const ungroupedSkills = [];
|
|
@@ -4034,13 +4673,13 @@ async function runList(args) {
|
|
|
4034
4673
|
const sortedGroups = Object.keys(groupedSkills).sort();
|
|
4035
4674
|
for (const group of sortedGroups) {
|
|
4036
4675
|
const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
4037
|
-
console.log(`${BOLD$
|
|
4676
|
+
console.log(`${BOLD$2}${title}${RESET$2}`);
|
|
4038
4677
|
const skills = groupedSkills[group];
|
|
4039
4678
|
if (skills) for (const skill of skills) printSkill(skill, true);
|
|
4040
4679
|
console.log();
|
|
4041
4680
|
}
|
|
4042
4681
|
if (ungroupedSkills.length > 0) {
|
|
4043
|
-
console.log(`${BOLD$
|
|
4682
|
+
console.log(`${BOLD$2}General${RESET$2}`);
|
|
4044
4683
|
for (const skill of ungroupedSkills) printSkill(skill, true);
|
|
4045
4684
|
console.log();
|
|
4046
4685
|
}
|
|
@@ -4239,133 +4878,1595 @@ function parseRemoveOptions(args) {
|
|
|
4239
4878
|
options
|
|
4240
4879
|
};
|
|
4241
4880
|
}
|
|
4242
|
-
|
|
4243
|
-
|
|
4881
|
+
function formatSourceInput(sourceUrl, ref) {
|
|
4882
|
+
if (!ref) return sourceUrl;
|
|
4883
|
+
return `${sourceUrl}#${ref}`;
|
|
4884
|
+
}
|
|
4885
|
+
function deriveSkillFolder(skillPath) {
|
|
4886
|
+
let folder = skillPath;
|
|
4887
|
+
if (folder.endsWith("/SKILL.md")) folder = folder.slice(0, -9);
|
|
4888
|
+
else if (folder.endsWith("SKILL.md")) folder = folder.slice(0, -8);
|
|
4889
|
+
if (folder.endsWith("/")) folder = folder.slice(0, -1);
|
|
4890
|
+
return folder;
|
|
4891
|
+
}
|
|
4892
|
+
function supportsAppendedSubpath(source) {
|
|
4893
|
+
if (source.startsWith("git@")) return false;
|
|
4894
|
+
if (source.endsWith(".git")) return false;
|
|
4895
|
+
if (source.startsWith("http://") || source.startsWith("https://")) try {
|
|
4896
|
+
const host = new URL(source).hostname;
|
|
4897
|
+
return host === "github.com" || host === "gitlab.com";
|
|
4898
|
+
} catch {
|
|
4899
|
+
return false;
|
|
4900
|
+
}
|
|
4901
|
+
return true;
|
|
4902
|
+
}
|
|
4903
|
+
function sourceUrlAlreadyTargetsTreePath(sourceUrl) {
|
|
4904
|
+
return /github\.com\/[^/]+\/[^/]+\/tree\/[^/]+\/.+/.test(sourceUrl);
|
|
4905
|
+
}
|
|
4906
|
+
function appendFolderAndRef(source, skillPath, ref) {
|
|
4907
|
+
if (!supportsAppendedSubpath(source)) return formatSourceInput(source, ref);
|
|
4908
|
+
const folder = deriveSkillFolder(skillPath);
|
|
4909
|
+
const withFolder = folder ? `${source}/${folder}` : source;
|
|
4910
|
+
return ref ? `${withFolder}#${ref}` : withFolder;
|
|
4911
|
+
}
|
|
4912
|
+
function buildUpdateInstallSource(entry, updateSource = entry.sourceUrl) {
|
|
4913
|
+
if (entry.sourceType === "hub") return updateSource;
|
|
4914
|
+
if (!entry.skillPath) return formatSourceInput(entry.sourceUrl, entry.ref);
|
|
4915
|
+
if (sourceUrlAlreadyTargetsTreePath(entry.sourceUrl)) return entry.sourceUrl;
|
|
4916
|
+
return appendFolderAndRef(entry.source, entry.skillPath, entry.ref);
|
|
4917
|
+
}
|
|
4918
|
+
function buildLocalUpdateSource(entry) {
|
|
4919
|
+
if (!entry.skillPath) return formatSourceInput(entry.source, entry.ref);
|
|
4920
|
+
return appendFolderAndRef(entry.source, entry.skillPath, entry.ref);
|
|
4921
|
+
}
|
|
4922
|
+
var blob_exports = /* @__PURE__ */ __exportAll({
|
|
4923
|
+
BLOB_ALLOWED_REPOS: () => BLOB_ALLOWED_REPOS,
|
|
4924
|
+
fetchRepoTree: () => fetchRepoTree,
|
|
4925
|
+
findSkillMdPaths: () => findSkillMdPaths,
|
|
4926
|
+
getSkillFolderHashFromTree: () => getSkillFolderHashFromTree,
|
|
4927
|
+
toSkillSlug: () => toSkillSlug,
|
|
4928
|
+
tryBlobInstall: () => tryBlobInstall
|
|
4929
|
+
});
|
|
4930
|
+
const DOWNLOAD_BASE_URL = process.env.SKILLS_DOWNLOAD_URL || "https://skills.sh";
|
|
4931
|
+
const BLOB_ALLOWED_REPOS = { "zapier/connectors": { downloadUrl: (slug) => `https://connectors-skills.zapier.com/download/${encodeURIComponent(slug)}/snapshot.json` } };
|
|
4932
|
+
const FETCH_TIMEOUT = 1e4;
|
|
4933
|
+
function toSkillSlug(name) {
|
|
4934
|
+
return name.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
4935
|
+
}
|
|
4936
|
+
let _rateLimitedThisSession = false;
|
|
4937
|
+
async function fetchTreeBranch(ownerRepo, branch, token) {
|
|
4244
4938
|
try {
|
|
4245
|
-
const
|
|
4246
|
-
|
|
4939
|
+
const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${encodeURIComponent(branch)}?recursive=1`;
|
|
4940
|
+
const headers = {
|
|
4941
|
+
Accept: "application/vnd.github.v3+json",
|
|
4942
|
+
"User-Agent": "skills-cli"
|
|
4943
|
+
};
|
|
4944
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
4945
|
+
const response = await fetch(url, {
|
|
4946
|
+
headers,
|
|
4947
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT)
|
|
4948
|
+
});
|
|
4949
|
+
if (response.ok) {
|
|
4950
|
+
const data = await response.json();
|
|
4951
|
+
return {
|
|
4952
|
+
tree: {
|
|
4953
|
+
sha: data.sha,
|
|
4954
|
+
branch,
|
|
4955
|
+
tree: data.tree
|
|
4956
|
+
},
|
|
4957
|
+
rateLimited: false
|
|
4958
|
+
};
|
|
4959
|
+
}
|
|
4960
|
+
return {
|
|
4961
|
+
tree: null,
|
|
4962
|
+
rateLimited: response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0"
|
|
4963
|
+
};
|
|
4247
4964
|
} catch {
|
|
4248
|
-
return
|
|
4965
|
+
return {
|
|
4966
|
+
tree: null,
|
|
4967
|
+
rateLimited: false
|
|
4968
|
+
};
|
|
4249
4969
|
}
|
|
4250
4970
|
}
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
const
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
" ████ ",
|
|
4264
|
-
" █ ",
|
|
4265
|
-
"█ █",
|
|
4266
|
-
" █████ "
|
|
4267
|
-
],
|
|
4268
|
-
a: [
|
|
4269
|
-
" ███ ",
|
|
4270
|
-
" █ █ ",
|
|
4271
|
-
" █ █ ",
|
|
4272
|
-
" █████ ",
|
|
4273
|
-
" █ █ ",
|
|
4274
|
-
" █ █ ",
|
|
4275
|
-
" █ █ "
|
|
4276
|
-
],
|
|
4277
|
-
f: [
|
|
4278
|
-
"██████ ",
|
|
4279
|
-
"█ ",
|
|
4280
|
-
"█ ",
|
|
4281
|
-
"█████ ",
|
|
4282
|
-
"█ ",
|
|
4283
|
-
"█ ",
|
|
4284
|
-
"█ "
|
|
4285
|
-
],
|
|
4286
|
-
e: [
|
|
4287
|
-
"██████ ",
|
|
4288
|
-
"█ ",
|
|
4289
|
-
"█ ",
|
|
4290
|
-
"█████ ",
|
|
4291
|
-
"█ ",
|
|
4292
|
-
"█ ",
|
|
4293
|
-
"██████ "
|
|
4294
|
-
],
|
|
4295
|
-
k: [
|
|
4296
|
-
"█ █ ",
|
|
4297
|
-
"█ █ ",
|
|
4298
|
-
"█ █ ",
|
|
4299
|
-
"███ ",
|
|
4300
|
-
"█ █ ",
|
|
4301
|
-
"█ █ ",
|
|
4302
|
-
"█ █ "
|
|
4303
|
-
],
|
|
4304
|
-
i: [
|
|
4305
|
-
"█████ ",
|
|
4306
|
-
" █ ",
|
|
4307
|
-
" █ ",
|
|
4308
|
-
" █ ",
|
|
4309
|
-
" █ ",
|
|
4310
|
-
" █ ",
|
|
4311
|
-
"█████ "
|
|
4312
|
-
],
|
|
4313
|
-
l: [
|
|
4314
|
-
"█ ",
|
|
4315
|
-
"█ ",
|
|
4316
|
-
"█ ",
|
|
4317
|
-
"█ ",
|
|
4318
|
-
"█ ",
|
|
4319
|
-
"█ ",
|
|
4320
|
-
"██████ "
|
|
4321
|
-
]
|
|
4322
|
-
};
|
|
4323
|
-
function buildLogoLines() {
|
|
4324
|
-
const words = ["Safe", "Skill"];
|
|
4325
|
-
const lines = Array.from({ length: LOGO_HEIGHT }, () => "");
|
|
4326
|
-
for (const word of words) {
|
|
4327
|
-
for (const character of word) {
|
|
4328
|
-
const glyph = LOGO_GLYPHS[character.toLowerCase()];
|
|
4329
|
-
if (!glyph) continue;
|
|
4330
|
-
for (let i = 0; i < LOGO_HEIGHT; i++) lines[i] += `${glyph[i] ?? ""} `;
|
|
4971
|
+
async function fetchRepoTree(ownerRepo, ref, getToken) {
|
|
4972
|
+
const branches = ref ? [ref] : [
|
|
4973
|
+
"HEAD",
|
|
4974
|
+
"main",
|
|
4975
|
+
"master"
|
|
4976
|
+
];
|
|
4977
|
+
if (_rateLimitedThisSession && getToken) {
|
|
4978
|
+
const token = getToken();
|
|
4979
|
+
if (!token) return null;
|
|
4980
|
+
for (const branch of branches) {
|
|
4981
|
+
const result = await fetchTreeBranch(ownerRepo, branch, token);
|
|
4982
|
+
if (result.tree) return result.tree;
|
|
4331
4983
|
}
|
|
4332
|
-
|
|
4984
|
+
return null;
|
|
4333
4985
|
}
|
|
4334
|
-
|
|
4986
|
+
let rateLimited = false;
|
|
4987
|
+
for (const branch of branches) {
|
|
4988
|
+
const result = await fetchTreeBranch(ownerRepo, branch, null);
|
|
4989
|
+
if (result.tree) return result.tree;
|
|
4990
|
+
if (result.rateLimited) {
|
|
4991
|
+
rateLimited = true;
|
|
4992
|
+
break;
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
if (!rateLimited || !getToken) return null;
|
|
4996
|
+
_rateLimitedThisSession = true;
|
|
4997
|
+
const token = getToken();
|
|
4998
|
+
if (!token) return null;
|
|
4999
|
+
for (const branch of branches) {
|
|
5000
|
+
const result = await fetchTreeBranch(ownerRepo, branch, token);
|
|
5001
|
+
if (result.tree) return result.tree;
|
|
5002
|
+
}
|
|
5003
|
+
return null;
|
|
4335
5004
|
}
|
|
4336
|
-
|
|
4337
|
-
"
|
|
4338
|
-
"
|
|
4339
|
-
"
|
|
4340
|
-
"
|
|
4341
|
-
|
|
4342
|
-
"
|
|
4343
|
-
|
|
5005
|
+
function getSkillFolderHashFromTree(tree, skillPath) {
|
|
5006
|
+
let folderPath = skillPath.replace(/\\/g, "/");
|
|
5007
|
+
if (folderPath.toLowerCase().endsWith("/skill.md")) folderPath = folderPath.slice(0, -9);
|
|
5008
|
+
else if (folderPath.toLowerCase().endsWith("skill.md")) folderPath = folderPath.slice(0, -8);
|
|
5009
|
+
if (folderPath.endsWith("/")) folderPath = folderPath.slice(0, -1);
|
|
5010
|
+
if (!folderPath) return tree.sha;
|
|
5011
|
+
return tree.tree.find((e) => e.type === "tree" && e.path === folderPath)?.sha ?? null;
|
|
5012
|
+
}
|
|
5013
|
+
const PRIORITY_PREFIXES = [
|
|
5014
|
+
"",
|
|
5015
|
+
"skills/",
|
|
5016
|
+
"skills/.curated/",
|
|
5017
|
+
"skills/.experimental/",
|
|
5018
|
+
"skills/.system/",
|
|
5019
|
+
".agents/skills/",
|
|
5020
|
+
".claude/skills/",
|
|
5021
|
+
".cline/skills/",
|
|
5022
|
+
".codebuddy/skills/",
|
|
5023
|
+
".codex/skills/",
|
|
5024
|
+
".commandcode/skills/",
|
|
5025
|
+
".continue/skills/",
|
|
5026
|
+
".github/skills/",
|
|
5027
|
+
".goose/skills/",
|
|
5028
|
+
".iflow/skills/",
|
|
5029
|
+
".junie/skills/",
|
|
5030
|
+
".kilocode/skills/",
|
|
5031
|
+
".kiro/skills/",
|
|
5032
|
+
".mux/skills/",
|
|
5033
|
+
".neovate/skills/",
|
|
5034
|
+
".opencode/skills/",
|
|
5035
|
+
".openhands/skills/",
|
|
5036
|
+
".pi/skills/",
|
|
5037
|
+
".qoder/skills/",
|
|
5038
|
+
".roo/skills/",
|
|
5039
|
+
".trae/skills/",
|
|
5040
|
+
".windsurf/skills/",
|
|
5041
|
+
".zencoder/skills/"
|
|
4344
5042
|
];
|
|
4345
|
-
function
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
5043
|
+
function findSkillMdPaths(tree, subpath) {
|
|
5044
|
+
const allSkillMds = tree.tree.filter((e) => {
|
|
5045
|
+
if (e.type !== "blob") return false;
|
|
5046
|
+
return e.path.replace(/\\/g, "/").split("/").pop()?.toLowerCase() === "skill.md";
|
|
5047
|
+
}).map((e) => e.path);
|
|
5048
|
+
const prefix = subpath ? subpath.endsWith("/") ? subpath : subpath + "/" : "";
|
|
5049
|
+
const filtered = prefix ? allSkillMds.filter((p) => p.startsWith(prefix) || p === prefix + "SKILL.md") : allSkillMds;
|
|
5050
|
+
if (filtered.length === 0) return [];
|
|
5051
|
+
const priorityResults = [];
|
|
5052
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5053
|
+
const SKIP_DIRS = new Set([
|
|
5054
|
+
"node_modules",
|
|
5055
|
+
".git",
|
|
5056
|
+
"dist",
|
|
5057
|
+
"build",
|
|
5058
|
+
"__pycache__"
|
|
5059
|
+
]);
|
|
5060
|
+
const lowerSkillMdSet = new Set(filtered.map((p) => p.toLowerCase()));
|
|
5061
|
+
for (const priorityPrefix of PRIORITY_PREFIXES) {
|
|
5062
|
+
const fullPrefix = prefix + priorityPrefix;
|
|
5063
|
+
const isContainer = priorityPrefix !== "";
|
|
5064
|
+
for (const skillMd of filtered) {
|
|
5065
|
+
if (!skillMd.startsWith(fullPrefix)) continue;
|
|
5066
|
+
const rest = skillMd.slice(fullPrefix.length);
|
|
5067
|
+
if (rest.toLowerCase() === "skill.md") {
|
|
5068
|
+
if (!seen.has(skillMd)) {
|
|
5069
|
+
priorityResults.push(skillMd);
|
|
5070
|
+
seen.add(skillMd);
|
|
5071
|
+
}
|
|
5072
|
+
continue;
|
|
5073
|
+
}
|
|
5074
|
+
const parts = rest.split("/");
|
|
5075
|
+
if (parts.length === 2 && parts[1].toLowerCase() === "skill.md") {
|
|
5076
|
+
if (!seen.has(skillMd)) {
|
|
5077
|
+
priorityResults.push(skillMd);
|
|
5078
|
+
seen.add(skillMd);
|
|
5079
|
+
}
|
|
5080
|
+
continue;
|
|
5081
|
+
}
|
|
5082
|
+
if (isContainer && parts.length === 3 && parts[2].toLowerCase() === "skill.md" && !SKIP_DIRS.has(parts[0]) && !SKIP_DIRS.has(parts[1])) {
|
|
5083
|
+
const parentSkillMd = `${fullPrefix}${parts[0]}/SKILL.md`.toLowerCase();
|
|
5084
|
+
if (!lowerSkillMdSet.has(parentSkillMd) && !seen.has(skillMd)) {
|
|
5085
|
+
priorityResults.push(skillMd);
|
|
5086
|
+
seen.add(skillMd);
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
if (priorityResults.length > 0) return priorityResults;
|
|
5092
|
+
return filtered.filter((p) => {
|
|
5093
|
+
return p.split("/").length <= 6;
|
|
4349
5094
|
});
|
|
4350
5095
|
}
|
|
4351
|
-
function
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
5096
|
+
async function fetchSkillMdContent(ownerRepo, branch, skillMdPath) {
|
|
5097
|
+
try {
|
|
5098
|
+
const url = `https://raw.githubusercontent.com/${ownerRepo}/${branch}/${skillMdPath}`;
|
|
5099
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT) });
|
|
5100
|
+
if (!response.ok) return null;
|
|
5101
|
+
return await response.text();
|
|
5102
|
+
} catch {
|
|
5103
|
+
return null;
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
async function fetchSkillDownload(source, slug) {
|
|
5107
|
+
try {
|
|
5108
|
+
const [owner, repo] = source.split("/");
|
|
5109
|
+
const defaultUrl = `${DOWNLOAD_BASE_URL}/api/download/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(slug)}`;
|
|
5110
|
+
const url = BLOB_ALLOWED_REPOS[source.toLowerCase()]?.downloadUrl(slug) ?? defaultUrl;
|
|
5111
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT) });
|
|
5112
|
+
if (!response.ok) return null;
|
|
5113
|
+
return await response.json();
|
|
5114
|
+
} catch {
|
|
5115
|
+
return null;
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
function computeSnapshotHash(files) {
|
|
5119
|
+
const hash = createHash$1("sha256");
|
|
5120
|
+
for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
5121
|
+
hash.update(file.path);
|
|
5122
|
+
hash.update(file.contents);
|
|
5123
|
+
}
|
|
5124
|
+
return hash.digest("hex");
|
|
5125
|
+
}
|
|
5126
|
+
function normalizeSnapshotFilePath(filePath) {
|
|
5127
|
+
if (typeof filePath !== "string") return null;
|
|
5128
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
5129
|
+
if (normalized.trim() === "") return null;
|
|
5130
|
+
if (normalized.startsWith("/")) return null;
|
|
5131
|
+
if (/^[a-zA-Z]:\//.test(normalized)) return null;
|
|
5132
|
+
const segments = normalized.split("/");
|
|
5133
|
+
if (segments.some((segment) => segment === "" || segment === "..")) return null;
|
|
5134
|
+
return segments.join("/");
|
|
5135
|
+
}
|
|
5136
|
+
function normalizeSnapshotFiles(files) {
|
|
5137
|
+
const normalizedFiles = [];
|
|
5138
|
+
for (const file of files) {
|
|
5139
|
+
const normalizedPath = normalizeSnapshotFilePath(file.path);
|
|
5140
|
+
if (!normalizedPath || typeof file.contents !== "string") return null;
|
|
5141
|
+
normalizedFiles.push({
|
|
5142
|
+
path: normalizedPath,
|
|
5143
|
+
contents: file.contents
|
|
5144
|
+
});
|
|
5145
|
+
}
|
|
5146
|
+
return normalizedFiles;
|
|
5147
|
+
}
|
|
5148
|
+
async function tryBlobInstall(ownerRepo, options = {}) {
|
|
5149
|
+
const tree = await fetchRepoTree(ownerRepo, options.ref, options.getToken);
|
|
5150
|
+
if (!tree) return null;
|
|
5151
|
+
let skillMdPaths = findSkillMdPaths(tree, options.subpath);
|
|
5152
|
+
if (skillMdPaths.length === 0) return null;
|
|
5153
|
+
if (options.skillFilter) {
|
|
5154
|
+
const filterSlug = toSkillSlug(options.skillFilter);
|
|
5155
|
+
const filtered = skillMdPaths.filter((p) => {
|
|
5156
|
+
const parts = p.split("/");
|
|
5157
|
+
if (parts.length < 2) return false;
|
|
5158
|
+
const folderName = parts[parts.length - 2];
|
|
5159
|
+
return toSkillSlug(folderName) === filterSlug;
|
|
5160
|
+
});
|
|
5161
|
+
if (filtered.length > 0) skillMdPaths = filtered;
|
|
5162
|
+
}
|
|
5163
|
+
const mdFetches = await Promise.all(skillMdPaths.map(async (mdPath) => {
|
|
5164
|
+
return {
|
|
5165
|
+
mdPath,
|
|
5166
|
+
content: await fetchSkillMdContent(ownerRepo, tree.branch, mdPath)
|
|
5167
|
+
};
|
|
5168
|
+
}));
|
|
5169
|
+
const parsedSkills = [];
|
|
5170
|
+
for (const { mdPath, content } of mdFetches) {
|
|
5171
|
+
if (!content) return null;
|
|
5172
|
+
const { data } = parseFrontmatter(content);
|
|
5173
|
+
if (!data.name || !data.description) return null;
|
|
5174
|
+
if (typeof data.name !== "string" || typeof data.description !== "string") return null;
|
|
5175
|
+
if (data.metadata?.internal === true && !options.includeInternal) continue;
|
|
5176
|
+
const safeName = sanitizeMetadata(data.name);
|
|
5177
|
+
const safeDescription = sanitizeMetadata(data.description);
|
|
5178
|
+
if (!safeName || !safeDescription) return null;
|
|
5179
|
+
parsedSkills.push({
|
|
5180
|
+
mdPath,
|
|
5181
|
+
name: safeName,
|
|
5182
|
+
description: safeDescription,
|
|
5183
|
+
content,
|
|
5184
|
+
slug: toSkillSlug(safeName),
|
|
5185
|
+
metadata: data.metadata
|
|
5186
|
+
});
|
|
5187
|
+
}
|
|
5188
|
+
if (parsedSkills.length === 0) return null;
|
|
5189
|
+
let filteredSkills = parsedSkills;
|
|
5190
|
+
if (options.skillFilter) {
|
|
5191
|
+
const filterSlug = toSkillSlug(options.skillFilter);
|
|
5192
|
+
const nameFiltered = parsedSkills.filter((s) => s.slug === filterSlug);
|
|
5193
|
+
if (nameFiltered.length > 0) filteredSkills = nameFiltered;
|
|
5194
|
+
else return null;
|
|
5195
|
+
}
|
|
5196
|
+
const source = ownerRepo.toLowerCase();
|
|
5197
|
+
const downloads = await Promise.all(filteredSkills.map(async (skill) => {
|
|
5198
|
+
return {
|
|
5199
|
+
skill,
|
|
5200
|
+
download: await fetchSkillDownload(source, skill.slug)
|
|
5201
|
+
};
|
|
5202
|
+
}));
|
|
5203
|
+
if (!downloads.every((d) => d.download !== null)) return null;
|
|
5204
|
+
const normalizedDownloads = downloads.map(({ skill, download }) => {
|
|
5205
|
+
return {
|
|
5206
|
+
skill,
|
|
5207
|
+
download,
|
|
5208
|
+
files: normalizeSnapshotFiles(download.files)
|
|
5209
|
+
};
|
|
5210
|
+
});
|
|
5211
|
+
if (normalizedDownloads.some(({ files }) => files === null)) return null;
|
|
5212
|
+
return {
|
|
5213
|
+
skills: normalizedDownloads.map(({ skill, download, files }) => {
|
|
5214
|
+
const mdPathLower = skill.mdPath.toLowerCase();
|
|
5215
|
+
const selectedFiles = (mdPathLower.endsWith("/skill.md") ? skill.mdPath.slice(0, -9) : mdPathLower === "skill.md" ? "" : skill.mdPath.slice(0, -9)) ? files : files.filter((file) => file.path.toLowerCase() === "skill.md");
|
|
5216
|
+
return {
|
|
5217
|
+
name: skill.name,
|
|
5218
|
+
description: skill.description,
|
|
5219
|
+
path: "",
|
|
5220
|
+
rawContent: skill.content,
|
|
5221
|
+
metadata: skill.metadata,
|
|
5222
|
+
files: selectedFiles,
|
|
5223
|
+
snapshotHash: selectedFiles.length === files.length ? download.hash : computeSnapshotHash(selectedFiles),
|
|
5224
|
+
repoPath: skill.mdPath
|
|
5225
|
+
};
|
|
5226
|
+
}),
|
|
5227
|
+
tree
|
|
5228
|
+
};
|
|
5229
|
+
}
|
|
5230
|
+
const __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
5231
|
+
const RESET$1 = "\x1B[0m";
|
|
5232
|
+
const BOLD$1 = "\x1B[1m";
|
|
5233
|
+
const DIM$1 = "\x1B[38;5;102m";
|
|
5234
|
+
const TEXT$1 = "\x1B[38;5;145m";
|
|
5235
|
+
function parseUpdateOptions(args) {
|
|
5236
|
+
const options = {};
|
|
5237
|
+
const positional = [];
|
|
5238
|
+
for (const arg of args) if (arg === "-g" || arg === "--global") options.global = true;
|
|
5239
|
+
else if (arg === "-p" || arg === "--project") options.project = true;
|
|
5240
|
+
else if (arg === "-y" || arg === "--yes") options.yes = true;
|
|
5241
|
+
else if (!arg.startsWith("-")) positional.push(arg);
|
|
5242
|
+
if (positional.length > 0) options.skills = positional;
|
|
5243
|
+
return options;
|
|
5244
|
+
}
|
|
5245
|
+
function hasProjectSkills(cwd) {
|
|
5246
|
+
const dir = cwd || process.cwd();
|
|
5247
|
+
if (existsSync(join(dir, "skills-lock.json"))) return true;
|
|
5248
|
+
const skillsDir = join(dir, ".agents", "skills");
|
|
5249
|
+
try {
|
|
5250
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
5251
|
+
for (const entry of entries) if (entry.isDirectory() && existsSync(join(skillsDir, entry.name, "SKILL.md"))) return true;
|
|
5252
|
+
} catch {}
|
|
5253
|
+
return false;
|
|
5254
|
+
}
|
|
5255
|
+
async function resolveUpdateScope(options) {
|
|
5256
|
+
if (options.skills && options.skills.length > 0) {
|
|
5257
|
+
if (options.global) return "global";
|
|
5258
|
+
if (options.project) return "project";
|
|
5259
|
+
return "both";
|
|
5260
|
+
}
|
|
5261
|
+
if (options.global && options.project) return "both";
|
|
5262
|
+
if (options.global) return "global";
|
|
5263
|
+
if (options.project) return "project";
|
|
5264
|
+
if (options.yes || !process.stdin.isTTY) return hasProjectSkills() ? "project" : "global";
|
|
5265
|
+
const scope = await ve({
|
|
5266
|
+
message: "Update scope",
|
|
5267
|
+
options: [
|
|
5268
|
+
{
|
|
5269
|
+
value: "project",
|
|
5270
|
+
label: "Project",
|
|
5271
|
+
hint: "Update skills in current directory"
|
|
5272
|
+
},
|
|
5273
|
+
{
|
|
5274
|
+
value: "global",
|
|
5275
|
+
label: "Global",
|
|
5276
|
+
hint: "Update skills in home directory"
|
|
5277
|
+
},
|
|
5278
|
+
{
|
|
5279
|
+
value: "both",
|
|
5280
|
+
label: "Both",
|
|
5281
|
+
hint: "Update all skills"
|
|
5282
|
+
}
|
|
5283
|
+
]
|
|
5284
|
+
});
|
|
5285
|
+
if (pD(scope)) {
|
|
5286
|
+
xe("Cancelled");
|
|
5287
|
+
process.exit(0);
|
|
5288
|
+
}
|
|
5289
|
+
return scope;
|
|
5290
|
+
}
|
|
5291
|
+
function matchesSkillFilter(name, filter) {
|
|
5292
|
+
if (!filter || filter.length === 0) return true;
|
|
5293
|
+
const lower = name.toLowerCase();
|
|
5294
|
+
return filter.some((f) => f.toLowerCase() === lower);
|
|
5295
|
+
}
|
|
5296
|
+
function getSkipReason(entry) {
|
|
5297
|
+
if (entry.sourceType === "local") return "Local path";
|
|
5298
|
+
if (entry.sourceType === "hub") {
|
|
5299
|
+
if (!entry.resolvedVersion) return "No hub version recorded";
|
|
5300
|
+
if (!entry.artifactDigest) return "No hub artifact metadata";
|
|
5301
|
+
return "No version tracking";
|
|
5302
|
+
}
|
|
5303
|
+
if (entry.sourceType === "well-known") return "Well-known skill";
|
|
5304
|
+
if (!entry.skillFolderHash) return "No version hash available";
|
|
5305
|
+
if (!entry.skillPath) return "No skill path recorded";
|
|
5306
|
+
return "No version tracking";
|
|
5307
|
+
}
|
|
5308
|
+
function replaceHubSourceVersion(source, version) {
|
|
5309
|
+
const trimmedSource = source.trim();
|
|
5310
|
+
if (!trimmedSource) return source;
|
|
5311
|
+
return `${trimmedSource.replace(/@[^/@]+$/, "")}@${version}`;
|
|
5312
|
+
}
|
|
5313
|
+
function getInstallSource(skill) {
|
|
5314
|
+
let url = skill.sourceUrl;
|
|
5315
|
+
if (skill.sourceType === "well-known") {
|
|
5316
|
+
const idx = url.indexOf("/.well-known/");
|
|
5317
|
+
if (idx !== -1) url = url.slice(0, idx);
|
|
5318
|
+
}
|
|
5319
|
+
return formatSourceInput(url, skill.ref);
|
|
5320
|
+
}
|
|
5321
|
+
function getProjectHubSkipReason(entry) {
|
|
5322
|
+
if (!entry.resolvedVersion) return "No hub version recorded";
|
|
5323
|
+
if (!entry.artifactDigest) return "No hub artifact metadata";
|
|
5324
|
+
return "No hub update metadata";
|
|
5325
|
+
}
|
|
5326
|
+
function getSourceRefKey(source, ref) {
|
|
5327
|
+
return `${source}\0${ref ?? ""}`;
|
|
5328
|
+
}
|
|
5329
|
+
function hasSameSourceAndRef(entry, source, ref) {
|
|
5330
|
+
return entry.source === source && (entry.ref ?? "") === (ref ?? "");
|
|
5331
|
+
}
|
|
5332
|
+
function printSkippedSkills(skipped) {
|
|
5333
|
+
if (skipped.length === 0) return;
|
|
5334
|
+
console.log();
|
|
5335
|
+
console.log(`${DIM$1}${skipped.length} skill(s) cannot be checked automatically:${RESET$1}`);
|
|
5336
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
5337
|
+
for (const skill of skipped) {
|
|
5338
|
+
const source = getInstallSource(skill);
|
|
5339
|
+
const existing = grouped.get(source) || [];
|
|
5340
|
+
existing.push(skill);
|
|
5341
|
+
grouped.set(source, existing);
|
|
5342
|
+
}
|
|
5343
|
+
for (const [source, skills] of grouped) {
|
|
5344
|
+
if (skills.length === 1) {
|
|
5345
|
+
const skill = skills[0];
|
|
5346
|
+
console.log(` ${TEXT$1}-${RESET$1} ${sanitizeMetadata(skill.name)} ${DIM$1}(${skill.reason})${RESET$1}`);
|
|
5347
|
+
} else {
|
|
5348
|
+
const reason = skills[0].reason;
|
|
5349
|
+
const names = skills.map((s) => sanitizeMetadata(s.name)).join(", ");
|
|
5350
|
+
console.log(` ${TEXT$1}-${RESET$1} ${names} ${DIM$1}(${reason})${RESET$1}`);
|
|
5351
|
+
}
|
|
5352
|
+
console.log(` ${DIM$1}To update: ${TEXT$1}npx safeskill add ${source} -g -y${RESET$1}`);
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
function printSkippedProjectHubSkills(skipped) {
|
|
5356
|
+
if (skipped.length === 0) return;
|
|
5357
|
+
console.log();
|
|
5358
|
+
console.log(`${DIM$1}${skipped.length} project hub skill(s) require manual refresh:${RESET$1}`);
|
|
5359
|
+
for (const skill of skipped) {
|
|
5360
|
+
console.log(` ${TEXT$1}-${RESET$1} ${sanitizeMetadata(skill.name)} ${DIM$1}(${getProjectHubSkipReason(skill.entry)})${RESET$1}`);
|
|
5361
|
+
console.log(` ${DIM$1}To refresh: ${TEXT$1}npx safeskill add ${skill.entry.source} -y${RESET$1}`);
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5364
|
+
async function getProjectSkillsForUpdate(skillFilter) {
|
|
5365
|
+
const localLock = await readLocalLock();
|
|
5366
|
+
const skills = [];
|
|
5367
|
+
for (const [name, entry] of Object.entries(localLock.skills)) {
|
|
5368
|
+
if (!matchesSkillFilter(name, skillFilter)) continue;
|
|
5369
|
+
if (entry.sourceType === "node_modules" || entry.sourceType === "local") continue;
|
|
5370
|
+
skills.push({
|
|
5371
|
+
name,
|
|
5372
|
+
source: entry.source,
|
|
5373
|
+
entry
|
|
5374
|
+
});
|
|
5375
|
+
}
|
|
5376
|
+
return skills;
|
|
5377
|
+
}
|
|
5378
|
+
async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discoveredPaths, mode = "update") {
|
|
5379
|
+
const deletedSkills = allLockedForSource.filter((name) => {
|
|
5380
|
+
const entry = lockSkills[name];
|
|
5381
|
+
if (!entry?.skillPath) return false;
|
|
5382
|
+
return !discoveredPaths.includes(entry.skillPath);
|
|
5383
|
+
});
|
|
5384
|
+
if (deletedSkills.length > 0) {
|
|
5385
|
+
console.log();
|
|
5386
|
+
console.log(`${DIM$1}Warning:${RESET$1} The following skills from ${DIM$1}${source}${RESET$1} appear to have been deleted upstream:`);
|
|
5387
|
+
for (const s of deletedSkills) console.log(` ${DIM$1}-${RESET$1} ${s}`);
|
|
5388
|
+
if (mode === "check") {
|
|
5389
|
+
console.log(`${DIM$1}Skipping deletion in check mode.${RESET$1}`);
|
|
5390
|
+
return deletedSkills;
|
|
5391
|
+
}
|
|
5392
|
+
if (options.yes || !process.stdin.isTTY) console.log(`${DIM$1}Skipping deletion in non-interactive mode.${RESET$1}`);
|
|
5393
|
+
else {
|
|
5394
|
+
const confirmed = await ye({ message: `Would you like to remove the local copies of these deleted skills?` });
|
|
5395
|
+
if (confirmed && !pD(confirmed)) for (const s of deletedSkills) {
|
|
5396
|
+
console.log(`${DIM$1}Removing${RESET$1} ${s}...`);
|
|
5397
|
+
await removeCommand([s], {
|
|
5398
|
+
yes: true,
|
|
5399
|
+
global: isGlobal
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
return deletedSkills;
|
|
5405
|
+
}
|
|
5406
|
+
async function updateGlobalSkills(options = {}, region, mode = "update") {
|
|
5407
|
+
const lock = await readSkillLock();
|
|
5408
|
+
const skillNames = Object.keys(lock.skills);
|
|
5409
|
+
let successCount = 0;
|
|
5410
|
+
let failCount = 0;
|
|
5411
|
+
let checkFailCount = 0;
|
|
5412
|
+
if (skillNames.length === 0) {
|
|
5413
|
+
if (!options.skills) {
|
|
5414
|
+
console.log(`${DIM$1}No global skills tracked in lock file.${RESET$1}`);
|
|
5415
|
+
console.log(`${DIM$1}Install skills with${RESET$1} ${TEXT$1}npx safeskill add <package> -g${RESET$1}`);
|
|
5416
|
+
}
|
|
5417
|
+
return {
|
|
5418
|
+
successCount,
|
|
5419
|
+
failCount,
|
|
5420
|
+
checkFailCount,
|
|
5421
|
+
checkedCount: 0,
|
|
5422
|
+
updateCount: 0
|
|
5423
|
+
};
|
|
5424
|
+
}
|
|
5425
|
+
const updates = [];
|
|
5426
|
+
const skipped = [];
|
|
5427
|
+
const checkable = [];
|
|
5428
|
+
const hubEntries = [];
|
|
5429
|
+
for (const skillName of skillNames) {
|
|
5430
|
+
if (!matchesSkillFilter(skillName, options.skills)) continue;
|
|
5431
|
+
const entry = lock.skills[skillName];
|
|
5432
|
+
if (!entry) continue;
|
|
5433
|
+
if (entry.sourceType === "hub") {
|
|
5434
|
+
if (!entry.resolvedVersion || !entry.artifactDigest) {
|
|
5435
|
+
skipped.push({
|
|
5436
|
+
name: skillName,
|
|
5437
|
+
reason: getSkipReason(entry),
|
|
5438
|
+
sourceUrl: entry.sourceUrl,
|
|
5439
|
+
sourceType: entry.sourceType,
|
|
5440
|
+
ref: entry.ref
|
|
5441
|
+
});
|
|
5442
|
+
continue;
|
|
5443
|
+
}
|
|
5444
|
+
hubEntries.push({
|
|
5445
|
+
name: skillName,
|
|
5446
|
+
entry
|
|
5447
|
+
});
|
|
5448
|
+
continue;
|
|
5449
|
+
}
|
|
5450
|
+
if (!entry.skillFolderHash || !entry.skillPath) {
|
|
5451
|
+
skipped.push({
|
|
5452
|
+
name: skillName,
|
|
5453
|
+
reason: getSkipReason(entry),
|
|
5454
|
+
sourceUrl: entry.sourceUrl,
|
|
5455
|
+
sourceType: entry.sourceType,
|
|
5456
|
+
ref: entry.ref
|
|
5457
|
+
});
|
|
5458
|
+
continue;
|
|
5459
|
+
}
|
|
5460
|
+
checkable.push({
|
|
5461
|
+
name: skillName,
|
|
5462
|
+
entry
|
|
5463
|
+
});
|
|
5464
|
+
}
|
|
5465
|
+
if (hubEntries.length > 0) try {
|
|
5466
|
+
(await checkHubUpdates(hubEntries.map(({ entry }) => ({
|
|
5467
|
+
source: entry.sourceUrl,
|
|
5468
|
+
version: entry.resolvedVersion,
|
|
5469
|
+
artifact_digest: entry.artifactDigest
|
|
5470
|
+
})))).items.forEach((item, index) => {
|
|
5471
|
+
const hubEntry = hubEntries[index];
|
|
5472
|
+
if (!hubEntry || !item.has_update || !item.latest_version) return;
|
|
5473
|
+
updates.push({
|
|
5474
|
+
name: hubEntry.name,
|
|
5475
|
+
source: hubEntry.entry.sourceUrl,
|
|
5476
|
+
installSource: replaceHubSourceVersion(hubEntry.entry.sourceUrl, item.latest_version),
|
|
5477
|
+
entry: hubEntry.entry
|
|
5478
|
+
});
|
|
5479
|
+
});
|
|
5480
|
+
} catch {
|
|
5481
|
+
checkFailCount += hubEntries.length;
|
|
5482
|
+
console.log(` ${DIM$1}Failed to check hub skills${RESET$1}`);
|
|
5483
|
+
}
|
|
5484
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
5485
|
+
for (const item of checkable) {
|
|
5486
|
+
const source = getSourceRefKey(item.entry.source, item.entry.ref);
|
|
5487
|
+
const existing = bySource.get(source) || [];
|
|
5488
|
+
existing.push(item);
|
|
5489
|
+
bySource.set(source, existing);
|
|
5490
|
+
}
|
|
5491
|
+
for (const [, itemsForSource] of bySource) {
|
|
5492
|
+
const firstEntry = itemsForSource[0].entry;
|
|
5493
|
+
const source = firstEntry.source;
|
|
5494
|
+
const ref = firstEntry.ref;
|
|
5495
|
+
const sourceUrl = firstEntry.sourceUrl || firstEntry.source;
|
|
5496
|
+
let tempDir = null;
|
|
5497
|
+
const sourceLabel = ref ? `${source}#${ref}` : source;
|
|
5498
|
+
process.stdout.write(`\r${DIM$1}Checking skills from source: ${sourceLabel}${RESET$1}\x1b[K\n`);
|
|
5499
|
+
try {
|
|
5500
|
+
if (firstEntry.sourceType === "github") {
|
|
5501
|
+
const tree = await fetchRepoTree(source, firstEntry.ref, getGitHubToken);
|
|
5502
|
+
if (!tree) {
|
|
5503
|
+
console.log(` ${DIM$1}Failed to fetch tree for ${source}${RESET$1}`);
|
|
5504
|
+
checkFailCount += itemsForSource.length;
|
|
5505
|
+
continue;
|
|
5506
|
+
}
|
|
5507
|
+
const discoveredPaths = findSkillMdPaths(tree);
|
|
5508
|
+
const deletedSkills = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([, entry]) => hasSameSourceAndRef(entry, source, ref)).map(([name]) => name), lock.skills, true, options, discoveredPaths, mode);
|
|
5509
|
+
const deletedSkillSet = new Set(deletedSkills);
|
|
5510
|
+
for (const { name: skillName, entry } of itemsForSource) {
|
|
5511
|
+
if (deletedSkillSet.has(skillName)) continue;
|
|
5512
|
+
const latestHash = getSkillFolderHashFromTree(tree, entry.skillPath);
|
|
5513
|
+
if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
|
|
5514
|
+
name: skillName,
|
|
5515
|
+
source,
|
|
5516
|
+
entry
|
|
5517
|
+
});
|
|
5518
|
+
}
|
|
5519
|
+
continue;
|
|
5520
|
+
}
|
|
5521
|
+
tempDir = await cloneRepo(sourceUrl, ref);
|
|
5522
|
+
const discoveredPaths = (await discoverSkills(tempDir)).map((skill) => {
|
|
5523
|
+
return join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/");
|
|
5524
|
+
});
|
|
5525
|
+
const deletedSkills = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([, entry]) => hasSameSourceAndRef(entry, source, ref)).map(([name]) => name), lock.skills, true, options, discoveredPaths, mode);
|
|
5526
|
+
const deletedSkillSet = new Set(deletedSkills);
|
|
5527
|
+
for (const { name: skillName, entry } of itemsForSource) {
|
|
5528
|
+
if (deletedSkillSet.has(skillName)) continue;
|
|
5529
|
+
const skillPath = entry.skillPath;
|
|
5530
|
+
if (!discoveredPaths.includes(skillPath)) continue;
|
|
5531
|
+
const latestHash = await computeSkillFolderHash(join(tempDir, dirname(skillPath)));
|
|
5532
|
+
if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
|
|
5533
|
+
name: skillName,
|
|
5534
|
+
source,
|
|
5535
|
+
entry
|
|
5536
|
+
});
|
|
5537
|
+
}
|
|
5538
|
+
} catch {
|
|
5539
|
+
console.log(` ${DIM$1}Failed to check skills from ${source}${RESET$1}`);
|
|
5540
|
+
checkFailCount += itemsForSource.length;
|
|
5541
|
+
} finally {
|
|
5542
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
if (checkable.length > 0) process.stdout.write("\r\x1B[K");
|
|
5546
|
+
const checkedCount = checkable.length + hubEntries.length + skipped.length;
|
|
5547
|
+
if (checkable.length === 0 && hubEntries.length === 0 && skipped.length === 0) {
|
|
5548
|
+
if (!options.skills) console.log(`${DIM$1}No global skills to check.${RESET$1}`);
|
|
5549
|
+
return {
|
|
5550
|
+
successCount,
|
|
5551
|
+
failCount,
|
|
5552
|
+
checkFailCount,
|
|
5553
|
+
checkedCount: 0,
|
|
5554
|
+
updateCount: 0
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5557
|
+
if (checkable.length === 0 && hubEntries.length === 0 && skipped.length > 0) {
|
|
5558
|
+
printSkippedSkills(skipped);
|
|
5559
|
+
return {
|
|
5560
|
+
successCount,
|
|
5561
|
+
failCount,
|
|
5562
|
+
checkFailCount,
|
|
5563
|
+
checkedCount,
|
|
5564
|
+
updateCount: 0
|
|
5565
|
+
};
|
|
5566
|
+
}
|
|
5567
|
+
if (updates.length === 0) {
|
|
5568
|
+
if (checkFailCount > 0) console.log(`${DIM$1}Failed to check ${checkFailCount} global skill(s)${RESET$1}`);
|
|
5569
|
+
else console.log(`${TEXT$1}All global skills are up to date${RESET$1}`);
|
|
5570
|
+
printSkippedSkills(skipped);
|
|
5571
|
+
return {
|
|
5572
|
+
successCount,
|
|
5573
|
+
failCount,
|
|
5574
|
+
checkFailCount,
|
|
5575
|
+
checkedCount,
|
|
5576
|
+
updateCount: 0
|
|
5577
|
+
};
|
|
5578
|
+
}
|
|
5579
|
+
if (mode === "check") {
|
|
5580
|
+
console.log(`${TEXT$1}Found ${updates.length} global update(s)${RESET$1}`);
|
|
5581
|
+
console.log();
|
|
5582
|
+
for (const update of updates) {
|
|
5583
|
+
console.log(` ${TEXT$1}-${RESET$1} ${sanitizeMetadata(update.name)}`);
|
|
5584
|
+
console.log(` ${DIM$1}source: ${getInstallSource({
|
|
5585
|
+
name: update.name,
|
|
5586
|
+
reason: "",
|
|
5587
|
+
sourceUrl: update.installSource || update.entry.sourceUrl,
|
|
5588
|
+
sourceType: update.entry.sourceType,
|
|
5589
|
+
ref: update.entry.ref
|
|
5590
|
+
})}${RESET$1}`);
|
|
5591
|
+
}
|
|
5592
|
+
printSkippedSkills(skipped);
|
|
5593
|
+
return {
|
|
5594
|
+
successCount,
|
|
5595
|
+
failCount,
|
|
5596
|
+
checkFailCount,
|
|
5597
|
+
checkedCount,
|
|
5598
|
+
updateCount: updates.length
|
|
5599
|
+
};
|
|
5600
|
+
}
|
|
5601
|
+
console.log(`${TEXT$1}Found ${updates.length} global update(s)${RESET$1}`);
|
|
5602
|
+
console.log();
|
|
5603
|
+
for (const update of updates) {
|
|
5604
|
+
const safeName = sanitizeMetadata(update.name);
|
|
5605
|
+
console.log(`${TEXT$1}Updating ${safeName}...${RESET$1}`);
|
|
5606
|
+
const installUrl = buildUpdateInstallSource(update.entry, update.installSource);
|
|
5607
|
+
const cliEntry = join(__dirname$1, "..", "bin", "cli.mjs");
|
|
5608
|
+
if (!existsSync(cliEntry)) {
|
|
5609
|
+
failCount++;
|
|
5610
|
+
console.log(` ${DIM$1}Failed to update ${safeName}: CLI entrypoint not found at ${cliEntry}${RESET$1}`);
|
|
5611
|
+
continue;
|
|
5612
|
+
}
|
|
5613
|
+
const addArgs = region ? [
|
|
5614
|
+
cliEntry,
|
|
5615
|
+
"--region",
|
|
5616
|
+
region,
|
|
5617
|
+
"add",
|
|
5618
|
+
installUrl,
|
|
5619
|
+
"-g",
|
|
5620
|
+
"-y"
|
|
5621
|
+
] : [
|
|
5622
|
+
cliEntry,
|
|
5623
|
+
"add",
|
|
5624
|
+
installUrl,
|
|
5625
|
+
"-g",
|
|
5626
|
+
"-y"
|
|
5627
|
+
];
|
|
5628
|
+
if (spawnSync(process.execPath, addArgs, {
|
|
5629
|
+
stdio: [
|
|
5630
|
+
"inherit",
|
|
5631
|
+
"pipe",
|
|
5632
|
+
"pipe"
|
|
5633
|
+
],
|
|
5634
|
+
encoding: "utf-8",
|
|
5635
|
+
shell: process.platform === "win32"
|
|
5636
|
+
}).status === 0) {
|
|
5637
|
+
successCount++;
|
|
5638
|
+
console.log(` ${TEXT$1}Updated ${safeName}${RESET$1}`);
|
|
5639
|
+
} else {
|
|
5640
|
+
failCount++;
|
|
5641
|
+
console.log(` ${DIM$1}Failed to update ${safeName}${RESET$1}`);
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
printSkippedSkills(skipped);
|
|
5645
|
+
return {
|
|
5646
|
+
successCount,
|
|
5647
|
+
failCount,
|
|
5648
|
+
checkFailCount,
|
|
5649
|
+
checkedCount,
|
|
5650
|
+
updateCount: updates.length
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
async function updateProjectSkills(options = {}, regionOrMode, maybeMode) {
|
|
5654
|
+
const region = regionOrMode === "check" || regionOrMode === "update" ? void 0 : regionOrMode;
|
|
5655
|
+
const mode = regionOrMode === "check" || regionOrMode === "update" ? regionOrMode : maybeMode || "update";
|
|
5656
|
+
const projectSkills = await getProjectSkillsForUpdate(options.skills);
|
|
5657
|
+
let successCount = 0;
|
|
5658
|
+
let failCount = 0;
|
|
5659
|
+
let checkFailCount = 0;
|
|
5660
|
+
let updateCount = 0;
|
|
5661
|
+
if (projectSkills.length === 0) {
|
|
5662
|
+
if (!options.skills) {
|
|
5663
|
+
console.log(`${DIM$1}No project skills to update.${RESET$1}`);
|
|
5664
|
+
console.log(`${DIM$1}Install project skills with${RESET$1} ${TEXT$1}npx safeskill add <package>${RESET$1}`);
|
|
5665
|
+
}
|
|
5666
|
+
return {
|
|
5667
|
+
successCount,
|
|
5668
|
+
failCount,
|
|
5669
|
+
checkFailCount,
|
|
5670
|
+
foundCount: 0,
|
|
5671
|
+
updateCount
|
|
5672
|
+
};
|
|
5673
|
+
}
|
|
5674
|
+
const updatable = projectSkills.filter((s) => s.entry.skillPath);
|
|
5675
|
+
const legacy = projectSkills.filter((s) => !s.entry.skillPath);
|
|
5676
|
+
const hubUpdatable = updatable.filter((s) => s.entry.sourceType === "hub" && s.entry.resolvedVersion && s.entry.artifactDigest);
|
|
5677
|
+
const skippedHub = updatable.filter((s) => s.entry.sourceType === "hub" && (!s.entry.resolvedVersion || !s.entry.artifactDigest));
|
|
5678
|
+
const sourceUpdatable = updatable.filter((s) => s.entry.sourceType !== "hub");
|
|
5679
|
+
if (updatable.length === 0) {
|
|
5680
|
+
console.log(`${DIM$1}No project skills can be updated in place.${RESET$1}`);
|
|
5681
|
+
printLegacyProjectSkills(legacy);
|
|
5682
|
+
return {
|
|
5683
|
+
successCount,
|
|
5684
|
+
failCount,
|
|
5685
|
+
checkFailCount,
|
|
5686
|
+
foundCount: projectSkills.length,
|
|
5687
|
+
updateCount
|
|
5688
|
+
};
|
|
5689
|
+
}
|
|
5690
|
+
const cwd = process.cwd();
|
|
5691
|
+
const targetAgentNames = [];
|
|
5692
|
+
let hasUniversal = false;
|
|
5693
|
+
for (const [type, config] of Object.entries(agents)) if (isUniversalAgent(type)) {
|
|
5694
|
+
if (!hasUniversal && existsSync(join(cwd, ".agents"))) hasUniversal = true;
|
|
5695
|
+
} else {
|
|
5696
|
+
const agentRoot = config.skillsDir.split("/")[0];
|
|
5697
|
+
if (existsSync(join(cwd, agentRoot))) targetAgentNames.push(config.displayName);
|
|
5698
|
+
}
|
|
5699
|
+
const targetParts = [];
|
|
5700
|
+
if (hasUniversal) targetParts.push("Universal");
|
|
5701
|
+
targetParts.push(...targetAgentNames);
|
|
5702
|
+
if (targetParts.length > 0) {
|
|
5703
|
+
const action = mode === "check" ? "Checking" : "Updating";
|
|
5704
|
+
console.log(`${TEXT$1}${action} for: ${targetParts.join(", ")}${RESET$1}`);
|
|
5705
|
+
}
|
|
5706
|
+
console.log(mode === "check" ? `${TEXT$1}Checking ${sourceUpdatable.length + hubUpdatable.length} project skill(s)...${RESET$1}` : `${TEXT$1}Refreshing ${sourceUpdatable.length + hubUpdatable.length} skill(s)...${RESET$1}`);
|
|
5707
|
+
console.log();
|
|
5708
|
+
const hubInstallSources = /* @__PURE__ */ new Map();
|
|
5709
|
+
if (hubUpdatable.length > 0) try {
|
|
5710
|
+
(await checkHubUpdates(hubUpdatable.map(({ entry }) => ({
|
|
5711
|
+
source: entry.source,
|
|
5712
|
+
version: entry.resolvedVersion,
|
|
5713
|
+
artifact_digest: entry.artifactDigest
|
|
5714
|
+
})))).items.forEach((item, index) => {
|
|
5715
|
+
const hubSkill = hubUpdatable[index];
|
|
5716
|
+
if (!hubSkill || !item.has_update || !item.latest_version) return;
|
|
5717
|
+
updateCount++;
|
|
5718
|
+
hubInstallSources.set(hubSkill.name, replaceHubSourceVersion(hubSkill.entry.source, item.latest_version));
|
|
5719
|
+
});
|
|
5720
|
+
} catch {
|
|
5721
|
+
console.log(`${DIM$1}Failed to check project hub skills${RESET$1}`);
|
|
5722
|
+
checkFailCount += hubUpdatable.length;
|
|
5723
|
+
}
|
|
5724
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
5725
|
+
for (const skill of sourceUpdatable) {
|
|
5726
|
+
const source = getSourceRefKey(skill.entry.source, skill.entry.ref);
|
|
5727
|
+
const existing = bySource.get(source) || [];
|
|
5728
|
+
existing.push(skill);
|
|
5729
|
+
bySource.set(source, existing);
|
|
5730
|
+
}
|
|
5731
|
+
const localLock = await readLocalLock();
|
|
5732
|
+
const cliEntry = join(__dirname$1, "..", "bin", "cli.mjs");
|
|
5733
|
+
if (mode === "update" && !existsSync(cliEntry)) {
|
|
5734
|
+
console.log(`${DIM$1}CLI entrypoint not found at ${cliEntry}${RESET$1}`);
|
|
5735
|
+
return {
|
|
5736
|
+
successCount,
|
|
5737
|
+
failCount: updatable.length,
|
|
5738
|
+
checkFailCount,
|
|
5739
|
+
foundCount: projectSkills.length,
|
|
5740
|
+
updateCount
|
|
5741
|
+
};
|
|
5742
|
+
}
|
|
5743
|
+
for (const [, skillsForSource] of bySource) {
|
|
5744
|
+
const firstEntry = skillsForSource[0].entry;
|
|
5745
|
+
const sourceUrl = firstEntry.source;
|
|
5746
|
+
const ref = firstEntry.ref;
|
|
5747
|
+
const allLockedForSource = Object.entries(localLock.skills).filter(([, entry]) => hasSameSourceAndRef(entry, sourceUrl, ref)).map(([name]) => name);
|
|
5748
|
+
let tempDir = null;
|
|
5749
|
+
let deletedSkills = [];
|
|
5750
|
+
let discoveredPaths = [];
|
|
5751
|
+
let sourceCheckFailed = false;
|
|
5752
|
+
try {
|
|
5753
|
+
tempDir = await cloneRepo(sourceUrl, ref);
|
|
5754
|
+
discoveredPaths = (await discoverSkills(tempDir)).map((s) => {
|
|
5755
|
+
return join(relative(tempDir, s.path), "SKILL.md").split(sep).join("/");
|
|
5756
|
+
});
|
|
5757
|
+
deletedSkills = await checkAndPromptForDeletions(sourceUrl, allLockedForSource, localLock.skills, false, options, discoveredPaths, mode);
|
|
5758
|
+
} catch {
|
|
5759
|
+
console.log(`${DIM$1}Failed to check skills from ${sourceUrl}${RESET$1}`);
|
|
5760
|
+
checkFailCount += skillsForSource.length;
|
|
5761
|
+
sourceCheckFailed = true;
|
|
5762
|
+
} finally {
|
|
5763
|
+
if (tempDir && (mode === "update" || sourceCheckFailed)) await cleanupTempDir(tempDir);
|
|
5764
|
+
}
|
|
5765
|
+
if (sourceCheckFailed) continue;
|
|
5766
|
+
const remainingSkills = skillsForSource.filter((s) => !deletedSkills.includes(s.name));
|
|
5767
|
+
if (mode === "check") {
|
|
5768
|
+
if (tempDir) try {
|
|
5769
|
+
for (const skill of remainingSkills) {
|
|
5770
|
+
const skillPath = skill.entry.skillPath;
|
|
5771
|
+
if (!discoveredPaths.includes(skillPath)) continue;
|
|
5772
|
+
const latestHash = await computeSkillFolderHash(join(tempDir, dirname(skillPath)));
|
|
5773
|
+
if (latestHash && latestHash !== skill.entry.computedHash) updateCount++;
|
|
5774
|
+
}
|
|
5775
|
+
} finally {
|
|
5776
|
+
await cleanupTempDir(tempDir);
|
|
5777
|
+
}
|
|
5778
|
+
continue;
|
|
5779
|
+
}
|
|
5780
|
+
for (const skill of remainingSkills) {
|
|
5781
|
+
const safeName = sanitizeMetadata(skill.name);
|
|
5782
|
+
console.log(`${TEXT$1}Updating ${safeName}...${RESET$1}`);
|
|
5783
|
+
const installUrl = buildLocalUpdateSource(skill.entry);
|
|
5784
|
+
const subagentArgs = skill.entry.subagents?.length ? ["--subagent", ...skill.entry.subagents.map((s) => s === "" ? "root" : s)] : [];
|
|
5785
|
+
const regionArgs = region ? ["--region", region] : [];
|
|
5786
|
+
if (spawnSync(process.execPath, [
|
|
5787
|
+
cliEntry,
|
|
5788
|
+
...regionArgs,
|
|
5789
|
+
"add",
|
|
5790
|
+
installUrl,
|
|
5791
|
+
"--skill",
|
|
5792
|
+
skill.name,
|
|
5793
|
+
...subagentArgs,
|
|
5794
|
+
"-y"
|
|
5795
|
+
], {
|
|
5796
|
+
stdio: [
|
|
5797
|
+
"inherit",
|
|
5798
|
+
"pipe",
|
|
5799
|
+
"pipe"
|
|
5800
|
+
],
|
|
5801
|
+
encoding: "utf-8",
|
|
5802
|
+
shell: process.platform === "win32"
|
|
5803
|
+
}).status === 0) {
|
|
5804
|
+
successCount++;
|
|
5805
|
+
console.log(` ${TEXT$1}Updated ${safeName}${RESET$1}`);
|
|
5806
|
+
} else {
|
|
5807
|
+
failCount++;
|
|
5808
|
+
console.log(` ${DIM$1}Failed to update ${safeName}${RESET$1}`);
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
if (mode === "update") for (const skill of hubUpdatable) {
|
|
5813
|
+
const installUrl = hubInstallSources.get(skill.name);
|
|
5814
|
+
if (!installUrl) continue;
|
|
5815
|
+
const safeName = sanitizeMetadata(skill.name);
|
|
5816
|
+
console.log(`${TEXT$1}Updating ${safeName}...${RESET$1}`);
|
|
5817
|
+
const subagentArgs = skill.entry.subagents?.length ? ["--subagent", ...skill.entry.subagents.map((s) => s === "" ? "root" : s)] : [];
|
|
5818
|
+
const regionArgs = region ? ["--region", region] : [];
|
|
5819
|
+
if (spawnSync(process.execPath, [
|
|
5820
|
+
cliEntry,
|
|
5821
|
+
...regionArgs,
|
|
5822
|
+
"add",
|
|
5823
|
+
installUrl,
|
|
5824
|
+
"--skill",
|
|
5825
|
+
skill.name,
|
|
5826
|
+
...subagentArgs,
|
|
5827
|
+
"-y"
|
|
5828
|
+
], {
|
|
5829
|
+
stdio: [
|
|
5830
|
+
"inherit",
|
|
5831
|
+
"pipe",
|
|
5832
|
+
"pipe"
|
|
5833
|
+
],
|
|
5834
|
+
encoding: "utf-8",
|
|
5835
|
+
shell: process.platform === "win32"
|
|
5836
|
+
}).status === 0) {
|
|
5837
|
+
successCount++;
|
|
5838
|
+
console.log(` ${TEXT$1}Updated ${safeName}${RESET$1}`);
|
|
5839
|
+
} else {
|
|
5840
|
+
failCount++;
|
|
5841
|
+
console.log(` ${DIM$1}Failed to update ${safeName}${RESET$1}`);
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
if (mode === "check") if (updateCount > 0) console.log(`${TEXT$1}Found ${updateCount} project update(s)${RESET$1}`);
|
|
5845
|
+
else if (checkFailCount > 0) console.log(`${DIM$1}Failed to check ${checkFailCount} project skill(s)${RESET$1}`);
|
|
5846
|
+
else console.log(`${TEXT$1}All project skills are up to date${RESET$1}`);
|
|
5847
|
+
printLegacyProjectSkills(legacy);
|
|
5848
|
+
printSkippedProjectHubSkills(skippedHub);
|
|
5849
|
+
return {
|
|
5850
|
+
successCount,
|
|
5851
|
+
failCount,
|
|
5852
|
+
checkFailCount,
|
|
5853
|
+
foundCount: projectSkills.length,
|
|
5854
|
+
updateCount
|
|
5855
|
+
};
|
|
5856
|
+
}
|
|
5857
|
+
function printLegacyProjectSkills(legacy) {
|
|
5858
|
+
if (legacy.length === 0) return;
|
|
5859
|
+
console.log();
|
|
5860
|
+
console.log(`${DIM$1}${legacy.length} project skill(s) cannot be updated automatically (installed before skillPath tracking):${RESET$1}`);
|
|
5861
|
+
for (const skill of legacy) {
|
|
5862
|
+
const reinstall = formatSourceInput(skill.entry.source, skill.entry.ref);
|
|
5863
|
+
console.log(` ${TEXT$1}-${RESET$1} ${sanitizeMetadata(skill.name)}`);
|
|
5864
|
+
console.log(` ${DIM$1}To refresh: ${TEXT$1}npx safeskill add ${reinstall} -y${RESET$1}`);
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
async function runUpdate(args = [], region) {
|
|
5868
|
+
const options = parseUpdateOptions(args);
|
|
5869
|
+
const scope = await resolveUpdateScope(options);
|
|
5870
|
+
if (options.skills) console.log(`${TEXT$1}Updating ${options.skills.join(", ")}...${RESET$1}`);
|
|
5871
|
+
else console.log(`${TEXT$1}Checking for skill updates...${RESET$1}`);
|
|
5872
|
+
console.log();
|
|
5873
|
+
let totalSuccess = 0;
|
|
5874
|
+
let totalFail = 0;
|
|
5875
|
+
let totalCheckFail = 0;
|
|
5876
|
+
let totalFound = 0;
|
|
5877
|
+
if (scope === "global" || scope === "both") {
|
|
5878
|
+
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Global Skills${RESET$1}`);
|
|
5879
|
+
const { successCount, failCount, checkFailCount, checkedCount } = await updateGlobalSkills(options, region);
|
|
5880
|
+
totalSuccess += successCount;
|
|
5881
|
+
totalFail += failCount;
|
|
5882
|
+
totalCheckFail += checkFailCount;
|
|
5883
|
+
totalFound += checkedCount;
|
|
5884
|
+
if (scope === "both" && !options.skills) console.log();
|
|
5885
|
+
}
|
|
5886
|
+
if (scope === "project" || scope === "both") {
|
|
5887
|
+
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Project Skills${RESET$1}`);
|
|
5888
|
+
const { successCount, failCount, checkFailCount, foundCount } = await updateProjectSkills(options, region);
|
|
5889
|
+
totalSuccess += successCount;
|
|
5890
|
+
totalFail += failCount;
|
|
5891
|
+
totalCheckFail += checkFailCount;
|
|
5892
|
+
totalFound += foundCount;
|
|
5893
|
+
}
|
|
5894
|
+
if (options.skills && totalFound === 0) console.log(`${DIM$1}No installed skills found matching: ${options.skills.join(", ")}${RESET$1}`);
|
|
5895
|
+
console.log();
|
|
5896
|
+
if (totalSuccess > 0) console.log(`${TEXT$1}Updated ${totalSuccess} skill(s)${RESET$1}`);
|
|
5897
|
+
if (totalFail > 0) console.log(`${DIM$1}Failed to update ${totalFail} skill(s)${RESET$1}`);
|
|
5898
|
+
if (totalCheckFail > 0) console.log(`${DIM$1}Failed to check ${totalCheckFail} skill(s)${RESET$1}`);
|
|
5899
|
+
track({
|
|
5900
|
+
event: "update",
|
|
5901
|
+
scope,
|
|
5902
|
+
skillCount: String(totalSuccess + totalFail),
|
|
5903
|
+
successCount: String(totalSuccess),
|
|
5904
|
+
failCount: String(totalFail),
|
|
5905
|
+
checkFailCount: String(totalCheckFail)
|
|
5906
|
+
});
|
|
5907
|
+
console.log();
|
|
5908
|
+
}
|
|
5909
|
+
async function runCheck(args = []) {
|
|
5910
|
+
const options = parseUpdateOptions(args);
|
|
5911
|
+
const scope = await resolveUpdateScope(options);
|
|
5912
|
+
if (options.skills) console.log(`${TEXT$1}Checking ${options.skills.join(", ")}...${RESET$1}`);
|
|
5913
|
+
else console.log(`${TEXT$1}Checking for skill updates...${RESET$1}`);
|
|
5914
|
+
console.log();
|
|
5915
|
+
let totalFound = 0;
|
|
5916
|
+
let totalUpdates = 0;
|
|
5917
|
+
let totalFail = 0;
|
|
5918
|
+
if (scope === "global" || scope === "both") {
|
|
5919
|
+
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Global Skills${RESET$1}`);
|
|
5920
|
+
const { failCount, checkFailCount, checkedCount, updateCount } = await updateGlobalSkills(options, void 0, "check");
|
|
5921
|
+
totalFail += failCount + checkFailCount;
|
|
5922
|
+
totalFound += checkedCount;
|
|
5923
|
+
totalUpdates += updateCount;
|
|
5924
|
+
if (scope === "both" && !options.skills) console.log();
|
|
5925
|
+
}
|
|
5926
|
+
if (scope === "project" || scope === "both") {
|
|
5927
|
+
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Project Skills${RESET$1}`);
|
|
5928
|
+
const { failCount, checkFailCount, foundCount, updateCount } = await updateProjectSkills(options, "check");
|
|
5929
|
+
totalFail += failCount + checkFailCount;
|
|
5930
|
+
totalFound += foundCount;
|
|
5931
|
+
totalUpdates += updateCount;
|
|
5932
|
+
}
|
|
5933
|
+
if (options.skills && totalFound === 0) console.log(`${DIM$1}No installed skills found matching: ${options.skills.join(", ")}${RESET$1}`);
|
|
5934
|
+
if (totalFail > 0) {
|
|
5935
|
+
console.log();
|
|
5936
|
+
console.log(`${DIM$1}Failed to check ${totalFail} skill(s)${RESET$1}`);
|
|
5937
|
+
}
|
|
5938
|
+
track({
|
|
5939
|
+
event: "check",
|
|
5940
|
+
skillCount: String(totalFound),
|
|
5941
|
+
updatesAvailable: String(totalUpdates),
|
|
5942
|
+
failCount: String(totalFail)
|
|
5943
|
+
});
|
|
5944
|
+
console.log();
|
|
5945
|
+
}
|
|
5946
|
+
const BLOB_ALLOWED_OWNERS = [
|
|
5947
|
+
"vercel",
|
|
5948
|
+
"vercel-labs",
|
|
5949
|
+
"heygen-com"
|
|
5950
|
+
];
|
|
5951
|
+
const EXCLUDE_FILES = new Set(["metadata.json"]);
|
|
5952
|
+
const EXCLUDE_DIRS = new Set([
|
|
5953
|
+
".git",
|
|
5954
|
+
"__pycache__",
|
|
5955
|
+
"__pypackages__"
|
|
5956
|
+
]);
|
|
5957
|
+
const USE_AGENT_CONFIGS = {
|
|
5958
|
+
"claude-code": {
|
|
5959
|
+
command: "claude",
|
|
5960
|
+
args: []
|
|
5961
|
+
},
|
|
5962
|
+
codex: {
|
|
5963
|
+
command: "codex",
|
|
5964
|
+
args: []
|
|
5965
|
+
}
|
|
5966
|
+
};
|
|
5967
|
+
const SUPPORTED_USE_AGENTS = Object.keys(USE_AGENT_CONFIGS);
|
|
5968
|
+
function parseUseOptions(args) {
|
|
5969
|
+
const source = [];
|
|
5970
|
+
const options = {};
|
|
5971
|
+
const errors = [];
|
|
5972
|
+
for (let i = 0; i < args.length; i++) {
|
|
5973
|
+
const arg = args[i];
|
|
5974
|
+
if (!arg) continue;
|
|
5975
|
+
if (arg === "--help" || arg === "-h") options.help = true;
|
|
5976
|
+
else if (arg === "--full-depth") options.fullDepth = true;
|
|
5977
|
+
else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true;
|
|
5978
|
+
else if (arg === "--skill" || arg === "-s") {
|
|
5979
|
+
const value = args[i + 1];
|
|
5980
|
+
if (!value || value.startsWith("-")) errors.push(`${arg} requires a skill name`);
|
|
5981
|
+
else if (options.skill) {
|
|
5982
|
+
errors.push("Only one --skill value can be provided");
|
|
5983
|
+
i++;
|
|
5984
|
+
} else {
|
|
5985
|
+
options.skill = value;
|
|
5986
|
+
i++;
|
|
5987
|
+
}
|
|
5988
|
+
} else if (arg === "--agent" || arg === "-a") {
|
|
5989
|
+
options.agent = options.agent || [];
|
|
5990
|
+
i++;
|
|
5991
|
+
let nextArg = args[i];
|
|
5992
|
+
const startCount = options.agent.length;
|
|
5993
|
+
while (i < args.length && nextArg && !nextArg.startsWith("-")) {
|
|
5994
|
+
options.agent.push(nextArg);
|
|
5995
|
+
i++;
|
|
5996
|
+
nextArg = args[i];
|
|
5997
|
+
}
|
|
5998
|
+
if (options.agent.length === startCount) errors.push(`${arg} requires an agent name`);
|
|
5999
|
+
i--;
|
|
6000
|
+
} else if (arg.startsWith("-")) errors.push(`Unknown option: ${arg}`);
|
|
6001
|
+
else source.push(arg);
|
|
6002
|
+
}
|
|
6003
|
+
errors.push(...validateUseAgentOption(options.agent));
|
|
6004
|
+
return {
|
|
6005
|
+
source,
|
|
6006
|
+
options,
|
|
6007
|
+
errors
|
|
6008
|
+
};
|
|
6009
|
+
}
|
|
6010
|
+
function buildUsePrompt(input) {
|
|
6011
|
+
const sections = [
|
|
6012
|
+
"You are being given a Skill to execute for the user's next request.",
|
|
6013
|
+
"Use the following SKILL.md as your instructions:",
|
|
6014
|
+
`<SKILL.md>\n${input.skillMd}\n</SKILL.md>`
|
|
6015
|
+
];
|
|
6016
|
+
if (input.hasSupportingFiles && input.supportDir) sections.push(`Supporting files for this skill were downloaded to:\n${input.supportDir}\n\nWhen the SKILL.md references relative paths, read them from that directory.`);
|
|
6017
|
+
return sections.join("\n\n") + "\n";
|
|
6018
|
+
}
|
|
6019
|
+
async function materializeUseSkill(skill) {
|
|
6020
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "safeskill-use-"));
|
|
6021
|
+
const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
|
|
6022
|
+
if (!isPathSafe(tempRoot, skillDir)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
6023
|
+
await mkdir(skillDir, { recursive: true });
|
|
6024
|
+
if (skill.kind === "blob") await writeSnapshotFiles(skillDir, skill.files);
|
|
6025
|
+
else if (skill.kind === "well-known") await writeMapFiles(skillDir, skill.files);
|
|
6026
|
+
else await copySkillDirectory(skill.path, skillDir);
|
|
6027
|
+
return {
|
|
6028
|
+
tempRoot,
|
|
6029
|
+
skillDir,
|
|
6030
|
+
skillMd: skill.rawContent ?? await readFile(join(skillDir, "SKILL.md"), "utf-8"),
|
|
6031
|
+
hasSupportingFiles: await containsSupportingFiles(skillDir, skillDir)
|
|
6032
|
+
};
|
|
6033
|
+
}
|
|
6034
|
+
async function runUse(sourceArgs, options = {}, parseErrors = []) {
|
|
6035
|
+
let cloneTempDir = null;
|
|
6036
|
+
try {
|
|
6037
|
+
if (options.help) {
|
|
6038
|
+
console.log(getUseHelp());
|
|
6039
|
+
return;
|
|
6040
|
+
}
|
|
6041
|
+
if (parseErrors.length > 0) fail(parseErrors.join("\n"));
|
|
6042
|
+
if (sourceArgs.length === 0) fail(`Missing required argument: source\n\n${getUseHelp()}`);
|
|
6043
|
+
if (sourceArgs.length > 1) fail(`Expected one source, received ${sourceArgs.length}: ${sourceArgs.join(", ")}`);
|
|
6044
|
+
const useAgent = options.agent?.[0];
|
|
6045
|
+
if (useAgent && !USE_AGENT_CONFIGS[useAgent]) fail(formatUnsupportedAgentError(useAgent));
|
|
6046
|
+
const source = sourceArgs[0];
|
|
6047
|
+
const parsed = parseSource(source);
|
|
6048
|
+
if (getOwnerRepo(parsed)?.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) fail([
|
|
6049
|
+
"OpenClaw skills are unverified community submissions.",
|
|
6050
|
+
"Skills run with full agent permissions and could be malicious.",
|
|
6051
|
+
`If you understand the risks, re-run with: safeskill use ${source} --dangerously-accept-openclaw-risks`
|
|
6052
|
+
].join("\n"));
|
|
6053
|
+
const selector = resolveSelector(parsed.skillFilter, options.skill);
|
|
6054
|
+
const includeInternal = selector !== void 0;
|
|
6055
|
+
let selectedSkill;
|
|
6056
|
+
if (parsed.type === "well-known") selectedSkill = selectWellKnownSkill(await wellKnownProvider.fetchAllSkills(parsed.url), selector, source);
|
|
6057
|
+
else {
|
|
6058
|
+
let skills;
|
|
6059
|
+
let blobResult = null;
|
|
6060
|
+
if (parsed.type === "local") {
|
|
6061
|
+
if (!existsSync(parsed.localPath)) fail(`Local path does not exist: ${parsed.localPath}`);
|
|
6062
|
+
skills = await discoverSkills(parsed.localPath, parsed.subpath, {
|
|
6063
|
+
includeInternal,
|
|
6064
|
+
fullDepth: options.fullDepth
|
|
6065
|
+
});
|
|
6066
|
+
} else if (parsed.type === "hub") {
|
|
6067
|
+
const downloaded = await downloadHubSource(source);
|
|
6068
|
+
cloneTempDir = downloaded.tempDir;
|
|
6069
|
+
skills = await discoverSkills(downloaded.tempDir, parsed.subpath, {
|
|
6070
|
+
includeInternal,
|
|
6071
|
+
fullDepth: options.fullDepth
|
|
6072
|
+
});
|
|
6073
|
+
if (skills.length === 0 && !parsed.subpath && downloaded.resolved.skill) {
|
|
6074
|
+
const fallbackSkill = await parseSkillMd(join(downloaded.tempDir, "SKILL.md"), {
|
|
6075
|
+
includeInternal,
|
|
6076
|
+
fallbackName: downloaded.resolved.skill.display_name || downloaded.resolved.skill.name,
|
|
6077
|
+
fallbackDescription: downloaded.resolved.skill.summary || downloaded.resolved.skill.name,
|
|
6078
|
+
fallbackSlug: downloaded.resolved.skill.name,
|
|
6079
|
+
fallbackInstallName: downloaded.resolved.skill.name
|
|
6080
|
+
});
|
|
6081
|
+
if (fallbackSkill) {
|
|
6082
|
+
fallbackSkill.rawContent = withFallbackFrontmatter(fallbackSkill);
|
|
6083
|
+
skills = [fallbackSkill];
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
} else if (parsed.type === "github" && !options.fullDepth) {
|
|
6087
|
+
const ownerRepo = getOwnerRepo(parsed);
|
|
6088
|
+
const owner = ownerRepo?.split("/")[0]?.toLowerCase();
|
|
6089
|
+
if (ownerRepo && owner && BLOB_ALLOWED_OWNERS.includes(owner)) blobResult = await tryBlobInstall(ownerRepo, {
|
|
6090
|
+
subpath: parsed.subpath,
|
|
6091
|
+
skillFilter: selector,
|
|
6092
|
+
ref: parsed.ref,
|
|
6093
|
+
getToken: getGitHubToken,
|
|
6094
|
+
includeInternal
|
|
6095
|
+
});
|
|
6096
|
+
if (blobResult) skills = blobResult.skills;
|
|
6097
|
+
else {
|
|
6098
|
+
cloneTempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
6099
|
+
skills = await discoverSkills(cloneTempDir, parsed.subpath, {
|
|
6100
|
+
includeInternal,
|
|
6101
|
+
fullDepth: options.fullDepth
|
|
6102
|
+
});
|
|
6103
|
+
}
|
|
6104
|
+
} else {
|
|
6105
|
+
cloneTempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
6106
|
+
skills = await discoverSkills(cloneTempDir, parsed.subpath, {
|
|
6107
|
+
includeInternal,
|
|
6108
|
+
fullDepth: options.fullDepth
|
|
6109
|
+
});
|
|
6110
|
+
}
|
|
6111
|
+
const selected = selectSkill(skills, selector, source);
|
|
6112
|
+
if (blobResult && isBlobSkill(selected)) selectedSkill = {
|
|
6113
|
+
kind: "blob",
|
|
6114
|
+
name: selected.name,
|
|
6115
|
+
directoryName: getDirectoryName(selected),
|
|
6116
|
+
rawContent: selected.rawContent ?? getSkillMdFromSnapshot(selected.files),
|
|
6117
|
+
files: selected.files
|
|
6118
|
+
};
|
|
6119
|
+
else selectedSkill = {
|
|
6120
|
+
kind: "disk",
|
|
6121
|
+
name: selected.name,
|
|
6122
|
+
directoryName: getDirectoryName(selected),
|
|
6123
|
+
rawContent: selected.rawContent,
|
|
6124
|
+
path: selected.path
|
|
6125
|
+
};
|
|
6126
|
+
}
|
|
6127
|
+
const materialized = await materializeUseSkill(selectedSkill);
|
|
6128
|
+
await cleanupClone(cloneTempDir);
|
|
6129
|
+
cloneTempDir = null;
|
|
6130
|
+
const prompt = buildUsePrompt({
|
|
6131
|
+
skillMd: materialized.skillMd,
|
|
6132
|
+
supportDir: materialized.skillDir,
|
|
6133
|
+
hasSupportingFiles: materialized.hasSupportingFiles
|
|
6134
|
+
});
|
|
6135
|
+
if (useAgent) {
|
|
6136
|
+
const exitCode = await launchAgentInteractively(useAgent, prompt);
|
|
6137
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
6138
|
+
return;
|
|
6139
|
+
}
|
|
6140
|
+
process.stdout.write(prompt);
|
|
6141
|
+
} catch (error) {
|
|
6142
|
+
await cleanupClone(cloneTempDir);
|
|
6143
|
+
if (error instanceof GitCloneError) fail(error.message);
|
|
6144
|
+
if (error instanceof UseCommandError) fail(error.message);
|
|
6145
|
+
fail(error instanceof Error ? error.message : "Unknown error");
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
async function launchAgentInteractively(agent, prompt, spawnImpl = spawnAgent) {
|
|
6149
|
+
const config = USE_AGENT_CONFIGS[agent];
|
|
6150
|
+
if (!config) throw new UseCommandError(formatUnsupportedAgentError(agent));
|
|
6151
|
+
return new Promise((resolve, reject) => {
|
|
6152
|
+
const child = spawnImpl(config.command, [...config.args, prompt], { stdio: "inherit" });
|
|
6153
|
+
let settled = false;
|
|
6154
|
+
child.on("error", (error) => {
|
|
6155
|
+
if (settled) return;
|
|
6156
|
+
settled = true;
|
|
6157
|
+
if (error.code === "ENOENT") {
|
|
6158
|
+
reject(new UseCommandError(`Could not launch ${agents[agent].displayName}: command not found: ${config.command}`));
|
|
6159
|
+
return;
|
|
6160
|
+
}
|
|
6161
|
+
reject(error);
|
|
6162
|
+
});
|
|
6163
|
+
child.on("close", (code) => {
|
|
6164
|
+
if (settled) return;
|
|
6165
|
+
settled = true;
|
|
6166
|
+
resolve(code ?? 1);
|
|
6167
|
+
});
|
|
6168
|
+
});
|
|
6169
|
+
}
|
|
6170
|
+
function spawnAgent(command, args) {
|
|
6171
|
+
return spawn(command, args, { stdio: "inherit" });
|
|
6172
|
+
}
|
|
6173
|
+
function getUseHelp() {
|
|
6174
|
+
return `Usage: safeskill use <source>[@<skill>] [options]
|
|
6175
|
+
|
|
6176
|
+
Generate a prompt for using one skill without installing it.
|
|
6177
|
+
|
|
6178
|
+
Options:
|
|
6179
|
+
-s, --skill <skill> Select the skill to use
|
|
6180
|
+
-a, --agent <agent> Start one supported agent interactively (${SUPPORTED_USE_AGENTS.join(", ")})
|
|
6181
|
+
--full-depth Search nested directories like safeskill add --full-depth
|
|
6182
|
+
--dangerously-accept-openclaw-risks
|
|
6183
|
+
Allow unverified OpenClaw community skills
|
|
6184
|
+
-h, --help Show this help message
|
|
6185
|
+
|
|
6186
|
+
Examples:
|
|
6187
|
+
safeskill use vercel-labs/agent-skills@web-design-guidelines | claude
|
|
6188
|
+
safeskill use vercel-labs/agent-skills --skill web-design-guidelines --agent claude-code
|
|
6189
|
+
safeskill use vercel-labs/agent-skills@web-design-guidelines --agent codex`;
|
|
6190
|
+
}
|
|
6191
|
+
function resolveSelector(sourceSelector, optionSelector) {
|
|
6192
|
+
if (sourceSelector && optionSelector) {
|
|
6193
|
+
if (sourceSelector.toLowerCase() !== optionSelector.toLowerCase()) throw new UseCommandError(`Conflicting skill selectors: source selects "${sourceSelector}" but --skill selects "${optionSelector}". Provide one selector.`);
|
|
6194
|
+
return optionSelector;
|
|
6195
|
+
}
|
|
6196
|
+
return optionSelector ?? sourceSelector;
|
|
6197
|
+
}
|
|
6198
|
+
function selectSkill(skills, selector, source) {
|
|
6199
|
+
if (skills.length === 0) throw new UseCommandError("No valid skills found. Skills require a SKILL.md with name and description.");
|
|
6200
|
+
if (!selector) {
|
|
6201
|
+
if (skills.length === 1) return skills[0];
|
|
6202
|
+
throw new UseCommandError(formatMultipleSkillsError(source, skills.map(getSkillDisplayName)));
|
|
6203
|
+
}
|
|
6204
|
+
const selected = filterSkills(skills, [selector]);
|
|
6205
|
+
if (selected.length === 0) throw new UseCommandError(formatNoMatchError(selector, skills.map(getSkillDisplayName)));
|
|
6206
|
+
if (selected.length > 1) throw new UseCommandError(`Skill selector "${selector}" matched multiple skills.`);
|
|
6207
|
+
return selected[0];
|
|
6208
|
+
}
|
|
6209
|
+
function selectWellKnownSkill(skills, selector, source) {
|
|
6210
|
+
if (skills.length === 0) throw new UseCommandError("No skills found at this URL. Make sure the server has a /.well-known/agent-skills/index.json or /.well-known/skills/index.json file.");
|
|
6211
|
+
let selected;
|
|
6212
|
+
if (!selector) {
|
|
6213
|
+
if (skills.length !== 1) throw new UseCommandError(formatMultipleSkillsError(source, skills.map((s) => s.installName)));
|
|
6214
|
+
selected = skills;
|
|
6215
|
+
} else {
|
|
6216
|
+
selected = skills.filter((skill) => skill.installName.toLowerCase() === selector.toLowerCase() || skill.name.toLowerCase() === selector.toLowerCase());
|
|
6217
|
+
if (selected.length === 0) throw new UseCommandError(formatNoMatchError(selector, skills.map((s) => s.installName)));
|
|
6218
|
+
if (selected.length > 1) throw new UseCommandError(`Skill selector "${selector}" matched multiple skills.`);
|
|
6219
|
+
}
|
|
6220
|
+
const skill = selected[0];
|
|
6221
|
+
return {
|
|
6222
|
+
kind: "well-known",
|
|
6223
|
+
name: skill.name,
|
|
6224
|
+
directoryName: skill.installName,
|
|
6225
|
+
rawContent: skill.content,
|
|
6226
|
+
files: skill.files
|
|
6227
|
+
};
|
|
6228
|
+
}
|
|
6229
|
+
function formatMultipleSkillsError(source, names) {
|
|
6230
|
+
return [
|
|
6231
|
+
"This source contains multiple skills. Specify exactly one skill:",
|
|
6232
|
+
...names.map((name) => ` - ${name}`),
|
|
6233
|
+
"",
|
|
6234
|
+
`Examples:\n safeskill use ${source}@${names[0] ?? "<skill>"}\n safeskill use ${source} --skill ${names[0] ?? "<skill>"}`
|
|
6235
|
+
].join("\n");
|
|
6236
|
+
}
|
|
6237
|
+
function formatNoMatchError(selector, names) {
|
|
6238
|
+
return [
|
|
6239
|
+
`No matching skill found for: ${selector}`,
|
|
6240
|
+
"Available skills:",
|
|
6241
|
+
...names.map((name) => ` - ${name}`)
|
|
6242
|
+
].join("\n");
|
|
6243
|
+
}
|
|
6244
|
+
function validateUseAgentOption(agentValues) {
|
|
6245
|
+
if (!agentValues || agentValues.length === 0) return [];
|
|
6246
|
+
const errors = [];
|
|
6247
|
+
const validAgents = Object.keys(agents);
|
|
6248
|
+
const invalidAgents = agentValues.filter((agent) => agent !== "*" && !validAgents.includes(agent));
|
|
6249
|
+
if (agentValues.includes("*")) errors.push("safeskill use --agent does not support '*'; specify exactly one agent.");
|
|
6250
|
+
if (agentValues.length > 1) errors.push("safeskill use --agent accepts exactly one agent.");
|
|
6251
|
+
if (invalidAgents.length > 0) errors.push(`Invalid agents: ${invalidAgents.join(", ")}\nValid agents: ${validAgents.join(", ")}`);
|
|
6252
|
+
return errors;
|
|
6253
|
+
}
|
|
6254
|
+
function formatUnsupportedAgentError(agent) {
|
|
6255
|
+
return [`Running ${agents[agent].displayName} is not supported yet.`, `Supported agents for safeskill use --agent: ${SUPPORTED_USE_AGENTS.join(", ")}`].join("\n");
|
|
6256
|
+
}
|
|
6257
|
+
async function writeSnapshotFiles(targetDir, files) {
|
|
6258
|
+
for (const file of files) await writeSafeFile(targetDir, file.path, file.contents);
|
|
6259
|
+
}
|
|
6260
|
+
async function writeMapFiles(targetDir, files) {
|
|
6261
|
+
for (const [path, contents] of files) await writeSafeFile(targetDir, path, contents);
|
|
6262
|
+
}
|
|
6263
|
+
async function writeSafeFile(targetDir, filePath, contents) {
|
|
6264
|
+
const fullPath = join(targetDir, filePath);
|
|
6265
|
+
if (!isPathSafe(targetDir, fullPath)) return;
|
|
6266
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
6267
|
+
if (typeof contents === "string") await writeFile(fullPath, contents, "utf-8");
|
|
6268
|
+
else await writeFile(fullPath, contents);
|
|
6269
|
+
}
|
|
6270
|
+
async function copySkillDirectory(src, dest) {
|
|
6271
|
+
await mkdir(dest, { recursive: true });
|
|
6272
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
6273
|
+
await Promise.all(entries.filter((entry) => !isExcluded(entry.name, entry.isDirectory())).map(async (entry) => {
|
|
6274
|
+
const srcPath = join(src, entry.name);
|
|
6275
|
+
const destPath = join(dest, entry.name);
|
|
6276
|
+
if (!isPathSafe(dest, destPath)) return;
|
|
6277
|
+
if (entry.isDirectory()) {
|
|
6278
|
+
await copySkillDirectory(srcPath, destPath);
|
|
6279
|
+
return;
|
|
6280
|
+
}
|
|
6281
|
+
try {
|
|
6282
|
+
await cp(srcPath, destPath, {
|
|
6283
|
+
dereference: true,
|
|
6284
|
+
recursive: true
|
|
6285
|
+
});
|
|
6286
|
+
} catch (err) {
|
|
6287
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT" && entry.isSymbolicLink()) {
|
|
6288
|
+
console.error(`Skipping broken symlink: ${srcPath}`);
|
|
6289
|
+
return;
|
|
6290
|
+
}
|
|
6291
|
+
throw err;
|
|
6292
|
+
}
|
|
6293
|
+
}));
|
|
6294
|
+
}
|
|
6295
|
+
async function containsSupportingFiles(rootDir, currentDir) {
|
|
6296
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
6297
|
+
for (const entry of entries) {
|
|
6298
|
+
const entryPath = join(currentDir, entry.name);
|
|
6299
|
+
const relPath = relative(rootDir, entryPath).split(sep).join("/");
|
|
6300
|
+
if (entry.isDirectory()) {
|
|
6301
|
+
if (await containsSupportingFiles(rootDir, entryPath)) return true;
|
|
6302
|
+
} else if (relPath.toLowerCase() !== "skill.md") return true;
|
|
6303
|
+
}
|
|
6304
|
+
return false;
|
|
6305
|
+
}
|
|
6306
|
+
function isBlobSkill(skill) {
|
|
6307
|
+
return Array.isArray(skill.files);
|
|
6308
|
+
}
|
|
6309
|
+
function getSkillMdFromSnapshot(files) {
|
|
6310
|
+
return files.find((file) => file.path.toLowerCase() === "skill.md")?.contents ?? "";
|
|
6311
|
+
}
|
|
6312
|
+
function getDirectoryName(skill) {
|
|
6313
|
+
return skill.installName || skill.name;
|
|
6314
|
+
}
|
|
6315
|
+
function withFallbackFrontmatter(skill) {
|
|
6316
|
+
if (!skill.rawContent || skill.rawContent.trimStart().startsWith("---")) return skill.rawContent;
|
|
6317
|
+
return [
|
|
6318
|
+
"---",
|
|
6319
|
+
`name: ${skill.name}`,
|
|
6320
|
+
`description: ${skill.description}`,
|
|
6321
|
+
"---",
|
|
6322
|
+
"",
|
|
6323
|
+
skill.rawContent
|
|
6324
|
+
].join("\n");
|
|
6325
|
+
}
|
|
6326
|
+
function isExcluded(name, isDirectory) {
|
|
6327
|
+
return EXCLUDE_FILES.has(name) || isDirectory && EXCLUDE_DIRS.has(name);
|
|
6328
|
+
}
|
|
6329
|
+
function isPathSafe(basePath, targetPath) {
|
|
6330
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
6331
|
+
const normalizedTarget = normalize(resolve(targetPath));
|
|
6332
|
+
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
6333
|
+
}
|
|
6334
|
+
async function cleanupClone(tempDir) {
|
|
6335
|
+
if (tempDir) await cleanupTempDir(tempDir).catch(() => {});
|
|
6336
|
+
}
|
|
6337
|
+
function fail(message) {
|
|
6338
|
+
console.error(message);
|
|
6339
|
+
process.exit(1);
|
|
6340
|
+
}
|
|
6341
|
+
var UseCommandError = class extends Error {};
|
|
6342
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6343
|
+
function getVersion() {
|
|
6344
|
+
try {
|
|
6345
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
6346
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
6347
|
+
} catch {
|
|
6348
|
+
return "0.0.0";
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
const VERSION = getVersion();
|
|
6352
|
+
initTelemetry(VERSION);
|
|
6353
|
+
const RESET = "\x1B[0m";
|
|
6354
|
+
const BOLD = "\x1B[1m";
|
|
6355
|
+
const DIM = "\x1B[38;5;102m";
|
|
6356
|
+
const TEXT = "\x1B[38;5;145m";
|
|
6357
|
+
const LOGO_HEIGHT = 7;
|
|
6358
|
+
const LOGO_GLYPHS = {
|
|
6359
|
+
s: [
|
|
6360
|
+
" █████ ",
|
|
6361
|
+
"█ ",
|
|
6362
|
+
"█ ",
|
|
6363
|
+
" ████ ",
|
|
6364
|
+
" █ ",
|
|
6365
|
+
"█ █",
|
|
6366
|
+
" █████ "
|
|
6367
|
+
],
|
|
6368
|
+
a: [
|
|
6369
|
+
" ███ ",
|
|
6370
|
+
" █ █ ",
|
|
6371
|
+
" █ █ ",
|
|
6372
|
+
" █████ ",
|
|
6373
|
+
" █ █ ",
|
|
6374
|
+
" █ █ ",
|
|
6375
|
+
" █ █ "
|
|
6376
|
+
],
|
|
6377
|
+
f: [
|
|
6378
|
+
"██████ ",
|
|
6379
|
+
"█ ",
|
|
6380
|
+
"█ ",
|
|
6381
|
+
"█████ ",
|
|
6382
|
+
"█ ",
|
|
6383
|
+
"█ ",
|
|
6384
|
+
"█ "
|
|
6385
|
+
],
|
|
6386
|
+
e: [
|
|
6387
|
+
"██████ ",
|
|
6388
|
+
"█ ",
|
|
6389
|
+
"█ ",
|
|
6390
|
+
"█████ ",
|
|
6391
|
+
"█ ",
|
|
6392
|
+
"█ ",
|
|
6393
|
+
"██████ "
|
|
6394
|
+
],
|
|
6395
|
+
k: [
|
|
6396
|
+
"█ █ ",
|
|
6397
|
+
"█ █ ",
|
|
6398
|
+
"█ █ ",
|
|
6399
|
+
"███ ",
|
|
6400
|
+
"█ █ ",
|
|
6401
|
+
"█ █ ",
|
|
6402
|
+
"█ █ "
|
|
6403
|
+
],
|
|
6404
|
+
i: [
|
|
6405
|
+
"█████ ",
|
|
6406
|
+
" █ ",
|
|
6407
|
+
" █ ",
|
|
6408
|
+
" █ ",
|
|
6409
|
+
" █ ",
|
|
6410
|
+
" █ ",
|
|
6411
|
+
"█████ "
|
|
6412
|
+
],
|
|
6413
|
+
l: [
|
|
6414
|
+
"█ ",
|
|
6415
|
+
"█ ",
|
|
6416
|
+
"█ ",
|
|
6417
|
+
"█ ",
|
|
6418
|
+
"█ ",
|
|
6419
|
+
"█ ",
|
|
6420
|
+
"██████ "
|
|
6421
|
+
]
|
|
6422
|
+
};
|
|
6423
|
+
function buildLogoLines() {
|
|
6424
|
+
const words = ["Safe", "Skill"];
|
|
6425
|
+
const lines = Array.from({ length: LOGO_HEIGHT }, () => "");
|
|
6426
|
+
for (const word of words) {
|
|
6427
|
+
for (const character of word) {
|
|
6428
|
+
const glyph = LOGO_GLYPHS[character.toLowerCase()];
|
|
6429
|
+
if (!glyph) continue;
|
|
6430
|
+
for (let i = 0; i < LOGO_HEIGHT; i++) lines[i] += `${glyph[i] ?? ""} `;
|
|
6431
|
+
}
|
|
6432
|
+
for (let i = 0; i < LOGO_HEIGHT; i++) lines[i] += " ";
|
|
6433
|
+
}
|
|
6434
|
+
return lines.map((line) => line.replace(/\s+$/, ""));
|
|
6435
|
+
}
|
|
6436
|
+
const GRAYS = [
|
|
6437
|
+
"\x1B[38;5;250m",
|
|
6438
|
+
"\x1B[38;5;248m",
|
|
6439
|
+
"\x1B[38;5;245m",
|
|
6440
|
+
"\x1B[38;5;243m",
|
|
6441
|
+
"\x1B[38;5;240m",
|
|
6442
|
+
"\x1B[38;5;238m",
|
|
6443
|
+
"\x1B[38;5;236m"
|
|
6444
|
+
];
|
|
6445
|
+
function showLogo() {
|
|
6446
|
+
console.log();
|
|
6447
|
+
buildLogoLines().forEach((line, i) => {
|
|
6448
|
+
console.log(`${GRAYS[i]}${line}${RESET}`);
|
|
6449
|
+
});
|
|
6450
|
+
}
|
|
6451
|
+
function showBanner() {
|
|
6452
|
+
showLogo();
|
|
6453
|
+
console.log();
|
|
6454
|
+
console.log(`${DIM}SafeSkill CLI for the open agent skills ecosystem${RESET}`);
|
|
6455
|
+
console.log();
|
|
6456
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill add ${DIM}<package>${RESET} ${DIM}Add a new skill${RESET}`);
|
|
6457
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill remove${RESET} ${DIM}Remove installed skills${RESET}`);
|
|
6458
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill list${RESET} ${DIM}List installed skills${RESET}`);
|
|
6459
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill find ${DIM}[query]${RESET} ${DIM}Search for skills${RESET}`);
|
|
6460
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill use ${DIM}<package>${RESET} ${DIM}Use a skill without installing${RESET}`);
|
|
6461
|
+
console.log();
|
|
6462
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill check${RESET} ${DIM}Check for updates${RESET}`);
|
|
6463
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill update${RESET} ${DIM}Update all skills${RESET}`);
|
|
6464
|
+
console.log();
|
|
6465
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
|
|
6466
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
|
|
6467
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
|
|
6468
|
+
console.log();
|
|
6469
|
+
console.log(`${DIM}try:${RESET} npx safeskill add example-org/agent-skills`);
|
|
4369
6470
|
console.log();
|
|
4370
6471
|
console.log(`Discover more skills at ${TEXT}https://skills.sh/${RESET}`);
|
|
4371
6472
|
console.log();
|
|
@@ -4382,6 +6483,7 @@ ${BOLD}Manage Skills:${RESET}
|
|
|
4382
6483
|
remove [skills] Remove installed skills
|
|
4383
6484
|
list, ls List installed skills
|
|
4384
6485
|
find [query] Search for skills interactively
|
|
6486
|
+
use <package> Generate a one-shot prompt for a skill
|
|
4385
6487
|
|
|
4386
6488
|
${BOLD}Updates:${RESET}
|
|
4387
6489
|
check Check for available skill updates
|
|
@@ -4439,6 +6541,8 @@ ${BOLD}Examples:${RESET}
|
|
|
4439
6541
|
${DIM}$${RESET} safeskill ls --json ${DIM}# JSON output${RESET}
|
|
4440
6542
|
${DIM}$${RESET} safeskill find ${DIM}# interactive search${RESET}
|
|
4441
6543
|
${DIM}$${RESET} safeskill find typescript ${DIM}# search by keyword${RESET}
|
|
6544
|
+
${DIM}$${RESET} safeskill use example-org/agent-skills@web-design
|
|
6545
|
+
${DIM}$${RESET} safeskill use safeskill://official/acme/code-review@1.2.0
|
|
4442
6546
|
${DIM}$${RESET} safeskill check
|
|
4443
6547
|
${DIM}$${RESET} safeskill update
|
|
4444
6548
|
${DIM}$${RESET} safeskill experimental_install ${DIM}# restore from skills-lock.json${RESET}
|
|
@@ -4526,327 +6630,6 @@ Describe when this skill should be used.
|
|
|
4526
6630
|
console.log(`Browse existing skills for inspiration at ${TEXT}https://skills.sh/${RESET}`);
|
|
4527
6631
|
console.log();
|
|
4528
6632
|
}
|
|
4529
|
-
const AGENTS_DIR = ".agents";
|
|
4530
|
-
const LOCK_FILE = ".skill-lock.json";
|
|
4531
|
-
const CURRENT_LOCK_VERSION = 4;
|
|
4532
|
-
function getSkillLockPath() {
|
|
4533
|
-
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
4534
|
-
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
|
|
4535
|
-
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
4536
|
-
}
|
|
4537
|
-
function readSkillLock() {
|
|
4538
|
-
const lockPath = getSkillLockPath();
|
|
4539
|
-
try {
|
|
4540
|
-
const content = readFileSync(lockPath, "utf-8");
|
|
4541
|
-
const parsed = JSON.parse(content);
|
|
4542
|
-
if (typeof parsed.version !== "number" || !parsed.skills) return {
|
|
4543
|
-
version: CURRENT_LOCK_VERSION,
|
|
4544
|
-
skills: {}
|
|
4545
|
-
};
|
|
4546
|
-
if (parsed.version < CURRENT_LOCK_VERSION) return {
|
|
4547
|
-
version: CURRENT_LOCK_VERSION,
|
|
4548
|
-
skills: {}
|
|
4549
|
-
};
|
|
4550
|
-
return parsed;
|
|
4551
|
-
} catch {
|
|
4552
|
-
return {
|
|
4553
|
-
version: CURRENT_LOCK_VERSION,
|
|
4554
|
-
skills: {}
|
|
4555
|
-
};
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
function getSkipReason(entry) {
|
|
4559
|
-
if (entry.sourceType === "local") return "Local path";
|
|
4560
|
-
if (entry.sourceType === "hub") {
|
|
4561
|
-
if (!entry.resolvedVersion) return "No hub version recorded";
|
|
4562
|
-
if (!entry.artifactDigest) return "No hub artifact metadata";
|
|
4563
|
-
return "No version tracking";
|
|
4564
|
-
}
|
|
4565
|
-
if (entry.sourceType === "git") return "Git URL (hash tracking not supported)";
|
|
4566
|
-
if (!entry.skillFolderHash) return "No version hash available";
|
|
4567
|
-
if (!entry.skillPath) return "No skill path recorded";
|
|
4568
|
-
return "No version tracking";
|
|
4569
|
-
}
|
|
4570
|
-
function replaceHubSourceVersion(source, version) {
|
|
4571
|
-
const trimmedSource = source.trim();
|
|
4572
|
-
if (!trimmedSource) return source;
|
|
4573
|
-
return `${trimmedSource.replace(/@[^/@]+$/, "")}@${version}`;
|
|
4574
|
-
}
|
|
4575
|
-
function printSkippedSkills(skipped) {
|
|
4576
|
-
if (skipped.length === 0) return;
|
|
4577
|
-
console.log();
|
|
4578
|
-
console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`);
|
|
4579
|
-
for (const skill of skipped) {
|
|
4580
|
-
console.log(` ${TEXT}•${RESET} ${skill.name} ${DIM}(${skill.reason})${RESET}`);
|
|
4581
|
-
console.log(` ${DIM}To update: ${TEXT}npx safeskill add ${skill.sourceUrl} -g -y${RESET}`);
|
|
4582
|
-
}
|
|
4583
|
-
}
|
|
4584
|
-
async function runCheck(args = []) {
|
|
4585
|
-
console.log(`${TEXT}Checking for skill updates...${RESET}`);
|
|
4586
|
-
console.log();
|
|
4587
|
-
const lock = readSkillLock();
|
|
4588
|
-
const skillNames = Object.keys(lock.skills);
|
|
4589
|
-
if (skillNames.length === 0) {
|
|
4590
|
-
console.log(`${DIM}No skills tracked in lock file.${RESET}`);
|
|
4591
|
-
console.log(`${DIM}Install skills with${RESET} ${TEXT}npx safeskill add <package>${RESET}`);
|
|
4592
|
-
return;
|
|
4593
|
-
}
|
|
4594
|
-
const token = getGitHubToken();
|
|
4595
|
-
const skillsBySource = /* @__PURE__ */ new Map();
|
|
4596
|
-
const hubEntries = [];
|
|
4597
|
-
const skipped = [];
|
|
4598
|
-
for (const skillName of skillNames) {
|
|
4599
|
-
const entry = lock.skills[skillName];
|
|
4600
|
-
if (!entry) continue;
|
|
4601
|
-
if (entry.sourceType === "hub") {
|
|
4602
|
-
if (!entry.resolvedVersion || !entry.artifactDigest) {
|
|
4603
|
-
skipped.push({
|
|
4604
|
-
name: skillName,
|
|
4605
|
-
reason: getSkipReason(entry),
|
|
4606
|
-
sourceUrl: entry.sourceUrl
|
|
4607
|
-
});
|
|
4608
|
-
continue;
|
|
4609
|
-
}
|
|
4610
|
-
hubEntries.push({
|
|
4611
|
-
name: skillName,
|
|
4612
|
-
entry
|
|
4613
|
-
});
|
|
4614
|
-
continue;
|
|
4615
|
-
}
|
|
4616
|
-
if (!entry.skillFolderHash || !entry.skillPath) {
|
|
4617
|
-
skipped.push({
|
|
4618
|
-
name: skillName,
|
|
4619
|
-
reason: getSkipReason(entry),
|
|
4620
|
-
sourceUrl: entry.sourceUrl
|
|
4621
|
-
});
|
|
4622
|
-
continue;
|
|
4623
|
-
}
|
|
4624
|
-
const existing = skillsBySource.get(entry.source) || [];
|
|
4625
|
-
existing.push({
|
|
4626
|
-
name: skillName,
|
|
4627
|
-
entry
|
|
4628
|
-
});
|
|
4629
|
-
skillsBySource.set(entry.source, existing);
|
|
4630
|
-
}
|
|
4631
|
-
const totalSkills = skillNames.length - skipped.length;
|
|
4632
|
-
if (totalSkills === 0) {
|
|
4633
|
-
console.log(`${DIM}No GitHub skills to check.${RESET}`);
|
|
4634
|
-
printSkippedSkills(skipped);
|
|
4635
|
-
return;
|
|
4636
|
-
}
|
|
4637
|
-
console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`);
|
|
4638
|
-
const updates = [];
|
|
4639
|
-
const errors = [];
|
|
4640
|
-
if (hubEntries.length > 0) try {
|
|
4641
|
-
(await checkHubUpdates(hubEntries.map(({ entry }) => ({
|
|
4642
|
-
source: entry.sourceUrl,
|
|
4643
|
-
version: entry.resolvedVersion,
|
|
4644
|
-
artifact_digest: entry.artifactDigest
|
|
4645
|
-
})))).items.forEach((item, index) => {
|
|
4646
|
-
const hubEntry = hubEntries[index];
|
|
4647
|
-
if (!hubEntry) return;
|
|
4648
|
-
if (item.has_update) updates.push({
|
|
4649
|
-
name: hubEntry.name,
|
|
4650
|
-
source: hubEntry.entry.sourceUrl
|
|
4651
|
-
});
|
|
4652
|
-
});
|
|
4653
|
-
} catch (err) {
|
|
4654
|
-
hubEntries.forEach(({ name, entry }) => {
|
|
4655
|
-
errors.push({
|
|
4656
|
-
name,
|
|
4657
|
-
source: entry.sourceUrl,
|
|
4658
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
4659
|
-
});
|
|
4660
|
-
});
|
|
4661
|
-
}
|
|
4662
|
-
for (const [source, skills] of skillsBySource) for (const { name, entry } of skills) try {
|
|
4663
|
-
const latestHash = await fetchSkillFolderHash(source, entry.skillPath, token);
|
|
4664
|
-
if (!latestHash) {
|
|
4665
|
-
errors.push({
|
|
4666
|
-
name,
|
|
4667
|
-
source,
|
|
4668
|
-
error: "Could not fetch from GitHub"
|
|
4669
|
-
});
|
|
4670
|
-
continue;
|
|
4671
|
-
}
|
|
4672
|
-
if (latestHash !== entry.skillFolderHash) updates.push({
|
|
4673
|
-
name,
|
|
4674
|
-
source
|
|
4675
|
-
});
|
|
4676
|
-
} catch (err) {
|
|
4677
|
-
errors.push({
|
|
4678
|
-
name,
|
|
4679
|
-
source,
|
|
4680
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
4681
|
-
});
|
|
4682
|
-
}
|
|
4683
|
-
console.log();
|
|
4684
|
-
if (updates.length === 0) console.log(`${TEXT}✓ All skills are up to date${RESET}`);
|
|
4685
|
-
else {
|
|
4686
|
-
console.log(`${TEXT}${updates.length} update(s) available:${RESET}`);
|
|
4687
|
-
console.log();
|
|
4688
|
-
for (const update of updates) {
|
|
4689
|
-
console.log(` ${TEXT}↑${RESET} ${update.name}`);
|
|
4690
|
-
console.log(` ${DIM}source: ${update.source}${RESET}`);
|
|
4691
|
-
}
|
|
4692
|
-
console.log();
|
|
4693
|
-
console.log(`${DIM}Run${RESET} ${TEXT}npx safeskill update${RESET} ${DIM}to update all skills${RESET}`);
|
|
4694
|
-
}
|
|
4695
|
-
if (errors.length > 0) {
|
|
4696
|
-
console.log();
|
|
4697
|
-
console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
|
|
4698
|
-
console.log();
|
|
4699
|
-
for (const error of errors) {
|
|
4700
|
-
console.log(` ${DIM}✗${RESET} ${error.name}`);
|
|
4701
|
-
console.log(` ${DIM}source: ${error.source}${RESET}`);
|
|
4702
|
-
}
|
|
4703
|
-
}
|
|
4704
|
-
printSkippedSkills(skipped);
|
|
4705
|
-
track({
|
|
4706
|
-
event: "check",
|
|
4707
|
-
skillCount: String(totalSkills),
|
|
4708
|
-
updatesAvailable: String(updates.length)
|
|
4709
|
-
});
|
|
4710
|
-
console.log();
|
|
4711
|
-
}
|
|
4712
|
-
async function runUpdate(region) {
|
|
4713
|
-
console.log(`${TEXT}Checking for skill updates...${RESET}`);
|
|
4714
|
-
console.log();
|
|
4715
|
-
const lock = readSkillLock();
|
|
4716
|
-
const skillNames = Object.keys(lock.skills);
|
|
4717
|
-
if (skillNames.length === 0) {
|
|
4718
|
-
console.log(`${DIM}No skills tracked in lock file.${RESET}`);
|
|
4719
|
-
console.log(`${DIM}Install skills with${RESET} ${TEXT}npx safeskill add <package>${RESET}`);
|
|
4720
|
-
return;
|
|
4721
|
-
}
|
|
4722
|
-
const token = getGitHubToken();
|
|
4723
|
-
const updates = [];
|
|
4724
|
-
const hubEntries = [];
|
|
4725
|
-
const skipped = [];
|
|
4726
|
-
for (const skillName of skillNames) {
|
|
4727
|
-
const entry = lock.skills[skillName];
|
|
4728
|
-
if (!entry) continue;
|
|
4729
|
-
if (entry.sourceType === "hub") {
|
|
4730
|
-
if (!entry.resolvedVersion || !entry.artifactDigest) {
|
|
4731
|
-
skipped.push({
|
|
4732
|
-
name: skillName,
|
|
4733
|
-
reason: getSkipReason(entry),
|
|
4734
|
-
sourceUrl: entry.sourceUrl
|
|
4735
|
-
});
|
|
4736
|
-
continue;
|
|
4737
|
-
}
|
|
4738
|
-
hubEntries.push({
|
|
4739
|
-
name: skillName,
|
|
4740
|
-
entry
|
|
4741
|
-
});
|
|
4742
|
-
continue;
|
|
4743
|
-
}
|
|
4744
|
-
if (!entry.skillFolderHash || !entry.skillPath) {
|
|
4745
|
-
skipped.push({
|
|
4746
|
-
name: skillName,
|
|
4747
|
-
reason: getSkipReason(entry),
|
|
4748
|
-
sourceUrl: entry.sourceUrl
|
|
4749
|
-
});
|
|
4750
|
-
continue;
|
|
4751
|
-
}
|
|
4752
|
-
try {
|
|
4753
|
-
const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
|
|
4754
|
-
if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
|
|
4755
|
-
name: skillName,
|
|
4756
|
-
source: entry.source,
|
|
4757
|
-
entry
|
|
4758
|
-
});
|
|
4759
|
-
} catch {}
|
|
4760
|
-
}
|
|
4761
|
-
if (hubEntries.length > 0) try {
|
|
4762
|
-
(await checkHubUpdates(hubEntries.map(({ entry }) => ({
|
|
4763
|
-
source: entry.sourceUrl,
|
|
4764
|
-
version: entry.resolvedVersion,
|
|
4765
|
-
artifact_digest: entry.artifactDigest
|
|
4766
|
-
})))).items.forEach((item, index) => {
|
|
4767
|
-
const hubEntry = hubEntries[index];
|
|
4768
|
-
if (!hubEntry || !item.has_update || !item.latest_version) return;
|
|
4769
|
-
updates.push({
|
|
4770
|
-
name: hubEntry.name,
|
|
4771
|
-
source: replaceHubSourceVersion(hubEntry.entry.sourceUrl, item.latest_version),
|
|
4772
|
-
entry: hubEntry.entry
|
|
4773
|
-
});
|
|
4774
|
-
});
|
|
4775
|
-
} catch {}
|
|
4776
|
-
if (skillNames.length - skipped.length === 0) {
|
|
4777
|
-
console.log(`${DIM}No skills to check.${RESET}`);
|
|
4778
|
-
printSkippedSkills(skipped);
|
|
4779
|
-
return;
|
|
4780
|
-
}
|
|
4781
|
-
if (updates.length === 0) {
|
|
4782
|
-
console.log(`${TEXT}✓ All skills are up to date${RESET}`);
|
|
4783
|
-
console.log();
|
|
4784
|
-
return;
|
|
4785
|
-
}
|
|
4786
|
-
console.log(`${TEXT}Found ${updates.length} update(s)${RESET}`);
|
|
4787
|
-
console.log();
|
|
4788
|
-
let successCount = 0;
|
|
4789
|
-
let failCount = 0;
|
|
4790
|
-
for (const update of updates) {
|
|
4791
|
-
console.log(`${TEXT}Updating ${update.name}...${RESET}`);
|
|
4792
|
-
let installUrl = update.entry.sourceUrl;
|
|
4793
|
-
if (update.entry.sourceType === "hub") installUrl = update.source;
|
|
4794
|
-
else if (update.entry.skillPath) {
|
|
4795
|
-
let skillFolder = update.entry.skillPath;
|
|
4796
|
-
if (skillFolder.endsWith("/SKILL.md")) skillFolder = skillFolder.slice(0, -9);
|
|
4797
|
-
else if (skillFolder.endsWith("SKILL.md")) skillFolder = skillFolder.slice(0, -8);
|
|
4798
|
-
if (skillFolder.endsWith("/")) skillFolder = skillFolder.slice(0, -1);
|
|
4799
|
-
installUrl = update.entry.sourceUrl.replace(/\.git$/, "").replace(/\/$/, "");
|
|
4800
|
-
installUrl = `${installUrl}/tree/main/${skillFolder}`;
|
|
4801
|
-
}
|
|
4802
|
-
const cliEntry = join(__dirname, "..", "bin", "cli.mjs");
|
|
4803
|
-
if (!existsSync(cliEntry)) {
|
|
4804
|
-
failCount++;
|
|
4805
|
-
console.log(` ${DIM}✗ Failed to update ${update.name}: CLI entrypoint not found at ${cliEntry}${RESET}`);
|
|
4806
|
-
continue;
|
|
4807
|
-
}
|
|
4808
|
-
const addArgs = region ? [
|
|
4809
|
-
cliEntry,
|
|
4810
|
-
"--region",
|
|
4811
|
-
region,
|
|
4812
|
-
"add",
|
|
4813
|
-
installUrl,
|
|
4814
|
-
"-g",
|
|
4815
|
-
"-y"
|
|
4816
|
-
] : [
|
|
4817
|
-
cliEntry,
|
|
4818
|
-
"add",
|
|
4819
|
-
installUrl,
|
|
4820
|
-
"-g",
|
|
4821
|
-
"-y"
|
|
4822
|
-
];
|
|
4823
|
-
if (spawnSync(process.execPath, addArgs, {
|
|
4824
|
-
stdio: [
|
|
4825
|
-
"inherit",
|
|
4826
|
-
"pipe",
|
|
4827
|
-
"pipe"
|
|
4828
|
-
],
|
|
4829
|
-
encoding: "utf-8",
|
|
4830
|
-
shell: process.platform === "win32"
|
|
4831
|
-
}).status === 0) {
|
|
4832
|
-
successCount++;
|
|
4833
|
-
console.log(` ${TEXT}✓${RESET} Updated ${update.name}`);
|
|
4834
|
-
} else {
|
|
4835
|
-
failCount++;
|
|
4836
|
-
console.log(` ${DIM}✗ Failed to update ${update.name}${RESET}`);
|
|
4837
|
-
}
|
|
4838
|
-
}
|
|
4839
|
-
console.log();
|
|
4840
|
-
if (successCount > 0) console.log(`${TEXT}✓ Updated ${successCount} skill(s)${RESET}`);
|
|
4841
|
-
if (failCount > 0) console.log(`${DIM}Failed to update ${failCount} skill(s)${RESET}`);
|
|
4842
|
-
track({
|
|
4843
|
-
event: "update",
|
|
4844
|
-
skillCount: String(updates.length),
|
|
4845
|
-
successCount: String(successCount),
|
|
4846
|
-
failCount: String(failCount)
|
|
4847
|
-
});
|
|
4848
|
-
console.log();
|
|
4849
|
-
}
|
|
4850
6633
|
function parseGlobalOptions(args) {
|
|
4851
6634
|
const parsedArgs = [];
|
|
4852
6635
|
let region;
|
|
@@ -4893,9 +6676,11 @@ async function main() {
|
|
|
4893
6676
|
return;
|
|
4894
6677
|
}
|
|
4895
6678
|
if (parsedGlobalOptions.region) setSafeSkillRegionOverride(parsedGlobalOptions.region);
|
|
6679
|
+
const inAgent = await isRunningInAgent();
|
|
4896
6680
|
const args = parsedGlobalOptions.args;
|
|
4897
6681
|
if (args.length === 0) {
|
|
4898
|
-
|
|
6682
|
+
if (inAgent) showHelp();
|
|
6683
|
+
else showBanner();
|
|
4899
6684
|
return;
|
|
4900
6685
|
}
|
|
4901
6686
|
const command = args[0];
|
|
@@ -4905,24 +6690,28 @@ async function main() {
|
|
|
4905
6690
|
case "search":
|
|
4906
6691
|
case "f":
|
|
4907
6692
|
case "s":
|
|
4908
|
-
|
|
4909
|
-
|
|
6693
|
+
if (!inAgent) {
|
|
6694
|
+
showLogo();
|
|
6695
|
+
console.log();
|
|
6696
|
+
}
|
|
4910
6697
|
await runFind(restArgs);
|
|
4911
6698
|
break;
|
|
4912
6699
|
case "init":
|
|
4913
|
-
|
|
4914
|
-
|
|
6700
|
+
if (!inAgent) {
|
|
6701
|
+
showLogo();
|
|
6702
|
+
console.log();
|
|
6703
|
+
}
|
|
4915
6704
|
runInit(restArgs);
|
|
4916
6705
|
break;
|
|
4917
6706
|
case "experimental_install":
|
|
4918
|
-
showLogo();
|
|
6707
|
+
if (!inAgent) showLogo();
|
|
4919
6708
|
await runInstallFromLock(restArgs);
|
|
4920
6709
|
break;
|
|
4921
6710
|
case "i":
|
|
4922
6711
|
case "install":
|
|
4923
6712
|
case "a":
|
|
4924
6713
|
case "add": {
|
|
4925
|
-
showLogo();
|
|
6714
|
+
if (!inAgent) showLogo();
|
|
4926
6715
|
const { source: addSource, options: addOpts } = parseAddOptions(restArgs);
|
|
4927
6716
|
await runAdd(addSource, addOpts);
|
|
4928
6717
|
break;
|
|
@@ -4938,7 +6727,7 @@ async function main() {
|
|
|
4938
6727
|
await removeCommand(skills, removeOptions);
|
|
4939
6728
|
break;
|
|
4940
6729
|
case "experimental_sync": {
|
|
4941
|
-
showLogo();
|
|
6730
|
+
if (!inAgent) showLogo();
|
|
4942
6731
|
const { options: syncOptions } = parseSyncOptions(restArgs);
|
|
4943
6732
|
await runSync(restArgs, syncOptions);
|
|
4944
6733
|
break;
|
|
@@ -4947,12 +6736,17 @@ async function main() {
|
|
|
4947
6736
|
case "ls":
|
|
4948
6737
|
await runList(restArgs);
|
|
4949
6738
|
break;
|
|
6739
|
+
case "use": {
|
|
6740
|
+
const { source: useSource, options: useOptions, errors: useErrors } = parseUseOptions(restArgs);
|
|
6741
|
+
await runUse(useSource, useOptions, useErrors);
|
|
6742
|
+
break;
|
|
6743
|
+
}
|
|
4950
6744
|
case "check":
|
|
4951
|
-
runCheck(restArgs);
|
|
6745
|
+
await runCheck(restArgs);
|
|
4952
6746
|
break;
|
|
4953
6747
|
case "update":
|
|
4954
6748
|
case "upgrade":
|
|
4955
|
-
runUpdate(parsedGlobalOptions.region);
|
|
6749
|
+
await runUpdate(restArgs, parsedGlobalOptions.region);
|
|
4956
6750
|
break;
|
|
4957
6751
|
case "--help":
|
|
4958
6752
|
case "-h":
|
|
@@ -4967,5 +6761,9 @@ async function main() {
|
|
|
4967
6761
|
console.log(`Run ${BOLD}safeskill --help${RESET} for usage.`);
|
|
4968
6762
|
}
|
|
4969
6763
|
}
|
|
4970
|
-
|
|
6764
|
+
function isCliEntrypoint() {
|
|
6765
|
+
const entrypoint = process.argv[1];
|
|
6766
|
+
return entrypoint ? import.meta.url === pathToFileURL(entrypoint).href : false;
|
|
6767
|
+
}
|
|
6768
|
+
if (isCliEntrypoint()) main();
|
|
4971
6769
|
export {};
|