skill-land 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,6 +56,8 @@ npx skill-land <source> --for <agent> [options]
56
56
  |---|---|
57
57
  | `--for <agent>` | target agent; repeatable or comma-separated |
58
58
  | `--skill <name>` | which skill, when the source contains several |
59
+ | `--all` | every skill in the source, not just one |
60
+ | `--verbose` | per-skill detail when `--all` (default: one line each) |
59
61
  | `--project` | install into `./` instead of the user-global directory |
60
62
  | `--dry-run` | resolve and report, write nothing |
61
63
  | `--verify` | check an existing install, write nothing |
@@ -69,8 +71,47 @@ skill-land Magithar/SKILLmama --for codex,antigravity
69
71
 
70
72
  # audit an install someone else did
71
73
  skill-land Magithar/SKILLmama --for codex --verify
74
+
75
+ # audit every skill you have, however it was installed
76
+ skill-land ~/.agents/skills --for antigravity --verify --all
72
77
  ```
73
78
 
79
+ ## Auditing a whole directory
80
+
81
+ `--all` takes every skill in the source instead of one. It is never the default:
82
+ [`selectSkill()`](skill-land.mjs) refuses to guess when a source holds several,
83
+ and `--all` is you saying you meant all of them.
84
+
85
+ The use it was built for is `--verify --all` — checking whether the skills you
86
+ already installed are where the agent actually reads, whatever put them there.
87
+ That question is unanswerable today even if your setup works, because nothing in
88
+ the ecosystem reads an install back:
89
+
90
+ ```
91
+ $ skill-land ~/.agents/skills --for antigravity --verify --all
92
+
93
+ source /Users/you/.agents/skills
94
+ skills 1 found
95
+ impeccable FAIL 0/3
96
+ x ~/.gemini/config/skills/impeccable/SKILL.md
97
+ file exists: ~/.gemini/config/skills/impeccable/SKILL.md
98
+ x ~/.gemini/skills/impeccable/SKILL.md
99
+ content matches source: 10401 bytes sha abd76e36cf1d vs 10401 bytes sha a1ea82ce80f4
100
+ 1 skill, 3 failed
101
+ ```
102
+
103
+ That middle line is why the check compares hashes and not sizes: both files are
104
+ 10401 bytes and they are not the same file. A copy that drifted after install
105
+ looks identical to `ls`.
106
+
107
+ One line per skill, because a report nobody reads verifies nothing; `--verbose`
108
+ restores the full per-target output. Failing paths are always named — a summary
109
+ you have to re-run to act on is not a summary.
110
+
111
+ With `--strict`, a flagged skill is skipped and the rest still install; the run
112
+ exits non-zero and lists what it skipped. All-or-nothing across a directory would
113
+ let one flagged skill block every clean one.
114
+
74
115
  ## What "verified" means
75
116
 
76
117
  An install is not done until read-back confirms it:
@@ -88,19 +129,63 @@ unwritable destination, and a real `npx skills add` install (correctly reported
88
129
 
89
130
  ## Antigravity
90
131
 
91
- Antigravity loads skills at startup, so an install cannot be confirmed live. Two
92
- candidate directories exist and both were created by Antigravity itself in the same
93
- second:
132
+ Antigravity loads skills at startup, so an install cannot be confirmed live. The
133
+ `agy` search path was stated by an Antigravity maintainer on 2026-09-05 in
134
+ [antigravity-cli#103](https://github.com/google-antigravity/antigravity-cli/issues/103#issuecomment-5547952239):
94
135
 
95
136
  ```
