vibe-splain 3.1.0 → 3.2.1

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 (40) hide show
  1. package/dist/commands/bundle.d.ts +4 -0
  2. package/dist/commands/bundle.js +68 -0
  3. package/dist/commands/gc.d.ts +3 -0
  4. package/dist/commands/gc.js +59 -0
  5. package/dist/commands/importBundle.d.ts +4 -0
  6. package/dist/commands/importBundle.js +80 -0
  7. package/dist/export/ExportOrchestrator.d.ts +19 -1
  8. package/dist/export/ExportOrchestrator.js +87 -1
  9. package/dist/index.js +1498 -36
  10. package/dist/mcp/BudgetGuard.d.ts +13 -0
  11. package/dist/mcp/BudgetGuard.js +55 -0
  12. package/dist/mcp/SessionScope.d.ts +26 -0
  13. package/dist/mcp/SessionScope.js +56 -0
  14. package/dist/mcp/server.js +38 -0
  15. package/dist/mcp/tools/apply_patch.d.ts +37 -0
  16. package/dist/mcp/tools/apply_patch.js +103 -0
  17. package/dist/mcp/tools/get_file_skeleton.d.ts +23 -0
  18. package/dist/mcp/tools/get_file_skeleton.js +124 -0
  19. package/dist/mcp/tools/hydration/get_evidence_slice.d.ts +31 -0
  20. package/dist/mcp/tools/hydration/get_evidence_slice.js +59 -0
  21. package/dist/mcp/tools/hydration/get_project_summary.d.ts +23 -0
  22. package/dist/mcp/tools/hydration/get_project_summary.js +58 -0
  23. package/dist/mcp/tools/hydration/get_start_here.d.ts +23 -0
  24. package/dist/mcp/tools/hydration/get_start_here.js +52 -0
  25. package/dist/mcp/tools/read_file.d.ts +31 -0
  26. package/dist/mcp/tools/read_file.js +90 -0
  27. package/dist/mcp/tools/scan_project.js +5 -2
  28. package/dist/mcp/tools/set_session_scope.d.ts +19 -0
  29. package/dist/mcp/tools/set_session_scope.js +40 -0
  30. package/dist/mcp/tools/submit_receipt.d.ts +68 -0
  31. package/dist/mcp/tools/submit_receipt.js +94 -0
  32. package/dist/mcp/tools/work_orders.d.ts +79 -0
  33. package/dist/mcp/tools/work_orders.js +126 -0
  34. package/dist/mcp/tools/yield_for_scope_expansion.d.ts +29 -0
  35. package/dist/mcp/tools/yield_for_scope_expansion.js +59 -0
  36. package/dist/store/BlobStore.d.ts +22 -0
  37. package/dist/store/BlobStore.js +96 -0
  38. package/dist/store/PointerStore.d.ts +52 -0
  39. package/dist/store/PointerStore.js +138 -0
  40. package/package.json +8 -1
