viberag 0.1.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 (151) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +219 -0
  3. package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
  4. package/dist/cli/__tests__/mcp-setup.test.js +597 -0
  5. package/dist/cli/app.d.ts +2 -0
  6. package/dist/cli/app.js +238 -0
  7. package/dist/cli/commands/handlers.d.ts +57 -0
  8. package/dist/cli/commands/handlers.js +231 -0
  9. package/dist/cli/commands/index.d.ts +2 -0
  10. package/dist/cli/commands/index.js +2 -0
  11. package/dist/cli/commands/mcp-setup.d.ts +107 -0
  12. package/dist/cli/commands/mcp-setup.js +509 -0
  13. package/dist/cli/commands/useRagCommands.d.ts +23 -0
  14. package/dist/cli/commands/useRagCommands.js +180 -0
  15. package/dist/cli/components/CleanWizard.d.ts +17 -0
  16. package/dist/cli/components/CleanWizard.js +169 -0
  17. package/dist/cli/components/InitWizard.d.ts +20 -0
  18. package/dist/cli/components/InitWizard.js +370 -0
  19. package/dist/cli/components/McpSetupWizard.d.ts +37 -0
  20. package/dist/cli/components/McpSetupWizard.js +387 -0
  21. package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
  22. package/dist/cli/components/SearchResultsDisplay.js +130 -0
  23. package/dist/cli/components/WelcomeBanner.d.ts +10 -0
  24. package/dist/cli/components/WelcomeBanner.js +26 -0
  25. package/dist/cli/components/index.d.ts +1 -0
  26. package/dist/cli/components/index.js +1 -0
  27. package/dist/cli/data/mcp-editors.d.ts +80 -0
  28. package/dist/cli/data/mcp-editors.js +270 -0
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.js +26 -0
  31. package/dist/cli-bundle.cjs +5269 -0
  32. package/dist/common/commands/terminalSetup.d.ts +2 -0
  33. package/dist/common/commands/terminalSetup.js +144 -0
  34. package/dist/common/components/CommandSuggestions.d.ts +9 -0
  35. package/dist/common/components/CommandSuggestions.js +20 -0
  36. package/dist/common/components/StaticWithResize.d.ts +23 -0
  37. package/dist/common/components/StaticWithResize.js +62 -0
  38. package/dist/common/components/StatusBar.d.ts +8 -0
  39. package/dist/common/components/StatusBar.js +64 -0
  40. package/dist/common/components/TextInput.d.ts +12 -0
  41. package/dist/common/components/TextInput.js +239 -0
  42. package/dist/common/components/index.d.ts +3 -0
  43. package/dist/common/components/index.js +3 -0
  44. package/dist/common/hooks/index.d.ts +4 -0
  45. package/dist/common/hooks/index.js +4 -0
  46. package/dist/common/hooks/useCommandHistory.d.ts +7 -0
  47. package/dist/common/hooks/useCommandHistory.js +51 -0
  48. package/dist/common/hooks/useCtrlC.d.ts +9 -0
  49. package/dist/common/hooks/useCtrlC.js +40 -0
  50. package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
  51. package/dist/common/hooks/useKittyKeyboard.js +26 -0
  52. package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
  53. package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
  54. package/dist/common/hooks/useTerminalResize.d.ts +28 -0
  55. package/dist/common/hooks/useTerminalResize.js +51 -0
  56. package/dist/common/hooks/useTextBuffer.d.ts +13 -0
  57. package/dist/common/hooks/useTextBuffer.js +165 -0
  58. package/dist/common/index.d.ts +13 -0
  59. package/dist/common/index.js +17 -0
  60. package/dist/common/types.d.ts +162 -0
  61. package/dist/common/types.js +1 -0
  62. package/dist/mcp/index.d.ts +12 -0
  63. package/dist/mcp/index.js +66 -0
  64. package/dist/mcp/server.d.ts +25 -0
  65. package/dist/mcp/server.js +837 -0
  66. package/dist/mcp/watcher.d.ts +86 -0
  67. package/dist/mcp/watcher.js +334 -0
  68. package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
  69. package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
  70. package/dist/rag/__tests__/helpers.d.ts +30 -0
  71. package/dist/rag/__tests__/helpers.js +67 -0
  72. package/dist/rag/__tests__/merkle.test.d.ts +5 -0
  73. package/dist/rag/__tests__/merkle.test.js +161 -0
  74. package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
  75. package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
  76. package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
  77. package/dist/rag/__tests__/multi-language.test.js +535 -0
  78. package/dist/rag/__tests__/rag.test.d.ts +10 -0
  79. package/dist/rag/__tests__/rag.test.js +311 -0
  80. package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
  81. package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
  82. package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
  83. package/dist/rag/__tests__/search-filters.test.js +250 -0
  84. package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
  85. package/dist/rag/__tests__/search-modes.test.js +133 -0
  86. package/dist/rag/config/index.d.ts +61 -0
  87. package/dist/rag/config/index.js +111 -0
  88. package/dist/rag/constants.d.ts +41 -0
  89. package/dist/rag/constants.js +57 -0
  90. package/dist/rag/embeddings/fastembed.d.ts +62 -0
  91. package/dist/rag/embeddings/fastembed.js +124 -0
  92. package/dist/rag/embeddings/gemini.d.ts +26 -0
  93. package/dist/rag/embeddings/gemini.js +116 -0
  94. package/dist/rag/embeddings/index.d.ts +10 -0
  95. package/dist/rag/embeddings/index.js +9 -0
  96. package/dist/rag/embeddings/local-4b.d.ts +28 -0
  97. package/dist/rag/embeddings/local-4b.js +51 -0
  98. package/dist/rag/embeddings/local.d.ts +29 -0
  99. package/dist/rag/embeddings/local.js +119 -0
  100. package/dist/rag/embeddings/mistral.d.ts +22 -0
  101. package/dist/rag/embeddings/mistral.js +85 -0
  102. package/dist/rag/embeddings/openai.d.ts +22 -0
  103. package/dist/rag/embeddings/openai.js +85 -0
  104. package/dist/rag/embeddings/types.d.ts +37 -0
  105. package/dist/rag/embeddings/types.js +1 -0
  106. package/dist/rag/gitignore/index.d.ts +57 -0
  107. package/dist/rag/gitignore/index.js +178 -0
  108. package/dist/rag/index.d.ts +15 -0
  109. package/dist/rag/index.js +25 -0
  110. package/dist/rag/indexer/chunker.d.ts +129 -0
  111. package/dist/rag/indexer/chunker.js +1352 -0
  112. package/dist/rag/indexer/index.d.ts +6 -0
  113. package/dist/rag/indexer/index.js +6 -0
  114. package/dist/rag/indexer/indexer.d.ts +73 -0
  115. package/dist/rag/indexer/indexer.js +356 -0
  116. package/dist/rag/indexer/types.d.ts +68 -0
  117. package/dist/rag/indexer/types.js +47 -0
  118. package/dist/rag/logger/index.d.ts +20 -0
  119. package/dist/rag/logger/index.js +75 -0
  120. package/dist/rag/manifest/index.d.ts +50 -0
  121. package/dist/rag/manifest/index.js +97 -0
  122. package/dist/rag/merkle/diff.d.ts +26 -0
  123. package/dist/rag/merkle/diff.js +95 -0
  124. package/dist/rag/merkle/hash.d.ts +34 -0
  125. package/dist/rag/merkle/hash.js +165 -0
  126. package/dist/rag/merkle/index.d.ts +68 -0
  127. package/dist/rag/merkle/index.js +298 -0
  128. package/dist/rag/merkle/node.d.ts +51 -0
  129. package/dist/rag/merkle/node.js +69 -0
  130. package/dist/rag/search/filters.d.ts +21 -0
  131. package/dist/rag/search/filters.js +100 -0
  132. package/dist/rag/search/fts.d.ts +32 -0
  133. package/dist/rag/search/fts.js +61 -0
  134. package/dist/rag/search/hybrid.d.ts +17 -0
  135. package/dist/rag/search/hybrid.js +58 -0
  136. package/dist/rag/search/index.d.ts +89 -0
  137. package/dist/rag/search/index.js +367 -0
  138. package/dist/rag/search/types.d.ts +130 -0
  139. package/dist/rag/search/types.js +4 -0
  140. package/dist/rag/search/vector.d.ts +25 -0
  141. package/dist/rag/search/vector.js +44 -0
  142. package/dist/rag/storage/index.d.ts +92 -0
  143. package/dist/rag/storage/index.js +287 -0
  144. package/dist/rag/storage/lancedb-native.d.ts +7 -0
  145. package/dist/rag/storage/lancedb-native.js +10 -0
  146. package/dist/rag/storage/schema.d.ts +23 -0
  147. package/dist/rag/storage/schema.js +50 -0
  148. package/dist/rag/storage/types.d.ts +100 -0
  149. package/dist/rag/storage/types.js +68 -0
  150. package/package.json +67 -0
  151. package/scripts/check-node-version.js +37 -0
