vibe-splain 3.0.0 → 3.2.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.
- package/dist/commands/bundle.d.ts +4 -0
- package/dist/commands/bundle.js +68 -0
- package/dist/commands/gc.d.ts +3 -0
- package/dist/commands/gc.js +59 -0
- package/dist/commands/importBundle.d.ts +4 -0
- package/dist/commands/importBundle.js +80 -0
- package/dist/export/ArtifactBundleWriter.js +24 -6
- package/dist/export/ExportOrchestrator.d.ts +19 -1
- package/dist/export/ExportOrchestrator.js +90 -3
- package/dist/export/Watcher.d.ts +1 -1
- package/dist/export/Watcher.js +9 -1
- package/dist/export/renderers/AgentMarkdownRenderer.d.ts +2 -1
- package/dist/export/renderers/AgentMarkdownRenderer.js +17 -1
- package/dist/export/renderers/HtmlRenderer.js +29 -6
- package/dist/index.js +1671 -129
- package/dist/mcp/BudgetGuard.d.ts +13 -0
- package/dist/mcp/BudgetGuard.js +55 -0
- package/dist/mcp/SessionScope.d.ts +26 -0
- package/dist/mcp/SessionScope.js +56 -0
- package/dist/mcp/server.js +38 -0
- package/dist/mcp/tools/apply_patch.d.ts +37 -0
- package/dist/mcp/tools/apply_patch.js +103 -0
- package/dist/mcp/tools/get_file_skeleton.d.ts +23 -0
- package/dist/mcp/tools/get_file_skeleton.js +124 -0
- package/dist/mcp/tools/hydration/get_evidence_slice.d.ts +31 -0
- package/dist/mcp/tools/hydration/get_evidence_slice.js +59 -0
- package/dist/mcp/tools/hydration/get_project_summary.d.ts +23 -0
- package/dist/mcp/tools/hydration/get_project_summary.js +58 -0
- package/dist/mcp/tools/hydration/get_start_here.d.ts +23 -0
- package/dist/mcp/tools/hydration/get_start_here.js +52 -0
- package/dist/mcp/tools/read_file.d.ts +31 -0
- package/dist/mcp/tools/read_file.js +90 -0
- package/dist/mcp/tools/scan_project.js +6 -3
- package/dist/mcp/tools/set_session_scope.d.ts +19 -0
- package/dist/mcp/tools/set_session_scope.js +40 -0
- package/dist/mcp/tools/submit_receipt.d.ts +68 -0
- package/dist/mcp/tools/submit_receipt.js +94 -0
- package/dist/mcp/tools/work_orders.d.ts +79 -0
- package/dist/mcp/tools/work_orders.js +126 -0
- package/dist/mcp/tools/yield_for_scope_expansion.d.ts +29 -0
- package/dist/mcp/tools/yield_for_scope_expansion.js +59 -0
- package/dist/store/BlobStore.d.ts +22 -0
- package/dist/store/BlobStore.js +96 -0
- package/dist/store/PointerStore.d.ts +52 -0
- package/dist/store/PointerStore.js +138 -0
- package/package.json +8 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface BudgetExceededResult {
|
|
2
|
+
pointerId: string;
|
|
3
|
+
contentHash: string;
|
|
4
|
+
sizeBytes: number;
|
|
5
|
+
summary: string;
|
|
6
|
+
hydrators: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function applyBudgetGuard(projectRoot: string, scanId: string, artifactName: string, output: unknown): Promise<unknown>;
|
|
9
|
+
/** Verify a pointer exists, is unexpired, and its blob hash matches */
|
|
10
|
+
export declare function hydratePointer(projectRoot: string, pointerId: string): Promise<{
|
|
11
|
+
content: Buffer;
|
|
12
|
+
row: import('../store/PointerStore.js').PointerRow;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { BlobStore } from '../store/BlobStore.js';
|
|
2
|
+
import { PointerStore } from '../store/PointerStore.js';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
/** ~2000 tokens ≈ 8000 chars */
|
|
5
|
+
const BUDGET_CHARS = 8000;
|
|
6
|
+
export async function applyBudgetGuard(projectRoot, scanId, artifactName, output) {
|
|
7
|
+
const serialized = JSON.stringify(output, null, 2);
|
|
8
|
+
if (serialized.length <= BUDGET_CHARS)
|
|
9
|
+
return output;
|
|
10
|
+
const blobStore = new BlobStore(projectRoot);
|
|
11
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
12
|
+
const { contentHash, blobPath } = await blobStore.writeAtomic(serialized);
|
|
13
|
+
const pointerId = `ptr_${uuidv4().replace(/-/g, '').slice(0, 16)}`;
|
|
14
|
+
await pointerStore.insertPointer({
|
|
15
|
+
pointerId,
|
|
16
|
+
scanId,
|
|
17
|
+
artifactName,
|
|
18
|
+
contentHash,
|
|
19
|
+
blobPath,
|
|
20
|
+
schemaVersion: '1.0.0',
|
|
21
|
+
createdAt: Date.now(),
|
|
22
|
+
expiresAt: null,
|
|
23
|
+
});
|
|
24
|
+
const result = {
|
|
25
|
+
pointerId,
|
|
26
|
+
contentHash,
|
|
27
|
+
sizeBytes: serialized.length,
|
|
28
|
+
summary: `Output exceeded context budget (${serialized.length} chars). Written to artifact blob.`,
|
|
29
|
+
hydrators: ['get_evidence_slice', 'get_start_here', 'get_project_summary'],
|
|
30
|
+
};
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
/** Verify a pointer exists, is unexpired, and its blob hash matches */
|
|
34
|
+
export async function hydratePointer(projectRoot, pointerId) {
|
|
35
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
36
|
+
const row = pointerStore.getPointer(pointerId);
|
|
37
|
+
if (!row) {
|
|
38
|
+
throw new Error(`ArtifactNotFound: pointer ${pointerId} does not exist`);
|
|
39
|
+
}
|
|
40
|
+
if (row.expiresAt !== null && row.expiresAt < Date.now()) {
|
|
41
|
+
throw new Error(`ArtifactCollectedError: pointer ${pointerId} has expired`);
|
|
42
|
+
}
|
|
43
|
+
const SUPPORTED_VERSIONS = ['1.0.0', '2.0.0'];
|
|
44
|
+
if (!SUPPORTED_VERSIONS.includes(row.schemaVersion)) {
|
|
45
|
+
throw new Error(`UnsupportedSchema: pointer ${pointerId} has schema version ${row.schemaVersion}`);
|
|
46
|
+
}
|
|
47
|
+
const blobStore = new BlobStore(projectRoot);
|
|
48
|
+
const content = await blobStore.readBlob(row.blobPath);
|
|
49
|
+
const valid = await blobStore.verifyIntegrity(row.blobPath, row.contentHash);
|
|
50
|
+
if (!valid) {
|
|
51
|
+
throw new Error(`IntegrityError: blob for pointer ${pointerId} failed hash verification`);
|
|
52
|
+
}
|
|
53
|
+
return { content, row };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=BudgetGuard.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
package/dist/mcp/server.js
CHANGED
|
@@ -12,6 +12,16 @@ import { handleInspectPillar, inspectPillarTool } from './tools/inspect_pillar.j
|
|
|
12
12
|
import { handleGetWildDiscoveries, getWildDiscoveriesTool } from './tools/get_wild_discoveries.js';
|
|
13
13
|
import { handleMarkStale, markStaleTool } from './tools/mark_stale.js';
|
|
14
14
|
import { handleGetCallChain, getCallChainTool } from './tools/get_call_chain.js';
|
|
15
|
+
import { handleGetFileSkeleton, getFileSkeletonTool } from './tools/get_file_skeleton.js';
|
|
16
|
+
import { handleReadFile, readFileTool } from './tools/read_file.js';
|
|
17
|
+
import { handleApplyPatch, applyPatchTool } from './tools/apply_patch.js';
|
|
18
|
+
import { handleCreateWorkOrder, createWorkOrderTool, handleSpawnWorker, spawnWorkerTool } from './tools/work_orders.js';
|
|
19
|
+
import { handleSubmitReceipt, submitReceiptTool } from './tools/submit_receipt.js';
|
|
20
|
+
import { handleSetSessionScope, setSessionScopeTool } from './tools/set_session_scope.js';
|
|
21
|
+
import { handleYieldForScopeExpansion, yieldForScopeExpansionTool } from './tools/yield_for_scope_expansion.js';
|
|
22
|
+
import { handleGetStartHere, getStartHereTool } from './tools/hydration/get_start_here.js';
|
|
23
|
+
import { handleGetProjectSummary, getProjectSummaryTool } from './tools/hydration/get_project_summary.js';
|
|
24
|
+
import { handleGetEvidenceSlice, getEvidenceSliceTool } from './tools/hydration/get_evidence_slice.js';
|
|
15
25
|
// ⚠️ CRITICAL: Never use console.log() anywhere in this codebase.
|
|
16
26
|
// stdout is owned by the MCP SDK for protocol messages.
|
|
17
27
|
// Use console.error() for all diagnostic output.
|
|
@@ -26,6 +36,20 @@ const ALL_TOOLS = [
|
|
|
26
36
|
inspectPillarTool,
|
|
27
37
|
getWildDiscoveriesTool,
|
|
28
38
|
markStaleTool,
|
|
39
|
+
// Phase 2: Skeletons + Hydration
|
|
40
|
+
getFileSkeletonTool,
|
|
41
|
+
readFileTool,
|
|
42
|
+
getStartHereTool,
|
|
43
|
+
getProjectSummaryTool,
|
|
44
|
+
getEvidenceSliceTool,
|
|
45
|
+
// Phase 3: Delegation & Proof
|
|
46
|
+
createWorkOrderTool,
|
|
47
|
+
spawnWorkerTool,
|
|
48
|
+
applyPatchTool,
|
|
49
|
+
submitReceiptTool,
|
|
50
|
+
// Phase 4: Scope & Escalation
|
|
51
|
+
setSessionScopeTool,
|
|
52
|
+
yieldForScopeExpansionTool,
|
|
29
53
|
];
|
|
30
54
|
const TOOL_HANDLERS = {
|
|
31
55
|
scan_project: handleScanProject,
|
|
@@ -38,6 +62,20 @@ const TOOL_HANDLERS = {
|
|
|
38
62
|
inspect_pillar: handleInspectPillar,
|
|
39
63
|
get_wild_discoveries: handleGetWildDiscoveries,
|
|
40
64
|
mark_stale: handleMarkStale,
|
|
65
|
+
// Phase 2
|
|
66
|
+
get_file_skeleton: handleGetFileSkeleton,
|
|
67
|
+
read_file: handleReadFile,
|
|
68
|
+
get_start_here: handleGetStartHere,
|
|
69
|
+
get_project_summary: handleGetProjectSummary,
|
|
70
|
+
get_evidence_slice: handleGetEvidenceSlice,
|
|
71
|
+
// Phase 3
|
|
72
|
+
create_work_order: handleCreateWorkOrder,
|
|
73
|
+
spawn_worker: handleSpawnWorker,
|
|
74
|
+
apply_patch: handleApplyPatch,
|
|
75
|
+
submit_receipt: handleSubmitReceipt,
|
|
76
|
+
// Phase 4
|
|
77
|
+
set_session_scope: handleSetSessionScope,
|
|
78
|
+
yield_for_scope_expansion: handleYieldForScopeExpansion,
|
|
41
79
|
};
|
|
42
80
|
export async function startMCPServer(options = {}) {
|
|
43
81
|
// Initialize Tree-Sitter WASM once at startup
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare class StalePatchError extends Error {
|
|
2
|
+
readonly filePath: string;
|
|
3
|
+
readonly expectedHash: string;
|
|
4
|
+
readonly actualHash: string;
|
|
5
|
+
constructor(filePath: string, expectedHash: string, actualHash: string);
|
|
6
|
+
}
|
|
7
|
+
export declare const applyPatchTool: {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: "object";
|
|
12
|
+
properties: {
|
|
13
|
+
projectRoot: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
filePath: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
newContent: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
expectedPrePatchHash: {
|
|
26
|
+
type: string;
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
scanId: {
|
|
30
|
+
type: string;
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
required: string[];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export declare function handleApplyPatch(args: Record<string, unknown>): Promise<unknown>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { writeFile, rename, mkdir } from 'fs/promises';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { SessionScope, ScopeViolation } from '../SessionScope.js';
|
|
4
|
+
import { hashFile, BlobStore } 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 class StalePatchError extends Error {
|
|
9
|
+
filePath;
|
|
10
|
+
expectedHash;
|
|
11
|
+
actualHash;
|
|
12
|
+
constructor(filePath, expectedHash, actualHash) {
|
|
13
|
+
super(`StalePatchError: ${filePath} hash mismatch — expected ${expectedHash}, got ${actualHash}. ` +
|
|
14
|
+
'File was modified since the expectedPrePatchHash was computed. Re-read the file and regenerate the patch.');
|
|
15
|
+
this.filePath = filePath;
|
|
16
|
+
this.expectedHash = expectedHash;
|
|
17
|
+
this.actualHash = actualHash;
|
|
18
|
+
this.name = 'StalePatchError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const applyPatchTool = {
|
|
22
|
+
name: 'apply_patch',
|
|
23
|
+
description: 'Applies a text patch to a file within the active workOrder scope. Requires expectedPrePatchHash to prevent stale-patch corruption. Records pre- and post-patch hashes.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
projectRoot: { type: 'string', description: 'Absolute project root' },
|
|
28
|
+
filePath: { type: 'string', description: 'Path relative to projectRoot' },
|
|
29
|
+
newContent: { type: 'string', description: 'Full new content of the file after the patch' },
|
|
30
|
+
expectedPrePatchHash: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'sha256:<hex> hash of the file BEFORE patching. Obtain via hashFile or the sourceHash from get_file_skeleton.',
|
|
33
|
+
},
|
|
34
|
+
scanId: { type: 'string', description: 'Current scan ID for pointer registration' },
|
|
35
|
+
},
|
|
36
|
+
required: ['projectRoot', 'filePath', 'newContent', 'expectedPrePatchHash', 'scanId'],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
export async function handleApplyPatch(args) {
|
|
40
|
+
const projectRoot = args.projectRoot;
|
|
41
|
+
const filePath = args.filePath;
|
|
42
|
+
const newContent = args.newContent;
|
|
43
|
+
const expectedPrePatchHash = args.expectedPrePatchHash;
|
|
44
|
+
const scanId = args.scanId;
|
|
45
|
+
if (!projectRoot || !filePath || !newContent || !expectedPrePatchHash || !scanId) {
|
|
46
|
+
throw new Error('projectRoot, filePath, newContent, expectedPrePatchHash, and scanId are all required');
|
|
47
|
+
}
|
|
48
|
+
// 1. Scope enforcement
|
|
49
|
+
try {
|
|
50
|
+
SessionScope.enforce(filePath);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
if (e instanceof ScopeViolation)
|
|
54
|
+
throw e;
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
const absolutePath = filePath.startsWith('/') ? filePath : join(projectRoot, filePath);
|
|
58
|
+
// 2. Preimage hash check
|
|
59
|
+
let actualPreHash;
|
|
60
|
+
try {
|
|
61
|
+
actualPreHash = await hashFile(absolutePath);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// File doesn't exist yet — for new files, expectedPrePatchHash must be 'sha256:new'
|
|
65
|
+
actualPreHash = 'sha256:new';
|
|
66
|
+
}
|
|
67
|
+
if (actualPreHash !== expectedPrePatchHash) {
|
|
68
|
+
throw new StalePatchError(filePath, expectedPrePatchHash, actualPreHash);
|
|
69
|
+
}
|
|
70
|
+
// 3. Atomic write
|
|
71
|
+
const dir = dirname(absolutePath);
|
|
72
|
+
await mkdir(dir, { recursive: true });
|
|
73
|
+
const tmpPath = absolutePath + `.tmp_${Date.now()}`;
|
|
74
|
+
await writeFile(tmpPath, newContent, 'utf8');
|
|
75
|
+
await rename(tmpPath, absolutePath);
|
|
76
|
+
// 4. Compute post-patch hash
|
|
77
|
+
const postPatchHash = await hashFile(absolutePath);
|
|
78
|
+
// 5. Record both hashes in blob store
|
|
79
|
+
const blobStore = new BlobStore(projectRoot);
|
|
80
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
81
|
+
const { blobPath } = await blobStore.writeAtomic(newContent);
|
|
82
|
+
const pointerId = `ptr_patch_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
|
|
83
|
+
await pointerStore.insertPointer({
|
|
84
|
+
pointerId,
|
|
85
|
+
scanId,
|
|
86
|
+
artifactName: 'patch_record',
|
|
87
|
+
contentHash: postPatchHash,
|
|
88
|
+
blobPath,
|
|
89
|
+
schemaVersion: '1.0.0',
|
|
90
|
+
createdAt: Date.now(),
|
|
91
|
+
expiresAt: null,
|
|
92
|
+
});
|
|
93
|
+
const result = {
|
|
94
|
+
ok: true,
|
|
95
|
+
filePath,
|
|
96
|
+
prePatchHash: actualPreHash,
|
|
97
|
+
postPatchHash,
|
|
98
|
+
pointerId,
|
|
99
|
+
message: `Patch applied to ${filePath}`,
|
|
100
|
+
};
|
|
101
|
+
return await applyBudgetGuard(projectRoot, scanId, 'patch_record', result);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=apply_patch.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
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>;
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
|
@@ -0,0 +1,31 @@
|
|
|
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>;
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
// 2. Scope enforcement for file-type artifacts: if a Worker scope is active,
|
|
31
|
+
// file_read and file_skeleton blobs embed the source filePath — enforce it.
|
|
32
|
+
const scope = SessionScope.get();
|
|
33
|
+
if (scope && (row.artifactName === 'file_read' || row.artifactName === 'file_skeleton')) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(rawText);
|
|
36
|
+
if (parsed.filePath) {
|
|
37
|
+
SessionScope.enforce(parsed.filePath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// If it's a ScopeViolation, re-throw; JSON parse failures are ignored.
|
|
42
|
+
if (e.name === 'ScopeViolation')
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const lines = rawText.split('\n');
|
|
47
|
+
const sliced = lines.slice(startLine - 1, endLine);
|
|
48
|
+
const result = {
|
|
49
|
+
pointerId,
|
|
50
|
+
artifactName: row.artifactName,
|
|
51
|
+
startLine,
|
|
52
|
+
endLine,
|
|
53
|
+
totalLines: lines.length,
|
|
54
|
+
slice: sliced,
|
|
55
|
+
};
|
|
56
|
+
// 2. Budget query result
|
|
57
|
+
return await applyBudgetGuard(projectRoot, scanId, 'evidence_slice', result);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=get_evidence_slice.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
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>;
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|