@amsterdamdatalabs/enact-docs-search 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/NOTICE +50 -0
  2. package/README.md +35 -0
  3. package/dist/adapter/repo-walker.d.ts +22 -0
  4. package/dist/adapter/repo-walker.d.ts.map +1 -0
  5. package/dist/adapter/repo-walker.js +96 -0
  6. package/dist/adapter/repo-walker.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +310 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/embeddings/embedding-service.d.ts +12 -0
  12. package/dist/embeddings/embedding-service.d.ts.map +1 -0
  13. package/dist/embeddings/embedding-service.js +67 -0
  14. package/dist/embeddings/embedding-service.js.map +1 -0
  15. package/dist/indexing/chunker.d.ts +52 -0
  16. package/dist/indexing/chunker.d.ts.map +1 -0
  17. package/dist/indexing/chunker.js +196 -0
  18. package/dist/indexing/chunker.js.map +1 -0
  19. package/dist/indexing/indexing-service.d.ts +29 -0
  20. package/dist/indexing/indexing-service.d.ts.map +1 -0
  21. package/dist/indexing/indexing-service.js +151 -0
  22. package/dist/indexing/indexing-service.js.map +1 -0
  23. package/dist/search/search-service.d.ts +53 -0
  24. package/dist/search/search-service.d.ts.map +1 -0
  25. package/dist/search/search-service.js +139 -0
  26. package/dist/search/search-service.js.map +1 -0
  27. package/dist/search/snippet.d.ts +65 -0
  28. package/dist/search/snippet.d.ts.map +1 -0
  29. package/dist/search/snippet.js +352 -0
  30. package/dist/search/snippet.js.map +1 -0
  31. package/dist/storage/document-store.d.ts +72 -0
  32. package/dist/storage/document-store.d.ts.map +1 -0
  33. package/dist/storage/document-store.js +134 -0
  34. package/dist/storage/document-store.js.map +1 -0
  35. package/dist/storage/persistence.d.ts +55 -0
  36. package/dist/storage/persistence.d.ts.map +1 -0
  37. package/dist/storage/persistence.js +132 -0
  38. package/dist/storage/persistence.js.map +1 -0
  39. package/dist/storage/vector-store.d.ts +63 -0
  40. package/dist/storage/vector-store.d.ts.map +1 -0
  41. package/dist/storage/vector-store.js +172 -0
  42. package/dist/storage/vector-store.js.map +1 -0
  43. package/dist/types.d.ts +12 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +24 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +40 -0
