@safeskill/cli 0.2.2 → 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 +2553 -737
- 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");
|
|
@@ -935,16 +1300,24 @@ async function parseSkillMd(skillMdPath, options) {
|
|
|
935
1300
|
...metaData
|
|
936
1301
|
};
|
|
937
1302
|
} catch (metaError) {}
|
|
1303
|
+
if (!skillData.name && options?.fallbackName) skillData.name = options.fallbackName;
|
|
1304
|
+
if (!skillData.description && options?.fallbackDescription) skillData.description = options.fallbackDescription;
|
|
938
1305
|
if (!skillData.name || !skillData.description) return null;
|
|
939
1306
|
if (typeof skillData.name !== "string" || typeof skillData.description !== "string") return null;
|
|
940
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;
|
|
941
1313
|
return {
|
|
942
|
-
name
|
|
943
|
-
description
|
|
1314
|
+
name,
|
|
1315
|
+
description,
|
|
944
1316
|
path: dirname(skillMdPath),
|
|
945
1317
|
rawContent: content,
|
|
946
1318
|
metadata: skillData.metadata,
|
|
947
|
-
slug: skillData.slug
|
|
1319
|
+
slug: skillData.slug || options?.fallbackSlug,
|
|
1320
|
+
installName: options?.fallbackInstallName
|
|
948
1321
|
};
|
|
949
1322
|
} catch {
|
|
950
1323
|
return null;
|
|
@@ -969,6 +1342,8 @@ function isSubpathSafe(basePath, subpath) {
|
|
|
969
1342
|
async function discoverSkills(basePath, subpath, options) {
|
|
970
1343
|
const skills = [];
|
|
971
1344
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1345
|
+
const localLock = await readLocalLock(basePath);
|
|
1346
|
+
const lockedSkillNames = new Set(Object.keys(localLock.skills).map(normalizeSkillName));
|
|
972
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.`);
|
|
973
1348
|
const searchPath = subpath ? join(basePath, subpath) : basePath;
|
|
974
1349
|
const pluginGroupings = await getPluginGroupings(searchPath);
|
|
@@ -977,13 +1352,23 @@ async function discoverSkills(basePath, subpath, options) {
|
|
|
977
1352
|
if (pluginGroupings.has(resolvedPath)) skill.pluginName = pluginGroupings.get(resolvedPath);
|
|
978
1353
|
return skill;
|
|
979
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
|
+
};
|
|
980
1363
|
if (await hasSkillMd(searchPath)) {
|
|
981
1364
|
let skill = await parseSkillMd(join(searchPath, "SKILL.md"), options);
|
|
982
1365
|
if (skill) {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1366
|
+
if (!isInstalledProjectSkill(skill)) {
|
|
1367
|
+
skill = enhanceSkill(skill);
|
|
1368
|
+
skills.push(skill);
|
|
1369
|
+
seenNames.add(skill.name);
|
|
1370
|
+
if (!options?.fullDepth) return skills;
|
|
1371
|
+
}
|
|
987
1372
|
}
|
|
988
1373
|
}
|
|
989
1374
|
const prioritySearchDirs = [
|
|
@@ -992,50 +1377,44 @@ async function discoverSkills(basePath, subpath, options) {
|
|
|
992
1377
|
join(searchPath, "skills/.curated"),
|
|
993
1378
|
join(searchPath, "skills/.experimental"),
|
|
994
1379
|
join(searchPath, "skills/.system"),
|
|
995
|
-
join(searchPath,
|
|
996
|
-
join(searchPath, ".claude/skills"),
|
|
997
|
-
join(searchPath, ".cline/skills"),
|
|
998
|
-
join(searchPath, ".codebuddy/skills"),
|
|
999
|
-
join(searchPath, ".codex/skills"),
|
|
1000
|
-
join(searchPath, ".commandcode/skills"),
|
|
1001
|
-
join(searchPath, ".continue/skills"),
|
|
1002
|
-
join(searchPath, ".github/skills"),
|
|
1003
|
-
join(searchPath, ".goose/skills"),
|
|
1004
|
-
join(searchPath, ".iflow/skills"),
|
|
1005
|
-
join(searchPath, ".junie/skills"),
|
|
1006
|
-
join(searchPath, ".kilocode/skills"),
|
|
1007
|
-
join(searchPath, ".kiro/skills"),
|
|
1008
|
-
join(searchPath, ".mux/skills"),
|
|
1009
|
-
join(searchPath, ".neovate/skills"),
|
|
1010
|
-
join(searchPath, ".opencode/skills"),
|
|
1011
|
-
join(searchPath, ".openhands/skills"),
|
|
1012
|
-
join(searchPath, ".pi/skills"),
|
|
1013
|
-
join(searchPath, ".qoder/skills"),
|
|
1014
|
-
join(searchPath, ".roo/skills"),
|
|
1015
|
-
join(searchPath, ".trae/skills"),
|
|
1016
|
-
join(searchPath, ".windsurf/skills"),
|
|
1017
|
-
join(searchPath, ".zencoder/skills")
|
|
1380
|
+
...AGENT_PROJECT_SKILL_DIRS.map((dir) => join(searchPath, dir))
|
|
1018
1381
|
];
|
|
1382
|
+
const deepContainerDirs = new Set(prioritySearchDirs.slice(1));
|
|
1019
1383
|
prioritySearchDirs.push(...await getPluginSkillPaths(searchPath));
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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 {}
|
|
1031
1410
|
}
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1411
|
+
} catch {}
|
|
1412
|
+
}
|
|
1034
1413
|
if (skills.length === 0 || options?.fullDepth) {
|
|
1035
1414
|
const allSkillDirs = await findSkillDirs(searchPath);
|
|
1036
1415
|
for (const skillDir of allSkillDirs) {
|
|
1037
1416
|
let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
|
|
1038
|
-
if (skill && !seenNames.has(skill.name)) {
|
|
1417
|
+
if (skill && !seenNames.has(skill.name) && !isInstalledProjectSkill(skill)) {
|
|
1039
1418
|
skill = enhanceSkill(skill);
|
|
1040
1419
|
skills.push(skill);
|
|
1041
1420
|
seenNames.add(skill.name);
|
|
@@ -1059,6 +1438,19 @@ const home = homedir();
|
|
|
1059
1438
|
const configHome = xdgConfig ?? join(home, ".config");
|
|
1060
1439
|
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
1061
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
|
+
}
|
|
1062
1454
|
function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
|
|
1063
1455
|
if (pathExists(join(homeDir, ".openclaw"))) return join(homeDir, ".openclaw/skills");
|
|
1064
1456
|
if (pathExists(join(homeDir, ".clawdbot"))) return join(homeDir, ".clawdbot/skills");
|
|
@@ -1066,6 +1458,13 @@ function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
|
|
|
1066
1458
|
return join(homeDir, ".openclaw/skills");
|
|
1067
1459
|
}
|
|
1068
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
|
+
},
|
|
1069
1468
|
amp: {
|
|
1070
1469
|
name: "amp",
|
|
1071
1470
|
displayName: "Amp",
|
|
@@ -1084,6 +1483,27 @@ const agents = {
|
|
|
1084
1483
|
return existsSync(join(home, ".gemini/antigravity"));
|
|
1085
1484
|
}
|
|
1086
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
|
+
},
|
|
1087
1507
|
augment: {
|
|
1088
1508
|
name: "augment",
|
|
1089
1509
|
displayName: "Augment",
|
|
@@ -1093,6 +1513,13 @@ const agents = {
|
|
|
1093
1513
|
return existsSync(join(home, ".augment"));
|
|
1094
1514
|
}
|
|
1095
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
|
+
},
|
|
1096
1523
|
"claude-code": {
|
|
1097
1524
|
name: "claude-code",
|
|
1098
1525
|
displayName: "Claude Code",
|
|
@@ -1120,6 +1547,13 @@ const agents = {
|
|
|
1120
1547
|
return existsSync(join(home, ".cline"));
|
|
1121
1548
|
}
|
|
1122
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
|
+
},
|
|
1123
1557
|
codebuddy: {
|
|
1124
1558
|
name: "codebuddy",
|
|
1125
1559
|
displayName: "CodeBuddy",
|
|
@@ -1129,6 +1563,20 @@ const agents = {
|
|
|
1129
1563
|
return existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home, ".codebuddy"));
|
|
1130
1564
|
}
|
|
1131
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
|
+
},
|
|
1132
1580
|
codex: {
|
|
1133
1581
|
name: "codex",
|
|
1134
1582
|
displayName: "Codex",
|
|
@@ -1192,6 +1640,21 @@ const agents = {
|
|
|
1192
1640
|
return existsSync(join(home, ".deepagents"));
|
|
1193
1641
|
}
|
|
1194
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
|
+
},
|
|
1195
1658
|
droid: {
|
|
1196
1659
|
name: "droid",
|
|
1197
1660
|
displayName: "Droid",
|
|
@@ -1201,15 +1664,33 @@ const agents = {
|
|
|
1201
1664
|
return existsSync(join(home, ".factory"));
|
|
1202
1665
|
}
|
|
1203
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
|
+
},
|
|
1204
1677
|
firebender: {
|
|
1205
1678
|
name: "firebender",
|
|
1206
1679
|
displayName: "Firebender",
|
|
1207
1680
|
skillsDir: ".agents/skills",
|
|
1208
1681
|
globalSkillsDir: join(home, ".firebender/skills"),
|
|
1682
|
+
showInUniversalPrompt: false,
|
|
1209
1683
|
detectInstalled: async () => {
|
|
1210
1684
|
return existsSync(join(home, ".firebender"));
|
|
1211
1685
|
}
|
|
1212
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
|
+
},
|
|
1213
1694
|
flocks: {
|
|
1214
1695
|
name: "flocks",
|
|
1215
1696
|
displayName: "Flocks",
|
|
@@ -1246,6 +1727,27 @@ const agents = {
|
|
|
1246
1727
|
return existsSync(join(configHome, "goose"));
|
|
1247
1728
|
}
|
|
1248
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
|
+
},
|
|
1249
1751
|
junie: {
|
|
1250
1752
|
name: "junie",
|
|
1251
1753
|
displayName: "Junie",
|
|
@@ -1275,13 +1777,22 @@ const agents = {
|
|
|
1275
1777
|
},
|
|
1276
1778
|
"kimi-cli": {
|
|
1277
1779
|
name: "kimi-cli",
|
|
1278
|
-
displayName: "Kimi
|
|
1780
|
+
displayName: "Kimi CLI",
|
|
1279
1781
|
skillsDir: ".agents/skills",
|
|
1280
1782
|
globalSkillsDir: join(home, ".config/agents/skills"),
|
|
1281
1783
|
detectInstalled: async () => {
|
|
1282
1784
|
return existsSync(join(home, ".kimi"));
|
|
1283
1785
|
}
|
|
1284
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
|
+
},
|
|
1285
1796
|
"kiro-cli": {
|
|
1286
1797
|
name: "kiro-cli",
|
|
1287
1798
|
displayName: "Kiro CLI",
|
|
@@ -1300,6 +1811,21 @@ const agents = {
|
|
|
1300
1811
|
return existsSync(join(home, ".kode"));
|
|
1301
1812
|
}
|
|
1302
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
|
+
},
|
|
1303
1829
|
mcpjam: {
|
|
1304
1830
|
name: "mcpjam",
|
|
1305
1831
|
displayName: "MCPJam",
|
|
@@ -1313,11 +1839,18 @@ const agents = {
|
|
|
1313
1839
|
name: "mistral-vibe",
|
|
1314
1840
|
displayName: "Mistral Vibe",
|
|
1315
1841
|
skillsDir: ".vibe/skills",
|
|
1316
|
-
globalSkillsDir: join(
|
|
1842
|
+
globalSkillsDir: join(vibeHome, "skills"),
|
|
1317
1843
|
detectInstalled: async () => {
|
|
1318
|
-
return existsSync(
|
|
1844
|
+
return existsSync(vibeHome);
|
|
1319
1845
|
}
|
|
1320
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
|
+
},
|
|
1321
1854
|
mux: {
|
|
1322
1855
|
name: "mux",
|
|
1323
1856
|
displayName: "Mux",
|
|
@@ -1345,6 +1878,13 @@ const agents = {
|
|
|
1345
1878
|
return existsSync(join(home, ".openhands"));
|
|
1346
1879
|
}
|
|
1347
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
|
+
},
|
|
1348
1888
|
pi: {
|
|
1349
1889
|
name: "pi",
|
|
1350
1890
|
displayName: "Pi",
|
|
@@ -1363,6 +1903,13 @@ const agents = {
|
|
|
1363
1903
|
return existsSync(join(home, ".qoder"));
|
|
1364
1904
|
}
|
|
1365
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
|
+
},
|
|
1366
1913
|
"qwen-code": {
|
|
1367
1914
|
name: "qwen-code",
|
|
1368
1915
|
displayName: "Qwen Code",
|
|
@@ -1382,6 +1929,20 @@ const agents = {
|
|
|
1382
1929
|
return existsSync(join(process.cwd(), ".replit"));
|
|
1383
1930
|
}
|
|
1384
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
|
+
},
|
|
1385
1946
|
roo: {
|
|
1386
1947
|
name: "roo",
|
|
1387
1948
|
displayName: "Roo Code",
|
|
@@ -1391,6 +1952,27 @@ const agents = {
|
|
|
1391
1952
|
return existsSync(join(home, ".roo"));
|
|
1392
1953
|
}
|
|
1393
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
|
+
},
|
|
1394
1976
|
trae: {
|
|
1395
1977
|
name: "trae",
|
|
1396
1978
|
displayName: "Trae",
|
|
@@ -1427,6 +2009,15 @@ const agents = {
|
|
|
1427
2009
|
return existsSync(join(home, ".codeium/windsurf"));
|
|
1428
2010
|
}
|
|
1429
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
|
+
},
|
|
1430
2021
|
zencoder: {
|
|
1431
2022
|
name: "zencoder",
|
|
1432
2023
|
displayName: "Zencoder",
|
|
@@ -1436,6 +2027,13 @@ const agents = {
|
|
|
1436
2027
|
return existsSync(join(home, ".zencoder"));
|
|
1437
2028
|
}
|
|
1438
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
|
+
},
|
|
1439
2037
|
neovate: {
|
|
1440
2038
|
name: "neovate",
|
|
1441
2039
|
displayName: "Neovate",
|
|
@@ -1454,6 +2052,16 @@ const agents = {
|
|
|
1454
2052
|
return existsSync(join(home, ".pochi"));
|
|
1455
2053
|
}
|
|
1456
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
|
+
},
|
|
1457
2065
|
adal: {
|
|
1458
2066
|
name: "adal",
|
|
1459
2067
|
displayName: "AdaL",
|
|
@@ -1478,6 +2086,16 @@ async function detectInstalledAgents() {
|
|
|
1478
2086
|
installed: await config.detectInstalled()
|
|
1479
2087
|
})))).filter((r) => r.installed).map((r) => r.type);
|
|
1480
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
|
+
}
|
|
1481
2099
|
function getUniversalAgents() {
|
|
1482
2100
|
return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type);
|
|
1483
2101
|
}
|
|
@@ -1487,7 +2105,7 @@ function getNonUniversalAgents() {
|
|
|
1487
2105
|
function isUniversalAgent(type) {
|
|
1488
2106
|
return agents[type].skillsDir === ".agents/skills";
|
|
1489
2107
|
}
|
|
1490
|
-
const AGENTS_DIR$
|
|
2108
|
+
const AGENTS_DIR$1 = ".agents";
|
|
1491
2109
|
const SKILLS_SUBDIR = "skills";
|
|
1492
2110
|
function sanitizeName(name) {
|
|
1493
2111
|
const asciiName = name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "");
|
|
@@ -1506,15 +2124,19 @@ function sanitizeName(name) {
|
|
|
1506
2124
|
function sanitizeSkillName(skill) {
|
|
1507
2125
|
return sanitizeName(skill.installName || skill.slug || skill.name);
|
|
1508
2126
|
}
|
|
1509
|
-
function isPathSafe(basePath, targetPath) {
|
|
2127
|
+
function isPathSafe$1(basePath, targetPath) {
|
|
1510
2128
|
const normalizedBase = normalize(resolve(basePath));
|
|
1511
2129
|
const normalizedTarget = normalize(resolve(targetPath));
|
|
1512
2130
|
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
1513
2131
|
}
|
|
1514
2132
|
function getCanonicalSkillsDir(global, cwd) {
|
|
1515
|
-
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");
|
|
1516
2137
|
}
|
|
1517
|
-
function getAgentBaseDir(agentType, global, cwd) {
|
|
2138
|
+
function getAgentBaseDir(agentType, global, cwd, eveSubagent) {
|
|
2139
|
+
if (agentType === "eve" && eveSubagent) return getEveSubagentSkillsDir(eveSubagent, cwd);
|
|
1518
2140
|
if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
|
|
1519
2141
|
const agent = agents[agentType];
|
|
1520
2142
|
const baseDir = global ? homedir() : cwd || process.cwd();
|
|
@@ -1582,18 +2204,18 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1582
2204
|
error: `${agent.displayName} does not support global skill installation`
|
|
1583
2205
|
};
|
|
1584
2206
|
const skillName = sanitizeSkillName(skill);
|
|
1585
|
-
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
2207
|
+
const canonicalBase = agentType === "eve" ? getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
|
|
1586
2208
|
const canonicalDir = join(canonicalBase, skillName);
|
|
1587
|
-
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
|
|
2209
|
+
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent);
|
|
1588
2210
|
const agentDir = join(agentBase, skillName);
|
|
1589
2211
|
const installMode = options.mode ?? "symlink";
|
|
1590
|
-
if (!isPathSafe(canonicalBase, canonicalDir)) return {
|
|
2212
|
+
if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
|
|
1591
2213
|
success: false,
|
|
1592
2214
|
path: agentDir,
|
|
1593
2215
|
mode: installMode,
|
|
1594
2216
|
error: "Invalid skill name: potential path traversal detected"
|
|
1595
2217
|
};
|
|
1596
|
-
if (!isPathSafe(agentBase, agentDir)) return {
|
|
2218
|
+
if (!isPathSafe$1(agentBase, agentDir)) return {
|
|
1597
2219
|
success: false,
|
|
1598
2220
|
path: agentDir,
|
|
1599
2221
|
mode: installMode,
|
|
@@ -1602,7 +2224,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1602
2224
|
try {
|
|
1603
2225
|
if (installMode === "copy") {
|
|
1604
2226
|
await cleanAndCreateDirectory(agentDir);
|
|
1605
|
-
await copyDirectory(skill.path, agentDir);
|
|
2227
|
+
await copyDirectory(skill.path, agentDir, agentType);
|
|
1606
2228
|
return {
|
|
1607
2229
|
success: true,
|
|
1608
2230
|
path: agentDir,
|
|
@@ -1610,7 +2232,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1610
2232
|
};
|
|
1611
2233
|
}
|
|
1612
2234
|
await cleanAndCreateDirectory(canonicalDir);
|
|
1613
|
-
await copyDirectory(skill.path, canonicalDir);
|
|
2235
|
+
await copyDirectory(skill.path, canonicalDir, agentType);
|
|
1614
2236
|
if (isGlobal && isUniversalAgent(agentType)) return {
|
|
1615
2237
|
success: true,
|
|
1616
2238
|
path: canonicalDir,
|
|
@@ -1643,26 +2265,44 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1643
2265
|
};
|
|
1644
2266
|
}
|
|
1645
2267
|
}
|
|
1646
|
-
const EXCLUDE_FILES = new Set(["metadata.json"]);
|
|
1647
|
-
const EXCLUDE_DIRS = new Set([
|
|
2268
|
+
const EXCLUDE_FILES$1 = new Set(["metadata.json"]);
|
|
2269
|
+
const EXCLUDE_DIRS$1 = new Set([
|
|
1648
2270
|
".git",
|
|
1649
2271
|
"__pycache__",
|
|
1650
2272
|
"__pypackages__"
|
|
1651
2273
|
]);
|
|
1652
|
-
const isExcluded = (name, isDirectory = false) => {
|
|
1653
|
-
if (EXCLUDE_FILES.has(name)) return true;
|
|
2274
|
+
const isExcluded$1 = (name, isDirectory = false) => {
|
|
2275
|
+
if (EXCLUDE_FILES$1.has(name)) return true;
|
|
1654
2276
|
if (name.startsWith(".")) return true;
|
|
1655
|
-
if (isDirectory && EXCLUDE_DIRS.has(name)) return true;
|
|
2277
|
+
if (isDirectory && EXCLUDE_DIRS$1.has(name)) return true;
|
|
1656
2278
|
return false;
|
|
1657
2279
|
};
|
|
1658
|
-
|
|
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) {
|
|
1659
2295
|
await mkdir(dest, { recursive: true });
|
|
1660
2296
|
const entries = await readdir(src, { withFileTypes: true });
|
|
1661
|
-
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) => {
|
|
1662
2298
|
const srcPath = join(src, entry.name);
|
|
1663
2299
|
const destPath = join(dest, entry.name);
|
|
1664
|
-
if (entry.isDirectory()) await copyDirectory(srcPath, destPath);
|
|
2300
|
+
if (entry.isDirectory()) await copyDirectory(srcPath, destPath, agentType);
|
|
1665
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
|
+
}
|
|
1666
2306
|
await cp(srcPath, destPath, {
|
|
1667
2307
|
dereference: true,
|
|
1668
2308
|
recursive: true
|
|
@@ -1677,9 +2317,9 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
|
|
|
1677
2317
|
const agent = agents[agentType];
|
|
1678
2318
|
const sanitized = sanitizeName(skillName);
|
|
1679
2319
|
if (options.global && agent.globalSkillsDir === void 0) return false;
|
|
1680
|
-
const targetBase = options.global
|
|
2320
|
+
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd, options.eveSubagent);
|
|
1681
2321
|
const skillDir = join(targetBase, sanitized);
|
|
1682
|
-
if (!isPathSafe(targetBase, skillDir)) return false;
|
|
2322
|
+
if (!isPathSafe$1(targetBase, skillDir)) return false;
|
|
1683
2323
|
try {
|
|
1684
2324
|
await access(skillDir);
|
|
1685
2325
|
return true;
|
|
@@ -1691,16 +2331,16 @@ function getInstallPath(skillName, agentType, options = {}) {
|
|
|
1691
2331
|
agents[agentType];
|
|
1692
2332
|
options.cwd || process.cwd();
|
|
1693
2333
|
const sanitized = sanitizeName(skillName);
|
|
1694
|
-
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd);
|
|
2334
|
+
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd, options.eveSubagent);
|
|
1695
2335
|
const installPath = join(targetBase, sanitized);
|
|
1696
|
-
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");
|
|
1697
2337
|
return installPath;
|
|
1698
2338
|
}
|
|
1699
2339
|
function getCanonicalPath(skillName, options = {}) {
|
|
1700
2340
|
const sanitized = sanitizeName(skillName);
|
|
1701
2341
|
const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
|
|
1702
2342
|
const canonicalPath = join(canonicalBase, sanitized);
|
|
1703
|
-
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");
|
|
1704
2344
|
return canonicalPath;
|
|
1705
2345
|
}
|
|
1706
2346
|
async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
@@ -1715,17 +2355,17 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
|
1715
2355
|
error: `${agent.displayName} does not support global skill installation`
|
|
1716
2356
|
};
|
|
1717
2357
|
const skillName = sanitizeSkillName(skill);
|
|
1718
|
-
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
2358
|
+
const canonicalBase = agentType === "eve" ? getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
|
|
1719
2359
|
const canonicalDir = join(canonicalBase, skillName);
|
|
1720
|
-
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
|
|
2360
|
+
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent);
|
|
1721
2361
|
const agentDir = join(agentBase, skillName);
|
|
1722
|
-
if (!isPathSafe(canonicalBase, canonicalDir)) return {
|
|
2362
|
+
if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
|
|
1723
2363
|
success: false,
|
|
1724
2364
|
path: agentDir,
|
|
1725
2365
|
mode: installMode,
|
|
1726
2366
|
error: "Invalid skill name: potential path traversal detected"
|
|
1727
2367
|
};
|
|
1728
|
-
if (!isPathSafe(agentBase, agentDir)) return {
|
|
2368
|
+
if (!isPathSafe$1(agentBase, agentDir)) return {
|
|
1729
2369
|
success: false,
|
|
1730
2370
|
path: agentDir,
|
|
1731
2371
|
mode: installMode,
|
|
@@ -1734,10 +2374,10 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
|
1734
2374
|
async function writeSkillFiles(targetDir) {
|
|
1735
2375
|
for (const [filePath, content] of skill.files) {
|
|
1736
2376
|
const fullPath = join(targetDir, filePath);
|
|
1737
|
-
if (!isPathSafe(targetDir, fullPath)) continue;
|
|
2377
|
+
if (!isPathSafe$1(targetDir, fullPath)) continue;
|
|
1738
2378
|
const parentDir = dirname(fullPath);
|
|
1739
2379
|
if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
|
|
1740
|
-
await writeFile(fullPath, content, "utf-8");
|
|
2380
|
+
await writeFile(fullPath, agentType === "eve" && basename(filePath).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(content) : content, "utf-8");
|
|
1741
2381
|
}
|
|
1742
2382
|
}
|
|
1743
2383
|
try {
|
|
@@ -1808,6 +2448,14 @@ async function listInstalledSkills(options = {}) {
|
|
|
1808
2448
|
path: agentDir,
|
|
1809
2449
|
agentType
|
|
1810
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
|
+
}
|
|
1811
2459
|
}
|
|
1812
2460
|
const allAgentTypes = Object.keys(agents);
|
|
1813
2461
|
for (const agentType of allAgentTypes) {
|
|
@@ -1821,6 +2469,14 @@ async function listInstalledSkills(options = {}) {
|
|
|
1821
2469
|
path: agentDir,
|
|
1822
2470
|
agentType
|
|
1823
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
|
+
}
|
|
1824
2480
|
}
|
|
1825
2481
|
}
|
|
1826
2482
|
for (const scope of scopes) try {
|
|
@@ -1866,7 +2522,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
1866
2522
|
]));
|
|
1867
2523
|
for (const possibleName of possibleNames) {
|
|
1868
2524
|
const agentSkillDir = join(agentBase, possibleName);
|
|
1869
|
-
if (!isPathSafe(agentBase, agentSkillDir)) continue;
|
|
2525
|
+
if (!isPathSafe$1(agentBase, agentSkillDir)) continue;
|
|
1870
2526
|
try {
|
|
1871
2527
|
await access(agentSkillDir);
|
|
1872
2528
|
found = true;
|
|
@@ -1878,7 +2534,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
1878
2534
|
for (const agentEntry of agentEntries) {
|
|
1879
2535
|
if (!agentEntry.isDirectory()) continue;
|
|
1880
2536
|
const candidateDir = join(agentBase, agentEntry.name);
|
|
1881
|
-
if (!isPathSafe(agentBase, candidateDir)) continue;
|
|
2537
|
+
if (!isPathSafe$1(agentBase, candidateDir)) continue;
|
|
1882
2538
|
try {
|
|
1883
2539
|
const candidateSkillMd = join(candidateDir, "SKILL.md");
|
|
1884
2540
|
await stat(candidateSkillMd);
|
|
@@ -1909,6 +2565,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
1909
2565
|
}
|
|
1910
2566
|
const AUDIT_URL = "https://add-skill.vercel.sh/audit";
|
|
1911
2567
|
let cliVersion = null;
|
|
2568
|
+
let detectedAgentName = null;
|
|
1912
2569
|
function isCI() {
|
|
1913
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);
|
|
1914
2571
|
}
|
|
@@ -1918,6 +2575,9 @@ function isEnabled() {
|
|
|
1918
2575
|
function setVersion(version) {
|
|
1919
2576
|
cliVersion = version;
|
|
1920
2577
|
}
|
|
2578
|
+
function setDetectedAgent(agentName) {
|
|
2579
|
+
detectedAgentName = agentName;
|
|
2580
|
+
}
|
|
1921
2581
|
async function fetchAuditData(source, skillSlugs, timeoutMs = 3e3) {
|
|
1922
2582
|
if (skillSlugs.length === 0) return null;
|
|
1923
2583
|
try {
|
|
@@ -1942,6 +2602,7 @@ function track(data) {
|
|
|
1942
2602
|
const params = new URLSearchParams();
|
|
1943
2603
|
if (cliVersion) params.set("v", cliVersion);
|
|
1944
2604
|
if (isCI()) params.set("ci", "1");
|
|
2605
|
+
if (detectedAgentName) params.set("agent", detectedAgentName);
|
|
1945
2606
|
for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
|
|
1946
2607
|
const telemetryUrl = await getSafeSkillTelemetryUrl();
|
|
1947
2608
|
fetch(`${telemetryUrl}?${params.toString()}`).catch(() => {});
|
|
@@ -1986,6 +2647,15 @@ var WellKnownProvider = class {
|
|
|
1986
2647
|
}
|
|
1987
2648
|
}
|
|
1988
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) {
|
|
1989
2659
|
try {
|
|
1990
2660
|
const parsed = new URL(baseUrl);
|
|
1991
2661
|
const basePath = parsed.pathname.replace(/\/$/, "");
|
|
@@ -2008,12 +2678,21 @@ var WellKnownProvider = class {
|
|
|
2008
2678
|
const index = await response.json();
|
|
2009
2679
|
if (!index.skills || !Array.isArray(index.skills)) continue;
|
|
2010
2680
|
let allValid = true;
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
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);
|
|
2014
2692
|
}
|
|
2015
|
-
if (allValid) return {
|
|
2016
|
-
index,
|
|
2693
|
+
if (allValid && sanitizedEntries.length > 0) return {
|
|
2694
|
+
index: { skills: sanitizedEntries },
|
|
2695
|
+
originalEntries,
|
|
2017
2696
|
resolvedBaseUrl: resolvedBase,
|
|
2018
2697
|
resolvedWellKnownPath: wellKnownPath
|
|
2019
2698
|
};
|
|
@@ -2031,28 +2710,41 @@ var WellKnownProvider = class {
|
|
|
2031
2710
|
if (typeof e.name !== "string" || !e.name) return false;
|
|
2032
2711
|
if (typeof e.description !== "string" || !e.description) return false;
|
|
2033
2712
|
if (!Array.isArray(e.files) || e.files.length === 0) return false;
|
|
2034
|
-
if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(e.name)
|
|
2035
|
-
if (e.name.length === 1 && !/^[a-z0-9]$/.test(e.name)) return false;
|
|
2036
|
-
}
|
|
2713
|
+
if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(e.name)) return false;
|
|
2037
2714
|
for (const file of e.files) {
|
|
2038
2715
|
if (typeof file !== "string") return false;
|
|
2039
2716
|
if (file.startsWith("/") || file.startsWith("\\") || file.includes("..")) return false;
|
|
2717
|
+
if (!this.isSafeWellKnownFilePath(file)) return false;
|
|
2040
2718
|
}
|
|
2041
2719
|
if (!e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md")) return false;
|
|
2042
2720
|
return true;
|
|
2043
2721
|
}
|
|
2044
|
-
|
|
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) {
|
|
2045
2737
|
try {
|
|
2046
2738
|
const parsed = new URL(url);
|
|
2047
|
-
const result = await this.
|
|
2739
|
+
const result = await this.fetchIndexForUse(url);
|
|
2048
2740
|
if (!result) return null;
|
|
2049
|
-
const { index, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2741
|
+
const { index, originalEntries, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2050
2742
|
let skillName = null;
|
|
2051
2743
|
const pathMatch = parsed.pathname.match(/\/.well-known\/(?:agent-skills|skills)\/([^/]+)\/?$/);
|
|
2052
2744
|
if (pathMatch && pathMatch[1] && pathMatch[1] !== "index.json") skillName = pathMatch[1];
|
|
2053
|
-
else if (index.skills.length === 1) skillName =
|
|
2745
|
+
else if (index.skills.length === 1) skillName = originalEntries[0].name;
|
|
2054
2746
|
if (!skillName) return null;
|
|
2055
|
-
const skillEntry =
|
|
2747
|
+
const skillEntry = originalEntries.find((s) => s.name === skillName);
|
|
2056
2748
|
if (!skillEntry) return null;
|
|
2057
2749
|
return this.fetchSkillByEntry(resolvedBaseUrl, skillEntry, resolvedWellKnownPath);
|
|
2058
2750
|
} catch {
|
|
@@ -2062,13 +2754,16 @@ var WellKnownProvider = class {
|
|
|
2062
2754
|
async fetchSkillByEntry(baseUrl, entry, wellKnownPath) {
|
|
2063
2755
|
try {
|
|
2064
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;
|
|
2065
2760
|
const skillBaseUrl = `${baseUrl.replace(/\/$/, "")}/${resolvedPath}/${entry.name}`;
|
|
2066
2761
|
const skillMdUrl = `${skillBaseUrl}/SKILL.md`;
|
|
2067
2762
|
const response = await fetch(skillMdUrl, { headers: getHeaders() });
|
|
2068
2763
|
if (!response.ok) return null;
|
|
2069
2764
|
const content = await response.text();
|
|
2070
|
-
const { data } = (
|
|
2071
|
-
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;
|
|
2072
2767
|
const files = /* @__PURE__ */ new Map();
|
|
2073
2768
|
files.set("SKILL.md", content);
|
|
2074
2769
|
const filePromises = entry.files.filter((f) => f.toLowerCase() !== "skill.md").map(async (filePath) => {
|
|
@@ -2084,15 +2779,18 @@ var WellKnownProvider = class {
|
|
|
2084
2779
|
});
|
|
2085
2780
|
const fileResults = await Promise.all(filePromises);
|
|
2086
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;
|
|
2087
2785
|
return {
|
|
2088
|
-
name
|
|
2089
|
-
description
|
|
2786
|
+
name,
|
|
2787
|
+
description,
|
|
2090
2788
|
content,
|
|
2091
2789
|
installName: entry.name,
|
|
2092
2790
|
sourceUrl: skillMdUrl,
|
|
2093
2791
|
metadata: data.metadata,
|
|
2094
2792
|
files,
|
|
2095
|
-
indexEntry:
|
|
2793
|
+
indexEntry: sanitizedEntry
|
|
2096
2794
|
};
|
|
2097
2795
|
} catch {
|
|
2098
2796
|
return null;
|
|
@@ -2100,10 +2798,10 @@ var WellKnownProvider = class {
|
|
|
2100
2798
|
}
|
|
2101
2799
|
async fetchAllSkills(url) {
|
|
2102
2800
|
try {
|
|
2103
|
-
const result = await this.
|
|
2801
|
+
const result = await this.fetchIndexForUse(url);
|
|
2104
2802
|
if (!result) return [];
|
|
2105
|
-
const {
|
|
2106
|
-
const skillPromises =
|
|
2803
|
+
const { originalEntries, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2804
|
+
const skillPromises = originalEntries.map((entry) => this.fetchSkillByEntry(resolvedBaseUrl, entry, resolvedWellKnownPath));
|
|
2107
2805
|
return (await Promise.all(skillPromises)).filter((s) => s !== null);
|
|
2108
2806
|
} catch {
|
|
2109
2807
|
return [];
|
|
@@ -2137,28 +2835,28 @@ var WellKnownProvider = class {
|
|
|
2137
2835
|
}
|
|
2138
2836
|
};
|
|
2139
2837
|
const wellKnownProvider = new WellKnownProvider();
|
|
2140
|
-
const AGENTS_DIR
|
|
2141
|
-
const LOCK_FILE
|
|
2142
|
-
const CURRENT_VERSION
|
|
2143
|
-
function getSkillLockPath
|
|
2838
|
+
const AGENTS_DIR = ".agents";
|
|
2839
|
+
const LOCK_FILE = ".skill-lock.json";
|
|
2840
|
+
const CURRENT_VERSION = 4;
|
|
2841
|
+
function getSkillLockPath() {
|
|
2144
2842
|
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
2145
|
-
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE
|
|
2146
|
-
return join(homedir(), AGENTS_DIR
|
|
2843
|
+
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
|
|
2844
|
+
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
2147
2845
|
}
|
|
2148
|
-
async function readSkillLock
|
|
2149
|
-
const lockPath = getSkillLockPath
|
|
2846
|
+
async function readSkillLock() {
|
|
2847
|
+
const lockPath = getSkillLockPath();
|
|
2150
2848
|
try {
|
|
2151
2849
|
const content = await readFile(lockPath, "utf-8");
|
|
2152
2850
|
const parsed = JSON.parse(content);
|
|
2153
2851
|
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLockFile();
|
|
2154
|
-
if (parsed.version < CURRENT_VERSION
|
|
2852
|
+
if (parsed.version < CURRENT_VERSION) return createEmptyLockFile();
|
|
2155
2853
|
return parsed;
|
|
2156
2854
|
} catch (error) {
|
|
2157
2855
|
return createEmptyLockFile();
|
|
2158
2856
|
}
|
|
2159
2857
|
}
|
|
2160
2858
|
async function writeSkillLock(lock) {
|
|
2161
|
-
const lockPath = getSkillLockPath
|
|
2859
|
+
const lockPath = getSkillLockPath();
|
|
2162
2860
|
await mkdir(dirname(lockPath), { recursive: true });
|
|
2163
2861
|
await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
2164
2862
|
}
|
|
@@ -2178,28 +2876,14 @@ function getGitHubToken() {
|
|
|
2178
2876
|
} catch {}
|
|
2179
2877
|
return null;
|
|
2180
2878
|
}
|
|
2181
|
-
async function fetchSkillFolderHash(ownerRepo, skillPath,
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
for (const branch of ["main", "master"]) try {
|
|
2187
|
-
const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`;
|
|
2188
|
-
const headers = getHeaders({ Accept: "application/vnd.github.v3+json" });
|
|
2189
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
2190
|
-
const response = await fetch(url, { headers });
|
|
2191
|
-
if (!response.ok) continue;
|
|
2192
|
-
const data = await response.json();
|
|
2193
|
-
if (!folderPath) return data.sha;
|
|
2194
|
-
const folderEntry = data.tree.find((entry) => entry.type === "tree" && entry.path === folderPath);
|
|
2195
|
-
if (folderEntry) return folderEntry.sha;
|
|
2196
|
-
} catch {
|
|
2197
|
-
continue;
|
|
2198
|
-
}
|
|
2199
|
-
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);
|
|
2200
2884
|
}
|
|
2201
2885
|
async function addSkillToLock(skillName, entry) {
|
|
2202
|
-
const lock = await readSkillLock
|
|
2886
|
+
const lock = await readSkillLock();
|
|
2203
2887
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2204
2888
|
const existingEntry = lock.skills[skillName];
|
|
2205
2889
|
lock.skills[skillName] = {
|
|
@@ -2210,109 +2894,43 @@ async function addSkillToLock(skillName, entry) {
|
|
|
2210
2894
|
await writeSkillLock(lock);
|
|
2211
2895
|
}
|
|
2212
2896
|
async function removeSkillFromLock(skillName) {
|
|
2213
|
-
const lock = await readSkillLock
|
|
2897
|
+
const lock = await readSkillLock();
|
|
2214
2898
|
if (!(skillName in lock.skills)) return false;
|
|
2215
2899
|
delete lock.skills[skillName];
|
|
2216
2900
|
await writeSkillLock(lock);
|
|
2217
2901
|
return true;
|
|
2218
2902
|
}
|
|
2219
2903
|
async function getSkillFromLock(skillName) {
|
|
2220
|
-
return (await readSkillLock
|
|
2904
|
+
return (await readSkillLock()).skills[skillName] ?? null;
|
|
2221
2905
|
}
|
|
2222
2906
|
async function getAllLockedSkills() {
|
|
2223
|
-
return (await readSkillLock
|
|
2907
|
+
return (await readSkillLock()).skills;
|
|
2224
2908
|
}
|
|
2225
2909
|
function createEmptyLockFile() {
|
|
2226
2910
|
return {
|
|
2227
|
-
version: CURRENT_VERSION
|
|
2911
|
+
version: CURRENT_VERSION,
|
|
2228
2912
|
skills: {},
|
|
2229
2913
|
dismissed: {}
|
|
2230
2914
|
};
|
|
2231
2915
|
}
|
|
2232
2916
|
async function isPromptDismissed(promptKey) {
|
|
2233
|
-
return (await readSkillLock
|
|
2917
|
+
return (await readSkillLock()).dismissed?.[promptKey] === true;
|
|
2234
2918
|
}
|
|
2235
2919
|
async function dismissPrompt(promptKey) {
|
|
2236
|
-
const lock = await readSkillLock
|
|
2920
|
+
const lock = await readSkillLock();
|
|
2237
2921
|
if (!lock.dismissed) lock.dismissed = {};
|
|
2238
2922
|
lock.dismissed[promptKey] = true;
|
|
2239
2923
|
await writeSkillLock(lock);
|
|
2240
2924
|
}
|
|
2241
2925
|
async function getLastSelectedAgents() {
|
|
2242
|
-
return (await readSkillLock
|
|
2926
|
+
return (await readSkillLock()).lastSelectedAgents;
|
|
2243
2927
|
}
|
|
2244
2928
|
async function saveSelectedAgents(agents) {
|
|
2245
|
-
const lock = await readSkillLock
|
|
2929
|
+
const lock = await readSkillLock();
|
|
2246
2930
|
lock.lastSelectedAgents = agents;
|
|
2247
2931
|
await writeSkillLock(lock);
|
|
2248
2932
|
}
|
|
2249
|
-
|
|
2250
|
-
const CURRENT_VERSION = 1;
|
|
2251
|
-
function getLocalLockPath(cwd) {
|
|
2252
|
-
return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
|
|
2253
|
-
}
|
|
2254
|
-
async function readLocalLock(cwd) {
|
|
2255
|
-
const lockPath = getLocalLockPath(cwd);
|
|
2256
|
-
try {
|
|
2257
|
-
const content = await readFile(lockPath, "utf-8");
|
|
2258
|
-
const parsed = JSON.parse(content);
|
|
2259
|
-
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
|
|
2260
|
-
if (parsed.version < CURRENT_VERSION) return createEmptyLocalLock();
|
|
2261
|
-
return parsed;
|
|
2262
|
-
} catch {
|
|
2263
|
-
return createEmptyLocalLock();
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
async function writeLocalLock(lock, cwd) {
|
|
2267
|
-
const lockPath = getLocalLockPath(cwd);
|
|
2268
|
-
const sortedSkills = {};
|
|
2269
|
-
for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
|
|
2270
|
-
const sorted = {
|
|
2271
|
-
version: lock.version,
|
|
2272
|
-
skills: sortedSkills
|
|
2273
|
-
};
|
|
2274
|
-
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
2275
|
-
}
|
|
2276
|
-
async function computeSkillFolderHash(skillDir) {
|
|
2277
|
-
const files = [];
|
|
2278
|
-
await collectFiles(skillDir, skillDir, files);
|
|
2279
|
-
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
2280
|
-
const hash = createHash("sha256");
|
|
2281
|
-
for (const file of files) {
|
|
2282
|
-
hash.update(file.relativePath);
|
|
2283
|
-
hash.update(file.content);
|
|
2284
|
-
}
|
|
2285
|
-
return hash.digest("hex");
|
|
2286
|
-
}
|
|
2287
|
-
async function collectFiles(baseDir, currentDir, results) {
|
|
2288
|
-
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
2289
|
-
await Promise.all(entries.map(async (entry) => {
|
|
2290
|
-
const fullPath = join(currentDir, entry.name);
|
|
2291
|
-
if (entry.isDirectory()) {
|
|
2292
|
-
if (entry.name === ".git" || entry.name === "node_modules") return;
|
|
2293
|
-
await collectFiles(baseDir, fullPath, results);
|
|
2294
|
-
} else if (entry.isFile()) {
|
|
2295
|
-
const content = await readFile(fullPath);
|
|
2296
|
-
const relativePath = relative(baseDir, fullPath).split("\\").join("/");
|
|
2297
|
-
results.push({
|
|
2298
|
-
relativePath,
|
|
2299
|
-
content
|
|
2300
|
-
});
|
|
2301
|
-
}
|
|
2302
|
-
}));
|
|
2303
|
-
}
|
|
2304
|
-
async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
2305
|
-
const lock = await readLocalLock(cwd);
|
|
2306
|
-
lock.skills[skillName] = entry;
|
|
2307
|
-
await writeLocalLock(lock, cwd);
|
|
2308
|
-
}
|
|
2309
|
-
function createEmptyLocalLock() {
|
|
2310
|
-
return {
|
|
2311
|
-
version: CURRENT_VERSION,
|
|
2312
|
-
skills: {}
|
|
2313
|
-
};
|
|
2314
|
-
}
|
|
2315
|
-
var version$1 = "0.2.2";
|
|
2933
|
+
var version$1 = "0.3.0";
|
|
2316
2934
|
const isCancelled$1 = (value) => typeof value === "symbol";
|
|
2317
2935
|
async function isSourcePrivate(source) {
|
|
2318
2936
|
const ownerRepo = parseOwnerRepo(source);
|
|
@@ -2322,6 +2940,12 @@ async function isSourcePrivate(source) {
|
|
|
2322
2940
|
function initTelemetry(version) {
|
|
2323
2941
|
setVersion(version);
|
|
2324
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
|
+
}
|
|
2325
2949
|
function riskLabel(risk) {
|
|
2326
2950
|
switch (risk) {
|
|
2327
2951
|
case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical Risk"));
|
|
@@ -2827,10 +3451,24 @@ async function runAdd(args, options = {}) {
|
|
|
2827
3451
|
}
|
|
2828
3452
|
const includeInternal = !!(options.skill && options.skill.length > 0);
|
|
2829
3453
|
spinner.start("Discovering skills...");
|
|
2830
|
-
|
|
3454
|
+
let skills = await discoverSkills(skillsDir, parsed.subpath, {
|
|
2831
3455
|
includeInternal,
|
|
2832
3456
|
fullDepth: options.fullDepth
|
|
2833
3457
|
});
|
|
3458
|
+
let usedHubMetadataFallback = false;
|
|
3459
|
+
if (skills.length === 0 && parsed.type === "hub" && !parsed.subpath && hubResolved?.skill) {
|
|
3460
|
+
const fallbackSkill = await parseSkillMd(join(skillsDir, "SKILL.md"), {
|
|
3461
|
+
includeInternal,
|
|
3462
|
+
fallbackName: hubResolved.skill.display_name || hubResolved.skill.name,
|
|
3463
|
+
fallbackDescription: hubResolved.skill.summary || hubResolved.skill.name,
|
|
3464
|
+
fallbackSlug: hubResolved.skill.name,
|
|
3465
|
+
fallbackInstallName: hubResolved.skill.name
|
|
3466
|
+
});
|
|
3467
|
+
if (fallbackSkill) {
|
|
3468
|
+
skills = [fallbackSkill];
|
|
3469
|
+
usedHubMetadataFallback = true;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
2834
3472
|
if (skills.length === 0) {
|
|
2835
3473
|
spinner.stop(import_picocolors.default.red("No skills found"));
|
|
2836
3474
|
Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
|
|
@@ -2838,6 +3476,7 @@ async function runAdd(args, options = {}) {
|
|
|
2838
3476
|
process.exit(1);
|
|
2839
3477
|
}
|
|
2840
3478
|
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
3479
|
+
if (usedHubMetadataFallback) M.warn("Warning: this hub artifact is missing SKILL.md frontmatter. Some agents may not recognize this skill until name and description are added to SKILL.md.");
|
|
2841
3480
|
if (options.list) {
|
|
2842
3481
|
console.log();
|
|
2843
3482
|
M.step(import_picocolors.default.bold("Available Skills"));
|
|
@@ -3123,7 +3762,7 @@ async function runAdd(args, options = {}) {
|
|
|
3123
3762
|
skillFiles[skill.name] = relativePath;
|
|
3124
3763
|
}
|
|
3125
3764
|
const normalizedSource = getOwnerRepo(parsed);
|
|
3126
|
-
const lockSource = parsed
|
|
3765
|
+
const lockSource = getLockSourceForParsedSource(parsed);
|
|
3127
3766
|
if (normalizedSource) {
|
|
3128
3767
|
const ownerRepo = parseOwnerRepo(normalizedSource);
|
|
3129
3768
|
if (ownerRepo) {
|
|
@@ -3152,13 +3791,14 @@ async function runAdd(args, options = {}) {
|
|
|
3152
3791
|
let skillFolderHash = "";
|
|
3153
3792
|
const skillPathValue = skillFiles[skill.name];
|
|
3154
3793
|
if (parsed.type === "github" && normalizedSource && skillPathValue) {
|
|
3155
|
-
const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken
|
|
3794
|
+
const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken, parsed.ref);
|
|
3156
3795
|
if (hash) skillFolderHash = hash;
|
|
3157
3796
|
}
|
|
3158
3797
|
await addSkillToLock(skill.name, {
|
|
3159
3798
|
source: lockSource || normalizedSource || parsed.url,
|
|
3160
3799
|
sourceType: parsed.type,
|
|
3161
3800
|
sourceUrl: parsed.url,
|
|
3801
|
+
ref: parsed.ref,
|
|
3162
3802
|
skillPath: skillPathValue,
|
|
3163
3803
|
skillFolderHash,
|
|
3164
3804
|
pluginName: skill.pluginName,
|
|
@@ -3174,10 +3814,15 @@ async function runAdd(args, options = {}) {
|
|
|
3174
3814
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
3175
3815
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
3176
3816
|
const computedHash = await computeSkillFolderHash(skill.path);
|
|
3817
|
+
const skillPathValue = skillFiles[skill.name];
|
|
3177
3818
|
await addSkillToLocalLock(skill.name, {
|
|
3178
3819
|
source: lockSource || parsed.url,
|
|
3820
|
+
ref: parsed.ref,
|
|
3179
3821
|
sourceType: parsed.type,
|
|
3180
|
-
|
|
3822
|
+
skillPath: skillPathValue,
|
|
3823
|
+
computedHash,
|
|
3824
|
+
resolvedVersion: parsed.type === "hub" ? hubResolved?.version : void 0,
|
|
3825
|
+
artifactDigest: parsed.type === "hub" ? hubResolved?.artifact_digest : void 0
|
|
3181
3826
|
}, cwd);
|
|
3182
3827
|
} catch {}
|
|
3183
3828
|
}
|
|
@@ -3343,10 +3988,21 @@ function parseAddOptions(args) {
|
|
|
3343
3988
|
options
|
|
3344
3989
|
};
|
|
3345
3990
|
}
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
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";
|
|
3350
4006
|
const CYAN$1 = "\x1B[36m";
|
|
3351
4007
|
const GREEN = "\x1B[32m";
|
|
3352
4008
|
const YELLOW$1 = "\x1B[33m";
|
|
@@ -3358,7 +4014,7 @@ function formatCount(count) {
|
|
|
3358
4014
|
}
|
|
3359
4015
|
function formatTrustScore(score) {
|
|
3360
4016
|
if (score === void 0 || score === null) return "";
|
|
3361
|
-
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}`;
|
|
3362
4018
|
}
|
|
3363
4019
|
function formatRelativeDate(ts) {
|
|
3364
4020
|
if (!ts) return "";
|
|
@@ -3373,8 +4029,8 @@ function formatRelativeDate(ts) {
|
|
|
3373
4029
|
}
|
|
3374
4030
|
function formatStatsBadges(installs, stars) {
|
|
3375
4031
|
const parts = [];
|
|
3376
|
-
if (installs > 0) parts.push(`${CYAN$1}↓ ${formatCount(installs)}${RESET$
|
|
3377
|
-
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}`);
|
|
3378
4034
|
return parts.join(" ");
|
|
3379
4035
|
}
|
|
3380
4036
|
async function searchSkillsAPI(query) {
|
|
@@ -3400,29 +4056,29 @@ async function runSearchPrompt(initialQuery = "") {
|
|
|
3400
4056
|
if (lastRenderedLines > 0) process.stdout.write(MOVE_UP(lastRenderedLines) + MOVE_TO_COL(1));
|
|
3401
4057
|
process.stdout.write(CLEAR_DOWN);
|
|
3402
4058
|
const lines = [];
|
|
3403
|
-
const cursor = `${BOLD$
|
|
3404
|
-
lines.push(`${TEXT$
|
|
4059
|
+
const cursor = `${BOLD$3}_${RESET$3}`;
|
|
4060
|
+
lines.push(`${TEXT$2}Search skills:${RESET$3} ${query}${cursor}`);
|
|
3405
4061
|
lines.push("");
|
|
3406
|
-
if (!query || query.length < 2) lines.push(`${DIM$
|
|
3407
|
-
else if (results.length === 0 && loading) lines.push(`${DIM$
|
|
3408
|
-
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}`);
|
|
3409
4065
|
else {
|
|
3410
4066
|
const visible = results.slice(0, 8);
|
|
3411
4067
|
for (let i = 0; i < visible.length; i++) {
|
|
3412
4068
|
const skill = visible[i];
|
|
3413
4069
|
const isSelected = i === selectedIndex;
|
|
3414
|
-
const arrow = isSelected ? `${BOLD$
|
|
4070
|
+
const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
|
|
3415
4071
|
const displayName = skill.label || skill.name;
|
|
3416
|
-
const name = isSelected ? `${BOLD$
|
|
3417
|
-
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}` : "";
|
|
3418
4074
|
const statsBadge = formatStatsBadges(skill.installs, skill.totalStars);
|
|
3419
|
-
const trustBadge = skill.trustScore !== void 0 ? ` ${skill.trustScore >= 80 ? GREEN : skill.trustScore >= 60 ? YELLOW$1 : RED}TrustScore:${skill.trustScore}${RESET$
|
|
3420
|
-
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}` : "";
|
|
3421
4077
|
lines.push(` ${arrow}${trustBadge} ${name}${source}${statsBadge ? ` ${statsBadge}` : ""}${loadingIndicator}`);
|
|
3422
4078
|
}
|
|
3423
4079
|
}
|
|
3424
4080
|
lines.push("");
|
|
3425
|
-
lines.push(`${DIM$
|
|
4081
|
+
lines.push(`${DIM$3}up/down navigate | enter select | esc cancel${RESET$3}`);
|
|
3426
4082
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
3427
4083
|
lastRenderedLines = lines.length;
|
|
3428
4084
|
}
|
|
@@ -3517,10 +4173,10 @@ async function isRepoPublic(owner, repo) {
|
|
|
3517
4173
|
}
|
|
3518
4174
|
async function runFind(args) {
|
|
3519
4175
|
const query = args.join(" ");
|
|
3520
|
-
const isNonInteractive = !process.stdin.isTTY;
|
|
3521
|
-
const agentTip = `${DIM$
|
|
3522
|
-
${DIM$
|
|
3523
|
-
${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}`;
|
|
3524
4180
|
if (query) {
|
|
3525
4181
|
const results = await searchSkillsAPI(query);
|
|
3526
4182
|
track({
|
|
@@ -3529,26 +4185,26 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3529
4185
|
resultCount: String(results.length)
|
|
3530
4186
|
});
|
|
3531
4187
|
if (results.length === 0) {
|
|
3532
|
-
console.log(`${DIM$
|
|
4188
|
+
console.log(`${DIM$3}No skills found for "${query}"${RESET$3}`);
|
|
3533
4189
|
return;
|
|
3534
4190
|
}
|
|
3535
|
-
console.log(`${DIM$
|
|
4191
|
+
console.log(`${DIM$3}Install with${RESET$3} npx safeskill add <owner/repo@skill>`);
|
|
3536
4192
|
console.log();
|
|
3537
4193
|
for (const skill of results.slice(0, 6)) {
|
|
3538
4194
|
const pkg = skill.source || skill.slug;
|
|
3539
4195
|
const installRef = pkg.startsWith("safeskill://") ? pkg : `${pkg}@${skill.name}`;
|
|
3540
|
-
console.log(`${TEXT$
|
|
4196
|
+
console.log(`${TEXT$2}${installRef}${RESET$3}`);
|
|
3541
4197
|
const statsBadge = formatStatsBadges(skill.installs, skill.totalStars);
|
|
3542
4198
|
const badges = [
|
|
3543
4199
|
formatTrustScore(skill.trustScore),
|
|
3544
4200
|
statsBadge,
|
|
3545
|
-
skill.updatedAt ? `${DIM$
|
|
4201
|
+
skill.updatedAt ? `${DIM$3}${formatRelativeDate(skill.updatedAt)}${RESET$3}` : ""
|
|
3546
4202
|
].filter(Boolean).join(` `);
|
|
3547
4203
|
if (badges) console.log(` ${badges}`);
|
|
3548
4204
|
const displayName = skill.label || skill.name;
|
|
3549
4205
|
const desc = skill.description || (displayName !== skill.name ? displayName : "");
|
|
3550
|
-
if (desc) console.log(`${DIM$
|
|
3551
|
-
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}`);
|
|
3552
4208
|
console.log();
|
|
3553
4209
|
}
|
|
3554
4210
|
return;
|
|
@@ -3556,6 +4212,7 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3556
4212
|
if (isNonInteractive) {
|
|
3557
4213
|
console.log(agentTip);
|
|
3558
4214
|
console.log();
|
|
4215
|
+
return;
|
|
3559
4216
|
}
|
|
3560
4217
|
const selected = await runSearchPrompt();
|
|
3561
4218
|
track({
|
|
@@ -3565,14 +4222,14 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3565
4222
|
interactive: "1"
|
|
3566
4223
|
});
|
|
3567
4224
|
if (!selected) {
|
|
3568
|
-
console.log(`${DIM$
|
|
4225
|
+
console.log(`${DIM$3}Search cancelled${RESET$3}`);
|
|
3569
4226
|
console.log();
|
|
3570
4227
|
return;
|
|
3571
4228
|
}
|
|
3572
4229
|
const pkg = selected.source || selected.slug;
|
|
3573
4230
|
const skillName = selected.name;
|
|
3574
4231
|
console.log();
|
|
3575
|
-
console.log(`${TEXT$
|
|
4232
|
+
console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} from ${DIM$3}${pkg}${RESET$3}...`);
|
|
3576
4233
|
console.log();
|
|
3577
4234
|
const { source, options } = parseAddOptions([
|
|
3578
4235
|
pkg,
|
|
@@ -3582,9 +4239,9 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3582
4239
|
await runAdd(source, options);
|
|
3583
4240
|
console.log();
|
|
3584
4241
|
const info = getOwnerRepoFromString(pkg);
|
|
3585
|
-
if (pkg.startsWith("safeskill://")) console.log(`${DIM$
|
|
3586
|
-
else if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$
|
|
3587
|
-
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}`);
|
|
3588
4245
|
console.log();
|
|
3589
4246
|
}
|
|
3590
4247
|
const isCancelled = (value) => typeof value === "symbol";
|
|
@@ -3921,9 +4578,9 @@ async function runInstallFromLock(args) {
|
|
|
3921
4578
|
}
|
|
3922
4579
|
}
|
|
3923
4580
|
}
|
|
3924
|
-
const RESET$
|
|
3925
|
-
const BOLD$
|
|
3926
|
-
const DIM$
|
|
4581
|
+
const RESET$2 = "\x1B[0m";
|
|
4582
|
+
const BOLD$2 = "\x1B[1m";
|
|
4583
|
+
const DIM$2 = "\x1B[38;5;102m";
|
|
3927
4584
|
const CYAN = "\x1B[36m";
|
|
3928
4585
|
const YELLOW = "\x1B[33m";
|
|
3929
4586
|
function shortenPath(fullPath, cwd) {
|
|
@@ -3959,8 +4616,8 @@ async function runList(args) {
|
|
|
3959
4616
|
const validAgents = Object.keys(agents);
|
|
3960
4617
|
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
3961
4618
|
if (invalidAgents.length > 0) {
|
|
3962
|
-
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$
|
|
3963
|
-
console.log(`${DIM$
|
|
4619
|
+
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$2}`);
|
|
4620
|
+
console.log(`${DIM$2}Valid agents: ${validAgents.join(", ")}${RESET$2}`);
|
|
3964
4621
|
process.exit(1);
|
|
3965
4622
|
}
|
|
3966
4623
|
agentFilter = options.agent;
|
|
@@ -3987,20 +4644,20 @@ async function runList(args) {
|
|
|
3987
4644
|
console.log("[]");
|
|
3988
4645
|
return;
|
|
3989
4646
|
}
|
|
3990
|
-
console.log(`${DIM$
|
|
3991
|
-
if (scope) console.log(`${DIM$
|
|
3992
|
-
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}`);
|
|
3993
4650
|
return;
|
|
3994
4651
|
}
|
|
3995
4652
|
function printSkill(skill, indent = false) {
|
|
3996
4653
|
const prefix = indent ? " " : "";
|
|
3997
4654
|
const shortPath = shortenPath(skill.canonicalPath, cwd);
|
|
3998
4655
|
const agentNames = skill.agents.map((a) => agents[a].displayName);
|
|
3999
|
-
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$
|
|
4000
|
-
console.log(`${prefix}${CYAN}${skill.name}${RESET$
|
|
4001
|
-
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}`);
|
|
4002
4659
|
}
|
|
4003
|
-
console.log(`${BOLD$
|
|
4660
|
+
console.log(`${BOLD$2}${scopeLabel} Skills${RESET$2}`);
|
|
4004
4661
|
console.log();
|
|
4005
4662
|
const groupedSkills = {};
|
|
4006
4663
|
const ungroupedSkills = [];
|
|
@@ -4016,13 +4673,13 @@ async function runList(args) {
|
|
|
4016
4673
|
const sortedGroups = Object.keys(groupedSkills).sort();
|
|
4017
4674
|
for (const group of sortedGroups) {
|
|
4018
4675
|
const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
4019
|
-
console.log(`${BOLD$
|
|
4676
|
+
console.log(`${BOLD$2}${title}${RESET$2}`);
|
|
4020
4677
|
const skills = groupedSkills[group];
|
|
4021
4678
|
if (skills) for (const skill of skills) printSkill(skill, true);
|
|
4022
4679
|
console.log();
|
|
4023
4680
|
}
|
|
4024
4681
|
if (ungroupedSkills.length > 0) {
|
|
4025
|
-
console.log(`${BOLD$
|
|
4682
|
+
console.log(`${BOLD$2}General${RESET$2}`);
|
|
4026
4683
|
for (const skill of ungroupedSkills) printSkill(skill, true);
|
|
4027
4684
|
console.log();
|
|
4028
4685
|
}
|
|
@@ -4221,130 +4878,1592 @@ function parseRemoveOptions(args) {
|
|
|
4221
4878
|
options
|
|
4222
4879
|
};
|
|
4223
4880
|
}
|
|
4224
|
-
|
|
4225
|
-
|
|
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) {
|
|
4226
4938
|
try {
|
|
4227
|
-
const
|
|
4228
|
-
|
|
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
|
+
};
|
|
4229
4964
|
} catch {
|
|
4230
|
-
return
|
|
4965
|
+
return {
|
|
4966
|
+
tree: null,
|
|
4967
|
+
rateLimited: false
|
|
4968
|
+
};
|
|
4231
4969
|
}
|
|
4232
4970
|
}
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
const
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
" ████ ",
|
|
4246
|
-
" █ ",
|
|
4247
|
-
"█ █",
|
|
4248
|
-
" █████ "
|
|
4249
|
-
],
|
|
4250
|
-
a: [
|
|
4251
|
-
" ███ ",
|
|
4252
|
-
" █ █ ",
|
|
4253
|
-
" █ █ ",
|
|
4254
|
-
" █████ ",
|
|
4255
|
-
" █ █ ",
|
|
4256
|
-
" █ █ ",
|
|
4257
|
-
" █ █ "
|
|
4258
|
-
],
|
|
4259
|
-
f: [
|
|
4260
|
-
"██████ ",
|
|
4261
|
-
"█ ",
|
|
4262
|
-
"█ ",
|
|
4263
|
-
"█████ ",
|
|
4264
|
-
"█ ",
|
|
4265
|
-
"█ ",
|
|
4266
|
-
"█ "
|
|
4267
|
-
],
|
|
4268
|
-
e: [
|
|
4269
|
-
"██████ ",
|
|
4270
|
-
"█ ",
|
|
4271
|
-
"█ ",
|
|
4272
|
-
"█████ ",
|
|
4273
|
-
"█ ",
|
|
4274
|
-
"█ ",
|
|
4275
|
-
"██████ "
|
|
4276
|
-
],
|
|
4277
|
-
k: [
|
|
4278
|
-
"█ █ ",
|
|
4279
|
-
"█ █ ",
|
|
4280
|
-
"█ █ ",
|
|
4281
|
-
"███ ",
|
|
4282
|
-
"█ █ ",
|
|
4283
|
-
"█ █ ",
|
|
4284
|
-
"█ █ "
|
|
4285
|
-
],
|
|
4286
|
-
i: [
|
|
4287
|
-
"█████ ",
|
|
4288
|
-
" █ ",
|
|
4289
|
-
" █ ",
|
|
4290
|
-
" █ ",
|
|
4291
|
-
" █ ",
|
|
4292
|
-
" █ ",
|
|
4293
|
-
"█████ "
|
|
4294
|
-
],
|
|
4295
|
-
l: [
|
|
4296
|
-
"█ ",
|
|
4297
|
-
"█ ",
|
|
4298
|
-
"█ ",
|
|
4299
|
-
"█ ",
|
|
4300
|
-
"█ ",
|
|
4301
|
-
"█ ",
|
|
4302
|
-
"██████ "
|
|
4303
|
-
]
|
|
4304
|
-
};
|
|
4305
|
-
function buildLogoLines() {
|
|
4306
|
-
const words = ["Safe", "Skill"];
|
|
4307
|
-
const lines = Array.from({ length: LOGO_HEIGHT }, () => "");
|
|
4308
|
-
for (const word of words) {
|
|
4309
|
-
for (const character of word) {
|
|
4310
|
-
const glyph = LOGO_GLYPHS[character.toLowerCase()];
|
|
4311
|
-
if (!glyph) continue;
|
|
4312
|
-
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;
|
|
4313
4983
|
}
|
|
4314
|
-
|
|
4984
|
+
return null;
|
|
4315
4985
|
}
|
|
4316
|
-
|
|
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;
|
|
4317
5004
|
}
|
|
4318
|
-
|
|
4319
|
-
"
|
|
4320
|
-
"
|
|
4321
|
-
"
|
|
4322
|
-
"
|
|
4323
|
-
|
|
4324
|
-
"
|
|
4325
|
-
|
|
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/"
|
|
4326
5042
|
];
|
|
4327
|
-
function
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
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;
|
|
4331
5094
|
});
|
|
4332
5095
|
}
|
|
4333
|
-
function
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
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}`);
|
|
4348
6467
|
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
|
|
4349
6468
|
console.log();
|
|
4350
6469
|
console.log(`${DIM}try:${RESET} npx safeskill add example-org/agent-skills`);
|
|
@@ -4364,6 +6483,7 @@ ${BOLD}Manage Skills:${RESET}
|
|
|
4364
6483
|
remove [skills] Remove installed skills
|
|
4365
6484
|
list, ls List installed skills
|
|
4366
6485
|
find [query] Search for skills interactively
|
|
6486
|
+
use <package> Generate a one-shot prompt for a skill
|
|
4367
6487
|
|
|
4368
6488
|
${BOLD}Updates:${RESET}
|
|
4369
6489
|
check Check for available skill updates
|
|
@@ -4421,6 +6541,8 @@ ${BOLD}Examples:${RESET}
|
|
|
4421
6541
|
${DIM}$${RESET} safeskill ls --json ${DIM}# JSON output${RESET}
|
|
4422
6542
|
${DIM}$${RESET} safeskill find ${DIM}# interactive search${RESET}
|
|
4423
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
|
|
4424
6546
|
${DIM}$${RESET} safeskill check
|
|
4425
6547
|
${DIM}$${RESET} safeskill update
|
|
4426
6548
|
${DIM}$${RESET} safeskill experimental_install ${DIM}# restore from skills-lock.json${RESET}
|
|
@@ -4508,327 +6630,6 @@ Describe when this skill should be used.
|
|
|
4508
6630
|
console.log(`Browse existing skills for inspiration at ${TEXT}https://skills.sh/${RESET}`);
|
|
4509
6631
|
console.log();
|
|
4510
6632
|
}
|
|
4511
|
-
const AGENTS_DIR = ".agents";
|
|
4512
|
-
const LOCK_FILE = ".skill-lock.json";
|
|
4513
|
-
const CURRENT_LOCK_VERSION = 4;
|
|
4514
|
-
function getSkillLockPath() {
|
|
4515
|
-
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
4516
|
-
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
|
|
4517
|
-
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
4518
|
-
}
|
|
4519
|
-
function readSkillLock() {
|
|
4520
|
-
const lockPath = getSkillLockPath();
|
|
4521
|
-
try {
|
|
4522
|
-
const content = readFileSync(lockPath, "utf-8");
|
|
4523
|
-
const parsed = JSON.parse(content);
|
|
4524
|
-
if (typeof parsed.version !== "number" || !parsed.skills) return {
|
|
4525
|
-
version: CURRENT_LOCK_VERSION,
|
|
4526
|
-
skills: {}
|
|
4527
|
-
};
|
|
4528
|
-
if (parsed.version < CURRENT_LOCK_VERSION) return {
|
|
4529
|
-
version: CURRENT_LOCK_VERSION,
|
|
4530
|
-
skills: {}
|
|
4531
|
-
};
|
|
4532
|
-
return parsed;
|
|
4533
|
-
} catch {
|
|
4534
|
-
return {
|
|
4535
|
-
version: CURRENT_LOCK_VERSION,
|
|
4536
|
-
skills: {}
|
|
4537
|
-
};
|
|
4538
|
-
}
|
|
4539
|
-
}
|
|
4540
|
-
function getSkipReason(entry) {
|
|
4541
|
-
if (entry.sourceType === "local") return "Local path";
|
|
4542
|
-
if (entry.sourceType === "hub") {
|
|
4543
|
-
if (!entry.resolvedVersion) return "No hub version recorded";
|
|
4544
|
-
if (!entry.artifactDigest) return "No hub artifact metadata";
|
|
4545
|
-
return "No version tracking";
|
|
4546
|
-
}
|
|
4547
|
-
if (entry.sourceType === "git") return "Git URL (hash tracking not supported)";
|
|
4548
|
-
if (!entry.skillFolderHash) return "No version hash available";
|
|
4549
|
-
if (!entry.skillPath) return "No skill path recorded";
|
|
4550
|
-
return "No version tracking";
|
|
4551
|
-
}
|
|
4552
|
-
function replaceHubSourceVersion(source, version) {
|
|
4553
|
-
const trimmedSource = source.trim();
|
|
4554
|
-
if (!trimmedSource) return source;
|
|
4555
|
-
return `${trimmedSource.replace(/@[^/@]+$/, "")}@${version}`;
|
|
4556
|
-
}
|
|
4557
|
-
function printSkippedSkills(skipped) {
|
|
4558
|
-
if (skipped.length === 0) return;
|
|
4559
|
-
console.log();
|
|
4560
|
-
console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`);
|
|
4561
|
-
for (const skill of skipped) {
|
|
4562
|
-
console.log(` ${TEXT}•${RESET} ${skill.name} ${DIM}(${skill.reason})${RESET}`);
|
|
4563
|
-
console.log(` ${DIM}To update: ${TEXT}npx safeskill add ${skill.sourceUrl} -g -y${RESET}`);
|
|
4564
|
-
}
|
|
4565
|
-
}
|
|
4566
|
-
async function runCheck(args = []) {
|
|
4567
|
-
console.log(`${TEXT}Checking for skill updates...${RESET}`);
|
|
4568
|
-
console.log();
|
|
4569
|
-
const lock = readSkillLock();
|
|
4570
|
-
const skillNames = Object.keys(lock.skills);
|
|
4571
|
-
if (skillNames.length === 0) {
|
|
4572
|
-
console.log(`${DIM}No skills tracked in lock file.${RESET}`);
|
|
4573
|
-
console.log(`${DIM}Install skills with${RESET} ${TEXT}npx safeskill add <package>${RESET}`);
|
|
4574
|
-
return;
|
|
4575
|
-
}
|
|
4576
|
-
const token = getGitHubToken();
|
|
4577
|
-
const skillsBySource = /* @__PURE__ */ new Map();
|
|
4578
|
-
const hubEntries = [];
|
|
4579
|
-
const skipped = [];
|
|
4580
|
-
for (const skillName of skillNames) {
|
|
4581
|
-
const entry = lock.skills[skillName];
|
|
4582
|
-
if (!entry) continue;
|
|
4583
|
-
if (entry.sourceType === "hub") {
|
|
4584
|
-
if (!entry.resolvedVersion || !entry.artifactDigest) {
|
|
4585
|
-
skipped.push({
|
|
4586
|
-
name: skillName,
|
|
4587
|
-
reason: getSkipReason(entry),
|
|
4588
|
-
sourceUrl: entry.sourceUrl
|
|
4589
|
-
});
|
|
4590
|
-
continue;
|
|
4591
|
-
}
|
|
4592
|
-
hubEntries.push({
|
|
4593
|
-
name: skillName,
|
|
4594
|
-
entry
|
|
4595
|
-
});
|
|
4596
|
-
continue;
|
|
4597
|
-
}
|
|
4598
|
-
if (!entry.skillFolderHash || !entry.skillPath) {
|
|
4599
|
-
skipped.push({
|
|
4600
|
-
name: skillName,
|
|
4601
|
-
reason: getSkipReason(entry),
|
|
4602
|
-
sourceUrl: entry.sourceUrl
|
|
4603
|
-
});
|
|
4604
|
-
continue;
|
|
4605
|
-
}
|
|
4606
|
-
const existing = skillsBySource.get(entry.source) || [];
|
|
4607
|
-
existing.push({
|
|
4608
|
-
name: skillName,
|
|
4609
|
-
entry
|
|
4610
|
-
});
|
|
4611
|
-
skillsBySource.set(entry.source, existing);
|
|
4612
|
-
}
|
|
4613
|
-
const totalSkills = skillNames.length - skipped.length;
|
|
4614
|
-
if (totalSkills === 0) {
|
|
4615
|
-
console.log(`${DIM}No GitHub skills to check.${RESET}`);
|
|
4616
|
-
printSkippedSkills(skipped);
|
|
4617
|
-
return;
|
|
4618
|
-
}
|
|
4619
|
-
console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`);
|
|
4620
|
-
const updates = [];
|
|
4621
|
-
const errors = [];
|
|
4622
|
-
if (hubEntries.length > 0) try {
|
|
4623
|
-
(await checkHubUpdates(hubEntries.map(({ entry }) => ({
|
|
4624
|
-
source: entry.sourceUrl,
|
|
4625
|
-
version: entry.resolvedVersion,
|
|
4626
|
-
artifact_digest: entry.artifactDigest
|
|
4627
|
-
})))).items.forEach((item, index) => {
|
|
4628
|
-
const hubEntry = hubEntries[index];
|
|
4629
|
-
if (!hubEntry) return;
|
|
4630
|
-
if (item.has_update) updates.push({
|
|
4631
|
-
name: hubEntry.name,
|
|
4632
|
-
source: hubEntry.entry.sourceUrl
|
|
4633
|
-
});
|
|
4634
|
-
});
|
|
4635
|
-
} catch (err) {
|
|
4636
|
-
hubEntries.forEach(({ name, entry }) => {
|
|
4637
|
-
errors.push({
|
|
4638
|
-
name,
|
|
4639
|
-
source: entry.sourceUrl,
|
|
4640
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
4641
|
-
});
|
|
4642
|
-
});
|
|
4643
|
-
}
|
|
4644
|
-
for (const [source, skills] of skillsBySource) for (const { name, entry } of skills) try {
|
|
4645
|
-
const latestHash = await fetchSkillFolderHash(source, entry.skillPath, token);
|
|
4646
|
-
if (!latestHash) {
|
|
4647
|
-
errors.push({
|
|
4648
|
-
name,
|
|
4649
|
-
source,
|
|
4650
|
-
error: "Could not fetch from GitHub"
|
|
4651
|
-
});
|
|
4652
|
-
continue;
|
|
4653
|
-
}
|
|
4654
|
-
if (latestHash !== entry.skillFolderHash) updates.push({
|
|
4655
|
-
name,
|
|
4656
|
-
source
|
|
4657
|
-
});
|
|
4658
|
-
} catch (err) {
|
|
4659
|
-
errors.push({
|
|
4660
|
-
name,
|
|
4661
|
-
source,
|
|
4662
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
4663
|
-
});
|
|
4664
|
-
}
|
|
4665
|
-
console.log();
|
|
4666
|
-
if (updates.length === 0) console.log(`${TEXT}✓ All skills are up to date${RESET}`);
|
|
4667
|
-
else {
|
|
4668
|
-
console.log(`${TEXT}${updates.length} update(s) available:${RESET}`);
|
|
4669
|
-
console.log();
|
|
4670
|
-
for (const update of updates) {
|
|
4671
|
-
console.log(` ${TEXT}↑${RESET} ${update.name}`);
|
|
4672
|
-
console.log(` ${DIM}source: ${update.source}${RESET}`);
|
|
4673
|
-
}
|
|
4674
|
-
console.log();
|
|
4675
|
-
console.log(`${DIM}Run${RESET} ${TEXT}npx safeskill update${RESET} ${DIM}to update all skills${RESET}`);
|
|
4676
|
-
}
|
|
4677
|
-
if (errors.length > 0) {
|
|
4678
|
-
console.log();
|
|
4679
|
-
console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
|
|
4680
|
-
console.log();
|
|
4681
|
-
for (const error of errors) {
|
|
4682
|
-
console.log(` ${DIM}✗${RESET} ${error.name}`);
|
|
4683
|
-
console.log(` ${DIM}source: ${error.source}${RESET}`);
|
|
4684
|
-
}
|
|
4685
|
-
}
|
|
4686
|
-
printSkippedSkills(skipped);
|
|
4687
|
-
track({
|
|
4688
|
-
event: "check",
|
|
4689
|
-
skillCount: String(totalSkills),
|
|
4690
|
-
updatesAvailable: String(updates.length)
|
|
4691
|
-
});
|
|
4692
|
-
console.log();
|
|
4693
|
-
}
|
|
4694
|
-
async function runUpdate(region) {
|
|
4695
|
-
console.log(`${TEXT}Checking for skill updates...${RESET}`);
|
|
4696
|
-
console.log();
|
|
4697
|
-
const lock = readSkillLock();
|
|
4698
|
-
const skillNames = Object.keys(lock.skills);
|
|
4699
|
-
if (skillNames.length === 0) {
|
|
4700
|
-
console.log(`${DIM}No skills tracked in lock file.${RESET}`);
|
|
4701
|
-
console.log(`${DIM}Install skills with${RESET} ${TEXT}npx safeskill add <package>${RESET}`);
|
|
4702
|
-
return;
|
|
4703
|
-
}
|
|
4704
|
-
const token = getGitHubToken();
|
|
4705
|
-
const updates = [];
|
|
4706
|
-
const hubEntries = [];
|
|
4707
|
-
const skipped = [];
|
|
4708
|
-
for (const skillName of skillNames) {
|
|
4709
|
-
const entry = lock.skills[skillName];
|
|
4710
|
-
if (!entry) continue;
|
|
4711
|
-
if (entry.sourceType === "hub") {
|
|
4712
|
-
if (!entry.resolvedVersion || !entry.artifactDigest) {
|
|
4713
|
-
skipped.push({
|
|
4714
|
-
name: skillName,
|
|
4715
|
-
reason: getSkipReason(entry),
|
|
4716
|
-
sourceUrl: entry.sourceUrl
|
|
4717
|
-
});
|
|
4718
|
-
continue;
|
|
4719
|
-
}
|
|
4720
|
-
hubEntries.push({
|
|
4721
|
-
name: skillName,
|
|
4722
|
-
entry
|
|
4723
|
-
});
|
|
4724
|
-
continue;
|
|
4725
|
-
}
|
|
4726
|
-
if (!entry.skillFolderHash || !entry.skillPath) {
|
|
4727
|
-
skipped.push({
|
|
4728
|
-
name: skillName,
|
|
4729
|
-
reason: getSkipReason(entry),
|
|
4730
|
-
sourceUrl: entry.sourceUrl
|
|
4731
|
-
});
|
|
4732
|
-
continue;
|
|
4733
|
-
}
|
|
4734
|
-
try {
|
|
4735
|
-
const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
|
|
4736
|
-
if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
|
|
4737
|
-
name: skillName,
|
|
4738
|
-
source: entry.source,
|
|
4739
|
-
entry
|
|
4740
|
-
});
|
|
4741
|
-
} catch {}
|
|
4742
|
-
}
|
|
4743
|
-
if (hubEntries.length > 0) try {
|
|
4744
|
-
(await checkHubUpdates(hubEntries.map(({ entry }) => ({
|
|
4745
|
-
source: entry.sourceUrl,
|
|
4746
|
-
version: entry.resolvedVersion,
|
|
4747
|
-
artifact_digest: entry.artifactDigest
|
|
4748
|
-
})))).items.forEach((item, index) => {
|
|
4749
|
-
const hubEntry = hubEntries[index];
|
|
4750
|
-
if (!hubEntry || !item.has_update || !item.latest_version) return;
|
|
4751
|
-
updates.push({
|
|
4752
|
-
name: hubEntry.name,
|
|
4753
|
-
source: replaceHubSourceVersion(hubEntry.entry.sourceUrl, item.latest_version),
|
|
4754
|
-
entry: hubEntry.entry
|
|
4755
|
-
});
|
|
4756
|
-
});
|
|
4757
|
-
} catch {}
|
|
4758
|
-
if (skillNames.length - skipped.length === 0) {
|
|
4759
|
-
console.log(`${DIM}No skills to check.${RESET}`);
|
|
4760
|
-
printSkippedSkills(skipped);
|
|
4761
|
-
return;
|
|
4762
|
-
}
|
|
4763
|
-
if (updates.length === 0) {
|
|
4764
|
-
console.log(`${TEXT}✓ All skills are up to date${RESET}`);
|
|
4765
|
-
console.log();
|
|
4766
|
-
return;
|
|
4767
|
-
}
|
|
4768
|
-
console.log(`${TEXT}Found ${updates.length} update(s)${RESET}`);
|
|
4769
|
-
console.log();
|
|
4770
|
-
let successCount = 0;
|
|
4771
|
-
let failCount = 0;
|
|
4772
|
-
for (const update of updates) {
|
|
4773
|
-
console.log(`${TEXT}Updating ${update.name}...${RESET}`);
|
|
4774
|
-
let installUrl = update.entry.sourceUrl;
|
|
4775
|
-
if (update.entry.sourceType === "hub") installUrl = update.source;
|
|
4776
|
-
else if (update.entry.skillPath) {
|
|
4777
|
-
let skillFolder = update.entry.skillPath;
|
|
4778
|
-
if (skillFolder.endsWith("/SKILL.md")) skillFolder = skillFolder.slice(0, -9);
|
|
4779
|
-
else if (skillFolder.endsWith("SKILL.md")) skillFolder = skillFolder.slice(0, -8);
|
|
4780
|
-
if (skillFolder.endsWith("/")) skillFolder = skillFolder.slice(0, -1);
|
|
4781
|
-
installUrl = update.entry.sourceUrl.replace(/\.git$/, "").replace(/\/$/, "");
|
|
4782
|
-
installUrl = `${installUrl}/tree/main/${skillFolder}`;
|
|
4783
|
-
}
|
|
4784
|
-
const cliEntry = join(__dirname, "..", "bin", "cli.mjs");
|
|
4785
|
-
if (!existsSync(cliEntry)) {
|
|
4786
|
-
failCount++;
|
|
4787
|
-
console.log(` ${DIM}✗ Failed to update ${update.name}: CLI entrypoint not found at ${cliEntry}${RESET}`);
|
|
4788
|
-
continue;
|
|
4789
|
-
}
|
|
4790
|
-
const addArgs = region ? [
|
|
4791
|
-
cliEntry,
|
|
4792
|
-
"--region",
|
|
4793
|
-
region,
|
|
4794
|
-
"add",
|
|
4795
|
-
installUrl,
|
|
4796
|
-
"-g",
|
|
4797
|
-
"-y"
|
|
4798
|
-
] : [
|
|
4799
|
-
cliEntry,
|
|
4800
|
-
"add",
|
|
4801
|
-
installUrl,
|
|
4802
|
-
"-g",
|
|
4803
|
-
"-y"
|
|
4804
|
-
];
|
|
4805
|
-
if (spawnSync(process.execPath, addArgs, {
|
|
4806
|
-
stdio: [
|
|
4807
|
-
"inherit",
|
|
4808
|
-
"pipe",
|
|
4809
|
-
"pipe"
|
|
4810
|
-
],
|
|
4811
|
-
encoding: "utf-8",
|
|
4812
|
-
shell: process.platform === "win32"
|
|
4813
|
-
}).status === 0) {
|
|
4814
|
-
successCount++;
|
|
4815
|
-
console.log(` ${TEXT}✓${RESET} Updated ${update.name}`);
|
|
4816
|
-
} else {
|
|
4817
|
-
failCount++;
|
|
4818
|
-
console.log(` ${DIM}✗ Failed to update ${update.name}${RESET}`);
|
|
4819
|
-
}
|
|
4820
|
-
}
|
|
4821
|
-
console.log();
|
|
4822
|
-
if (successCount > 0) console.log(`${TEXT}✓ Updated ${successCount} skill(s)${RESET}`);
|
|
4823
|
-
if (failCount > 0) console.log(`${DIM}Failed to update ${failCount} skill(s)${RESET}`);
|
|
4824
|
-
track({
|
|
4825
|
-
event: "update",
|
|
4826
|
-
skillCount: String(updates.length),
|
|
4827
|
-
successCount: String(successCount),
|
|
4828
|
-
failCount: String(failCount)
|
|
4829
|
-
});
|
|
4830
|
-
console.log();
|
|
4831
|
-
}
|
|
4832
6633
|
function parseGlobalOptions(args) {
|
|
4833
6634
|
const parsedArgs = [];
|
|
4834
6635
|
let region;
|
|
@@ -4875,9 +6676,11 @@ async function main() {
|
|
|
4875
6676
|
return;
|
|
4876
6677
|
}
|
|
4877
6678
|
if (parsedGlobalOptions.region) setSafeSkillRegionOverride(parsedGlobalOptions.region);
|
|
6679
|
+
const inAgent = await isRunningInAgent();
|
|
4878
6680
|
const args = parsedGlobalOptions.args;
|
|
4879
6681
|
if (args.length === 0) {
|
|
4880
|
-
|
|
6682
|
+
if (inAgent) showHelp();
|
|
6683
|
+
else showBanner();
|
|
4881
6684
|
return;
|
|
4882
6685
|
}
|
|
4883
6686
|
const command = args[0];
|
|
@@ -4887,24 +6690,28 @@ async function main() {
|
|
|
4887
6690
|
case "search":
|
|
4888
6691
|
case "f":
|
|
4889
6692
|
case "s":
|
|
4890
|
-
|
|
4891
|
-
|
|
6693
|
+
if (!inAgent) {
|
|
6694
|
+
showLogo();
|
|
6695
|
+
console.log();
|
|
6696
|
+
}
|
|
4892
6697
|
await runFind(restArgs);
|
|
4893
6698
|
break;
|
|
4894
6699
|
case "init":
|
|
4895
|
-
|
|
4896
|
-
|
|
6700
|
+
if (!inAgent) {
|
|
6701
|
+
showLogo();
|
|
6702
|
+
console.log();
|
|
6703
|
+
}
|
|
4897
6704
|
runInit(restArgs);
|
|
4898
6705
|
break;
|
|
4899
6706
|
case "experimental_install":
|
|
4900
|
-
showLogo();
|
|
6707
|
+
if (!inAgent) showLogo();
|
|
4901
6708
|
await runInstallFromLock(restArgs);
|
|
4902
6709
|
break;
|
|
4903
6710
|
case "i":
|
|
4904
6711
|
case "install":
|
|
4905
6712
|
case "a":
|
|
4906
6713
|
case "add": {
|
|
4907
|
-
showLogo();
|
|
6714
|
+
if (!inAgent) showLogo();
|
|
4908
6715
|
const { source: addSource, options: addOpts } = parseAddOptions(restArgs);
|
|
4909
6716
|
await runAdd(addSource, addOpts);
|
|
4910
6717
|
break;
|
|
@@ -4920,7 +6727,7 @@ async function main() {
|
|
|
4920
6727
|
await removeCommand(skills, removeOptions);
|
|
4921
6728
|
break;
|
|
4922
6729
|
case "experimental_sync": {
|
|
4923
|
-
showLogo();
|
|
6730
|
+
if (!inAgent) showLogo();
|
|
4924
6731
|
const { options: syncOptions } = parseSyncOptions(restArgs);
|
|
4925
6732
|
await runSync(restArgs, syncOptions);
|
|
4926
6733
|
break;
|
|
@@ -4929,12 +6736,17 @@ async function main() {
|
|
|
4929
6736
|
case "ls":
|
|
4930
6737
|
await runList(restArgs);
|
|
4931
6738
|
break;
|
|
6739
|
+
case "use": {
|
|
6740
|
+
const { source: useSource, options: useOptions, errors: useErrors } = parseUseOptions(restArgs);
|
|
6741
|
+
await runUse(useSource, useOptions, useErrors);
|
|
6742
|
+
break;
|
|
6743
|
+
}
|
|
4932
6744
|
case "check":
|
|
4933
|
-
runCheck(restArgs);
|
|
6745
|
+
await runCheck(restArgs);
|
|
4934
6746
|
break;
|
|
4935
6747
|
case "update":
|
|
4936
6748
|
case "upgrade":
|
|
4937
|
-
runUpdate(parsedGlobalOptions.region);
|
|
6749
|
+
await runUpdate(restArgs, parsedGlobalOptions.region);
|
|
4938
6750
|
break;
|
|
4939
6751
|
case "--help":
|
|
4940
6752
|
case "-h":
|
|
@@ -4949,5 +6761,9 @@ async function main() {
|
|
|
4949
6761
|
console.log(`Run ${BOLD}safeskill --help${RESET} for usage.`);
|
|
4950
6762
|
}
|
|
4951
6763
|
}
|
|
4952
|
-
|
|
6764
|
+
function isCliEntrypoint() {
|
|
6765
|
+
const entrypoint = process.argv[1];
|
|
6766
|
+
return entrypoint ? import.meta.url === pathToFileURL(entrypoint).href : false;
|
|
6767
|
+
}
|
|
6768
|
+
if (isCliEntrypoint()) main();
|
|
4953
6769
|
export {};
|