96
- ~/.gemini/antigravity/skills what the skills CLI registry declares
97
- ~/.gemini/config/skills confirmed working by live testing
137
+ ~/.gemini/config/skills primary; independently confirmed by live testing
138
+ ~/.gemini/skills fallback
139
+ ~/.gemini/antigravity-cli/skills fallback
98
140
  ```
99
141
 
100
- The `language_server` binary contains `skillsPaths` and `skills_paths`, so the search
101
- path is a configurable list rather than a single directory. Until that list is pinned
102
- down, `--for antigravity` writes to both and says so. Restart Antigravity fully after
103
- installing.
142
+ `--for antigravity` writes to all three and says so. The `language_server` binary
143
+ contains `skillsPaths` and `skills_paths`, so this is a configurable list rather than a
144
+ single directory, and the maintainer statement covers the CLI — whether the IDE surface
145
+ reads the same list is **unverified**.
146
+
147
+ Checked against the shipped binaries (Antigravity 2026-07-30 build, inspected
148
+ 2026-09-05). `language_server` contains **no** `.gemini/*/skills` literal at all:
149
+ `skills_paths` is a repeated protobuf field, so the client passes the list in, and
150
+ skills resolve to `skills/<name>/` under a *customization root*. Both roots we write
151
+ to are named in the shipped strings and config:
152
+
153
+ - `~/.gemini/config/` holds `config.json` and `skills/` — the global root
154
+ - `~/.gemini/antigravity-cli/` — "The CLI is configured via
155
+ `~/.gemini/antigravity-cli/settings.json`" (`language_server`)
156
+
157
+ 1.1.0 and earlier also wrote `~/.gemini/antigravity/skills`, on the reasoning that
158
+ Antigravity created that directory itself. That reasoning was wrong: `app.asar` defines
159
+ `~/.gemini/antigravity` as `IDE_OLD_DATA_DIR`, the **legacy IDE data directory** being
160
+ migrated to `~/.gemini/antigravity-ide`. It was never a skills root. No longer written.
161
+
162
+ ### Live verification
163
+
164
+ Confirmed against **`agy` 1.1.27 on 2026-09-05**, not inferred. A uniquely-named
165
+ canary skill was planted in each candidate directory and `agy -p "/skills"` run from
166
+ a neutral workspace:
167
+
168
+ | directory | listed? |
169
+ |---|---|
170
+ | `~/.gemini/config/skills` | **yes** |
171
+ | `~/.gemini/skills` | **yes** |
172
+ | `~/.gemini/antigravity-cli/skills` | **yes** |
173
+ | `~/.agents/skills` | no |
174
+ | `~/.gemini/antigravity/skills` | no |
175
+
176
+ All three directories `skill-land` writes to are read. The two it does not write to
177
+ are not.
178
+
179
+ `~/.agents/skills` appears **only** when its parent is the active workspace
180
+ (`agy --add-dir $HOME`) — that is project-local behaviour, not a global search path,
181
+ which is exactly what [#103](https://github.com/google-antigravity/antigravity-cli/issues/103)
182
+ is asking to change. Confirmed still open at 1.1.27.
183
+
184
+ Two side findings: project-local `.agents/skills` is read only when the directory is
185
+ a **registered workspace** — being the current directory was not enough — and
186
+ `.antigravity/skills` and `.agy/skills` are not read at all.
187
+
188
+ Restart Antigravity fully after installing.
104
189
 
105
190
  ## Security scan
106
191
 
package/agents.json CHANGED
@@ -18,10 +18,13 @@
18
18
  "broken": true
19
19
  },
20
20
  "antigravity": {
21
- "_paths": "Global dir is ~/.gemini/config/skills for ALL Antigravity surfaces (2.0, IDE, CLI); they consolidated config across the three clients. Sources: vercel-labs/skills#1470, where the agy binary's own docs give global root ~/.gemini/config/ with skills at skills/<name>/, and no ~/.gemini/antigravity-cli/skills string exists in the binary. Project dir <workspace>/.agents/skills confirmed by behavior 2026-08-08. The other two are written as fallbacks only, pending PR #1483 landing upstream.",
21
+ "_paths": "LIVE VERIFIED against agy 1.1.27 on 2026-09-05: a uniquely-named canary skill was planted in each candidate directory and `agy -p /skills` run from a neutral workspace. Read as global: ~/.gemini/config/skills, ~/.gemini/skills, ~/.gemini/antigravity-cli/skills — matching the maintainer statement in google-antigravity/antigravity-cli#103 comment 5547952239. NOT read: ~/.agents/skills (appears only when its parent is the active workspace, i.e. `--add-dir $HOME`, which is project-local behavior, not global) and ~/.gemini/antigravity/skills (app.asar defines ~/.gemini/antigravity as IDE_OLD_DATA_DIR, the legacy IDE data dir migrating to ~/.gemini/antigravity-ide; never a skills root). Project dir .agents/skills is read only when the directory is a registered workspace — cwd alone was not enough; .antigravity/skills and .agy/skills are not read at all. language_server hardcodes no .gemini/*/skills literal: skills_paths is a repeated protobuf field the client fills in.",
22
22
  "display": "Antigravity",
