vibe-splain 3.1.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/ExportOrchestrator.d.ts +19 -1
- package/dist/export/ExportOrchestrator.js +87 -1
- package/dist/index.js +1498 -36
- 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 +5 -2
- 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,68 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { writeFile, mkdir, copyFile, rm } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import * as tar from 'tar';
|
|
5
|
+
import { PointerStore } from '../store/PointerStore.js';
|
|
6
|
+
import { BlobStore } from '../store/BlobStore.js';
|
|
7
|
+
export async function bundleCommand(scanId, opts = {}) {
|
|
8
|
+
const root = opts.projectRoot ?? process.cwd();
|
|
9
|
+
const outputPath = opts.output ?? join(root, `vibe-bundle-${scanId}.tar.gz`);
|
|
10
|
+
console.error(`[vibe-splain bundle] Bundling scan ${scanId} from ${root}`);
|
|
11
|
+
const pointerStore = PointerStore.open(root);
|
|
12
|
+
const blobStore = new BlobStore(root);
|
|
13
|
+
const pointers = pointerStore.listPointersByScan(scanId);
|
|
14
|
+
if (pointers.length === 0) {
|
|
15
|
+
throw new Error(`No pointers found for scanId "${scanId}"`);
|
|
16
|
+
}
|
|
17
|
+
// Stage bundle into a temp directory with predictable layout
|
|
18
|
+
const stagingDir = join(root, '.vibe-splainer', 'tmp', `bundle-stage-${scanId}`);
|
|
19
|
+
const blobsStageDir = join(stagingDir, 'blobs');
|
|
20
|
+
await mkdir(blobsStageDir, { recursive: true });
|
|
21
|
+
try {
|
|
22
|
+
// Build manifest for the bundle
|
|
23
|
+
const bundleManifest = {
|
|
24
|
+
schemaVersion: '1.0.0',
|
|
25
|
+
scanId,
|
|
26
|
+
exportedAt: new Date().toISOString(),
|
|
27
|
+
projectRoot: root,
|
|
28
|
+
pointers: pointers.map(p => ({
|
|
29
|
+
pointerId: p.pointerId,
|
|
30
|
+
scanId: p.scanId,
|
|
31
|
+
artifactName: p.artifactName,
|
|
32
|
+
contentHash: p.contentHash,
|
|
33
|
+
blobFile: `blobs/${p.contentHash.replace('sha256:', 'sha256_')}`,
|
|
34
|
+
schemaVersion: p.schemaVersion,
|
|
35
|
+
createdAt: p.createdAt,
|
|
36
|
+
expiresAt: p.expiresAt,
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
await writeFile(join(stagingDir, 'bundle-manifest.json'), JSON.stringify(bundleManifest, null, 2), 'utf8');
|
|
40
|
+
// Copy blobs (deduplicated by contentHash)
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
for (const p of pointers) {
|
|
43
|
+
const hex = p.contentHash.replace('sha256:', '');
|
|
44
|
+
if (seen.has(hex))
|
|
45
|
+
continue;
|
|
46
|
+
seen.add(hex);
|
|
47
|
+
const srcPath = p.blobPath;
|
|
48
|
+
if (!existsSync(srcPath)) {
|
|
49
|
+
console.error(`[vibe-splain bundle] Warning: blob missing for ${p.pointerId}: ${srcPath}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
await copyFile(srcPath, join(blobsStageDir, `sha256_${hex}`));
|
|
53
|
+
}
|
|
54
|
+
// Create tarball from staging directory
|
|
55
|
+
await tar.create({
|
|
56
|
+
gzip: true,
|
|
57
|
+
file: outputPath,
|
|
58
|
+
cwd: stagingDir,
|
|
59
|
+
portable: true,
|
|
60
|
+
}, ['.']);
|
|
61
|
+
console.error(`[vibe-splain bundle] Bundle written: ${outputPath}`);
|
|
62
|
+
console.error(`[vibe-splain bundle] ${pointers.length} pointers, ${seen.size} blobs`);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=bundle.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { rm, readdir } from 'fs/promises';
|
|
3
|
+
import { PointerStore } from '../store/PointerStore.js';
|
|
4
|
+
import { BlobStore } from '../store/BlobStore.js';
|
|
5
|
+
const DEFAULT_KEEP_SCANS = 3;
|
|
6
|
+
export async function gcCommand(projectRoot, opts = {}) {
|
|
7
|
+
const root = projectRoot ?? process.cwd();
|
|
8
|
+
const keepScans = opts.keepScans ?? DEFAULT_KEEP_SCANS;
|
|
9
|
+
console.error(`[vibe-splain gc] Running GC on ${root} (keeping last ${keepScans} scans)`);
|
|
10
|
+
const pointerStore = PointerStore.open(root);
|
|
11
|
+
const blobStore = new BlobStore(root);
|
|
12
|
+
// 1. Get all scan IDs ordered by createdAt desc
|
|
13
|
+
const allScanIds = pointerStore.listAllScanIds();
|
|
14
|
+
console.error(`[vibe-splain gc] Found ${allScanIds.length} scans`);
|
|
15
|
+
// Keep the N most recent by taking last N from sorted list
|
|
16
|
+
// Scan IDs contain timestamps, sort lexicographically descending
|
|
17
|
+
const sorted = [...allScanIds].sort().reverse();
|
|
18
|
+
const keepIds = sorted.slice(0, keepScans);
|
|
19
|
+
const deleteIds = sorted.slice(keepScans);
|
|
20
|
+
if (deleteIds.length === 0) {
|
|
21
|
+
console.error('[vibe-splain gc] Nothing to collect');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// 2. Collect all blob paths still referenced by kept pointers before deletion
|
|
25
|
+
const keptPointers = keepIds.flatMap(id => pointerStore.listPointersByScan(id));
|
|
26
|
+
const referencedBlobs = new Set(keptPointers.map(p => p.blobPath));
|
|
27
|
+
// 3. Delete old scan pointers
|
|
28
|
+
const deleted = await pointerStore.gcScanPointers(keepIds);
|
|
29
|
+
console.error(`[vibe-splain gc] Deleted ${deleted} pointer rows`);
|
|
30
|
+
// 4. Delete unreferenced blobs (reference count = 0)
|
|
31
|
+
const allBlobs = await blobStore.listBlobPaths();
|
|
32
|
+
let blobsDeleted = 0;
|
|
33
|
+
for (const blobPath of allBlobs) {
|
|
34
|
+
if (!referencedBlobs.has(blobPath)) {
|
|
35
|
+
try {
|
|
36
|
+
await rm(blobPath);
|
|
37
|
+
blobsDeleted++;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// ignore — may have already been deleted
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
console.error(`[vibe-splain gc] Deleted ${blobsDeleted} unreferenced blobs`);
|
|
45
|
+
// 5. Clean up tmp dir
|
|
46
|
+
const tmpDir = join(root, '.vibe-splainer', 'tmp');
|
|
47
|
+
try {
|
|
48
|
+
const tmpFiles = await readdir(tmpDir);
|
|
49
|
+
for (const f of tmpFiles) {
|
|
50
|
+
await rm(join(tmpDir, f), { force: true });
|
|
51
|
+
}
|
|
52
|
+
console.error(`[vibe-splain gc] Cleaned ${tmpFiles.length} tmp files`);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// tmp dir may not exist
|
|
56
|
+
}
|
|
57
|
+
console.error('[vibe-splain gc] Done');
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=gc.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { readFile, mkdir, rm } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import * as tar from 'tar';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
import { BlobStore } from '../store/BlobStore.js';
|
|
7
|
+
import { PointerStore } from '../store/PointerStore.js';
|
|
8
|
+
export async function importBundleCommand(tarballPath, opts = {}) {
|
|
9
|
+
const root = opts.projectRoot ?? process.cwd();
|
|
10
|
+
const namespace = opts.namespace ?? `imported_${Date.now()}`;
|
|
11
|
+
console.error(`[vibe-splain import] Importing ${tarballPath} into ${root} (namespace: ${namespace})`);
|
|
12
|
+
if (!existsSync(tarballPath)) {
|
|
13
|
+
throw new Error(`Tarball not found: ${tarballPath}`);
|
|
14
|
+
}
|
|
15
|
+
// Extract to a temp directory
|
|
16
|
+
const extractDir = join(root, '.vibe-splainer', 'tmp', `import-${namespace}`);
|
|
17
|
+
await mkdir(extractDir, { recursive: true });
|
|
18
|
+
try {
|
|
19
|
+
await tar.extract({
|
|
20
|
+
file: tarballPath,
|
|
21
|
+
cwd: extractDir,
|
|
22
|
+
});
|
|
23
|
+
// Read bundle manifest
|
|
24
|
+
const manifestPath = join(extractDir, 'bundle-manifest.json');
|
|
25
|
+
if (!existsSync(manifestPath)) {
|
|
26
|
+
throw new Error('Invalid bundle: missing bundle-manifest.json');
|
|
27
|
+
}
|
|
28
|
+
const manifestRaw = await readFile(manifestPath, 'utf8');
|
|
29
|
+
const manifest = JSON.parse(manifestRaw);
|
|
30
|
+
if (manifest.schemaVersion !== '1.0.0') {
|
|
31
|
+
throw new Error(`Unsupported bundle schema version: ${manifest.schemaVersion}`);
|
|
32
|
+
}
|
|
33
|
+
const blobStore = new BlobStore(root);
|
|
34
|
+
const pointerStore = PointerStore.open(root);
|
|
35
|
+
await blobStore.ensureDirs();
|
|
36
|
+
let imported = 0;
|
|
37
|
+
let hashErrors = 0;
|
|
38
|
+
for (const entry of manifest.pointers) {
|
|
39
|
+
const blobSrcPath = join(extractDir, entry.blobFile);
|
|
40
|
+
if (!existsSync(blobSrcPath)) {
|
|
41
|
+
console.error(`[vibe-splain import] Missing blob for pointer ${entry.pointerId}: ${entry.blobFile}`);
|
|
42
|
+
hashErrors++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Verify hash before importing
|
|
46
|
+
const content = await readFile(blobSrcPath);
|
|
47
|
+
const actualHash = `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
48
|
+
if (actualHash !== entry.contentHash) {
|
|
49
|
+
console.error(`[vibe-splain import] Hash mismatch for ${entry.pointerId}: expected ${entry.contentHash}, got ${actualHash}`);
|
|
50
|
+
hashErrors++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// Write blob to local store (atomic)
|
|
54
|
+
const { blobPath } = await blobStore.writeAtomic(content);
|
|
55
|
+
// Insert pointer under bundle namespace alias
|
|
56
|
+
const namespacedPointerId = `${namespace}::${entry.pointerId}`;
|
|
57
|
+
const namespacedScanId = `${namespace}::${entry.scanId}`;
|
|
58
|
+
await pointerStore.insertPointer({
|
|
59
|
+
pointerId: namespacedPointerId,
|
|
60
|
+
scanId: namespacedScanId,
|
|
61
|
+
artifactName: entry.artifactName,
|
|
62
|
+
contentHash: entry.contentHash,
|
|
63
|
+
blobPath,
|
|
64
|
+
schemaVersion: entry.schemaVersion,
|
|
65
|
+
createdAt: entry.createdAt,
|
|
66
|
+
expiresAt: entry.expiresAt,
|
|
67
|
+
});
|
|
68
|
+
imported++;
|
|
69
|
+
}
|
|
70
|
+
if (hashErrors > 0) {
|
|
71
|
+
console.error(`[vibe-splain import] Warning: ${hashErrors} blobs failed hash verification and were skipped`);
|
|
72
|
+
}
|
|
73
|
+
console.error(`[vibe-splain import] Imported ${imported}/${manifest.pointers.length} pointers under namespace "${namespace}"`);
|
|
74
|
+
console.error(`[vibe-splain import] Original scanId: ${manifest.scanId} → namespaced as: ${namespace}::${manifest.scanId}`);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await rm(extractDir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=importBundle.js.map
|
|
@@ -4,9 +4,27 @@ export interface ExportOptions {
|
|
|
4
4
|
budget?: number;
|
|
5
5
|
scope?: string;
|
|
6
6
|
}
|
|
7
|
+
export interface ManifestArtifactEntry {
|
|
8
|
+
name: string;
|
|
9
|
+
pointer: string;
|
|
10
|
+
contentHash: string;
|
|
11
|
+
sizeBytes: number;
|
|
12
|
+
indexes?: Record<string, string>;
|
|
13
|
+
hydrators?: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface ScanManifest {
|
|
16
|
+
schemaVersion: '2.0.0';
|
|
17
|
+
scanId: string;
|
|
18
|
+
generatedAt: string;
|
|
19
|
+
projectRoot: string;
|
|
20
|
+
artifacts: ManifestArtifactEntry[];
|
|
21
|
+
}
|
|
7
22
|
export declare class ExportOrchestrator {
|
|
8
23
|
private projectRoot;
|
|
9
24
|
constructor(projectRoot: string);
|
|
10
|
-
writeBundle(dossier: Dossier, options?: ExportOptions, store?: AnalysisStore, graph?: ImportGraph): Promise<
|
|
25
|
+
writeBundle(dossier: Dossier, options?: ExportOptions, store?: AnalysisStore, graph?: ImportGraph, scanId?: string): Promise<{
|
|
26
|
+
scanId: string;
|
|
27
|
+
manifestPointer: string;
|
|
28
|
+
}>;
|
|
11
29
|
private buildViewModel;
|
|
12
30
|
}
|
|
@@ -7,12 +7,15 @@ import { AgentMarkdownRenderer } from './renderers/AgentMarkdownRenderer.js';
|
|
|
7
7
|
import { ValidationRenderer } from './renderers/ValidationRenderer.js';
|
|
8
8
|
import { RawAnalysisRenderer } from './renderers/RawAnalysisRenderer.js';
|
|
9
9
|
import { GraphRenderer } from './renderers/GraphRenderer.js';
|
|
10
|
+
import { BlobStore } from '../store/BlobStore.js';
|
|
11
|
+
import { PointerStore } from '../store/PointerStore.js';
|
|
12
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
10
13
|
export class ExportOrchestrator {
|
|
11
14
|
projectRoot;
|
|
12
15
|
constructor(projectRoot) {
|
|
13
16
|
this.projectRoot = projectRoot;
|
|
14
17
|
}
|
|
15
|
-
async writeBundle(dossier, options = {}, store, graph) {
|
|
18
|
+
async writeBundle(dossier, options = {}, store, graph, scanId) {
|
|
16
19
|
const finalStore = store || await readAnalysis(this.projectRoot);
|
|
17
20
|
if (!finalStore) {
|
|
18
21
|
throw new Error('Analysis store not found. Scan the project first.');
|
|
@@ -47,6 +50,89 @@ export class ExportOrchestrator {
|
|
|
47
50
|
}
|
|
48
51
|
const writer = new ArtifactBundleWriter(this.projectRoot);
|
|
49
52
|
await writer.writeBundle(artifacts);
|
|
53
|
+
// Register artifacts in BlobStore + PointerStore and build manifest
|
|
54
|
+
const effectiveScanId = scanId ?? `scan_${Date.now()}`;
|
|
55
|
+
const blobStore = new BlobStore(this.projectRoot);
|
|
56
|
+
const pointerStore = PointerStore.open(this.projectRoot);
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const manifestEntries = [];
|
|
59
|
+
for (const artifact of artifacts) {
|
|
60
|
+
const content = typeof artifact.content === 'string'
|
|
61
|
+
? Buffer.from(artifact.content, 'utf8')
|
|
62
|
+
: artifact.content;
|
|
63
|
+
const { contentHash, blobPath } = await blobStore.writeAtomic(content);
|
|
64
|
+
const pointerId = `ptr_${uuidv4().replace(/-/g, '').slice(0, 16)}`;
|
|
65
|
+
await pointerStore.insertPointer({
|
|
66
|
+
pointerId,
|
|
67
|
+
scanId: effectiveScanId,
|
|
68
|
+
artifactName: artifact.type,
|
|
69
|
+
contentHash,
|
|
70
|
+
blobPath,
|
|
71
|
+
schemaVersion: '1.0.0',
|
|
72
|
+
createdAt: now,
|
|
73
|
+
expiresAt: null,
|
|
74
|
+
});
|
|
75
|
+
const entry = {
|
|
76
|
+
name: artifact.type,
|
|
77
|
+
pointer: pointerId,
|
|
78
|
+
contentHash,
|
|
79
|
+
sizeBytes: content.length,
|
|
80
|
+
};
|
|
81
|
+
// Attach hydrator hints for the large analysis artifact
|
|
82
|
+
if (artifact.type === 'analysis') {
|
|
83
|
+
entry.hydrators = ['get_project_summary', 'get_start_here'];
|
|
84
|
+
// Generate analysis.index.json (Start-Here + Top-Heat)
|
|
85
|
+
const analysisIndex = {
|
|
86
|
+
schemaVersion: '1.0.0',
|
|
87
|
+
scanId: effectiveScanId,
|
|
88
|
+
startHere: dossier.map.topGravity.slice(0, 12),
|
|
89
|
+
topHeat: dossier.map.topHeat.slice(0, 12),
|
|
90
|
+
pillarSummary: dossier.map.pillars.map(p => ({
|
|
91
|
+
name: p.name,
|
|
92
|
+
fileCount: p.memberFiles?.length ?? 0,
|
|
93
|
+
})),
|
|
94
|
+
totalFiles: Object.keys(finalStore.files).length,
|
|
95
|
+
realSourceFiles: Object.values(finalStore.files).filter(f => f.isRealSource).length,
|
|
96
|
+
};
|
|
97
|
+
const indexContent = Buffer.from(JSON.stringify(analysisIndex, null, 2), 'utf8');
|
|
98
|
+
const indexWrite = await blobStore.writeAtomic(indexContent);
|
|
99
|
+
const indexPointerId = `ptr_${uuidv4().replace(/-/g, '').slice(0, 16)}`;
|
|
100
|
+
await pointerStore.insertPointer({
|
|
101
|
+
pointerId: indexPointerId,
|
|
102
|
+
scanId: effectiveScanId,
|
|
103
|
+
artifactName: 'analysis.index',
|
|
104
|
+
contentHash: indexWrite.contentHash,
|
|
105
|
+
blobPath: indexWrite.blobPath,
|
|
106
|
+
schemaVersion: '1.0.0',
|
|
107
|
+
createdAt: now,
|
|
108
|
+
expiresAt: null,
|
|
109
|
+
});
|
|
110
|
+
entry.indexes = { startHere: indexPointerId };
|
|
111
|
+
}
|
|
112
|
+
manifestEntries.push(entry);
|
|
113
|
+
}
|
|
114
|
+
// Write and register the scan manifest itself
|
|
115
|
+
const manifest = {
|
|
116
|
+
schemaVersion: '2.0.0',
|
|
117
|
+
scanId: effectiveScanId,
|
|
118
|
+
generatedAt: new Date(now).toISOString(),
|
|
119
|
+
projectRoot: this.projectRoot,
|
|
120
|
+
artifacts: manifestEntries,
|
|
121
|
+
};
|
|
122
|
+
const manifestContent = Buffer.from(JSON.stringify(manifest, null, 2), 'utf8');
|
|
123
|
+
const manifestWrite = await blobStore.writeAtomic(manifestContent);
|
|
124
|
+
const manifestPointerId = `ptr_manifest_${effectiveScanId}`;
|
|
125
|
+
await pointerStore.insertPointer({
|
|
126
|
+
pointerId: manifestPointerId,
|
|
127
|
+
scanId: effectiveScanId,
|
|
128
|
+
artifactName: 'artifact_manifest',
|
|
129
|
+
contentHash: manifestWrite.contentHash,
|
|
130
|
+
blobPath: manifestWrite.blobPath,
|
|
131
|
+
schemaVersion: '2.0.0',
|
|
132
|
+
createdAt: now,
|
|
133
|
+
expiresAt: null,
|
|
134
|
+
});
|
|
135
|
+
return { scanId: effectiveScanId, manifestPointer: manifestPointerId };
|
|
50
136
|
}
|
|
51
137
|
buildViewModel(dossier, store) {
|
|
52
138
|
const recommendations = {};
|