@kolisachint/hoocode-agent 0.4.140 → 0.4.142
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/CHANGELOG.md +22 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +10 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/embsearch/chunker.d.ts +31 -0
- package/dist/core/embsearch/chunker.d.ts.map +1 -0
- package/dist/core/embsearch/chunker.js +68 -0
- package/dist/core/embsearch/chunker.js.map +1 -0
- package/dist/core/embsearch/client.d.ts +64 -0
- package/dist/core/embsearch/client.d.ts.map +1 -0
- package/dist/core/embsearch/client.js +123 -0
- package/dist/core/embsearch/client.js.map +1 -0
- package/dist/core/embsearch/embsearch-service.d.ts +76 -0
- package/dist/core/embsearch/embsearch-service.d.ts.map +1 -0
- package/dist/core/embsearch/embsearch-service.js +254 -0
- package/dist/core/embsearch/embsearch-service.js.map +1 -0
- package/dist/core/embsearch/index-meta.d.ts +45 -0
- package/dist/core/embsearch/index-meta.d.ts.map +1 -0
- package/dist/core/embsearch/index-meta.js +78 -0
- package/dist/core/embsearch/index-meta.js.map +1 -0
- package/dist/core/embsearch/repo-scan.d.ts +26 -0
- package/dist/core/embsearch/repo-scan.d.ts.map +1 -0
- package/dist/core/embsearch/repo-scan.js +99 -0
- package/dist/core/embsearch/repo-scan.js.map +1 -0
- package/dist/core/sdk.d.ts +8 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +2 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +2 -0
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +14 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +3 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +9 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +4 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +3 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/semantic-search.d.ts +28 -0
- package/dist/core/tools/semantic-search.d.ts.map +1 -0
- package/dist/core/tools/semantic-search.js +98 -0
- package/dist/core/tools/semantic-search.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +83 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +5 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +24 -0
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrates semantic indexing and search for a repository.
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle (all behind --enable-embsearchtools):
|
|
5
|
+
* 1. `start()` — resolve the embsearch binary, scan the repo (ignore-aware),
|
|
6
|
+
* apply the byte threshold. Under threshold → dormant. Over → spawn the
|
|
7
|
+
* daemon, verify the backend is not the mock embedder, then index changed
|
|
8
|
+
* files in the background in small batches, reporting progress.
|
|
9
|
+
* 2. `search()` — top-k semantic query, mapping chunk ids back to
|
|
10
|
+
* `path:start-end` via the sidecar metadata.
|
|
11
|
+
* 3. `dispose()` — save + close the daemon.
|
|
12
|
+
*
|
|
13
|
+
* Every failure degrades to `unavailable` with a reason; nothing here ever
|
|
14
|
+
* blocks session startup or affects grep/find.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from "fs";
|
|
17
|
+
import { ensureTool } from "../../utils/tools-manager.js";
|
|
18
|
+
import { chunkFile } from "./chunker.js";
|
|
19
|
+
import { EmbSearchClient } from "./client.js";
|
|
20
|
+
import { emptyIndexMeta, getEmbsearchStoreDir, getVectorStoreDir, hashContent, loadIndexMeta, saveIndexMeta, } from "./index-meta.js";
|
|
21
|
+
import { scanRepo } from "./repo-scan.js";
|
|
22
|
+
/** Chunks per bulk request. Small enough that a concurrent query is never
|
|
23
|
+
* stuck long behind one padded batch inference. */
|
|
24
|
+
const BULK_BATCH_SIZE = 48;
|
|
25
|
+
/** Yield between batches so background indexing doesn't starve the session. */
|
|
26
|
+
const BATCH_YIELD_MS = 15;
|
|
27
|
+
/** The Rust mock backend's model id — semantically meaningless, never index with it. */
|
|
28
|
+
const MOCK_MODEL_ID = "mock-hash-v1";
|
|
29
|
+
export class EmbsearchService {
|
|
30
|
+
options;
|
|
31
|
+
client;
|
|
32
|
+
meta;
|
|
33
|
+
state = { phase: "idle" };
|
|
34
|
+
disposed = false;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.options = options;
|
|
37
|
+
}
|
|
38
|
+
getState() {
|
|
39
|
+
return this.state;
|
|
40
|
+
}
|
|
41
|
+
/** Semantic search is usable (index ready, or still building with partial data). */
|
|
42
|
+
isAvailable() {
|
|
43
|
+
return this.state.phase === "ready" || this.state.phase === "indexing";
|
|
44
|
+
}
|
|
45
|
+
setState(state) {
|
|
46
|
+
this.state = state;
|
|
47
|
+
this.options.onProgress?.(state);
|
|
48
|
+
}
|
|
49
|
+
async resolveBinary() {
|
|
50
|
+
if (this.options.binaryPath) {
|
|
51
|
+
return this.options.binaryPath;
|
|
52
|
+
}
|
|
53
|
+
return await ensureTool("embsearch", true);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Scan, threshold-check, and (when needed) index in the background.
|
|
57
|
+
* Resolves when indexing completes or the feature settles dormant.
|
|
58
|
+
*/
|
|
59
|
+
async start(signal) {
|
|
60
|
+
try {
|
|
61
|
+
await this.run(signal);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
65
|
+
this.setState({ phase: "unavailable", reason });
|
|
66
|
+
await this.closeClient();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async run(signal) {
|
|
70
|
+
const binary = await this.resolveBinary();
|
|
71
|
+
if (!binary) {
|
|
72
|
+
this.setState({
|
|
73
|
+
phase: "unavailable",
|
|
74
|
+
reason: "embsearch binary not found (PATH or embsearchBinaryPath setting)",
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const scan = scanRepo(this.options.cwd, signal);
|
|
79
|
+
if (scan.totalBytes < this.options.thresholdBytes) {
|
|
80
|
+
this.setState({
|
|
81
|
+
phase: "skipped",
|
|
82
|
+
reason: `repo under threshold (${scan.totalBytes} < ${this.options.thresholdBytes} bytes)`,
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const storeDir = getEmbsearchStoreDir(this.options.cwd);
|
|
87
|
+
this.client = new EmbSearchClient({ binaryPath: binary, storePath: getVectorStoreDir(storeDir) });
|
|
88
|
+
await this.client.ready();
|
|
89
|
+
const info = await this.client.info();
|
|
90
|
+
if (info.modelId === MOCK_MODEL_ID) {
|
|
91
|
+
throw new Error("embsearch binary uses the mock embedder (not semantic); install an onnx build");
|
|
92
|
+
}
|
|
93
|
+
// Missing/stale sidecar (format, chunker, or model changed) → clean rebuild.
|
|
94
|
+
this.meta = loadIndexMeta(storeDir, info.modelId) ?? emptyIndexMeta(this.options.cwd, info.modelId);
|
|
95
|
+
this.meta.lastUsedMs = Date.now();
|
|
96
|
+
await this.indexChangedFiles(scan.files, storeDir, signal);
|
|
97
|
+
}
|
|
98
|
+
async indexChangedFiles(files, storeDir, signal) {
|
|
99
|
+
const meta = this.meta;
|
|
100
|
+
const client = this.client;
|
|
101
|
+
// Diff scan vs sidecar: cheap mtime+size check first, hash only on delta.
|
|
102
|
+
const toIndex = [];
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
seen.add(file.rel);
|
|
106
|
+
const known = meta.files[file.rel];
|
|
107
|
+
if (known && known.mtimeMs === file.mtimeMs && known.size === file.size)
|
|
108
|
+
continue;
|
|
109
|
+
let content;
|
|
110
|
+
try {
|
|
111
|
+
content = readFileSync(file.abs, "utf-8");
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const hash = hashContent(content);
|
|
117
|
+
if (known && known.hash === hash) {
|
|
118
|
+
// Touched but unchanged — refresh stat info only.
|
|
119
|
+
known.mtimeMs = file.mtimeMs;
|
|
120
|
+
known.size = file.size;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
toIndex.push({ file, content, hash });
|
|
124
|
+
}
|
|
125
|
+
const toRemove = Object.keys(meta.files).filter((rel) => !seen.has(rel));
|
|
126
|
+
if (toIndex.length === 0 && toRemove.length === 0) {
|
|
127
|
+
saveIndexMeta(storeDir, meta);
|
|
128
|
+
this.setState({ phase: "ready", chunkCount: this.countChunks(meta) });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Chunk changed files; count total upserts for exact progress.
|
|
132
|
+
const work = [];
|
|
133
|
+
let totalChunks = 0;
|
|
134
|
+
for (const { file, content, hash } of toIndex) {
|
|
135
|
+
const chunks = chunkFile(file.rel, content);
|
|
136
|
+
work.push({
|
|
137
|
+
rel: file.rel,
|
|
138
|
+
fileMeta: {
|
|
139
|
+
mtimeMs: file.mtimeMs,
|
|
140
|
+
size: file.size,
|
|
141
|
+
hash,
|
|
142
|
+
chunks: chunks.map((c) => [c.startLine, c.endLine]),
|
|
143
|
+
},
|
|
144
|
+
chunks: chunks.map((c) => ({ id: c.id, text: c.text })),
|
|
145
|
+
});
|
|
146
|
+
totalChunks += chunks.length;
|
|
147
|
+
}
|
|
148
|
+
this.setState({ phase: "indexing", done: 0, total: totalChunks });
|
|
149
|
+
// Drop vectors of deleted files and superseded chunk tails.
|
|
150
|
+
for (const rel of toRemove) {
|
|
151
|
+
for (let i = 0; i < meta.files[rel].chunks.length; i++)
|
|
152
|
+
await client.remove(`${rel}#${i}`);
|
|
153
|
+
delete meta.files[rel];
|
|
154
|
+
}
|
|
155
|
+
let done = 0;
|
|
156
|
+
for (const item of work) {
|
|
157
|
+
if (signal?.aborted || this.disposed)
|
|
158
|
+
return;
|
|
159
|
+
const oldChunkCount = meta.files[item.rel]?.chunks.length ?? 0;
|
|
160
|
+
// Remove old chunks beyond the new count (upsert covers the rest).
|
|
161
|
+
for (let i = item.chunks.length; i < oldChunkCount; i++)
|
|
162
|
+
await client.remove(`${item.rel}#${i}`);
|
|
163
|
+
for (let offset = 0; offset < item.chunks.length; offset += BULK_BATCH_SIZE) {
|
|
164
|
+
if (signal?.aborted || this.disposed)
|
|
165
|
+
return;
|
|
166
|
+
const batch = item.chunks.slice(offset, offset + BULK_BATCH_SIZE);
|
|
167
|
+
await client.bulk(batch);
|
|
168
|
+
done += batch.length;
|
|
169
|
+
this.setState({ phase: "indexing", done, total: totalChunks });
|
|
170
|
+
// Yield so queries and the event loop stay responsive.
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, BATCH_YIELD_MS));
|
|
172
|
+
}
|
|
173
|
+
meta.files[item.rel] = item.fileMeta;
|
|
174
|
+
}
|
|
175
|
+
await client.compact();
|
|
176
|
+
await client.save();
|
|
177
|
+
saveIndexMeta(storeDir, meta);
|
|
178
|
+
this.setState({ phase: "ready", chunkCount: this.countChunks(meta) });
|
|
179
|
+
}
|
|
180
|
+
countChunks(meta) {
|
|
181
|
+
let n = 0;
|
|
182
|
+
for (const rel of Object.keys(meta.files))
|
|
183
|
+
n += meta.files[rel].chunks.length;
|
|
184
|
+
return n;
|
|
185
|
+
}
|
|
186
|
+
/** Top-`k` semantic hits as `path` + line range + score. */
|
|
187
|
+
async search(query, k = 10) {
|
|
188
|
+
if (!this.client || this.client.isClosed || !this.meta) {
|
|
189
|
+
throw new Error("semantic index is not available");
|
|
190
|
+
}
|
|
191
|
+
const results = await this.client.query(query, k);
|
|
192
|
+
const hits = [];
|
|
193
|
+
for (const result of results) {
|
|
194
|
+
const sep = result.id.lastIndexOf("#");
|
|
195
|
+
if (sep === -1)
|
|
196
|
+
continue;
|
|
197
|
+
const rel = result.id.slice(0, sep);
|
|
198
|
+
const chunkIndex = Number.parseInt(result.id.slice(sep + 1), 10);
|
|
199
|
+
const range = this.meta.files[rel]?.chunks[chunkIndex];
|
|
200
|
+
if (!range)
|
|
201
|
+
continue;
|
|
202
|
+
hits.push({ path: rel, startLine: range[0], endLine: range[1], score: result.score });
|
|
203
|
+
}
|
|
204
|
+
return hits;
|
|
205
|
+
}
|
|
206
|
+
async closeClient() {
|
|
207
|
+
const client = this.client;
|
|
208
|
+
this.client = undefined;
|
|
209
|
+
if (client && !client.isClosed) {
|
|
210
|
+
try {
|
|
211
|
+
await client.close();
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// already dead
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Persist state and shut the daemon down. Safe to call twice. */
|
|
219
|
+
async dispose() {
|
|
220
|
+
if (this.disposed)
|
|
221
|
+
return;
|
|
222
|
+
this.disposed = true;
|
|
223
|
+
if (this.client && !this.client.isClosed) {
|
|
224
|
+
try {
|
|
225
|
+
await this.client.save();
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// daemon may have exited; nothing to save
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
await this.closeClient();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// --- Per-cwd service registry ---
|
|
235
|
+
//
|
|
236
|
+
// The semantic_search tool is constructed by the generic tool factory table and
|
|
237
|
+
// only receives `cwd`; the service is created later during session init (flag
|
|
238
|
+
// gated). This registry connects the two without threading a service instance
|
|
239
|
+
// through every layer between main.ts and the tool factories.
|
|
240
|
+
const services = new Map();
|
|
241
|
+
export function registerEmbsearchService(cwd, service) {
|
|
242
|
+
const old = services.get(cwd);
|
|
243
|
+
if (old && old !== service) {
|
|
244
|
+
old.dispose().catch(() => { });
|
|
245
|
+
}
|
|
246
|
+
services.set(cwd, service);
|
|
247
|
+
}
|
|
248
|
+
export function getEmbsearchService(cwd) {
|
|
249
|
+
return services.get(cwd);
|
|
250
|
+
}
|
|
251
|
+
export function unregisterEmbsearchService(cwd) {
|
|
252
|
+
services.delete(cwd);
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=embsearch-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embsearch-service.js","sourceRoot":"","sources":["../../../src/core/embsearch/embsearch-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Orchestrates semantic indexing and search for a repository.\n *\n * Lifecycle (all behind --enable-embsearchtools):\n * 1. `start()` — resolve the embsearch binary, scan the repo (ignore-aware),\n * apply the byte threshold. Under threshold → dormant. Over → spawn the\n * daemon, verify the backend is not the mock embedder, then index changed\n * files in the background in small batches, reporting progress.\n * 2. `search()` — top-k semantic query, mapping chunk ids back to\n * `path:start-end` via the sidecar metadata.\n * 3. `dispose()` — save + close the daemon.\n *\n * Every failure degrades to `unavailable` with a reason; nothing here ever\n * blocks session startup or affects grep/find.\n */\n\nimport { readFileSync } from \"fs\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { chunkFile } from \"./chunker.js\";\nimport { EmbSearchClient } from \"./client.js\";\nimport {\n\temptyIndexMeta,\n\ttype FileMeta,\n\tgetEmbsearchStoreDir,\n\tgetVectorStoreDir,\n\thashContent,\n\ttype IndexMeta,\n\tloadIndexMeta,\n\tsaveIndexMeta,\n} from \"./index-meta.js\";\nimport { type RepoScanFile, scanRepo } from \"./repo-scan.js\";\n\n/** Chunks per bulk request. Small enough that a concurrent query is never\n * stuck long behind one padded batch inference. */\nconst BULK_BATCH_SIZE = 48;\n/** Yield between batches so background indexing doesn't starve the session. */\nconst BATCH_YIELD_MS = 15;\n/** The Rust mock backend's model id — semantically meaningless, never index with it. */\nconst MOCK_MODEL_ID = \"mock-hash-v1\";\n\nexport type EmbsearchState =\n\t| { phase: \"idle\" }\n\t| { phase: \"skipped\"; reason: string }\n\t| { phase: \"indexing\"; done: number; total: number }\n\t| { phase: \"ready\"; chunkCount: number }\n\t| { phase: \"unavailable\"; reason: string };\n\nexport interface EmbsearchServiceOptions {\n\tcwd: string;\n\t/** Explicit binary path (settings override). Default: \"embsearch\" from PATH. */\n\tbinaryPath?: string;\n\t/** Minimum indexable bytes before indexing kicks in. */\n\tthresholdBytes: number;\n\t/** Progress callback for UI (footer / stderr lines). */\n\tonProgress?: (state: EmbsearchState) => void;\n}\n\nexport interface SemanticHit {\n\tpath: string;\n\tstartLine: number;\n\tendLine: number;\n\tscore: number;\n}\n\nexport class EmbsearchService {\n\tprivate readonly options: EmbsearchServiceOptions;\n\tprivate client: EmbSearchClient | undefined;\n\tprivate meta: IndexMeta | undefined;\n\tprivate state: EmbsearchState = { phase: \"idle\" };\n\tprivate disposed = false;\n\n\tconstructor(options: EmbsearchServiceOptions) {\n\t\tthis.options = options;\n\t}\n\n\tgetState(): EmbsearchState {\n\t\treturn this.state;\n\t}\n\n\t/** Semantic search is usable (index ready, or still building with partial data). */\n\tisAvailable(): boolean {\n\t\treturn this.state.phase === \"ready\" || this.state.phase === \"indexing\";\n\t}\n\n\tprivate setState(state: EmbsearchState): void {\n\t\tthis.state = state;\n\t\tthis.options.onProgress?.(state);\n\t}\n\n\tprivate async resolveBinary(): Promise<string | undefined> {\n\t\tif (this.options.binaryPath) {\n\t\t\treturn this.options.binaryPath;\n\t\t}\n\t\treturn await ensureTool(\"embsearch\", true);\n\t}\n\n\t/**\n\t * Scan, threshold-check, and (when needed) index in the background.\n\t * Resolves when indexing completes or the feature settles dormant.\n\t */\n\tasync start(signal?: AbortSignal): Promise<void> {\n\t\ttry {\n\t\t\tawait this.run(signal);\n\t\t} catch (e) {\n\t\t\tconst reason = e instanceof Error ? e.message : String(e);\n\t\t\tthis.setState({ phase: \"unavailable\", reason });\n\t\t\tawait this.closeClient();\n\t\t}\n\t}\n\n\tprivate async run(signal?: AbortSignal): Promise<void> {\n\t\tconst binary = await this.resolveBinary();\n\t\tif (!binary) {\n\t\t\tthis.setState({\n\t\t\t\tphase: \"unavailable\",\n\t\t\t\treason: \"embsearch binary not found (PATH or embsearchBinaryPath setting)\",\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tconst scan = scanRepo(this.options.cwd, signal);\n\t\tif (scan.totalBytes < this.options.thresholdBytes) {\n\t\t\tthis.setState({\n\t\t\t\tphase: \"skipped\",\n\t\t\t\treason: `repo under threshold (${scan.totalBytes} < ${this.options.thresholdBytes} bytes)`,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tconst storeDir = getEmbsearchStoreDir(this.options.cwd);\n\t\tthis.client = new EmbSearchClient({ binaryPath: binary, storePath: getVectorStoreDir(storeDir) });\n\t\tawait this.client.ready();\n\n\t\tconst info = await this.client.info();\n\t\tif (info.modelId === MOCK_MODEL_ID) {\n\t\t\tthrow new Error(\"embsearch binary uses the mock embedder (not semantic); install an onnx build\");\n\t\t}\n\n\t\t// Missing/stale sidecar (format, chunker, or model changed) → clean rebuild.\n\t\tthis.meta = loadIndexMeta(storeDir, info.modelId) ?? emptyIndexMeta(this.options.cwd, info.modelId);\n\t\tthis.meta.lastUsedMs = Date.now();\n\n\t\tawait this.indexChangedFiles(scan.files, storeDir, signal);\n\t}\n\n\tprivate async indexChangedFiles(files: RepoScanFile[], storeDir: string, signal?: AbortSignal): Promise<void> {\n\t\tconst meta = this.meta!;\n\t\tconst client = this.client!;\n\n\t\t// Diff scan vs sidecar: cheap mtime+size check first, hash only on delta.\n\t\tconst toIndex: Array<{ file: RepoScanFile; content: string; hash: string }> = [];\n\t\tconst seen = new Set<string>();\n\t\tfor (const file of files) {\n\t\t\tseen.add(file.rel);\n\t\t\tconst known = meta.files[file.rel];\n\t\t\tif (known && known.mtimeMs === file.mtimeMs && known.size === file.size) continue;\n\t\t\tlet content: string;\n\t\t\ttry {\n\t\t\t\tcontent = readFileSync(file.abs, \"utf-8\");\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst hash = hashContent(content);\n\t\t\tif (known && known.hash === hash) {\n\t\t\t\t// Touched but unchanged — refresh stat info only.\n\t\t\t\tknown.mtimeMs = file.mtimeMs;\n\t\t\t\tknown.size = file.size;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttoIndex.push({ file, content, hash });\n\t\t}\n\t\tconst toRemove = Object.keys(meta.files).filter((rel) => !seen.has(rel));\n\n\t\tif (toIndex.length === 0 && toRemove.length === 0) {\n\t\t\tsaveIndexMeta(storeDir, meta);\n\t\t\tthis.setState({ phase: \"ready\", chunkCount: this.countChunks(meta) });\n\t\t\treturn;\n\t\t}\n\n\t\t// Chunk changed files; count total upserts for exact progress.\n\t\tconst work: Array<{ rel: string; fileMeta: FileMeta; chunks: Array<{ id: string; text: string }> }> = [];\n\t\tlet totalChunks = 0;\n\t\tfor (const { file, content, hash } of toIndex) {\n\t\t\tconst chunks = chunkFile(file.rel, content);\n\t\t\twork.push({\n\t\t\t\trel: file.rel,\n\t\t\t\tfileMeta: {\n\t\t\t\t\tmtimeMs: file.mtimeMs,\n\t\t\t\t\tsize: file.size,\n\t\t\t\t\thash,\n\t\t\t\t\tchunks: chunks.map((c) => [c.startLine, c.endLine]),\n\t\t\t\t},\n\t\t\t\tchunks: chunks.map((c) => ({ id: c.id, text: c.text })),\n\t\t\t});\n\t\t\ttotalChunks += chunks.length;\n\t\t}\n\n\t\tthis.setState({ phase: \"indexing\", done: 0, total: totalChunks });\n\n\t\t// Drop vectors of deleted files and superseded chunk tails.\n\t\tfor (const rel of toRemove) {\n\t\t\tfor (let i = 0; i < meta.files[rel].chunks.length; i++) await client.remove(`${rel}#${i}`);\n\t\t\tdelete meta.files[rel];\n\t\t}\n\n\t\tlet done = 0;\n\t\tfor (const item of work) {\n\t\t\tif (signal?.aborted || this.disposed) return;\n\t\t\tconst oldChunkCount = meta.files[item.rel]?.chunks.length ?? 0;\n\t\t\t// Remove old chunks beyond the new count (upsert covers the rest).\n\t\t\tfor (let i = item.chunks.length; i < oldChunkCount; i++) await client.remove(`${item.rel}#${i}`);\n\n\t\t\tfor (let offset = 0; offset < item.chunks.length; offset += BULK_BATCH_SIZE) {\n\t\t\t\tif (signal?.aborted || this.disposed) return;\n\t\t\t\tconst batch = item.chunks.slice(offset, offset + BULK_BATCH_SIZE);\n\t\t\t\tawait client.bulk(batch);\n\t\t\t\tdone += batch.length;\n\t\t\t\tthis.setState({ phase: \"indexing\", done, total: totalChunks });\n\t\t\t\t// Yield so queries and the event loop stay responsive.\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, BATCH_YIELD_MS));\n\t\t\t}\n\t\t\tmeta.files[item.rel] = item.fileMeta;\n\t\t}\n\n\t\tawait client.compact();\n\t\tawait client.save();\n\t\tsaveIndexMeta(storeDir, meta);\n\t\tthis.setState({ phase: \"ready\", chunkCount: this.countChunks(meta) });\n\t}\n\n\tprivate countChunks(meta: IndexMeta): number {\n\t\tlet n = 0;\n\t\tfor (const rel of Object.keys(meta.files)) n += meta.files[rel].chunks.length;\n\t\treturn n;\n\t}\n\n\t/** Top-`k` semantic hits as `path` + line range + score. */\n\tasync search(query: string, k = 10): Promise<SemanticHit[]> {\n\t\tif (!this.client || this.client.isClosed || !this.meta) {\n\t\t\tthrow new Error(\"semantic index is not available\");\n\t\t}\n\t\tconst results = await this.client.query(query, k);\n\t\tconst hits: SemanticHit[] = [];\n\t\tfor (const result of results) {\n\t\t\tconst sep = result.id.lastIndexOf(\"#\");\n\t\t\tif (sep === -1) continue;\n\t\t\tconst rel = result.id.slice(0, sep);\n\t\t\tconst chunkIndex = Number.parseInt(result.id.slice(sep + 1), 10);\n\t\t\tconst range = this.meta.files[rel]?.chunks[chunkIndex];\n\t\t\tif (!range) continue;\n\t\t\thits.push({ path: rel, startLine: range[0], endLine: range[1], score: result.score });\n\t\t}\n\t\treturn hits;\n\t}\n\n\tprivate async closeClient(): Promise<void> {\n\t\tconst client = this.client;\n\t\tthis.client = undefined;\n\t\tif (client && !client.isClosed) {\n\t\t\ttry {\n\t\t\t\tawait client.close();\n\t\t\t} catch {\n\t\t\t\t// already dead\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Persist state and shut the daemon down. Safe to call twice. */\n\tasync dispose(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\t\tif (this.client && !this.client.isClosed) {\n\t\t\ttry {\n\t\t\t\tawait this.client.save();\n\t\t\t} catch {\n\t\t\t\t// daemon may have exited; nothing to save\n\t\t\t}\n\t\t}\n\t\tawait this.closeClient();\n\t}\n}\n\n// --- Per-cwd service registry ---\n//\n// The semantic_search tool is constructed by the generic tool factory table and\n// only receives `cwd`; the service is created later during session init (flag\n// gated). This registry connects the two without threading a service instance\n// through every layer between main.ts and the tool factories.\n\nconst services = new Map<string, EmbsearchService>();\n\nexport function registerEmbsearchService(cwd: string, service: EmbsearchService): void {\n\tconst old = services.get(cwd);\n\tif (old && old !== service) {\n\t\told.dispose().catch(() => {});\n\t}\n\tservices.set(cwd, service);\n}\n\nexport function getEmbsearchService(cwd: string): EmbsearchService | undefined {\n\treturn services.get(cwd);\n}\n\nexport function unregisterEmbsearchService(cwd: string): void {\n\tservices.delete(cwd);\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidecar metadata for a semantic index store.
|
|
3
|
+
*
|
|
4
|
+
* The Rust store (manifest.json/ids.json/vectors.bin) only knows vector ids;
|
|
5
|
+
* this sidecar (index-meta.json, written next to the store) tracks the source
|
|
6
|
+
* side: per-file freshness (mtime+size, content hash recomputed only when they
|
|
7
|
+
* change) and each file's chunk line-ranges so search hits can be rendered as
|
|
8
|
+
* `path:start-end`. It also pins the chunker version and embedding model id —
|
|
9
|
+
* a mismatch on either triggers a clean rebuild instead of an inconsistent
|
|
10
|
+
* incremental update.
|
|
11
|
+
*/
|
|
12
|
+
export interface FileMeta {
|
|
13
|
+
mtimeMs: number;
|
|
14
|
+
size: number;
|
|
15
|
+
/** SHA-256 of content, hex. Recomputed only when mtime/size differ. */
|
|
16
|
+
hash: string;
|
|
17
|
+
/** Per-chunk 1-based inclusive [start, end] line ranges; index = chunk number. */
|
|
18
|
+
chunks: Array<[number, number]>;
|
|
19
|
+
}
|
|
20
|
+
export interface IndexMeta {
|
|
21
|
+
formatVersion: number;
|
|
22
|
+
chunkerVersion: number;
|
|
23
|
+
modelId: string;
|
|
24
|
+
/** Absolute repo root this index was built from. */
|
|
25
|
+
repoRoot: string;
|
|
26
|
+
/** Last time this index was opened (ms). Enables later GC of dead stores. */
|
|
27
|
+
lastUsedMs: number;
|
|
28
|
+
files: Record<string, FileMeta>;
|
|
29
|
+
}
|
|
30
|
+
/** Directory holding the vector store + sidecar for `repoRoot`. */
|
|
31
|
+
export declare function getEmbsearchStoreDir(repoRoot: string): string;
|
|
32
|
+
/** Subdirectory the Rust store lives in (sidecar sits next to it). */
|
|
33
|
+
export declare function getVectorStoreDir(storeDir: string): string;
|
|
34
|
+
export declare function emptyIndexMeta(repoRoot: string, modelId: string): IndexMeta;
|
|
35
|
+
/**
|
|
36
|
+
* Load the sidecar. Returns undefined when absent, unreadable, or built with a
|
|
37
|
+
* different format/chunker/model — callers treat all of those as "rebuild".
|
|
38
|
+
*/
|
|
39
|
+
export declare function loadIndexMeta(storeDir: string, modelId: string): IndexMeta | undefined;
|
|
40
|
+
/** Atomically persist the sidecar (temp + rename, matching the Rust store). */
|
|
41
|
+
export declare function saveIndexMeta(storeDir: string, meta: IndexMeta): void;
|
|
42
|
+
export declare function hashContent(content: string): string;
|
|
43
|
+
/** Display helper: `~/.hoocode/embsearch/<hash>` instead of the absolute path. */
|
|
44
|
+
export declare function shortenStoreDir(storeDir: string): string;
|
|
45
|
+
//# sourceMappingURL=index-meta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-meta.d.ts","sourceRoot":"","sources":["../../../src/core/embsearch/index-meta.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAYH,MAAM,WAAW,QAAQ;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,SAAS;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAChC;AAED,mEAAmE;AACnE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,sEAAsE;AACtE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAS3E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAiBtF;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAMrE;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGxD","sourcesContent":["/**\n * Sidecar metadata for a semantic index store.\n *\n * The Rust store (manifest.json/ids.json/vectors.bin) only knows vector ids;\n * this sidecar (index-meta.json, written next to the store) tracks the source\n * side: per-file freshness (mtime+size, content hash recomputed only when they\n * change) and each file's chunk line-ranges so search hits can be rendered as\n * `path:start-end`. It also pins the chunker version and embedding model id —\n * a mismatch on either triggers a clean rebuild instead of an inconsistent\n * incremental update.\n */\n\nimport { createHash } from \"crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport path, { join } from \"path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { CHUNKER_VERSION } from \"./chunker.js\";\n\nconst META_FILE = \"index-meta.json\";\nconst META_FORMAT_VERSION = 1;\n\nexport interface FileMeta {\n\tmtimeMs: number;\n\tsize: number;\n\t/** SHA-256 of content, hex. Recomputed only when mtime/size differ. */\n\thash: string;\n\t/** Per-chunk 1-based inclusive [start, end] line ranges; index = chunk number. */\n\tchunks: Array<[number, number]>;\n}\n\nexport interface IndexMeta {\n\tformatVersion: number;\n\tchunkerVersion: number;\n\tmodelId: string;\n\t/** Absolute repo root this index was built from. */\n\trepoRoot: string;\n\t/** Last time this index was opened (ms). Enables later GC of dead stores. */\n\tlastUsedMs: number;\n\tfiles: Record<string, FileMeta>;\n}\n\n/** Directory holding the vector store + sidecar for `repoRoot`. */\nexport function getEmbsearchStoreDir(repoRoot: string): string {\n\tconst resolved = path.resolve(repoRoot);\n\tconst hash = createHash(\"sha256\").update(resolved).digest(\"hex\").slice(0, 16);\n\treturn join(getAgentDir(), \"embsearch\", hash);\n}\n\n/** Subdirectory the Rust store lives in (sidecar sits next to it). */\nexport function getVectorStoreDir(storeDir: string): string {\n\treturn join(storeDir, \"store\");\n}\n\nexport function emptyIndexMeta(repoRoot: string, modelId: string): IndexMeta {\n\treturn {\n\t\tformatVersion: META_FORMAT_VERSION,\n\t\tchunkerVersion: CHUNKER_VERSION,\n\t\tmodelId,\n\t\trepoRoot: path.resolve(repoRoot),\n\t\tlastUsedMs: Date.now(),\n\t\tfiles: {},\n\t};\n}\n\n/**\n * Load the sidecar. Returns undefined when absent, unreadable, or built with a\n * different format/chunker/model — callers treat all of those as \"rebuild\".\n */\nexport function loadIndexMeta(storeDir: string, modelId: string): IndexMeta | undefined {\n\tconst file = join(storeDir, META_FILE);\n\tif (!existsSync(file)) return undefined;\n\tlet meta: IndexMeta;\n\ttry {\n\t\tmeta = JSON.parse(readFileSync(file, \"utf-8\")) as IndexMeta;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (\n\t\tmeta.formatVersion !== META_FORMAT_VERSION ||\n\t\tmeta.chunkerVersion !== CHUNKER_VERSION ||\n\t\tmeta.modelId !== modelId\n\t) {\n\t\treturn undefined;\n\t}\n\treturn meta;\n}\n\n/** Atomically persist the sidecar (temp + rename, matching the Rust store). */\nexport function saveIndexMeta(storeDir: string, meta: IndexMeta): void {\n\tmkdirSync(storeDir, { recursive: true });\n\tconst file = join(storeDir, META_FILE);\n\tconst tmp = `${file}.tmp`;\n\twriteFileSync(tmp, JSON.stringify(meta));\n\trenameSync(tmp, file);\n}\n\nexport function hashContent(content: string): string {\n\treturn createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n/** Display helper: `~/.hoocode/embsearch/<hash>` instead of the absolute path. */\nexport function shortenStoreDir(storeDir: string): string {\n\tconst home = homedir();\n\treturn storeDir.startsWith(home) ? `~${storeDir.slice(home.length)}` : storeDir;\n}\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidecar metadata for a semantic index store.
|
|
3
|
+
*
|
|
4
|
+
* The Rust store (manifest.json/ids.json/vectors.bin) only knows vector ids;
|
|
5
|
+
* this sidecar (index-meta.json, written next to the store) tracks the source
|
|
6
|
+
* side: per-file freshness (mtime+size, content hash recomputed only when they
|
|
7
|
+
* change) and each file's chunk line-ranges so search hits can be rendered as
|
|
8
|
+
* `path:start-end`. It also pins the chunker version and embedding model id —
|
|
9
|
+
* a mismatch on either triggers a clean rebuild instead of an inconsistent
|
|
10
|
+
* incremental update.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "crypto";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import path, { join } from "path";
|
|
16
|
+
import { getAgentDir } from "../../config.js";
|
|
17
|
+
import { CHUNKER_VERSION } from "./chunker.js";
|
|
18
|
+
const META_FILE = "index-meta.json";
|
|
19
|
+
const META_FORMAT_VERSION = 1;
|
|
20
|
+
/** Directory holding the vector store + sidecar for `repoRoot`. */
|
|
21
|
+
export function getEmbsearchStoreDir(repoRoot) {
|
|
22
|
+
const resolved = path.resolve(repoRoot);
|
|
23
|
+
const hash = createHash("sha256").update(resolved).digest("hex").slice(0, 16);
|
|
24
|
+
return join(getAgentDir(), "embsearch", hash);
|
|
25
|
+
}
|
|
26
|
+
/** Subdirectory the Rust store lives in (sidecar sits next to it). */
|
|
27
|
+
export function getVectorStoreDir(storeDir) {
|
|
28
|
+
return join(storeDir, "store");
|
|
29
|
+
}
|
|
30
|
+
export function emptyIndexMeta(repoRoot, modelId) {
|
|
31
|
+
return {
|
|
32
|
+
formatVersion: META_FORMAT_VERSION,
|
|
33
|
+
chunkerVersion: CHUNKER_VERSION,
|
|
34
|
+
modelId,
|
|
35
|
+
repoRoot: path.resolve(repoRoot),
|
|
36
|
+
lastUsedMs: Date.now(),
|
|
37
|
+
files: {},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Load the sidecar. Returns undefined when absent, unreadable, or built with a
|
|
42
|
+
* different format/chunker/model — callers treat all of those as "rebuild".
|
|
43
|
+
*/
|
|
44
|
+
export function loadIndexMeta(storeDir, modelId) {
|
|
45
|
+
const file = join(storeDir, META_FILE);
|
|
46
|
+
if (!existsSync(file))
|
|
47
|
+
return undefined;
|
|
48
|
+
let meta;
|
|
49
|
+
try {
|
|
50
|
+
meta = JSON.parse(readFileSync(file, "utf-8"));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (meta.formatVersion !== META_FORMAT_VERSION ||
|
|
56
|
+
meta.chunkerVersion !== CHUNKER_VERSION ||
|
|
57
|
+
meta.modelId !== modelId) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return meta;
|
|
61
|
+
}
|
|
62
|
+
/** Atomically persist the sidecar (temp + rename, matching the Rust store). */
|
|
63
|
+
export function saveIndexMeta(storeDir, meta) {
|
|
64
|
+
mkdirSync(storeDir, { recursive: true });
|
|
65
|
+
const file = join(storeDir, META_FILE);
|
|
66
|
+
const tmp = `${file}.tmp`;
|
|
67
|
+
writeFileSync(tmp, JSON.stringify(meta));
|
|
68
|
+
renameSync(tmp, file);
|
|
69
|
+
}
|
|
70
|
+
export function hashContent(content) {
|
|
71
|
+
return createHash("sha256").update(content).digest("hex");
|
|
72
|
+
}
|
|
73
|
+
/** Display helper: `~/.hoocode/embsearch/<hash>` instead of the absolute path. */
|
|
74
|
+
export function shortenStoreDir(storeDir) {
|
|
75
|
+
const home = homedir();
|
|
76
|
+
return storeDir.startsWith(home) ? `~${storeDir.slice(home.length)}` : storeDir;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=index-meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-meta.js","sourceRoot":"","sources":["../../../src/core/embsearch/index-meta.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AACpF,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,SAAS,GAAG,iBAAiB,CAAC;AACpC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAsB9B,mEAAmE;AACnE,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAU;IAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC9E,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;AAAA,CAC9C;AAED,sEAAsE;AACtE,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAU;IAC3D,OAAO,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AAAA,CAC/B;AAED,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,OAAe,EAAa;IAC5E,OAAO;QACN,aAAa,EAAE,mBAAmB;QAClC,cAAc,EAAE,eAAe;QAC/B,OAAO;QACP,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;QAChC,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;QACtB,KAAK,EAAE,EAAE;KACT,CAAC;AAAA,CACF;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,OAAe,EAAyB;IACvF,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACvC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,IAAe,CAAC;IACpB,IAAI,CAAC;QACJ,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAc,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,IACC,IAAI,CAAC,aAAa,KAAK,mBAAmB;QAC1C,IAAI,CAAC,cAAc,KAAK,eAAe;QACvC,IAAI,CAAC,OAAO,KAAK,OAAO,EACvB,CAAC;QACF,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,IAAe,EAAQ;IACtE,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACvC,MAAM,GAAG,GAAG,GAAG,IAAI,MAAM,CAAC;IAC1B,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IACzC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAAA,CACtB;AAED,MAAM,UAAU,WAAW,CAAC,OAAe,EAAU;IACpD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CAC1D;AAED,kFAAkF;AAClF,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAU;IACzD,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,OAAO,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;AAAA,CAChF","sourcesContent":["/**\n * Sidecar metadata for a semantic index store.\n *\n * The Rust store (manifest.json/ids.json/vectors.bin) only knows vector ids;\n * this sidecar (index-meta.json, written next to the store) tracks the source\n * side: per-file freshness (mtime+size, content hash recomputed only when they\n * change) and each file's chunk line-ranges so search hits can be rendered as\n * `path:start-end`. It also pins the chunker version and embedding model id —\n * a mismatch on either triggers a clean rebuild instead of an inconsistent\n * incremental update.\n */\n\nimport { createHash } from \"crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport path, { join } from \"path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { CHUNKER_VERSION } from \"./chunker.js\";\n\nconst META_FILE = \"index-meta.json\";\nconst META_FORMAT_VERSION = 1;\n\nexport interface FileMeta {\n\tmtimeMs: number;\n\tsize: number;\n\t/** SHA-256 of content, hex. Recomputed only when mtime/size differ. */\n\thash: string;\n\t/** Per-chunk 1-based inclusive [start, end] line ranges; index = chunk number. */\n\tchunks: Array<[number, number]>;\n}\n\nexport interface IndexMeta {\n\tformatVersion: number;\n\tchunkerVersion: number;\n\tmodelId: string;\n\t/** Absolute repo root this index was built from. */\n\trepoRoot: string;\n\t/** Last time this index was opened (ms). Enables later GC of dead stores. */\n\tlastUsedMs: number;\n\tfiles: Record<string, FileMeta>;\n}\n\n/** Directory holding the vector store + sidecar for `repoRoot`. */\nexport function getEmbsearchStoreDir(repoRoot: string): string {\n\tconst resolved = path.resolve(repoRoot);\n\tconst hash = createHash(\"sha256\").update(resolved).digest(\"hex\").slice(0, 16);\n\treturn join(getAgentDir(), \"embsearch\", hash);\n}\n\n/** Subdirectory the Rust store lives in (sidecar sits next to it). */\nexport function getVectorStoreDir(storeDir: string): string {\n\treturn join(storeDir, \"store\");\n}\n\nexport function emptyIndexMeta(repoRoot: string, modelId: string): IndexMeta {\n\treturn {\n\t\tformatVersion: META_FORMAT_VERSION,\n\t\tchunkerVersion: CHUNKER_VERSION,\n\t\tmodelId,\n\t\trepoRoot: path.resolve(repoRoot),\n\t\tlastUsedMs: Date.now(),\n\t\tfiles: {},\n\t};\n}\n\n/**\n * Load the sidecar. Returns undefined when absent, unreadable, or built with a\n * different format/chunker/model — callers treat all of those as \"rebuild\".\n */\nexport function loadIndexMeta(storeDir: string, modelId: string): IndexMeta | undefined {\n\tconst file = join(storeDir, META_FILE);\n\tif (!existsSync(file)) return undefined;\n\tlet meta: IndexMeta;\n\ttry {\n\t\tmeta = JSON.parse(readFileSync(file, \"utf-8\")) as IndexMeta;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (\n\t\tmeta.formatVersion !== META_FORMAT_VERSION ||\n\t\tmeta.chunkerVersion !== CHUNKER_VERSION ||\n\t\tmeta.modelId !== modelId\n\t) {\n\t\treturn undefined;\n\t}\n\treturn meta;\n}\n\n/** Atomically persist the sidecar (temp + rename, matching the Rust store). */\nexport function saveIndexMeta(storeDir: string, meta: IndexMeta): void {\n\tmkdirSync(storeDir, { recursive: true });\n\tconst file = join(storeDir, META_FILE);\n\tconst tmp = `${file}.tmp`;\n\twriteFileSync(tmp, JSON.stringify(meta));\n\trenameSync(tmp, file);\n}\n\nexport function hashContent(content: string): string {\n\treturn createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n/** Display helper: `~/.hoocode/embsearch/<hash>` instead of the absolute path. */\nexport function shortenStoreDir(storeDir: string): string {\n\tconst home = homedir();\n\treturn storeDir.startsWith(home) ? `~${storeDir.slice(home.length)}` : storeDir;\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository scan for semantic indexing: enumerates indexable source files
|
|
3
|
+
* using the same ignore-aware walk the native find/grep fallbacks use, and
|
|
4
|
+
* totals their bytes so the caller can apply the size threshold.
|
|
5
|
+
*/
|
|
6
|
+
export interface RepoScanFile {
|
|
7
|
+
/** Absolute path. */
|
|
8
|
+
abs: string;
|
|
9
|
+
/** POSIX path relative to the scan root. */
|
|
10
|
+
rel: string;
|
|
11
|
+
/** File size in bytes. */
|
|
12
|
+
size: number;
|
|
13
|
+
/** mtime in ms. */
|
|
14
|
+
mtimeMs: number;
|
|
15
|
+
}
|
|
16
|
+
export interface RepoScanResult {
|
|
17
|
+
files: RepoScanFile[];
|
|
18
|
+
totalBytes: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Enumerate indexable files under `root`: respects hierarchical `.gitignore`,
|
|
22
|
+
* always skips `.git`/`node_modules`, drops binary-ish extensions and files
|
|
23
|
+
* over 1MB. Returns files plus their byte total for threshold checks.
|
|
24
|
+
*/
|
|
25
|
+
export declare function scanRepo(root: string, signal?: AbortSignal): RepoScanResult;
|
|
26
|
+
//# sourceMappingURL=repo-scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-scan.d.ts","sourceRoot":"","sources":["../../../src/core/embsearch/repo-scan.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0DH,MAAM,WAAW,YAAY;IAC5B,qBAAqB;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACnB;AAUD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,cAAc,CAsB3E","sourcesContent":["/**\n * Repository scan for semantic indexing: enumerates indexable source files\n * using the same ignore-aware walk the native find/grep fallbacks use, and\n * totals their bytes so the caller can apply the size threshold.\n */\n\nimport { statSync } from \"fs\";\nimport path from \"path\";\nimport { collectEntries } from \"../tools/native-search.js\";\n\n/** Files larger than this are never indexed (vendored bundles, lockfiles, data dumps). */\nconst MAX_FILE_BYTES = 1024 * 1024;\n\n/** Files that are part of the ignore mechanism, not source. */\nconst SKIP_NAMES = new Set([\".gitignore\", \".gitattributes\", \".gitkeep\"]);\n\n/** Extensions that are never worth embedding. */\nconst SKIP_EXTENSIONS = new Set([\n\t\".png\",\n\t\".jpg\",\n\t\".jpeg\",\n\t\".gif\",\n\t\".webp\",\n\t\".ico\",\n\t\".bmp\",\n\t\".svg\",\n\t\".pdf\",\n\t\".zip\",\n\t\".gz\",\n\t\".tar\",\n\t\".bz2\",\n\t\".xz\",\n\t\".7z\",\n\t\".jar\",\n\t\".war\",\n\t\".class\",\n\t\".exe\",\n\t\".dll\",\n\t\".so\",\n\t\".dylib\",\n\t\".bin\",\n\t\".dat\",\n\t\".onnx\",\n\t\".pt\",\n\t\".safetensors\",\n\t\".woff\",\n\t\".woff2\",\n\t\".ttf\",\n\t\".otf\",\n\t\".eot\",\n\t\".mp3\",\n\t\".mp4\",\n\t\".wav\",\n\t\".avi\",\n\t\".mov\",\n\t\".webm\",\n\t\".lock\",\n\t\".min.js\",\n\t\".min.css\",\n\t\".map\",\n]);\n\nexport interface RepoScanFile {\n\t/** Absolute path. */\n\tabs: string;\n\t/** POSIX path relative to the scan root. */\n\trel: string;\n\t/** File size in bytes. */\n\tsize: number;\n\t/** mtime in ms. */\n\tmtimeMs: number;\n}\n\nexport interface RepoScanResult {\n\tfiles: RepoScanFile[];\n\ttotalBytes: number;\n}\n\nfunction hasSkippedExtension(rel: string): boolean {\n\tconst lower = rel.toLowerCase();\n\tfor (const ext of SKIP_EXTENSIONS) {\n\t\tif (lower.endsWith(ext)) return true;\n\t}\n\treturn false;\n}\n\n/**\n * Enumerate indexable files under `root`: respects hierarchical `.gitignore`,\n * always skips `.git`/`node_modules`, drops binary-ish extensions and files\n * over 1MB. Returns files plus their byte total for threshold checks.\n */\nexport function scanRepo(root: string, signal?: AbortSignal): RepoScanResult {\n\tconst entries = collectEntries(root, {\n\t\tsignal,\n\t\talwaysSkipDirs: new Set([\".git\", \"node_modules\"]),\n\t});\n\tconst files: RepoScanFile[] = [];\n\tlet totalBytes = 0;\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"f\") continue;\n\t\tif (SKIP_NAMES.has(path.basename(entry.rel))) continue;\n\t\tif (hasSkippedExtension(entry.rel)) continue;\n\t\tlet stat: ReturnType<typeof statSync>;\n\t\ttry {\n\t\t\tstat = statSync(entry.abs);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (stat.size === 0 || stat.size > MAX_FILE_BYTES) continue;\n\t\tfiles.push({ abs: entry.abs, rel: entry.rel, size: stat.size, mtimeMs: stat.mtimeMs });\n\t\ttotalBytes += stat.size;\n\t}\n\treturn { files, totalBytes };\n}\n"]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository scan for semantic indexing: enumerates indexable source files
|
|
3
|
+
* using the same ignore-aware walk the native find/grep fallbacks use, and
|
|
4
|
+
* totals their bytes so the caller can apply the size threshold.
|
|
5
|
+
*/
|
|
6
|
+
import { statSync } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { collectEntries } from "../tools/native-search.js";
|
|
9
|
+
/** Files larger than this are never indexed (vendored bundles, lockfiles, data dumps). */
|
|
10
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
11
|
+
/** Files that are part of the ignore mechanism, not source. */
|
|
12
|
+
const SKIP_NAMES = new Set([".gitignore", ".gitattributes", ".gitkeep"]);
|
|
13
|
+
/** Extensions that are never worth embedding. */
|
|
14
|
+
const SKIP_EXTENSIONS = new Set([
|
|
15
|
+
".png",
|
|
16
|
+
".jpg",
|
|
17
|
+
".jpeg",
|
|
18
|
+
".gif",
|
|
19
|
+
".webp",
|
|
20
|
+
".ico",
|
|
21
|
+
".bmp",
|
|
22
|
+
".svg",
|
|
23
|
+
".pdf",
|
|
24
|
+
".zip",
|
|
25
|
+
".gz",
|
|
26
|
+
".tar",
|
|
27
|
+
".bz2",
|
|
28
|
+
".xz",
|
|
29
|
+
".7z",
|
|
30
|
+
".jar",
|
|
31
|
+
".war",
|
|
32
|
+
".class",
|
|
33
|
+
".exe",
|
|
34
|
+
".dll",
|
|
35
|
+
".so",
|
|
36
|
+
".dylib",
|
|
37
|
+
".bin",
|
|
38
|
+
".dat",
|
|
39
|
+
".onnx",
|
|
40
|
+
".pt",
|
|
41
|
+
".safetensors",
|
|
42
|
+
".woff",
|
|
43
|
+
".woff2",
|
|
44
|
+
".ttf",
|
|
45
|
+
".otf",
|
|
46
|
+
".eot",
|
|
47
|
+
".mp3",
|
|
48
|
+
".mp4",
|
|
49
|
+
".wav",
|
|
50
|
+
".avi",
|
|
51
|
+
".mov",
|
|
52
|
+
".webm",
|
|
53
|
+
".lock",
|
|
54
|
+
".min.js",
|
|
55
|
+
".min.css",
|
|
56
|
+
".map",
|
|
57
|
+
]);
|
|
58
|
+
function hasSkippedExtension(rel) {
|
|
59
|
+
const lower = rel.toLowerCase();
|
|
60
|
+
for (const ext of SKIP_EXTENSIONS) {
|
|
61
|
+
if (lower.endsWith(ext))
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Enumerate indexable files under `root`: respects hierarchical `.gitignore`,
|
|
68
|
+
* always skips `.git`/`node_modules`, drops binary-ish extensions and files
|
|
69
|
+
* over 1MB. Returns files plus their byte total for threshold checks.
|
|
70
|
+
*/
|
|
71
|
+
export function scanRepo(root, signal) {
|
|
72
|
+
const entries = collectEntries(root, {
|
|
73
|
+
signal,
|
|
74
|
+
alwaysSkipDirs: new Set([".git", "node_modules"]),
|
|
75
|
+
});
|
|
76
|
+
const files = [];
|
|
77
|
+
let totalBytes = 0;
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (entry.type !== "f")
|
|
80
|
+
continue;
|
|
81
|
+
if (SKIP_NAMES.has(path.basename(entry.rel)))
|
|
82
|
+
continue;
|
|
83
|
+
if (hasSkippedExtension(entry.rel))
|
|
84
|
+
continue;
|
|
85
|
+
let stat;
|
|
86
|
+
try {
|
|
87
|
+
stat = statSync(entry.abs);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (stat.size === 0 || stat.size > MAX_FILE_BYTES)
|
|
93
|
+
continue;
|
|
94
|
+
files.push({ abs: entry.abs, rel: entry.rel, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
95
|
+
totalBytes += stat.size;
|
|
96
|
+
}
|
|
97
|
+
return { files, totalBytes };
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=repo-scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-scan.js","sourceRoot":"","sources":["../../../src/core/embsearch/repo-scan.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAC9B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE3D,0FAA0F;AAC1F,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAAC;AAEnC,+DAA+D;AAC/D,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,gBAAgB,EAAE,UAAU,CAAC,CAAC,CAAC;AAEzE,iDAAiD;AACjD,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC/B,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,KAAK;IACL,MAAM;IACN,MAAM;IACN,KAAK;IACL,KAAK;IACL,MAAM;IACN,MAAM;IACN,QAAQ;IACR,MAAM;IACN,MAAM;IACN,KAAK;IACL,QAAQ;IACR,MAAM;IACN,MAAM;IACN,OAAO;IACP,KAAK;IACL,cAAc;IACd,OAAO;IACP,QAAQ;IACR,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,OAAO;IACP,SAAS;IACT,UAAU;IACV,MAAM;CACN,CAAC,CAAC;AAkBH,SAAS,mBAAmB,CAAC,GAAW,EAAW;IAClD,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACnC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;IACtC,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,MAAoB,EAAkB;IAC5E,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,EAAE;QACpC,MAAM;QACN,cAAc,EAAE,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;KACjD,CAAC,CAAC;IACH,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,IAAI,KAAK,GAAG;YAAE,SAAS;QACjC,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAAE,SAAS;QACvD,IAAI,mBAAmB,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,SAAS;QAC7C,IAAI,IAAiC,CAAC;QACtC,IAAI,CAAC;YACJ,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACR,SAAS;QACV,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,cAAc;YAAE,SAAS;QAC5D,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACvF,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC;IACzB,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAAA,CAC7B","sourcesContent":["/**\n * Repository scan for semantic indexing: enumerates indexable source files\n * using the same ignore-aware walk the native find/grep fallbacks use, and\n * totals their bytes so the caller can apply the size threshold.\n */\n\nimport { statSync } from \"fs\";\nimport path from \"path\";\nimport { collectEntries } from \"../tools/native-search.js\";\n\n/** Files larger than this are never indexed (vendored bundles, lockfiles, data dumps). */\nconst MAX_FILE_BYTES = 1024 * 1024;\n\n/** Files that are part of the ignore mechanism, not source. */\nconst SKIP_NAMES = new Set([\".gitignore\", \".gitattributes\", \".gitkeep\"]);\n\n/** Extensions that are never worth embedding. */\nconst SKIP_EXTENSIONS = new Set([\n\t\".png\",\n\t\".jpg\",\n\t\".jpeg\",\n\t\".gif\",\n\t\".webp\",\n\t\".ico\",\n\t\".bmp\",\n\t\".svg\",\n\t\".pdf\",\n\t\".zip\",\n\t\".gz\",\n\t\".tar\",\n\t\".bz2\",\n\t\".xz\",\n\t\".7z\",\n\t\".jar\",\n\t\".war\",\n\t\".class\",\n\t\".exe\",\n\t\".dll\",\n\t\".so\",\n\t\".dylib\",\n\t\".bin\",\n\t\".dat\",\n\t\".onnx\",\n\t\".pt\",\n\t\".safetensors\",\n\t\".woff\",\n\t\".woff2\",\n\t\".ttf\",\n\t\".otf\",\n\t\".eot\",\n\t\".mp3\",\n\t\".mp4\",\n\t\".wav\",\n\t\".avi\",\n\t\".mov\",\n\t\".webm\",\n\t\".lock\",\n\t\".min.js\",\n\t\".min.css\",\n\t\".map\",\n]);\n\nexport interface RepoScanFile {\n\t/** Absolute path. */\n\tabs: string;\n\t/** POSIX path relative to the scan root. */\n\trel: string;\n\t/** File size in bytes. */\n\tsize: number;\n\t/** mtime in ms. */\n\tmtimeMs: number;\n}\n\nexport interface RepoScanResult {\n\tfiles: RepoScanFile[];\n\ttotalBytes: number;\n}\n\nfunction hasSkippedExtension(rel: string): boolean {\n\tconst lower = rel.toLowerCase();\n\tfor (const ext of SKIP_EXTENSIONS) {\n\t\tif (lower.endsWith(ext)) return true;\n\t}\n\treturn false;\n}\n\n/**\n * Enumerate indexable files under `root`: respects hierarchical `.gitignore`,\n * always skips `.git`/`node_modules`, drops binary-ish extensions and files\n * over 1MB. Returns files plus their byte total for threshold checks.\n */\nexport function scanRepo(root: string, signal?: AbortSignal): RepoScanResult {\n\tconst entries = collectEntries(root, {\n\t\tsignal,\n\t\talwaysSkipDirs: new Set([\".git\", \"node_modules\"]),\n\t});\n\tconst files: RepoScanFile[] = [];\n\tlet totalBytes = 0;\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"f\") continue;\n\t\tif (SKIP_NAMES.has(path.basename(entry.rel))) continue;\n\t\tif (hasSkippedExtension(entry.rel)) continue;\n\t\tlet stat: ReturnType<typeof statSync>;\n\t\ttry {\n\t\t\tstat = statSync(entry.abs);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (stat.size === 0 || stat.size > MAX_FILE_BYTES) continue;\n\t\tfiles.push({ abs: entry.abs, rel: entry.rel, size: stat.size, mtimeMs: stat.mtimeMs });\n\t\ttotalBytes += stat.size;\n\t}\n\treturn { files, totalBytes };\n}\n"]}
|
package/dist/core/sdk.d.ts
CHANGED
|
@@ -70,6 +70,14 @@ export interface CreateAgentSessionOptions {
|
|
|
70
70
|
* losslessly extract/edit structured/binary documents.
|
|
71
71
|
*/
|
|
72
72
|
enableFileTools?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Enable the built-in `semantic_search` tool (local embedding index via the
|
|
75
|
+
* `embsearch` binary). Defined but inactive by default. Ignored when an
|
|
76
|
+
* explicit `tools` allowlist is provided (list it there instead). The tool
|
|
77
|
+
* only returns results once the per-session index service reports available;
|
|
78
|
+
* otherwise it errors with a pointer back to grep/find.
|
|
79
|
+
*/
|
|
80
|
+
enableEmbsearchTools?: boolean;
|
|
73
81
|
/** Custom tools to register (in addition to built-in tools). */
|
|
74
82
|
customTools?: ToolDefinition[];
|
|
75
83
|
/**
|