vibe-splain 3.5.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +110 -158
  2. package/dist/commands/install.d.ts +0 -5
  3. package/dist/commands/install.js +5 -55
  4. package/dist/commands/serve.js +1 -1
  5. package/dist/export/ArtifactBundleWriter.js +3 -3
  6. package/dist/export/ExportOrchestrator.d.ts +2 -20
  7. package/dist/export/ExportOrchestrator.js +5 -116
  8. package/dist/export/Watcher.js +5 -5
  9. package/dist/export/renderers/AgentMarkdownRenderer.d.ts +1 -1
  10. package/dist/export/renderers/GraphRenderer.d.ts +2 -2
  11. package/dist/export/renderers/HtmlRenderer.d.ts +1 -1
  12. package/dist/export/renderers/HtmlRenderer.js +1 -1
  13. package/dist/export/renderers/JsonRenderer.d.ts +1 -1
  14. package/dist/export/renderers/RawAnalysisRenderer.d.ts +1 -1
  15. package/dist/export/renderers/Renderer.d.ts +1 -1
  16. package/dist/export/renderers/ValidationRenderer.d.ts +1 -1
  17. package/dist/index.js +441 -3787
  18. package/dist/mcp/server.js +6 -23
  19. package/dist/mcp/tools/get_file_context.js +1 -1
  20. package/dist/mcp/tools/get_project_map.js +1 -1
  21. package/dist/mcp/tools/get_strategic_overview.js +1 -1
  22. package/dist/mcp/tools/get_wild_discoveries.js +1 -1
  23. package/dist/mcp/tools/inspect_pillar.js +1 -1
  24. package/dist/mcp/tools/mark_stale.js +1 -1
  25. package/dist/mcp/tools/scan_project.d.ts +2 -3
  26. package/dist/mcp/tools/scan_project.js +12 -13
  27. package/dist/mcp/tools/set_project_brief.js +1 -1
  28. package/dist/mcp/tools/write_decision_card.d.ts +1 -1
  29. package/dist/mcp/tools/write_decision_card.js +2 -2
  30. package/dist/ui/index.html +1 -1
  31. package/package.json +9 -14
  32. package/dist/commands/hook.d.ts +0 -9
  33. package/dist/commands/hook.js +0 -84
  34. package/dist/hook/preToolUse.d.ts +0 -30
  35. package/dist/hook/preToolUse.js +0 -81
  36. package/dist/hook.d.ts +0 -1
  37. package/dist/hook.js +0 -337
  38. package/dist/mcp/BudgetGuard.d.ts +0 -13
  39. package/dist/mcp/BudgetGuard.js +0 -55
  40. package/dist/mcp/SessionScope.d.ts +0 -26
  41. package/dist/mcp/SessionScope.js +0 -56
  42. package/dist/mcp/tools/get_file_skeleton.d.ts +0 -23
  43. package/dist/mcp/tools/get_file_skeleton.js +0 -124
  44. package/dist/mcp/tools/hydration/get_evidence_slice.d.ts +0 -31
  45. package/dist/mcp/tools/hydration/get_evidence_slice.js +0 -57
  46. package/dist/mcp/tools/hydration/get_project_summary.d.ts +0 -23
  47. package/dist/mcp/tools/hydration/get_project_summary.js +0 -58
  48. package/dist/mcp/tools/hydration/get_start_here.d.ts +0 -23
  49. package/dist/mcp/tools/hydration/get_start_here.js +0 -52
  50. package/dist/mcp/tools/read_file.d.ts +0 -31
  51. package/dist/mcp/tools/read_file.js +0 -90
  52. package/dist/store/BlobStore.d.ts +0 -22
  53. package/dist/store/BlobStore.js +0 -96
  54. package/dist/store/PointerStore.d.ts +0 -52
  55. package/dist/store/PointerStore.js +0 -154
