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,4 @@
1
+ export declare function bundleCommand(scanId: string, opts?: {
2
+ output?: string;
3
+ projectRoot?: string;
4
+ }): Promise<void>;
@@ -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,3 @@
1
+ export declare function gcCommand(projectRoot?: string, opts?: {
2
+ keepScans?: number;
3
+ }): Promise<void>;
@@ -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,4 @@
1
+ export declare function importBundleCommand(tarballPath: string, opts?: {
2
+ projectRoot?: string;
3
+ namespace?: string;
4
+ }): Promise<void>;
@@ -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<void>;
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 = {};