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.
Files changed (46) 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/ArtifactBundleWriter.js +24 -6
  8. package/dist/export/ExportOrchestrator.d.ts +19 -1
  9. package/dist/export/ExportOrchestrator.js +90 -3
  10. package/dist/export/Watcher.d.ts +1 -1
  11. package/dist/export/Watcher.js +9 -1
  12. package/dist/export/renderers/AgentMarkdownRenderer.d.ts +2 -1
  13. package/dist/export/renderers/AgentMarkdownRenderer.js +17 -1
  14. package/dist/export/renderers/HtmlRenderer.js +29 -6
  15. package/dist/index.js +1671 -129
  16. package/dist/mcp/BudgetGuard.d.ts +13 -0
  17. package/dist/mcp/BudgetGuard.js +55 -0
  18. package/dist/mcp/SessionScope.d.ts +26 -0
  19. package/dist/mcp/SessionScope.js +56 -0
  20. package/dist/mcp/server.js +38 -0
  21. package/dist/mcp/tools/apply_patch.d.ts +37 -0
  22. package/dist/mcp/tools/apply_patch.js +103 -0
  23. package/dist/mcp/tools/get_file_skeleton.d.ts +23 -0
  24. package/dist/mcp/tools/get_file_skeleton.js +124 -0
  25. package/dist/mcp/tools/hydration/get_evidence_slice.d.ts +31 -0
  26. package/dist/mcp/tools/hydration/get_evidence_slice.js +59 -0
  27. package/dist/mcp/tools/hydration/get_project_summary.d.ts +23 -0
  28. package/dist/mcp/tools/hydration/get_project_summary.js +58 -0
  29. package/dist/mcp/tools/hydration/get_start_here.d.ts +23 -0
  30. package/dist/mcp/tools/hydration/get_start_here.js +52 -0
  31. package/dist/mcp/tools/read_file.d.ts +31 -0
  32. package/dist/mcp/tools/read_file.js +90 -0
  33. package/dist/mcp/tools/scan_project.js +6 -3
  34. package/dist/mcp/tools/set_session_scope.d.ts +19 -0
  35. package/dist/mcp/tools/set_session_scope.js +40 -0
  36. package/dist/mcp/tools/submit_receipt.d.ts +68 -0
  37. package/dist/mcp/tools/submit_receipt.js +94 -0
  38. package/dist/mcp/tools/work_orders.d.ts +79 -0
  39. package/dist/mcp/tools/work_orders.js +126 -0
  40. package/dist/mcp/tools/yield_for_scope_expansion.d.ts +29 -0
  41. package/dist/mcp/tools/yield_for_scope_expansion.js +59 -0
  42. package/dist/store/BlobStore.d.ts +22 -0
  43. package/dist/store/BlobStore.js +96 -0
  44. package/dist/store/PointerStore.d.ts +52 -0
  45. package/dist/store/PointerStore.js +138 -0
  46. 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
