@safeskill/cli 0.2.3 → 0.3.1

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