@@ -0,0 +1,29 @@
1
+ export declare const yieldForScopeExpansionTool: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ requestedPaths: {
8
+ type: string;
9
+ items: {
10
+ type: string;
11
+ };
12
+ description: string;
13
+ };
14
+ reason: {
15
+ type: string;
16
+ description: string;
17
+ };
18
+ evidencePointers: {
19
+ type: string;
20
+ items: {
21
+ type: string;
22
+ };
23
+ description: string;
24
+ };
25
+ };
26
+ required: string[];
27
+ };
28
+ };
29
+ export declare function handleYieldForScopeExpansion(args: Record<string, unknown>): Promise<unknown>;
@@ -0,0 +1,59 @@
1
+ import { SessionScope } from '../SessionScope.js';
2
+ export const yieldForScopeExpansionTool = {
3
+ name: 'yield_for_scope_expansion',
4
+ description: 'Worker signals that it needs to access files outside its current scope. Immediately terminates the active scope and returns a Blocked receipt. The Manager must evaluate the evidence and decide whether to spawn a new Worker with expanded scope.',
5
+ inputSchema: {
6
+ type: 'object',
7
+ properties: {
8
+ requestedPaths: {
9
+ type: 'array',
10
+ items: { type: 'string' },
11
+ description: 'Paths the worker needs but cannot access under current scope',
12
+ },
13
+ reason: {
14
+ type: 'string',
15
+ description: 'Why these paths are needed — root cause found in out-of-scope file',
16
+ },
17
+ evidencePointers: {
18
+ type: 'array',
19
+ items: { type: 'string' },
20
+ description: 'Pointer IDs for artifacts that justify the expansion request',
21
+ },
22
+ },
23
+ required: ['requestedPaths', 'reason'],
24
+ },
25
+ };
26
+ export async function handleYieldForScopeExpansion(args) {
27
+ const requestedPaths = args.requestedPaths ?? [];
28
+ const reason = args.reason;
29
+ const evidencePointers = args.evidencePointers ?? [];
30
+ if (!requestedPaths.length || !reason) {
31
+ throw new Error('requestedPaths and reason are required');
32
+ }
33
+ const currentScope = SessionScope.get();
34
+ const workOrderId = currentScope?.workOrderId ?? 'unknown';
35
+ // Immediately clear the active scope — Worker is now blocked
36
+ SessionScope.clear();
37
+ // Return strict Blocked status per ADR-033
38
+ return {
39
+ status: 'blocked',
40
+ workOrderId,
41
+ requestedPaths,
42
+ reason,
43
+ evidencePointers,
44
+ receipt: {
45
+ workOrderId,
46
+ status: 'blocked',
47
+ proofPointers: [],
48
+ changedFiles: [],
49
+ summary: `Worker blocked: scope expansion required. Reason: ${reason}`,
50
+ },
51
+ managerInstructions: [
52
+ 'Worker has been terminated. Active session scope has been cleared.',
53
+ 'Evaluate the evidence pointers and reason.',
54
+ 'If expansion is warranted, create a new Work Order with the expanded allowedFiles and spawn a new Worker.',
55
+ 'If expansion is NOT warranted, the task is failed — do not retry with same scope.',
56
+ ],
57
+ };
58
+ }
59
+ //# sourceMappingURL=yield_for_scope_expansion.js.map
@@ -0,0 +1,22 @@
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>;
@@ -0,0 +1,96 @@
1
+ import { createHash } from 'crypto';
2
+ import { mkdir, writeFile, open, rename, stat } from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ export class BlobStore {
6
+ blobsDir;
7
+ tmpDir;
8
+ constructor(projectRoot) {
9
+ this.blobsDir = join(projectRoot, '.vibe-splainer', 'blobs');
10
+ this.tmpDir = join(projectRoot, '.vibe-splainer', 'tmp');
11
+ }
12
+ async ensureDirs() {
13
+ await mkdir(this.blobsDir, { recursive: true });
14
+ await mkdir(this.tmpDir, { recursive: true });
15
+ }
16
+ async writeAtomic(payload) {
17
+ await this.ensureDirs();
18
+ const buf = typeof payload === 'string' ? Buffer.from(payload, 'utf8') : payload;
19
+ const hex = createHash('sha256').update(buf).digest('hex');
20
+ const contentHash = `sha256:${hex}`;
21
+ const blobPath = join(this.blobsDir, `sha256_${hex}`);
22
+ if (existsSync(blobPath)) {
23
+ return { contentHash, blobPath };
24
+ }
25
+ const tmpPath = join(this.tmpDir, `tmp_${hex}_${Date.now()}`);
26
+ await writeFile(tmpPath, buf);
27
+ // fsync the file before rename
28
+ const fh = await open(tmpPath, 'r');
29
+ try {
30
+ await fh.datasync();
31
+ }
32
+ finally {
33
+ await fh.close();
34
+ }
35
+ await rename(tmpPath, blobPath);
36
+ return { contentHash, blobPath };
37
+ }
38
+ async readBlob(blobPath) {
39
+ const { readFile } = await import('fs/promises');
40
+ return readFile(blobPath);
41
+ }
42
+ async blobExists(contentHash) {
43
+ const hex = contentHash.replace('sha256:', '');
44
+ const blobPath = join(this.blobsDir, `sha256_${hex}`);
45
+ return existsSync(blobPath);
46
+ }
47
+ blobPathForHash(contentHash) {
48
+ const hex = contentHash.replace('sha256:', '');
49
+ return join(this.blobsDir, `sha256_${hex}`);
50
+ }
51
+ async verifyIntegrity(blobPath, expectedHash) {
52
+ try {
53
+ const { readFile } = await import('fs/promises');
54
+ const buf = await readFile(blobPath);
55
+ const hex = createHash('sha256').update(buf).digest('hex');
56
+ return `sha256:${hex}` === expectedHash;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ /** List all blob paths for GC reference counting */
63
+ async listBlobPaths() {
64
+ try {
65
+ const { readdir } = await import('fs/promises');
66
+ const files = await readdir(this.blobsDir);
67
+ return files
68
+ .filter(f => f.startsWith('sha256_'))
69
+ .map(f => join(this.blobsDir, f));
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ async getBlobSize(blobPath) {
76
+ try {
77
+ const info = await stat(blobPath);
78
+ return info.size;
79
+ }
80
+ catch {
81
+ return 0;
82
+ }
83
+ }
84
+ }
85
+ /** Hash a string or buffer without writing it */
86
+ export function computeHash(payload) {
87
+ const buf = typeof payload === 'string' ? Buffer.from(payload, 'utf8') : payload;
88
+ return `sha256:${createHash('sha256').update(buf).digest('hex')}`;
89
+ }
90
+ /** Hash a file on disk */
91
+ export async function hashFile(filePath) {
92
+ const { readFile } = await import('fs/promises');
93
+ const buf = await readFile(filePath);
94
+ return `sha256:${createHash('sha256').update(buf).digest('hex')}`;
95
+ }
96
+ //# sourceMappingURL=BlobStore.js.map
@@ -0,0 +1,52 @@
1
+ export interface PointerRow {
2
+ pointerId: string;
3
+ scanId: string;
4
+ artifactName: string;
5
+ contentHash: string;
6
+ blobPath: string;
7
+ schemaVersion: string;
8
+ createdAt: number;
9
+ expiresAt: number | null;
10
+ }
11
+ export interface WorkOrderRow {
12
+ workOrderId: string;
13
+ intent: string;
14
+ allowedFiles: string;
15
+ allowedGlobs: string;
16
+ deniedGlobs: string;
17
+ requiredProof: string;
18
+ status: 'pending' | 'active' | 'completed' | 'failed' | 'blocked';
19
+ createdAt: number;
20
+ }
21
+ export interface ProofDescriptor {
22
+ proofId: string;
23
+ schemaName: string;
24
+ description: string;
25
+ }
26
+ export declare class PointerStore {
27
+ private db;
28
+ private writeMutex;
29
+ constructor(projectRoot: string);
30
+ static open(projectRoot: string): PointerStore;
31
+ static reset(): void;
32
+ private _migrate;
33
+ insertPointer(row: PointerRow): Promise<void>;
34
+ getPointer(pointerId: string): PointerRow | null;
35
+ listPointersByScan(scanId: string): PointerRow[];
36
+ insertWorkOrder(row: WorkOrderRow): Promise<void>;
37
+ getWorkOrder(workOrderId: string): WorkOrderRow | null;
38
+ updateWorkOrderStatus(workOrderId: string, status: WorkOrderRow['status']): Promise<void>;
39
+ insertReceipt(receipt: {
40
+ receiptId: string;
41
+ workOrderId: string;
42
+ status: string;
43
+ proofPointers: unknown[];
44
+ changedFiles: unknown[];
45
+ summary: string;
46
+ }): Promise<void>;
47
+ /** GC: delete pointers older than cutoffMs and not pinned, return deleted count */
48
+ gcScanPointers(keepScanIds: string[]): Promise<number>;
49
+ listAllScanIds(): string[];
50
+ countPointers(): number;
51
+ close(): void;
52
+ }
@@ -0,0 +1,138 @@
1
+ import Database from 'better-sqlite3';
2
+ import { join } from 'path';
3
+ import { mkdirSync } from 'fs';
4
+ import { Mutex } from 'async-mutex';
5
+ let instance = null;
6
+ export class PointerStore {
7
+ db;
8
+ writeMutex = new Mutex();
9
+ constructor(projectRoot) {
10
+ const dir = join(projectRoot, '.vibe-splainer');
11
+ mkdirSync(dir, { recursive: true });
12
+ this.db = new Database(join(dir, 'pointer_store.db'));
13
+ // busy_timeout must be set first — it governs the WAL switch itself
14
+ this.db.pragma('busy_timeout = 5000');
15
+ this.db.pragma('journal_mode = WAL');
16
+ this.db.pragma('foreign_keys = ON');
17
+ this._migrate();
18
+ }
19
+ static open(projectRoot) {
20
+ if (!instance)
21
+ instance = new PointerStore(projectRoot);
22
+ return instance;
23
+ }
24
+ static reset() {
25
+ instance = null;
26
+ }
27
+ _migrate() {
28
+ this.db.exec(`
29
+ CREATE TABLE IF NOT EXISTS pointers (
30
+ pointerId TEXT PRIMARY KEY,
31
+ scanId TEXT NOT NULL,
32
+ artifactName TEXT NOT NULL,
33
+ contentHash TEXT NOT NULL,
34
+ blobPath TEXT NOT NULL,
35
+ schemaVersion TEXT NOT NULL DEFAULT '1.0.0',
36
+ createdAt INTEGER NOT NULL,
37
+ expiresAt INTEGER
38
+ );
39
+ CREATE INDEX IF NOT EXISTS idx_pointers_scan ON pointers(scanId);
40
+ CREATE INDEX IF NOT EXISTS idx_pointers_hash ON pointers(contentHash);
41
+
42
+ CREATE TABLE IF NOT EXISTS work_orders (
43
+ workOrderId TEXT PRIMARY KEY,
44
+ intent TEXT NOT NULL,
45
+ allowedFiles TEXT NOT NULL DEFAULT '[]',
46
+ allowedGlobs TEXT NOT NULL DEFAULT '[]',
47
+ deniedGlobs TEXT NOT NULL DEFAULT '[]',
48
+ requiredProof TEXT NOT NULL DEFAULT '[]',
49
+ status TEXT NOT NULL DEFAULT 'pending',
50
+ createdAt INTEGER NOT NULL
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS receipts (
54
+ receiptId TEXT PRIMARY KEY,
55
+ workOrderId TEXT NOT NULL REFERENCES work_orders(workOrderId),
56
+ status TEXT NOT NULL,
57
+ proofPointers TEXT NOT NULL DEFAULT '[]',
58
+ changedFiles TEXT NOT NULL DEFAULT '[]',
59
+ summary TEXT NOT NULL DEFAULT '',
60
+ createdAt INTEGER NOT NULL,
61
+ FOREIGN KEY (workOrderId) REFERENCES work_orders(workOrderId)
62
+ );
63
+ `);
64
+ }
65
+ async insertPointer(row) {
66
+ await this.writeMutex.runExclusive(() => {
67
+ this.db.prepare(`
68
+ INSERT OR REPLACE INTO pointers
69
+ (pointerId, scanId, artifactName, contentHash, blobPath, schemaVersion, createdAt, expiresAt)
70
+ VALUES
71
+ (@pointerId, @scanId, @artifactName, @contentHash, @blobPath, @schemaVersion, @createdAt, @expiresAt)
72
+ `).run(row);
73
+ });
74
+ }
75
+ getPointer(pointerId) {
76
+ return this.db.prepare('SELECT * FROM pointers WHERE pointerId = ?').get(pointerId) ?? null;
77
+ }
78
+ listPointersByScan(scanId) {
79
+ return this.db.prepare('SELECT * FROM pointers WHERE scanId = ?').all(scanId);
80
+ }
81
+ async insertWorkOrder(row) {
82
+ await this.writeMutex.runExclusive(() => {
83
+ this.db.prepare(`
84
+ INSERT OR REPLACE INTO work_orders
85
+ (workOrderId, intent, allowedFiles, allowedGlobs, deniedGlobs, requiredProof, status, createdAt)
86
+ VALUES
87
+ (@workOrderId, @intent, @allowedFiles, @allowedGlobs, @deniedGlobs, @requiredProof, @status, @createdAt)
88
+ `).run(row);
89
+ });
90
+ }
91
+ getWorkOrder(workOrderId) {
92
+ return this.db.prepare('SELECT * FROM work_orders WHERE workOrderId = ?').get(workOrderId) ?? null;
93
+ }
94
+ async updateWorkOrderStatus(workOrderId, status) {
95
+ await this.writeMutex.runExclusive(() => {
96
+ this.db.prepare('UPDATE work_orders SET status = ? WHERE workOrderId = ?').run(status, workOrderId);
97
+ });
98
+ }
99
+ async insertReceipt(receipt) {
100
+ await this.writeMutex.runExclusive(() => {
101
+ this.db.prepare(`
102
+ INSERT OR REPLACE INTO receipts
103
+ (receiptId, workOrderId, status, proofPointers, changedFiles, summary, createdAt)
104
+ VALUES
105
+ (@receiptId, @workOrderId, @status, @proofPointers, @changedFiles, @summary, @createdAt)
106
+ `).run({
107
+ ...receipt,
108
+ proofPointers: JSON.stringify(receipt.proofPointers),
109
+ changedFiles: JSON.stringify(receipt.changedFiles),
110
+ createdAt: Date.now(),
111
+ });
112
+ });
113
+ }
114
+ /** GC: delete pointers older than cutoffMs and not pinned, return deleted count */
115
+ async gcScanPointers(keepScanIds) {
116
+ return await this.writeMutex.runExclusive(() => {
117
+ const placeholders = keepScanIds.map(() => '?').join(',');
118
+ const whereClause = keepScanIds.length > 0
119
+ ? `WHERE scanId NOT IN (${placeholders})`
120
+ : '';
121
+ const result = this.db.prepare(`DELETE FROM pointers ${whereClause}`).run(...keepScanIds);
122
+ return result.changes;
123
+ });
124
+ }
125
+ listAllScanIds() {
126
+ const rows = this.db.prepare('SELECT DISTINCT scanId FROM pointers').all();
127
+ return rows.map(r => r.scanId);
128
+ }
129
+ countPointers() {
130
+ const row = this.db.prepare('SELECT COUNT(*) as cnt FROM pointers').get();
131
+ return row.cnt;
132
+ }
133
+ close() {
134
+ this.db.close();
135
+ instance = null;
136
+ }
137
+ }
138
+ //# sourceMappingURL=PointerStore.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-splain",
3
- "version": "3.1.0",
3
+ "version": "3.2.1",
4
4
  "description": "Architectural mapping and behavioral call-chain engine. Built on a language-agnostic foundation with specialized optimization for TypeScript/JavaScript projects.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,17 +38,24 @@
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.0.0",
40
40
  "async-mutex": "^0.5.0",
41
+ "better-sqlite3": "^12.10.0",
41
42
  "chokidar": "^3.6.0",
42
43
  "commander": "^12.0.0",
43
44
  "fs-extra": "^11.0.0",
45
+ "tar": "^7.5.16",
44
46
  "tree-sitter-wasms": "^0.1.11",
45
47
  "uuid": "^9.0.0",
46
48
  "web-tree-sitter": "^0.22.0"
47
49
  },
48
50
  "devDependencies": {
51
+ "@types/better-sqlite3": "^7.6.13",
49
52
  "@types/fs-extra": "^11.0.0",
53
+ "@types/minimatch": "^5.1.2",
54
+ "@types/tar": "^6.1.13",
50
55
  "@types/uuid": "^9.0.0",
51
56
  "esbuild": "^0.28.0",
57
+ "minimatch": "^10.2.5",
58
+ "tsx": "^4.22.4",
52
59
  "typescript": "^5.4.0"
53
60
  },
54
61
  "files": [