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/CLAUDE.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
|
3
|
+
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Default to using Bun instead of Node.js.
|
|
8
|
+
|
|
9
|
+
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
10
|
+
- Use `bun test` instead of `jest` or `vitest`
|
|
11
|
+
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
12
|
+
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
13
|
+
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
14
|
+
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
15
|
+
- Bun automatically loads .env, so don't use dotenv.
|
|
16
|
+
|
|
17
|
+
## APIs
|
|
18
|
+
|
|
19
|
+
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
20
|
+
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
21
|
+
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
22
|
+
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
23
|
+
- `WebSocket` is built-in. Don't use `ws`.
|
|
24
|
+
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
25
|
+
- Bun.$`ls` instead of execa.
|
|
26
|
+
|
|
27
|
+
## Testing
|
|
28
|
+
|
|
29
|
+
Use `bun test` to run tests.
|
|
30
|
+
|
|
31
|
+
```ts#index.test.ts
|
|
32
|
+
import { test, expect } from "bun:test";
|
|
33
|
+
|
|
34
|
+
test("hello world", () => {
|
|
35
|
+
expect(1).toBe(1);
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Frontend
|
|
40
|
+
|
|
41
|
+
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
42
|
+
|
|
43
|
+
Server:
|
|
44
|
+
|
|
45
|
+
```ts#index.ts
|
|
46
|
+
import index from "./index.html"
|
|
47
|
+
|
|
48
|
+
Bun.serve({
|
|
49
|
+
routes: {
|
|
50
|
+
"/": index,
|
|
51
|
+
"/api/users/:id": {
|
|
52
|
+
GET: (req) => {
|
|
53
|
+
return new Response(JSON.stringify({ id: req.params.id }));
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
// optional websocket support
|
|
58
|
+
websocket: {
|
|
59
|
+
open: (ws) => {
|
|
60
|
+
ws.send("Hello, world!");
|
|
61
|
+
},
|
|
62
|
+
message: (ws, message) => {
|
|
63
|
+
ws.send(message);
|
|
64
|
+
},
|
|
65
|
+
close: (ws) => {
|
|
66
|
+
// handle close
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
development: {
|
|
70
|
+
hmr: true,
|
|
71
|
+
console: true,
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
77
|
+
|
|
78
|
+
```html#index.html
|
|
79
|
+
<html>
|
|
80
|
+
<body>
|
|
81
|
+
<h1>Hello, world!</h1>
|
|
82
|
+
<script type="module" src="./frontend.tsx"></script>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
With the following `frontend.tsx`:
|
|
88
|
+
|
|
89
|
+
```tsx#frontend.tsx
|
|
90
|
+
import React from "react";
|
|
91
|
+
import { createRoot } from "react-dom/client";
|
|
92
|
+
|
|
93
|
+
// import .css files directly and it works
|
|
94
|
+
import './index.css';
|
|
95
|
+
|
|
96
|
+
const root = createRoot(document.body);
|
|
97
|
+
|
|
98
|
+
export default function Frontend() {
|
|
99
|
+
return <h1>Hello, world!</h1>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
root.render(<Frontend />);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then, run index.ts
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
bun --hot ./index.ts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# semantic-search
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.3.5. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
package/bin/semantic.ts
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { GlobalStore } from "./src/storage/global-store";
|
|
2
|
+
export { EmbeddingCache } from "./src/storage/embedding-cache";
|
|
3
|
+
export { Embedder } from "./src/core/embedder";
|
|
4
|
+
export { Indexer } from "./src/core/indexer";
|
|
5
|
+
export { GitTracker } from "./src/watcher/git-tracker";
|
|
6
|
+
export { createFileWatcher } from "./src/watcher/file-watcher";
|
|
7
|
+
|
|
8
|
+
export * from "./src/types";
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "semantic-grep",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"semantic": "./bin/semantic.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "bun run bin/semantic.ts",
|
|
11
|
+
"semantic": "bun run bin/semantic.ts"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/bun": "latest",
|
|
15
|
+
"tsx": "^4.21.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"typescript": "^5"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"ai": "^6.0.6",
|
|
22
|
+
"code-chunk": "/Users/nisarg/Documents/experiments/code-chunk/packages/code-chunk/code-chunk-0.1.12.tgz",
|
|
23
|
+
"commander": "^14.0.3",
|
|
24
|
+
"effect": "^3.19.14",
|
|
25
|
+
"simple-git": "^3.30.0",
|
|
26
|
+
"voyage-ai-provider": "^3.0.0",
|
|
27
|
+
"zod": "^4.3.6"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/search.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { Effect, Console } from "effect";
|
|
3
|
+
import { Embedder } from "./src/core/embedder";
|
|
4
|
+
import type { ChunkWithEmbedding } from "./src/types";
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name("search")
|
|
10
|
+
.description("Standalone search script using final.json")
|
|
11
|
+
.argument("<query>", "Search query")
|
|
12
|
+
.option("--topk <number>", "Number of results to return", "10")
|
|
13
|
+
.option("--compact", "Compact output format", false)
|
|
14
|
+
.action(
|
|
15
|
+
async (query: string, options: { topk: string; compact: boolean }) => {
|
|
16
|
+
const topk = Math.min(20, Math.max(1, parseInt(options.topk, 10) || 10));
|
|
17
|
+
await Effect.runPromise(runSearch(query, topk, options.compact));
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const runSearch = (query: string, topk: number, compact: boolean) =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
yield* Console.log(`\nSearching for: "${query}"`);
|
|
24
|
+
yield* Console.log(`Top ${topk} results\n`);
|
|
25
|
+
|
|
26
|
+
const chunksFile = Bun.file("final.json");
|
|
27
|
+
const chunks: ChunkWithEmbedding[] = yield* Effect.tryPromise({
|
|
28
|
+
try: () => chunksFile.json().then((chunks) => chunks.flat()),
|
|
29
|
+
catch: (error) => new Error(`Failed to load final.json: ${error}`),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const queryEmbedding = yield* Embedder.embedQuery(query);
|
|
33
|
+
|
|
34
|
+
const results = chunks
|
|
35
|
+
.filter((chunk) => chunk.embedding && chunk.embedding.length > 0)
|
|
36
|
+
.map((chunk) => ({
|
|
37
|
+
chunk,
|
|
38
|
+
similarity: Embedder.cosineSimilarity(queryEmbedding, chunk.embedding),
|
|
39
|
+
}))
|
|
40
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
41
|
+
.slice(0, topk);
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < results.length; i++) {
|
|
44
|
+
const result = results[i]!;
|
|
45
|
+
const { chunk, similarity } = result;
|
|
46
|
+
const { filepath, lineRange } = chunk;
|
|
47
|
+
|
|
48
|
+
if (compact) {
|
|
49
|
+
yield* Console.log(
|
|
50
|
+
`#${i + 1} | ${similarity.toFixed(4)} | ${filepath}:${
|
|
51
|
+
lineRange.start + 1
|
|
52
|
+
}-${lineRange.end + 1}`
|
|
53
|
+
);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
yield* Console.log("─".repeat(80));
|
|
58
|
+
yield* Console.log(
|
|
59
|
+
`#${i + 1} | Score: ${similarity.toFixed(4)} | ${filepath}:${
|
|
60
|
+
lineRange.start + 1
|
|
61
|
+
}-${lineRange.end + 1}`
|
|
62
|
+
);
|
|
63
|
+
yield* Console.log("─".repeat(80));
|
|
64
|
+
yield* Console.log(chunk.text);
|
|
65
|
+
yield* Console.log("");
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Effect, Console } from "effect";
|
|
2
|
+
import { GlobalStore } from "../../storage/global-store";
|
|
3
|
+
|
|
4
|
+
export const clearRepoCacheCommand = () =>
|
|
5
|
+
Effect.gen(function* () {
|
|
6
|
+
const pwd = process.cwd();
|
|
7
|
+
const projectId = GlobalStore.getProjectId(pwd);
|
|
8
|
+
|
|
9
|
+
const result = yield* GlobalStore.clearProjectCache(projectId);
|
|
10
|
+
|
|
11
|
+
if (!result.manifestCleared && !result.gitTreeCleared) {
|
|
12
|
+
yield* Console.log("[Cache] Repo cache already empty for this project");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const clearedParts = [
|
|
17
|
+
result.manifestCleared ? "manifest" : null,
|
|
18
|
+
result.gitTreeCleared ? "git tree" : null,
|
|
19
|
+
].filter((part): part is string => part !== null);
|
|
20
|
+
|
|
21
|
+
yield* Console.log(
|
|
22
|
+
`[Cache] Cleared repo cache (${clearedParts.join(
|
|
23
|
+
", "
|
|
24
|
+
)}). Embeddings preserved.`
|
|
25
|
+
);
|
|
26
|
+
}).pipe(
|
|
27
|
+
Effect.catchTags({
|
|
28
|
+
ConfigError: (e) =>
|
|
29
|
+
Console.error(`Config error: ${e.message}`).pipe(
|
|
30
|
+
Effect.flatMap(() => Effect.sync(() => process.exit(1)))
|
|
31
|
+
),
|
|
32
|
+
})
|
|
33
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Effect, Console } from "effect";
|
|
2
|
+
import { GlobalStore } from "../../storage/global-store";
|
|
3
|
+
|
|
4
|
+
export const configCommand = (
|
|
5
|
+
subcommand: "add-api-key" | "show",
|
|
6
|
+
apiKey?: string
|
|
7
|
+
) =>
|
|
8
|
+
Effect.gen(function* () {
|
|
9
|
+
switch (subcommand) {
|
|
10
|
+
case "add-api-key": {
|
|
11
|
+
yield* GlobalStore.setApiKey(apiKey!);
|
|
12
|
+
yield* Console.log("API key saved successfully");
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
case "show": {
|
|
17
|
+
const key = yield* GlobalStore.getApiKey;
|
|
18
|
+
if (key) {
|
|
19
|
+
const masked = key.slice(0, 4) + "..." + key.slice(-4);
|
|
20
|
+
yield* Console.log(`Voyage API Key: ${masked}`);
|
|
21
|
+
} else {
|
|
22
|
+
yield* Console.log("No API key configured");
|
|
23
|
+
}
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}).pipe(
|
|
28
|
+
Effect.catchTags({
|
|
29
|
+
ConfigError: (e) =>
|
|
30
|
+
Console.error(`Config error: ${e.message}`).pipe(
|
|
31
|
+
Effect.flatMap(() => Effect.sync(() => process.exit(1)))
|
|
32
|
+
),
|
|
33
|
+
})
|
|
34
|
+
);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Effect, Console } from "effect";
|
|
2
|
+
import type { ChunkReference } from "../../types";
|
|
3
|
+
import { GlobalStore } from "../../storage/global-store";
|
|
4
|
+
import { EmbeddingCache } from "../../storage/embedding-cache";
|
|
5
|
+
import { Embedder } from "../../core/embedder";
|
|
6
|
+
|
|
7
|
+
interface SearchResult {
|
|
8
|
+
chunk: ChunkReference;
|
|
9
|
+
similarity: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SearchOptions {
|
|
13
|
+
topk: number;
|
|
14
|
+
compact: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const searchCommand = (query: string, options: SearchOptions) =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const { topk, compact } = options;
|
|
20
|
+
|
|
21
|
+
const pwd = process.cwd();
|
|
22
|
+
const projectId = GlobalStore.getProjectId(pwd);
|
|
23
|
+
|
|
24
|
+
const manifest = yield* GlobalStore.loadProjectManifest(projectId);
|
|
25
|
+
|
|
26
|
+
if (!manifest) {
|
|
27
|
+
yield* Console.error("Error: No index found for current directory");
|
|
28
|
+
yield* Console.error("Run 'semantic watch' first to index this project");
|
|
29
|
+
return yield* Effect.sync(() => process.exit(1));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
yield* Console.log(`\nSearching for: "${query}"`);
|
|
33
|
+
yield* Console.log(`Top ${topk} results\n`);
|
|
34
|
+
|
|
35
|
+
const queryEmbedding = yield* Embedder.embedQuery(query);
|
|
36
|
+
|
|
37
|
+
const results: SearchResult[] = [];
|
|
38
|
+
|
|
39
|
+
for (const chunk of manifest.chunks) {
|
|
40
|
+
const embedding = yield* EmbeddingCache.get(chunk.chunkHash);
|
|
41
|
+
|
|
42
|
+
if (embedding && embedding.length > 0) {
|
|
43
|
+
const similarity = Embedder.cosineSimilarity(queryEmbedding, embedding);
|
|
44
|
+
results.push({ chunk, similarity });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
results.sort((a, b) => b.similarity - a.similarity);
|
|
49
|
+
const topResults = results.slice(0, topk);
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < topResults.length; i++) {
|
|
52
|
+
const result = topResults[i]!;
|
|
53
|
+
const { chunk, similarity } = result;
|
|
54
|
+
const { filepath, lineRange } = chunk;
|
|
55
|
+
|
|
56
|
+
if (compact) {
|
|
57
|
+
yield* Console.log(
|
|
58
|
+
`#${i + 1} | ${similarity.toFixed(4)} | ${filepath}:${
|
|
59
|
+
lineRange.start + 1
|
|
60
|
+
}-${lineRange.end + 1}`
|
|
61
|
+
);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
yield* Console.log("─".repeat(80));
|
|
66
|
+
yield* Console.log(
|
|
67
|
+
`#${i + 1} | Score: ${similarity.toFixed(4)} | ${filepath}:${
|
|
68
|
+
lineRange.start + 1
|
|
69
|
+
}-${lineRange.end + 1}`
|
|
70
|
+
);
|
|
71
|
+
yield* Console.log("─".repeat(80));
|
|
72
|
+
|
|
73
|
+
const codeBlock = yield* Effect.tryPromise({
|
|
74
|
+
try: async () => {
|
|
75
|
+
const file = Bun.file(`${pwd}/${filepath}`);
|
|
76
|
+
const content = await file.text();
|
|
77
|
+
const lines = content.split("\n");
|
|
78
|
+
return lines.slice(lineRange.start, lineRange.end + 1).join("\n");
|
|
79
|
+
},
|
|
80
|
+
catch: () => chunk.text,
|
|
81
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(chunk.text)));
|
|
82
|
+
|
|
83
|
+
yield* Console.log(codeBlock);
|
|
84
|
+
yield* Console.log("");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (topResults.length === 0) {
|
|
88
|
+
yield* Console.log("No results found.");
|
|
89
|
+
}
|
|
90
|
+
}).pipe(
|
|
91
|
+
Effect.catchTags({
|
|
92
|
+
ApiKeyMissingError: (e) =>
|
|
93
|
+
Console.error(`Error: ${e.message}`).pipe(
|
|
94
|
+
Effect.flatMap(() => Effect.sync(() => process.exit(1)))
|
|
95
|
+
),
|
|
96
|
+
EmbeddingError: (e) =>
|
|
97
|
+
Console.error(`Error embedding query: ${e.message}`).pipe(
|
|
98
|
+
Effect.flatMap(() => Effect.sync(() => process.exit(1)))
|
|
99
|
+
),
|
|
100
|
+
ConfigError: (e) =>
|
|
101
|
+
Console.error(`Config error: ${e.message}`).pipe(
|
|
102
|
+
Effect.flatMap(() => Effect.sync(() => process.exit(1)))
|
|
103
|
+
),
|
|
104
|
+
FileReadError: (e) =>
|
|
105
|
+
Console.error(`File read error: ${e.path}`).pipe(
|
|
106
|
+
Effect.flatMap(() => Effect.sync(() => process.exit(1)))
|
|
107
|
+
),
|
|
108
|
+
})
|
|
109
|
+
);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Effect, Console } from "effect";
|
|
2
|
+
import { GlobalStore } from "../../storage/global-store";
|
|
3
|
+
import { Indexer } from "../../core/indexer";
|
|
4
|
+
import { GitTracker } from "../../watcher/git-tracker";
|
|
5
|
+
import { createFileWatcher } from "../../watcher/file-watcher";
|
|
6
|
+
|
|
7
|
+
const performInitialSync = (pwd: string) =>
|
|
8
|
+
Effect.gen(function* () {
|
|
9
|
+
yield* Console.log("[Initial Sync] Checking for changes...");
|
|
10
|
+
|
|
11
|
+
const { changes, currentTree, isFullReindex } =
|
|
12
|
+
yield* GitTracker.getFilesToIndex(pwd);
|
|
13
|
+
|
|
14
|
+
if (changes.length === 0) {
|
|
15
|
+
yield* Console.log(
|
|
16
|
+
"[Initial Sync] No changes detected, index is up to date"
|
|
17
|
+
);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (isFullReindex) {
|
|
22
|
+
yield* Console.log(
|
|
23
|
+
`[Initial Sync] Full reindex required (${changes.length} files)`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const chunks = yield* Indexer.indexProject(pwd);
|
|
27
|
+
|
|
28
|
+
yield* Indexer.saveIndexedChunks(
|
|
29
|
+
pwd,
|
|
30
|
+
[...chunks],
|
|
31
|
+
currentTree.commitHash
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
yield* GitTracker.saveGitTree(pwd, currentTree);
|
|
35
|
+
|
|
36
|
+
yield* Console.log("[Initial Sync] Complete");
|
|
37
|
+
} else {
|
|
38
|
+
yield* Console.log(
|
|
39
|
+
`[Initial Sync] Incremental update (${changes.length} files changed)`
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const addedOrModified = changes.filter((c) => c.changeType !== "deleted");
|
|
43
|
+
const deleted = changes.filter((c) => c.changeType === "deleted");
|
|
44
|
+
|
|
45
|
+
if (addedOrModified.length > 0) {
|
|
46
|
+
const chunks = yield* Indexer.indexFiles(
|
|
47
|
+
pwd,
|
|
48
|
+
addedOrModified.map((c) => c.filepath)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
yield* Indexer.updateManifestWithChanges(
|
|
52
|
+
pwd,
|
|
53
|
+
[...chunks],
|
|
54
|
+
deleted.map((c) => c.filepath),
|
|
55
|
+
currentTree.commitHash
|
|
56
|
+
);
|
|
57
|
+
} else if (deleted.length > 0) {
|
|
58
|
+
yield* Indexer.updateManifestWithChanges(
|
|
59
|
+
pwd,
|
|
60
|
+
[],
|
|
61
|
+
deleted.map((c) => c.filepath)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
yield* GitTracker.saveGitTree(pwd, currentTree);
|
|
66
|
+
|
|
67
|
+
yield* Console.log("[Initial Sync] Complete");
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const handleFileChanges = (pwd: string, changedFiles: string[]) =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
yield* Console.log(
|
|
74
|
+
`\n[Watcher] Detected changes in ${changedFiles.length} file(s)`
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
for (const file of changedFiles) {
|
|
78
|
+
yield* Console.log(` - ${file}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const chunks = yield* Indexer.indexFiles(pwd, changedFiles);
|
|
82
|
+
|
|
83
|
+
const commitHash = yield* GitTracker.getCurrentCommitHash(pwd);
|
|
84
|
+
|
|
85
|
+
yield* Indexer.updateManifestWithChanges(
|
|
86
|
+
pwd,
|
|
87
|
+
[...chunks],
|
|
88
|
+
[],
|
|
89
|
+
commitHash ?? undefined
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const currentFileHashes = yield* GitTracker.getGitTree(pwd);
|
|
93
|
+
yield* GitTracker.saveGitTree(pwd, {
|
|
94
|
+
commitHash: commitHash ?? "no-commit",
|
|
95
|
+
fileHashes: currentFileHashes,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
yield* Console.log(
|
|
99
|
+
`[Watcher] Indexed ${chunks.length} chunks from ${changedFiles.length} file(s)\n`
|
|
100
|
+
);
|
|
101
|
+
}).pipe(
|
|
102
|
+
Effect.catchAll((error) =>
|
|
103
|
+
Console.error(`[Watcher] Error indexing changes: ${error}`)
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
export const watchCommand = () =>
|
|
108
|
+
Effect.gen(function* () {
|
|
109
|
+
const pwd = process.cwd();
|
|
110
|
+
|
|
111
|
+
yield* GlobalStore.getApiKeyOrFail;
|
|
112
|
+
|
|
113
|
+
yield* GlobalStore.ensureDirectories;
|
|
114
|
+
|
|
115
|
+
yield* Console.log(`[semantic] Starting watch for: ${pwd}`);
|
|
116
|
+
yield* Console.log("");
|
|
117
|
+
|
|
118
|
+
yield* performInitialSync(pwd);
|
|
119
|
+
|
|
120
|
+
const watcher = createFileWatcher(pwd, {
|
|
121
|
+
debounceMs: 500,
|
|
122
|
+
onChanges: async (changedFiles) => {
|
|
123
|
+
await Effect.runPromise(handleFileChanges(pwd, changedFiles));
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
watcher.start();
|
|
128
|
+
|
|
129
|
+
const shutdown = () => {
|
|
130
|
+
console.log("\n[semantic] Shutting down...");
|
|
131
|
+
watcher.stop();
|
|
132
|
+
process.exit(0);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
process.on("SIGINT", shutdown);
|
|
136
|
+
process.on("SIGTERM", shutdown);
|
|
137
|
+
|
|
138
|
+
yield* Console.log("");
|
|
139
|
+
yield* Console.log(
|
|
140
|
+
"[semantic] Watching for changes. Press Ctrl+C to stop."
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
yield* Effect.never;
|
|
144
|
+
}).pipe(
|
|
145
|
+
Effect.catchTags({
|
|
146
|
+
ApiKeyMissingError: (e) =>
|
|
147
|
+
Console.error(`Error: ${e.message}`).pipe(
|
|
148
|
+
Effect.flatMap(() => Effect.sync(() => process.exit(1)))
|
|
149
|
+
),
|
|
150
|
+
ConfigError: (e) =>
|
|
151
|
+
Console.error(`Config error: ${e.message}`).pipe(
|
|
152
|
+
Effect.flatMap(() => Effect.sync(() => process.exit(1)))
|
|
153
|
+
),
|
|
154
|
+
GitError: (e) =>
|
|
155
|
+
Console.error(`Git error (${e.command}): ${e.message}`).pipe(
|
|
156
|
+
Effect.flatMap(() => Effect.sync(() => process.exit(1)))
|
|
157
|
+
),
|
|
158
|
+
})
|
|
159
|
+
);
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { clearRepoCacheCommand } from "./commands/cache";
|
|
4
|
+
import { configCommand } from "./commands/config";
|
|
5
|
+
import { searchCommand } from "./commands/search";
|
|
6
|
+
import { watchCommand } from "./commands/watch";
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name("semantic")
|
|
12
|
+
.description("Semantic code search CLI")
|
|
13
|
+
.version("1.0.0");
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command("watch")
|
|
17
|
+
.description("Watch and index the current directory")
|
|
18
|
+
.action(async () => {
|
|
19
|
+
await Effect.runPromise(watchCommand());
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command("search <query>")
|
|
24
|
+
.description("Search the indexed codebase")
|
|
25
|
+
.option("--topk <number>", "Number of results to return", "10")
|
|
26
|
+
.option("--compact", "Compact output format", false)
|
|
27
|
+
.action(
|
|
28
|
+
async (query: string, options: { topk: string; compact: boolean }) => {
|
|
29
|
+
const topk = Math.min(20, Math.max(1, parseInt(options.topk, 10) || 10));
|
|
30
|
+
await Effect.runPromise(
|
|
31
|
+
searchCommand(query, { topk, compact: options.compact })
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const cacheCmd = program.command("cache").description("Manage repo cache");
|
|
37
|
+
|
|
38
|
+
cacheCmd
|
|
39
|
+
.command("clear")
|
|
40
|
+
.description("Clear repo cache for current directory (keeps embeddings)")
|
|
41
|
+
.action(async () => {
|
|
42
|
+
await Effect.runPromise(clearRepoCacheCommand());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const configCmd = program.command("config").description("Manage configuration");
|
|
46
|
+
|
|
47
|
+
configCmd
|
|
48
|
+
.command("add-api-key <key>")
|
|
49
|
+
.description("Store your Voyage AI API key")
|
|
50
|
+
.action(async (key: string) => {
|
|
51
|
+
await Effect.runPromise(configCommand("add-api-key", key));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
configCmd
|
|
55
|
+
.command("show")
|
|
56
|
+
.description("Show current configuration")
|
|
57
|
+
.action(async () => {
|
|
58
|
+
await Effect.runPromise(configCommand("show"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export async function main(): Promise<void> {
|
|
62
|
+
await program.parseAsync(process.argv);
|
|
63
|
+
}
|