package/NOTICE ADDED
@@ -0,0 +1,50 @@
1
+ enact-docs-search — third-party notices
2
+ ========================================
3
+
4
+ This tool's Chunker, DocumentStore, and VectorStore modules are adapted from:
5
+
6
+ obsidian-qmd
7
+ https://github.com/thirteen37/obsidian-qmd
8
+ Licence: MIT, as declared in the project's README (`## License` -> `MIT`).
9
+ The upstream repository carries no LICENSE file and no per-file copyright
10
+ header, so there is no copyright notice to reproduce here; this entry
11
+ records provenance in its place.
12
+
13
+ Files adapted (each carries an `Adapted from obsidian-qmd (MIT): <url>`
14
+ header comment pointing back here):
15
+
16
+ src/indexing/chunker.ts <- src/indexing/Chunker.ts (near-verbatim port)
17
+ src/storage/document-store.ts <- src/storage/DocumentStore.ts (near-verbatim port)
18
+ src/storage/vector-store.ts <- src/storage/VectorStore.ts (ported from the
19
+ hnswlib-wasm API to hnswlib-node's API)
20
+ src/search/search-service.ts <- src/search/SearchService.ts (RRF fusion logic
21
+ carried over; query-expansion/reranking, which
22
+ call out to an LLM, were dropped as out of scope)
23
+
24
+ Written fresh, not adapted (the upstream equivalents are Obsidian/Electron-
25
+ specific plumbing with no Node analogue worth porting):
26
+
27
+ src/embeddings/embedding-service.ts (upstream patches ONNX Runtime for
28
+ Obsidian's Electron renderer via blob
29
+ URLs; @xenova/transformers needs none
30
+ of that under plain Node/Bun)
31
+ src/adapter/repo-walker.ts (replaces Obsidian's `Vault` with
32
+ node:fs/promises)
33
+ src/storage/persistence.ts (replaces the plugin's IndexedDB/
34
+ localforage persistence with plain
35
+ per-repo cache files)
36
+ src/indexing/indexing-service.ts (replaces Obsidian's long-lived
37
+ in-process indexing loop with a
38
+ manifest-driven incremental diff,
39
+ since this CLI process exits between
40
+ runs)
41
+ src/cli.ts, src/types.ts
42
+
43
+ Runtime dependencies and their licences (all permissive, verified in
44
+ package.json / node_modules at time of writing):
45
+
46
+ minisearch MIT BM25 full-text index
47
+ @xenova/transformers Apache-2.0 local embeddings (no API key/network
48
+ call after the model is first cached)
49
+ hnswlib-node Apache-2.0 HNSW vector index (native Node build,
50
+ not the wasm build obsidian-qmd uses)
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @amsterdamdatalabs/enact-docs-search
2
+
3
+ Hybrid BM25 and vector search over a repository's `docs/`, `.plans/`, and
4
+ `.specs/` knowledge trees.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install -g @amsterdamdatalabs/enact-docs-search
10
+ ```
11
+
12
+ Node.js 22 or newer is required. The first semantic query downloads the local
13
+ `Xenova/all-MiniLM-L6-v2` embedding model; subsequent queries use the per-repo
14
+ cache under `~/.cache/enact-docs-search/`.
15
+
16
+ ## Usage
17
+
18
+ Run inside a target repository:
19
+
20
+ ```bash
21
+ enact-docs-search query "how crate boundaries are defined?"
22
+ enact-docs-search index
23
+ enact-docs-search index --force
24
+ ```
25
+
26
+ Queries automatically refresh a missing or stale cache incrementally. Use
27
+ `enact-docs-search --help` for advanced scope, mode, JSON, and exclusion
28
+ controls.
29
+
30
+ The package is also usable through `npx` or `bunx`:
31
+
32
+ ```bash
33
+ npx @amsterdamdatalabs/enact-docs-search query "conversation vs tasks"
34
+ bunx @amsterdamdatalabs/enact-docs-search query "conversation vs tasks"
35
+ ```
@@ -0,0 +1,22 @@
1
+ export interface RepoFile {
2
+ /** Path relative to the repo root, using forward slashes. */
3
+ path: string;
4
+ /** Absolute path on disk. */
5
+ absPath: string;
6
+ /** Last-modified time in epoch milliseconds. */
7
+ mtimeMs: number;
8
+ }
9
+ export interface WalkOptions {
10
+ /** Extra glob patterns (relative to repoRoot) to exclude, in addition to the defaults. */
11
+ exclude?: string[];
12
+ }
13
+ /**
14
+ * List every Markdown/MDX file under the given roots (each relative to
15
+ * repoRoot), sorted for deterministic output.
16
+ */
17
+ export declare function listMarkdownFiles(repoRoot: string, roots: string[], options?: WalkOptions): Promise<RepoFile[]>;
18
+ /** Read a file's text content (equivalent to `vault.cachedRead`). */
19
+ export declare function readRepoFile(absPath: string): Promise<string>;
20
+ /** Repo-relative path with forward slashes, given an absolute path under repoRoot. */
21
+ export declare function toRepoRelative(repoRoot: string, absPath: string): string;
22
+ //# sourceMappingURL=repo-walker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-walker.d.ts","sourceRoot":"","sources":["../../src/adapter/repo-walker.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,QAAQ;IACvB,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,MAAM,WAAW,WAAW;IAC1B,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAcrB;AA4DD,qEAAqE;AACrE,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEnE;AAED,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAExE"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * repo-walker.ts — small Node/Bun adapter that stands in for Obsidian's
3
+ * `App`/`Vault` in the upstream obsidian-qmd design.
4
+ *
5
+ * obsidian-qmd's IndexingService only ever touches the vault through three
6
+ * calls: `vault.getMarkdownFiles()`, `vault.cachedRead(file)`, and
7
+ * `file.stat.mtime` / `file.path`. This module provides the same three
8
+ * operations against a plain directory on disk, using Node's filesystem APIs
9
+ * for discovery, reads, and stats — no Obsidian or runtime-specific types
10
+ * anywhere in this file.
11
+ */
12
+ import { readdir, readFile, stat } from "node:fs/promises";
13
+ import { join, relative, sep } from "node:path";
14
+ const DEFAULT_EXCLUDES = [
15
+ "**/.astro/**",
16
+ "**/node_modules/**",
17
+ "**/.git/**",
18
+ ];
19
+ /**
20
+ * List every Markdown/MDX file under the given roots (each relative to
21
+ * repoRoot), sorted for deterministic output.
22
+ */
23
+ export async function listMarkdownFiles(repoRoot, roots, options = {}) {
24
+ const excludes = [...DEFAULT_EXCLUDES, ...(options.exclude ?? [])];
25
+ const files = [];
26
+ for (const root of roots) {
27
+ const absRoot = join(repoRoot, root);
28
+ const rootStat = await stat(absRoot).catch(() => null);
29
+ if (!rootStat || !rootStat.isDirectory())
30
+ continue;
31
+ await walk(absRoot, root, excludes, files);
32
+ }
33
+ files.sort((a, b) => a.path.localeCompare(b.path));
34
+ return files;
35
+ }
36
+ function isExcluded(relPath, excludes) {
37
+ for (const pattern of excludes) {
38
+ if (globToRegExp(pattern).test(relPath))
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ async function walk(absDir, relDir, excludes, files) {
44
+ const entries = await readdir(absDir, { withFileTypes: true });
45
+ entries.sort((a, b) => a.name.localeCompare(b.name));
46
+ for (const entry of entries) {
47
+ const relPath = `${relDir}/${entry.name}`;
48
+ if (isExcluded(relPath, excludes))
49
+ continue;
50
+ const absPath = join(absDir, entry.name);
51
+ if (entry.isDirectory()) {
52
+ await walk(absPath, relPath, excludes, files);
53
+ continue;
54
+ }
55
+ if (!entry.isFile() || !/\.(?:md|mdx)$/i.test(entry.name))
56
+ continue;
57
+ const fileStat = await stat(absPath);
58
+ files.push({ path: relPath, absPath, mtimeMs: fileStat.mtimeMs });
59
+ }
60
+ }
61
+ function globToRegExp(pattern) {
62
+ const normalized = pattern.replaceAll("\\", "/");
63
+ let source = "^";
64
+ for (let i = 0; i < normalized.length; i++) {
65
+ const char = normalized[i];
66
+ if (char === "*" && normalized[i + 1] === "*") {
67
+ i++;
68
+ if (normalized[i + 1] === "/") {
69
+ i++;
70
+ source += "(?:.*/)?";
71
+ }
72
+ else {
73
+ source += ".*";
74
+ }
75
+ }
76
+ else if (char === "*") {
77
+ source += "[^/]*";
78
+ }
79
+ else if (char === "?") {
80
+ source += "[^/]";
81
+ }
82
+ else {
83
+ source += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
84
+ }
85
+ }
86
+ return new RegExp(`${source}$`);
87
+ }
88
+ /** Read a file's text content (equivalent to `vault.cachedRead`). */
89
+ export async function readRepoFile(absPath) {
90
+ return readFile(absPath, "utf-8");
91
+ }
92
+ /** Repo-relative path with forward slashes, given an absolute path under repoRoot. */
93
+ export function toRepoRelative(repoRoot, absPath) {
94
+ return relative(repoRoot, absPath).split(sep).join("/");
95
+ }
96
+ //# sourceMappingURL=repo-walker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-walker.js","sourceRoot":"","sources":["../../src/adapter/repo-walker.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * enact-docs-search — hybrid (BM25 + vector) search CLI over a repo's
4
+ * docs/.plans/.specs knowledge trees. Takes the target repo as an argument;
5
+ * does not hardcode any particular checkout.
6
+ *
7
+ * Usage:
8
+ * enact-docs-search index --repo <path> [--scope docs|plans|specs|all] [--force] [--keyword-only]
9
+ * enact-docs-search query "<text>" --repo <path> [--scope ...] [--mode hybrid|keyword|semantic] [--limit N]
10
+ */
11
+ import { DocumentStore } from "./storage/document-store.js";
12
+ import { VectorStore, DependencyError as VectorDependencyError } from "./storage/vector-store.js";
13
+ import { EmbeddingService, DependencyError as EmbeddingDependencyError } from "./embeddings/embedding-service.js";
14
+ import { SearchService } from "./search/search-service.js";
15
+ import { runIndex } from "./indexing/indexing-service.js";
16
+ import { listMarkdownFiles } from "./adapter/repo-walker.js";
17
+ import { repoCacheDir, paths, readManifest, readDocumentStoreExport, readVectorSidecar, vectorIndexExists, checkFreshness, } from "./storage/persistence.js";
18
+ import { scopesToRoots } from "./types.js";
19
+ import { existsSync } from "node:fs";
20
+ import { dirname, join } from "node:path";
21
+ const EMBEDDING_DIMENSIONS = 384;
22
+ function usage() {
23
+ return `enact-docs-search — hybrid semantic + keyword search over docs/.plans/.specs
24
+
25
+ Usage:
26
+ enact-docs-search index --repo <path> [--scope docs|plans|specs|all] [--force] [--keyword-only] [--exclude <glob>]
27
+ enact-docs-search query "<text>" --repo <path> [--scope docs|plans|specs|all] [--mode hybrid|keyword|semantic] [--limit N] [--exclude <glob>]
28
+
29
+ Options:
30
+ --repo <path> Target repository root. If omitted, auto-detected by
31
+ walking up from the current directory for docs/,
32
+ .plans/, or .specs/
33
+ --scope <scope> docs | plans | specs | all (default: all)
34
+ --mode <mode> hybrid | keyword | semantic (default: hybrid, query only)
35
+ --limit <n> Max results to return (default: 10, query only)
36
+ --force Full rebuild, ignoring the incremental manifest (index only)
37
+ --keyword-only Skip embedding generation entirely (index only, much faster)
38
+ --exclude <glob> Additional glob to exclude from scanning; repeatable
39
+ --json Emit machine-readable JSON instead of text (query only)
40
+
41
+ Query automatically incrementally refreshes a missing or stale cache. Use
42
+ index --force only for an explicit full rebuild.
43
+
44
+ Exit codes: 0 = matches found, 1 = no matches, 2 = usage/dependency error.`;
45
+ }
46
+ function parseArgs(argv) {
47
+ const parsed = {
48
+ positional: [],
49
+ scope: "all",
50
+ mode: "hybrid",
51
+ limit: 10,
52
+ force: false,
53
+ keywordOnly: false,
54
+ exclude: [],
55
+ json: false,
56
+ };
57
+ for (let i = 0; i < argv.length; i++) {
58
+ const arg = argv[i];
59
+ switch (arg) {
60
+ case "--repo":
61
+ parsed.repo = argv[++i];
62
+ break;
63
+ case "--scope":
64
+ parsed.scope = argv[++i];
65
+ break;
66
+ case "--mode":
67
+ parsed.mode = argv[++i];
68
+ break;
69
+ case "--limit":
70
+ parsed.limit = Number(argv[++i]);
71
+ break;
72
+ case "--force":
73
+ parsed.force = true;
74
+ break;
75
+ case "--keyword-only":
76
+ parsed.keywordOnly = true;
77
+ break;
78
+ case "--exclude":
79
+ parsed.exclude.push(argv[++i]);
80
+ break;
81
+ case "--json":
82
+ parsed.json = true;
83
+ break;
84
+ case "-h":
85
+ case "--help":
86
+ console.log(usage());
87
+ process.exit(0);
88
+ break;
89
+ default:
90
+ parsed.positional.push(arg);
91
+ }
92
+ }
93
+ return parsed;
94
+ }
95
+ function fail(message) {
96
+ console.error(`enact-docs-search: FAIL — ${message}`);
97
+ process.exit(2);
98
+ }
99
+ /** Routine, non-error status line (detected repo root, staleness notice,
100
+ * ...). Deliberately on stdout via console.log, not console.error/stderr —
101
+ * stderr renders red in most terminals, and a purely informational message
102
+ * (nothing failed, nothing needs the user's attention as a problem) reading
103
+ * as an error is misleading. Reserve stderr for fail() and genuine
104
+ * unexpected errors. */
105
+ function info(message) {
106
+ console.log(message);
107
+ }
108
+ /**
109
+ * Auto-detect a repo root when --repo is omitted: walk up from cwd looking
110
+ * for a directory that has any of docs/, .plans/, or .specs/. Stops at the
111
+ * filesystem root. Returns null if nothing is found, so the caller can fail
112
+ * with a clear message rather than silently picking the wrong directory.
113
+ */
114
+ function detectRepoRoot(startDir) {
115
+ let dir = startDir;
116
+ for (;;) {
117
+ for (const marker of ["docs", ".plans", ".specs"]) {
118
+ if (existsSync(join(dir, marker)))
119
+ return dir;
120
+ }
121
+ const parent = dirname(dir);
122
+ if (parent === dir)
123
+ return null;
124
+ dir = parent;
125
+ }
126
+ }
127
+ function validateScope(scope) {
128
+ if (!["docs", "plans", "specs", "all"].includes(scope)) {
129
+ fail(`unknown scope '${scope}' (use docs, plans, specs, or all)`);
130
+ }
131
+ }
132
+ function validateMode(mode) {
133
+ if (!["keyword", "semantic", "hybrid"].includes(mode)) {
134
+ fail(`unknown mode '${mode}' (use keyword, semantic, or hybrid)`);
135
+ }
136
+ }
137
+ async function cmdIndex(args) {
138
+ if (!args.repo) {
139
+ const detected = detectRepoRoot(process.cwd());
140
+ if (!detected) {
141
+ fail("--repo <path> was not given, and no docs/, .plans/, or .specs/ found walking up from " +
142
+ process.cwd() +
143
+ " — pass --repo explicitly");
144
+ }
145
+ args.repo = detected;
146
+ }
147
+ validateScope(args.scope);
148
+ info(`root: ${args.repo} (scope: ${args.scope})`);
149
+ const t0 = Date.now();
150
+ let lastPhase = "";
151
+ const result = await runIndex({
152
+ repoPath: args.repo,
153
+ scope: args.scope,
154
+ force: args.force,
155
+ skipEmbeddings: args.keywordOnly,
156
+ exclude: args.exclude,
157
+ onProgress: (p) => {
158
+ if (p.phase !== lastPhase) {
159
+ process.stderr.write(`\n[${p.phase}] `);
160
+ lastPhase = p.phase;
161
+ }
162
+ if (p.totalFiles > 0) {
163
+ process.stderr.write(`\r[${p.phase}] ${p.filesProcessed}/${p.totalFiles}` + (p.currentFile ? ` ${p.currentFile}` : " "));
164
+ }
165
+ },
166
+ });
167
+ process.stderr.write("\n");
168
+ console.log(`enact-docs-search: indexed scope='${result.scope}' — ` +
169
+ `${result.filesScanned} files scanned, ${result.filesChanged} changed, ${result.filesRemoved} removed, ` +
170
+ `${result.chunksIndexed} chunks, ${result.embeddingsGenerated} embeddings ` +
171
+ `(${(Date.now() - t0)}ms)`);
172
+ console.log(`enact-docs-search: cache at ${repoCacheDir(args.repo)}`);
173
+ }
174
+ async function cmdQuery(args) {
175
+ const query = args.positional[0];
176
+ if (!query)
177
+ fail("a query string is required");
178
+ if (!args.repo) {
179
+ const detected = detectRepoRoot(process.cwd());
180
+ if (!detected) {
181
+ fail("--repo <path> was not given, and no docs/, .plans/, or .specs/ found walking up from " +
182
+ process.cwd() +
183
+ " — pass --repo explicitly");
184
+ }
185
+ args.repo = detected;
186
+ }
187
+ validateScope(args.scope);
188
+ validateMode(args.mode);
189
+ // In --json mode, stdout must be pure JSON for downstream parsers — route
190
+ // this status line to stderr instead of suppressing it outright.
191
+ if (args.json) {
192
+ console.error(`root: ${args.repo} (scope: ${args.scope}, mode: ${args.mode})`);
193
+ }
194
+ else {
195
+ info(`root: ${args.repo} (scope: ${args.scope}, mode: ${args.mode})`);
196
+ }
197
+ const cacheDir = repoCacheDir(args.repo);
198
+ const manifest = await readManifest(cacheDir);
199
+ // Freshness check (I-08: refresh stale or missing results before serving them).
200
+ const roots = scopesToRoots(args.scope);
201
+ const currentFiles = await listMarkdownFiles(args.repo, roots, { exclude: args.exclude });
202
+ const freshness = checkFreshness(manifest, args.scope, currentFiles);
203
+ const hasVectorFile = args.mode !== "keyword" && await vectorIndexExists(cacheDir);
204
+ const vectorSidecar = args.mode !== "keyword" ? await readVectorSidecar(cacheDir) : null;
205
+ const needsVectorIndex = args.mode !== "keyword" && (!hasVectorFile || !vectorSidecar);
206
+ const rebuildVectorIndex = args.mode !== "keyword" && hasVectorFile && !vectorSidecar;
207
+ const needsRefresh = freshness.neverIndexed || freshness.staleCount > 0 || needsVectorIndex;
208
+ if (needsRefresh) {
209
+ const reason = freshness.neverIndexed
210
+ ? "no index"
211
+ : freshness.staleCount > 0
212
+ ? `${freshness.staleCount} changed file(s)`
213
+ : "no usable vector index";
214
+ const message = `enact-docs-search: refreshing ${args.scope} index (${reason})`;
215
+ if (args.json)
216
+ console.error(message);
217
+ else
218
+ info(message);
219
+ try {
220
+ await runIndex({
221
+ repoPath: args.repo,
222
+ scope: args.scope,
223
+ force: rebuildVectorIndex,
224
+ skipEmbeddings: args.mode === "keyword",
225
+ exclude: args.exclude,
226
+ });
227
+ }
228
+ catch (error) {
229
+ if (error instanceof VectorDependencyError || error instanceof EmbeddingDependencyError) {
230
+ fail(error.message);
231
+ }
232
+ throw error;
233
+ }
234
+ }
235
+ const docStoreExport = await readDocumentStoreExport(cacheDir);
236
+ const docStore = new DocumentStore();
237
+ await docStore.initialize(docStoreExport ?? undefined);
238
+ let embeddingService = null;
239
+ const vectorStore = new VectorStore({ dimensions: EMBEDDING_DIMENSIONS });
240
+ if (args.mode !== "keyword") {
241
+ const hasVectors = await vectorIndexExists(cacheDir);
242
+ if (!hasVectors) {
243
+ fail(`no vector index for scope '${args.scope}' after automatic refresh`);
244
+ }
245
+ const sidecar = await readVectorSidecar(cacheDir);
246
+ if (!sidecar)
247
+ fail("vector index sidecar is missing or corrupt; re-run index --force");
248
+ try {
249
+ await vectorStore.loadFromDisk(paths(cacheDir).vectorIndex, sidecar);
250
+ embeddingService = new EmbeddingService();
251
+ await embeddingService.initialize();
252
+ }
253
+ catch (error) {
254
+ if (error instanceof VectorDependencyError || error instanceof EmbeddingDependencyError) {
255
+ fail(error.message);
256
+ }
257
+ throw error;
258
+ }
259
+ }
260
+ const highlight = !args.json && process.stdout.isTTY ? "ansi" : "plain";
261
+ const searchService = new SearchService(docStore, vectorStore, embeddingService);
262
+ const results = await searchService.search(query, {
263
+ mode: args.mode,
264
+ scope: args.scope,
265
+ limit: args.limit,
266
+ highlight,
267
+ repoRoot: args.repo,
268
+ });
269
+ if (args.json) {
270
+ console.log(JSON.stringify({ query, mode: args.mode, scope: args.scope, results }, null, 2));
271
+ }
272
+ else {
273
+ printResults(query, results);
274
+ }
275
+ process.exit(results.length > 0 ? 0 : 1);
276
+ }
277
+ function printResults(query, results) {
278
+ console.log(`${results.length} result(s) for "${query}"`);
279
+ if (results.length === 0)
280
+ return;
281
+ results.forEach((r, i) => {
282
+ console.log("");
283
+ const heading = r.headingBreadcrumb?.length ? ` [${r.headingBreadcrumb.join(" > ")}]` : "";
284
+ console.log(`${i + 1}. ${r.filePath}:${r.startLine}${heading}`);
285
+ console.log(` ${r.snippet ?? ""}`);
286
+ });
287
+ }
288
+ async function main() {
289
+ const [command, ...rest] = process.argv.slice(2);
290
+ if (!command || command === "-h" || command === "--help") {
291
+ console.log(usage());
292
+ process.exit(command ? 0 : 2);
293
+ }
294
+ const args = parseArgs(rest);
295
+ switch (command) {
296
+ case "index":
297
+ await cmdIndex(args);
298
+ break;
299
+ case "query":
300
+ await cmdQuery(args);
301
+ break;
302
+ default:
303
+ fail(`unknown command '${command}' (use 'index' or 'query')`);
304
+ }
305
+ }
306
+ main().catch((error) => {
307
+ console.error(`enact-docs-search: FAIL — unexpected error: ${error.stack ?? error}`);
308
+ process.exit(2);
309
+ });
310
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export declare const EMBEDDING_DIMENSIONS = 384;
2
+ export declare class DependencyError extends Error {
3
+ }
4
+ export declare class EmbeddingService {
5
+ private embeddingPipeline;
6
+ private ready;
7
+ initialize(): Promise<void>;
8
+ isReady(): boolean;
9
+ embed(text: string): Promise<number[]>;
10
+ embedBatch(texts: string[]): Promise<number[][]>;
11
+ }
12
+ //# sourceMappingURL=embedding-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-service.d.ts","sourceRoot":"","sources":["../../src/embeddings/embedding-service.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,qBAAa,eAAgB,SAAQ,KAAK;CAAG;AAE7C,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,iBAAiB,CAA0C;IACnE,OAAO,CAAC,KAAK,CAAS;IAEhB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA8BjC,OAAO,IAAI,OAAO;IAIZ,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAWtC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;CAqBvD"}
@@ -0,0 +1,67 @@
1
+ const MODEL_ID = "Xenova/all-MiniLM-L6-v2";
2
+ export const EMBEDDING_DIMENSIONS = 384;
3
+ export class DependencyError extends Error {
4
+ }
5
+ export class EmbeddingService {
6
+ embeddingPipeline = null;
7
+ ready = false;
8
+ async initialize() {
9
+ let transformers;
10
+ try {
11
+ transformers = await import("@xenova/transformers");
12
+ }
13
+ catch (error) {
14
+ throw new DependencyError("Semantic search requires @xenova/transformers, which is not installed.\n" +
15
+ "Run `npm install` in the enact-docs-search directory, then retry.\n" +
16
+ `Original error: ${error.message}`);
17
+ }
18
+ const { pipeline, env } = transformers;
19
+ env.allowLocalModels = false;
20
+ try {
21
+ this.embeddingPipeline = (await pipeline("feature-extraction", MODEL_ID, {
22
+ quantized: false,
23
+ }));
24
+ this.ready = true;
25
+ }
26
+ catch (error) {
27
+ throw new DependencyError("Failed to load the local embedding model (Xenova/all-MiniLM-L6-v2).\n" +
28
+ "This downloads on first use and is then cached; check network access " +
29
+ "and retry.\n" +
30
+ `Original error: ${error.message}`);
31
+ }
32
+ }
33
+ isReady() {
34
+ return this.ready && this.embeddingPipeline !== null;
35
+ }
36
+ async embed(text) {
37
+ if (!this.isReady() || !this.embeddingPipeline) {
38
+ throw new Error("Embedding service not initialized");
39
+ }
40
+ const output = await this.embeddingPipeline(text ?? "", {
41
+ pooling: "mean",
42
+ normalize: true,
43
+ });
44
+ return Array.from(output.data);
45
+ }
46
+ async embedBatch(texts) {
47
+ if (!this.isReady() || !this.embeddingPipeline) {
48
+ throw new Error("Embedding service not initialized");
49
+ }
50
+ if (texts.length === 0)
51
+ return [];
52
+ const safeTexts = texts.map((t) => t ?? "");
53
+ const output = await this.embeddingPipeline(safeTexts, {
54
+ pooling: "mean",
55
+ normalize: true,
56
+ });
57
+ const data = output.data;
58
+ const results = [];
59
+ for (let i = 0; i < safeTexts.length; i++) {
60
+ const start = i * EMBEDDING_DIMENSIONS;
61
+ const end = start + EMBEDDING_DIMENSIONS;
62
+ results.push(Array.from(data.slice(start, end)));
63
+ }
64
+ return results;
65
+ }
66
+ }
67
+ //# sourceMappingURL=embedding-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-service.js","sourceRoot":"","sources":["../../src/embeddings/embedding-service.ts"],"names":[],"mappings":"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"}