codebase-snapshot 0.9.0 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var childProcess = require('child_process');
4
- var fs10 = require('fs');
5
- var path11 = require('path');
4
+ var fs11 = require('fs');
5
+ var path12 = require('path');
6
6
  var util = require('util');
7
7
  var crypto2 = require('crypto');
8
8
  var module$1 = require('module');
@@ -33,8 +33,8 @@ function _interopNamespace(e) {
33
33
  }
34
34
 
35
35
  var childProcess__namespace = /*#__PURE__*/_interopNamespace(childProcess);
36
- var fs10__namespace = /*#__PURE__*/_interopNamespace(fs10);
37
- var path11__namespace = /*#__PURE__*/_interopNamespace(path11);
36
+ var fs11__namespace = /*#__PURE__*/_interopNamespace(fs11);
37
+ var path12__namespace = /*#__PURE__*/_interopNamespace(path12);
38
38
  var crypto2__namespace = /*#__PURE__*/_interopNamespace(crypto2);
39
39
  var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
40
40
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
@@ -87,15 +87,15 @@ var SKIPPED_WALK_DIRS = /* @__PURE__ */ new Set([
87
87
  ]);
88
88
  function readSmallFile(fullPath) {
89
89
  try {
90
- const stats = fs10__namespace.statSync(fullPath);
90
+ const stats = fs11__namespace.statSync(fullPath);
91
91
  if (!stats.isFile() || stats.size > MANIFEST_MAX_BYTES) return null;
92
- return fs10__namespace.readFileSync(fullPath, "utf8");
92
+ return fs11__namespace.readFileSync(fullPath, "utf8");
93
93
  } catch {
94
94
  return null;
95
95
  }
96
96
  }
97
97
  function readManifest(cwd, name) {
98
- return readSmallFile(path11__namespace.join(cwd, name));
98
+ return readSmallFile(path12__namespace.join(cwd, name));
99
99
  }
