skillpass 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -5
  3. package/dist/cli.js +329 -266
  4. package/package.json +1 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brad Traversy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -20,10 +20,10 @@ Requires Node 20 or newer. No runtime dependencies.
20
20
  ## Commands
21
21
 
22
22
  ```
23
- skillpass search [query] [--target <tool>] [--category <slug>] [--packs] [--json]
23
+ skillpass search [query] [--ai] [--target <tool>] [--category <slug>] [--packs] [--json]
24
24
  skillpass scan <path> [--json]
25
25
  skillpass report <slug>[@version] [--json]
26
- skillpass add <slug>[@version] [--target <tool> [--global] | --dir <path>] [--yes]
26
+ skillpass add <slug>[@version] [--target <tool>... [--global] | --dir <path>] [--yes]
27
27
  skillpass remove <slug> [--target <tool> [--global] | --dir <path>]
28
28
  skillpass update <slug>[@version] [--target <tool> [--global]] [--yes]
29
29
  skillpass outdated
@@ -38,6 +38,10 @@ skillpass --version | --help
38
38
  `--target claude-code`, `--category security-review`, or `--packs` for
39
39
  multi-skill workflow packs.
40
40
 
41
+ `skillpass search --ai "turn a video into an article"` searches by meaning
42
+ instead of by keyword, through the directory's free AI search. Results come
43
+ back in relevance order and the same filters apply.
44
+
41
45
  ### Inspect
42
46
 
43
47
  `skillpass report <slug>` prints the version's Skill Passport before you
@@ -52,9 +56,15 @@ risk, then downloads the validated snapshot and re-verifies every byte against
52
56
  the pinned source hash before anything touches disk. Installs are atomic - a
53
57
  failed write leaves nothing behind.
54
58
 
55
- - `--target claude-code` installs to `.claude/skills/<slug>` (project) or
56
- `~/.claude/skills/<slug>` with `--global`
57
- - `--target codex` installs to `.agents/skills/<slug>`
59
+ - `--target <tool>` installs into the tool's skills folder, or its user-level
60
+ folder with `--global`:
61
+ - `claude-code`: `.claude/skills/` and `~/.claude/skills/`
62
+ - `agents`: the shared `.agents/skills/` and `~/.agents/skills/` that
63
+ `codex`, `cursor`, `windsurf`, `github-copilot`, `gemini-cli`, and
64
+ `opencode` all read; any of those names resolves to the same folders
65
+ - `cline`: `.cline/skills/` and `~/.cline/skills/`
66
+ - Repeat `--target` to install into several tools in one pass with one
67
+ pre-flight; targets that share a folder install once
58
68
  - `--dir <path>` installs anywhere
59
69
  - No flags in a terminal: an interactive picker
60
70
 
package/dist/cli.js CHANGED
@@ -492,16 +492,7 @@ function unzipSync(data, opts) {
492
492
  }
493
493
 
494
494
  // src/add.ts
495
- import {
496
- existsSync as existsSync2,
497
- mkdirSync as mkdirSync2,
498
- readdirSync as readdirSync3,
499
- renameSync,
500
- rmdirSync,
501
- rmSync,
502
- statSync as statSync3,
503
- writeFileSync as writeFileSync2
504
- } from "node:fs";
495
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync3, renameSync, rmdirSync, rmSync, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
505
496
  import { dirname, isAbsolute, join as join4, resolve as resolve2 } from "node:path";
506
497
 
507
498
  // ../validator/src/load.ts
@@ -15348,14 +15339,7 @@ var aiReviewSchema = external_exports.object({
15348
15339
 
15349
15340
  // ../skill-schema/src/submission.ts
15350
15341
  var SUBMISSION_SOURCE_TYPES = ["github_url", "zip"];
15351
- var SUBMISSION_STATUSES = [
15352
- "draft",
15353
- "validating",
15354
- "passed",
15355
- "warning",
15356
- "failed",
15357
- "published"
15358
- ];
15342
+ var SUBMISSION_STATUSES = ["draft", "validating", "passed", "warning", "failed", "published"];
15359
15343
  var submissionSourceTypeSchema = external_exports.enum(SUBMISSION_SOURCE_TYPES);
15360
15344
  var submissionStatusSchema = external_exports.enum(SUBMISSION_STATUSES);
15361
15345
  var MAX_ZIP_BYTES = 10 * 1024 * 1024;
@@ -15716,6 +15700,13 @@ function readManifest(files) {
15716
15700
  return { state: "ok", data: result.data };
15717
15701
  }
15718
15702
  var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
15703
+ function readDeclaredVersion(frontmatter) {
15704
+ const top = /^version:\s*["']?([^"'\s]+)["']?\s*$/m.exec(frontmatter);
15705
+ if (top) return top[1];
15706
+ const block = /^metadata:\s*\n((?:[ \t]+\S.*(?:\n|$))*)/m.exec(frontmatter);
15707
+ const nested = block && /^[ \t]+version:\s*["']?([^"'\s]+)["']?\s*$/m.exec(block[1]);
15708
+ return nested ? nested[1] : void 0;
15709
+ }
15719
15710
  function readSkillMeta(content) {
15720
15711
  const out = {};
15721
15712
  const fm = FRONTMATTER_RE.exec(content);
@@ -15743,6 +15734,8 @@ function readSkillMeta(content) {
15743
15734
  out[key] = parts.join(" ").replace(/^["']|["']$/g, "");
15744
15735
  }
15745
15736
  }
15737
+ const version2 = readDeclaredVersion(fm[1]);
15738
+ if (version2) out.version = version2;
15746
15739
  }
15747
15740
  if (!out.description) {
15748
15741
  const prose = firstParagraph(body);
@@ -15792,6 +15785,7 @@ function inferManifest(files, fallbackName) {
15792
15785
  return {
15793
15786
  schemaVersion: "0.1",
15794
15787
  name,
15788
+ ...meta3.version && SEMVER_RE.test(meta3.version) ? { version: meta3.version } : {},
15795
15789
  description: meta3.description ?? name,
15796
15790
  targets: inferTargets(files),
15797
15791
  permissions,
@@ -15826,9 +15820,7 @@ function readmeProse(files) {
15826
15820
  return readme ? firstParagraph(readme.content) : void 0;
15827
15821
  }
15828
15822
  function inferNestedManifest(files, fallbackName) {
15829
- const members = collectMemberFiles(files).sort(
15830
- (a, b) => a.folder < b.folder ? -1 : a.folder > b.folder ? 1 : 0
15831
- );
15823
+ const members = collectMemberFiles(files).sort((a, b) => a.folder < b.folder ? -1 : a.folder > b.folder ? 1 : 0);
15832
15824
  if (members.length === 0) {
15833
15825
  return null;
15834
15826
  }
@@ -15841,10 +15833,7 @@ function inferNestedManifest(files, fallbackName) {
15841
15833
  let entryName = slugForSkill(meta3.name ?? member.folder);
15842
15834
  if (used.has(entryName)) entryName = slugForSkill(member.folder);
15843
15835
  used.add(entryName);
15844
- const targets = member.plainPath ? inferTargets(files) : [
15845
- ...member.claudePath ? ["claude-code"] : [],
15846
- ...member.agentsPath ? ["codex"] : []
15847
- ];
15836
+ const targets = member.plainPath ? inferTargets(files) : [...member.claudePath ? ["claude-code"] : [], ...member.agentsPath ? ["codex"] : []];
15848
15837
  for (const target of targets) packTargets.add(target);
15849
15838
  return {
15850
15839
  name: entryName,
@@ -16031,10 +16020,12 @@ var CREDENTIAL_PATTERNS = [
16031
16020
  },
16032
16021
  {
16033
16022
  code: "credential-harvesting",
16034
- // The ::/. lookbehinds skip code identifiers (`Box::leak(secret)`,
16035
- // `.leak(`) so security docs naming anti-patterns don't flag.
16023
+ // The verb has to sit where an instruction puts it (line or sentence
16024
+ // start, or after then/and/to/should/must): narrative like "timing leaks
16025
+ // secret bits" has a noun subject and does not fire. The ::/. lookbehinds
16026
+ // skip code identifiers (`Box::leak(secret)`, `.leak(`).
16036
16027
  pattern: new RegExp(
16037
- String.raw`(?<!\b(?:never|not|don'?t|avoids?|prevents?|stops?)\s)(?<!::)(?<!\.)\b(?:exfiltrates?|steals?|harvests?|dumps?|leaks?)\b[^.\n]{0,60}?${SECRET_NOUN}`,
16028
+ String.raw`(?:^[-*\d.)\s]*|[.:;!?]\s+|\b(?:then|and|or|to|should|must|now|please)\s+)(?<!\b(?:never|not|don'?t|avoids?|prevents?|stops?)\s)(?<!::)(?<!\.)(?:exfiltrate|steal|harvest|dump|leak)s?\b[^.\n]{0,60}?${SECRET_NOUN}`,
16038
16029
  "i"
16039
16030
  ),
16040
16031
  message: "matches a credential-exfiltration phrase"
@@ -16048,14 +16039,28 @@ var CREDENTIAL_PATTERNS = [
16048
16039
  message: "matches a credential-file network-transmission phrase"
16049
16040
  }
16050
16041
  ];
16051
- var advisory = (rows) => rows.map((row2) => ({ ...row2, severity: "warning" }));
16042
+ var advisory = (rows, mentionAware = false) => rows.map((row2) => ({ ...row2, severity: "warning", mentionAware }));
16052
16043
  var ALL_ROWS = [
16053
16044
  ...SECRET_PATTERNS,
16054
- ...advisory(INJECTION_PATTERNS),
16045
+ ...advisory(INJECTION_PATTERNS, true),
16055
16046
  ...advisory(DANGEROUS_PATTERNS),
16056
16047
  ...advisory(MALWARE_PATTERNS),
16057
- ...advisory(CREDENTIAL_PATTERNS)
16048
+ ...advisory(CREDENTIAL_PATTERNS, true)
16058
16049
  ];
16050
+ var MENTION_LEAD_RE = /\b(?:flags?|flagged|detects?|detecting|look(?:ing)?\s+for|watch\s+for|check(?:ing)?\s+for|signs?\s+of|indicators?\s+of|evidence\s+of|red\s+flags?|examples?|e\.g\.|such\s+as|attempts?\s+to|designed\s+to|(?:code|scripts?|files?|comments?|text|instructions?)\s+that)\b/i;
16051
+ var TRANSMIT_VERB_RE = /\b(?:post|send|curl|transmit|exfiltrate)s?\b/i;
16052
+ function matchAnchor(line, match) {
16053
+ if (match[0]) return match.index;
16054
+ const verb = line.search(TRANSMIT_VERB_RE);
16055
+ return verb < 0 ? line.length : verb;
16056
+ }
16057
+ function isMention(line, anchor, inFence) {
16058
+ if (inFence || line.trimStart().startsWith("|")) return true;
16059
+ const before = line.slice(0, anchor);
16060
+ const inside = (mark) => (before.split(mark).length - 1) % 2 === 1;
16061
+ return inside('"') || inside("`") || MENTION_LEAD_RE.test(before);
16062
+ }
16063
+ var FENCE_RE = /^\s*(?:```|~~~)/;
16059
16064
  var DOCUMENTED_PLACEHOLDERS = ["AKIAIOSFODNN7EXAMPLE", "AKIAI44QH8DHBEXAMPLE"];
16060
16065
  var withoutPlaceholders = (line) => DOCUMENTED_PLACEHOLDERS.reduce((l, p) => l.replaceAll(p, " "), line);
16061
16066
  var REDACTIONS = ALL_ROWS.filter((row2) => row2.redact).map(
@@ -16065,9 +16070,17 @@ var redactLine = (line) => REDACTIONS.reduce((l, re) => l.replace(re, "[redacted
16065
16070
  var contentRule = (pkg) => {
16066
16071
  const findings = [];
16067
16072
  for (const file2 of pkg.files) {
16073
+ let inFence = false;
16068
16074
  file2.content.split("\n").forEach((rawLine, i2) => {
16075
+ if (FENCE_RE.test(rawLine)) {
16076
+ inFence = !inFence;
16077
+ return;
16078
+ }
16069
16079
  const line = withoutPlaceholders(rawLine);
16070
- const hits = ALL_ROWS.filter((row2) => row2.pattern.test(line));
16080
+ const hits = ALL_ROWS.filter((row2) => {
16081
+ const match = row2.pattern.exec(line);
16082
+ return match !== null && !(row2.mentionAware && isMention(line, matchAnchor(line, match), inFence));
16083
+ });
16071
16084
  if (hits.length === 0) return;
16072
16085
  const snippet = redactLine(line).trim();
16073
16086
  for (const row2 of hits) {
@@ -16225,10 +16238,23 @@ async function getParsed(fetchImpl, url2, schema) {
16225
16238
  try {
16226
16239
  body = await res.json();
16227
16240
  } catch {
16228
- return { ok: false, notFound: false, message: `unexpected response from the API (${res.status})` };
16241
+ return {
16242
+ ok: false,
16243
+ notFound: false,
16244
+ message: `unexpected response from the API (${res.status})`,
16245
+ status: res.status
16246
+ };
16229
16247
  }
16230
16248
  if (typeof body !== "object" || body === null || !("success" in body) || body.success !== true) {
16231
- return res.status === 404 ? { ok: false, notFound: true, message: "not found" } : { ok: false, notFound: false, message: `the API returned an error (${res.status})` };
16249
+ const error51 = body?.error;
16250
+ const apiError = typeof error51 === "string" ? { apiError: error51 } : {};
16251
+ return res.status === 404 ? { ok: false, notFound: true, message: "not found", status: 404, ...apiError } : {
16252
+ ok: false,
16253
+ notFound: false,
16254
+ message: `the API returned an error (${res.status})`,
16255
+ status: res.status,
16256
+ ...apiError
16257
+ };
16232
16258
  }
16233
16259
  const parsed = schema.safeParse(body.data);
16234
16260
  if (!parsed.success) {
@@ -16458,10 +16484,7 @@ function renderPreflightReport(detail, preflight, st = PLAIN) {
16458
16484
  ...renderDiff(preflight.diff)
16459
16485
  ];
16460
16486
  if (preflight.blocked) {
16461
- lines.push(
16462
- "",
16463
- st.red(`BLOCKED: ${preflight.blockedReason ?? "this version cannot be downloaded"}`)
16464
- );
16487
+ lines.push("", st.red(`BLOCKED: ${preflight.blockedReason ?? "this version cannot be downloaded"}`));
16465
16488
  }
16466
16489
  return lines;
16467
16490
  }
@@ -16484,37 +16507,39 @@ function renderDiff(diff) {
16484
16507
  // src/targets.ts
16485
16508
  import { homedir } from "node:os";
16486
16509
  import { join as join3, resolve } from "node:path";
16487
- function installAreas(home = homedir()) {
16488
- return {
16489
- "claude-code": { project: join3(".claude", "skills"), global: join3(home, ".claude", "skills") },
16490
- codex: { project: join3(".agents", "skills") }
16491
- };
16510
+ var SHARED = { project: join3(".agents", "skills"), global: join3(".agents", "skills"), layout: "codex" };
16511
+ var INSTALL_TOOLS = {
16512
+ "claude-code": { project: join3(".claude", "skills"), global: join3(".claude", "skills"), layout: "claude-code" },
16513
+ agents: SHARED,
16514
+ codex: SHARED,
16515
+ cursor: SHARED,
16516
+ windsurf: SHARED,
16517
+ "github-copilot": SHARED,
16518
+ "gemini-cli": SHARED,
16519
+ opencode: SHARED,
16520
+ cline: { project: join3(".cline", "skills"), global: join3(".cline", "skills"), layout: "codex" }
16521
+ };
16522
+ var INSTALL_TOOL_NAMES = Object.keys(INSTALL_TOOLS);
16523
+ function lookupTool(name) {
16524
+ return Object.hasOwn(INSTALL_TOOLS, name) ? INSTALL_TOOLS[name] : void 0;
16525
+ }
16526
+ var MAPPED_TARGETS = TARGETS.filter((t) => lookupTool(t) !== void 0);
16527
+ function layoutTarget(tool) {
16528
+ return lookupTool(tool)?.layout;
16529
+ }
16530
+ function declaresTool(declared, tool) {
16531
+ const layout = layoutTarget(tool);
16532
+ return declared.includes(tool) || layout !== void 0 && declared.includes(layout);
16492
16533
  }
16493
- var MAPPED_TARGETS = Object.keys(installAreas());
16494
16534
  function resolveTargetArea(target, global = false, home) {
16495
- if (!TARGETS.includes(target)) {
16496
- return {
16497
- ok: false,
16498
- message: `unknown target "${target}" (known tools: ${TARGETS.join(", ")})`
16499
- };
16500
- }
16501
- const area = installAreas(home)[target];
16502
- if (!area) {
16503
- return {
16504
- ok: false,
16505
- message: `${target} has no standard skills folder yet; use --dir to pick a location`
16506
- };
16507
- }
16508
- if (global) {
16509
- if (!area.global) {
16510
- return {
16511
- ok: false,
16512
- message: `${target} has no user-level skills folder; install per project instead`
16513
- };
16535
+ const tool = lookupTool(target);
16536
+ if (!tool) {
16537
+ if (TARGETS.includes(target)) {
16538
+ return { ok: false, message: `${target} has no standard skills folder yet; use --dir to pick a location` };
16514
16539
  }
16515
- return { ok: true, dir: area.global };
16540
+ return { ok: false, message: `unknown target "${target}" (known tools: ${INSTALL_TOOL_NAMES.join(", ")})` };
16516
16541
  }
16517
- return { ok: true, dir: area.project };
16542
+ return { ok: true, dir: global ? join3(home ?? homedir(), tool.global) : tool.project };
16518
16543
  }
16519
16544
  function resolveTargetDir(target, slug, global = false, home) {
16520
16545
  const area = resolveTargetArea(target, global, home);
@@ -16524,24 +16549,48 @@ function mappableDeclaredTargets(declared) {
16524
16549
  return declared.filter((t) => MAPPED_TARGETS.includes(t));
16525
16550
  }
16526
16551
  function knownAreas(cwd, home) {
16527
- const areas = [];
16528
- const byTarget = installAreas(home);
16529
- for (const tool of MAPPED_TARGETS) {
16530
- const area = byTarget[tool];
16531
- if (!area) {
16532
- continue;
16533
- }
16534
- areas.push({
16535
- tool,
16536
- global: false,
16537
- label: `${tool} project (${area.project})`,
16538
- dir: resolve(cwd, area.project)
16539
- });
16540
- if (area.global) {
16541
- areas.push({ tool, global: true, label: `${tool} user (${area.global})`, dir: area.global });
16552
+ const areas = /* @__PURE__ */ new Map();
16553
+ for (const [name, tool] of Object.entries(INSTALL_TOOLS)) {
16554
+ const scopes = [
16555
+ { global: false, display: tool.project, dir: resolve(cwd, tool.project) },
16556
+ { global: true, display: join3(home ?? homedir(), tool.global), dir: join3(home ?? homedir(), tool.global) }
16557
+ ];
16558
+ for (const scope of scopes) {
16559
+ const existing = areas.get(scope.dir);
16560
+ if (existing) {
16561
+ if (!existing.tools.includes(name)) existing.tools.push(name);
16562
+ existing.project ||= !scope.global;
16563
+ existing.global ||= scope.global;
16564
+ existing.label = areaLabel(existing);
16565
+ continue;
16566
+ }
16567
+ const area = {
16568
+ tools: [name],
16569
+ layout: tool.layout,
16570
+ project: !scope.global,
16571
+ global: scope.global,
16572
+ label: "",
16573
+ dir: scope.dir
16574
+ };
16575
+ area.label = areaLabel(area, scope.display);
16576
+ areas.set(scope.dir, area);
16542
16577
  }
16543
16578
  }
16544
- return areas;
16579
+ return [...areas.values()];
16580
+ }
16581
+ function areaLabel(area, display) {
16582
+ const shown = display ?? area.label.slice(0, area.label.indexOf(" ("));
16583
+ const scopes = [...area.project ? ["project"] : [], ...area.global ? ["user"] : []].join(", ");
16584
+ return `${shown} (${scopes}) - ${area.tools.join(", ")}`;
16585
+ }
16586
+ function resolveArea(target, global, cwd, home) {
16587
+ const resolved = resolveTargetArea(target, global, home);
16588
+ if (!resolved.ok) {
16589
+ return resolved;
16590
+ }
16591
+ const dir = resolve(cwd, resolved.dir);
16592
+ const area = knownAreas(cwd, home).find((a) => a.dir === dir);
16593
+ return area ? { ok: true, area } : { ok: false, message: `no known skills area for ${target}` };
16545
16594
  }
16546
16595
 
16547
16596
  // src/add.ts
@@ -16623,54 +16672,26 @@ function writeTree(files, target) {
16623
16672
  throw err2;
16624
16673
  }
16625
16674
  }
16626
- function installChoices(declared, slug) {
16627
- const choices = [];
16628
- const ordered = [
16629
- ...MAPPED_TARGETS.filter((t) => declared.includes(t)),
16630
- ...MAPPED_TARGETS.filter((t) => !declared.includes(t))
16631
- ];
16632
- for (const tool of ordered) {
16633
- const note = declared.includes(tool) ? "" : " - not declared by this skill";
16634
- const resolved = resolveTargetDir(tool, slug);
16635
- if (resolved.ok) {
16636
- choices.push({ label: `${tool} skills folder (${resolved.dir})${note}`, dir: resolved.dir });
16637
- }
16638
- const global = resolveTargetDir(tool, slug, true);
16639
- if (global.ok) {
16640
- choices.push({
16641
- label: `${tool} user-level skills folder (${global.dir})${note}`,
16642
- dir: global.dir
16643
- });
16644
- }
16645
- }
16675
+ function orderedAreas(declared, cwd, home) {
16676
+ const areas = knownAreas(cwd, home).map((area) => ({
16677
+ ...area,
16678
+ declared: area.tools.some((tool) => declaresTool(declared, tool))
16679
+ }));
16680
+ return [...areas.filter((a) => a.declared), ...areas.filter((a) => !a.declared)];
16681
+ }
16682
+ function installChoices(declared, slug, cwd, home) {
16683
+ const choices = orderedAreas(declared, cwd, home).map((area) => ({
16684
+ label: `${area.label}${area.declared ? "" : " - not declared by this skill"}`,
16685
+ dir: join4(area.dir, slug)
16686
+ }));
16646
16687
  choices.push({ label: `current directory (./${slug})`, dir: slug });
16647
16688
  return choices;
16648
16689
  }
16649
- function packChoices(declared, slug, count) {
16650
- const choices = [];
16651
- const ordered = [
16652
- ...MAPPED_TARGETS.filter((t) => declared.includes(t)),
16653
- ...MAPPED_TARGETS.filter((t) => !declared.includes(t))
16654
- ];
16655
- for (const tool of ordered) {
16656
- const note = declared.includes(tool) ? "" : " - not declared by this pack";
16657
- const project = resolveTargetArea(tool);
16658
- if (project.ok) {
16659
- choices.push({
16660
- label: `${tool} skills folder (${project.dir}) - ${count} skills${note}`,
16661
- tool,
16662
- global: false
16663
- });
16664
- }
16665
- const global = resolveTargetArea(tool, true);
16666
- if (global.ok) {
16667
- choices.push({
16668
- label: `${tool} user-level skills folder (${global.dir}) - ${count} skills${note}`,
16669
- tool,
16670
- global: true
16671
- });
16672
- }
16673
- }
16690
+ function packChoices(declared, slug, count, cwd, home) {
16691
+ const choices = orderedAreas(declared, cwd, home).map((area) => ({
16692
+ label: `${area.label} - ${count} skills${area.declared ? "" : " - not declared by this pack"}`,
16693
+ area
16694
+ }));
16674
16695
  choices.push({ label: `current directory (./${slug}, raw pack source)` });
16675
16696
  return choices;
16676
16697
  }
@@ -16685,23 +16706,20 @@ async function promptIndex(ask, count, push) {
16685
16706
  }
16686
16707
  async function choosePackLocation(ctx, detail, memberCount, prompt) {
16687
16708
  if (prompt) {
16688
- const choices = packChoices(detail.targets, ctx.slug, memberCount);
16709
+ const choices = packChoices(detail.targets, ctx.slug, memberCount, ctx.cwd, ctx.home);
16689
16710
  ctx.push("", "Install location:");
16690
16711
  choices.forEach((choice, i2) => ctx.push(` ${i2 + 1}) ${choice.label}`));
16691
16712
  const chosen = choices[await promptIndex(prompt, choices.length, ctx.push)];
16692
- return chosen.tool ? { tool: chosen.tool, global: chosen.global ?? false } : { dir: ctx.slug };
16713
+ return chosen.area ? { area: chosen.area } : { dir: ctx.slug };
16693
16714
  }
16694
16715
  const mappable = mappableDeclaredTargets(detail.targets);
16695
16716
  if (mappable.length > 0) {
16696
- ctx.push(
16697
- "",
16698
- `tip: --target ${mappable[0]} installs the ${memberCount} skills into the tool's skills folder`
16699
- );
16717
+ ctx.push("", `tip: --target ${mappable[0]} installs the ${memberCount} skills into the tool's skills folder`);
16700
16718
  }
