@ankurhalder/codebase-snapshot 2.1.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import * as fs10 from "fs";
5
- import * as path11 from "path";
4
+ import * as fs11 from "fs";
5
+ import * as path12 from "path";
6
6
  import { parseArgs } from "util";
7
7
 
8
8
  // src/diff.ts
@@ -2107,11 +2107,12 @@ var PRESET_NAMES = [
2107
2107
  "java-spring \xB7 dotnet \xB7 php-laravel \xB7 ruby-rails",
2108
2108
  "monorepo \xB7 docs \xB7 infra \xB7 generic"
2109
2109
  ];
2110
+ var CONFIG_HEADER = `// Generated by codebase-snapshot. A plain object on purpose: this file must load
2111
+ // even when the package itself is only run through npx.
2112
+ `;
2110
2113
  function renderConfig(answers) {
2111
2114
  const { artifacts } = answers;
2112
- return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
2113
-
2114
- export default defineConfig({
2115
+ return `${CONFIG_HEADER}export default {
2115
2116
  // \u2500\u2500 Project \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2116
2117
  // Which project shape to use. Detected from the repo when omitted.
2117
2118
  // ${PRESET_NAMES.join("\n // ")}
@@ -2180,7 +2181,7 @@ export default defineConfig({
2180
2181
  // \u2500\u2500 Advanced \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2181
2182
  // afterGenerate: (manifest) => { /* write your own artifact */ }
2182
2183
  hooks: {},
2183
- });
2184
+ };
2184
2185
  `;
2185
2186
  }
2186
2187
  function renderMinimalConfig(answers) {
@@ -2208,16 +2209,12 @@ ${entries},
2208
2209
  },`);
2209
2210
  }
2210
2211
  if (lines.length === 0) {
2211
- return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
2212
-
2213
- export default defineConfig({});
2212
+ return `${CONFIG_HEADER}export default {};
2214
2213
  `;
2215
2214
  }
2216
- return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
2217
-
2218
- export default defineConfig({
2215
+ return `${CONFIG_HEADER}export default {
2219
2216
  ${lines.join("\n")}
2220
- });
2217
+ };
2221
2218
  `;
2222
2219
  }
2223
2220
 
@@ -2481,6 +2478,17 @@ var CONFIG_CANDIDATES = [
2481
2478
  ".snapshotrc.json",
2482
2479
  ".snapshotrc"
2483
2480
  ];
2481
+ function chooseConfigFileName(cwd) {
2482
+ if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
2483
+ const pkgPath = path7.join(cwd, "package.json");
2484
+ if (!fs6.existsSync(pkgPath)) return "snapshot.config.mjs";
2485
+ try {
2486
+ const parsed = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
2487
+ return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
2488
+ } catch {
2489
+ return "snapshot.config.mjs";
2490
+ }
2491
+ }
2484
2492
  var SIZE_CHOICES = [
2485
2493
  { value: "60k", label: "Small", hint: "60 KB \xB7 ~15k tokens \xB7 tight context windows" },
2486
2494
  { value: "120k", label: "Medium", hint: "120 KB \xB7 ~30k tokens \xB7 recommended" },
@@ -2686,7 +2694,7 @@ async function runInit(options) {
2686
2694
  if (!proceed) throw new PromptCancelled();
2687
2695
  }
2688
2696
  const configText = options.minimal ? renderMinimalConfig(answers) : renderConfig(answers);
2689
- const configName = supportsTypeScriptImports() ? "snapshot.config.ts" : "snapshot.config.mjs";
2697
+ const configName = chooseConfigFileName(options.cwd);
2690
2698
  const configPath = path7.join(options.cwd, configName);
2691
2699
  if (options.dryRun) {
2692
2700
  return {
@@ -2949,9 +2957,9 @@ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)
2949
2957
  var jvmProfile = {
2950
2958
  id: "jvm",
2951
2959
  extensions: [".java", ".kt", ".kts", ".scala"],
2952
- summarize(path12, content) {
2960
+ summarize(path13, content) {
2953
2961
  const summary = emptySummary();
2954
- const lower = path12.toLowerCase();
2962
+ const lower = path13.toLowerCase();
2955
2963
  const isJava = lower.endsWith(".java");
2956
2964
  const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
2957
2965
  eachLine(content, (line) => {
@@ -3375,16 +3383,16 @@ function summarizeTypeScriptSource(relativePath, content) {
3375
3383
  // src/ast/profiles/sfc.ts
3376
3384
  var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
3377
3385
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
3378
- function componentName(path12) {
3379
- const base = path12.split("/").pop() ?? path12;
3386
+ function componentName(path13) {
3387
+ const base = path13.split("/").pop() ?? path13;
3380
3388
  return base.replace(/\.[^.]+$/, "");
3381
3389
  }
3382
3390
  var sfcProfile = {
3383
3391
  id: "sfc",
3384
3392
  extensions: [".vue", ".svelte", ".astro"],
3385
- summarize(path12, content) {
3393
+ summarize(path13, content) {
3386
3394
  let script = "";
3387
- if (path12.toLowerCase().endsWith(".astro")) {
3395
+ if (path13.toLowerCase().endsWith(".astro")) {
3388
3396
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
3389
3397
  if (frontmatter) script += `${frontmatter[1]}
3390
3398
  `;
@@ -3393,7 +3401,7 @@ var sfcProfile = {
3393
3401
  script += `${match[1]}
