semantic-grep 0.0.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.
- package/CLAUDE.md +111 -0
- package/README.md +15 -0
- package/bin/semantic.ts +7 -0
- package/index.ts +8 -0
- package/package.json +29 -0
- package/search.ts +69 -0
- package/src/cli/commands/cache.ts +33 -0
- package/src/cli/commands/config.ts +34 -0
- package/src/cli/commands/search.ts +109 -0
- package/src/cli/commands/watch.ts +159 -0
- package/src/cli/index.ts +63 -0
- package/src/core/embedder.ts +89 -0
- package/src/core/indexer.ts +343 -0
- package/src/storage/embedding-cache.ts +71 -0
- package/src/storage/global-store.ts +208 -0
- package/src/types.ts +132 -0
- package/src/watcher/file-watcher.ts +119 -0
- package/src/watcher/git-tracker.ts +177 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
|
|
3
|
+
export class ApiKeyMissingError extends Data.TaggedError("ApiKeyMissingError")<{
|
|
4
|
+
message: string;
|
|
5
|
+
}> {}
|
|
6
|
+
|
|
7
|
+
export class EmbeddingError extends Data.TaggedError("EmbeddingError")<{
|
|
8
|
+
message: string;
|
|
9
|
+
cause?: unknown;
|
|
10
|
+
}> {}
|
|
11
|
+
|
|
12
|
+
export class FileNotFoundError extends Data.TaggedError("FileNotFoundError")<{
|
|
13
|
+
path: string;
|
|
14
|
+
}> {}
|
|
15
|
+
|
|
16
|
+
export class FileReadError extends Data.TaggedError("FileReadError")<{
|
|
17
|
+
path: string;
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
}> {}
|
|
20
|
+
|
|
21
|
+
export class GitError extends Data.TaggedError("GitError")<{
|
|
22
|
+
command: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}> {}
|
|
25
|
+
|
|
26
|
+
export class ManifestNotFoundError extends Data.TaggedError(
|
|
27
|
+
"ManifestNotFoundError"
|
|
28
|
+
)<{
|
|
29
|
+
projectId: string;
|
|
30
|
+
}> {}
|
|
31
|
+
|
|
32
|
+
export class ConfigError extends Data.TaggedError("ConfigError")<{
|
|
33
|
+
message: string;
|
|
34
|
+
cause?: unknown;
|
|
35
|
+
}> {}
|
|
36
|
+
|
|
37
|
+
export class IndexingError extends Data.TaggedError("IndexingError")<{
|
|
38
|
+
message: string;
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
}> {}
|
|
41
|
+
|
|
42
|
+
export interface GlobalConfig {
|
|
43
|
+
voyageApiKey?: string;
|
|
44
|
+
version: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ProjectManifest {
|
|
48
|
+
projectId: string;
|
|
49
|
+
projectPath: string;
|
|
50
|
+
lastIndexedAt: string;
|
|
51
|
+
gitCommitHash?: string;
|
|
52
|
+
chunks: ChunkReference[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ChunkReference {
|
|
56
|
+
chunkHash: string;
|
|
57
|
+
filepath: string;
|
|
58
|
+
lineRange: { start: number; end: number };
|
|
59
|
+
text: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface GitTreeCache {
|
|
63
|
+
commitHash: string;
|
|
64
|
+
fileHashes: Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ChunkWithMeta {
|
|
68
|
+
filepath: string;
|
|
69
|
+
hash: string;
|
|
70
|
+
text: string;
|
|
71
|
+
lineRange: { start: number; end: number };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ChunkWithEmbedding extends ChunkWithMeta {
|
|
75
|
+
embedding: number[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface FileChange {
|
|
79
|
+
filepath: string;
|
|
80
|
+
changeType: "added" | "modified" | "deleted";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const SEMANTIC_HOME = `${process.env.HOME}/.semantic`;
|
|
84
|
+
export const EMBEDDINGS_DIR = `${SEMANTIC_HOME}/embeddings`;
|
|
85
|
+
export const PROJECTS_DIR = `${SEMANTIC_HOME}/projects`;
|
|
86
|
+
export const CONFIG_PATH = `${SEMANTIC_HOME}/config.json`;
|
|
87
|
+
|
|
88
|
+
export const TEXT_EXTENSIONS = new Set([
|
|
89
|
+
".md",
|
|
90
|
+
".mdx",
|
|
91
|
+
".txt",
|
|
92
|
+
".rst",
|
|
93
|
+
".adoc",
|
|
94
|
+
".ts",
|
|
95
|
+
".tsx",
|
|
96
|
+
".js",
|
|
97
|
+
".jsx",
|
|
98
|
+
".mjs",
|
|
99
|
+
".cjs",
|
|
100
|
+
".py",
|
|
101
|
+
".java",
|
|
102
|
+
".kt",
|
|
103
|
+
".go",
|
|
104
|
+
".rs",
|
|
105
|
+
".rb",
|
|
106
|
+
".php",
|
|
107
|
+
".cpp",
|
|
108
|
+
".c",
|
|
109
|
+
".h",
|
|
110
|
+
".cs",
|
|
111
|
+
".json",
|
|
112
|
+
".yml",
|
|
113
|
+
".yaml",
|
|
114
|
+
".toml",
|
|
115
|
+
".ini",
|
|
116
|
+
".sql",
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
export const IRRELEVANT_FILES = new Set([
|
|
120
|
+
".gitignore",
|
|
121
|
+
".git",
|
|
122
|
+
".github",
|
|
123
|
+
".vscode",
|
|
124
|
+
".DS_Store",
|
|
125
|
+
".env",
|
|
126
|
+
".env.local",
|
|
127
|
+
"pnpm-lock.yaml",
|
|
128
|
+
"package-lock.json",
|
|
129
|
+
"yarn.lock",
|
|
130
|
+
"bun.lockb",
|
|
131
|
+
"bun.lock",
|
|
132
|
+
]);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { GitTracker } from "./git-tracker";
|
|
5
|
+
|
|
6
|
+
export interface WatcherOptions {
|
|
7
|
+
debounceMs?: number;
|
|
8
|
+
onChanges: (changedFiles: string[]) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FileWatcher {
|
|
12
|
+
start: () => void;
|
|
13
|
+
stop: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createFileWatcher(
|
|
17
|
+
pwd: string,
|
|
18
|
+
options: WatcherOptions
|
|
19
|
+
): FileWatcher {
|
|
20
|
+
const debounceMs = options.debounceMs ?? 500;
|
|
21
|
+
|
|
22
|
+
let watcher: FSWatcher | null = null;
|
|
23
|
+
let pendingChanges = new Set<string>();
|
|
24
|
+
let debounceTimer: Timer | null = null;
|
|
25
|
+
let isProcessing = false;
|
|
26
|
+
|
|
27
|
+
const flushChanges = async () => {
|
|
28
|
+
if (pendingChanges.size === 0 || isProcessing) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
isProcessing = true;
|
|
33
|
+
const filesToProcess = Array.from(pendingChanges);
|
|
34
|
+
pendingChanges.clear();
|
|
35
|
+
|
|
36
|
+
const trackedFiles: string[] = [];
|
|
37
|
+
for (const filepath of filesToProcess) {
|
|
38
|
+
const isTracked = await Effect.runPromise(
|
|
39
|
+
GitTracker.isTracked(pwd, filepath)
|
|
40
|
+
);
|
|
41
|
+
if (isTracked && GitTracker.filterFilePath(filepath)) {
|
|
42
|
+
trackedFiles.push(filepath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (trackedFiles.length > 0) {
|
|
47
|
+
try {
|
|
48
|
+
await options.onChanges(trackedFiles);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("[Watcher] Error processing changes:", error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
isProcessing = false;
|
|
55
|
+
|
|
56
|
+
if (pendingChanges.size > 0) {
|
|
57
|
+
scheduleFlush();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const scheduleFlush = () => {
|
|
62
|
+
if (debounceTimer) {
|
|
63
|
+
clearTimeout(debounceTimer);
|
|
64
|
+
}
|
|
65
|
+
debounceTimer = setTimeout(flushChanges, debounceMs);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleChange = (event: string, filename: string | null) => {
|
|
69
|
+
if (!filename) return;
|
|
70
|
+
|
|
71
|
+
const filepath = filename.startsWith(pwd)
|
|
72
|
+
? path.relative(pwd, filename)
|
|
73
|
+
: filename;
|
|
74
|
+
|
|
75
|
+
if (filepath.startsWith(".") || filepath.includes("/.")) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (filepath.includes("node_modules")) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pendingChanges.add(filepath);
|
|
84
|
+
scheduleFlush();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const start = () => {
|
|
88
|
+
if (watcher) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
watcher = watch(pwd, { recursive: true }, handleChange);
|
|
94
|
+
|
|
95
|
+
watcher.on("error", (error) => {
|
|
96
|
+
console.error("[Watcher] Error:", error);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
console.log(`[Watcher] Watching ${pwd} for changes...`);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("[Watcher] Failed to start:", error);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const stop = () => {
|
|
106
|
+
if (debounceTimer) {
|
|
107
|
+
clearTimeout(debounceTimer);
|
|
108
|
+
debounceTimer = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (watcher) {
|
|
112
|
+
watcher.close();
|
|
113
|
+
watcher = null;
|
|
114
|
+
console.log("[Watcher] Stopped watching");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return { start, stop };
|
|
119
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import simpleGit from "simple-git";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {
|
|
5
|
+
type FileChange,
|
|
6
|
+
type GitTreeCache,
|
|
7
|
+
GitError,
|
|
8
|
+
TEXT_EXTENSIONS,
|
|
9
|
+
IRRELEVANT_FILES,
|
|
10
|
+
} from "../types";
|
|
11
|
+
import { GlobalStore } from "../storage/global-store";
|
|
12
|
+
|
|
13
|
+
export namespace GitTracker {
|
|
14
|
+
const getGit = (pwd: string) => simpleGit(pwd);
|
|
15
|
+
|
|
16
|
+
export const filterFilePath = (filepath: string): boolean => {
|
|
17
|
+
return (
|
|
18
|
+
TEXT_EXTENSIONS.has(path.extname(filepath)) &&
|
|
19
|
+
!IRRELEVANT_FILES.has(path.basename(filepath))
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const detectChanges = (
|
|
24
|
+
cached: Record<string, string>,
|
|
25
|
+
current: Record<string, string>
|
|
26
|
+
): FileChange[] => {
|
|
27
|
+
const changes: FileChange[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [filepath, hash] of Object.entries(cached)) {
|
|
30
|
+
const currentHash = current[filepath];
|
|
31
|
+
if (!currentHash) {
|
|
32
|
+
changes.push({ filepath, changeType: "deleted" });
|
|
33
|
+
} else if (currentHash !== hash) {
|
|
34
|
+
changes.push({ filepath, changeType: "modified" });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const filepath of Object.keys(current)) {
|
|
39
|
+
if (!(filepath in cached)) {
|
|
40
|
+
changes.push({ filepath, changeType: "added" });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return changes;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const getCurrentCommitHash = (pwd: string) =>
|
|
48
|
+
Effect.tryPromise({
|
|
49
|
+
try: () => getGit(pwd).revparse(["HEAD"]),
|
|
50
|
+
catch: (error) =>
|
|
51
|
+
new GitError({
|
|
52
|
+
command: "git rev-parse HEAD",
|
|
53
|
+
message: String(error),
|
|
54
|
+
}),
|
|
55
|
+
}).pipe(
|
|
56
|
+
Effect.map((hash) => hash.trim()),
|
|
57
|
+
Effect.catchAll(() => Effect.succeed(null as string | null))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export const getTrackedFiles = (pwd: string) =>
|
|
61
|
+
Effect.tryPromise({
|
|
62
|
+
try: () => getGit(pwd).raw(["ls-files"]),
|
|
63
|
+
catch: (error) =>
|
|
64
|
+
new GitError({
|
|
65
|
+
command: "git ls-files",
|
|
66
|
+
message: String(error),
|
|
67
|
+
}),
|
|
68
|
+
}).pipe(
|
|
69
|
+
Effect.map((output) =>
|
|
70
|
+
output
|
|
71
|
+
.trim()
|
|
72
|
+
.split("\n")
|
|
73
|
+
.filter((f) => f.length > 0)
|
|
74
|
+
.filter(filterFilePath)
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export const getGitTree = (pwd: string) =>
|
|
79
|
+
Effect.tryPromise({
|
|
80
|
+
try: () =>
|
|
81
|
+
getGit(pwd).raw([
|
|
82
|
+
"ls-tree",
|
|
83
|
+
"-r",
|
|
84
|
+
"--format=%(objectname)\t%(path)",
|
|
85
|
+
"HEAD",
|
|
86
|
+
]),
|
|
87
|
+
catch: (error) =>
|
|
88
|
+
new GitError({
|
|
89
|
+
command: "git ls-tree",
|
|
90
|
+
message: String(error),
|
|
91
|
+
}),
|
|
92
|
+
}).pipe(
|
|
93
|
+
Effect.map((output) => parseGitTreeOutput(output)),
|
|
94
|
+
Effect.catchAll(() => getGitTreeFromLsFiles(pwd))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const parseGitTreeOutput = (output: string): Record<string, string> => {
|
|
98
|
+
const fileHashes: Record<string, string> = {};
|
|
99
|
+
|
|
100
|
+
for (const line of output.trim().split("\n")) {
|
|
101
|
+
if (!line) continue;
|
|
102
|
+
const [hash, filepath] = line.split("\t");
|
|
103
|
+
if (hash && filepath && filterFilePath(filepath)) {
|
|
104
|
+
fileHashes[filepath] = hash;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return fileHashes;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const getGitTreeFromLsFiles = (pwd: string) =>
|
|
112
|
+
Effect.tryPromise({
|
|
113
|
+
try: async () => {
|
|
114
|
+
const git = getGit(pwd);
|
|
115
|
+
const output = await git.raw(["ls-files"]);
|
|
116
|
+
const fileHashes: Record<string, string> = {};
|
|
117
|
+
|
|
118
|
+
for (const filepath of output.trim().split("\n")) {
|
|
119
|
+
if (filepath && filterFilePath(filepath)) {
|
|
120
|
+
try {
|
|
121
|
+
const stat = await Bun.file(`${pwd}/${filepath}`).stat();
|
|
122
|
+
fileHashes[filepath] = stat?.mtimeMs?.toString() ?? "unknown";
|
|
123
|
+
} catch {
|
|
124
|
+
fileHashes[filepath] = "unknown";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return fileHashes;
|
|
130
|
+
},
|
|
131
|
+
catch: (error) =>
|
|
132
|
+
new GitError({
|
|
133
|
+
command: "git ls-files (fallback)",
|
|
134
|
+
message: String(error),
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const isTracked = (pwd: string, filepath: string) =>
|
|
139
|
+
Effect.tryPromise({
|
|
140
|
+
try: () => getGit(pwd).raw(["ls-files", filepath]),
|
|
141
|
+
catch: () => "",
|
|
142
|
+
}).pipe(
|
|
143
|
+
Effect.map((output) => output.trim().length > 0),
|
|
144
|
+
Effect.catchAll(() => Effect.succeed(false))
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
export const getFilesToIndex = (pwd: string) =>
|
|
148
|
+
Effect.gen(function* () {
|
|
149
|
+
const projectId = GlobalStore.getProjectId(pwd);
|
|
150
|
+
const commitHash = yield* getCurrentCommitHash(pwd);
|
|
151
|
+
const currentFileHashes = yield* getGitTree(pwd);
|
|
152
|
+
|
|
153
|
+
const currentTree: GitTreeCache = {
|
|
154
|
+
commitHash: commitHash ?? "no-commit",
|
|
155
|
+
fileHashes: currentFileHashes,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const cachedTree = yield* GlobalStore.loadGitTreeCache(projectId);
|
|
159
|
+
|
|
160
|
+
if (!cachedTree) {
|
|
161
|
+
const allFiles = Object.keys(currentFileHashes).map((filepath) => ({
|
|
162
|
+
filepath,
|
|
163
|
+
changeType: "added" as const,
|
|
164
|
+
}));
|
|
165
|
+
return { changes: allFiles, currentTree, isFullReindex: true };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const changes = detectChanges(cachedTree.fileHashes, currentFileHashes);
|
|
169
|
+
|
|
170
|
+
return { changes, currentTree, isFullReindex: false };
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export const saveGitTree = (pwd: string, tree: GitTreeCache) => {
|
|
174
|
+
const projectId = GlobalStore.getProjectId(pwd);
|
|
175
|
+
return GlobalStore.saveGitTreeCache(projectId, tree);
|
|
176
|
+
};
|
|
177
|
+
}
|