16701
16719
  return { dir: ctx.slug };
16702
16720
  }
16703
16721
  async function chooseSingleLocation(ctx, detail, prompt) {
16704
- const choices = installChoices(detail.targets, ctx.slug);
16722
+ const choices = installChoices(detail.targets, ctx.slug, ctx.cwd, ctx.home);
16705
16723
  if (prompt && choices.length > 1) {
16706
16724
  ctx.push("", "Install location:");
16707
16725
  choices.forEach((choice, i2) => ctx.push(` ${i2 + 1}) ${choice.label}`));
@@ -16713,20 +16731,15 @@ async function chooseSingleLocation(ctx, detail, prompt) {
16713
16731
  }
16714
16732
  return ctx.slug;
16715
16733
  }
16716
- async function installPack(ctx, members, tool, global) {
16734
+ async function installPack(ctx, members, targets) {
16717
16735
  const { push, done, slug, preflight } = ctx;
16718
- const area = resolveTargetArea(tool, global);
16719
- if (!area.ok) {
16720
- push("", `error: ${area.message}`);
16736
+ const resolved = targets.map(({ area, tool }) => ({ area, tool, ...resolvePackMembers(members, area.layout) }));
16737
+ const unsupported = resolved.find((r) => r.installs.length === 0);
16738
+ if (unsupported) {
16739
+ push("", `error: none of this pack's skills support ${unsupported.tool}`);
16721
16740
  return done(2);
16722
16741
  }
16723
- const { installs, skipped } = resolvePackMembers(members, tool);
16724
- if (installs.length === 0) {
16725
- push("", `error: none of this pack's skills support ${tool}`);
16726
- return done(2);
16727
- }
16728
- const areaAbs = resolve2(ctx.cwd, area.dir);
16729
- const conflicts = installs.map((m) => join4(areaAbs, m.name)).filter(isOccupied);
16742
+ const conflicts = resolved.flatMap((r) => r.installs.map((m) => join4(r.area.dir, m.name))).filter(isOccupied);
16730
16743
  if (conflicts.length > 0) {
16731
16744
  push(
16732
16745
  "",
@@ -16740,41 +16753,47 @@ async function installPack(ctx, members, tool, global) {
16740
16753
  push("", `error: ${download.message}`);
16741
16754
  return done(2);
16742
16755
  }
16743
- const plans = planMembers(installs, download.files);
16744
- const missing = plans.find((p) => p.files.length === 0);
16756
+ const planned = resolved.map((r) => ({ ...r, plans: planMembers(r.installs, download.files) }));
16757
+ const missing = planned.flatMap((r) => r.plans).find((p) => p.files.length === 0);
16745
16758
  if (missing) {
16746
16759
  push("", `error: the snapshot has no files for "${missing.name}"; nothing was installed`);
16747
16760
  return done(2);
16748
16761
  }
16749
- for (const name of skipped) {
16750
- push("", `note: ${name} does not support ${tool}; skipped`);
16762
+ for (const r of planned) {
16763
+ for (const name of r.skipped) {
16764
+ push("", `note: ${name} does not support ${r.tool}; skipped`);
16765
+ }
16751
16766
  }
16752
- const written = [];
16753
- try {
16754
- for (const plan of plans) {
16755
- writeTree(plan.files, join4(areaAbs, plan.name));
16756
- written.push(plan.name);
16757
- recordReceipt(areaAbs, plan.name, receiptFor(preflight, slug));
16767
+ const landed = [];
16768
+ const describe3 = (dir, names) => planned.length > 1 ? `${dir}: ${names.join(", ")}` : names.join(", ");
16769
+ for (const r of planned) {
16770
+ const written = [];
16771
+ try {
16772
+ for (const plan of r.plans) {
16773
+ writeTree(plan.files, join4(r.area.dir, plan.name));
16774
+ written.push(plan.name);
16775
+ recordReceipt(r.area.dir, plan.name, receiptFor(preflight, slug));
16776
+ }
16777
+ } catch {
16778
+ const before = [...landed, ...written.length > 0 ? [describe3(r.area.dir, written)] : []];
16779
+ const where = planned.length > 1 ? ` in ${r.area.dir}` : "";
16780
+ push(
16781
+ "",
16782
+ `error: could not write ${r.plans[written.length].name}${where}; installed before the failure: ${before.join("; ") || "none"}`
16783
+ );
16784
+ return done(2);
16758
16785
  }
16759
- } catch {
16760
- push(
16761
- "",
16762
- `error: could not write ${plans[written.length].name}; installed before the failure: ${written.join(", ") || "none"}`
16763
- );
16764
- return done(2);
16786
+ landed.push(describe3(r.area.dir, written));
16787
+ push("", `Installed ${written.length} skills to ${r.area.dir}`, ` ${written.join(", ")}`);
16765
16788
  }
16766
- push(
16767
- "",
16768
- `Installed ${written.length} skills to ${areaAbs}`,
16769
- ` ${written.join(", ")}`,
16770
- "Source hash verified against the Skill Passport."
16771
- );
16789
+ push("Source hash verified against the Skill Passport.");
16772
16790
  return done(0);
16773
16791
  }
16774
- async function installSingle(ctx, targetDir) {
16792
+ async function installSingle(ctx, targetDirs) {
16775
16793
  const { push, done, slug, preflight } = ctx;
16776
- const target = resolve2(ctx.cwd, targetDir);
16777
- if (existsSync2(target)) {
16794
+ const targets = targetDirs.map((dir) => resolve2(ctx.cwd, dir));
16795
+ for (const target of targets) {
16796
+ if (!existsSync2(target)) continue;
16778
16797
  if (!statSync3(target).isDirectory()) {
16779
16798
  push("", `error: target ${target} already exists and is not a directory`);
16780
16799
  return done(2);
@@ -16789,33 +16808,40 @@ async function installSingle(ctx, targetDir) {
16789
16808
  push("", `error: ${download.message}`);
16790
16809
  return done(2);
16791
16810
  }
16792
- try {
16793
- writeTree(download.files, target);
16794
- } catch {
16795
- push("", "error: could not write the install; nothing was installed");
16796
- return done(2);
16797
- }
16798
- const area = knownAreas(ctx.cwd).find((a) => a.dir === dirname(target));
16799
- if (area) {
16800
- recordReceipt(area.dir, slug, receiptFor(preflight));
16811
+ const areas = knownAreas(ctx.cwd, ctx.home);
16812
+ const landed = [];
16813
+ for (const target of targets) {
16814
+ try {
16815
+ writeTree(download.files, target);
16816
+ } catch {
16817
+ push(
16818
+ "",
16819
+ landed.length === 0 ? "error: could not write the install; nothing was installed" : `error: could not write ${target}; installed before the failure: ${landed.join(", ")}`
16820
+ );
16821
+ return done(2);
16822
+ }
16823
+ landed.push(target);
16824
+ const area = areas.find((a) => a.dir === dirname(target));
16825
+ if (area) {
16826
+ recordReceipt(area.dir, slug, receiptFor(preflight));
16827
+ }
16828
+ push("", `Installed ${download.files.length} file(s) to ${target}`);
16801
16829
  }
16802
- push(
16803
- "",
16804
- `Installed ${download.files.length} file(s) to ${target}`,
16805
- "Source hash verified against the Skill Passport."
16806
- );
16830
+ push("Source hash verified against the Skill Passport.");
16807
16831
  return done(0);
16808
16832
  }
16809
16833
  async function runAdd(ref, opts = {}) {
16810
16834
  const { push, done } = createOutput(opts.emit);
16811
- if (opts.target && opts.dir) {
16835
+ const tools = [...new Set([opts.target ?? []].flat())];
16836
+ if (tools.length > 0 && opts.dir) {
16812
16837
  push(TARGET_OR_DIR);
16813
16838
  return done(2);
16814
16839
  }
16815
- if (opts.global && !opts.target) {
16840
+ if (opts.global && tools.length === 0) {
16816
16841
  push(GLOBAL_NEEDS_TARGET);
16817
16842
  return done(2);
16818
16843
  }
16844
+ const cwd = opts.cwd ?? process.cwd();
16819
16845
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
16820
16846
  const apiUrl = resolveApiUrl(opts.apiUrl);
16821
16847
  const fetched = await fetchPreflight(fetchImpl, apiUrl, ref);
@@ -16830,21 +16856,21 @@ async function runAdd(ref, opts = {}) {
16830
16856
  }
16831
16857
  const members = detail.packMembers ?? [];
16832
16858
  const isPack = members.length > 0;
16833
- let targetDir = opts.dir;
16834
- let pack;
16835
- if (opts.target) {
16836
- const resolved = isPack ? resolveTargetArea(opts.target, opts.global) : resolveTargetDir(opts.target, slug, opts.global);
16859
+ const targets = [];
16860
+ for (const tool of tools) {
16861
+ const resolved = resolveArea(tool, opts.global ?? false, cwd, opts.home);
16837
16862
  if (!resolved.ok) {
16838
16863
  push("", `error: ${resolved.message}`);
16839
16864
  return done(2);
16840
16865
  }
16841
- if (isPack) {
16842
- pack = { tool: opts.target, global: opts.global ?? false };
16843
- } else {
16844
- targetDir = resolved.dir;
16866
+ const shared = targets.find((t) => t.area.dir === resolved.area.dir);
16867
+ if (shared) {
16868
+ push("", `note: ${tool} and ${shared.tool} share ${resolved.area.dir}; installing once`);
16869
+ continue;
16845
16870
  }
16846
- if (!detail.targets.includes(opts.target)) {
16847
- push("", `warning: this skill does not declare ${opts.target} as a target`);
16871
+ targets.push({ area: resolved.area, tool });
16872
+ if (!declaresTool(detail.targets, tool)) {
16873
+ push("", `warning: this skill does not declare ${tool} as a target`);
16848
16874
  }
16849
16875
  }
16850
16876
  const confirmed = await confirmRisk(
@@ -16855,30 +16881,28 @@ async function runAdd(ref, opts = {}) {
16855
16881
  push
16856
16882
  );
16857
16883
  if (!confirmed) return done(2);
16858
- const ctx = {
16859
- push,
16860
- done,
16861
- fetchImpl,
16862
- apiUrl,
16863
- cwd: opts.cwd ?? process.cwd(),
16864
- slug,
16865
- preflight
16866
- };
16867
- if (isPack && pack === void 0 && targetDir === void 0) {
16868
- const location = await choosePackLocation(ctx, detail, members.length, opts.promptImpl);
16869
- if ("tool" in location) pack = location;
16870
- else targetDir = location.dir;
16871
- }
16872
- if (pack !== void 0) {
16873
- return installPack(ctx, members, pack.tool, pack.global);
16874
- }
16875
- if (isPack && opts.dir) {
16884
+ const ctx = { push, done, fetchImpl, apiUrl, cwd, home: opts.home, slug, preflight };
16885
+ if (isPack) {
16886
+ if (targets.length > 0) {
16887
+ return installPack(ctx, members, targets);
16888
+ }
16889
+ if (opts.dir === void 0) {
16890
+ const location = await choosePackLocation(ctx, detail, members.length, opts.promptImpl);
16891
+ return "area" in location ? installPack(ctx, members, [{ area: location.area, tool: location.area.tools[0] }]) : installSingle(ctx, [location.dir]);
16892
+ }
16876
16893
  push(
16877
16894
  "",
16878
16895
  `note: --dir installs the raw pack source; --target <tool> installs the ${members.length} skills individually`
16879
16896
  );
16897
+ return installSingle(ctx, [opts.dir]);
16880
16898
  }
16881
- return installSingle(ctx, targetDir ?? await chooseSingleLocation(ctx, detail, opts.promptImpl));
16899
+ if (targets.length > 0) {
16900
+ return installSingle(
16901
+ ctx,
16902
+ targets.map((t) => join4(t.area.dir, slug))
16903
+ );
16904
+ }
16905
+ return installSingle(ctx, [opts.dir ?? await chooseSingleLocation(ctx, detail, opts.promptImpl)]);
16882
16906
  }
16883
16907
 
16884
16908
  // src/list.ts
@@ -16923,11 +16947,7 @@ async function identifyByHash(fetchImpl, apiUrl, slug, dir) {
16923
16947
  } catch {
16924
16948
  return void 0;
16925
16949
  }
16926
- const detail = await getParsed(
16927
- fetchImpl,
16928
- `${apiUrl}/skills/${encodeURIComponent(slug)}`,
16929
- publicSkillDetailSchema
16930
- );
16950
+ const detail = await getParsed(fetchImpl, `${apiUrl}/skills/${encodeURIComponent(slug)}`, publicSkillDetailSchema);
16931
16951
  if (!detail.ok) {
16932
16952
  return void 0;
16933
16953
  }
@@ -16972,9 +16992,7 @@ async function runOutdated(opts = {}) {
16972
16992
  }
16973
16993
  seenPacks.add(receipt.pack.slug);
16974
16994
  tracked += 1;
16975
- const members = Object.values(receipts).filter(
16976
- (r) => r.pack?.slug === receipt.pack?.slug
16977
- ).length;
16995
+ const members = Object.values(receipts).filter((r) => r.pack?.slug === receipt.pack?.slug).length;
16978
16996
  const latest = latestBySlug.get(receipt.pack.slug)?.version;
16979
16997
  if (latest === void 0) {
16980
16998
  rows.push(` ${receipt.pack.slug} ${receipt.pack.version} not in the directory`);
@@ -16982,9 +17000,7 @@ async function runOutdated(opts = {}) {
16982
17000
  rows.push(` ${receipt.pack.slug} ${receipt.pack.version} current (pack, ${members} skills)`);
16983
17001
  } else {
16984
17002
  outdatedCount += 1;
16985
- rows.push(
16986
- ` ${receipt.pack.slug} ${receipt.pack.version} -> ${latest} (pack, ${members} skills)`
16987
- );
17003
+ rows.push(` ${receipt.pack.slug} ${receipt.pack.version} -> ${latest} (pack, ${members} skills)`);
16988
17004
  }
16989
17005
  continue;
16990
17006
  }
@@ -17060,7 +17076,7 @@ function runRemove(slug, opts = {}) {
17060
17076
  if (!opts.dir) {
17061
17077
  let areas = knownAreas(cwd, opts.home);
17062
17078
  if (opts.target) {
17063
- areas = areas.filter((a) => a.tool === opts.target && a.global === (opts.global ?? false));
17079
+ areas = areas.filter((a) => a.tools.includes(opts.target) && (opts.global ? a.global : a.project));
17064
17080
  }
17065
17081
  const packHits = areas.map((area) => ({
17066
17082
  area,
@@ -17082,10 +17098,7 @@ function runRemove(slug, opts = {}) {
17082
17098
  removeReceipt(area.dir, name);
17083
17099
  }
17084
17100
  return {
17085
- lines: [
17086
- `Removed pack ${slug} (${members.length} skills) from ${area.dir}`,
17087
- ` ${members.sort().join(", ")}`
17088
- ],
17101
+ lines: [`Removed pack ${slug} (${members.length} skills) from ${area.dir}`, ` ${members.sort().join(", ")}`],
17089
17102
  exitCode: 0
17090
17103
  };
17091
17104
  }
@@ -17100,9 +17113,7 @@ function runRemove(slug, opts = {}) {
17100
17113
  } else if (opts.dir) {
17101
17114
  dir = resolve3(cwd, opts.dir);
17102
17115
  } else {
17103
- const found = candidateDirs(slug, cwd, opts.home).filter(
17104
- (candidate) => existsSync4(candidate)
17105
- );
17116
+ const found = candidateDirs(slug, cwd, opts.home).filter((candidate) => existsSync4(candidate));
17106
17117
  if (found.length === 0) {
17107
17118
  return { lines: [`error: ${slug} is not installed in any known location`], exitCode: 2 };
17108
17119
  }
@@ -17221,25 +17232,23 @@ async function runSearch(opts = {}) {
17221
17232
  }
17222
17233
  if (opts.category && !CATEGORY_SLUGS.includes(opts.category)) {
17223
17234
  return {
17224
- lines: [
17225
- `error: unknown category "${opts.category}" (known categories: ${CATEGORY_SLUGS.join(", ")})`
17226
- ],
17235
+ lines: [`error: unknown category "${opts.category}" (known categories: ${CATEGORY_SLUGS.join(", ")})`],
17227
17236
  exitCode: 2
17228
17237
  };
17229
17238
  }
17230
17239
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
17231
17240
  const apiUrl = resolveApiUrl(opts.apiUrl);
17241
+ if (opts.ai) {
17242
+ return runAiSearch(opts, fetchImpl, apiUrl);
17243
+ }
17232
17244
  const fetched = await getParsed(fetchImpl, `${apiUrl}/skills`, publicSkillListSchema);
17233
17245
  if (!fetched.ok) {
17234
17246
  return { lines: [`error: ${fetched.message}`], exitCode: 2 };
17235
17247
  }
17236
17248
  const query = (opts.query ?? "").trim().toLowerCase();
17237
- const matches = fetched.data.filter((skill) => {
17238
- if (opts.target && !skill.targets.includes(opts.target)) return false;
17239
- if (opts.category && (skill.category ?? null) !== opts.category) return false;
17240
- if (opts.packs && (skill.packSkills?.length ?? 0) === 0) return false;
17241
- return query === "" || matchesQuery(skill, query);
17242
- });
17249
+ const matches = fetched.data.filter(
17250
+ (skill) => matchesFilters(skill, opts) && (query === "" || matchesQuery(skill, query))
17251
+ );
17243
17252
  if (opts.json) {
17244
17253
  return { lines: [JSON.stringify(matches, null, 2)], exitCode: 0 };
17245
17254
  }
@@ -17251,9 +17260,53 @@ async function runSearch(opts = {}) {
17251
17260
  lines: [
17252
17261
  ...renderRows(matches, opts.width, st),
17253
17262
  "",
17254
- st.dim(
17255
- `${matches.length} of ${fetched.data.length} skills - skillpass report <slug> shows the passport`
17256
- )
17263
+ st.dim(`${matches.length} of ${fetched.data.length} skills - skillpass report <slug> shows the passport`)
17264
+ ],
17265
+ exitCode: 0
17266
+ };
17267
+ }
17268
+ function matchesFilters(skill, opts) {
17269
+ if (opts.target && !skill.targets.includes(opts.target)) return false;
17270
+ if (opts.category && (skill.category ?? null) !== opts.category) return false;
17271
+ if (opts.packs && (skill.packSkills?.length ?? 0) === 0) return false;
17272
+ return true;
17273
+ }
17274
+ var AI_ERRORS = {
17275
+ 429: "AI search is rate limited (20 requests a minute per address); try again in a moment",
17276
+ 503: "AI search is not configured on this API",
17277
+ 502: "AI search is temporarily unavailable; try again shortly"
17278
+ };
17279
+ function aiErrorMessage(outcome) {
17280
+ if (outcome.status !== void 0 && AI_ERRORS[outcome.status]) return AI_ERRORS[outcome.status];
17281
+ if (outcome.status === 400 && outcome.apiError) return outcome.apiError;
17282
+ return outcome.message;
17283
+ }
17284
+ async function runAiSearch(opts, fetchImpl, apiUrl) {
17285
+ const query = (opts.query ?? "").trim();
17286
+ if (query === "") {
17287
+ return { lines: ["error: search --ai needs a query (describe what you need)"], exitCode: 2 };
17288
+ }
17289
+ const fetched = await getParsed(
17290
+ fetchImpl,
17291
+ `${apiUrl}/skills/search?q=${encodeURIComponent(query)}`,
17292
+ publicSkillListSchema
17293
+ );
17294
+ if (!fetched.ok) {
17295
+ return { lines: [`error: ${aiErrorMessage(fetched)}`], exitCode: 2 };
17296
+ }
17297
+ const matches = fetched.data.filter((skill) => matchesFilters(skill, opts));
17298
+ if (opts.json) {
17299
+ return { lines: [JSON.stringify(matches, null, 2)], exitCode: 0 };
17300
+ }
17301
+ if (matches.length === 0) {
17302
+ return { lines: ["No skills match by meaning - try other words"], exitCode: 0 };
17303
+ }
17304
+ const st = opts.style ?? PLAIN;
17305
+ return {
17306
+ lines: [
17307
+ ...renderRows(matches, opts.width, st),
17308
+ "",
17309
+ st.dim(`${matches.length} skills by meaning - skillpass report <slug> shows the passport`)
17257
17310
  ],
17258
17311
  exitCode: 0
17259
17312
  };
@@ -17266,7 +17319,7 @@ async function locate(slug, opts, fetchImpl, apiUrl) {
17266
17319
  const cwd = opts.cwd ?? process.cwd();
17267
17320
  let areas = knownAreas(cwd, opts.home);
17268
17321
  if (opts.target) {
17269
- areas = areas.filter((a) => a.tool === opts.target && a.global === (opts.global ?? false));
17322
+ areas = areas.filter((a) => a.tools.includes(opts.target) && (opts.global ? a.global : a.project));
17270
17323
  }
17271
17324
  const hits = [];
17272
17325
  for (const area of areas) {
@@ -17337,9 +17390,7 @@ async function runUpdate(ref, opts = {}) {
17337
17390
  const hits = await locate(slug, opts, fetchImpl, apiUrl);
17338
17391
  const memberHit = hits.find((h) => h.packSlug !== void 0 && h.packSlug !== slug);
17339
17392
  if (memberHit?.packSlug !== void 0) {
17340
- push(
17341
- `error: ${slug} is part of the ${memberHit.packSlug} pack; run skillpass update ${memberHit.packSlug}`
17342
- );
17393
+ push(`error: ${slug} is part of the ${memberHit.packSlug} pack; run skillpass update ${memberHit.packSlug}`);
17343
17394
  return done(2);
17344
17395
  }
17345
17396
  if (hits.length === 0) {
@@ -17411,9 +17462,7 @@ async function changesVsInstalled(fetchImpl, apiUrl, slug, installed, target, st
17411
17462
  publicPreflightSchema
17412
17463
  );
17413
17464
  if (!before.ok) {
17414
- return [
17415
- `Changes vs installed v${installed.installedVersion}: unavailable (that version is no longer published)`
17416
- ];
17465
+ return [`Changes vs installed v${installed.installedVersion}: unavailable (that version is no longer published)`];
17417
17466
  }
17418
17467
  const { added, removed } = permissionChanges(before.data.permissions, target.permissions);
17419
17468
  if (added.length === 0 && removed.length === 0) {
@@ -17448,9 +17497,10 @@ async function runPackUpdate(slug, installed, detail, preflight, opts, fetchImpl
17448
17497
  );
17449
17498
  if (!confirmed) return done(2);
17450
17499
  const areaDir = installed.area.dir;
17451
- const { installs, skipped } = resolvePackMembers(detail.packMembers ?? [], installed.area.tool);
17500
+ const tool = opts.target ?? installed.area.tools[0];
17501
+ const { installs, skipped } = resolvePackMembers(detail.packMembers ?? [], installed.area.layout);
17452
17502
  if (installs.length === 0) {
17453
- push("", `error: none of the new version's skills support ${installed.area.tool}`);
17503
+ push("", `error: none of the new version's skills support ${tool}`);
17454
17504
  return done(2);
17455
17505
  }
17456
17506
  const receipts = readReceipts(areaDir);
@@ -17479,7 +17529,7 @@ async function runPackUpdate(slug, installed, detail, preflight, opts, fetchImpl
17479
17529
  return done(2);
17480
17530
  }
17481
17531
  for (const name of skipped) {
17482
- push("", `note: ${name} does not support ${installed.area.tool}; skipped`);
17532
+ push("", `note: ${name} does not support ${tool}; skipped`);
17483
17533
  }
17484
17534
  const done1 = [];
17485
17535
  try {
@@ -17560,11 +17610,11 @@ var USAGE = [
17560
17610
  "skillpass - validate AI agent skills locally and inspect hosted passports",
17561
17611
  "",
17562
17612
  "Usage:",
17563
- " skillpass search [query] [--target <tool>] [--category <slug>] [--packs] [--json]",
17613
+ " skillpass search [query] [--ai] [--target <tool>] [--category <slug>] [--packs] [--json]",
17564
17614
  " find skills in the directory",
17565
17615
  " skillpass scan <path> [--json] run the validator on a local skill package",
17566
17616
  " skillpass report <slug>[@version] [--json] fetch the hosted passport pre-flight",
17567
- " skillpass add <slug>[@version] [--target <tool> [--global] | --dir <path>] [--yes]",
17617
+ " skillpass add <slug>[@version] [--target <tool>... [--global] | --dir <path>] [--yes]",
17568
17618
  " install a skill through the pre-flight gate",
17569
17619
  " skillpass remove <slug> [--target <tool> [--global] | --dir <path>]",
17570
17620
  " remove an installed skill",
@@ -17575,11 +17625,15 @@ var USAGE = [
17575
17625
  "",
17576
17626
  "Flags:",
17577
17627
  " --json print machine-readable JSON instead of the readable report",
17578
- " --target install into a tool's skills folder: claude-code (.claude/skills),",
17579
- " codex (.agents/skills)",
17580
- " --global with --target claude-code, install to ~/.claude/skills instead",
17628
+ " --target a tool's skills folder: claude-code, cline, or agents (the shared",
17629
+ " .agents/skills read by codex, cursor, windsurf, github-copilot,",
17630
+ " gemini-cli, and opencode; those names work too). add takes it more",
17631
+ " than once to install into several tools",
17632
+ " --global with --target, use the tool's user-level folder (~/.claude/skills,",
17633
+ " ~/.agents/skills, ~/.cline/skills)",
17581
17634
  " --dir install target directory (default ./<slug>)",
17582
17635
  " --yes skip the confirmation prompt for medium+ risk skills",
17636
+ " --ai search by meaning: describe what you need (needs a query)",
17583
17637
  " --category search filter: a directory category slug",
17584
17638
  " --packs search filter: multi-skill packs only",
17585
17639
  " --version print the CLI version",
@@ -17597,7 +17651,7 @@ var COMMAND_FLAGS = {
17597
17651
  list: [],
17598
17652
  outdated: [],
17599
17653
  update: ["--yes", "--target", "--global"],
17600
- search: ["--target", "--category", "--packs", "--json"]
17654
+ search: ["--ai", "--target", "--category", "--packs", "--json"]
17601
17655
  };
17602
17656
  function parseCliArgs(argv) {
17603
17657
  const parsed = {
@@ -17608,6 +17662,7 @@ function parseCliArgs(argv) {
17608
17662
  yes: false,
17609
17663
  global: false,
17610
17664
  packs: false,
17665
+ ai: false,
17611
17666
  seen: []
17612
17667
  };
17613
17668
  for (let i2 = 0; i2 < argv.length; i2++) {
@@ -17624,6 +17679,9 @@ function parseCliArgs(argv) {
17624
17679
  } else if (arg === "--packs") {
17625
17680
  parsed.packs = true;
17626
17681
  parsed.seen.push(arg);
17682
+ } else if (arg === "--ai") {
17683
+ parsed.ai = true;
17684
+ parsed.seen.push(arg);
17627
17685
  } else if (arg === "--dir" || arg === "--target" || arg === "--category") {
17628
17686
  parsed.seen.push(arg);
17629
17687
  const value = argv[i2 + 1];
@@ -17634,7 +17692,7 @@ function parseCliArgs(argv) {
17634
17692
  if (arg === "--dir") {
17635
17693
  parsed.dir = value;
17636
17694
  } else if (arg === "--target") {
17637
- parsed.target = value;
17695
+ parsed.targets = [.../* @__PURE__ */ new Set([...parsed.targets ?? [], value])];
17638
17696
  } else {
17639
17697
  parsed.category = value;
17640
17698
  }
@@ -17684,12 +17742,17 @@ async function run(argv) {
17684
17742
  if (disallowed) {
17685
17743
  return { lines: [`error: ${args.command} does not take ${disallowed}`, "", USAGE], exitCode: 2 };
17686
17744
  }
17745
+ const target = args.targets?.[0];
17746
+ if (args.command !== "add" && (args.targets?.length ?? 0) > 1) {
17747
+ return { lines: [`error: ${args.command} takes one --target`, "", USAGE], exitCode: 2 };
17748
+ }
17687
17749
  if (args.command === "search") {
17688
17750
  return runSearch({
17689
17751
  query: args.positional.join(" "),
17690
- target: args.target,
17752
+ target,
17691
17753
  category: args.category,
17692
17754
  packs: args.packs,
17755
+ ai: args.ai,
17693
17756
  json: args.json,
17694
17757
  width: process.stdout.isTTY ? process.stdout.columns : void 0,
17695
17758
  style: styler(Boolean(process.stdout.isTTY))
@@ -17707,7 +17770,7 @@ async function run(argv) {
17707
17770
  if (!slug) {
17708
17771
  return { lines: ["error: remove needs a skill slug", "", USAGE], exitCode: 2 };
17709
17772
  }
17710
- return runRemove(slug, { target: args.target, dir: args.dir, global: args.global });
17773
+ return runRemove(slug, { target, dir: args.dir, global: args.global });
17711
17774
  }
17712
17775
  if (args.command === "list") {
17713
17776
  if (args.positional.length > 0) {
@@ -17723,7 +17786,7 @@ async function run(argv) {
17723
17786
  const tty = Boolean(process.stdin.isTTY);
17724
17787
  return runUpdate(ref, {
17725
17788
  yes: args.yes,
17726
- target: args.target,
17789
+ target,
17727
17790
  global: args.global,
17728
17791
  style: styler(Boolean(process.stdout.isTTY)),
17729
17792
  confirmImpl: tty ? confirmViaTty : void 0,
@@ -17752,7 +17815,7 @@ async function run(argv) {
17752
17815
  return runAdd(ref, {
17753
17816
  yes: args.yes,
17754
17817
  dir: args.dir,
17755
- target: args.target,
17818
+ target: args.targets,
17756
17819
  global: args.global,
17757
17820
  style: styler(Boolean(process.stdout.isTTY)),
17758
17821
  confirmImpl: tty ? confirmViaTty : void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillpass",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Validation-first installer for AI agent skills: search the SkillPass directory, inspect a skill's passport, and install hash-verified skills and packs for Claude Code, Codex, and more.",
5
5
  "keywords": [
6
6
  "ai",