codebase-snapshot 0.9.9 → 0.9.12

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,3 +1,4 @@
1
+ export { G as GenerateSummary, W as WorkerResults, g as generateInWorker } from './generate-worker-D5Xgh27c.cjs';
1
2
  import { Readable, Writable } from 'node:stream';
2
3
 
3
4
  /**
@@ -187,7 +188,7 @@ interface ToolContext {
187
188
  * Client-facing hints.
188
189
  *
189
190
  * `readOnlyHint` is the one that changes behaviour rather than appearance: clients use it to
190
- * auto-approve a call. Six of the seven tools never touch the filesystem, so they are marked
191
+ * auto-approve a call. Seven of the eight tools never touch the filesystem, so they are marked
191
192
  * read-only and only `snapshot_generate` prompts.
192
193
  */
193
194
  interface ToolAnnotations {
@@ -297,6 +298,73 @@ declare function createMcpServer(options: McpServerOptions): McpServer;
297
298
  /** Every tool this server exposes, in the order a first-time caller should read them. */
298
299
  declare function buildTools(): McpTool[];
299
300
 
301
+ /**
302
+ * Reads one file's section back out of a rendered part.
303
+ *
304
+ * `snapshot_read` must not open the working tree. The snapshot is the contract: sensitive files
305
+ * are excluded from it, secret-shaped strings inside ordinary files are redacted on the way in,
306
+ * and a large file is emitted as a bounded preview. Reading the source directly would hand the
307
+ * model the `.env` the snapshot deliberately skipped and the credential it masked — so the read
308
+ * goes through the artifact instead.
309
+ *
310
+ * The format it parses is written by `renderFileSection`, and the parser is deliberately tied to
311
+ * that shape rather than to a regex over the whole document: a file's *content* may legitimately
312
+ * contain a line that looks like `### FILE: something`, and only fence tracking tells the two
313
+ * apart. Content fences are four or more backticks (`fenceForContent` never emits three), which
314
+ * is what makes "the next line equal to this exact fence" an unambiguous terminator.
315
+ */
316
+ interface PartSection {
317
+ /** The path as rendered in the `### FILE:` heading. */
318
+ path: string;
319
+ /** The `<a id="…">` anchor from the line above the heading, when present. */
320
+ anchor: string | null;
321
+ /** The file body, without its fence. Redacted and possibly a bounded preview. */
322
+ body: string;
323
+ /** `full`, `preview` or `signatures`. */
324
+ inclusion: string | null;
325
+ includedLines: number | null;
326
+ }
327
+ /** Splits a part into one section per emitted file. */
328
+ declare function parsePartSections(partText: string): PartSection[];
329
+ /**
330
+ * Finds the section for one file.
331
+ *
332
+ * The **anchor** is the identifier, not the heading. `anchorForPath` hashes the real
333
+ * `relativePath`, so it is exact and injective; the heading is the display form, and for a path
334
+ * with a control character (escaped as `\uXXXX`) or one longer than 300 characters (capped from
335
+ * the middle) it is not the path at all. The old fallback for those cases was the content
336
+ * SHA-256, which the part no longer carries — and which could never disambiguate two files with
337
+ * the same content, the exact case that needs it.
338
+ *
339
+ * The heading comparison is kept as a second chance, for a snapshot written before the anchor
340
+ * was read, or a part whose anchor line was edited by hand.
341
+ */
342
+ declare function findSection(sections: readonly PartSection[], relativePath: string): PartSection | null;
343
+ interface Window {
344
+ text: string;
345
+ fromLine: number;
346
+ returnedLines: number;
347
+ totalLines: number;
348
+ truncated: boolean;
349
+ nextFromLine: number | null;
350
+ /**
351
+ * True when `fromLine` is beyond the last line of the body.
352
+ *
353
+ * The start used to be clamped to the last line, so asking for line 9,000 of a 40-line file
354
+ * returned line 40 with a header that read `Lines 40–40 of 40` — a wrong answer dressed as a
355
+ * right one. The caller is told the request was out of range instead.
356
+ */
357
+ pastEnd: boolean;
358
+ }
359
+ /**
360
+ * Cuts a line window out of a body.
361
+ *
362
+ * Windows are the whole point of this module: a part holds hundreds of kilobytes, and the only
363
+ * safe default for a tool that returns content is "as little as answers the question". `next` is
364
+ * an explicit pointer rather than an instruction to keep reading, so an agent decides.
365
+ */
366
+ declare function windowText(body: string, fromLine: number, lineCount: number): Window;
367
+
300
368
  declare function buildPrompts(): McpPrompt[];
301
369
 
302
370
  declare function buildResourceProvider(): ResourceProvider;
@@ -409,4 +477,4 @@ declare function validateArguments(schema: InputSchema, value: unknown): string[
409
477
  */
410
478
  declare function coerceNumericStrings(schema: InputSchema, value: JsonObject): JsonObject;
411
479
 
412
- export { ERROR_CODES, type JsonRpcResponse, type JsonSchema, LEGACY_PROTOCOL_VERSIONS, type LineReaderOptions, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, type McpPrompt, type McpResourceContents, type McpResourceDescriptor, type McpResourceTemplate, type McpServer, type McpServerInfo, type McpServerOptions, type McpTool, type MessageWriter, type ProtocolEra, type ResourceProvider, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, type ServerOptions, type SessionState, type ToolAnnotations, type ToolContext, type ToolOutcome, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, inspectModernMeta, negotiateLegacyVersion, runStdioServer, validateArguments };
480
+ export { ERROR_CODES, type JsonRpcResponse, type JsonSchema, LEGACY_PROTOCOL_VERSIONS, type LineReaderOptions, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, type McpPrompt, type McpResourceContents, type McpResourceDescriptor, type McpResourceTemplate, type McpServer, type McpServerInfo, type McpServerOptions, type McpTool, type MessageWriter, type PartSection, type ProtocolEra, type ResourceProvider, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, type ServerOptions, type SessionState, type ToolAnnotations, type ToolContext, type ToolOutcome, type Window, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, findSection, inspectModernMeta, negotiateLegacyVersion, parsePartSections, runStdioServer, validateArguments, windowText };
package/dist/mcp.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { G as GenerateSummary, W as WorkerResults, g as generateInWorker } from './generate-worker-D5Xgh27c.js';
1
2
  import { Readable, Writable } from 'node:stream';
2
3
 
3
4
  /**
@@ -187,7 +188,7 @@ interface ToolContext {
187
188
  * Client-facing hints.
188
189
  *
189
190
  * `readOnlyHint` is the one that changes behaviour rather than appearance: clients use it to
190
- * auto-approve a call. Six of the seven tools never touch the filesystem, so they are marked
191
+ * auto-approve a call. Seven of the eight tools never touch the filesystem, so they are marked
191
192
  * read-only and only `snapshot_generate` prompts.
192
193
  */
193
194
  interface ToolAnnotations {
@@ -297,6 +298,73 @@ declare function createMcpServer(options: McpServerOptions): McpServer;
297
298
  /** Every tool this server exposes, in the order a first-time caller should read them. */
298
299
  declare function buildTools(): McpTool[];
299
300
 
301
+ /**
302
+ * Reads one file's section back out of a rendered part.
303
+ *
304
+ * `snapshot_read` must not open the working tree. The snapshot is the contract: sensitive files
305
+ * are excluded from it, secret-shaped strings inside ordinary files are redacted on the way in,
306
+ * and a large file is emitted as a bounded preview. Reading the source directly would hand the
307
+ * model the `.env` the snapshot deliberately skipped and the credential it masked — so the read
308
+ * goes through the artifact instead.
309
+ *
310
+ * The format it parses is written by `renderFileSection`, and the parser is deliberately tied to
311
+ * that shape rather than to a regex over the whole document: a file's *content* may legitimately
312
+ * contain a line that looks like `### FILE: something`, and only fence tracking tells the two
313
+ * apart. Content fences are four or more backticks (`fenceForContent` never emits three), which
314
+ * is what makes "the next line equal to this exact fence" an unambiguous terminator.
315
+ */
316
+ interface PartSection {
317
+ /** The path as rendered in the `### FILE:` heading. */
318
+ path: string;
319
+ /** The `<a id="…">` anchor from the line above the heading, when present. */
320
+ anchor: string | null;
321
+ /** The file body, without its fence. Redacted and possibly a bounded preview. */
322
+ body: string;
323
+ /** `full`, `preview` or `signatures`. */
324
+ inclusion: string | null;
325
+ includedLines: number | null;
326
+ }
327
+ /** Splits a part into one section per emitted file. */
328
+ declare function parsePartSections(partText: string): PartSection[];
329
+ /**
330
+ * Finds the section for one file.
331
+ *
332
+ * The **anchor** is the identifier, not the heading. `anchorForPath` hashes the real
333
+ * `relativePath`, so it is exact and injective; the heading is the display form, and for a path
334
+ * with a control character (escaped as `\uXXXX`) or one longer than 300 characters (capped from
335
+ * the middle) it is not the path at all. The old fallback for those cases was the content
336
+ * SHA-256, which the part no longer carries — and which could never disambiguate two files with
337
+ * the same content, the exact case that needs it.
338
+ *
339
+ * The heading comparison is kept as a second chance, for a snapshot written before the anchor
340
+ * was read, or a part whose anchor line was edited by hand.
341
+ */
342
+ declare function findSection(sections: readonly PartSection[], relativePath: string): PartSection | null;
343
+ interface Window {
344
+ text: string;
345
+ fromLine: number;
346
+ returnedLines: number;
347
+ totalLines: number;
348
+ truncated: boolean;
349
+ nextFromLine: number | null;
350
+ /**
351
+ * True when `fromLine` is beyond the last line of the body.
352
+ *
353
+ * The start used to be clamped to the last line, so asking for line 9,000 of a 40-line file
354
+ * returned line 40 with a header that read `Lines 40–40 of 40` — a wrong answer dressed as a
355
+ * right one. The caller is told the request was out of range instead.
356
+ */
357
+ pastEnd: boolean;
358
+ }
359
+ /**
360
+ * Cuts a line window out of a body.
361
+ *
362
+ * Windows are the whole point of this module: a part holds hundreds of kilobytes, and the only
363
+ * safe default for a tool that returns content is "as little as answers the question". `next` is
364
+ * an explicit pointer rather than an instruction to keep reading, so an agent decides.
365
+ */
366
+ declare function windowText(body: string, fromLine: number, lineCount: number): Window;
367
+
300
368
  declare function buildPrompts(): McpPrompt[];
301
369
 
302
370
  declare function buildResourceProvider(): ResourceProvider;
@@ -409,4 +477,4 @@ declare function validateArguments(schema: InputSchema, value: unknown): string[
409
477
  */
410
478
  declare function coerceNumericStrings(schema: InputSchema, value: JsonObject): JsonObject;
411
479
 
412
- export { ERROR_CODES, type JsonRpcResponse, type JsonSchema, LEGACY_PROTOCOL_VERSIONS, type LineReaderOptions, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, type McpPrompt, type McpResourceContents, type McpResourceDescriptor, type McpResourceTemplate, type McpServer, type McpServerInfo, type McpServerOptions, type McpTool, type MessageWriter, type ProtocolEra, type ResourceProvider, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, type ServerOptions, type SessionState, type ToolAnnotations, type ToolContext, type ToolOutcome, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, inspectModernMeta, negotiateLegacyVersion, runStdioServer, validateArguments };
480
+ export { ERROR_CODES, type JsonRpcResponse, type JsonSchema, LEGACY_PROTOCOL_VERSIONS, type LineReaderOptions, MAX_LINE_BYTES, METHOD, MODERN_PROTOCOL_VERSIONS, type McpPrompt, type McpResourceContents, type McpResourceDescriptor, type McpResourceTemplate, type McpServer, type McpServerInfo, type McpServerOptions, type McpTool, type MessageWriter, type PartSection, type ProtocolEra, type ResourceProvider, SERVER_INSTRUCTIONS, SUPPORTED_PROTOCOL_VERSIONS, type ServerOptions, type SessionState, type ToolAnnotations, type ToolContext, type ToolOutcome, type Window, buildPrompts, buildResourceProvider, buildTools, classifyRequest, coerceNumericStrings, createLineReader, createMcpServer, createServer, createSession, createWriter, findSection, inspectModernMeta, negotiateLegacyVersion, parsePartSections, runStdioServer, validateArguments, windowText };
package/dist/mcp.js CHANGED
@@ -1,4 +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, inspectModernMeta, negotiateLegacyVersion, runStdioServer, validateArguments } from './chunk-HQBEQ67K.js';
2
- import './chunk-KUVFTJHK.js';
3
- //# sourceMappingURL=mcp.js.map
4
- //# sourceMappingURL=mcp.js.map
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';
@@ -21,7 +21,7 @@ type TextEncoding = "utf-8" | "utf-8-lossy" | "utf-16le" | "utf-16be";
21
21
  * `full` – the complete file body was emitted.
22
22
  * `preview` – the body was truncated (by lines and/or bytes) before emission.
23
23
  */
24
- type InclusionMode = "full" | "preview";
24
+ type InclusionMode = "full" | "preview" | "signatures";
25
25
  interface SourceSummary {
26
26
  directives: string[];
27
27
  imports: string[];
@@ -88,7 +88,15 @@ type SkipReason = "binary" | "read-error" | "not-file"
88
88
  * expected — which used to be recorded as the generic `not-file`, implying the path was a
89
89
  * socket or a FIFO rather than a repository whose contents are simply somebody else's.
90
90
  */
91
- | "nested-repository";
91
+ | "nested-repository"
92
+ /**
93
+ * Not in the set that `--changed-since` or `--staged` asked for.
94
+ *
95
+ * Deliberately distinct from `excluded`, which means the user's own glob rejected the path.
96
+ * Here the file is perfectly includable and simply did not change; reporting it as
97
+ * `excluded` would send a reader looking for an exclude pattern they never wrote.
98
+ */
99
+ | "unchanged";
92
100
  interface SkipRecord {
93
101
  /** Path as it may be shown in an artifact: secret spans are masked. */
94
102
  path: string;
@@ -144,6 +152,23 @@ interface PartMetadata {
144
152
  filename: string;
145
153
  /** Exact UTF-8 byte length of the emitted part file. */
146
154
  byteSize: number;
155
+ /**
156
+ * Estimated tokens in this part.
157
+ *
158
+ * `byteSize` answers "will this fit the byte budget"; this answers the question a caller
159
+ * actually has, which is "will this fit my context window". An estimate, and labelled as
160
+ * one: `estimateTokens` documents exactly what it counts.
161
+ */
162
+ estimatedTokens: number;
163
+ /**
164
+ * The byte budget this part was partitioned against.
165
+ *
166
+ * With per-layer budgets a snapshot has several budgets at once, so "within budget"
167
+ * cannot be answered without knowing which one applied. Verification compared every
168
+ * part against the *largest* budget in the run, which passed a part that was over its
169
+ * own.
170
+ */
171
+ budgetBytes: number;
147
172
  sha256: string;
148
173
  lines: number;
149
174
  fileCount: number;
@@ -198,6 +223,14 @@ interface ManifestTotals {
198
223
  skippedFiles: number;
199
224
  totalSourceBytes: number;
200
225
  totalContentBytes: number;
226
+ /**
227
+ * Estimated tokens in the emitted parts.
228
+ *
229
+ * Optional because a manifest written before this was recorded has no such key. The number
230
+ * is an estimate — see `estimateTokens` for the method — and it exists because the snapshot
231
+ * is pasted into a context window, which is measured in tokens rather than bytes.
232
+ */
233
+ estimatedTokens?: number;
201
234
  partCount: number;
202
235
  warningCount: number;
203
236
  /**
@@ -256,13 +289,34 @@ interface ManifestConfigSnapshot {
256
289
  /** Digest of the effective settings; see `configDigest`. */
257
290
  digest?: string;
258
291
  maxPartBytes: number;
292
+ /**
293
+ * The token ceiling the run was given, or `undefined` when only bytes were named.
294
+ *
295
+ * Recorded so the manifest states the budget the user chose rather than only the byte
296
+ * value it was converted to.
297
+ */
298
+ maxPartTokens?: number;
259
299
  largeFileBytes: number;
260
300
  previewHeadLines: number;
261
301
  previewTailLines: number;
262
302
  includeMonolith: boolean;
303
+ /**
304
+ * The ceiling above which the monolith is not written; `null` for no ceiling.
305
+ *
306
+ * Optional because a manifest written before this was recorded has no such key.
307
+ */
308
+ maxMonolithBytes?: number | null;
263
309
  includeSensitiveFiles: boolean;
264
310
  /** How files were discovered; `undefined` on manifests written before this was recorded. */
265
311
  discovery?: DiscoveryMode;
312
+ /** The changed-files scope the snapshot was taken over, when one was used. */
313
+ changedSince?: string;
314
+ staged?: boolean;
315
+ withDependents?: boolean;
316
+ /** True when bodies were replaced by signatures. */
317
+ skeleton?: boolean;
318
+ /** True when `reports/audit.sarif` was written. */
319
+ sarif?: boolean;
266
320
  artifacts: ResolvedArtifacts;
267
321
  include: string[];
268
322
  preset: string;
@@ -315,6 +369,14 @@ interface Manifest {
315
369
  artifacts: {
316
370
  fullJson: ManifestArtifact | null;
317
371
  fullMarkdown: ManifestArtifact | null;
372
+ /**
373
+ * `reports/audit.sarif`, when `--sarif` asked for it.
374
+ *
375
+ * Optional because most runs do not write one, and a manifest written before this
376
+ * existed has no such key at all — the two are indistinguishable to a reader, which is
377
+ * why the presence of the file is also recorded in `config`.
378
+ */
379
+ sarif?: ManifestArtifact;
318
380
  };
319
381
  layers: Record<string, number>;
320
382
  kinds: Record<string, number>;
@@ -382,6 +444,15 @@ interface SnapshotConfig {
382
444
  tmpDir?: string;
383
445
  /** Raw bytes, or a human-readable size (`"120k"`, `"1.5mb"`). */
384
446
  maxPartBytes?: ByteSize;
447
+ /**
448
+ * A per-part ceiling in *tokens* rather than bytes.
449
+ *
450
+ * The snapshot is pasted into a context window, and a context window is measured in tokens;
451
+ * bytes are what the partitioner writes. Naming a token budget sets `maxPartBytes` to the
452
+ * byte count that corresponds to it, so the byte bound — the thing the partitioner actually
453
+ * enforces — never stops being true. The smaller of the two ceilings wins.
454
+ */
455
+ maxPartTokens?: ByteSize;
385
456
  largeFileBytes?: ByteSize;
386
457
  previewHeadLines?: number;
387
458
  previewTailLines?: number;
@@ -393,6 +464,31 @@ interface SnapshotConfig {
393
464
  include?: string[];
394
465
  /** Force a preset by name instead of auto-detecting one. */
395
466
  preset?: string;
467
+ /**
468
+ * Only include files changed since this git ref.
469
+ *
470
+ * The pull-request case: the diff is small and the repository is not. Unchanged files are
471
+ * recorded in `skipped[]` with reason `unchanged`, so the manifest accounts for them.
472
+ */
473
+ changedSince?: string;
474
+ /** Only include files staged for the next commit. */
475
+ staged?: boolean;
476
+ /**
477
+ * Emit signatures instead of bodies for TypeScript and TSX files.
478
+ *
479
+ * Matches `inclusionMode: "signatures"` on the entries it applies to, so a reader can tell a
480
+ * compressed file from a whole one without diffing it against the source.
481
+ */
482
+ skeleton?: boolean;
483
+ /** Also write `reports/audit.sarif` so a CI security gate can read the findings. */
484
+ sarif?: boolean;
485
+ /**
486
+ * With `changedSince` or `staged`, also include the files that import a changed one.
487
+ *
488
+ * A one-hop closure: the file you changed and the files that reference it are the context
489
+ * needed to change it safely.
490
+ */
491
+ withDependents?: boolean;
396
492
  /** Switch individual snapshot artifacts off. Everything is on by default. */
397
493
  artifacts?: ArtifactSelection;
398
494
  /**
@@ -418,6 +514,8 @@ interface ResolvedSnapshotConfig {
418
514
  outDir: string;
419
515
  tmpDir: string;
420
516
  maxPartBytes: number;
517
+ /** The token ceiling the user asked for, before it was turned into `maxPartBytes`. */
518
+ maxPartTokens?: number;
421
519
  largeFileBytes: number;
422
520
  previewHeadLines: number;
423
521
  previewTailLines: number;
@@ -430,6 +528,21 @@ interface ResolvedSnapshotConfig {
430
528
  * reader treats `undefined` as `auto`, so the effective mode is never guessed twice.
431
529
  */
432
530
  discovery?: DiscoveryMode;
531
+ /** Only files changed since this git ref; `undefined` scans everything. */
532
+ changedSince?: string;
533
+ /** Only files staged for the next commit. */
534
+ staged?: boolean;
535
+ /** Emit signatures instead of bodies for TypeScript and TSX files. */
536
+ skeleton?: boolean;
537
+ /**
538
+ * Also write `reports/audit.sarif`, the findings in SARIF 2.1.0.
539
+ *
540
+ * For a CI security gate: GitHub Code Scanning reads SARIF, and a markdown report it cannot
541
+ * ingest is a finding nobody's build can act on.
542
+ */
543
+ sarif?: boolean;
544
+ /** Also include the files that import a changed one. */
545
+ withDependents?: boolean;
433
546
  /** Empty means "no allow-list"; every discovered file is a candidate. */
434
547
  include: string[];
435
548
  /** The preset that was resolved (detected or forced). */
@@ -21,7 +21,7 @@ type TextEncoding = "utf-8" | "utf-8-lossy" | "utf-16le" | "utf-16be";
21
21
  * `full` – the complete file body was emitted.
22
22
  * `preview` – the body was truncated (by lines and/or bytes) before emission.
23
23
  */
24
- type InclusionMode = "full" | "preview";
24
+ type InclusionMode = "full" | "preview" | "signatures";
25
25
  interface SourceSummary {
26
26
  directives: string[];
27
27
  imports: string[];
@@ -88,7 +88,15 @@ type SkipReason = "binary" | "read-error" | "not-file"
88
88
  * expected — which used to be recorded as the generic `not-file`, implying the path was a
89
89
  * socket or a FIFO rather than a repository whose contents are simply somebody else's.
90
90
  */
91
- | "nested-repository";
91
+ | "nested-repository"
92
+ /**
93
+ * Not in the set that `--changed-since` or `--staged` asked for.
94
+ *
95
+ * Deliberately distinct from `excluded`, which means the user's own glob rejected the path.
96
+ * Here the file is perfectly includable and simply did not change; reporting it as
97
+ * `excluded` would send a reader looking for an exclude pattern they never wrote.
98
+ */
99
+ | "unchanged";
92
100
  interface SkipRecord {
93
101
  /** Path as it may be shown in an artifact: secret spans are masked. */
94
102
  path: string;
@@ -144,6 +152,23 @@ interface PartMetadata {
144
152
  filename: string;
145
153
  /** Exact UTF-8 byte length of the emitted part file. */
146
154
  byteSize: number;
155
+ /**
156
+ * Estimated tokens in this part.
157
+ *
158
+ * `byteSize` answers "will this fit the byte budget"; this answers the question a caller
159
+ * actually has, which is "will this fit my context window". An estimate, and labelled as
160
+ * one: `estimateTokens` documents exactly what it counts.
161
+ */
162
+ estimatedTokens: number;
163
+ /**
164
+ * The byte budget this part was partitioned against.
165
+ *
166
+ * With per-layer budgets a snapshot has several budgets at once, so "within budget"
167
+ * cannot be answered without knowing which one applied. Verification compared every
168
+ * part against the *largest* budget in the run, which passed a part that was over its
169
+ * own.
170
+ */
171
+ budgetBytes: number;
147
172
  sha256: string;
148
173
  lines: number;
149
174
  fileCount: number;
@@ -198,6 +223,14 @@ interface ManifestTotals {
198
223
  skippedFiles: number;
199
224
  totalSourceBytes: number;
200
225
  totalContentBytes: number;
226
+ /**
227
+ * Estimated tokens in the emitted parts.
228
+ *
229
+ * Optional because a manifest written before this was recorded has no such key. The number
230
+ * is an estimate — see `estimateTokens` for the method — and it exists because the snapshot
231
+ * is pasted into a context window, which is measured in tokens rather than bytes.
232
+ */
233
+ estimatedTokens?: number;
201
234
  partCount: number;
202
235
  warningCount: number;
203
236
  /**
@@ -256,13 +289,34 @@ interface ManifestConfigSnapshot {
256
289
  /** Digest of the effective settings; see `configDigest`. */
257
290
  digest?: string;
258
291
  maxPartBytes: number;
292
+ /**
293
+ * The token ceiling the run was given, or `undefined` when only bytes were named.
294
+ *
295
+ * Recorded so the manifest states the budget the user chose rather than only the byte
296
+ * value it was converted to.
297
+ */
298
+ maxPartTokens?: number;
259
299
  largeFileBytes: number;
260
300
  previewHeadLines: number;
261
301
  previewTailLines: number;
262
302
  includeMonolith: boolean;
303
+ /**
304
+ * The ceiling above which the monolith is not written; `null` for no ceiling.
305
+ *
306
+ * Optional because a manifest written before this was recorded has no such key.
307
+ */
308
+ maxMonolithBytes?: number | null;
263
309
  includeSensitiveFiles: boolean;
264
310
  /** How files were discovered; `undefined` on manifests written before this was recorded. */
265
311
  discovery?: DiscoveryMode;
312
+ /** The changed-files scope the snapshot was taken over, when one was used. */
313
+ changedSince?: string;
314
+ staged?: boolean;
315
+ withDependents?: boolean;
316
+ /** True when bodies were replaced by signatures. */
317
+ skeleton?: boolean;
318
+ /** True when `reports/audit.sarif` was written. */
319
+ sarif?: boolean;
266
320
  artifacts: ResolvedArtifacts;
267
321
  include: string[];
268
322
  preset: string;
@@ -315,6 +369,14 @@ interface Manifest {
315
369
  artifacts: {
316
370
  fullJson: ManifestArtifact | null;
317
371
  fullMarkdown: ManifestArtifact | null;
372
+ /**
373
+ * `reports/audit.sarif`, when `--sarif` asked for it.
374
+ *
375
+ * Optional because most runs do not write one, and a manifest written before this
376
+ * existed has no such key at all — the two are indistinguishable to a reader, which is
377
+ * why the presence of the file is also recorded in `config`.
378
+ */
379
+ sarif?: ManifestArtifact;
318
380
  };
319
381
  layers: Record<string, number>;
320
382
  kinds: Record<string, number>;
@@ -382,6 +444,15 @@ interface SnapshotConfig {
382
444
  tmpDir?: string;
383
445
  /** Raw bytes, or a human-readable size (`"120k"`, `"1.5mb"`). */
384
446
  maxPartBytes?: ByteSize;
447
+ /**
448
+ * A per-part ceiling in *tokens* rather than bytes.
449
+ *
450
+ * The snapshot is pasted into a context window, and a context window is measured in tokens;
451
+ * bytes are what the partitioner writes. Naming a token budget sets `maxPartBytes` to the
452
+ * byte count that corresponds to it, so the byte bound — the thing the partitioner actually
453
+ * enforces — never stops being true. The smaller of the two ceilings wins.
454
+ */
455
+ maxPartTokens?: ByteSize;
385
456
  largeFileBytes?: ByteSize;
386
457
  previewHeadLines?: number;
387
458
  previewTailLines?: number;
@@ -393,6 +464,31 @@ interface SnapshotConfig {
393
464
  include?: string[];
394
465
  /** Force a preset by name instead of auto-detecting one. */
395
466
  preset?: string;
467
+ /**
468
+ * Only include files changed since this git ref.
469
+ *
470
+ * The pull-request case: the diff is small and the repository is not. Unchanged files are
471
+ * recorded in `skipped[]` with reason `unchanged`, so the manifest accounts for them.
472
+ */
473
+ changedSince?: string;
474
+ /** Only include files staged for the next commit. */
475
+ staged?: boolean;
476
+ /**
477
+ * Emit signatures instead of bodies for TypeScript and TSX files.
478
+ *
479
+ * Matches `inclusionMode: "signatures"` on the entries it applies to, so a reader can tell a
480
+ * compressed file from a whole one without diffing it against the source.
481
+ */
482
+ skeleton?: boolean;
483
+ /** Also write `reports/audit.sarif` so a CI security gate can read the findings. */
484
+ sarif?: boolean;
485
+ /**
486
+ * With `changedSince` or `staged`, also include the files that import a changed one.
487
+ *
488
+ * A one-hop closure: the file you changed and the files that reference it are the context
489
+ * needed to change it safely.
490
+ */
491
+ withDependents?: boolean;
396
492
  /** Switch individual snapshot artifacts off. Everything is on by default. */
397
493
  artifacts?: ArtifactSelection;
398
494
  /**
@@ -418,6 +514,8 @@ interface ResolvedSnapshotConfig {
418
514
  outDir: string;
419
515
  tmpDir: string;
420
516
  maxPartBytes: number;
517
+ /** The token ceiling the user asked for, before it was turned into `maxPartBytes`. */
518
+ maxPartTokens?: number;
421
519
  largeFileBytes: number;
422
520
  previewHeadLines: number;
423
521
  previewTailLines: number;
@@ -430,6 +528,21 @@ interface ResolvedSnapshotConfig {
430
528
  * reader treats `undefined` as `auto`, so the effective mode is never guessed twice.
431
529
  */
432
530
  discovery?: DiscoveryMode;
531
+ /** Only files changed since this git ref; `undefined` scans everything. */
532
+ changedSince?: string;
533
+ /** Only files staged for the next commit. */
534
+ staged?: boolean;
535
+ /** Emit signatures instead of bodies for TypeScript and TSX files. */
536
+ skeleton?: boolean;
537
+ /**
538
+ * Also write `reports/audit.sarif`, the findings in SARIF 2.1.0.
539
+ *
540
+ * For a CI security gate: GitHub Code Scanning reads SARIF, and a markdown report it cannot
541
+ * ingest is a finding nobody's build can act on.
542
+ */
543
+ sarif?: boolean;
544
+ /** Also include the files that import a changed one. */
545
+ withDependents?: boolean;
433
546
  /** Empty means "no allow-list"; every discovered file is a candidate. */
434
547
  include: string[];
435
548
  /** The preset that was resolved (detected or forced). */
package/docs/agent.md CHANGED
@@ -115,7 +115,7 @@ devDependency and the `.gitignore` entries for the snapshot directory, and nothi
115
115
 
116
116
  ## When the client speaks MCP
117
117
 
118
- `codebase-snapshot mcp` serves the repository to a Model Context Protocol client over stdio: seven
118
+ `codebase-snapshot mcp` serves the repository to a Model Context Protocol client over stdio: eight
119
119
  tools, `snapshot://` resources and two prompts. Prefer it over shelling out when the agent already
120
120
  has an MCP connection, for two reasons that are properties of the design rather than of the wiring:
121
121
 
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.9
177
+ Added devDependency: codebase-snapshot ^0.9.12
178
178
  Added to .gitignore: snapshot/, snapshot.tmp/
179
179
  ⏳ Installing dependencies…
180
180
  ✅ Installed dependencies (npm install, 2.1s)
@@ -211,7 +211,7 @@ It also has three side effects on files you own, and it says so when it does the
211
211
  ```
212
212
  ✅ Created snapshot.config.ts (preset: node-lib)
213
213
  Added npm scripts: snapshot, snapshot:validate
214
- Added devDependency: codebase-snapshot ^0.9.9
214
+ Added devDependency: codebase-snapshot ^0.9.12
215
215
  Added to .gitignore: snapshot/, snapshot.tmp/
216
216
  ```
217
217
 
@@ -378,7 +378,7 @@ codebase-snapshot mcp # serve the current directory over stdio
378
378
  codebase-snapshot mcp --cwd <path> # serve another repository
379
379
  ```
380
380
 
381
- Serves the repository to a Model Context Protocol client over stdio: seven tools, `snapshot://`
381
+ Serves the repository to a Model Context Protocol client over stdio: eight tools, `snapshot://`
382
382
  resources and two prompts. Stdout carries JSON-RPC messages and nothing else; every note goes to
383
383
  stderr. It speaks both protocol eras — the legacy `initialize` handshake and the stateless
384
384
  `2026-07-28` revision — and answers whichever the client starts with.
@@ -401,6 +401,7 @@ narrowest cases of the same rule.
401
401
  | `--cwd <path>` | path | current directory | generate, validate, report, explain, init, skill | Repository to operate on. Must exist and be a directory. `diff` and `describe` take no `--cwd`. |
402
402
  | `--out-dir <path>` | path | `snapshot` | generate, validate, report, explain, init | Where the snapshot is written or read. May be absolute. |
403
403
  | `--max-part-bytes <n>` | size | `307200` (300 KB) | generate, validate, report, explain, init | Hard ceiling per markdown part. Accepts `120k`, `1.5mb`. Minimum 4096. |
404
+ | `--max-part-tokens <n>` | size | unset | generate, validate, setup | A per-part ceiling in **tokens**, for a context window rather than a disk. Narrows `--max-part-bytes` to the equivalent byte count (`tokens × 4`); the smaller of the two ceilings always wins, and both are recorded in the manifest. Accepts `32k`. Minimum 1024 — below that it is a usage error, not a value quietly raised to the floor. |
404
405
  | `--large-file-bytes <n>` | size | `204800` (200 KB) | generate, validate, report, explain | Above this, a file becomes a head/tail preview. |
405
406
  | `--max-monolith-bytes <n>` | size | unset | generate, validate, report, explain | Skip the single-file monolith when it would exceed this. |
406
407
  | `--include-monolith` | flag | off | generate, validate, report, explain | Also write `full-codebase.json` and `full-codebase.md`. |
@@ -408,6 +409,12 @@ narrowest cases of the same rule.
408
409
  | `--skip-artifacts <names>` | list | unset | generate, validate, report, explain, init | Comma-separated artifacts to omit. |
409
410
  | `--only-artifacts <names>` | list | unset | generate, validate, report, explain, init | Write only these. Cannot be combined with `--skip-artifacts`. |
410
411
  | `--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
+ | `--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. |
414
+ | `--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
+ | `--staged` | flag | off | generate, validate, explain | Include only files staged for the next commit. |
416
+ | `--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
+ | `--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. |
411
418
  | `--dry-run` | flag | off | generate, init, setup | Compute everything, print it, write nothing. |
412
419
  | `--watch` | flag | off | generate | Regenerate on every change until interrupted. |
413
420
  | `--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`). |
@@ -430,6 +437,17 @@ with and the comparison is like for like. `validate --skip-artifacts parts` on a
430
437
  built without parts therefore agrees, while the same flag on a full snapshot reports a
431
438
  config mismatch.
432
439
 
440
+ The same rule applies to the compression and scope options. `--skeleton`, `--sarif` and
441
+ `--max-part-tokens` are recorded in `manifest.config.digest`, so `validate` accepts them too —
442
+ without that, a snapshot generated with `--skeleton` could not be validated at all, and passing
443
+ the flag to `validate` was rejected as a flag that "does not apply to validate". `--changed-since`,
444
+ `--staged` and `--with-dependents` are accepted by `validate` and `explain` because they decide
445
+ which recorded `unchanged` skips are re-checked and what `explain` answers about one file.
446
+
447
+ `report` does not take any of them: it reads the manifest and prints what is there, so a
448
+ generation setting would change nothing. A flag that parses and does nothing is the silent
449
+ fallback this table exists to prevent.
450
+
433
451
  Every option also accepts the `--option=value` form. That form is required for negative
434
452
  numbers, which otherwise look like flags.
435
453