100
100
  function dependenciesFromPackageJson(raw) {
101
101
  const names = /* @__PURE__ */ new Set();
@@ -155,7 +155,7 @@ function pnpmWorkspacePatterns(raw) {
155
155
  function childDirectories(cwd, parent) {
156
156
  let entries;
157
157
  try {
158
- entries = fs10__namespace.readdirSync(path11__namespace.join(cwd, parent), { withFileTypes: true });
158
+ entries = fs11__namespace.readdirSync(path12__namespace.join(cwd, parent), { withFileTypes: true });
159
159
  } catch {
160
160
  return [];
161
161
  }
@@ -190,13 +190,13 @@ function workspacePackageDirs(cwd, patterns) {
190
190
  function scanWorkspacePackages(cwd, patterns) {
191
191
  const files = /* @__PURE__ */ new Set();
192
192
  const dependencies = /* @__PURE__ */ new Set();
193
- for (const relative8 of workspacePackageDirs(cwd, patterns)) {
194
- const full = path11__namespace.join(cwd, relative8);
193
+ for (const relative11 of workspacePackageDirs(cwd, patterns)) {
194
+ const full = path12__namespace.join(cwd, relative11);
195
195
  try {
196
- for (const entry of fs10__namespace.readdirSync(full, { withFileTypes: true })) files.add(entry.name);
196
+ for (const entry of fs11__namespace.readdirSync(full, { withFileTypes: true })) files.add(entry.name);
197
197
  } catch {
198
198
  }
199
- const raw = readSmallFile(path11__namespace.join(full, "package.json"));
199
+ const raw = readSmallFile(path12__namespace.join(full, "package.json"));
200
200
  if (raw !== null) {
201
201
  for (const name of dependenciesFromPackageJson(raw)) dependencies.add(name);
202
202
  }
@@ -213,7 +213,7 @@ function countMarkdownFiles(cwd) {
213
213
  directoriesSeen += 1;
214
214
  let entries;
215
215
  try {
216
- entries = fs10__namespace.readdirSync(current.dir, { withFileTypes: true });
216
+ entries = fs11__namespace.readdirSync(current.dir, { withFileTypes: true });
217
217
  } catch {
218
218
  continue;
219
219
  }
@@ -224,7 +224,7 @@ function countMarkdownFiles(cwd) {
224
224
  }
225
225
  if (!entry.isDirectory() || current.depth >= 2) continue;
226
226
  if (entry.name.startsWith(".") || SKIPPED_WALK_DIRS.has(entry.name)) continue;
227
- queue.push({ dir: path11__namespace.join(current.dir, entry.name), depth: current.depth + 1 });
227
+ queue.push({ dir: path12__namespace.join(current.dir, entry.name), depth: current.depth + 1 });
228
228
  }
229
229
  }
230
230
  return count;
@@ -233,7 +233,7 @@ function buildPresetContext(cwd) {
233
233
  const files = /* @__PURE__ */ new Set();
234
234
  const dirs = /* @__PURE__ */ new Set();
235
235
  try {
236
- for (const entry of fs10__namespace.readdirSync(cwd, { withFileTypes: true })) {
236
+ for (const entry of fs11__namespace.readdirSync(cwd, { withFileTypes: true })) {
237
237
  if (entry.isDirectory()) dirs.add(entry.name);
238
238
  else if (entry.isFile()) files.add(entry.name);
239
239
  }
@@ -250,7 +250,7 @@ function buildPresetContext(cwd) {
250
250
  const isWorkspace = files.has("pnpm-workspace.yaml") || files.has("nx.json") || files.has("turbo.json") || files.has("lerna.json") || packageJson !== void 0 && packageJson.includes('"workspaces"') || declaredWorkspaces.length > 0;
251
251
  let workspace = { files: /* @__PURE__ */ new Set(), dependencies: /* @__PURE__ */ new Set() };
252
252
  if (isWorkspace) {
253
- const pnpmRaw = files.has("pnpm-workspace.yaml") ? readSmallFile(path11__namespace.join(cwd, "pnpm-workspace.yaml")) : null;
253
+ const pnpmRaw = files.has("pnpm-workspace.yaml") ? readSmallFile(path12__namespace.join(cwd, "pnpm-workspace.yaml")) : null;
254
254
  const declared = pnpmRaw === null ? [] : pnpmWorkspacePatterns(pnpmRaw);
255
255
  const patterns = declared.length > 0 ? declared : declaredWorkspaces.length > 0 ? declaredWorkspaces : DEFAULT_WORKSPACE_PATTERNS;
256
256
  workspace = scanWorkspacePackages(cwd, patterns);
@@ -268,25 +268,41 @@ function buildPresetContext(cwd) {
268
268
  markdownCount: files.has("package.json") ? 0 : countMarkdownFiles(cwd)
269
269
  };
270
270
  }
271
- function hasFile(context, name) {
272
- return context.files.has(name);
271
+
272
+ // src/presets/evidence.ts
273
+ function fileEvidence(context, name) {
274
+ return context.files.has(name) ? name : null;
273
275
  }
274
- function hasFileStartingWith(context, prefix) {
276
+ function fileStartingWithEvidence(context, prefix) {
275
277
  for (const file of context.files) {
276
- if (file.startsWith(prefix)) return true;
278
+ if (file.startsWith(prefix)) return file;
277
279
  }
278
280
  for (const file of context.workspaceFiles) {
279
- if (file.startsWith(prefix)) return true;
281
+ if (file.startsWith(prefix)) return file;
280
282
  }
281
- return false;
283
+ return null;
284
+ }
285
+ function dependencyEvidence(context, ...names) {
286
+ for (const name of names) {
287
+ if (context.dependencies.has(name)) return `depends on ${name}`;
288
+ }
289
+ return null;
282
290
  }
283
- function manifestContains(context, manifest, needle) {
291
+ function manifestEvidence(context, manifest, needle) {
284
292
  const content = context.manifests[manifest];
285
- if (!content) return false;
286
- return content.toLowerCase().includes(needle.toLowerCase());
293
+ if (!content) return null;
294
+ if (!content.toLowerCase().includes(needle.toLowerCase())) return null;
295
+ return `${needle} in ${manifest}`;
296
+ }
297
+ function suffixEvidence(context, suffix) {
298
+ for (const file of context.files) {
299
+ if (file.endsWith(suffix)) return file;
300
+ }
301
+ return null;
287
302
  }
288
- function hasDependency(context, ...names) {
289
- return names.some((name) => context.dependencies.has(name));
303
+ function markdownEvidence(context) {
304
+ if (context.markdownCount < 3 || context.files.has("package.json")) return null;
305
+ return `${context.markdownCount} markdown files, no package.json`;
290
306
  }
291
307
 
292
308
  // src/presets/helpers.ts
@@ -494,7 +510,7 @@ var JS_PRESETS = [
494
510
  {
495
511
  name: "nextjs",
496
512
  priority: 100,
497
- detect: (context) => hasFileStartingWith(context, "next.config") || hasDependency(context, "next"),
513
+ detect: (context) => fileStartingWithEvidence(context, "next.config") ?? dependencyEvidence(context, "next"),
498
514
  layers: NEXTJS_LAYERS,
499
515
  kinds: NEXTJS_KINDS,
500
516
  exclude: NEXT_LIKE_EXCLUDES,
@@ -503,7 +519,7 @@ var JS_PRESETS = [
503
519
  {
504
520
  name: "nuxt",
505
521
  priority: 98,
506
- detect: (context) => hasFileStartingWith(context, "nuxt.config") || hasDependency(context, "nuxt", "nuxt3"),
522
+ detect: (context) => fileStartingWithEvidence(context, "nuxt.config") ?? dependencyEvidence(context, "nuxt", "nuxt3"),
507
523
  layers: [
508
524
  layer("pages", "pages/", "src/pages/"),
509
525
  layer("components", "components/", "src/components/"),
@@ -527,7 +543,7 @@ var JS_PRESETS = [
527
543
  {
528
544
  name: "sveltekit",
529
545
  priority: 96,
530
- detect: (context) => hasFileStartingWith(context, "svelte.config") || hasDependency(context, "@sveltejs/kit", "svelte"),
546
+ detect: (context) => fileStartingWithEvidence(context, "svelte.config") ?? dependencyEvidence(context, "@sveltejs/kit", "svelte"),
531
547
  layers: [
532
548
  layer("routes", "src/routes/", "routes/"),
533
549
  layer("lib", "src/lib/", "lib/"),
@@ -548,7 +564,7 @@ var JS_PRESETS = [
548
564
  {
549
565
  name: "remix",
550
566
  priority: 94,
551
- detect: (context) => hasDependency(context, "@remix-run/react", "@remix-run/node", "@remix-run/dev"),
567
+ detect: (context) => dependencyEvidence(context, "@remix-run/react", "@remix-run/node", "@remix-run/dev"),
552
568
  layers: [
553
569
  layer("routes", "app/routes/", "routes/"),
554
570
  layer("components", "app/components/"),
@@ -562,7 +578,7 @@ var JS_PRESETS = [
562
578
  {
563
579
  name: "astro",
564
580
  priority: 92,
565
- detect: (context) => hasFileStartingWith(context, "astro.config") || hasDependency(context, "astro"),
581
+ detect: (context) => fileStartingWithEvidence(context, "astro.config") ?? dependencyEvidence(context, "astro"),
566
582
  layers: [
567
583
  layer("pages", "src/pages/"),
568
584
  layer("layouts", "src/layouts/"),
@@ -577,7 +593,7 @@ var JS_PRESETS = [
577
593
  {
578
594
  name: "angular",
579
595
  priority: 90,
580
- detect: (context) => hasFile(context, "angular.json") || hasDependency(context, "@angular/core"),
596
+ detect: (context) => fileEvidence(context, "angular.json") ?? dependencyEvidence(context, "@angular/core"),
581
597
  layers: [
582
598
  layer("app", "src/app/"),
583
599
  layer("features", "src/app/features/"),
@@ -601,7 +617,7 @@ var JS_PRESETS = [
601
617
  {
602
618
  name: "solid",
603
619
  priority: 88,
604
- detect: (context) => hasDependency(context, "solid-js", "solid-start"),
620
+ detect: (context) => dependencyEvidence(context, "solid-js", "solid-start"),
605
621
  layers: [
606
622
  layer("pages", "src/routes/", "src/pages/"),
607
623
  layer("components", "src/components/"),
@@ -614,7 +630,15 @@ var JS_PRESETS = [
614
630
  {
615
631
  name: "vue-vite",
616
632
  priority: 86,
617
- detect: (context) => hasDependency(context, "vue") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
633
+ // `vue` is the decisive signal; `vite` (or its config file) only says the app is
634
+ // built with Vite rather than shipped pre-compiled, so it is required but not what
635
+ // gets reported.
636
+ detect: (context) => {
637
+ const framework = dependencyEvidence(context, "vue");
638
+ if (framework === null) return null;
639
+ const build = dependencyEvidence(context, "vite") ?? fileStartingWithEvidence(context, "vite.config");
640
+ return build === null ? null : framework;
641
+ },
618
642
  layers: [
619
643
  layer("pages", "src/pages/", "src/views/"),
620
644
  layer("components", "src/components/"),
@@ -639,7 +663,12 @@ var JS_PRESETS = [
639
663
  {
640
664
  name: "react-vite",
641
665
  priority: 84,
642
- detect: (context) => hasDependency(context, "react") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
666
+ detect: (context) => {
667
+ const framework = dependencyEvidence(context, "react");
668
+ if (framework === null) return null;
669
+ const build = dependencyEvidence(context, "vite") ?? fileStartingWithEvidence(context, "vite.config");
670
+ return build === null ? null : framework;
671
+ },
643
672
  layers: VITE_LAYERS,
644
673
  kinds: VITE_KINDS,
645
674
  exclude: [".vite", "dist"],
@@ -648,7 +677,7 @@ var JS_PRESETS = [
648
677
  {
649
678
  name: "nest",
650
679
  priority: 80,
651
- detect: (context) => hasDependency(context, "@nestjs/core"),
680
+ detect: (context) => dependencyEvidence(context, "@nestjs/core"),
652
681
  layers: [
653
682
  layer("modules", "src/modules/", "modules/"),
654
683
  layer("controllers", "src/controllers/", "controllers/"),
@@ -674,7 +703,7 @@ var JS_PRESETS = [
674
703
  {
675
704
  name: "node-express",
676
705
  priority: 78,
677
- detect: (context) => hasDependency(context, "express", "fastify", "koa", "@hapi/hapi"),
706
+ detect: (context) => dependencyEvidence(context, "express", "fastify", "koa", "@hapi/hapi"),
678
707
  layers: NODE_LAYERS,
679
708
  kinds: NODE_KINDS,
680
709
  exclude: ["dist"],
@@ -685,7 +714,7 @@ var JS_PRESETS = [
685
714
  // A bare package.json is often just tooling around a non-JS project, so it
686
715
  // ranks below every definitive language manifest.
687
716
  priority: 45,
688
- detect: (context) => hasFile(context, "package.json"),
717
+ detect: (context) => fileEvidence(context, "package.json"),
689
718
  layers: [
690
719
  layer("src", "src/"),
691
720
  layer("lib", "lib/"),
@@ -699,13 +728,12 @@ var JS_PRESETS = [
699
728
  ];
700
729
 
701
730
  // src/presets/lang-presets.ts
702
- var anyFileEndingWith = (context, suffix) => Array.from(context.files).some((file) => file.endsWith(suffix));
703
731
  var DIR = (name) => `${name}/`;
704
732
  var LANGUAGE_PRESETS = [
705
733
  {
706
734
  name: "python-django",
707
735
  priority: 56,
708
- detect: (context) => hasFile(context, "manage.py") || manifestContains(context, "pyproject.toml", "django") || manifestContains(context, "requirements.txt", "django"),
736
+ detect: (context) => fileEvidence(context, "manage.py") ?? manifestEvidence(context, "pyproject.toml", "django") ?? manifestEvidence(context, "requirements.txt", "django"),
709
737
  layers: [
710
738
  layer("views", DIR("views"), DIR("app/views"), DIR("apps/views")),
711
739
  layer("models", DIR("models"), DIR("app/models"), DIR("apps/models")),
@@ -731,7 +759,7 @@ var LANGUAGE_PRESETS = [
731
759
  {
732
760
  name: "python-fastapi",
733
761
  priority: 54,
734
- detect: (context) => manifestContains(context, "requirements.txt", "fastapi") || manifestContains(context, "requirements.txt", "flask") || manifestContains(context, "pyproject.toml", "fastapi") || manifestContains(context, "pyproject.toml", "flask"),
762
+ detect: (context) => manifestEvidence(context, "requirements.txt", "fastapi") ?? manifestEvidence(context, "requirements.txt", "flask") ?? manifestEvidence(context, "pyproject.toml", "fastapi") ?? manifestEvidence(context, "pyproject.toml", "flask"),
735
763
  layers: [
736
764
  layer("routers", DIR("routers"), DIR("routes"), DIR("api")),
737
765
  layer("models", DIR("models"), DIR("db")),
@@ -755,7 +783,7 @@ var LANGUAGE_PRESETS = [
755
783
  name: "python-lib",
756
784
  // Below node-lib: a Node repo with a stray .py file is still a Node repo.
757
785
  priority: 30,
758
- detect: (context) => hasFile(context, "pyproject.toml") || hasFile(context, "setup.py") || hasFile(context, "requirements.txt") || anyFileEndingWith(context, ".py"),
786
+ detect: (context) => fileEvidence(context, "pyproject.toml") ?? fileEvidence(context, "setup.py") ?? fileEvidence(context, "requirements.txt") ?? suffixEvidence(context, ".py"),
759
787
  layers: [
760
788
  layer("package", DIR("src"), DIR("lib")),
761
789
  layer("tests", DIR("tests"), DIR("test")),
@@ -768,7 +796,7 @@ var LANGUAGE_PRESETS = [
768
796
  {
769
797
  name: "go",
770
798
  priority: 50,
771
- detect: (context) => hasFile(context, "go.mod"),
799
+ detect: (context) => fileEvidence(context, "go.mod"),
772
800
  layers: [
773
801
  layer("cmd", "cmd/"),
774
802
  layer("internal", "internal/"),
@@ -792,7 +820,7 @@ var LANGUAGE_PRESETS = [
792
820
  {
793
821
  name: "rust",
794
822
  priority: 48,
795
- detect: (context) => hasFile(context, "Cargo.toml"),
823
+ detect: (context) => fileEvidence(context, "Cargo.toml"),
796
824
  layers: [
797
825
  layer("src", "src/"),
798
826
  layer("crates", "crates/"),
@@ -808,7 +836,7 @@ var LANGUAGE_PRESETS = [
808
836
  {
809
837
  name: "java-spring",
810
838
  priority: 46,
811
- detect: (context) => hasFile(context, "pom.xml") || hasFile(context, "build.gradle") || hasFile(context, "build.gradle.kts"),
839
+ detect: (context) => fileEvidence(context, "pom.xml") ?? fileEvidence(context, "build.gradle") ?? fileEvidence(context, "build.gradle.kts"),
812
840
  layers: [
813
841
  layer("api", "src/main/java/"),
814
842
  layer("test", "src/test/java/"),
@@ -831,7 +859,7 @@ var LANGUAGE_PRESETS = [
831
859
  {
832
860
  name: "dotnet",
833
861
  priority: 44,
834
- detect: (context) => anyFileEndingWith(context, ".csproj") || anyFileEndingWith(context, ".sln") || hasFile(context, "global.json"),
862
+ detect: (context) => suffixEvidence(context, ".csproj") ?? suffixEvidence(context, ".sln") ?? fileEvidence(context, "global.json"),
835
863
  layers: [
836
864
  layer("web", "src/", "Web/"),
837
865
  layer("domain", "Domain/", "Core/"),
@@ -854,7 +882,7 @@ var LANGUAGE_PRESETS = [
854
882
  {
855
883
  name: "php-laravel",
856
884
  priority: 42,
857
- detect: (context) => hasFile(context, "artisan") || manifestContains(context, "composer.json", "laravel") || hasFile(context, "composer.json"),
885
+ detect: (context) => fileEvidence(context, "artisan") ?? manifestEvidence(context, "composer.json", "laravel") ?? fileEvidence(context, "composer.json"),
858
886
  layers: [
859
887
  layer("app", "app/"),
860
888
  layer("routes", "routes/"),
@@ -880,7 +908,7 @@ var LANGUAGE_PRESETS = [
880
908
  {
881
909
  name: "ruby-rails",
882
910
  priority: 40,
883
- detect: (context) => hasFile(context, "Gemfile") || hasFile(context, "config.ru") || hasFile(context, "Rakefile"),
911
+ detect: (context) => fileEvidence(context, "Gemfile") ?? fileEvidence(context, "config.ru") ?? fileEvidence(context, "Rakefile"),
884
912
  layers: [
885
913
  layer("app", "app/"),
886
914
  layer("controllers", "app/controllers/"),
@@ -953,7 +981,7 @@ var SHAPE_PRESETS = [
953
981
  {
954
982
  name: "monorepo",
955
983
  priority: 70,
956
- detect: (context) => hasFile(context, "pnpm-workspace.yaml") || hasFile(context, "nx.json") || hasFile(context, "turbo.json") || hasFile(context, "lerna.json") || manifestContains(context, "package.json", '"workspaces"'),
984
+ detect: (context) => fileEvidence(context, "pnpm-workspace.yaml") ?? fileEvidence(context, "nx.json") ?? fileEvidence(context, "turbo.json") ?? fileEvidence(context, "lerna.json") ?? manifestEvidence(context, "package.json", '"workspaces"'),
957
985
  layers: [
958
986
  layer("apps", "apps/"),
959
987
  layer("packages", "packages/"),
@@ -967,7 +995,7 @@ var SHAPE_PRESETS = [
967
995
  {
968
996
  name: "infra",
969
997
  priority: 24,
970
- 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"),
998
+ detect: (context) => suffixEvidence(context, ".tf") ?? fileEvidence(context, "Chart.yaml") ?? fileEvidence(context, "kustomization.yaml") ?? fileEvidence(context, "docker-compose.yml") ?? fileEvidence(context, "docker-compose.yaml"),
971
999
  layers: [
972
1000
  layer("modules", "modules/"),
973
1001
  layer("environments", "env/", "environments/"),
@@ -989,7 +1017,7 @@ var SHAPE_PRESETS = [
989
1017
  // up to two directories down (bounded in the context builder). Counting
990
1018
  // only the root turned every real documentation repository — README.md plus
991
1019
  // the pages under `docs/` — into `generic`.
992
- detect: (context) => context.markdownCount >= 3 && !hasFile(context, "package.json"),
1020
+ detect: (context) => markdownEvidence(context),
993
1021
  layers: [
994
1022
  layer("content", "content/", "docs/"),
995
1023
  layer("assets", "assets/", "static/", "public/"),
@@ -1002,7 +1030,10 @@ var SHAPE_PRESETS = [
1002
1030
  {
1003
1031
  name: "generic",
1004
1032
  priority: 0,
1005
- detect: () => true,
1033
+ // No detector: this is the fallback, and `detectPresetFromContext` reaches it only
1034
+ // when every other preset reported no evidence. `fallback: true` is what lets the
1035
+ // wizard tell "I recognised this" apart from "I gave up".
1036
+ fallback: true,
1006
1037
  layers: DEFAULT_LAYERS,
1007
1038
  kinds: DEFAULT_KINDS,
1008
1039
  exclude: [],
@@ -1034,20 +1065,27 @@ function detectPresetFromContext(context, forced) {
1034
1065
  if (forced !== void 0) {
1035
1066
  const preset = BY_NAME.get(forced);
1036
1067
  if (!preset) throw new Error(unknownPresetMessage(forced));
1037
- return toDetected(preset);
1068
+ return toDetected(preset, null);
1038
1069
  }
1039
1070
  for (const preset of PRESETS) {
1040
- if (preset.detect(context)) return toDetected(preset);
1071
+ const reason = preset.detect?.(context) ?? null;
1072
+ if (reason !== null) return toDetected(preset, reason);
1073
+ }
1074
+ const fallback = PRESETS.find((preset) => preset.fallback === true);
1075
+ if (fallback === void 0) {
1076
+ throw new Error("No fallback preset is defined. Exactly one preset must set `fallback: true`.");
1041
1077
  }
1042
- return toDetected(PRESETS[PRESETS.length - 1]);
1078
+ return toDetected(fallback, null, true);
1043
1079
  }
1044
- function toDetected(preset) {
1080
+ function toDetected(preset, reason, fallback = false) {
1045
1081
  return {
1046
1082
  presetName: preset.name,
1047
1083
  layers: preset.layers,
1048
1084
  kinds: preset.kinds,
1049
1085
  exclude: preset.exclude ? [...preset.exclude] : [],
1050
- recommendedBudget: preset.recommendedBudget ?? null
1086
+ recommendedBudget: preset.recommendedBudget ?? null,
1087
+ reason,
1088
+ fallback
1051
1089
  };
1052
1090
  }
1053
1091
  function detectPreset(cwd, forced) {
@@ -1487,14 +1525,14 @@ var PRESET_IGNORES = new Set(
1487
1525
  );
1488
1526
  function readLinkTarget(linkPath, fallback) {
1489
1527
  try {
1490
- return fs10__namespace.readlinkSync(linkPath);
1528
+ return fs11__namespace.readlinkSync(linkPath);
1491
1529
  } catch {
1492
1530
  return fallback;
1493
1531
  }
1494
1532
  }
1495
1533
  function isInside(root, candidate) {
1496
- const relative8 = path11__namespace.relative(root, candidate);
1497
- return relative8 === "" || !relative8.startsWith(`..${path11__namespace.sep}`) && relative8 !== ".." && !path11__namespace.isAbsolute(relative8);
1534
+ const relative11 = path12__namespace.relative(root, candidate);
1535
+ return relative11 === "" || !relative11.startsWith(`..${path12__namespace.sep}`) && relative11 !== ".." && !path12__namespace.isAbsolute(relative11);
1498
1536
  }
1499
1537
  function getGitInfo(cwd, fallbackBranch = "main") {
1500
1538
  try {
@@ -1699,10 +1737,10 @@ function trimUnescapedTrailingSpaces(line) {
1699
1737
  return line.slice(0, end);
1700
1738
  }
1701
1739
  function readGitignoreRules(dir) {
1702
- const gitignorePath2 = path11__namespace.join(dir, ".gitignore");
1740
+ const gitignorePath2 = path12__namespace.join(dir, ".gitignore");
1703
1741
  let raw;
1704
1742
  try {
1705
- raw = fs10__namespace.readFileSync(gitignorePath2, "utf8");
1743
+ raw = fs11__namespace.readFileSync(gitignorePath2, "utf8");
1706
1744
  } catch {
1707
1745
  return [];
1708
1746
  }
@@ -1829,7 +1867,7 @@ function isToolOwnedDirName(name) {
1829
1867
  }
1830
1868
  function hasSnapshotMarker(target) {
1831
1869
  try {
1832
- return fs10__namespace.statSync(target).isDirectory() && fs10__namespace.existsSync(path11__namespace.join(target, ".codebase-snapshot"));
1870
+ return fs11__namespace.statSync(target).isDirectory() && fs11__namespace.existsSync(path12__namespace.join(target, ".codebase-snapshot"));
1833
1871
  } catch {
1834
1872
  return false;
1835
1873
  }
@@ -1843,7 +1881,7 @@ function makeToolOwnedDecider(cwd, toolOwnedPaths) {
1843
1881
  const prefix = parts.slice(0, depth).join("/");
1844
1882
  let hasMarker = markers.get(prefix);
1845
1883
  if (hasMarker === void 0) {
1846
- hasMarker = fs10__namespace.existsSync(path11__namespace.join(cwd, prefix, ".codebase-snapshot"));
1884
+ hasMarker = fs11__namespace.existsSync(path12__namespace.join(cwd, prefix, ".codebase-snapshot"));
1847
1885
  markers.set(prefix, hasMarker);
1848
1886
  }
1849
1887
  if (hasMarker) return true;
@@ -1858,7 +1896,7 @@ function makeLinkProbe() {
1858
1896
  let known = cache.get(absolutePath);
1859
1897
  if (known === void 0) {
1860
1898
  try {
1861
- known = fs10__namespace.lstatSync(absolutePath).isSymbolicLink();
1899
+ known = fs11__namespace.lstatSync(absolutePath).isSymbolicLink();
1862
1900
  } catch {
1863
1901
  known = false;
1864
1902
  }
@@ -1871,7 +1909,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
1871
1909
  const isLink = makeLinkProbe();
1872
1910
  let rootReal = cwd;
1873
1911
  try {
1874
- rootReal = fs10__namespace.realpathSync(cwd);
1912
+ rootReal = fs11__namespace.realpathSync(cwd);
1875
1913
  } catch {
1876
1914
  }
1877
1915
  const reported = /* @__PURE__ */ new Set();
@@ -1881,7 +1919,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
1881
1919
  let linkedDir = null;
1882
1920
  for (let depth = 1; depth <= parts.length; depth += 1) {
1883
1921
  const prefix = parts.slice(0, depth).join("/");
1884
- if (isLink(path11__namespace.join(cwd, prefix))) {
1922
+ if (isLink(path12__namespace.join(cwd, prefix))) {
1885
1923
  linkedDir = prefix;
1886
1924
  break;
1887
1925
  }
@@ -1892,10 +1930,10 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
1892
1930
  }
1893
1931
  if (reported.has(linkedDir)) continue;
1894
1932
  reported.add(linkedDir);
1895
- const absolute = path11__namespace.join(cwd, linkedDir);
1933
+ const absolute = path12__namespace.join(cwd, linkedDir);
1896
1934
  let targetReal = absolute;
1897
1935
  try {
1898
- targetReal = fs10__namespace.realpathSync(absolute);
1936
+ targetReal = fs11__namespace.realpathSync(absolute);
1899
1937
  } catch {
1900
1938
  }
1901
1939
  const target = readLinkTarget(absolute, targetReal);
@@ -1972,7 +2010,7 @@ function collectFiles(cwd, options = {}) {
1972
2010
  "Discovery mode is `git`, but git could not list this directory's files. Run inside a working repository, or pass --discovery walk (or auto) to scan the tree directly."
1973
2011
  );
1974
2012
  }
1975
- if (fs10__namespace.existsSync(path11__namespace.join(cwd, ".git"))) {
2013
+ if (fs11__namespace.existsSync(path12__namespace.join(cwd, ".git"))) {
1976
2014
  throw new Error(
1977
2015
  "This directory is a git repository, but git could not list its files. A plain walk honours .gitignore files, but git also applies .git/info/exclude and your global excludes file, includes files that are tracked although a .gitignore now matches them, and treats a submodule as a single entry. Fix the repository (git status should work), run outside git, or pass --discovery walk to accept those differences."
1978
2016
  );
@@ -2031,7 +2069,7 @@ function collectFiles(cwd, options = {}) {
2031
2069
  }
2032
2070
  function rootRealFor(root) {
2033
2071
  try {
2034
- return fs10__namespace.realpathSync(root);
2072
+ return fs11__namespace.realpathSync(root);
2035
2073
  } catch {
2036
2074
  return root;
2037
2075
  }
@@ -2039,18 +2077,18 @@ function rootRealFor(root) {
2039
2077
  function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PURE__ */ new Set(), parentGitignoreLayers = []) {
2040
2078
  const results = { files: [], skipped: [] };
2041
2079
  const stack = [];
2042
- const relativeToBase = (target) => path11__namespace.relative(baseDir, target).replace(/\\/g, "/") || ".";
2080
+ const relativeToBase = (target) => path12__namespace.relative(baseDir, target).replace(/\\/g, "/") || ".";
2043
2081
  const enter = (current, layers) => {
2044
2082
  if (visited.has(current)) return false;
2045
2083
  visited.add(current);
2046
2084
  const localRules = readGitignoreRules(current);
2047
2085
  const gitignoreLayers = localRules.length === 0 ? layers : [
2048
2086
  ...layers,
2049
- { prefix: path11__namespace.relative(baseDir, current).replace(/\\/g, "/"), rules: localRules }
2087
+ { prefix: path12__namespace.relative(baseDir, current).replace(/\\/g, "/"), rules: localRules }
2050
2088
  ];
2051
2089
  let entries;
2052
2090
  try {
2053
- entries = fs10__namespace.readdirSync(current, { withFileTypes: true });
2091
+ entries = fs11__namespace.readdirSync(current, { withFileTypes: true });
2054
2092
  } catch {
2055
2093
  results.skipped.push({
2056
2094
  path: relativeToBase(current),
@@ -2072,8 +2110,8 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
2072
2110
  }
2073
2111
  const entry = frame.entries[frame.index];
2074
2112
  frame.index += 1;
2075
- const fullPath = path11__namespace.join(frame.dir, entry.name);
2076
- const relPath = path11__namespace.relative(baseDir, fullPath).replace(/\\/g, "/");
2113
+ const fullPath = path12__namespace.join(frame.dir, entry.name);
2114
+ const relPath = path12__namespace.relative(baseDir, fullPath).replace(/\\/g, "/");
2077
2115
  if (isToolOwnedDirName(entry.name) && reservedPaths.has(relPath)) continue;
2078
2116
  if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
2079
2117
  results.skipped.push({
@@ -2106,14 +2144,14 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
2106
2144
  if (entry.isSymbolicLink()) {
2107
2145
  let targetReal;
2108
2146
  try {
2109
- targetReal = fs10__namespace.realpathSync(fullPath);
2147
+ targetReal = fs11__namespace.realpathSync(fullPath);
2110
2148
  } catch {
2111
2149
  results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
2112
2150
  continue;
2113
2151
  }
2114
2152
  let stats;
2115
2153
  try {
2116
- stats = fs10__namespace.statSync(fullPath);
2154
+ stats = fs11__namespace.statSync(fullPath);
2117
2155
  } catch {
2118
2156
  results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
2119
2157
  continue;
@@ -2164,6 +2202,20 @@ var CONFIG_FILENAMES = [
2164
2202
  ".snapshotrc.json",
2165
2203
  ".snapshotrc"
2166
2204
  ];
2205
+ function findConfigFile(cwd) {
2206
+ for (const filename of CONFIG_FILENAMES) {
2207
+ const fullPath = path12__namespace.join(cwd, filename);
2208
+ if (fs11__namespace.existsSync(fullPath)) return fullPath;
2209
+ }
2210
+ const pkgJsonPath = path12__namespace.join(cwd, "package.json");
2211
+ if (!fs11__namespace.existsSync(pkgJsonPath)) return null;
2212
+ try {
2213
+ const parsed = JSON.parse(fs11__namespace.readFileSync(pkgJsonPath, "utf8"));
2214
+ return parsed.snapshot === void 0 ? null : pkgJsonPath;
2215
+ } catch {
2216
+ return null;
2217
+ }
2218
+ }
2167
2219
  var RESOLVED_CONFIG_BRAND = /* @__PURE__ */ Symbol.for(
2168
2220
  "codebase-snapshot/resolved-config"
2169
2221
  );
@@ -2409,11 +2461,11 @@ async function loadConfigFileDetailed(cwd) {
2409
2461
  const warnings = [];
2410
2462
  const errors = [];
2411
2463
  for (const filename of CONFIG_FILENAMES) {
2412
- const fullPath = path11__namespace.join(cwd, filename);
2413
- if (!fs10__namespace.existsSync(fullPath)) continue;
2464
+ const fullPath = path12__namespace.join(cwd, filename);
2465
+ if (!fs11__namespace.existsSync(fullPath)) continue;
2414
2466
  if (filename.endsWith(".json") || filename === ".snapshotrc") {
2415
2467
  try {
2416
- const config = parseJsonish(fs10__namespace.readFileSync(fullPath, "utf8"), fullPath);
2468
+ const config = parseJsonish(fs11__namespace.readFileSync(fullPath, "utf8"), fullPath);
2417
2469
  if (config === null || typeof config !== "object" || Array.isArray(config)) {
2418
2470
  throw new Error(
2419
2471
  `${fullPath} does not hold a configuration object (received ${describeValue2(config)}).`
@@ -2447,10 +2499,10 @@ async function loadConfigFileDetailed(cwd) {
2447
2499
  continue;
2448
2500
  }
2449
2501
  }
2450
- const pkgJsonPath = path11__namespace.join(cwd, "package.json");
2451
- if (fs10__namespace.existsSync(pkgJsonPath)) {
2502
+ const pkgJsonPath = path12__namespace.join(cwd, "package.json");
2503
+ if (fs11__namespace.existsSync(pkgJsonPath)) {
2452
2504
  try {
2453
- const parsed = parseJsonish(fs10__namespace.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
2505
+ const parsed = parseJsonish(fs11__namespace.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
2454
2506
  const snapshotKey = parsed.snapshot;
2455
2507
  if (snapshotKey && typeof snapshotKey === "object" && !Array.isArray(snapshotKey)) {
2456
2508
  return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
@@ -2618,12 +2670,12 @@ function resolveKindRules(value) {
2618
2670
  function outputExcludePatterns(cwd, outDir, tmpDir) {
2619
2671
  const patterns = [];
2620
2672
  for (const dir of [outDir, tmpDir]) {
2621
- const absolute = path11__namespace.resolve(cwd, dir);
2622
- const relative8 = path11__namespace.relative(cwd, absolute);
2623
- if (!relative8 || relative8 === ".." || relative8.startsWith(`..${path11__namespace.sep}`) || path11__namespace.isAbsolute(relative8)) {
2673
+ const absolute = path12__namespace.resolve(cwd, dir);
2674
+ const relative11 = path12__namespace.relative(cwd, absolute);
2675
+ if (!relative11 || relative11 === ".." || relative11.startsWith(`..${path12__namespace.sep}`) || path12__namespace.isAbsolute(relative11)) {
2624
2676
  continue;
2625
2677
  }
2626
- patterns.push(`${relative8.replace(/\\/g, "/")}/**`);
2678
+ patterns.push(`${relative11.replace(/\\/g, "/")}/**`);
2627
2679
  }
2628
2680
  return patterns;
2629
2681
  }
@@ -2739,11 +2791,11 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
2739
2791
  );
2740
2792
  }
2741
2793
  const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
2742
- const resolved = path11__namespace.resolve(candidate);
2743
- if (!fs10__namespace.existsSync(resolved)) {
2794
+ const resolved = path12__namespace.resolve(candidate);
2795
+ if (!fs11__namespace.existsSync(resolved)) {
2744
2796
  throw new Error(`Working directory does not exist: ${resolved}`);
2745
2797
  }
2746
- if (!fs10__namespace.statSync(resolved).isDirectory()) {
2798
+ if (!fs11__namespace.statSync(resolved).isDirectory()) {
2747
2799
  throw new Error(`Working directory is not a directory: ${resolved}`);
2748
2800
  }
2749
2801
  return resolved;
@@ -2969,7 +3021,7 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2969
3021
  ".so"
2970
3022
  ]);
2971
3023
  function isBinaryExtension(filePath) {
2972
- const ext = path11__namespace.extname(filePath).toLowerCase();
3024
+ const ext = path12__namespace.extname(filePath).toLowerCase();
2973
3025
  return BINARY_EXTENSIONS.has(ext);
2974
3026
  }
2975
3027
  function countControlBytes(buffer) {
@@ -3136,13 +3188,13 @@ function isBinaryFile(absolutePath) {
3136
3188
  if (isBinaryExtension(absolutePath)) return true;
3137
3189
  let fd;
3138
3190
  try {
3139
- fd = fs10__namespace.openSync(absolutePath, "r");
3191
+ fd = fs11__namespace.openSync(absolutePath, "r");
3140
3192
  const buffer = Buffer.alloc(CHUNK);
3141
3193
  let sample;
3142
3194
  let position = 0;
3143
3195
  while (position < BINARY_SCAN_BYTES) {
3144
3196
  const wanted = Math.min(CHUNK, BINARY_SCAN_BYTES - position);
3145
- const bytesRead = fs10__namespace.readSync(fd, buffer, 0, wanted, position);
3197
+ const bytesRead = fs11__namespace.readSync(fd, buffer, 0, wanted, position);
3146
3198
  if (bytesRead === 0) return false;
3147
3199
  const chunk = buffer.subarray(0, bytesRead);
3148
3200
  if (sample === void 0) {
@@ -3160,7 +3212,7 @@ function isBinaryFile(absolutePath) {
3160
3212
  } finally {
3161
3213
  if (fd !== void 0) {
3162
3214
  try {
3163
- fs10__namespace.closeSync(fd);
3215
+ fs11__namespace.closeSync(fd);
3164
3216
  } catch {
3165
3217
  }
3166
3218
  }
@@ -3173,33 +3225,33 @@ function maskedPath(value, secretRules) {
3173
3225
  }
3174
3226
  function confineToDirectory(directory, filename) {
3175
3227
  if (filename.includes("\0")) return null;
3176
- if (path11__namespace.isAbsolute(filename)) return null;
3177
- const base = path11__namespace.resolve(directory);
3178
- const resolved = path11__namespace.resolve(base, filename);
3228
+ if (path12__namespace.isAbsolute(filename)) return null;
3229
+ const base = path12__namespace.resolve(directory);
3230
+ const resolved = path12__namespace.resolve(base, filename);
3179
3231
  const caseInsensitive = process.platform === "win32" || process.platform === "darwin";
3180
- const prefix = caseInsensitive ? `${base}${path11__namespace.sep}`.toLowerCase() : `${base}${path11__namespace.sep}`;
3232
+ const prefix = caseInsensitive ? `${base}${path12__namespace.sep}`.toLowerCase() : `${base}${path12__namespace.sep}`;
3181
3233
  const candidate = caseInsensitive ? resolved.toLowerCase() : resolved;
3182
3234
  return candidate.startsWith(prefix) ? resolved : null;
3183
3235
  }
3184
3236
  function findStagingDirectory(config) {
3185
- const tmpDir = path11__namespace.resolve(config.cwd, config.tmpDir);
3237
+ const tmpDir = path12__namespace.resolve(config.cwd, config.tmpDir);
3186
3238
  let entries;
3187
3239
  try {
3188
- entries = fs10__namespace.readdirSync(tmpDir);
3240
+ entries = fs11__namespace.readdirSync(tmpDir);
3189
3241
  } catch {
3190
3242
  return null;
3191
3243
  }
3192
3244
  const runs = entries.filter((name) => name.startsWith("run-")).sort();
3193
3245
  const newest = runs[runs.length - 1];
3194
- return newest ? path11__namespace.join(tmpDir, newest) : null;
3246
+ return newest ? path12__namespace.join(tmpDir, newest) : null;
3195
3247
  }
3196
3248
  function describeMissingManifest(config, manifestPath) {
3197
- const outDir = path11__namespace.resolve(config.cwd, config.outDir);
3198
- const markerPath = path11__namespace.join(outDir, ".codebase-snapshot");
3249
+ const outDir = path12__namespace.resolve(config.cwd, config.outDir);
3250
+ const markerPath = path12__namespace.join(outDir, ".codebase-snapshot");
3199
3251
  let marker2 = null;
3200
3252
  try {
3201
- if (fs10__namespace.existsSync(markerPath)) {
3202
- const parsed = JSON.parse(fs10__namespace.readFileSync(markerPath, "utf8"));
3253
+ if (fs11__namespace.existsSync(markerPath)) {
3254
+ const parsed = JSON.parse(fs11__namespace.readFileSync(markerPath, "utf8"));
3203
3255
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3204
3256
  marker2 = parsed;
3205
3257
  }
@@ -3235,14 +3287,14 @@ function sha256Hex(buffer) {
3235
3287
  }
3236
3288
  var VERIFY_SNIFF_MAX_BYTES = 8 * 1024 * 1024;
3237
3289
  function wouldNowBeIncluded(cwd, relativePath, reason) {
3238
- const absolutePath = path11__namespace.join(cwd, relativePath);
3290
+ const absolutePath = path12__namespace.join(cwd, relativePath);
3239
3291
  try {
3240
- const stats = fs10__namespace.statSync(absolutePath);
3292
+ const stats = fs11__namespace.statSync(absolutePath);
3241
3293
  if (!stats.isFile()) return false;
3242
3294
  if (isBinaryFile(absolutePath)) return false;
3243
3295
  if (reason !== "binary") return true;
3244
3296
  if (stats.size > VERIFY_SNIFF_MAX_BYTES) return false;
3245
- return !decodeText(fs10__namespace.readFileSync(absolutePath)).binary;
3297
+ return !decodeText(fs11__namespace.readFileSync(absolutePath)).binary;
3246
3298
  } catch {
3247
3299
  return false;
3248
3300
  }
@@ -3300,8 +3352,8 @@ function describeManifestShapeProblem(value) {
3300
3352
  function validateSnapshot(configInput) {
3301
3353
  assertUsableConfigInput(configInput);
3302
3354
  const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
3303
- const manifestPath = path11__namespace.resolve(config.cwd, config.outDir, "manifest.json");
3304
- if (!fs10__namespace.existsSync(manifestPath)) {
3355
+ const manifestPath = path12__namespace.resolve(config.cwd, config.outDir, "manifest.json");
3356
+ if (!fs11__namespace.existsSync(manifestPath)) {
3305
3357
  return {
3306
3358
  valid: false,
3307
3359
  manifestPresent: false,
@@ -3312,7 +3364,7 @@ function validateSnapshot(configInput) {
3312
3364
  }
3313
3365
  let manifest;
3314
3366
  try {
3315
- manifest = JSON.parse(fs10__namespace.readFileSync(manifestPath, "utf8"));
3367
+ manifest = JSON.parse(fs11__namespace.readFileSync(manifestPath, "utf8"));
3316
3368
  } catch (err) {
3317
3369
  return {
3318
3370
  valid: false,
@@ -3334,7 +3386,7 @@ function validateSnapshot(configInput) {
3334
3386
  }
3335
3387
  const drift = emptyDrift();
3336
3388
  const warnings = [];
3337
- const snapshotDir = path11__namespace.resolve(config.cwd, config.outDir);
3389
+ const snapshotDir = path12__namespace.resolve(config.cwd, config.outDir);
3338
3390
  const manifestFileMap = /* @__PURE__ */ new Map();
3339
3391
  for (const file of manifest.files) {
3340
3392
  if (file && typeof file.relativePath === "string") {
@@ -3368,7 +3420,7 @@ function validateSnapshot(configInput) {
3368
3420
  }
3369
3421
  let diskHash;
3370
3422
  try {
3371
- diskHash = sha256Hex(fs10__namespace.readFileSync(path11__namespace.join(config.cwd, diskFile)));
3423
+ diskHash = sha256Hex(fs11__namespace.readFileSync(path12__namespace.join(config.cwd, diskFile)));
3372
3424
  } catch (err) {
3373
3425
  if (err.code === "ENOENT") {
3374
3426
  drift.missingOnDisk.push(diskFile);
@@ -3392,7 +3444,7 @@ function validateSnapshot(configInput) {
3392
3444
  }
3393
3445
  let statFailure = null;
3394
3446
  try {
3395
- fs10__namespace.statSync(confined);
3447
+ fs11__namespace.statSync(confined);
3396
3448
  } catch (err) {
3397
3449
  statFailure = err;
3398
3450
  }
@@ -3412,7 +3464,7 @@ function validateSnapshot(configInput) {
3412
3464
  ...Object.values(config.budgets.byLayer)
3413
3465
  );
3414
3466
  const partNames = /* @__PURE__ */ new Set();
3415
- const partsDir = path11__namespace.join(snapshotDir, "parts");
3467
+ const partsDir = path12__namespace.join(snapshotDir, "parts");
3416
3468
  for (const part of manifest.parts) {
3417
3469
  if (!part || typeof part.filename !== "string") continue;
3418
3470
  partNames.add(part.filename);
@@ -3427,7 +3479,7 @@ function validateSnapshot(configInput) {
3427
3479
  }
3428
3480
  let buffer;
3429
3481
  try {
3430
- buffer = fs10__namespace.readFileSync(partPath);
3482
+ buffer = fs11__namespace.readFileSync(partPath);
3431
3483
  } catch {
3432
3484
  drift.partIssues.push({ part: part.filename, issue: "missing from snapshot/parts" });
3433
3485
  continue;
@@ -3641,9 +3693,9 @@ function validateSnapshot(configInput) {
3641
3693
  ["full-codebase.md", manifest.artifacts?.fullMarkdown]
3642
3694
  ]) {
3643
3695
  if (!artifact) continue;
3644
- const artifactPath = path11__namespace.join(snapshotDir, key);
3696
+ const artifactPath = path12__namespace.join(snapshotDir, key);
3645
3697
  try {
3646
- const buffer = fs10__namespace.readFileSync(artifactPath);
3698
+ const buffer = fs11__namespace.readFileSync(artifactPath);
3647
3699
  if (sha256Hex(buffer) !== artifact.sha256 || buffer.length !== artifact.byteSize) {
3648
3700
  drift.partIssues.push({ part: key, issue: "artifact does not match its recorded digest" });
3649
3701
  }
@@ -3904,14 +3956,14 @@ function compareManifests(before, after) {
3904
3956
  function loadManifest(target) {
3905
3957
  let isDirectory;
3906
3958
  try {
3907
- isDirectory = fs10__namespace.statSync(target).isDirectory();
3959
+ isDirectory = fs11__namespace.statSync(target).isDirectory();
3908
3960
  } catch {
3909
3961
  throw new Error(`No snapshot found at ${target}: the path does not exist.`);
3910
3962
  }
3911
- const manifestPath = isDirectory ? path11__namespace.join(target, "manifest.json") : target;
3963
+ const manifestPath = isDirectory ? path12__namespace.join(target, "manifest.json") : target;
3912
3964
  let raw;
3913
3965
  try {
3914
- raw = fs10__namespace.readFileSync(manifestPath, "utf8");
3966
+ raw = fs11__namespace.readFileSync(manifestPath, "utf8");
3915
3967
  } catch {
3916
3968
  throw new Error(`Could not read a manifest at ${manifestPath}.`);
3917
3969
  }
@@ -4062,10 +4114,10 @@ var LANGUAGE_BY_BASENAME = {
4062
4114
  };
4063
4115
  function detectLanguage(filePath) {
4064
4116
  const normalized = filePath.replace(/\\/g, "/");
4065
- const base = path11__namespace.posix.basename(normalized);
4117
+ const base = path12__namespace.posix.basename(normalized);
4066
4118
  if (LANGUAGE_BY_BASENAME[base]) return LANGUAGE_BY_BASENAME[base];
4067
4119
  if (base === ".env" || base.startsWith(".env.")) return "dotenv";
4068
- const ext = path11__namespace.posix.extname(base).toLowerCase();
4120
+ const ext = path12__namespace.posix.extname(base).toLowerCase();
4069
4121
  return LANGUAGE_BY_EXTENSION[ext] ?? "text";
4070
4122
  }
4071
4123
 
@@ -4107,42 +4159,42 @@ function describeMatcher(matcher) {
4107
4159
  return String(matcher);
4108
4160
  }
4109
4161
  function normalizeTarget(cwd, target) {
4110
- const absolute = path11__namespace.resolve(cwd, target);
4111
- const relative8 = path11__namespace.relative(cwd, absolute);
4112
- if (relative8.startsWith("..") || path11__namespace.isAbsolute(relative8)) return target;
4113
- return relative8.split(path11__namespace.sep).join("/");
4162
+ const absolute = path12__namespace.resolve(cwd, target);
4163
+ const relative11 = path12__namespace.relative(cwd, absolute);
4164
+ if (relative11.startsWith("..") || path12__namespace.isAbsolute(relative11)) return target;
4165
+ return relative11.split(path12__namespace.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
  }
4129
4181
  return nearest;
4130
4182
  }
4131
4183
  function readManifest2(config) {
4132
- const manifestPath = path11__namespace.resolve(config.cwd, config.outDir, "manifest.json");
4184
+ const manifestPath = path12__namespace.resolve(config.cwd, config.outDir, "manifest.json");
4133
4185
  try {
4134
- return JSON.parse(fs10__namespace.readFileSync(manifestPath, "utf8"));
4186
+ return JSON.parse(fs11__namespace.readFileSync(manifestPath, "utf8"));
4135
4187
  } catch {
4136
4188
  return null;
4137
4189
  }
4138
4190
  }
4139
4191
  function explainPath(config, target) {
4140
- const relative8 = normalizeTarget(config.cwd, target);
4141
- const absolute = path11__namespace.join(config.cwd, relative8);
4142
- const maskedPath2 = findSecretInPath(relative8, config.secretRules)?.masked ?? relative8;
4192
+ const relative11 = normalizeTarget(config.cwd, target);
4193
+ const absolute = path12__namespace.join(config.cwd, relative11);
4194
+ const maskedPath2 = findSecretInPath(relative11, config.secretRules)?.masked ?? relative11;
4143
4195
  const result = {
4144
4196
  path: maskedPath2,
4145
- exists: fs10__namespace.existsSync(absolute),
4197
+ exists: fs11__namespace.existsSync(absolute),
4146
4198
  included: false
4147
4199
  };
4148
4200
  const collection = collectFiles(config.cwd, {
@@ -4153,12 +4205,12 @@ function explainPath(config, target) {
4153
4205
  // Same exclusion `generate` applies, so `explain` never says "not-discovered" about a file
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
- (directory) => path11__namespace.relative(config.cwd, path11__namespace.resolve(config.cwd, directory)).replace(/\\/g, "/")
4157
- ).filter((relative9) => relative9.length > 0 && !relative9.startsWith(".."))
4208
+ (directory) => path12__namespace.relative(config.cwd, path12__namespace.resolve(config.cwd, directory)).replace(/\\/g, "/")
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,15 +4237,15 @@ 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
- if (fs10__namespace.existsSync(absolute) && fs10__namespace.statSync(absolute).isFile()) {
4196
- const buffer = fs10__namespace.readFileSync(absolute);
4247
+ if (fs11__namespace.existsSync(absolute) && fs11__namespace.statSync(absolute).isFile()) {
4248
+ const buffer = fs11__namespace.readFileSync(absolute);
4197
4249
  const decoded = decodeText(buffer);
4198
4250
  if (decoded.binary) {
4199
4251
  result.included = false;
@@ -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;
@@ -4252,15 +4304,15 @@ function renderExplainText(result) {
4252
4304
  function appendGitignoreEntries(cwd, entries) {
4253
4305
  const wanted = [...new Set(entries)].filter((entry) => entry.trim().length > 0);
4254
4306
  if (wanted.length === 0) return { added: [], note: null };
4255
- const ignorePath = path11__namespace.join(cwd, ".gitignore");
4307
+ const ignorePath = path12__namespace.join(cwd, ".gitignore");
4256
4308
  const manual = `Add ${wanted.map((entry) => `"${entry}"`).join(" and ")} to your .gitignore yourself.`;
4257
4309
  let existing = "";
4258
4310
  try {
4259
- if (fs10__namespace.existsSync(ignorePath)) {
4260
- if (fs10__namespace.statSync(ignorePath).isDirectory()) {
4311
+ if (fs11__namespace.existsSync(ignorePath)) {
4312
+ if (fs11__namespace.statSync(ignorePath).isDirectory()) {
4261
4313
  return { added: [], note: `${ignorePath} is a directory, so it was not updated. ${manual}` };
4262
4314
  }
4263
- existing = fs10__namespace.readFileSync(ignorePath, "utf8");
4315
+ existing = fs11__namespace.readFileSync(ignorePath, "utf8");
4264
4316
  }
4265
4317
  } catch (err) {
4266
4318
  return {
@@ -4273,7 +4325,7 @@ function appendGitignoreEntries(cwd, entries) {
4273
4325
  if (added.length === 0) return { added: [], note: null };
4274
4326
  const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
4275
4327
  try {
4276
- fs10__namespace.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
4328
+ fs11__namespace.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
4277
4329
  `, "utf8");
4278
4330
  } catch (err) {
4279
4331
  return { added: [], note: `${err.message}. ${manual}` };
@@ -4281,12 +4333,12 @@ function appendGitignoreEntries(cwd, entries) {
4281
4333
  return { added, note: null };
4282
4334
  }
4283
4335
  function gitignorePath(cwd, filePath) {
4284
- return path11__namespace.relative(cwd, filePath).split(path11__namespace.sep).join("/");
4336
+ return path12__namespace.relative(cwd, filePath).split(path12__namespace.sep).join("/");
4285
4337
  }
4286
4338
 
4287
4339
  // src/version.ts
4288
4340
  var GENERATOR_NAME = "codebase-snapshot";
4289
- var GENERATOR_VERSION = "0.9.0";
4341
+ var GENERATOR_VERSION = "0.9.4";
4290
4342
  var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
4291
4343
  function isOurGeneratorName(name) {
4292
4344
  if (typeof name !== "string") return false;
@@ -4491,7 +4543,7 @@ function keyReader(input) {
4491
4543
  next() {
4492
4544
  const queued = queue.shift();
4493
4545
  if (queued) return Promise.resolve(queued);
4494
- return new Promise((resolve6) => waiters.push(resolve6));
4546
+ return new Promise((resolve9) => waiters.push(resolve9));
4495
4547
  },
4496
4548
  close() {
4497
4549
  input.off("keypress", onKey);
@@ -4674,10 +4726,10 @@ var CONFIG_CANDIDATES = [
4674
4726
  ];
4675
4727
  function chooseConfigFileName(cwd) {
4676
4728
  if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
4677
- const pkgPath = path11__namespace.join(cwd, "package.json");
4678
- if (!fs10__namespace.existsSync(pkgPath)) return "snapshot.config.mjs";
4729
+ const pkgPath = path12__namespace.join(cwd, "package.json");
4730
+ if (!fs11__namespace.existsSync(pkgPath)) return "snapshot.config.mjs";
4679
4731
  try {
4680
- const parsed = JSON.parse(fs10__namespace.readFileSync(pkgPath, "utf8"));
4732
+ const parsed = JSON.parse(fs11__namespace.readFileSync(pkgPath, "utf8"));
4681
4733
  return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
4682
4734
  } catch {
4683
4735
  return "snapshot.config.mjs";
@@ -4691,7 +4743,7 @@ var SIZE_CHOICES = [
4691
4743
  ];
4692
4744
  function packageJsonHasConfig(cwd) {
4693
4745
  try {
4694
- const parsed = JSON.parse(fs10__namespace.readFileSync(path11__namespace.join(cwd, "package.json"), "utf8"));
4746
+ const parsed = JSON.parse(fs11__namespace.readFileSync(path12__namespace.join(cwd, "package.json"), "utf8"));
4695
4747
  return typeof parsed.snapshot === "object" && parsed.snapshot !== null && !Array.isArray(parsed.snapshot);
4696
4748
  } catch {
4697
4749
  return false;
@@ -4712,7 +4764,7 @@ function artifactAnswers(options) {
4712
4764
  }
4713
4765
  function topLevelDirs(cwd) {
4714
4766
  try {
4715
- return fs10__namespace.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
4767
+ return fs11__namespace.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
4716
4768
  } catch {
4717
4769
  return [];
4718
4770
  }
@@ -4725,7 +4777,7 @@ function describeJsonValue(value) {
4725
4777
  }
4726
4778
  function writeFileOrExplain(target, content) {
4727
4779
  try {
4728
- fs10__namespace.writeFileSync(target, content, "utf8");
4780
+ fs11__namespace.writeFileSync(target, content, "utf8");
4729
4781
  } catch (err) {
4730
4782
  const code = err.code;
4731
4783
  const reason = code === "EACCES" || code === "EPERM" || code === "EROFS" ? `permission denied (${code})` : code === "EISDIR" ? "it is a directory" : `${err.message}`;
@@ -4736,7 +4788,7 @@ function canWriteModuleConfig(name, cwd) {
4736
4788
  if (name.endsWith(".mjs") || name.endsWith(".ts")) return true;
4737
4789
  if (!name.endsWith(".js")) return false;
4738
4790
  try {
4739
- const parsed = JSON.parse(fs10__namespace.readFileSync(path11__namespace.join(cwd, "package.json"), "utf8"));
4791
+ const parsed = JSON.parse(fs11__namespace.readFileSync(path12__namespace.join(cwd, "package.json"), "utf8"));
4740
4792
  return parsed.type === "module";
4741
4793
  } catch {
4742
4794
  return false;
@@ -4771,19 +4823,19 @@ function serializePackageJson(raw, value) {
4771
4823
  return `${text2}${raw.endsWith("\n") ? crlf ? "\r\n" : "\n" : ""}`;
4772
4824
  }
4773
4825
  function updatePackageJson(cwd, notes) {
4774
- const packagePath = path11__namespace.join(cwd, "package.json");
4826
+ const packagePath = path12__namespace.join(cwd, "package.json");
4775
4827
  const unchanged = {
4776
4828
  scripts: [],
4777
4829
  dependency: { name: GENERATOR_NAME, range: null, added: false }
4778
4830
  };
4779
- if (!fs10__namespace.existsSync(packagePath)) {
4831
+ if (!fs11__namespace.existsSync(packagePath)) {
4780
4832
  notes.push("No package.json found, so npm scripts and the devDependency were not added.");
4781
4833
  return unchanged;
4782
4834
  }
4783
4835
  let raw;
4784
4836
  let parsed;
4785
4837
  try {
4786
- raw = fs10__namespace.readFileSync(packagePath, "utf8");
4838
+ raw = fs11__namespace.readFileSync(packagePath, "utf8");
4787
4839
  parsed = JSON.parse(raw);
4788
4840
  } catch {
4789
4841
  notes.push("Could not parse package.json; npm scripts and the devDependency were not added.");
@@ -4846,16 +4898,18 @@ function updatePackageJson(cwd, notes) {
4846
4898
  return { scripts: added, dependency };
4847
4899
  }
4848
4900
  var GITIGNORE_ENTRIES = ["snapshot/", "snapshot.tmp/"];
4849
- function resolvePresetChoice(answers, presetName) {
4901
+ function resolvePresetChoice(detectedName, presetName) {
4850
4902
  return {
4851
4903
  value: presetName,
4852
4904
  label: presetName,
4853
- hint: presetName === answers.preset ? "detected" : void 0
4905
+ hint: presetName === detectedName ? "detected" : void 0
4854
4906
  };
4855
4907
  }
4856
4908
  async function runInit(options) {
4857
4909
  const io = options.io ?? defaultIO();
4858
4910
  const detected = detectPreset(options.cwd, options.preset);
4911
+ let reason = detected.reason;
4912
+ let fallback = detected.fallback;
4859
4913
  const notes = [];
4860
4914
  const answers = {
4861
4915
  preset: detected.presetName,
@@ -4877,117 +4931,148 @@ async function runInit(options) {
4877
4931
  if (interactive) {
4878
4932
  io.output.write("\n codebase-snapshot\n");
4879
4933
  io.output.write(" Turn this repository into an LLM-ready, redacted, byte-bounded snapshot.\n\n");
4880
- io.output.write(` Detected ${detected.presetName}
4934
+ if (fallback) {
4935
+ io.output.write(" \u26A0 Could not tell what kind of project this is.\n");
4936
+ io.output.write(" No package.json, go.mod, Cargo.toml, pyproject.toml, Gemfile,\n");
4937
+ io.output.write(" composer.json, pom.xml, *.csproj, next.config.*, vite.config.*, *.tf\n");
4938
+ io.output.write(" or workspace marker was found here.\n\n");
4939
+ } else {
4940
+ io.output.write(` Detected ${detected.presetName}
4941
+ `);
4942
+ io.output.write(` Because ${reason}
4943
+
4881
4944
  `);
4945
+ }
4882
4946
  io.output.write(` Directory ${options.cwd}
4883
4947
 
4884
4948
  `);
4885
- const useDetected = await confirm(io, `Set up for ${detected.presetName}?`, {
4886
- defaultValue: true
4949
+ const paceChoices = fallback ? [
4950
+ { value: "choose", label: "Pick the closest preset", hint: "then generate with the defaults" },
4951
+ { value: "recommended", label: "Use the defaults", hint: "generic \xB7 everything the scanner finds" },
4952
+ { value: "walk", label: "Go through each setting", hint: "every question starts on its default" }
4953
+ ] : [
4954
+ {
4955
+ value: "recommended",
4956
+ label: "Use the recommended setup",
4957
+ hint: `${detected.presetName} \xB7 config \xB7 npm scripts \xB7 install \xB7 generate`
4958
+ },
4959
+ { value: "walk", label: "Go through each setting", hint: "every question starts on its default" },
4960
+ { value: "choose", label: "Pick a different preset", hint: "everything else stays at the defaults" }
4961
+ ];
4962
+ const pace = await select(io, "How should I set this up?", paceChoices, {
4963
+ defaultValue: fallback ? "choose" : "recommended"
4887
4964
  });
4888
- if (!useDetected) {
4965
+ if (pace === "choose") {
4889
4966
  answers.preset = await select(
4890
4967
  io,
4891
4968
  "What kind of project is this?",
4892
- listPresetNames().map((name) => resolvePresetChoice(answers, name))
4969
+ listPresetNames().map(
4970
+ (name) => resolvePresetChoice(fallback ? null : detected.presetName, name)
4971
+ ),
4972
+ { defaultValue: answers.preset }
4893
4973
  );
4894
4974
  const chosen = detectPreset(options.cwd, answers.preset);
4895
4975
  answers.preset = chosen.presetName;
4976
+ reason = null;
4977
+ fallback = false;
4896
4978
  }
4897
- const scope = await select(
4898
- io,
4899
- "What should go into the snapshot?",
4900
- [
4901
- { value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
4902
- { value: "folders", label: "Only specific folders\u2026" },
4903
- { value: "no-tests", label: "Everything, but skip tests" },
4904
- { value: "custom", label: "Let me write glob patterns" }
4905
- ],
4906
- { defaultValue: "all" }
4907
- );
4908
- if (scope === "folders") {
4909
- const dirs = topLevelDirs(options.cwd).slice(0, 40);
4910
- if (dirs.length === 0) {
4911
- notes.push("No top-level directories found; the snapshot will include everything.");
4979
+ if (pace === "walk") {
4980
+ const scope = await select(
4981
+ io,
4982
+ "What should go into the snapshot?",
4983
+ [
4984
+ { value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
4985
+ { value: "folders", label: "Only specific folders\u2026" },
4986
+ { value: "no-tests", label: "Everything, but skip tests" },
4987
+ { value: "custom", label: "Let me write glob patterns" }
4988
+ ],
4989
+ { defaultValue: "all" }
4990
+ );
4991
+ if (scope === "folders") {
4992
+ const dirs = topLevelDirs(options.cwd).slice(0, 40);
4993
+ if (dirs.length === 0) {
4994
+ notes.push("No top-level directories found; the snapshot will include everything.");
4995
+ } else {
4996
+ const chosen = await multiSelect(
4997
+ io,
4998
+ "Select folders",
4999
+ dirs.map((name) => ({ value: name, label: `${name}/` })),
5000
+ { defaults: dirs }
5001
+ );
5002
+ answers.include = chosen.map((name) => `${name}/**`);
5003
+ }
5004
+ } else if (scope === "no-tests") {
5005
+ answers.excludePatterns = [
5006
+ ...answers.excludePatterns,
5007
+ "**/*.test.*",
5008
+ "**/*.spec.*",
5009
+ "tests",
5010
+ "test"
5011
+ ];
5012
+ } else if (scope === "custom") {
5013
+ const raw = await text(io, "Glob patterns to include (comma separated)", {
5014
+ defaultValue: ""
5015
+ });
5016
+ answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
5017
+ }
5018
+ const currentBudget = String(answers.maxPartBytes);
5019
+ const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
5020
+ defaultValue: SIZE_CHOICES.some((choice) => choice.value === currentBudget) ? currentBudget : "120k"
5021
+ });
5022
+ if (sizeChoice === "custom") {
5023
+ const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
5024
+ defaultValue: String(answers.maxPartBytes),
5025
+ validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null
5026
+ });
5027
+ answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
4912
5028
  } else {
4913
- const chosen = await multiSelect(
4914
- io,
4915
- "Select folders",
4916
- dirs.map((name) => ({ value: name, label: `${name}/` })),
4917
- { defaults: dirs }
4918
- );
4919
- answers.include = chosen.map((name) => `${name}/**`);
4920
- }
4921
- } else if (scope === "no-tests") {
4922
- answers.excludePatterns = [
4923
- ...answers.excludePatterns,
4924
- "**/*.test.*",
4925
- "**/*.spec.*",
4926
- "tests",
4927
- "test"
5029
+ answers.maxPartBytes = sizeChoice;
5030
+ }
5031
+ const artifactChoices = [
5032
+ { value: "readme", label: "README.md", hint: "orientation page" },
5033
+ { value: "manifest", label: "manifest.json", hint: "required by validate and report" },
5034
+ { value: "parts", label: "parts/", hint: "the code payload" },
5035
+ { value: "navigation", label: "reports/navigation.md", hint: "file \u2192 part index" },
5036
+ { value: "projectTree", label: "reports/project-tree.md", hint: "ASCII tree" },
5037
+ { value: "audit", label: "reports/audit.md", hint: "security and health findings" },
5038
+ { value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
4928
5039
  ];
4929
- } else if (scope === "custom") {
4930
- const raw = await text(io, "Glob patterns to include (comma separated)", {
4931
- defaultValue: ""
4932
- });
4933
- answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
4934
- }
4935
- const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
4936
- defaultValue: "120k"
4937
- });
4938
- if (sizeChoice === "custom") {
4939
- const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
4940
- defaultValue: String(answers.maxPartBytes),
4941
- validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null
4942
- });
4943
- answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
4944
- } else {
4945
- answers.maxPartBytes = sizeChoice;
4946
- }
4947
- const artifactChoices = [
4948
- { value: "readme", label: "README.md", hint: "orientation page" },
4949
- { value: "manifest", label: "manifest.json", hint: "required by validate and report" },
4950
- { value: "parts", label: "parts/", hint: "the code payload" },
4951
- { value: "navigation", label: "reports/navigation.md", hint: "file \u2192 part index" },
4952
- { value: "projectTree", label: "reports/project-tree.md", hint: "ASCII tree" },
4953
- { value: "audit", label: "reports/audit.md", hint: "security and health findings" },
4954
- { value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
4955
- ];
4956
- const presetEnabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
4957
- const selectedArtifacts = await multiSelect(
4958
- io,
4959
- "Which artifacts should be generated?",
4960
- artifactChoices,
4961
- { defaults: presetEnabled }
4962
- );
4963
- for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
4964
- const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
4965
- answers.includeSensitiveFiles = await confirm(
4966
- io,
4967
- "Include credential files (.env, *.pem, .npmrc, .ssh/)?",
4968
- { defaultValue: false }
4969
- );
4970
- io.output.write("\n Review\n\n");
4971
- io.output.write(` project ${answers.preset}
5040
+ const presetEnabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
5041
+ const selectedArtifacts = await multiSelect(
5042
+ io,
5043
+ "Which artifacts should be generated?",
5044
+ artifactChoices,
5045
+ { defaults: presetEnabled }
5046
+ );
5047
+ for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
5048
+ const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
5049
+ answers.includeSensitiveFiles = await confirm(
5050
+ io,
5051
+ "Include credential files (.env, *.pem, .npmrc, .ssh/)?",
5052
+ { defaultValue: false }
5053
+ );
5054
+ io.output.write("\n Review\n\n");
5055
+ io.output.write(` project ${answers.preset}
4972
5056
  `);
4973
- io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
5057
+ io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
4974
5058
  `);
4975
- io.output.write(` part size ${String(answers.maxPartBytes)}
5059
+ io.output.write(` part size ${String(answers.maxPartBytes)}
4976
5060
  `);
4977
- io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
5061
+ io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
4978
5062
  `);
4979
- io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
5063
+ io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
4980
5064
 
4981
5065
  `);
4982
- io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
4983
- io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
4984
- const proceed = await confirm(io, "Write this and continue?", { defaultValue: true });
4985
- if (!proceed) throw new PromptCancelled();
5066
+ io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
5067
+ io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
5068
+ const proceed = await confirm(io, "Write this and continue?", { defaultValue: true });
5069
+ if (!proceed) throw new PromptCancelled();
5070
+ }
4986
5071
  }
4987
5072
  const configText = renderConfig(answers);
4988
5073
  const preferredName = chooseConfigFileName(options.cwd);
4989
5074
  const presentConfigs = CONFIG_CANDIDATES.filter(
4990
- (name) => fs10__namespace.existsSync(path11__namespace.join(options.cwd, name))
5075
+ (name) => fs11__namespace.existsSync(path12__namespace.join(options.cwd, name))
4991
5076
  );
4992
5077
  const present = presentConfigs[0] ?? null;
4993
5078
  if (presentConfigs.length > 1) {
@@ -4996,7 +5081,7 @@ async function runInit(options) {
4996
5081
  );
4997
5082
  }
4998
5083
  const configName = present !== null && options.force ? present : preferredName;
4999
- const configPath = path11__namespace.join(options.cwd, configName);
5084
+ const configPath = path12__namespace.join(options.cwd, configName);
5000
5085
  const reused = present !== null && options.force !== true && options.reuseConfig === true;
5001
5086
  if (reused) notes.push(`Kept the existing ${present} instead of writing a new one.`);
5002
5087
  if (options.dryRun) {
@@ -5006,6 +5091,8 @@ async function runInit(options) {
5006
5091
  written: false,
5007
5092
  reused,
5008
5093
  preset: answers.preset,
5094
+ reason,
5095
+ fallback,
5009
5096
  scripts: [],
5010
5097
  gitignoreEntries: [],
5011
5098
  dependency: { name: GENERATOR_NAME, range: `^${GENERATOR_VERSION}`, added: false },
@@ -5027,7 +5114,7 @@ async function runInit(options) {
5027
5114
  'package.json already defines a "snapshot" config. Remove that key, or re-run with --force to write a config file that takes precedence over it.'
5028
5115
  );
5029
5116
  }
5030
- if (fs10__namespace.existsSync(configPath) && !options.force && !reused) {
5117
+ if (fs11__namespace.existsSync(configPath) && !options.force && !reused) {
5031
5118
  throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
5032
5119
  }
5033
5120
  if (!reused) writeFileOrExplain(configPath, configText);
@@ -5040,12 +5127,56 @@ async function runInit(options) {
5040
5127
  written: true,
5041
5128
  reused,
5042
5129
  preset: answers.preset,
5130
+ reason,
5131
+ fallback,
5043
5132
  scripts,
5044
5133
  gitignoreEntries: ignore.added,
5045
5134
  dependency,
5046
5135
  notes
5047
5136
  };
5048
5137
  }
5138
+ function describeTarget(cwd) {
5139
+ const relative11 = path12__namespace.relative(process.cwd(), cwd);
5140
+ if (relative11 === "") return "this directory";
5141
+ if (relative11.startsWith("..") || path12__namespace.isAbsolute(relative11)) return cwd;
5142
+ return relative11;
5143
+ }
5144
+ function firstRunMessage(reason, cwd) {
5145
+ const where = describeTarget(cwd);
5146
+ if (reason === "empty") {
5147
+ return `Nothing to snapshot: ${where} has no files. Run this inside the repository you want to snapshot, or pass --cwd.`;
5148
+ }
5149
+ return `No configuration found in ${where} and this session is not interactive, so nothing was generated.
5150
+ Choose one:
5151
+ ${GENERATOR_NAME} --autopilot detect the project, set it up and generate, no questions
5152
+ ${GENERATOR_NAME} init --yes write a config from the detected defaults, then generate
5153
+ ${GENERATOR_NAME} init write a config interactively`;
5154
+ }
5155
+ function planFirstRun(facts) {
5156
+ if (facts.emptyTarget) return { action: "refuse", exitCode: 1, reason: "empty" };
5157
+ if (facts.autopilot) return { action: "setup", interactive: false };
5158
+ if (facts.hasConfig || facts.hasPriorSnapshot) return { action: "generate" };
5159
+ if (facts.interactive) return { action: "setup", interactive: true };
5160
+ return { action: "refuse", exitCode: 2, reason: "undecided" };
5161
+ }
5162
+ var NOT_CONTENT = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr", ".DS_Store", "Thumbs.db"]);
5163
+ function isEffectivelyEmpty(cwd) {
5164
+ try {
5165
+ return fs11__namespace.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
5166
+ } catch {
5167
+ return false;
5168
+ }
5169
+ }
5170
+ function hasPriorSnapshot(cwd, outDir) {
5171
+ let parsed;
5172
+ try {
5173
+ parsed = JSON.parse(fs11__namespace.readFileSync(path12__namespace.join(cwd, outDir, ".codebase-snapshot"), "utf8"));
5174
+ } catch {
5175
+ return false;
5176
+ }
5177
+ if (parsed === null || typeof parsed !== "object") return false;
5178
+ return isOurGeneratorName(parsed.generator);
5179
+ }
5049
5180
 
5050
5181
  // src/ast/profiles/shared.ts
5051
5182
  var MAX_SUMMARY_CHARS = 1e6;
@@ -5269,9 +5400,9 @@ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)
5269
5400
  var jvmProfile = {
5270
5401
  id: "jvm",
5271
5402
  extensions: [".java", ".kt", ".kts", ".scala"],
5272
- summarize(path14, content) {
5403
+ summarize(path18, content) {
5273
5404
  const summary = emptySummary();
5274
- const lower = path14.toLowerCase();
5405
+ const lower = path18.toLowerCase();
5275
5406
  const isJava = lower.endsWith(".java");
5276
5407
  const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
5277
5408
  eachLine(content, (line) => {
@@ -5724,16 +5855,16 @@ function summarizeTypeScriptSource(relativePath, content) {
5724
5855
  // src/ast/profiles/sfc.ts
5725
5856
  var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
5726
5857
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
5727
- function componentName(path14) {
5728
- const base = path14.split("/").pop() ?? path14;
5858
+ function componentName(path18) {
5859
+ const base = path18.split("/").pop() ?? path18;
5729
5860
  return base.replace(/\.[^.]+$/, "");
5730
5861
  }
5731
5862
  var sfcProfile = {
5732
5863
  id: "sfc",
5733
5864
  extensions: [".vue", ".svelte", ".astro"],
5734
- summarize(path14, content) {
5865
+ summarize(path18, content) {
5735
5866
  let script = "";
5736
- if (path14.toLowerCase().endsWith(".astro")) {
5867
+ if (path18.toLowerCase().endsWith(".astro")) {
5737
5868
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
5738
5869
  if (frontmatter) script += `${frontmatter[1]}
5739
5870
  `;
@@ -5742,7 +5873,7 @@ var sfcProfile = {
5742
5873
  script += `${match[1]}
5743
5874
  `;
5744
5875
  }
5745
- const name = componentName(path14);
5876
+ const name = componentName(path18);
5746
5877
  if (!script.trim()) {
5747
5878
  const summary2 = emptySummary();
5748
5879
  if (name) {
@@ -5776,15 +5907,15 @@ for (const profile of LANGUAGE_PROFILES) {
5776
5907
  BY_EXTENSION.set(extension.toLowerCase(), profile);
5777
5908
  }
5778
5909
  }
5779
- function profileForPath(path14) {
5780
- const lower = path14.toLowerCase();
5910
+ function profileForPath(path18) {
5911
+ const lower = path18.toLowerCase();
5781
5912
  const dot = lower.lastIndexOf(".");
5782
5913
  if (dot === -1) return null;
5783
5914
  return BY_EXTENSION.get(lower.slice(dot)) ?? null;
5784
5915
  }
5785
- function summarizeWithProfile(path14, content) {
5786
- const profile = profileForPath(path14);
5787
- return profile ? profile.summarize(path14, content) : null;
5916
+ function summarizeWithProfile(path18, content) {
5917
+ const profile = profileForPath(path18);
5918
+ return profile ? profile.summarize(path18, content) : null;
5788
5919
  }
5789
5920
 
5790
5921
  // src/ast/structured.ts
@@ -6710,13 +6841,13 @@ function buildImportResolver(files) {
6710
6841
  const relativePath = file.relativePath;
6711
6842
  remember(byPath, relativePath, relativePath);
6712
6843
  remember(byPathFolded, relativePath.toLowerCase(), relativePath);
6713
- const extension = path11__namespace.posix.extname(relativePath);
6844
+ const extension = path12__namespace.posix.extname(relativePath);
6714
6845
  if (!SOURCE_EXTENSIONS.has(extension)) continue;
6715
6846
  const stem = relativePath.slice(0, -extension.length);
6716
6847
  remember(byStem, stem, relativePath);
6717
6848
  remember(byStemFolded, stem.toLowerCase(), relativePath);
6718
- if (path11__namespace.posix.basename(stem) === "index") {
6719
- const directory = path11__namespace.posix.dirname(stem);
6849
+ if (path12__namespace.posix.basename(stem) === "index") {
6850
+ const directory = path12__namespace.posix.dirname(stem);
6720
6851
  remember(byStem, directory, relativePath);
6721
6852
  remember(byStemFolded, directory.toLowerCase(), relativePath);
6722
6853
  }
@@ -6725,8 +6856,8 @@ function buildImportResolver(files) {
6725
6856
  }
6726
6857
  function resolveSpecifier(fromFile, specifier, resolver) {
6727
6858
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
6728
- const joined = path11__namespace.posix.normalize(
6729
- path11__namespace.posix.join(path11__namespace.posix.dirname(fromFile), specifier)
6859
+ const joined = path12__namespace.posix.normalize(
6860
+ path12__namespace.posix.join(path12__namespace.posix.dirname(fromFile), specifier)
6730
6861
  );
6731
6862
  const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
6732
6863
  const direct = exact(joined);
@@ -6819,11 +6950,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
6819
6950
  return errors;
6820
6951
  }
6821
6952
  function resolveSafeOutputDirs(config) {
6822
- const cwd = path11__namespace.resolve(config.cwd);
6823
- const outDir = path11__namespace.resolve(cwd, config.outDir);
6824
- const tmpDir = path11__namespace.resolve(cwd, config.tmpDir);
6825
- const root = path11__namespace.parse(cwd).root;
6826
- const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path11__namespace.sep}`);
6953
+ const cwd = path12__namespace.resolve(config.cwd);
6954
+ const outDir = path12__namespace.resolve(cwd, config.outDir);
6955
+ const tmpDir = path12__namespace.resolve(cwd, config.tmpDir);
6956
+ const root = path12__namespace.parse(cwd).root;
6957
+ const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path12__namespace.sep}`);
6827
6958
  const reject = (reason) => {
6828
6959
  throw new Error(
6829
6960
  `Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
@@ -6879,14 +7010,14 @@ function listEntries(dir) {
6879
7010
  const walk = (absolute, prefix) => {
6880
7011
  let children;
6881
7012
  try {
6882
- children = fs10__namespace.readdirSync(absolute, { withFileTypes: true });
7013
+ children = fs11__namespace.readdirSync(absolute, { withFileTypes: true });
6883
7014
  } catch {
6884
7015
  return;
6885
7016
  }
6886
7017
  for (const child of children) {
6887
- const relative8 = prefix === "" ? child.name : `${prefix}/${child.name}`;
6888
- entries.push(relative8);
6889
- if (child.isDirectory()) walk(path11__namespace.join(absolute, child.name), relative8);
7018
+ const relative11 = prefix === "" ? child.name : `${prefix}/${child.name}`;
7019
+ entries.push(relative11);
7020
+ if (child.isDirectory()) walk(path12__namespace.join(absolute, child.name), relative11);
6890
7021
  }
6891
7022
  };
6892
7023
  walk(dir, "");
@@ -6903,28 +7034,28 @@ function markerPayload(dir, state, id, createdAt) {
6903
7034
  pid: process.pid,
6904
7035
  entries: listEntries(dir)
6905
7036
  };
6906
- const manifestPath = path11__namespace.join(dir, "manifest.json");
6907
- if (fs10__namespace.existsSync(manifestPath)) {
6908
- marker2.manifestSha256 = sha256Hex2(fs10__namespace.readFileSync(manifestPath));
7037
+ const manifestPath = path12__namespace.join(dir, "manifest.json");
7038
+ if (fs11__namespace.existsSync(manifestPath)) {
7039
+ marker2.manifestSha256 = sha256Hex2(fs11__namespace.readFileSync(manifestPath));
6909
7040
  }
6910
7041
  return marker2;
6911
7042
  }
6912
7043
  function writeOwnershipMarker(dir, marker2) {
6913
- fs10__namespace.writeFileSync(
6914
- path11__namespace.join(dir, OWNERSHIP_MARKER),
7044
+ fs11__namespace.writeFileSync(
7045
+ path12__namespace.join(dir, OWNERSHIP_MARKER),
6915
7046
  `${JSON.stringify(marker2, null, 2)}
6916
7047
  `,
6917
7048
  "utf8"
6918
7049
  );
6919
7050
  }
6920
7051
  function readOwnershipMarker(dir) {
6921
- const markerPath = path11__namespace.join(dir, OWNERSHIP_MARKER);
6922
- if (!fs10__namespace.existsSync(markerPath)) {
7052
+ const markerPath = path12__namespace.join(dir, OWNERSHIP_MARKER);
7053
+ if (!fs11__namespace.existsSync(markerPath)) {
6923
7054
  return { marker: null, problem: "it holds no .codebase-snapshot marker" };
6924
7055
  }
6925
7056
  let raw;
6926
7057
  try {
6927
- raw = fs10__namespace.readFileSync(markerPath, "utf8");
7058
+ raw = fs11__namespace.readFileSync(markerPath, "utf8");
6928
7059
  } catch {
6929
7060
  return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
6930
7061
  }
@@ -6992,11 +7123,11 @@ function assessSnapshotDirectory(dir) {
6992
7123
  marker: marker2
6993
7124
  };
6994
7125
  }
6995
- const manifestPath = path11__namespace.join(dir, "manifest.json");
6996
- if (fs10__namespace.existsSync(manifestPath)) {
7126
+ const manifestPath = path12__namespace.join(dir, "manifest.json");
7127
+ if (fs11__namespace.existsSync(manifestPath)) {
6997
7128
  let raw;
6998
7129
  try {
6999
- raw = fs10__namespace.readFileSync(manifestPath, "utf8");
7130
+ raw = fs11__namespace.readFileSync(manifestPath, "utf8");
7000
7131
  } catch {
7001
7132
  return {
7002
7133
  owned: false,
@@ -7078,13 +7209,13 @@ function containsNoFiles(dir) {
7078
7209
  const current = pending.pop();
7079
7210
  let entries;
7080
7211
  try {
7081
- entries = fs10__namespace.readdirSync(current, { withFileTypes: true });
7212
+ entries = fs11__namespace.readdirSync(current, { withFileTypes: true });
7082
7213
  } catch {
7083
7214
  return false;
7084
7215
  }
7085
7216
  for (const entry of entries) {
7086
7217
  if (entry.isDirectory()) {
7087
- pending.push(path11__namespace.join(current, entry.name));
7218
+ pending.push(path12__namespace.join(current, entry.name));
7088
7219
  } else if (!IGNORABLE_ENTRIES.has(entry.name)) {
7089
7220
  return false;
7090
7221
  }
@@ -7093,13 +7224,13 @@ function containsNoFiles(dir) {
7093
7224
  return true;
7094
7225
  }
7095
7226
  function assertReplaceable(target, label, secretRules) {
7096
- if (!fs10__namespace.existsSync(target)) return;
7097
- if (!fs10__namespace.statSync(target).isDirectory()) {
7227
+ if (!fs11__namespace.existsSync(target)) return;
7228
+ if (!fs11__namespace.statSync(target).isDirectory()) {
7098
7229
  throw new Error(
7099
7230
  `Refusing to write into ${label} ${target}: it exists but is not a directory.`
7100
7231
  );
7101
7232
  }
7102
- const entries = fs10__namespace.readdirSync(target);
7233
+ const entries = fs11__namespace.readdirSync(target);
7103
7234
  if (entries.length === 0) return;
7104
7235
  if (entries.every((name) => IGNORABLE_ENTRIES.has(name))) return;
7105
7236
  if (containsNoFiles(target)) return;
@@ -7119,18 +7250,18 @@ function assertReplaceable(target, label, secretRules) {
7119
7250
  );
7120
7251
  }
7121
7252
  function assertStagingRootUsable(tmpDir, secretRules) {
7122
- if (!fs10__namespace.existsSync(tmpDir)) return;
7123
- if (!fs10__namespace.statSync(tmpDir).isDirectory()) {
7253
+ if (!fs11__namespace.existsSync(tmpDir)) return;
7254
+ if (!fs11__namespace.statSync(tmpDir).isDirectory()) {
7124
7255
  throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
7125
7256
  }
7126
- const entries = fs10__namespace.readdirSync(tmpDir);
7257
+ const entries = fs11__namespace.readdirSync(tmpDir);
7127
7258
  if (entries.length === 0) return;
7128
7259
  const foreign = entries.filter((name) => {
7129
- const full = path11__namespace.join(tmpDir, name);
7260
+ const full = path12__namespace.join(tmpDir, name);
7130
7261
  if (!name.startsWith(RUN_DIR_PREFIX)) return true;
7131
7262
  let stats;
7132
7263
  try {
7133
- stats = fs10__namespace.statSync(full);
7264
+ stats = fs11__namespace.statSync(full);
7134
7265
  } catch {
7135
7266
  return true;
7136
7267
  }
@@ -7138,7 +7269,7 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7138
7269
  const { marker: marker2 } = readOwnershipMarker(full);
7139
7270
  if (marker2) return false;
7140
7271
  try {
7141
- return fs10__namespace.readdirSync(full).length > 0;
7272
+ return fs11__namespace.readdirSync(full).length > 0;
7142
7273
  } catch {
7143
7274
  return true;
7144
7275
  }
@@ -7153,13 +7284,13 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7153
7284
  function cleanStaleRunDirs(tmpDir) {
7154
7285
  let entries;
7155
7286
  try {
7156
- entries = fs10__namespace.readdirSync(tmpDir);
7287
+ entries = fs11__namespace.readdirSync(tmpDir);
7157
7288
  } catch {
7158
7289
  return;
7159
7290
  }
7160
7291
  for (const name of entries) {
7161
7292
  if (!name.startsWith(RUN_DIR_PREFIX)) continue;
7162
- const full = path11__namespace.join(tmpDir, name);
7293
+ const full = path12__namespace.join(tmpDir, name);
7163
7294
  const { marker: marker2 } = readOwnershipMarker(full);
7164
7295
  if (marker2) {
7165
7296
  const created = Date.parse(marker2.createdAt);
@@ -7167,33 +7298,33 @@ function cleanStaleRunDirs(tmpDir) {
7167
7298
  const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
7168
7299
  if (aged || abandoned) {
7169
7300
  try {
7170
- fs10__namespace.rmSync(full, { recursive: true, force: true });
7301
+ fs11__namespace.rmSync(full, { recursive: true, force: true });
7171
7302
  } catch {
7172
7303
  }
7173
7304
  }
7174
7305
  continue;
7175
7306
  }
7176
7307
  try {
7177
- if (fs10__namespace.readdirSync(full).length === 0) fs10__namespace.rmSync(full, { recursive: true, force: true });
7308
+ if (fs11__namespace.readdirSync(full).length === 0) fs11__namespace.rmSync(full, { recursive: true, force: true });
7178
7309
  } catch {
7179
7310
  }
7180
7311
  }
7181
7312
  }
7182
7313
  function removeIfEmpty(dir) {
7183
7314
  try {
7184
- if (fs10__namespace.readdirSync(dir).length === 0) fs10__namespace.rmSync(dir, { recursive: true, force: true });
7315
+ if (fs11__namespace.readdirSync(dir).length === 0) fs11__namespace.rmSync(dir, { recursive: true, force: true });
7185
7316
  } catch {
7186
7317
  }
7187
7318
  }
7188
7319
  function ensureParentDirectory(target, label) {
7189
- const parent = path11__namespace.dirname(target);
7190
- if (fs10__namespace.existsSync(parent) && !fs10__namespace.statSync(parent).isDirectory()) {
7320
+ const parent = path12__namespace.dirname(target);
7321
+ if (fs11__namespace.existsSync(parent) && !fs11__namespace.statSync(parent).isDirectory()) {
7191
7322
  throw new Error(
7192
7323
  `Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
7193
7324
  );
7194
7325
  }
7195
7326
  try {
7196
- fs10__namespace.mkdirSync(parent, { recursive: true });
7327
+ fs11__namespace.mkdirSync(parent, { recursive: true });
7197
7328
  } catch (err) {
7198
7329
  throw new Error(
7199
7330
  `Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
@@ -7202,7 +7333,7 @@ function ensureParentDirectory(target, label) {
7202
7333
  }
7203
7334
  function acquireRunLock(stagingRoot) {
7204
7335
  const key = lockKeyFor(stagingRoot);
7205
- const lockPath = path11__namespace.join(os__namespace.tmpdir(), `codebase-snapshot-${key}.lock`);
7336
+ const lockPath = path12__namespace.join(os__namespace.tmpdir(), `codebase-snapshot-${key}.lock`);
7206
7337
  const payload = `${JSON.stringify({
7207
7338
  pid: process.pid,
7208
7339
  stagingRoot,
@@ -7211,22 +7342,22 @@ function acquireRunLock(stagingRoot) {
7211
7342
  `;
7212
7343
  for (let attempt = 0; attempt < 2; attempt += 1) {
7213
7344
  try {
7214
- const handle = fs10__namespace.openSync(lockPath, "wx");
7345
+ const handle = fs11__namespace.openSync(lockPath, "wx");
7215
7346
  try {
7216
- fs10__namespace.writeSync(handle, payload);
7347
+ fs11__namespace.writeSync(handle, payload);
7217
7348
  } finally {
7218
- fs10__namespace.closeSync(handle);
7349
+ fs11__namespace.closeSync(handle);
7219
7350
  }
7220
7351
  return () => {
7221
7352
  try {
7222
- fs10__namespace.rmSync(lockPath, { force: true });
7353
+ fs11__namespace.rmSync(lockPath, { force: true });
7223
7354
  } catch {
7224
7355
  }
7225
7356
  };
7226
7357
  } catch (err) {
7227
7358
  if (err.code !== "EEXIST") throw err;
7228
7359
  if (attempt === 0 && lockOwnerIsGone(lockPath)) {
7229
- fs10__namespace.rmSync(lockPath, { force: true });
7360
+ fs11__namespace.rmSync(lockPath, { force: true });
7230
7361
  continue;
7231
7362
  }
7232
7363
  throw new Error(
@@ -7239,7 +7370,7 @@ function acquireRunLock(stagingRoot) {
7239
7370
  function lockKeyFor(stagingRoot) {
7240
7371
  let key = stagingRoot;
7241
7372
  try {
7242
- key = fs10__namespace.realpathSync(stagingRoot);
7373
+ key = fs11__namespace.realpathSync(stagingRoot);
7243
7374
  } catch {
7244
7375
  }
7245
7376
  if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
@@ -7248,7 +7379,7 @@ function lockKeyFor(stagingRoot) {
7248
7379
  function lockOwnerIsGone(lockPath) {
7249
7380
  let raw;
7250
7381
  try {
7251
- raw = fs10__namespace.readFileSync(lockPath, "utf8");
7382
+ raw = fs11__namespace.readFileSync(lockPath, "utf8");
7252
7383
  } catch {
7253
7384
  return false;
7254
7385
  }
@@ -7268,12 +7399,12 @@ function lockOwnerIsGone(lockPath) {
7268
7399
  }
7269
7400
  function writeManifestAtomically(manifestPath, manifest) {
7270
7401
  const tempPath = `${manifestPath}.${process.pid}.tmp`;
7271
- fs10__namespace.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
7402
+ fs11__namespace.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
7272
7403
  try {
7273
- fs10__namespace.renameSync(tempPath, manifestPath);
7404
+ fs11__namespace.renameSync(tempPath, manifestPath);
7274
7405
  } catch (err) {
7275
7406
  try {
7276
- fs10__namespace.rmSync(tempPath, { force: true });
7407
+ fs11__namespace.rmSync(tempPath, { force: true });
7277
7408
  } catch {
7278
7409
  }
7279
7410
  throw err;
@@ -7296,31 +7427,31 @@ function swapIntoPlace(stagingDir, outDir) {
7296
7427
  }
7297
7428
  function swapOnce(stagingDir, outDir) {
7298
7429
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
7299
- const hadPrevious = fs10__namespace.existsSync(outDir);
7300
- if (hadPrevious) fs10__namespace.renameSync(outDir, backupDir);
7430
+ const hadPrevious = fs11__namespace.existsSync(outDir);
7431
+ if (hadPrevious) fs11__namespace.renameSync(outDir, backupDir);
7301
7432
  try {
7302
7433
  try {
7303
- fs10__namespace.renameSync(stagingDir, outDir);
7434
+ fs11__namespace.renameSync(stagingDir, outDir);
7304
7435
  } catch (err) {
7305
7436
  if (err.code !== "EXDEV") throw err;
7306
7437
  const partialDir = `${outDir}.partial-${process.pid}-${Date.now()}`;
7307
7438
  try {
7308
- fs10__namespace.cpSync(stagingDir, partialDir, { recursive: true });
7309
- fs10__namespace.renameSync(partialDir, outDir);
7310
- fs10__namespace.rmSync(stagingDir, { recursive: true, force: true });
7439
+ fs11__namespace.cpSync(stagingDir, partialDir, { recursive: true });
7440
+ fs11__namespace.renameSync(partialDir, outDir);
7441
+ fs11__namespace.rmSync(stagingDir, { recursive: true, force: true });
7311
7442
  } catch (copyErr) {
7312
7443
  try {
7313
- fs10__namespace.rmSync(partialDir, { recursive: true, force: true });
7444
+ fs11__namespace.rmSync(partialDir, { recursive: true, force: true });
7314
7445
  } catch {
7315
7446
  }
7316
7447
  throw copyErr;
7317
7448
  }
7318
7449
  }
7319
- if (hadPrevious) fs10__namespace.rmSync(backupDir, { recursive: true, force: true });
7450
+ if (hadPrevious) fs11__namespace.rmSync(backupDir, { recursive: true, force: true });
7320
7451
  } catch (err) {
7321
- if (hadPrevious && fs10__namespace.existsSync(backupDir)) {
7322
- if (fs10__namespace.existsSync(outDir)) fs10__namespace.rmSync(outDir, { recursive: true, force: true });
7323
- fs10__namespace.renameSync(backupDir, outDir);
7452
+ if (hadPrevious && fs11__namespace.existsSync(backupDir)) {
7453
+ if (fs11__namespace.existsSync(outDir)) fs11__namespace.rmSync(outDir, { recursive: true, force: true });
7454
+ fs11__namespace.renameSync(backupDir, outDir);
7324
7455
  }
7325
7456
  throw err;
7326
7457
  }
@@ -7415,8 +7546,8 @@ function generateSnapshot(configInput, options) {
7415
7546
  // first run cannot have marked them yet, so the name alone keeps a run from recording its own
7416
7547
  // output — but only at these exact paths, not at every directory that shares the name.
7417
7548
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
7418
- (directory) => path11__namespace.relative(config.cwd, path11__namespace.resolve(config.cwd, directory)).replace(/\\/g, "/")
7419
- ).filter((relative8) => relative8.length > 0 && !relative8.startsWith(".."))
7549
+ (directory) => path12__namespace.relative(config.cwd, path12__namespace.resolve(config.cwd, directory)).replace(/\\/g, "/")
7550
+ ).filter((relative11) => relative11.length > 0 && !relative11.startsWith(".."))
7420
7551
  });
7421
7552
  const files = [];
7422
7553
  const monolithBodies = /* @__PURE__ */ new Map();
@@ -7436,12 +7567,12 @@ function generateSnapshot(configInput, options) {
7436
7567
  let totalSizeBytes = 0;
7437
7568
  let largeFileCount = 0;
7438
7569
  for (const relativePath of collection.files) {
7439
- const absolutePath = path11__namespace.join(config.cwd, relativePath);
7570
+ const absolutePath = path12__namespace.join(config.cwd, relativePath);
7440
7571
  let stats;
7441
7572
  try {
7442
- stats = fs10__namespace.statSync(absolutePath);
7573
+ stats = fs11__namespace.statSync(absolutePath);
7443
7574
  if (!stats.isFile()) {
7444
- const nestedRepository = stats.isDirectory() && fs10__namespace.existsSync(path11__namespace.join(absolutePath, ".git"));
7575
+ const nestedRepository = stats.isDirectory() && fs11__namespace.existsSync(path12__namespace.join(absolutePath, ".git"));
7445
7576
  skipped.push(
7446
7577
  nestedRepository ? {
7447
7578
  path: relativePath,
@@ -7461,7 +7592,7 @@ function generateSnapshot(configInput, options) {
7461
7592
  }
7462
7593
  let buffer;
7463
7594
  try {
7464
- buffer = fs10__namespace.readFileSync(absolutePath);
7595
+ buffer = fs11__namespace.readFileSync(absolutePath);
7465
7596
  } catch {
7466
7597
  skipped.push({ path: relativePath, reason: "read-error" });
7467
7598
  continue;
@@ -7791,12 +7922,12 @@ function generateSnapshot(configInput, options) {
7791
7922
  let stagingDir = null;
7792
7923
  try {
7793
7924
  ensureParentDirectory(outDir, "outDir");
7794
- fs10__namespace.mkdirSync(tmpDir, { recursive: true });
7925
+ fs11__namespace.mkdirSync(tmpDir, { recursive: true });
7795
7926
  cleanStaleRunDirs(tmpDir);
7796
7927
  const snapshotId = crypto2__namespace.randomUUID();
7797
7928
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7798
- stagingDir = path11__namespace.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
7799
- fs10__namespace.mkdirSync(stagingDir);
7929
+ stagingDir = path12__namespace.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
7930
+ fs11__namespace.mkdirSync(stagingDir);
7800
7931
  writeOwnershipMarker(stagingDir, {
7801
7932
  generator: GENERATOR_NAME,
7802
7933
  version: GENERATOR_VERSION,
@@ -7806,41 +7937,41 @@ function generateSnapshot(configInput, options) {
7806
7937
  id: snapshotId,
7807
7938
  pid: process.pid
7808
7939
  });
7809
- const partsDir = path11__namespace.join(stagingDir, "parts");
7810
- const reportsDir = path11__namespace.join(stagingDir, "reports");
7811
- if (artifacts.parts) fs10__namespace.mkdirSync(partsDir, { recursive: true });
7940
+ const partsDir = path12__namespace.join(stagingDir, "parts");
7941
+ const reportsDir = path12__namespace.join(stagingDir, "reports");
7942
+ if (artifacts.parts) fs11__namespace.mkdirSync(partsDir, { recursive: true });
7812
7943
  if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
7813
- fs10__namespace.mkdirSync(reportsDir, { recursive: true });
7944
+ fs11__namespace.mkdirSync(reportsDir, { recursive: true });
7814
7945
  }
7815
7946
  if (artifacts.parts) {
7816
7947
  for (const part of parts) {
7817
- fs10__namespace.writeFileSync(path11__namespace.join(partsDir, part.filename), part.markdown, "utf8");
7948
+ fs11__namespace.writeFileSync(path12__namespace.join(partsDir, part.filename), part.markdown, "utf8");
7818
7949
  }
7819
7950
  }
7820
7951
  if (projectTree !== null) {
7821
- fs10__namespace.writeFileSync(path11__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
7952
+ fs11__namespace.writeFileSync(path12__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
7822
7953
  }
7823
7954
  if (artifacts.navigation) {
7824
- fs10__namespace.writeFileSync(path11__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
7955
+ fs11__namespace.writeFileSync(path12__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
7825
7956
  }
7826
7957
  if (artifacts.audit) {
7827
- fs10__namespace.writeFileSync(
7828
- path11__namespace.join(reportsDir, "audit.md"),
7958
+ fs11__namespace.writeFileSync(
7959
+ path12__namespace.join(reportsDir, "audit.md"),
7829
7960
  generateAuditReport(manifest, secretFindings, config.secretRules),
7830
7961
  "utf8"
7831
7962
  );
7832
7963
  }
7833
7964
  if (artifacts.readme) {
7834
- fs10__namespace.writeFileSync(path11__namespace.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
7965
+ fs11__namespace.writeFileSync(path12__namespace.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
7835
7966
  }
7836
7967
  if (artifacts.manifest) {
7837
- fs10__namespace.writeFileSync(path11__namespace.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
7968
+ fs11__namespace.writeFileSync(path12__namespace.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
7838
7969
  }
7839
7970
  if (monolithJson !== null) {
7840
- fs10__namespace.writeFileSync(path11__namespace.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
7971
+ fs11__namespace.writeFileSync(path12__namespace.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
7841
7972
  }
7842
7973
  if (monolithMarkdown !== null) {
7843
- fs10__namespace.writeFileSync(path11__namespace.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
7974
+ fs11__namespace.writeFileSync(path12__namespace.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
7844
7975
  }
7845
7976
  writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
7846
7977
  swapIntoPlace(stagingDir, outDir);
@@ -7849,7 +7980,7 @@ function generateSnapshot(configInput, options) {
7849
7980
  const pendingHook = runAfterGenerate(config, manifest);
7850
7981
  const shipHookState = () => {
7851
7982
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
7852
- writeManifestAtomically(path11__namespace.join(outDir, "manifest.json"), manifest);
7983
+ writeManifestAtomically(path12__namespace.join(outDir, "manifest.json"), manifest);
7853
7984
  }
7854
7985
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
7855
7986
  };
@@ -7872,7 +8003,7 @@ function generateSnapshot(configInput, options) {
7872
8003
  } catch (err) {
7873
8004
  if (stagingDir !== null) {
7874
8005
  try {
7875
- fs10__namespace.rmSync(stagingDir, { recursive: true, force: true });
8006
+ fs11__namespace.rmSync(stagingDir, { recursive: true, force: true });
7876
8007
  } catch {
7877
8008
  }
7878
8009
  }
@@ -7883,6 +8014,989 @@ function generateSnapshot(configInput, options) {
7883
8014
  }
7884
8015
  }
7885
8016
 
8017
+ // src/mcp/part.ts
8018
+ var FILE_HEADING = "### FILE: ";
8019
+ var CONTENT_MARKERS = /* @__PURE__ */ new Set(["#### Content", "#### Bounded Preview"]);
8020
+ function parsePartSections(partText) {
8021
+ const lines = partText.split("\n");
8022
+ const sections = [];
8023
+ let index = 0;
8024
+ while (index < lines.length) {
8025
+ if (!lines[index].startsWith(FILE_HEADING)) {
8026
+ index += 1;
8027
+ continue;
8028
+ }
8029
+ const heading = lines[index].slice(FILE_HEADING.length);
8030
+ let cursor = index + 1;
8031
+ let inclusion = null;
8032
+ let sha256 = null;
8033
+ let includedLines = null;
8034
+ while (cursor < lines.length && !CONTENT_MARKERS.has(lines[cursor])) {
8035
+ const line = lines[cursor];
8036
+ if (line.startsWith(FILE_HEADING)) break;
8037
+ if (line.startsWith("- Inclusion: ")) inclusion = line.slice("- Inclusion: ".length).trim();
8038
+ else if (line.startsWith("- SHA-256: ")) sha256 = line.slice("- SHA-256: ".length).trim();
8039
+ else if (line.startsWith("- Included Lines: ")) {
8040
+ const value = Number(line.slice("- Included Lines: ".length).trim());
8041
+ includedLines = Number.isFinite(value) ? value : null;
8042
+ }
8043
+ cursor += 1;
8044
+ }
8045
+ if (cursor >= lines.length || !CONTENT_MARKERS.has(lines[cursor])) {
8046
+ index = cursor + 1;
8047
+ continue;
8048
+ }
8049
+ let fenceLine = cursor + 1;
8050
+ while (fenceLine < lines.length && lines[fenceLine].trim().length === 0) fenceLine += 1;
8051
+ const fenceMatch = /^(`{4,})/.exec(lines[fenceLine] ?? "");
8052
+ if (!fenceMatch) {
8053
+ index = cursor + 1;
8054
+ continue;
8055
+ }
8056
+ const fence = fenceMatch[1];
8057
+ const body = [];
8058
+ let bodyLine = fenceLine + 1;
8059
+ while (bodyLine < lines.length && lines[bodyLine] !== fence) {
8060
+ body.push(lines[bodyLine]);
8061
+ bodyLine += 1;
8062
+ }
8063
+ sections.push({ path: heading, body: body.join("\n"), inclusion, sha256, includedLines });
8064
+ index = bodyLine + 1;
8065
+ }
8066
+ return sections;
8067
+ }
8068
+ function findSection(sections, relativePath, sha256) {
8069
+ const exact = sections.find((section) => section.path === relativePath);
8070
+ if (exact) return exact;
8071
+ if (sha256) {
8072
+ const byHash = sections.filter((section) => section.sha256 === sha256);
8073
+ if (byHash.length === 1) return byHash[0];
8074
+ if (byHash.length > 1) {
8075
+ const byPath = byHash.find(
8076
+ (section) => section.path.toLowerCase() === relativePath.toLowerCase()
8077
+ );
8078
+ if (byPath) return byPath;
8079
+ }
8080
+ }
8081
+ const folded = sections.filter((section) => section.path.toLowerCase() === relativePath.toLowerCase());
8082
+ return folded.length === 1 ? folded[0] : null;
8083
+ }
8084
+ function windowText(body, fromLine, lineCount) {
8085
+ const lines = body.split("\n");
8086
+ const total = lines.length;
8087
+ const start = Math.min(Math.max(1, Math.trunc(fromLine)), Math.max(1, total));
8088
+ const end = Math.min(total, start + Math.max(1, Math.trunc(lineCount)) - 1);
8089
+ const slice = lines.slice(start - 1, end);
8090
+ return {
8091
+ text: slice.join("\n"),
8092
+ fromLine: start,
8093
+ returnedLines: slice.length,
8094
+ totalLines: total,
8095
+ truncated: end < total,
8096
+ nextFromLine: end < total ? end + 1 : null
8097
+ };
8098
+ }
8099
+ function foldForComparison(value) {
8100
+ return process.platform === "win32" || process.platform === "darwin" ? value.toLowerCase() : value;
8101
+ }
8102
+ function canonicalPath(value) {
8103
+ const absolute = path12__namespace.resolve(value);
8104
+ try {
8105
+ return fs11__namespace.realpathSync(absolute);
8106
+ } catch {
8107
+ return absolute;
8108
+ }
8109
+ }
8110
+ function isInside2(parent, child) {
8111
+ const foldedParent = foldForComparison(parent);
8112
+ const foldedChild = foldForComparison(child);
8113
+ return foldedChild === foldedParent || foldedChild.startsWith(`${foldedParent}${path12__namespace.sep}`);
8114
+ }
8115
+ function resolveRoot(requested, context) {
8116
+ if (requested !== void 0 && (typeof requested !== "string" || requested.trim().length === 0)) {
8117
+ return { ok: false, message: '"root" must be a non-empty string when provided.' };
8118
+ }
8119
+ const wanted = typeof requested === "string" ? requested.trim() : context.cwd;
8120
+ const absolute = path12__namespace.resolve(context.cwd, wanted);
8121
+ let stats;
8122
+ try {
8123
+ stats = fs11__namespace.statSync(absolute);
8124
+ } catch {
8125
+ return { ok: false, message: `"${wanted}" does not exist, so it cannot be a repository root.` };
8126
+ }
8127
+ if (!stats.isDirectory()) {
8128
+ return { ok: false, message: `"${wanted}" is not a directory.` };
8129
+ }
8130
+ const resolved = canonicalPath(absolute);
8131
+ for (const allowed of context.roots) {
8132
+ if (isInside2(canonicalPath(allowed), resolved)) return { ok: true, root: resolved };
8133
+ }
8134
+ const shown = context.roots.join(", ");
8135
+ return {
8136
+ ok: false,
8137
+ message: `"${wanted}" resolves outside the directories this server may read (${shown}). Restart it with --root <path> to add a repository.`
8138
+ };
8139
+ }
8140
+ async function openWorkspace(root, overrides = {}) {
8141
+ const loaded = await loadConfigFileDetailed(root);
8142
+ if (loaded.errors.length > 0) {
8143
+ return {
8144
+ ok: false,
8145
+ 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.`
8146
+ };
8147
+ }
8148
+ const cliOptions = { cwd: root };
8149
+ if (overrides.maxPartBytes !== void 0) {
8150
+ const parsed = parseByteSize(overrides.maxPartBytes);
8151
+ if (parsed === void 0) {
8152
+ return {
8153
+ ok: false,
8154
+ message: '"maxPartBytes" must be a byte count, for example 307200 or "300k".'
8155
+ };
8156
+ }
8157
+ cliOptions.maxPartBytes = parsed;
8158
+ }
8159
+ if (overrides.outDir !== void 0) {
8160
+ if (typeof overrides.outDir !== "string" || overrides.outDir.trim().length === 0) {
8161
+ return { ok: false, message: '"outDir" must be a non-empty string.' };
8162
+ }
8163
+ cliOptions.outDir = overrides.outDir.trim();
8164
+ }
8165
+ try {
8166
+ const config = resolveConfig(loaded.config, cliOptions, root);
8167
+ return { ok: true, workspace: { root, config, configFile: loaded.file } };
8168
+ } catch (error) {
8169
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
8170
+ }
8171
+ }
8172
+ var manifestCache = /* @__PURE__ */ new Map();
8173
+ function manifestPathFor(workspace) {
8174
+ return path12__namespace.resolve(workspace.root, workspace.config.outDir, "manifest.json");
8175
+ }
8176
+ function readManifest3(workspace) {
8177
+ const file = manifestPathFor(workspace);
8178
+ const shown = path12__namespace.relative(workspace.root, file).split(path12__namespace.sep).join("/") || file;
8179
+ let stats;
8180
+ try {
8181
+ stats = fs11__namespace.statSync(file);
8182
+ } catch {
8183
+ return {
8184
+ ok: false,
8185
+ missing: true,
8186
+ message: `There is no snapshot manifest at ${shown}. Call snapshot_plan to preview what would be included, or snapshot_generate to write it.`
8187
+ };
8188
+ }
8189
+ const cached = manifestCache.get(file);
8190
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
8191
+ return { ok: true, manifest: cached.manifest, file };
8192
+ }
8193
+ let raw;
8194
+ try {
8195
+ raw = fs11__namespace.readFileSync(file, "utf8");
8196
+ } catch (error) {
8197
+ return {
8198
+ ok: false,
8199
+ missing: false,
8200
+ message: `${shown} exists but could not be read: ${error instanceof Error ? error.message : String(error)}`
8201
+ };
8202
+ }
8203
+ let parsed;
8204
+ try {
8205
+ parsed = JSON.parse(raw);
8206
+ } catch {
8207
+ return { ok: false, missing: false, message: `${shown} is not valid JSON.` };
8208
+ }
8209
+ const problem = describeManifestShapeProblem(parsed);
8210
+ if (problem) {
8211
+ return { ok: false, missing: false, message: `${shown} is not a snapshot manifest: ${problem}` };
8212
+ }
8213
+ const manifest = parsed;
8214
+ manifestCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, manifest });
8215
+ return { ok: true, manifest, file };
8216
+ }
8217
+ function forgetManifest(file) {
8218
+ manifestCache.delete(file);
8219
+ }
8220
+
8221
+ // src/mcp/tools.ts
8222
+ var READ_ONLY = {
8223
+ readOnlyHint: true,
8224
+ destructiveHint: false,
8225
+ idempotentHint: true,
8226
+ openWorldHint: false
8227
+ };
8228
+ var WRITES = {
8229
+ readOnlyHint: false,
8230
+ // It replaces the snapshot directory but never anything it did not write, so it is additive with
8231
+ // respect to the user's files.
8232
+ destructiveHint: false,
8233
+ idempotentHint: true,
8234
+ openWorldHint: false
8235
+ };
8236
+ var MAX_RESULT_BYTES = 64 * 1024;
8237
+ var ROOT_PROPERTY = {
8238
+ type: "string",
8239
+ description: "Repository root. Defaults to the directory this server was started in."
8240
+ };
8241
+ function failure(message) {
8242
+ return { text: message, isError: true };
8243
+ }
8244
+ function plural(count, word) {
8245
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
8246
+ }
8247
+ function formatBytes(bytes) {
8248
+ if (bytes < 1024) return `${bytes} B`;
8249
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
8250
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
8251
+ }
8252
+ function relativeFor(workspace, absolute) {
8253
+ return path12__namespace.relative(workspace.root, absolute).split(path12__namespace.sep).join("/");
8254
+ }
8255
+ function capped(values, limit = 20) {
8256
+ return { count: values.length, sample: values.slice(0, limit) };
8257
+ }
8258
+ function summarizeDrift(drift) {
8259
+ return {
8260
+ missingInSnapshot: capped(drift.missingInSnapshot),
8261
+ missingOnDisk: capped(drift.missingOnDisk),
8262
+ unreadable: capped(drift.unreadable),
8263
+ hashMismatches: capped(drift.hashMismatches),
8264
+ partIssues: capped(drift.partIssues),
8265
+ totalsMismatches: capped(drift.totalsMismatches),
8266
+ configMismatches: capped(drift.configMismatches)
8267
+ };
8268
+ }
8269
+ function toRelativePath(root, value) {
8270
+ const cleaned = value.trim();
8271
+ if (cleaned.length === 0) return null;
8272
+ if (!path12__namespace.isAbsolute(cleaned)) {
8273
+ return cleaned.replace(/\\/g, "/").replace(/^\.\//, "");
8274
+ }
8275
+ const relative11 = path12__namespace.relative(root, cleaned);
8276
+ if (relative11.startsWith("..") || path12__namespace.isAbsolute(relative11)) return null;
8277
+ return relative11.split(path12__namespace.sep).join("/");
8278
+ }
8279
+ function symbolsOf(entry) {
8280
+ const summary = entry.summary;
8281
+ if (!summary || typeof summary !== "object" || !("exports" in summary)) return [];
8282
+ const source = summary;
8283
+ return [...source.exports, ...source.reactComponents, ...source.routeHandlers];
8284
+ }
8285
+ async function prepare(args, context, overrides = {}) {
8286
+ const rootOutcome = resolveRoot(args.root, context);
8287
+ if (!rootOutcome.ok) return { ok: false, outcome: failure(rootOutcome.message) };
8288
+ const opened = await openWorkspace(rootOutcome.root, overrides);
8289
+ if (!opened.ok) return { ok: false, outcome: failure(opened.message) };
8290
+ return { ok: true, workspace: opened.workspace };
8291
+ }
8292
+ var statusTool = {
8293
+ name: "snapshot_status",
8294
+ title: "Snapshot status",
8295
+ 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.",
8296
+ inputSchema: {
8297
+ type: "object",
8298
+ properties: { root: ROOT_PROPERTY },
8299
+ additionalProperties: false
8300
+ },
8301
+ outputSchema: {
8302
+ type: "object",
8303
+ properties: {
8304
+ present: { type: "boolean" },
8305
+ root: { type: "string" },
8306
+ outDir: { type: "string" },
8307
+ fingerprint: { type: "string" },
8308
+ generatedAt: { type: "string" },
8309
+ generatorVersion: { type: "string" },
8310
+ generatorVersionMatches: { type: "boolean" },
8311
+ includedFiles: { type: "integer" },
8312
+ skippedFiles: { type: "integer" },
8313
+ partCount: { type: "integer" },
8314
+ warningCount: { type: "integer" }
8315
+ }
8316
+ },
8317
+ annotations: READ_ONLY,
8318
+ async handler(args, context) {
8319
+ const prepared = await prepare(args, context);
8320
+ if (!prepared.ok) return prepared.outcome;
8321
+ const workspace = prepared.workspace;
8322
+ const read = readManifest3(workspace);
8323
+ if (!read.ok) {
8324
+ return {
8325
+ text: read.message,
8326
+ structured: {
8327
+ present: false,
8328
+ root: workspace.root,
8329
+ outDir: workspace.config.outDir,
8330
+ message: read.message
8331
+ }
8332
+ };
8333
+ }
8334
+ const manifest = read.manifest;
8335
+ const versionMatches = manifest.generator.version === GENERATOR_VERSION;
8336
+ const lines = [
8337
+ `Snapshot: ${workspace.config.outDir}/`,
8338
+ `Generated: ${manifest.generatedAt} by ${manifest.generator.name} ${manifest.generator.version}`,
8339
+ `Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
8340
+ `Content: ${formatBytes(manifest.counts.totalSizeBytes)} of source in ${formatBytes(manifest.totals.totalContentBytes)} emitted`,
8341
+ `Fingerprint: ${manifest.fingerprint}`,
8342
+ `Git: ${manifest.git.branch} @ ${manifest.git.commit}${manifest.git.isDirty ? " (dirty)" : " (clean)"}`
8343
+ ];
8344
+ if (manifest.totals.warningCount > 0) lines.push(`Warnings: ${manifest.totals.warningCount}`);
8345
+ if (!versionMatches) {
8346
+ lines.push(
8347
+ `Note: written by ${manifest.generator.version}, this server is ${GENERATOR_VERSION}. Re-run snapshot_generate if the difference matters.`
8348
+ );
8349
+ }
8350
+ return {
8351
+ text: lines.join("\n"),
8352
+ structured: {
8353
+ present: true,
8354
+ root: workspace.root,
8355
+ outDir: workspace.config.outDir,
8356
+ fingerprint: manifest.fingerprint,
8357
+ generatedAt: manifest.generatedAt,
8358
+ generatorVersion: manifest.generator.version,
8359
+ generatorVersionMatches: versionMatches,
8360
+ includedFiles: manifest.counts.totalFiles,
8361
+ skippedFiles: manifest.counts.skippedCount,
8362
+ partCount: manifest.counts.partsCount,
8363
+ warningCount: manifest.totals.warningCount,
8364
+ preset: manifest.config.preset,
8365
+ maxPartBytes: manifest.budgets.maxPartBytes
8366
+ }
8367
+ };
8368
+ }
8369
+ };
8370
+ var planTool = {
8371
+ name: "snapshot_plan",
8372
+ title: "Preview a snapshot",
8373
+ 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.",
8374
+ inputSchema: {
8375
+ type: "object",
8376
+ properties: {
8377
+ root: ROOT_PROPERTY,
8378
+ maxPartBytes: {
8379
+ type: "string",
8380
+ description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
8381
+ }
8382
+ },
8383
+ additionalProperties: false
8384
+ },
8385
+ outputSchema: {
8386
+ type: "object",
8387
+ properties: {
8388
+ files: { type: "integer" },
8389
+ skipped: { type: "integer" },
8390
+ parts: { type: "integer" },
8391
+ sourceBytes: { type: "integer" },
8392
+ emittedBytes: { type: "integer" },
8393
+ preset: { type: "string" }
8394
+ }
8395
+ },
8396
+ annotations: READ_ONLY,
8397
+ async handler(args, context) {
8398
+ const prepared = await prepare(args, context, { maxPartBytes: args.maxPartBytes });
8399
+ if (!prepared.ok) return prepared.outcome;
8400
+ const workspace = prepared.workspace;
8401
+ let manifest;
8402
+ try {
8403
+ manifest = generateSnapshot(workspace.config, { dryRun: true });
8404
+ } catch (error) {
8405
+ return failure(error instanceof Error ? error.message : String(error));
8406
+ }
8407
+ const reasons = /* @__PURE__ */ new Map();
8408
+ for (const record of manifest.skipped) {
8409
+ reasons.set(record.reason, (reasons.get(record.reason) ?? 0) + 1);
8410
+ }
8411
+ 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 }));
8412
+ const lines = [
8413
+ "Dry run \u2014 nothing was written.",
8414
+ `Preset: ${manifest.config.preset}`,
8415
+ `Would include ${plural(manifest.counts.totalFiles, "file")} in ${plural(manifest.counts.partsCount, "part")} (${formatBytes(manifest.counts.totalSizeBytes)} of source, ${formatBytes(manifest.totals.totalContentBytes)} emitted).`,
8416
+ `Would skip ${plural(manifest.counts.skippedCount, "file")}.`
8417
+ ];
8418
+ if (topReasons.length > 0) {
8419
+ lines.push("Top skip reasons:");
8420
+ for (const entry of topReasons) lines.push(` - ${entry.reason} (${entry.count})`);
8421
+ }
8422
+ if (manifest.warnings.length > 0) {
8423
+ lines.push(`Warnings (${manifest.warnings.length}):`);
8424
+ for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
8425
+ }
8426
+ return {
8427
+ text: lines.join("\n"),
8428
+ structured: {
8429
+ files: manifest.counts.totalFiles,
8430
+ skipped: manifest.counts.skippedCount,
8431
+ parts: manifest.counts.partsCount,
8432
+ sourceBytes: manifest.counts.totalSizeBytes,
8433
+ emittedBytes: manifest.totals.totalContentBytes,
8434
+ preset: manifest.config.preset,
8435
+ maxPartBytes: manifest.budgets.maxPartBytes,
8436
+ layers: manifest.layers,
8437
+ skipReasons: topReasons,
8438
+ warnings: manifest.warnings.slice(0, 5),
8439
+ warningCount: manifest.warnings.length
8440
+ }
8441
+ };
8442
+ }
8443
+ };
8444
+ var generateTool = {
8445
+ name: "snapshot_generate",
8446
+ title: "Generate the snapshot",
8447
+ 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.",
8448
+ inputSchema: {
8449
+ type: "object",
8450
+ properties: {
8451
+ root: ROOT_PROPERTY,
8452
+ maxPartBytes: {
8453
+ type: "string",
8454
+ description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
8455
+ },
8456
+ outDir: {
8457
+ type: "string",
8458
+ description: "Output directory relative to the root. Defaults to the project configuration."
8459
+ }
8460
+ },
8461
+ additionalProperties: false
8462
+ },
8463
+ outputSchema: {
8464
+ type: "object",
8465
+ properties: {
8466
+ fingerprint: { type: "string" },
8467
+ files: { type: "integer" },
8468
+ skipped: { type: "integer" },
8469
+ parts: { type: "integer" },
8470
+ emittedBytes: { type: "integer" }
8471
+ }
8472
+ },
8473
+ annotations: WRITES,
8474
+ async handler(args, context) {
8475
+ const prepared = await prepare(args, context, {
8476
+ maxPartBytes: args.maxPartBytes,
8477
+ outDir: args.outDir
8478
+ });
8479
+ if (!prepared.ok) return prepared.outcome;
8480
+ const workspace = prepared.workspace;
8481
+ let manifest;
8482
+ try {
8483
+ manifest = generateSnapshot(workspace.config);
8484
+ } catch (error) {
8485
+ return failure(error instanceof Error ? error.message : String(error));
8486
+ }
8487
+ forgetManifest(manifestPathFor(workspace));
8488
+ const written = relativeFor(workspace, path12__namespace.resolve(workspace.root, workspace.config.outDir));
8489
+ const lines = [
8490
+ `Wrote ${written}/`,
8491
+ `Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
8492
+ `Parts: ${manifest.parts.map((part) => `${part.filename} (${formatBytes(part.byteSize)})`).join(", ")}`,
8493
+ `Fingerprint: ${manifest.fingerprint}`
8494
+ ];
8495
+ if (manifest.warnings.length > 0) {
8496
+ lines.push(`Warnings (${manifest.warnings.length}):`);
8497
+ for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
8498
+ }
8499
+ return {
8500
+ text: lines.join("\n"),
8501
+ structured: {
8502
+ fingerprint: manifest.fingerprint,
8503
+ files: manifest.counts.totalFiles,
8504
+ skipped: manifest.counts.skippedCount,
8505
+ parts: manifest.counts.partsCount,
8506
+ emittedBytes: manifest.totals.totalContentBytes,
8507
+ outDir: written,
8508
+ partFiles: manifest.parts.map((part) => part.filename),
8509
+ warningCount: manifest.warnings.length
8510
+ }
8511
+ };
8512
+ }
8513
+ };
8514
+ var validateTool = {
8515
+ name: "snapshot_validate",
8516
+ title: "Validate the snapshot",
8517
+ 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.",
8518
+ inputSchema: {
8519
+ type: "object",
8520
+ properties: { root: ROOT_PROPERTY },
8521
+ additionalProperties: false
8522
+ },
8523
+ outputSchema: {
8524
+ type: "object",
8525
+ properties: {
8526
+ current: { type: "boolean" },
8527
+ manifestPresent: { type: "boolean" }
8528
+ }
8529
+ },
8530
+ annotations: READ_ONLY,
8531
+ async handler(args, context) {
8532
+ const prepared = await prepare(args, context);
8533
+ if (!prepared.ok) return prepared.outcome;
8534
+ const workspace = prepared.workspace;
8535
+ let result;
8536
+ try {
8537
+ result = validateSnapshot(workspace.config);
8538
+ } catch (error) {
8539
+ return failure(error instanceof Error ? error.message : String(error));
8540
+ }
8541
+ const lines = [];
8542
+ if (result.valid) {
8543
+ lines.push("The snapshot matches the repository.");
8544
+ } else {
8545
+ lines.push("The snapshot is out of date.");
8546
+ const drift = result.drift;
8547
+ const observations = [
8548
+ ["file(s) changed on disk", drift.hashMismatches.length],
8549
+ ["file(s) added since the snapshot", drift.missingInSnapshot.length],
8550
+ ["file(s) gone since the snapshot", drift.missingOnDisk.length],
8551
+ ["part file(s) damaged or missing", drift.partIssues.length],
8552
+ ["setting(s) changed since the snapshot", drift.configMismatches.length],
8553
+ ["internal count(s) disagreeing", drift.totalsMismatches.length]
8554
+ ];
8555
+ for (const [label, count] of observations) {
8556
+ if (count > 0) lines.push(` - ${count} ${label}`);
8557
+ }
8558
+ for (const error of result.errors.slice(0, 5)) lines.push(` - ${error}`);
8559
+ lines.push("Run snapshot_generate to refresh it.");
8560
+ }
8561
+ for (const warning of result.warnings.slice(0, 5)) lines.push(`Note: ${warning}`);
8562
+ return {
8563
+ text: lines.join("\n"),
8564
+ structured: {
8565
+ current: result.valid,
8566
+ manifestPresent: result.manifestPresent,
8567
+ drift: summarizeDrift(result.drift),
8568
+ errors: result.errors.slice(0, 10),
8569
+ warnings: result.warnings.slice(0, 10)
8570
+ }
8571
+ // A stale snapshot is an answer, not a tool failure: the caller asked whether it is current.
8572
+ };
8573
+ }
8574
+ };
8575
+ var locateTool = {
8576
+ name: "snapshot_locate",
8577
+ title: "Locate code in the snapshot",
8578
+ 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.",
8579
+ inputSchema: {
8580
+ type: "object",
8581
+ properties: {
8582
+ query: {
8583
+ type: "string",
8584
+ description: "Text to match, case-insensitively, against paths and exported symbols."
8585
+ },
8586
+ by: {
8587
+ type: "string",
8588
+ enum: ["auto", "path", "symbol", "route", "layer", "kind"],
8589
+ default: "auto",
8590
+ description: "Restrict what is matched. Defaults to every category."
8591
+ },
8592
+ limit: {
8593
+ type: "integer",
8594
+ minimum: 1,
8595
+ maximum: 100,
8596
+ default: 20,
8597
+ description: "Maximum matches to return."
8598
+ },
8599
+ root: ROOT_PROPERTY
8600
+ },
8601
+ required: ["query"],
8602
+ additionalProperties: false
8603
+ },
8604
+ outputSchema: {
8605
+ type: "object",
8606
+ properties: { total: { type: "integer" } }
8607
+ },
8608
+ annotations: READ_ONLY,
8609
+ async handler(args, context) {
8610
+ const prepared = await prepare(args, context);
8611
+ if (!prepared.ok) return prepared.outcome;
8612
+ const workspace = prepared.workspace;
8613
+ const read = readManifest3(workspace);
8614
+ if (!read.ok) return failure(read.message);
8615
+ const query = String(args.query ?? "").trim();
8616
+ if (query.length === 0) return failure('"query" must not be empty.');
8617
+ const needle = query.toLowerCase();
8618
+ const by = typeof args.by === "string" ? args.by : "auto";
8619
+ const limit = typeof args.limit === "number" ? args.limit : 20;
8620
+ const manifest = read.manifest;
8621
+ const matches = [];
8622
+ const add = (entry, matchedBy, detail) => {
8623
+ matches.push({
8624
+ path: entry.relativePath,
8625
+ part: entry.part,
8626
+ layer: entry.layer,
8627
+ kind: entry.kind,
8628
+ matchedBy,
8629
+ ...detail ? { detail } : {}
8630
+ });
8631
+ };
8632
+ const wants = (category) => by === "auto" || by === category;
8633
+ for (const entry of manifest.files) {
8634
+ if (wants("path") && entry.relativePath.toLowerCase().includes(needle)) {
8635
+ add(entry, "path");
8636
+ continue;
8637
+ }
8638
+ if (wants("symbol")) {
8639
+ const symbol = symbolsOf(entry).find((name) => name.toLowerCase().includes(needle));
8640
+ if (symbol) {
8641
+ add(entry, "symbol", symbol);
8642
+ continue;
8643
+ }
8644
+ }
8645
+ if (wants("layer") && entry.layer.toLowerCase() === needle) add(entry, "layer");
8646
+ else if (wants("kind") && entry.kind.toLowerCase() === needle) add(entry, "kind");
8647
+ }
8648
+ if (wants("route")) {
8649
+ for (const route of manifest.indexes.routes) {
8650
+ if (!route.path.toLowerCase().includes(needle)) continue;
8651
+ if (matches.some((match) => match.path === route.path)) continue;
8652
+ matches.push({
8653
+ path: route.path,
8654
+ part: route.part,
8655
+ layer: "route",
8656
+ kind: "route",
8657
+ matchedBy: "route",
8658
+ detail: route.methods.join(", ")
8659
+ });
8660
+ }
8661
+ }
8662
+ if (matches.length === 0) {
8663
+ return {
8664
+ text: `Nothing in the snapshot matches "${query}". Try a shorter fragment, or snapshot_explain a path to see whether it is included at all.`,
8665
+ structured: { total: 0, matches: [] }
8666
+ };
8667
+ }
8668
+ const shown = matches.slice(0, limit);
8669
+ const lines = [
8670
+ `${matches.length} match${matches.length === 1 ? "" : "es"} for "${query}"` + (matches.length > shown.length ? ` (showing ${shown.length})` : "") + ":"
8671
+ ];
8672
+ for (const match of shown) {
8673
+ const detail = match.detail ? ` \u2014 ${match.detail}` : "";
8674
+ lines.push(` ${match.path}${detail} [${match.part}, ${match.layer}/${match.kind}]`);
8675
+ }
8676
+ if (matches.length > shown.length) {
8677
+ lines.push("Raise `limit` to see more.");
8678
+ }
8679
+ return {
8680
+ text: lines.join("\n"),
8681
+ structured: { total: matches.length, returned: shown.length, matches: shown }
8682
+ };
8683
+ }
8684
+ };
8685
+ var readTool = {
8686
+ name: "snapshot_read",
8687
+ title: "Read a file from the snapshot",
8688
+ 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.",
8689
+ inputSchema: {
8690
+ type: "object",
8691
+ properties: {
8692
+ path: {
8693
+ type: "string",
8694
+ description: "File path relative to the repository root, as snapshot_locate reports it."
8695
+ },
8696
+ fromLine: {
8697
+ type: "integer",
8698
+ minimum: 1,
8699
+ default: 1,
8700
+ description: "First line to return, 1-based."
8701
+ },
8702
+ lines: {
8703
+ type: "integer",
8704
+ minimum: 1,
8705
+ maximum: 2e3,
8706
+ default: 200,
8707
+ description: "How many lines to return."
8708
+ },
8709
+ root: ROOT_PROPERTY
8710
+ },
8711
+ required: ["path"],
8712
+ additionalProperties: false
8713
+ },
8714
+ outputSchema: {
8715
+ type: "object",
8716
+ properties: {
8717
+ path: { type: "string" },
8718
+ part: { type: "string" },
8719
+ fromLine: { type: "integer" },
8720
+ returnedLines: { type: "integer" },
8721
+ totalLines: { type: "integer" },
8722
+ nextFromLine: { type: "integer" }
8723
+ }
8724
+ },
8725
+ annotations: READ_ONLY,
8726
+ async handler(args, context) {
8727
+ const prepared = await prepare(args, context);
8728
+ if (!prepared.ok) return prepared.outcome;
8729
+ const workspace = prepared.workspace;
8730
+ if (typeof args.path !== "string") return failure('"path" must be a string.');
8731
+ const relativePath = toRelativePath(workspace.root, args.path);
8732
+ if (!relativePath) {
8733
+ return failure(
8734
+ `"${args.path}" is outside ${workspace.root}. Paths are read relative to the repository root.`
8735
+ );
8736
+ }
8737
+ const read = readManifest3(workspace);
8738
+ if (!read.ok) return failure(read.message);
8739
+ const manifest = read.manifest;
8740
+ const entry = manifest.files.find((file) => file.relativePath === relativePath) ?? manifest.files.find((file) => file.relativePath.toLowerCase() === relativePath.toLowerCase());
8741
+ if (!entry) {
8742
+ const skipped = manifest.skipped.find((record) => record.path === relativePath);
8743
+ if (skipped) {
8744
+ const detail = skipped.detail ? ` (${skipped.detail})` : "";
8745
+ return failure(
8746
+ `${relativePath} is not in the snapshot: ${skipped.reason}${detail}. Sensitive and ignored files stay out unless they are included explicitly.`
8747
+ );
8748
+ }
8749
+ return failure(
8750
+ `${relativePath} is not in the snapshot. Call snapshot_locate to find the path, or snapshot_explain to learn why it was excluded.`
8751
+ );
8752
+ }
8753
+ const partPath = path12__namespace.resolve(workspace.root, workspace.config.outDir, "parts", entry.part);
8754
+ let partText;
8755
+ try {
8756
+ partText = fs11__namespace.readFileSync(partPath, "utf8");
8757
+ } catch {
8758
+ return failure(
8759
+ `${entry.part} could not be read, so the snapshot is damaged. Run snapshot_generate to rebuild it.`
8760
+ );
8761
+ }
8762
+ const section = findSection(parsePartSections(partText), entry.relativePath, entry.hashSha256);
8763
+ if (!section) {
8764
+ return failure(
8765
+ `${relativePath} is recorded in ${entry.part} but its section could not be located there. Run snapshot_generate to rebuild the snapshot.`
8766
+ );
8767
+ }
8768
+ const fromLine = typeof args.fromLine === "number" ? args.fromLine : 1;
8769
+ const lineCount = typeof args.lines === "number" ? args.lines : 200;
8770
+ const window = windowText(section.body, fromLine, lineCount);
8771
+ let body = window.text;
8772
+ let truncatedByBytes = false;
8773
+ if (Buffer.byteLength(body, "utf8") > MAX_RESULT_BYTES) {
8774
+ body = body.slice(0, MAX_RESULT_BYTES);
8775
+ const lastBreak = body.lastIndexOf("\n");
8776
+ body = lastBreak > 0 ? body.slice(0, lastBreak) : body;
8777
+ truncatedByBytes = true;
8778
+ }
8779
+ const returnedLines = body.length === 0 ? 0 : body.split("\n").length;
8780
+ const moreRemain = window.truncated || truncatedByBytes;
8781
+ const header = [
8782
+ `${entry.relativePath} \u2014 ${entry.part}, ${entry.layer}/${entry.kind}, ${entry.language}`,
8783
+ 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}.`
8784
+ ];
8785
+ const lines = [...header, "", body];
8786
+ if (moreRemain) {
8787
+ const next = window.fromLine + returnedLines;
8788
+ lines.push("", `Truncated. Call snapshot_read with fromLine=${next} for the rest.`);
8789
+ }
8790
+ return {
8791
+ text: lines.join("\n"),
8792
+ structured: {
8793
+ path: entry.relativePath,
8794
+ part: entry.part,
8795
+ layer: entry.layer,
8796
+ kind: entry.kind,
8797
+ language: entry.language,
8798
+ inclusion: section.inclusion ?? entry.inclusionMode,
8799
+ fromLine: window.fromLine,
8800
+ returnedLines,
8801
+ totalLines: window.totalLines,
8802
+ truncated: moreRemain,
8803
+ nextFromLine: moreRemain ? window.fromLine + returnedLines : null,
8804
+ sha256: entry.hashSha256
8805
+ }
8806
+ };
8807
+ }
8808
+ };
8809
+ var explainTool = {
8810
+ name: "snapshot_explain",
8811
+ title: "Explain one path",
8812
+ 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".',
8813
+ inputSchema: {
8814
+ type: "object",
8815
+ properties: {
8816
+ path: {
8817
+ type: "string",
8818
+ description: "Path relative to the repository root. Directories are explained as a whole."
8819
+ },
8820
+ root: ROOT_PROPERTY
8821
+ },
8822
+ required: ["path"],
8823
+ additionalProperties: false
8824
+ },
8825
+ outputSchema: {
8826
+ type: "object",
8827
+ properties: {
8828
+ path: { type: "string" },
8829
+ included: { type: "boolean" },
8830
+ reason: { type: "string" }
8831
+ }
8832
+ },
8833
+ annotations: READ_ONLY,
8834
+ async handler(args, context) {
8835
+ const prepared = await prepare(args, context);
8836
+ if (!prepared.ok) return prepared.outcome;
8837
+ const workspace = prepared.workspace;
8838
+ if (typeof args.path !== "string") return failure('"path" must be a string.');
8839
+ const relativePath = toRelativePath(workspace.root, args.path);
8840
+ if (!relativePath) {
8841
+ return failure(`"${args.path}" is outside ${workspace.root}.`);
8842
+ }
8843
+ let result;
8844
+ try {
8845
+ result = explainPath(workspace.config, relativePath);
8846
+ } catch (error) {
8847
+ return failure(error instanceof Error ? error.message : String(error));
8848
+ }
8849
+ return {
8850
+ text: renderExplainText(result),
8851
+ structured: {
8852
+ path: result.path,
8853
+ included: result.included,
8854
+ ...result.reason ? { reason: result.reason } : {},
8855
+ ...result.detail ? { detail: result.detail } : {},
8856
+ ...result.layer ? { layer: result.layer } : {},
8857
+ ...result.kind ? { kind: result.kind } : {},
8858
+ ...result.language ? { language: result.language } : {},
8859
+ ...result.part ? { part: result.part } : {},
8860
+ ...result.inclusionMode ? { inclusion: result.inclusionMode } : {},
8861
+ ...typeof result.lines === "number" ? { lines: result.lines } : {},
8862
+ ...typeof result.bytes === "number" ? { bytes: result.bytes } : {},
8863
+ ...typeof result.secretCount === "number" ? { secretCount: result.secretCount } : {}
8864
+ }
8865
+ };
8866
+ }
8867
+ };
8868
+ function buildTools() {
8869
+ return [
8870
+ statusTool,
8871
+ planTool,
8872
+ generateTool,
8873
+ validateTool,
8874
+ locateTool,
8875
+ readTool,
8876
+ explainTool
8877
+ ];
8878
+ }
8879
+
8880
+ // src/mcp/prompts.ts
8881
+ var ORIENT = `You have a codebase-snapshot server for this repository. Build a map of it before reading any code.
8882
+
8883
+ 1. Call snapshot_status. If no snapshot exists, call snapshot_plan to preview it, then snapshot_generate.
8884
+ 2. Call snapshot_locate for the areas that matter. Search by exported symbol, route or path fragment rather than guessing filenames.
8885
+ 3. Report back: the layers present, the entry points, and the files that matter here \u2014 cited as paths with their part.
8886
+ 4. Read one file at a time with snapshot_read, one window at a time. Do not read a part end to end.`;
8887
+ var KEEP_CURRENT = `Keep the context you were given provably current while you work.
8888
+
8889
+ 1. Before relying on a snapshot from an earlier session, call snapshot_validate.
8890
+ 2. If it reports drift, call snapshot_generate once, then snapshot_validate again.
8891
+ 3. After changes you make, call snapshot_validate and report what it said.
8892
+ 4. Never call a snapshot current unless snapshot_validate said so in this session.`;
8893
+ function withFocus(base, focus) {
8894
+ return typeof focus === "string" && focus.trim().length > 0 ? `${base}
8895
+
8896
+ Focus for this pass: ${focus.trim()}` : base;
8897
+ }
8898
+ function buildPrompts() {
8899
+ return [
8900
+ {
8901
+ name: "orient",
8902
+ title: "Orient in this repository",
8903
+ description: "Map the repository from the snapshot before reading any of it.",
8904
+ arguments: [
8905
+ {
8906
+ name: "focus",
8907
+ description: 'Optional area to concentrate on, for example "authentication".',
8908
+ required: false
8909
+ }
8910
+ ],
8911
+ build(args) {
8912
+ return {
8913
+ description: "Map the repository before reading it.",
8914
+ messages: [{ role: "user", content: { type: "text", text: withFocus(ORIENT, args.focus) } }]
8915
+ };
8916
+ }
8917
+ },
8918
+ {
8919
+ name: "keep-current",
8920
+ title: "Keep the snapshot current",
8921
+ description: "Validate before trusting earlier context, and again after making changes.",
8922
+ arguments: [],
8923
+ build() {
8924
+ return {
8925
+ description: "Keep the context current.",
8926
+ messages: [{ role: "user", content: { type: "text", text: KEEP_CURRENT } }]
8927
+ };
8928
+ }
8929
+ }
8930
+ ];
8931
+ }
8932
+
8933
+ // src/mcp/protocol.ts
8934
+ var LEGACY_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18"];
8935
+ var MODERN_PROTOCOL_VERSIONS = ["2026-07-28"];
8936
+ var SUPPORTED_PROTOCOL_VERSIONS = [
8937
+ ...MODERN_PROTOCOL_VERSIONS,
8938
+ ...LEGACY_PROTOCOL_VERSIONS
8939
+ ];
8940
+ var DEFAULT_LEGACY_PROTOCOL_VERSION = LEGACY_PROTOCOL_VERSIONS[0];
8941
+ var ERROR_CODES = {
8942
+ parseError: -32700,
8943
+ invalidRequest: -32600,
8944
+ methodNotFound: -32601,
8945
+ invalidParams: -32602,
8946
+ internalError: -32603,
8947
+ unsupportedProtocolVersion: -32022
8948
+ };
8949
+ var METHOD3 = {
8950
+ initialize: "initialize",
8951
+ initialized: "notifications/initialized",
8952
+ discover: "server/discover",
8953
+ ping: "ping",
8954
+ toolsList: "tools/list",
8955
+ toolsCall: "tools/call",
8956
+ resourcesList: "resources/list",
8957
+ resourcesRead: "resources/read",
8958
+ resourceTemplatesList: "resources/templates/list",
8959
+ promptsList: "prompts/list",
8960
+ promptsGet: "prompts/get"};
8961
+ var META_KEYS = {
8962
+ protocolVersion: "io.modelcontextprotocol/protocolVersion",
8963
+ clientCapabilities: "io.modelcontextprotocol/clientCapabilities",
8964
+ clientInfo: "io.modelcontextprotocol/clientInfo",
8965
+ serverInfo: "io.modelcontextprotocol/serverInfo"};
8966
+ function isJsonObject(value) {
8967
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8968
+ }
8969
+ function serverInfoMeta(info2) {
8970
+ const value = { name: info2.name, version: info2.version };
8971
+ if (info2.title) value.title = info2.title;
8972
+ return { [META_KEYS.serverInfo]: value };
8973
+ }
8974
+ function successResponse(id, result, modern, info2) {
8975
+ if (!modern) return { jsonrpc: "2.0", id, result };
8976
+ const withMeta = {
8977
+ ...result,
8978
+ resultType: "complete",
8979
+ _meta: { ...isJsonObject(result._meta) ? result._meta : {}, ...serverInfoMeta(info2) }
8980
+ };
8981
+ return { jsonrpc: "2.0", id, result: withMeta };
8982
+ }
8983
+ function errorResponse(id, code, message, data) {
8984
+ const error = { code, message };
8985
+ if (data !== void 0) error.data = data;
8986
+ return { jsonrpc: "2.0", id, error };
8987
+ }
8988
+
8989
+ // src/mcp/uris.ts
8990
+ var STATIC_RESOURCE_URIS = [
8991
+ "snapshot://manifest",
8992
+ "snapshot://contract",
8993
+ "snapshot://skill",
8994
+ "snapshot://reports/audit",
8995
+ "snapshot://reports/tree",
8996
+ "snapshot://reports/navigation"
8997
+ ];
8998
+ var RESOURCE_TEMPLATE_URIS = ["snapshot://parts/{index}"];
8999
+
7886
9000
  // src/agent/option-scope.ts
7887
9001
  var VALIDATE_OPTIONS = [
7888
9002
  "cwd",
@@ -7918,6 +9032,8 @@ var COMMAND_OPTION_SCOPE = {
7918
9032
  "mode",
7919
9033
  "dry-run",
7920
9034
  "watch",
9035
+ "autopilot",
9036
+ "no-install",
7921
9037
  "help",
7922
9038
  "version"
7923
9039
  ],
@@ -7976,6 +9092,9 @@ var COMMAND_OPTION_SCOPE = {
7976
9092
  "version"
7977
9093
  ],
7978
9094
  skill: ["cwd", "format", "mode", "install", "target", "help", "version"],
9095
+ // `mcp` reads the repository, so it takes --cwd. It has no --format: stdout carries JSON-RPC
9096
+ // messages, and there is no second shape to choose, so offering the flag would be a lie.
9097
+ mcp: ["cwd", "mode", "help", "version"],
7979
9098
  setup: [
7980
9099
  "cwd",
7981
9100
  "out-dir",
@@ -8005,7 +9124,8 @@ var COMMAND_ORDER = [
8005
9124
  "setup",
8006
9125
  "diff",
8007
9126
  "describe",
8008
- "skill"
9127
+ "skill",
9128
+ "mcp"
8009
9129
  ];
8010
9130
  function commandsForOption(option) {
8011
9131
  return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
@@ -8079,6 +9199,19 @@ function renderContractText(contract) {
8079
9199
  lines.push(` ${"".padEnd(24)} ${option.summary}`);
8080
9200
  }
8081
9201
  lines.push("");
9202
+ lines.push("MCP:");
9203
+ lines.push(` command ${contract.mcp.command}`);
9204
+ lines.push(` transport ${contract.mcp.transport}`);
9205
+ lines.push(
9206
+ ` versions modern ${contract.mcp.protocolVersions.modern.join(", ")} / legacy ${contract.mcp.protocolVersions.legacy.join(", ")}`
9207
+ );
9208
+ lines.push(
9209
+ ` tools ${contract.mcp.tools.map((tool) => tool.readOnly ? tool.name : `${tool.name} (writes)`).join(", ")}`
9210
+ );
9211
+ lines.push(` prompts ${contract.mcp.prompts.map((prompt) => prompt.name).join(", ")}`);
9212
+ lines.push(` resources ${contract.mcp.resources.join(", ")}`);
9213
+ lines.push(` templates ${contract.mcp.resourceTemplates.join(", ")}`);
9214
+ lines.push("");
8082
9215
  lines.push("Safety:");
8083
9216
  for (const note of contract.safety) lines.push(` - ${note}`);
8084
9217
  lines.push("");
@@ -8104,11 +9237,13 @@ function buildAgentContract() {
8104
9237
  invocation: {
8105
9238
  oneShot: `npx ${PACKAGE_NAME}@latest`,
8106
9239
  installed: "npx codebase-snapshot",
8107
- bins: ["codebase-snapshot", "snap"],
9240
+ bins: ["codebase-snapshot", "snapshot"],
8108
9241
  notes: [
8109
9242
  `The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
8110
- "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.",
9243
+ "`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.",
9244
+ `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.`,
8111
9245
  "Both bin names point at the same file. npm resolves a package by its bin entries, so they must stay aliases of one executable.",
9246
+ "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`.",
8112
9247
  "Human progress goes to stderr. With --format json the payload on stdout is pure JSON.",
8113
9248
  "A failing run also writes pure JSON to stdout: { error: { message, exitCode } }. The exit code and the stderr line are unchanged."
8114
9249
  ]
@@ -8119,7 +9254,7 @@ function buildAgentContract() {
8119
9254
  id: "verify",
8120
9255
  run: `npx ${PACKAGE_NAME}@latest --version`,
8121
9256
  // The requirement is the major version; the exact build is context. A
8122
- // floating tag may print 4.0.7 today and 4.0.9 next week, and an agent
9257
+ // floating tag may print a different patch today than next week, and an agent
8123
9258
  // must not conclude that a working tool is broken because of a patch.
8124
9259
  expect: `prints v${MAJOR_VERSION} or newer on stdout and exits 0 (this build is v${GENERATOR_VERSION}: compare the major version, not the patch)`,
8125
9260
  notes: `If it prints an older major than this contract's, the @latest tag is behind the release: pin the exact version with \`npx ${PACKAGE_NAME}@${GENERATOR_VERSION} --version\` instead of retrying @latest. If it prints nothing, clear the npm cache and retry, or use \`npx -p <pkg> codebase-snapshot\`.`
@@ -8163,9 +9298,9 @@ function buildAgentContract() {
8163
9298
  {
8164
9299
  name: "generate",
8165
9300
  aliases: ["default"],
8166
- summary: "Write the snapshot. This is the command you normally want.",
9301
+ 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`.",
8167
9302
  positionals: [],
8168
- exits: [0, 1]
9303
+ exits: [0, 1, 2]
8169
9304
  },
8170
9305
  {
8171
9306
  name: "validate",
@@ -8222,6 +9357,13 @@ function buildAgentContract() {
8222
9357
  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.",
8223
9358
  positionals: [],
8224
9359
  exits: [0, 1, 2]
9360
+ },
9361
+ {
9362
+ name: "mcp",
9363
+ aliases: [],
9364
+ 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.",
9365
+ positionals: [],
9366
+ exits: [0, 2]
8225
9367
  }
8226
9368
  ],
8227
9369
  exitCodes: {
@@ -8372,6 +9514,13 @@ function buildAgentContract() {
8372
9514
  appliesTo: optionAppliesTo("force"),
8373
9515
  summary: "Overwrite an existing config file."
8374
9516
  },
9517
+ {
9518
+ flag: "--autopilot",
9519
+ type: "boolean",
9520
+ default: false,
9521
+ appliesTo: optionAppliesTo("autopilot"),
9522
+ 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)."
9523
+ },
8375
9524
  {
8376
9525
  flag: "--install",
8377
9526
  type: "boolean",
@@ -8391,7 +9540,7 @@ function buildAgentContract() {
8391
9540
  type: "boolean",
8392
9541
  default: false,
8393
9542
  appliesTo: optionAppliesTo("no-install"),
8394
- summary: "setup only: skip installing the dependency before generating (CI, offline, or a lockfile you do not want a package manager to touch)."
9543
+ 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)."
8395
9544
  }
8396
9545
  ],
8397
9546
  presets: PRESETS.map((preset) => ({
@@ -8463,6 +9612,36 @@ function buildAgentContract() {
8463
9612
  "The tool refuses to replace an outDir that it did not create, so a mistyped outDir cannot delete source code.",
8464
9613
  "Nothing is ever written above the repository root; --out-dir may not be the root or an ancestor."
8465
9614
  ],
9615
+ mcp: {
9616
+ command: `npx ${PACKAGE_NAME} mcp`,
9617
+ transport: "stdio",
9618
+ protocolVersions: {
9619
+ modern: [...MODERN_PROTOCOL_VERSIONS],
9620
+ legacy: [...LEGACY_PROTOCOL_VERSIONS],
9621
+ default: DEFAULT_LEGACY_PROTOCOL_VERSION
9622
+ },
9623
+ tools: buildTools().map((tool) => ({
9624
+ name: tool.name,
9625
+ title: tool.title,
9626
+ summary: tool.description,
9627
+ readOnly: tool.annotations.readOnlyHint,
9628
+ parameters: Object.keys(tool.inputSchema.properties),
9629
+ required: tool.inputSchema.required ?? []
9630
+ })),
9631
+ prompts: buildPrompts().map((prompt) => ({
9632
+ name: prompt.name,
9633
+ summary: prompt.description,
9634
+ arguments: prompt.arguments.map((argument) => argument.name)
9635
+ })),
9636
+ resources: [...STATIC_RESOURCE_URIS],
9637
+ resourceTemplates: [...RESOURCE_TEMPLATE_URIS],
9638
+ notes: [
9639
+ "stdout carries JSON-RPC messages only; every note and log line goes to stderr.",
9640
+ "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`.",
9641
+ "Everything it can do is a subset of the CLI. It never installs packages and never writes a config file.",
9642
+ "A `root` argument may name a sub-directory of the server's --cwd, and nothing outside it is readable."
9643
+ ]
9644
+ },
8466
9645
  troubleshooting: [
8467
9646
  {
8468
9647
  symptom: "Command prints nothing and exits 0",
@@ -8520,10 +9699,11 @@ globally, and you do not need to clone anything.
8520
9699
 
8521
9700
  **Pin a version** \u2014 \`npx ${PACKAGE_NAME2}@${GENERATOR_VERSION} <command>\` \u2014 when you
8522
9701
  need the same version on every machine. The bare package name works too: this package ships
8523
- one executable file, published under two names (\`codebase-snapshot\` and \`snap\`), which is what
8524
- lets npm pick it. If a command exits 0 printing nothing,
8525
- the executable was not resolved \u2014 clear the npm cache and retry; do not assume the repository
8526
- is broken.
9702
+ one executable file, published under two names (\`codebase-snapshot\` and \`snapshot\`), which is
9703
+ what lets npm pick it. A global install gives the short command \`snapshot\`; inside a project
9704
+ that has this package installed, \`npx snapshot\` resolves the local copy first. If a command
9705
+ exits 0 printing nothing, the executable was not resolved \u2014 clear the npm cache and retry;
9706
+ do not assume the repository is broken.
8527
9707
 
8528
9708
  ## Step by step
8529
9709
 
@@ -8548,8 +9728,11 @@ is broken.
8548
9728
  does steps 4\u20136 in one go with defaults: it writes the config (an existing one is kept),
8549
9729
  adds the npm scripts and the devDependency, installs with the project's package manager,
8550
9730
  and generates. Use \`--no-install\` when the machine is offline or the user does not want
8551
- their lockfile touched. Prefer the explicit steps below when you need to control any of
8552
- those, or when something fails and you want to see which part.
9731
+ their lockfile touched. \`--autopilot\` does exactly the same thing from the default
9732
+ command (\`npx ${PACKAGE_NAME2}@latest --autopilot\`) and is the form to reach for when a
9733
+ single invocation must both set the project up and produce the snapshot. Prefer the
9734
+ explicit steps below when you need to control any of those, or when something fails and
9735
+ you want to see which part.
8553
9736
 
8554
9737
  4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
8555
9738
  the project shape, then writes a commented config file listing **every** setting at its
@@ -8557,7 +9740,10 @@ is broken.
8557
9740
  the running Node can strip types (Node >= 22.6) **and** the project is an ES module
8558
9741
  (\`"type": "module"\` in package.json); otherwise \`snapshot.config.mjs\`. Pass
8559
9742
  \`--preset <name>\` only if detection is wrong; the preset names are in the contract.
8560
- This step is optional: without a config the tool runs on the same defaults.
9743
+ Skipping this step is safe **at a terminal**: the first \`generate\` asks once, can write
9744
+ the config for you and then generates. Unattended, a \`generate\` with no config exits 2 \u2014
9745
+ run this step, or \`--autopilot\`, or pass \`--no-config\` to run deliberately on the
9746
+ built-in defaults.
8561
9747
 
8562
9748
  5. **Generate.** Run \`npx ${PACKAGE_NAME2}@latest generate\`. Expect exit 0 and a
8563
9749
  \`./snapshot/\` directory. If you need a different location use \`--out-dir <path>\`; if
@@ -8605,6 +9791,10 @@ any of this to get a good first snapshot.
8605
9791
  deleting anything.
8606
9792
  - **Exit 2 is a usage error**, not a failure of the repository. Re-read the contract
8607
9793
  before retrying.
9794
+ - **A first run refuses rather than guessing.** In a project with no config and no previous
9795
+ snapshot, \`generate\` without a terminal exits 2 instead of silently using defaults. That
9796
+ is deliberate: say which set-up you want (\`--autopilot\`, \`init --yes\`, or \`--no-config\`)
9797
+ rather than retrying the same command.
8608
9798
  - **\`--format json\` keeps stdout pure.** Progress goes to stderr, so \`stdout\` is safe to
8609
9799
  pipe into a JSON parser.
8610
9800
 
@@ -8620,6 +9810,14 @@ It never writes \`AGENTS.md\`. That file is the repository's own agent contract
8620
9810
  commands, conventions, house rules \u2014 and this tool has no business editing it. If you want
8621
9811
  this skill in there, run \`npx ${PACKAGE_NAME2}@latest skill\` and paste the text yourself: the
8622
9812
  diff, and the commit, are then yours.
9813
+
9814
+ ## When the client speaks MCP
9815
+
9816
+ If the agent runs inside an MCP client, \`npx ${PACKAGE_NAME2}@latest mcp\` serves this repository
9817
+ as seven tools over stdio instead of shelling out. Its reads come from the snapshot rather than
9818
+ the working tree, so excluded files and redacted values never reach the context. Use
9819
+ \`snapshot_locate\` before \`snapshot_read\`, and \`snapshot_validate\` before trusting a snapshot
9820
+ from an earlier session.
8623
9821
  `;
8624
9822
  }
8625
9823
 
@@ -8630,17 +9828,17 @@ var LEGACY_AGENTS_MARKER = "<!-- codebase-snapshot:skill:start -->";
8630
9828
  function targetPath(cwd, target) {
8631
9829
  switch (target) {
8632
9830
  case "claude":
8633
- return path11__namespace.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
9831
+ return path12__namespace.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
8634
9832
  case "cursor":
8635
- return path11__namespace.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
9833
+ return path12__namespace.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
8636
9834
  }
8637
9835
  }
8638
9836
  function assertWritableInsideRepository(cwd, filePath) {
8639
- const root = fs10__namespace.realpathSync(cwd);
8640
- const shown = path11__namespace.relative(root, filePath) || filePath;
9837
+ const root = fs11__namespace.realpathSync(cwd);
9838
+ const shown = path12__namespace.relative(root, filePath) || filePath;
8641
9839
  let stat = null;
8642
9840
  try {
8643
- stat = fs10__namespace.lstatSync(filePath);
9841
+ stat = fs11__namespace.lstatSync(filePath);
8644
9842
  } catch {
8645
9843
  stat = null;
8646
9844
  }
@@ -8656,32 +9854,32 @@ function assertWritableInsideRepository(cwd, filePath) {
8656
9854
  }
8657
9855
  let existing = filePath;
8658
9856
  const missing = [];
8659
- while (!fs10__namespace.existsSync(existing)) {
8660
- missing.unshift(path11__namespace.basename(existing));
8661
- const parent = path11__namespace.dirname(existing);
9857
+ while (!fs11__namespace.existsSync(existing)) {
9858
+ missing.unshift(path12__namespace.basename(existing));
9859
+ const parent = path12__namespace.dirname(existing);
8662
9860
  if (parent === existing) break;
8663
9861
  existing = parent;
8664
9862
  }
8665
- const realAncestor = fs10__namespace.realpathSync(existing);
8666
- const resolved = missing.length === 0 ? realAncestor : path11__namespace.join(realAncestor, ...missing);
8667
- if (resolved !== root && !resolved.startsWith(`${root}${path11__namespace.sep}`)) {
9863
+ const realAncestor = fs11__namespace.realpathSync(existing);
9864
+ const resolved = missing.length === 0 ? realAncestor : path12__namespace.join(realAncestor, ...missing);
9865
+ if (resolved !== root && !resolved.startsWith(`${root}${path12__namespace.sep}`)) {
8668
9866
  throw new Error(
8669
9867
  `${REFUSED}: ${shown} resolves outside the repository (${resolved}). Move it back inside ${root}, or remove the link, and run again.`
8670
9868
  );
8671
9869
  }
8672
9870
  }
8673
9871
  function writeIfChanged(target, filePath, content) {
8674
- const existing = fs10__namespace.existsSync(filePath) ? fs10__namespace.readFileSync(filePath, "utf8") : null;
9872
+ const existing = fs11__namespace.existsSync(filePath) ? fs11__namespace.readFileSync(filePath, "utf8") : null;
8675
9873
  if (existing === content) return { target, path: filePath, action: "unchanged" };
8676
- fs10__namespace.mkdirSync(path11__namespace.dirname(filePath), { recursive: true });
8677
- fs10__namespace.writeFileSync(filePath, content, "utf8");
9874
+ fs11__namespace.mkdirSync(path12__namespace.dirname(filePath), { recursive: true });
9875
+ fs11__namespace.writeFileSync(filePath, content, "utf8");
8678
9876
  return { target, path: filePath, action: existing === null ? "created" : "updated" };
8679
9877
  }
8680
9878
  function legacyAgentsNote(cwd) {
8681
- const filePath = path11__namespace.join(cwd, "AGENTS.md");
9879
+ const filePath = path12__namespace.join(cwd, "AGENTS.md");
8682
9880
  try {
8683
- if (!fs10__namespace.existsSync(filePath)) return null;
8684
- if (!fs10__namespace.readFileSync(filePath, "utf8").includes(LEGACY_AGENTS_MARKER)) return null;
9881
+ if (!fs11__namespace.existsSync(filePath)) return null;
9882
+ if (!fs11__namespace.readFileSync(filePath, "utf8").includes(LEGACY_AGENTS_MARKER)) return null;
8685
9883
  } catch {
8686
9884
  return null;
8687
9885
  }
@@ -8693,9 +9891,9 @@ function installSkill(cwd, target = "all") {
8693
9891
  const notes = [];
8694
9892
  const wanted = target === "all" ? ["claude", "cursor"] : [target];
8695
9893
  for (const item of wanted) assertWritableInsideRepository(cwd, targetPath(cwd, item));
8696
- const createsSomething = wanted.some((item) => !fs10__namespace.existsSync(targetPath(cwd, item)));
9894
+ const createsSomething = wanted.some((item) => !fs11__namespace.existsSync(targetPath(cwd, item)));
8697
9895
  if (createsSomething) {
8698
- assertWritableInsideRepository(cwd, path11__namespace.join(cwd, ".gitignore"));
9896
+ assertWritableInsideRepository(cwd, path12__namespace.join(cwd, ".gitignore"));
8699
9897
  }
8700
9898
  for (const item of wanted) {
8701
9899
  written.push(writeIfChanged(item, targetPath(cwd, item), skill));
@@ -8711,6 +9909,765 @@ function isSkillTarget(value) {
8711
9909
  return SKILL_TARGETS.includes(value);
8712
9910
  }
8713
9911
 
9912
+ // src/mcp/era.ts
9913
+ function createSession() {
9914
+ return { era: null, protocolVersion: null, ready: false };
9915
+ }
9916
+ function isLegacyVersion(version) {
9917
+ return LEGACY_PROTOCOL_VERSIONS.includes(version);
9918
+ }
9919
+ function isModernVersion(version) {
9920
+ return MODERN_PROTOCOL_VERSIONS.includes(version);
9921
+ }
9922
+ function inspectModernMeta(params) {
9923
+ const raw = params?._meta;
9924
+ if (!isJsonObject(raw)) return { kind: "absent" };
9925
+ const version = raw[META_KEYS.protocolVersion];
9926
+ if (typeof version !== "string" || version.length === 0) {
9927
+ return { kind: "incomplete", missing: META_KEYS.protocolVersion };
9928
+ }
9929
+ const capabilities2 = raw[META_KEYS.clientCapabilities];
9930
+ if (!isJsonObject(capabilities2)) {
9931
+ return { kind: "incomplete", missing: META_KEYS.clientCapabilities };
9932
+ }
9933
+ const info2 = raw[META_KEYS.clientInfo];
9934
+ return {
9935
+ kind: "present",
9936
+ meta: {
9937
+ version,
9938
+ clientCapabilities: capabilities2,
9939
+ clientInfo: isJsonObject(info2) ? info2 : null
9940
+ }
9941
+ };
9942
+ }
9943
+ function unsupportedVersionData(requested) {
9944
+ return { requested, supported: [...SUPPORTED_PROTOCOL_VERSIONS] };
9945
+ }
9946
+ function classifyRequest(method, params, session) {
9947
+ const inspection = inspectModernMeta(params);
9948
+ if (session.era === "modern") {
9949
+ if (inspection.kind === "absent") {
9950
+ return {
9951
+ kind: "error",
9952
+ code: ERROR_CODES.invalidRequest,
9953
+ message: `This request has no _meta. The modern era requires "${META_KEYS.protocolVersion}" and "${META_KEYS.clientCapabilities}" on every request.`
9954
+ };
9955
+ }
9956
+ if (inspection.kind === "incomplete") {
9957
+ return {
9958
+ kind: "error",
9959
+ code: ERROR_CODES.invalidParams,
9960
+ message: `The request _meta is missing the required key "${inspection.missing}".`
9961
+ };
9962
+ }
9963
+ if (!isModernVersion(inspection.meta.version)) {
9964
+ return {
9965
+ kind: "error",
9966
+ code: ERROR_CODES.unsupportedProtocolVersion,
9967
+ message: `Unsupported protocol version "${inspection.meta.version}".`,
9968
+ data: unsupportedVersionData(inspection.meta.version)
9969
+ };
9970
+ }
9971
+ return { kind: "era", era: "modern", version: inspection.meta.version };
9972
+ }
9973
+ if (session.era === "legacy") {
9974
+ return { kind: "era", era: "legacy", version: session.protocolVersion };
9975
+ }
9976
+ if (method === METHOD3.initialize) {
9977
+ return { kind: "era", era: "legacy", version: null };
9978
+ }
9979
+ if (inspection.kind === "present") {
9980
+ if (!isModernVersion(inspection.meta.version)) {
9981
+ return {
9982
+ kind: "error",
9983
+ code: ERROR_CODES.unsupportedProtocolVersion,
9984
+ message: `Unsupported protocol version "${inspection.meta.version}".`,
9985
+ data: unsupportedVersionData(inspection.meta.version)
9986
+ };
9987
+ }
9988
+ return { kind: "era", era: "modern", version: inspection.meta.version };
9989
+ }
9990
+ if (inspection.kind === "incomplete") {
9991
+ return {
9992
+ kind: "error",
9993
+ code: ERROR_CODES.invalidParams,
9994
+ message: `The request _meta is missing the required key "${inspection.missing}".`
9995
+ };
9996
+ }
9997
+ if (method === METHOD3.discover) {
9998
+ return { kind: "era", era: "modern", version: MODERN_PROTOCOL_VERSIONS[0] };
9999
+ }
10000
+ return { kind: "era", era: "legacy", version: null };
10001
+ }
10002
+ function negotiateLegacyVersion(requested) {
10003
+ if (typeof requested === "string" && isLegacyVersion(requested)) {
10004
+ return { version: requested, matched: true };
10005
+ }
10006
+ return { version: DEFAULT_LEGACY_PROTOCOL_VERSION, matched: false };
10007
+ }
10008
+
10009
+ // src/mcp/schema.ts
10010
+ function validateArguments(schema, value) {
10011
+ if (value === void 0) value = {};
10012
+ if (!isJsonObject(value)) return ["arguments must be a JSON object"];
10013
+ const problems = [];
10014
+ const known = new Set(Object.keys(schema.properties));
10015
+ for (const key of Object.keys(value)) {
10016
+ if (!known.has(key) && schema.additionalProperties !== true) {
10017
+ const allowed = [...known].sort().join(", ");
10018
+ problems.push(`unknown argument "${key}" (accepted: ${allowed || "none"})`);
10019
+ }
10020
+ }
10021
+ for (const required of schema.required ?? []) {
10022
+ if (value[required] === void 0) problems.push(`missing required argument "${required}"`);
10023
+ }
10024
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
10025
+ const argument = value[key];
10026
+ if (argument === void 0) continue;
10027
+ problems.push(...checkValue(key, propertySchema, argument));
10028
+ }
10029
+ return problems;
10030
+ }
10031
+ function checkValue(path18, schema, value) {
10032
+ if (schema.enum && !schema.enum.includes(value)) {
10033
+ return [`"${path18}" must be one of: ${schema.enum.join(", ")}`];
10034
+ }
10035
+ switch (schema.type) {
10036
+ case "string": {
10037
+ if (typeof value !== "string") return [`"${path18}" must be a string`];
10038
+ const problems = [];
10039
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
10040
+ problems.push(`"${path18}" must be at least ${schema.minLength} character(s)`);
10041
+ }
10042
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
10043
+ problems.push(`"${path18}" must be at most ${schema.maxLength} character(s)`);
10044
+ }
10045
+ return problems;
10046
+ }
10047
+ case "number":
10048
+ case "integer": {
10049
+ if (typeof value !== "number" || Number.isNaN(value)) {
10050
+ return [`"${path18}" must be a number`];
10051
+ }
10052
+ if (schema.type === "integer" && !Number.isInteger(value)) {
10053
+ return [`"${path18}" must be an integer`];
10054
+ }
10055
+ const problems = [];
10056
+ if (schema.minimum !== void 0 && value < schema.minimum) {
10057
+ problems.push(`"${path18}" must be >= ${schema.minimum}`);
10058
+ }
10059
+ if (schema.maximum !== void 0 && value > schema.maximum) {
10060
+ problems.push(`"${path18}" must be <= ${schema.maximum}`);
10061
+ }
10062
+ return problems;
10063
+ }
10064
+ case "boolean":
10065
+ return typeof value === "boolean" ? [] : [`"${path18}" must be a boolean`];
10066
+ case "array": {
10067
+ if (!Array.isArray(value)) return [`"${path18}" must be an array`];
10068
+ if (!schema.items) return [];
10069
+ const problems = [];
10070
+ value.forEach((item, index) => {
10071
+ problems.push(...checkValue(`${path18}[${index}]`, schema.items, item));
10072
+ });
10073
+ return problems;
10074
+ }
10075
+ case "object":
10076
+ return isJsonObject(value) ? [] : [`"${path18}" must be an object`];
10077
+ default:
10078
+ return [];
10079
+ }
10080
+ }
10081
+ function applyDefaults(schema, value) {
10082
+ const out = { ...value };
10083
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
10084
+ if (out[key] === void 0 && propertySchema.default !== void 0) {
10085
+ out[key] = propertySchema.default;
10086
+ }
10087
+ }
10088
+ return out;
10089
+ }
10090
+
10091
+ // src/mcp/server.ts
10092
+ function capabilities() {
10093
+ return {
10094
+ tools: { listChanged: false },
10095
+ resources: { subscribe: false, listChanged: false },
10096
+ prompts: { listChanged: false }
10097
+ };
10098
+ }
10099
+ function requestIdOf(message) {
10100
+ const raw = message.id;
10101
+ return typeof raw === "string" || typeof raw === "number" ? raw : null;
10102
+ }
10103
+ function createMcpServer(options) {
10104
+ const session = createSession();
10105
+ const toolsByName = new Map(options.tools.map((tool) => [tool.name, tool]));
10106
+ const promptsByName = new Map(options.prompts.map((prompt) => [prompt.name, prompt]));
10107
+ const identity = () => ({
10108
+ name: options.info.name,
10109
+ title: options.info.title,
10110
+ version: options.info.version,
10111
+ websiteUrl: options.info.websiteUrl
10112
+ });
10113
+ const ok = (id, result, modern) => successResponse(id, result, modern, options.info);
10114
+ function handleNotification(method) {
10115
+ if (method === METHOD3.initialized && session.era === "legacy") session.ready = true;
10116
+ }
10117
+ function initialize(id, params) {
10118
+ const negotiation = negotiateLegacyVersion(params?.protocolVersion);
10119
+ session.era = "legacy";
10120
+ session.protocolVersion = negotiation.version;
10121
+ session.ready = true;
10122
+ options.context.log(
10123
+ negotiation.matched ? `mcp: client initialized with protocol ${negotiation.version}` : `mcp: client asked for an unsupported version and was offered ${negotiation.version}`
10124
+ );
10125
+ return {
10126
+ jsonrpc: "2.0",
10127
+ id,
10128
+ result: {
10129
+ protocolVersion: negotiation.version,
10130
+ capabilities: capabilities(),
10131
+ serverInfo: identity(),
10132
+ instructions: options.instructions
10133
+ }
10134
+ };
10135
+ }
10136
+ function discover(id) {
10137
+ session.era = "modern";
10138
+ session.ready = true;
10139
+ return ok(
10140
+ id,
10141
+ {
10142
+ supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
10143
+ capabilities: capabilities(),
10144
+ instructions: options.instructions,
10145
+ // The capability set depends on which repository the server was started for, so the answer
10146
+ // may be cached only by the client that asked, and not at all after this response.
10147
+ cacheScope: "private",
10148
+ ttlMs: 0
10149
+ },
10150
+ true
10151
+ );
10152
+ }
10153
+ function listTools(id, modern) {
10154
+ const tools = options.tools.map((tool) => ({
10155
+ name: tool.name,
10156
+ title: tool.title,
10157
+ description: tool.description,
10158
+ inputSchema: tool.inputSchema,
10159
+ ...tool.outputSchema ? { outputSchema: tool.outputSchema } : {},
10160
+ annotations: tool.annotations
10161
+ }));
10162
+ return ok(id, { tools }, modern);
10163
+ }
10164
+ async function callTool(id, modern, params) {
10165
+ const name = params?.name;
10166
+ if (typeof name !== "string") {
10167
+ return errorResponse(id, ERROR_CODES.invalidParams, 'tools/call requires a "name" string.');
10168
+ }
10169
+ const tool = toolsByName.get(name);
10170
+ if (!tool) {
10171
+ const known = [...toolsByName.keys()].sort().join(", ");
10172
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown tool "${name}". Available: ${known}.`);
10173
+ }
10174
+ const rawArguments = params?.arguments ?? {};
10175
+ if (!isJsonObject(rawArguments)) {
10176
+ return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
10177
+ }
10178
+ const problems = validateArguments(tool.inputSchema, rawArguments);
10179
+ if (problems.length > 0) {
10180
+ return ok(
10181
+ id,
10182
+ {
10183
+ content: [{ type: "text", text: `Invalid arguments for ${tool.name}:
10184
+ - ${problems.join("\n- ")}` }],
10185
+ isError: true
10186
+ },
10187
+ modern
10188
+ );
10189
+ }
10190
+ const args = applyDefaults(tool.inputSchema, rawArguments);
10191
+ let outcome;
10192
+ try {
10193
+ outcome = await tool.handler(args, options.context);
10194
+ } catch (error) {
10195
+ const detail = error instanceof Error ? error.message : String(error);
10196
+ options.context.log(`mcp: tool ${tool.name} failed: ${detail}`);
10197
+ return ok(
10198
+ id,
10199
+ { content: [{ type: "text", text: `${tool.name} failed: ${detail}` }], isError: true },
10200
+ modern
10201
+ );
10202
+ }
10203
+ return ok(
10204
+ id,
10205
+ {
10206
+ content: [{ type: "text", text: outcome.text }],
10207
+ ...outcome.structured ? { structuredContent: outcome.structured } : {},
10208
+ ...outcome.isError ? { isError: true } : {}
10209
+ },
10210
+ modern
10211
+ );
10212
+ }
10213
+ async function listResources(id, modern) {
10214
+ const resources = await options.resources.list(options.context);
10215
+ return ok(id, { resources }, modern);
10216
+ }
10217
+ async function readResource(id, modern, params) {
10218
+ const uri = params?.uri;
10219
+ if (typeof uri !== "string") {
10220
+ return errorResponse(id, ERROR_CODES.invalidParams, 'resources/read requires a "uri" string.');
10221
+ }
10222
+ const contents = await options.resources.read(uri, options.context);
10223
+ if (!contents) {
10224
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown resource "${uri}".`);
10225
+ }
10226
+ return ok(id, { contents: [contents] }, modern);
10227
+ }
10228
+ function listPrompts(id, modern) {
10229
+ const prompts = options.prompts.map((prompt) => ({
10230
+ name: prompt.name,
10231
+ ...prompt.title ? { title: prompt.title } : {},
10232
+ description: prompt.description,
10233
+ arguments: prompt.arguments
10234
+ }));
10235
+ return ok(id, { prompts }, modern);
10236
+ }
10237
+ function getPrompt(id, modern, params) {
10238
+ const name = params?.name;
10239
+ if (typeof name !== "string") {
10240
+ return errorResponse(id, ERROR_CODES.invalidParams, 'prompts/get requires a "name" string.');
10241
+ }
10242
+ const prompt = promptsByName.get(name);
10243
+ if (!prompt) {
10244
+ const known = [...promptsByName.keys()].sort().join(", ");
10245
+ return errorResponse(id, ERROR_CODES.invalidParams, `Unknown prompt "${name}". Available: ${known}.`);
10246
+ }
10247
+ const rawArguments = params?.arguments ?? {};
10248
+ if (!isJsonObject(rawArguments)) {
10249
+ return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
10250
+ }
10251
+ const built = prompt.build(rawArguments);
10252
+ const messages = built.messages.map((message) => ({
10253
+ role: message.role,
10254
+ content: { type: "text", text: message.content.text }
10255
+ }));
10256
+ return ok(id, { description: built.description, messages }, modern);
10257
+ }
10258
+ async function route(name, id, modern, params) {
10259
+ switch (name) {
10260
+ case METHOD3.initialize:
10261
+ return modern ? errorResponse(
10262
+ id,
10263
+ ERROR_CODES.methodNotFound,
10264
+ '"initialize" does not exist in this protocol revision; call "server/discover".'
10265
+ ) : initialize(id, params);
10266
+ case METHOD3.discover:
10267
+ return discover(id);
10268
+ case METHOD3.ping:
10269
+ return modern ? errorResponse(
10270
+ id,
10271
+ ERROR_CODES.methodNotFound,
10272
+ '"ping" was removed in this protocol revision; call "server/discover".'
10273
+ ) : ok(id, {}, modern);
10274
+ }
10275
+ if (!session.ready) {
10276
+ return errorResponse(
10277
+ id,
10278
+ ERROR_CODES.invalidRequest,
10279
+ 'This connection has not been initialized. Send "initialize" first.'
10280
+ );
10281
+ }
10282
+ switch (name) {
10283
+ case METHOD3.toolsList:
10284
+ return listTools(id, modern);
10285
+ case METHOD3.toolsCall:
10286
+ return callTool(id, modern, params);
10287
+ case METHOD3.resourcesList:
10288
+ return listResources(id, modern);
10289
+ case METHOD3.resourcesRead:
10290
+ return readResource(id, modern, params);
10291
+ case METHOD3.resourceTemplatesList:
10292
+ return ok(id, { resourceTemplates: options.resources.templates() }, modern);
10293
+ case METHOD3.promptsList:
10294
+ return listPrompts(id, modern);
10295
+ case METHOD3.promptsGet:
10296
+ return getPrompt(id, modern, params);
10297
+ default:
10298
+ return errorResponse(id, ERROR_CODES.methodNotFound, `Unknown method "${name}".`);
10299
+ }
10300
+ }
10301
+ async function dispatch(message) {
10302
+ if (!isJsonObject(message)) {
10303
+ return errorResponse(null, ERROR_CODES.parseError, "Message is not a JSON object.");
10304
+ }
10305
+ const isRequest = Object.prototype.hasOwnProperty.call(message, "id");
10306
+ const method = message.method;
10307
+ const params = message.params;
10308
+ if (message.jsonrpc !== "2.0" && isRequest) {
10309
+ return errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry "jsonrpc": "2.0".');
10310
+ }
10311
+ if (typeof method !== "string" || method.length === 0) {
10312
+ return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry a "method" string.') : null;
10313
+ }
10314
+ if (params !== void 0 && !isJsonObject(params)) {
10315
+ return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, '"params" must be a JSON object when present.') : null;
10316
+ }
10317
+ const named = params;
10318
+ const decision = classifyRequest(method, named, session);
10319
+ const modern = decision.kind === "era" ? decision.era === "modern" : session.era === "modern";
10320
+ if (!isRequest) {
10321
+ if (decision.kind === "era" && session.era === null) {
10322
+ session.era = decision.era;
10323
+ session.protocolVersion = decision.version;
10324
+ }
10325
+ handleNotification(method);
10326
+ return null;
10327
+ }
10328
+ const id = requestIdOf(message);
10329
+ if (id === null) {
10330
+ return errorResponse(null, ERROR_CODES.invalidRequest, 'A request "id" must be a string or a number.');
10331
+ }
10332
+ if (decision.kind === "error") {
10333
+ return errorResponse(id, decision.code, decision.message, decision.data);
10334
+ }
10335
+ if (session.era === null) {
10336
+ session.era = decision.era;
10337
+ session.protocolVersion = decision.version;
10338
+ }
10339
+ return route(method, id, modern, named);
10340
+ }
10341
+ return {
10342
+ session,
10343
+ async handle(message) {
10344
+ try {
10345
+ return await dispatch(message);
10346
+ } catch (error) {
10347
+ const detail = error instanceof Error ? error.message : String(error);
10348
+ options.context.log(`mcp: internal error: ${detail}`);
10349
+ const id = isJsonObject(message) ? requestIdOf(message) : null;
10350
+ return errorResponse(id, ERROR_CODES.internalError, "Internal server error.");
10351
+ }
10352
+ }
10353
+ };
10354
+ }
10355
+ var MAX_RESOURCE_BYTES = 512 * 1024;
10356
+ var PART_URI = /^snapshot:\/\/parts\/(\d+)$/;
10357
+ var REPORT_URIS = /* @__PURE__ */ new Map([
10358
+ ["snapshot://reports/audit", { file: "audit.md", title: "Secret audit and risk findings" }],
10359
+ ["snapshot://reports/tree", { file: "project-tree.md", title: "Project tree" }],
10360
+ ["snapshot://reports/navigation", { file: "navigation.md", title: "Navigation index" }]
10361
+ ]);
10362
+ function formatBytes2(bytes) {
10363
+ if (bytes < 1024) return `${bytes} B`;
10364
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
10365
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
10366
+ }
10367
+ function truncationMarker(originalBytes) {
10368
+ return `
10369
+
10370
+ [truncated: ${formatBytes2(originalBytes)} total. Use the snapshot_read tool for a line window.]
10371
+ `;
10372
+ }
10373
+ function capUtf8(text2, limit) {
10374
+ const size2 = Buffer.byteLength(text2, "utf8");
10375
+ if (size2 <= limit) return { text: text2, truncated: false };
10376
+ const cut = Buffer.from(text2, "utf8").subarray(0, limit).toString("utf8").replace(/\uFFFD+$/, "");
10377
+ return { text: `${cut}${truncationMarker(size2)}`, truncated: true };
10378
+ }
10379
+ async function workspaceFor(context) {
10380
+ const root = resolveRoot(void 0, context);
10381
+ if (!root.ok) return { error: root.message };
10382
+ const opened = await openWorkspace(root.root);
10383
+ if (!opened.ok) return { error: opened.message };
10384
+ return opened.workspace;
10385
+ }
10386
+ function readFileIfPresent(file) {
10387
+ try {
10388
+ return fs11__namespace.readFileSync(file, "utf8");
10389
+ } catch {
10390
+ return null;
10391
+ }
10392
+ }
10393
+ function buildResourceProvider() {
10394
+ return {
10395
+ async list(context) {
10396
+ const resources = [
10397
+ {
10398
+ uri: "snapshot://manifest",
10399
+ name: "manifest",
10400
+ title: "Snapshot manifest",
10401
+ description: "Every file in the snapshot with its layer, kind, language, part and hash, plus the reason each excluded file was excluded.",
10402
+ mimeType: "application/json"
10403
+ },
10404
+ {
10405
+ uri: "snapshot://contract",
10406
+ name: "contract",
10407
+ title: "Tool contract",
10408
+ description: "The machine-readable description of the codebase-snapshot command line.",
10409
+ mimeType: "application/json"
10410
+ },
10411
+ {
10412
+ uri: "snapshot://skill",
10413
+ name: "skill",
10414
+ title: "Agent skill",
10415
+ description: "Usage guidance for an agent working with this tool.",
10416
+ mimeType: "text/markdown"
10417
+ }
10418
+ ];
10419
+ for (const [uri, report] of REPORT_URIS) {
10420
+ resources.push({
10421
+ uri,
10422
+ name: report.file.replace(/\.md$/, ""),
10423
+ title: report.title,
10424
+ description: `${report.title} from the most recent snapshot.`,
10425
+ mimeType: "text/markdown"
10426
+ });
10427
+ }
10428
+ const workspace = await workspaceFor(context);
10429
+ if ("error" in workspace) return resources;
10430
+ const read = readManifest3(workspace);
10431
+ if (!read.ok) return resources;
10432
+ for (const part of read.manifest.parts) {
10433
+ resources.push({
10434
+ uri: `snapshot://parts/${part.index}`,
10435
+ name: `part-${String(part.index).padStart(3, "0")}`,
10436
+ title: `Part ${part.index} (${part.fileCount} files, ${formatBytes2(part.byteSize)})`,
10437
+ description: `Part ${part.index} of ${read.manifest.parts.length}: ${part.fileCount} file(s), ${part.lines} line(s), ${formatBytes2(part.byteSize)}.`,
10438
+ mimeType: "text/markdown"
10439
+ });
10440
+ }
10441
+ return resources;
10442
+ },
10443
+ templates() {
10444
+ return [
10445
+ {
10446
+ uriTemplate: "snapshot://parts/{index}",
10447
+ name: "part",
10448
+ title: "Snapshot part",
10449
+ description: "One budgeted part of the snapshot, by its 1-based index.",
10450
+ mimeType: "text/markdown"
10451
+ }
10452
+ ];
10453
+ },
10454
+ async read(uri, context) {
10455
+ if (uri === "snapshot://contract") {
10456
+ const contract = buildAgentContract();
10457
+ return { uri, mimeType: "application/json", text: JSON.stringify(contract, null, 2) };
10458
+ }
10459
+ if (uri === "snapshot://skill") {
10460
+ return { uri, mimeType: "text/markdown", text: renderSkill() };
10461
+ }
10462
+ const workspace = await workspaceFor(context);
10463
+ if ("error" in workspace) return null;
10464
+ const report = REPORT_URIS.get(uri);
10465
+ if (report) {
10466
+ const text2 = readFileIfPresent(
10467
+ path12__namespace.resolve(workspace.root, workspace.config.outDir, "reports", report.file)
10468
+ );
10469
+ if (text2 === null) return null;
10470
+ return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
10471
+ }
10472
+ if (uri === "snapshot://manifest") {
10473
+ const read = readManifest3(workspace);
10474
+ if (!read.ok) return null;
10475
+ const text2 = readFileIfPresent(read.file) ?? JSON.stringify(read.manifest, null, 2);
10476
+ return { uri, mimeType: "application/json", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
10477
+ }
10478
+ const partMatch = PART_URI.exec(uri);
10479
+ if (partMatch) {
10480
+ const read = readManifest3(workspace);
10481
+ if (!read.ok) return null;
10482
+ const index = Number(partMatch[1]);
10483
+ const part = read.manifest.parts.find((candidate) => candidate.index === index);
10484
+ if (!part) return null;
10485
+ const text2 = readFileIfPresent(
10486
+ path12__namespace.resolve(workspace.root, workspace.config.outDir, "parts", part.filename)
10487
+ );
10488
+ if (text2 === null) return null;
10489
+ return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
10490
+ }
10491
+ return null;
10492
+ }
10493
+ };
10494
+ }
10495
+
10496
+ // src/mcp/instructions.ts
10497
+ var SERVER_INSTRUCTIONS = [
10498
+ "codebase-snapshot serves this repository as a size-budgeted snapshot, not as a directory walk.",
10499
+ "",
10500
+ "Work in this order:",
10501
+ "1. snapshot_status \u2014 is there a snapshot here, and has the tree moved since it was built?",
10502
+ "2. snapshot_locate \u2014 find which part holds the code you need.",
10503
+ "3. snapshot_read \u2014 read a window of one file. Never read a part end to end.",
10504
+ "4. snapshot_validate \u2014 before trusting context that came from an earlier conversation.",
10505
+ "",
10506
+ "Rules that matter:",
10507
+ "- Parts are budgeted, not small: a 12-part snapshot is hundreds of kilobytes. Use snapshot_locate first.",
10508
+ "- A file absent from the snapshot has a recorded reason. Ask snapshot_explain, do not assume.",
10509
+ "- snapshot_generate is the only tool that writes. It never installs packages and never writes config.",
10510
+ "- Paths are relative to the repository root the server was started for, and nothing outside it is readable."
10511
+ ].join("\n");
10512
+
10513
+ // src/mcp/stdio.ts
10514
+ var MAX_LINE_BYTES = 32 * 1024 * 1024;
10515
+ function createWriter(output) {
10516
+ return {
10517
+ write(value) {
10518
+ return new Promise((resolve9, reject) => {
10519
+ output.write(`${JSON.stringify(value)}
10520
+ `, (error) => {
10521
+ if (error) reject(error);
10522
+ else resolve9();
10523
+ });
10524
+ });
10525
+ }
10526
+ };
10527
+ }
10528
+ function createLineReader(input, options) {
10529
+ let buffer = "";
10530
+ let discarding = false;
10531
+ let discardedBytes = 0;
10532
+ const deliver = (line) => {
10533
+ if (discarding) {
10534
+ discarding = false;
10535
+ discardedBytes = 0;
10536
+ return;
10537
+ }
10538
+ const trimmed = line.endsWith("\r") ? line.slice(0, -1) : line;
10539
+ if (trimmed.trim().length === 0) return;
10540
+ options.onLine(trimmed);
10541
+ };
10542
+ const drain = () => {
10543
+ let index = buffer.indexOf("\n");
10544
+ while (index >= 0) {
10545
+ const line = buffer.slice(0, index);
10546
+ buffer = buffer.slice(index + 1);
10547
+ deliver(line);
10548
+ index = buffer.indexOf("\n");
10549
+ }
10550
+ };
10551
+ const onData = (chunk) => {
10552
+ if (discarding) {
10553
+ const index = chunk.indexOf("\n");
10554
+ if (index < 0) {
10555
+ discardedBytes += chunk.length;
10556
+ return;
10557
+ }
10558
+ buffer = chunk.slice(index + 1);
10559
+ deliver("");
10560
+ drain();
10561
+ return;
10562
+ }
10563
+ buffer += chunk;
10564
+ if (buffer.length > MAX_LINE_BYTES && buffer.indexOf("\n") < 0) {
10565
+ discarding = true;
10566
+ discardedBytes = buffer.length;
10567
+ buffer = "";
10568
+ options.onOverflow?.(discardedBytes);
10569
+ return;
10570
+ }
10571
+ drain();
10572
+ };
10573
+ const onEnd = () => {
10574
+ if (buffer.length > 0) {
10575
+ const rest = buffer;
10576
+ buffer = "";
10577
+ deliver(rest);
10578
+ }
10579
+ };
10580
+ input.setEncoding("utf8");
10581
+ input.on("data", onData);
10582
+ input.on("end", onEnd);
10583
+ return () => {
10584
+ input.off("data", onData);
10585
+ input.off("end", onEnd);
10586
+ };
10587
+ }
10588
+
10589
+ // src/mcp/run.ts
10590
+ var PROJECT_URL = "https://ankurhalder.github.io/codebase-snapshot/";
10591
+ function createServer(options) {
10592
+ const cwd = options.cwd;
10593
+ const roots = options.roots && options.roots.length > 0 ? options.roots : [cwd];
10594
+ const context = {
10595
+ cwd,
10596
+ roots,
10597
+ log: options.log ?? ((line) => process.stderr.write(`${line}
10598
+ `))
10599
+ };
10600
+ return createMcpServer({
10601
+ info: {
10602
+ name: GENERATOR_NAME,
10603
+ title: "codebase-snapshot",
10604
+ version: GENERATOR_VERSION,
10605
+ websiteUrl: PROJECT_URL
10606
+ },
10607
+ instructions: SERVER_INSTRUCTIONS,
10608
+ tools: buildTools(),
10609
+ resources: buildResourceProvider(),
10610
+ prompts: buildPrompts(),
10611
+ context
10612
+ });
10613
+ }
10614
+ async function runStdioServer(options) {
10615
+ const log = options.log ?? ((line) => process.stderr.write(`${line}
10616
+ `));
10617
+ const server = createServer(options);
10618
+ const writer = createWriter(process.stdout);
10619
+ let writeChain = Promise.resolve();
10620
+ const send = (message) => {
10621
+ writeChain = writeChain.then(() => writer.write(message)).catch((error) => {
10622
+ log(`mcp: could not write a response: ${error instanceof Error ? error.message : String(error)}`);
10623
+ });
10624
+ return writeChain;
10625
+ };
10626
+ let inFlight = 0;
10627
+ let inputEnded = false;
10628
+ let resolveDone = () => void 0;
10629
+ const finished = new Promise((resolve9) => {
10630
+ resolveDone = resolve9;
10631
+ });
10632
+ const finishIfIdle = () => {
10633
+ if (inputEnded && inFlight === 0) resolveDone(0);
10634
+ };
10635
+ const handleLine = async (line) => {
10636
+ let message;
10637
+ try {
10638
+ message = JSON.parse(line);
10639
+ } catch {
10640
+ await send(errorResponse(null, ERROR_CODES.parseError, "Message is not valid JSON."));
10641
+ return;
10642
+ }
10643
+ const response = await server.handle(message);
10644
+ if (response) await send(response);
10645
+ };
10646
+ const stopReading = createLineReader(process.stdin, {
10647
+ onOverflow: (bytes) => log(`mcp: discarded an oversized message (${bytes} bytes)`),
10648
+ onLine: (line) => {
10649
+ inFlight += 1;
10650
+ void handleLine(line).finally(() => {
10651
+ inFlight -= 1;
10652
+ finishIfIdle();
10653
+ });
10654
+ }
10655
+ });
10656
+ const onEnd = () => {
10657
+ inputEnded = true;
10658
+ finishIfIdle();
10659
+ };
10660
+ process.stdin.on("end", onEnd);
10661
+ process.stdin.on("close", onEnd);
10662
+ log(`mcp: serving ${options.cwd} over stdio`);
10663
+ const code = await finished;
10664
+ stopReading();
10665
+ process.stdin.off("end", onEnd);
10666
+ process.stdin.off("close", onEnd);
10667
+ await writeChain;
10668
+ return code;
10669
+ }
10670
+
8714
10671
  // src/cli.ts
8715
10672
  var VERSION = GENERATOR_VERSION;
8716
10673
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -8724,7 +10681,8 @@ var COMMAND_ALIASES = /* @__PURE__ */ new Map([
8724
10681
  ["explain", "explain"],
8725
10682
  ["diff", "diff"],
8726
10683
  ["describe", "describe"],
8727
- ["skill", "skill"]
10684
+ ["skill", "skill"],
10685
+ ["mcp", "mcp"]
8728
10686
  ]);
8729
10687
  var COMMAND_ARITY = {
8730
10688
  generate: 0,
@@ -8735,7 +10693,8 @@ var COMMAND_ARITY = {
8735
10693
  explain: 1,
8736
10694
  diff: 2,
8737
10695
  describe: 0,
8738
- skill: 0
10696
+ skill: 0,
10697
+ mcp: 0
8739
10698
  };
8740
10699
  var CLI_OPTIONS = {
8741
10700
  cwd: { type: "string" },
@@ -8758,6 +10717,7 @@ var CLI_OPTIONS = {
8758
10717
  exclude: { type: "string" },
8759
10718
  yes: { type: "boolean" },
8760
10719
  force: { type: "boolean" },
10720
+ autopilot: { type: "boolean" },
8761
10721
  install: { type: "boolean" },
8762
10722
  "no-install": { type: "boolean" },
8763
10723
  target: { type: "string" },
@@ -8773,6 +10733,14 @@ Production-grade LLM codebase snapshot generator, auditor, and AST partitioner
8773
10733
  Usage:
8774
10734
  codebase-snapshot [command] [options]
8775
10735
 
10736
+ First run:
10737
+ codebase-snapshot No config yet: asks once, sets the project up, generates.
10738
+ A configured project just generates. It never falls back
10739
+ to defaults it was not given.
10740
+ codebase-snapshot --autopilot The same without questions: detect the preset, write the
10741
+ config, add the npm scripts and devDependency, install,
10742
+ then generate.
10743
+
8776
10744
  Commands:
8777
10745
  setup Everything at once: config, scripts, dependency, install, generate
8778
10746
  generate (default) Generate a complete partitioned codebase snapshot
@@ -8783,6 +10751,7 @@ Commands:
8783
10751
  diff <a> <b> Compare two snapshots (directories or manifest.json files)
8784
10752
  describe Print the machine-readable contract (--format json for agents)
8785
10753
  skill Print the bundled agent skill; --install writes it into the repo
10754
+ mcp Serve this repository to an MCP client over stdio
8786
10755
 
8787
10756
  Options:
8788
10757
  --cwd <path> Target repository directory (default: current directory)
@@ -8805,6 +10774,12 @@ Options:
8805
10774
  auto: git when usable. git: require git. walk: read
8806
10775
  .gitignore directly (escape hatch for a broken .git)
8807
10776
 
10777
+ generate only:
10778
+ --autopilot Set the project up without asking, then generate: write the
10779
+ config and .gitignore, add the npm scripts and devDependency,
10780
+ and install with the project's own package manager.
10781
+ --no-install With --autopilot: skip the dependency install.
10782
+
8808
10783
  init only:
8809
10784
  --preset <name> Force a project preset instead of detecting one
8810
10785
  --include <globs> Comma-separated allow-list
@@ -8896,10 +10871,10 @@ function parseCliArgs(argv) {
8896
10871
  }
8897
10872
  if (values.cwd !== void 0) {
8898
10873
  const target = String(values.cwd);
8899
- if (!fs10__namespace.existsSync(target)) {
10874
+ if (!fs11__namespace.existsSync(target)) {
8900
10875
  throw new UsageError(`--cwd directory does not exist: ${target}`);
8901
10876
  }
8902
- if (!fs10__namespace.statSync(target).isDirectory()) {
10877
+ if (!fs11__namespace.statSync(target).isDirectory()) {
8903
10878
  throw new UsageError(`--cwd is not a directory: ${target}`);
8904
10879
  }
8905
10880
  }
@@ -9019,8 +10994,17 @@ function parseCliArgs(argv) {
9019
10994
  `--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
9020
10995
  );
9021
10996
  }
10997
+ if (values["no-install"] === true && command === "generate" && values.autopilot !== true) {
10998
+ throw new UsageError("--no-install only applies to `setup`, or to `generate --autopilot`.");
10999
+ }
11000
+ if (values.autopilot === true && values["no-config"] === true) {
11001
+ throw new UsageError(
11002
+ "--autopilot sets a project up and writes a config; --no-config ignores config files. Pass one or the other."
11003
+ );
11004
+ }
9022
11005
  return {
9023
11006
  command,
11007
+ explicitCommand: firstPositional !== void 0 || explicitMode !== void 0,
9024
11008
  args,
9025
11009
  format: rawFormat,
9026
11010
  dryRun: values["dry-run"] === true,
@@ -9032,10 +11016,11 @@ function parseCliArgs(argv) {
9032
11016
  exclude: values.exclude === void 0 ? void 0 : String(values.exclude),
9033
11017
  yes: values.yes === true,
9034
11018
  force: values.force === true,
11019
+ autopilot: values.autopilot === true,
9035
11020
  install: values.install === true,
9036
11021
  noInstall: values["no-install"] === true,
9037
11022
  target: values.target === void 0 ? void 0 : String(values.target),
9038
- cwd: values.cwd === void 0 ? process.cwd() : path11__namespace.resolve(String(values.cwd)),
11023
+ cwd: values.cwd === void 0 ? process.cwd() : path12__namespace.resolve(String(values.cwd)),
9039
11024
  outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
9040
11025
  includeMonolith: values["include-monolith"] === true || onlyNamesMonolith === true ? true : void 0,
9041
11026
  includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
@@ -9092,26 +11077,29 @@ function splitList(value) {
9092
11077
  return value.split(",").map((entry) => entry.trim()).filter(Boolean);
9093
11078
  }
9094
11079
  function displayOutPath(cwd, outDir, ...segments) {
9095
- const absolute = path11__namespace.resolve(cwd, outDir, ...segments);
9096
- const relative8 = path11__namespace.relative(cwd, absolute);
9097
- if (relative8 && !relative8.startsWith("..") && !path11__namespace.isAbsolute(relative8)) return relative8;
11080
+ const absolute = path12__namespace.resolve(cwd, outDir, ...segments);
11081
+ const relative11 = path12__namespace.relative(cwd, absolute);
11082
+ if (relative11 && !relative11.startsWith("..") && !path12__namespace.isAbsolute(relative11)) return relative11;
9098
11083
  return absolute;
9099
11084
  }
9100
11085
  function nextStepHint(cwd, scriptsAddedNow) {
9101
11086
  let hasScript = scriptsAddedNow;
9102
11087
  if (!hasScript) {
9103
11088
  try {
9104
- const parsed = JSON.parse(fs10__namespace.readFileSync(path11__namespace.join(cwd, "package.json"), "utf8"));
11089
+ const parsed = JSON.parse(fs11__namespace.readFileSync(path12__namespace.join(cwd, "package.json"), "utf8"));
9105
11090
  hasScript = typeof parsed.scripts?.snapshot === "string";
9106
11091
  } catch {
9107
11092
  hasScript = false;
9108
11093
  }
9109
11094
  }
9110
11095
  if (!hasScript) return `npx ${GENERATOR_NAME} generate`;
9111
- const bin = path11__namespace.join(cwd, "node_modules", ".bin", "codebase-snapshot");
9112
- const installed = fs10__namespace.existsSync(bin) || fs10__namespace.existsSync(`${bin}.cmd`);
11096
+ const bin = path12__namespace.join(cwd, "node_modules", ".bin", "codebase-snapshot");
11097
+ const installed = fs11__namespace.existsSync(bin) || fs11__namespace.existsSync(`${bin}.cmd`);
9113
11098
  return installed ? "npm run snapshot" : "npm install && npm run snapshot";
9114
11099
  }
11100
+ function presetSummary(init) {
11101
+ return init.reason === null ? `preset: ${init.preset}` : `preset: ${init.preset}, because ${init.reason}`;
11102
+ }
9115
11103
  async function runInitCommand(invocation) {
9116
11104
  try {
9117
11105
  const result = await runInit({
@@ -9136,7 +11124,7 @@ async function runInitCommand(invocation) {
9136
11124
  info(result.configText);
9137
11125
  return 0;
9138
11126
  }
9139
- info(`\u2705 Created ${path11__namespace.basename(result.configPath)} (preset: ${result.preset})`);
11127
+ info(`\u2705 Created ${path12__namespace.basename(result.configPath)} (${presetSummary(result)})`);
9140
11128
  if (result.scripts.length > 0) info(` Added npm scripts: ${result.scripts.join(", ")}`);
9141
11129
  if (result.dependency.added && result.dependency.range !== null) {
9142
11130
  info(` Added devDependency: ${result.dependency.name} ${result.dependency.range}`);
@@ -9156,7 +11144,7 @@ async function runInitCommand(invocation) {
9156
11144
  }
9157
11145
  }
9158
11146
  function detectPackageManager(cwd) {
9159
- const has = (name) => fs10__namespace.existsSync(path11__namespace.join(cwd, name));
11147
+ const has = (name) => fs11__namespace.existsSync(path12__namespace.join(cwd, name));
9160
11148
  if (has("bun.lockb") || has("bun.lock")) return { name: "bun", command: "bun", args: ["install"] };
9161
11149
  if (has("pnpm-lock.yaml")) return { name: "pnpm", command: "pnpm", args: ["install"] };
9162
11150
  if (has("yarn.lock")) return { name: "yarn", command: "yarn", args: ["install"] };
@@ -9207,8 +11195,9 @@ async function captureStdout(fn) {
9207
11195
  process.stdout.write = original;
9208
11196
  }
9209
11197
  }
9210
- async function runSetupCommand(invocation) {
11198
+ async function runSetupCommand(invocation, options = {}) {
9211
11199
  try {
11200
+ const interactive = options.interactive === true;
9212
11201
  const init = await runInit({
9213
11202
  cwd: invocation.cwd,
9214
11203
  preset: invocation.preset,
@@ -9218,7 +11207,7 @@ async function runSetupCommand(invocation) {
9218
11207
  exclude: splitList(invocation.exclude),
9219
11208
  skipArtifacts: invocation.skipArtifacts,
9220
11209
  includeSensitiveFiles: invocation.includeSensitiveFiles,
9221
- yes: true,
11210
+ yes: !interactive,
9222
11211
  force: invocation.force,
9223
11212
  reuseConfig: true,
9224
11213
  dryRun: invocation.dryRun
@@ -9245,7 +11234,7 @@ async function runSetupCommand(invocation) {
9245
11234
  }
9246
11235
  info("\n\u{1F9EA} Dry run \u2014 setup would:");
9247
11236
  info(
9248
- ` 1. ${init.reused ? `keep ${path11__namespace.basename(init.configPath)}` : `write ${path11__namespace.basename(init.configPath)} (preset: ${init.preset})`}`
11237
+ ` 1. ${init.reused ? `keep ${path12__namespace.basename(init.configPath)}` : `write ${path12__namespace.basename(init.configPath)} (preset: ${init.preset})`}`
9249
11238
  );
9250
11239
  info(" 2. add the npm scripts, the devDependency and the .gitignore entries");
9251
11240
  if (manager !== null) info(` 3. install with ${manager}`);
@@ -9255,11 +11244,11 @@ async function runSetupCommand(invocation) {
9255
11244
  return 0;
9256
11245
  }
9257
11246
  if (invocation.format !== "json") {
9258
- info(`\u{1F680} Setup for: ${invocation.cwd}`);
11247
+ if (!interactive) info(`\u{1F680} Setup for: ${invocation.cwd}`);
9259
11248
  if (init.reused) {
9260
- info(`\u267B Keeping ${path11__namespace.basename(init.configPath)} (preset: ${init.preset})`);
11249
+ info(`\u267B Keeping ${path12__namespace.basename(init.configPath)} (${presetSummary(init)})`);
9261
11250
  } else {
9262
- info(`\u2705 Created ${path11__namespace.basename(init.configPath)} (preset: ${init.preset})`);
11251
+ info(`\u2705 Created ${path12__namespace.basename(init.configPath)} (${presetSummary(init)})`);
9263
11252
  }
9264
11253
  if (init.scripts.length > 0) info(` Added npm scripts: ${init.scripts.join(", ")}`);
9265
11254
  if (init.dependency.added && init.dependency.range !== null) {
@@ -9288,6 +11277,7 @@ async function runSetupCommand(invocation) {
9288
11277
  if ((invocation.skipArtifacts ?? []).length > 0) {
9289
11278
  generateArgs.push("--skip-artifacts", (invocation.skipArtifacts ?? []).join(","));
9290
11279
  }
11280
+ if (invocation.watch) generateArgs.push("--watch");
9291
11281
  if (invocation.format === "json") {
9292
11282
  const generated = await captureStdout(() => main(generateArgs));
9293
11283
  let manifest = null;
@@ -9327,6 +11317,24 @@ async function runSetupCommand(invocation) {
9327
11317
  return fail(invocation.format, err.message);
9328
11318
  }
9329
11319
  }
11320
+ async function firstRun(invocation) {
11321
+ const plan = planFirstRun({
11322
+ hasConfig: findConfigFile(invocation.cwd) !== null,
11323
+ hasPriorSnapshot: hasPriorSnapshot(invocation.cwd, invocation.outDir ?? DEFAULT_ANSWERS.outDir),
11324
+ // A machine must never be asked a question, and `--format json` is how a machine says so.
11325
+ interactive: invocation.format === "text" && isInteractive(defaultIO()),
11326
+ autopilot: invocation.autopilot,
11327
+ emptyTarget: isEffectivelyEmpty(invocation.cwd)
11328
+ });
11329
+ if (plan.action === "generate") return null;
11330
+ if (plan.action === "refuse") {
11331
+ return fail(invocation.format, firstRunMessage(plan.reason, invocation.cwd), plan.exitCode);
11332
+ }
11333
+ info(
11334
+ plan.interactive ? "\u2699 This project has no snapshot configuration yet." : "\u2699 --autopilot: setting this project up before generating."
11335
+ );
11336
+ return await runSetupCommand(invocation, { interactive: plan.interactive });
11337
+ }
9330
11338
  function runSkillCommand(invocation) {
9331
11339
  if (!invocation.install) {
9332
11340
  if (invocation.format === "json") {
@@ -9348,7 +11356,7 @@ function runSkillCommand(invocation) {
9348
11356
  }
9349
11357
  info(`\u2705 Agent skill installed (${SKILL_NAME}):`);
9350
11358
  for (const item of result.written) {
9351
- const shown = path11__namespace.relative(invocation.cwd, item.path) || item.path;
11359
+ const shown = path12__namespace.relative(invocation.cwd, item.path) || item.path;
9352
11360
  info(` - ${item.action.padEnd(9)} ${shown}`);
9353
11361
  }
9354
11362
  if (result.gitignored.length > 0) {
@@ -9443,9 +11451,16 @@ ${USAGE}
9443
11451
  if (invocation.command === "skill") {
9444
11452
  return runSkillCommand(invocation);
9445
11453
  }
11454
+ if (invocation.command === "mcp") {
11455
+ return await runStdioServer({ cwd: path12__namespace.resolve(invocation.cwd) });
11456
+ }
9446
11457
  if (invocation.command === "setup") {
9447
11458
  return runSetupCommand(invocation);
9448
11459
  }
11460
+ if (invocation.command === "generate" && !invocation.noConfig && (!invocation.explicitCommand || invocation.autopilot)) {
11461
+ const decided = await firstRun(invocation);
11462
+ if (decided !== null) return decided;
11463
+ }
9449
11464
  const skipConfig = invocation.noConfig || invocation.command === "diff";
9450
11465
  const loaded = skipConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
9451
11466
  if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
@@ -9454,7 +11469,7 @@ ${USAGE}
9454
11469
  warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
9455
11470
  return 1;
9456
11471
  }
9457
- if (loaded.file) info(`\u2699 Using config ${path11__namespace.relative(process.cwd(), loaded.file) || loaded.file}`);
11472
+ if (loaded.file) info(`\u2699 Using config ${path12__namespace.relative(process.cwd(), loaded.file) || loaded.file}`);
9458
11473
  const resolved = resolveConfig(
9459
11474
  loaded.config,
9460
11475
  {
@@ -9476,10 +11491,10 @@ ${USAGE}
9476
11491
  if (invocation.command === "validate") {
9477
11492
  const result = validateSnapshot(resolved);
9478
11493
  if (result.valid) {
9479
- const manifestPath = path11__namespace.resolve(resolved.cwd, resolved.outDir, "manifest.json");
11494
+ const manifestPath = path12__namespace.resolve(resolved.cwd, resolved.outDir, "manifest.json");
9480
11495
  let manifest;
9481
11496
  try {
9482
- manifest = JSON.parse(fs10__namespace.readFileSync(manifestPath, "utf8"));
11497
+ manifest = JSON.parse(fs11__namespace.readFileSync(manifestPath, "utf8"));
9483
11498
  } catch {
9484
11499
  manifest = void 0;
9485
11500
  }
@@ -9492,13 +11507,13 @@ ${USAGE}
9492
11507
  return result.valid ? 0 : 1;
9493
11508
  }
9494
11509
  if (invocation.command === "report") {
9495
- const manifestPath = path11__namespace.resolve(resolved.cwd, resolved.outDir, "manifest.json");
9496
- if (!fs10__namespace.existsSync(manifestPath)) {
11510
+ const manifestPath = path12__namespace.resolve(resolved.cwd, resolved.outDir, "manifest.json");
11511
+ if (!fs11__namespace.existsSync(manifestPath)) {
9497
11512
  return fail(invocation.format, `Manifest not found at ${manifestPath}. Run generate first.`);
9498
11513
  }
9499
11514
  let manifest;
9500
11515
  try {
9501
- manifest = JSON.parse(fs10__namespace.readFileSync(manifestPath, "utf8"));
11516
+ manifest = JSON.parse(fs11__namespace.readFileSync(manifestPath, "utf8"));
9502
11517
  } catch (err) {
9503
11518
  return fail(invocation.format, `Could not read ${manifestPath}: ${err.message}`);
9504
11519
  }
@@ -9662,20 +11677,20 @@ function finishWatchRun(state, now) {
9662
11677
  };
9663
11678
  }
9664
11679
  function comparablePath(value) {
9665
- const absolute = path11__namespace.resolve(value);
11680
+ const absolute = path12__namespace.resolve(value);
9666
11681
  return process.platform === "win32" ? absolute.toLowerCase() : absolute;
9667
11682
  }
9668
11683
  function isSameOrInside(parent, candidate) {
9669
- const relative8 = path11__namespace.relative(parent, candidate);
9670
- return relative8 === "" || relative8 !== ".." && !relative8.startsWith(`..${path11__namespace.sep}`) && !path11__namespace.isAbsolute(relative8);
11684
+ const relative11 = path12__namespace.relative(parent, candidate);
11685
+ return relative11 === "" || relative11 !== ".." && !relative11.startsWith(`..${path12__namespace.sep}`) && !path12__namespace.isAbsolute(relative11);
9671
11686
  }
9672
11687
  function isGeneratedWatchPath(candidate, paths) {
9673
- const absolute = comparablePath(path11__namespace.resolve(paths.cwd, candidate));
9674
- const outDir = comparablePath(path11__namespace.resolve(paths.cwd, paths.outDir));
9675
- const tmpDir = comparablePath(path11__namespace.resolve(paths.cwd, paths.tmpDir));
11688
+ const absolute = comparablePath(path12__namespace.resolve(paths.cwd, candidate));
11689
+ const outDir = comparablePath(path12__namespace.resolve(paths.cwd, paths.outDir));
11690
+ const tmpDir = comparablePath(path12__namespace.resolve(paths.cwd, paths.tmpDir));
9676
11691
  if (isSameOrInside(outDir, absolute) || isSameOrInside(tmpDir, absolute)) return true;
9677
- const parent = path11__namespace.dirname(absolute);
9678
- if (parent === path11__namespace.dirname(outDir) && path11__namespace.basename(absolute).startsWith(`${path11__namespace.basename(outDir)}.backup-`)) {
11692
+ const parent = path12__namespace.dirname(absolute);
11693
+ if (parent === path12__namespace.dirname(outDir) && path12__namespace.basename(absolute).startsWith(`${path12__namespace.basename(outDir)}.backup-`)) {
9679
11694
  return true;
9680
11695
  }
9681
11696
  return absolute.split(/[\\/]+/).some((segment) => VCS_SEGMENTS.has(segment));
@@ -9692,8 +11707,8 @@ function decideWatchEvent(state, event, paths) {
9692
11707
  return { state, action: "schedule" };
9693
11708
  }
9694
11709
  async function runWatchLoop(invocation, resolved) {
9695
- const outDir = path11__namespace.resolve(resolved.cwd, resolved.outDir);
9696
- const tmpDir = path11__namespace.resolve(resolved.cwd, resolved.tmpDir);
11710
+ const outDir = path12__namespace.resolve(resolved.cwd, resolved.outDir);
11711
+ const tmpDir = path12__namespace.resolve(resolved.cwd, resolved.tmpDir);
9697
11712
  const paths = { cwd: resolved.cwd, outDir, tmpDir };
9698
11713
  let state = initialWatchState();
9699
11714
  let timer = null;
@@ -9758,11 +11773,11 @@ async function runWatchLoop(invocation, resolved) {
9758
11773
  };
9759
11774
  info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
9760
11775
  void regenerate();
9761
- const watcher = fs10__namespace.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
11776
+ const watcher = fs11__namespace.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
9762
11777
  const decision = decideWatchEvent(
9763
11778
  state,
9764
11779
  {
9765
- path: filename ? path11__namespace.resolve(resolved.cwd, filename.toString()) : null,
11780
+ path: filename ? path12__namespace.resolve(resolved.cwd, filename.toString()) : null,
9766
11781
  now: performance.now()
9767
11782
  },
9768
11783
  paths
@@ -9777,12 +11792,12 @@ async function runWatchLoop(invocation, resolved) {
9777
11792
  }
9778
11793
  if (decision.action === "schedule") schedule();
9779
11794
  });
9780
- await new Promise((resolve6) => {
11795
+ await new Promise((resolve9) => {
9781
11796
  const stop = () => {
9782
11797
  stopped = true;
9783
11798
  if (timer) clearTimeout(timer);
9784
11799
  watcher.close();
9785
- resolve6();
11800
+ resolve9();
9786
11801
  };
9787
11802
  process.once("SIGINT", stop);
9788
11803
  process.once("SIGTERM", stop);
@@ -9790,7 +11805,7 @@ async function runWatchLoop(invocation, resolved) {
9790
11805
  return 0;
9791
11806
  }
9792
11807
  var entryFile = process.argv[1] ?? "";
9793
- var entryName = path11__namespace.basename(entryFile).toLowerCase();
11808
+ var entryName = path12__namespace.basename(entryFile).toLowerCase();
9794
11809
  var invokedDirectly = entryName === "cli.js" || entryName === "cli.cjs";
9795
11810
  if (invokedDirectly) {
9796
11811
  void main().then(