@@ -9,8 +9,10 @@ export class ArtifactBundleWriter {
9
9
  async writeBundle(artifacts) {
10
10
  const outputDir = join(this.projectRoot, '.vibe-splainer');
11
11
  const stagingDir = join(this.projectRoot, '.vibe-splainer.tmp');
12
+ const oldDir = join(this.projectRoot, '.vibe-splainer.old');
12
13
  try {
13
14
  await rm(stagingDir, { recursive: true, force: true });
15
+ await rm(oldDir, { recursive: true, force: true });
14
16
  const { existsSync } = await import('fs');
15
17
  const { cp } = await import('fs/promises');
16
18
  if (existsSync(outputDir)) {
@@ -40,12 +42,28 @@ export class ArtifactBundleWriter {
40
42
  artifacts: manifestArtifacts,
41
43
  };
42
44
  await writeFile(join(stagingDir, 'artifact_manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
43
- // Atomic rename.
44
- // Rename fails if destination is a non-empty directory.
45
- // So we first delete the existing outputDir.
46
- // Since it's inside the project root, it's safe to do rm -rf .vibe-splainer
47
- await rm(outputDir, { recursive: true, force: true });
48
- await rename(stagingDir, outputDir);
45
+ // Atomic swap pattern:
46
+ // 1. Rename current -> old
47
+ // 2. Rename staging -> current
48
+ // 3. Remove old
49
+ let swapped = false;
50
+ if (existsSync(outputDir)) {
51
+ await rename(outputDir, oldDir);
52
+ swapped = true;
53
+ }
54
+ try {
55
+ await rename(stagingDir, outputDir);
56
+ }
57
+ catch (err) {
58
+ // Rollback if possible
59
+ if (swapped) {
60
+ await rename(oldDir, outputDir);
61
+ }
62
+ throw err;
63
+ }
64
+ if (swapped) {
65
+ await rm(oldDir, { recursive: true, force: true });
66
+ }
49
67
  }
50
68
  catch (err) {
51
69
  await rm(stagingDir, { recursive: true, force: true });
@@ -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
  }
@@ -1,4 +1,4 @@
1
- import { readAnalysis, RecommendationEngine } from '@vibe-splain/brain';
1
+ import { readAnalysis, readActionBindings, RecommendationEngine } from '@vibe-splain/brain';
2
2
  import { ArtifactBundleWriter } from './ArtifactBundleWriter.js';
3
3
  import { JsonRenderer } from './renderers/JsonRenderer.js';
4
4
  import { HtmlRenderer } from './renderers/HtmlRenderer.js';
@@ -7,16 +7,20 @@ 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.');
19
22
  }
23
+ const bindings = await readActionBindings(this.projectRoot);
20
24
  // Aggressive Boilerplate Culling
21
25
  for (const p of dossier.pillars) {
22
26
  p.decisions = p.decisions.filter(c => !(c.severity === 1 && c.category === 'Convention'));
@@ -42,10 +46,93 @@ export class ExportOrchestrator {
42
46
  artifacts.push(...await new HtmlRenderer().render(viewModel, finalStore));
43
47
  }
44
48
  if (formats.includes('markdown')) {
45
- artifacts.push(...await new AgentMarkdownRenderer(options.budget).render(viewModel, finalStore));
49
+ artifacts.push(...await new AgentMarkdownRenderer(options.budget, bindings).render(viewModel, finalStore));
46
50
  }
47
51
  const writer = new ArtifactBundleWriter(this.projectRoot);
48
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 };
49
136
  }
50
137
  buildViewModel(dossier, store) {
51
138
  const recommendations = {};
@@ -1 +1 @@
1
- export declare function startWatcher(projectRoot: string, watchedPaths: string[]): void;
1
+ export declare function startWatcher(projectRoot: string, watchedPaths: string[]): Promise<void>;
@@ -4,7 +4,14 @@ import { readFile } from 'fs/promises';
4
4
  import { join } from 'path';
5
5
  import { readDossier } from '@vibe-splain/brain';
6
6
  import { ExportOrchestrator } from './ExportOrchestrator.js';
7
- export function startWatcher(projectRoot, watchedPaths) {
7
+ const activeWatchers = new Map();
8
+ export async function startWatcher(projectRoot, watchedPaths) {
9
+ // Clean up existing watcher for this project to prevent resource leaks
10
+ const existing = activeWatchers.get(projectRoot);
11
+ if (existing) {
12
+ await existing.close();
13
+ activeWatchers.delete(projectRoot);
14
+ }
8
15
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
9
16
  ignoreInitial: true,
10
17
  ignored: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.vibe-splainer/**'],
@@ -42,6 +49,7 @@ export function startWatcher(projectRoot, watchedPaths) {
42
49
  console.error('[vibe-splain] Watcher error:', err);
43
50
  }
44
51
  });
52
+ activeWatchers.set(projectRoot, watcher);
45
53
  console.error('[vibe-splain] File watcher started');
46
54
  }
47
55
  //# sourceMappingURL=Watcher.js.map
@@ -3,6 +3,7 @@ import type { Renderer } from './Renderer.js';
3
3
  import type { Artifact } from '../ArtifactBundleWriter.js';
4
4
  export declare class AgentMarkdownRenderer implements Renderer {
5
5
  private budget;
6
- constructor(budget?: number);
6
+ private bindings;
7
+ constructor(budget?: number, bindings?: any | null);
7
8
  render(viewModel: DossierViewModel, store: AnalysisStore): Artifact[];
8
9
  }
@@ -1,7 +1,9 @@
1
1
  export class AgentMarkdownRenderer {
2
2
  budget;
3
- constructor(budget = 8000) {
3
+ bindings;
4
+ constructor(budget = 8000, bindings = null) {
4
5
  this.budget = budget;
6
+ this.bindings = bindings;
5
7
  }
6
8
  render(viewModel, store) {
7
9
  let md = `# Architectural Dossier: ${viewModel.projectRoot}\n\n`;
@@ -52,6 +54,20 @@ export class AgentMarkdownRenderer {
52
54
  md += `**Severity**: ${card.severity} | **Category**: ${card.category}\n`;
53
55
  md += `**Narrative**: ${card.narrative}\n`;
54
56
  }
57
+ // Add Function-Level Action Bindings for Tier 1
58
+ if (this.bindings && this.bindings.files[path]) {
59
+ const fileBinding = this.bindings.files[path];
60
+ const criticalFunctions = fileBinding.functions.filter((fn) => fn.semanticActions.length > 0 || fn.isEntrypoint);
61
+ if (criticalFunctions.length > 0) {
62
+ md += `\n**Critical Functions**:\n`;
63
+ for (const fn of criticalFunctions) {
64
+ md += `- \`${fn.displayName}\` (lines ${fn.startLine}-${fn.endLine})${fn.isEntrypoint ? ' [Entrypoint]' : ''}\n`;
65
+ for (const action of fn.semanticActions) {
66
+ md += ` - **${action.actionKind}**${action.targetModel ? ` on ${action.targetModel}` : ''}: \`${action.calleeText}\` (line ${action.sourceLine})\n`;
67
+ }
68
+ }
69
+ }
70
+ }
55
71
  if (recs.length > 0) {
56
72
  md += `\n**Safe Patch Strategies**:\n`;
57
73
  for (const r of recs) {
@@ -17,13 +17,36 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
17
17
  }
18
18
  export class HtmlRenderer {
19
19
  render(viewModel, _store) {
20
- let templateDir = join(__dirname, '..', '..', 'ui'); // from dist/export/renderers -> dist/ui
21
- if (!existsSync(templateDir)) {
22
- // Unbundled path (from cli/src/export/renderers -> cli/dist/ui)
23
- templateDir = join(__dirname, '..', '..', '..', '..', 'dist', 'ui');
20
+ // Robust template resolution for bundled/unbundled environments
21
+ const candidatePaths = [
22
+ join(__dirname, 'ui'), // bundled: dist/index.js -> dist/ui
23
+ join(__dirname, '..', '..', 'ui'), // unbundled: dist/export/renderers -> dist/ui
24
+ join(__dirname, '..', 'ui'), // alt bundle
25
+ join(__dirname, '..', '..', '..', 'ui', 'dist'), // dev: packages/cli/src/export/renderers -> packages/ui/dist
26
+ join(__dirname, '..', '..', 'packages', 'ui', 'dist'), // repo root -> packages/ui/dist
27
+ ];
28
+ let templateDir = '';
29
+ for (const p of candidatePaths) {
30
+ if (existsSync(p) && existsSync(join(p, 'index.html'))) {
31
+ // Double check it's not the source packages/ui (which has vite.config.ts)
32
+ // We want the BUILT UI in the dist folder.
33
+ if (!existsSync(join(p, 'vite.config.ts')) || p.endsWith('dist')) {
34
+ templateDir = p;
35
+ break;
36
+ }
37
+ }
38
+ }
39
+ // Fallback to any 'ui' folder that has index.html as a last resort
40
+ if (!templateDir) {
41
+ for (const p of candidatePaths) {
42
+ if (existsSync(join(p, 'index.html'))) {
43
+ templateDir = p;
44
+ break;
45
+ }
46
+ }
24
47
  }
25
- if (!existsSync(templateDir)) {
26
- console.error('[vibe-splain] UI template not found at', templateDir, '- skipping UI regeneration');
48
+ if (!templateDir) {
49
+ console.error('[vibe-splain] UI template not found. Checked:', candidatePaths);
27
50
  return [];
28
51
  }
29
52
  const artifacts = [];