codebase-snapshot 0.9.12 → 0.9.14

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/mcp.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { G as GenerateSummary, W as WorkerResults, g as generateInWorker } from './generate-worker-D5Xgh27c.cjs';
1
+ export { G as GenerateSummary, W as WorkerResults, g as generateInWorker } from './generate-worker-D_mUjgXw.cjs';
2
2
  import { Readable, Writable } from 'node:stream';
3
3
 
4
4
  /**
@@ -101,72 +101,6 @@ interface InputSchema extends JsonSchema {
101
101
  properties: Record<string, JsonSchema>;
102
102
  }
103
103
 
104
- type ProtocolEra = "legacy" | "modern";
105
- /** Per-connection state. The modern era has no session, so this stays empty until legacy init. */
106
- interface SessionState {
107
- /** Which era the connection committed to, or null before the first decisive request. */
108
- era: ProtocolEra | null;
109
- /** The revision in force. Null in legacy until `initialize` has been negotiated. */
110
- protocolVersion: string | null;
111
- /** True once `initialize` has been answered, which is the legacy lifecycle's gate. */
112
- ready: boolean;
113
- }
114
- declare function createSession(): SessionState;
115
- /** The modern request metadata, when a request carries it at all. */
116
- interface ModernMeta {
117
- version: string;
118
- clientCapabilities: JsonObject;
119
- clientInfo: JsonObject | null;
120
- }
121
- type MetaInspection = {
122
- kind: "absent";
123
- } | {
124
- kind: "incomplete";
125
- missing: string;
126
- } | {
127
- kind: "present";
128
- meta: ModernMeta;
129
- };
130
- /**
131
- * Reads `_meta` from a request.
132
- *
133
- * `absent` and `incomplete` are different answers: a request with no `_meta` at all is a legacy
134
- * client, while a request with `_meta` that lacks a required key is a modern client making a
135
- * mistake — and telling those two apart is what keeps the legacy era from being rejected for a
136
- * field it never had.
137
- */
138
- declare function inspectModernMeta(params: JsonObject | undefined): MetaInspection;
139
- type EraDecision = {
140
- kind: "era";
141
- era: ProtocolEra;
142
- version: string | null;
143
- } | {
144
- kind: "error";
145
- code: number;
146
- message: string;
147
- data?: unknown;
148
- };
149
- /**
150
- * Decides which era a request belongs to, and whether its version is acceptable.
151
- *
152
- * Once a connection commits to an era it stays committed. A legacy client that suddenly sends
153
- * modern `_meta` is confused rather than clever, and silently switching would answer half a
154
- * session in each shape.
155
- */
156
- declare function classifyRequest(method: string, params: JsonObject | undefined, session: SessionState): EraDecision;
157
- interface LegacyNegotiation {
158
- version: string;
159
- /** False when the client asked for something we cannot speak and was negotiated down. */
160
- matched: boolean;
161
- }
162
- /**
163
- * Legacy version negotiation: echo a supported request, otherwise answer with our best.
164
- *
165
- * A client that asked for a modern revision here gets the newest *legacy* revision, not an error:
166
- * under this lifecycle the server's answer is the offer, and the client decides.
167
- */
168
- declare function negotiateLegacyVersion(requested: unknown): LegacyNegotiation;
169
-
170
104
  /**
171
105
  * The capability records the MCP server serves.
172
106
  *
@@ -279,6 +213,72 @@ interface McpServerInfo {
279
213
  websiteUrl: string;
280
214
  }
281
215
 
216
+ type ProtocolEra = "legacy" | "modern";
217
+ /** Per-connection state. The modern era has no session, so this stays empty until legacy init. */
218
+ interface SessionState {
219
+ /** Which era the connection committed to, or null before the first decisive request. */
220
+ era: ProtocolEra | null;
221
+ /** The revision in force. Null in legacy until `initialize` has been negotiated. */
222
+ protocolVersion: string | null;
223
+ /** True once `initialize` has been answered, which is the legacy lifecycle's gate. */
224
+ ready: boolean;
225
+ }
226
+ declare function createSession(): SessionState;
227
+ /** The modern request metadata, when a request carries it at all. */
228
+ interface ModernMeta {
229
+ version: string;
230
+ clientCapabilities: JsonObject;
231
+ clientInfo: JsonObject | null;
232
+ }
233
+ type MetaInspection = {
234
+ kind: "absent";
235
+ } | {
236
+ kind: "incomplete";
237
+ missing: string;
238
+ } | {
239
+ kind: "present";
240
+ meta: ModernMeta;
241
+ };
242
+ /**
243
+ * Reads `_meta` from a request.
244
+ *
245
+ * `absent` and `incomplete` are different answers: a request with no `_meta` at all is a legacy
246
+ * client, while a request with `_meta` that lacks a required key is a modern client making a
247
+ * mistake — and telling those two apart is what keeps the legacy era from being rejected for a
248
+ * field it never had.
249
+ */
250
+ declare function inspectModernMeta(params: JsonObject | undefined): MetaInspection;
251
+ type EraDecision = {
252
+ kind: "era";
253
+ era: ProtocolEra;
254
+ version: string | null;
255
+ } | {
256
+ kind: "error";
257
+ code: number;
258
+ message: string;
259
+ data?: unknown;
260
+ };
261
+ /**
262
+ * Decides which era a request belongs to, and whether its version is acceptable.
263
+ *
264
+ * Once a connection commits to an era it stays committed. A legacy client that suddenly sends
265
+ * modern `_meta` is confused rather than clever, and silently switching would answer half a
266
+ * session in each shape.
267
+ */
268
+ declare function classifyRequest(method: string, params: JsonObject | undefined, session: SessionState): EraDecision;
269
+ interface LegacyNegotiation {
270
+ version: string;
271
+ /** False when the client asked for something we cannot speak and was negotiated down. */
272
+ matched: boolean;
273
+ }
274
+ /**
275
+ * Legacy version negotiation: echo a supported request, otherwise answer with our best.
276
+ *
277
+ * A client that asked for a modern revision here gets the newest *legacy* revision, not an error:
278
+ * under this lifecycle the server's answer is the offer, and the client decides.
279
+ */
280
+ declare function negotiateLegacyVersion(requested: unknown): LegacyNegotiation;
281
+
282
282
  interface McpServerOptions {
283
283
  info: McpServerInfo;
284
284
  instructions: string;
package/dist/mcp.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { G as GenerateSummary, W as WorkerResults, g as generateInWorker } from './generate-worker-D5Xgh27c.js';
1
+ export { G as GenerateSummary, W as WorkerResults, g as generateInWorker } from './generate-worker-D_mUjgXw.js';
2
2
  import { Readable, Writable } from 'node:stream';
3
3
 
4
4
  /**
@@ -101,72 +101,6 @@ interface InputSchema extends JsonSchema {
101
101
  properties: Record<string, JsonSchema>;
102
102
  }
103
103
 
104
- type ProtocolEra = "legacy" | "modern";
105
- /** Per-connection state. The modern era has no session, so this stays empty until legacy init. */
106
- interface SessionState {
107
- /** Which era the connection committed to, or null before the first decisive request. */
108
- era: ProtocolEra | null;
109
- /** The revision in force. Null in legacy until `initialize` has been negotiated. */
110
- protocolVersion: string | null;
111
- /** True once `initialize` has been answered, which is the legacy lifecycle's gate. */
112
- ready: boolean;
113
- }
114
- declare function createSession(): SessionState;
115
- /** The modern request metadata, when a request carries it at all. */
116
- interface ModernMeta {
117
- version: string;
118
- clientCapabilities: JsonObject;
119
- clientInfo: JsonObject | null;
120
- }
121
- type MetaInspection = {
122
- kind: "absent";
123
- } | {
124
- kind: "incomplete";
125
- missing: string;
126
- } | {
127
- kind: "present";
128
- meta: ModernMeta;
129
- };
130
- /**
131
- * Reads `_meta` from a request.
132
- *
133
- * `absent` and `incomplete` are different answers: a request with no `_meta` at all is a legacy
134
- * client, while a request with `_meta` that lacks a required key is a modern client making a
135
- * mistake — and telling those two apart is what keeps the legacy era from being rejected for a
136
- * field it never had.
137
- */
138
- declare function inspectModernMeta(params: JsonObject | undefined): MetaInspection;
139
- type EraDecision = {
140
- kind: "era";
141
- era: ProtocolEra;
142
- version: string | null;
143
- } | {
144
- kind: "error";
145
- code: number;
146
- message: string;
147
- data?: unknown;
148
- };
149
- /**
150
- * Decides which era a request belongs to, and whether its version is acceptable.
151
- *
152
- * Once a connection commits to an era it stays committed. A legacy client that suddenly sends
153
- * modern `_meta` is confused rather than clever, and silently switching would answer half a
154
- * session in each shape.
155
- */
156
- declare function classifyRequest(method: string, params: JsonObject | undefined, session: SessionState): EraDecision;
157
- interface LegacyNegotiation {
158
- version: string;
159
- /** False when the client asked for something we cannot speak and was negotiated down. */
160
- matched: boolean;
161
- }
162
- /**
163
- * Legacy version negotiation: echo a supported request, otherwise answer with our best.
164
- *
165
- * A client that asked for a modern revision here gets the newest *legacy* revision, not an error:
166
- * under this lifecycle the server's answer is the offer, and the client decides.
167
- */
168
- declare function negotiateLegacyVersion(requested: unknown): LegacyNegotiation;
169
-
170
104
  /**
171
105
  * The capability records the MCP server serves.
172
106
  *
@@ -279,6 +213,72 @@ interface McpServerInfo {
279
213
  websiteUrl: string;
280
214
  }
281
215
 
216
+ type ProtocolEra = "legacy" | "modern";
217
+ /** Per-connection state. The modern era has no session, so this stays empty until legacy init. */
218
+ interface SessionState {
219
+ /** Which era the connection committed to, or null before the first decisive request. */
220
+ era: ProtocolEra | null;
221
+ /** The revision in force. Null in legacy until `initialize` has been negotiated. */
222
+ protocolVersion: string | null;
223
+ /** True once `initialize` has been answered, which is the legacy lifecycle's gate. */
224
+ ready: boolean;
225
+ }
226
+ declare function createSession(): SessionState;
227
+ /** The modern request metadata, when a request carries it at all. */
228
+ interface ModernMeta {
229
+ version: string;
230
+ clientCapabilities: JsonObject;
231
+ clientInfo: JsonObject | null;
232
+ }
233
+ type MetaInspection = {
234
+ kind: "absent";
235
+ } | {
236
+ kind: "incomplete";
237
+ missing: string;
238
+ } | {
239
+ kind: "present";
240
+ meta: ModernMeta;
241
+ };
242
+ /**
243
+ * Reads `_meta` from a request.
244
+ *
245
+ * `absent` and `incomplete` are different answers: a request with no `_meta` at all is a legacy
246
+ * client, while a request with `_meta` that lacks a required key is a modern client making a
247
+ * mistake — and telling those two apart is what keeps the legacy era from being rejected for a
248
+ * field it never had.
249
+ */
250
+ declare function inspectModernMeta(params: JsonObject | undefined): MetaInspection;
251
+ type EraDecision = {
252
+ kind: "era";
253
+ era: ProtocolEra;
254
+ version: string | null;
255
+ } | {
256
+ kind: "error";
257
+ code: number;
258
+ message: string;
259
+ data?: unknown;
260
+ };
261
+ /**
262
+ * Decides which era a request belongs to, and whether its version is acceptable.
263
+ *
264
+ * Once a connection commits to an era it stays committed. A legacy client that suddenly sends
265
+ * modern `_meta` is confused rather than clever, and silently switching would answer half a
266
+ * session in each shape.
267
+ */
268
+ declare function classifyRequest(method: string, params: JsonObject | undefined, session: SessionState): EraDecision;
269
+ interface LegacyNegotiation {
270
+ version: string;
271
+ /** False when the client asked for something we cannot speak and was negotiated down. */
272
+ matched: boolean;
273
+ }
274
+ /**
275
+ * Legacy version negotiation: echo a supported request, otherwise answer with our best.
276
+ *
277
+ * A client that asked for a modern revision here gets the newest *legacy* revision, not an error:
278
+ * under this lifecycle the server's answer is the offer, and the client decides.
279
+ */
280
+ declare function negotiateLegacyVersion(requested: unknown): LegacyNegotiation;
281
+
282
282
  interface McpServerOptions {
283
283
  info: McpServerInfo;
284
284
  instructions: string;
package/dist/mcp.js CHANGED
@@ -1,2 +1,2 @@
1
- export { ERROR_CODES, LEGACY_PROTOCOL_VERSIONS, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, findSection, inspectModernMeta, negotiateLegacyVersion, parsePartSections, runStdioServer, validateArguments, windowText } from './chunk-5OR3WVPW.js';
2
- export { generateInWorker } from './chunk-3H45YMBG.js';
1
+ export { ERROR_CODES, LEGACY_PROTOCOL_VERSIONS, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, findSection, inspectModernMeta, negotiateLegacyVersion, parsePartSections, runStdioServer, validateArguments, windowText } from './chunk-J4W74N4Y.js';
2
+ export { generateInWorker } from './chunk-DUZK5VL2.js';
@@ -568,9 +568,24 @@ interface CliOptions {
568
568
  includeSensitiveFiles?: boolean;
569
569
  maxMonolithBytes?: number | null;
570
570
  maxPartBytes?: number;
571
+ maxPartTokens?: ByteSize | null;
571
572
  largeFileBytes?: number;
572
573
  /** Artifact names to suppress for this invocation (`--skip-artifacts`). */
573
574
  skipArtifacts?: string[];
575
+ /**
576
+ * Artifact names to keep, with everything else off (`--only-artifacts`).
577
+ *
578
+ * Separate from the derived `skipArtifacts` because the two are not interchangeable: "only
579
+ * these" is a statement about the whole selection, and the suppression list cannot express it
580
+ * when the configuration has already turned one of the named artifacts off.
581
+ */
582
+ onlyArtifacts?: string[];
583
+ discovery?: DiscoveryMode;
584
+ changedSince?: string;
585
+ staged?: boolean;
586
+ withDependents?: boolean;
587
+ skeleton?: boolean;
588
+ sarif?: boolean;
574
589
  }
575
590
  interface ValidationDrift {
576
591
  missingInSnapshot: string[];
@@ -568,9 +568,24 @@ interface CliOptions {
568
568
  includeSensitiveFiles?: boolean;
569
569
  maxMonolithBytes?: number | null;
570
570
  maxPartBytes?: number;
571
+ maxPartTokens?: ByteSize | null;
571
572
  largeFileBytes?: number;
572
573
  /** Artifact names to suppress for this invocation (`--skip-artifacts`). */
573
574
  skipArtifacts?: string[];
575
+ /**
576
+ * Artifact names to keep, with everything else off (`--only-artifacts`).
577
+ *
578
+ * Separate from the derived `skipArtifacts` because the two are not interchangeable: "only
579
+ * these" is a statement about the whole selection, and the suppression list cannot express it
580
+ * when the configuration has already turned one of the named artifacts off.
581
+ */
582
+ onlyArtifacts?: string[];
583
+ discovery?: DiscoveryMode;
584
+ changedSince?: string;
585
+ staged?: boolean;
586
+ withDependents?: boolean;
587
+ skeleton?: boolean;
588
+ sarif?: boolean;
574
589
  }
575
590
  interface ValidationDrift {
576
591
  missingInSnapshot: string[];
package/docs/cli.md CHANGED
@@ -174,7 +174,7 @@ The whole first run in one command, with defaults and no prompts. This is the sa
174
174
  🚀 Setup for: /path/to/repo
175
175
  ✅ Created snapshot.config.ts (preset: nextjs)
176
176
  Added npm scripts: snapshot, snapshot:validate
177
- Added devDependency: codebase-snapshot ^0.9.12
177
+ Added devDependency: codebase-snapshot ^0.9.14
178
178
  Added to .gitignore: snapshot/, snapshot.tmp/
179
179
  ⏳ Installing dependencies…
180
180
  ✅ Installed dependencies (npm install, 2.1s)
@@ -192,7 +192,9 @@ The whole first run in one command, with defaults and no prompts. This is the sa
192
192
  `generate` and `init`.
193
193
  - `--format json` emits **one** document:
194
194
  `{ "setup": { configPath, configReused, preset, scripts, dependency, gitignoreEntries, install, notes }, "manifest": { … } }`,
195
- where `manifest` is the same object `generate --format json` writes.
195
+ where `manifest` is the same object `generate --format json` writes. If that generate fails, the
196
+ document is `{ "error": { … }, "setup": { … }, "manifest": null }` and the exit code is the
197
+ generate's: one document per invocation, a payload or an error, never a payload holding an error.
196
198
  - If `outDir` holds files this tool did not write, `setup` still refuses to replace it (exit 1,
197
199
  naming the entries) after writing the config — the guard is not weakened by the convenience.
198
200
  ### `init`
@@ -211,7 +213,7 @@ It also has three side effects on files you own, and it says so when it does the
211
213
  ```
212
214
  ✅ Created snapshot.config.ts (preset: node-lib)
213
215
  Added npm scripts: snapshot, snapshot:validate
214
- Added devDependency: codebase-snapshot ^0.9.12
216
+ Added devDependency: codebase-snapshot ^0.9.14
215
217
  Added to .gitignore: snapshot/, snapshot.tmp/
216
218
  ```
217
219
 
@@ -407,16 +409,16 @@ narrowest cases of the same rule.
407
409
  | `--include-monolith` | flag | off | generate, validate, report, explain | Also write `full-codebase.json` and `full-codebase.md`. |
408
410
  | `--include-sensitive-files` | flag | off | generate, validate, report, explain, init | Emit `.env`, `*.pem`, `.npmrc` and similar. Off by default; they are reported as skipped. |
409
411
  | `--skip-artifacts <names>` | list | unset | generate, validate, report, explain, init | Comma-separated artifacts to omit. |
410
- | `--only-artifacts <names>` | list | unset | generate, validate, report, explain, init | Write only these. Cannot be combined with `--skip-artifacts`. |
412
+ | `--only-artifacts <names>` | list | unset | generate, validate, report, explain, init | Write *only* these, and **turn on** any this names: a config with `artifacts: { audit: false }` still gets `reports/audit.md` from `--only-artifacts audit`, because "only these" is a statement about the whole selection rather than a suppression list. Cannot be combined with `--skip-artifacts`. |
411
413
  | `--format <text\|json>` | enum | `text` | generate, validate, report, explain, init, setup, diff, describe, skill | `json` writes a pure JSON payload to stdout. Every command honours it, and `describe` reports the same nine-command list. |
412
414
  | `--sarif` | flag | off | generate, validate, setup | Also write `reports/audit.sarif`: the redaction findings as SARIF 2.1.0, for upload to GitHub Code Scanning. The artefact carries the path, the line and the rule — never the value. Works with `--skip-artifacts audit`, because a CI gate does not read markdown. Rule ids are fixed per rule name, so adding a custom rule never re-points an existing alert. |
413
- | `--skeleton` | flag | off | generate, validate, setup | Emit TypeScript and TSX signatures instead of bodies: each function or method body becomes `{ /* ... */ }` in place, so formatting and comments around it are untouched. The summary still describes the whole file, and affected entries carry `inclusionMode: "signatures"`. `--include-monolith` still writes the whole body, so the monolith's `inclusionMode: "full"` stays true. |
415
+ | `--skeleton` | flag | off | generate, validate, setup | Emit signatures instead of bodies: each function or method body becomes `{ /* ... */ }` in place, so formatting and comments around it are untouched. Covers `.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs` and `.cjs`; `.d.ts` files are left alone because they are already declarations. The summary still describes the whole file, and affected entries carry `inclusionMode: "signatures"`. `--include-monolith` still writes the whole body, so the monolith's `inclusionMode: "full"` stays true. |
414
416
  | `--changed-since <ref>` | git ref | unset | generate, validate, explain | Include only files changed since this ref, plus untracked ones. Every other file is recorded in `manifest.skipped[]` with reason `unchanged` and is never opened. `validate` re-resolves the scope: a skipped file that has changed since is reported as drift, and one that has not is left alone — and with `--with-dependents`, so is a file that now imports one that changed. `explain` answers with the same rule. A ref beginning with `-` is refused. |
415
417
  | `--staged` | flag | off | generate, validate, explain | Include only files staged for the next commit. |
416
418
  | `--with-dependents` | flag | off | generate, validate, explain | With either of the two above, also include the files that import a changed one — a one-hop closure, not a transitive walk. Needs a scope; alone it is a usage error. |
417
419
  | `--install-hook` | flag | off | init, setup | Write `.git/hooks/pre-commit` so a commit is refused while the snapshot is out of date. The hook checks the `outDir` the config names, so it works for a project that does not use `snapshot/`. An existing hook is never replaced; the note says what to add to it instead. |
418
420
  | `--dry-run` | flag | off | generate, init, setup | Compute everything, print it, write nothing. |
419
- | `--watch` | flag | off | generate | Regenerate on every change until interrupted. |
421
+ | `--watch` | flag | off | generate | Regenerate on every change until interrupted. Redaction and parsing are cached per file across runs in the same process, keyed on the content hash, so a one-file edit regenerates from that one file instead of from the whole repository. The cache is memory only and is dropped when the process exits. |
420
422
  | `--autopilot` | flag | off | generate | Set the project up without asking — config, `.gitignore`, npm scripts, devDependency, install — then generate. Never overwrites an existing config; refuses an empty directory (exit `1`); contradicts `--no-config` (exit `2`). |
421
423
  | `--no-install` | flag | off | setup, generate `--autopilot` | Skip the dependency install. Anywhere else it is a usage error, because it would change nothing. |
422
424
  | `--no-config` | flag | off | generate, validate, report, explain, diff | Ignore every `snapshot.config.*` file and run on the built-in defaults. `diff` never loads a config anyway and accepts the flag for consistency. |
@@ -192,7 +192,7 @@ export default {
192
192
  // changedSince: "main", // only files changed since this git ref
193
193
  // staged: true, // only files staged for the next commit
194
194
  // withDependents: true, // ...and the files that import them
195
- // skeleton: true, // TypeScript signatures instead of bodies (--skeleton)
195
+ // skeleton: true, // signatures instead of bodies (--skeleton); TS, JS, TSX and JSX
196
196
  largeFileBytes: "200k", // above this a file becomes a head/tail preview
197
197
  previewHeadLines: 80,
198
198
  previewTailLines: 40,
@@ -229,8 +229,9 @@ export default {
229
229
  ```
230
230
 
231
231
  Everything except `monolith` is on by default; `monolith` follows `includeMonolith`. Turn
232
- artifacts off with the config, `--skip-artifacts a,b`, or `--only-artifacts a,b` (the two
233
- cannot be combined).
232
+ artifacts off with the config or `--skip-artifacts a,b`. `--only-artifacts a,b` is different: it
233
+ *writes only* those and turns each named artifact **on**, so `--only-artifacts audit` still produces
234
+ `reports/audit.md` in a project whose config says `audit: false`. The two flags cannot be combined.
234
235
 
235
236
  Two combinations warn because they produce a snapshot that contradicts itself: `manifest` off
236
237
  means `validate` and `report` cannot inspect the snapshot, and `parts` off with `navigation`
@@ -266,19 +267,52 @@ makes an audit entry mean something.
266
267
  matched span is masked, never the whole line, and no excerpt of a matched line reaches any
267
268
  artifact. Multi-line PEM keys are masked as a block.
268
269
  - `customPatterns` add rules; `disableDefaultPatterns` removes the built-in set — it does not
269
- disable redaction itself.
270
+ disable redaction itself, and it applies to **paths** as well as to content, so a
271
+ credential-shaped file name is no longer skipped by a rule you switched off.
270
272
 
271
273
  Redaction is **fail-closed**, so a placeholder is indistinguishable from a secret: a
272
274
  `.env.example` whose line reads `JWT_SECRET=replace-with-base64url-48-bytes` is masked like a
273
275
  live key would be, and a test fixture literal such as `process.env.X_SECRET = "test-value"` is
274
- masked too. The same holds for an environment reference written as a quoted value —
275
- `password: "${DB_PASSWORD}"` is masked exactly like a literal password, because the rule sees a
276
- credential-shaped key followed by a string and a `${…}` reference cannot be told apart from a
277
- secret by shape alone. That is the intended trade-off — the alternative is guessing which values
278
- are throwaway — and it is worth knowing before you hand the snapshot to a model and find the
279
- template less readable than the original. To narrow it, remove a rule by name with
276
+ masked too. That is the intended trade-off — the alternative is guessing which values are
277
+ throwaway — and it is worth knowing before you hand the snapshot to a model and find the template
278
+ less readable than the original. To narrow it, remove a rule by name with
280
279
  `disableDefaultPatterns: true` plus your own `customPatterns`.
281
280
 
281
+ The one shape that is *not* masked is a value that is a single `${…}` reference and nothing else —
282
+ `password: "${DB_PASSWORD}"`, `` `${process.env.TOKEN}` `` — because that is an expression and
283
+ masking it rewrites a program (or an assertion message) rather than hiding a credential. Anything
284
+ inside the braces that is not a plain reference settles it the other way:
285
+ `password: "${DB_PASSWORD:-hunter2}"` is masked, because the shell and Compose idiom for a default
286
+ puts a **hardcoded credential** in the fallback position. So does a second interpolation, or any
287
+ quote inside the braces.
288
+
289
+ The same default is masked when it is not quoted, which is how it is usually written:
290
+
291
+ ```bash
292
+ # .env, docker-compose.yml, a workflow, a shell script — all of these are masked
293
+ POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-MyProdPassword123!}
294
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-MyProdPassword123!}
295
+ ```
296
+
297
+ The fallback is what gets replaced; the variable name stays, exactly as `POSTGRES_PASSWORD:` does.
298
+ `${PORT:-3000}` is untouched: the name is not a credential name, and a port number is not one
299
+ either.
300
+
301
+ A credential assigned to a name inside a string is also masked — a connection string is a string:
302
+
303
+ ```js
304
+ const dsn = "password=MySuperSecret123;host=db.internal";
305
+ ```
306
+
307
+ What is left alone is a credential *name* in a literal, with nothing assigned to it:
308
+ `["awsSecret:", "apiKey:"]` is a list of keys, not two credentials, and a documentation line showing
309
+ `password: "…"` inside a code span is showing an example. The difference is whether the literal
310
+ closes between the name and the value.
311
+
312
+ `readonly` and `declare` are shell assignments in a shell file and declarations in a source file, and
313
+ the file decides: `readonly api_key=hunter2` in `.envrc`, `.sh`, `.zsh`, a YAML or an INI file is
314
+ masked, while `private readonly apiKey = someValue` in TypeScript is left alone.
315
+
282
316
  Findings are summarised in `reports/audit.md` (one row per line, naming every rule that matched
283
317
  it), counted in `manifest.totals.redactedLines`, and shown in the `generate` summary as a
284
318
  `Redacted:` line separate from `Warnings:`.
package/docs/mcp.md CHANGED
@@ -10,7 +10,7 @@ walk — that the client sees.
10
10
  ## Start it
11
11
 
12
12
  ```bash
13
- npx -y codebase-snapshot@0.9.12 mcp
13
+ npx -y codebase-snapshot@0.9.14 mcp
14
14
  ```
15
15
 
16
16
  The server serves the directory it was started in. `--cwd <path>` points it elsewhere, and that
@@ -25,7 +25,7 @@ Claude Desktop and Cursor:
25
25
  "mcpServers": {
26
26
  "codebase-snapshot": {
27
27
  "command": "npx",
28
- "args": ["-y", "codebase-snapshot@0.9.12", "mcp", "--cwd", "/absolute/path/to/repo"]
28
+ "args": ["-y", "codebase-snapshot@0.9.14", "mcp", "--cwd", "/absolute/path/to/repo"]
29
29
  }
30
30
  }
31
31
  }
@@ -34,7 +34,7 @@ Claude Desktop and Cursor:
34
34
  Claude Code registers the same thing from a shell:
35
35
 
36
36
  ```bash
37
- claude mcp add codebase-snapshot -- npx -y codebase-snapshot@0.9.12 mcp --cwd /absolute/path/to/repo
37
+ claude mcp add codebase-snapshot -- npx -y codebase-snapshot@0.9.14 mcp --cwd /absolute/path/to/repo
38
38
  ```
39
39
 
40
40
  On Windows, wrap the command as `cmd /c npx` with the same arguments — `npx` is a `.cmd` shim
@@ -61,6 +61,19 @@ still reachable: pass the same value to the read side that you passed to `snapsh
61
61
  that, only `snapshot_generate` honoured it and every other tool looked in the configured directory
62
62
  and reported "there is no snapshot manifest here".
63
63
 
64
+ `snapshot_plan`, `snapshot_generate`, `snapshot_validate` and `snapshot_explain` also take the
65
+ **scope and compression** switches `generate` accepts: `maxPartTokens`, `skeleton`, `sarif`,
66
+ `changedSince`, `staged` and `withDependents`. They have to: `manifest.config.digest` covers those
67
+ settings, so a snapshot generated with `--skeleton` was not merely un-inspectable over MCP, it was
68
+ reported as configuration drift on every `snapshot_validate`. Pass the same switches you generated
69
+ with, exactly as you would on the command line. The four remaining tools do not take them, because
70
+ none of them re-derives the configuration and a flag that changes nothing is the command-line form
71
+ of a silent fallback.
72
+
73
+ `includeSensitiveFiles` is deliberately absent from every one of them. A prompt can ask for
74
+ anything, and a tool that emits `.env` on request is a secret-exfiltration primitive with a
75
+ friendly name.
76
+
64
77
  `snapshot_locate` is the one that makes the others usable: a twelve-part snapshot is hundreds of
65
78
  kilobytes, and asking "which part holds authentication?" is cheaper than reading all twelve. It
66
79
  searches the manifest's index — file paths, exported symbols, component names, routes, layers and
package/docs/output.md CHANGED
@@ -31,6 +31,12 @@ fields in `manifest.json` for the same file. `manifest.json` reports what the pa
31
31
  (CRLF normalised, the trailing newline not counted); the monolith reports the content it
32
32
  embedded. Compare a file's fields within one artifact, not across the two.
33
33
 
34
+ "CRLF normalised" covers all three line endings a file can have. A CR-only file — an old Mac file,
35
+ or anything written by a tool that predates LF on that platform — is written to a part with LF too,
36
+ and its `lines` count is the real one: the preview budget and the line counts used to split on `\n`
37
+ alone, so such a file was one line, `largeFileBytes` never applied, and the whole file went into a
38
+ part with its carriage returns intact.
39
+
34
40
  The manifest embedded in `full-codebase.json` is otherwise identical to `manifest.json` with one
35
41
  impossible field: a file cannot record its own sha256, so `artifacts.fullJson` is `null` in the
36
42
  embedded copy and a `selfReference` sentence points at `manifest.json`, which carries the real
@@ -202,6 +208,13 @@ has several at once, and "within budget" cannot be answered without knowing whic
202
208
  `variables`, `reactComponents`, `hooks`, `routeHandlers`), plus `parseErrors` when the parser
203
209
  reported syntax errors. JSON gets `{ shape, itemCount, keys, sampleKeys }` and Markdown gets
204
210
  `{ headings, frontmatterKeys }` — a shape description rather than a code summary.
211
+ `imports` covers `import` declarations, named and star re-exports, `require("…")`, `import("…")`
212
+ and `import x = require("…")`; a computed specifier is an expression and is not recorded, because
213
+ a wrong edge in the import graph is worse than a missing one. The JSON reader follows JSONC —
214
+ comments, trailing commas and a BOM — and reports `invalid-json` only for a file that still does
215
+ not parse. `headings` skips headings inside fenced code and inside YAML front matter, and a
216
+ document that *opens* with a `---` horizontal rule keeps its headings: the block has to look like
217
+ YAML before it counts as front matter.
205
218
  - `summaryOmitted: true` means the file's summary was dropped because the summary alone would
206
219
  not fit the part budget. The entry then has `summaryQuality` but no `summary`, and the
207
220
  file's body is still emitted in full. It is only ever present when `true`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebase-snapshot",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
4
4
  "description": "Pack a codebase for an LLM into budgeted Markdown parts, with a manifest of every file and skip, and validate to prove the snapshot is current.",
5
5
  "keywords": [
6
6
  "codebase",