3394
3402
  `;
3395
3403
  }
3396
- const name = componentName(path12);
3404
+ const name = componentName(path13);
3397
3405
  if (!script.trim()) {
3398
3406
  const summary2 = emptySummary();
3399
3407
  if (name) {
@@ -3427,15 +3435,15 @@ for (const profile of LANGUAGE_PROFILES) {
3427
3435
  BY_EXTENSION.set(extension.toLowerCase(), profile);
3428
3436
  }
3429
3437
  }
3430
- function profileForPath(path12) {
3431
- const lower = path12.toLowerCase();
3438
+ function profileForPath(path13) {
3439
+ const lower = path13.toLowerCase();
3432
3440
  const dot = lower.lastIndexOf(".");
3433
3441
  if (dot === -1) return null;
3434
3442
  return BY_EXTENSION.get(lower.slice(dot)) ?? null;
3435
3443
  }
3436
- function summarizeWithProfile(path12, content) {
3437
- const profile = profileForPath(path12);
3438
- return profile ? profile.summarize(path12, content) : null;
3444
+ function summarizeWithProfile(path13, content) {
3445
+ const profile = profileForPath(path13);
3446
+ return profile ? profile.summarize(path13, content) : null;
3439
3447
  }
3440
3448
 
3441
3449
  // src/ast/structured.ts
@@ -4568,7 +4576,7 @@ function validateSnapshot(configInput) {
4568
4576
 
4569
4577
  // src/index.ts
4570
4578
  var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
4571
- var GENERATOR_VERSION = "2.1.1";
4579
+ var GENERATOR_VERSION = "3.0.0";
4572
4580
  var SCHEMA_VERSION = "3.0.0";
4573
4581
  function sha256Hex2(input) {
4574
4582
  return crypto3.createHash("sha256").update(input).digest("hex");
@@ -5141,6 +5149,635 @@ function generateSnapshot(configInput, options) {
5141
5149
  return manifest;
5142
5150
  }
5143
5151
 
5152
+ // src/agent/contract.ts
5153
+ var CONTRACT_VERSION = "1.0.0";
5154
+ var PACKAGE_NAME = "@ankurhalder/codebase-snapshot";
5155
+ var ARTIFACT_SUMMARY = {
5156
+ readme: "README.md describing the snapshot at the output root.",
5157
+ manifest: "manifest.json: machine-readable index of every file, part, hash and config.",
5158
+ parts: "parts/*.md: the packed, budget-limited markdown parts that carry the code.",
5159
+ navigation: "reports/navigation.md: links between parts and layers.",
5160
+ projectTree: "reports/project-tree.md: the directory tree of included files.",
5161
+ audit: "reports/audit.md: secret findings, large files, parse diagnostics, git state.",
5162
+ monolith: "full-codebase.json and full-codebase.md: the whole repository in one file (off by default)."
5163
+ };
5164
+ var ARTIFACT_DEFAULTS = {
5165
+ readme: true,
5166
+ manifest: true,
5167
+ parts: true,
5168
+ navigation: true,
5169
+ projectTree: true,
5170
+ audit: true,
5171
+ monolith: false
5172
+ };
5173
+ function renderContractText(contract) {
5174
+ const lines = [];
5175
+ lines.push(`${contract.package} v${contract.version}`);
5176
+ lines.push(contract.summary);
5177
+ lines.push("");
5178
+ lines.push(`Node : ${contract.requires.node}`);
5179
+ lines.push(`Schema : ${contract.schemaVersion} (contract ${contract.contractVersion})`);
5180
+ lines.push(`One-shot : ${contract.invocation.oneShot} <command>`);
5181
+ lines.push(`Installed : ${contract.invocation.installed} <command>`);
5182
+ lines.push("");
5183
+ lines.push("Workflow:");
5184
+ for (const step of contract.workflow) {
5185
+ lines.push(` ${step.step}. ${step.id}`);
5186
+ lines.push(` run : ${step.run}`);
5187
+ lines.push(` expect : ${step.expect}`);
5188
+ if (step.notes) lines.push(` note : ${step.notes}`);
5189
+ }
5190
+ lines.push("");
5191
+ lines.push("Commands:");
5192
+ for (const command of contract.commands) {
5193
+ const args = command.positionals.length > 0 ? ` ${command.positionals.join(" ")}` : "";
5194
+ lines.push(` ${(command.name + args).padEnd(26)} ${command.summary}`);
5195
+ }
5196
+ lines.push("");
5197
+ lines.push("Exit codes:");
5198
+ for (const [code, meaning] of Object.entries(contract.exitCodes)) {
5199
+ lines.push(` ${code} ${meaning}`);
5200
+ }
5201
+ lines.push("");
5202
+ lines.push(`Presets (${contract.presets.length}): ${contract.presets.map((p) => p.name).join(", ")}`);
5203
+ lines.push("");
5204
+ lines.push("Artifacts:");
5205
+ for (const artifact of contract.artifacts) {
5206
+ lines.push(` ${artifact.name.padEnd(12)} ${artifact.default ? "on " : "off"} ${artifact.summary}`);
5207
+ }
5208
+ lines.push("");
5209
+ lines.push("Options:");
5210
+ for (const option of contract.options) {
5211
+ const fallback = option.default === void 0 ? "" : ` [${JSON.stringify(option.default)}]`;
5212
+ lines.push(` ${option.flag.padEnd(24)} ${option.appliesTo.padEnd(32)}${fallback}`);
5213
+ lines.push(` ${"".padEnd(24)} ${option.summary}`);
5214
+ }
5215
+ lines.push("");
5216
+ lines.push("Safety:");
5217
+ for (const note of contract.safety) lines.push(` - ${note}`);
5218
+ lines.push("");
5219
+ lines.push("Troubleshooting:");
5220
+ for (const item of contract.troubleshooting) {
5221
+ lines.push(` ${item.symptom}`);
5222
+ lines.push(` cause: ${item.cause}`);
5223
+ lines.push(` fix : ${item.fix}`);
5224
+ }
5225
+ lines.push("");
5226
+ lines.push("This is the human rendering. Agents should use: describe --format json");
5227
+ return lines.join("\n");
5228
+ }
5229
+ function buildAgentContract() {
5230
+ return {
5231
+ generator: GENERATOR_NAME,
5232
+ package: PACKAGE_NAME,
5233
+ version: GENERATOR_VERSION,
5234
+ schemaVersion: SCHEMA_VERSION,
5235
+ contractVersion: CONTRACT_VERSION,
5236
+ summary: "Turns a repository into an LLM-ready snapshot: code packed into size-budgeted markdown parts, plus a manifest, a project tree, navigation and a secret audit. Auto-detects the project shape (framework, language, monorepo, docs, infra).",
5237
+ requires: { node: ">=18.0.0", dependencies: ["typescript"] },
5238
+ invocation: {
5239
+ oneShot: `npx ${PACKAGE_NAME}@latest`,
5240
+ installed: "npx codebase-snapshot",
5241
+ bins: ["codebase-snapshot"],
5242
+ notes: [
5243
+ `The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
5244
+ "Pin @latest (or @2) when you need the exact same version on every machine.",
5245
+ "This package exposes exactly one executable on purpose: npm cannot infer which one to run for a scoped package that ships several.",
5246
+ "Human progress goes to stderr. With --format json the payload on stdout is pure JSON."
5247
+ ]
5248
+ },
5249
+ workflow: [
5250
+ {
5251
+ step: 1,
5252
+ id: "verify",
5253
+ run: `npx ${PACKAGE_NAME}@latest --version`,
5254
+ expect: "prints v2.x.x on stdout and exits 0",
5255
+ notes: "If it prints nothing, clear the npm cache and retry, or use `npx -p <pkg> codebase-snapshot`."
5256
+ },
5257
+ {
5258
+ step: 2,
5259
+ id: "understand",
5260
+ run: `npx ${PACKAGE_NAME}@latest describe --format json`,
5261
+ expect: "the full contract as JSON: commands, options, presets, exit codes",
5262
+ notes: "Read this before acting if you have no prior knowledge of the tool."
5263
+ },
5264
+ {
5265
+ step: 3,
5266
+ id: "configure",
5267
+ run: `npx ${PACKAGE_NAME}@latest init --yes`,
5268
+ expect: "writes snapshot.config.ts listing every setting at its current value",
5269
+ notes: "Non-interactive. The detected preset is chosen automatically; override with --preset <name>. Skip this step to run on built-in defaults."
5270
+ },
5271
+ {
5272
+ step: 4,
5273
+ id: "generate",
5274
+ run: `npx ${PACKAGE_NAME}@latest generate`,
5275
+ expect: "exit 0, and ./snapshot/ written with parts, reports and manifest.json"
5276
+ },
5277
+ {
5278
+ step: 5,
5279
+ id: "verify-output",
5280
+ run: `npx ${PACKAGE_NAME}@latest validate`,
5281
+ expect: "exit 0 means the snapshot matches the files on disk",
5282
+ notes: "Exit 1 means drift or a damaged snapshot; re-run generate."
5283
+ },
5284
+ {
5285
+ step: 6,
5286
+ id: "consume",
5287
+ run: "read ./snapshot/parts/*.md, then ./snapshot/manifest.json, then ./snapshot/reports/audit.md",
5288
+ expect: "the code, the index of what was included, and the risk findings",
5289
+ notes: "manifest.json.files[] lists every included file with layer, kind, language, part and summary. manifest.skipped[] explains every file that was left out and why."
5290
+ }
5291
+ ],
5292
+ commands: [
5293
+ {
5294
+ name: "generate",
5295
+ aliases: ["default"],
5296
+ summary: "Write the snapshot. This is the command you normally want.",
5297
+ positionals: [],
5298
+ exits: [0, 1]
5299
+ },
5300
+ {
5301
+ name: "validate",
5302
+ aliases: ["check"],
5303
+ summary: "Compare the snapshot on disk with the repository; exit 1 on drift.",
5304
+ positionals: [],
5305
+ exits: [0, 1]
5306
+ },
5307
+ {
5308
+ name: "report",
5309
+ aliases: [],
5310
+ summary: "Print snapshot health, layer metrics and the secret audit.",
5311
+ positionals: [],
5312
+ exits: [0, 1]
5313
+ },
5314
+ {
5315
+ name: "init",
5316
+ aliases: [],
5317
+ summary: "Write a commented snapshot.config.ts (interactive, or --yes for CI).",
5318
+ positionals: [],
5319
+ exits: [0, 1, 2]
5320
+ },
5321
+ {
5322
+ name: "explain",
5323
+ aliases: [],
5324
+ summary: "Explain how one path would be treated: layer, kind, part, skip reason.",
5325
+ positionals: ["<path>"],
5326
+ exits: [0, 1]
5327
+ },
5328
+ {
5329
+ name: "diff",
5330
+ aliases: [],
5331
+ summary: "Compare two snapshots (directories or manifest.json files).",
5332
+ positionals: ["<before>", "<after>"],
5333
+ exits: [0, 1]
5334
+ },
5335
+ {
5336
+ name: "describe",
5337
+ aliases: [],
5338
+ summary: "Print this contract. --format json for machines, text for humans.",
5339
+ positionals: [],
5340
+ exits: [0]
5341
+ },
5342
+ {
5343
+ name: "skill",
5344
+ aliases: [],
5345
+ summary: "Print the agent skill, or --install it into the repository (AGENTS.md, .claude/skills, .cursor/rules).",
5346
+ positionals: [],
5347
+ exits: [0, 1, 2]
5348
+ }
5349
+ ],
5350
+ exitCodes: {
5351
+ "0": "success (and validate: no drift)",
5352
+ "1": "drift detected, or generation refused/failed (nothing was written)",
5353
+ "2": "usage error: unknown flag/command, bad value, wrong argument count"
5354
+ },
5355
+ options: [
5356
+ {
5357
+ flag: "--out-dir",
5358
+ type: "string",
5359
+ default: "snapshot",
5360
+ appliesTo: "generate/validate/report/explain",
5361
+ summary: "Where the snapshot is written. Must not be the repo root or a directory the tool did not create."
5362
+ },
5363
+ {
5364
+ flag: "--max-part-bytes",
5365
+ type: "string",
5366
+ default: 307200,
5367
+ appliesTo: "generate/validate/report",
5368
+ summary: 'Byte budget per markdown part. Accepts "120k" / "1.5mb". Minimum 4096.'
5369
+ },
5370
+ {
5371
+ flag: "--large-file-bytes",
5372
+ type: "string",
5373
+ default: 204800,
5374
+ appliesTo: "generate/validate/report",
5375
+ summary: "Above this size a file gets a head/tail preview instead of the full body."
5376
+ },
5377
+ {
5378
+ flag: "--max-monolith-bytes",
5379
+ type: "string",
5380
+ default: "unset",
5381
+ appliesTo: "generate",
5382
+ summary: "Skip the monolith artifacts when they would exceed this size."
5383
+ },
5384
+ {
5385
+ flag: "--include-monolith",
5386
+ type: "boolean",
5387
+ default: false,
5388
+ appliesTo: "generate",
5389
+ summary: "Also write full-codebase.json and full-codebase.md."
5390
+ },
5391
+ {
5392
+ flag: "--include-sensitive-files",
5393
+ type: "boolean",
5394
+ default: false,
5395
+ appliesTo: "generate/validate/report",
5396
+ summary: "Also emit .env / *.pem / .npmrc style files. Off by default; they are recorded as skipped."
5397
+ },
5398
+ {
5399
+ flag: "--skip-artifacts",
5400
+ type: "string",
5401
+ default: "unset",
5402
+ appliesTo: "generate",
5403
+ summary: "Comma-separated artifacts to omit (readme, manifest, parts, navigation, projectTree, audit, monolith)."
5404
+ },
5405
+ {
5406
+ flag: "--only-artifacts",
5407
+ type: "string",
5408
+ default: "unset",
5409
+ appliesTo: "generate",
5410
+ summary: "Write only these artifacts. Cannot be combined with --skip-artifacts."
5411
+ },
5412
+ {
5413
+ flag: "--format",
5414
+ type: "string",
5415
+ default: "text",
5416
+ appliesTo: "all commands",
5417
+ summary: 'Output format: "text" or "json". JSON goes to stdout, progress to stderr.'
5418
+ },
5419
+ {
5420
+ flag: "--cwd",
5421
+ type: "string",
5422
+ default: "process.cwd()",
5423
+ appliesTo: "all commands",
5424
+ summary: "Repository to operate on. Must be an existing directory."
5425
+ },
5426
+ {
5427
+ flag: "--dry-run",
5428
+ type: "boolean",
5429
+ default: false,
5430
+ appliesTo: "generate",
5431
+ summary: "Compute everything and print the result without writing."
5432
+ },
5433
+ {
5434
+ flag: "--watch",
5435
+ type: "boolean",
5436
+ default: false,
5437
+ appliesTo: "generate",
5438
+ summary: "Regenerate on every change until interrupted."
5439
+ },
5440
+ {
5441
+ flag: "--preset",
5442
+ type: "string",
5443
+ default: "detected",
5444
+ appliesTo: "init",
5445
+ summary: "Force a project preset instead of detecting one. Unknown names are a usage error."
5446
+ },
5447
+ {
5448
+ flag: "--include",
5449
+ type: "string",
5450
+ default: "unset",
5451
+ appliesTo: "init",
5452
+ summary: "Comma-separated allow-list of globs to record in the generated config."
5453
+ },
5454
+ {
5455
+ flag: "--exclude",
5456
+ type: "string",
5457
+ default: "unset",
5458
+ appliesTo: "init",
5459
+ summary: "Comma-separated globs to record in the generated config."
5460
+ },
5461
+ {
5462
+ flag: "--yes",
5463
+ type: "boolean",
5464
+ default: false,
5465
+ appliesTo: "init",
5466
+ summary: "Accept every default without prompting. Required in a non-TTY."
5467
+ },
5468
+ {
5469
+ flag: "--minimal",
5470
+ type: "boolean",
5471
+ default: false,
5472
+ appliesTo: "init",
5473
+ summary: "Write only the settings that differ from the defaults."
5474
+ },
5475
+ {
5476
+ flag: "--force",
5477
+ type: "boolean",
5478
+ default: false,
5479
+ appliesTo: "init",
5480
+ summary: "Overwrite an existing config file."
5481
+ },
5482
+ {
5483
+ flag: "--install",
5484
+ type: "boolean",
5485
+ default: false,
5486
+ appliesTo: "skill",
5487
+ summary: "Write the skill into the repository instead of printing it."
5488
+ },
5489
+ {
5490
+ flag: "--target",
5491
+ type: "string",
5492
+ default: "all",
5493
+ appliesTo: "skill",
5494
+ summary: 'Where to install: "claude", "agents", "cursor" or "all".'
5495
+ }
5496
+ ],
5497
+ presets: PRESETS.map((preset) => ({
5498
+ name: preset.name,
5499
+ priority: preset.priority,
5500
+ recommendedBudget: preset.recommendedBudget ?? "300k",
5501
+ excludes: preset.exclude ? [...preset.exclude] : []
5502
+ })),
5503
+ artifacts: ARTIFACT_NAMES.map((name) => ({
5504
+ name,
5505
+ default: ARTIFACT_DEFAULTS[name] ?? true,
5506
+ summary: ARTIFACT_SUMMARY[name] ?? name
5507
+ })),
5508
+ config: {
5509
+ fileNames: [
5510
+ "snapshot.config.ts",
5511
+ "snapshot.config.js",
5512
+ "snapshot.config.mjs",
5513
+ "snapshot.config.cjs",
5514
+ "snapshot.config.json",
5515
+ ".snapshotrc",
5516
+ ".snapshotrc.json",
5517
+ 'package.json \u2192 "snapshot" key'
5518
+ ],
5519
+ settings: [
5520
+ { key: "outDir", type: "string", default: "snapshot", summary: "Snapshot output directory." },
5521
+ { key: "tmpDir", type: "string", default: "snapshot.tmp", summary: "Scratch directory used for the atomic swap." },
5522
+ { key: "branch", type: "string", default: "main", summary: "Branch recorded when git cannot report one." },
5523
+ { key: "preset", type: "string", default: "detected", summary: "Force a preset. Unknown names are an error." },
5524
+ { key: "maxPartBytes", type: "number | string", default: 307200, summary: "Per-part byte budget." },
5525
+ { key: "largeFileBytes", type: "number | string", default: 204800, summary: "Preview threshold for large files." },
5526
+ { key: "previewHeadLines", type: "number", default: 80, summary: "Lines kept from the head of a large file." },
5527
+ { key: "previewTailLines", type: "number", default: 40, summary: "Lines kept from the tail of a large file." },
5528
+ { key: "includeMonolith", type: "boolean", default: false, summary: "Also write the single-file monolith." },
5529
+ { key: "maxMonolithBytes", type: "number | string | null", default: null, summary: "Skip the monolith above this size." },
5530
+ { key: "includeSensitiveFiles", type: "boolean", default: false, summary: "Emit .env / *.pem / credential files." },
5531
+ { key: "include", type: "string[]", default: "[]", summary: "Allow-list of globs; empty means everything." },
5532
+ { key: "excludePatterns", type: "string[]", default: "[]", summary: "Extra exclude globs." },
5533
+ { key: "artifacts", type: "object", default: "all on except monolith", summary: "Per-artifact opt-out." },
5534
+ { key: "budgets", type: "object", default: "{ default, byLayer }", summary: "Per-layer byte budgets." },
5535
+ { key: "layersMode", type: '"merge" | "replace"', default: "merge", summary: "Whether custom layers extend or replace the preset's." },
5536
+ { key: "layers", type: "array", default: "preset layers", summary: "Custom layer rules (glob or RegExp)." },
5537
+ { key: "kinds", type: "array", default: "preset kinds", summary: "Custom kind rules." },
5538
+ { key: "secrets", type: "object", default: "{}", summary: "Custom secret patterns, or disable the defaults." },
5539
+ { key: "hooks", type: "object", default: "{}", summary: "afterGenerate(manifest) runs after a successful generate." }
5540
+ ]
5541
+ },
5542
+ output: {
5543
+ layout: [
5544
+ "<outDir>/README.md",
5545
+ "<outDir>/manifest.json",
5546
+ "<outDir>/parts/part-NNN.md",
5547
+ "<outDir>/reports/project-tree.md",
5548
+ "<outDir>/reports/navigation.md",
5549
+ "<outDir>/reports/audit.md",
5550
+ "<outDir>/.codebase-snapshot (ownership marker, always written)"
5551
+ ],
5552
+ notes: [
5553
+ "Parts are ordered and budget-limited; a single file larger than the budget becomes a head/tail preview.",
5554
+ "manifest.json.files[] carries relativePath, layer, kind, language, part, inclusionMode, hash and a structural summary.",
5555
+ "manifest.skipped[] carries the path and the exact reason it was excluded.",
5556
+ `The output directory is replaced atomically; ${MIN_MAX_PART_BYTES} bytes is the smallest legal budget.`
5557
+ ]
5558
+ },
5559
+ safety: [
5560
+ "Sensitive files (.env, *.pem, .npmrc, .ssh/, .tfstate) are excluded by default and listed in manifest.skipped[].",
5561
+ "Secret-shaped strings in ordinary source files are redacted in the emitted parts.",
5562
+ "The tool refuses to replace an outDir that it did not create, so a mistyped outDir cannot delete source code.",
5563
+ "Nothing is ever written above the repository root; --out-dir may not be the root or an ancestor."
5564
+ ],
5565
+ troubleshooting: [
5566
+ {
5567
+ symptom: "Command prints nothing and exits 0",
5568
+ cause: "The executable was not found: an offline or partially populated npm cache, or an npm proxy serving an incomplete manifest.",
5569
+ fix: `Clear the npx cache (npm cache clean --force), then run npx ${PACKAGE_NAME}@latest <command>.`
5570
+ },
5571
+ {
5572
+ symptom: "Exit 1 with 'Refusing to replace outDir'",
5573
+ cause: "The outDir exists and contains files this tool did not write.",
5574
+ fix: "Choose a new/empty --out-dir, or delete the existing directory yourself."
5575
+ },
5576
+ {
5577
+ symptom: "Exit 1 with 'The config file could not be read'",
5578
+ cause: "snapshot.config.* exists but does not parse.",
5579
+ fix: "Fix the syntax, or delete the config to use the defaults."
5580
+ },
5581
+ {
5582
+ symptom: "Exit 1 from validate",
5583
+ cause: "The snapshot no longer matches the files on disk.",
5584
+ fix: "Re-run generate."
5585
+ },
5586
+ {
5587
+ symptom: "A directory you expected is missing from the snapshot",
5588
+ cause: "It is excluded by the preset, by default ignores, or by your own globs.",
5589
+ fix: "Run `explain <path>`; manifest.skipped[] also records the reason for every file."
5590
+ }
5591
+ ]
5592
+ };
5593
+ }
5594
+
5595
+ // src/agent/install-skill.ts
5596
+ import * as fs10 from "fs";
5597
+ import * as path11 from "path";
5598
+
5599
+ // src/agent/skill.ts
5600
+ var PACKAGE_NAME2 = "@ankurhalder/codebase-snapshot";
5601
+ var SKILL_NAME = "codebase-snapshot";
5602
+ var AGENTS_MARKER_START = "<!-- codebase-snapshot:skill:start -->";
5603
+ var AGENTS_MARKER_END = "<!-- codebase-snapshot:skill:end -->";
5604
+ function renderSkill() {
5605
+ return `---
5606
+ name: ${SKILL_NAME}
5607
+ description: Create an LLM-ready snapshot of a repository (size-budgeted markdown parts, a JSON manifest, project tree, navigation and a secret audit) when a whole codebase has to be read, summarized or handed to a model that cannot clone it. Use when asked to snapshot a codebase, prepare repository context, or analyze a project's structure.
5608
+ version: ${GENERATOR_VERSION}
5609
+ ---
5610
+
5611
+ # codebase-snapshot
5612
+
5613
+ Turns a repository into an LLM-ready snapshot under \`./snapshot/\`: code packed into
5614
+ size-budgeted markdown parts, plus a manifest that indexes every file, a project tree, a
5615
+ navigation index and a secret audit.
5616
+
5617
+ Use it when you need to understand a whole repository, or produce repository context for
5618
+ another model, and reading files one by one would not fit in context.
5619
+
5620
+ ## Before you start
5621
+
5622
+ This tool is a Node CLI. It needs **Node.js 18 or newer**. You do not need to install it
5623
+ globally, and you do not need to clone anything.
5624
+
5625
+ **Pin the tag** \u2014 \`npx ${PACKAGE_NAME2}@latest <command>\` \u2014 when you need the same version on
5626
+ every machine. The bare package name works too: this package ships exactly one executable,
5627
+ which is what lets npm pick it. If a command exits 0 printing nothing, the executable was
5628
+ not resolved \u2014 clear the npm cache and retry; do not assume the repository is broken.
5629
+
5630
+ ## Step by step
5631
+
5632
+ 1. **Verify the tool.** Run \`npx ${PACKAGE_NAME2}@latest --version\`. Expect \`v${GENERATOR_VERSION}\` or
5633
+ newer on stdout. Empty output means you hit the bare-spec problem above.
5634
+
5635
+ 2. **Read the contract, not the prose.** Run
5636
+ \`npx ${PACKAGE_NAME2}@latest describe --format json\`. It returns the full machine
5637
+ contract: every command, every option with its default, the exit-code meanings, the
5638
+ 25 project presets, the artifact list, the config settings and a troubleshooting
5639
+ table. Read it before you make decisions; do not guess option names.
5640
+
5641
+ 3. **Look before you leap (optional but cheap).** To see how a specific file or directory
5642
+ would be treated, run \`npx ${PACKAGE_NAME2}@latest explain <path>\`. It reports the
5643
+ layer, kind, language, target part, and the exact reason a path would be skipped.
5644
+ \`--format json\` if you want to parse it.
5645
+
5646
+ 4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
5647
+ the project shape, then writes \`snapshot.config.ts\` listing **every** setting at its
5648
+ current value, commented. Pass \`--preset <name>\` only if detection is wrong; the
5649
+ preset names are in the contract. This step is optional: without a config the tool
5650
+ runs on the same defaults.
5651
+
5652
+ 5. **Generate.** Run \`npx ${PACKAGE_NAME2}@latest generate\`. Expect exit 0 and a
5653
+ \`./snapshot/\` directory. If you need a different location use \`--out-dir <path>\`; if
5654
+ you need smaller or larger parts use \`--max-part-bytes 120k\`.
5655
+
5656
+ 6. **Verify.** Run \`npx ${PACKAGE_NAME2}@latest validate\`. Exit 0 means the snapshot
5657
+ matches the files on disk. Exit 1 means drift \u2014 re-run step 5.
5658
+
5659
+ 7. **Read the output in this order:**
5660
+ - \`snapshot/parts/part-*.md\` \u2014 the code itself, each file tagged with its layer, kind,
5661
+ language and a structural summary (imports, exports, classes, functions).
5662
+ - \`snapshot/manifest.json\` \u2014 the index. \`files[]\` has \`relativePath\`, \`layer\`,
5663
+ \`kind\`, \`language\`, \`part\`, \`inclusionMode\`, \`hashSha256\` and \`summary\`.
5664
+ \`skipped[]\` records every excluded file **and why**. Prefer querying this over
5665
+ re-reading the parts.
5666
+ - \`snapshot/reports/audit.md\` \u2014 secrets found, large files, parse diagnostics, git
5667
+ state. Read this before you quote code back to a user.
5668
+ - \`snapshot/reports/project-tree.md\` and \`navigation.md\` \u2014 structure and cross-links.
5669
+
5670
+ ## Project types
5671
+
5672
+ Detection is automatic and covers Next.js, Nuxt, SvelteKit, Remix, Astro, Angular, Solid,
5673
+ Vue+React with Vite, Nest, Express/Fastify/Koa, any Node library, monorepos, Django,
5674
+ FastAPI/Flask, plain Python, Go, Rust, Java/Spring, .NET, PHP/Laravel, Ruby/Rails,
5675
+ Terraform/Docker infra, docs-only repositories, and a generic fallback. It sets sensible
5676
+ layers, kinds, ecosystem ignore patterns and a recommended part budget. For non-TypeScript
5677
+ languages it extracts imports, exports, classes and functions structurally; Vue, Svelte and
5678
+ Astro script blocks go through the real TypeScript compiler. You do not need to configure
5679
+ any of this to get a good first snapshot.
5680
+
5681
+ ## Things that will surprise you
5682
+
5683
+ - **Sensitive files are excluded by default** (\`.env\`, \`*.pem\`, \`.npmrc\`, \`.ssh/\`,
5684
+ \`.tfstate\`) and are listed in \`manifest.skipped[]\`. Add \`--include-sensitive-files\` only
5685
+ if the user asks for them explicitly.
5686
+ - **Secret-shaped strings inside normal source files are redacted** in the emitted parts.
5687
+ If the user needs the raw value, say so rather than working around it.
5688
+ - **The output directory is owned by the tool.** It is replaced atomically, and the tool
5689
+ refuses to replace a directory it did not create. Never point \`--out-dir\` at source
5690
+ code; if you see \`Refusing to replace outDir\`, choose a new directory rather than
5691
+ deleting anything.
5692
+ - **Exit 2 is a usage error**, not a failure of the repository. Re-read the contract
5693
+ before retrying.
5694
+ - **\`--format json\` keeps stdout pure.** Progress goes to stderr, so \`stdout\` is safe to
5695
+ pipe into a JSON parser.
5696
+
5697
+ ## Making this available to other agents
5698
+
5699
+ Run \`npx ${PACKAGE_NAME2}@latest skill --install\`. It writes this skill into the
5700
+ repository in the places agents look for it by default: an \`AGENTS.md\` section,
5701
+ \`.claude/skills/${SKILL_NAME}/SKILL.md\`, and \`.cursor/rules/${SKILL_NAME}.mdc\`. The command
5702
+ is idempotent and only replaces its own marked section.
5703
+ `;
5704
+ }
5705
+
5706
+ // src/agent/install-skill.ts
5707
+ var SKILL_TARGETS = ["claude", "agents", "cursor", "all"];
5708
+ function targetPath(cwd, target) {
5709
+ switch (target) {
5710
+ case "claude":
5711
+ return path11.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
5712
+ case "cursor":
5713
+ return path11.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
5714
+ case "agents":
5715
+ return path11.join(cwd, "AGENTS.md");
5716
+ }
5717
+ }
5718
+ function writeIfChanged(target, filePath, content) {
5719
+ const existing = fs10.existsSync(filePath) ? fs10.readFileSync(filePath, "utf8") : null;
5720
+ if (existing === content) return { target, path: filePath, action: "unchanged" };
5721
+ fs10.mkdirSync(path11.dirname(filePath), { recursive: true });
5722
+ fs10.writeFileSync(filePath, content, "utf8");
5723
+ return { target, path: filePath, action: existing === null ? "created" : "updated" };
5724
+ }
5725
+ function agentsBlock(skill) {
5726
+ return [
5727
+ AGENTS_MARKER_START,
5728
+ "",
5729
+ `## ${SKILL_NAME} (${"@ankurhalder/codebase-snapshot"})`,
5730
+ "",
5731
+ // Drop the YAML frontmatter: inside AGENTS.md it is noise, and the heading
5732
+ // above already names the skill.
5733
+ skill.replace(/^---[\s\S]*?---\r?\n/, "").trim(),
5734
+ "",
5735
+ AGENTS_MARKER_END,
5736
+ ""
5737
+ ].join("\n");
5738
+ }
5739
+ function mergeAgentsFile(cwd, skill) {
5740
+ const filePath = targetPath(cwd, "agents");
5741
+ const block = agentsBlock(skill);
5742
+ const existing = fs10.existsSync(filePath) ? fs10.readFileSync(filePath, "utf8") : null;
5743
+ let next;
5744
+ if (existing === null) {
5745
+ next = `# AGENTS.md
5746
+
5747
+ ${block}`;
5748
+ } else if (existing.includes(AGENTS_MARKER_START) && existing.includes(AGENTS_MARKER_END)) {
5749
+ const start = existing.indexOf(AGENTS_MARKER_START);
5750
+ const end = existing.indexOf(AGENTS_MARKER_END) + AGENTS_MARKER_END.length;
5751
+ next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(end)}`;
5752
+ } else {
5753
+ next = `${existing.trimEnd()}
5754
+
5755
+ ${block}`;
5756
+ }
5757
+ const action = existing === null ? "created" : existing === next ? "unchanged" : "updated";
5758
+ if (action !== "unchanged") {
5759
+ fs10.mkdirSync(path11.dirname(filePath), { recursive: true });
5760
+ fs10.writeFileSync(filePath, next, "utf8");
5761
+ }
5762
+ return { target: "agents", path: filePath, action };
5763
+ }
5764
+ function installSkill(cwd, target = "all") {
5765
+ const skill = renderSkill();
5766
+ const written = [];
5767
+ const wanted = target === "all" ? ["agents", "claude", "cursor"] : [target];
5768
+ for (const item of wanted) {
5769
+ if (item === "agents") {
5770
+ written.push(mergeAgentsFile(cwd, skill));
5771
+ continue;
5772
+ }
5773
+ written.push(writeIfChanged(item, targetPath(cwd, item), skill));
5774
+ }
5775
+ return { cwd, written };
5776
+ }
5777
+ function isSkillTarget(value) {
5778
+ return SKILL_TARGETS.includes(value);
5779
+ }
5780
+
5144
5781
  // src/cli.ts
5145
5782
  var VERSION = GENERATOR_VERSION;
5146
5783
  var COMMAND_ALIASES = /* @__PURE__ */ new Map([
@@ -5150,7 +5787,9 @@ var COMMAND_ALIASES = /* @__PURE__ */ new Map([
5150
5787
  ["report", "report"],
5151
5788
  ["init", "init"],
5152
5789
  ["explain", "explain"],
5153
- ["diff", "diff"]
5790
+ ["diff", "diff"],
5791
+ ["describe", "describe"],
5792
+ ["skill", "skill"]
5154
5793
  ]);
5155
5794
  var COMMAND_ARITY = {
5156
5795
  generate: 0,
@@ -5158,7 +5797,9 @@ var COMMAND_ARITY = {
5158
5797
  report: 0,
5159
5798
  init: 0,
5160
5799
  explain: 1,
5161
- diff: 2
5800
+ diff: 2,
5801
+ describe: 0,
5802
+ skill: 0
5162
5803
  };
5163
5804
  var CLI_OPTIONS = {
5164
5805
  cwd: { type: "string" },
@@ -5180,6 +5821,8 @@ var CLI_OPTIONS = {
5180
5821
  yes: { type: "boolean" },
5181
5822
  minimal: { type: "boolean" },
5182
5823
  force: { type: "boolean" },
5824
+ install: { type: "boolean" },
5825
+ target: { type: "string" },
5183
5826
  help: { type: "boolean", short: "h" },
5184
5827
  version: { type: "boolean", short: "v" }
5185
5828
  };
@@ -5191,7 +5834,6 @@ Production-grade LLM codebase snapshot generator, auditor, and AST partitioner
5191
5834
 
5192
5835
  Usage:
5193
5836
  codebase-snapshot [command] [options]
5194
- snap [command] [options]
5195
5837
 
5196
5838
  Commands:
5197
5839
  generate (default) Generate a complete partitioned codebase snapshot
@@ -5200,6 +5842,8 @@ Commands:
5200
5842
  report Print codebase health, layer metrics, and secret audit
5201
5843
  explain <path> Show how one path would be treated: layer, kind, part, skips
5202
5844
  diff <a> <b> Compare two snapshots (directories or manifest.json files)
5845
+ describe Print the machine-readable contract (--format json for agents)
5846
+ skill Print the bundled agent skill; --install writes it into the repo
5203
5847
 
5204
5848
  Options:
5205
5849
  --cwd <path> Target repository directory (default: current directory)
@@ -5225,11 +5869,17 @@ Options:
5225
5869
  --minimal Write only the settings that differ from defaults
5226
5870
  --yes Accept every default without prompting
5227
5871
  --force Overwrite an existing config
5872
+
5873
+ skill only:
5874
+ --install Write the skill into the repository instead of printing
5875
+ --target <name> claude | agents | cursor | all (default: all)
5876
+
5228
5877
  -h, --help Display this help message
5229
5878
  -v, --version Display version
5230
5879
 
5231
5880
  All options also accept the --option=value form.
5232
5881
  Human-readable progress is written to stderr; payloads (--format json) go to stdout.
5882
+ Agents with no prior knowledge of this tool should start with: describe --format json
5233
5883
  `.trim();
5234
5884
  function parseByteOption(raw, optionName, minimum) {
5235
5885
  if (raw === void 0) return void 0;
@@ -5288,10 +5938,10 @@ function parseCliArgs(argv) {
5288
5938
  }
5289
5939
  if (values.cwd !== void 0) {
5290
5940
  const target = String(values.cwd);
5291
- if (!fs10.existsSync(target)) {
5941
+ if (!fs11.existsSync(target)) {
5292
5942
  throw new UsageError(`--cwd directory does not exist: ${target}`);
5293
5943
  }
5294
- if (!fs10.statSync(target).isDirectory()) {
5944
+ if (!fs11.statSync(target).isDirectory()) {
5295
5945
  throw new UsageError(`--cwd is not a directory: ${target}`);
5296
5946
  }
5297
5947
  }
@@ -5370,7 +6020,7 @@ function parseCliArgs(argv) {
5370
6020
  if (values.force === true) initOnly.push("--force");
5371
6021
  if (initOnly.length > 0) {
5372
6022
  throw new UsageError(
5373
- `${initOnly.join(", ")} ${initOnly.length === 1 ? "is" : "are"} only valid with \`init\`. To scope \`${command}\`, set include/excludePatterns/preset in snapshot.config.ts (or run \`snap init\` to generate one).`
6023
+ `${initOnly.join(", ")} ${initOnly.length === 1 ? "is" : "are"} only valid with \`init\`. To scope \`${command}\`, set include/excludePatterns/preset in snapshot.config.ts (or run \`codebase-snapshot init\` to generate one).`
5374
6024
  );
5375
6025
  }
5376
6026
  }
@@ -5378,6 +6028,14 @@ function parseCliArgs(argv) {
5378
6028
  const requested = String(values.preset);
5379
6029
  if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
5380
6030
  }
6031
+ if (command !== "skill" && (values.install !== void 0 || values.target !== void 0)) {
6032
+ throw new UsageError("--install and --target are only valid with `skill`.");
6033
+ }
6034
+ if (values.target !== void 0 && !isSkillTarget(String(values.target))) {
6035
+ throw new UsageError(
6036
+ `--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
6037
+ );
6038
+ }
5381
6039
  return {
5382
6040
  command,
5383
6041
  args,
@@ -5391,7 +6049,9 @@ function parseCliArgs(argv) {
5391
6049
  yes: values.yes === true,
5392
6050
  minimal: values.minimal === true,
5393
6051
  force: values.force === true,
5394
- cwd: values.cwd === void 0 ? process.cwd() : path11.resolve(String(values.cwd)),
6052
+ install: values.install === true,
6053
+ target: values.target === void 0 ? void 0 : String(values.target),
6054
+ cwd: values.cwd === void 0 ? process.cwd() : path12.resolve(String(values.cwd)),
5395
6055
  outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
5396
6056
  includeMonolith: values["include-monolith"] === true ? true : void 0,
5397
6057
  includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
@@ -5473,7 +6133,7 @@ async function runInitCommand(invocation) {
5473
6133
  info(result.configText);
5474
6134
  return 0;
5475
6135
  }
5476
- info(`\u2705 Created ${path11.basename(result.configPath)} (preset: ${result.preset})`);
6136
+ info(`\u2705 Created ${path12.basename(result.configPath)} (preset: ${result.preset})`);
5477
6137
  if (result.scripts.length > 0) info(` Added npm scripts: ${result.scripts.join(", ")}`);
5478
6138
  if (result.gitignoreEntries.length > 0) {
5479
6139
  info(` Added to .gitignore: ${result.gitignoreEntries.join(", ")}`);
@@ -5481,6 +6141,9 @@ async function runInitCommand(invocation) {
5481
6141
  for (const note of result.notes) info(` \u26A0 ${note}`);
5482
6142
  info("");
5483
6143
  info(" Next: npm run snapshot");
6144
+ info(
6145
+ ` Tip: \`${GENERATOR_NAME} skill --install\` makes this repo discoverable by AI agents.`
6146
+ );
5484
6147
  return 0;
5485
6148
  } catch (err) {
5486
6149
  if (err instanceof PromptCancelled) {
@@ -5491,6 +6154,31 @@ async function runInitCommand(invocation) {
5491
6154
  return 1;
5492
6155
  }
5493
6156
  }
6157
+ function runSkillCommand(invocation) {
6158
+ if (!invocation.install) {
6159
+ process.stdout.write(renderSkill());
6160
+ return 0;
6161
+ }
6162
+ try {
6163
+ const result = installSkill(invocation.cwd, invocation.target);
6164
+ if (invocation.format === "json") {
6165
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
6166
+ `);
6167
+ return 0;
6168
+ }
6169
+ info(`\u2705 Agent skill installed (${SKILL_NAME}):`);
6170
+ for (const item of result.written) {
6171
+ const shown = path12.relative(invocation.cwd, item.path) || item.path;
6172
+ info(` - ${item.action.padEnd(9)} ${shown}`);
6173
+ }
6174
+ info("");
6175
+ info(" Agents that read AGENTS.md, .claude/skills or .cursor/rules will pick this up automatically.");
6176
+ return 0;
6177
+ } catch (err) {
6178
+ warn(`\u274C Could not install the skill: ${err.message}`);
6179
+ return 1;
6180
+ }
6181
+ }
5494
6182
  function renderValidateText(result) {
5495
6183
  if (result.valid) {
5496
6184
  info("\u2705 Snapshot is valid and up to date with disk.");
@@ -5536,13 +6224,21 @@ ${USAGE}
5536
6224
  if (invocation.command === "init") {
5537
6225
  return await runInitCommand(invocation);
5538
6226
  }
6227
+ if (invocation.command === "describe") {
6228
+ const contract = buildAgentContract();
6229
+ emit(invocation.format, contract, () => info(renderContractText(contract)));
6230
+ return 0;
6231
+ }
6232
+ if (invocation.command === "skill") {
6233
+ return runSkillCommand(invocation);
6234
+ }
5539
6235
  const loaded = await loadConfigFileDetailed(invocation.cwd);
5540
6236
  for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
5541
6237
  if (loaded.errors.length > 0) {
5542
6238
  warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
5543
6239
  return 1;
5544
6240
  }
5545
- if (loaded.file) info(`\u2699 Using config ${path11.relative(process.cwd(), loaded.file) || loaded.file}`);
6241
+ if (loaded.file) info(`\u2699 Using config ${path12.relative(process.cwd(), loaded.file) || loaded.file}`);
5546
6242
  const resolved = resolveConfig(
5547
6243
  loaded.config,
5548
6244
  {
@@ -5566,14 +6262,14 @@ ${USAGE}
5566
6262
  return result.valid ? 0 : 1;
5567
6263
  }
5568
6264
  if (invocation.command === "report") {
5569
- const manifestPath = path11.resolve(resolved.cwd, resolved.outDir, "manifest.json");
5570
- if (!fs10.existsSync(manifestPath)) {
6265
+ const manifestPath = path12.resolve(resolved.cwd, resolved.outDir, "manifest.json");
6266
+ if (!fs11.existsSync(manifestPath)) {
5571
6267
  warn(`\u274C Manifest not found at ${manifestPath}. Run generate first.`);
5572
6268
  return 1;
5573
6269
  }
5574
6270
  let manifest;
5575
6271
  try {
5576
- manifest = JSON.parse(fs10.readFileSync(manifestPath, "utf8"));
6272
+ manifest = JSON.parse(fs11.readFileSync(manifestPath, "utf8"));
5577
6273
  } catch (err) {
5578
6274
  warn(`\u274C Could not read ${manifestPath}: ${err.message}`);
5579
6275
  return 1;
@@ -5691,10 +6387,10 @@ ${USAGE}
5691
6387
  }
5692
6388
  }
5693
6389
  async function runWatchLoop(invocation, resolved) {
5694
- const outDir = path11.resolve(resolved.cwd, resolved.outDir);
5695
- const tmpDir = path11.resolve(resolved.cwd, resolved.tmpDir);
5696
- const outName = path11.basename(outDir).toLowerCase();
5697
- const tmpName = path11.basename(tmpDir).toLowerCase();
6390
+ const outDir = path12.resolve(resolved.cwd, resolved.outDir);
6391
+ const tmpDir = path12.resolve(resolved.cwd, resolved.tmpDir);
6392
+ const outName = path12.basename(outDir).toLowerCase();
6393
+ const tmpName = path12.basename(tmpDir).toLowerCase();
5698
6394
  const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
5699
6395
  (segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
5700
6396
  );
@@ -5718,9 +6414,9 @@ async function runWatchLoop(invocation, resolved) {
5718
6414
  info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
5719
6415
  regenerate();
5720
6416
  let timer = null;
5721
- const watcher = fs10.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
6417
+ const watcher = fs11.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
5722
6418
  if (filename) {
5723
- const changed = path11.resolve(resolved.cwd, filename.toString());
6419
+ const changed = path12.resolve(resolved.cwd, filename.toString());
5724
6420
  if (isGenerated(changed)) return;
5725
6421
  }
5726
6422
  if (timer) clearTimeout(timer);