codebase-snapshot 0.9.1 → 0.9.5

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
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { spawnSync } from "child_process";
5
- import * as fs12 from "fs";
6
- import * as path13 from "path";
5
+ import * as fs16 from "fs";
6
+ import * as path17 from "path";
7
7
  import { parseArgs } from "util";
8
8
 
9
9
  // src/diff.ts
@@ -167,8 +167,8 @@ function workspacePackageDirs(cwd, patterns) {
167
167
  function scanWorkspacePackages(cwd, patterns) {
168
168
  const files = /* @__PURE__ */ new Set();
169
169
  const dependencies = /* @__PURE__ */ new Set();
170
- for (const relative8 of workspacePackageDirs(cwd, patterns)) {
171
- const full = path.join(cwd, relative8);
170
+ for (const relative11 of workspacePackageDirs(cwd, patterns)) {
171
+ const full = path.join(cwd, relative11);
172
172
  try {
173
173
  for (const entry of fs.readdirSync(full, { withFileTypes: true })) files.add(entry.name);
174
174
  } catch {
@@ -245,25 +245,41 @@ function buildPresetContext(cwd) {
245
245
  markdownCount: files.has("package.json") ? 0 : countMarkdownFiles(cwd)
246
246
  };
247
247
  }
248
- function hasFile(context, name) {
249
- return context.files.has(name);
248
+
249
+ // src/presets/evidence.ts
250
+ function fileEvidence(context, name) {
251
+ return context.files.has(name) ? name : null;
250
252
  }
251
- function hasFileStartingWith(context, prefix) {
253
+ function fileStartingWithEvidence(context, prefix) {
252
254
  for (const file of context.files) {
253
- if (file.startsWith(prefix)) return true;
255
+ if (file.startsWith(prefix)) return file;
254
256
  }
255
257
  for (const file of context.workspaceFiles) {
256
- if (file.startsWith(prefix)) return true;
258
+ if (file.startsWith(prefix)) return file;
257
259
  }
258
- return false;
260
+ return null;
261
+ }
262
+ function dependencyEvidence(context, ...names) {
263
+ for (const name of names) {
264
+ if (context.dependencies.has(name)) return `depends on ${name}`;
265
+ }
266
+ return null;
259
267
  }
260
- function manifestContains(context, manifest, needle) {
268
+ function manifestEvidence(context, manifest, needle) {
261
269
  const content = context.manifests[manifest];
262
- if (!content) return false;
263
- return content.toLowerCase().includes(needle.toLowerCase());
270
+ if (!content) return null;
271
+ if (!content.toLowerCase().includes(needle.toLowerCase())) return null;
272
+ return `${needle} in ${manifest}`;
273
+ }
274
+ function suffixEvidence(context, suffix) {
275
+ for (const file of context.files) {
276
+ if (file.endsWith(suffix)) return file;
277
+ }
278
+ return null;
264
279
  }
265
- function hasDependency(context, ...names) {
266
- return names.some((name) => context.dependencies.has(name));
280
+ function markdownEvidence(context) {
281
+ if (context.markdownCount < 3 || context.files.has("package.json")) return null;
282
+ return `${context.markdownCount} markdown files, no package.json`;
267
283
  }
268
284
 
269
285
  // src/presets/helpers.ts
@@ -471,7 +487,7 @@ var JS_PRESETS = [
471
487
  {
472
488
  name: "nextjs",
473
489
  priority: 100,
474
- detect: (context) => hasFileStartingWith(context, "next.config") || hasDependency(context, "next"),
490
+ detect: (context) => fileStartingWithEvidence(context, "next.config") ?? dependencyEvidence(context, "next"),
475
491
  layers: NEXTJS_LAYERS,
476
492
  kinds: NEXTJS_KINDS,
477
493
  exclude: NEXT_LIKE_EXCLUDES,
@@ -480,7 +496,7 @@ var JS_PRESETS = [
480
496
  {
481
497
  name: "nuxt",
482
498
  priority: 98,
483
- detect: (context) => hasFileStartingWith(context, "nuxt.config") || hasDependency(context, "nuxt", "nuxt3"),
499
+ detect: (context) => fileStartingWithEvidence(context, "nuxt.config") ?? dependencyEvidence(context, "nuxt", "nuxt3"),
484
500
  layers: [
485
501
  layer("pages", "pages/", "src/pages/"),
486
502
  layer("components", "components/", "src/components/"),
@@ -504,7 +520,7 @@ var JS_PRESETS = [
504
520
  {
505
521
  name: "sveltekit",
506
522
  priority: 96,
507
- detect: (context) => hasFileStartingWith(context, "svelte.config") || hasDependency(context, "@sveltejs/kit", "svelte"),
523
+ detect: (context) => fileStartingWithEvidence(context, "svelte.config") ?? dependencyEvidence(context, "@sveltejs/kit", "svelte"),
508
524
  layers: [
509
525
  layer("routes", "src/routes/", "routes/"),
510
526
  layer("lib", "src/lib/", "lib/"),
@@ -525,7 +541,7 @@ var JS_PRESETS = [
525
541
  {
526
542
  name: "remix",
527
543
  priority: 94,
528
- detect: (context) => hasDependency(context, "@remix-run/react", "@remix-run/node", "@remix-run/dev"),
544
+ detect: (context) => dependencyEvidence(context, "@remix-run/react", "@remix-run/node", "@remix-run/dev"),
529
545
  layers: [
530
546
  layer("routes", "app/routes/", "routes/"),
531
547
  layer("components", "app/components/"),
@@ -539,7 +555,7 @@ var JS_PRESETS = [
539
555
  {
540
556
  name: "astro",
541
557
  priority: 92,
542
- detect: (context) => hasFileStartingWith(context, "astro.config") || hasDependency(context, "astro"),
558
+ detect: (context) => fileStartingWithEvidence(context, "astro.config") ?? dependencyEvidence(context, "astro"),
543
559
  layers: [
544
560
  layer("pages", "src/pages/"),
545
561
  layer("layouts", "src/layouts/"),
@@ -554,7 +570,7 @@ var JS_PRESETS = [
554
570
  {
555
571
  name: "angular",
556
572
  priority: 90,
557
- detect: (context) => hasFile(context, "angular.json") || hasDependency(context, "@angular/core"),
573
+ detect: (context) => fileEvidence(context, "angular.json") ?? dependencyEvidence(context, "@angular/core"),
558
574
  layers: [
559
575
  layer("app", "src/app/"),
560
576
  layer("features", "src/app/features/"),
@@ -578,7 +594,7 @@ var JS_PRESETS = [
578
594
  {
579
595
  name: "solid",
580
596
  priority: 88,
581
- detect: (context) => hasDependency(context, "solid-js", "solid-start"),
597
+ detect: (context) => dependencyEvidence(context, "solid-js", "solid-start"),
582
598
  layers: [
583
599
  layer("pages", "src/routes/", "src/pages/"),
584
600
  layer("components", "src/components/"),
@@ -591,7 +607,15 @@ var JS_PRESETS = [
591
607
  {
592
608
  name: "vue-vite",
593
609
  priority: 86,
594
- detect: (context) => hasDependency(context, "vue") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
610
+ // `vue` is the decisive signal; `vite` (or its config file) only says the app is
611
+ // built with Vite rather than shipped pre-compiled, so it is required but not what
612
+ // gets reported.
613
+ detect: (context) => {
614
+ const framework = dependencyEvidence(context, "vue");
615
+ if (framework === null) return null;
616
+ const build = dependencyEvidence(context, "vite") ?? fileStartingWithEvidence(context, "vite.config");
617
+ return build === null ? null : framework;
618
+ },
595
619
  layers: [
596
620
  layer("pages", "src/pages/", "src/views/"),
597
621
  layer("components", "src/components/"),
@@ -616,7 +640,12 @@ var JS_PRESETS = [
616
640
  {
617
641
  name: "react-vite",
618
642
  priority: 84,
619
- detect: (context) => hasDependency(context, "react") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
643
+ detect: (context) => {
644
+ const framework = dependencyEvidence(context, "react");
645
+ if (framework === null) return null;
646
+ const build = dependencyEvidence(context, "vite") ?? fileStartingWithEvidence(context, "vite.config");
647
+ return build === null ? null : framework;
648
+ },
620
649
  layers: VITE_LAYERS,
621
650
  kinds: VITE_KINDS,
622
651
  exclude: [".vite", "dist"],
@@ -625,7 +654,7 @@ var JS_PRESETS = [
625
654
  {
626
655
  name: "nest",
627
656
  priority: 80,
628
- detect: (context) => hasDependency(context, "@nestjs/core"),
657
+ detect: (context) => dependencyEvidence(context, "@nestjs/core"),
629
658
  layers: [
630
659
  layer("modules", "src/modules/", "modules/"),
631
660
  layer("controllers", "src/controllers/", "controllers/"),
@@ -651,7 +680,7 @@ var JS_PRESETS = [
651
680
  {
652
681
  name: "node-express",
653
682
  priority: 78,
654
- detect: (context) => hasDependency(context, "express", "fastify", "koa", "@hapi/hapi"),
683
+ detect: (context) => dependencyEvidence(context, "express", "fastify", "koa", "@hapi/hapi"),
655
684
  layers: NODE_LAYERS,
656
685
  kinds: NODE_KINDS,
657
686
  exclude: ["dist"],
@@ -662,7 +691,7 @@ var JS_PRESETS = [
662
691
  // A bare package.json is often just tooling around a non-JS project, so it
663
692
  // ranks below every definitive language manifest.
664
693
  priority: 45,
665
- detect: (context) => hasFile(context, "package.json"),
694
+ detect: (context) => fileEvidence(context, "package.json"),
666
695
  layers: [
667
696
  layer("src", "src/"),
668
697
  layer("lib", "lib/"),
@@ -676,13 +705,12 @@ var JS_PRESETS = [
676
705
  ];
677
706
 
678
707
  // src/presets/lang-presets.ts
679
- var anyFileEndingWith = (context, suffix) => Array.from(context.files).some((file) => file.endsWith(suffix));
680
708
  var DIR = (name) => `${name}/`;
681
709
  var LANGUAGE_PRESETS = [
682
710
  {
683
711
  name: "python-django",
684
712
  priority: 56,
685
- detect: (context) => hasFile(context, "manage.py") || manifestContains(context, "pyproject.toml", "django") || manifestContains(context, "requirements.txt", "django"),
713
+ detect: (context) => fileEvidence(context, "manage.py") ?? manifestEvidence(context, "pyproject.toml", "django") ?? manifestEvidence(context, "requirements.txt", "django"),
686
714
  layers: [
687
715
  layer("views", DIR("views"), DIR("app/views"), DIR("apps/views")),
688
716
  layer("models", DIR("models"), DIR("app/models"), DIR("apps/models")),
@@ -708,7 +736,7 @@ var LANGUAGE_PRESETS = [
708
736
  {
709
737
  name: "python-fastapi",
710
738
  priority: 54,
711
- detect: (context) => manifestContains(context, "requirements.txt", "fastapi") || manifestContains(context, "requirements.txt", "flask") || manifestContains(context, "pyproject.toml", "fastapi") || manifestContains(context, "pyproject.toml", "flask"),
739
+ detect: (context) => manifestEvidence(context, "requirements.txt", "fastapi") ?? manifestEvidence(context, "requirements.txt", "flask") ?? manifestEvidence(context, "pyproject.toml", "fastapi") ?? manifestEvidence(context, "pyproject.toml", "flask"),
712
740
  layers: [
713
741
  layer("routers", DIR("routers"), DIR("routes"), DIR("api")),
714
742
  layer("models", DIR("models"), DIR("db")),
@@ -732,7 +760,7 @@ var LANGUAGE_PRESETS = [
732
760
  name: "python-lib",
733
761
  // Below node-lib: a Node repo with a stray .py file is still a Node repo.
734
762
  priority: 30,
735
- detect: (context) => hasFile(context, "pyproject.toml") || hasFile(context, "setup.py") || hasFile(context, "requirements.txt") || anyFileEndingWith(context, ".py"),
763
+ detect: (context) => fileEvidence(context, "pyproject.toml") ?? fileEvidence(context, "setup.py") ?? fileEvidence(context, "requirements.txt") ?? suffixEvidence(context, ".py"),
736
764
  layers: [
737
765
  layer("package", DIR("src"), DIR("lib")),
738
766
  layer("tests", DIR("tests"), DIR("test")),
@@ -745,7 +773,7 @@ var LANGUAGE_PRESETS = [
745
773
  {
746
774
  name: "go",
747
775
  priority: 50,
748
- detect: (context) => hasFile(context, "go.mod"),
776
+ detect: (context) => fileEvidence(context, "go.mod"),
749
777
  layers: [
750
778
  layer("cmd", "cmd/"),
751
779
  layer("internal", "internal/"),
@@ -769,7 +797,7 @@ var LANGUAGE_PRESETS = [
769
797
  {
770
798
  name: "rust",
771
799
  priority: 48,
772
- detect: (context) => hasFile(context, "Cargo.toml"),
800
+ detect: (context) => fileEvidence(context, "Cargo.toml"),
773
801
  layers: [
774
802
  layer("src", "src/"),
775
803
  layer("crates", "crates/"),
@@ -785,7 +813,7 @@ var LANGUAGE_PRESETS = [
785
813
  {
786
814
  name: "java-spring",
787
815
  priority: 46,
788
- detect: (context) => hasFile(context, "pom.xml") || hasFile(context, "build.gradle") || hasFile(context, "build.gradle.kts"),
816
+ detect: (context) => fileEvidence(context, "pom.xml") ?? fileEvidence(context, "build.gradle") ?? fileEvidence(context, "build.gradle.kts"),
789
817
  layers: [
790
818
  layer("api", "src/main/java/"),
791
819
  layer("test", "src/test/java/"),
@@ -808,7 +836,7 @@ var LANGUAGE_PRESETS = [
808
836
  {
809
837
  name: "dotnet",
810
838
  priority: 44,
811
- detect: (context) => anyFileEndingWith(context, ".csproj") || anyFileEndingWith(context, ".sln") || hasFile(context, "global.json"),
839
+ detect: (context) => suffixEvidence(context, ".csproj") ?? suffixEvidence(context, ".sln") ?? fileEvidence(context, "global.json"),
812
840
  layers: [
813
841
  layer("web", "src/", "Web/"),
814
842
  layer("domain", "Domain/", "Core/"),
@@ -831,7 +859,7 @@ var LANGUAGE_PRESETS = [
831
859
  {
832
860
  name: "php-laravel",
833
861
  priority: 42,
834
- detect: (context) => hasFile(context, "artisan") || manifestContains(context, "composer.json", "laravel") || hasFile(context, "composer.json"),
862
+ detect: (context) => fileEvidence(context, "artisan") ?? manifestEvidence(context, "composer.json", "laravel") ?? fileEvidence(context, "composer.json"),
835
863
  layers: [
836
864
  layer("app", "app/"),
837
865
  layer("routes", "routes/"),
@@ -857,7 +885,7 @@ var LANGUAGE_PRESETS = [
857
885
  {
858
886
  name: "ruby-rails",
859
887
  priority: 40,
860
- detect: (context) => hasFile(context, "Gemfile") || hasFile(context, "config.ru") || hasFile(context, "Rakefile"),
888
+ detect: (context) => fileEvidence(context, "Gemfile") ?? fileEvidence(context, "config.ru") ?? fileEvidence(context, "Rakefile"),
861
889
  layers: [
862
890
  layer("app", "app/"),
863
891
  layer("controllers", "app/controllers/"),
@@ -930,7 +958,7 @@ var SHAPE_PRESETS = [
930
958
  {
931
959
  name: "monorepo",
932
960
  priority: 70,
933
- detect: (context) => hasFile(context, "pnpm-workspace.yaml") || hasFile(context, "nx.json") || hasFile(context, "turbo.json") || hasFile(context, "lerna.json") || manifestContains(context, "package.json", '"workspaces"'),
961
+ detect: (context) => fileEvidence(context, "pnpm-workspace.yaml") ?? fileEvidence(context, "nx.json") ?? fileEvidence(context, "turbo.json") ?? fileEvidence(context, "lerna.json") ?? manifestEvidence(context, "package.json", '"workspaces"'),
934
962
  layers: [
935
963
  layer("apps", "apps/"),
936
964
  layer("packages", "packages/"),
@@ -944,7 +972,7 @@ var SHAPE_PRESETS = [
944
972
  {
945
973
  name: "infra",
946
974
  priority: 24,
947
- detect: (context) => Array.from(context.files).some((file) => file.endsWith(".tf")) || hasFile(context, "Chart.yaml") || hasFile(context, "kustomization.yaml") || hasFile(context, "docker-compose.yml") || hasFile(context, "docker-compose.yaml"),
975
+ detect: (context) => suffixEvidence(context, ".tf") ?? fileEvidence(context, "Chart.yaml") ?? fileEvidence(context, "kustomization.yaml") ?? fileEvidence(context, "docker-compose.yml") ?? fileEvidence(context, "docker-compose.yaml"),
948
976
  layers: [
949
977
  layer("modules", "modules/"),
950
978
  layer("environments", "env/", "environments/"),
@@ -966,7 +994,7 @@ var SHAPE_PRESETS = [
966
994
  // up to two directories down (bounded in the context builder). Counting
967
995
  // only the root turned every real documentation repository — README.md plus
968
996
  // the pages under `docs/` — into `generic`.
969
- detect: (context) => context.markdownCount >= 3 && !hasFile(context, "package.json"),
997
+ detect: (context) => markdownEvidence(context),
970
998
  layers: [
971
999
  layer("content", "content/", "docs/"),
972
1000
  layer("assets", "assets/", "static/", "public/"),
@@ -979,7 +1007,10 @@ var SHAPE_PRESETS = [
979
1007
  {
980
1008
  name: "generic",
981
1009
  priority: 0,
982
- detect: () => true,
1010
+ // No detector: this is the fallback, and `detectPresetFromContext` reaches it only
1011
+ // when every other preset reported no evidence. `fallback: true` is what lets the
1012
+ // wizard tell "I recognised this" apart from "I gave up".
1013
+ fallback: true,
983
1014
  layers: DEFAULT_LAYERS,
984
1015
  kinds: DEFAULT_KINDS,
985
1016
  exclude: [],
@@ -1011,20 +1042,27 @@ function detectPresetFromContext(context, forced) {
1011
1042
  if (forced !== void 0) {
1012
1043
  const preset = BY_NAME.get(forced);
1013
1044
  if (!preset) throw new Error(unknownPresetMessage(forced));
1014
- return toDetected(preset);
1045
+ return toDetected(preset, null);
1015
1046
  }
1016
1047
  for (const preset of PRESETS) {
1017
- if (preset.detect(context)) return toDetected(preset);
1048
+ const reason = preset.detect?.(context) ?? null;
1049
+ if (reason !== null) return toDetected(preset, reason);
1050
+ }
1051
+ const fallback = PRESETS.find((preset) => preset.fallback === true);
1052
+ if (fallback === void 0) {
1053
+ throw new Error("No fallback preset is defined. Exactly one preset must set `fallback: true`.");
1018
1054
  }
1019
- return toDetected(PRESETS[PRESETS.length - 1]);
1055
+ return toDetected(fallback, null, true);
1020
1056
  }
1021
- function toDetected(preset) {
1057
+ function toDetected(preset, reason, fallback = false) {
1022
1058
  return {
1023
1059
  presetName: preset.name,
1024
1060
  layers: preset.layers,
1025
1061
  kinds: preset.kinds,
1026
1062
  exclude: preset.exclude ? [...preset.exclude] : [],
1027
- recommendedBudget: preset.recommendedBudget ?? null
1063
+ recommendedBudget: preset.recommendedBudget ?? null,
1064
+ reason,
1065
+ fallback
1028
1066
  };
1029
1067
  }
1030
1068
  function detectPreset(cwd, forced) {
@@ -1470,8 +1508,8 @@ function readLinkTarget(linkPath, fallback) {
1470
1508
  }
1471
1509
  }
1472
1510
  function isInside(root, candidate) {
1473
- const relative8 = path2.relative(root, candidate);
1474
- return relative8 === "" || !relative8.startsWith(`..${path2.sep}`) && relative8 !== ".." && !path2.isAbsolute(relative8);
1511
+ const relative11 = path2.relative(root, candidate);
1512
+ return relative11 === "" || !relative11.startsWith(`..${path2.sep}`) && relative11 !== ".." && !path2.isAbsolute(relative11);
1475
1513
  }
1476
1514
  function getGitInfo(cwd, fallbackBranch = "main") {
1477
1515
  try {
@@ -2153,6 +2191,20 @@ var CONFIG_FILENAMES = [
2153
2191
  ".snapshotrc.json",
2154
2192
  ".snapshotrc"
2155
2193
  ];
2194
+ function findConfigFile(cwd) {
2195
+ for (const filename of CONFIG_FILENAMES) {
2196
+ const fullPath = path3.join(cwd, filename);
2197
+ if (fs3.existsSync(fullPath)) return fullPath;
2198
+ }
2199
+ const pkgJsonPath = path3.join(cwd, "package.json");
2200
+ if (!fs3.existsSync(pkgJsonPath)) return null;
2201
+ try {
2202
+ const parsed = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf8"));
2203
+ return parsed.snapshot === void 0 ? null : pkgJsonPath;
2204
+ } catch {
2205
+ return null;
2206
+ }
2207
+ }
2156
2208
  var RESOLVED_CONFIG_BRAND = /* @__PURE__ */ Symbol.for(
2157
2209
  "codebase-snapshot/resolved-config"
2158
2210
  );
@@ -2608,11 +2660,11 @@ function outputExcludePatterns(cwd, outDir, tmpDir) {
2608
2660
  const patterns = [];
2609
2661
  for (const dir of [outDir, tmpDir]) {
2610
2662
  const absolute = path3.resolve(cwd, dir);
2611
- const relative8 = path3.relative(cwd, absolute);
2612
- if (!relative8 || relative8 === ".." || relative8.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative8)) {
2663
+ const relative11 = path3.relative(cwd, absolute);
2664
+ if (!relative11 || relative11 === ".." || relative11.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative11)) {
2613
2665
  continue;
2614
2666
  }
2615
- patterns.push(`${relative8.replace(/\\/g, "/")}/**`);
2667
+ patterns.push(`${relative11.replace(/\\/g, "/")}/**`);
2616
2668
  }
2617
2669
  return patterns;
2618
2670
  }
@@ -4108,21 +4160,21 @@ function describeMatcher(matcher) {
4108
4160
  }
4109
4161
  function normalizeTarget(cwd, target) {
4110
4162
  const absolute = path8.resolve(cwd, target);
4111
- const relative8 = path8.relative(cwd, absolute);
4112
- if (relative8.startsWith("..") || path8.isAbsolute(relative8)) return target;
4113
- return relative8.split(path8.sep).join("/");
4163
+ const relative11 = path8.relative(cwd, absolute);
4164
+ if (relative11.startsWith("..") || path8.isAbsolute(relative11)) return target;
4165
+ return relative11.split(path8.sep).join("/");
4114
4166
  }
4115
4167
  function findRuleMatch(rules, relativePath) {
4116
4168
  return rules.find(
4117
4169
  (rule) => matchRule(relativePath, rule.match)
4118
4170
  );
4119
4171
  }
4120
- function findRecordedAncestor(skipped, relative8, maskedPath2) {
4172
+ function findRecordedAncestor(skipped, relative11, maskedPath2) {
4121
4173
  let nearest;
4122
4174
  for (const record of skipped) {
4123
4175
  const ancestorPath = record.path;
4124
4176
  if (ancestorPath.length === 0) continue;
4125
- const isAncestor = relative8.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
4177
+ const isAncestor = relative11.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
4126
4178
  if (!isAncestor) continue;
4127
4179
  if (!nearest || ancestorPath.length > nearest.path.length) nearest = record;
4128
4180
  }
@@ -4137,9 +4189,9 @@ function readManifest2(config) {
4137
4189
  }
4138
4190
  }
4139
4191
  function explainPath(config, target) {
4140
- const relative8 = normalizeTarget(config.cwd, target);
4141
- const absolute = path8.join(config.cwd, relative8);
4142
- const maskedPath2 = findSecretInPath(relative8, config.secretRules)?.masked ?? relative8;
4192
+ const relative11 = normalizeTarget(config.cwd, target);
4193
+ const absolute = path8.join(config.cwd, relative11);
4194
+ const maskedPath2 = findSecretInPath(relative11, config.secretRules)?.masked ?? relative11;
4143
4195
  const result = {
4144
4196
  path: maskedPath2,
4145
4197
  exists: fs7.existsSync(absolute),
@@ -4154,11 +4206,11 @@ function explainPath(config, target) {
4154
4206
  // that is only missing because the walker dropped the tool's own output directory.
4155
4207
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
4156
4208
  (directory) => path8.relative(config.cwd, path8.resolve(config.cwd, directory)).replace(/\\/g, "/")
4157
- ).filter((relative9) => relative9.length > 0 && !relative9.startsWith(".."))
4209
+ ).filter((relative12) => relative12.length > 0 && !relative12.startsWith(".."))
4158
4210
  });
4159
- if (!collection.files.includes(relative8)) {
4211
+ if (!collection.files.includes(relative11)) {
4160
4212
  const record = collection.skipped.find(
4161
- (entry2) => entry2.path === relative8 || entry2.path === maskedPath2
4213
+ (entry2) => entry2.path === relative11 || entry2.path === maskedPath2
4162
4214
  );
4163
4215
  if (record) {
4164
4216
  result.reason = record.reason;
@@ -4167,7 +4219,7 @@ function explainPath(config, target) {
4167
4219
  result.reason = "missing";
4168
4220
  result.detail = "no such file or directory, relative to the configured cwd";
4169
4221
  } else {
4170
- const ancestor = findRecordedAncestor(collection.skipped, relative8, maskedPath2);
4222
+ const ancestor = findRecordedAncestor(collection.skipped, relative11, maskedPath2);
4171
4223
  if (ancestor) {
4172
4224
  result.reason = ancestor.reason;
4173
4225
  result.detail = ancestor.detail ? `${ancestor.detail} \u2014 inherited from the skipped directory \`${ancestor.path}\`` : `inside the skipped directory \`${ancestor.path}\``;
@@ -4185,12 +4237,12 @@ function explainPath(config, target) {
4185
4237
  result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
4186
4238
  return result;
4187
4239
  }
4188
- result.layer = classifyLayer(relative8, config.layers);
4189
- result.kind = classifyKind(relative8, config.kinds);
4190
- result.language = detectLanguage(relative8);
4191
- const layerRule = findRuleMatch(config.layers, relative8);
4240
+ result.layer = classifyLayer(relative11, config.layers);
4241
+ result.kind = classifyKind(relative11, config.kinds);
4242
+ result.language = detectLanguage(relative11);
4243
+ const layerRule = findRuleMatch(config.layers, relative11);
4192
4244
  if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
4193
- const kindRule = findRuleMatch(config.kinds, relative8);
4245
+ const kindRule = findRuleMatch(config.kinds, relative11);
4194
4246
  if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
4195
4247
  if (fs7.existsSync(absolute) && fs7.statSync(absolute).isFile()) {
4196
4248
  const buffer = fs7.readFileSync(absolute);
@@ -4205,12 +4257,12 @@ function explainPath(config, target) {
4205
4257
  result.bytes = buffer.length;
4206
4258
  result.lines = countLines(text2);
4207
4259
  if (decoded.encoding !== "utf-8") result.encoding = decoded.encoding;
4208
- const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative8);
4260
+ const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative11);
4209
4261
  result.secretCount = findings.length;
4210
4262
  result.secrets = findings.slice(0, 20).map((finding) => ({ rules: finding.rules, line: finding.line }));
4211
4263
  }
4212
4264
  const manifest = readManifest2(config);
4213
- const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative8) : void 0;
4265
+ const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative11) : void 0;
4214
4266
  result.part = entry?.part ?? null;
4215
4267
  result.inclusionMode = entry?.inclusionMode;
4216
4268
  return result;
@@ -4294,7 +4346,7 @@ function gitignorePath(cwd, filePath) {
4294
4346
 
4295
4347
  // src/version.ts
4296
4348
  var GENERATOR_NAME = "codebase-snapshot";
4297
- var GENERATOR_VERSION = "0.9.1";
4349
+ var GENERATOR_VERSION = "0.9.5";
4298
4350
  var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
4299
4351
  function isOurGeneratorName(name) {
4300
4352
  if (typeof name !== "string") return false;
@@ -4435,6 +4487,12 @@ var PromptCancelled = class extends Error {
4435
4487
  this.name = "PromptCancelled";
4436
4488
  }
4437
4489
  };
4490
+ var PromptBack = class extends Error {
4491
+ constructor() {
4492
+ super("Prompt back");
4493
+ this.name = "PromptBack";
4494
+ }
4495
+ };
4438
4496
  function defaultIO() {
4439
4497
  return { input: process.stdin, output: process.stdout };
4440
4498
  }
@@ -4458,35 +4516,40 @@ function choiceLine(choice, active) {
4458
4516
  const hint = choice.hint ? ` ${choice.hint}` : "";
4459
4517
  return ` ${marker(active)} ${choice.label}${hint}`;
4460
4518
  }
4461
- function renderSelect(message, choices, index) {
4519
+ function listFooter(base, allowBack) {
4520
+ return allowBack ? `${base} \xB7 \u2190 back` : base;
4521
+ }
4522
+ function renderSelect(message, choices, index, allowBack = false) {
4462
4523
  const lines = [`\u25C6 ${message}`, ""];
4463
4524
  choices.forEach((choice, position) => lines.push(choiceLine(choice, position === index)));
4464
- lines.push("", " \u2191\u2193 move \xB7 enter select");
4525
+ lines.push("", ` ${listFooter("\u2191\u2193 move \xB7 enter select", allowBack)}`);
4465
4526
  return lines.join("\n");
4466
4527
  }
4467
- function renderMultiSelect(message, choices, index, selected) {
4528
+ function renderMultiSelect(message, choices, index, selected, allowBack = false) {
4468
4529
  const lines = [`\u25C6 ${message}`, ""];
4469
4530
  choices.forEach((choice, position) => {
4470
4531
  const box = selected.has(position) ? "\u25FC" : "\u25FB";
4471
4532
  const hint = choice.hint ? ` ${choice.hint}` : "";
4472
4533
  lines.push(` ${marker(position === index)} ${box} ${choice.label}${hint}`);
4473
4534
  });
4474
- lines.push("", " \u2191\u2193 move \xB7 space toggle \xB7 enter confirm");
4535
+ lines.push("", ` ${listFooter("\u2191\u2193 move \xB7 space toggle \xB7 enter confirm", allowBack)}`);
4475
4536
  return lines.join("\n");
4476
4537
  }
4477
- function renderConfirm(message, value) {
4538
+ function renderConfirm(message, value, allowBack = false) {
4539
+ const footer = allowBack ? "\u2190\u2192 or y/n \xB7 enter confirm \xB7 b back" : "\u2190\u2192 or y/n \xB7 enter confirm";
4478
4540
  return [
4479
4541
  `\u25C6 ${message}`,
4480
4542
  "",
4481
4543
  ` ${marker(value)} Yes`,
4482
4544
  ` ${marker(!value)} No`,
4483
4545
  "",
4484
- " \u2190\u2192 or y/n \xB7 enter confirm"
4546
+ ` ${footer}`
4485
4547
  ].join("\n");
4486
4548
  }
4487
- function renderText(message, value, error) {
4549
+ function renderText(message, value, error, allowBack = false) {
4488
4550
  const lines = [`\u25C6 ${message}`, "", ` ${value || "\u258F"}`];
4489
4551
  if (error) lines.push("", ` \u2716 ${error}`);
4552
+ if (allowBack) lines.push("", " enter confirm \xB7 \u2190 back");
4490
4553
  return lines.join("\n");
4491
4554
  }
4492
4555
  function keyReader(input) {
@@ -4506,7 +4569,7 @@ function keyReader(input) {
4506
4569
  next() {
4507
4570
  const queued = queue.shift();
4508
4571
  if (queued) return Promise.resolve(queued);
4509
- return new Promise((resolve6) => waiters.push(resolve6));
4572
+ return new Promise((resolve9) => waiters.push(resolve9));
4510
4573
  },
4511
4574
  close() {
4512
4575
  input.off("keypress", onKey);
@@ -4535,14 +4598,18 @@ var Frame = class {
4535
4598
  function isCancel(key) {
4536
4599
  return key.ctrl === true && key.name === "c" || key.ctrl === true && key.name === "d";
4537
4600
  }
4601
+ function isListBack(key, allowBack) {
4602
+ return allowBack && (key.name === "left" || key.name === "b");
4603
+ }
4538
4604
  async function select(io, message, choices, options = {}) {
4539
4605
  if (choices.length === 0) throw new Error("select() requires at least one choice.");
4606
+ const allowBack = options.allowBack === true;
4540
4607
  const fallbackIndex = Math.max(
4541
4608
  0,
4542
4609
  choices.findIndex((choice) => choice.value === options.defaultValue)
4543
4610
  );
4544
4611
  if (!isInteractive(io)) {
4545
- io.output.write(`${renderSelect(message, choices, fallbackIndex)}
4612
+ io.output.write(`${renderSelect(message, choices, fallbackIndex, allowBack)}
4546
4613
  `);
4547
4614
  return choices[fallbackIndex].value;
4548
4615
  }
@@ -4551,9 +4618,10 @@ async function select(io, message, choices, options = {}) {
4551
4618
  let index = fallbackIndex;
4552
4619
  try {
4553
4620
  for (; ; ) {
4554
- frame.draw(renderSelect(message, choices, index));
4621
+ frame.draw(renderSelect(message, choices, index, allowBack));
4555
4622
  const key = await reader.next();
4556
4623
  if (isCancel(key)) throw new PromptCancelled();
4624
+ if (isListBack(key, allowBack)) throw new PromptBack();
4557
4625
  if (key.name === "up") index = moveSelection(index, -1, choices.length);
4558
4626
  else if (key.name === "down") index = moveSelection(index, 1, choices.length);
4559
4627
  else if (key.name === "return" || key.name === "enter") {
@@ -4562,18 +4630,22 @@ async function select(io, message, choices, options = {}) {
4562
4630
  } else if (key.name === "j") index = moveSelection(index, 1, choices.length);
4563
4631
  else if (key.name === "k") index = moveSelection(index, -1, choices.length);
4564
4632
  }
4633
+ } catch (error) {
4634
+ frame.erase();
4635
+ throw error;
4565
4636
  } finally {
4566
4637
  reader.close();
4567
4638
  }
4568
4639
  }
4569
4640
  async function multiSelect(io, message, choices, options = {}) {
4641
+ const allowBack = options.allowBack === true;
4570
4642
  const selected = /* @__PURE__ */ new Set();
4571
4643
  choices.forEach((choice, position) => {
4572
4644
  if (options.defaults?.includes(choice.value)) selected.add(position);
4573
4645
  });
4574
4646
  const result = () => choices.filter((_, position) => selected.has(position)).map((choice) => choice.value);
4575
4647
  if (!isInteractive(io)) {
4576
- io.output.write(`${renderMultiSelect(message, choices, 0, selected)}
4648
+ io.output.write(`${renderMultiSelect(message, choices, 0, selected, allowBack)}
4577
4649
  `);
4578
4650
  return result();
4579
4651
  }
@@ -4582,9 +4654,10 @@ async function multiSelect(io, message, choices, options = {}) {
4582
4654
  let index = 0;
4583
4655
  try {
4584
4656
  for (; ; ) {
4585
- frame.draw(renderMultiSelect(message, choices, index, selected));
4657
+ frame.draw(renderMultiSelect(message, choices, index, selected, allowBack));
4586
4658
  const key = await reader.next();
4587
4659
  if (isCancel(key)) throw new PromptCancelled();
4660
+ if (isListBack(key, allowBack)) throw new PromptBack();
4588
4661
  if (key.name === "up") index = moveSelection(index, -1, choices.length);
4589
4662
  else if (key.name === "down") index = moveSelection(index, 1, choices.length);
4590
4663
  else if (key.name === "space") {
@@ -4599,14 +4672,18 @@ async function multiSelect(io, message, choices, options = {}) {
4599
4672
  return result();
4600
4673
  }
4601
4674
  }
4675
+ } catch (error) {
4676
+ frame.erase();
4677
+ throw error;
4602
4678
  } finally {
4603
4679
  reader.close();
4604
4680
  }
4605
4681
  }
4606
4682
  async function confirm(io, message, options = {}) {
4683
+ const allowBack = options.allowBack === true;
4607
4684
  let value = options.defaultValue ?? true;
4608
4685
  if (!isInteractive(io)) {
4609
- io.output.write(`${renderConfirm(message, value)}
4686
+ io.output.write(`${renderConfirm(message, value, allowBack)}
4610
4687
  `);
4611
4688
  return value;
4612
4689
  }
@@ -4614,9 +4691,10 @@ async function confirm(io, message, options = {}) {
4614
4691
  const reader = keyReader(io.input);
4615
4692
  try {
4616
4693
  for (; ; ) {
4617
- frame.draw(renderConfirm(message, value));
4694
+ frame.draw(renderConfirm(message, value, allowBack));
4618
4695
  const key = await reader.next();
4619
4696
  if (isCancel(key)) throw new PromptCancelled();
4697
+ if (allowBack && key.name === "b") throw new PromptBack();
4620
4698
  if (key.name === "up" || key.name === "left") value = true;
4621
4699
  else if (key.name === "down" || key.name === "right") value = false;
4622
4700
  else if (key.name === "y") value = true;
@@ -4626,14 +4704,18 @@ async function confirm(io, message, options = {}) {
4626
4704
  return value;
4627
4705
  }
4628
4706
  }
4707
+ } catch (error) {
4708
+ frame.erase();
4709
+ throw error;
4629
4710
  } finally {
4630
4711
  reader.close();
4631
4712
  }
4632
4713
  }
4633
4714
  async function text(io, message, options = {}) {
4715
+ const allowBack = options.allowBack === true;
4634
4716
  const fallback = options.defaultValue ?? "";
4635
4717
  if (!isInteractive(io)) {
4636
- io.output.write(`${renderText(message, fallback)}
4718
+ io.output.write(`${renderText(message, fallback, void 0, allowBack)}
4637
4719
  `);
4638
4720
  return fallback;
4639
4721
  }
@@ -4643,9 +4725,10 @@ async function text(io, message, options = {}) {
4643
4725
  let error;
4644
4726
  try {
4645
4727
  for (; ; ) {
4646
- frame.draw(renderText(message, value, error));
4728
+ frame.draw(renderText(message, value, error, allowBack));
4647
4729
  const key = await reader.next();
4648
4730
  if (isCancel(key)) throw new PromptCancelled();
4731
+ if (allowBack && key.name === "left") throw new PromptBack();
4649
4732
  error = void 0;
4650
4733
  if (key.name === "return" || key.name === "enter") {
4651
4734
  const problem = options.validate ? options.validate(value) : null;
@@ -4666,6 +4749,9 @@ async function text(io, message, options = {}) {
4666
4749
  }
4667
4750
  if (_isPrintable(key)) value += _keyText(key);
4668
4751
  }
4752
+ } catch (error_) {
4753
+ frame.erase();
4754
+ throw error_;
4669
4755
  } finally {
4670
4756
  reader.close();
4671
4757
  }
@@ -4861,16 +4947,18 @@ function updatePackageJson(cwd, notes) {
4861
4947
  return { scripts: added, dependency };
4862
4948
  }
4863
4949
  var GITIGNORE_ENTRIES = ["snapshot/", "snapshot.tmp/"];
4864
- function resolvePresetChoice(answers, presetName) {
4950
+ function resolvePresetChoice(detectedName, presetName) {
4865
4951
  return {
4866
4952
  value: presetName,
4867
4953
  label: presetName,
4868
- hint: presetName === answers.preset ? "detected" : void 0
4954
+ hint: presetName === detectedName ? "detected" : void 0
4869
4955
  };
4870
4956
  }
4871
4957
  async function runInit(options) {
4872
4958
  const io = options.io ?? defaultIO();
4873
4959
  const detected = detectPreset(options.cwd, options.preset);
4960
+ let reason = detected.reason;
4961
+ let fallback = detected.fallback;
4874
4962
  const notes = [];
4875
4963
  const answers = {
4876
4964
  preset: detected.presetName,
@@ -4892,73 +4980,37 @@ async function runInit(options) {
4892
4980
  if (interactive) {
4893
4981
  io.output.write("\n codebase-snapshot\n");
4894
4982
  io.output.write(" Turn this repository into an LLM-ready, redacted, byte-bounded snapshot.\n\n");
4895
- io.output.write(` Detected ${detected.presetName}
4983
+ if (fallback) {
4984
+ io.output.write(" \u26A0 Could not tell what kind of project this is.\n");
4985
+ io.output.write(" No package.json, go.mod, Cargo.toml, pyproject.toml, Gemfile,\n");
4986
+ io.output.write(" composer.json, pom.xml, *.csproj, next.config.*, vite.config.*, *.tf\n");
4987
+ io.output.write(" or workspace marker was found here.\n\n");
4988
+ } else {
4989
+ io.output.write(` Detected ${detected.presetName}
4896
4990
  `);
4897
- io.output.write(` Directory ${options.cwd}
4991
+ io.output.write(` Because ${reason}
4898
4992
 
4899
4993
  `);
4900
- const useDetected = await confirm(io, `Set up for ${detected.presetName}?`, {
4901
- defaultValue: true
4902
- });
4903
- if (!useDetected) {
4904
- answers.preset = await select(
4905
- io,
4906
- "What kind of project is this?",
4907
- listPresetNames().map((name) => resolvePresetChoice(answers, name))
4908
- );
4909
- const chosen = detectPreset(options.cwd, answers.preset);
4910
- answers.preset = chosen.presetName;
4911
- }
4912
- const scope = await select(
4913
- io,
4914
- "What should go into the snapshot?",
4915
- [
4916
- { value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
4917
- { value: "folders", label: "Only specific folders\u2026" },
4918
- { value: "no-tests", label: "Everything, but skip tests" },
4919
- { value: "custom", label: "Let me write glob patterns" }
4920
- ],
4921
- { defaultValue: "all" }
4922
- );
4923
- if (scope === "folders") {
4924
- const dirs = topLevelDirs(options.cwd).slice(0, 40);
4925
- if (dirs.length === 0) {
4926
- notes.push("No top-level directories found; the snapshot will include everything.");
4927
- } else {
4928
- const chosen = await multiSelect(
4929
- io,
4930
- "Select folders",
4931
- dirs.map((name) => ({ value: name, label: `${name}/` })),
4932
- { defaults: dirs }
4933
- );
4934
- answers.include = chosen.map((name) => `${name}/**`);
4935
- }
4936
- } else if (scope === "no-tests") {
4937
- answers.excludePatterns = [
4938
- ...answers.excludePatterns,
4939
- "**/*.test.*",
4940
- "**/*.spec.*",
4941
- "tests",
4942
- "test"
4943
- ];
4944
- } else if (scope === "custom") {
4945
- const raw = await text(io, "Glob patterns to include (comma separated)", {
4946
- defaultValue: ""
4947
- });
4948
- answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
4949
- }
4950
- const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
4951
- defaultValue: "120k"
4952
- });
4953
- if (sizeChoice === "custom") {
4954
- const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
4955
- defaultValue: String(answers.maxPartBytes),
4956
- validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null
4957
- });
4958
- answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
4959
- } else {
4960
- answers.maxPartBytes = sizeChoice;
4961
4994
  }
4995
+ io.output.write(` Directory ${options.cwd}
4996
+
4997
+ `);
4998
+ const paceChoices = fallback ? [
4999
+ { value: "choose", label: "Pick the closest preset", hint: "then generate with the defaults" },
5000
+ { value: "recommended", label: "Use the defaults", hint: "generic \xB7 everything the scanner finds" },
5001
+ { value: "walk", label: "Go through each setting", hint: "every question starts on its default" }
5002
+ ] : [
5003
+ {
5004
+ value: "recommended",
5005
+ label: "Use the recommended setup",
5006
+ hint: `${detected.presetName} \xB7 config \xB7 npm scripts \xB7 install \xB7 generate`
5007
+ },
5008
+ { value: "walk", label: "Go through each setting", hint: "every question starts on its default" },
5009
+ { value: "choose", label: "Pick a different preset", hint: "everything else stays at the defaults" }
5010
+ ];
5011
+ const baseInclude = [...answers.include];
5012
+ const baseExclude = [...answers.excludePatterns];
5013
+ let lastScope = "all";
4962
5014
  const artifactChoices = [
4963
5015
  { value: "readme", label: "README.md", hint: "orientation page" },
4964
5016
  { value: "manifest", label: "manifest.json", hint: "required by validate and report" },
@@ -4968,36 +5020,158 @@ async function runInit(options) {
4968
5020
  { value: "audit", label: "reports/audit.md", hint: "security and health findings" },
4969
5021
  { value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
4970
5022
  ];
4971
- const presetEnabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
4972
- const selectedArtifacts = await multiSelect(
4973
- io,
4974
- "Which artifacts should be generated?",
4975
- artifactChoices,
4976
- { defaults: presetEnabled }
4977
- );
4978
- for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
4979
- const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
4980
- answers.includeSensitiveFiles = await confirm(
4981
- io,
4982
- "Include credential files (.env, *.pem, .npmrc, .ssh/)?",
4983
- { defaultValue: false }
4984
- );
4985
- io.output.write("\n Review\n\n");
4986
- io.output.write(` project ${answers.preset}
5023
+ const scopeStep = async () => {
5024
+ for (; ; ) {
5025
+ const scope = await select(
5026
+ io,
5027
+ "What should go into the snapshot?",
5028
+ [
5029
+ { value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
5030
+ { value: "folders", label: "Only specific folders\u2026" },
5031
+ { value: "no-tests", label: "Everything, but skip tests" },
5032
+ { value: "custom", label: "Let me write glob patterns" }
5033
+ ],
5034
+ { defaultValue: lastScope, allowBack: true }
5035
+ );
5036
+ lastScope = scope;
5037
+ answers.include = [...baseInclude];
5038
+ answers.excludePatterns = [...baseExclude];
5039
+ try {
5040
+ if (scope === "folders") {
5041
+ const dirs = topLevelDirs(options.cwd).slice(0, 40);
5042
+ if (dirs.length === 0) {
5043
+ notes.push("No top-level directories found; the snapshot will include everything.");
5044
+ } else {
5045
+ const chosen = await multiSelect(
5046
+ io,
5047
+ "Select folders",
5048
+ dirs.map((name) => ({ value: name, label: `${name}/` })),
5049
+ { defaults: dirs, allowBack: true }
5050
+ );
5051
+ answers.include = chosen.map((name) => `${name}/**`);
5052
+ }
5053
+ } else if (scope === "no-tests") {
5054
+ answers.excludePatterns = [...baseExclude, "**/*.test.*", "**/*.spec.*", "tests", "test"];
5055
+ } else if (scope === "custom") {
5056
+ const raw = await text(io, "Glob patterns to include (comma separated)", {
5057
+ defaultValue: baseInclude.join(", "),
5058
+ allowBack: true
5059
+ });
5060
+ answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
5061
+ }
5062
+ return;
5063
+ } catch (error) {
5064
+ if (error instanceof PromptBack) continue;
5065
+ throw error;
5066
+ }
5067
+ }
5068
+ };
5069
+ const sizeStep = async () => {
5070
+ for (; ; ) {
5071
+ const currentBudget = String(answers.maxPartBytes);
5072
+ const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
5073
+ defaultValue: SIZE_CHOICES.some((choice) => choice.value === currentBudget) ? currentBudget : "custom",
5074
+ allowBack: true
5075
+ });
5076
+ if (sizeChoice !== "custom") {
5077
+ answers.maxPartBytes = sizeChoice;
5078
+ return;
5079
+ }
5080
+ try {
5081
+ const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
5082
+ defaultValue: currentBudget,
5083
+ validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null,
5084
+ allowBack: true
5085
+ });
5086
+ answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
5087
+ return;
5088
+ } catch (error) {
5089
+ if (error instanceof PromptBack) continue;
5090
+ throw error;
5091
+ }
5092
+ }
5093
+ };
5094
+ const artifactsStep = async () => {
5095
+ const enabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
5096
+ const selectedArtifacts = await multiSelect(
5097
+ io,
5098
+ "Which artifacts should be generated?",
5099
+ artifactChoices,
5100
+ { defaults: enabled, allowBack: true }
5101
+ );
5102
+ for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
5103
+ };
5104
+ const credentialsStep = async () => {
5105
+ answers.includeSensitiveFiles = await confirm(
5106
+ io,
5107
+ "Include credential files (.env, *.pem, *.npmrc, *.ssh/)?",
5108
+ { defaultValue: answers.includeSensitiveFiles, allowBack: true }
5109
+ );
5110
+ };
5111
+ const reviewStep = async () => {
5112
+ const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
5113
+ io.output.write("\n Review\n\n");
5114
+ io.output.write(` project ${answers.preset}
4987
5115
  `);
4988
- io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
5116
+ io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
4989
5117
  `);
4990
- io.output.write(` part size ${String(answers.maxPartBytes)}
5118
+ io.output.write(` part size ${String(answers.maxPartBytes)}
4991
5119
  `);
4992
- io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
5120
+ io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
4993
5121
  `);
4994
- io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
5122
+ io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
4995
5123
 
4996
5124
  `);
4997
- io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
4998
- io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
4999
- const proceed = await confirm(io, "Write this and continue?", { defaultValue: true });
5000
- if (!proceed) throw new PromptCancelled();
5125
+ io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
5126
+ io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
5127
+ const proceed = await confirm(io, "Write this and continue?", {
5128
+ defaultValue: true,
5129
+ allowBack: true
5130
+ });
5131
+ if (!proceed) throw new PromptBack();
5132
+ };
5133
+ const walk = async () => {
5134
+ const steps = [scopeStep, sizeStep, artifactsStep, credentialsStep, reviewStep];
5135
+ let index = 0;
5136
+ while (index < steps.length) {
5137
+ try {
5138
+ await steps[index]();
5139
+ index += 1;
5140
+ } catch (error) {
5141
+ if (!(error instanceof PromptBack)) throw error;
5142
+ if (index === 0) return false;
5143
+ index -= 1;
5144
+ io.output.write("\n");
5145
+ }
5146
+ }
5147
+ return true;
5148
+ };
5149
+ for (; ; ) {
5150
+ const pace = await select(io, "How should I set this up?", paceChoices, {
5151
+ defaultValue: fallback ? "choose" : "recommended"
5152
+ });
5153
+ if (pace === "choose") {
5154
+ try {
5155
+ answers.preset = await select(
5156
+ io,
5157
+ "What kind of project is this?",
5158
+ listPresetNames().map(
5159
+ (name) => resolvePresetChoice(fallback ? null : detected.presetName, name)
5160
+ ),
5161
+ { defaultValue: answers.preset, allowBack: true }
5162
+ );
5163
+ } catch (error) {
5164
+ if (error instanceof PromptBack) continue;
5165
+ throw error;
5166
+ }
5167
+ const chosen = detectPreset(options.cwd, answers.preset);
5168
+ answers.preset = chosen.presetName;
5169
+ reason = null;
5170
+ fallback = false;
5171
+ }
5172
+ if (pace === "walk" && !await walk()) continue;
5173
+ break;
5174
+ }
5001
5175
  }
5002
5176
  const configText = renderConfig(answers);
5003
5177
  const preferredName = chooseConfigFileName(options.cwd);
@@ -5021,6 +5195,8 @@ async function runInit(options) {
5021
5195
  written: false,
5022
5196
  reused,
5023
5197
  preset: answers.preset,
5198
+ reason,
5199
+ fallback,
5024
5200
  scripts: [],
5025
5201
  gitignoreEntries: [],
5026
5202
  dependency: { name: GENERATOR_NAME, range: `^${GENERATOR_VERSION}`, added: false },
@@ -5055,6 +5231,8 @@ async function runInit(options) {
5055
5231
  written: true,
5056
5232
  reused,
5057
5233
  preset: answers.preset,
5234
+ reason,
5235
+ fallback,
5058
5236
  scripts,
5059
5237
  gitignoreEntries: ignore.added,
5060
5238
  dependency,
@@ -5062,12 +5240,58 @@ async function runInit(options) {
5062
5240
  };
5063
5241
  }
5064
5242
 
5243
+ // src/wizard/autopilot.ts
5244
+ import * as fs10 from "fs";
5245
+ import * as path11 from "path";
5246
+ function describeTarget(cwd) {
5247
+ const relative11 = path11.relative(process.cwd(), cwd);
5248
+ if (relative11 === "") return "this directory";
5249
+ if (relative11.startsWith("..") || path11.isAbsolute(relative11)) return cwd;
5250
+ return relative11;
5251
+ }
5252
+ function firstRunMessage(reason, cwd) {
5253
+ const where = describeTarget(cwd);
5254
+ if (reason === "empty") {
5255
+ return `Nothing to snapshot: ${where} has no files. Run this inside the repository you want to snapshot, or pass --cwd.`;
5256
+ }
5257
+ return `No configuration found in ${where} and this session is not interactive, so nothing was generated.
5258
+ Choose one:
5259
+ ${GENERATOR_NAME} --autopilot detect the project, set it up and generate, no questions
5260
+ ${GENERATOR_NAME} init --yes write a config from the detected defaults, then generate
5261
+ ${GENERATOR_NAME} init write a config interactively`;
5262
+ }
5263
+ function planFirstRun(facts) {
5264
+ if (facts.emptyTarget) return { action: "refuse", exitCode: 1, reason: "empty" };
5265
+ if (facts.autopilot) return { action: "setup", interactive: false };
5266
+ if (facts.hasConfig || facts.hasPriorSnapshot) return { action: "generate" };
5267
+ if (facts.interactive) return { action: "setup", interactive: true };
5268
+ return { action: "refuse", exitCode: 2, reason: "undecided" };
5269
+ }
5270
+ var NOT_CONTENT = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr", ".DS_Store", "Thumbs.db"]);
5271
+ function isEffectivelyEmpty(cwd) {
5272
+ try {
5273
+ return fs10.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
5274
+ } catch {
5275
+ return false;
5276
+ }
5277
+ }
5278
+ function hasPriorSnapshot(cwd, outDir) {
5279
+ let parsed;
5280
+ try {
5281
+ parsed = JSON.parse(fs10.readFileSync(path11.join(cwd, outDir, ".codebase-snapshot"), "utf8"));
5282
+ } catch {
5283
+ return false;
5284
+ }
5285
+ if (parsed === null || typeof parsed !== "object") return false;
5286
+ return isOurGeneratorName(parsed.generator);
5287
+ }
5288
+
5065
5289
  // src/index.ts
5066
5290
  import * as crypto3 from "crypto";
5067
5291
  import { AsyncLocalStorage } from "async_hooks";
5068
- import * as fs10 from "fs";
5292
+ import * as fs11 from "fs";
5069
5293
  import * as os from "os";
5070
- import * as path11 from "path";
5294
+ import * as path12 from "path";
5071
5295
 
5072
5296
  // src/ast/profiles/shared.ts
5073
5297
  var MAX_SUMMARY_CHARS = 1e6;
@@ -5291,9 +5515,9 @@ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)
5291
5515
  var jvmProfile = {
5292
5516
  id: "jvm",
5293
5517
  extensions: [".java", ".kt", ".kts", ".scala"],
5294
- summarize(path14, content) {
5518
+ summarize(path18, content) {
5295
5519
  const summary = emptySummary();
5296
- const lower = path14.toLowerCase();
5520
+ const lower = path18.toLowerCase();
5297
5521
  const isJava = lower.endsWith(".java");
5298
5522
  const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
5299
5523
  eachLine(content, (line) => {
@@ -5749,16 +5973,16 @@ function summarizeTypeScriptSource(relativePath, content) {
5749
5973
  // src/ast/profiles/sfc.ts
5750
5974
  var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
5751
5975
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
5752
- function componentName(path14) {
5753
- const base = path14.split("/").pop() ?? path14;
5976
+ function componentName(path18) {
5977
+ const base = path18.split("/").pop() ?? path18;
5754
5978
  return base.replace(/\.[^.]+$/, "");
5755
5979
  }
5756
5980
  var sfcProfile = {
5757
5981
  id: "sfc",
5758
5982
  extensions: [".vue", ".svelte", ".astro"],
5759
- summarize(path14, content) {
5983
+ summarize(path18, content) {
5760
5984
  let script = "";
5761
- if (path14.toLowerCase().endsWith(".astro")) {
5985
+ if (path18.toLowerCase().endsWith(".astro")) {
5762
5986
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
5763
5987
  if (frontmatter) script += `${frontmatter[1]}
5764
5988
  `;
@@ -5767,7 +5991,7 @@ var sfcProfile = {
5767
5991
  script += `${match[1]}
5768
5992
  `;
5769
5993
  }
5770
- const name = componentName(path14);
5994
+ const name = componentName(path18);
5771
5995
  if (!script.trim()) {
5772
5996
  const summary2 = emptySummary();
5773
5997
  if (name) {
@@ -5801,15 +6025,15 @@ for (const profile of LANGUAGE_PROFILES) {
5801
6025
  BY_EXTENSION.set(extension.toLowerCase(), profile);
5802
6026
  }
5803
6027
  }
5804
- function profileForPath(path14) {
5805
- const lower = path14.toLowerCase();
6028
+ function profileForPath(path18) {
6029
+ const lower = path18.toLowerCase();
5806
6030
  const dot = lower.lastIndexOf(".");
5807
6031
  if (dot === -1) return null;
5808
6032
  return BY_EXTENSION.get(lower.slice(dot)) ?? null;
5809
6033
  }
5810
- function summarizeWithProfile(path14, content) {
5811
- const profile = profileForPath(path14);
5812
- return profile ? profile.summarize(path14, content) : null;
6034
+ function summarizeWithProfile(path18, content) {
6035
+ const profile = profileForPath(path18);
6036
+ return profile ? profile.summarize(path18, content) : null;
5813
6037
  }
5814
6038
 
5815
6039
  // src/ast/structured.ts
@@ -6739,13 +6963,13 @@ function buildImportResolver(files) {
6739
6963
  const relativePath = file.relativePath;
6740
6964
  remember(byPath, relativePath, relativePath);
6741
6965
  remember(byPathFolded, relativePath.toLowerCase(), relativePath);
6742
- const extension = path11.posix.extname(relativePath);
6966
+ const extension = path12.posix.extname(relativePath);
6743
6967
  if (!SOURCE_EXTENSIONS.has(extension)) continue;
6744
6968
  const stem = relativePath.slice(0, -extension.length);
6745
6969
  remember(byStem, stem, relativePath);
6746
6970
  remember(byStemFolded, stem.toLowerCase(), relativePath);
6747
- if (path11.posix.basename(stem) === "index") {
6748
- const directory = path11.posix.dirname(stem);
6971
+ if (path12.posix.basename(stem) === "index") {
6972
+ const directory = path12.posix.dirname(stem);
6749
6973
  remember(byStem, directory, relativePath);
6750
6974
  remember(byStemFolded, directory.toLowerCase(), relativePath);
6751
6975
  }
@@ -6754,8 +6978,8 @@ function buildImportResolver(files) {
6754
6978
  }
6755
6979
  function resolveSpecifier(fromFile, specifier, resolver) {
6756
6980
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
6757
- const joined = path11.posix.normalize(
6758
- path11.posix.join(path11.posix.dirname(fromFile), specifier)
6981
+ const joined = path12.posix.normalize(
6982
+ path12.posix.join(path12.posix.dirname(fromFile), specifier)
6759
6983
  );
6760
6984
  const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
6761
6985
  const direct = exact(joined);
@@ -6848,11 +7072,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
6848
7072
  return errors;
6849
7073
  }
6850
7074
  function resolveSafeOutputDirs(config) {
6851
- const cwd = path11.resolve(config.cwd);
6852
- const outDir = path11.resolve(cwd, config.outDir);
6853
- const tmpDir = path11.resolve(cwd, config.tmpDir);
6854
- const root = path11.parse(cwd).root;
6855
- const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path11.sep}`);
7075
+ const cwd = path12.resolve(config.cwd);
7076
+ const outDir = path12.resolve(cwd, config.outDir);
7077
+ const tmpDir = path12.resolve(cwd, config.tmpDir);
7078
+ const root = path12.parse(cwd).root;
7079
+ const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path12.sep}`);
6856
7080
  const reject = (reason) => {
6857
7081
  throw new Error(
6858
7082
  `Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
@@ -6908,14 +7132,14 @@ function listEntries(dir) {
6908
7132
  const walk = (absolute, prefix) => {
6909
7133
  let children;
6910
7134
  try {
6911
- children = fs10.readdirSync(absolute, { withFileTypes: true });
7135
+ children = fs11.readdirSync(absolute, { withFileTypes: true });
6912
7136
  } catch {
6913
7137
  return;
6914
7138
  }
6915
7139
  for (const child of children) {
6916
- const relative8 = prefix === "" ? child.name : `${prefix}/${child.name}`;
6917
- entries.push(relative8);
6918
- if (child.isDirectory()) walk(path11.join(absolute, child.name), relative8);
7140
+ const relative11 = prefix === "" ? child.name : `${prefix}/${child.name}`;
7141
+ entries.push(relative11);
7142
+ if (child.isDirectory()) walk(path12.join(absolute, child.name), relative11);
6919
7143
  }
6920
7144
  };
6921
7145
  walk(dir, "");
@@ -6932,28 +7156,28 @@ function markerPayload(dir, state, id, createdAt) {
6932
7156
  pid: process.pid,
6933
7157
  entries: listEntries(dir)
6934
7158
  };
6935
- const manifestPath = path11.join(dir, "manifest.json");
6936
- if (fs10.existsSync(manifestPath)) {
6937
- marker2.manifestSha256 = sha256Hex2(fs10.readFileSync(manifestPath));
7159
+ const manifestPath = path12.join(dir, "manifest.json");
7160
+ if (fs11.existsSync(manifestPath)) {
7161
+ marker2.manifestSha256 = sha256Hex2(fs11.readFileSync(manifestPath));
6938
7162
  }
6939
7163
  return marker2;
6940
7164
  }
6941
7165
  function writeOwnershipMarker(dir, marker2) {
6942
- fs10.writeFileSync(
6943
- path11.join(dir, OWNERSHIP_MARKER),
7166
+ fs11.writeFileSync(
7167
+ path12.join(dir, OWNERSHIP_MARKER),
6944
7168
  `${JSON.stringify(marker2, null, 2)}
6945
7169
  `,
6946
7170
  "utf8"
6947
7171
  );
6948
7172
  }
6949
7173
  function readOwnershipMarker(dir) {
6950
- const markerPath = path11.join(dir, OWNERSHIP_MARKER);
6951
- if (!fs10.existsSync(markerPath)) {
7174
+ const markerPath = path12.join(dir, OWNERSHIP_MARKER);
7175
+ if (!fs11.existsSync(markerPath)) {
6952
7176
  return { marker: null, problem: "it holds no .codebase-snapshot marker" };
6953
7177
  }
6954
7178
  let raw;
6955
7179
  try {
6956
- raw = fs10.readFileSync(markerPath, "utf8");
7180
+ raw = fs11.readFileSync(markerPath, "utf8");
6957
7181
  } catch {
6958
7182
  return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
6959
7183
  }
@@ -7021,11 +7245,11 @@ function assessSnapshotDirectory(dir) {
7021
7245
  marker: marker2
7022
7246
  };
7023
7247
  }
7024
- const manifestPath = path11.join(dir, "manifest.json");
7025
- if (fs10.existsSync(manifestPath)) {
7248
+ const manifestPath = path12.join(dir, "manifest.json");
7249
+ if (fs11.existsSync(manifestPath)) {
7026
7250
  let raw;
7027
7251
  try {
7028
- raw = fs10.readFileSync(manifestPath, "utf8");
7252
+ raw = fs11.readFileSync(manifestPath, "utf8");
7029
7253
  } catch {
7030
7254
  return {
7031
7255
  owned: false,
@@ -7107,13 +7331,13 @@ function containsNoFiles(dir) {
7107
7331
  const current = pending.pop();
7108
7332
  let entries;
7109
7333
  try {
7110
- entries = fs10.readdirSync(current, { withFileTypes: true });
7334
+ entries = fs11.readdirSync(current, { withFileTypes: true });
7111
7335
  } catch {
7112
7336
  return false;
7113
7337
  }
7114
7338
  for (const entry of entries) {
7115
7339
  if (entry.isDirectory()) {
7116
- pending.push(path11.join(current, entry.name));
7340
+ pending.push(path12.join(current, entry.name));
7117
7341
  } else if (!IGNORABLE_ENTRIES.has(entry.name)) {
7118
7342
  return false;
7119
7343
  }
@@ -7122,13 +7346,13 @@ function containsNoFiles(dir) {
7122
7346
  return true;
7123
7347
  }
7124
7348
  function assertReplaceable(target, label, secretRules) {
7125
- if (!fs10.existsSync(target)) return;
7126
- if (!fs10.statSync(target).isDirectory()) {
7349
+ if (!fs11.existsSync(target)) return;
7350
+ if (!fs11.statSync(target).isDirectory()) {
7127
7351
  throw new Error(
7128
7352
  `Refusing to write into ${label} ${target}: it exists but is not a directory.`
7129
7353
  );
7130
7354
  }
7131
- const entries = fs10.readdirSync(target);
7355
+ const entries = fs11.readdirSync(target);
7132
7356
  if (entries.length === 0) return;
7133
7357
  if (entries.every((name) => IGNORABLE_ENTRIES.has(name))) return;
7134
7358
  if (containsNoFiles(target)) return;
@@ -7148,18 +7372,18 @@ function assertReplaceable(target, label, secretRules) {
7148
7372
  );
7149
7373
  }
7150
7374
  function assertStagingRootUsable(tmpDir, secretRules) {
7151
- if (!fs10.existsSync(tmpDir)) return;
7152
- if (!fs10.statSync(tmpDir).isDirectory()) {
7375
+ if (!fs11.existsSync(tmpDir)) return;
7376
+ if (!fs11.statSync(tmpDir).isDirectory()) {
7153
7377
  throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
7154
7378
  }
7155
- const entries = fs10.readdirSync(tmpDir);
7379
+ const entries = fs11.readdirSync(tmpDir);
7156
7380
  if (entries.length === 0) return;
7157
7381
  const foreign = entries.filter((name) => {
7158
- const full = path11.join(tmpDir, name);
7382
+ const full = path12.join(tmpDir, name);
7159
7383
  if (!name.startsWith(RUN_DIR_PREFIX)) return true;
7160
7384
  let stats;
7161
7385
  try {
7162
- stats = fs10.statSync(full);
7386
+ stats = fs11.statSync(full);
7163
7387
  } catch {
7164
7388
  return true;
7165
7389
  }
@@ -7167,7 +7391,7 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7167
7391
  const { marker: marker2 } = readOwnershipMarker(full);
7168
7392
  if (marker2) return false;
7169
7393
  try {
7170
- return fs10.readdirSync(full).length > 0;
7394
+ return fs11.readdirSync(full).length > 0;
7171
7395
  } catch {
7172
7396
  return true;
7173
7397
  }
@@ -7182,13 +7406,13 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7182
7406
  function cleanStaleRunDirs(tmpDir) {
7183
7407
  let entries;
7184
7408
  try {
7185
- entries = fs10.readdirSync(tmpDir);
7409
+ entries = fs11.readdirSync(tmpDir);
7186
7410
  } catch {
7187
7411
  return;
7188
7412
  }
7189
7413
  for (const name of entries) {
7190
7414
  if (!name.startsWith(RUN_DIR_PREFIX)) continue;
7191
- const full = path11.join(tmpDir, name);
7415
+ const full = path12.join(tmpDir, name);
7192
7416
  const { marker: marker2 } = readOwnershipMarker(full);
7193
7417
  if (marker2) {
7194
7418
  const created = Date.parse(marker2.createdAt);
@@ -7196,33 +7420,33 @@ function cleanStaleRunDirs(tmpDir) {
7196
7420
  const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
7197
7421
  if (aged || abandoned) {
7198
7422
  try {
7199
- fs10.rmSync(full, { recursive: true, force: true });
7423
+ fs11.rmSync(full, { recursive: true, force: true });
7200
7424
  } catch {
7201
7425
  }
7202
7426
  }
7203
7427
  continue;
7204
7428
  }
7205
7429
  try {
7206
- if (fs10.readdirSync(full).length === 0) fs10.rmSync(full, { recursive: true, force: true });
7430
+ if (fs11.readdirSync(full).length === 0) fs11.rmSync(full, { recursive: true, force: true });
7207
7431
  } catch {
7208
7432
  }
7209
7433
  }
7210
7434
  }
7211
7435
  function removeIfEmpty(dir) {
7212
7436
  try {
7213
- if (fs10.readdirSync(dir).length === 0) fs10.rmSync(dir, { recursive: true, force: true });
7437
+ if (fs11.readdirSync(dir).length === 0) fs11.rmSync(dir, { recursive: true, force: true });
7214
7438
  } catch {
7215
7439
  }
7216
7440
  }
7217
7441
  function ensureParentDirectory(target, label) {
7218
- const parent = path11.dirname(target);
7219
- if (fs10.existsSync(parent) && !fs10.statSync(parent).isDirectory()) {
7442
+ const parent = path12.dirname(target);
7443
+ if (fs11.existsSync(parent) && !fs11.statSync(parent).isDirectory()) {
7220
7444
  throw new Error(
7221
7445
  `Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
7222
7446
  );
7223
7447
  }
7224
7448
  try {
7225
- fs10.mkdirSync(parent, { recursive: true });
7449
+ fs11.mkdirSync(parent, { recursive: true });
7226
7450
  } catch (err) {
7227
7451
  throw new Error(
7228
7452
  `Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
@@ -7231,7 +7455,7 @@ function ensureParentDirectory(target, label) {
7231
7455
  }
7232
7456
  function acquireRunLock(stagingRoot) {
7233
7457
  const key = lockKeyFor(stagingRoot);
7234
- const lockPath = path11.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
7458
+ const lockPath = path12.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
7235
7459
  const payload = `${JSON.stringify({
7236
7460
  pid: process.pid,
7237
7461
  stagingRoot,
@@ -7240,22 +7464,22 @@ function acquireRunLock(stagingRoot) {
7240
7464
  `;
7241
7465
  for (let attempt = 0; attempt < 2; attempt += 1) {
7242
7466
  try {
7243
- const handle = fs10.openSync(lockPath, "wx");
7467
+ const handle = fs11.openSync(lockPath, "wx");
7244
7468
  try {
7245
- fs10.writeSync(handle, payload);
7469
+ fs11.writeSync(handle, payload);
7246
7470
  } finally {
7247
- fs10.closeSync(handle);
7471
+ fs11.closeSync(handle);
7248
7472
  }
7249
7473
  return () => {
7250
7474
  try {
7251
- fs10.rmSync(lockPath, { force: true });
7475
+ fs11.rmSync(lockPath, { force: true });
7252
7476
  } catch {
7253
7477
  }
7254
7478
  };
7255
7479
  } catch (err) {
7256
7480
  if (err.code !== "EEXIST") throw err;
7257
7481
  if (attempt === 0 && lockOwnerIsGone(lockPath)) {
7258
- fs10.rmSync(lockPath, { force: true });
7482
+ fs11.rmSync(lockPath, { force: true });
7259
7483
  continue;
7260
7484
  }
7261
7485
  throw new Error(
@@ -7268,7 +7492,7 @@ function acquireRunLock(stagingRoot) {
7268
7492
  function lockKeyFor(stagingRoot) {
7269
7493
  let key = stagingRoot;
7270
7494
  try {
7271
- key = fs10.realpathSync(stagingRoot);
7495
+ key = fs11.realpathSync(stagingRoot);
7272
7496
  } catch {
7273
7497
  }
7274
7498
  if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
@@ -7277,7 +7501,7 @@ function lockKeyFor(stagingRoot) {
7277
7501
  function lockOwnerIsGone(lockPath) {
7278
7502
  let raw;
7279
7503
  try {
7280
- raw = fs10.readFileSync(lockPath, "utf8");
7504
+ raw = fs11.readFileSync(lockPath, "utf8");
7281
7505
  } catch {
7282
7506
  return false;
7283
7507
  }
@@ -7297,12 +7521,12 @@ function lockOwnerIsGone(lockPath) {
7297
7521
  }
7298
7522
  function writeManifestAtomically(manifestPath, manifest) {
7299
7523
  const tempPath = `${manifestPath}.${process.pid}.tmp`;
7300
- fs10.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
7524
+ fs11.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
7301
7525
  try {
7302
- fs10.renameSync(tempPath, manifestPath);
7526
+ fs11.renameSync(tempPath, manifestPath);
7303
7527
  } catch (err) {
7304
7528
  try {
7305
- fs10.rmSync(tempPath, { force: true });
7529
+ fs11.rmSync(tempPath, { force: true });
7306
7530
  } catch {
7307
7531
  }
7308
7532
  throw err;
@@ -7325,31 +7549,31 @@ function swapIntoPlace(stagingDir, outDir) {
7325
7549
  }
7326
7550
  function swapOnce(stagingDir, outDir) {
7327
7551
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
7328
- const hadPrevious = fs10.existsSync(outDir);
7329
- if (hadPrevious) fs10.renameSync(outDir, backupDir);
7552
+ const hadPrevious = fs11.existsSync(outDir);
7553
+ if (hadPrevious) fs11.renameSync(outDir, backupDir);
7330
7554
  try {
7331
7555
  try {
7332
- fs10.renameSync(stagingDir, outDir);
7556
+ fs11.renameSync(stagingDir, outDir);
7333
7557
  } catch (err) {
7334
7558
  if (err.code !== "EXDEV") throw err;
7335
7559
  const partialDir = `${outDir}.partial-${process.pid}-${Date.now()}`;
7336
7560
  try {
7337
- fs10.cpSync(stagingDir, partialDir, { recursive: true });
7338
- fs10.renameSync(partialDir, outDir);
7339
- fs10.rmSync(stagingDir, { recursive: true, force: true });
7561
+ fs11.cpSync(stagingDir, partialDir, { recursive: true });
7562
+ fs11.renameSync(partialDir, outDir);
7563
+ fs11.rmSync(stagingDir, { recursive: true, force: true });
7340
7564
  } catch (copyErr) {
7341
7565
  try {
7342
- fs10.rmSync(partialDir, { recursive: true, force: true });
7566
+ fs11.rmSync(partialDir, { recursive: true, force: true });
7343
7567
  } catch {
7344
7568
  }
7345
7569
  throw copyErr;
7346
7570
  }
7347
7571
  }
7348
- if (hadPrevious) fs10.rmSync(backupDir, { recursive: true, force: true });
7572
+ if (hadPrevious) fs11.rmSync(backupDir, { recursive: true, force: true });
7349
7573
  } catch (err) {
7350
- if (hadPrevious && fs10.existsSync(backupDir)) {
7351
- if (fs10.existsSync(outDir)) fs10.rmSync(outDir, { recursive: true, force: true });
7352
- fs10.renameSync(backupDir, outDir);
7574
+ if (hadPrevious && fs11.existsSync(backupDir)) {
7575
+ if (fs11.existsSync(outDir)) fs11.rmSync(outDir, { recursive: true, force: true });
7576
+ fs11.renameSync(backupDir, outDir);
7353
7577
  }
7354
7578
  throw err;
7355
7579
  }
@@ -7444,8 +7668,8 @@ function generateSnapshot(configInput, options) {
7444
7668
  // first run cannot have marked them yet, so the name alone keeps a run from recording its own
7445
7669
  // output — but only at these exact paths, not at every directory that shares the name.
7446
7670
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
7447
- (directory) => path11.relative(config.cwd, path11.resolve(config.cwd, directory)).replace(/\\/g, "/")
7448
- ).filter((relative8) => relative8.length > 0 && !relative8.startsWith(".."))
7671
+ (directory) => path12.relative(config.cwd, path12.resolve(config.cwd, directory)).replace(/\\/g, "/")
7672
+ ).filter((relative11) => relative11.length > 0 && !relative11.startsWith(".."))
7449
7673
  });
7450
7674
  const files = [];
7451
7675
  const monolithBodies = /* @__PURE__ */ new Map();
@@ -7465,12 +7689,12 @@ function generateSnapshot(configInput, options) {
7465
7689
  let totalSizeBytes = 0;
7466
7690
  let largeFileCount = 0;
7467
7691
  for (const relativePath of collection.files) {
7468
- const absolutePath = path11.join(config.cwd, relativePath);
7692
+ const absolutePath = path12.join(config.cwd, relativePath);
7469
7693
  let stats;
7470
7694
  try {
7471
- stats = fs10.statSync(absolutePath);
7695
+ stats = fs11.statSync(absolutePath);
7472
7696
  if (!stats.isFile()) {
7473
- const nestedRepository = stats.isDirectory() && fs10.existsSync(path11.join(absolutePath, ".git"));
7697
+ const nestedRepository = stats.isDirectory() && fs11.existsSync(path12.join(absolutePath, ".git"));
7474
7698
  skipped.push(
7475
7699
  nestedRepository ? {
7476
7700
  path: relativePath,
@@ -7490,7 +7714,7 @@ function generateSnapshot(configInput, options) {
7490
7714
  }
7491
7715
  let buffer;
7492
7716
  try {
7493
- buffer = fs10.readFileSync(absolutePath);
7717
+ buffer = fs11.readFileSync(absolutePath);
7494
7718
  } catch {
7495
7719
  skipped.push({ path: relativePath, reason: "read-error" });
7496
7720
  continue;
@@ -7820,12 +8044,12 @@ function generateSnapshot(configInput, options) {
7820
8044
  let stagingDir = null;
7821
8045
  try {
7822
8046
  ensureParentDirectory(outDir, "outDir");
7823
- fs10.mkdirSync(tmpDir, { recursive: true });
8047
+ fs11.mkdirSync(tmpDir, { recursive: true });
7824
8048
  cleanStaleRunDirs(tmpDir);
7825
8049
  const snapshotId = crypto3.randomUUID();
7826
8050
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7827
- stagingDir = path11.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
7828
- fs10.mkdirSync(stagingDir);
8051
+ stagingDir = path12.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
8052
+ fs11.mkdirSync(stagingDir);
7829
8053
  writeOwnershipMarker(stagingDir, {
7830
8054
  generator: GENERATOR_NAME,
7831
8055
  version: GENERATOR_VERSION,
@@ -7835,41 +8059,41 @@ function generateSnapshot(configInput, options) {
7835
8059
  id: snapshotId,
7836
8060
  pid: process.pid
7837
8061
  });
7838
- const partsDir = path11.join(stagingDir, "parts");
7839
- const reportsDir = path11.join(stagingDir, "reports");
7840
- if (artifacts.parts) fs10.mkdirSync(partsDir, { recursive: true });
8062
+ const partsDir = path12.join(stagingDir, "parts");
8063
+ const reportsDir = path12.join(stagingDir, "reports");
8064
+ if (artifacts.parts) fs11.mkdirSync(partsDir, { recursive: true });
7841
8065
  if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
7842
- fs10.mkdirSync(reportsDir, { recursive: true });
8066
+ fs11.mkdirSync(reportsDir, { recursive: true });
7843
8067
  }
7844
8068
  if (artifacts.parts) {
7845
8069
  for (const part of parts) {
7846
- fs10.writeFileSync(path11.join(partsDir, part.filename), part.markdown, "utf8");
8070
+ fs11.writeFileSync(path12.join(partsDir, part.filename), part.markdown, "utf8");
7847
8071
  }
7848
8072
  }
7849
8073
  if (projectTree !== null) {
7850
- fs10.writeFileSync(path11.join(reportsDir, "project-tree.md"), projectTree, "utf8");
8074
+ fs11.writeFileSync(path12.join(reportsDir, "project-tree.md"), projectTree, "utf8");
7851
8075
  }
7852
8076
  if (artifacts.navigation) {
7853
- fs10.writeFileSync(path11.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
8077
+ fs11.writeFileSync(path12.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
7854
8078
  }
7855
8079
  if (artifacts.audit) {
7856
- fs10.writeFileSync(
7857
- path11.join(reportsDir, "audit.md"),
8080
+ fs11.writeFileSync(
8081
+ path12.join(reportsDir, "audit.md"),
7858
8082
  generateAuditReport(manifest, secretFindings, config.secretRules),
7859
8083
  "utf8"
7860
8084
  );
7861
8085
  }
7862
8086
  if (artifacts.readme) {
7863
- fs10.writeFileSync(path11.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
8087
+ fs11.writeFileSync(path12.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
7864
8088
  }
7865
8089
  if (artifacts.manifest) {
7866
- fs10.writeFileSync(path11.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
8090
+ fs11.writeFileSync(path12.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
7867
8091
  }
7868
8092
  if (monolithJson !== null) {
7869
- fs10.writeFileSync(path11.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
8093
+ fs11.writeFileSync(path12.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
7870
8094
  }
7871
8095
  if (monolithMarkdown !== null) {
7872
- fs10.writeFileSync(path11.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
8096
+ fs11.writeFileSync(path12.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
7873
8097
  }
7874
8098
  writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
7875
8099
  swapIntoPlace(stagingDir, outDir);
@@ -7878,7 +8102,7 @@ function generateSnapshot(configInput, options) {
7878
8102
  const pendingHook = runAfterGenerate(config, manifest);
7879
8103
  const shipHookState = () => {
7880
8104
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
7881
- writeManifestAtomically(path11.join(outDir, "manifest.json"), manifest);
8105
+ writeManifestAtomically(path12.join(outDir, "manifest.json"), manifest);
7882
8106
  }
7883
8107
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
7884
8108
  };
@@ -7901,7 +8125,7 @@ function generateSnapshot(configInput, options) {
7901
8125
  } catch (err) {
7902
8126
  if (stagingDir !== null) {
7903
8127
  try {
7904
- fs10.rmSync(stagingDir, { recursive: true, force: true });
8128
+ fs11.rmSync(stagingDir, { recursive: true, force: true });
7905
8129
  } catch {
7906
8130
  }
7907
8131
  }
@@ -7912,143 +8136,1144 @@ function generateSnapshot(configInput, options) {
7912
8136
  }
7913
8137
  }
7914
8138
 
7915
- // src/agent/option-scope.ts
7916
- var VALIDATE_OPTIONS = [
7917
- "cwd",
7918
- "out-dir",
7919
- "max-part-bytes",
7920
- "large-file-bytes",
7921
- "max-monolith-bytes",
7922
- "include-monolith",
7923
- "include-sensitive-files",
7924
- "skip-artifacts",
7925
- "only-artifacts",
7926
- "no-config",
7927
- "discovery",
7928
- "format",
7929
- "mode",
7930
- "help",
7931
- "version"
7932
- ];
7933
- var COMMAND_OPTION_SCOPE = {
7934
- generate: [
7935
- "cwd",
7936
- "out-dir",
7937
- "max-part-bytes",
7938
- "large-file-bytes",
7939
- "max-monolith-bytes",
7940
- "include-monolith",
7941
- "include-sensitive-files",
7942
- "skip-artifacts",
7943
- "only-artifacts",
7944
- "no-config",
7945
- "discovery",
7946
- "format",
7947
- "mode",
7948
- "dry-run",
7949
- "watch",
7950
- "help",
7951
- "version"
7952
- ],
7953
- validate: VALIDATE_OPTIONS,
7954
- report: [
7955
- "cwd",
7956
- "out-dir",
7957
- "max-part-bytes",
7958
- "large-file-bytes",
7959
- "max-monolith-bytes",
7960
- "include-monolith",
7961
- "include-sensitive-files",
7962
- "skip-artifacts",
7963
- "only-artifacts",
7964
- "no-config",
7965
- "format",
7966
- "mode",
7967
- "help",
7968
- "version"
7969
- ],
7970
- explain: [
7971
- "cwd",
7972
- "out-dir",
7973
- "max-part-bytes",
7974
- "large-file-bytes",
7975
- "max-monolith-bytes",
7976
- "include-monolith",
7977
- "include-sensitive-files",
7978
- "skip-artifacts",
7979
- "only-artifacts",
7980
- "no-config",
7981
- "discovery",
7982
- "format",
7983
- "mode",
7984
- "help",
7985
- "version"
7986
- ],
7987
- diff: ["format", "mode", "no-config", "help", "version"],
7988
- describe: ["format", "mode", "help", "version"],
7989
- init: [
7990
- "cwd",
7991
- "out-dir",
7992
- "max-part-bytes",
7993
- "include-sensitive-files",
7994
- "skip-artifacts",
7995
- "only-artifacts",
7996
- "preset",
7997
- "include",
7998
- "exclude",
7999
- "yes",
8000
- "force",
8001
- "dry-run",
8002
- "format",
8003
- "mode",
8004
- "help",
8005
- "version"
8006
- ],
8007
- skill: ["cwd", "format", "mode", "install", "target", "help", "version"],
8008
- setup: [
8009
- "cwd",
8010
- "out-dir",
8011
- "max-part-bytes",
8012
- "include-sensitive-files",
8013
- "skip-artifacts",
8014
- "only-artifacts",
8015
- "preset",
8016
- "include",
8017
- "exclude",
8018
- "yes",
8019
- "force",
8020
- "no-install",
8021
- "dry-run",
8022
- "format",
8023
- "mode",
8024
- "help",
8025
- "version"
8026
- ]
8027
- };
8028
- var COMMAND_ORDER = [
8029
- "generate",
8030
- "validate",
8031
- "report",
8032
- "explain",
8033
- "init",
8034
- "setup",
8035
- "diff",
8036
- "describe",
8037
- "skill"
8038
- ];
8039
- function commandsForOption(option) {
8040
- return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
8139
+ // src/mcp/tools.ts
8140
+ import * as fs13 from "fs";
8141
+ import * as path14 from "path";
8142
+
8143
+ // src/mcp/part.ts
8144
+ var FILE_HEADING = "### FILE: ";
8145
+ var CONTENT_MARKERS = /* @__PURE__ */ new Set(["#### Content", "#### Bounded Preview"]);
8146
+ function parsePartSections(partText) {
8147
+ const lines = partText.split("\n");
8148
+ const sections = [];
8149
+ let index = 0;
8150
+ while (index < lines.length) {
8151
+ if (!lines[index].startsWith(FILE_HEADING)) {
8152
+ index += 1;
8153
+ continue;
8154
+ }
8155
+ const heading = lines[index].slice(FILE_HEADING.length);
8156
+ let cursor = index + 1;
8157
+ let inclusion = null;
8158
+ let sha256 = null;
8159
+ let includedLines = null;
8160
+ while (cursor < lines.length && !CONTENT_MARKERS.has(lines[cursor])) {
8161
+ const line = lines[cursor];
8162
+ if (line.startsWith(FILE_HEADING)) break;
8163
+ if (line.startsWith("- Inclusion: ")) inclusion = line.slice("- Inclusion: ".length).trim();
8164
+ else if (line.startsWith("- SHA-256: ")) sha256 = line.slice("- SHA-256: ".length).trim();
8165
+ else if (line.startsWith("- Included Lines: ")) {
8166
+ const value = Number(line.slice("- Included Lines: ".length).trim());
8167
+ includedLines = Number.isFinite(value) ? value : null;
8168
+ }
8169
+ cursor += 1;
8170
+ }
8171
+ if (cursor >= lines.length || !CONTENT_MARKERS.has(lines[cursor])) {
8172
+ index = cursor + 1;
8173
+ continue;
8174
+ }
8175
+ let fenceLine = cursor + 1;
8176
+ while (fenceLine < lines.length && lines[fenceLine].trim().length === 0) fenceLine += 1;
8177
+ const fenceMatch = /^(`{4,})/.exec(lines[fenceLine] ?? "");
8178
+ if (!fenceMatch) {
8179
+ index = cursor + 1;
8180
+ continue;
8181
+ }
8182
+ const fence = fenceMatch[1];
8183
+ const body = [];
8184
+ let bodyLine = fenceLine + 1;
8185
+ while (bodyLine < lines.length && lines[bodyLine] !== fence) {
8186
+ body.push(lines[bodyLine]);
8187
+ bodyLine += 1;
8188
+ }
8189
+ sections.push({ path: heading, body: body.join("\n"), inclusion, sha256, includedLines });
8190
+ index = bodyLine + 1;
8191
+ }
8192
+ return sections;
8193
+ }
8194
+ function findSection(sections, relativePath, sha256) {
8195
+ const exact = sections.find((section) => section.path === relativePath);
8196
+ if (exact) return exact;
8197
+ if (sha256) {
8198
+ const byHash = sections.filter((section) => section.sha256 === sha256);
8199
+ if (byHash.length === 1) return byHash[0];
8200
+ if (byHash.length > 1) {
8201
+ const byPath = byHash.find(
8202
+ (section) => section.path.toLowerCase() === relativePath.toLowerCase()
8203
+ );
8204
+ if (byPath) return byPath;
8205
+ }
8206
+ }
8207
+ const folded = sections.filter((section) => section.path.toLowerCase() === relativePath.toLowerCase());
8208
+ return folded.length === 1 ? folded[0] : null;
8041
8209
  }
8042
- function optionAppliesTo(option) {
8043
- return commandsForOption(option).join("/");
8210
+ function windowText(body, fromLine, lineCount) {
8211
+ const lines = body.split("\n");
8212
+ const total = lines.length;
8213
+ const start = Math.min(Math.max(1, Math.trunc(fromLine)), Math.max(1, total));
8214
+ const end = Math.min(total, start + Math.max(1, Math.trunc(lineCount)) - 1);
8215
+ const slice = lines.slice(start - 1, end);
8216
+ return {
8217
+ text: slice.join("\n"),
8218
+ fromLine: start,
8219
+ returnedLines: slice.length,
8220
+ totalLines: total,
8221
+ truncated: end < total,
8222
+ nextFromLine: end < total ? end + 1 : null
8223
+ };
8044
8224
  }
8045
8225
 
8046
- // src/agent/contract.ts
8047
- var CONTRACT_VERSION = "1.0.0";
8048
- var PACKAGE_NAME = "codebase-snapshot";
8049
- var MAJOR_VERSION = GENERATOR_VERSION.split(".")[0];
8050
- var ARTIFACT_SUMMARY = {
8051
- readme: "README.md describing the snapshot at the output root.",
8226
+ // src/mcp/workspace.ts
8227
+ import * as fs12 from "fs";
8228
+ import * as path13 from "path";
8229
+ function foldForComparison(value) {
8230
+ return process.platform === "win32" || process.platform === "darwin" ? value.toLowerCase() : value;
8231
+ }
8232
+ function canonicalPath(value) {
8233
+ const absolute = path13.resolve(value);
8234
+ try {
8235
+ return fs12.realpathSync(absolute);
8236
+ } catch {
8237
+ return absolute;
8238
+ }
8239
+ }
8240
+ function isInside2(parent, child) {
8241
+ const foldedParent = foldForComparison(parent);
8242
+ const foldedChild = foldForComparison(child);
8243
+ return foldedChild === foldedParent || foldedChild.startsWith(`${foldedParent}${path13.sep}`);
8244
+ }
8245
+ function resolveRoot(requested, context) {
8246
+ if (requested !== void 0 && (typeof requested !== "string" || requested.trim().length === 0)) {
8247
+ return { ok: false, message: '"root" must be a non-empty string when provided.' };
8248
+ }
8249
+ const wanted = typeof requested === "string" ? requested.trim() : context.cwd;
8250
+ const absolute = path13.resolve(context.cwd, wanted);
8251
+ let stats;
8252
+ try {
8253
+ stats = fs12.statSync(absolute);
8254
+ } catch {
8255
+ return { ok: false, message: `"${wanted}" does not exist, so it cannot be a repository root.` };
8256
+ }
8257
+ if (!stats.isDirectory()) {
8258
+ return { ok: false, message: `"${wanted}" is not a directory.` };
8259
+ }
8260
+ const resolved = canonicalPath(absolute);
8261
+ for (const allowed of context.roots) {
8262
+ if (isInside2(canonicalPath(allowed), resolved)) return { ok: true, root: resolved };
8263
+ }
8264
+ const shown = context.roots.join(", ");
8265
+ return {
8266
+ ok: false,
8267
+ message: `"${wanted}" resolves outside the directories this server may read (${shown}). Restart it with --root <path> to add a repository.`
8268
+ };
8269
+ }
8270
+ async function openWorkspace(root, overrides = {}) {
8271
+ const loaded = await loadConfigFileDetailed(root);
8272
+ if (loaded.errors.length > 0) {
8273
+ return {
8274
+ ok: false,
8275
+ message: `The config file could not be read: ${loaded.errors.join("; ")} Fix it, or delete it to use the defaults \u2014 this server will not silently ignore it.`
8276
+ };
8277
+ }
8278
+ const cliOptions = { cwd: root };
8279
+ if (overrides.maxPartBytes !== void 0) {
8280
+ const parsed = parseByteSize(overrides.maxPartBytes);
8281
+ if (parsed === void 0) {
8282
+ return {
8283
+ ok: false,
8284
+ message: '"maxPartBytes" must be a byte count, for example 307200 or "300k".'
8285
+ };
8286
+ }
8287
+ cliOptions.maxPartBytes = parsed;
8288
+ }
8289
+ if (overrides.outDir !== void 0) {
8290
+ if (typeof overrides.outDir !== "string" || overrides.outDir.trim().length === 0) {
8291
+ return { ok: false, message: '"outDir" must be a non-empty string.' };
8292
+ }
8293
+ cliOptions.outDir = overrides.outDir.trim();
8294
+ }
8295
+ try {
8296
+ const config = resolveConfig(loaded.config, cliOptions, root);
8297
+ return { ok: true, workspace: { root, config, configFile: loaded.file } };
8298
+ } catch (error) {
8299
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
8300
+ }
8301
+ }
8302
+ var manifestCache = /* @__PURE__ */ new Map();
8303
+ function manifestPathFor(workspace) {
8304
+ return path13.resolve(workspace.root, workspace.config.outDir, "manifest.json");
8305
+ }
8306
+ function readManifest3(workspace) {
8307
+ const file = manifestPathFor(workspace);
8308
+ const shown = path13.relative(workspace.root, file).split(path13.sep).join("/") || file;
8309
+ let stats;
8310
+ try {
8311
+ stats = fs12.statSync(file);
8312
+ } catch {
8313
+ return {
8314
+ ok: false,
8315
+ missing: true,
8316
+ message: `There is no snapshot manifest at ${shown}. Call snapshot_plan to preview what would be included, or snapshot_generate to write it.`
8317
+ };
8318
+ }
8319
+ const cached = manifestCache.get(file);
8320
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
8321
+ return { ok: true, manifest: cached.manifest, file };
8322
+ }
8323
+ let raw;
8324
+ try {
8325
+ raw = fs12.readFileSync(file, "utf8");
8326
+ } catch (error) {
8327
+ return {
8328
+ ok: false,
8329
+ missing: false,
8330
+ message: `${shown} exists but could not be read: ${error instanceof Error ? error.message : String(error)}`
8331
+ };
8332
+ }
8333
+ let parsed;
8334
+ try {
8335
+ parsed = JSON.parse(raw);
8336
+ } catch {
8337
+ return { ok: false, missing: false, message: `${shown} is not valid JSON.` };
8338
+ }
8339
+ const problem = describeManifestShapeProblem(parsed);
8340
+ if (problem) {
8341
+ return { ok: false, missing: false, message: `${shown} is not a snapshot manifest: ${problem}` };
8342
+ }
8343
+ const manifest = parsed;
8344
+ manifestCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, manifest });
8345
+ return { ok: true, manifest, file };
8346
+ }
8347
+ function forgetManifest(file) {
8348
+ manifestCache.delete(file);
8349
+ }
8350
+
8351
+ // src/mcp/tools.ts
8352
+ var READ_ONLY = {
8353
+ readOnlyHint: true,
8354
+ destructiveHint: false,
8355
+ idempotentHint: true,
8356
+ openWorldHint: false
8357
+ };
8358
+ var WRITES = {
8359
+ readOnlyHint: false,
8360
+ // It replaces the snapshot directory but never anything it did not write, so it is additive with
8361
+ // respect to the user's files.
8362
+ destructiveHint: false,
8363
+ idempotentHint: true,
8364
+ openWorldHint: false
8365
+ };
8366
+ var MAX_RESULT_BYTES = 64 * 1024;
8367
+ var ROOT_PROPERTY = {
8368
+ type: "string",
8369
+ description: "Repository root. Defaults to the directory this server was started in."
8370
+ };
8371
+ function failure(message) {
8372
+ return { text: message, isError: true };
8373
+ }
8374
+ function plural(count, word) {
8375
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
8376
+ }
8377
+ function formatBytes(bytes) {
8378
+ if (bytes < 1024) return `${bytes} B`;
8379
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
8380
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
8381
+ }
8382
+ function relativeFor(workspace, absolute) {
8383
+ return path14.relative(workspace.root, absolute).split(path14.sep).join("/");
8384
+ }
8385
+ function capped(values, limit = 20) {
8386
+ return { count: values.length, sample: values.slice(0, limit) };
8387
+ }
8388
+ function summarizeDrift(drift) {
8389
+ return {
8390
+ missingInSnapshot: capped(drift.missingInSnapshot),
8391
+ missingOnDisk: capped(drift.missingOnDisk),
8392
+ unreadable: capped(drift.unreadable),
8393
+ hashMismatches: capped(drift.hashMismatches),
8394
+ partIssues: capped(drift.partIssues),
8395
+ totalsMismatches: capped(drift.totalsMismatches),
8396
+ configMismatches: capped(drift.configMismatches)
8397
+ };
8398
+ }
8399
+ function toRelativePath(root, value) {
8400
+ const cleaned = value.trim();
8401
+ if (cleaned.length === 0) return null;
8402
+ if (!path14.isAbsolute(cleaned)) {
8403
+ return cleaned.replace(/\\/g, "/").replace(/^\.\//, "");
8404
+ }
8405
+ const relative11 = path14.relative(root, cleaned);
8406
+ if (relative11.startsWith("..") || path14.isAbsolute(relative11)) return null;
8407
+ return relative11.split(path14.sep).join("/");
8408
+ }
8409
+ function symbolsOf(entry) {
8410
+ const summary = entry.summary;
8411
+ if (!summary || typeof summary !== "object" || !("exports" in summary)) return [];
8412
+ const source = summary;
8413
+ return [...source.exports, ...source.reactComponents, ...source.routeHandlers];
8414
+ }
8415
+ async function prepare(args, context, overrides = {}) {
8416
+ const rootOutcome = resolveRoot(args.root, context);
8417
+ if (!rootOutcome.ok) return { ok: false, outcome: failure(rootOutcome.message) };
8418
+ const opened = await openWorkspace(rootOutcome.root, overrides);
8419
+ if (!opened.ok) return { ok: false, outcome: failure(opened.message) };
8420
+ return { ok: true, workspace: opened.workspace };
8421
+ }
8422
+ var statusTool = {
8423
+ name: "snapshot_status",
8424
+ title: "Snapshot status",
8425
+ description: "Report whether a snapshot exists for this repository, when it was built, and what it covers. Reads the manifest rather than the repository, so it is cheap. Call it before deciding whether a snapshot needs to be generated.",
8426
+ inputSchema: {
8427
+ type: "object",
8428
+ properties: { root: ROOT_PROPERTY },
8429
+ additionalProperties: false
8430
+ },
8431
+ outputSchema: {
8432
+ type: "object",
8433
+ properties: {
8434
+ present: { type: "boolean" },
8435
+ root: { type: "string" },
8436
+ outDir: { type: "string" },
8437
+ fingerprint: { type: "string" },
8438
+ generatedAt: { type: "string" },
8439
+ generatorVersion: { type: "string" },
8440
+ generatorVersionMatches: { type: "boolean" },
8441
+ includedFiles: { type: "integer" },
8442
+ skippedFiles: { type: "integer" },
8443
+ partCount: { type: "integer" },
8444
+ warningCount: { type: "integer" }
8445
+ }
8446
+ },
8447
+ annotations: READ_ONLY,
8448
+ async handler(args, context) {
8449
+ const prepared = await prepare(args, context);
8450
+ if (!prepared.ok) return prepared.outcome;
8451
+ const workspace = prepared.workspace;
8452
+ const read = readManifest3(workspace);
8453
+ if (!read.ok) {
8454
+ return {
8455
+ text: read.message,
8456
+ structured: {
8457
+ present: false,
8458
+ root: workspace.root,
8459
+ outDir: workspace.config.outDir,
8460
+ message: read.message
8461
+ }
8462
+ };
8463
+ }
8464
+ const manifest = read.manifest;
8465
+ const versionMatches = manifest.generator.version === GENERATOR_VERSION;
8466
+ const lines = [
8467
+ `Snapshot: ${workspace.config.outDir}/`,
8468
+ `Generated: ${manifest.generatedAt} by ${manifest.generator.name} ${manifest.generator.version}`,
8469
+ `Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
8470
+ `Content: ${formatBytes(manifest.counts.totalSizeBytes)} of source in ${formatBytes(manifest.totals.totalContentBytes)} emitted`,
8471
+ `Fingerprint: ${manifest.fingerprint}`,
8472
+ `Git: ${manifest.git.branch} @ ${manifest.git.commit}${manifest.git.isDirty ? " (dirty)" : " (clean)"}`
8473
+ ];
8474
+ if (manifest.totals.warningCount > 0) lines.push(`Warnings: ${manifest.totals.warningCount}`);
8475
+ if (!versionMatches) {
8476
+ lines.push(
8477
+ `Note: written by ${manifest.generator.version}, this server is ${GENERATOR_VERSION}. Re-run snapshot_generate if the difference matters.`
8478
+ );
8479
+ }
8480
+ return {
8481
+ text: lines.join("\n"),
8482
+ structured: {
8483
+ present: true,
8484
+ root: workspace.root,
8485
+ outDir: workspace.config.outDir,
8486
+ fingerprint: manifest.fingerprint,
8487
+ generatedAt: manifest.generatedAt,
8488
+ generatorVersion: manifest.generator.version,
8489
+ generatorVersionMatches: versionMatches,
8490
+ includedFiles: manifest.counts.totalFiles,
8491
+ skippedFiles: manifest.counts.skippedCount,
8492
+ partCount: manifest.counts.partsCount,
8493
+ warningCount: manifest.totals.warningCount,
8494
+ preset: manifest.config.preset,
8495
+ maxPartBytes: manifest.budgets.maxPartBytes
8496
+ }
8497
+ };
8498
+ }
8499
+ };
8500
+ var planTool = {
8501
+ name: "snapshot_plan",
8502
+ title: "Preview a snapshot",
8503
+ description: "Preview a snapshot without writing anything: how many files and parts it would contain, and why every excluded file was excluded. Use it to choose a part budget before generating.",
8504
+ inputSchema: {
8505
+ type: "object",
8506
+ properties: {
8507
+ root: ROOT_PROPERTY,
8508
+ maxPartBytes: {
8509
+ type: "string",
8510
+ description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
8511
+ }
8512
+ },
8513
+ additionalProperties: false
8514
+ },
8515
+ outputSchema: {
8516
+ type: "object",
8517
+ properties: {
8518
+ files: { type: "integer" },
8519
+ skipped: { type: "integer" },
8520
+ parts: { type: "integer" },
8521
+ sourceBytes: { type: "integer" },
8522
+ emittedBytes: { type: "integer" },
8523
+ preset: { type: "string" }
8524
+ }
8525
+ },
8526
+ annotations: READ_ONLY,
8527
+ async handler(args, context) {
8528
+ const prepared = await prepare(args, context, { maxPartBytes: args.maxPartBytes });
8529
+ if (!prepared.ok) return prepared.outcome;
8530
+ const workspace = prepared.workspace;
8531
+ let manifest;
8532
+ try {
8533
+ manifest = generateSnapshot(workspace.config, { dryRun: true });
8534
+ } catch (error) {
8535
+ return failure(error instanceof Error ? error.message : String(error));
8536
+ }
8537
+ const reasons = /* @__PURE__ */ new Map();
8538
+ for (const record of manifest.skipped) {
8539
+ reasons.set(record.reason, (reasons.get(record.reason) ?? 0) + 1);
8540
+ }
8541
+ const topReasons = [...reasons.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 8).map(([reason, count]) => ({ reason, count }));
8542
+ const lines = [
8543
+ "Dry run \u2014 nothing was written.",
8544
+ `Preset: ${manifest.config.preset}`,
8545
+ `Would include ${plural(manifest.counts.totalFiles, "file")} in ${plural(manifest.counts.partsCount, "part")} (${formatBytes(manifest.counts.totalSizeBytes)} of source, ${formatBytes(manifest.totals.totalContentBytes)} emitted).`,
8546
+ `Would skip ${plural(manifest.counts.skippedCount, "file")}.`
8547
+ ];
8548
+ if (topReasons.length > 0) {
8549
+ lines.push("Top skip reasons:");
8550
+ for (const entry of topReasons) lines.push(` - ${entry.reason} (${entry.count})`);
8551
+ }
8552
+ if (manifest.warnings.length > 0) {
8553
+ lines.push(`Warnings (${manifest.warnings.length}):`);
8554
+ for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
8555
+ }
8556
+ return {
8557
+ text: lines.join("\n"),
8558
+ structured: {
8559
+ files: manifest.counts.totalFiles,
8560
+ skipped: manifest.counts.skippedCount,
8561
+ parts: manifest.counts.partsCount,
8562
+ sourceBytes: manifest.counts.totalSizeBytes,
8563
+ emittedBytes: manifest.totals.totalContentBytes,
8564
+ preset: manifest.config.preset,
8565
+ maxPartBytes: manifest.budgets.maxPartBytes,
8566
+ layers: manifest.layers,
8567
+ skipReasons: topReasons,
8568
+ warnings: manifest.warnings.slice(0, 5),
8569
+ warningCount: manifest.warnings.length
8570
+ }
8571
+ };
8572
+ }
8573
+ };
8574
+ var generateTool = {
8575
+ name: "snapshot_generate",
8576
+ title: "Generate the snapshot",
8577
+ description: "Write the snapshot to its output directory and return its fingerprint, parts and counts. Replaces the previous snapshot in that directory. A large repository can take several seconds.",
8578
+ inputSchema: {
8579
+ type: "object",
8580
+ properties: {
8581
+ root: ROOT_PROPERTY,
8582
+ maxPartBytes: {
8583
+ type: "string",
8584
+ description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
8585
+ },
8586
+ outDir: {
8587
+ type: "string",
8588
+ description: "Output directory relative to the root. Defaults to the project configuration."
8589
+ }
8590
+ },
8591
+ additionalProperties: false
8592
+ },
8593
+ outputSchema: {
8594
+ type: "object",
8595
+ properties: {
8596
+ fingerprint: { type: "string" },
8597
+ files: { type: "integer" },
8598
+ skipped: { type: "integer" },
8599
+ parts: { type: "integer" },
8600
+ emittedBytes: { type: "integer" }
8601
+ }
8602
+ },
8603
+ annotations: WRITES,
8604
+ async handler(args, context) {
8605
+ const prepared = await prepare(args, context, {
8606
+ maxPartBytes: args.maxPartBytes,
8607
+ outDir: args.outDir
8608
+ });
8609
+ if (!prepared.ok) return prepared.outcome;
8610
+ const workspace = prepared.workspace;
8611
+ let manifest;
8612
+ try {
8613
+ manifest = generateSnapshot(workspace.config);
8614
+ } catch (error) {
8615
+ return failure(error instanceof Error ? error.message : String(error));
8616
+ }
8617
+ forgetManifest(manifestPathFor(workspace));
8618
+ const written = relativeFor(workspace, path14.resolve(workspace.root, workspace.config.outDir));
8619
+ const lines = [
8620
+ `Wrote ${written}/`,
8621
+ `Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
8622
+ `Parts: ${manifest.parts.map((part) => `${part.filename} (${formatBytes(part.byteSize)})`).join(", ")}`,
8623
+ `Fingerprint: ${manifest.fingerprint}`
8624
+ ];
8625
+ if (manifest.warnings.length > 0) {
8626
+ lines.push(`Warnings (${manifest.warnings.length}):`);
8627
+ for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
8628
+ }
8629
+ return {
8630
+ text: lines.join("\n"),
8631
+ structured: {
8632
+ fingerprint: manifest.fingerprint,
8633
+ files: manifest.counts.totalFiles,
8634
+ skipped: manifest.counts.skippedCount,
8635
+ parts: manifest.counts.partsCount,
8636
+ emittedBytes: manifest.totals.totalContentBytes,
8637
+ outDir: written,
8638
+ partFiles: manifest.parts.map((part) => part.filename),
8639
+ warningCount: manifest.warnings.length
8640
+ }
8641
+ };
8642
+ }
8643
+ };
8644
+ var validateTool = {
8645
+ name: "snapshot_validate",
8646
+ title: "Validate the snapshot",
8647
+ description: "Re-read the repository and report whether the snapshot on disk still matches it. Use it before trusting context that came from an earlier session, or after changing the config.",
8648
+ inputSchema: {
8649
+ type: "object",
8650
+ properties: { root: ROOT_PROPERTY },
8651
+ additionalProperties: false
8652
+ },
8653
+ outputSchema: {
8654
+ type: "object",
8655
+ properties: {
8656
+ current: { type: "boolean" },
8657
+ manifestPresent: { type: "boolean" }
8658
+ }
8659
+ },
8660
+ annotations: READ_ONLY,
8661
+ async handler(args, context) {
8662
+ const prepared = await prepare(args, context);
8663
+ if (!prepared.ok) return prepared.outcome;
8664
+ const workspace = prepared.workspace;
8665
+ let result;
8666
+ try {
8667
+ result = validateSnapshot(workspace.config);
8668
+ } catch (error) {
8669
+ return failure(error instanceof Error ? error.message : String(error));
8670
+ }
8671
+ const lines = [];
8672
+ if (result.valid) {
8673
+ lines.push("The snapshot matches the repository.");
8674
+ } else {
8675
+ lines.push("The snapshot is out of date.");
8676
+ const drift = result.drift;
8677
+ const observations = [
8678
+ ["file(s) changed on disk", drift.hashMismatches.length],
8679
+ ["file(s) added since the snapshot", drift.missingInSnapshot.length],
8680
+ ["file(s) gone since the snapshot", drift.missingOnDisk.length],
8681
+ ["part file(s) damaged or missing", drift.partIssues.length],
8682
+ ["setting(s) changed since the snapshot", drift.configMismatches.length],
8683
+ ["internal count(s) disagreeing", drift.totalsMismatches.length]
8684
+ ];
8685
+ for (const [label, count] of observations) {
8686
+ if (count > 0) lines.push(` - ${count} ${label}`);
8687
+ }
8688
+ for (const error of result.errors.slice(0, 5)) lines.push(` - ${error}`);
8689
+ lines.push("Run snapshot_generate to refresh it.");
8690
+ }
8691
+ for (const warning of result.warnings.slice(0, 5)) lines.push(`Note: ${warning}`);
8692
+ return {
8693
+ text: lines.join("\n"),
8694
+ structured: {
8695
+ current: result.valid,
8696
+ manifestPresent: result.manifestPresent,
8697
+ drift: summarizeDrift(result.drift),
8698
+ errors: result.errors.slice(0, 10),
8699
+ warnings: result.warnings.slice(0, 10)
8700
+ }
8701
+ // A stale snapshot is an answer, not a tool failure: the caller asked whether it is current.
8702
+ };
8703
+ }
8704
+ };
8705
+ var locateTool = {
8706
+ name: "snapshot_locate",
8707
+ title: "Locate code in the snapshot",
8708
+ description: "Find which part holds the code you need. Matches file paths, exported symbols, component names and routes from the snapshot's index, so one part can be read instead of all of them.",
8709
+ inputSchema: {
8710
+ type: "object",
8711
+ properties: {
8712
+ query: {
8713
+ type: "string",
8714
+ description: "Text to match, case-insensitively, against paths and exported symbols."
8715
+ },
8716
+ by: {
8717
+ type: "string",
8718
+ enum: ["auto", "path", "symbol", "route", "layer", "kind"],
8719
+ default: "auto",
8720
+ description: "Restrict what is matched. Defaults to every category."
8721
+ },
8722
+ limit: {
8723
+ type: "integer",
8724
+ minimum: 1,
8725
+ maximum: 100,
8726
+ default: 20,
8727
+ description: "Maximum matches to return."
8728
+ },
8729
+ root: ROOT_PROPERTY
8730
+ },
8731
+ required: ["query"],
8732
+ additionalProperties: false
8733
+ },
8734
+ outputSchema: {
8735
+ type: "object",
8736
+ properties: { total: { type: "integer" } }
8737
+ },
8738
+ annotations: READ_ONLY,
8739
+ async handler(args, context) {
8740
+ const prepared = await prepare(args, context);
8741
+ if (!prepared.ok) return prepared.outcome;
8742
+ const workspace = prepared.workspace;
8743
+ const read = readManifest3(workspace);
8744
+ if (!read.ok) return failure(read.message);
8745
+ const query = String(args.query ?? "").trim();
8746
+ if (query.length === 0) return failure('"query" must not be empty.');
8747
+ const needle = query.toLowerCase();
8748
+ const by = typeof args.by === "string" ? args.by : "auto";
8749
+ const limit = typeof args.limit === "number" ? args.limit : 20;
8750
+ const manifest = read.manifest;
8751
+ const matches = [];
8752
+ const add = (entry, matchedBy, detail) => {
8753
+ matches.push({
8754
+ path: entry.relativePath,
8755
+ part: entry.part,
8756
+ layer: entry.layer,
8757
+ kind: entry.kind,
8758
+ matchedBy,
8759
+ ...detail ? { detail } : {}
8760
+ });
8761
+ };
8762
+ const wants = (category) => by === "auto" || by === category;
8763
+ for (const entry of manifest.files) {
8764
+ if (wants("path") && entry.relativePath.toLowerCase().includes(needle)) {
8765
+ add(entry, "path");
8766
+ continue;
8767
+ }
8768
+ if (wants("symbol")) {
8769
+ const symbol = symbolsOf(entry).find((name) => name.toLowerCase().includes(needle));
8770
+ if (symbol) {
8771
+ add(entry, "symbol", symbol);
8772
+ continue;
8773
+ }
8774
+ }
8775
+ if (wants("layer") && entry.layer.toLowerCase() === needle) add(entry, "layer");
8776
+ else if (wants("kind") && entry.kind.toLowerCase() === needle) add(entry, "kind");
8777
+ }
8778
+ if (wants("route")) {
8779
+ for (const route of manifest.indexes.routes) {
8780
+ if (!route.path.toLowerCase().includes(needle)) continue;
8781
+ if (matches.some((match) => match.path === route.path)) continue;
8782
+ matches.push({
8783
+ path: route.path,
8784
+ part: route.part,
8785
+ layer: "route",
8786
+ kind: "route",
8787
+ matchedBy: "route",
8788
+ detail: route.methods.join(", ")
8789
+ });
8790
+ }
8791
+ }
8792
+ if (matches.length === 0) {
8793
+ return {
8794
+ text: `Nothing in the snapshot matches "${query}". Try a shorter fragment, or snapshot_explain a path to see whether it is included at all.`,
8795
+ structured: { total: 0, matches: [] }
8796
+ };
8797
+ }
8798
+ const shown = matches.slice(0, limit);
8799
+ const lines = [
8800
+ `${matches.length} match${matches.length === 1 ? "" : "es"} for "${query}"` + (matches.length > shown.length ? ` (showing ${shown.length})` : "") + ":"
8801
+ ];
8802
+ for (const match of shown) {
8803
+ const detail = match.detail ? ` \u2014 ${match.detail}` : "";
8804
+ lines.push(` ${match.path}${detail} [${match.part}, ${match.layer}/${match.kind}]`);
8805
+ }
8806
+ if (matches.length > shown.length) {
8807
+ lines.push("Raise `limit` to see more.");
8808
+ }
8809
+ return {
8810
+ text: lines.join("\n"),
8811
+ structured: { total: matches.length, returned: shown.length, matches: shown }
8812
+ };
8813
+ }
8814
+ };
8815
+ var readTool = {
8816
+ name: "snapshot_read",
8817
+ title: "Read a file from the snapshot",
8818
+ description: "Return a line window of one file as the snapshot contains it: redacted, and bounded to a preview when the source is large. Reports the next `fromLine` when more remains.",
8819
+ inputSchema: {
8820
+ type: "object",
8821
+ properties: {
8822
+ path: {
8823
+ type: "string",
8824
+ description: "File path relative to the repository root, as snapshot_locate reports it."
8825
+ },
8826
+ fromLine: {
8827
+ type: "integer",
8828
+ minimum: 1,
8829
+ default: 1,
8830
+ description: "First line to return, 1-based."
8831
+ },
8832
+ lines: {
8833
+ type: "integer",
8834
+ minimum: 1,
8835
+ maximum: 2e3,
8836
+ default: 200,
8837
+ description: "How many lines to return."
8838
+ },
8839
+ root: ROOT_PROPERTY
8840
+ },
8841
+ required: ["path"],
8842
+ additionalProperties: false
8843
+ },
8844
+ outputSchema: {
8845
+ type: "object",
8846
+ properties: {
8847
+ path: { type: "string" },
8848
+ part: { type: "string" },
8849
+ fromLine: { type: "integer" },
8850
+ returnedLines: { type: "integer" },
8851
+ totalLines: { type: "integer" },
8852
+ nextFromLine: { type: "integer" }
8853
+ }
8854
+ },
8855
+ annotations: READ_ONLY,
8856
+ async handler(args, context) {
8857
+ const prepared = await prepare(args, context);
8858
+ if (!prepared.ok) return prepared.outcome;
8859
+ const workspace = prepared.workspace;
8860
+ if (typeof args.path !== "string") return failure('"path" must be a string.');
8861
+ const relativePath = toRelativePath(workspace.root, args.path);
8862
+ if (!relativePath) {
8863
+ return failure(
8864
+ `"${args.path}" is outside ${workspace.root}. Paths are read relative to the repository root.`
8865
+ );
8866
+ }
8867
+ const read = readManifest3(workspace);
8868
+ if (!read.ok) return failure(read.message);
8869
+ const manifest = read.manifest;
8870
+ const entry = manifest.files.find((file) => file.relativePath === relativePath) ?? manifest.files.find((file) => file.relativePath.toLowerCase() === relativePath.toLowerCase());
8871
+ if (!entry) {
8872
+ const skipped = manifest.skipped.find((record) => record.path === relativePath);
8873
+ if (skipped) {
8874
+ const detail = skipped.detail ? ` (${skipped.detail})` : "";
8875
+ return failure(
8876
+ `${relativePath} is not in the snapshot: ${skipped.reason}${detail}. Sensitive and ignored files stay out unless they are included explicitly.`
8877
+ );
8878
+ }
8879
+ return failure(
8880
+ `${relativePath} is not in the snapshot. Call snapshot_locate to find the path, or snapshot_explain to learn why it was excluded.`
8881
+ );
8882
+ }
8883
+ const partPath = path14.resolve(workspace.root, workspace.config.outDir, "parts", entry.part);
8884
+ let partText;
8885
+ try {
8886
+ partText = fs13.readFileSync(partPath, "utf8");
8887
+ } catch {
8888
+ return failure(
8889
+ `${entry.part} could not be read, so the snapshot is damaged. Run snapshot_generate to rebuild it.`
8890
+ );
8891
+ }
8892
+ const section = findSection(parsePartSections(partText), entry.relativePath, entry.hashSha256);
8893
+ if (!section) {
8894
+ return failure(
8895
+ `${relativePath} is recorded in ${entry.part} but its section could not be located there. Run snapshot_generate to rebuild the snapshot.`
8896
+ );
8897
+ }
8898
+ const fromLine = typeof args.fromLine === "number" ? args.fromLine : 1;
8899
+ const lineCount = typeof args.lines === "number" ? args.lines : 200;
8900
+ const window = windowText(section.body, fromLine, lineCount);
8901
+ let body = window.text;
8902
+ let truncatedByBytes = false;
8903
+ if (Buffer.byteLength(body, "utf8") > MAX_RESULT_BYTES) {
8904
+ body = body.slice(0, MAX_RESULT_BYTES);
8905
+ const lastBreak = body.lastIndexOf("\n");
8906
+ body = lastBreak > 0 ? body.slice(0, lastBreak) : body;
8907
+ truncatedByBytes = true;
8908
+ }
8909
+ const returnedLines = body.length === 0 ? 0 : body.split("\n").length;
8910
+ const moreRemain = window.truncated || truncatedByBytes;
8911
+ const header = [
8912
+ `${entry.relativePath} \u2014 ${entry.part}, ${entry.layer}/${entry.kind}, ${entry.language}`,
8913
+ section.inclusion === "preview" ? "Bounded preview: this source file is large, so the snapshot holds only its head and tail." : `Lines ${window.fromLine}\u2013${window.fromLine + returnedLines - 1} of ${window.totalLines}.`
8914
+ ];
8915
+ const lines = [...header, "", body];
8916
+ if (moreRemain) {
8917
+ const next = window.fromLine + returnedLines;
8918
+ lines.push("", `Truncated. Call snapshot_read with fromLine=${next} for the rest.`);
8919
+ }
8920
+ return {
8921
+ text: lines.join("\n"),
8922
+ structured: {
8923
+ path: entry.relativePath,
8924
+ part: entry.part,
8925
+ layer: entry.layer,
8926
+ kind: entry.kind,
8927
+ language: entry.language,
8928
+ inclusion: section.inclusion ?? entry.inclusionMode,
8929
+ fromLine: window.fromLine,
8930
+ returnedLines,
8931
+ totalLines: window.totalLines,
8932
+ truncated: moreRemain,
8933
+ nextFromLine: moreRemain ? window.fromLine + returnedLines : null,
8934
+ sha256: entry.hashSha256
8935
+ }
8936
+ };
8937
+ }
8938
+ };
8939
+ var explainTool = {
8940
+ name: "snapshot_explain",
8941
+ title: "Explain one path",
8942
+ description: 'Explain how the next snapshot would treat one path: its layer, kind, language and part, or the exact reason it is excluded. Answers "why is this file missing".',
8943
+ inputSchema: {
8944
+ type: "object",
8945
+ properties: {
8946
+ path: {
8947
+ type: "string",
8948
+ description: "Path relative to the repository root. Directories are explained as a whole."
8949
+ },
8950
+ root: ROOT_PROPERTY
8951
+ },
8952
+ required: ["path"],
8953
+ additionalProperties: false
8954
+ },
8955
+ outputSchema: {
8956
+ type: "object",
8957
+ properties: {
8958
+ path: { type: "string" },
8959
+ included: { type: "boolean" },
8960
+ reason: { type: "string" }
8961
+ }
8962
+ },
8963
+ annotations: READ_ONLY,
8964
+ async handler(args, context) {
8965
+ const prepared = await prepare(args, context);
8966
+ if (!prepared.ok) return prepared.outcome;
8967
+ const workspace = prepared.workspace;
8968
+ if (typeof args.path !== "string") return failure('"path" must be a string.');
8969
+ const relativePath = toRelativePath(workspace.root, args.path);
8970
+ if (!relativePath) {
8971
+ return failure(`"${args.path}" is outside ${workspace.root}.`);
8972
+ }
8973
+ let result;
8974
+ try {
8975
+ result = explainPath(workspace.config, relativePath);
8976
+ } catch (error) {
8977
+ return failure(error instanceof Error ? error.message : String(error));
8978
+ }
8979
+ return {
8980
+ text: renderExplainText(result),
8981
+ structured: {
8982
+ path: result.path,
8983
+ included: result.included,
8984
+ ...result.reason ? { reason: result.reason } : {},
8985
+ ...result.detail ? { detail: result.detail } : {},
8986
+ ...result.layer ? { layer: result.layer } : {},
8987
+ ...result.kind ? { kind: result.kind } : {},
8988
+ ...result.language ? { language: result.language } : {},
8989
+ ...result.part ? { part: result.part } : {},
8990
+ ...result.inclusionMode ? { inclusion: result.inclusionMode } : {},
8991
+ ...typeof result.lines === "number" ? { lines: result.lines } : {},
8992
+ ...typeof result.bytes === "number" ? { bytes: result.bytes } : {},
8993
+ ...typeof result.secretCount === "number" ? { secretCount: result.secretCount } : {}
8994
+ }
8995
+ };
8996
+ }
8997
+ };
8998
+ function buildTools() {
8999
+ return [
9000
+ statusTool,
9001
+ planTool,
9002
+ generateTool,
9003
+ validateTool,
9004
+ locateTool,
9005
+ readTool,
9006
+ explainTool
9007
+ ];
9008
+ }
9009
+
9010
+ // src/mcp/prompts.ts
9011
+ var ORIENT = `You have a codebase-snapshot server for this repository. Build a map of it before reading any code.
9012
+
9013
+ 1. Call snapshot_status. If no snapshot exists, call snapshot_plan to preview it, then snapshot_generate.
9014
+ 2. Call snapshot_locate for the areas that matter. Search by exported symbol, route or path fragment rather than guessing filenames.
9015
+ 3. Report back: the layers present, the entry points, and the files that matter here \u2014 cited as paths with their part.
9016
+ 4. Read one file at a time with snapshot_read, one window at a time. Do not read a part end to end.`;
9017
+ var KEEP_CURRENT = `Keep the context you were given provably current while you work.
9018
+
9019
+ 1. Before relying on a snapshot from an earlier session, call snapshot_validate.
9020
+ 2. If it reports drift, call snapshot_generate once, then snapshot_validate again.
9021
+ 3. After changes you make, call snapshot_validate and report what it said.
9022
+ 4. Never call a snapshot current unless snapshot_validate said so in this session.`;
9023
+ function withFocus(base, focus) {
9024
+ return typeof focus === "string" && focus.trim().length > 0 ? `${base}
9025
+
9026
+ Focus for this pass: ${focus.trim()}` : base;
9027
+ }
9028
+ function buildPrompts() {
9029
+ return [
9030
+ {
9031
+ name: "orient",
9032
+ title: "Orient in this repository",
9033
+ description: "Map the repository from the snapshot before reading any of it.",
9034
+ arguments: [
9035
+ {
9036
+ name: "focus",
9037
+ description: 'Optional area to concentrate on, for example "authentication".',
9038
+ required: false
9039
+ }
9040
+ ],
9041
+ build(args) {
9042
+ return {
9043
+ description: "Map the repository before reading it.",
9044
+ messages: [{ role: "user", content: { type: "text", text: withFocus(ORIENT, args.focus) } }]
9045
+ };
9046
+ }
9047
+ },
9048
+ {
9049
+ name: "keep-current",
9050
+ title: "Keep the snapshot current",
9051
+ description: "Validate before trusting earlier context, and again after making changes.",
9052
+ arguments: [],
9053
+ build() {
9054
+ return {
9055
+ description: "Keep the context current.",
9056
+ messages: [{ role: "user", content: { type: "text", text: KEEP_CURRENT } }]
9057
+ };
9058
+ }
9059
+ }
9060
+ ];
9061
+ }
9062
+
9063
+ // src/mcp/protocol.ts
9064
+ var LEGACY_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18"];
9065
+ var MODERN_PROTOCOL_VERSIONS = ["2026-07-28"];
9066
+ var SUPPORTED_PROTOCOL_VERSIONS = [
9067
+ ...MODERN_PROTOCOL_VERSIONS,
9068
+ ...LEGACY_PROTOCOL_VERSIONS
9069
+ ];
9070
+ var DEFAULT_LEGACY_PROTOCOL_VERSION = LEGACY_PROTOCOL_VERSIONS[0];
9071
+ var ERROR_CODES = {
9072
+ parseError: -32700,
9073
+ invalidRequest: -32600,
9074
+ methodNotFound: -32601,
9075
+ invalidParams: -32602,
9076
+ internalError: -32603,
9077
+ unsupportedProtocolVersion: -32022
9078
+ };
9079
+ var METHOD3 = {
9080
+ initialize: "initialize",
9081
+ initialized: "notifications/initialized",
9082
+ discover: "server/discover",
9083
+ ping: "ping",
9084
+ toolsList: "tools/list",
9085
+ toolsCall: "tools/call",
9086
+ resourcesList: "resources/list",
9087
+ resourcesRead: "resources/read",
9088
+ resourceTemplatesList: "resources/templates/list",
9089
+ promptsList: "prompts/list",
9090
+ promptsGet: "prompts/get",
9091
+ cancelled: "notifications/cancelled"
9092
+ };
9093
+ var META_KEYS = {
9094
+ protocolVersion: "io.modelcontextprotocol/protocolVersion",
9095
+ clientCapabilities: "io.modelcontextprotocol/clientCapabilities",
9096
+ clientInfo: "io.modelcontextprotocol/clientInfo",
9097
+ serverInfo: "io.modelcontextprotocol/serverInfo",
9098
+ progressToken: "progressToken"
9099
+ };
9100
+ function isJsonObject(value) {
9101
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9102
+ }
9103
+ function serverInfoMeta(info2) {
9104
+ const value = { name: info2.name, version: info2.version };
9105
+ if (info2.title) value.title = info2.title;
9106
+ return { [META_KEYS.serverInfo]: value };
9107
+ }
9108
+ function successResponse(id, result, modern, info2) {
9109
+ if (!modern) return { jsonrpc: "2.0", id, result };
9110
+ const withMeta = {
9111
+ ...result,
9112
+ resultType: "complete",
9113
+ _meta: { ...isJsonObject(result._meta) ? result._meta : {}, ...serverInfoMeta(info2) }
9114
+ };
9115
+ return { jsonrpc: "2.0", id, result: withMeta };
9116
+ }
9117
+ function errorResponse(id, code, message, data) {
9118
+ const error = { code, message };
9119
+ if (data !== void 0) error.data = data;
9120
+ return { jsonrpc: "2.0", id, error };
9121
+ }
9122
+
9123
+ // src/mcp/uris.ts
9124
+ var STATIC_RESOURCE_URIS = [
9125
+ "snapshot://manifest",
9126
+ "snapshot://contract",
9127
+ "snapshot://skill",
9128
+ "snapshot://reports/audit",
9129
+ "snapshot://reports/tree",
9130
+ "snapshot://reports/navigation"
9131
+ ];
9132
+ var RESOURCE_TEMPLATE_URIS = ["snapshot://parts/{index}"];
9133
+
9134
+ // src/agent/option-scope.ts
9135
+ var VALIDATE_OPTIONS = [
9136
+ "cwd",
9137
+ "out-dir",
9138
+ "max-part-bytes",
9139
+ "large-file-bytes",
9140
+ "max-monolith-bytes",
9141
+ "include-monolith",
9142
+ "include-sensitive-files",
9143
+ "skip-artifacts",
9144
+ "only-artifacts",
9145
+ "no-config",
9146
+ "discovery",
9147
+ "format",
9148
+ "mode",
9149
+ "help",
9150
+ "version"
9151
+ ];
9152
+ var COMMAND_OPTION_SCOPE = {
9153
+ generate: [
9154
+ "cwd",
9155
+ "out-dir",
9156
+ "max-part-bytes",
9157
+ "large-file-bytes",
9158
+ "max-monolith-bytes",
9159
+ "include-monolith",
9160
+ "include-sensitive-files",
9161
+ "skip-artifacts",
9162
+ "only-artifacts",
9163
+ "no-config",
9164
+ "discovery",
9165
+ "format",
9166
+ "mode",
9167
+ "dry-run",
9168
+ "watch",
9169
+ "autopilot",
9170
+ "no-install",
9171
+ "help",
9172
+ "version"
9173
+ ],
9174
+ validate: VALIDATE_OPTIONS,
9175
+ report: [
9176
+ "cwd",
9177
+ "out-dir",
9178
+ "max-part-bytes",
9179
+ "large-file-bytes",
9180
+ "max-monolith-bytes",
9181
+ "include-monolith",
9182
+ "include-sensitive-files",
9183
+ "skip-artifacts",
9184
+ "only-artifacts",
9185
+ "no-config",
9186
+ "format",
9187
+ "mode",
9188
+ "help",
9189
+ "version"
9190
+ ],
9191
+ explain: [
9192
+ "cwd",
9193
+ "out-dir",
9194
+ "max-part-bytes",
9195
+ "large-file-bytes",
9196
+ "max-monolith-bytes",
9197
+ "include-monolith",
9198
+ "include-sensitive-files",
9199
+ "skip-artifacts",
9200
+ "only-artifacts",
9201
+ "no-config",
9202
+ "discovery",
9203
+ "format",
9204
+ "mode",
9205
+ "help",
9206
+ "version"
9207
+ ],
9208
+ diff: ["format", "mode", "no-config", "help", "version"],
9209
+ describe: ["format", "mode", "help", "version"],
9210
+ init: [
9211
+ "cwd",
9212
+ "out-dir",
9213
+ "max-part-bytes",
9214
+ "include-sensitive-files",
9215
+ "skip-artifacts",
9216
+ "only-artifacts",
9217
+ "preset",
9218
+ "include",
9219
+ "exclude",
9220
+ "yes",
9221
+ "force",
9222
+ "dry-run",
9223
+ "format",
9224
+ "mode",
9225
+ "help",
9226
+ "version"
9227
+ ],
9228
+ skill: ["cwd", "format", "mode", "install", "target", "help", "version"],
9229
+ // `mcp` reads the repository, so it takes --cwd. It has no --format: stdout carries JSON-RPC
9230
+ // messages, and there is no second shape to choose, so offering the flag would be a lie.
9231
+ mcp: ["cwd", "mode", "help", "version"],
9232
+ setup: [
9233
+ "cwd",
9234
+ "out-dir",
9235
+ "max-part-bytes",
9236
+ "include-sensitive-files",
9237
+ "skip-artifacts",
9238
+ "only-artifacts",
9239
+ "preset",
9240
+ "include",
9241
+ "exclude",
9242
+ "yes",
9243
+ "force",
9244
+ "no-install",
9245
+ "dry-run",
9246
+ "format",
9247
+ "mode",
9248
+ "help",
9249
+ "version"
9250
+ ]
9251
+ };
9252
+ var COMMAND_ORDER = [
9253
+ "generate",
9254
+ "validate",
9255
+ "report",
9256
+ "explain",
9257
+ "init",
9258
+ "setup",
9259
+ "diff",
9260
+ "describe",
9261
+ "skill",
9262
+ "mcp"
9263
+ ];
9264
+ function commandsForOption(option) {
9265
+ return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
9266
+ }
9267
+ function optionAppliesTo(option) {
9268
+ return commandsForOption(option).join("/");
9269
+ }
9270
+
9271
+ // src/agent/contract.ts
9272
+ var CONTRACT_VERSION = "1.0.0";
9273
+ var PACKAGE_NAME = "codebase-snapshot";
9274
+ var MAJOR_VERSION = GENERATOR_VERSION.split(".")[0];
9275
+ var ARTIFACT_SUMMARY = {
9276
+ readme: "README.md describing the snapshot at the output root.",
8052
9277
  manifest: "manifest.json: machine-readable index of every file, part, hash and config.",
8053
9278
  parts: "parts/*.md: the packed, budget-limited markdown parts that carry the code.",
8054
9279
  navigation: "reports/navigation.md: links between parts and layers.",
@@ -8108,6 +9333,19 @@ function renderContractText(contract) {
8108
9333
  lines.push(` ${"".padEnd(24)} ${option.summary}`);
8109
9334
  }
8110
9335
  lines.push("");
9336
+ lines.push("MCP:");
9337
+ lines.push(` command ${contract.mcp.command}`);
9338
+ lines.push(` transport ${contract.mcp.transport}`);
9339
+ lines.push(
9340
+ ` versions modern ${contract.mcp.protocolVersions.modern.join(", ")} / legacy ${contract.mcp.protocolVersions.legacy.join(", ")}`
9341
+ );
9342
+ lines.push(
9343
+ ` tools ${contract.mcp.tools.map((tool) => tool.readOnly ? tool.name : `${tool.name} (writes)`).join(", ")}`
9344
+ );
9345
+ lines.push(` prompts ${contract.mcp.prompts.map((prompt) => prompt.name).join(", ")}`);
9346
+ lines.push(` resources ${contract.mcp.resources.join(", ")}`);
9347
+ lines.push(` templates ${contract.mcp.resourceTemplates.join(", ")}`);
9348
+ lines.push("");
8111
9349
  lines.push("Safety:");
8112
9350
  for (const note of contract.safety) lines.push(` - ${note}`);
8113
9351
  lines.push("");
@@ -8133,11 +9371,13 @@ function buildAgentContract() {
8133
9371
  invocation: {
8134
9372
  oneShot: `npx ${PACKAGE_NAME}@latest`,
8135
9373
  installed: "npx codebase-snapshot",
8136
- bins: ["codebase-snapshot", "snap"],
9374
+ bins: ["codebase-snapshot", "snapshot"],
8137
9375
  notes: [
8138
9376
  `The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
8139
- "Pin an exact version \u2014 for example `codebase-snapshot@0.9.0` \u2014 when you need the same build on every machine. A major range is not enough before 1.0: npm's caret on a 0.x version pins the minor, and 0.x minors are breaking by convention.",
9377
+ "`codebase-snapshot` is the canonical name and the one to script against. `snapshot` is the short alias a global install provides; in a project that has the package installed, `npx snapshot` resolves the local bin first.",
9378
+ `Pin an exact version \u2014 for example \`npx ${PACKAGE_NAME}@${GENERATOR_VERSION} --version\` \u2014 when you need the same build on every machine. A major range is not enough before 1.0: npm's caret on a 0.x version pins the minor, and 0.x minors are breaking by convention.`,
8140
9379
  "Both bin names point at the same file. npm resolves a package by its bin entries, so they must stay aliases of one executable.",
9380
+ "A bare run in a project with no configuration asks once, sets the project up and generates \u2014 but only at a terminal. With no TTY it exits 2 and names the alternatives, so an unattended caller must say what it wants: `generate` after `init --yes`, or `--autopilot`.",
8141
9381
  "Human progress goes to stderr. With --format json the payload on stdout is pure JSON.",
8142
9382
  "A failing run also writes pure JSON to stdout: { error: { message, exitCode } }. The exit code and the stderr line are unchanged."
8143
9383
  ]
@@ -8192,9 +9432,9 @@ function buildAgentContract() {
8192
9432
  {
8193
9433
  name: "generate",
8194
9434
  aliases: ["default"],
8195
- summary: "Write the snapshot. This is the command you normally want.",
9435
+ summary: "Write the snapshot. This is the command you normally want. In a project with no configuration, a terminal asks once and sets the project up first; without a terminal it exits 2 and names `--autopilot` and `init --yes`.",
8196
9436
  positionals: [],
8197
- exits: [0, 1]
9437
+ exits: [0, 1, 2]
8198
9438
  },
8199
9439
  {
8200
9440
  name: "validate",
@@ -8251,6 +9491,13 @@ function buildAgentContract() {
8251
9491
  summary: "Print the agent skill, or --install it into the tool registries this repository's agent reads (.claude/skills, .cursor/rules). Files it creates are added to .gitignore. It never writes AGENTS.md: that file belongs to the repository.",
8252
9492
  positionals: [],
8253
9493
  exits: [0, 1, 2]
9494
+ },
9495
+ {
9496
+ name: "mcp",
9497
+ aliases: [],
9498
+ summary: "Serve this repository to an MCP client over stdio. The tools, resources and protocol versions it offers are listed in this contract's `mcp` block.",
9499
+ positionals: [],
9500
+ exits: [0, 2]
8254
9501
  }
8255
9502
  ],
8256
9503
  exitCodes: {
@@ -8401,6 +9648,13 @@ function buildAgentContract() {
8401
9648
  appliesTo: optionAppliesTo("force"),
8402
9649
  summary: "Overwrite an existing config file."
8403
9650
  },
9651
+ {
9652
+ flag: "--autopilot",
9653
+ type: "boolean",
9654
+ default: false,
9655
+ appliesTo: optionAppliesTo("autopilot"),
9656
+ summary: "Set the project up without asking, then generate: detect the preset, write the config and .gitignore, add the npm scripts and devDependency, install with the project's own package manager, generate. Never overwrites an existing config, and never prompts. Refuses an empty directory (exit 1); contradicts --no-config (exit 2)."
9657
+ },
8404
9658
  {
8405
9659
  flag: "--install",
8406
9660
  type: "boolean",
@@ -8420,7 +9674,7 @@ function buildAgentContract() {
8420
9674
  type: "boolean",
8421
9675
  default: false,
8422
9676
  appliesTo: optionAppliesTo("no-install"),
8423
- summary: "setup only: skip installing the dependency before generating (CI, offline, or a lockfile you do not want a package manager to touch)."
9677
+ summary: "Skip installing the dependency before generating: `setup`, or `generate --autopilot` (CI, offline, or a lockfile you do not want a package manager to touch)."
8424
9678
  }
8425
9679
  ],
8426
9680
  presets: PRESETS.map((preset) => ({
@@ -8492,6 +9746,36 @@ function buildAgentContract() {
8492
9746
  "The tool refuses to replace an outDir that it did not create, so a mistyped outDir cannot delete source code.",
8493
9747
  "Nothing is ever written above the repository root; --out-dir may not be the root or an ancestor."
8494
9748
  ],
9749
+ mcp: {
9750
+ command: `npx ${PACKAGE_NAME} mcp`,
9751
+ transport: "stdio",
9752
+ protocolVersions: {
9753
+ modern: [...MODERN_PROTOCOL_VERSIONS],
9754
+ legacy: [...LEGACY_PROTOCOL_VERSIONS],
9755
+ default: DEFAULT_LEGACY_PROTOCOL_VERSION
9756
+ },
9757
+ tools: buildTools().map((tool) => ({
9758
+ name: tool.name,
9759
+ title: tool.title,
9760
+ summary: tool.description,
9761
+ readOnly: tool.annotations.readOnlyHint,
9762
+ parameters: Object.keys(tool.inputSchema.properties),
9763
+ required: tool.inputSchema.required ?? []
9764
+ })),
9765
+ prompts: buildPrompts().map((prompt) => ({
9766
+ name: prompt.name,
9767
+ summary: prompt.description,
9768
+ arguments: prompt.arguments.map((argument) => argument.name)
9769
+ })),
9770
+ resources: [...STATIC_RESOURCE_URIS],
9771
+ resourceTemplates: [...RESOURCE_TEMPLATE_URIS],
9772
+ notes: [
9773
+ "stdout carries JSON-RPC messages only; every note and log line goes to stderr.",
9774
+ "The server speaks both protocol eras: the legacy `initialize` handshake and the stateless revision, which is detected from `server/discover` or from per-request `_meta`.",
9775
+ "Everything it can do is a subset of the CLI. It never installs packages and never writes a config file.",
9776
+ "A `root` argument may name a sub-directory of the server's --cwd, and nothing outside it is readable."
9777
+ ]
9778
+ },
8495
9779
  troubleshooting: [
8496
9780
  {
8497
9781
  symptom: "Command prints nothing and exits 0",
@@ -8523,8 +9807,8 @@ function buildAgentContract() {
8523
9807
  }
8524
9808
 
8525
9809
  // src/agent/install-skill.ts
8526
- import * as fs11 from "fs";
8527
- import * as path12 from "path";
9810
+ import * as fs14 from "fs";
9811
+ import * as path15 from "path";
8528
9812
 
8529
9813
  // src/agent/skill.ts
8530
9814
  var PACKAGE_NAME2 = "codebase-snapshot";
@@ -8553,10 +9837,11 @@ globally, and you do not need to clone anything.
8553
9837
 
8554
9838
  **Pin a version** \u2014 \`npx ${PACKAGE_NAME2}@${GENERATOR_VERSION} <command>\` \u2014 when you
8555
9839
  need the same version on every machine. The bare package name works too: this package ships
8556
- one executable file, published under two names (\`codebase-snapshot\` and \`snap\`), which is what
8557
- lets npm pick it. If a command exits 0 printing nothing,
8558
- the executable was not resolved \u2014 clear the npm cache and retry; do not assume the repository
8559
- is broken.
9840
+ one executable file, published under two names (\`codebase-snapshot\` and \`snapshot\`), which is
9841
+ what lets npm pick it. A global install gives the short command \`snapshot\`; inside a project
9842
+ that has this package installed, \`npx snapshot\` resolves the local copy first. If a command
9843
+ exits 0 printing nothing, the executable was not resolved \u2014 clear the npm cache and retry;
9844
+ do not assume the repository is broken.
8560
9845
 
8561
9846
  ## Step by step
8562
9847
 
@@ -8581,8 +9866,11 @@ is broken.
8581
9866
  does steps 4\u20136 in one go with defaults: it writes the config (an existing one is kept),
8582
9867
  adds the npm scripts and the devDependency, installs with the project's package manager,
8583
9868
  and generates. Use \`--no-install\` when the machine is offline or the user does not want
8584
- their lockfile touched. Prefer the explicit steps below when you need to control any of
8585
- those, or when something fails and you want to see which part.
9869
+ their lockfile touched. \`--autopilot\` does exactly the same thing from the default
9870
+ command (\`npx ${PACKAGE_NAME2}@latest --autopilot\`) and is the form to reach for when a
9871
+ single invocation must both set the project up and produce the snapshot. Prefer the
9872
+ explicit steps below when you need to control any of those, or when something fails and
9873
+ you want to see which part.
8586
9874
 
8587
9875
  4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
8588
9876
  the project shape, then writes a commented config file listing **every** setting at its
@@ -8590,7 +9878,10 @@ is broken.
8590
9878
  the running Node can strip types (Node >= 22.6) **and** the project is an ES module
8591
9879
  (\`"type": "module"\` in package.json); otherwise \`snapshot.config.mjs\`. Pass
8592
9880
  \`--preset <name>\` only if detection is wrong; the preset names are in the contract.
8593
- This step is optional: without a config the tool runs on the same defaults.
9881
+ Skipping this step is safe **at a terminal**: the first \`generate\` asks once, can write
9882
+ the config for you and then generates. Unattended, a \`generate\` with no config exits 2 \u2014
9883
+ run this step, or \`--autopilot\`, or pass \`--no-config\` to run deliberately on the
9884
+ built-in defaults.
8594
9885
 
8595
9886
  5. **Generate.** Run \`npx ${PACKAGE_NAME2}@latest generate\`. Expect exit 0 and a
8596
9887
  \`./snapshot/\` directory. If you need a different location use \`--out-dir <path>\`; if
@@ -8638,6 +9929,10 @@ any of this to get a good first snapshot.
8638
9929
  deleting anything.
8639
9930
  - **Exit 2 is a usage error**, not a failure of the repository. Re-read the contract
8640
9931
  before retrying.
9932
+ - **A first run refuses rather than guessing.** In a project with no config and no previous
9933
+ snapshot, \`generate\` without a terminal exits 2 instead of silently using defaults. That
9934
+ is deliberate: say which set-up you want (\`--autopilot\`, \`init --yes\`, or \`--no-config\`)
9935
+ rather than retrying the same command.
8641
9936
  - **\`--format json\` keeps stdout pure.** Progress goes to stderr, so \`stdout\` is safe to
8642
9937
  pipe into a JSON parser.
8643
9938
 
@@ -8653,6 +9948,14 @@ It never writes \`AGENTS.md\`. That file is the repository's own agent contract
8653
9948
  commands, conventions, house rules \u2014 and this tool has no business editing it. If you want
8654
9949
  this skill in there, run \`npx ${PACKAGE_NAME2}@latest skill\` and paste the text yourself: the
8655
9950
  diff, and the commit, are then yours.
9951
+
9952
+ ## When the client speaks MCP
9953
+
9954
+ If the agent runs inside an MCP client, \`npx ${PACKAGE_NAME2}@latest mcp\` serves this repository
9955
+ as seven tools over stdio instead of shelling out. Its reads come from the snapshot rather than
9956
+ the working tree, so excluded files and redacted values never reach the context. Use
9957
+ \`snapshot_locate\` before \`snapshot_read\`, and \`snapshot_validate\` before trusting a snapshot
9958
+ from an earlier session.
8656
9959
  `;
8657
9960
  }
8658
9961
 
@@ -8663,17 +9966,17 @@ var LEGACY_AGENTS_MARKER = "<!-- codebase-snapshot:skill:start -->";
8663
9966
  function targetPath(cwd, target) {
8664
9967
  switch (target) {
8665
9968
  case "claude":
8666
- return path12.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
9969
+ return path15.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
8667
9970
  case "cursor":
8668
- return path12.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
9971
+ return path15.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
8669
9972
  }
8670
9973
  }
8671
9974
  function assertWritableInsideRepository(cwd, filePath) {
8672
- const root = fs11.realpathSync(cwd);
8673
- const shown = path12.relative(root, filePath) || filePath;
9975
+ const root = fs14.realpathSync(cwd);
9976
+ const shown = path15.relative(root, filePath) || filePath;
8674
9977
  let stat = null;
8675
9978
  try {
8676
- stat = fs11.lstatSync(filePath);
9979
+ stat = fs14.lstatSync(filePath);
8677
9980
  } catch {
8678
9981
  stat = null;
8679
9982
  }
@@ -8689,32 +9992,32 @@ function assertWritableInsideRepository(cwd, filePath) {
8689
9992
  }
8690
9993
  let existing = filePath;
8691
9994
  const missing = [];
8692
- while (!fs11.existsSync(existing)) {
8693
- missing.unshift(path12.basename(existing));
8694
- const parent = path12.dirname(existing);
9995
+ while (!fs14.existsSync(existing)) {
9996
+ missing.unshift(path15.basename(existing));
9997
+ const parent = path15.dirname(existing);
8695
9998
  if (parent === existing) break;
8696
9999
  existing = parent;
8697
10000
  }
8698
- const realAncestor = fs11.realpathSync(existing);
8699
- const resolved = missing.length === 0 ? realAncestor : path12.join(realAncestor, ...missing);
8700
- if (resolved !== root && !resolved.startsWith(`${root}${path12.sep}`)) {
10001
+ const realAncestor = fs14.realpathSync(existing);
10002
+ const resolved = missing.length === 0 ? realAncestor : path15.join(realAncestor, ...missing);
10003
+ if (resolved !== root && !resolved.startsWith(`${root}${path15.sep}`)) {
8701
10004
  throw new Error(
8702
10005
  `${REFUSED}: ${shown} resolves outside the repository (${resolved}). Move it back inside ${root}, or remove the link, and run again.`
8703
10006
  );
8704
10007
  }
8705
10008
  }
8706
10009
  function writeIfChanged(target, filePath, content) {
8707
- const existing = fs11.existsSync(filePath) ? fs11.readFileSync(filePath, "utf8") : null;
10010
+ const existing = fs14.existsSync(filePath) ? fs14.readFileSync(filePath, "utf8") : null;
8708
10011
  if (existing === content) return { target, path: filePath, action: "unchanged" };
8709
- fs11.mkdirSync(path12.dirname(filePath), { recursive: true });
8710
- fs11.writeFileSync(filePath, content, "utf8");
10012
+ fs14.mkdirSync(path15.dirname(filePath), { recursive: true });
10013
+ fs14.writeFileSync(filePath, content, "utf8");
8711
10014
  return { target, path: filePath, action: existing === null ? "created" : "updated" };
8712
10015
  }
8713
10016
  function legacyAgentsNote(cwd) {
8714
- const filePath = path12.join(cwd, "AGENTS.md");
10017
+ const filePath = path15.join(cwd, "AGENTS.md");
8715
10018
  try {
8716
- if (!fs11.existsSync(filePath)) return null;
8717
- if (!fs11.readFileSync(filePath, "utf8").includes(LEGACY_AGENTS_MARKER)) return null;
10019
+ if (!fs14.existsSync(filePath)) return null;
10020
+ if (!fs14.readFileSync(filePath, "utf8").includes(LEGACY_AGENTS_MARKER)) return null;
8718
10021
  } catch {
8719
10022
  return null;
8720
10023
  }
@@ -8726,9 +10029,9 @@ function installSkill(cwd, target = "all") {
8726
10029
  const notes = [];
8727
10030
  const wanted = target === "all" ? ["claude", "cursor"] : [target];
8728
10031
  for (const item of wanted) assertWritableInsideRepository(cwd, targetPath(cwd, item));
8729
- const createsSomething = wanted.some((item) => !fs11.existsSync(targetPath(cwd, item)));
10032
+ const createsSomething = wanted.some((item) => !fs14.existsSync(targetPath(cwd, item)));
8730
10033
  if (createsSomething) {
8731
- assertWritableInsideRepository(cwd, path12.join(cwd, ".gitignore"));
10034
+ assertWritableInsideRepository(cwd, path15.join(cwd, ".gitignore"));
8732
10035
  }
8733
10036
  for (const item of wanted) {
8734
10037
  written.push(writeIfChanged(item, targetPath(cwd, item), skill));
@@ -8744,6 +10047,771 @@ function isSkillTarget(value) {
8744
10047
  return SKILL_TARGETS.includes(value);
8745
10048
  }
8746
10049
 
10050
+ // src/mcp/era.ts
10051
+ function createSession() {
10052
+ return { era: null, protocolVersion: null, ready: false };
10053
+ }
10054
+ function isLegacyVersion(version) {
10055
+ return LEGACY_PROTOCOL_VERSIONS.includes(version);
10056
+ }
10057
+ function isModernVersion(version) {
10058
+ return MODERN_PROTOCOL_VERSIONS.includes(version);
10059
+ }
10060
+ function inspectModernMeta(params) {
10061
+ const raw = params?._meta;
10062
+ if (!isJsonObject(raw)) return { kind: "absent" };
10063
+ const version = raw[META_KEYS.protocolVersion];
10064
+ if (typeof version !== "string" || version.length === 0) {
10065
+ return { kind: "incomplete", missing: META_KEYS.protocolVersion };
10066
+ }
10067
+ const capabilities2 = raw[META_KEYS.clientCapabilities];
10068
+ if (!isJsonObject(capabilities2)) {
10069
+ return { kind: "incomplete", missing: META_KEYS.clientCapabilities };
10070
+ }
10071
+ const info2 = raw[META_KEYS.clientInfo];
10072
+ return {
10073
+ kind: "present",
10074
+ meta: {
10075
+ version,
10076
+ clientCapabilities: capabilities2,
10077
+ clientInfo: isJsonObject(info2) ? info2 : null
10078
+ }
10079
+ };
10080
+ }
10081
+ function unsupportedVersionData(requested) {
10082
+ return { requested, supported: [...SUPPORTED_PROTOCOL_VERSIONS] };
10083
+ }
10084
+ function classifyRequest(method, params, session) {
10085
+ const inspection = inspectModernMeta(params);
10086
+ if (session.era === "modern") {
10087
+ if (inspection.kind === "absent") {
10088
+ return {
10089
+ kind: "error",
10090
+ code: ERROR_CODES.invalidRequest,
10091
+ message: `This request has no _meta. The modern era requires "${META_KEYS.protocolVersion}" and "${META_KEYS.clientCapabilities}" on every request.`
10092
+ };
10093
+ }
10094
+ if (inspection.kind === "incomplete") {
10095
+ return {
10096
+ kind: "error",
10097
+ code: ERROR_CODES.invalidParams,
10098
+ message: `The request _meta is missing the required key "${inspection.missing}".`
10099
+ };
10100
+ }
10101
+ if (!isModernVersion(inspection.meta.version)) {
10102
+ return {
10103
+ kind: "error",
10104
+ code: ERROR_CODES.unsupportedProtocolVersion,
10105
+ message: `Unsupported protocol version "${inspection.meta.version}".`,
10106
+ data: unsupportedVersionData(inspection.meta.version)
10107
+ };
10108
+ }
10109
+ return { kind: "era", era: "modern", version: inspection.meta.version };
10110
+ }
10111
+ if (session.era === "legacy") {
10112
+ return { kind: "era", era: "legacy", version: session.protocolVersion };
10113
+ }
10114
+ if (method === METHOD3.initialize) {
10115
+ return { kind: "era", era: "legacy", version: null };
10116
+ }
10117
+ if (inspection.kind === "present") {
10118
+ if (!isModernVersion(inspection.meta.version)) {
10119
+ return {
10120
+ kind: "error",
10121
+ code: ERROR_CODES.unsupportedProtocolVersion,
10122
+ message: `Unsupported protocol version "${inspection.meta.version}".`,
10123
+ data: unsupportedVersionData(inspection.meta.version)
10124
+ };
10125
+ }
10126
+ return { kind: "era", era: "modern", version: inspection.meta.version };
10127
+ }
10128
+ if (inspection.kind === "incomplete") {
10129
+ return {
10130
+ kind: "error",
10131
+ code: ERROR_CODES.invalidParams,
10132
+ message: `The request _meta is missing the required key "${inspection.missing}".`
10133
+ };
10134
+ }
10135
+ if (method === METHOD3.discover) {
10136
+ return { kind: "era", era: "modern", version: MODERN_PROTOCOL_VERSIONS[0] };
10137
+ }
10138
+ return { kind: "era", era: "legacy", version: null };
10139
+ }
10140
+ function negotiateLegacyVersion(requested) {
10141
+ if (typeof requested === "string" && isLegacyVersion(requested)) {
10142
+ return { version: requested, matched: true };
10143
+ }
10144
+ return { version: DEFAULT_LEGACY_PROTOCOL_VERSION, matched: false };
10145
+ }
10146
+
10147
+ // src/mcp/schema.ts
10148
+ function validateArguments(schema, value) {
10149
+ if (value === void 0) value = {};
10150
+ if (!isJsonObject(value)) return ["arguments must be a JSON object"];
10151
+ const problems = [];
10152
+ const known = new Set(Object.keys(schema.properties));
10153
+ for (const key of Object.keys(value)) {
10154
+ if (!known.has(key) && schema.additionalProperties !== true) {
10155
+ const allowed = [...known].sort().join(", ");
10156
+ problems.push(`unknown argument "${key}" (accepted: ${allowed || "none"})`);
10157
+ }
10158
+ }
10159
+ for (const required of schema.required ?? []) {
10160
+ if (value[required] === void 0) problems.push(`missing required argument "${required}"`);
10161
+ }
10162
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
10163
+ const argument = value[key];
10164
+ if (argument === void 0) continue;
10165
+ problems.push(...checkValue(key, propertySchema, argument));
10166
+ }
10167
+ return problems;
10168
+ }
10169
+ function checkValue(path18, schema, value) {
10170
+ if (schema.enum && !schema.enum.includes(value)) {
10171
+ return [`"${path18}" must be one of: ${schema.enum.join(", ")}`];
10172
+ }
10173
+ switch (schema.type) {
10174
+ case "string": {
10175
+ if (typeof value !== "string") return [`"${path18}" must be a string`];
10176
+ const problems = [];
10177
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
10178
+ problems.push(`"${path18}" must be at least ${schema.minLength} character(s)`);
10179
+ }
10180
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
10181
+ problems.push(`"${path18}" must be at most ${schema.maxLength} character(s)`);
10182
+ }
10183
+ return problems;
10184
+ }
10185
+ case "number":
10186
+ case "integer": {
10187
+ if (typeof value !== "number" || Number.isNaN(value)) {
10188
+ return [`"${path18}" must be a number`];
10189
+ }
10190
+ if (schema.type === "integer" && !Number.isInteger(value)) {
10191
+ return [`"${path18}" must be an integer`];
10192
+ }
10193
+ const problems = [];
10194
+ if (schema.minimum !== void 0 && value < schema.minimum) {
10195
+ problems.push(`"${path18}" must be >= ${schema.minimum}`);
10196
+ }
10197
+ if (schema.maximum !== void 0 && value > schema.maximum) {
10198
+ problems.push(`"${path18}" must be <= ${schema.maximum}`);
10199
+ }
10200
+ return problems;
10201
+ }
10202
+ case "boolean":
10203
+ return typeof value === "boolean" ? [] : [`"${path18}" must be a boolean`];
10204
+ case "array": {
10205
+ if (!Array.isArray(value)) return [`"${path18}" must be an array`];
10206
+ if (!schema.items) return [];
10207
+ const problems = [];
10208
+ value.forEach((item, index) => {
10209
+ problems.push(...checkValue(`${path18}[${index}]`, schema.items, item));
10210
+ });
10211
+ return problems;
10212
+ }
10213
+ case "object":
10214
+ return isJsonObject(value) ? [] : [`"${path18}" must be an object`];
10215
+ default:
10216
+ return [];
10217
+ }
10218
+ }
10219
+ function applyDefaults(schema, value) {
10220
+ const out = { ...value };
10221
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
10222
+ if (out[key] === void 0 && propertySchema.default !== void 0) {
10223
+ out[key] = propertySchema.default;
10224
+ }
10225
+ }
10226
+ return out;
10227
+ }
10228
+
10229
+ // src/mcp/server.ts
10230
+ function capabilities() {
10231
+ return {
10232
+ tools: { listChanged: false },
10233
+ resources: { subscribe: false, listChanged: false },
10234
+ prompts: { listChanged: false }
10235
+ };
10236
+ }
10237
+ function requestIdOf(message) {
10238
+ const raw = message.id;
10239
+ return typeof raw === "string" || typeof raw === "number" ? raw : null;
10240
+ }
10241
+ function createMcpServer(options) {
10242
+ const session = createSession();
10243
+ const toolsByName = new Map(options.tools.map((tool) => [tool.name, tool]));
10244
+ const promptsByName = new Map(options.prompts.map((prompt) => [prompt.name, prompt]));
10245
+ const identity = () => ({
10246
+ name: options.info.name,
10247
+ title: options.info.title,
10248
+ version: options.info.version,
10249
+ websiteUrl: options.info.websiteUrl
10250
+ });
10251
+ const ok = (id, result, modern) => successResponse(id, result, modern, options.info);
10252
+ function handleNotification(method) {
10253
+ if (method === METHOD3.initialized && session.era === "legacy") session.ready = true;
10254
+ }
10255
+ function initialize(id, params) {
10256
+ const negotiation = negotiateLegacyVersion(params?.protocolVersion);
10257
+ session.era = "legacy";
10258
+ session.protocolVersion = negotiation.version;
10259
+ session.ready = true;
10260
+ options.context.log(
10261
+ negotiation.matched ? `mcp: client initialized with protocol ${negotiation.version}` : `mcp: client asked for an unsupported version and was offered ${negotiation.version}`
10262
+ );
10263
+ return {
10264
+ jsonrpc: "2.0",
10265
+ id,
10266
+ result: {
10267
+ protocolVersion: negotiation.version,
10268
+ capabilities: capabilities(),
10269
+ serverInfo: identity(),
10270
+ instructions: options.instructions
10271
+ }
10272
+ };
10273
+ }
10274
+ function discover(id) {
10275
+ session.era = "modern";
10276
+ session.ready = true;
10277
+ return ok(
10278
+ id,
10279
+ {
10280
+ supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
10281
+ capabilities: capabilities(),
10282
+ instructions: options.instructions,
10283
+ // The capability set depends on which repository the server was started for, so the answer
10284
+ // may be cached only by the client that asked, and not at all after this response.
10285
+ cacheScope: "private",
10286
+ ttlMs: 0
10287
+ },
10288
+ true
10289
+ );
10290
+ }
10291
+ function listTools(id, modern) {
10292
+ const tools = options.tools.map((tool) => ({
10293
+ name: tool.name,
10294
+ title: tool.title,
10295
+ description: tool.description,
10296
+ inputSchema: tool.inputSchema,
10297
+ ...tool.outputSchema ? { outputSchema: tool.outputSchema } : {},
10298
+ annotations: tool.annotations
10299
+ }));
10300
+ return ok(id, { tools }, modern);
10301
+ }
10302
+ async function callTool(id, modern, params) {
10303
+ const name = params?.name;
10304
+ if (typeof name !== "string") {
10305
+ return errorResponse(id, ERROR_CODES.invalidParams, 'tools/call requires a "name" string.');
10306
+ }
10307
+ const tool = toolsByName.get(name);
10308
+ if (!tool) {
10309
+ const known = [...toolsByName.keys()].sort().join(", ");
10310
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown tool "${name}". Available: ${known}.`);
10311
+ }
10312
+ const rawArguments = params?.arguments ?? {};
10313
+ if (!isJsonObject(rawArguments)) {
10314
+ return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
10315
+ }
10316
+ const problems = validateArguments(tool.inputSchema, rawArguments);
10317
+ if (problems.length > 0) {
10318
+ return ok(
10319
+ id,
10320
+ {
10321
+ content: [{ type: "text", text: `Invalid arguments for ${tool.name}:
10322
+ - ${problems.join("\n- ")}` }],
10323
+ isError: true
10324
+ },
10325
+ modern
10326
+ );
10327
+ }
10328
+ const args = applyDefaults(tool.inputSchema, rawArguments);
10329
+ let outcome;
10330
+ try {
10331
+ outcome = await tool.handler(args, options.context);
10332
+ } catch (error) {
10333
+ const detail = error instanceof Error ? error.message : String(error);
10334
+ options.context.log(`mcp: tool ${tool.name} failed: ${detail}`);
10335
+ return ok(
10336
+ id,
10337
+ { content: [{ type: "text", text: `${tool.name} failed: ${detail}` }], isError: true },
10338
+ modern
10339
+ );
10340
+ }
10341
+ return ok(
10342
+ id,
10343
+ {
10344
+ content: [{ type: "text", text: outcome.text }],
10345
+ ...outcome.structured ? { structuredContent: outcome.structured } : {},
10346
+ ...outcome.isError ? { isError: true } : {}
10347
+ },
10348
+ modern
10349
+ );
10350
+ }
10351
+ async function listResources(id, modern) {
10352
+ const resources = await options.resources.list(options.context);
10353
+ return ok(id, { resources }, modern);
10354
+ }
10355
+ async function readResource(id, modern, params) {
10356
+ const uri = params?.uri;
10357
+ if (typeof uri !== "string") {
10358
+ return errorResponse(id, ERROR_CODES.invalidParams, 'resources/read requires a "uri" string.');
10359
+ }
10360
+ const contents = await options.resources.read(uri, options.context);
10361
+ if (!contents) {
10362
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown resource "${uri}".`);
10363
+ }
10364
+ return ok(id, { contents: [contents] }, modern);
10365
+ }
10366
+ function listPrompts(id, modern) {
10367
+ const prompts = options.prompts.map((prompt) => ({
10368
+ name: prompt.name,
10369
+ ...prompt.title ? { title: prompt.title } : {},
10370
+ description: prompt.description,
10371
+ arguments: prompt.arguments
10372
+ }));
10373
+ return ok(id, { prompts }, modern);
10374
+ }
10375
+ function getPrompt(id, modern, params) {
10376
+ const name = params?.name;
10377
+ if (typeof name !== "string") {
10378
+ return errorResponse(id, ERROR_CODES.invalidParams, 'prompts/get requires a "name" string.');
10379
+ }
10380
+ const prompt = promptsByName.get(name);
10381
+ if (!prompt) {
10382
+ const known = [...promptsByName.keys()].sort().join(", ");
10383
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown prompt "${name}". Available: ${known}.`);
10384
+ }
10385
+ const rawArguments = params?.arguments ?? {};
10386
+ if (!isJsonObject(rawArguments)) {
10387
+ return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
10388
+ }
10389
+ const built = prompt.build(rawArguments);
10390
+ const messages = built.messages.map((message) => ({
10391
+ role: message.role,
10392
+ content: { type: "text", text: message.content.text }
10393
+ }));
10394
+ return ok(id, { description: built.description, messages }, modern);
10395
+ }
10396
+ async function route(name, id, modern, params) {
10397
+ switch (name) {
10398
+ case METHOD3.initialize:
10399
+ return modern ? errorResponse(
10400
+ id,
10401
+ ERROR_CODES.methodNotFound,
10402
+ '"initialize" does not exist in this protocol revision; call "server/discover".'
10403
+ ) : initialize(id, params);
10404
+ case METHOD3.discover:
10405
+ return discover(id);
10406
+ case METHOD3.ping:
10407
+ return modern ? errorResponse(
10408
+ id,
10409
+ ERROR_CODES.methodNotFound,
10410
+ '"ping" was removed in this protocol revision; call "server/discover".'
10411
+ ) : ok(id, {}, modern);
10412
+ default:
10413
+ break;
10414
+ }
10415
+ if (!session.ready) {
10416
+ return errorResponse(
10417
+ id,
10418
+ ERROR_CODES.invalidRequest,
10419
+ 'This connection has not been initialized. Send "initialize" first.'
10420
+ );
10421
+ }
10422
+ switch (name) {
10423
+ case METHOD3.toolsList:
10424
+ return listTools(id, modern);
10425
+ case METHOD3.toolsCall:
10426
+ return callTool(id, modern, params);
10427
+ case METHOD3.resourcesList:
10428
+ return listResources(id, modern);
10429
+ case METHOD3.resourcesRead:
10430
+ return readResource(id, modern, params);
10431
+ case METHOD3.resourceTemplatesList:
10432
+ return ok(id, { resourceTemplates: options.resources.templates() }, modern);
10433
+ case METHOD3.promptsList:
10434
+ return listPrompts(id, modern);
10435
+ case METHOD3.promptsGet:
10436
+ return getPrompt(id, modern, params);
10437
+ default:
10438
+ return errorResponse(id, ERROR_CODES.methodNotFound, `Unknown method "${name}".`);
10439
+ }
10440
+ }
10441
+ async function dispatch(message) {
10442
+ if (!isJsonObject(message)) {
10443
+ return errorResponse(null, ERROR_CODES.parseError, "Message is not a JSON object.");
10444
+ }
10445
+ const isRequest = Object.prototype.hasOwnProperty.call(message, "id");
10446
+ const method = message.method;
10447
+ const params = message.params;
10448
+ if (message.jsonrpc !== "2.0" && isRequest) {
10449
+ return errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry "jsonrpc": "2.0".');
10450
+ }
10451
+ if (typeof method !== "string" || method.length === 0) {
10452
+ return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry a "method" string.') : null;
10453
+ }
10454
+ if (params !== void 0 && !isJsonObject(params)) {
10455
+ return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, '"params" must be a JSON object when present.') : null;
10456
+ }
10457
+ const named = params;
10458
+ const decision = classifyRequest(method, named, session);
10459
+ const modern = decision.kind === "era" ? decision.era === "modern" : session.era === "modern";
10460
+ if (!isRequest) {
10461
+ if (decision.kind === "era" && session.era === null) {
10462
+ session.era = decision.era;
10463
+ session.protocolVersion = decision.version;
10464
+ }
10465
+ handleNotification(method);
10466
+ return null;
10467
+ }
10468
+ const id = requestIdOf(message);
10469
+ if (id === null) {
10470
+ return errorResponse(null, ERROR_CODES.invalidRequest, 'A request "id" must be a string or a number.');
10471
+ }
10472
+ if (decision.kind === "error") {
10473
+ return errorResponse(id, decision.code, decision.message, decision.data);
10474
+ }
10475
+ if (session.era === null) {
10476
+ session.era = decision.era;
10477
+ session.protocolVersion = decision.version;
10478
+ }
10479
+ return route(method, id, modern, named);
10480
+ }
10481
+ return {
10482
+ session,
10483
+ async handle(message) {
10484
+ try {
10485
+ return await dispatch(message);
10486
+ } catch (error) {
10487
+ const detail = error instanceof Error ? error.message : String(error);
10488
+ options.context.log(`mcp: internal error: ${detail}`);
10489
+ const id = isJsonObject(message) ? requestIdOf(message) : null;
10490
+ return errorResponse(id, ERROR_CODES.internalError, "Internal server error.");
10491
+ }
10492
+ }
10493
+ };
10494
+ }
10495
+
10496
+ // src/mcp/resources.ts
10497
+ import * as fs15 from "fs";
10498
+ import * as path16 from "path";
10499
+ var MAX_RESOURCE_BYTES = 512 * 1024;
10500
+ var PART_URI = /^snapshot:\/\/parts\/(\d+)$/;
10501
+ var REPORT_URIS = /* @__PURE__ */ new Map([
10502
+ ["snapshot://reports/audit", { file: "audit.md", title: "Secret audit and risk findings" }],
10503
+ ["snapshot://reports/tree", { file: "project-tree.md", title: "Project tree" }],
10504
+ ["snapshot://reports/navigation", { file: "navigation.md", title: "Navigation index" }]
10505
+ ]);
10506
+ function formatBytes2(bytes) {
10507
+ if (bytes < 1024) return `${bytes} B`;
10508
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
10509
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
10510
+ }
10511
+ function truncationMarker(originalBytes) {
10512
+ return `
10513
+
10514
+ [truncated: ${formatBytes2(originalBytes)} total. Use the snapshot_read tool for a line window.]
10515
+ `;
10516
+ }
10517
+ function capUtf8(text2, limit) {
10518
+ const size2 = Buffer.byteLength(text2, "utf8");
10519
+ if (size2 <= limit) return { text: text2, truncated: false };
10520
+ const cut = Buffer.from(text2, "utf8").subarray(0, limit).toString("utf8").replace(/\uFFFD+$/, "");
10521
+ return { text: `${cut}${truncationMarker(size2)}`, truncated: true };
10522
+ }
10523
+ async function workspaceFor(context) {
10524
+ const root = resolveRoot(void 0, context);
10525
+ if (!root.ok) return { error: root.message };
10526
+ const opened = await openWorkspace(root.root);
10527
+ if (!opened.ok) return { error: opened.message };
10528
+ return opened.workspace;
10529
+ }
10530
+ function readFileIfPresent(file) {
10531
+ try {
10532
+ return fs15.readFileSync(file, "utf8");
10533
+ } catch {
10534
+ return null;
10535
+ }
10536
+ }
10537
+ function buildResourceProvider() {
10538
+ return {
10539
+ async list(context) {
10540
+ const resources = [
10541
+ {
10542
+ uri: "snapshot://manifest",
10543
+ name: "manifest",
10544
+ title: "Snapshot manifest",
10545
+ description: "Every file in the snapshot with its layer, kind, language, part and hash, plus the reason each excluded file was excluded.",
10546
+ mimeType: "application/json"
10547
+ },
10548
+ {
10549
+ uri: "snapshot://contract",
10550
+ name: "contract",
10551
+ title: "Tool contract",
10552
+ description: "The machine-readable description of the codebase-snapshot command line.",
10553
+ mimeType: "application/json"
10554
+ },
10555
+ {
10556
+ uri: "snapshot://skill",
10557
+ name: "skill",
10558
+ title: "Agent skill",
10559
+ description: "Usage guidance for an agent working with this tool.",
10560
+ mimeType: "text/markdown"
10561
+ }
10562
+ ];
10563
+ for (const [uri, report] of REPORT_URIS) {
10564
+ resources.push({
10565
+ uri,
10566
+ name: report.file.replace(/\.md$/, ""),
10567
+ title: report.title,
10568
+ description: `${report.title} from the most recent snapshot.`,
10569
+ mimeType: "text/markdown"
10570
+ });
10571
+ }
10572
+ const workspace = await workspaceFor(context);
10573
+ if ("error" in workspace) return resources;
10574
+ const read = readManifest3(workspace);
10575
+ if (!read.ok) return resources;
10576
+ for (const part of read.manifest.parts) {
10577
+ resources.push({
10578
+ uri: `snapshot://parts/${part.index}`,
10579
+ name: `part-${String(part.index).padStart(3, "0")}`,
10580
+ title: `Part ${part.index} (${part.fileCount} files, ${formatBytes2(part.byteSize)})`,
10581
+ description: `Part ${part.index} of ${read.manifest.parts.length}: ${part.fileCount} file(s), ${part.lines} line(s), ${formatBytes2(part.byteSize)}.`,
10582
+ mimeType: "text/markdown"
10583
+ });
10584
+ }
10585
+ return resources;
10586
+ },
10587
+ templates() {
10588
+ return [
10589
+ {
10590
+ uriTemplate: "snapshot://parts/{index}",
10591
+ name: "part",
10592
+ title: "Snapshot part",
10593
+ description: "One budgeted part of the snapshot, by its 1-based index.",
10594
+ mimeType: "text/markdown"
10595
+ }
10596
+ ];
10597
+ },
10598
+ async read(uri, context) {
10599
+ if (uri === "snapshot://contract") {
10600
+ const contract = buildAgentContract();
10601
+ return { uri, mimeType: "application/json", text: JSON.stringify(contract, null, 2) };
10602
+ }
10603
+ if (uri === "snapshot://skill") {
10604
+ return { uri, mimeType: "text/markdown", text: renderSkill() };
10605
+ }
10606
+ const workspace = await workspaceFor(context);
10607
+ if ("error" in workspace) return null;
10608
+ const report = REPORT_URIS.get(uri);
10609
+ if (report) {
10610
+ const text2 = readFileIfPresent(
10611
+ path16.resolve(workspace.root, workspace.config.outDir, "reports", report.file)
10612
+ );
10613
+ if (text2 === null) return null;
10614
+ return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
10615
+ }
10616
+ if (uri === "snapshot://manifest") {
10617
+ const read = readManifest3(workspace);
10618
+ if (!read.ok) return null;
10619
+ const text2 = readFileIfPresent(read.file) ?? JSON.stringify(read.manifest, null, 2);
10620
+ return { uri, mimeType: "application/json", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
10621
+ }
10622
+ const partMatch = PART_URI.exec(uri);
10623
+ if (partMatch) {
10624
+ const read = readManifest3(workspace);
10625
+ if (!read.ok) return null;
10626
+ const index = Number(partMatch[1]);
10627
+ const part = read.manifest.parts.find((candidate) => candidate.index === index);
10628
+ if (!part) return null;
10629
+ const text2 = readFileIfPresent(
10630
+ path16.resolve(workspace.root, workspace.config.outDir, "parts", part.filename)
10631
+ );
10632
+ if (text2 === null) return null;
10633
+ return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
10634
+ }
10635
+ return null;
10636
+ }
10637
+ };
10638
+ }
10639
+
10640
+ // src/mcp/instructions.ts
10641
+ var SERVER_INSTRUCTIONS = [
10642
+ "codebase-snapshot serves this repository as a size-budgeted snapshot, not as a directory walk.",
10643
+ "",
10644
+ "Work in this order:",
10645
+ "1. snapshot_status \u2014 is there a snapshot here, and has the tree moved since it was built?",
10646
+ "2. snapshot_locate \u2014 find which part holds the code you need.",
10647
+ "3. snapshot_read \u2014 read a window of one file. Never read a part end to end.",
10648
+ "4. snapshot_validate \u2014 before trusting context that came from an earlier conversation.",
10649
+ "",
10650
+ "Rules that matter:",
10651
+ "- Parts are budgeted, not small: a 12-part snapshot is hundreds of kilobytes. Use snapshot_locate first.",
10652
+ "- A file absent from the snapshot has a recorded reason. Ask snapshot_explain, do not assume.",
10653
+ "- snapshot_generate is the only tool that writes. It never installs packages and never writes config.",
10654
+ "- Paths are relative to the repository root the server was started for, and nothing outside it is readable."
10655
+ ].join("\n");
10656
+
10657
+ // src/mcp/stdio.ts
10658
+ var MAX_LINE_BYTES = 32 * 1024 * 1024;
10659
+ function createWriter(output) {
10660
+ return {
10661
+ write(value) {
10662
+ return new Promise((resolve9, reject) => {
10663
+ output.write(`${JSON.stringify(value)}
10664
+ `, (error) => {
10665
+ if (error) reject(error);
10666
+ else resolve9();
10667
+ });
10668
+ });
10669
+ }
10670
+ };
10671
+ }
10672
+ function createLineReader(input, options) {
10673
+ let buffer = "";
10674
+ let discarding = false;
10675
+ let discardedBytes = 0;
10676
+ const deliver = (line) => {
10677
+ if (discarding) {
10678
+ discarding = false;
10679
+ discardedBytes = 0;
10680
+ return;
10681
+ }
10682
+ const trimmed = line.endsWith("\r") ? line.slice(0, -1) : line;
10683
+ if (trimmed.trim().length === 0) return;
10684
+ options.onLine(trimmed);
10685
+ };
10686
+ const drain = () => {
10687
+ let index = buffer.indexOf("\n");
10688
+ while (index >= 0) {
10689
+ const line = buffer.slice(0, index);
10690
+ buffer = buffer.slice(index + 1);
10691
+ deliver(line);
10692
+ index = buffer.indexOf("\n");
10693
+ }
10694
+ };
10695
+ const onData = (chunk) => {
10696
+ if (discarding) {
10697
+ const index = chunk.indexOf("\n");
10698
+ if (index < 0) {
10699
+ discardedBytes += chunk.length;
10700
+ return;
10701
+ }
10702
+ buffer = chunk.slice(index + 1);
10703
+ deliver("");
10704
+ drain();
10705
+ return;
10706
+ }
10707
+ buffer += chunk;
10708
+ if (buffer.length > MAX_LINE_BYTES && buffer.indexOf("\n") < 0) {
10709
+ discarding = true;
10710
+ discardedBytes = buffer.length;
10711
+ buffer = "";
10712
+ options.onOverflow?.(discardedBytes);
10713
+ return;
10714
+ }
10715
+ drain();
10716
+ };
10717
+ const onEnd = () => {
10718
+ if (buffer.length > 0) {
10719
+ const rest = buffer;
10720
+ buffer = "";
10721
+ deliver(rest);
10722
+ }
10723
+ };
10724
+ input.setEncoding("utf8");
10725
+ input.on("data", onData);
10726
+ input.on("end", onEnd);
10727
+ return () => {
10728
+ input.off("data", onData);
10729
+ input.off("end", onEnd);
10730
+ };
10731
+ }
10732
+
10733
+ // src/mcp/run.ts
10734
+ var PROJECT_URL = "https://ankurhalder.github.io/codebase-snapshot/";
10735
+ function createServer(options) {
10736
+ const cwd = options.cwd;
10737
+ const roots = options.roots && options.roots.length > 0 ? options.roots : [cwd];
10738
+ const context = {
10739
+ cwd,
10740
+ roots,
10741
+ log: options.log ?? ((line) => process.stderr.write(`${line}
10742
+ `))
10743
+ };
10744
+ return createMcpServer({
10745
+ info: {
10746
+ name: GENERATOR_NAME,
10747
+ title: "codebase-snapshot",
10748
+ version: GENERATOR_VERSION,
10749
+ websiteUrl: PROJECT_URL
10750
+ },
10751
+ instructions: SERVER_INSTRUCTIONS,
10752
+ tools: buildTools(),
10753
+ resources: buildResourceProvider(),
10754
+ prompts: buildPrompts(),
10755
+ context
10756
+ });
10757
+ }
10758
+ async function runStdioServer(options) {
10759
+ const log = options.log ?? ((line) => process.stderr.write(`${line}
10760
+ `));
10761
+ const server = createServer(options);
10762
+ const writer = createWriter(process.stdout);
10763
+ let writeChain = Promise.resolve();
10764
+ const send = (message) => {
10765
+ writeChain = writeChain.then(() => writer.write(message)).catch((error) => {
10766
+ log(`mcp: could not write a response: ${error instanceof Error ? error.message : String(error)}`);
10767
+ });
10768
+ return writeChain;
10769
+ };
10770
+ let inFlight = 0;
10771
+ let inputEnded = false;
10772
+ let resolveDone = () => void 0;
10773
+ const finished = new Promise((resolve9) => {
10774
+ resolveDone = resolve9;
10775
+ });
10776
+ const finishIfIdle = () => {
10777
+ if (inputEnded && inFlight === 0) resolveDone(0);
10778
+ };
10779
+ const handleLine = async (line) => {
10780
+ let message;
10781
+ try {
10782
+ message = JSON.parse(line);
10783
+ } catch {
10784
+ await send(errorResponse(null, ERROR_CODES.parseError, "Message is not valid JSON."));
10785
+ return;
10786
+ }
10787
+ const response = await server.handle(message);
10788
+ if (response) await send(response);
10789
+ };
10790
+ const stopReading = createLineReader(process.stdin, {
10791
+ onOverflow: (bytes) => log(`mcp: discarded an oversized message (${bytes} bytes)`),
10792
+ onLine: (line) => {
10793
+ inFlight += 1;
10794
+ void handleLine(line).finally(() => {
10795
+ inFlight -= 1;
10796
+ finishIfIdle();
10797
+ });
10798
+ }
10799
+ });
10800
+ const onEnd = () => {
10801
+ inputEnded = true;
10802
+ finishIfIdle();
10803
+ };
10804
+ process.stdin.on("end", onEnd);
10805
+ process.stdin.on("close", onEnd);
10806
+ log(`mcp: serving ${options.cwd} over stdio`);
10807
+ const code = await finished;
10808
+ stopReading();
10809
+ process.stdin.off("end", onEnd);
10810
+ process.stdin.off("close", onEnd);
10811
+ await writeChain;
10812
+ return code;
10813
+ }
10814
+
8747
10815
  // src/cli.ts
8748
10816
  var VERSION = GENERATOR_VERSION;
8749
10817
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -8757,7 +10825,8 @@ var COMMAND_ALIASES = /* @__PURE__ */ new Map([
8757
10825
  ["explain", "explain"],
8758
10826
  ["diff", "diff"],
8759
10827
  ["describe", "describe"],
8760
- ["skill", "skill"]
10828
+ ["skill", "skill"],
10829
+ ["mcp", "mcp"]
8761
10830
  ]);
8762
10831
  var COMMAND_ARITY = {
8763
10832
  generate: 0,
@@ -8768,7 +10837,8 @@ var COMMAND_ARITY = {
8768
10837
  explain: 1,
8769
10838
  diff: 2,
8770
10839
  describe: 0,
8771
- skill: 0
10840
+ skill: 0,
10841
+ mcp: 0
8772
10842
  };
8773
10843
  var CLI_OPTIONS = {
8774
10844
  cwd: { type: "string" },
@@ -8791,6 +10861,7 @@ var CLI_OPTIONS = {
8791
10861
  exclude: { type: "string" },
8792
10862
  yes: { type: "boolean" },
8793
10863
  force: { type: "boolean" },
10864
+ autopilot: { type: "boolean" },
8794
10865
  install: { type: "boolean" },
8795
10866
  "no-install": { type: "boolean" },
8796
10867
  target: { type: "string" },
@@ -8806,6 +10877,14 @@ Production-grade LLM codebase snapshot generator, auditor, and AST partitioner
8806
10877
  Usage:
8807
10878
  codebase-snapshot [command] [options]
8808
10879
 
10880
+ First run:
10881
+ codebase-snapshot No config yet: asks once, sets the project up, generates.
10882
+ A configured project just generates. It never falls back
10883
+ to defaults it was not given.
10884
+ codebase-snapshot --autopilot The same without questions: detect the preset, write the
10885
+ config, add the npm scripts and devDependency, install,
10886
+ then generate.
10887
+
8809
10888
  Commands:
8810
10889
  setup Everything at once: config, scripts, dependency, install, generate
8811
10890
  generate (default) Generate a complete partitioned codebase snapshot
@@ -8816,6 +10895,7 @@ Commands:
8816
10895
  diff <a> <b> Compare two snapshots (directories or manifest.json files)
8817
10896
  describe Print the machine-readable contract (--format json for agents)
8818
10897
  skill Print the bundled agent skill; --install writes it into the repo
10898
+ mcp Serve this repository to an MCP client over stdio
8819
10899
 
8820
10900
  Options:
8821
10901
  --cwd <path> Target repository directory (default: current directory)
@@ -8838,6 +10918,12 @@ Options:
8838
10918
  auto: git when usable. git: require git. walk: read
8839
10919
  .gitignore directly (escape hatch for a broken .git)
8840
10920
 
10921
+ generate only:
10922
+ --autopilot Set the project up without asking, then generate: write the
10923
+ config and .gitignore, add the npm scripts and devDependency,
10924
+ and install with the project's own package manager.
10925
+ --no-install With --autopilot: skip the dependency install.
10926
+
8841
10927
  init only:
8842
10928
  --preset <name> Force a project preset instead of detecting one
8843
10929
  --include <globs> Comma-separated allow-list
@@ -8917,10 +11003,10 @@ function parseCliArgs(argv) {
8917
11003
  }
8918
11004
  if (values.cwd !== void 0) {
8919
11005
  const target = String(values.cwd);
8920
- if (!fs12.existsSync(target)) {
11006
+ if (!fs16.existsSync(target)) {
8921
11007
  throw new UsageError(`--cwd directory does not exist: ${target}`);
8922
11008
  }
8923
- if (!fs12.statSync(target).isDirectory()) {
11009
+ if (!fs16.statSync(target).isDirectory()) {
8924
11010
  throw new UsageError(`--cwd is not a directory: ${target}`);
8925
11011
  }
8926
11012
  }
@@ -9040,8 +11126,17 @@ function parseCliArgs(argv) {
9040
11126
  `--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
9041
11127
  );
9042
11128
  }
11129
+ if (values["no-install"] === true && command === "generate" && values.autopilot !== true) {
11130
+ throw new UsageError("--no-install only applies to `setup`, or to `generate --autopilot`.");
11131
+ }
11132
+ if (values.autopilot === true && values["no-config"] === true) {
11133
+ throw new UsageError(
11134
+ "--autopilot sets a project up and writes a config; --no-config ignores config files. Pass one or the other."
11135
+ );
11136
+ }
9043
11137
  return {
9044
11138
  command,
11139
+ explicitCommand: firstPositional !== void 0 || explicitMode !== void 0,
9045
11140
  args,
9046
11141
  format: rawFormat,
9047
11142
  dryRun: values["dry-run"] === true,
@@ -9053,10 +11148,11 @@ function parseCliArgs(argv) {
9053
11148
  exclude: values.exclude === void 0 ? void 0 : String(values.exclude),
9054
11149
  yes: values.yes === true,
9055
11150
  force: values.force === true,
11151
+ autopilot: values.autopilot === true,
9056
11152
  install: values.install === true,
9057
11153
  noInstall: values["no-install"] === true,
9058
11154
  target: values.target === void 0 ? void 0 : String(values.target),
9059
- cwd: values.cwd === void 0 ? process.cwd() : path13.resolve(String(values.cwd)),
11155
+ cwd: values.cwd === void 0 ? process.cwd() : path17.resolve(String(values.cwd)),
9060
11156
  outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
9061
11157
  includeMonolith: values["include-monolith"] === true || onlyNamesMonolith === true ? true : void 0,
9062
11158
  includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
@@ -9113,26 +11209,29 @@ function splitList(value) {
9113
11209
  return value.split(",").map((entry) => entry.trim()).filter(Boolean);
9114
11210
  }
9115
11211
  function displayOutPath(cwd, outDir, ...segments) {
9116
- const absolute = path13.resolve(cwd, outDir, ...segments);
9117
- const relative8 = path13.relative(cwd, absolute);
9118
- if (relative8 && !relative8.startsWith("..") && !path13.isAbsolute(relative8)) return relative8;
11212
+ const absolute = path17.resolve(cwd, outDir, ...segments);
11213
+ const relative11 = path17.relative(cwd, absolute);
11214
+ if (relative11 && !relative11.startsWith("..") && !path17.isAbsolute(relative11)) return relative11;
9119
11215
  return absolute;
9120
11216
  }
9121
11217
  function nextStepHint(cwd, scriptsAddedNow) {
9122
11218
  let hasScript = scriptsAddedNow;
9123
11219
  if (!hasScript) {
9124
11220
  try {
9125
- const parsed = JSON.parse(fs12.readFileSync(path13.join(cwd, "package.json"), "utf8"));
11221
+ const parsed = JSON.parse(fs16.readFileSync(path17.join(cwd, "package.json"), "utf8"));
9126
11222
  hasScript = typeof parsed.scripts?.snapshot === "string";
9127
11223
  } catch {
9128
11224
  hasScript = false;
9129
11225
  }
9130
11226
  }
9131
11227
  if (!hasScript) return `npx ${GENERATOR_NAME} generate`;
9132
- const bin = path13.join(cwd, "node_modules", ".bin", "codebase-snapshot");
9133
- const installed = fs12.existsSync(bin) || fs12.existsSync(`${bin}.cmd`);
11228
+ const bin = path17.join(cwd, "node_modules", ".bin", "codebase-snapshot");
11229
+ const installed = fs16.existsSync(bin) || fs16.existsSync(`${bin}.cmd`);
9134
11230
  return installed ? "npm run snapshot" : "npm install && npm run snapshot";
9135
11231
  }
11232
+ function presetSummary(init) {
11233
+ return init.reason === null ? `preset: ${init.preset}` : `preset: ${init.preset}, because ${init.reason}`;
11234
+ }
9136
11235
  async function runInitCommand(invocation) {
9137
11236
  try {
9138
11237
  const result = await runInit({
@@ -9157,7 +11256,7 @@ async function runInitCommand(invocation) {
9157
11256
  info(result.configText);
9158
11257
  return 0;
9159
11258
  }
9160
- info(`\u2705 Created ${path13.basename(result.configPath)} (preset: ${result.preset})`);
11259
+ info(`\u2705 Created ${path17.basename(result.configPath)} (${presetSummary(result)})`);
9161
11260
  if (result.scripts.length > 0) info(` Added npm scripts: ${result.scripts.join(", ")}`);
9162
11261
  if (result.dependency.added && result.dependency.range !== null) {
9163
11262
  info(` Added devDependency: ${result.dependency.name} ${result.dependency.range}`);
@@ -9177,7 +11276,7 @@ async function runInitCommand(invocation) {
9177
11276
  }
9178
11277
  }
9179
11278
  function detectPackageManager(cwd) {
9180
- const has = (name) => fs12.existsSync(path13.join(cwd, name));
11279
+ const has = (name) => fs16.existsSync(path17.join(cwd, name));
9181
11280
  if (has("bun.lockb") || has("bun.lock")) return { name: "bun", command: "bun", args: ["install"] };
9182
11281
  if (has("pnpm-lock.yaml")) return { name: "pnpm", command: "pnpm", args: ["install"] };
9183
11282
  if (has("yarn.lock")) return { name: "yarn", command: "yarn", args: ["install"] };
@@ -9196,14 +11295,13 @@ function installDependencies(cwd) {
9196
11295
  const manager = detectPackageManager(cwd);
9197
11296
  const command = `${manager.command} ${manager.args.join(" ")}`;
9198
11297
  const started = performance.now();
9199
- const result = spawnSync(manager.command, manager.args, {
11298
+ const spawnOptions = {
9200
11299
  cwd,
9201
11300
  encoding: "utf8",
9202
- // npm, pnpm and yarn ship as `.cmd` shims on Windows, which spawnSync cannot exec directly.
9203
- shell: process.platform === "win32",
9204
11301
  timeout: INSTALL_TIMEOUT_MS,
9205
11302
  env: cleanInstallEnv()
9206
- });
11303
+ };
11304
+ const result = process.platform === "win32" ? spawnSync(command, { ...spawnOptions, shell: true }) : spawnSync(manager.command, manager.args, spawnOptions);
9207
11305
  const seconds = `${((performance.now() - started) / 1e3).toFixed(1)}s`;
9208
11306
  if (result.error) {
9209
11307
  return { manager: manager.name, command, ok: false, detail: `${result.error.message} (${seconds})` };
@@ -9228,8 +11326,9 @@ async function captureStdout(fn) {
9228
11326
  process.stdout.write = original;
9229
11327
  }
9230
11328
  }
9231
- async function runSetupCommand(invocation) {
11329
+ async function runSetupCommand(invocation, options = {}) {
9232
11330
  try {
11331
+ const interactive = options.interactive === true;
9233
11332
  const init = await runInit({
9234
11333
  cwd: invocation.cwd,
9235
11334
  preset: invocation.preset,
@@ -9239,7 +11338,7 @@ async function runSetupCommand(invocation) {
9239
11338
  exclude: splitList(invocation.exclude),
9240
11339
  skipArtifacts: invocation.skipArtifacts,
9241
11340
  includeSensitiveFiles: invocation.includeSensitiveFiles,
9242
- yes: true,
11341
+ yes: !interactive,
9243
11342
  force: invocation.force,
9244
11343
  reuseConfig: true,
9245
11344
  dryRun: invocation.dryRun
@@ -9266,7 +11365,7 @@ async function runSetupCommand(invocation) {
9266
11365
  }
9267
11366
  info("\n\u{1F9EA} Dry run \u2014 setup would:");
9268
11367
  info(
9269
- ` 1. ${init.reused ? `keep ${path13.basename(init.configPath)}` : `write ${path13.basename(init.configPath)} (preset: ${init.preset})`}`
11368
+ ` 1. ${init.reused ? `keep ${path17.basename(init.configPath)}` : `write ${path17.basename(init.configPath)} (preset: ${init.preset})`}`
9270
11369
  );
9271
11370
  info(" 2. add the npm scripts, the devDependency and the .gitignore entries");
9272
11371
  if (manager !== null) info(` 3. install with ${manager}`);
@@ -9276,11 +11375,11 @@ async function runSetupCommand(invocation) {
9276
11375
  return 0;
9277
11376
  }
9278
11377
  if (invocation.format !== "json") {
9279
- info(`\u{1F680} Setup for: ${invocation.cwd}`);
11378
+ if (!interactive) info(`\u{1F680} Setup for: ${invocation.cwd}`);
9280
11379
  if (init.reused) {
9281
- info(`\u267B Keeping ${path13.basename(init.configPath)} (preset: ${init.preset})`);
11380
+ info(`\u267B Keeping ${path17.basename(init.configPath)} (${presetSummary(init)})`);
9282
11381
  } else {
9283
- info(`\u2705 Created ${path13.basename(init.configPath)} (preset: ${init.preset})`);
11382
+ info(`\u2705 Created ${path17.basename(init.configPath)} (${presetSummary(init)})`);
9284
11383
  }
9285
11384
  if (init.scripts.length > 0) info(` Added npm scripts: ${init.scripts.join(", ")}`);
9286
11385
  if (init.dependency.added && init.dependency.range !== null) {
@@ -9309,6 +11408,7 @@ async function runSetupCommand(invocation) {
9309
11408
  if ((invocation.skipArtifacts ?? []).length > 0) {
9310
11409
  generateArgs.push("--skip-artifacts", (invocation.skipArtifacts ?? []).join(","));
9311
11410
  }
11411
+ if (invocation.watch) generateArgs.push("--watch");
9312
11412
  if (invocation.format === "json") {
9313
11413
  const generated = await captureStdout(() => main(generateArgs));
9314
11414
  let manifest = null;
@@ -9348,6 +11448,24 @@ async function runSetupCommand(invocation) {
9348
11448
  return fail(invocation.format, err.message);
9349
11449
  }
9350
11450
  }
11451
+ async function firstRun(invocation) {
11452
+ const plan = planFirstRun({
11453
+ hasConfig: findConfigFile(invocation.cwd) !== null,
11454
+ hasPriorSnapshot: hasPriorSnapshot(invocation.cwd, invocation.outDir ?? DEFAULT_ANSWERS.outDir),
11455
+ // A machine must never be asked a question, and `--format json` is how a machine says so.
11456
+ interactive: invocation.format === "text" && isInteractive(defaultIO()),
11457
+ autopilot: invocation.autopilot,
11458
+ emptyTarget: isEffectivelyEmpty(invocation.cwd)
11459
+ });
11460
+ if (plan.action === "generate") return null;
11461
+ if (plan.action === "refuse") {
11462
+ return fail(invocation.format, firstRunMessage(plan.reason, invocation.cwd), plan.exitCode);
11463
+ }
11464
+ info(
11465
+ plan.interactive ? "\u2699 This project has no snapshot configuration yet." : "\u2699 --autopilot: setting this project up before generating."
11466
+ );
11467
+ return await runSetupCommand(invocation, { interactive: plan.interactive });
11468
+ }
9351
11469
  function runSkillCommand(invocation) {
9352
11470
  if (!invocation.install) {
9353
11471
  if (invocation.format === "json") {
@@ -9369,7 +11487,7 @@ function runSkillCommand(invocation) {
9369
11487
  }
9370
11488
  info(`\u2705 Agent skill installed (${SKILL_NAME}):`);
9371
11489
  for (const item of result.written) {
9372
- const shown = path13.relative(invocation.cwd, item.path) || item.path;
11490
+ const shown = path17.relative(invocation.cwd, item.path) || item.path;
9373
11491
  info(` - ${item.action.padEnd(9)} ${shown}`);
9374
11492
  }
9375
11493
  if (result.gitignored.length > 0) {
@@ -9464,9 +11582,16 @@ ${USAGE}
9464
11582
  if (invocation.command === "skill") {
9465
11583
  return runSkillCommand(invocation);
9466
11584
  }
11585
+ if (invocation.command === "mcp") {
11586
+ return await runStdioServer({ cwd: path17.resolve(invocation.cwd) });
11587
+ }
9467
11588
  if (invocation.command === "setup") {
9468
11589
  return runSetupCommand(invocation);
9469
11590
  }
11591
+ if (invocation.command === "generate" && !invocation.noConfig && (!invocation.explicitCommand || invocation.autopilot)) {
11592
+ const decided = await firstRun(invocation);
11593
+ if (decided !== null) return decided;
11594
+ }
9470
11595
  const skipConfig = invocation.noConfig || invocation.command === "diff";
9471
11596
  const loaded = skipConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
9472
11597
  if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
@@ -9475,7 +11600,7 @@ ${USAGE}
9475
11600
  warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
9476
11601
  return 1;
9477
11602
  }
9478
- if (loaded.file) info(`\u2699 Using config ${path13.relative(process.cwd(), loaded.file) || loaded.file}`);
11603
+ if (loaded.file) info(`\u2699 Using config ${path17.relative(process.cwd(), loaded.file) || loaded.file}`);
9479
11604
  const resolved = resolveConfig(
9480
11605
  loaded.config,
9481
11606
  {
@@ -9497,10 +11622,10 @@ ${USAGE}
9497
11622
  if (invocation.command === "validate") {
9498
11623
  const result = validateSnapshot(resolved);
9499
11624
  if (result.valid) {
9500
- const manifestPath = path13.resolve(resolved.cwd, resolved.outDir, "manifest.json");
11625
+ const manifestPath = path17.resolve(resolved.cwd, resolved.outDir, "manifest.json");
9501
11626
  let manifest;
9502
11627
  try {
9503
- manifest = JSON.parse(fs12.readFileSync(manifestPath, "utf8"));
11628
+ manifest = JSON.parse(fs16.readFileSync(manifestPath, "utf8"));
9504
11629
  } catch {
9505
11630
  manifest = void 0;
9506
11631
  }
@@ -9513,13 +11638,13 @@ ${USAGE}
9513
11638
  return result.valid ? 0 : 1;
9514
11639
  }
9515
11640
  if (invocation.command === "report") {
9516
- const manifestPath = path13.resolve(resolved.cwd, resolved.outDir, "manifest.json");
9517
- if (!fs12.existsSync(manifestPath)) {
11641
+ const manifestPath = path17.resolve(resolved.cwd, resolved.outDir, "manifest.json");
11642
+ if (!fs16.existsSync(manifestPath)) {
9518
11643
  return fail(invocation.format, `Manifest not found at ${manifestPath}. Run generate first.`);
9519
11644
  }
9520
11645
  let manifest;
9521
11646
  try {
9522
- manifest = JSON.parse(fs12.readFileSync(manifestPath, "utf8"));
11647
+ manifest = JSON.parse(fs16.readFileSync(manifestPath, "utf8"));
9523
11648
  } catch (err) {
9524
11649
  return fail(invocation.format, `Could not read ${manifestPath}: ${err.message}`);
9525
11650
  }
@@ -9683,20 +11808,20 @@ function finishWatchRun(state, now) {
9683
11808
  };
9684
11809
  }
9685
11810
  function comparablePath(value) {
9686
- const absolute = path13.resolve(value);
11811
+ const absolute = path17.resolve(value);
9687
11812
  return process.platform === "win32" ? absolute.toLowerCase() : absolute;
9688
11813
  }
9689
11814
  function isSameOrInside(parent, candidate) {
9690
- const relative8 = path13.relative(parent, candidate);
9691
- return relative8 === "" || relative8 !== ".." && !relative8.startsWith(`..${path13.sep}`) && !path13.isAbsolute(relative8);
11815
+ const relative11 = path17.relative(parent, candidate);
11816
+ return relative11 === "" || relative11 !== ".." && !relative11.startsWith(`..${path17.sep}`) && !path17.isAbsolute(relative11);
9692
11817
  }
9693
11818
  function isGeneratedWatchPath(candidate, paths) {
9694
- const absolute = comparablePath(path13.resolve(paths.cwd, candidate));
9695
- const outDir = comparablePath(path13.resolve(paths.cwd, paths.outDir));
9696
- const tmpDir = comparablePath(path13.resolve(paths.cwd, paths.tmpDir));
11819
+ const absolute = comparablePath(path17.resolve(paths.cwd, candidate));
11820
+ const outDir = comparablePath(path17.resolve(paths.cwd, paths.outDir));
11821
+ const tmpDir = comparablePath(path17.resolve(paths.cwd, paths.tmpDir));
9697
11822
  if (isSameOrInside(outDir, absolute) || isSameOrInside(tmpDir, absolute)) return true;
9698
- const parent = path13.dirname(absolute);
9699
- if (parent === path13.dirname(outDir) && path13.basename(absolute).startsWith(`${path13.basename(outDir)}.backup-`)) {
11823
+ const parent = path17.dirname(absolute);
11824
+ if (parent === path17.dirname(outDir) && path17.basename(absolute).startsWith(`${path17.basename(outDir)}.backup-`)) {
9700
11825
  return true;
9701
11826
  }
9702
11827
  return absolute.split(/[\\/]+/).some((segment) => VCS_SEGMENTS.has(segment));
@@ -9713,8 +11838,8 @@ function decideWatchEvent(state, event, paths) {
9713
11838
  return { state, action: "schedule" };
9714
11839
  }
9715
11840
  async function runWatchLoop(invocation, resolved) {
9716
- const outDir = path13.resolve(resolved.cwd, resolved.outDir);
9717
- const tmpDir = path13.resolve(resolved.cwd, resolved.tmpDir);
11841
+ const outDir = path17.resolve(resolved.cwd, resolved.outDir);
11842
+ const tmpDir = path17.resolve(resolved.cwd, resolved.tmpDir);
9718
11843
  const paths = { cwd: resolved.cwd, outDir, tmpDir };
9719
11844
  let state = initialWatchState();
9720
11845
  let timer = null;
@@ -9779,11 +11904,11 @@ async function runWatchLoop(invocation, resolved) {
9779
11904
  };
9780
11905
  info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
9781
11906
  void regenerate();
9782
- const watcher = fs12.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
11907
+ const watcher = fs16.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
9783
11908
  const decision = decideWatchEvent(
9784
11909
  state,
9785
11910
  {
9786
- path: filename ? path13.resolve(resolved.cwd, filename.toString()) : null,
11911
+ path: filename ? path17.resolve(resolved.cwd, filename.toString()) : null,
9787
11912
  now: performance.now()
9788
11913
  },
9789
11914
  paths
@@ -9798,12 +11923,12 @@ async function runWatchLoop(invocation, resolved) {
9798
11923
  }
9799
11924
  if (decision.action === "schedule") schedule();
9800
11925
  });
9801
- await new Promise((resolve6) => {
11926
+ await new Promise((resolve9) => {
9802
11927
  const stop = () => {
9803
11928
  stopped = true;
9804
11929
  if (timer) clearTimeout(timer);
9805
11930
  watcher.close();
9806
- resolve6();
11931
+ resolve9();
9807
11932
  };
9808
11933
  process.once("SIGINT", stop);
9809
11934
  process.once("SIGTERM", stop);
@@ -9811,7 +11936,7 @@ async function runWatchLoop(invocation, resolved) {
9811
11936
  return 0;
9812
11937
  }
9813
11938
  var entryFile = process.argv[1] ?? "";
9814
- var entryName = path13.basename(entryFile).toLowerCase();
11939
+ var entryName = path17.basename(entryFile).toLowerCase();
9815
11940
  var invokedDirectly = entryName === "cli.js" || entryName === "cli.cjs";
9816
11941
  if (invokedDirectly) {
9817
11942
  void main().then(