23
23
  "global": "~/.gemini/config/skills",
24
- "alsoWrite": ["~/.gemini/antigravity/skills", "~/.gemini/skills"],
24
+ "alsoWrite": [
25
+ "~/.gemini/skills",
26
+ "~/.gemini/antigravity-cli/skills"
27
+ ],
25
28
  "project": ".agents/skills",
26
29
  "file": "SKILL.md",
27
30
  "restart": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-land",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Install an agent skill to the directory the agent actually reads, then verify it landed. Exits non-zero if it didn't.",
5
5
  "keywords": [
6
6
  "agent-skills",
package/skill-land.mjs CHANGED
@@ -120,7 +120,7 @@ function selectSkill(root, wanted) {
120
120
  const shown = named.slice(0, 15).map((n) => ` ${n.name.padEnd(28)} ${n.path.replace(root, ".")}`);
121
121
  die(
122
122
  `${named.length} skills found in ${root}; refusing to guess.\n` +
123
- ` Pick one with --skill <name>:\n` +
123
+ ` Pick one with --skill <name>, or --all for every one of them:\n` +
124
124
  shown.join("\n") +
125
125
  (named.length > 15 ? `\n ... and ${named.length - 15} more` : "")
126
126
  );
@@ -128,6 +128,17 @@ function selectSkill(root, wanted) {
128
128
  return named[0].path;
129
129
  }
130
130
 
131
+ // --all is the third arm of the same decision: refuse, pick one, or take them
132
+ // all. Deliberately never the default — silently installing 78 skills because
133
+ // a source happened to contain them is the failure selectSkill() exists to
134
+ // prevent, and --all is the user saying they meant it.
135
+ function selectAll(root) {
136
+ const hits = findSkills(root);
137
+ if (!hits.length)
138
+ die(`no SKILL.md found under ${root}\n searched ${MAX_DEPTH} levels deep`);
139
+ return hits;
140
+ }
141
+
131
142
  function parseFrontmatter(text) {
132
143
  const m = text.match(/^---\n([\s\S]*?)\n---\n/);
133
144
  if (!m) return {};
@@ -287,66 +298,17 @@ function verify(target, expected) {
287
298
  return checks;
288
299
  }
289
300
 
290
- // ---------------------------------------------------------------- main
291
-
292
- const argv = process.argv.slice(2);
293
- if (!argv.length || argv.includes("-h") || argv.includes("--help")) {
294
- console.log(`
295
- skill-land <source> --for <agent> [options]
296
-
297
- <source> owner/repo | github URL | raw .md URL | local path
298
-
299
- Options
300
- --for <agent> target agent (repeatable, or comma-separated)
301
- --skill <name> which skill, when the source contains several
302
- --project install into ./ instead of the user-global dir
303
- --dry-run resolve and report, write nothing
304
- --verify check an existing install, write nothing
305
- --skip-security skip the SkillSpector scan (acknowledged, not silent)
306
- --strict refuse to install anything SkillSpector flags
307
- --list show known agents and their directories
308
-
309
- Verifies the file landed where the agent actually reads. Exits non-zero if not.
310
- `);
311
- process.exit(0);
312
- }
313
-
314
- if (argv.includes("--list")) {
315
- console.log("\n agent directory upstream");
316
- console.log(" " + "-".repeat(70));
317
- for (const [k, a] of Object.entries(AGENTS)) {
318
- console.log(` ${k.padEnd(17)} ${a.global.padEnd(38)} ${a.broken ? "BROKEN" : "ok"}`);
319
- }
320
- console.log();
321
- process.exit(0);
322
- }
323
-
324
- const flag = (n) => argv.includes(n);
325
- const values = (n) => argv.reduce((acc, a, i) =>
326
- a === n && argv[i + 1] ? acc.concat(argv[i + 1].split(",")) : acc, []);
327
-
328
- // Flags that consume the next argument; their values are not the source.
329
- const VALUE_FLAGS = new Set(["--for", "--skill"]);
330
- const source = argv.find((a, i) => !a.startsWith("--") && !VALUE_FLAGS.has(argv[i - 1]));
331
- if (!source) die("no source given");
332
-
333
- const targets = values("--for").map((s) => s.trim()).filter(Boolean);
334
- if (!targets.length) die(`no --for <agent> given\n known: ${Object.keys(AGENTS).join(", ")}`);
335
- for (const t of targets) if (!AGENTS[t]) die(`unknown agent "${t}"\n known: ${Object.keys(AGENTS).join(", ")}`);
301
+ // ---------------------------------------------------------------- per skill
336
302
 
337
- const isProject = flag("--project");
338
- const dryRun = flag("--dry-run");
339
- const verifyOnly = flag("--verify");
340
- const skipSecurity = flag("--skip-security");
341
- const strict = flag("--strict");
342
-
343
- const src = resolveSource(source);
344
- const tmp = mkdtempSync(join(tmpdir(), "skill-install-"));
345
- let failed = 0;
346
-
347
- try {
348
- const root = fetchSource(src, tmp);
349
- const skillPath = selectSkill(root, values("--skill")[0]);
303
+ // One skill, every target path: scan, disclose, write, read back. Factored out
304
+ // of main so --all is a loop over it rather than a second code path — the
305
+ // single-skill output is unchanged, byte for byte.
306
+ //
307
+ // Returns what the caller needs to summarise: how many targets failed
308
+ // verification, and whether --strict refused this one.
309
+ function runSkill(skillPath, root, quiet) {
310
+ let skillFailed = 0;
311
+ let skillSkipped = false;
350
312
  const body = readFileSync(skillPath, "utf-8");
351
313
  const fm = parseFrontmatter(body);
352
314
  const skillName = (fm.name || "skill").toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
@@ -354,7 +316,7 @@ try {
354
316
  const expected = { bytes: rawBody.length, hash: sha256(rawBody), name: fm.name };
355
317
 
356
318
  // Scan before any write. A blocked skill must never reach disk.
357
- const scan = skipSecurity ? { status: "skipped" } : securityScan(skillPath);
319
+ const scan = (skipSecurity || verifyOnly) ? { status: "skipped" } : securityScan(skillPath);
358
320
  const contents = disclose(rawBody.toString("utf-8"));
359
321
  const findings = (scan.top || []).length
360
322
  ? "\n" + scan.top.map((t) => ` ${t}`).join("\n") : "";
@@ -372,10 +334,14 @@ try {
372
334
  skipped: () => ` security NOT SCANNED --skip-security given`,
373
335
  };
374
336
 
337
+ if (quiet) {
338
+ process.stdout.write(` ${(fm.name || basename(dirname(skillPath))).slice(0, 28).padEnd(30)}`);
339
+ } else {
375
340
  console.log(`\n source ${src.label}`);
376
341
  console.log(` skill ${fm.name || "(no frontmatter name)"} [${rawBody.length} bytes, sha ${expected.hash.slice(0,12)}]`);
377
342
  console.log(` from ${skillPath.replace(root, ".")}`);
378
- if (!verifyOnly) {
343
+ }
344
+ if (!verifyOnly && !quiet) {
379
345
  // Always shown, with or without a scanner. Counts and quotes only.
380
346
  if (contents.length) {
381
347
  console.log(" contains");
@@ -388,27 +354,39 @@ try {
388
354
  }
389
355
  console.log(SCAN_LINE[scan.status]());
390
356
  }
391
- console.log("");
357
+ if (!quiet) console.log("");
392
358
 
393
359
  // Refuse to write a skill the scanner flagged. --force is an explicit,
394
360
  // logged override, never a silent one.
395
361
  if (!verifyOnly && strict && scan.status === "flagged") {
396
- die(`--strict: SkillSpector reports ${scan.detail}\n Review the findings above. Drop --strict to install anyway.`);
362
+ if (!quiet)
363
+ console.error(`\n error --strict: SkillSpector reports ${scan.detail}\n Review the findings above. Drop --strict to install anyway.\n`);
364
+ else console.log(`SKIP --strict: ${scan.detail}`);
365
+ // One flagged skill must not block the clean ones, but the run still fails.
366
+ return { name: skillName, failed: 0, skipped: true };
397
367
  }
398
368
 
369
+ const bad = [];
370
+ let targetCount = 0;
371
+
399
372
  for (const agent of targets) {
400
373
  const a = AGENTS[agent];
401
374
  const bases = isProject
402
375
  ? [resolve(a.project)]
403
376
  : [a.global, ...(a.alsoWrite || [])].map(expand);
404
377
 
405
- console.log(` ${a.display}`);
378
+ if (!quiet) console.log(` ${a.display}`);
406
379
 
407
380
  for (const base of bases) {
408
381
  const destDir = join(base, skillName);
409
382
  const target = join(destDir, a.file);
410
383
 
411
- if (dryRun) { console.log(` would write ${target}`); continue; }
384
+ targetCount++;
385
+ if (dryRun) {
386
+ if (quiet) continue;
387
+ console.log(` would write ${target}`);
388
+ continue;
389
+ }
412
390
 
413
391
  // A write that throws is a failed install, not a crash. Report it the
414
392
  // same way as a failed read-back so one exit path covers both.
@@ -426,18 +404,123 @@ try {
426
404
  ? [{ name: "write", pass: false, detail: writeErr }]
427
405
  : verify(target, expected);
428
406
  const passed = checks.every((c) => c.pass);
429
- if (!passed) failed++;
407
+ if (!passed) { skillFailed++; bad.push({ target, checks }); }
430
408
 
409
+ if (quiet) continue;
431
410
  console.log(` ${passed ? "OK " : "FAIL"} ${target}${verifyOnly ? " (verify only)" : ""}`);
432
411
  for (const c of checks) {
433
412
  if (!c.pass) console.log(` x ${c.name}: ${c.detail}`);
434
413
  }
435
414
  }
436
415
 
416
+ if (quiet) continue;
437
417
  if (!dryRun && a.restart) console.log(` note restart ${a.display} fully; skills load at startup`);
438
418
  if (a.broken) console.log(` note \`npx skills add -a ${agent} -g\` writes to ~/.agents/skills instead`);
439
419
  console.log();
440
420
  }
421
+
422
+ // One line per skill: the counts, then every failing path underneath it. A
423
+ // summary that hid which path failed would need a second run to be actionable.
424
+ if (quiet) {
425
+ const okCount = targetCount - skillFailed;
426
+ if (dryRun) console.log(`-- ${targetCount} target${targetCount === 1 ? "" : "s"} (dry run)`);
427
+ else console.log(`${skillFailed ? "FAIL" : "OK "} ${okCount}/${targetCount}`);
428
+ for (const b of bad) {
429
+ console.log(` x ${b.target}`);
430
+ for (const c of b.checks) if (!c.pass) console.log(` ${c.name}: ${c.detail}`);
431
+ }
432
+ }
433
+ return { name: skillName, failed: skillFailed, skipped: skillSkipped };
434
+ }
435
+
436
+ // ---------------------------------------------------------------- main
437
+
438
+ const argv = process.argv.slice(2);
439
+ if (!argv.length || argv.includes("-h") || argv.includes("--help")) {
440
+ console.log(`
441
+ skill-land <source> --for <agent> [options]
442
+
443
+ <source> owner/repo | github URL | raw .md URL | local path
444
+
445
+ Options
446
+ --for <agent> target agent (repeatable, or comma-separated)
447
+ --skill <name> which skill, when the source contains several
448
+ --all every skill in the source, not just one
449
+ --verbose per-skill detail when --all (default: one line each)
450
+ --project install into ./ instead of the user-global dir
451
+ --dry-run resolve and report, write nothing
452
+ --verify check an existing install, write nothing
453
+ --skip-security skip the SkillSpector scan (acknowledged, not silent)
454
+ --strict refuse to install anything SkillSpector flags
455
+ --list show known agents and their directories
456
+
457
+ Verifies the file landed where the agent actually reads. Exits non-zero if not.
458
+ `);
459
+ process.exit(0);
460
+ }
461
+
462
+ if (argv.includes("--list")) {
463
+ console.log("\n agent directory upstream");
464
+ console.log(" " + "-".repeat(70));
465
+ for (const [k, a] of Object.entries(AGENTS)) {
466
+ console.log(` ${k.padEnd(17)} ${a.global.padEnd(38)} ${a.broken ? "BROKEN" : "ok"}`);
467
+ }
468
+ console.log();
469
+ process.exit(0);
470
+ }
471
+
472
+ const flag = (n) => argv.includes(n);
473
+ const values = (n) => argv.reduce((acc, a, i) =>
474
+ a === n && argv[i + 1] ? acc.concat(argv[i + 1].split(",")) : acc, []);
475
+
476
+ // Flags that consume the next argument; their values are not the source.
477
+ const VALUE_FLAGS = new Set(["--for", "--skill"]);
478
+ const source = argv.find((a, i) => !a.startsWith("--") && !VALUE_FLAGS.has(argv[i - 1]));
479
+ if (!source) die("no source given");
480
+
481
+ const targets = values("--for").map((s) => s.trim()).filter(Boolean);
482
+ if (!targets.length) die(`no --for <agent> given\n known: ${Object.keys(AGENTS).join(", ")}`);
483
+ for (const t of targets) if (!AGENTS[t]) die(`unknown agent "${t}"\n known: ${Object.keys(AGENTS).join(", ")}`);
484
+
485
+ const isProject = flag("--project");
486
+ const dryRun = flag("--dry-run");
487
+ const verifyOnly = flag("--verify");
488
+ const skipSecurity = flag("--skip-security");
489
+ const strict = flag("--strict");
490
+ const allSkills = flag("--all");
491
+ const verbose = flag("--verbose");
492
+
493
+ // --skill names one, --all takes every one. Asking for both is a contradiction
494
+ // about intent, not something to resolve by precedence.
495
+ if (allSkills && values("--skill").length)
496
+ die("--all and --skill are mutually exclusive: --skill names one, --all takes every one");
497
+
498
+ const src = resolveSource(source);
499
+ const tmp = mkdtempSync(join(tmpdir(), "skill-install-"));
500
+ let failed = 0;
501
+ const skipped = [];
502
+
503
+ try {
504
+ const root = fetchSource(src, tmp);
505
+ const skillPaths = allSkills ? selectAll(root) : [selectSkill(root, values("--skill")[0])];
506
+
507
+ // With --all the per-skill detail becomes a wall nobody reads, and an unread
508
+ // report verifies nothing. Collapse to one line each unless asked otherwise.
509
+ const quiet = allSkills && !verbose;
510
+ if (quiet) console.log(`\n source ${src.label}\n skills ${skillPaths.length} found`);
511
+
512
+ for (const skillPath of skillPaths) {
513
+ const r = runSkill(skillPath, root, quiet);
514
+ failed += r.failed;
515
+ if (r.skipped) skipped.push(r.name);
516
+ }
517
+
518
+ if (allSkills) {
519
+ const n = skillPaths.length;
520
+ console.log(` ${n} skill${n === 1 ? "" : "s"}, ${failed} failed${skipped.length ? `, ${skipped.length} skipped by --strict` : ""}`);
521
+ if (skipped.length) console.log(` skipped ${skipped.join(", ")}`);
522
+ console.log();
523
+ }
441
524
  } finally {
442
525
  rmSync(tmp, { recursive: true, force: true });
443
526
  }