@safeskill/cli 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -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 +2563 -793
- 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) {
|
|
@@ -404,10 +623,8 @@ const REGION_DOMAINS = {
|
|
|
404
623
|
global: "safeskill.io"
|
|
405
624
|
};
|
|
406
625
|
const REGION_DETECT_URLS = ["https://ifconfig.co/country-iso", "https://ipapi.co/country/"];
|
|
407
|
-
const STATUS_TIMEOUT_MS = 2500;
|
|
408
626
|
const DETECT_TIMEOUT_MS = 1500;
|
|
409
627
|
let resolvedRegionPromise = null;
|
|
410
|
-
let regionAvailabilityCache = /* @__PURE__ */ new Map();
|
|
411
628
|
let safeSkillRegionOverride = null;
|
|
412
629
|
function normalizeSafeSkillRegion(region) {
|
|
413
630
|
const value = region?.trim().toLowerCase();
|
|
@@ -443,61 +660,11 @@ async function detectRegionByPublicIP() {
|
|
|
443
660
|
}
|
|
444
661
|
return "global";
|
|
445
662
|
}
|
|
446
|
-
function hasCustomEndpoints() {
|
|
447
|
-
return !!(process.env.SAFESKILL_HUB_URL || process.env.SAFESKILL_SEARCH_URL || process.env.SAFESKILL_TELEMETRY_URL || process.env.SKILLS_API_URL);
|
|
448
|
-
}
|
|
449
663
|
function ensureAbsoluteUrl(url, fallbackPath) {
|
|
450
664
|
const parsed = new URL(url);
|
|
451
665
|
if (!parsed.pathname || parsed.pathname === "/") parsed.pathname = fallbackPath;
|
|
452
666
|
return parsed.toString().replace(/\/$/, "");
|
|
453
667
|
}
|
|
454
|
-
function getShellSpecificCommands(variable, value) {
|
|
455
|
-
const shell = process.env.SHELL;
|
|
456
|
-
const platform = process.platform;
|
|
457
|
-
const parentProcess = process.env.PROMPT || process.env.COMSPEC;
|
|
458
|
-
const commands = [];
|
|
459
|
-
if (platform === "win32") {
|
|
460
|
-
process.env.WT_SESSION || process.env.TERM_PROGRAM;
|
|
461
|
-
const isPowerShell = process.env.PSModulePath || parentProcess?.includes("powershell.exe") || parentProcess?.includes("pwsh.exe");
|
|
462
|
-
const isCMD = parentProcess?.includes("cmd.exe");
|
|
463
|
-
if (isCMD && !isPowerShell) commands.push(`CMD: set ${variable}=${value}`);
|
|
464
|
-
else if (isPowerShell && !isCMD) commands.push(`PowerShell: $env:${variable}="${value}"`);
|
|
465
|
-
else if (isCMD && isPowerShell) {
|
|
466
|
-
commands.push(`CMD: set ${variable}=${value}`);
|
|
467
|
-
commands.push(`PowerShell: $env:${variable}="${value}"`);
|
|
468
|
-
} else {
|
|
469
|
-
commands.push(`CMD: set ${variable}=${value}`);
|
|
470
|
-
commands.push(`PowerShell: $env:${variable}="${value}"`);
|
|
471
|
-
}
|
|
472
|
-
} else if (shell?.includes("bash") || shell?.includes("zsh")) commands.push(`bash/zsh: export ${variable}="${value}"`);
|
|
473
|
-
else if (shell?.includes("fish")) commands.push(`fish: set -gx ${variable} "${value}"`);
|
|
474
|
-
else commands.push(`export ${variable}="${value}"`);
|
|
475
|
-
return commands;
|
|
476
|
-
}
|
|
477
|
-
async function ensureRegionAvailable(region, hubBaseUrl) {
|
|
478
|
-
if (hasCustomEndpoints()) return;
|
|
479
|
-
const cacheKey = `${region}`;
|
|
480
|
-
if (regionAvailabilityCache.has(cacheKey)) {
|
|
481
|
-
await regionAvailabilityCache.get(cacheKey);
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
const availabilityPromise = (async () => {
|
|
485
|
-
const statusUrl = new URL("/api/v1/status", hubBaseUrl).toString();
|
|
486
|
-
try {
|
|
487
|
-
if ((await fetch(statusUrl, {
|
|
488
|
-
signal: withTimeout(STATUS_TIMEOUT_MS),
|
|
489
|
-
headers: getHeaders()
|
|
490
|
-
})).ok) return;
|
|
491
|
-
} catch {}
|
|
492
|
-
const alternateRegion = region === "cn" ? "global" : "cn";
|
|
493
|
-
REGION_DOMAINS[alternateRegion];
|
|
494
|
-
const currentDomain = REGION_DOMAINS[region];
|
|
495
|
-
const commandList = getShellSpecificCommands("SAFESKILL_REGION", alternateRegion).map((cmd) => ` ${cmd}`).join("\n");
|
|
496
|
-
throw new Error(`SafeSkill ${region} region (${currentDomain}) is currently unavailable.\nTry these solutions:\n 1. Set environment variable:\n${commandList}\n 2. Or disable telemetry: DISABLE_TELEMETRY=1\n\nFor assistance, contact: dev@safeskill.io`);
|
|
497
|
-
})();
|
|
498
|
-
regionAvailabilityCache.set(cacheKey, availabilityPromise);
|
|
499
|
-
await availabilityPromise;
|
|
500
|
-
}
|
|
501
668
|
async function resolveSafeSkillRegion() {
|
|
502
669
|
if (resolvedRegionPromise) return resolvedRegionPromise;
|
|
503
670
|
resolvedRegionPromise = (async () => {
|
|
@@ -510,10 +677,7 @@ async function resolveSafeSkillRegion() {
|
|
|
510
677
|
}
|
|
511
678
|
async function getSafeSkillHubBaseUrl() {
|
|
512
679
|
if (process.env.SAFESKILL_HUB_URL) return process.env.SAFESKILL_HUB_URL;
|
|
513
|
-
|
|
514
|
-
const baseUrl = `https://hubapi.${REGION_DOMAINS[region]}`;
|
|
515
|
-
await ensureRegionAvailable(region, baseUrl);
|
|
516
|
-
return baseUrl;
|
|
680
|
+
return `https://hubapi.${REGION_DOMAINS[await resolveSafeSkillRegion()]}`;
|
|
517
681
|
}
|
|
518
682
|
async function getSafeSkillSearchUrlWithHub(hubBaseUrl) {
|
|
519
683
|
const configured = process.env.SAFESKILL_SEARCH_URL || process.env.SKILLS_API_URL;
|
|
@@ -753,7 +917,7 @@ async function resolveHubSkill(source, agent) {
|
|
|
753
917
|
source,
|
|
754
918
|
agent
|
|
755
919
|
})
|
|
756
|
-
}) : null;
|
|
920
|
+
}).catch(() => null) : null;
|
|
757
921
|
return resolveWithAgent && resolveWithAgent.ok ? resolveWithAgent : fetch(resolveUrl, {
|
|
758
922
|
method: "POST",
|
|
759
923
|
headers: getHeaders({ "content-type": "application/json" }),
|
|
@@ -761,15 +925,25 @@ async function resolveHubSkill(source, agent) {
|
|
|
761
925
|
});
|
|
762
926
|
};
|
|
763
927
|
let resolvedBaseUrl = baseUrl;
|
|
764
|
-
let response =
|
|
765
|
-
|
|
928
|
+
let response = null;
|
|
929
|
+
let lastError;
|
|
930
|
+
try {
|
|
931
|
+
response = await resolveFromBaseUrl(baseUrl);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
lastError = error;
|
|
934
|
+
}
|
|
935
|
+
const alternateBaseUrl = !response || response.status >= 500 ? getAlternateHubBaseUrl(baseUrl) : null;
|
|
766
936
|
if (alternateBaseUrl) {
|
|
767
|
-
const alternateResponse = await resolveFromBaseUrl(alternateBaseUrl)
|
|
768
|
-
|
|
937
|
+
const alternateResponse = await resolveFromBaseUrl(alternateBaseUrl).catch((error) => {
|
|
938
|
+
lastError = error;
|
|
939
|
+
return null;
|
|
940
|
+
});
|
|
941
|
+
if (alternateResponse && (alternateResponse.ok || !response || !response.ok)) {
|
|
769
942
|
response = alternateResponse;
|
|
770
943
|
resolvedBaseUrl = alternateBaseUrl;
|
|
771
944
|
}
|
|
772
945
|
}
|
|
946
|
+
if (!response) throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("hub resolve failed");
|
|
773
947
|
if (!response.ok) throw new Error(`hub resolve failed: ${response.status}`);
|
|
774
948
|
const resolved = await readEnvelope(response);
|
|
775
949
|
resolved.hubBaseUrl = resolvedBaseUrl;
|
|
@@ -831,6 +1005,18 @@ async function checkHubUpdates(items) {
|
|
|
831
1005
|
return readEnvelope(response);
|
|
832
1006
|
}
|
|
833
1007
|
var import_gray_matter = /* @__PURE__ */ __toESM(require_gray_matter(), 1);
|
|
1008
|
+
function parseFrontmatter(content) {
|
|
1009
|
+
const match = content.match(/^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?([\s\S]*)$/);
|
|
1010
|
+
if (!match) return {
|
|
1011
|
+
data: {},
|
|
1012
|
+
content
|
|
1013
|
+
};
|
|
1014
|
+
const parsed = (0, import_gray_matter.default)(`---\n${match[1] ?? ""}\n---\n`);
|
|
1015
|
+
return {
|
|
1016
|
+
data: parsed.data && typeof parsed.data === "object" && !Array.isArray(parsed.data) ? parsed.data : {},
|
|
1017
|
+
content: match[2] ?? ""
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
834
1020
|
function isContainedIn(targetPath, basePath) {
|
|
835
1021
|
const normalizedBase = normalize(resolve(basePath));
|
|
836
1022
|
const normalizedTarget = normalize(resolve(targetPath));
|
|
@@ -896,6 +1082,85 @@ async function getPluginGroupings(basePath) {
|
|
|
896
1082
|
} catch {}
|
|
897
1083
|
return groupings;
|
|
898
1084
|
}
|
|
1085
|
+
const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
|
|
1086
|
+
const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
|
|
1087
|
+
const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
|
|
1088
|
+
const SIMPLE_ESC_RE = /\x1b[\x20-\x2f]*[\x30-\x7e]/g;
|
|
1089
|
+
const C1_RE = /[\x80-\x9f]/g;
|
|
1090
|
+
const CONTROL_RE = /[\x00-\x08\x0b\x0c\x0d-\x1f\x7f]/g;
|
|
1091
|
+
function stripTerminalEscapes(input) {
|
|
1092
|
+
return input.replace(OSC_RE, "").replace(DCS_PM_APC_RE, "").replace(CSI_RE, "").replace(SIMPLE_ESC_RE, "").replace(C1_RE, "").replace(CONTROL_RE, "");
|
|
1093
|
+
}
|
|
1094
|
+
function sanitizeMetadata(input) {
|
|
1095
|
+
if (typeof input !== "string") return "";
|
|
1096
|
+
return stripTerminalEscapes(input).replace(/[\r\n]+/g, " ").trim();
|
|
1097
|
+
}
|
|
1098
|
+
const LOCAL_LOCK_FILE = "skills-lock.json";
|
|
1099
|
+
const CURRENT_VERSION$1 = 1;
|
|
1100
|
+
function getLocalLockPath(cwd) {
|
|
1101
|
+
return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
|
|
1102
|
+
}
|
|
1103
|
+
async function readLocalLock(cwd) {
|
|
1104
|
+
const lockPath = getLocalLockPath(cwd);
|
|
1105
|
+
try {
|
|
1106
|
+
const content = await readFile(lockPath, "utf-8");
|
|
1107
|
+
const parsed = JSON.parse(content);
|
|
1108
|
+
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
|
|
1109
|
+
if (parsed.version < CURRENT_VERSION$1) return createEmptyLocalLock();
|
|
1110
|
+
return parsed;
|
|
1111
|
+
} catch {
|
|
1112
|
+
return createEmptyLocalLock();
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async function writeLocalLock(lock, cwd) {
|
|
1116
|
+
const lockPath = getLocalLockPath(cwd);
|
|
1117
|
+
const sortedSkills = {};
|
|
1118
|
+
for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
|
|
1119
|
+
const sorted = {
|
|
1120
|
+
version: lock.version,
|
|
1121
|
+
skills: sortedSkills
|
|
1122
|
+
};
|
|
1123
|
+
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
1124
|
+
}
|
|
1125
|
+
async function computeSkillFolderHash(skillDir) {
|
|
1126
|
+
const files = [];
|
|
1127
|
+
await collectFiles(skillDir, skillDir, files);
|
|
1128
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
1129
|
+
const hash = createHash("sha256");
|
|
1130
|
+
for (const file of files) {
|
|
1131
|
+
hash.update(file.relativePath);
|
|
1132
|
+
hash.update(file.content);
|
|
1133
|
+
}
|
|
1134
|
+
return hash.digest("hex");
|
|
1135
|
+
}
|
|
1136
|
+
async function collectFiles(baseDir, currentDir, results) {
|
|
1137
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
1138
|
+
await Promise.all(entries.map(async (entry) => {
|
|
1139
|
+
const fullPath = join(currentDir, entry.name);
|
|
1140
|
+
if (entry.isDirectory()) {
|
|
1141
|
+
if (entry.name === ".git" || entry.name === "node_modules") return;
|
|
1142
|
+
await collectFiles(baseDir, fullPath, results);
|
|
1143
|
+
} else if (entry.isFile()) {
|
|
1144
|
+
const content = await readFile(fullPath);
|
|
1145
|
+
const relativePath = relative(baseDir, fullPath).split("\\").join("/");
|
|
1146
|
+
results.push({
|
|
1147
|
+
relativePath,
|
|
1148
|
+
content
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
}));
|
|
1152
|
+
}
|
|
1153
|
+
async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
1154
|
+
const lock = await readLocalLock(cwd);
|
|
1155
|
+
lock.skills[skillName] = entry;
|
|
1156
|
+
await writeLocalLock(lock, cwd);
|
|
1157
|
+
}
|
|
1158
|
+
function createEmptyLocalLock() {
|
|
1159
|
+
return {
|
|
1160
|
+
version: CURRENT_VERSION$1,
|
|
1161
|
+
skills: {}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
899
1164
|
const SKIP_DIRS = [
|
|
900
1165
|
"node_modules",
|
|
901
1166
|
".git",
|
|
@@ -903,6 +1168,61 @@ const SKIP_DIRS = [
|
|
|
903
1168
|
"build",
|
|
904
1169
|
"__pycache__"
|
|
905
1170
|
];
|
|
1171
|
+
const AGENT_PROJECT_SKILL_DIRS = [
|
|
1172
|
+
".aider-desk/skills",
|
|
1173
|
+
".agents/skills",
|
|
1174
|
+
"agent/skills",
|
|
1175
|
+
"data/skills",
|
|
1176
|
+
".autohand/skills",
|
|
1177
|
+
".adal/skills",
|
|
1178
|
+
".bob/skills",
|
|
1179
|
+
".claude/skills",
|
|
1180
|
+
".cline/skills",
|
|
1181
|
+
".codeartsdoer/skills",
|
|
1182
|
+
".codebuddy/skills",
|
|
1183
|
+
".codemaker/skills",
|
|
1184
|
+
".codestudio/skills",
|
|
1185
|
+
".codex/skills",
|
|
1186
|
+
".commandcode/skills",
|
|
1187
|
+
".continue/skills",
|
|
1188
|
+
".github/skills",
|
|
1189
|
+
".goose/skills",
|
|
1190
|
+
".devin/skills",
|
|
1191
|
+
".flocks/plugins/skills",
|
|
1192
|
+
".forge/skills",
|
|
1193
|
+
".hermes/skills",
|
|
1194
|
+
".iflow/skills",
|
|
1195
|
+
".inferencesh/skills",
|
|
1196
|
+
".jazz/skills",
|
|
1197
|
+
".junie/skills",
|
|
1198
|
+
".kilocode/skills",
|
|
1199
|
+
".kiro/skills",
|
|
1200
|
+
".mux/skills",
|
|
1201
|
+
".lingma/skills",
|
|
1202
|
+
".moxby/skills",
|
|
1203
|
+
".neovate/skills",
|
|
1204
|
+
".ona/skills",
|
|
1205
|
+
".opencode/skills",
|
|
1206
|
+
".openhands/skills",
|
|
1207
|
+
".pi/skills",
|
|
1208
|
+
".pochi/skills",
|
|
1209
|
+
".qoder/skills",
|
|
1210
|
+
".reasonix/skills",
|
|
1211
|
+
".rovodev/skills",
|
|
1212
|
+
".roo/skills",
|
|
1213
|
+
".tabnine/agent/skills",
|
|
1214
|
+
".terramind/skills",
|
|
1215
|
+
".tinycloud/skills",
|
|
1216
|
+
".trae/skills",
|
|
1217
|
+
".windsurf/skills",
|
|
1218
|
+
".zencoder/skills"
|
|
1219
|
+
];
|
|
1220
|
+
function normalizeSkillName(name) {
|
|
1221
|
+
return name.toLowerCase().replace(/[\s_]+/g, "-");
|
|
1222
|
+
}
|
|
1223
|
+
function normalizeRelativePath(path) {
|
|
1224
|
+
return path.split(sep).join("/").replace(/\/+/g, "/");
|
|
1225
|
+
}
|
|
906
1226
|
function shouldInstallInternalSkills() {
|
|
907
1227
|
const envValue = process.env.INSTALL_INTERNAL_SKILLS;
|
|
908
1228
|
return envValue === "1" || envValue === "true";
|
|
@@ -917,7 +1237,7 @@ async function hasSkillMd(dir) {
|
|
|
917
1237
|
async function parseSkillMd(skillMdPath, options) {
|
|
918
1238
|
try {
|
|
919
1239
|
const content = await readFile(skillMdPath, "utf-8");
|
|
920
|
-
const { data } = (
|
|
1240
|
+
const { data } = parseFrontmatter(content);
|
|
921
1241
|
let skillData = data;
|
|
922
1242
|
if (!skillData.name || !skillData.description) try {
|
|
923
1243
|
const metaContent = await readFile(join(dirname(skillMdPath), "_meta.json"), "utf-8");
|
|
@@ -940,9 +1260,14 @@ async function parseSkillMd(skillMdPath, options) {
|
|
|
940
1260
|
if (!skillData.name || !skillData.description) return null;
|
|
941
1261
|
if (typeof skillData.name !== "string" || typeof skillData.description !== "string") return null;
|
|
942
1262
|
if (skillData.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
|
|
1263
|
+
let name = sanitizeMetadata(skillData.name);
|
|
1264
|
+
let description = sanitizeMetadata(skillData.description);
|
|
1265
|
+
if (!name && options?.fallbackName) name = sanitizeMetadata(options.fallbackName);
|
|
1266
|
+
if (!description && options?.fallbackDescription) description = sanitizeMetadata(options.fallbackDescription);
|
|
1267
|
+
if (!name || !description) return null;
|
|
943
1268
|
return {
|
|
944
|
-
name
|
|
945
|
-
description
|
|
1269
|
+
name,
|
|
1270
|
+
description,
|
|
946
1271
|
path: dirname(skillMdPath),
|
|
947
1272
|
rawContent: content,
|
|
948
1273
|
metadata: skillData.metadata,
|
|
@@ -972,6 +1297,8 @@ function isSubpathSafe(basePath, subpath) {
|
|
|
972
1297
|
async function discoverSkills(basePath, subpath, options) {
|
|
973
1298
|
const skills = [];
|
|
974
1299
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1300
|
+
const localLock = await readLocalLock(basePath);
|
|
1301
|
+
const lockedSkillNames = new Set(Object.keys(localLock.skills).map(normalizeSkillName));
|
|
975
1302
|
if (subpath && !isSubpathSafe(basePath, subpath)) throw new Error(`Invalid subpath: "${subpath}" resolves outside the repository directory. Subpath must not contain ".." segments that escape the base path.`);
|
|
976
1303
|
const searchPath = subpath ? join(basePath, subpath) : basePath;
|
|
977
1304
|
const pluginGroupings = await getPluginGroupings(searchPath);
|
|
@@ -980,13 +1307,23 @@ async function discoverSkills(basePath, subpath, options) {
|
|
|
980
1307
|
if (pluginGroupings.has(resolvedPath)) skill.pluginName = pluginGroupings.get(resolvedPath);
|
|
981
1308
|
return skill;
|
|
982
1309
|
};
|
|
1310
|
+
const isInstalledProjectSkill = (skill) => {
|
|
1311
|
+
if (lockedSkillNames.size === 0) return false;
|
|
1312
|
+
const relativeDir = normalizeRelativePath(relative(basePath, skill.path));
|
|
1313
|
+
if (!AGENT_PROJECT_SKILL_DIRS.some((dir) => relativeDir === dir || relativeDir.startsWith(`${dir}/`))) return false;
|
|
1314
|
+
const skillName = normalizeSkillName(skill.name);
|
|
1315
|
+
const directoryName = normalizeSkillName(basename(skill.path));
|
|
1316
|
+
return lockedSkillNames.has(skillName) || lockedSkillNames.has(directoryName);
|
|
1317
|
+
};
|
|
983
1318
|
if (await hasSkillMd(searchPath)) {
|
|
984
1319
|
let skill = await parseSkillMd(join(searchPath, "SKILL.md"), options);
|
|
985
1320
|
if (skill) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1321
|
+
if (!isInstalledProjectSkill(skill)) {
|
|
1322
|
+
skill = enhanceSkill(skill);
|
|
1323
|
+
skills.push(skill);
|
|
1324
|
+
seenNames.add(skill.name);
|
|
1325
|
+
if (!options?.fullDepth) return skills;
|
|
1326
|
+
}
|
|
990
1327
|
}
|
|
991
1328
|
}
|
|
992
1329
|
const prioritySearchDirs = [
|
|
@@ -995,50 +1332,44 @@ async function discoverSkills(basePath, subpath, options) {
|
|
|
995
1332
|
join(searchPath, "skills/.curated"),
|
|
996
1333
|
join(searchPath, "skills/.experimental"),
|
|
997
1334
|
join(searchPath, "skills/.system"),
|
|
998
|
-
join(searchPath,
|
|
999
|
-
join(searchPath, ".claude/skills"),
|
|
1000
|
-
join(searchPath, ".cline/skills"),
|
|
1001
|
-
join(searchPath, ".codebuddy/skills"),
|
|
1002
|
-
join(searchPath, ".codex/skills"),
|
|
1003
|
-
join(searchPath, ".commandcode/skills"),
|
|
1004
|
-
join(searchPath, ".continue/skills"),
|
|
1005
|
-
join(searchPath, ".github/skills"),
|
|
1006
|
-
join(searchPath, ".goose/skills"),
|
|
1007
|
-
join(searchPath, ".iflow/skills"),
|
|
1008
|
-
join(searchPath, ".junie/skills"),
|
|
1009
|
-
join(searchPath, ".kilocode/skills"),
|
|
1010
|
-
join(searchPath, ".kiro/skills"),
|
|
1011
|
-
join(searchPath, ".mux/skills"),
|
|
1012
|
-
join(searchPath, ".neovate/skills"),
|
|
1013
|
-
join(searchPath, ".opencode/skills"),
|
|
1014
|
-
join(searchPath, ".openhands/skills"),
|
|
1015
|
-
join(searchPath, ".pi/skills"),
|
|
1016
|
-
join(searchPath, ".qoder/skills"),
|
|
1017
|
-
join(searchPath, ".roo/skills"),
|
|
1018
|
-
join(searchPath, ".trae/skills"),
|
|
1019
|
-
join(searchPath, ".windsurf/skills"),
|
|
1020
|
-
join(searchPath, ".zencoder/skills")
|
|
1335
|
+
...AGENT_PROJECT_SKILL_DIRS.map((dir) => join(searchPath, dir))
|
|
1021
1336
|
];
|
|
1337
|
+
const deepContainerDirs = new Set(prioritySearchDirs.slice(1));
|
|
1022
1338
|
prioritySearchDirs.push(...await getPluginSkillPaths(searchPath));
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1339
|
+
const tryAddSkillAt = async (skillDir) => {
|
|
1340
|
+
if (!await hasSkillMd(skillDir)) return false;
|
|
1341
|
+
let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
|
|
1342
|
+
if (!skill || seenNames.has(skill.name)) return true;
|
|
1343
|
+
if (isInstalledProjectSkill(skill)) return true;
|
|
1344
|
+
skill = enhanceSkill(skill);
|
|
1345
|
+
skills.push(skill);
|
|
1346
|
+
seenNames.add(skill.name);
|
|
1347
|
+
return true;
|
|
1348
|
+
};
|
|
1349
|
+
for (const dir of prioritySearchDirs) {
|
|
1350
|
+
const walkDeep = deepContainerDirs.has(dir);
|
|
1351
|
+
try {
|
|
1352
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1353
|
+
for (const entry of entries) {
|
|
1354
|
+
if (!entry.isDirectory()) continue;
|
|
1355
|
+
const childDir = join(dir, entry.name);
|
|
1356
|
+
if (await tryAddSkillAt(childDir) || !walkDeep) continue;
|
|
1357
|
+
if (SKIP_DIRS.includes(entry.name)) continue;
|
|
1358
|
+
try {
|
|
1359
|
+
const grandEntries = await readdir(childDir, { withFileTypes: true });
|
|
1360
|
+
for (const grand of grandEntries) {
|
|
1361
|
+
if (!grand.isDirectory() || SKIP_DIRS.includes(grand.name)) continue;
|
|
1362
|
+
await tryAddSkillAt(join(childDir, grand.name));
|
|
1363
|
+
}
|
|
1364
|
+
} catch {}
|
|
1034
1365
|
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1366
|
+
} catch {}
|
|
1367
|
+
}
|
|
1037
1368
|
if (skills.length === 0 || options?.fullDepth) {
|
|
1038
1369
|
const allSkillDirs = await findSkillDirs(searchPath);
|
|
1039
1370
|
for (const skillDir of allSkillDirs) {
|
|
1040
1371
|
let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
|
|
1041
|
-
if (skill && !seenNames.has(skill.name)) {
|
|
1372
|
+
if (skill && !seenNames.has(skill.name) && !isInstalledProjectSkill(skill)) {
|
|
1042
1373
|
skill = enhanceSkill(skill);
|
|
1043
1374
|
skills.push(skill);
|
|
1044
1375
|
seenNames.add(skill.name);
|
|
@@ -1062,6 +1393,19 @@ const home = homedir();
|
|
|
1062
1393
|
const configHome = xdgConfig ?? join(home, ".config");
|
|
1063
1394
|
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
1064
1395
|
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
1396
|
+
const vibeHome = process.env.VIBE_HOME?.trim() || join(home, ".vibe");
|
|
1397
|
+
const hermesHome = process.env.HERMES_HOME?.trim() || join(home, ".hermes");
|
|
1398
|
+
const autohandHome = process.env.AUTOHAND_HOME?.trim() || join(home, ".autohand");
|
|
1399
|
+
const zedAppDataHome = process.env.APPDATA?.trim();
|
|
1400
|
+
const zedFlatpakConfigHome = process.env.FLATPAK_XDG_CONFIG_HOME?.trim();
|
|
1401
|
+
function packageJsonHasDependency(packageJsonPath, dependencyName) {
|
|
1402
|
+
try {
|
|
1403
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1404
|
+
return !!(packageJson.dependencies?.[dependencyName] || packageJson.devDependencies?.[dependencyName]);
|
|
1405
|
+
} catch {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1065
1409
|
function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
|
|
1066
1410
|
if (pathExists(join(homeDir, ".openclaw"))) return join(homeDir, ".openclaw/skills");
|
|
1067
1411
|
if (pathExists(join(homeDir, ".clawdbot"))) return join(homeDir, ".clawdbot/skills");
|
|
@@ -1069,6 +1413,13 @@ function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
|
|
|
1069
1413
|
return join(homeDir, ".openclaw/skills");
|
|
1070
1414
|
}
|
|
1071
1415
|
const agents = {
|
|
1416
|
+
"aider-desk": {
|
|
1417
|
+
name: "aider-desk",
|
|
1418
|
+
displayName: "AiderDesk",
|
|
1419
|
+
skillsDir: ".aider-desk/skills",
|
|
1420
|
+
globalSkillsDir: join(home, ".aider-desk/skills"),
|
|
1421
|
+
detectInstalled: async () => existsSync(join(home, ".aider-desk"))
|
|
1422
|
+
},
|
|
1072
1423
|
amp: {
|
|
1073
1424
|
name: "amp",
|
|
1074
1425
|
displayName: "Amp",
|
|
@@ -1087,6 +1438,27 @@ const agents = {
|
|
|
1087
1438
|
return existsSync(join(home, ".gemini/antigravity"));
|
|
1088
1439
|
}
|
|
1089
1440
|
},
|
|
1441
|
+
"antigravity-cli": {
|
|
1442
|
+
name: "antigravity-cli",
|
|
1443
|
+
displayName: "Antigravity CLI",
|
|
1444
|
+
skillsDir: ".agents/skills",
|
|
1445
|
+
globalSkillsDir: join(home, ".gemini/antigravity-cli/skills"),
|
|
1446
|
+
detectInstalled: async () => existsSync(join(home, ".gemini/antigravity-cli"))
|
|
1447
|
+
},
|
|
1448
|
+
astrbot: {
|
|
1449
|
+
name: "astrbot",
|
|
1450
|
+
displayName: "AstrBot",
|
|
1451
|
+
skillsDir: "data/skills",
|
|
1452
|
+
globalSkillsDir: join(home, ".astrbot/data/skills"),
|
|
1453
|
+
detectInstalled: async () => existsSync(join(process.cwd(), "data/skills")) || existsSync(join(home, ".astrbot"))
|
|
1454
|
+
},
|
|
1455
|
+
"autohand-code": {
|
|
1456
|
+
name: "autohand-code",
|
|
1457
|
+
displayName: "Autohand Code CLI",
|
|
1458
|
+
skillsDir: ".autohand/skills",
|
|
1459
|
+
globalSkillsDir: join(autohandHome, "skills"),
|
|
1460
|
+
detectInstalled: async () => existsSync(autohandHome)
|
|
1461
|
+
},
|
|
1090
1462
|
augment: {
|
|
1091
1463
|
name: "augment",
|
|
1092
1464
|
displayName: "Augment",
|
|
@@ -1096,6 +1468,13 @@ const agents = {
|
|
|
1096
1468
|
return existsSync(join(home, ".augment"));
|
|
1097
1469
|
}
|
|
1098
1470
|
},
|
|
1471
|
+
bob: {
|
|
1472
|
+
name: "bob",
|
|
1473
|
+
displayName: "IBM Bob",
|
|
1474
|
+
skillsDir: ".bob/skills",
|
|
1475
|
+
globalSkillsDir: join(home, ".bob/skills"),
|
|
1476
|
+
detectInstalled: async () => existsSync(join(home, ".bob"))
|
|
1477
|
+
},
|
|
1099
1478
|
"claude-code": {
|
|
1100
1479
|
name: "claude-code",
|
|
1101
1480
|
displayName: "Claude Code",
|
|
@@ -1123,6 +1502,13 @@ const agents = {
|
|
|
1123
1502
|
return existsSync(join(home, ".cline"));
|
|
1124
1503
|
}
|
|
1125
1504
|
},
|
|
1505
|
+
"codearts-agent": {
|
|
1506
|
+
name: "codearts-agent",
|
|
1507
|
+
displayName: "CodeArts Agent",
|
|
1508
|
+
skillsDir: ".codeartsdoer/skills",
|
|
1509
|
+
globalSkillsDir: join(home, ".codeartsdoer/skills"),
|
|
1510
|
+
detectInstalled: async () => existsSync(join(home, ".codeartsdoer"))
|
|
1511
|
+
},
|
|
1126
1512
|
codebuddy: {
|
|
1127
1513
|
name: "codebuddy",
|
|
1128
1514
|
displayName: "CodeBuddy",
|
|
@@ -1132,6 +1518,20 @@ const agents = {
|
|
|
1132
1518
|
return existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home, ".codebuddy"));
|
|
1133
1519
|
}
|
|
1134
1520
|
},
|
|
1521
|
+
codemaker: {
|
|
1522
|
+
name: "codemaker",
|
|
1523
|
+
displayName: "Codemaker",
|
|
1524
|
+
skillsDir: ".codemaker/skills",
|
|
1525
|
+
globalSkillsDir: join(home, ".codemaker/skills"),
|
|
1526
|
+
detectInstalled: async () => existsSync(join(home, ".codemaker"))
|
|
1527
|
+
},
|
|
1528
|
+
codestudio: {
|
|
1529
|
+
name: "codestudio",
|
|
1530
|
+
displayName: "Code Studio",
|
|
1531
|
+
skillsDir: ".codestudio/skills",
|
|
1532
|
+
globalSkillsDir: join(home, ".codestudio/skills"),
|
|
1533
|
+
detectInstalled: async () => existsSync(join(home, ".codestudio"))
|
|
1534
|
+
},
|
|
1135
1535
|
codex: {
|
|
1136
1536
|
name: "codex",
|
|
1137
1537
|
displayName: "Codex",
|
|
@@ -1195,6 +1595,21 @@ const agents = {
|
|
|
1195
1595
|
return existsSync(join(home, ".deepagents"));
|
|
1196
1596
|
}
|
|
1197
1597
|
},
|
|
1598
|
+
devin: {
|
|
1599
|
+
name: "devin",
|
|
1600
|
+
displayName: "Devin for Terminal",
|
|
1601
|
+
skillsDir: ".devin/skills",
|
|
1602
|
+
globalSkillsDir: join(configHome, "devin/skills"),
|
|
1603
|
+
detectInstalled: async () => existsSync(join(configHome, "devin"))
|
|
1604
|
+
},
|
|
1605
|
+
dexto: {
|
|
1606
|
+
name: "dexto",
|
|
1607
|
+
displayName: "Dexto",
|
|
1608
|
+
skillsDir: ".agents/skills",
|
|
1609
|
+
globalSkillsDir: join(home, ".agents/skills"),
|
|
1610
|
+
showInUniversalPrompt: false,
|
|
1611
|
+
detectInstalled: async () => existsSync(join(home, ".dexto"))
|
|
1612
|
+
},
|
|
1198
1613
|
droid: {
|
|
1199
1614
|
name: "droid",
|
|
1200
1615
|
displayName: "Droid",
|
|
@@ -1204,15 +1619,33 @@ const agents = {
|
|
|
1204
1619
|
return existsSync(join(home, ".factory"));
|
|
1205
1620
|
}
|
|
1206
1621
|
},
|
|
1622
|
+
eve: {
|
|
1623
|
+
name: "eve",
|
|
1624
|
+
displayName: "Eve",
|
|
1625
|
+
skillsDir: "agent/skills",
|
|
1626
|
+
globalSkillsDir: void 0,
|
|
1627
|
+
detectInstalled: async () => {
|
|
1628
|
+
const cwd = process.cwd();
|
|
1629
|
+
return existsSync(join(cwd, "agent")) && packageJsonHasDependency(join(cwd, "package.json"), "eve");
|
|
1630
|
+
}
|
|
1631
|
+
},
|
|
1207
1632
|
firebender: {
|
|
1208
1633
|
name: "firebender",
|
|
1209
1634
|
displayName: "Firebender",
|
|
1210
1635
|
skillsDir: ".agents/skills",
|
|
1211
1636
|
globalSkillsDir: join(home, ".firebender/skills"),
|
|
1637
|
+
showInUniversalPrompt: false,
|
|
1212
1638
|
detectInstalled: async () => {
|
|
1213
1639
|
return existsSync(join(home, ".firebender"));
|
|
1214
1640
|
}
|
|
1215
1641
|
},
|
|
1642
|
+
forgecode: {
|
|
1643
|
+
name: "forgecode",
|
|
1644
|
+
displayName: "ForgeCode",
|
|
1645
|
+
skillsDir: ".forge/skills",
|
|
1646
|
+
globalSkillsDir: join(home, ".forge/skills"),
|
|
1647
|
+
detectInstalled: async () => existsSync(join(home, ".forge"))
|
|
1648
|
+
},
|
|
1216
1649
|
flocks: {
|
|
1217
1650
|
name: "flocks",
|
|
1218
1651
|
displayName: "Flocks",
|
|
@@ -1249,6 +1682,27 @@ const agents = {
|
|
|
1249
1682
|
return existsSync(join(configHome, "goose"));
|
|
1250
1683
|
}
|
|
1251
1684
|
},
|
|
1685
|
+
"hermes-agent": {
|
|
1686
|
+
name: "hermes-agent",
|
|
1687
|
+
displayName: "Hermes Agent",
|
|
1688
|
+
skillsDir: ".hermes/skills",
|
|
1689
|
+
globalSkillsDir: join(hermesHome, "skills"),
|
|
1690
|
+
detectInstalled: async () => existsSync(hermesHome)
|
|
1691
|
+
},
|
|
1692
|
+
"inference-sh": {
|
|
1693
|
+
name: "inference-sh",
|
|
1694
|
+
displayName: "inference.sh",
|
|
1695
|
+
skillsDir: ".inferencesh/skills",
|
|
1696
|
+
globalSkillsDir: join(home, ".inferencesh/skills"),
|
|
1697
|
+
detectInstalled: async () => existsSync(join(home, ".inferencesh"))
|
|
1698
|
+
},
|
|
1699
|
+
jazz: {
|
|
1700
|
+
name: "jazz",
|
|
1701
|
+
displayName: "Jazz",
|
|
1702
|
+
skillsDir: ".jazz/skills",
|
|
1703
|
+
globalSkillsDir: join(home, ".jazz/skills"),
|
|
1704
|
+
detectInstalled: async () => existsSync(join(home, ".jazz")) || existsSync(join(process.cwd(), ".jazz"))
|
|
1705
|
+
},
|
|
1252
1706
|
junie: {
|
|
1253
1707
|
name: "junie",
|
|
1254
1708
|
displayName: "Junie",
|
|
@@ -1278,13 +1732,22 @@ const agents = {
|
|
|
1278
1732
|
},
|
|
1279
1733
|
"kimi-cli": {
|
|
1280
1734
|
name: "kimi-cli",
|
|
1281
|
-
displayName: "Kimi
|
|
1735
|
+
displayName: "Kimi CLI",
|
|
1282
1736
|
skillsDir: ".agents/skills",
|
|
1283
1737
|
globalSkillsDir: join(home, ".config/agents/skills"),
|
|
1284
1738
|
detectInstalled: async () => {
|
|
1285
1739
|
return existsSync(join(home, ".kimi"));
|
|
1286
1740
|
}
|
|
1287
1741
|
},
|
|
1742
|
+
"kimi-code-cli": {
|
|
1743
|
+
name: "kimi-code-cli",
|
|
1744
|
+
displayName: "Kimi Code CLI",
|
|
1745
|
+
skillsDir: ".agents/skills",
|
|
1746
|
+
globalSkillsDir: join(home, ".agents/skills"),
|
|
1747
|
+
detectInstalled: async () => {
|
|
1748
|
+
return existsSync(join(home, ".kimi-code")) || existsSync(join(home, ".kimi"));
|
|
1749
|
+
}
|
|
1750
|
+
},
|
|
1288
1751
|
"kiro-cli": {
|
|
1289
1752
|
name: "kiro-cli",
|
|
1290
1753
|
displayName: "Kiro CLI",
|
|
@@ -1303,6 +1766,21 @@ const agents = {
|
|
|
1303
1766
|
return existsSync(join(home, ".kode"));
|
|
1304
1767
|
}
|
|
1305
1768
|
},
|
|
1769
|
+
lingma: {
|
|
1770
|
+
name: "lingma",
|
|
1771
|
+
displayName: "Lingma",
|
|
1772
|
+
skillsDir: ".lingma/skills",
|
|
1773
|
+
globalSkillsDir: join(home, ".lingma/skills"),
|
|
1774
|
+
detectInstalled: async () => existsSync(join(home, ".lingma"))
|
|
1775
|
+
},
|
|
1776
|
+
loaf: {
|
|
1777
|
+
name: "loaf",
|
|
1778
|
+
displayName: "Loaf",
|
|
1779
|
+
skillsDir: ".agents/skills",
|
|
1780
|
+
globalSkillsDir: join(home, ".agents/skills"),
|
|
1781
|
+
showInUniversalPrompt: false,
|
|
1782
|
+
detectInstalled: async () => existsSync(join(home, ".loaf"))
|
|
1783
|
+
},
|
|
1306
1784
|
mcpjam: {
|
|
1307
1785
|
name: "mcpjam",
|
|
1308
1786
|
displayName: "MCPJam",
|
|
@@ -1316,11 +1794,18 @@ const agents = {
|
|
|
1316
1794
|
name: "mistral-vibe",
|
|
1317
1795
|
displayName: "Mistral Vibe",
|
|
1318
1796
|
skillsDir: ".vibe/skills",
|
|
1319
|
-
globalSkillsDir: join(
|
|
1797
|
+
globalSkillsDir: join(vibeHome, "skills"),
|
|
1320
1798
|
detectInstalled: async () => {
|
|
1321
|
-
return existsSync(
|
|
1799
|
+
return existsSync(vibeHome);
|
|
1322
1800
|
}
|
|
1323
1801
|
},
|
|
1802
|
+
moxby: {
|
|
1803
|
+
name: "moxby",
|
|
1804
|
+
displayName: "Moxby",
|
|
1805
|
+
skillsDir: ".moxby/skills",
|
|
1806
|
+
globalSkillsDir: join(home, ".moxby/skills"),
|
|
1807
|
+
detectInstalled: async () => existsSync(join(home, ".moxby"))
|
|
1808
|
+
},
|
|
1324
1809
|
mux: {
|
|
1325
1810
|
name: "mux",
|
|
1326
1811
|
displayName: "Mux",
|
|
@@ -1348,6 +1833,13 @@ const agents = {
|
|
|
1348
1833
|
return existsSync(join(home, ".openhands"));
|
|
1349
1834
|
}
|
|
1350
1835
|
},
|
|
1836
|
+
ona: {
|
|
1837
|
+
name: "ona",
|
|
1838
|
+
displayName: "Ona",
|
|
1839
|
+
skillsDir: ".ona/skills",
|
|
1840
|
+
globalSkillsDir: join(home, ".ona/skills"),
|
|
1841
|
+
detectInstalled: async () => existsSync(join(home, ".ona"))
|
|
1842
|
+
},
|
|
1351
1843
|
pi: {
|
|
1352
1844
|
name: "pi",
|
|
1353
1845
|
displayName: "Pi",
|
|
@@ -1366,6 +1858,13 @@ const agents = {
|
|
|
1366
1858
|
return existsSync(join(home, ".qoder"));
|
|
1367
1859
|
}
|
|
1368
1860
|
},
|
|
1861
|
+
"qoder-cn": {
|
|
1862
|
+
name: "qoder-cn",
|
|
1863
|
+
displayName: "Qoder CN",
|
|
1864
|
+
skillsDir: ".qoder/skills",
|
|
1865
|
+
globalSkillsDir: join(home, ".qoder-cn/skills"),
|
|
1866
|
+
detectInstalled: async () => existsSync(join(home, ".qoder-cn"))
|
|
1867
|
+
},
|
|
1369
1868
|
"qwen-code": {
|
|
1370
1869
|
name: "qwen-code",
|
|
1371
1870
|
displayName: "Qwen Code",
|
|
@@ -1385,6 +1884,20 @@ const agents = {
|
|
|
1385
1884
|
return existsSync(join(process.cwd(), ".replit"));
|
|
1386
1885
|
}
|
|
1387
1886
|
},
|
|
1887
|
+
reasonix: {
|
|
1888
|
+
name: "reasonix",
|
|
1889
|
+
displayName: "Reasonix",
|
|
1890
|
+
skillsDir: ".reasonix/skills",
|
|
1891
|
+
globalSkillsDir: join(home, ".reasonix/skills"),
|
|
1892
|
+
detectInstalled: async () => existsSync(join(home, ".reasonix"))
|
|
1893
|
+
},
|
|
1894
|
+
rovodev: {
|
|
1895
|
+
name: "rovodev",
|
|
1896
|
+
displayName: "Rovo Dev",
|
|
1897
|
+
skillsDir: ".rovodev/skills",
|
|
1898
|
+
globalSkillsDir: join(home, ".rovodev/skills"),
|
|
1899
|
+
detectInstalled: async () => existsSync(join(home, ".rovodev"))
|
|
1900
|
+
},
|
|
1388
1901
|
roo: {
|
|
1389
1902
|
name: "roo",
|
|
1390
1903
|
displayName: "Roo Code",
|
|
@@ -1394,6 +1907,27 @@ const agents = {
|
|
|
1394
1907
|
return existsSync(join(home, ".roo"));
|
|
1395
1908
|
}
|
|
1396
1909
|
},
|
|
1910
|
+
"tabnine-cli": {
|
|
1911
|
+
name: "tabnine-cli",
|
|
1912
|
+
displayName: "Tabnine CLI",
|
|
1913
|
+
skillsDir: ".tabnine/agent/skills",
|
|
1914
|
+
globalSkillsDir: join(home, ".tabnine/agent/skills"),
|
|
1915
|
+
detectInstalled: async () => existsSync(join(home, ".tabnine"))
|
|
1916
|
+
},
|
|
1917
|
+
terramind: {
|
|
1918
|
+
name: "terramind",
|
|
1919
|
+
displayName: "Terramind",
|
|
1920
|
+
skillsDir: ".terramind/skills",
|
|
1921
|
+
globalSkillsDir: join(home, ".terramind/skills"),
|
|
1922
|
+
detectInstalled: async () => existsSync(join(home, ".terramind"))
|
|
1923
|
+
},
|
|
1924
|
+
tinycloud: {
|
|
1925
|
+
name: "tinycloud",
|
|
1926
|
+
displayName: "Tinycloud",
|
|
1927
|
+
skillsDir: ".tinycloud/skills",
|
|
1928
|
+
globalSkillsDir: join(home, ".tinycloud/skills"),
|
|
1929
|
+
detectInstalled: async () => existsSync(join(home, ".tinycloud"))
|
|
1930
|
+
},
|
|
1397
1931
|
trae: {
|
|
1398
1932
|
name: "trae",
|
|
1399
1933
|
displayName: "Trae",
|
|
@@ -1430,6 +1964,15 @@ const agents = {
|
|
|
1430
1964
|
return existsSync(join(home, ".codeium/windsurf"));
|
|
1431
1965
|
}
|
|
1432
1966
|
},
|
|
1967
|
+
zed: {
|
|
1968
|
+
name: "zed",
|
|
1969
|
+
displayName: "Zed",
|
|
1970
|
+
skillsDir: ".agents/skills",
|
|
1971
|
+
globalSkillsDir: join(home, ".agents/skills"),
|
|
1972
|
+
detectInstalled: async () => {
|
|
1973
|
+
return existsSync(join(configHome, "zed")) || !!zedAppDataHome && existsSync(join(zedAppDataHome, "Zed")) || !!zedFlatpakConfigHome && existsSync(join(zedFlatpakConfigHome, "zed"));
|
|
1974
|
+
}
|
|
1975
|
+
},
|
|
1433
1976
|
zencoder: {
|
|
1434
1977
|
name: "zencoder",
|
|
1435
1978
|
displayName: "Zencoder",
|
|
@@ -1439,6 +1982,13 @@ const agents = {
|
|
|
1439
1982
|
return existsSync(join(home, ".zencoder"));
|
|
1440
1983
|
}
|
|
1441
1984
|
},
|
|
1985
|
+
zenflow: {
|
|
1986
|
+
name: "zenflow",
|
|
1987
|
+
displayName: "Zenflow",
|
|
1988
|
+
skillsDir: ".zencoder/skills",
|
|
1989
|
+
globalSkillsDir: join(home, ".zencoder/skills"),
|
|
1990
|
+
detectInstalled: async () => existsSync(join(home, ".zencoder"))
|
|
1991
|
+
},
|
|
1442
1992
|
neovate: {
|
|
1443
1993
|
name: "neovate",
|
|
1444
1994
|
displayName: "Neovate",
|
|
@@ -1457,6 +2007,16 @@ const agents = {
|
|
|
1457
2007
|
return existsSync(join(home, ".pochi"));
|
|
1458
2008
|
}
|
|
1459
2009
|
},
|
|
2010
|
+
promptscript: {
|
|
2011
|
+
name: "promptscript",
|
|
2012
|
+
displayName: "PromptScript",
|
|
2013
|
+
skillsDir: ".agents/skills",
|
|
2014
|
+
globalSkillsDir: void 0,
|
|
2015
|
+
showInUniversalPrompt: false,
|
|
2016
|
+
detectInstalled: async () => {
|
|
2017
|
+
return existsSync(join(process.cwd(), ".promptscript")) || existsSync(join(process.cwd(), "promptscript.yaml"));
|
|
2018
|
+
}
|
|
2019
|
+
},
|
|
1460
2020
|
adal: {
|
|
1461
2021
|
name: "adal",
|
|
1462
2022
|
displayName: "AdaL",
|
|
@@ -1475,12 +2035,37 @@ const agents = {
|
|
|
1475
2035
|
detectInstalled: async () => false
|
|
1476
2036
|
}
|
|
1477
2037
|
};
|
|
2038
|
+
const agentAliases = { hermes: "hermes-agent" };
|
|
2039
|
+
function normalizeAgentType(input) {
|
|
2040
|
+
if (input in agents) return input;
|
|
2041
|
+
return agentAliases[input] ?? null;
|
|
2042
|
+
}
|
|
2043
|
+
function normalizeAgentTypes(inputs) {
|
|
2044
|
+
const normalized = inputs.map((agent) => ({
|
|
2045
|
+
input: agent,
|
|
2046
|
+
normalized: normalizeAgentType(agent)
|
|
2047
|
+
}));
|
|
2048
|
+
return {
|
|
2049
|
+
normalizedAgents: normalized.filter((agent) => Boolean(agent.normalized)).map((agent) => agent.normalized),
|
|
2050
|
+
invalidAgents: normalized.filter((agent) => !agent.normalized).map((agent) => agent.input)
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
1478
2053
|
async function detectInstalledAgents() {
|
|
1479
2054
|
return (await Promise.all(Object.entries(agents).map(async ([type, config]) => ({
|
|
1480
2055
|
type,
|
|
1481
2056
|
installed: await config.detectInstalled()
|
|
1482
2057
|
})))).filter((r) => r.installed).map((r) => r.type);
|
|
1483
2058
|
}
|
|
2059
|
+
const EVE_SUBAGENTS_DIR = join("agent", "subagents");
|
|
2060
|
+
function getEveSubagents(cwd = process.cwd()) {
|
|
2061
|
+
const dir = join(cwd, EVE_SUBAGENTS_DIR);
|
|
2062
|
+
if (!existsSync(dir)) return [];
|
|
2063
|
+
try {
|
|
2064
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
2065
|
+
} catch {
|
|
2066
|
+
return [];
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
1484
2069
|
function getUniversalAgents() {
|
|
1485
2070
|
return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type);
|
|
1486
2071
|
}
|
|
@@ -1490,7 +2075,7 @@ function getNonUniversalAgents() {
|
|
|
1490
2075
|
function isUniversalAgent(type) {
|
|
1491
2076
|
return agents[type].skillsDir === ".agents/skills";
|
|
1492
2077
|
}
|
|
1493
|
-
const AGENTS_DIR$
|
|
2078
|
+
const AGENTS_DIR$1 = ".agents";
|
|
1494
2079
|
const SKILLS_SUBDIR = "skills";
|
|
1495
2080
|
function sanitizeName(name) {
|
|
1496
2081
|
const asciiName = name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "");
|
|
@@ -1509,15 +2094,19 @@ function sanitizeName(name) {
|
|
|
1509
2094
|
function sanitizeSkillName(skill) {
|
|
1510
2095
|
return sanitizeName(skill.installName || skill.slug || skill.name);
|
|
1511
2096
|
}
|
|
1512
|
-
function isPathSafe(basePath, targetPath) {
|
|
2097
|
+
function isPathSafe$1(basePath, targetPath) {
|
|
1513
2098
|
const normalizedBase = normalize(resolve(basePath));
|
|
1514
2099
|
const normalizedTarget = normalize(resolve(targetPath));
|
|
1515
2100
|
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
1516
2101
|
}
|
|
1517
2102
|
function getCanonicalSkillsDir(global, cwd) {
|
|
1518
|
-
return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$
|
|
2103
|
+
return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
|
|
1519
2104
|
}
|
|
1520
|
-
function
|
|
2105
|
+
function getEveSubagentSkillsDir(subagent, cwd) {
|
|
2106
|
+
return join(cwd || process.cwd(), EVE_SUBAGENTS_DIR, sanitizeName(subagent), "skills");
|
|
2107
|
+
}
|
|
2108
|
+
function getAgentBaseDir(agentType, global, cwd, eveSubagent) {
|
|
2109
|
+
if (agentType === "eve" && eveSubagent) return getEveSubagentSkillsDir(eveSubagent, cwd);
|
|
1521
2110
|
if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
|
|
1522
2111
|
const agent = agents[agentType];
|
|
1523
2112
|
const baseDir = global ? homedir() : cwd || process.cwd();
|
|
@@ -1585,18 +2174,18 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1585
2174
|
error: `${agent.displayName} does not support global skill installation`
|
|
1586
2175
|
};
|
|
1587
2176
|
const skillName = sanitizeSkillName(skill);
|
|
1588
|
-
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
2177
|
+
const canonicalBase = agentType === "eve" ? getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
|
|
1589
2178
|
const canonicalDir = join(canonicalBase, skillName);
|
|
1590
|
-
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
|
|
2179
|
+
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent);
|
|
1591
2180
|
const agentDir = join(agentBase, skillName);
|
|
1592
2181
|
const installMode = options.mode ?? "symlink";
|
|
1593
|
-
if (!isPathSafe(canonicalBase, canonicalDir)) return {
|
|
2182
|
+
if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
|
|
1594
2183
|
success: false,
|
|
1595
2184
|
path: agentDir,
|
|
1596
2185
|
mode: installMode,
|
|
1597
2186
|
error: "Invalid skill name: potential path traversal detected"
|
|
1598
2187
|
};
|
|
1599
|
-
if (!isPathSafe(agentBase, agentDir)) return {
|
|
2188
|
+
if (!isPathSafe$1(agentBase, agentDir)) return {
|
|
1600
2189
|
success: false,
|
|
1601
2190
|
path: agentDir,
|
|
1602
2191
|
mode: installMode,
|
|
@@ -1605,7 +2194,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1605
2194
|
try {
|
|
1606
2195
|
if (installMode === "copy") {
|
|
1607
2196
|
await cleanAndCreateDirectory(agentDir);
|
|
1608
|
-
await copyDirectory(skill.path, agentDir);
|
|
2197
|
+
await copyDirectory(skill.path, agentDir, agentType);
|
|
1609
2198
|
return {
|
|
1610
2199
|
success: true,
|
|
1611
2200
|
path: agentDir,
|
|
@@ -1613,7 +2202,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1613
2202
|
};
|
|
1614
2203
|
}
|
|
1615
2204
|
await cleanAndCreateDirectory(canonicalDir);
|
|
1616
|
-
await copyDirectory(skill.path, canonicalDir);
|
|
2205
|
+
await copyDirectory(skill.path, canonicalDir, agentType);
|
|
1617
2206
|
if (isGlobal && isUniversalAgent(agentType)) return {
|
|
1618
2207
|
success: true,
|
|
1619
2208
|
path: canonicalDir,
|
|
@@ -1646,26 +2235,44 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1646
2235
|
};
|
|
1647
2236
|
}
|
|
1648
2237
|
}
|
|
1649
|
-
const EXCLUDE_FILES = new Set(["metadata.json"]);
|
|
1650
|
-
const EXCLUDE_DIRS = new Set([
|
|
2238
|
+
const EXCLUDE_FILES$1 = new Set(["metadata.json"]);
|
|
2239
|
+
const EXCLUDE_DIRS$1 = new Set([
|
|
1651
2240
|
".git",
|
|
1652
2241
|
"__pycache__",
|
|
1653
2242
|
"__pypackages__"
|
|
1654
2243
|
]);
|
|
1655
|
-
const isExcluded = (name, isDirectory = false) => {
|
|
1656
|
-
if (EXCLUDE_FILES.has(name)) return true;
|
|
2244
|
+
const isExcluded$1 = (name, isDirectory = false) => {
|
|
2245
|
+
if (EXCLUDE_FILES$1.has(name)) return true;
|
|
1657
2246
|
if (name.startsWith(".")) return true;
|
|
1658
|
-
if (isDirectory && EXCLUDE_DIRS.has(name)) return true;
|
|
2247
|
+
if (isDirectory && EXCLUDE_DIRS$1.has(name)) return true;
|
|
1659
2248
|
return false;
|
|
1660
2249
|
};
|
|
1661
|
-
|
|
2250
|
+
function stripIgnoredEveFrontmatter(raw) {
|
|
2251
|
+
const { data, content } = parseFrontmatter(raw);
|
|
2252
|
+
const eveData = {};
|
|
2253
|
+
if (typeof data.description === "string") eveData.description = data.description;
|
|
2254
|
+
if (typeof data.license === "string") eveData.license = data.license;
|
|
2255
|
+
if (data.metadata && typeof data.metadata === "object") {
|
|
2256
|
+
const metadata = { ...data.metadata };
|
|
2257
|
+
delete metadata.internal;
|
|
2258
|
+
if (Object.keys(metadata).length > 0) eveData.metadata = metadata;
|
|
2259
|
+
}
|
|
2260
|
+
const keys = Object.keys(eveData);
|
|
2261
|
+
if (keys.length === 0) return content.trimStart();
|
|
2262
|
+
return `---\n${keys.map((key) => `${key}: ${JSON.stringify(eveData[key])}`).join("\n")}\n---\n${content}`;
|
|
2263
|
+
}
|
|
2264
|
+
async function copyDirectory(src, dest, agentType) {
|
|
1662
2265
|
await mkdir(dest, { recursive: true });
|
|
1663
2266
|
const entries = await readdir(src, { withFileTypes: true });
|
|
1664
|
-
await Promise.all(entries.filter((entry) => !isExcluded(entry.name, entry.isDirectory())).map(async (entry) => {
|
|
2267
|
+
await Promise.all(entries.filter((entry) => !isExcluded$1(entry.name, entry.isDirectory())).map(async (entry) => {
|
|
1665
2268
|
const srcPath = join(src, entry.name);
|
|
1666
2269
|
const destPath = join(dest, entry.name);
|
|
1667
|
-
if (entry.isDirectory()) await copyDirectory(srcPath, destPath);
|
|
2270
|
+
if (entry.isDirectory()) await copyDirectory(srcPath, destPath, agentType);
|
|
1668
2271
|
else try {
|
|
2272
|
+
if (agentType === "eve" && entry.name.toLowerCase() === "skill.md") {
|
|
2273
|
+
await writeFile(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
1669
2276
|
await cp(srcPath, destPath, {
|
|
1670
2277
|
dereference: true,
|
|
1671
2278
|
recursive: true
|
|
@@ -1680,9 +2287,9 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
|
|
|
1680
2287
|
const agent = agents[agentType];
|
|
1681
2288
|
const sanitized = sanitizeName(skillName);
|
|
1682
2289
|
if (options.global && agent.globalSkillsDir === void 0) return false;
|
|
1683
|
-
const targetBase = options.global
|
|
2290
|
+
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd, options.eveSubagent);
|
|
1684
2291
|
const skillDir = join(targetBase, sanitized);
|
|
1685
|
-
if (!isPathSafe(targetBase, skillDir)) return false;
|
|
2292
|
+
if (!isPathSafe$1(targetBase, skillDir)) return false;
|
|
1686
2293
|
try {
|
|
1687
2294
|
await access(skillDir);
|
|
1688
2295
|
return true;
|
|
@@ -1694,16 +2301,16 @@ function getInstallPath(skillName, agentType, options = {}) {
|
|
|
1694
2301
|
agents[agentType];
|
|
1695
2302
|
options.cwd || process.cwd();
|
|
1696
2303
|
const sanitized = sanitizeName(skillName);
|
|
1697
|
-
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd);
|
|
2304
|
+
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd, options.eveSubagent);
|
|
1698
2305
|
const installPath = join(targetBase, sanitized);
|
|
1699
|
-
if (!isPathSafe(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
2306
|
+
if (!isPathSafe$1(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
1700
2307
|
return installPath;
|
|
1701
2308
|
}
|
|
1702
2309
|
function getCanonicalPath(skillName, options = {}) {
|
|
1703
2310
|
const sanitized = sanitizeName(skillName);
|
|
1704
2311
|
const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
|
|
1705
2312
|
const canonicalPath = join(canonicalBase, sanitized);
|
|
1706
|
-
if (!isPathSafe(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
2313
|
+
if (!isPathSafe$1(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
1707
2314
|
return canonicalPath;
|
|
1708
2315
|
}
|
|
1709
2316
|
async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
@@ -1718,17 +2325,17 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
|
1718
2325
|
error: `${agent.displayName} does not support global skill installation`
|
|
1719
2326
|
};
|
|
1720
2327
|
const skillName = sanitizeSkillName(skill);
|
|
1721
|
-
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
2328
|
+
const canonicalBase = agentType === "eve" ? getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
|
|
1722
2329
|
const canonicalDir = join(canonicalBase, skillName);
|
|
1723
|
-
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
|
|
2330
|
+
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, options.eveSubagent);
|
|
1724
2331
|
const agentDir = join(agentBase, skillName);
|
|
1725
|
-
if (!isPathSafe(canonicalBase, canonicalDir)) return {
|
|
2332
|
+
if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
|
|
1726
2333
|
success: false,
|
|
1727
2334
|
path: agentDir,
|
|
1728
2335
|
mode: installMode,
|
|
1729
2336
|
error: "Invalid skill name: potential path traversal detected"
|
|
1730
2337
|
};
|
|
1731
|
-
if (!isPathSafe(agentBase, agentDir)) return {
|
|
2338
|
+
if (!isPathSafe$1(agentBase, agentDir)) return {
|
|
1732
2339
|
success: false,
|
|
1733
2340
|
path: agentDir,
|
|
1734
2341
|
mode: installMode,
|
|
@@ -1737,10 +2344,10 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
|
1737
2344
|
async function writeSkillFiles(targetDir) {
|
|
1738
2345
|
for (const [filePath, content] of skill.files) {
|
|
1739
2346
|
const fullPath = join(targetDir, filePath);
|
|
1740
|
-
if (!isPathSafe(targetDir, fullPath)) continue;
|
|
2347
|
+
if (!isPathSafe$1(targetDir, fullPath)) continue;
|
|
1741
2348
|
const parentDir = dirname(fullPath);
|
|
1742
2349
|
if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
|
|
1743
|
-
await writeFile(fullPath, content, "utf-8");
|
|
2350
|
+
await writeFile(fullPath, agentType === "eve" && basename(filePath).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(content) : content, "utf-8");
|
|
1744
2351
|
}
|
|
1745
2352
|
}
|
|
1746
2353
|
try {
|
|
@@ -1811,6 +2418,14 @@ async function listInstalledSkills(options = {}) {
|
|
|
1811
2418
|
path: agentDir,
|
|
1812
2419
|
agentType
|
|
1813
2420
|
});
|
|
2421
|
+
if (agentType === "eve" && !isGlobal) for (const subagent of getEveSubagents(cwd)) {
|
|
2422
|
+
const subagentDir = getEveSubagentSkillsDir(subagent, cwd);
|
|
2423
|
+
if (!scopes.some((s) => s.path === subagentDir && s.global === isGlobal)) scopes.push({
|
|
2424
|
+
global: isGlobal,
|
|
2425
|
+
path: subagentDir,
|
|
2426
|
+
agentType
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
1814
2429
|
}
|
|
1815
2430
|
const allAgentTypes = Object.keys(agents);
|
|
1816
2431
|
for (const agentType of allAgentTypes) {
|
|
@@ -1824,6 +2439,14 @@ async function listInstalledSkills(options = {}) {
|
|
|
1824
2439
|
path: agentDir,
|
|
1825
2440
|
agentType
|
|
1826
2441
|
});
|
|
2442
|
+
if (agentType === "eve" && !isGlobal) for (const subagent of getEveSubagents(cwd)) {
|
|
2443
|
+
const subagentDir = getEveSubagentSkillsDir(subagent, cwd);
|
|
2444
|
+
if (!scopes.some((s) => s.path === subagentDir && s.global === isGlobal)) scopes.push({
|
|
2445
|
+
global: isGlobal,
|
|
2446
|
+
path: subagentDir,
|
|
2447
|
+
agentType
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
1827
2450
|
}
|
|
1828
2451
|
}
|
|
1829
2452
|
for (const scope of scopes) try {
|
|
@@ -1869,7 +2492,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
1869
2492
|
]));
|
|
1870
2493
|
for (const possibleName of possibleNames) {
|
|
1871
2494
|
const agentSkillDir = join(agentBase, possibleName);
|
|
1872
|
-
if (!isPathSafe(agentBase, agentSkillDir)) continue;
|
|
2495
|
+
if (!isPathSafe$1(agentBase, agentSkillDir)) continue;
|
|
1873
2496
|
try {
|
|
1874
2497
|
await access(agentSkillDir);
|
|
1875
2498
|
found = true;
|
|
@@ -1881,7 +2504,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
1881
2504
|
for (const agentEntry of agentEntries) {
|
|
1882
2505
|
if (!agentEntry.isDirectory()) continue;
|
|
1883
2506
|
const candidateDir = join(agentBase, agentEntry.name);
|
|
1884
|
-
if (!isPathSafe(agentBase, candidateDir)) continue;
|
|
2507
|
+
if (!isPathSafe$1(agentBase, candidateDir)) continue;
|
|
1885
2508
|
try {
|
|
1886
2509
|
const candidateSkillMd = join(candidateDir, "SKILL.md");
|
|
1887
2510
|
await stat(candidateSkillMd);
|
|
@@ -1912,6 +2535,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
1912
2535
|
}
|
|
1913
2536
|
const AUDIT_URL = "https://add-skill.vercel.sh/audit";
|
|
1914
2537
|
let cliVersion = null;
|
|
2538
|
+
let detectedAgentName = null;
|
|
1915
2539
|
function isCI() {
|
|
1916
2540
|
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
|
|
1917
2541
|
}
|
|
@@ -1921,6 +2545,9 @@ function isEnabled() {
|
|
|
1921
2545
|
function setVersion(version) {
|
|
1922
2546
|
cliVersion = version;
|
|
1923
2547
|
}
|
|
2548
|
+
function setDetectedAgent(agentName) {
|
|
2549
|
+
detectedAgentName = agentName;
|
|
2550
|
+
}
|
|
1924
2551
|
async function fetchAuditData(source, skillSlugs, timeoutMs = 3e3) {
|
|
1925
2552
|
if (skillSlugs.length === 0) return null;
|
|
1926
2553
|
try {
|
|
@@ -1945,6 +2572,7 @@ function track(data) {
|
|
|
1945
2572
|
const params = new URLSearchParams();
|
|
1946
2573
|
if (cliVersion) params.set("v", cliVersion);
|
|
1947
2574
|
if (isCI()) params.set("ci", "1");
|
|
2575
|
+
if (detectedAgentName) params.set("agent", detectedAgentName);
|
|
1948
2576
|
for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
|
|
1949
2577
|
const telemetryUrl = await getSafeSkillTelemetryUrl();
|
|
1950
2578
|
fetch(`${telemetryUrl}?${params.toString()}`).catch(() => {});
|
|
@@ -1989,6 +2617,15 @@ var WellKnownProvider = class {
|
|
|
1989
2617
|
}
|
|
1990
2618
|
}
|
|
1991
2619
|
async fetchIndex(baseUrl) {
|
|
2620
|
+
const result = await this.fetchIndexForUse(baseUrl);
|
|
2621
|
+
if (!result) return null;
|
|
2622
|
+
return {
|
|
2623
|
+
index: result.index,
|
|
2624
|
+
resolvedBaseUrl: result.resolvedBaseUrl,
|
|
2625
|
+
resolvedWellKnownPath: result.resolvedWellKnownPath
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
async fetchIndexForUse(baseUrl) {
|
|
1992
2629
|
try {
|
|
1993
2630
|
const parsed = new URL(baseUrl);
|
|
1994
2631
|
const basePath = parsed.pathname.replace(/\/$/, "");
|
|
@@ -2011,12 +2648,21 @@ var WellKnownProvider = class {
|
|
|
2011
2648
|
const index = await response.json();
|
|
2012
2649
|
if (!index.skills || !Array.isArray(index.skills)) continue;
|
|
2013
2650
|
let allValid = true;
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2651
|
+
const originalEntries = [];
|
|
2652
|
+
const sanitizedEntries = [];
|
|
2653
|
+
for (const entry of index.skills) {
|
|
2654
|
+
if (!this.isValidSkillEntry(entry)) {
|
|
2655
|
+
allValid = false;
|
|
2656
|
+
break;
|
|
2657
|
+
}
|
|
2658
|
+
const sanitizedEntry = this.sanitizeSkillEntry(entry);
|
|
2659
|
+
if (!sanitizedEntry) continue;
|
|
2660
|
+
originalEntries.push(entry);
|
|
2661
|
+
sanitizedEntries.push(sanitizedEntry);
|
|
2017
2662
|
}
|
|
2018
|
-
if (allValid) return {
|
|
2019
|
-
index,
|
|
2663
|
+
if (allValid && sanitizedEntries.length > 0) return {
|
|
2664
|
+
index: { skills: sanitizedEntries },
|
|
2665
|
+
originalEntries,
|
|
2020
2666
|
resolvedBaseUrl: resolvedBase,
|
|
2021
2667
|
resolvedWellKnownPath: wellKnownPath
|
|
2022
2668
|
};
|
|
@@ -2034,28 +2680,41 @@ var WellKnownProvider = class {
|
|
|
2034
2680
|
if (typeof e.name !== "string" || !e.name) return false;
|
|
2035
2681
|
if (typeof e.description !== "string" || !e.description) return false;
|
|
2036
2682
|
if (!Array.isArray(e.files) || e.files.length === 0) return false;
|
|
2037
|
-
if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(e.name)
|
|
2038
|
-
if (e.name.length === 1 && !/^[a-z0-9]$/.test(e.name)) return false;
|
|
2039
|
-
}
|
|
2683
|
+
if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(e.name)) return false;
|
|
2040
2684
|
for (const file of e.files) {
|
|
2041
2685
|
if (typeof file !== "string") return false;
|
|
2042
2686
|
if (file.startsWith("/") || file.startsWith("\\") || file.includes("..")) return false;
|
|
2687
|
+
if (!this.isSafeWellKnownFilePath(file)) return false;
|
|
2043
2688
|
}
|
|
2044
2689
|
if (!e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md")) return false;
|
|
2045
2690
|
return true;
|
|
2046
2691
|
}
|
|
2692
|
+
isSafeWellKnownFilePath(file) {
|
|
2693
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(file)) return false;
|
|
2694
|
+
return stripTerminalEscapes(file) === file;
|
|
2695
|
+
}
|
|
2696
|
+
sanitizeSkillEntry(entry) {
|
|
2697
|
+
const name = sanitizeMetadata(entry.name);
|
|
2698
|
+
const description = sanitizeMetadata(entry.description);
|
|
2699
|
+
if (!name || !description) return null;
|
|
2700
|
+
return {
|
|
2701
|
+
...entry,
|
|
2702
|
+
name,
|
|
2703
|
+
description
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2047
2706
|
async fetchSkill(url) {
|
|
2048
2707
|
try {
|
|
2049
2708
|
const parsed = new URL(url);
|
|
2050
|
-
const result = await this.
|
|
2709
|
+
const result = await this.fetchIndexForUse(url);
|
|
2051
2710
|
if (!result) return null;
|
|
2052
|
-
const { index, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2711
|
+
const { index, originalEntries, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2053
2712
|
let skillName = null;
|
|
2054
2713
|
const pathMatch = parsed.pathname.match(/\/.well-known\/(?:agent-skills|skills)\/([^/]+)\/?$/);
|
|
2055
2714
|
if (pathMatch && pathMatch[1] && pathMatch[1] !== "index.json") skillName = pathMatch[1];
|
|
2056
|
-
else if (index.skills.length === 1) skillName =
|
|
2715
|
+
else if (index.skills.length === 1) skillName = originalEntries[0].name;
|
|
2057
2716
|
if (!skillName) return null;
|
|
2058
|
-
const skillEntry =
|
|
2717
|
+
const skillEntry = originalEntries.find((s) => s.name === skillName);
|
|
2059
2718
|
if (!skillEntry) return null;
|
|
2060
2719
|
return this.fetchSkillByEntry(resolvedBaseUrl, skillEntry, resolvedWellKnownPath);
|
|
2061
2720
|
} catch {
|
|
@@ -2065,13 +2724,16 @@ var WellKnownProvider = class {
|
|
|
2065
2724
|
async fetchSkillByEntry(baseUrl, entry, wellKnownPath) {
|
|
2066
2725
|
try {
|
|
2067
2726
|
const resolvedPath = wellKnownPath ?? this.WELL_KNOWN_PATHS[0];
|
|
2727
|
+
if (!this.isValidSkillEntry(entry)) return null;
|
|
2728
|
+
const sanitizedEntry = this.sanitizeSkillEntry(entry);
|
|
2729
|
+
if (!sanitizedEntry) return null;
|
|
2068
2730
|
const skillBaseUrl = `${baseUrl.replace(/\/$/, "")}/${resolvedPath}/${entry.name}`;
|
|
2069
2731
|
const skillMdUrl = `${skillBaseUrl}/SKILL.md`;
|
|
2070
2732
|
const response = await fetch(skillMdUrl, { headers: getHeaders() });
|
|
2071
2733
|
if (!response.ok) return null;
|
|
2072
2734
|
const content = await response.text();
|
|
2073
|
-
const { data } = (
|
|
2074
|
-
if (!data.name || !data.description) return null;
|
|
2735
|
+
const { data } = parseFrontmatter(content);
|
|
2736
|
+
if (typeof data.name !== "string" || typeof data.description !== "string" || !data.name || !data.description) return null;
|
|
2075
2737
|
const files = /* @__PURE__ */ new Map();
|
|
2076
2738
|
files.set("SKILL.md", content);
|
|
2077
2739
|
const filePromises = entry.files.filter((f) => f.toLowerCase() !== "skill.md").map(async (filePath) => {
|
|
@@ -2087,15 +2749,18 @@ var WellKnownProvider = class {
|
|
|
2087
2749
|
});
|
|
2088
2750
|
const fileResults = await Promise.all(filePromises);
|
|
2089
2751
|
for (const result of fileResults) if (result) files.set(result.path, result.content);
|
|
2752
|
+
const name = sanitizeMetadata(data.name);
|
|
2753
|
+
const description = sanitizeMetadata(data.description);
|
|
2754
|
+
if (!name || !description) return null;
|
|
2090
2755
|
return {
|
|
2091
|
-
name
|
|
2092
|
-
description
|
|
2756
|
+
name,
|
|
2757
|
+
description,
|
|
2093
2758
|
content,
|
|
2094
2759
|
installName: entry.name,
|
|
2095
2760
|
sourceUrl: skillMdUrl,
|
|
2096
2761
|
metadata: data.metadata,
|
|
2097
2762
|
files,
|
|
2098
|
-
indexEntry:
|
|
2763
|
+
indexEntry: sanitizedEntry
|
|
2099
2764
|
};
|
|
2100
2765
|
} catch {
|
|
2101
2766
|
return null;
|
|
@@ -2103,10 +2768,10 @@ var WellKnownProvider = class {
|
|
|
2103
2768
|
}
|
|
2104
2769
|
async fetchAllSkills(url) {
|
|
2105
2770
|
try {
|
|
2106
|
-
const result = await this.
|
|
2771
|
+
const result = await this.fetchIndexForUse(url);
|
|
2107
2772
|
if (!result) return [];
|
|
2108
|
-
const {
|
|
2109
|
-
const skillPromises =
|
|
2773
|
+
const { originalEntries, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2774
|
+
const skillPromises = originalEntries.map((entry) => this.fetchSkillByEntry(resolvedBaseUrl, entry, resolvedWellKnownPath));
|
|
2110
2775
|
return (await Promise.all(skillPromises)).filter((s) => s !== null);
|
|
2111
2776
|
} catch {
|
|
2112
2777
|
return [];
|
|
@@ -2140,28 +2805,28 @@ var WellKnownProvider = class {
|
|
|
2140
2805
|
}
|
|
2141
2806
|
};
|
|
2142
2807
|
const wellKnownProvider = new WellKnownProvider();
|
|
2143
|
-
const AGENTS_DIR
|
|
2144
|
-
const LOCK_FILE
|
|
2145
|
-
const CURRENT_VERSION
|
|
2146
|
-
function getSkillLockPath
|
|
2808
|
+
const AGENTS_DIR = ".agents";
|
|
2809
|
+
const LOCK_FILE = ".skill-lock.json";
|
|
2810
|
+
const CURRENT_VERSION = 4;
|
|
2811
|
+
function getSkillLockPath() {
|
|
2147
2812
|
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
2148
|
-
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE
|
|
2149
|
-
return join(homedir(), AGENTS_DIR
|
|
2813
|
+
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
|
|
2814
|
+
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
2150
2815
|
}
|
|
2151
|
-
async function readSkillLock
|
|
2152
|
-
const lockPath = getSkillLockPath
|
|
2816
|
+
async function readSkillLock() {
|
|
2817
|
+
const lockPath = getSkillLockPath();
|
|
2153
2818
|
try {
|
|
2154
2819
|
const content = await readFile(lockPath, "utf-8");
|
|
2155
2820
|
const parsed = JSON.parse(content);
|
|
2156
2821
|
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLockFile();
|
|
2157
|
-
if (parsed.version < CURRENT_VERSION
|
|
2822
|
+
if (parsed.version < CURRENT_VERSION) return createEmptyLockFile();
|
|
2158
2823
|
return parsed;
|
|
2159
2824
|
} catch (error) {
|
|
2160
2825
|
return createEmptyLockFile();
|
|
2161
2826
|
}
|
|
2162
2827
|
}
|
|
2163
2828
|
async function writeSkillLock(lock) {
|
|
2164
|
-
const lockPath = getSkillLockPath
|
|
2829
|
+
const lockPath = getSkillLockPath();
|
|
2165
2830
|
await mkdir(dirname(lockPath), { recursive: true });
|
|
2166
2831
|
await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
2167
2832
|
}
|
|
@@ -2181,28 +2846,14 @@ function getGitHubToken() {
|
|
|
2181
2846
|
} catch {}
|
|
2182
2847
|
return null;
|
|
2183
2848
|
}
|
|
2184
|
-
async function fetchSkillFolderHash(ownerRepo, skillPath,
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
for (const branch of ["main", "master"]) try {
|
|
2190
|
-
const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`;
|
|
2191
|
-
const headers = getHeaders({ Accept: "application/vnd.github.v3+json" });
|
|
2192
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
2193
|
-
const response = await fetch(url, { headers });
|
|
2194
|
-
if (!response.ok) continue;
|
|
2195
|
-
const data = await response.json();
|
|
2196
|
-
if (!folderPath) return data.sha;
|
|
2197
|
-
const folderEntry = data.tree.find((entry) => entry.type === "tree" && entry.path === folderPath);
|
|
2198
|
-
if (folderEntry) return folderEntry.sha;
|
|
2199
|
-
} catch {
|
|
2200
|
-
continue;
|
|
2201
|
-
}
|
|
2202
|
-
return null;
|
|
2849
|
+
async function fetchSkillFolderHash(ownerRepo, skillPath, getToken, ref) {
|
|
2850
|
+
const { fetchRepoTree, getSkillFolderHashFromTree } = await Promise.resolve().then(() => blob_exports);
|
|
2851
|
+
const tree = await fetchRepoTree(ownerRepo, ref, getToken ?? void 0);
|
|
2852
|
+
if (!tree) return null;
|
|
2853
|
+
return getSkillFolderHashFromTree(tree, skillPath);
|
|
2203
2854
|
}
|
|
2204
2855
|
async function addSkillToLock(skillName, entry) {
|
|
2205
|
-
const lock = await readSkillLock
|
|
2856
|
+
const lock = await readSkillLock();
|
|
2206
2857
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2207
2858
|
const existingEntry = lock.skills[skillName];
|
|
2208
2859
|
lock.skills[skillName] = {
|
|
@@ -2213,109 +2864,43 @@ async function addSkillToLock(skillName, entry) {
|
|
|
2213
2864
|
await writeSkillLock(lock);
|
|
2214
2865
|
}
|
|
2215
2866
|
async function removeSkillFromLock(skillName) {
|
|
2216
|
-
const lock = await readSkillLock
|
|
2867
|
+
const lock = await readSkillLock();
|
|
2217
2868
|
if (!(skillName in lock.skills)) return false;
|
|
2218
2869
|
delete lock.skills[skillName];
|
|
2219
2870
|
await writeSkillLock(lock);
|
|
2220
2871
|
return true;
|
|
2221
2872
|
}
|
|
2222
2873
|
async function getSkillFromLock(skillName) {
|
|
2223
|
-
return (await readSkillLock
|
|
2874
|
+
return (await readSkillLock()).skills[skillName] ?? null;
|
|
2224
2875
|
}
|
|
2225
2876
|
async function getAllLockedSkills() {
|
|
2226
|
-
return (await readSkillLock
|
|
2877
|
+
return (await readSkillLock()).skills;
|
|
2227
2878
|
}
|
|
2228
2879
|
function createEmptyLockFile() {
|
|
2229
2880
|
return {
|
|
2230
|
-
version: CURRENT_VERSION
|
|
2881
|
+
version: CURRENT_VERSION,
|
|
2231
2882
|
skills: {},
|
|
2232
2883
|
dismissed: {}
|
|
2233
2884
|
};
|
|
2234
2885
|
}
|
|
2235
2886
|
async function isPromptDismissed(promptKey) {
|
|
2236
|
-
return (await readSkillLock
|
|
2887
|
+
return (await readSkillLock()).dismissed?.[promptKey] === true;
|
|
2237
2888
|
}
|
|
2238
2889
|
async function dismissPrompt(promptKey) {
|
|
2239
|
-
const lock = await readSkillLock
|
|
2890
|
+
const lock = await readSkillLock();
|
|
2240
2891
|
if (!lock.dismissed) lock.dismissed = {};
|
|
2241
2892
|
lock.dismissed[promptKey] = true;
|
|
2242
2893
|
await writeSkillLock(lock);
|
|
2243
2894
|
}
|
|
2244
2895
|
async function getLastSelectedAgents() {
|
|
2245
|
-
return (await readSkillLock
|
|
2896
|
+
return (await readSkillLock()).lastSelectedAgents;
|
|
2246
2897
|
}
|
|
2247
2898
|
async function saveSelectedAgents(agents) {
|
|
2248
|
-
const lock = await readSkillLock
|
|
2899
|
+
const lock = await readSkillLock();
|
|
2249
2900
|
lock.lastSelectedAgents = agents;
|
|
2250
2901
|
await writeSkillLock(lock);
|
|
2251
2902
|
}
|
|
2252
|
-
|
|
2253
|
-
const CURRENT_VERSION = 1;
|
|
2254
|
-
function getLocalLockPath(cwd) {
|
|
2255
|
-
return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
|
|
2256
|
-
}
|
|
2257
|
-
async function readLocalLock(cwd) {
|
|
2258
|
-
const lockPath = getLocalLockPath(cwd);
|
|
2259
|
-
try {
|
|
2260
|
-
const content = await readFile(lockPath, "utf-8");
|
|
2261
|
-
const parsed = JSON.parse(content);
|
|
2262
|
-
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
|
|
2263
|
-
if (parsed.version < CURRENT_VERSION) return createEmptyLocalLock();
|
|
2264
|
-
return parsed;
|
|
2265
|
-
} catch {
|
|
2266
|
-
return createEmptyLocalLock();
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
async function writeLocalLock(lock, cwd) {
|
|
2270
|
-
const lockPath = getLocalLockPath(cwd);
|
|
2271
|
-
const sortedSkills = {};
|
|
2272
|
-
for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
|
|
2273
|
-
const sorted = {
|
|
2274
|
-
version: lock.version,
|
|
2275
|
-
skills: sortedSkills
|
|
2276
|
-
};
|
|
2277
|
-
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
2278
|
-
}
|
|
2279
|
-
async function computeSkillFolderHash(skillDir) {
|
|
2280
|
-
const files = [];
|
|
2281
|
-
await collectFiles(skillDir, skillDir, files);
|
|
2282
|
-
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
2283
|
-
const hash = createHash("sha256");
|
|
2284
|
-
for (const file of files) {
|
|
2285
|
-
hash.update(file.relativePath);
|
|
2286
|
-
hash.update(file.content);
|
|
2287
|
-
}
|
|
2288
|
-
return hash.digest("hex");
|
|
2289
|
-
}
|
|
2290
|
-
async function collectFiles(baseDir, currentDir, results) {
|
|
2291
|
-
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
2292
|
-
await Promise.all(entries.map(async (entry) => {
|
|
2293
|
-
const fullPath = join(currentDir, entry.name);
|
|
2294
|
-
if (entry.isDirectory()) {
|
|
2295
|
-
if (entry.name === ".git" || entry.name === "node_modules") return;
|
|
2296
|
-
await collectFiles(baseDir, fullPath, results);
|
|
2297
|
-
} else if (entry.isFile()) {
|
|
2298
|
-
const content = await readFile(fullPath);
|
|
2299
|
-
const relativePath = relative(baseDir, fullPath).split("\\").join("/");
|
|
2300
|
-
results.push({
|
|
2301
|
-
relativePath,
|
|
2302
|
-
content
|
|
2303
|
-
});
|
|
2304
|
-
}
|
|
2305
|
-
}));
|
|
2306
|
-
}
|
|
2307
|
-
async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
2308
|
-
const lock = await readLocalLock(cwd);
|
|
2309
|
-
lock.skills[skillName] = entry;
|
|
2310
|
-
await writeLocalLock(lock, cwd);
|
|
2311
|
-
}
|
|
2312
|
-
function createEmptyLocalLock() {
|
|
2313
|
-
return {
|
|
2314
|
-
version: CURRENT_VERSION,
|
|
2315
|
-
skills: {}
|
|
2316
|
-
};
|
|
2317
|
-
}
|
|
2318
|
-
var version$1 = "0.2.3";
|
|
2903
|
+
var version$1 = "0.3.1";
|
|
2319
2904
|
const isCancelled$1 = (value) => typeof value === "symbol";
|
|
2320
2905
|
async function isSourcePrivate(source) {
|
|
2321
2906
|
const ownerRepo = parseOwnerRepo(source);
|
|
@@ -2325,6 +2910,12 @@ async function isSourcePrivate(source) {
|
|
|
2325
2910
|
function initTelemetry(version) {
|
|
2326
2911
|
setVersion(version);
|
|
2327
2912
|
}
|
|
2913
|
+
function getLockSourceForParsedSource(parsed) {
|
|
2914
|
+
const normalizedSource = getOwnerRepo(parsed);
|
|
2915
|
+
if (parsed.type === "github") return normalizedSource;
|
|
2916
|
+
if (parsed.type === "gitlab" || parsed.type === "git") return parsed.url;
|
|
2917
|
+
return normalizedSource;
|
|
2918
|
+
}
|
|
2328
2919
|
function riskLabel(risk) {
|
|
2329
2920
|
switch (risk) {
|
|
2330
2921
|
case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical Risk"));
|
|
@@ -2548,13 +3139,13 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
2548
3139
|
targetAgents = validAgents;
|
|
2549
3140
|
M.info(`Installing to all ${targetAgents.length} agents`);
|
|
2550
3141
|
} else if (options.agent && options.agent.length > 0) {
|
|
2551
|
-
const invalidAgents = options.agent
|
|
3142
|
+
const { normalizedAgents, invalidAgents } = normalizeAgentTypes(options.agent);
|
|
2552
3143
|
if (invalidAgents.length > 0) {
|
|
2553
3144
|
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
2554
3145
|
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
2555
3146
|
process.exit(1);
|
|
2556
3147
|
}
|
|
2557
|
-
targetAgents =
|
|
3148
|
+
targetAgents = normalizedAgents;
|
|
2558
3149
|
} else {
|
|
2559
3150
|
spinner.start("Loading agents...");
|
|
2560
3151
|
const installedAgents = await detectInstalledAgents();
|
|
@@ -2960,14 +3551,14 @@ async function runAdd(args, options = {}) {
|
|
|
2960
3551
|
targetAgents = validAgents;
|
|
2961
3552
|
M.info(`Installing to all ${targetAgents.length} agents`);
|
|
2962
3553
|
} else if (options.agent && options.agent.length > 0) {
|
|
2963
|
-
const invalidAgents = options.agent
|
|
3554
|
+
const { normalizedAgents, invalidAgents } = normalizeAgentTypes(options.agent);
|
|
2964
3555
|
if (invalidAgents.length > 0) {
|
|
2965
3556
|
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
2966
3557
|
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
2967
3558
|
await cleanup(tempDir);
|
|
2968
3559
|
process.exit(1);
|
|
2969
3560
|
}
|
|
2970
|
-
targetAgents =
|
|
3561
|
+
targetAgents = normalizedAgents;
|
|
2971
3562
|
} else {
|
|
2972
3563
|
spinner.start("Loading agents...");
|
|
2973
3564
|
const installedAgents = await detectInstalledAgents();
|
|
@@ -3141,7 +3732,7 @@ async function runAdd(args, options = {}) {
|
|
|
3141
3732
|
skillFiles[skill.name] = relativePath;
|
|
3142
3733
|
}
|
|
3143
3734
|
const normalizedSource = getOwnerRepo(parsed);
|
|
3144
|
-
const lockSource = parsed
|
|
3735
|
+
const lockSource = getLockSourceForParsedSource(parsed);
|
|
3145
3736
|
if (normalizedSource) {
|
|
3146
3737
|
const ownerRepo = parseOwnerRepo(normalizedSource);
|
|
3147
3738
|
if (ownerRepo) {
|
|
@@ -3170,13 +3761,14 @@ async function runAdd(args, options = {}) {
|
|
|
3170
3761
|
let skillFolderHash = "";
|
|
3171
3762
|
const skillPathValue = skillFiles[skill.name];
|
|
3172
3763
|
if (parsed.type === "github" && normalizedSource && skillPathValue) {
|
|
3173
|
-
const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken
|
|
3764
|
+
const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken, parsed.ref);
|
|
3174
3765
|
if (hash) skillFolderHash = hash;
|
|
3175
3766
|
}
|
|
3176
3767
|
await addSkillToLock(skill.name, {
|
|
3177
3768
|
source: lockSource || normalizedSource || parsed.url,
|
|
3178
3769
|
sourceType: parsed.type,
|
|
3179
3770
|
sourceUrl: parsed.url,
|
|
3771
|
+
ref: parsed.ref,
|
|
3180
3772
|
skillPath: skillPathValue,
|
|
3181
3773
|
skillFolderHash,
|
|
3182
3774
|
pluginName: skill.pluginName,
|
|
@@ -3192,10 +3784,15 @@ async function runAdd(args, options = {}) {
|
|
|
3192
3784
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
3193
3785
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
3194
3786
|
const computedHash = await computeSkillFolderHash(skill.path);
|
|
3787
|
+
const skillPathValue = skillFiles[skill.name];
|
|
3195
3788
|
await addSkillToLocalLock(skill.name, {
|
|
3196
3789
|
source: lockSource || parsed.url,
|
|
3790
|
+
ref: parsed.ref,
|
|
3197
3791
|
sourceType: parsed.type,
|
|
3198
|
-
|
|
3792
|
+
skillPath: skillPathValue,
|
|
3793
|
+
computedHash,
|
|
3794
|
+
resolvedVersion: parsed.type === "hub" ? hubResolved?.version : void 0,
|
|
3795
|
+
artifactDigest: parsed.type === "hub" ? hubResolved?.artifact_digest : void 0
|
|
3199
3796
|
}, cwd);
|
|
3200
3797
|
} catch {}
|
|
3201
3798
|
}
|
|
@@ -3361,10 +3958,21 @@ function parseAddOptions(args) {
|
|
|
3361
3958
|
options
|
|
3362
3959
|
};
|
|
3363
3960
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3961
|
+
var import_dist = require_dist();
|
|
3962
|
+
let cachedResult = null;
|
|
3963
|
+
async function detectAgent() {
|
|
3964
|
+
if (cachedResult) return cachedResult;
|
|
3965
|
+
cachedResult = await (0, import_dist.determineAgent)();
|
|
3966
|
+
if (cachedResult.isAgent) setDetectedAgent(cachedResult.agent.name);
|
|
3967
|
+
return cachedResult;
|
|
3968
|
+
}
|
|
3969
|
+
async function isRunningInAgent() {
|
|
3970
|
+
return (await detectAgent()).isAgent;
|
|
3971
|
+
}
|
|
3972
|
+
const RESET$3 = "\x1B[0m";
|
|
3973
|
+
const BOLD$3 = "\x1B[1m";
|
|
3974
|
+
const DIM$3 = "\x1B[38;5;102m";
|
|
3975
|
+
const TEXT$2 = "\x1B[38;5;145m";
|
|
3368
3976
|
const CYAN$1 = "\x1B[36m";
|
|
3369
3977
|
const GREEN = "\x1B[32m";
|
|
3370
3978
|
const YELLOW$1 = "\x1B[33m";
|
|
@@ -3376,7 +3984,7 @@ function formatCount(count) {
|
|
|
3376
3984
|
}
|
|
3377
3985
|
function formatTrustScore(score) {
|
|
3378
3986
|
if (score === void 0 || score === null) return "";
|
|
3379
|
-
return `${score >= 80 ? GREEN : score >= 60 ? YELLOW$1 : RED}TrustScore:${score}${RESET$
|
|
3987
|
+
return `${score >= 80 ? GREEN : score >= 60 ? YELLOW$1 : RED}TrustScore:${score}${RESET$3}`;
|
|
3380
3988
|
}
|
|
3381
3989
|
function formatRelativeDate(ts) {
|
|
3382
3990
|
if (!ts) return "";
|
|
@@ -3391,8 +3999,8 @@ function formatRelativeDate(ts) {
|
|
|
3391
3999
|
}
|
|
3392
4000
|
function formatStatsBadges(installs, stars) {
|
|
3393
4001
|
const parts = [];
|
|
3394
|
-
if (installs > 0) parts.push(`${CYAN$1}↓ ${formatCount(installs)}${RESET$
|
|
3395
|
-
if (stars > 0) parts.push(`${YELLOW$1}★ ${formatCount(stars)}${RESET$
|
|
4002
|
+
if (installs > 0) parts.push(`${CYAN$1}↓ ${formatCount(installs)}${RESET$3}`);
|
|
4003
|
+
if (stars > 0) parts.push(`${YELLOW$1}★ ${formatCount(stars)}${RESET$3}`);
|
|
3396
4004
|
return parts.join(" ");
|
|
3397
4005
|
}
|
|
3398
4006
|
async function searchSkillsAPI(query) {
|
|
@@ -3418,29 +4026,29 @@ async function runSearchPrompt(initialQuery = "") {
|
|
|
3418
4026
|
if (lastRenderedLines > 0) process.stdout.write(MOVE_UP(lastRenderedLines) + MOVE_TO_COL(1));
|
|
3419
4027
|
process.stdout.write(CLEAR_DOWN);
|
|
3420
4028
|
const lines = [];
|
|
3421
|
-
const cursor = `${BOLD$
|
|
3422
|
-
lines.push(`${TEXT$
|
|
4029
|
+
const cursor = `${BOLD$3}_${RESET$3}`;
|
|
4030
|
+
lines.push(`${TEXT$2}Search skills:${RESET$3} ${query}${cursor}`);
|
|
3423
4031
|
lines.push("");
|
|
3424
|
-
if (!query || query.length < 2) lines.push(`${DIM$
|
|
3425
|
-
else if (results.length === 0 && loading) lines.push(`${DIM$
|
|
3426
|
-
else if (results.length === 0) lines.push(`${DIM$
|
|
4032
|
+
if (!query || query.length < 2) lines.push(`${DIM$3}Start typing to search (min 2 chars)${RESET$3}`);
|
|
4033
|
+
else if (results.length === 0 && loading) lines.push(`${DIM$3}Searching...${RESET$3}`);
|
|
4034
|
+
else if (results.length === 0) lines.push(`${DIM$3}No SafeSkill found${RESET$3}`);
|
|
3427
4035
|
else {
|
|
3428
4036
|
const visible = results.slice(0, 8);
|
|
3429
4037
|
for (let i = 0; i < visible.length; i++) {
|
|
3430
4038
|
const skill = visible[i];
|
|
3431
4039
|
const isSelected = i === selectedIndex;
|
|
3432
|
-
const arrow = isSelected ? `${BOLD$
|
|
4040
|
+
const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
|
|
3433
4041
|
const displayName = skill.label || skill.name;
|
|
3434
|
-
const name = isSelected ? `${BOLD$
|
|
3435
|
-
const source = skill.source ? ` ${DIM$
|
|
4042
|
+
const name = isSelected ? `${BOLD$3}${displayName}${RESET$3}` : `${TEXT$2}${displayName}${RESET$3}`;
|
|
4043
|
+
const source = skill.source ? ` ${DIM$3}${skill.source}${RESET$3}` : "";
|
|
3436
4044
|
const statsBadge = formatStatsBadges(skill.installs, skill.totalStars);
|
|
3437
|
-
const trustBadge = skill.trustScore !== void 0 ? ` ${skill.trustScore >= 80 ? GREEN : skill.trustScore >= 60 ? YELLOW$1 : RED}TrustScore:${skill.trustScore}${RESET$
|
|
3438
|
-
const loadingIndicator = loading && i === 0 ? ` ${DIM$
|
|
4045
|
+
const trustBadge = skill.trustScore !== void 0 ? ` ${skill.trustScore >= 80 ? GREEN : skill.trustScore >= 60 ? YELLOW$1 : RED}TrustScore:${skill.trustScore}${RESET$3}` : "";
|
|
4046
|
+
const loadingIndicator = loading && i === 0 ? ` ${DIM$3}...${RESET$3}` : "";
|
|
3439
4047
|
lines.push(` ${arrow}${trustBadge} ${name}${source}${statsBadge ? ` ${statsBadge}` : ""}${loadingIndicator}`);
|
|
3440
4048
|
}
|
|
3441
4049
|
}
|
|
3442
4050
|
lines.push("");
|
|
3443
|
-
lines.push(`${DIM$
|
|
4051
|
+
lines.push(`${DIM$3}up/down navigate | enter select | esc cancel${RESET$3}`);
|
|
3444
4052
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
3445
4053
|
lastRenderedLines = lines.length;
|
|
3446
4054
|
}
|
|
@@ -3535,10 +4143,10 @@ async function isRepoPublic(owner, repo) {
|
|
|
3535
4143
|
}
|
|
3536
4144
|
async function runFind(args) {
|
|
3537
4145
|
const query = args.join(" ");
|
|
3538
|
-
const isNonInteractive = !process.stdin.isTTY;
|
|
3539
|
-
const agentTip = `${DIM$
|
|
3540
|
-
${DIM$
|
|
3541
|
-
${DIM$
|
|
4146
|
+
const isNonInteractive = !process.stdin.isTTY || await isRunningInAgent();
|
|
4147
|
+
const agentTip = `${DIM$3}Tip: if running in a coding agent, follow these steps:${RESET$3}
|
|
4148
|
+
${DIM$3} 1) npx safeskill find [query]${RESET$3}
|
|
4149
|
+
${DIM$3} 2) npx safeskill add <source>${RESET$3}`;
|
|
3542
4150
|
if (query) {
|
|
3543
4151
|
const results = await searchSkillsAPI(query);
|
|
3544
4152
|
track({
|
|
@@ -3547,26 +4155,26 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3547
4155
|
resultCount: String(results.length)
|
|
3548
4156
|
});
|
|
3549
4157
|
if (results.length === 0) {
|
|
3550
|
-
console.log(`${DIM$
|
|
4158
|
+
console.log(`${DIM$3}No skills found for "${query}"${RESET$3}`);
|
|
3551
4159
|
return;
|
|
3552
4160
|
}
|
|
3553
|
-
console.log(`${DIM$
|
|
4161
|
+
console.log(`${DIM$3}Install with${RESET$3} npx safeskill add <owner/repo@skill>`);
|
|
3554
4162
|
console.log();
|
|
3555
4163
|
for (const skill of results.slice(0, 6)) {
|
|
3556
4164
|
const pkg = skill.source || skill.slug;
|
|
3557
4165
|
const installRef = pkg.startsWith("safeskill://") ? pkg : `${pkg}@${skill.name}`;
|
|
3558
|
-
console.log(`${TEXT$
|
|
4166
|
+
console.log(`${TEXT$2}${installRef}${RESET$3}`);
|
|
3559
4167
|
const statsBadge = formatStatsBadges(skill.installs, skill.totalStars);
|
|
3560
4168
|
const badges = [
|
|
3561
4169
|
formatTrustScore(skill.trustScore),
|
|
3562
4170
|
statsBadge,
|
|
3563
|
-
skill.updatedAt ? `${DIM$
|
|
4171
|
+
skill.updatedAt ? `${DIM$3}${formatRelativeDate(skill.updatedAt)}${RESET$3}` : ""
|
|
3564
4172
|
].filter(Boolean).join(` `);
|
|
3565
4173
|
if (badges) console.log(` ${badges}`);
|
|
3566
4174
|
const displayName = skill.label || skill.name;
|
|
3567
4175
|
const desc = skill.description || (displayName !== skill.name ? displayName : "");
|
|
3568
|
-
if (desc) console.log(`${DIM$
|
|
3569
|
-
if (skill.detailUrl) console.log(`${DIM$
|
|
4176
|
+
if (desc) console.log(`${DIM$3} ${desc}${RESET$3}`);
|
|
4177
|
+
if (skill.detailUrl) console.log(`${DIM$3}└ ${skill.detailUrl}${RESET$3}`);
|
|
3570
4178
|
console.log();
|
|
3571
4179
|
}
|
|
3572
4180
|
return;
|
|
@@ -3574,6 +4182,7 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3574
4182
|
if (isNonInteractive) {
|
|
3575
4183
|
console.log(agentTip);
|
|
3576
4184
|
console.log();
|
|
4185
|
+
return;
|
|
3577
4186
|
}
|
|
3578
4187
|
const selected = await runSearchPrompt();
|
|
3579
4188
|
track({
|
|
@@ -3583,14 +4192,14 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3583
4192
|
interactive: "1"
|
|
3584
4193
|
});
|
|
3585
4194
|
if (!selected) {
|
|
3586
|
-
console.log(`${DIM$
|
|
4195
|
+
console.log(`${DIM$3}Search cancelled${RESET$3}`);
|
|
3587
4196
|
console.log();
|
|
3588
4197
|
return;
|
|
3589
4198
|
}
|
|
3590
4199
|
const pkg = selected.source || selected.slug;
|
|
3591
4200
|
const skillName = selected.name;
|
|
3592
4201
|
console.log();
|
|
3593
|
-
console.log(`${TEXT$
|
|
4202
|
+
console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} from ${DIM$3}${pkg}${RESET$3}...`);
|
|
3594
4203
|
console.log();
|
|
3595
4204
|
const { source, options } = parseAddOptions([
|
|
3596
4205
|
pkg,
|
|
@@ -3600,9 +4209,9 @@ ${DIM$2} 2) npx safeskill add <owner/repo@skill>${RESET$2}`;
|
|
|
3600
4209
|
await runAdd(source, options);
|
|
3601
4210
|
console.log();
|
|
3602
4211
|
const info = getOwnerRepoFromString(pkg);
|
|
3603
|
-
if (pkg.startsWith("safeskill://")) console.log(`${DIM$
|
|
3604
|
-
else if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$
|
|
3605
|
-
else console.log(`${DIM$
|
|
4212
|
+
if (pkg.startsWith("safeskill://")) console.log(`${DIM$3}Installed from safeskill source${RESET$3} ${TEXT$2}${pkg}${RESET$3}`);
|
|
4213
|
+
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}`);
|
|
4214
|
+
else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
|
|
3606
4215
|
console.log();
|
|
3607
4216
|
}
|
|
3608
4217
|
const isCancelled = (value) => typeof value === "symbol";
|
|
@@ -3725,13 +4334,13 @@ async function runSync(args, options = {}) {
|
|
|
3725
4334
|
targetAgents = validAgents;
|
|
3726
4335
|
M.info(`Installing to all ${targetAgents.length} agents`);
|
|
3727
4336
|
} else if (options.agent && options.agent.length > 0) {
|
|
3728
|
-
const invalidAgents = options.agent
|
|
4337
|
+
const { normalizedAgents, invalidAgents } = normalizeAgentTypes(options.agent);
|
|
3729
4338
|
if (invalidAgents.length > 0) {
|
|
3730
4339
|
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
3731
4340
|
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
3732
4341
|
process.exit(1);
|
|
3733
4342
|
}
|
|
3734
|
-
targetAgents =
|
|
4343
|
+
targetAgents = normalizedAgents;
|
|
3735
4344
|
} else {
|
|
3736
4345
|
spinner.start("Loading agents...");
|
|
3737
4346
|
const installedAgents = await detectInstalledAgents();
|
|
@@ -3939,9 +4548,9 @@ async function runInstallFromLock(args) {
|
|
|
3939
4548
|
}
|
|
3940
4549
|
}
|
|
3941
4550
|
}
|
|
3942
|
-
const RESET$
|
|
3943
|
-
const BOLD$
|
|
3944
|
-
const DIM$
|
|
4551
|
+
const RESET$2 = "\x1B[0m";
|
|
4552
|
+
const BOLD$2 = "\x1B[1m";
|
|
4553
|
+
const DIM$2 = "\x1B[38;5;102m";
|
|
3945
4554
|
const CYAN = "\x1B[36m";
|
|
3946
4555
|
const YELLOW = "\x1B[33m";
|
|
3947
4556
|
function shortenPath(fullPath, cwd) {
|
|
@@ -3975,13 +4584,13 @@ async function runList(args) {
|
|
|
3975
4584
|
let agentFilter;
|
|
3976
4585
|
if (options.agent && options.agent.length > 0) {
|
|
3977
4586
|
const validAgents = Object.keys(agents);
|
|
3978
|
-
const invalidAgents = options.agent
|
|
4587
|
+
const { normalizedAgents, invalidAgents } = normalizeAgentTypes(options.agent);
|
|
3979
4588
|
if (invalidAgents.length > 0) {
|
|
3980
|
-
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$
|
|
3981
|
-
console.log(`${DIM$
|
|
4589
|
+
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$2}`);
|
|
4590
|
+
console.log(`${DIM$2}Valid agents: ${validAgents.join(", ")}${RESET$2}`);
|
|
3982
4591
|
process.exit(1);
|
|
3983
4592
|
}
|
|
3984
|
-
agentFilter =
|
|
4593
|
+
agentFilter = normalizedAgents;
|
|
3985
4594
|
}
|
|
3986
4595
|
const installedSkills = await listInstalledSkills({
|
|
3987
4596
|
global: scope,
|
|
@@ -4005,20 +4614,20 @@ async function runList(args) {
|
|
|
4005
4614
|
console.log("[]");
|
|
4006
4615
|
return;
|
|
4007
4616
|
}
|
|
4008
|
-
console.log(`${DIM$
|
|
4009
|
-
if (scope) console.log(`${DIM$
|
|
4010
|
-
else console.log(`${DIM$
|
|
4617
|
+
console.log(`${DIM$2}No ${scopeLabel.toLowerCase()} skills found.${RESET$2}`);
|
|
4618
|
+
if (scope) console.log(`${DIM$2}Try listing project skills without -g${RESET$2}`);
|
|
4619
|
+
else console.log(`${DIM$2}Try listing global skills with -g${RESET$2}`);
|
|
4011
4620
|
return;
|
|
4012
4621
|
}
|
|
4013
4622
|
function printSkill(skill, indent = false) {
|
|
4014
4623
|
const prefix = indent ? " " : "";
|
|
4015
4624
|
const shortPath = shortenPath(skill.canonicalPath, cwd);
|
|
4016
4625
|
const agentNames = skill.agents.map((a) => agents[a].displayName);
|
|
4017
|
-
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$
|
|
4018
|
-
console.log(`${prefix}${CYAN}${skill.name}${RESET$
|
|
4019
|
-
console.log(`${prefix} ${DIM$
|
|
4626
|
+
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$2}`;
|
|
4627
|
+
console.log(`${prefix}${CYAN}${skill.name}${RESET$2} ${DIM$2}${shortPath}${RESET$2}`);
|
|
4628
|
+
console.log(`${prefix} ${DIM$2}Agents:${RESET$2} ${agentInfo}`);
|
|
4020
4629
|
}
|
|
4021
|
-
console.log(`${BOLD$
|
|
4630
|
+
console.log(`${BOLD$2}${scopeLabel} Skills${RESET$2}`);
|
|
4022
4631
|
console.log();
|
|
4023
4632
|
const groupedSkills = {};
|
|
4024
4633
|
const ungroupedSkills = [];
|
|
@@ -4034,13 +4643,13 @@ async function runList(args) {
|
|
|
4034
4643
|
const sortedGroups = Object.keys(groupedSkills).sort();
|
|
4035
4644
|
for (const group of sortedGroups) {
|
|
4036
4645
|
const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
4037
|
-
console.log(`${BOLD$
|
|
4646
|
+
console.log(`${BOLD$2}${title}${RESET$2}`);
|
|
4038
4647
|
const skills = groupedSkills[group];
|
|
4039
4648
|
if (skills) for (const skill of skills) printSkill(skill, true);
|
|
4040
4649
|
console.log();
|
|
4041
4650
|
}
|
|
4042
4651
|
if (ungroupedSkills.length > 0) {
|
|
4043
|
-
console.log(`${BOLD$
|
|
4652
|
+
console.log(`${BOLD$2}General${RESET$2}`);
|
|
4044
4653
|
for (const skill of ungroupedSkills) printSkill(skill, true);
|
|
4045
4654
|
console.log();
|
|
4046
4655
|
}
|
|
@@ -4076,14 +4685,16 @@ async function removeCommand(skillNames, options) {
|
|
|
4076
4685
|
Se(import_picocolors.default.yellow("No skills found to remove."));
|
|
4077
4686
|
return;
|
|
4078
4687
|
}
|
|
4688
|
+
let normalizedAgentFilter;
|
|
4079
4689
|
if (options.agent && options.agent.length > 0) {
|
|
4080
4690
|
const validAgents = Object.keys(agents);
|
|
4081
|
-
const invalidAgents = options.agent
|
|
4691
|
+
const { normalizedAgents, invalidAgents } = normalizeAgentTypes(options.agent);
|
|
4082
4692
|
if (invalidAgents.length > 0) {
|
|
4083
4693
|
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
4084
4694
|
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
4085
4695
|
process.exit(1);
|
|
4086
4696
|
}
|
|
4697
|
+
normalizedAgentFilter = normalizedAgents;
|
|
4087
4698
|
}
|
|
4088
4699
|
let selectedSkills = [];
|
|
4089
4700
|
if (options.all) selectedSkills = installedSkills;
|
|
@@ -4110,7 +4721,7 @@ async function removeCommand(skillNames, options) {
|
|
|
4110
4721
|
selectedSkills = selected;
|
|
4111
4722
|
}
|
|
4112
4723
|
let targetAgents;
|
|
4113
|
-
if (
|
|
4724
|
+
if (normalizedAgentFilter && normalizedAgentFilter.length > 0) targetAgents = normalizedAgentFilter;
|
|
4114
4725
|
else {
|
|
4115
4726
|
targetAgents = Object.keys(agents);
|
|
4116
4727
|
spinner.stop(`Targeting ${targetAgents.length} potential agent(s)`);
|
|
@@ -4239,124 +4850,1586 @@ function parseRemoveOptions(args) {
|
|
|
4239
4850
|
options
|
|
4240
4851
|
};
|
|
4241
4852
|
}
|
|
4242
|
-
|
|
4243
|
-
|
|
4853
|
+
function formatSourceInput(sourceUrl, ref) {
|
|
4854
|
+
if (!ref) return sourceUrl;
|
|
4855
|
+
return `${sourceUrl}#${ref}`;
|
|
4856
|
+
}
|
|
4857
|
+
function deriveSkillFolder(skillPath) {
|
|
4858
|
+
let folder = skillPath;
|
|
4859
|
+
if (folder.endsWith("/SKILL.md")) folder = folder.slice(0, -9);
|
|
4860
|
+
else if (folder.endsWith("SKILL.md")) folder = folder.slice(0, -8);
|
|
4861
|
+
if (folder.endsWith("/")) folder = folder.slice(0, -1);
|
|
4862
|
+
return folder;
|
|
4863
|
+
}
|
|
4864
|
+
function supportsAppendedSubpath(source) {
|
|
4865
|
+
if (source.startsWith("git@")) return false;
|
|
4866
|
+
if (source.endsWith(".git")) return false;
|
|
4867
|
+
if (source.startsWith("http://") || source.startsWith("https://")) try {
|
|
4868
|
+
const host = new URL(source).hostname;
|
|
4869
|
+
return host === "github.com" || host === "gitlab.com";
|
|
4870
|
+
} catch {
|
|
4871
|
+
return false;
|
|
4872
|
+
}
|
|
4873
|
+
return true;
|
|
4874
|
+
}
|
|
4875
|
+
function sourceUrlAlreadyTargetsTreePath(sourceUrl) {
|
|
4876
|
+
return /github\.com\/[^/]+\/[^/]+\/tree\/[^/]+\/.+/.test(sourceUrl);
|
|
4877
|
+
}
|
|
4878
|
+
function appendFolderAndRef(source, skillPath, ref) {
|
|
4879
|
+
if (!supportsAppendedSubpath(source)) return formatSourceInput(source, ref);
|
|
4880
|
+
const folder = deriveSkillFolder(skillPath);
|
|
4881
|
+
const withFolder = folder ? `${source}/${folder}` : source;
|
|
4882
|
+
return ref ? `${withFolder}#${ref}` : withFolder;
|
|
4883
|
+
}
|
|
4884
|
+
function buildUpdateInstallSource(entry, updateSource = entry.sourceUrl) {
|
|
4885
|
+
if (entry.sourceType === "hub") return updateSource;
|
|
4886
|
+
if (!entry.skillPath) return formatSourceInput(entry.sourceUrl, entry.ref);
|
|
4887
|
+
if (sourceUrlAlreadyTargetsTreePath(entry.sourceUrl)) return entry.sourceUrl;
|
|
4888
|
+
return appendFolderAndRef(entry.source, entry.skillPath, entry.ref);
|
|
4889
|
+
}
|
|
4890
|
+
function buildLocalUpdateSource(entry) {
|
|
4891
|
+
if (!entry.skillPath) return formatSourceInput(entry.source, entry.ref);
|
|
4892
|
+
return appendFolderAndRef(entry.source, entry.skillPath, entry.ref);
|
|
4893
|
+
}
|
|
4894
|
+
var blob_exports = /* @__PURE__ */ __exportAll({
|
|
4895
|
+
BLOB_ALLOWED_REPOS: () => BLOB_ALLOWED_REPOS,
|
|
4896
|
+
fetchRepoTree: () => fetchRepoTree,
|
|
4897
|
+
findSkillMdPaths: () => findSkillMdPaths,
|
|
4898
|
+
getSkillFolderHashFromTree: () => getSkillFolderHashFromTree,
|
|
4899
|
+
toSkillSlug: () => toSkillSlug,
|
|
4900
|
+
tryBlobInstall: () => tryBlobInstall
|
|
4901
|
+
});
|
|
4902
|
+
const DOWNLOAD_BASE_URL = process.env.SKILLS_DOWNLOAD_URL || "https://skills.sh";
|
|
4903
|
+
const BLOB_ALLOWED_REPOS = { "zapier/connectors": { downloadUrl: (slug) => `https://connectors-skills.zapier.com/download/${encodeURIComponent(slug)}/snapshot.json` } };
|
|
4904
|
+
const FETCH_TIMEOUT = 1e4;
|
|
4905
|
+
function toSkillSlug(name) {
|
|
4906
|
+
return name.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
4907
|
+
}
|
|
4908
|
+
let _rateLimitedThisSession = false;
|
|
4909
|
+
async function fetchTreeBranch(ownerRepo, branch, token) {
|
|
4244
4910
|
try {
|
|
4245
|
-
const
|
|
4246
|
-
|
|
4911
|
+
const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${encodeURIComponent(branch)}?recursive=1`;
|
|
4912
|
+
const headers = {
|
|
4913
|
+
Accept: "application/vnd.github.v3+json",
|
|
4914
|
+
"User-Agent": "skills-cli"
|
|
4915
|
+
};
|
|
4916
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
4917
|
+
const response = await fetch(url, {
|
|
4918
|
+
headers,
|
|
4919
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT)
|
|
4920
|
+
});
|
|
4921
|
+
if (response.ok) {
|
|
4922
|
+
const data = await response.json();
|
|
4923
|
+
return {
|
|
4924
|
+
tree: {
|
|
4925
|
+
sha: data.sha,
|
|
4926
|
+
branch,
|
|
4927
|
+
tree: data.tree
|
|
4928
|
+
},
|
|
4929
|
+
rateLimited: false
|
|
4930
|
+
};
|
|
4931
|
+
}
|
|
4932
|
+
return {
|
|
4933
|
+
tree: null,
|
|
4934
|
+
rateLimited: response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0"
|
|
4935
|
+
};
|
|
4247
4936
|
} catch {
|
|
4248
|
-
return
|
|
4937
|
+
return {
|
|
4938
|
+
tree: null,
|
|
4939
|
+
rateLimited: false
|
|
4940
|
+
};
|
|
4249
4941
|
}
|
|
4250
4942
|
}
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
const
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
" ████ ",
|
|
4264
|
-
" █ ",
|
|
4265
|
-
"█ █",
|
|
4266
|
-
" █████ "
|
|
4267
|
-
],
|
|
4268
|
-
a: [
|
|
4269
|
-
" ███ ",
|
|
4270
|
-
" █ █ ",
|
|
4271
|
-
" █ █ ",
|
|
4272
|
-
" █████ ",
|
|
4273
|
-
" █ █ ",
|
|
4274
|
-
" █ █ ",
|
|
4275
|
-
" █ █ "
|
|
4276
|
-
],
|
|
4277
|
-
f: [
|
|
4278
|
-
"██████ ",
|
|
4279
|
-
"█ ",
|
|
4280
|
-
"█ ",
|
|
4281
|
-
"█████ ",
|
|
4282
|
-
"█ ",
|
|
4283
|
-
"█ ",
|
|
4284
|
-
"█ "
|
|
4285
|
-
],
|
|
4286
|
-
e: [
|
|
4287
|
-
"██████ ",
|
|
4288
|
-
"█ ",
|
|
4289
|
-
"█ ",
|
|
4290
|
-
"█████ ",
|
|
4291
|
-
"█ ",
|
|
4292
|
-
"█ ",
|
|
4293
|
-
"██████ "
|
|
4294
|
-
],
|
|
4295
|
-
k: [
|
|
4296
|
-
"█ █ ",
|
|
4297
|
-
"█ █ ",
|
|
4298
|
-
"█ █ ",
|
|
4299
|
-
"███ ",
|
|
4300
|
-
"█ █ ",
|
|
4301
|
-
"█ █ ",
|
|
4302
|
-
"█ █ "
|
|
4303
|
-
],
|
|
4304
|
-
i: [
|
|
4305
|
-
"█████ ",
|
|
4306
|
-
" █ ",
|
|
4307
|
-
" █ ",
|
|
4308
|
-
" █ ",
|
|
4309
|
-
" █ ",
|
|
4310
|
-
" █ ",
|
|
4311
|
-
"█████ "
|
|
4312
|
-
],
|
|
4313
|
-
l: [
|
|
4314
|
-
"█ ",
|
|
4315
|
-
"█ ",
|
|
4316
|
-
"█ ",
|
|
4317
|
-
"█ ",
|
|
4318
|
-
"█ ",
|
|
4319
|
-
"█ ",
|
|
4320
|
-
"██████ "
|
|
4321
|
-
]
|
|
4322
|
-
};
|
|
4323
|
-
function buildLogoLines() {
|
|
4324
|
-
const words = ["Safe", "Skill"];
|
|
4325
|
-
const lines = Array.from({ length: LOGO_HEIGHT }, () => "");
|
|
4326
|
-
for (const word of words) {
|
|
4327
|
-
for (const character of word) {
|
|
4328
|
-
const glyph = LOGO_GLYPHS[character.toLowerCase()];
|
|
4329
|
-
if (!glyph) continue;
|
|
4330
|
-
for (let i = 0; i < LOGO_HEIGHT; i++) lines[i] += `${glyph[i] ?? ""} `;
|
|
4943
|
+
async function fetchRepoTree(ownerRepo, ref, getToken) {
|
|
4944
|
+
const branches = ref ? [ref] : [
|
|
4945
|
+
"HEAD",
|
|
4946
|
+
"main",
|
|
4947
|
+
"master"
|
|
4948
|
+
];
|
|
4949
|
+
if (_rateLimitedThisSession && getToken) {
|
|
4950
|
+
const token = getToken();
|
|
4951
|
+
if (!token) return null;
|
|
4952
|
+
for (const branch of branches) {
|
|
4953
|
+
const result = await fetchTreeBranch(ownerRepo, branch, token);
|
|
4954
|
+
if (result.tree) return result.tree;
|
|
4331
4955
|
}
|
|
4332
|
-
|
|
4956
|
+
return null;
|
|
4333
4957
|
}
|
|
4334
|
-
|
|
4958
|
+
let rateLimited = false;
|
|
4959
|
+
for (const branch of branches) {
|
|
4960
|
+
const result = await fetchTreeBranch(ownerRepo, branch, null);
|
|
4961
|
+
if (result.tree) return result.tree;
|
|
4962
|
+
if (result.rateLimited) {
|
|
4963
|
+
rateLimited = true;
|
|
4964
|
+
break;
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
if (!rateLimited || !getToken) return null;
|
|
4968
|
+
_rateLimitedThisSession = true;
|
|
4969
|
+
const token = getToken();
|
|
4970
|
+
if (!token) return null;
|
|
4971
|
+
for (const branch of branches) {
|
|
4972
|
+
const result = await fetchTreeBranch(ownerRepo, branch, token);
|
|
4973
|
+
if (result.tree) return result.tree;
|
|
4974
|
+
}
|
|
4975
|
+
return null;
|
|
4335
4976
|
}
|
|
4336
|
-
|
|
4337
|
-
"
|
|
4338
|
-
"
|
|
4339
|
-
"
|
|
4340
|
-
"
|
|
4341
|
-
|
|
4342
|
-
"
|
|
4343
|
-
|
|
4977
|
+
function getSkillFolderHashFromTree(tree, skillPath) {
|
|
4978
|
+
let folderPath = skillPath.replace(/\\/g, "/");
|
|
4979
|
+
if (folderPath.toLowerCase().endsWith("/skill.md")) folderPath = folderPath.slice(0, -9);
|
|
4980
|
+
else if (folderPath.toLowerCase().endsWith("skill.md")) folderPath = folderPath.slice(0, -8);
|
|
4981
|
+
if (folderPath.endsWith("/")) folderPath = folderPath.slice(0, -1);
|
|
4982
|
+
if (!folderPath) return tree.sha;
|
|
4983
|
+
return tree.tree.find((e) => e.type === "tree" && e.path === folderPath)?.sha ?? null;
|
|
4984
|
+
}
|
|
4985
|
+
const PRIORITY_PREFIXES = [
|
|
4986
|
+
"",
|
|
4987
|
+
"skills/",
|
|
4988
|
+
"skills/.curated/",
|
|
4989
|
+
"skills/.experimental/",
|
|
4990
|
+
"skills/.system/",
|
|
4991
|
+
".agents/skills/",
|
|
4992
|
+
".claude/skills/",
|
|
4993
|
+
".cline/skills/",
|
|
4994
|
+
".codebuddy/skills/",
|
|
4995
|
+
".codex/skills/",
|
|
4996
|
+
".commandcode/skills/",
|
|
4997
|
+
".continue/skills/",
|
|
4998
|
+
".github/skills/",
|
|
4999
|
+
".goose/skills/",
|
|
5000
|
+
".iflow/skills/",
|
|
5001
|
+
".junie/skills/",
|
|
5002
|
+
".kilocode/skills/",
|
|
5003
|
+
".kiro/skills/",
|
|
5004
|
+
".mux/skills/",
|
|
5005
|
+
".neovate/skills/",
|
|
5006
|
+
".opencode/skills/",
|
|
5007
|
+
".openhands/skills/",
|
|
5008
|
+
".pi/skills/",
|
|
5009
|
+
".qoder/skills/",
|
|
5010
|
+
".roo/skills/",
|
|
5011
|
+
".trae/skills/",
|
|
5012
|
+
".windsurf/skills/",
|
|
5013
|
+
".zencoder/skills/"
|
|
4344
5014
|
];
|
|
4345
|
-
function
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
5015
|
+
function findSkillMdPaths(tree, subpath) {
|
|
5016
|
+
const allSkillMds = tree.tree.filter((e) => {
|
|
5017
|
+
if (e.type !== "blob") return false;
|
|
5018
|
+
return e.path.replace(/\\/g, "/").split("/").pop()?.toLowerCase() === "skill.md";
|
|
5019
|
+
}).map((e) => e.path);
|
|
5020
|
+
const prefix = subpath ? subpath.endsWith("/") ? subpath : subpath + "/" : "";
|
|
5021
|
+
const filtered = prefix ? allSkillMds.filter((p) => p.startsWith(prefix) || p === prefix + "SKILL.md") : allSkillMds;
|
|
5022
|
+
if (filtered.length === 0) return [];
|
|
5023
|
+
const priorityResults = [];
|
|
5024
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5025
|
+
const SKIP_DIRS = new Set([
|
|
5026
|
+
"node_modules",
|
|
5027
|
+
".git",
|
|
5028
|
+
"dist",
|
|
5029
|
+
"build",
|
|
5030
|
+
"__pycache__"
|
|
5031
|
+
]);
|
|
5032
|
+
const lowerSkillMdSet = new Set(filtered.map((p) => p.toLowerCase()));
|
|
5033
|
+
for (const priorityPrefix of PRIORITY_PREFIXES) {
|
|
5034
|
+
const fullPrefix = prefix + priorityPrefix;
|
|
5035
|
+
const isContainer = priorityPrefix !== "";
|
|
5036
|
+
for (const skillMd of filtered) {
|
|
5037
|
+
if (!skillMd.startsWith(fullPrefix)) continue;
|
|
5038
|
+
const rest = skillMd.slice(fullPrefix.length);
|
|
5039
|
+
if (rest.toLowerCase() === "skill.md") {
|
|
5040
|
+
if (!seen.has(skillMd)) {
|
|
5041
|
+
priorityResults.push(skillMd);
|
|
5042
|
+
seen.add(skillMd);
|
|
5043
|
+
}
|
|
5044
|
+
continue;
|
|
5045
|
+
}
|
|
5046
|
+
const parts = rest.split("/");
|
|
5047
|
+
if (parts.length === 2 && parts[1].toLowerCase() === "skill.md") {
|
|
5048
|
+
if (!seen.has(skillMd)) {
|
|
5049
|
+
priorityResults.push(skillMd);
|
|
5050
|
+
seen.add(skillMd);
|
|
5051
|
+
}
|
|
5052
|
+
continue;
|
|
5053
|
+
}
|
|
5054
|
+
if (isContainer && parts.length === 3 && parts[2].toLowerCase() === "skill.md" && !SKIP_DIRS.has(parts[0]) && !SKIP_DIRS.has(parts[1])) {
|
|
5055
|
+
const parentSkillMd = `${fullPrefix}${parts[0]}/SKILL.md`.toLowerCase();
|
|
5056
|
+
if (!lowerSkillMdSet.has(parentSkillMd) && !seen.has(skillMd)) {
|
|
5057
|
+
priorityResults.push(skillMd);
|
|
5058
|
+
seen.add(skillMd);
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
if (priorityResults.length > 0) return priorityResults;
|
|
5064
|
+
return filtered.filter((p) => {
|
|
5065
|
+
return p.split("/").length <= 6;
|
|
4349
5066
|
});
|
|
4350
5067
|
}
|
|
4351
|
-
function
|
|
4352
|
-
|
|
4353
|
-
|
|
5068
|
+
async function fetchSkillMdContent(ownerRepo, branch, skillMdPath) {
|
|
5069
|
+
try {
|
|
5070
|
+
const url = `https://raw.githubusercontent.com/${ownerRepo}/${branch}/${skillMdPath}`;
|
|
5071
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT) });
|
|
5072
|
+
if (!response.ok) return null;
|
|
5073
|
+
return await response.text();
|
|
5074
|
+
} catch {
|
|
5075
|
+
return null;
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
async function fetchSkillDownload(source, slug) {
|
|
5079
|
+
try {
|
|
5080
|
+
const [owner, repo] = source.split("/");
|
|
5081
|
+
const defaultUrl = `${DOWNLOAD_BASE_URL}/api/download/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(slug)}`;
|
|
5082
|
+
const url = BLOB_ALLOWED_REPOS[source.toLowerCase()]?.downloadUrl(slug) ?? defaultUrl;
|
|
5083
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT) });
|
|
5084
|
+
if (!response.ok) return null;
|
|
5085
|
+
return await response.json();
|
|
5086
|
+
} catch {
|
|
5087
|
+
return null;
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
function computeSnapshotHash(files) {
|
|
5091
|
+
const hash = createHash$1("sha256");
|
|
5092
|
+
for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
5093
|
+
hash.update(file.path);
|
|
5094
|
+
hash.update(file.contents);
|
|
5095
|
+
}
|
|
5096
|
+
return hash.digest("hex");
|
|
5097
|
+
}
|
|
5098
|
+
function normalizeSnapshotFilePath(filePath) {
|
|
5099
|
+
if (typeof filePath !== "string") return null;
|
|
5100
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
5101
|
+
if (normalized.trim() === "") return null;
|
|
5102
|
+
if (normalized.startsWith("/")) return null;
|
|
5103
|
+
if (/^[a-zA-Z]:\//.test(normalized)) return null;
|
|
5104
|
+
const segments = normalized.split("/");
|
|
5105
|
+
if (segments.some((segment) => segment === "" || segment === "..")) return null;
|
|
5106
|
+
return segments.join("/");
|
|
5107
|
+
}
|
|
5108
|
+
function normalizeSnapshotFiles(files) {
|
|
5109
|
+
const normalizedFiles = [];
|
|
5110
|
+
for (const file of files) {
|
|
5111
|
+
const normalizedPath = normalizeSnapshotFilePath(file.path);
|
|
5112
|
+
if (!normalizedPath || typeof file.contents !== "string") return null;
|
|
5113
|
+
normalizedFiles.push({
|
|
5114
|
+
path: normalizedPath,
|
|
5115
|
+
contents: file.contents
|
|
5116
|
+
});
|
|
5117
|
+
}
|
|
5118
|
+
return normalizedFiles;
|
|
5119
|
+
}
|
|
5120
|
+
async function tryBlobInstall(ownerRepo, options = {}) {
|
|
5121
|
+
const tree = await fetchRepoTree(ownerRepo, options.ref, options.getToken);
|
|
5122
|
+
if (!tree) return null;
|
|
5123
|
+
let skillMdPaths = findSkillMdPaths(tree, options.subpath);
|
|
5124
|
+
if (skillMdPaths.length === 0) return null;
|
|
5125
|
+
if (options.skillFilter) {
|
|
5126
|
+
const filterSlug = toSkillSlug(options.skillFilter);
|
|
5127
|
+
const filtered = skillMdPaths.filter((p) => {
|
|
5128
|
+
const parts = p.split("/");
|
|
5129
|
+
if (parts.length < 2) return false;
|
|
5130
|
+
const folderName = parts[parts.length - 2];
|
|
5131
|
+
return toSkillSlug(folderName) === filterSlug;
|
|
5132
|
+
});
|
|
5133
|
+
if (filtered.length > 0) skillMdPaths = filtered;
|
|
5134
|
+
}
|
|
5135
|
+
const mdFetches = await Promise.all(skillMdPaths.map(async (mdPath) => {
|
|
5136
|
+
return {
|
|
5137
|
+
mdPath,
|
|
5138
|
+
content: await fetchSkillMdContent(ownerRepo, tree.branch, mdPath)
|
|
5139
|
+
};
|
|
5140
|
+
}));
|
|
5141
|
+
const parsedSkills = [];
|
|
5142
|
+
for (const { mdPath, content } of mdFetches) {
|
|
5143
|
+
if (!content) return null;
|
|
5144
|
+
const { data } = parseFrontmatter(content);
|
|
5145
|
+
if (!data.name || !data.description) return null;
|
|
5146
|
+
if (typeof data.name !== "string" || typeof data.description !== "string") return null;
|
|
5147
|
+
if (data.metadata?.internal === true && !options.includeInternal) continue;
|
|
5148
|
+
const safeName = sanitizeMetadata(data.name);
|
|
5149
|
+
const safeDescription = sanitizeMetadata(data.description);
|
|
5150
|
+
if (!safeName || !safeDescription) return null;
|
|
5151
|
+
parsedSkills.push({
|
|
5152
|
+
mdPath,
|
|
5153
|
+
name: safeName,
|
|
5154
|
+
description: safeDescription,
|
|
5155
|
+
content,
|
|
5156
|
+
slug: toSkillSlug(safeName),
|
|
5157
|
+
metadata: data.metadata
|
|
5158
|
+
});
|
|
5159
|
+
}
|
|
5160
|
+
if (parsedSkills.length === 0) return null;
|
|
5161
|
+
let filteredSkills = parsedSkills;
|
|
5162
|
+
if (options.skillFilter) {
|
|
5163
|
+
const filterSlug = toSkillSlug(options.skillFilter);
|
|
5164
|
+
const nameFiltered = parsedSkills.filter((s) => s.slug === filterSlug);
|
|
5165
|
+
if (nameFiltered.length > 0) filteredSkills = nameFiltered;
|
|
5166
|
+
else return null;
|
|
5167
|
+
}
|
|
5168
|
+
const source = ownerRepo.toLowerCase();
|
|
5169
|
+
const downloads = await Promise.all(filteredSkills.map(async (skill) => {
|
|
5170
|
+
return {
|
|
5171
|
+
skill,
|
|
5172
|
+
download: await fetchSkillDownload(source, skill.slug)
|
|
5173
|
+
};
|
|
5174
|
+
}));
|
|
5175
|
+
if (!downloads.every((d) => d.download !== null)) return null;
|
|
5176
|
+
const normalizedDownloads = downloads.map(({ skill, download }) => {
|
|
5177
|
+
return {
|
|
5178
|
+
skill,
|
|
5179
|
+
download,
|
|
5180
|
+
files: normalizeSnapshotFiles(download.files)
|
|
5181
|
+
};
|
|
5182
|
+
});
|
|
5183
|
+
if (normalizedDownloads.some(({ files }) => files === null)) return null;
|
|
5184
|
+
return {
|
|
5185
|
+
skills: normalizedDownloads.map(({ skill, download, files }) => {
|
|
5186
|
+
const mdPathLower = skill.mdPath.toLowerCase();
|
|
5187
|
+
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");
|
|
5188
|
+
return {
|
|
5189
|
+
name: skill.name,
|
|
5190
|
+
description: skill.description,
|
|
5191
|
+
path: "",
|
|
5192
|
+
rawContent: skill.content,
|
|
5193
|
+
metadata: skill.metadata,
|
|
5194
|
+
files: selectedFiles,
|
|
5195
|
+
snapshotHash: selectedFiles.length === files.length ? download.hash : computeSnapshotHash(selectedFiles),
|
|
5196
|
+
repoPath: skill.mdPath
|
|
5197
|
+
};
|
|
5198
|
+
}),
|
|
5199
|
+
tree
|
|
5200
|
+
};
|
|
5201
|
+
}
|
|
5202
|
+
const __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
5203
|
+
const RESET$1 = "\x1B[0m";
|
|
5204
|
+
const BOLD$1 = "\x1B[1m";
|
|
5205
|
+
const DIM$1 = "\x1B[38;5;102m";
|
|
5206
|
+
const TEXT$1 = "\x1B[38;5;145m";
|
|
5207
|
+
function parseUpdateOptions(args) {
|
|
5208
|
+
const options = {};
|
|
5209
|
+
const positional = [];
|
|
5210
|
+
for (const arg of args) if (arg === "-g" || arg === "--global") options.global = true;
|
|
5211
|
+
else if (arg === "-p" || arg === "--project") options.project = true;
|
|
5212
|
+
else if (arg === "-y" || arg === "--yes") options.yes = true;
|
|
5213
|
+
else if (!arg.startsWith("-")) positional.push(arg);
|
|
5214
|
+
if (positional.length > 0) options.skills = positional;
|
|
5215
|
+
return options;
|
|
5216
|
+
}
|
|
5217
|
+
function hasProjectSkills(cwd) {
|
|
5218
|
+
const dir = cwd || process.cwd();
|
|
5219
|
+
if (existsSync(join(dir, "skills-lock.json"))) return true;
|
|
5220
|
+
const skillsDir = join(dir, ".agents", "skills");
|
|
5221
|
+
try {
|
|
5222
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
5223
|
+
for (const entry of entries) if (entry.isDirectory() && existsSync(join(skillsDir, entry.name, "SKILL.md"))) return true;
|
|
5224
|
+
} catch {}
|
|
5225
|
+
return false;
|
|
5226
|
+
}
|
|
5227
|
+
async function resolveUpdateScope(options) {
|
|
5228
|
+
if (options.skills && options.skills.length > 0) {
|
|
5229
|
+
if (options.global) return "global";
|
|
5230
|
+
if (options.project) return "project";
|
|
5231
|
+
return "both";
|
|
5232
|
+
}
|
|
5233
|
+
if (options.global && options.project) return "both";
|
|
5234
|
+
if (options.global) return "global";
|
|
5235
|
+
if (options.project) return "project";
|
|
5236
|
+
if (options.yes || !process.stdin.isTTY) return hasProjectSkills() ? "project" : "global";
|
|
5237
|
+
const scope = await ve({
|
|
5238
|
+
message: "Update scope",
|
|
5239
|
+
options: [
|
|
5240
|
+
{
|
|
5241
|
+
value: "project",
|
|
5242
|
+
label: "Project",
|
|
5243
|
+
hint: "Update skills in current directory"
|
|
5244
|
+
},
|
|
5245
|
+
{
|
|
5246
|
+
value: "global",
|
|
5247
|
+
label: "Global",
|
|
5248
|
+
hint: "Update skills in home directory"
|
|
5249
|
+
},
|
|
5250
|
+
{
|
|
5251
|
+
value: "both",
|
|
5252
|
+
label: "Both",
|
|
5253
|
+
hint: "Update all skills"
|
|
5254
|
+
}
|
|
5255
|
+
]
|
|
5256
|
+
});
|
|
5257
|
+
if (pD(scope)) {
|
|
5258
|
+
xe("Cancelled");
|
|
5259
|
+
process.exit(0);
|
|
5260
|
+
}
|
|
5261
|
+
return scope;
|
|
5262
|
+
}
|
|
5263
|
+
function matchesSkillFilter(name, filter) {
|
|
5264
|
+
if (!filter || filter.length === 0) return true;
|
|
5265
|
+
const lower = name.toLowerCase();
|
|
5266
|
+
return filter.some((f) => f.toLowerCase() === lower);
|
|
5267
|
+
}
|
|
5268
|
+
function getSkipReason(entry) {
|
|
5269
|
+
if (entry.sourceType === "local") return "Local path";
|
|
5270
|
+
if (entry.sourceType === "hub") {
|
|
5271
|
+
if (!entry.resolvedVersion) return "No hub version recorded";
|
|
5272
|
+
if (!entry.artifactDigest) return "No hub artifact metadata";
|
|
5273
|
+
return "No version tracking";
|
|
5274
|
+
}
|
|
5275
|
+
if (entry.sourceType === "well-known") return "Well-known skill";
|
|
5276
|
+
if (!entry.skillFolderHash) return "No version hash available";
|
|
5277
|
+
if (!entry.skillPath) return "No skill path recorded";
|
|
5278
|
+
return "No version tracking";
|
|
5279
|
+
}
|
|
5280
|
+
function replaceHubSourceVersion(source, version) {
|
|
5281
|
+
const trimmedSource = source.trim();
|
|
5282
|
+
if (!trimmedSource) return source;
|
|
5283
|
+
return `${trimmedSource.replace(/@[^/@]+$/, "")}@${version}`;
|
|
5284
|
+
}
|
|
5285
|
+
function getInstallSource(skill) {
|
|
5286
|
+
let url = skill.sourceUrl;
|
|
5287
|
+
if (skill.sourceType === "well-known") {
|
|
5288
|
+
const idx = url.indexOf("/.well-known/");
|
|
5289
|
+
if (idx !== -1) url = url.slice(0, idx);
|
|
5290
|
+
}
|
|
5291
|
+
return formatSourceInput(url, skill.ref);
|
|
5292
|
+
}
|
|
5293
|
+
function getProjectHubSkipReason(entry) {
|
|
5294
|
+
if (!entry.resolvedVersion) return "No hub version recorded";
|
|
5295
|
+
if (!entry.artifactDigest) return "No hub artifact metadata";
|
|
5296
|
+
return "No hub update metadata";
|
|
5297
|
+
}
|
|
5298
|
+
function getSourceRefKey(source, ref) {
|
|
5299
|
+
return `${source}\0${ref ?? ""}`;
|
|
5300
|
+
}
|
|
5301
|
+
function hasSameSourceAndRef(entry, source, ref) {
|
|
5302
|
+
return entry.source === source && (entry.ref ?? "") === (ref ?? "");
|
|
5303
|
+
}
|
|
5304
|
+
function printSkippedSkills(skipped) {
|
|
5305
|
+
if (skipped.length === 0) return;
|
|
5306
|
+
console.log();
|
|
5307
|
+
console.log(`${DIM$1}${skipped.length} skill(s) cannot be checked automatically:${RESET$1}`);
|
|
5308
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
5309
|
+
for (const skill of skipped) {
|
|
5310
|
+
const source = getInstallSource(skill);
|
|
5311
|
+
const existing = grouped.get(source) || [];
|
|
5312
|
+
existing.push(skill);
|
|
5313
|
+
grouped.set(source, existing);
|
|
5314
|
+
}
|
|
5315
|
+
for (const [source, skills] of grouped) {
|
|
5316
|
+
if (skills.length === 1) {
|
|
5317
|
+
const skill = skills[0];
|
|
5318
|
+
console.log(` ${TEXT$1}-${RESET$1} ${sanitizeMetadata(skill.name)} ${DIM$1}(${skill.reason})${RESET$1}`);
|
|
5319
|
+
} else {
|
|
5320
|
+
const reason = skills[0].reason;
|
|
5321
|
+
const names = skills.map((s) => sanitizeMetadata(s.name)).join(", ");
|
|
5322
|
+
console.log(` ${TEXT$1}-${RESET$1} ${names} ${DIM$1}(${reason})${RESET$1}`);
|
|
5323
|
+
}
|
|
5324
|
+
console.log(` ${DIM$1}To update: ${TEXT$1}npx safeskill add ${source} -g -y${RESET$1}`);
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
function printSkippedProjectHubSkills(skipped) {
|
|
5328
|
+
if (skipped.length === 0) return;
|
|
5329
|
+
console.log();
|
|
5330
|
+
console.log(`${DIM$1}${skipped.length} project hub skill(s) require manual refresh:${RESET$1}`);
|
|
5331
|
+
for (const skill of skipped) {
|
|
5332
|
+
console.log(` ${TEXT$1}-${RESET$1} ${sanitizeMetadata(skill.name)} ${DIM$1}(${getProjectHubSkipReason(skill.entry)})${RESET$1}`);
|
|
5333
|
+
console.log(` ${DIM$1}To refresh: ${TEXT$1}npx safeskill add ${skill.entry.source} -y${RESET$1}`);
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
async function getProjectSkillsForUpdate(skillFilter) {
|
|
5337
|
+
const localLock = await readLocalLock();
|
|
5338
|
+
const skills = [];
|
|
5339
|
+
for (const [name, entry] of Object.entries(localLock.skills)) {
|
|
5340
|
+
if (!matchesSkillFilter(name, skillFilter)) continue;
|
|
5341
|
+
if (entry.sourceType === "node_modules" || entry.sourceType === "local") continue;
|
|
5342
|
+
skills.push({
|
|
5343
|
+
name,
|
|
5344
|
+
source: entry.source,
|
|
5345
|
+
entry
|
|
5346
|
+
});
|
|
5347
|
+
}
|
|
5348
|
+
return skills;
|
|
5349
|
+
}
|
|
5350
|
+
async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discoveredPaths, mode = "update") {
|
|
5351
|
+
const deletedSkills = allLockedForSource.filter((name) => {
|
|
5352
|
+
const entry = lockSkills[name];
|
|
5353
|
+
if (!entry?.skillPath) return false;
|
|
5354
|
+
return !discoveredPaths.includes(entry.skillPath);
|
|
5355
|
+
});
|
|
5356
|
+
if (deletedSkills.length > 0) {
|
|
5357
|
+
console.log();
|
|
5358
|
+
console.log(`${DIM$1}Warning:${RESET$1} The following skills from ${DIM$1}${source}${RESET$1} appear to have been deleted upstream:`);
|
|
5359
|
+
for (const s of deletedSkills) console.log(` ${DIM$1}-${RESET$1} ${s}`);
|
|
5360
|
+
if (mode === "check") {
|
|
5361
|
+
console.log(`${DIM$1}Skipping deletion in check mode.${RESET$1}`);
|
|
5362
|
+
return deletedSkills;
|
|
5363
|
+
}
|
|
5364
|
+
if (options.yes || !process.stdin.isTTY) console.log(`${DIM$1}Skipping deletion in non-interactive mode.${RESET$1}`);
|
|
5365
|
+
else {
|
|
5366
|
+
const confirmed = await ye({ message: `Would you like to remove the local copies of these deleted skills?` });
|
|
5367
|
+
if (confirmed && !pD(confirmed)) for (const s of deletedSkills) {
|
|
5368
|
+
console.log(`${DIM$1}Removing${RESET$1} ${s}...`);
|
|
5369
|
+
await removeCommand([s], {
|
|
5370
|
+
yes: true,
|
|
5371
|
+
global: isGlobal
|
|
5372
|
+
});
|
|
5373
|
+
}
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
return deletedSkills;
|
|
5377
|
+
}
|
|
5378
|
+
async function updateGlobalSkills(options = {}, region, mode = "update") {
|
|
5379
|
+
const lock = await readSkillLock();
|
|
5380
|
+
const skillNames = Object.keys(lock.skills);
|
|
5381
|
+
let successCount = 0;
|
|
5382
|
+
let failCount = 0;
|
|
5383
|
+
let checkFailCount = 0;
|
|
5384
|
+
if (skillNames.length === 0) {
|
|
5385
|
+
if (!options.skills) {
|
|
5386
|
+
console.log(`${DIM$1}No global skills tracked in lock file.${RESET$1}`);
|
|
5387
|
+
console.log(`${DIM$1}Install skills with${RESET$1} ${TEXT$1}npx safeskill add <package> -g${RESET$1}`);
|
|
5388
|
+
}
|
|
5389
|
+
return {
|
|
5390
|
+
successCount,
|
|
5391
|
+
failCount,
|
|
5392
|
+
checkFailCount,
|
|
5393
|
+
checkedCount: 0,
|
|
5394
|
+
updateCount: 0
|
|
5395
|
+
};
|
|
5396
|
+
}
|
|
5397
|
+
const updates = [];
|
|
5398
|
+
const skipped = [];
|
|
5399
|
+
const checkable = [];
|
|
5400
|
+
const hubEntries = [];
|
|
5401
|
+
for (const skillName of skillNames) {
|
|
5402
|
+
if (!matchesSkillFilter(skillName, options.skills)) continue;
|
|
5403
|
+
const entry = lock.skills[skillName];
|
|
5404
|
+
if (!entry) continue;
|
|
5405
|
+
if (entry.sourceType === "hub") {
|
|
5406
|
+
if (!entry.resolvedVersion || !entry.artifactDigest) {
|
|
5407
|
+
skipped.push({
|
|
5408
|
+
name: skillName,
|
|
5409
|
+
reason: getSkipReason(entry),
|
|
5410
|
+
sourceUrl: entry.sourceUrl,
|
|
5411
|
+
sourceType: entry.sourceType,
|
|
5412
|
+
ref: entry.ref
|
|
5413
|
+
});
|
|
5414
|
+
continue;
|
|
5415
|
+
}
|
|
5416
|
+
hubEntries.push({
|
|
5417
|
+
name: skillName,
|
|
5418
|
+
entry
|
|
5419
|
+
});
|
|
5420
|
+
continue;
|
|
5421
|
+
}
|
|
5422
|
+
if (!entry.skillFolderHash || !entry.skillPath) {
|
|
5423
|
+
skipped.push({
|
|
5424
|
+
name: skillName,
|
|
5425
|
+
reason: getSkipReason(entry),
|
|
5426
|
+
sourceUrl: entry.sourceUrl,
|
|
5427
|
+
sourceType: entry.sourceType,
|
|
5428
|
+
ref: entry.ref
|
|
5429
|
+
});
|
|
5430
|
+
continue;
|
|
5431
|
+
}
|
|
5432
|
+
checkable.push({
|
|
5433
|
+
name: skillName,
|
|
5434
|
+
entry
|
|
5435
|
+
});
|
|
5436
|
+
}
|
|
5437
|
+
if (hubEntries.length > 0) try {
|
|
5438
|
+
(await checkHubUpdates(hubEntries.map(({ entry }) => ({
|
|
5439
|
+
source: entry.sourceUrl,
|
|
5440
|
+
version: entry.resolvedVersion,
|
|
5441
|
+
artifact_digest: entry.artifactDigest
|
|
5442
|
+
})))).items.forEach((item, index) => {
|
|
5443
|
+
const hubEntry = hubEntries[index];
|
|
5444
|
+
if (!hubEntry || !item.has_update || !item.latest_version) return;
|
|
5445
|
+
updates.push({
|
|
5446
|
+
name: hubEntry.name,
|
|
5447
|
+
source: hubEntry.entry.sourceUrl,
|
|
5448
|
+
installSource: replaceHubSourceVersion(hubEntry.entry.sourceUrl, item.latest_version),
|
|
5449
|
+
entry: hubEntry.entry
|
|
5450
|
+
});
|
|
5451
|
+
});
|
|
5452
|
+
} catch {
|
|
5453
|
+
checkFailCount += hubEntries.length;
|
|
5454
|
+
console.log(` ${DIM$1}Failed to check hub skills${RESET$1}`);
|
|
5455
|
+
}
|
|
5456
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
5457
|
+
for (const item of checkable) {
|
|
5458
|
+
const source = getSourceRefKey(item.entry.source, item.entry.ref);
|
|
5459
|
+
const existing = bySource.get(source) || [];
|
|
5460
|
+
existing.push(item);
|
|
5461
|
+
bySource.set(source, existing);
|
|
5462
|
+
}
|
|
5463
|
+
for (const [, itemsForSource] of bySource) {
|
|
5464
|
+
const firstEntry = itemsForSource[0].entry;
|
|
5465
|
+
const source = firstEntry.source;
|
|
5466
|
+
const ref = firstEntry.ref;
|
|
5467
|
+
const sourceUrl = firstEntry.sourceUrl || firstEntry.source;
|
|
5468
|
+
let tempDir = null;
|
|
5469
|
+
const sourceLabel = ref ? `${source}#${ref}` : source;
|
|
5470
|
+
process.stdout.write(`\r${DIM$1}Checking skills from source: ${sourceLabel}${RESET$1}\x1b[K\n`);
|
|
5471
|
+
try {
|
|
5472
|
+
if (firstEntry.sourceType === "github") {
|
|
5473
|
+
const tree = await fetchRepoTree(source, firstEntry.ref, getGitHubToken);
|
|
5474
|
+
if (!tree) {
|
|
5475
|
+
console.log(` ${DIM$1}Failed to fetch tree for ${source}${RESET$1}`);
|
|
5476
|
+
checkFailCount += itemsForSource.length;
|
|
5477
|
+
continue;
|
|
5478
|
+
}
|
|
5479
|
+
const discoveredPaths = findSkillMdPaths(tree);
|
|
5480
|
+
const deletedSkills = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([, entry]) => hasSameSourceAndRef(entry, source, ref)).map(([name]) => name), lock.skills, true, options, discoveredPaths, mode);
|
|
5481
|
+
const deletedSkillSet = new Set(deletedSkills);
|
|
5482
|
+
for (const { name: skillName, entry } of itemsForSource) {
|
|
5483
|
+
if (deletedSkillSet.has(skillName)) continue;
|
|
5484
|
+
const latestHash = getSkillFolderHashFromTree(tree, entry.skillPath);
|
|
5485
|
+
if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
|
|
5486
|
+
name: skillName,
|
|
5487
|
+
source,
|
|
5488
|
+
entry
|
|
5489
|
+
});
|
|
5490
|
+
}
|
|
5491
|
+
continue;
|
|
5492
|
+
}
|
|
5493
|
+
tempDir = await cloneRepo(sourceUrl, ref);
|
|
5494
|
+
const discoveredPaths = (await discoverSkills(tempDir)).map((skill) => {
|
|
5495
|
+
return join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/");
|
|
5496
|
+
});
|
|
5497
|
+
const deletedSkills = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([, entry]) => hasSameSourceAndRef(entry, source, ref)).map(([name]) => name), lock.skills, true, options, discoveredPaths, mode);
|
|
5498
|
+
const deletedSkillSet = new Set(deletedSkills);
|
|
5499
|
+
for (const { name: skillName, entry } of itemsForSource) {
|
|
5500
|
+
if (deletedSkillSet.has(skillName)) continue;
|
|
5501
|
+
const skillPath = entry.skillPath;
|
|
5502
|
+
if (!discoveredPaths.includes(skillPath)) continue;
|
|
5503
|
+
const latestHash = await computeSkillFolderHash(join(tempDir, dirname(skillPath)));
|
|
5504
|
+
if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
|
|
5505
|
+
name: skillName,
|
|
5506
|
+
source,
|
|
5507
|
+
entry
|
|
5508
|
+
});
|
|
5509
|
+
}
|
|
5510
|
+
} catch {
|
|
5511
|
+
console.log(` ${DIM$1}Failed to check skills from ${source}${RESET$1}`);
|
|
5512
|
+
checkFailCount += itemsForSource.length;
|
|
5513
|
+
} finally {
|
|
5514
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
if (checkable.length > 0) process.stdout.write("\r\x1B[K");
|
|
5518
|
+
const checkedCount = checkable.length + hubEntries.length + skipped.length;
|
|
5519
|
+
if (checkable.length === 0 && hubEntries.length === 0 && skipped.length === 0) {
|
|
5520
|
+
if (!options.skills) console.log(`${DIM$1}No global skills to check.${RESET$1}`);
|
|
5521
|
+
return {
|
|
5522
|
+
successCount,
|
|
5523
|
+
failCount,
|
|
5524
|
+
checkFailCount,
|
|
5525
|
+
checkedCount: 0,
|
|
5526
|
+
updateCount: 0
|
|
5527
|
+
};
|
|
5528
|
+
}
|
|
5529
|
+
if (checkable.length === 0 && hubEntries.length === 0 && skipped.length > 0) {
|
|
5530
|
+
printSkippedSkills(skipped);
|
|
5531
|
+
return {
|
|
5532
|
+
successCount,
|
|
5533
|
+
failCount,
|
|
5534
|
+
checkFailCount,
|
|
5535
|
+
checkedCount,
|
|
5536
|
+
updateCount: 0
|
|
5537
|
+
};
|
|
5538
|
+
}
|
|
5539
|
+
if (updates.length === 0) {
|
|
5540
|
+
if (checkFailCount > 0) console.log(`${DIM$1}Failed to check ${checkFailCount} global skill(s)${RESET$1}`);
|
|
5541
|
+
else console.log(`${TEXT$1}All global skills are up to date${RESET$1}`);
|
|
5542
|
+
printSkippedSkills(skipped);
|
|
5543
|
+
return {
|
|
5544
|
+
successCount,
|
|
5545
|
+
failCount,
|
|
5546
|
+
checkFailCount,
|
|
5547
|
+
checkedCount,
|
|
5548
|
+
updateCount: 0
|
|
5549
|
+
};
|
|
5550
|
+
}
|
|
5551
|
+
if (mode === "check") {
|
|
5552
|
+
console.log(`${TEXT$1}Found ${updates.length} global update(s)${RESET$1}`);
|
|
5553
|
+
console.log();
|
|
5554
|
+
for (const update of updates) {
|
|
5555
|
+
console.log(` ${TEXT$1}-${RESET$1} ${sanitizeMetadata(update.name)}`);
|
|
5556
|
+
console.log(` ${DIM$1}source: ${getInstallSource({
|
|
5557
|
+
name: update.name,
|
|
5558
|
+
reason: "",
|
|
5559
|
+
sourceUrl: update.installSource || update.entry.sourceUrl,
|
|
5560
|
+
sourceType: update.entry.sourceType,
|
|
5561
|
+
ref: update.entry.ref
|
|
5562
|
+
})}${RESET$1}`);
|
|
5563
|
+
}
|
|
5564
|
+
printSkippedSkills(skipped);
|
|
5565
|
+
return {
|
|
5566
|
+
successCount,
|
|
5567
|
+
failCount,
|
|
5568
|
+
checkFailCount,
|
|
5569
|
+
checkedCount,
|
|
5570
|
+
updateCount: updates.length
|
|
5571
|
+
};
|
|
5572
|
+
}
|
|
5573
|
+
console.log(`${TEXT$1}Found ${updates.length} global update(s)${RESET$1}`);
|
|
5574
|
+
console.log();
|
|
5575
|
+
for (const update of updates) {
|
|
5576
|
+
const safeName = sanitizeMetadata(update.name);
|
|
5577
|
+
console.log(`${TEXT$1}Updating ${safeName}...${RESET$1}`);
|
|
5578
|
+
const installUrl = buildUpdateInstallSource(update.entry, update.installSource);
|
|
5579
|
+
const cliEntry = join(__dirname$1, "..", "bin", "cli.mjs");
|
|
5580
|
+
if (!existsSync(cliEntry)) {
|
|
5581
|
+
failCount++;
|
|
5582
|
+
console.log(` ${DIM$1}Failed to update ${safeName}: CLI entrypoint not found at ${cliEntry}${RESET$1}`);
|
|
5583
|
+
continue;
|
|
5584
|
+
}
|
|
5585
|
+
const addArgs = region ? [
|
|
5586
|
+
cliEntry,
|
|
5587
|
+
"--region",
|
|
5588
|
+
region,
|
|
5589
|
+
"add",
|
|
5590
|
+
installUrl,
|
|
5591
|
+
"-g",
|
|
5592
|
+
"-y"
|
|
5593
|
+
] : [
|
|
5594
|
+
cliEntry,
|
|
5595
|
+
"add",
|
|
5596
|
+
installUrl,
|
|
5597
|
+
"-g",
|
|
5598
|
+
"-y"
|
|
5599
|
+
];
|
|
5600
|
+
if (spawnSync(process.execPath, addArgs, {
|
|
5601
|
+
stdio: [
|
|
5602
|
+
"inherit",
|
|
5603
|
+
"pipe",
|
|
5604
|
+
"pipe"
|
|
5605
|
+
],
|
|
5606
|
+
encoding: "utf-8",
|
|
5607
|
+
shell: process.platform === "win32"
|
|
5608
|
+
}).status === 0) {
|
|
5609
|
+
successCount++;
|
|
5610
|
+
console.log(` ${TEXT$1}Updated ${safeName}${RESET$1}`);
|
|
5611
|
+
} else {
|
|
5612
|
+
failCount++;
|
|
5613
|
+
console.log(` ${DIM$1}Failed to update ${safeName}${RESET$1}`);
|
|
5614
|
+
}
|
|
5615
|
+
}
|
|
5616
|
+
printSkippedSkills(skipped);
|
|
5617
|
+
return {
|
|
5618
|
+
successCount,
|
|
5619
|
+
failCount,
|
|
5620
|
+
checkFailCount,
|
|
5621
|
+
checkedCount,
|
|
5622
|
+
updateCount: updates.length
|
|
5623
|
+
};
|
|
5624
|
+
}
|
|
5625
|
+
async function updateProjectSkills(options = {}, regionOrMode, maybeMode) {
|
|
5626
|
+
const region = regionOrMode === "check" || regionOrMode === "update" ? void 0 : regionOrMode;
|
|
5627
|
+
const mode = regionOrMode === "check" || regionOrMode === "update" ? regionOrMode : maybeMode || "update";
|
|
5628
|
+
const projectSkills = await getProjectSkillsForUpdate(options.skills);
|
|
5629
|
+
let successCount = 0;
|
|
5630
|
+
let failCount = 0;
|
|
5631
|
+
let checkFailCount = 0;
|
|
5632
|
+
let updateCount = 0;
|
|
5633
|
+
if (projectSkills.length === 0) {
|
|
5634
|
+
if (!options.skills) {
|
|
5635
|
+
console.log(`${DIM$1}No project skills to update.${RESET$1}`);
|
|
5636
|
+
console.log(`${DIM$1}Install project skills with${RESET$1} ${TEXT$1}npx safeskill add <package>${RESET$1}`);
|
|
5637
|
+
}
|
|
5638
|
+
return {
|
|
5639
|
+
successCount,
|
|
5640
|
+
failCount,
|
|
5641
|
+
checkFailCount,
|
|
5642
|
+
foundCount: 0,
|
|
5643
|
+
updateCount
|
|
5644
|
+
};
|
|
5645
|
+
}
|
|
5646
|
+
const updatable = projectSkills.filter((s) => s.entry.skillPath);
|
|
5647
|
+
const legacy = projectSkills.filter((s) => !s.entry.skillPath);
|
|
5648
|
+
const hubUpdatable = updatable.filter((s) => s.entry.sourceType === "hub" && s.entry.resolvedVersion && s.entry.artifactDigest);
|
|
5649
|
+
const skippedHub = updatable.filter((s) => s.entry.sourceType === "hub" && (!s.entry.resolvedVersion || !s.entry.artifactDigest));
|
|
5650
|
+
const sourceUpdatable = updatable.filter((s) => s.entry.sourceType !== "hub");
|
|
5651
|
+
if (updatable.length === 0) {
|
|
5652
|
+
console.log(`${DIM$1}No project skills can be updated in place.${RESET$1}`);
|
|
5653
|
+
printLegacyProjectSkills(legacy);
|
|
5654
|
+
return {
|
|
5655
|
+
successCount,
|
|
5656
|
+
failCount,
|
|
5657
|
+
checkFailCount,
|
|
5658
|
+
foundCount: projectSkills.length,
|
|
5659
|
+
updateCount
|
|
5660
|
+
};
|
|
5661
|
+
}
|
|
5662
|
+
const cwd = process.cwd();
|
|
5663
|
+
const targetAgentNames = [];
|
|
5664
|
+
let hasUniversal = false;
|
|
5665
|
+
for (const [type, config] of Object.entries(agents)) if (isUniversalAgent(type)) {
|
|
5666
|
+
if (!hasUniversal && existsSync(join(cwd, ".agents"))) hasUniversal = true;
|
|
5667
|
+
} else {
|
|
5668
|
+
const agentRoot = config.skillsDir.split("/")[0];
|
|
5669
|
+
if (existsSync(join(cwd, agentRoot))) targetAgentNames.push(config.displayName);
|
|
5670
|
+
}
|
|
5671
|
+
const targetParts = [];
|
|
5672
|
+
if (hasUniversal) targetParts.push("Universal");
|
|
5673
|
+
targetParts.push(...targetAgentNames);
|
|
5674
|
+
if (targetParts.length > 0) {
|
|
5675
|
+
const action = mode === "check" ? "Checking" : "Updating";
|
|
5676
|
+
console.log(`${TEXT$1}${action} for: ${targetParts.join(", ")}${RESET$1}`);
|
|
5677
|
+
}
|
|
5678
|
+
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}`);
|
|
5679
|
+
console.log();
|
|
5680
|
+
const hubInstallSources = /* @__PURE__ */ new Map();
|
|
5681
|
+
if (hubUpdatable.length > 0) try {
|
|
5682
|
+
(await checkHubUpdates(hubUpdatable.map(({ entry }) => ({
|
|
5683
|
+
source: entry.source,
|
|
5684
|
+
version: entry.resolvedVersion,
|
|
5685
|
+
artifact_digest: entry.artifactDigest
|
|
5686
|
+
})))).items.forEach((item, index) => {
|
|
5687
|
+
const hubSkill = hubUpdatable[index];
|
|
5688
|
+
if (!hubSkill || !item.has_update || !item.latest_version) return;
|
|
5689
|
+
updateCount++;
|
|
5690
|
+
hubInstallSources.set(hubSkill.name, replaceHubSourceVersion(hubSkill.entry.source, item.latest_version));
|
|
5691
|
+
});
|
|
5692
|
+
} catch {
|
|
5693
|
+
console.log(`${DIM$1}Failed to check project hub skills${RESET$1}`);
|
|
5694
|
+
checkFailCount += hubUpdatable.length;
|
|
5695
|
+
}
|
|
5696
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
5697
|
+
for (const skill of sourceUpdatable) {
|
|
5698
|
+
const source = getSourceRefKey(skill.entry.source, skill.entry.ref);
|
|
5699
|
+
const existing = bySource.get(source) || [];
|
|
5700
|
+
existing.push(skill);
|
|
5701
|
+
bySource.set(source, existing);
|
|
5702
|
+
}
|
|
5703
|
+
const localLock = await readLocalLock();
|
|
5704
|
+
const cliEntry = join(__dirname$1, "..", "bin", "cli.mjs");
|
|
5705
|
+
if (mode === "update" && !existsSync(cliEntry)) {
|
|
5706
|
+
console.log(`${DIM$1}CLI entrypoint not found at ${cliEntry}${RESET$1}`);
|
|
5707
|
+
return {
|
|
5708
|
+
successCount,
|
|
5709
|
+
failCount: updatable.length,
|
|
5710
|
+
checkFailCount,
|
|
5711
|
+
foundCount: projectSkills.length,
|
|
5712
|
+
updateCount
|
|
5713
|
+
};
|
|
5714
|
+
}
|
|
5715
|
+
for (const [, skillsForSource] of bySource) {
|
|
5716
|
+
const firstEntry = skillsForSource[0].entry;
|
|
5717
|
+
const sourceUrl = firstEntry.source;
|
|
5718
|
+
const ref = firstEntry.ref;
|
|
5719
|
+
const allLockedForSource = Object.entries(localLock.skills).filter(([, entry]) => hasSameSourceAndRef(entry, sourceUrl, ref)).map(([name]) => name);
|
|
5720
|
+
let tempDir = null;
|
|
5721
|
+
let deletedSkills = [];
|
|
5722
|
+
let discoveredPaths = [];
|
|
5723
|
+
let sourceCheckFailed = false;
|
|
5724
|
+
try {
|
|
5725
|
+
tempDir = await cloneRepo(sourceUrl, ref);
|
|
5726
|
+
discoveredPaths = (await discoverSkills(tempDir)).map((s) => {
|
|
5727
|
+
return join(relative(tempDir, s.path), "SKILL.md").split(sep).join("/");
|
|
5728
|
+
});
|
|
5729
|
+
deletedSkills = await checkAndPromptForDeletions(sourceUrl, allLockedForSource, localLock.skills, false, options, discoveredPaths, mode);
|
|
5730
|
+
} catch {
|
|
5731
|
+
console.log(`${DIM$1}Failed to check skills from ${sourceUrl}${RESET$1}`);
|
|
5732
|
+
checkFailCount += skillsForSource.length;
|
|
5733
|
+
sourceCheckFailed = true;
|
|
5734
|
+
} finally {
|
|
5735
|
+
if (tempDir && (mode === "update" || sourceCheckFailed)) await cleanupTempDir(tempDir);
|
|
5736
|
+
}
|
|
5737
|
+
if (sourceCheckFailed) continue;
|
|
5738
|
+
const remainingSkills = skillsForSource.filter((s) => !deletedSkills.includes(s.name));
|
|
5739
|
+
if (mode === "check") {
|
|
5740
|
+
if (tempDir) try {
|
|
5741
|
+
for (const skill of remainingSkills) {
|
|
5742
|
+
const skillPath = skill.entry.skillPath;
|
|
5743
|
+
if (!discoveredPaths.includes(skillPath)) continue;
|
|
5744
|
+
const latestHash = await computeSkillFolderHash(join(tempDir, dirname(skillPath)));
|
|
5745
|
+
if (latestHash && latestHash !== skill.entry.computedHash) updateCount++;
|
|
5746
|
+
}
|
|
5747
|
+
} finally {
|
|
5748
|
+
await cleanupTempDir(tempDir);
|
|
5749
|
+
}
|
|
5750
|
+
continue;
|
|
5751
|
+
}
|
|
5752
|
+
for (const skill of remainingSkills) {
|
|
5753
|
+
const safeName = sanitizeMetadata(skill.name);
|
|
5754
|
+
console.log(`${TEXT$1}Updating ${safeName}...${RESET$1}`);
|
|
5755
|
+
const installUrl = buildLocalUpdateSource(skill.entry);
|
|
5756
|
+
const subagentArgs = skill.entry.subagents?.length ? ["--subagent", ...skill.entry.subagents.map((s) => s === "" ? "root" : s)] : [];
|
|
5757
|
+
const regionArgs = region ? ["--region", region] : [];
|
|
5758
|
+
if (spawnSync(process.execPath, [
|
|
5759
|
+
cliEntry,
|
|
5760
|
+
...regionArgs,
|
|
5761
|
+
"add",
|
|
5762
|
+
installUrl,
|
|
5763
|
+
"--skill",
|
|
5764
|
+
skill.name,
|
|
5765
|
+
...subagentArgs,
|
|
5766
|
+
"-y"
|
|
5767
|
+
], {
|
|
5768
|
+
stdio: [
|
|
5769
|
+
"inherit",
|
|
5770
|
+
"pipe",
|
|
5771
|
+
"pipe"
|
|
5772
|
+
],
|
|
5773
|
+
encoding: "utf-8",
|
|
5774
|
+
shell: process.platform === "win32"
|
|
5775
|
+
}).status === 0) {
|
|
5776
|
+
successCount++;
|
|
5777
|
+
console.log(` ${TEXT$1}Updated ${safeName}${RESET$1}`);
|
|
5778
|
+
} else {
|
|
5779
|
+
failCount++;
|
|
5780
|
+
console.log(` ${DIM$1}Failed to update ${safeName}${RESET$1}`);
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
if (mode === "update") for (const skill of hubUpdatable) {
|
|
5785
|
+
const installUrl = hubInstallSources.get(skill.name);
|
|
5786
|
+
if (!installUrl) continue;
|
|
5787
|
+
const safeName = sanitizeMetadata(skill.name);
|
|
5788
|
+
console.log(`${TEXT$1}Updating ${safeName}...${RESET$1}`);
|
|
5789
|
+
const subagentArgs = skill.entry.subagents?.length ? ["--subagent", ...skill.entry.subagents.map((s) => s === "" ? "root" : s)] : [];
|
|
5790
|
+
const regionArgs = region ? ["--region", region] : [];
|
|
5791
|
+
if (spawnSync(process.execPath, [
|
|
5792
|
+
cliEntry,
|
|
5793
|
+
...regionArgs,
|
|
5794
|
+
"add",
|
|
5795
|
+
installUrl,
|
|
5796
|
+
"--skill",
|
|
5797
|
+
skill.name,
|
|
5798
|
+
...subagentArgs,
|
|
5799
|
+
"-y"
|
|
5800
|
+
], {
|
|
5801
|
+
stdio: [
|
|
5802
|
+
"inherit",
|
|
5803
|
+
"pipe",
|
|
5804
|
+
"pipe"
|
|
5805
|
+
],
|
|
5806
|
+
encoding: "utf-8",
|
|
5807
|
+
shell: process.platform === "win32"
|
|
5808
|
+
}).status === 0) {
|
|
5809
|
+
successCount++;
|
|
5810
|
+
console.log(` ${TEXT$1}Updated ${safeName}${RESET$1}`);
|
|
5811
|
+
} else {
|
|
5812
|
+
failCount++;
|
|
5813
|
+
console.log(` ${DIM$1}Failed to update ${safeName}${RESET$1}`);
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5816
|
+
if (mode === "check") if (updateCount > 0) console.log(`${TEXT$1}Found ${updateCount} project update(s)${RESET$1}`);
|
|
5817
|
+
else if (checkFailCount > 0) console.log(`${DIM$1}Failed to check ${checkFailCount} project skill(s)${RESET$1}`);
|
|
5818
|
+
else console.log(`${TEXT$1}All project skills are up to date${RESET$1}`);
|
|
5819
|
+
printLegacyProjectSkills(legacy);
|
|
5820
|
+
printSkippedProjectHubSkills(skippedHub);
|
|
5821
|
+
return {
|
|
5822
|
+
successCount,
|
|
5823
|
+
failCount,
|
|
5824
|
+
checkFailCount,
|
|
5825
|
+
foundCount: projectSkills.length,
|
|
5826
|
+
updateCount
|
|
5827
|
+
};
|
|
5828
|
+
}
|
|
5829
|
+
function printLegacyProjectSkills(legacy) {
|
|
5830
|
+
if (legacy.length === 0) return;
|
|
5831
|
+
console.log();
|
|
5832
|
+
console.log(`${DIM$1}${legacy.length} project skill(s) cannot be updated automatically (installed before skillPath tracking):${RESET$1}`);
|
|
5833
|
+
for (const skill of legacy) {
|
|
5834
|
+
const reinstall = formatSourceInput(skill.entry.source, skill.entry.ref);
|
|
5835
|
+
console.log(` ${TEXT$1}-${RESET$1} ${sanitizeMetadata(skill.name)}`);
|
|
5836
|
+
console.log(` ${DIM$1}To refresh: ${TEXT$1}npx safeskill add ${reinstall} -y${RESET$1}`);
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
async function runUpdate(args = [], region) {
|
|
5840
|
+
const options = parseUpdateOptions(args);
|
|
5841
|
+
const scope = await resolveUpdateScope(options);
|
|
5842
|
+
if (options.skills) console.log(`${TEXT$1}Updating ${options.skills.join(", ")}...${RESET$1}`);
|
|
5843
|
+
else console.log(`${TEXT$1}Checking for skill updates...${RESET$1}`);
|
|
5844
|
+
console.log();
|
|
5845
|
+
let totalSuccess = 0;
|
|
5846
|
+
let totalFail = 0;
|
|
5847
|
+
let totalCheckFail = 0;
|
|
5848
|
+
let totalFound = 0;
|
|
5849
|
+
if (scope === "global" || scope === "both") {
|
|
5850
|
+
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Global Skills${RESET$1}`);
|
|
5851
|
+
const { successCount, failCount, checkFailCount, checkedCount } = await updateGlobalSkills(options, region);
|
|
5852
|
+
totalSuccess += successCount;
|
|
5853
|
+
totalFail += failCount;
|
|
5854
|
+
totalCheckFail += checkFailCount;
|
|
5855
|
+
totalFound += checkedCount;
|
|
5856
|
+
if (scope === "both" && !options.skills) console.log();
|
|
5857
|
+
}
|
|
5858
|
+
if (scope === "project" || scope === "both") {
|
|
5859
|
+
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Project Skills${RESET$1}`);
|
|
5860
|
+
const { successCount, failCount, checkFailCount, foundCount } = await updateProjectSkills(options, region);
|
|
5861
|
+
totalSuccess += successCount;
|
|
5862
|
+
totalFail += failCount;
|
|
5863
|
+
totalCheckFail += checkFailCount;
|
|
5864
|
+
totalFound += foundCount;
|
|
5865
|
+
}
|
|
5866
|
+
if (options.skills && totalFound === 0) console.log(`${DIM$1}No installed skills found matching: ${options.skills.join(", ")}${RESET$1}`);
|
|
5867
|
+
console.log();
|
|
5868
|
+
if (totalSuccess > 0) console.log(`${TEXT$1}Updated ${totalSuccess} skill(s)${RESET$1}`);
|
|
5869
|
+
if (totalFail > 0) console.log(`${DIM$1}Failed to update ${totalFail} skill(s)${RESET$1}`);
|
|
5870
|
+
if (totalCheckFail > 0) console.log(`${DIM$1}Failed to check ${totalCheckFail} skill(s)${RESET$1}`);
|
|
5871
|
+
track({
|
|
5872
|
+
event: "update",
|
|
5873
|
+
scope,
|
|
5874
|
+
skillCount: String(totalSuccess + totalFail),
|
|
5875
|
+
successCount: String(totalSuccess),
|
|
5876
|
+
failCount: String(totalFail),
|
|
5877
|
+
checkFailCount: String(totalCheckFail)
|
|
5878
|
+
});
|
|
5879
|
+
console.log();
|
|
5880
|
+
}
|
|
5881
|
+
async function runCheck(args = []) {
|
|
5882
|
+
const options = parseUpdateOptions(args);
|
|
5883
|
+
const scope = await resolveUpdateScope(options);
|
|
5884
|
+
if (options.skills) console.log(`${TEXT$1}Checking ${options.skills.join(", ")}...${RESET$1}`);
|
|
5885
|
+
else console.log(`${TEXT$1}Checking for skill updates...${RESET$1}`);
|
|
5886
|
+
console.log();
|
|
5887
|
+
let totalFound = 0;
|
|
5888
|
+
let totalUpdates = 0;
|
|
5889
|
+
let totalFail = 0;
|
|
5890
|
+
if (scope === "global" || scope === "both") {
|
|
5891
|
+
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Global Skills${RESET$1}`);
|
|
5892
|
+
const { failCount, checkFailCount, checkedCount, updateCount } = await updateGlobalSkills(options, void 0, "check");
|
|
5893
|
+
totalFail += failCount + checkFailCount;
|
|
5894
|
+
totalFound += checkedCount;
|
|
5895
|
+
totalUpdates += updateCount;
|
|
5896
|
+
if (scope === "both" && !options.skills) console.log();
|
|
5897
|
+
}
|
|
5898
|
+
if (scope === "project" || scope === "both") {
|
|
5899
|
+
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Project Skills${RESET$1}`);
|
|
5900
|
+
const { failCount, checkFailCount, foundCount, updateCount } = await updateProjectSkills(options, "check");
|
|
5901
|
+
totalFail += failCount + checkFailCount;
|
|
5902
|
+
totalFound += foundCount;
|
|
5903
|
+
totalUpdates += updateCount;
|
|
5904
|
+
}
|
|
5905
|
+
if (options.skills && totalFound === 0) console.log(`${DIM$1}No installed skills found matching: ${options.skills.join(", ")}${RESET$1}`);
|
|
5906
|
+
if (totalFail > 0) {
|
|
5907
|
+
console.log();
|
|
5908
|
+
console.log(`${DIM$1}Failed to check ${totalFail} skill(s)${RESET$1}`);
|
|
5909
|
+
}
|
|
5910
|
+
track({
|
|
5911
|
+
event: "check",
|
|
5912
|
+
skillCount: String(totalFound),
|
|
5913
|
+
updatesAvailable: String(totalUpdates),
|
|
5914
|
+
failCount: String(totalFail)
|
|
5915
|
+
});
|
|
5916
|
+
console.log();
|
|
5917
|
+
}
|
|
5918
|
+
const BLOB_ALLOWED_OWNERS = [
|
|
5919
|
+
"vercel",
|
|
5920
|
+
"vercel-labs",
|
|
5921
|
+
"heygen-com"
|
|
5922
|
+
];
|
|
5923
|
+
const EXCLUDE_FILES = new Set(["metadata.json"]);
|
|
5924
|
+
const EXCLUDE_DIRS = new Set([
|
|
5925
|
+
".git",
|
|
5926
|
+
"__pycache__",
|
|
5927
|
+
"__pypackages__"
|
|
5928
|
+
]);
|
|
5929
|
+
const USE_AGENT_CONFIGS = {
|
|
5930
|
+
"claude-code": {
|
|
5931
|
+
command: "claude",
|
|
5932
|
+
args: []
|
|
5933
|
+
},
|
|
5934
|
+
codex: {
|
|
5935
|
+
command: "codex",
|
|
5936
|
+
args: []
|
|
5937
|
+
}
|
|
5938
|
+
};
|
|
5939
|
+
const SUPPORTED_USE_AGENTS = Object.keys(USE_AGENT_CONFIGS);
|
|
5940
|
+
function parseUseOptions(args) {
|
|
5941
|
+
const source = [];
|
|
5942
|
+
const options = {};
|
|
5943
|
+
const errors = [];
|
|
5944
|
+
for (let i = 0; i < args.length; i++) {
|
|
5945
|
+
const arg = args[i];
|
|
5946
|
+
if (!arg) continue;
|
|
5947
|
+
if (arg === "--help" || arg === "-h") options.help = true;
|
|
5948
|
+
else if (arg === "--full-depth") options.fullDepth = true;
|
|
5949
|
+
else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true;
|
|
5950
|
+
else if (arg === "--skill" || arg === "-s") {
|
|
5951
|
+
const value = args[i + 1];
|
|
5952
|
+
if (!value || value.startsWith("-")) errors.push(`${arg} requires a skill name`);
|
|
5953
|
+
else if (options.skill) {
|
|
5954
|
+
errors.push("Only one --skill value can be provided");
|
|
5955
|
+
i++;
|
|
5956
|
+
} else {
|
|
5957
|
+
options.skill = value;
|
|
5958
|
+
i++;
|
|
5959
|
+
}
|
|
5960
|
+
} else if (arg === "--agent" || arg === "-a") {
|
|
5961
|
+
options.agent = options.agent || [];
|
|
5962
|
+
i++;
|
|
5963
|
+
let nextArg = args[i];
|
|
5964
|
+
const startCount = options.agent.length;
|
|
5965
|
+
while (i < args.length && nextArg && !nextArg.startsWith("-")) {
|
|
5966
|
+
options.agent.push(nextArg);
|
|
5967
|
+
i++;
|
|
5968
|
+
nextArg = args[i];
|
|
5969
|
+
}
|
|
5970
|
+
if (options.agent.length === startCount) errors.push(`${arg} requires an agent name`);
|
|
5971
|
+
i--;
|
|
5972
|
+
} else if (arg.startsWith("-")) errors.push(`Unknown option: ${arg}`);
|
|
5973
|
+
else source.push(arg);
|
|
5974
|
+
}
|
|
5975
|
+
errors.push(...validateUseAgentOption(options.agent));
|
|
5976
|
+
return {
|
|
5977
|
+
source,
|
|
5978
|
+
options,
|
|
5979
|
+
errors
|
|
5980
|
+
};
|
|
5981
|
+
}
|
|
5982
|
+
function buildUsePrompt(input) {
|
|
5983
|
+
const sections = [
|
|
5984
|
+
"You are being given a Skill to execute for the user's next request.",
|
|
5985
|
+
"Use the following SKILL.md as your instructions:",
|
|
5986
|
+
`<SKILL.md>\n${input.skillMd}\n</SKILL.md>`
|
|
5987
|
+
];
|
|
5988
|
+
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.`);
|
|
5989
|
+
return sections.join("\n\n") + "\n";
|
|
5990
|
+
}
|
|
5991
|
+
async function materializeUseSkill(skill) {
|
|
5992
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "safeskill-use-"));
|
|
5993
|
+
const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
|
|
5994
|
+
if (!isPathSafe(tempRoot, skillDir)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
5995
|
+
await mkdir(skillDir, { recursive: true });
|
|
5996
|
+
if (skill.kind === "blob") await writeSnapshotFiles(skillDir, skill.files);
|
|
5997
|
+
else if (skill.kind === "well-known") await writeMapFiles(skillDir, skill.files);
|
|
5998
|
+
else await copySkillDirectory(skill.path, skillDir);
|
|
5999
|
+
return {
|
|
6000
|
+
tempRoot,
|
|
6001
|
+
skillDir,
|
|
6002
|
+
skillMd: skill.rawContent ?? await readFile(join(skillDir, "SKILL.md"), "utf-8"),
|
|
6003
|
+
hasSupportingFiles: await containsSupportingFiles(skillDir, skillDir)
|
|
6004
|
+
};
|
|
6005
|
+
}
|
|
6006
|
+
async function runUse(sourceArgs, options = {}, parseErrors = []) {
|
|
6007
|
+
let cloneTempDir = null;
|
|
6008
|
+
try {
|
|
6009
|
+
if (options.help) {
|
|
6010
|
+
console.log(getUseHelp());
|
|
6011
|
+
return;
|
|
6012
|
+
}
|
|
6013
|
+
if (parseErrors.length > 0) fail(parseErrors.join("\n"));
|
|
6014
|
+
if (sourceArgs.length === 0) fail(`Missing required argument: source\n\n${getUseHelp()}`);
|
|
6015
|
+
if (sourceArgs.length > 1) fail(`Expected one source, received ${sourceArgs.length}: ${sourceArgs.join(", ")}`);
|
|
6016
|
+
const useAgent = options.agent?.[0];
|
|
6017
|
+
if (useAgent && !USE_AGENT_CONFIGS[useAgent]) fail(formatUnsupportedAgentError(useAgent));
|
|
6018
|
+
const source = sourceArgs[0];
|
|
6019
|
+
const parsed = parseSource(source);
|
|
6020
|
+
if (getOwnerRepo(parsed)?.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) fail([
|
|
6021
|
+
"OpenClaw skills are unverified community submissions.",
|
|
6022
|
+
"Skills run with full agent permissions and could be malicious.",
|
|
6023
|
+
`If you understand the risks, re-run with: safeskill use ${source} --dangerously-accept-openclaw-risks`
|
|
6024
|
+
].join("\n"));
|
|
6025
|
+
const selector = resolveSelector(parsed.skillFilter, options.skill);
|
|
6026
|
+
const includeInternal = selector !== void 0;
|
|
6027
|
+
let selectedSkill;
|
|
6028
|
+
if (parsed.type === "well-known") selectedSkill = selectWellKnownSkill(await wellKnownProvider.fetchAllSkills(parsed.url), selector, source);
|
|
6029
|
+
else {
|
|
6030
|
+
let skills;
|
|
6031
|
+
let blobResult = null;
|
|
6032
|
+
if (parsed.type === "local") {
|
|
6033
|
+
if (!existsSync(parsed.localPath)) fail(`Local path does not exist: ${parsed.localPath}`);
|
|
6034
|
+
skills = await discoverSkills(parsed.localPath, parsed.subpath, {
|
|
6035
|
+
includeInternal,
|
|
6036
|
+
fullDepth: options.fullDepth
|
|
6037
|
+
});
|
|
6038
|
+
} else if (parsed.type === "hub") {
|
|
6039
|
+
const downloaded = await downloadHubSource(source);
|
|
6040
|
+
cloneTempDir = downloaded.tempDir;
|
|
6041
|
+
skills = await discoverSkills(downloaded.tempDir, parsed.subpath, {
|
|
6042
|
+
includeInternal,
|
|
6043
|
+
fullDepth: options.fullDepth
|
|
6044
|
+
});
|
|
6045
|
+
if (skills.length === 0 && !parsed.subpath && downloaded.resolved.skill) {
|
|
6046
|
+
const fallbackSkill = await parseSkillMd(join(downloaded.tempDir, "SKILL.md"), {
|
|
6047
|
+
includeInternal,
|
|
6048
|
+
fallbackName: downloaded.resolved.skill.display_name || downloaded.resolved.skill.name,
|
|
6049
|
+
fallbackDescription: downloaded.resolved.skill.summary || downloaded.resolved.skill.name,
|
|
6050
|
+
fallbackSlug: downloaded.resolved.skill.name,
|
|
6051
|
+
fallbackInstallName: downloaded.resolved.skill.name
|
|
6052
|
+
});
|
|
6053
|
+
if (fallbackSkill) {
|
|
6054
|
+
fallbackSkill.rawContent = withFallbackFrontmatter(fallbackSkill);
|
|
6055
|
+
skills = [fallbackSkill];
|
|
6056
|
+
}
|
|
6057
|
+
}
|
|
6058
|
+
} else if (parsed.type === "github" && !options.fullDepth) {
|
|
6059
|
+
const ownerRepo = getOwnerRepo(parsed);
|
|
6060
|
+
const owner = ownerRepo?.split("/")[0]?.toLowerCase();
|
|
6061
|
+
if (ownerRepo && owner && BLOB_ALLOWED_OWNERS.includes(owner)) blobResult = await tryBlobInstall(ownerRepo, {
|
|
6062
|
+
subpath: parsed.subpath,
|
|
6063
|
+
skillFilter: selector,
|
|
6064
|
+
ref: parsed.ref,
|
|
6065
|
+
getToken: getGitHubToken,
|
|
6066
|
+
includeInternal
|
|
6067
|
+
});
|
|
6068
|
+
if (blobResult) skills = blobResult.skills;
|
|
6069
|
+
else {
|
|
6070
|
+
cloneTempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
6071
|
+
skills = await discoverSkills(cloneTempDir, parsed.subpath, {
|
|
6072
|
+
includeInternal,
|
|
6073
|
+
fullDepth: options.fullDepth
|
|
6074
|
+
});
|
|
6075
|
+
}
|
|
6076
|
+
} else {
|
|
6077
|
+
cloneTempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
6078
|
+
skills = await discoverSkills(cloneTempDir, parsed.subpath, {
|
|
6079
|
+
includeInternal,
|
|
6080
|
+
fullDepth: options.fullDepth
|
|
6081
|
+
});
|
|
6082
|
+
}
|
|
6083
|
+
const selected = selectSkill(skills, selector, source);
|
|
6084
|
+
if (blobResult && isBlobSkill(selected)) selectedSkill = {
|
|
6085
|
+
kind: "blob",
|
|
6086
|
+
name: selected.name,
|
|
6087
|
+
directoryName: getDirectoryName(selected),
|
|
6088
|
+
rawContent: selected.rawContent ?? getSkillMdFromSnapshot(selected.files),
|
|
6089
|
+
files: selected.files
|
|
6090
|
+
};
|
|
6091
|
+
else selectedSkill = {
|
|
6092
|
+
kind: "disk",
|
|
6093
|
+
name: selected.name,
|
|
6094
|
+
directoryName: getDirectoryName(selected),
|
|
6095
|
+
rawContent: selected.rawContent,
|
|
6096
|
+
path: selected.path
|
|
6097
|
+
};
|
|
6098
|
+
}
|
|
6099
|
+
const materialized = await materializeUseSkill(selectedSkill);
|
|
6100
|
+
await cleanupClone(cloneTempDir);
|
|
6101
|
+
cloneTempDir = null;
|
|
6102
|
+
const prompt = buildUsePrompt({
|
|
6103
|
+
skillMd: materialized.skillMd,
|
|
6104
|
+
supportDir: materialized.skillDir,
|
|
6105
|
+
hasSupportingFiles: materialized.hasSupportingFiles
|
|
6106
|
+
});
|
|
6107
|
+
if (useAgent) {
|
|
6108
|
+
const exitCode = await launchAgentInteractively(useAgent, prompt);
|
|
6109
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
6110
|
+
return;
|
|
6111
|
+
}
|
|
6112
|
+
process.stdout.write(prompt);
|
|
6113
|
+
} catch (error) {
|
|
6114
|
+
await cleanupClone(cloneTempDir);
|
|
6115
|
+
if (error instanceof GitCloneError) fail(error.message);
|
|
6116
|
+
if (error instanceof UseCommandError) fail(error.message);
|
|
6117
|
+
fail(error instanceof Error ? error.message : "Unknown error");
|
|
6118
|
+
}
|
|
6119
|
+
}
|
|
6120
|
+
async function launchAgentInteractively(agent, prompt, spawnImpl = spawnAgent) {
|
|
6121
|
+
const config = USE_AGENT_CONFIGS[agent];
|
|
6122
|
+
if (!config) throw new UseCommandError(formatUnsupportedAgentError(agent));
|
|
6123
|
+
return new Promise((resolve, reject) => {
|
|
6124
|
+
const child = spawnImpl(config.command, [...config.args, prompt], { stdio: "inherit" });
|
|
6125
|
+
let settled = false;
|
|
6126
|
+
child.on("error", (error) => {
|
|
6127
|
+
if (settled) return;
|
|
6128
|
+
settled = true;
|
|
6129
|
+
if (error.code === "ENOENT") {
|
|
6130
|
+
reject(new UseCommandError(`Could not launch ${agents[agent].displayName}: command not found: ${config.command}`));
|
|
6131
|
+
return;
|
|
6132
|
+
}
|
|
6133
|
+
reject(error);
|
|
6134
|
+
});
|
|
6135
|
+
child.on("close", (code) => {
|
|
6136
|
+
if (settled) return;
|
|
6137
|
+
settled = true;
|
|
6138
|
+
resolve(code ?? 1);
|
|
6139
|
+
});
|
|
6140
|
+
});
|
|
6141
|
+
}
|
|
6142
|
+
function spawnAgent(command, args) {
|
|
6143
|
+
return spawn(command, args, { stdio: "inherit" });
|
|
6144
|
+
}
|
|
6145
|
+
function getUseHelp() {
|
|
6146
|
+
return `Usage: safeskill use <source>[@<skill>] [options]
|
|
6147
|
+
|
|
6148
|
+
Generate a prompt for using one skill without installing it.
|
|
6149
|
+
|
|
6150
|
+
Options:
|
|
6151
|
+
-s, --skill <skill> Select the skill to use
|
|
6152
|
+
-a, --agent <agent> Start one supported agent interactively (${SUPPORTED_USE_AGENTS.join(", ")})
|
|
6153
|
+
--full-depth Search nested directories like safeskill add --full-depth
|
|
6154
|
+
--dangerously-accept-openclaw-risks
|
|
6155
|
+
Allow unverified OpenClaw community skills
|
|
6156
|
+
-h, --help Show this help message
|
|
6157
|
+
|
|
6158
|
+
Examples:
|
|
6159
|
+
safeskill use vercel-labs/agent-skills@web-design-guidelines | claude
|
|
6160
|
+
safeskill use vercel-labs/agent-skills --skill web-design-guidelines --agent claude-code
|
|
6161
|
+
safeskill use vercel-labs/agent-skills@web-design-guidelines --agent codex`;
|
|
6162
|
+
}
|
|
6163
|
+
function resolveSelector(sourceSelector, optionSelector) {
|
|
6164
|
+
if (sourceSelector && optionSelector) {
|
|
6165
|
+
if (sourceSelector.toLowerCase() !== optionSelector.toLowerCase()) throw new UseCommandError(`Conflicting skill selectors: source selects "${sourceSelector}" but --skill selects "${optionSelector}". Provide one selector.`);
|
|
6166
|
+
return optionSelector;
|
|
6167
|
+
}
|
|
6168
|
+
return optionSelector ?? sourceSelector;
|
|
6169
|
+
}
|
|
6170
|
+
function selectSkill(skills, selector, source) {
|
|
6171
|
+
if (skills.length === 0) throw new UseCommandError("No valid skills found. Skills require a SKILL.md with name and description.");
|
|
6172
|
+
if (!selector) {
|
|
6173
|
+
if (skills.length === 1) return skills[0];
|
|
6174
|
+
throw new UseCommandError(formatMultipleSkillsError(source, skills.map(getSkillDisplayName)));
|
|
6175
|
+
}
|
|
6176
|
+
const selected = filterSkills(skills, [selector]);
|
|
6177
|
+
if (selected.length === 0) throw new UseCommandError(formatNoMatchError(selector, skills.map(getSkillDisplayName)));
|
|
6178
|
+
if (selected.length > 1) throw new UseCommandError(`Skill selector "${selector}" matched multiple skills.`);
|
|
6179
|
+
return selected[0];
|
|
6180
|
+
}
|
|
6181
|
+
function selectWellKnownSkill(skills, selector, source) {
|
|
6182
|
+
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.");
|
|
6183
|
+
let selected;
|
|
6184
|
+
if (!selector) {
|
|
6185
|
+
if (skills.length !== 1) throw new UseCommandError(formatMultipleSkillsError(source, skills.map((s) => s.installName)));
|
|
6186
|
+
selected = skills;
|
|
6187
|
+
} else {
|
|
6188
|
+
selected = skills.filter((skill) => skill.installName.toLowerCase() === selector.toLowerCase() || skill.name.toLowerCase() === selector.toLowerCase());
|
|
6189
|
+
if (selected.length === 0) throw new UseCommandError(formatNoMatchError(selector, skills.map((s) => s.installName)));
|
|
6190
|
+
if (selected.length > 1) throw new UseCommandError(`Skill selector "${selector}" matched multiple skills.`);
|
|
6191
|
+
}
|
|
6192
|
+
const skill = selected[0];
|
|
6193
|
+
return {
|
|
6194
|
+
kind: "well-known",
|
|
6195
|
+
name: skill.name,
|
|
6196
|
+
directoryName: skill.installName,
|
|
6197
|
+
rawContent: skill.content,
|
|
6198
|
+
files: skill.files
|
|
6199
|
+
};
|
|
6200
|
+
}
|
|
6201
|
+
function formatMultipleSkillsError(source, names) {
|
|
6202
|
+
return [
|
|
6203
|
+
"This source contains multiple skills. Specify exactly one skill:",
|
|
6204
|
+
...names.map((name) => ` - ${name}`),
|
|
6205
|
+
"",
|
|
6206
|
+
`Examples:\n safeskill use ${source}@${names[0] ?? "<skill>"}\n safeskill use ${source} --skill ${names[0] ?? "<skill>"}`
|
|
6207
|
+
].join("\n");
|
|
6208
|
+
}
|
|
6209
|
+
function formatNoMatchError(selector, names) {
|
|
6210
|
+
return [
|
|
6211
|
+
`No matching skill found for: ${selector}`,
|
|
6212
|
+
"Available skills:",
|
|
6213
|
+
...names.map((name) => ` - ${name}`)
|
|
6214
|
+
].join("\n");
|
|
6215
|
+
}
|
|
6216
|
+
function validateUseAgentOption(agentValues) {
|
|
6217
|
+
if (!agentValues || agentValues.length === 0) return [];
|
|
6218
|
+
const errors = [];
|
|
6219
|
+
const validAgents = Object.keys(agents);
|
|
6220
|
+
const invalidAgents = agentValues.filter((agent) => agent !== "*" && !validAgents.includes(agent));
|
|
6221
|
+
if (agentValues.includes("*")) errors.push("safeskill use --agent does not support '*'; specify exactly one agent.");
|
|
6222
|
+
if (agentValues.length > 1) errors.push("safeskill use --agent accepts exactly one agent.");
|
|
6223
|
+
if (invalidAgents.length > 0) errors.push(`Invalid agents: ${invalidAgents.join(", ")}\nValid agents: ${validAgents.join(", ")}`);
|
|
6224
|
+
return errors;
|
|
6225
|
+
}
|
|
6226
|
+
function formatUnsupportedAgentError(agent) {
|
|
6227
|
+
return [`Running ${agents[agent].displayName} is not supported yet.`, `Supported agents for safeskill use --agent: ${SUPPORTED_USE_AGENTS.join(", ")}`].join("\n");
|
|
6228
|
+
}
|
|
6229
|
+
async function writeSnapshotFiles(targetDir, files) {
|
|
6230
|
+
for (const file of files) await writeSafeFile(targetDir, file.path, file.contents);
|
|
6231
|
+
}
|
|
6232
|
+
async function writeMapFiles(targetDir, files) {
|
|
6233
|
+
for (const [path, contents] of files) await writeSafeFile(targetDir, path, contents);
|
|
6234
|
+
}
|
|
6235
|
+
async function writeSafeFile(targetDir, filePath, contents) {
|
|
6236
|
+
const fullPath = join(targetDir, filePath);
|
|
6237
|
+
if (!isPathSafe(targetDir, fullPath)) return;
|
|
6238
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
6239
|
+
if (typeof contents === "string") await writeFile(fullPath, contents, "utf-8");
|
|
6240
|
+
else await writeFile(fullPath, contents);
|
|
6241
|
+
}
|
|
6242
|
+
async function copySkillDirectory(src, dest) {
|
|
6243
|
+
await mkdir(dest, { recursive: true });
|
|
6244
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
6245
|
+
await Promise.all(entries.filter((entry) => !isExcluded(entry.name, entry.isDirectory())).map(async (entry) => {
|
|
6246
|
+
const srcPath = join(src, entry.name);
|
|
6247
|
+
const destPath = join(dest, entry.name);
|
|
6248
|
+
if (!isPathSafe(dest, destPath)) return;
|
|
6249
|
+
if (entry.isDirectory()) {
|
|
6250
|
+
await copySkillDirectory(srcPath, destPath);
|
|
6251
|
+
return;
|
|
6252
|
+
}
|
|
6253
|
+
try {
|
|
6254
|
+
await cp(srcPath, destPath, {
|
|
6255
|
+
dereference: true,
|
|
6256
|
+
recursive: true
|
|
6257
|
+
});
|
|
6258
|
+
} catch (err) {
|
|
6259
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT" && entry.isSymbolicLink()) {
|
|
6260
|
+
console.error(`Skipping broken symlink: ${srcPath}`);
|
|
6261
|
+
return;
|
|
6262
|
+
}
|
|
6263
|
+
throw err;
|
|
6264
|
+
}
|
|
6265
|
+
}));
|
|
6266
|
+
}
|
|
6267
|
+
async function containsSupportingFiles(rootDir, currentDir) {
|
|
6268
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
6269
|
+
for (const entry of entries) {
|
|
6270
|
+
const entryPath = join(currentDir, entry.name);
|
|
6271
|
+
const relPath = relative(rootDir, entryPath).split(sep).join("/");
|
|
6272
|
+
if (entry.isDirectory()) {
|
|
6273
|
+
if (await containsSupportingFiles(rootDir, entryPath)) return true;
|
|
6274
|
+
} else if (relPath.toLowerCase() !== "skill.md") return true;
|
|
6275
|
+
}
|
|
6276
|
+
return false;
|
|
6277
|
+
}
|
|
6278
|
+
function isBlobSkill(skill) {
|
|
6279
|
+
return Array.isArray(skill.files);
|
|
6280
|
+
}
|
|
6281
|
+
function getSkillMdFromSnapshot(files) {
|
|
6282
|
+
return files.find((file) => file.path.toLowerCase() === "skill.md")?.contents ?? "";
|
|
6283
|
+
}
|
|
6284
|
+
function getDirectoryName(skill) {
|
|
6285
|
+
return skill.installName || skill.name;
|
|
6286
|
+
}
|
|
6287
|
+
function withFallbackFrontmatter(skill) {
|
|
6288
|
+
if (!skill.rawContent || skill.rawContent.trimStart().startsWith("---")) return skill.rawContent;
|
|
6289
|
+
return [
|
|
6290
|
+
"---",
|
|
6291
|
+
`name: ${skill.name}`,
|
|
6292
|
+
`description: ${skill.description}`,
|
|
6293
|
+
"---",
|
|
6294
|
+
"",
|
|
6295
|
+
skill.rawContent
|
|
6296
|
+
].join("\n");
|
|
6297
|
+
}
|
|
6298
|
+
function isExcluded(name, isDirectory) {
|
|
6299
|
+
return EXCLUDE_FILES.has(name) || isDirectory && EXCLUDE_DIRS.has(name);
|
|
6300
|
+
}
|
|
6301
|
+
function isPathSafe(basePath, targetPath) {
|
|
6302
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
6303
|
+
const normalizedTarget = normalize(resolve(targetPath));
|
|
6304
|
+
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
6305
|
+
}
|
|
6306
|
+
async function cleanupClone(tempDir) {
|
|
6307
|
+
if (tempDir) await cleanupTempDir(tempDir).catch(() => {});
|
|
6308
|
+
}
|
|
6309
|
+
function fail(message) {
|
|
6310
|
+
console.error(message);
|
|
6311
|
+
process.exit(1);
|
|
6312
|
+
}
|
|
6313
|
+
var UseCommandError = class extends Error {};
|
|
6314
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6315
|
+
function getVersion() {
|
|
6316
|
+
try {
|
|
6317
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
6318
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
6319
|
+
} catch {
|
|
6320
|
+
return "0.0.0";
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
const VERSION = getVersion();
|
|
6324
|
+
initTelemetry(VERSION);
|
|
6325
|
+
const RESET = "\x1B[0m";
|
|
6326
|
+
const BOLD = "\x1B[1m";
|
|
6327
|
+
const DIM = "\x1B[38;5;102m";
|
|
6328
|
+
const TEXT = "\x1B[38;5;145m";
|
|
6329
|
+
const LOGO_HEIGHT = 7;
|
|
6330
|
+
const LOGO_GLYPHS = {
|
|
6331
|
+
s: [
|
|
6332
|
+
" █████ ",
|
|
6333
|
+
"█ ",
|
|
6334
|
+
"█ ",
|
|
6335
|
+
" ████ ",
|
|
6336
|
+
" █ ",
|
|
6337
|
+
"█ █",
|
|
6338
|
+
" █████ "
|
|
6339
|
+
],
|
|
6340
|
+
a: [
|
|
6341
|
+
" ███ ",
|
|
6342
|
+
" █ █ ",
|
|
6343
|
+
" █ █ ",
|
|
6344
|
+
" █████ ",
|
|
6345
|
+
" █ █ ",
|
|
6346
|
+
" █ █ ",
|
|
6347
|
+
" █ █ "
|
|
6348
|
+
],
|
|
6349
|
+
f: [
|
|
6350
|
+
"██████ ",
|
|
6351
|
+
"█ ",
|
|
6352
|
+
"█ ",
|
|
6353
|
+
"█████ ",
|
|
6354
|
+
"█ ",
|
|
6355
|
+
"█ ",
|
|
6356
|
+
"█ "
|
|
6357
|
+
],
|
|
6358
|
+
e: [
|
|
6359
|
+
"██████ ",
|
|
6360
|
+
"█ ",
|
|
6361
|
+
"█ ",
|
|
6362
|
+
"█████ ",
|
|
6363
|
+
"█ ",
|
|
6364
|
+
"█ ",
|
|
6365
|
+
"██████ "
|
|
6366
|
+
],
|
|
6367
|
+
k: [
|
|
6368
|
+
"█ █ ",
|
|
6369
|
+
"█ █ ",
|
|
6370
|
+
"█ █ ",
|
|
6371
|
+
"███ ",
|
|
6372
|
+
"█ █ ",
|
|
6373
|
+
"█ █ ",
|
|
6374
|
+
"█ █ "
|
|
6375
|
+
],
|
|
6376
|
+
i: [
|
|
6377
|
+
"█████ ",
|
|
6378
|
+
" █ ",
|
|
6379
|
+
" █ ",
|
|
6380
|
+
" █ ",
|
|
6381
|
+
" █ ",
|
|
6382
|
+
" █ ",
|
|
6383
|
+
"█████ "
|
|
6384
|
+
],
|
|
6385
|
+
l: [
|
|
6386
|
+
"█ ",
|
|
6387
|
+
"█ ",
|
|
6388
|
+
"█ ",
|
|
6389
|
+
"█ ",
|
|
6390
|
+
"█ ",
|
|
6391
|
+
"█ ",
|
|
6392
|
+
"██████ "
|
|
6393
|
+
]
|
|
6394
|
+
};
|
|
6395
|
+
function buildLogoLines() {
|
|
6396
|
+
const words = ["Safe", "Skill"];
|
|
6397
|
+
const lines = Array.from({ length: LOGO_HEIGHT }, () => "");
|
|
6398
|
+
for (const word of words) {
|
|
6399
|
+
for (const character of word) {
|
|
6400
|
+
const glyph = LOGO_GLYPHS[character.toLowerCase()];
|
|
6401
|
+
if (!glyph) continue;
|
|
6402
|
+
for (let i = 0; i < LOGO_HEIGHT; i++) lines[i] += `${glyph[i] ?? ""} `;
|
|
6403
|
+
}
|
|
6404
|
+
for (let i = 0; i < LOGO_HEIGHT; i++) lines[i] += " ";
|
|
6405
|
+
}
|
|
6406
|
+
return lines.map((line) => line.replace(/\s+$/, ""));
|
|
6407
|
+
}
|
|
6408
|
+
const GRAYS = [
|
|
6409
|
+
"\x1B[38;5;250m",
|
|
6410
|
+
"\x1B[38;5;248m",
|
|
6411
|
+
"\x1B[38;5;245m",
|
|
6412
|
+
"\x1B[38;5;243m",
|
|
6413
|
+
"\x1B[38;5;240m",
|
|
6414
|
+
"\x1B[38;5;238m",
|
|
6415
|
+
"\x1B[38;5;236m"
|
|
6416
|
+
];
|
|
6417
|
+
function showLogo() {
|
|
6418
|
+
console.log();
|
|
6419
|
+
buildLogoLines().forEach((line, i) => {
|
|
6420
|
+
console.log(`${GRAYS[i]}${line}${RESET}`);
|
|
6421
|
+
});
|
|
6422
|
+
}
|
|
6423
|
+
function showBanner() {
|
|
6424
|
+
showLogo();
|
|
6425
|
+
console.log();
|
|
4354
6426
|
console.log(`${DIM}SafeSkill CLI for the open agent skills ecosystem${RESET}`);
|
|
4355
6427
|
console.log();
|
|
4356
6428
|
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill add ${DIM}<package>${RESET} ${DIM}Add a new skill${RESET}`);
|
|
4357
6429
|
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill remove${RESET} ${DIM}Remove installed skills${RESET}`);
|
|
4358
6430
|
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill list${RESET} ${DIM}List installed skills${RESET}`);
|
|
4359
6431
|
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill find ${DIM}[query]${RESET} ${DIM}Search for skills${RESET}`);
|
|
6432
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill use ${DIM}<package>${RESET} ${DIM}Use a skill without installing${RESET}`);
|
|
4360
6433
|
console.log();
|
|
4361
6434
|
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill check${RESET} ${DIM}Check for updates${RESET}`);
|
|
4362
6435
|
console.log(` ${DIM}$${RESET} ${TEXT}npx safeskill update${RESET} ${DIM}Update all skills${RESET}`);
|
|
@@ -4382,6 +6455,7 @@ ${BOLD}Manage Skills:${RESET}
|
|
|
4382
6455
|
remove [skills] Remove installed skills
|
|
4383
6456
|
list, ls List installed skills
|
|
4384
6457
|
find [query] Search for skills interactively
|
|
6458
|
+
use <package> Generate a one-shot prompt for a skill
|
|
4385
6459
|
|
|
4386
6460
|
${BOLD}Updates:${RESET}
|
|
4387
6461
|
check Check for available skill updates
|
|
@@ -4439,6 +6513,8 @@ ${BOLD}Examples:${RESET}
|
|
|
4439
6513
|
${DIM}$${RESET} safeskill ls --json ${DIM}# JSON output${RESET}
|
|
4440
6514
|
${DIM}$${RESET} safeskill find ${DIM}# interactive search${RESET}
|
|
4441
6515
|
${DIM}$${RESET} safeskill find typescript ${DIM}# search by keyword${RESET}
|
|
6516
|
+
${DIM}$${RESET} safeskill use example-org/agent-skills@web-design
|
|
6517
|
+
${DIM}$${RESET} safeskill use safeskill://official/acme/code-review@1.2.0
|
|
4442
6518
|
${DIM}$${RESET} safeskill check
|
|
4443
6519
|
${DIM}$${RESET} safeskill update
|
|
4444
6520
|
${DIM}$${RESET} safeskill experimental_install ${DIM}# restore from skills-lock.json${RESET}
|
|
@@ -4526,327 +6602,6 @@ Describe when this skill should be used.
|
|
|
4526
6602
|
console.log(`Browse existing skills for inspiration at ${TEXT}https://skills.sh/${RESET}`);
|
|
4527
6603
|
console.log();
|
|
4528
6604
|
}
|
|
4529
|
-
const AGENTS_DIR = ".agents";
|
|
4530
|
-
const LOCK_FILE = ".skill-lock.json";
|
|
4531
|
-
const CURRENT_LOCK_VERSION = 4;
|
|
4532
|
-
function getSkillLockPath() {
|
|
4533
|
-
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
4534
|
-
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
|
|
4535
|
-
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
4536
|
-
}
|
|
4537
|
-
function readSkillLock() {
|
|
4538
|
-
const lockPath = getSkillLockPath();
|
|
4539
|
-
try {
|
|
4540
|
-
const content = readFileSync(lockPath, "utf-8");
|
|
4541
|
-
const parsed = JSON.parse(content);
|
|
4542
|
-
if (typeof parsed.version !== "number" || !parsed.skills) return {
|
|
4543
|
-
version: CURRENT_LOCK_VERSION,
|
|
4544
|
-
skills: {}
|
|
4545
|
-
};
|
|
4546
|
-
if (parsed.version < CURRENT_LOCK_VERSION) return {
|
|
4547
|
-
version: CURRENT_LOCK_VERSION,
|
|
4548
|
-
skills: {}
|
|
4549
|
-
};
|
|
4550
|
-
return parsed;
|
|
4551
|
-
} catch {
|
|
4552
|
-
return {
|
|
4553
|
-
version: CURRENT_LOCK_VERSION,
|
|
4554
|
-
skills: {}
|
|
4555
|
-
};
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
function getSkipReason(entry) {
|
|
4559
|
-
if (entry.sourceType === "local") return "Local path";
|
|
4560
|
-
if (entry.sourceType === "hub") {
|
|
4561
|
-
if (!entry.resolvedVersion) return "No hub version recorded";
|
|
4562
|
-
if (!entry.artifactDigest) return "No hub artifact metadata";
|
|
4563
|
-
return "No version tracking";
|
|
4564
|
-
}
|
|
4565
|
-
if (entry.sourceType === "git") return "Git URL (hash tracking not supported)";
|
|
4566
|
-
if (!entry.skillFolderHash) return "No version hash available";
|
|
4567
|
-
if (!entry.skillPath) return "No skill path recorded";
|
|
4568
|
-
return "No version tracking";
|
|
4569
|
-
}
|
|
4570
|
-
function replaceHubSourceVersion(source, version) {
|
|
4571
|
-
const trimmedSource = source.trim();
|
|
4572
|
-
if (!trimmedSource) return source;
|
|
4573
|
-
return `${trimmedSource.replace(/@[^/@]+$/, "")}@${version}`;
|
|
4574
|
-
}
|
|
4575
|
-
function printSkippedSkills(skipped) {
|
|
4576
|
-
if (skipped.length === 0) return;
|
|
4577
|
-
console.log();
|
|
4578
|
-
console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`);
|
|
4579
|
-
for (const skill of skipped) {
|
|
4580
|
-
console.log(` ${TEXT}•${RESET} ${skill.name} ${DIM}(${skill.reason})${RESET}`);
|
|
4581
|
-
console.log(` ${DIM}To update: ${TEXT}npx safeskill add ${skill.sourceUrl} -g -y${RESET}`);
|
|
4582
|
-
}
|
|
4583
|
-
}
|
|
4584
|
-
async function runCheck(args = []) {
|
|
4585
|
-
console.log(`${TEXT}Checking for skill updates...${RESET}`);
|
|
4586
|
-
console.log();
|
|
4587
|
-
const lock = readSkillLock();
|
|
4588
|
-
const skillNames = Object.keys(lock.skills);
|
|
4589
|
-
if (skillNames.length === 0) {
|
|
4590
|
-
console.log(`${DIM}No skills tracked in lock file.${RESET}`);
|
|
4591
|
-
console.log(`${DIM}Install skills with${RESET} ${TEXT}npx safeskill add <package>${RESET}`);
|
|
4592
|
-
return;
|
|
4593
|
-
}
|
|
4594
|
-
const token = getGitHubToken();
|
|
4595
|
-
const skillsBySource = /* @__PURE__ */ new Map();
|
|
4596
|
-
const hubEntries = [];
|
|
4597
|
-
const skipped = [];
|
|
4598
|
-
for (const skillName of skillNames) {
|
|
4599
|
-
const entry = lock.skills[skillName];
|
|
4600
|
-
if (!entry) continue;
|
|
4601
|
-
if (entry.sourceType === "hub") {
|
|
4602
|
-
if (!entry.resolvedVersion || !entry.artifactDigest) {
|
|
4603
|
-
skipped.push({
|
|
4604
|
-
name: skillName,
|
|
4605
|
-
reason: getSkipReason(entry),
|
|
4606
|
-
sourceUrl: entry.sourceUrl
|
|
4607
|
-
});
|
|
4608
|
-
continue;
|
|
4609
|
-
}
|
|
4610
|
-
hubEntries.push({
|
|
4611
|
-
name: skillName,
|
|
4612
|
-
entry
|
|
4613
|
-
});
|
|
4614
|
-
continue;
|
|
4615
|
-
}
|
|
4616
|
-
if (!entry.skillFolderHash || !entry.skillPath) {
|
|
4617
|
-
skipped.push({
|
|
4618
|
-
name: skillName,
|
|
4619
|
-
reason: getSkipReason(entry),
|
|
4620
|
-
sourceUrl: entry.sourceUrl
|
|
4621
|
-
});
|
|
4622
|
-
continue;
|
|
4623
|
-
}
|
|
4624
|
-
const existing = skillsBySource.get(entry.source) || [];
|
|
4625
|
-
existing.push({
|
|
4626
|
-
name: skillName,
|
|
4627
|
-
entry
|
|
4628
|
-
});
|
|
4629
|
-
skillsBySource.set(entry.source, existing);
|
|
4630
|
-
}
|
|
4631
|
-
const totalSkills = skillNames.length - skipped.length;
|
|
4632
|
-
if (totalSkills === 0) {
|
|
4633
|
-
console.log(`${DIM}No GitHub skills to check.${RESET}`);
|
|
4634
|
-
printSkippedSkills(skipped);
|
|
4635
|
-
return;
|
|
4636
|
-
}
|
|
4637
|
-
console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`);
|
|
4638
|
-
const updates = [];
|
|
4639
|
-
const errors = [];
|
|
4640
|
-
if (hubEntries.length > 0) try {
|
|
4641
|
-
(await checkHubUpdates(hubEntries.map(({ entry }) => ({
|
|
4642
|
-
source: entry.sourceUrl,
|
|
4643
|
-
version: entry.resolvedVersion,
|
|
4644
|
-
artifact_digest: entry.artifactDigest
|
|
4645
|
-
})))).items.forEach((item, index) => {
|
|
4646
|
-
const hubEntry = hubEntries[index];
|
|
4647
|
-
if (!hubEntry) return;
|
|
4648
|
-
if (item.has_update) updates.push({
|
|
4649
|
-
name: hubEntry.name,
|
|
4650
|
-
source: hubEntry.entry.sourceUrl
|
|
4651
|
-
});
|
|
4652
|
-
});
|
|
4653
|
-
} catch (err) {
|
|
4654
|
-
hubEntries.forEach(({ name, entry }) => {
|
|
4655
|
-
errors.push({
|
|
4656
|
-
name,
|
|
4657
|
-
source: entry.sourceUrl,
|
|
4658
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
4659
|
-
});
|
|
4660
|
-
});
|
|
4661
|
-
}
|
|
4662
|
-
for (const [source, skills] of skillsBySource) for (const { name, entry } of skills) try {
|
|
4663
|
-
const latestHash = await fetchSkillFolderHash(source, entry.skillPath, token);
|
|
4664
|
-
if (!latestHash) {
|
|
4665
|
-
errors.push({
|
|
4666
|
-
name,
|
|
4667
|
-
source,
|
|
4668
|
-
error: "Could not fetch from GitHub"
|
|
4669
|
-
});
|
|
4670
|
-
continue;
|
|
4671
|
-
}
|
|
4672
|
-
if (latestHash !== entry.skillFolderHash) updates.push({
|
|
4673
|
-
name,
|
|
4674
|
-
source
|
|
4675
|
-
});
|
|
4676
|
-
} catch (err) {
|
|
4677
|
-
errors.push({
|
|
4678
|
-
name,
|
|
4679
|
-
source,
|
|
4680
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
4681
|
-
});
|
|
4682
|
-
}
|
|
4683
|
-
console.log();
|
|
4684
|
-
if (updates.length === 0) console.log(`${TEXT}✓ All skills are up to date${RESET}`);
|
|
4685
|
-
else {
|
|
4686
|
-
console.log(`${TEXT}${updates.length} update(s) available:${RESET}`);
|
|
4687
|
-
console.log();
|
|
4688
|
-
for (const update of updates) {
|
|
4689
|
-
console.log(` ${TEXT}↑${RESET} ${update.name}`);
|
|
4690
|
-
console.log(` ${DIM}source: ${update.source}${RESET}`);
|
|
4691
|
-
}
|
|
4692
|
-
console.log();
|
|
4693
|
-
console.log(`${DIM}Run${RESET} ${TEXT}npx safeskill update${RESET} ${DIM}to update all skills${RESET}`);
|
|
4694
|
-
}
|
|
4695
|
-
if (errors.length > 0) {
|
|
4696
|
-
console.log();
|
|
4697
|
-
console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
|
|
4698
|
-
console.log();
|
|
4699
|
-
for (const error of errors) {
|
|
4700
|
-
console.log(` ${DIM}✗${RESET} ${error.name}`);
|
|
4701
|
-
console.log(` ${DIM}source: ${error.source}${RESET}`);
|
|
4702
|
-
}
|
|
4703
|
-
}
|
|
4704
|
-
printSkippedSkills(skipped);
|
|
4705
|
-
track({
|
|
4706
|
-
event: "check",
|
|
4707
|
-
skillCount: String(totalSkills),
|
|
4708
|
-
updatesAvailable: String(updates.length)
|
|
4709
|
-
});
|
|
4710
|
-
console.log();
|
|
4711
|
-
}
|
|
4712
|
-
async function runUpdate(region) {
|
|
4713
|
-
console.log(`${TEXT}Checking for skill updates...${RESET}`);
|
|
4714
|
-
console.log();
|
|
4715
|
-
const lock = readSkillLock();
|
|
4716
|
-
const skillNames = Object.keys(lock.skills);
|
|
4717
|
-
if (skillNames.length === 0) {
|
|
4718
|
-
console.log(`${DIM}No skills tracked in lock file.${RESET}`);
|
|
4719
|
-
console.log(`${DIM}Install skills with${RESET} ${TEXT}npx safeskill add <package>${RESET}`);
|
|
4720
|
-
return;
|
|
4721
|
-
}
|
|
4722
|
-
const token = getGitHubToken();
|
|
4723
|
-
const updates = [];
|
|
4724
|
-
const hubEntries = [];
|
|
4725
|
-
const skipped = [];
|
|
4726
|
-
for (const skillName of skillNames) {
|
|
4727
|
-
const entry = lock.skills[skillName];
|
|
4728
|
-
if (!entry) continue;
|
|
4729
|
-
if (entry.sourceType === "hub") {
|
|
4730
|
-
if (!entry.resolvedVersion || !entry.artifactDigest) {
|
|
4731
|
-
skipped.push({
|
|
4732
|
-
name: skillName,
|
|
4733
|
-
reason: getSkipReason(entry),
|
|
4734
|
-
sourceUrl: entry.sourceUrl
|
|
4735
|
-
});
|
|
4736
|
-
continue;
|
|
4737
|
-
}
|
|
4738
|
-
hubEntries.push({
|
|
4739
|
-
name: skillName,
|
|
4740
|
-
entry
|
|
4741
|
-
});
|
|
4742
|
-
continue;
|
|
4743
|
-
}
|
|
4744
|
-
if (!entry.skillFolderHash || !entry.skillPath) {
|
|
4745
|
-
skipped.push({
|
|
4746
|
-
name: skillName,
|
|
4747
|
-
reason: getSkipReason(entry),
|
|
4748
|
-
sourceUrl: entry.sourceUrl
|
|
4749
|
-
});
|
|
4750
|
-
continue;
|
|
4751
|
-
}
|
|
4752
|
-
try {
|
|
4753
|
-
const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
|
|
4754
|
-
if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
|
|
4755
|
-
name: skillName,
|
|
4756
|
-
source: entry.source,
|
|
4757
|
-
entry
|
|
4758
|
-
});
|
|
4759
|
-
} catch {}
|
|
4760
|
-
}
|
|
4761
|
-
if (hubEntries.length > 0) try {
|
|
4762
|
-
(await checkHubUpdates(hubEntries.map(({ entry }) => ({
|
|
4763
|
-
source: entry.sourceUrl,
|
|
4764
|
-
version: entry.resolvedVersion,
|
|
4765
|
-
artifact_digest: entry.artifactDigest
|
|
4766
|
-
})))).items.forEach((item, index) => {
|
|
4767
|
-
const hubEntry = hubEntries[index];
|
|
4768
|
-
if (!hubEntry || !item.has_update || !item.latest_version) return;
|
|
4769
|
-
updates.push({
|
|
4770
|
-
name: hubEntry.name,
|
|
4771
|
-
source: replaceHubSourceVersion(hubEntry.entry.sourceUrl, item.latest_version),
|
|
4772
|
-
entry: hubEntry.entry
|
|
4773
|
-
});
|
|
4774
|
-
});
|
|
4775
|
-
} catch {}
|
|
4776
|
-
if (skillNames.length - skipped.length === 0) {
|
|
4777
|
-
console.log(`${DIM}No skills to check.${RESET}`);
|
|
4778
|
-
printSkippedSkills(skipped);
|
|
4779
|
-
return;
|
|
4780
|
-
}
|
|
4781
|
-
if (updates.length === 0) {
|
|
4782
|
-
console.log(`${TEXT}✓ All skills are up to date${RESET}`);
|
|
4783
|
-
console.log();
|
|
4784
|
-
return;
|
|
4785
|
-
}
|
|
4786
|
-
console.log(`${TEXT}Found ${updates.length} update(s)${RESET}`);
|
|
4787
|
-
console.log();
|
|
4788
|
-
let successCount = 0;
|
|
4789
|
-
let failCount = 0;
|
|
4790
|
-
for (const update of updates) {
|
|
4791
|
-
console.log(`${TEXT}Updating ${update.name}...${RESET}`);
|
|
4792
|
-
let installUrl = update.entry.sourceUrl;
|
|
4793
|
-
if (update.entry.sourceType === "hub") installUrl = update.source;
|
|
4794
|
-
else if (update.entry.skillPath) {
|
|
4795
|
-
let skillFolder = update.entry.skillPath;
|
|
4796
|
-
if (skillFolder.endsWith("/SKILL.md")) skillFolder = skillFolder.slice(0, -9);
|
|
4797
|
-
else if (skillFolder.endsWith("SKILL.md")) skillFolder = skillFolder.slice(0, -8);
|
|
4798
|
-
if (skillFolder.endsWith("/")) skillFolder = skillFolder.slice(0, -1);
|
|
4799
|
-
installUrl = update.entry.sourceUrl.replace(/\.git$/, "").replace(/\/$/, "");
|
|
4800
|
-
installUrl = `${installUrl}/tree/main/${skillFolder}`;
|
|
4801
|
-
}
|
|
4802
|
-
const cliEntry = join(__dirname, "..", "bin", "cli.mjs");
|
|
4803
|
-
if (!existsSync(cliEntry)) {
|
|
4804
|
-
failCount++;
|
|
4805
|
-
console.log(` ${DIM}✗ Failed to update ${update.name}: CLI entrypoint not found at ${cliEntry}${RESET}`);
|
|
4806
|
-
continue;
|
|
4807
|
-
}
|
|
4808
|
-
const addArgs = region ? [
|
|
4809
|
-
cliEntry,
|
|
4810
|
-
"--region",
|
|
4811
|
-
region,
|
|
4812
|
-
"add",
|
|
4813
|
-
installUrl,
|
|
4814
|
-
"-g",
|
|
4815
|
-
"-y"
|
|
4816
|
-
] : [
|
|
4817
|
-
cliEntry,
|
|
4818
|
-
"add",
|
|
4819
|
-
installUrl,
|
|
4820
|
-
"-g",
|
|
4821
|
-
"-y"
|
|
4822
|
-
];
|
|
4823
|
-
if (spawnSync(process.execPath, addArgs, {
|
|
4824
|
-
stdio: [
|
|
4825
|
-
"inherit",
|
|
4826
|
-
"pipe",
|
|
4827
|
-
"pipe"
|
|
4828
|
-
],
|
|
4829
|
-
encoding: "utf-8",
|
|
4830
|
-
shell: process.platform === "win32"
|
|
4831
|
-
}).status === 0) {
|
|
4832
|
-
successCount++;
|
|
4833
|
-
console.log(` ${TEXT}✓${RESET} Updated ${update.name}`);
|
|
4834
|
-
} else {
|
|
4835
|
-
failCount++;
|
|
4836
|
-
console.log(` ${DIM}✗ Failed to update ${update.name}${RESET}`);
|
|
4837
|
-
}
|
|
4838
|
-
}
|
|
4839
|
-
console.log();
|
|
4840
|
-
if (successCount > 0) console.log(`${TEXT}✓ Updated ${successCount} skill(s)${RESET}`);
|
|
4841
|
-
if (failCount > 0) console.log(`${DIM}Failed to update ${failCount} skill(s)${RESET}`);
|
|
4842
|
-
track({
|
|
4843
|
-
event: "update",
|
|
4844
|
-
skillCount: String(updates.length),
|
|
4845
|
-
successCount: String(successCount),
|
|
4846
|
-
failCount: String(failCount)
|
|
4847
|
-
});
|
|
4848
|
-
console.log();
|
|
4849
|
-
}
|
|
4850
6605
|
function parseGlobalOptions(args) {
|
|
4851
6606
|
const parsedArgs = [];
|
|
4852
6607
|
let region;
|
|
@@ -4893,9 +6648,11 @@ async function main() {
|
|
|
4893
6648
|
return;
|
|
4894
6649
|
}
|
|
4895
6650
|
if (parsedGlobalOptions.region) setSafeSkillRegionOverride(parsedGlobalOptions.region);
|
|
6651
|
+
const inAgent = await isRunningInAgent();
|
|
4896
6652
|
const args = parsedGlobalOptions.args;
|
|
4897
6653
|
if (args.length === 0) {
|
|
4898
|
-
|
|
6654
|
+
if (inAgent) showHelp();
|
|
6655
|
+
else showBanner();
|
|
4899
6656
|
return;
|
|
4900
6657
|
}
|
|
4901
6658
|
const command = args[0];
|
|
@@ -4905,24 +6662,28 @@ async function main() {
|
|
|
4905
6662
|
case "search":
|
|
4906
6663
|
case "f":
|
|
4907
6664
|
case "s":
|
|
4908
|
-
|
|
4909
|
-
|
|
6665
|
+
if (!inAgent) {
|
|
6666
|
+
showLogo();
|
|
6667
|
+
console.log();
|
|
6668
|
+
}
|
|
4910
6669
|
await runFind(restArgs);
|
|
4911
6670
|
break;
|
|
4912
6671
|
case "init":
|
|
4913
|
-
|
|
4914
|
-
|
|
6672
|
+
if (!inAgent) {
|
|
6673
|
+
showLogo();
|
|
6674
|
+
console.log();
|
|
6675
|
+
}
|
|
4915
6676
|
runInit(restArgs);
|
|
4916
6677
|
break;
|
|
4917
6678
|
case "experimental_install":
|
|
4918
|
-
showLogo();
|
|
6679
|
+
if (!inAgent) showLogo();
|
|
4919
6680
|
await runInstallFromLock(restArgs);
|
|
4920
6681
|
break;
|
|
4921
6682
|
case "i":
|
|
4922
6683
|
case "install":
|
|
4923
6684
|
case "a":
|
|
4924
6685
|
case "add": {
|
|
4925
|
-
showLogo();
|
|
6686
|
+
if (!inAgent) showLogo();
|
|
4926
6687
|
const { source: addSource, options: addOpts } = parseAddOptions(restArgs);
|
|
4927
6688
|
await runAdd(addSource, addOpts);
|
|
4928
6689
|
break;
|
|
@@ -4938,7 +6699,7 @@ async function main() {
|
|
|
4938
6699
|
await removeCommand(skills, removeOptions);
|
|
4939
6700
|
break;
|
|
4940
6701
|
case "experimental_sync": {
|
|
4941
|
-
showLogo();
|
|
6702
|
+
if (!inAgent) showLogo();
|
|
4942
6703
|
const { options: syncOptions } = parseSyncOptions(restArgs);
|
|
4943
6704
|
await runSync(restArgs, syncOptions);
|
|
4944
6705
|
break;
|
|
@@ -4947,12 +6708,17 @@ async function main() {
|
|
|
4947
6708
|
case "ls":
|
|
4948
6709
|
await runList(restArgs);
|
|
4949
6710
|
break;
|
|
6711
|
+
case "use": {
|
|
6712
|
+
const { source: useSource, options: useOptions, errors: useErrors } = parseUseOptions(restArgs);
|
|
6713
|
+
await runUse(useSource, useOptions, useErrors);
|
|
6714
|
+
break;
|
|
6715
|
+
}
|
|
4950
6716
|
case "check":
|
|
4951
|
-
runCheck(restArgs);
|
|
6717
|
+
await runCheck(restArgs);
|
|
4952
6718
|
break;
|
|
4953
6719
|
case "update":
|
|
4954
6720
|
case "upgrade":
|
|
4955
|
-
runUpdate(parsedGlobalOptions.region);
|
|
6721
|
+
await runUpdate(restArgs, parsedGlobalOptions.region);
|
|
4956
6722
|
break;
|
|
4957
6723
|
case "--help":
|
|
4958
6724
|
case "-h":
|
|
@@ -4967,5 +6733,9 @@ async function main() {
|
|
|
4967
6733
|
console.log(`Run ${BOLD}safeskill --help${RESET} for usage.`);
|
|
4968
6734
|
}
|
|
4969
6735
|
}
|
|
4970
|
-
|
|
6736
|
+
function isCliEntrypoint() {
|
|
6737
|
+
const entrypoint = process.argv[1];
|
|
6738
|
+
return entrypoint ? import.meta.url === pathToFileURL(entrypoint).href : false;
|
|
6739
|
+
}
|
|
6740
|
+
if (isCliEntrypoint()) main();
|
|
4971
6741
|
export {};
|