@safeskill/cli 0.2.3 → 0.3.0

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