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.
- package/LICENSE +661 -0
- package/README.md +219 -0
- package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
- package/dist/cli/__tests__/mcp-setup.test.js +597 -0
- package/dist/cli/app.d.ts +2 -0
- package/dist/cli/app.js +238 -0
- package/dist/cli/commands/handlers.d.ts +57 -0
- package/dist/cli/commands/handlers.js +231 -0
- package/dist/cli/commands/index.d.ts +2 -0
- package/dist/cli/commands/index.js +2 -0
- package/dist/cli/commands/mcp-setup.d.ts +107 -0
- package/dist/cli/commands/mcp-setup.js +509 -0
- package/dist/cli/commands/useRagCommands.d.ts +23 -0
- package/dist/cli/commands/useRagCommands.js +180 -0
- package/dist/cli/components/CleanWizard.d.ts +17 -0
- package/dist/cli/components/CleanWizard.js +169 -0
- package/dist/cli/components/InitWizard.d.ts +20 -0
- package/dist/cli/components/InitWizard.js +370 -0
- package/dist/cli/components/McpSetupWizard.d.ts +37 -0
- package/dist/cli/components/McpSetupWizard.js +387 -0
- package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
- package/dist/cli/components/SearchResultsDisplay.js +130 -0
- package/dist/cli/components/WelcomeBanner.d.ts +10 -0
- package/dist/cli/components/WelcomeBanner.js +26 -0
- package/dist/cli/components/index.d.ts +1 -0
- package/dist/cli/components/index.js +1 -0
- package/dist/cli/data/mcp-editors.d.ts +80 -0
- package/dist/cli/data/mcp-editors.js +270 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +26 -0
- package/dist/cli-bundle.cjs +5269 -0
- package/dist/common/commands/terminalSetup.d.ts +2 -0
- package/dist/common/commands/terminalSetup.js +144 -0
- package/dist/common/components/CommandSuggestions.d.ts +9 -0
- package/dist/common/components/CommandSuggestions.js +20 -0
- package/dist/common/components/StaticWithResize.d.ts +23 -0
- package/dist/common/components/StaticWithResize.js +62 -0
- package/dist/common/components/StatusBar.d.ts +8 -0
- package/dist/common/components/StatusBar.js +64 -0
- package/dist/common/components/TextInput.d.ts +12 -0
- package/dist/common/components/TextInput.js +239 -0
- package/dist/common/components/index.d.ts +3 -0
- package/dist/common/components/index.js +3 -0
- package/dist/common/hooks/index.d.ts +4 -0
- package/dist/common/hooks/index.js +4 -0
- package/dist/common/hooks/useCommandHistory.d.ts +7 -0
- package/dist/common/hooks/useCommandHistory.js +51 -0
- package/dist/common/hooks/useCtrlC.d.ts +9 -0
- package/dist/common/hooks/useCtrlC.js +40 -0
- package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
- package/dist/common/hooks/useKittyKeyboard.js +26 -0
- package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
- package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
- package/dist/common/hooks/useTerminalResize.d.ts +28 -0
- package/dist/common/hooks/useTerminalResize.js +51 -0
- package/dist/common/hooks/useTextBuffer.d.ts +13 -0
- package/dist/common/hooks/useTextBuffer.js +165 -0
- package/dist/common/index.d.ts +13 -0
- package/dist/common/index.js +17 -0
- package/dist/common/types.d.ts +162 -0
- package/dist/common/types.js +1 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.js +66 -0
- package/dist/mcp/server.d.ts +25 -0
- package/dist/mcp/server.js +837 -0
- package/dist/mcp/watcher.d.ts +86 -0
- package/dist/mcp/watcher.js +334 -0
- package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
- package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
- package/dist/rag/__tests__/helpers.d.ts +30 -0
- package/dist/rag/__tests__/helpers.js +67 -0
- package/dist/rag/__tests__/merkle.test.d.ts +5 -0
- package/dist/rag/__tests__/merkle.test.js +161 -0
- package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
- package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
- package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
- package/dist/rag/__tests__/multi-language.test.js +535 -0
- package/dist/rag/__tests__/rag.test.d.ts +10 -0
- package/dist/rag/__tests__/rag.test.js +311 -0
- package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
- package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
- package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
- package/dist/rag/__tests__/search-filters.test.js +250 -0
- package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
- package/dist/rag/__tests__/search-modes.test.js +133 -0
- package/dist/rag/config/index.d.ts +61 -0
- package/dist/rag/config/index.js +111 -0
- package/dist/rag/constants.d.ts +41 -0
- package/dist/rag/constants.js +57 -0
- package/dist/rag/embeddings/fastembed.d.ts +62 -0
- package/dist/rag/embeddings/fastembed.js +124 -0
- package/dist/rag/embeddings/gemini.d.ts +26 -0
- package/dist/rag/embeddings/gemini.js +116 -0
- package/dist/rag/embeddings/index.d.ts +10 -0
- package/dist/rag/embeddings/index.js +9 -0
- package/dist/rag/embeddings/local-4b.d.ts +28 -0
- package/dist/rag/embeddings/local-4b.js +51 -0
- package/dist/rag/embeddings/local.d.ts +29 -0
- package/dist/rag/embeddings/local.js +119 -0
- package/dist/rag/embeddings/mistral.d.ts +22 -0
- package/dist/rag/embeddings/mistral.js +85 -0
- package/dist/rag/embeddings/openai.d.ts +22 -0
- package/dist/rag/embeddings/openai.js +85 -0
- package/dist/rag/embeddings/types.d.ts +37 -0
- package/dist/rag/embeddings/types.js +1 -0
- package/dist/rag/gitignore/index.d.ts +57 -0
- package/dist/rag/gitignore/index.js +178 -0
- package/dist/rag/index.d.ts +15 -0
- package/dist/rag/index.js +25 -0
- package/dist/rag/indexer/chunker.d.ts +129 -0
- package/dist/rag/indexer/chunker.js +1352 -0
- package/dist/rag/indexer/index.d.ts +6 -0
- package/dist/rag/indexer/index.js +6 -0
- package/dist/rag/indexer/indexer.d.ts +73 -0
- package/dist/rag/indexer/indexer.js +356 -0
- package/dist/rag/indexer/types.d.ts +68 -0
- package/dist/rag/indexer/types.js +47 -0
- package/dist/rag/logger/index.d.ts +20 -0
- package/dist/rag/logger/index.js +75 -0
- package/dist/rag/manifest/index.d.ts +50 -0
- package/dist/rag/manifest/index.js +97 -0
- package/dist/rag/merkle/diff.d.ts +26 -0
- package/dist/rag/merkle/diff.js +95 -0
- package/dist/rag/merkle/hash.d.ts +34 -0
- package/dist/rag/merkle/hash.js +165 -0
- package/dist/rag/merkle/index.d.ts +68 -0
- package/dist/rag/merkle/index.js +298 -0
- package/dist/rag/merkle/node.d.ts +51 -0
- package/dist/rag/merkle/node.js +69 -0
- package/dist/rag/search/filters.d.ts +21 -0
- package/dist/rag/search/filters.js +100 -0
- package/dist/rag/search/fts.d.ts +32 -0
- package/dist/rag/search/fts.js +61 -0
- package/dist/rag/search/hybrid.d.ts +17 -0
- package/dist/rag/search/hybrid.js +58 -0
- package/dist/rag/search/index.d.ts +89 -0
- package/dist/rag/search/index.js +367 -0
- package/dist/rag/search/types.d.ts +130 -0
- package/dist/rag/search/types.js +4 -0
- package/dist/rag/search/vector.d.ts +25 -0
- package/dist/rag/search/vector.js +44 -0
- package/dist/rag/storage/index.d.ts +92 -0
- package/dist/rag/storage/index.js +287 -0
- package/dist/rag/storage/lancedb-native.d.ts +7 -0
- package/dist/rag/storage/lancedb-native.js +10 -0
- package/dist/rag/storage/schema.d.ts +23 -0
- package/dist/rag/storage/schema.js +50 -0
- package/dist/rag/storage/types.d.ts +100 -0
- package/dist/rag/storage/types.js +68 -0
- package/package.json +67 -0
- 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
|
+
}
|