@@ -0,0 +1,50 @@
1
+ export interface ManifestStats {
2
+ totalFiles: number;
3
+ totalChunks: number;
4
+ }
5
+ export interface Manifest {
6
+ version: number;
7
+ schemaVersion: number;
8
+ createdAt: string;
9
+ updatedAt: string;
10
+ tree: object | null;
11
+ stats: ManifestStats;
12
+ }
13
+ /**
14
+ * Create an empty manifest with current schema version.
15
+ */
16
+ export declare function createEmptyManifest(): Manifest;
17
+ /**
18
+ * Check if manifest schema version is current.
19
+ */
20
+ export declare function isSchemaVersionCurrent(manifest: Manifest): boolean;
21
+ /**
22
+ * Get schema version mismatch info for display.
23
+ */
24
+ export declare function getSchemaVersionInfo(manifest: Manifest): {
25
+ current: number;
26
+ required: number;
27
+ needsReindex: boolean;
28
+ };
29
+ /**
30
+ * Load manifest from disk.
31
+ * Returns an empty manifest if no file exists.
32
+ */
33
+ export declare function loadManifest(projectRoot: string): Promise<Manifest>;
34
+ /**
35
+ * Save manifest to disk.
36
+ * Creates the .viberag directory if it doesn't exist.
37
+ */
38
+ export declare function saveManifest(projectRoot: string, manifest: Manifest): Promise<void>;
39
+ /**
40
+ * Check if a manifest file exists.
41
+ */
42
+ export declare function manifestExists(projectRoot: string): Promise<boolean>;
43
+ /**
44
+ * Update manifest stats.
45
+ */
46
+ export declare function updateManifestStats(manifest: Manifest, stats: ManifestStats): Manifest;
47
+ /**
48
+ * Update manifest tree.
49
+ */
50
+ export declare function updateManifestTree(manifest: Manifest, tree: object | null): Manifest;
@@ -0,0 +1,97 @@
1
+ import fs from 'node:fs/promises';
2
+ import { getManifestPath, getViberagDir } from '../constants.js';
3
+ import { SCHEMA_VERSION } from '../storage/schema.js';
4
+ /**
5
+ * Create an empty manifest with current schema version.
6
+ */
7
+ export function createEmptyManifest() {
8
+ const now = new Date().toISOString();
9
+ return {
10
+ version: 1,
11
+ schemaVersion: SCHEMA_VERSION,
12
+ createdAt: now,
13
+ updatedAt: now,
14
+ tree: null,
15
+ stats: {
16
+ totalFiles: 0,
17
+ totalChunks: 0,
18
+ },
19
+ };
20
+ }
21
+ /**
22
+ * Check if manifest schema version is current.
23
+ */
24
+ export function isSchemaVersionCurrent(manifest) {
25
+ return manifest.schemaVersion === SCHEMA_VERSION;
26
+ }
27
+ /**
28
+ * Get schema version mismatch info for display.
29
+ */
30
+ export function getSchemaVersionInfo(manifest) {
31
+ return {
32
+ current: manifest.schemaVersion ?? 1, // Default to 1 for old manifests
33
+ required: SCHEMA_VERSION,
34
+ needsReindex: (manifest.schemaVersion ?? 1) < SCHEMA_VERSION,
35
+ };
36
+ }
37
+ /**
38
+ * Load manifest from disk.
39
+ * Returns an empty manifest if no file exists.
40
+ */
41
+ export async function loadManifest(projectRoot) {
42
+ const manifestPath = getManifestPath(projectRoot);
43
+ try {
44
+ const content = await fs.readFile(manifestPath, 'utf-8');
45
+ return JSON.parse(content);
46
+ }
47
+ catch {
48
+ return createEmptyManifest();
49
+ }
50
+ }
51
+ /**
52
+ * Save manifest to disk.
53
+ * Creates the .viberag directory if it doesn't exist.
54
+ */
55
+ export async function saveManifest(projectRoot, manifest) {
56
+ const viberagDir = getViberagDir(projectRoot);
57
+ await fs.mkdir(viberagDir, { recursive: true });
58
+ const manifestPath = getManifestPath(projectRoot);
59
+ const updated = {
60
+ ...manifest,
61
+ updatedAt: new Date().toISOString(),
62
+ };
63
+ await fs.writeFile(manifestPath, JSON.stringify(updated, null, '\t') + '\n');
64
+ }
65
+ /**
66
+ * Check if a manifest file exists.
67
+ */
68
+ export async function manifestExists(projectRoot) {
69
+ const manifestPath = getManifestPath(projectRoot);
70
+ try {
71
+ await fs.access(manifestPath);
72
+ return true;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ /**
79
+ * Update manifest stats.
80
+ */
81
+ export function updateManifestStats(manifest, stats) {
82
+ return {
83
+ ...manifest,
84
+ stats,
85
+ updatedAt: new Date().toISOString(),
86
+ };
87
+ }
88
+ /**
89
+ * Update manifest tree.
90
+ */
91
+ export function updateManifestTree(manifest, tree) {
92
+ return {
93
+ ...manifest,
94
+ tree,
95
+ updatedAt: new Date().toISOString(),
96
+ };
97
+ }
@@ -0,0 +1,26 @@
1
+ import type { MerkleNode } from './node.js';
2
+ /**
3
+ * Result of comparing two Merkle trees.
4
+ */
5
+ export interface TreeDiff {
6
+ /** Paths of new files */
7
+ new: string[];
8
+ /** Paths of modified files */
9
+ modified: string[];
10
+ /** Paths of deleted files */
11
+ deleted: string[];
12
+ /** Whether there are any changes */
13
+ hasChanges: boolean;
14
+ }
15
+ /**
16
+ * Create an empty TreeDiff.
17
+ */
18
+ export declare function createEmptyDiff(): TreeDiff;
19
+ /**
20
+ * Compare two Merkle trees and return the differences.
21
+ *
22
+ * @param oldRoot - The previous tree's root node (or null if no previous tree)
23
+ * @param newRoot - The current tree's root node
24
+ * @returns TreeDiff with new, modified, and deleted file paths
25
+ */
26
+ export declare function compareTrees(oldRoot: MerkleNode | null, newRoot: MerkleNode | null): TreeDiff;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Create an empty TreeDiff.
3
+ */
4
+ export function createEmptyDiff() {
5
+ return {
6
+ new: [],
7
+ modified: [],
8
+ deleted: [],
9
+ hasChanges: false,
10
+ };
11
+ }
12
+ /**
13
+ * Collect all file paths from a node (recursively).
14
+ */
15
+ function collectAllFiles(node, paths) {
16
+ if (node.type === 'file') {
17
+ paths.push(node.path);
18
+ }
19
+ else if (node.children) {
20
+ for (const child of node.children.values()) {
21
+ collectAllFiles(child, paths);
22
+ }
23
+ }
24
+ }
25
+ /**
26
+ * Compare two Merkle nodes and populate the diff.
27
+ */
28
+ function compareNodes(oldNode, newNode, diff) {
29
+ // Quick check: if hashes match, entire subtree unchanged
30
+ if (oldNode.hash === newNode.hash) {
31
+ return;
32
+ }
33
+ // File modified
34
+ if (oldNode.type === 'file' && newNode.type === 'file') {
35
+ diff.modified.push(newNode.path);
36
+ return;
37
+ }
38
+ // Type changed (file→dir or dir→file)
39
+ if (oldNode.type !== newNode.type) {
40
+ collectAllFiles(oldNode, diff.deleted);
41
+ collectAllFiles(newNode, diff.new);
42
+ return;
43
+ }
44
+ // Both directories: compare children
45
+ const oldChildren = oldNode.children ?? new Map();
46
+ const newChildren = newNode.children ?? new Map();
47
+ // Find new entries (in new but not in old)
48
+ for (const [name, child] of newChildren) {
49
+ if (!oldChildren.has(name)) {
50
+ collectAllFiles(child, diff.new);
51
+ }
52
+ }
53
+ // Find deleted entries (in old but not in new)
54
+ for (const [name, child] of oldChildren) {
55
+ if (!newChildren.has(name)) {
56
+ collectAllFiles(child, diff.deleted);
57
+ }
58
+ }
59
+ // Recurse into shared entries
60
+ for (const [name, newChild] of newChildren) {
61
+ const oldChild = oldChildren.get(name);
62
+ if (oldChild) {
63
+ compareNodes(oldChild, newChild, diff);
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Compare two Merkle trees and return the differences.
69
+ *
70
+ * @param oldRoot - The previous tree's root node (or null if no previous tree)
71
+ * @param newRoot - The current tree's root node
72
+ * @returns TreeDiff with new, modified, and deleted file paths
73
+ */
74
+ export function compareTrees(oldRoot, newRoot) {
75
+ const diff = createEmptyDiff();
76
+ // No old tree - everything is new
77
+ if (!oldRoot) {
78
+ if (newRoot) {
79
+ collectAllFiles(newRoot, diff.new);
80
+ }
81
+ diff.hasChanges = diff.new.length > 0;
82
+ return diff;
83
+ }
84
+ // No new tree - everything is deleted
85
+ if (!newRoot) {
86
+ collectAllFiles(oldRoot, diff.deleted);
87
+ diff.hasChanges = diff.deleted.length > 0;
88
+ return diff;
89
+ }
90
+ // Both trees exist - compare them
91
+ compareNodes(oldRoot, newRoot, diff);
92
+ diff.hasChanges =
93
+ diff.new.length > 0 || diff.modified.length > 0 || diff.deleted.length > 0;
94
+ return diff;
95
+ }
@@ -0,0 +1,34 @@
1
+ import type { MerkleNode } from './node.js';
2
+ /**
3
+ * Compute SHA256 hash of a file's content.
4
+ */
5
+ export declare function computeFileHash(filepath: string): Promise<string>;
6
+ /**
7
+ * Compute SHA256 hash of a string.
8
+ */
9
+ export declare function computeStringHash(content: string): string;
10
+ /**
11
+ * Compute SHA256 hash of a directory based on its children.
12
+ *
13
+ * Hash = SHA256(sorted child name+hash pairs)
14
+ * Format: "name1:hash1\nname2:hash2\n..."
15
+ */
16
+ export declare function computeDirectoryHash(children: Map<string, MerkleNode>): string;
17
+ /**
18
+ * Check if a file is likely binary based on extension.
19
+ * Falls back to checking for null bytes in the first chunk.
20
+ */
21
+ export declare function isBinaryFile(filepath: string): Promise<boolean>;
22
+ /**
23
+ * Check if a path should be excluded based on patterns.
24
+ *
25
+ * Supported pattern types:
26
+ * - "node_modules" - matches any path containing a "node_modules" segment
27
+ * - "*.pyc" - matches any file ending with .pyc
28
+ * - ".git" - matches any path containing a ".git" segment
29
+ */
30
+ export declare function shouldExclude(relativePath: string, excludePatterns: string[]): boolean;
31
+ /**
32
+ * Check if a file has a supported extension.
33
+ */
34
+ export declare function hasValidExtension(filepath: string, extensions: string[]): boolean;
@@ -0,0 +1,165 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ /**
4
+ * Compute SHA256 hash of a file's content.
5
+ */
6
+ export async function computeFileHash(filepath) {
7
+ const content = await fs.readFile(filepath);
8
+ return createHash('sha256').update(content).digest('hex');
9
+ }
10
+ /**
11
+ * Compute SHA256 hash of a string.
12
+ */
13
+ export function computeStringHash(content) {
14
+ return createHash('sha256').update(content).digest('hex');
15
+ }
16
+ /**
17
+ * Compute SHA256 hash of a directory based on its children.
18
+ *
19
+ * Hash = SHA256(sorted child name+hash pairs)
20
+ * Format: "name1:hash1\nname2:hash2\n..."
21
+ */
22
+ export function computeDirectoryHash(children) {
23
+ // Sort children by name for deterministic hashing
24
+ const sortedNames = [...children.keys()].sort();
25
+ // Build content string from name:hash pairs
26
+ const content = sortedNames
27
+ .map(name => {
28
+ const child = children.get(name);
29
+ return `${name}:${child.hash}`;
30
+ })
31
+ .join('\n');
32
+ return createHash('sha256').update(content).digest('hex');
33
+ }
34
+ /**
35
+ * Known binary file extensions.
36
+ */
37
+ const BINARY_EXTENSIONS = new Set([
38
+ // Images
39
+ '.png',
40
+ '.jpg',
41
+ '.jpeg',
42
+ '.gif',
43
+ '.bmp',
44
+ '.ico',
45
+ '.webp',
46
+ '.svg',
47
+ '.tiff',
48
+ '.tif',
49
+ // Audio/Video
50
+ '.mp3',
51
+ '.mp4',
52
+ '.wav',
53
+ '.avi',
54
+ '.mov',
55
+ '.webm',
56
+ '.flac',
57
+ '.ogg',
58
+ // Archives
59
+ '.zip',
60
+ '.tar',
61
+ '.gz',
62
+ '.bz2',
63
+ '.7z',
64
+ '.rar',
65
+ // Documents
66
+ '.pdf',
67
+ '.doc',
68
+ '.docx',
69
+ '.xls',
70
+ '.xlsx',
71
+ '.ppt',
72
+ '.pptx',
73
+ // Executables
74
+ '.exe',
75
+ '.dll',
76
+ '.so',
77
+ '.dylib',
78
+ '.bin',
79
+ // Fonts
80
+ '.ttf',
81
+ '.otf',
82
+ '.woff',
83
+ '.woff2',
84
+ '.eot',
85
+ // Other
86
+ '.wasm',
87
+ '.node',
88
+ '.pyc',
89
+ '.pyo',
90
+ '.class',
91
+ '.o',
92
+ '.a',
93
+ ]);
94
+ /**
95
+ * Check if a file is likely binary based on extension.
96
+ * Falls back to checking for null bytes in the first chunk.
97
+ */
98
+ export async function isBinaryFile(filepath) {
99
+ // Check extension first (fast path)
100
+ const ext = filepath.slice(filepath.lastIndexOf('.')).toLowerCase();
101
+ if (BINARY_EXTENSIONS.has(ext)) {
102
+ return true;
103
+ }
104
+ // Check for null bytes in first 8KB
105
+ try {
106
+ const handle = await fs.open(filepath, 'r');
107
+ try {
108
+ const buffer = Buffer.alloc(8192);
109
+ const { bytesRead } = await handle.read(buffer, 0, 8192, 0);
110
+ // Check for null bytes (common in binary files)
111
+ for (let i = 0; i < bytesRead; i++) {
112
+ if (buffer[i] === 0) {
113
+ return true;
114
+ }
115
+ }
116
+ return false;
117
+ }
118
+ finally {
119
+ await handle.close();
120
+ }
121
+ }
122
+ catch {
123
+ // If we can't read the file, assume it's not binary
124
+ return false;
125
+ }
126
+ }
127
+ /**
128
+ * Check if a path should be excluded based on patterns.
129
+ *
130
+ * Supported pattern types:
131
+ * - "node_modules" - matches any path containing a "node_modules" segment
132
+ * - "*.pyc" - matches any file ending with .pyc
133
+ * - ".git" - matches any path containing a ".git" segment
134
+ */
135
+ export function shouldExclude(relativePath, excludePatterns) {
136
+ // Split path into segments
137
+ const segments = relativePath.split('/');
138
+ const filename = segments[segments.length - 1] ?? '';
139
+ for (const pattern of excludePatterns) {
140
+ // Glob pattern: *.ext matches files with that extension
141
+ if (pattern.startsWith('*.')) {
142
+ const ext = pattern.slice(1); // ".pyc"
143
+ if (filename.endsWith(ext)) {
144
+ return true;
145
+ }
146
+ continue;
147
+ }
148
+ // Check if any segment matches the pattern exactly
149
+ if (segments.includes(pattern)) {
150
+ return true;
151
+ }
152
+ // Also check if the path starts with the pattern (for top-level exclusions)
153
+ if (relativePath.startsWith(pattern + '/') || relativePath === pattern) {
154
+ return true;
155
+ }
156
+ }
157
+ return false;
158
+ }
159
+ /**
160
+ * Check if a file has a supported extension.
161
+ */
162
+ export function hasValidExtension(filepath, extensions) {
163
+ const ext = filepath.slice(filepath.lastIndexOf('.'));
164
+ return extensions.includes(ext);
165
+ }
@@ -0,0 +1,68 @@
1
+ import { type MerkleNode, type SerializedNode } from './node.js';
2
+ import { type TreeDiff } from './diff.js';
3
+ export * from './node.js';
4
+ export * from './hash.js';
5
+ export * from './diff.js';
6
+ /**
7
+ * Statistics from building a Merkle tree.
8
+ */
9
+ export interface BuildStats {
10
+ /** Total files scanned (before filtering) */
11
+ filesScanned: number;
12
+ /** Files indexed (after filtering) */
13
+ filesIndexed: number;
14
+ /** Hash cache hits (mtime optimization) */
15
+ cacheHits: number;
16
+ /** Hash cache misses (computed hash) */
17
+ cacheMisses: number;
18
+ /** Files skipped (binary, symlink, errors) */
19
+ filesSkipped: number;
20
+ }
21
+ /**
22
+ * A Merkle tree for efficient codebase change detection.
23
+ *
24
+ * The tree is content-addressed: if a file's content doesn't change,
25
+ * its hash stays the same. Directory hashes are computed from their
26
+ * children's hashes, so unchanged subtrees have unchanged hashes.
27
+ */
28
+ export declare class MerkleTree {
29
+ /** Root node of the tree */
30
+ readonly root: MerkleNode | null;
31
+ /** Total number of files in the tree */
32
+ readonly fileCount: number;
33
+ /** Build statistics (populated after build) */
34
+ readonly buildStats: BuildStats;
35
+ private constructor();
36
+ /**
37
+ * Build a Merkle tree from the filesystem.
38
+ *
39
+ * Uses .gitignore for exclusions instead of hardcoded patterns.
40
+ * If extensions is provided, only files with those extensions are included.
41
+ * If extensions is empty/undefined, all text files are included.
42
+ *
43
+ * @param projectRoot - Absolute path to project root
44
+ * @param extensions - File extensions to include (e.g., [".py", ".ts"]), or empty for all
45
+ * @param _excludePatterns - DEPRECATED: Use .gitignore instead. This parameter is ignored.
46
+ * @param previousTree - Previous tree for mtime optimization
47
+ */
48
+ static build(projectRoot: string, extensions: string[], _excludePatterns: string[], previousTree?: MerkleTree): Promise<MerkleTree>;
49
+ /**
50
+ * Compare this tree with another tree.
51
+ *
52
+ * @param other - The other tree (usually the new/current tree)
53
+ * @returns TreeDiff with new, modified, and deleted files
54
+ */
55
+ compare(other: MerkleTree): TreeDiff;
56
+ /**
57
+ * Serialize the tree to a plain object for JSON storage.
58
+ */
59
+ toJSON(): SerializedNode | null;
60
+ /**
61
+ * Deserialize a tree from a plain object.
62
+ */
63
+ static fromJSON(data: SerializedNode | null): MerkleTree;
64
+ /**
65
+ * Create an empty tree.
66
+ */
67
+ static empty(): MerkleTree;
68
+ }