@safeskill/cli 0.2.2 → 0.3.0

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