@@ -1,26 +0,0 @@
1
- import type { WorkOrderRow, ProofDescriptor } from '../store/PointerStore.js';
2
- export interface ScopePolicy {
3
- workOrderId: string;
4
- allowedFiles: string[];
5
- allowedGlobs: string[];
6
- deniedGlobs: string[];
7
- requiredProof: ProofDescriptor[];
8
- }
9
- export declare class ScopeViolation extends Error {
10
- readonly path: string;
11
- readonly workOrderId: string;
12
- constructor(path: string, workOrderId: string, reason: string);
13
- }
14
- export declare const SessionScope: {
15
- set(policy: ScopePolicy): void;
16
- clear(): void;
17
- get(): ScopePolicy | null;
18
- /**
19
- * Enforce scope for a file path.
20
- * Throws ScopeViolation if:
21
- * - a scope is active AND the path is not allowed
22
- * If no scope is active, all paths are permitted.
23
- */
24
- enforce(filePath: string): void;
25
- fromWorkOrderRow(row: WorkOrderRow): ScopePolicy;
26
- };
@@ -1,56 +0,0 @@
1
- import { minimatch } from 'minimatch';
2
- export class ScopeViolation extends Error {
3
- path;
4
- workOrderId;
5
- constructor(path, workOrderId, reason) {
6
- super(`ScopeViolation [${workOrderId}]: ${reason} — path: ${path}`);
7
- this.path = path;
8
- this.workOrderId = workOrderId;
9
- this.name = 'ScopeViolation';
10
- }
11
- }
12
- let activeScope = null;
13
- export const SessionScope = {
14
- set(policy) {
15
- activeScope = policy;
16
- },
17
- clear() {
18
- activeScope = null;
19
- },
20
- get() {
21
- return activeScope;
22
- },
23
- /**
24
- * Enforce scope for a file path.
25
- * Throws ScopeViolation if:
26
- * - a scope is active AND the path is not allowed
27
- * If no scope is active, all paths are permitted.
28
- */
29
- enforce(filePath) {
30
- if (!activeScope)
31
- return;
32
- const { workOrderId, allowedFiles, allowedGlobs, deniedGlobs } = activeScope;
33
- // Explicit file list match (exact suffix or relative path)
34
- const inAllowedFiles = allowedFiles.some(f => filePath === f || filePath.endsWith('/' + f) || filePath.endsWith(f));
35
- // Glob match
36
- const inAllowedGlobs = allowedGlobs.some(g => minimatch(filePath, g, { matchBase: true }));
37
- if (!inAllowedFiles && !inAllowedGlobs) {
38
- throw new ScopeViolation(filePath, workOrderId, 'path not in allowedFiles or allowedGlobs');
39
- }
40
- // Deny globs have priority over allow
41
- const isDenied = deniedGlobs.some(g => minimatch(filePath, g, { matchBase: true }));
42
- if (isDenied) {
43
- throw new ScopeViolation(filePath, workOrderId, 'path matches deniedGlobs');
44
- }
45
- },
46
- fromWorkOrderRow(row) {
47
- return {
48
- workOrderId: row.workOrderId,
49
- allowedFiles: JSON.parse(row.allowedFiles),
50
- allowedGlobs: JSON.parse(row.allowedGlobs),
51
- deniedGlobs: JSON.parse(row.deniedGlobs),
52
- requiredProof: JSON.parse(row.requiredProof),
53
- };
54
- },
55
- };
56
- //# sourceMappingURL=SessionScope.js.map
@@ -1,23 +0,0 @@
1
- export declare const getFileSkeletonTool: {
2
- name: string;
3
- description: string;
4
- inputSchema: {
5
- type: "object";
6
- properties: {
7
- projectRoot: {
8
- type: string;
9
- description: string;
10
- };
11
- filePath: {
12
- type: string;
13
- description: string;
14
- };
15
- scanId: {
16
- type: string;
17
- description: string;
18
- };
19
- };
20
- required: string[];
21
- };
22
- };
23
- export declare function handleGetFileSkeleton(args: Record<string, unknown>): Promise<unknown>;
@@ -1,124 +0,0 @@
1
- import { readFile } from 'fs/promises';
2
- import { join } from 'path';
3
- import { SessionScope, ScopeViolation } from '../SessionScope.js';
4
- import { BlobStore, hashFile } from '../../store/BlobStore.js';
5
- import { PointerStore } from '../../store/PointerStore.js';
6
- import { applyBudgetGuard } from '../BudgetGuard.js';
7
- import { v4 as uuidv4 } from 'uuid';
8
- export const getFileSkeletonTool = {
9
- name: 'get_file_skeleton',
10
- description: 'Returns a content-addressed skeleton view of a source file (function signatures, class names, exported symbols). Enforces active workOrder scope. Results are content-addressed — repeated calls on unchanged files return cached pointers.',
11
- inputSchema: {
12
- type: 'object',
13
- properties: {
14
- projectRoot: { type: 'string', description: 'Absolute project root' },
15
- filePath: { type: 'string', description: 'Path relative to projectRoot' },
16
- scanId: { type: 'string', description: 'Current scan ID for pointer registration' },
17
- },
18
- required: ['projectRoot', 'filePath', 'scanId'],
19
- },
20
- };
21
- export async function handleGetFileSkeleton(args) {
22
- const projectRoot = args.projectRoot;
23
- const filePath = args.filePath;
24
- const scanId = args.scanId;
25
- if (!projectRoot || !filePath || !scanId) {
26
- throw new Error('projectRoot, filePath, and scanId are required');
27
- }
28
- // 1. Scope enforcement
29
- try {
30
- SessionScope.enforce(filePath);
31
- }
32
- catch (e) {
33
- if (e instanceof ScopeViolation)
34
- throw e;
35
- throw e;
36
- }
37
- const absolutePath = filePath.startsWith('/') ? filePath : join(projectRoot, filePath);
38
- // 2. Compute preimage hash of source file
39
- let currentHash;
40
- try {
41
- currentHash = await hashFile(absolutePath);
42
- }
43
- catch {
44
- throw new Error(`FileNotFound: cannot read ${filePath}`);
45
- }
46
- // 3. Check cache: same hash + parser version = return existing pointer
47
- const parserVersion = '1.0.0';
48
- const cacheKey = `skeleton:${currentHash}:${parserVersion}`;
49
- const pointerStore = PointerStore.open(projectRoot);
50
- // Look up by contentHash in existing pointers for this scan
51
- const existingPointers = pointerStore.listPointersByScan(scanId);
52
- const cached = existingPointers.find(p => p.artifactName === 'file_skeleton' && p.contentHash === cacheKey);
53
- if (cached) {
54
- return {
55
- pointerId: cached.pointerId,
56
- contentHash: currentHash,
57
- cached: true,
58
- filePath,
59
- };
60
- }
61
- // 4. Build skeleton
62
- const source = await readFile(absolutePath, 'utf8');
63
- const skeleton = extractSkeleton(source, filePath);
64
- // 5. Record content hash
65
- const skeletonPayload = {
66
- filePath,
67
- sourceHash: currentHash,
68
- parserVersion,
69
- skeleton,
70
- };
71
- // 6. Budget enforcement
72
- const blobStore = new BlobStore(projectRoot);
73
- const serialized = JSON.stringify(skeletonPayload, null, 2);
74
- const { contentHash: skeletonHash, blobPath } = await blobStore.writeAtomic(serialized);
75
- const pointerId = `ptr_skel_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
76
- await pointerStore.insertPointer({
77
- pointerId,
78
- scanId,
79
- artifactName: 'file_skeleton',
80
- contentHash: cacheKey, // cache key encodes source hash + parser version
81
- blobPath,
82
- schemaVersion: '1.0.0',
83
- createdAt: Date.now(),
84
- expiresAt: null,
85
- });
86
- const result = {
87
- filePath,
88
- sourceHash: currentHash,
89
- pointerId,
90
- skeleton,
91
- };
92
- return await applyBudgetGuard(projectRoot, scanId, 'file_skeleton', result);
93
- }
94
- function extractSkeleton(source, filePath) {
95
- const lines = source.split('\n');
96
- const skeleton = [];
97
- const ext = filePath.split('.').pop() ?? '';
98
- const isTS = ['ts', 'tsx'].includes(ext);
99
- const isJS = ['js', 'jsx', 'mjs', 'cjs'].includes(ext);
100
- if (isTS || isJS) {
101
- for (let i = 0; i < lines.length; i++) {
102
- const line = lines[i].trim();
103
- // Exported declarations, function/class/interface/type/enum signatures
104
- if (/^(export\s+)?(async\s+)?function\b/.test(line) ||
105
- /^(export\s+)?(abstract\s+)?class\b/.test(line) ||
106
- /^(export\s+)?interface\b/.test(line) ||
107
- /^(export\s+)?type\s+\w+/.test(line) ||
108
- /^(export\s+)?enum\b/.test(line) ||
109
- /^(export\s+)?const\s+\w+\s*[:=(]/.test(line) ||
110
- /^(export\s+)?let\s+\w+\s*[:=(]/.test(line) ||
111
- /^(export\s+)?(default\s+)/.test(line) ||
112
- /^\s*(public|private|protected|static|readonly|abstract)\s+/.test(line) ||
113
- /^import\b/.test(line)) {
114
- skeleton.push(`L${i + 1}: ${lines[i]}`);
115
- }
116
- }
117
- }
118
- else {
119
- // Generic: return first 80 lines as skeleton for unsupported types
120
- return lines.slice(0, 80).map((l, i) => `L${i + 1}: ${l}`);
121
- }
122
- return skeleton;
123
- }
124
- //# sourceMappingURL=get_file_skeleton.js.map
@@ -1,31 +0,0 @@
1
- export declare const getEvidenceSliceTool: {
2
- name: string;
3
- description: string;
4
- inputSchema: {
5
- type: "object";
6
- properties: {
7
- projectRoot: {
8
- type: string;
9
- description: string;
10
- };
11
- pointerId: {
12
- type: string;
13
- description: string;
14
- };
15
- startLine: {
16
- type: string;
17
- description: string;
18
- };
19
- endLine: {
20
- type: string;
21
- description: string;
22
- };
23
- scanId: {
24
- type: string;
25
- description: string;
26
- };
27
- };
28
- required: string[];
29
- };
30
- };
31
- export declare function handleGetEvidenceSlice(args: Record<string, unknown>): Promise<unknown>;
@@ -1,57 +0,0 @@
1
- import { hydratePointer, applyBudgetGuard } from '../../BudgetGuard.js';
2
- import { SessionScope } from '../../SessionScope.js';
3
- export const getEvidenceSliceTool = {
4
- name: 'get_evidence_slice',
5
- description: 'Raw fallback: returns a line-range slice from a blob artifact. Pointer must be valid and unexpired. Output is budgeted.',
6
- inputSchema: {
7
- type: 'object',
8
- properties: {
9
- projectRoot: { type: 'string', description: 'Absolute project root' },
10
- pointerId: { type: 'string', description: 'Pointer ID for the target artifact' },
11
- startLine: { type: 'number', description: 'Inclusive start line (1-based)' },
12
- endLine: { type: 'number', description: 'Inclusive end line (1-based). Capped at startLine+200.' },
13
- scanId: { type: 'string', description: 'Current scan ID for budget pointer registration' },
14
- },
15
- required: ['projectRoot', 'pointerId', 'startLine', 'endLine', 'scanId'],
16
- },
17
- };
18
- export async function handleGetEvidenceSlice(args) {
19
- const projectRoot = args.projectRoot;
20
- const pointerId = args.pointerId;
21
- const startLine = Number(args.startLine);
22
- const endLine = Math.min(Number(args.endLine), startLine + 200);
23
- const scanId = args.scanId;
24
- if (!projectRoot || !pointerId || !scanId) {
25
- throw new Error('projectRoot, pointerId, startLine, endLine, and scanId are required');
26
- }
27
- // 1. Verify pointer, lifetime, hash
28
- const { content, row } = await hydratePointer(projectRoot, pointerId);
29
- const rawText = content.toString('utf8');
30
- // Scope enforcement: if a Worker scope is active, enforce it for file-type artifacts
31
- const scope = SessionScope.get();
32
- if (scope && (row.artifactName === 'file_read' || row.artifactName === 'file_skeleton')) {
33
- try {
34
- const parsed = JSON.parse(rawText);
35
- if (parsed.filePath) {
36
- SessionScope.enforce(parsed.filePath);
37
- }
38
- }
39
- catch (e) {
40
- if (e.name === 'ScopeViolation')
41
- throw e;
42
- }
43
- }
44
- const lines = rawText.split('\n');
45
- const sliced = lines.slice(startLine - 1, endLine);
46
- const result = {
47
- pointerId,
48
- artifactName: row.artifactName,
49
- startLine,
50
- endLine,
51
- totalLines: lines.length,
52
- slice: sliced,
53
- };
54
- // 2. Budget query result
55
- return await applyBudgetGuard(projectRoot, scanId, 'evidence_slice', result);
56
- }
57
- //# sourceMappingURL=get_evidence_slice.js.map
@@ -1,23 +0,0 @@
1
- export declare const getProjectSummaryTool: {
2
- name: string;
3
- description: string;
4
- inputSchema: {
5
- type: "object";
6
- properties: {
7
- projectRoot: {
8
- type: string;
9
- description: string;
10
- };
11
- manifestPointer: {
12
- type: string;
13
- description: string;
14
- };
15
- scanId: {
16
- type: string;
17
- description: string;
18
- };
19
- };
20
- required: string[];
21
- };
22
- };
23
- export declare function handleGetProjectSummary(args: Record<string, unknown>): Promise<unknown>;
@@ -1,58 +0,0 @@
1
- import { hydratePointer, applyBudgetGuard } from '../../BudgetGuard.js';
2
- export const getProjectSummaryTool = {
3
- name: 'get_project_summary',
4
- description: 'Returns high-level project metrics from a scan manifest pointer: file counts, pillar summary, stack. Token-safe. Pointer must be valid and unexpired.',
5
- inputSchema: {
6
- type: 'object',
7
- properties: {
8
- projectRoot: { type: 'string', description: 'Absolute project root' },
9
- manifestPointer: { type: 'string', description: 'Pointer ID for the scan manifest' },
10
- scanId: { type: 'string', description: 'Current scan ID for budget pointer registration' },
11
- },
12
- required: ['projectRoot', 'manifestPointer', 'scanId'],
13
- },
14
- };
15
- export async function handleGetProjectSummary(args) {
16
- const projectRoot = args.projectRoot;
17
- const manifestPointer = args.manifestPointer;
18
- const scanId = args.scanId;
19
- if (!projectRoot || !manifestPointer || !scanId) {
20
- throw new Error('projectRoot, manifestPointer, and scanId are required');
21
- }
22
- const { content, row } = await hydratePointer(projectRoot, manifestPointer);
23
- const payload = JSON.parse(content.toString('utf8'));
24
- if (row.artifactName === 'artifact_manifest') {
25
- const manifest = payload;
26
- const analysisEntry = manifest.artifacts.find(a => a.name === 'analysis' && a.indexes?.startHere);
27
- let indexData = {};
28
- if (analysisEntry?.indexes?.startHere) {
29
- const { content: ic } = await hydratePointer(projectRoot, analysisEntry.indexes.startHere);
30
- indexData = JSON.parse(ic.toString('utf8'));
31
- }
32
- const result = {
33
- scanId: manifest.scanId,
34
- generatedAt: manifest.generatedAt,
35
- artifactCount: manifest.artifacts.length,
36
- totalArtifactBytes: manifest.artifacts.reduce((s, a) => s + (a.sizeBytes ?? 0), 0),
37
- startHere: indexData.startHere,
38
- topHeat: indexData.topHeat,
39
- pillarSummary: indexData.pillarSummary,
40
- totalFiles: indexData.totalFiles,
41
- realSourceFiles: indexData.realSourceFiles,
42
- };
43
- return await applyBudgetGuard(projectRoot, scanId, 'get_project_summary_result', result);
44
- }
45
- if (row.artifactName === 'analysis.index') {
46
- const result = {
47
- scanId: payload.scanId,
48
- startHere: payload.startHere,
49
- topHeat: payload.topHeat,
50
- pillarSummary: payload.pillarSummary,
51
- totalFiles: payload.totalFiles,
52
- realSourceFiles: payload.realSourceFiles,
53
- };
54
- return await applyBudgetGuard(projectRoot, scanId, 'get_project_summary_result', result);
55
- }
56
- throw new Error(`Unsupported artifact type for get_project_summary: ${row.artifactName}`);
57
- }
58
- //# sourceMappingURL=get_project_summary.js.map
@@ -1,23 +0,0 @@
1
- export declare const getStartHereTool: {
2
- name: string;
3
- description: string;
4
- inputSchema: {
5
- type: "object";
6
- properties: {
7
- projectRoot: {
8
- type: string;
9
- description: string;
10
- };
11
- manifestPointer: {
12
- type: string;
13
- description: string;
14
- };
15
- scanId: {
16
- type: string;
17
- description: string;
18
- };
19
- };
20
- required: string[];
21
- };
22
- };
23
- export declare function handleGetStartHere(args: Record<string, unknown>): Promise<unknown>;
@@ -1,52 +0,0 @@
1
- import { hydratePointer, applyBudgetGuard } from '../../BudgetGuard.js';
2
- export const getStartHereTool = {
3
- name: 'get_start_here',
4
- description: 'Hydrates the start-here index for a scan manifest pointer. Returns the top 5 highest-gravity files. Pointer must be valid and unexpired.',
5
- inputSchema: {
6
- type: 'object',
7
- properties: {
8
- projectRoot: { type: 'string', description: 'Absolute project root' },
9
- manifestPointer: { type: 'string', description: 'Pointer ID for the scan manifest or analysis.index artifact' },
10
- scanId: { type: 'string', description: 'Current scan ID for budget pointer registration' },
11
- },
12
- required: ['projectRoot', 'manifestPointer', 'scanId'],
13
- },
14
- };
15
- export async function handleGetStartHere(args) {
16
- const projectRoot = args.projectRoot;
17
- const manifestPointer = args.manifestPointer;
18
- const scanId = args.scanId;
19
- if (!projectRoot || !manifestPointer || !scanId) {
20
- throw new Error('projectRoot, manifestPointer, and scanId are required');
21
- }
22
- // 1. Verify pointer exists, unexpired, hash matches blob
23
- const { content, row } = await hydratePointer(projectRoot, manifestPointer);
24
- const payload = JSON.parse(content.toString('utf8'));
25
- // 2. If it's a manifest pointer, find the analysis.index pointer
26
- if (row.artifactName === 'artifact_manifest') {
27
- const manifest = payload;
28
- const analysisEntry = manifest.artifacts.find(a => a.name === 'analysis' || a.name === 'analysis.index');
29
- if (!analysisEntry?.indexes?.startHere) {
30
- throw new Error('Manifest has no analysis.index entry — rescan to regenerate');
31
- }
32
- const { content: indexContent } = await hydratePointer(projectRoot, analysisEntry.indexes.startHere);
33
- const index = JSON.parse(indexContent.toString('utf8'));
34
- const result = {
35
- startHere: index.startHere.slice(0, 5),
36
- schemaVersion: index.schemaVersion,
37
- scanId: index.scanId,
38
- };
39
- return await applyBudgetGuard(projectRoot, scanId, 'get_start_here_result', result);
40
- }
41
- // 3. If it's an analysis.index pointer directly
42
- if (row.artifactName === 'analysis.index') {
43
- const result = {
44
- startHere: payload.startHere.slice(0, 5),
45
- schemaVersion: payload.schemaVersion,
46
- scanId: payload.scanId,
47
- };
48
- return await applyBudgetGuard(projectRoot, scanId, 'get_start_here_result', result);
49
- }
50
- throw new Error(`Unsupported artifact type for get_start_here: ${row.artifactName}`);
51
- }
52
- //# sourceMappingURL=get_start_here.js.map
@@ -1,31 +0,0 @@
1
- export declare const readFileTool: {
2
- name: string;
3
- description: string;
4
- inputSchema: {
5
- type: "object";
6
- properties: {
7
- projectRoot: {
8
- type: string;
9
- description: string;
10
- };
11
- filePath: {
12
- type: string;
13
- description: string;
14
- };
15
- scanId: {
16
- type: string;
17
- description: string;
18
- };
19
- startLine: {
20
- type: string;
21
- description: string;
22
- };
23
- endLine: {
24
- type: string;
25
- description: string;
26
- };
27
- };
28
- required: string[];
29
- };
30
- };
31
- export declare function handleReadFile(args: Record<string, unknown>): Promise<unknown>;
@@ -1,90 +0,0 @@
1
- import { readFile } from 'fs/promises';
2
- import { join } from 'path';
3
- import { SessionScope, ScopeViolation } from '../SessionScope.js';
4
- import { hashFile } from '../../store/BlobStore.js';
5
- import { BlobStore } from '../../store/BlobStore.js';
6
- import { PointerStore } from '../../store/PointerStore.js';
7
- import { applyBudgetGuard } from '../BudgetGuard.js';
8
- import { v4 as uuidv4 } from 'uuid';
9
- export const readFileTool = {
10
- name: 'read_file',
11
- description: 'Reads a file within the active workOrder scope. Enforces allowedFiles/allowedGlobs/deniedGlobs. Records content hash. Output is budgeted.',
12
- inputSchema: {
13
- type: 'object',
14
- properties: {
15
- projectRoot: { type: 'string', description: 'Absolute project root' },
16
- filePath: { type: 'string', description: 'Path relative to projectRoot' },
17
- scanId: { type: 'string', description: 'Current scan ID for pointer registration' },
18
- startLine: { type: 'number', description: 'Optional: 1-based start line to return a slice' },
19
- endLine: { type: 'number', description: 'Optional: 1-based end line (inclusive). Capped at startLine+500.' },
20
- },
21
- required: ['projectRoot', 'filePath', 'scanId'],
22
- },
23
- };
24
- export async function handleReadFile(args) {
25
- const projectRoot = args.projectRoot;
26
- const filePath = args.filePath;
27
- const scanId = args.scanId;
28
- const startLine = args.startLine !== undefined ? Number(args.startLine) : undefined;
29
- const endLine = args.endLine !== undefined
30
- ? Math.min(Number(args.endLine), (startLine ?? 1) + 500)
31
- : undefined;
32
- if (!projectRoot || !filePath || !scanId) {
33
- throw new Error('projectRoot, filePath, and scanId are required');
34
- }
35
- // 1. Scope enforcement
36
- try {
37
- SessionScope.enforce(filePath);
38
- }
39
- catch (e) {
40
- if (e instanceof ScopeViolation)
41
- throw e;
42
- throw e;
43
- }
44
- const absolutePath = filePath.startsWith('/') ? filePath : join(projectRoot, filePath);
45
- // 2. Read file
46
- let content;
47
- try {
48
- content = await readFile(absolutePath, 'utf8');
49
- }
50
- catch {
51
- throw new Error(`FileNotFound: cannot read ${filePath}`);
52
- }
53
- // 3. Content hash recording
54
- const contentHash = await hashFile(absolutePath);
55
- // 4. Apply line slice if requested
56
- let output = content;
57
- let sliceInfo;
58
- if (startLine !== undefined) {
59
- const lines = content.split('\n');
60
- const end = endLine ?? lines.length;
61
- output = lines.slice(startLine - 1, end).join('\n');
62
- sliceInfo = { startLine, endLine: end, totalLines: lines.length };
63
- }
64
- // 5. Record content hash in pointer store
65
- const blobStore = new BlobStore(projectRoot);
66
- const pointerStore = PointerStore.open(projectRoot);
67
- const { blobPath } = await blobStore.writeAtomic(content);
68
- const pointerId = `ptr_file_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
69
- await pointerStore.insertPointer({
70
- pointerId,
71
- scanId,
72
- artifactName: 'file_read',
73
- contentHash,
74
- blobPath,
75
- schemaVersion: '1.0.0',
76
- createdAt: Date.now(),
77
- expiresAt: null,
78
- });
79
- const result = {
80
- filePath,
81
- contentHash,
82
- pointerId,
83
- content: output,
84
- };
85
- if (sliceInfo)
86
- result.slice = sliceInfo;
87
- // 6. Budget enforcement
88
- return await applyBudgetGuard(projectRoot, scanId, 'file_read', result);
89
- }
90
- //# sourceMappingURL=read_file.js.map
@@ -1,22 +0,0 @@
1
- export interface BlobWriteResult {
2
- contentHash: string;
3
- blobPath: string;
4
- }
5
- export declare class BlobStore {
6
- private blobsDir;
7
- private tmpDir;
8
- constructor(projectRoot: string);
9
- ensureDirs(): Promise<void>;
10
- writeAtomic(payload: Buffer | string): Promise<BlobWriteResult>;
11
- readBlob(blobPath: string): Promise<Buffer>;
12
- blobExists(contentHash: string): Promise<boolean>;
13
- blobPathForHash(contentHash: string): string;
14
- verifyIntegrity(blobPath: string, expectedHash: string): Promise<boolean>;
15
- /** List all blob paths for GC reference counting */
16
- listBlobPaths(): Promise<string[]>;
17
- getBlobSize(blobPath: string): Promise<number>;
18
- }
19
- /** Hash a string or buffer without writing it */
20
- export declare function computeHash(payload: Buffer | string): string;
21
- /** Hash a file on disk */
22
- export declare function hashFile(filePath: string): Promise<string>;