@cstart/coldstart 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -0
- package/dist/cache/disk-cache.d.ts +14 -0
- package/dist/cache/disk-cache.d.ts.map +1 -0
- package/dist/cache/disk-cache.js +371 -0
- package/dist/cache/disk-cache.js.map +1 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +206 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +111 -0
- package/dist/constants.js.map +1 -0
- package/dist/daemon-lock.d.ts +63 -0
- package/dist/daemon-lock.d.ts.map +1 -0
- package/dist/daemon-lock.js +218 -0
- package/dist/daemon-lock.js.map +1 -0
- package/dist/daemon-log.d.ts +26 -0
- package/dist/daemon-log.d.ts.map +1 -0
- package/dist/daemon-log.js +110 -0
- package/dist/daemon-log.js.map +1 -0
- package/dist/index-manager.d.ts +103 -0
- package/dist/index-manager.d.ts.map +1 -0
- package/dist/index-manager.js +361 -0
- package/dist/index-manager.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +724 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/content-tokens.d.ts +74 -0
- package/dist/indexer/content-tokens.d.ts.map +1 -0
- package/dist/indexer/content-tokens.js +406 -0
- package/dist/indexer/content-tokens.js.map +1 -0
- package/dist/indexer/cpp-include-roots.d.ts +7 -0
- package/dist/indexer/cpp-include-roots.d.ts.map +1 -0
- package/dist/indexer/cpp-include-roots.js +166 -0
- package/dist/indexer/cpp-include-roots.js.map +1 -0
- package/dist/indexer/csharp-synthetic.d.ts +9 -0
- package/dist/indexer/csharp-synthetic.d.ts.map +1 -0
- package/dist/indexer/csharp-synthetic.js +51 -0
- package/dist/indexer/csharp-synthetic.js.map +1 -0
- package/dist/indexer/django-synthetic.d.ts +12 -0
- package/dist/indexer/django-synthetic.d.ts.map +1 -0
- package/dist/indexer/django-synthetic.js +49 -0
- package/dist/indexer/django-synthetic.js.map +1 -0
- package/dist/indexer/extractors/angularjs.d.ts +16 -0
- package/dist/indexer/extractors/angularjs.d.ts.map +1 -0
- package/dist/indexer/extractors/angularjs.js +49 -0
- package/dist/indexer/extractors/angularjs.js.map +1 -0
- package/dist/indexer/extractors/cpp.d.ts +9 -0
- package/dist/indexer/extractors/cpp.d.ts.map +1 -0
- package/dist/indexer/extractors/cpp.js +254 -0
- package/dist/indexer/extractors/cpp.js.map +1 -0
- package/dist/indexer/extractors/csharp.d.ts +19 -0
- package/dist/indexer/extractors/csharp.d.ts.map +1 -0
- package/dist/indexer/extractors/csharp.js +327 -0
- package/dist/indexer/extractors/csharp.js.map +1 -0
- package/dist/indexer/extractors/env.d.ts +9 -0
- package/dist/indexer/extractors/env.d.ts.map +1 -0
- package/dist/indexer/extractors/env.js +43 -0
- package/dist/indexer/extractors/env.js.map +1 -0
- package/dist/indexer/extractors/go.d.ts +9 -0
- package/dist/indexer/extractors/go.d.ts.map +1 -0
- package/dist/indexer/extractors/go.js +227 -0
- package/dist/indexer/extractors/go.js.map +1 -0
- package/dist/indexer/extractors/graphql.d.ts +22 -0
- package/dist/indexer/extractors/graphql.d.ts.map +1 -0
- package/dist/indexer/extractors/graphql.js +88 -0
- package/dist/indexer/extractors/graphql.js.map +1 -0
- package/dist/indexer/extractors/groovy.d.ts +9 -0
- package/dist/indexer/extractors/groovy.d.ts.map +1 -0
- package/dist/indexer/extractors/groovy.js +356 -0
- package/dist/indexer/extractors/groovy.js.map +1 -0
- package/dist/indexer/extractors/java.d.ts +10 -0
- package/dist/indexer/extractors/java.d.ts.map +1 -0
- package/dist/indexer/extractors/java.js +586 -0
- package/dist/indexer/extractors/java.js.map +1 -0
- package/dist/indexer/extractors/kotlin.d.ts +10 -0
- package/dist/indexer/extractors/kotlin.d.ts.map +1 -0
- package/dist/indexer/extractors/kotlin.js +398 -0
- package/dist/indexer/extractors/kotlin.js.map +1 -0
- package/dist/indexer/extractors/node-helpers.d.ts +13 -0
- package/dist/indexer/extractors/node-helpers.d.ts.map +1 -0
- package/dist/indexer/extractors/node-helpers.js +17 -0
- package/dist/indexer/extractors/node-helpers.js.map +1 -0
- package/dist/indexer/extractors/parser-factory.d.ts +4 -0
- package/dist/indexer/extractors/parser-factory.d.ts.map +1 -0
- package/dist/indexer/extractors/parser-factory.js +28 -0
- package/dist/indexer/extractors/parser-factory.js.map +1 -0
- package/dist/indexer/extractors/php.d.ts +17 -0
- package/dist/indexer/extractors/php.d.ts.map +1 -0
- package/dist/indexer/extractors/php.js +363 -0
- package/dist/indexer/extractors/php.js.map +1 -0
- package/dist/indexer/extractors/python.d.ts +14 -0
- package/dist/indexer/extractors/python.d.ts.map +1 -0
- package/dist/indexer/extractors/python.js +449 -0
- package/dist/indexer/extractors/python.js.map +1 -0
- package/dist/indexer/extractors/ruby.d.ts +10 -0
- package/dist/indexer/extractors/ruby.d.ts.map +1 -0
- package/dist/indexer/extractors/ruby.js +996 -0
- package/dist/indexer/extractors/ruby.js.map +1 -0
- package/dist/indexer/extractors/rust.d.ts +9 -0
- package/dist/indexer/extractors/rust.d.ts.map +1 -0
- package/dist/indexer/extractors/rust.js +322 -0
- package/dist/indexer/extractors/rust.js.map +1 -0
- package/dist/indexer/extractors/toml.d.ts +9 -0
- package/dist/indexer/extractors/toml.d.ts.map +1 -0
- package/dist/indexer/extractors/toml.js +84 -0
- package/dist/indexer/extractors/toml.js.map +1 -0
- package/dist/indexer/extractors/xml.d.ts +9 -0
- package/dist/indexer/extractors/xml.d.ts.map +1 -0
- package/dist/indexer/extractors/xml.js +163 -0
- package/dist/indexer/extractors/xml.js.map +1 -0
- package/dist/indexer/extractors/yaml.d.ts +9 -0
- package/dist/indexer/extractors/yaml.d.ts.map +1 -0
- package/dist/indexer/extractors/yaml.js +117 -0
- package/dist/indexer/extractors/yaml.js.map +1 -0
- package/dist/indexer/git.d.ts +19 -0
- package/dist/indexer/git.d.ts.map +1 -0
- package/dist/indexer/git.js +55 -0
- package/dist/indexer/git.js.map +1 -0
- package/dist/indexer/graph.d.ts +7 -0
- package/dist/indexer/graph.d.ts.map +1 -0
- package/dist/indexer/graph.js +20 -0
- package/dist/indexer/graph.js.map +1 -0
- package/dist/indexer/indexed-file.d.ts +15 -0
- package/dist/indexer/indexed-file.d.ts.map +1 -0
- package/dist/indexer/indexed-file.js +50 -0
- package/dist/indexer/indexed-file.js.map +1 -0
- package/dist/indexer/invariants.d.ts +15 -0
- package/dist/indexer/invariants.d.ts.map +1 -0
- package/dist/indexer/invariants.js +96 -0
- package/dist/indexer/invariants.js.map +1 -0
- package/dist/indexer/laravel-synthetic.d.ts +10 -0
- package/dist/indexer/laravel-synthetic.d.ts.map +1 -0
- package/dist/indexer/laravel-synthetic.js +78 -0
- package/dist/indexer/laravel-synthetic.js.map +1 -0
- package/dist/indexer/parser.d.ts +4 -0
- package/dist/indexer/parser.d.ts.map +1 -0
- package/dist/indexer/parser.js +499 -0
- package/dist/indexer/parser.js.map +1 -0
- package/dist/indexer/patch.d.ts +3 -0
- package/dist/indexer/patch.d.ts.map +1 -0
- package/dist/indexer/patch.js +291 -0
- package/dist/indexer/patch.js.map +1 -0
- package/dist/indexer/rails-synthetic.d.ts +11 -0
- package/dist/indexer/rails-synthetic.d.ts.map +1 -0
- package/dist/indexer/rails-synthetic.js +80 -0
- package/dist/indexer/rails-synthetic.js.map +1 -0
- package/dist/indexer/reconcile.d.ts +9 -0
- package/dist/indexer/reconcile.d.ts.map +1 -0
- package/dist/indexer/reconcile.js +96 -0
- package/dist/indexer/reconcile.js.map +1 -0
- package/dist/indexer/resolvers/cpp.d.ts +15 -0
- package/dist/indexer/resolvers/cpp.d.ts.map +1 -0
- package/dist/indexer/resolvers/cpp.js +35 -0
- package/dist/indexer/resolvers/cpp.js.map +1 -0
- package/dist/indexer/resolvers/csharp.d.ts +2 -0
- package/dist/indexer/resolvers/csharp.d.ts.map +1 -0
- package/dist/indexer/resolvers/csharp.js +89 -0
- package/dist/indexer/resolvers/csharp.js.map +1 -0
- package/dist/indexer/resolvers/generic.d.ts +10 -0
- package/dist/indexer/resolvers/generic.d.ts.map +1 -0
- package/dist/indexer/resolvers/generic.js +35 -0
- package/dist/indexer/resolvers/generic.js.map +1 -0
- package/dist/indexer/resolvers/go.d.ts +2 -0
- package/dist/indexer/resolvers/go.d.ts.map +1 -0
- package/dist/indexer/resolvers/go.js +171 -0
- package/dist/indexer/resolvers/go.js.map +1 -0
- package/dist/indexer/resolvers/index.d.ts +26 -0
- package/dist/indexer/resolvers/index.d.ts.map +1 -0
- package/dist/indexer/resolvers/index.js +301 -0
- package/dist/indexer/resolvers/index.js.map +1 -0
- package/dist/indexer/resolvers/java.d.ts +17 -0
- package/dist/indexer/resolvers/java.d.ts.map +1 -0
- package/dist/indexer/resolvers/java.js +104 -0
- package/dist/indexer/resolvers/java.js.map +1 -0
- package/dist/indexer/resolvers/php.d.ts +2 -0
- package/dist/indexer/resolvers/php.d.ts.map +1 -0
- package/dist/indexer/resolvers/php.js +108 -0
- package/dist/indexer/resolvers/php.js.map +1 -0
- package/dist/indexer/resolvers/python.d.ts +17 -0
- package/dist/indexer/resolvers/python.d.ts.map +1 -0
- package/dist/indexer/resolvers/python.js +63 -0
- package/dist/indexer/resolvers/python.js.map +1 -0
- package/dist/indexer/resolvers/ruby.d.ts +13 -0
- package/dist/indexer/resolvers/ruby.d.ts.map +1 -0
- package/dist/indexer/resolvers/ruby.js +199 -0
- package/dist/indexer/resolvers/ruby.js.map +1 -0
- package/dist/indexer/resolvers/rust.d.ts +12 -0
- package/dist/indexer/resolvers/rust.d.ts.map +1 -0
- package/dist/indexer/resolvers/rust.js +61 -0
- package/dist/indexer/resolvers/rust.js.map +1 -0
- package/dist/indexer/resolvers/shared.d.ts +18 -0
- package/dist/indexer/resolvers/shared.d.ts.map +1 -0
- package/dist/indexer/resolvers/shared.js +71 -0
- package/dist/indexer/resolvers/shared.js.map +1 -0
- package/dist/indexer/rust-workspace.d.ts +25 -0
- package/dist/indexer/rust-workspace.d.ts.map +1 -0
- package/dist/indexer/rust-workspace.js +155 -0
- package/dist/indexer/rust-workspace.js.map +1 -0
- package/dist/indexer/symbol-edges.d.ts +19 -0
- package/dist/indexer/symbol-edges.d.ts.map +1 -0
- package/dist/indexer/symbol-edges.js +117 -0
- package/dist/indexer/symbol-edges.js.map +1 -0
- package/dist/indexer/tokenize.d.ts +26 -0
- package/dist/indexer/tokenize.d.ts.map +1 -0
- package/dist/indexer/tokenize.js +200 -0
- package/dist/indexer/tokenize.js.map +1 -0
- package/dist/indexer/ts-parser.d.ts +10 -0
- package/dist/indexer/ts-parser.d.ts.map +1 -0
- package/dist/indexer/ts-parser.js +605 -0
- package/dist/indexer/ts-parser.js.map +1 -0
- package/dist/indexer/walker.d.ts +9 -0
- package/dist/indexer/walker.d.ts.map +1 -0
- package/dist/indexer/walker.js +91 -0
- package/dist/indexer/walker.js.map +1 -0
- package/dist/init.d.ts +111 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +787 -0
- package/dist/init.js.map +1 -0
- package/dist/kb/cli.d.ts +2 -0
- package/dist/kb/cli.d.ts.map +1 -0
- package/dist/kb/cli.js +377 -0
- package/dist/kb/cli.js.map +1 -0
- package/dist/kb/commit.d.ts +6 -0
- package/dist/kb/commit.d.ts.map +1 -0
- package/dist/kb/commit.js +84 -0
- package/dist/kb/commit.js.map +1 -0
- package/dist/kb/fold.d.ts +42 -0
- package/dist/kb/fold.d.ts.map +1 -0
- package/dist/kb/fold.js +285 -0
- package/dist/kb/fold.js.map +1 -0
- package/dist/kb/freshness.d.ts +9 -0
- package/dist/kb/freshness.d.ts.map +1 -0
- package/dist/kb/freshness.js +52 -0
- package/dist/kb/freshness.js.map +1 -0
- package/dist/kb/git.d.ts +4 -0
- package/dist/kb/git.d.ts.map +1 -0
- package/dist/kb/git.js +28 -0
- package/dist/kb/git.js.map +1 -0
- package/dist/kb/ids.d.ts +8 -0
- package/dist/kb/ids.d.ts.map +1 -0
- package/dist/kb/ids.js +42 -0
- package/dist/kb/ids.js.map +1 -0
- package/dist/kb/lint.d.ts +23 -0
- package/dist/kb/lint.d.ts.map +1 -0
- package/dist/kb/lint.js +102 -0
- package/dist/kb/lint.js.map +1 -0
- package/dist/kb/lookup.d.ts +55 -0
- package/dist/kb/lookup.d.ts.map +1 -0
- package/dist/kb/lookup.js +92 -0
- package/dist/kb/lookup.js.map +1 -0
- package/dist/kb/notes-index.d.ts +31 -0
- package/dist/kb/notes-index.d.ts.map +1 -0
- package/dist/kb/notes-index.js +0 -0
- package/dist/kb/notes-index.js.map +1 -0
- package/dist/kb/raw-log.d.ts +34 -0
- package/dist/kb/raw-log.d.ts.map +1 -0
- package/dist/kb/raw-log.js +106 -0
- package/dist/kb/raw-log.js.map +1 -0
- package/dist/kb/render.d.ts +17 -0
- package/dist/kb/render.d.ts.map +1 -0
- package/dist/kb/render.js +101 -0
- package/dist/kb/render.js.map +1 -0
- package/dist/kb/search.d.ts +110 -0
- package/dist/kb/search.d.ts.map +1 -0
- package/dist/kb/search.js +453 -0
- package/dist/kb/search.js.map +1 -0
- package/dist/kb/store.d.ts +35 -0
- package/dist/kb/store.d.ts.map +1 -0
- package/dist/kb/store.js +160 -0
- package/dist/kb/store.js.map +1 -0
- package/dist/kb/types.d.ts +142 -0
- package/dist/kb/types.d.ts.map +1 -0
- package/dist/kb/types.js +13 -0
- package/dist/kb/types.js.map +1 -0
- package/dist/kb/view-template.d.ts +8 -0
- package/dist/kb/view-template.d.ts.map +1 -0
- package/dist/kb/view-template.js +8 -0
- package/dist/kb/view-template.js.map +1 -0
- package/dist/kb/view.d.ts +42 -0
- package/dist/kb/view.d.ts.map +1 -0
- package/dist/kb/view.js +118 -0
- package/dist/kb/view.js.map +1 -0
- package/dist/kb/write.d.ts +72 -0
- package/dist/kb/write.d.ts.map +1 -0
- package/dist/kb/write.js +345 -0
- package/dist/kb/write.js.map +1 -0
- package/dist/keeper-state.d.ts +29 -0
- package/dist/keeper-state.d.ts.map +1 -0
- package/dist/keeper-state.js +89 -0
- package/dist/keeper-state.js.map +1 -0
- package/dist/keeper.d.ts +23 -0
- package/dist/keeper.d.ts.map +1 -0
- package/dist/keeper.js +105 -0
- package/dist/keeper.js.map +1 -0
- package/dist/migrate.d.ts +9 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +102 -0
- package/dist/migrate.js.map +1 -0
- package/dist/restart.d.ts +15 -0
- package/dist/restart.d.ts.map +1 -0
- package/dist/restart.js +48 -0
- package/dist/restart.js.map +1 -0
- package/dist/server/find.d.ts +39 -0
- package/dist/server/find.d.ts.map +1 -0
- package/dist/server/find.js +979 -0
- package/dist/server/find.js.map +1 -0
- package/dist/server/mcp.d.ts +117 -0
- package/dist/server/mcp.d.ts.map +1 -0
- package/dist/server/mcp.js +301 -0
- package/dist/server/mcp.js.map +1 -0
- package/dist/server/searcher.d.ts +14 -0
- package/dist/server/searcher.d.ts.map +1 -0
- package/dist/server/searcher.js +146 -0
- package/dist/server/searcher.js.map +1 -0
- package/dist/server/tools.d.ts +17 -0
- package/dist/server/tools.d.ts.map +1 -0
- package/dist/server/tools.js +875 -0
- package/dist/server/tools.js.map +1 -0
- package/dist/status.d.ts +12 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +157 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +138 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/watcher.d.ts +11 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +67 -0
- package/dist/watcher.js.map +1 -0
- package/hooks/canonical-find-key.mjs +68 -0
- package/hooks/codex-find-nudge.mjs +15 -0
- package/hooks/codex-find-preguard.mjs +14 -0
- package/hooks/codex-kb-elicit.mjs +329 -0
- package/hooks/codex-kb-recall.mjs +151 -0
- package/hooks/codex-nudge-handler.mjs +370 -0
- package/hooks/codex-preguard-handler.mjs +75 -0
- package/hooks/codex-run-hook.mjs +132 -0
- package/hooks/coldstart-call.mjs +58 -0
- package/hooks/cursor-find-nudge.mjs +15 -0
- package/hooks/cursor-find-preguard.mjs +14 -0
- package/hooks/cursor-input.mjs +72 -0
- package/hooks/cursor-kb-elicit.mjs +335 -0
- package/hooks/cursor-kb-recall.mjs +138 -0
- package/hooks/cursor-nudge-handler.mjs +33 -0
- package/hooks/cursor-preguard-handler.mjs +39 -0
- package/hooks/cursor-run-hook.mjs +129 -0
- package/hooks/find-nudge.mjs +15 -0
- package/hooks/find-preguard.mjs +14 -0
- package/hooks/kb-elicit.mjs +334 -0
- package/hooks/kb-recall.mjs +151 -0
- package/hooks/nudge-handler.mjs +370 -0
- package/hooks/preguard-handler.mjs +75 -0
- package/hooks/run-hook.mjs +132 -0
- package/package.json +69 -0
- package/templates/coldstart.cli.md +60 -0
- package/templates/coldstart.mcp.md +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* coldstart — fast static codebase navigation for AI agents (CLI + MCP).
|
|
4
|
+
*
|
|
5
|
+
* Default invocation = stdio MCP reader over the keeper's cache:
|
|
6
|
+
* coldstart --root /path/to/project
|
|
7
|
+
* coldstart --root . --exclude vendor --quiet
|
|
8
|
+
* coldstart --root . --no-cache
|
|
9
|
+
* coldstart --root . --no-daemon # self-contained stdio MCP (no keeper)
|
|
10
|
+
*
|
|
11
|
+
* Subcommands:
|
|
12
|
+
* coldstart init # wire coldstart.md into the project
|
|
13
|
+
* coldstart find / gs # CLI readers (load the cache, print, exit)
|
|
14
|
+
* coldstart status # list every keeper + its index freshness
|
|
15
|
+
* coldstart restart [--all] # stop the keeper(s); respawn on next read
|
|
16
|
+
*
|
|
17
|
+
* Internal (spawned automatically by readers):
|
|
18
|
+
* coldstart --root . --daemon # background keeper (keeps cache fresh)
|
|
19
|
+
*/
|
|
20
|
+
import { resolve, join } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { statSync } from 'node:fs';
|
|
23
|
+
import { walkDirectory } from './indexer/walker.js';
|
|
24
|
+
import { parseFile, buildFileId } from './indexer/parser.js';
|
|
25
|
+
import { resolveImports } from './indexer/resolvers/index.js';
|
|
26
|
+
import { addRailsSyntheticEdges } from './indexer/rails-synthetic.js';
|
|
27
|
+
import { addLaravelSyntheticEdges } from './indexer/laravel-synthetic.js';
|
|
28
|
+
import { addCSharpSyntheticEdges } from './indexer/csharp-synthetic.js';
|
|
29
|
+
import { addDjangoSyntheticEdges } from './indexer/django-synthetic.js';
|
|
30
|
+
import { buildGraph } from './indexer/graph.js';
|
|
31
|
+
import { buildFileDomains, isTestPath } from './indexer/tokenize.js';
|
|
32
|
+
import { baseIndexedFile } from './indexer/indexed-file.js';
|
|
33
|
+
import { buildContentTokenPostings } from './indexer/content-tokens.js';
|
|
34
|
+
import { buildSymbolEdges } from './indexer/symbol-edges.js';
|
|
35
|
+
import { getGitHead } from './indexer/git.js';
|
|
36
|
+
import { reconcileChanges } from './indexer/reconcile.js';
|
|
37
|
+
import { patchIndex } from './indexer/patch.js';
|
|
38
|
+
import { lintIndexInvariants } from './indexer/invariants.js';
|
|
39
|
+
import { updateKeeperState, appendRepairLog } from './keeper-state.js';
|
|
40
|
+
import { patchThreshold } from './constants.js';
|
|
41
|
+
import { loadCachedIndex, saveCachedIndex, getCacheDir, updateCachedGitHead } from './cache/disk-cache.js';
|
|
42
|
+
import { startMCPServer } from './server/mcp.js';
|
|
43
|
+
import { IndexManager } from './index-manager.js';
|
|
44
|
+
import { readLock, writeLock, deleteLock, isDaemonAlive, getCurrentVersion, watchOwnLockfile } from './daemon-lock.js';
|
|
45
|
+
import { ensureKeeper, waitForKeeperCache } from './keeper.js';
|
|
46
|
+
import { attachDaemonLogger } from './daemon-log.js';
|
|
47
|
+
import { migrateLegacyMcpConfig } from './migrate.js';
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Argument parsing
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
function parseArgs(argv) {
|
|
52
|
+
const args = argv.slice(2);
|
|
53
|
+
let root = '.';
|
|
54
|
+
let rootExplicit = false;
|
|
55
|
+
const excludes = [];
|
|
56
|
+
const includes = [];
|
|
57
|
+
let cacheDir;
|
|
58
|
+
let quiet = false;
|
|
59
|
+
let noCache = false;
|
|
60
|
+
let daemon = false;
|
|
61
|
+
let noDaemon = false;
|
|
62
|
+
let probe = false;
|
|
63
|
+
for (let i = 0; i < args.length; i++) {
|
|
64
|
+
const arg = args[i];
|
|
65
|
+
switch (arg) {
|
|
66
|
+
case '--root':
|
|
67
|
+
root = args[++i] ?? root;
|
|
68
|
+
rootExplicit = true;
|
|
69
|
+
break;
|
|
70
|
+
case '--exclude':
|
|
71
|
+
excludes.push(args[++i] ?? '');
|
|
72
|
+
break;
|
|
73
|
+
case '--include':
|
|
74
|
+
includes.push(args[++i] ?? '');
|
|
75
|
+
break;
|
|
76
|
+
case '--cache-dir':
|
|
77
|
+
cacheDir = args[++i];
|
|
78
|
+
break;
|
|
79
|
+
case '--quiet':
|
|
80
|
+
quiet = true;
|
|
81
|
+
break;
|
|
82
|
+
case '--no-cache':
|
|
83
|
+
noCache = true;
|
|
84
|
+
break;
|
|
85
|
+
case '--daemon':
|
|
86
|
+
daemon = true;
|
|
87
|
+
break;
|
|
88
|
+
case '--no-daemon':
|
|
89
|
+
noDaemon = true;
|
|
90
|
+
break;
|
|
91
|
+
case '--probe':
|
|
92
|
+
probe = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { root, rootExplicit, excludes, includes, cacheDir, quiet, noCache, daemon, noDaemon, probe };
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Logging
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
function log(quiet, ...args) {
|
|
102
|
+
if (!quiet)
|
|
103
|
+
process.stderr.write(args.join(' ') + '\n');
|
|
104
|
+
}
|
|
105
|
+
// MCP clients advertise their workspace as a `file://` URI (or sometimes a bare
|
|
106
|
+
// path). Resolve the first advertised root to an absolute filesystem path,
|
|
107
|
+
// falling back to the CLI-provided root when the client advertises none.
|
|
108
|
+
function resolveClientRoot(clientRoots, fallback) {
|
|
109
|
+
if (clientRoots.length === 0)
|
|
110
|
+
return fallback;
|
|
111
|
+
const uri = clientRoots[0];
|
|
112
|
+
return uri.startsWith('file://') ? fileURLToPath(uri) : resolve(uri);
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Full indexing pipeline
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
export async function buildIndex(rootDir, excludes, includes, quiet) {
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
log(quiet, '[coldstart] Walking filesystem...');
|
|
120
|
+
const walkedFiles = await walkDirectory({ rootDir, excludes, includes });
|
|
121
|
+
log(quiet, `[coldstart] Found ${walkedFiles.length} source files`);
|
|
122
|
+
log(quiet, '[coldstart] Parsing files...');
|
|
123
|
+
const indexedFiles = [];
|
|
124
|
+
const langCount = {};
|
|
125
|
+
const allSymbolEdges = [];
|
|
126
|
+
const BATCH_SIZE = 100;
|
|
127
|
+
const PROGRESS_INTERVAL = 500;
|
|
128
|
+
let parsed_count = 0;
|
|
129
|
+
for (let i = 0; i < walkedFiles.length; i += BATCH_SIZE) {
|
|
130
|
+
const batch = walkedFiles.slice(i, i + BATCH_SIZE);
|
|
131
|
+
await Promise.all(batch.map(async (wf) => {
|
|
132
|
+
try {
|
|
133
|
+
const id = buildFileId(wf.relativePath);
|
|
134
|
+
const parsed = await parseFile(wf.absolutePath, wf.language, id);
|
|
135
|
+
if (!parsed)
|
|
136
|
+
return;
|
|
137
|
+
const file = {
|
|
138
|
+
...baseIndexedFile(id, wf.absolutePath, wf.relativePath, wf.language, parsed),
|
|
139
|
+
domainMap: buildFileDomains(wf.relativePath, parsed.exports),
|
|
140
|
+
importedByCount: 0,
|
|
141
|
+
transitiveImportedByCount: 0,
|
|
142
|
+
isBarrel: false,
|
|
143
|
+
isTestFile: isTestPath(wf.relativePath),
|
|
144
|
+
};
|
|
145
|
+
indexedFiles.push(file);
|
|
146
|
+
langCount[wf.language] = (langCount[wf.language] ?? 0) + 1;
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
log(quiet, `[coldstart] Error parsing ${wf.relativePath}: ${err}`);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
parsed_count++;
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
const prevMilestone = Math.floor((parsed_count - batch.length) / PROGRESS_INTERVAL);
|
|
156
|
+
const currMilestone = Math.floor(parsed_count / PROGRESS_INTERVAL);
|
|
157
|
+
const isLast = parsed_count === walkedFiles.length;
|
|
158
|
+
if (currMilestone > prevMilestone || isLast) {
|
|
159
|
+
const pct = Math.round((parsed_count / walkedFiles.length) * 100);
|
|
160
|
+
log(quiet, `[coldstart] ${parsed_count} / ${walkedFiles.length} parsed (${pct}%)`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
log(quiet, '[coldstart] Resolving imports...');
|
|
164
|
+
const { edges, unresolved } = await resolveImports(indexedFiles, rootDir);
|
|
165
|
+
log(quiet, `[coldstart] Resolved ${edges.length} edges (${unresolved.length} unresolved)`);
|
|
166
|
+
const fullFileIdSet = new Set(indexedFiles.map(f => f.id));
|
|
167
|
+
await addRailsSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
|
|
168
|
+
await addLaravelSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
|
|
169
|
+
await addCSharpSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
|
|
170
|
+
await addDjangoSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
|
|
171
|
+
if (!quiet) {
|
|
172
|
+
const langById = new Map(indexedFiles.map(f => [f.id, f.language]));
|
|
173
|
+
const stats = {};
|
|
174
|
+
for (const e of edges) {
|
|
175
|
+
const l = langById.get(e.from);
|
|
176
|
+
(stats[l] ??= { r: 0, u: 0 }).r++;
|
|
177
|
+
}
|
|
178
|
+
for (const u of unresolved) {
|
|
179
|
+
const l = langById.get(u.from);
|
|
180
|
+
(stats[l] ??= { r: 0, u: 0 }).u++;
|
|
181
|
+
}
|
|
182
|
+
const breakdown = Object.entries(stats)
|
|
183
|
+
.sort((a, b) => (b[1].r + b[1].u) - (a[1].r + a[1].u))
|
|
184
|
+
.map(([l, s]) => `${l}(${s.r}/${s.r + s.u})`)
|
|
185
|
+
.join(', ');
|
|
186
|
+
if (breakdown)
|
|
187
|
+
process.stderr.write(`[coldstart] Resolution by language: ${breakdown}\n`);
|
|
188
|
+
}
|
|
189
|
+
log(quiet, '[coldstart] Building graph...');
|
|
190
|
+
const nodeIds = indexedFiles.map(f => f.id);
|
|
191
|
+
const { outEdges, inEdges } = buildGraph(nodeIds, edges);
|
|
192
|
+
for (const file of indexedFiles) {
|
|
193
|
+
file.importedByCount = inEdges.get(file.id)?.length ?? 0;
|
|
194
|
+
}
|
|
195
|
+
const filesMap = new Map(indexedFiles.map(f => [f.id, f]));
|
|
196
|
+
const allSymbolEdgesBuilt = buildSymbolEdges(indexedFiles, outEdges, filesMap);
|
|
197
|
+
for (const e of allSymbolEdgesBuilt)
|
|
198
|
+
allSymbolEdges.push(e);
|
|
199
|
+
for (const file of indexedFiles) {
|
|
200
|
+
if (file.language === 'typescript' || file.language === 'javascript') {
|
|
201
|
+
file.isBarrel = ((file.reexportRatio ?? 0) > 0.5 &&
|
|
202
|
+
file.importedByCount > 1 &&
|
|
203
|
+
file.exports.length > 0);
|
|
204
|
+
}
|
|
205
|
+
file.transitiveImportedByCount = file.importedByCount;
|
|
206
|
+
}
|
|
207
|
+
for (const file of indexedFiles) {
|
|
208
|
+
if (!file.isBarrel)
|
|
209
|
+
continue;
|
|
210
|
+
for (const [token, ev] of Object.entries(file.domainMap)) {
|
|
211
|
+
if (ev.filename === 0 && ev.path === 0) {
|
|
212
|
+
delete file.domainMap[token];
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
file.domainMap[token] = { ...ev, symbol: 0 };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const tokenDocFreq = new Map();
|
|
220
|
+
for (const file of indexedFiles) {
|
|
221
|
+
if (file.isBarrel)
|
|
222
|
+
continue;
|
|
223
|
+
for (const token of Object.keys(file.domainMap)) {
|
|
224
|
+
tokenDocFreq.set(token, (tokenDocFreq.get(token) ?? 0) + 1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const contentTokenPostings = buildContentTokenPostings(indexedFiles);
|
|
228
|
+
const gitHead = await getGitHead(rootDir);
|
|
229
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
230
|
+
log(quiet, `[coldstart] Indexed ${indexedFiles.length} files in ${elapsed}s`);
|
|
231
|
+
log(quiet, `[coldstart] Languages: ${Object.entries(langCount).map(([l, c]) => `${l}(${c})`).join(', ')}`);
|
|
232
|
+
for (const file of indexedFiles) {
|
|
233
|
+
if (!file.isBarrel)
|
|
234
|
+
continue;
|
|
235
|
+
for (const childId of outEdges.get(file.id) ?? []) {
|
|
236
|
+
const child = filesMap.get(childId);
|
|
237
|
+
if (child)
|
|
238
|
+
child.transitiveImportedByCount += file.importedByCount;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
rootDir,
|
|
243
|
+
files: filesMap,
|
|
244
|
+
edges,
|
|
245
|
+
symbolEdges: allSymbolEdges,
|
|
246
|
+
outEdges,
|
|
247
|
+
inEdges,
|
|
248
|
+
tokenDocFreq,
|
|
249
|
+
contentTokenPostings,
|
|
250
|
+
indexedAt: Date.now(),
|
|
251
|
+
gitHead,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// Bucket an edge specifier into a coarse key for --probe output.
|
|
255
|
+
// - synthetic prefixes like `const:Foo` / `partial:UserService` → `const:*` / `partial:*`
|
|
256
|
+
// (last segment is class-like, generalize it)
|
|
257
|
+
// - `convention:django:middleware` → keep as-is (categorical, not class-specific)
|
|
258
|
+
// - regular imports (file paths, package names, no colon) → `regular`
|
|
259
|
+
function bucketSpecifier(spec) {
|
|
260
|
+
if (!spec.includes(':'))
|
|
261
|
+
return 'regular';
|
|
262
|
+
const parts = spec.split(':');
|
|
263
|
+
const last = parts[parts.length - 1];
|
|
264
|
+
if (/^[A-Z]/.test(last))
|
|
265
|
+
return parts.slice(0, -1).join(':') + ':*';
|
|
266
|
+
return spec;
|
|
267
|
+
}
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Probe mode: walk → parse → resolve, emit JSON stats to stdout, exit.
|
|
270
|
+
// Per-language: total imports, resolved, unresolved, plus top-N unresolved
|
|
271
|
+
// specifiers (with sample fromFile) so we can tell external libs apart from
|
|
272
|
+
// broken-internal resolution gaps.
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
async function runProbe(rootDir, excludes, includes) {
|
|
275
|
+
const start = Date.now();
|
|
276
|
+
const walked = await walkDirectory({ rootDir, excludes, includes });
|
|
277
|
+
const tWalk = Date.now() - start;
|
|
278
|
+
const tParseStart = Date.now();
|
|
279
|
+
const indexedFiles = [];
|
|
280
|
+
for (let i = 0; i < walked.length; i += 100) {
|
|
281
|
+
const batch = walked.slice(i, i + 100);
|
|
282
|
+
await Promise.all(batch.map(async (wf) => {
|
|
283
|
+
try {
|
|
284
|
+
const id = buildFileId(wf.relativePath);
|
|
285
|
+
const parsed = await parseFile(wf.absolutePath, wf.language, id);
|
|
286
|
+
if (!parsed)
|
|
287
|
+
return;
|
|
288
|
+
indexedFiles.push({
|
|
289
|
+
...baseIndexedFile(id, wf.absolutePath, wf.relativePath, wf.language, parsed),
|
|
290
|
+
domainMap: {},
|
|
291
|
+
importedByCount: 0,
|
|
292
|
+
transitiveImportedByCount: 0,
|
|
293
|
+
isBarrel: false,
|
|
294
|
+
isTestFile: false,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch { /* skip parse errors */ }
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
const tParse = Date.now() - tParseStart;
|
|
301
|
+
const tResolveStart = Date.now();
|
|
302
|
+
const { resolveImportsForFiles, buildPackageIndex } = await import('./indexer/resolvers/index.js');
|
|
303
|
+
// Per-language resolve timing — pass the FULL fileIdSet (resolvers index it
|
|
304
|
+
// once via WeakMap) but invoke per-language so we can attribute time. Java
|
|
305
|
+
// resolves to Kotlin files and vice versa, so we must not narrow the set.
|
|
306
|
+
const fullFileIdSet = new Set(indexedFiles.map(f => f.id));
|
|
307
|
+
// JVM package index over the FULL set — anchors Java/Kotlin FQCN resolution on
|
|
308
|
+
// each file's declared `package`, so non-Maven layouts (e.g. JMRI's java/src/)
|
|
309
|
+
// resolve correctly instead of mis-deriving the package from the path.
|
|
310
|
+
const pkgById = buildPackageIndex(indexedFiles);
|
|
311
|
+
const filesByLang = new Map();
|
|
312
|
+
for (const f of indexedFiles) {
|
|
313
|
+
const arr = filesByLang.get(f.language) ?? [];
|
|
314
|
+
arr.push(f);
|
|
315
|
+
filesByLang.set(f.language, arr);
|
|
316
|
+
}
|
|
317
|
+
const langTimes = {};
|
|
318
|
+
let allEdges = [];
|
|
319
|
+
let allUnresolved = [];
|
|
320
|
+
for (const [lang, files] of filesByLang) {
|
|
321
|
+
const t0 = Date.now();
|
|
322
|
+
const r = await resolveImportsForFiles(files, fullFileIdSet, rootDir, pkgById);
|
|
323
|
+
langTimes[lang] = Date.now() - t0;
|
|
324
|
+
allEdges = allEdges.concat(r.edges);
|
|
325
|
+
allUnresolved = allUnresolved.concat(r.unresolved);
|
|
326
|
+
}
|
|
327
|
+
const edges = allEdges;
|
|
328
|
+
const unresolved = allUnresolved;
|
|
329
|
+
const tResolve = Date.now() - tResolveStart;
|
|
330
|
+
await addRailsSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
|
|
331
|
+
await addLaravelSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
|
|
332
|
+
await addCSharpSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
|
|
333
|
+
await addDjangoSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
|
|
334
|
+
const langById = new Map(indexedFiles.map(f => [f.id, f.language]));
|
|
335
|
+
const fileById = new Map(indexedFiles.map(f => [f.id, f.relativePath]));
|
|
336
|
+
const byLang = new Map();
|
|
337
|
+
const get = (l) => {
|
|
338
|
+
let b = byLang.get(l);
|
|
339
|
+
if (!b) {
|
|
340
|
+
b = { files: 0, totalImports: 0, resolved: 0, unresolved: 0, unresolvedBySpec: new Map() };
|
|
341
|
+
byLang.set(l, b);
|
|
342
|
+
}
|
|
343
|
+
return b;
|
|
344
|
+
};
|
|
345
|
+
for (const f of indexedFiles) {
|
|
346
|
+
get(f.language).files++;
|
|
347
|
+
get(f.language).totalImports += f.imports.length;
|
|
348
|
+
}
|
|
349
|
+
for (const e of edges) {
|
|
350
|
+
get(langById.get(e.from)).resolved++;
|
|
351
|
+
}
|
|
352
|
+
for (const u of unresolved) {
|
|
353
|
+
const b = get(langById.get(u.from));
|
|
354
|
+
b.unresolved++;
|
|
355
|
+
const existing = b.unresolvedBySpec.get(u.specifier);
|
|
356
|
+
if (existing)
|
|
357
|
+
existing.count++;
|
|
358
|
+
else
|
|
359
|
+
b.unresolvedBySpec.set(u.specifier, { count: 1, sampleFrom: fileById.get(u.from) ?? '' });
|
|
360
|
+
}
|
|
361
|
+
const edgesBySpecifier = {};
|
|
362
|
+
for (const e of edges) {
|
|
363
|
+
const bucket = bucketSpecifier(e.specifier);
|
|
364
|
+
edgesBySpecifier[bucket] = (edgesBySpecifier[bucket] ?? 0) + 1;
|
|
365
|
+
}
|
|
366
|
+
const sortedEdgesBySpecifier = Object.fromEntries(Object.entries(edgesBySpecifier).sort((a, b) => b[1] - a[1]));
|
|
367
|
+
const out = {
|
|
368
|
+
rootDir,
|
|
369
|
+
totalFiles: indexedFiles.length,
|
|
370
|
+
totalEdges: edges.length,
|
|
371
|
+
totalUnresolved: unresolved.length,
|
|
372
|
+
elapsedMs: Date.now() - start,
|
|
373
|
+
phaseMs: { walk: tWalk, parse: tParse, resolve: tResolve, resolveByLang: langTimes },
|
|
374
|
+
edgesBySpecifier: sortedEdgesBySpecifier,
|
|
375
|
+
languages: Object.fromEntries([...byLang.entries()]
|
|
376
|
+
.sort((a, b) => b[1].totalImports - a[1].totalImports)
|
|
377
|
+
.map(([lang, b]) => [lang, {
|
|
378
|
+
files: b.files,
|
|
379
|
+
totalImports: b.totalImports,
|
|
380
|
+
resolved: b.resolved,
|
|
381
|
+
unresolved: b.unresolved,
|
|
382
|
+
resolvedRatio: b.totalImports > 0 ? +(b.resolved / b.totalImports).toFixed(3) : 0,
|
|
383
|
+
topUnresolved: [...b.unresolvedBySpec.entries()]
|
|
384
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
385
|
+
.slice(0, 30)
|
|
386
|
+
.map(([specifier, v]) => ({ specifier, count: v.count, sampleFrom: v.sampleFrom })),
|
|
387
|
+
}])),
|
|
388
|
+
};
|
|
389
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
390
|
+
}
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// Shared: load from cache or build, then create and return an IndexManager
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
async function buildManager(finalRoot, excludes, includes, cacheDir, quiet, noCache) {
|
|
395
|
+
let index = null;
|
|
396
|
+
if (!noCache) {
|
|
397
|
+
index = await loadCachedIndex(finalRoot, cacheDir);
|
|
398
|
+
if (index) {
|
|
399
|
+
// Startup reconcile: the cache is trusted but files may have changed
|
|
400
|
+
// while no keeper watched (branch switch, edits, pulls). Patch the
|
|
401
|
+
// drift in place — a full rebuild only when the drift is repo-scale.
|
|
402
|
+
const rec = await reconcileChanges(index, finalRoot, excludes, includes);
|
|
403
|
+
const threshold = patchThreshold(index.files.size);
|
|
404
|
+
let outcome;
|
|
405
|
+
if (!rec) {
|
|
406
|
+
log(quiet, '[coldstart] Reconcile unavailable — rebuilding index...');
|
|
407
|
+
outcome = 'unavailable → rebuild';
|
|
408
|
+
index = null;
|
|
409
|
+
}
|
|
410
|
+
else if (rec.changed.size === 0) {
|
|
411
|
+
log(quiet, `[coldstart] Loaded from cache — fresh (${rec.reason})`);
|
|
412
|
+
outcome = `${rec.reason} → fresh`;
|
|
413
|
+
// HEAD can move with zero file drift (committing files the keeper
|
|
414
|
+
// already patched). Refresh the stored head or every reader pays the
|
|
415
|
+
// full HEAD-drift wait on every query, forever.
|
|
416
|
+
const head = await getGitHead(finalRoot);
|
|
417
|
+
if (head !== index.gitHead) {
|
|
418
|
+
index.gitHead = head;
|
|
419
|
+
try {
|
|
420
|
+
await updateCachedGitHead(finalRoot, head, cacheDir);
|
|
421
|
+
}
|
|
422
|
+
catch { /* next full save fixes it */ }
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (rec.changed.size <= threshold) {
|
|
426
|
+
log(quiet, `[coldstart] Loaded from cache — patching ${rec.changed.size} drifted file(s) (${rec.reason})`);
|
|
427
|
+
try {
|
|
428
|
+
await patchIndex(index, rec.changed, finalRoot);
|
|
429
|
+
const problems = lintIndexInvariants(index);
|
|
430
|
+
if (problems.length > 0)
|
|
431
|
+
throw new Error(`invariant violation: ${problems.join(' | ')}`);
|
|
432
|
+
index.gitHead = await getGitHead(finalRoot);
|
|
433
|
+
index.indexedAt = Date.now();
|
|
434
|
+
await saveCachedIndex(index, cacheDir);
|
|
435
|
+
log(quiet, '[coldstart] Reconcile patch done');
|
|
436
|
+
outcome = `${rec.reason} → patched`;
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
log(quiet, `[coldstart] Reconcile patch failed (${err}) — rebuilding index...`);
|
|
440
|
+
if (!noCache)
|
|
441
|
+
await appendRepairLog(finalRoot, 'reconcile-failed', String(err), cacheDir);
|
|
442
|
+
outcome = `${rec.reason} → patch failed, rebuild`;
|
|
443
|
+
index = null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
log(quiet, `[coldstart] ${rec.changed.size} file(s) drifted (> ${threshold}) — rebuilding index...`);
|
|
448
|
+
outcome = `${rec.reason} → over threshold, rebuild`;
|
|
449
|
+
index = null;
|
|
450
|
+
}
|
|
451
|
+
if (!noCache)
|
|
452
|
+
await updateKeeperState(finalRoot, { lastReconcile: { at: Date.now(), detail: outcome } }, cacheDir);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (!index) {
|
|
456
|
+
index = await buildIndex(finalRoot, excludes, includes, quiet);
|
|
457
|
+
if (!noCache) {
|
|
458
|
+
try {
|
|
459
|
+
await saveCachedIndex(index, cacheDir);
|
|
460
|
+
log(quiet, '[coldstart] Index saved to cache');
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
log(quiet, `[coldstart] Warning: could not save cache: ${err}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const manager = new IndexManager(index, () => buildIndex(finalRoot, excludes, includes, quiet), cacheDir, noCache, quiet);
|
|
468
|
+
manager.startWatching();
|
|
469
|
+
return manager;
|
|
470
|
+
}
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
// Keeper mode (`--daemon`): keep the on-disk index FRESH. Watches the repo,
|
|
473
|
+
// patches/rebuilds the in-memory index, and writes it to the disk cache that
|
|
474
|
+
// the CLI and MCP readers load. It does NOT serve queries — every reader reads
|
|
475
|
+
// the cache directly. Lazy-spawned by the first reader; runs until killed or
|
|
476
|
+
// its lockfile is removed.
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
async function runKeeper(finalRoot, excludes, includes, cacheDir, _quiet, noCache) {
|
|
479
|
+
// Attach the file-backed logger BEFORE the first log() call: the keeper is
|
|
480
|
+
// auto-spawned with stdio: 'ignore', so the log file is the ONLY debug trace.
|
|
481
|
+
const detachLogger = attachDaemonLogger(finalRoot);
|
|
482
|
+
// The keeper IGNORES --quiet: its stderr goes to the log file, never a
|
|
483
|
+
// console (readers spawn it with stdio 'ignore'), and an empty log makes
|
|
484
|
+
// every keeper incident undiagnosable. --quiet keeps its meaning for the
|
|
485
|
+
// MCP reader / --no-daemon modes, which do write to a real stderr.
|
|
486
|
+
const quiet = false;
|
|
487
|
+
void _quiet;
|
|
488
|
+
log(quiet, `[coldstart] Keeper starting — root: ${finalRoot} (PID ${process.pid})`);
|
|
489
|
+
// Exit immediately if another keeper is already alive for this root.
|
|
490
|
+
const existing = await readLock(finalRoot);
|
|
491
|
+
if (existing && isDaemonAlive(existing.pid)) {
|
|
492
|
+
log(quiet, '[coldstart] Another keeper is already running — exiting.');
|
|
493
|
+
detachLogger();
|
|
494
|
+
process.exit(0);
|
|
495
|
+
}
|
|
496
|
+
// Version in the lock lets a reader replace an outdated keeper (cache-format
|
|
497
|
+
// compatibility) on upgrade.
|
|
498
|
+
await writeLock(finalRoot, process.pid, getCurrentVersion());
|
|
499
|
+
log(quiet, `[coldstart] Keeper lock written (PID ${process.pid})`);
|
|
500
|
+
if (!noCache)
|
|
501
|
+
await updateKeeperState(finalRoot, { startedAt: Date.now() }, cacheDir);
|
|
502
|
+
// Auto-migrate legacy npx-based .mcp.json entries (non-fatal).
|
|
503
|
+
try {
|
|
504
|
+
await migrateLegacyMcpConfig(finalRoot);
|
|
505
|
+
}
|
|
506
|
+
catch { /* ignore */ }
|
|
507
|
+
let manager = null;
|
|
508
|
+
try {
|
|
509
|
+
// buildManager loads-or-builds the index AND starts the watcher, which
|
|
510
|
+
// patches/rebuilds and debounce-saves the cache on every change.
|
|
511
|
+
manager = await buildManager(finalRoot, excludes, includes, cacheDir, quiet, noCache);
|
|
512
|
+
log(quiet, '[coldstart] Keeper index ready — watching for changes');
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
log(quiet, `[coldstart] Keeper index build failed: ${err}`);
|
|
516
|
+
await deleteLock(finalRoot).catch(() => { });
|
|
517
|
+
detachLogger();
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
const cleanup = () => {
|
|
521
|
+
manager?.stopWatching();
|
|
522
|
+
deleteLock(finalRoot).catch(() => { }).finally(() => {
|
|
523
|
+
detachLogger();
|
|
524
|
+
process.exit(0);
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
// Exit cleanly if the user removes our lockfile.
|
|
528
|
+
const stopLockWatcher = watchOwnLockfile(finalRoot, () => {
|
|
529
|
+
log(quiet, '[coldstart] Lockfile deleted — shutting down keeper');
|
|
530
|
+
cleanup();
|
|
531
|
+
});
|
|
532
|
+
process.on('SIGTERM', cleanup);
|
|
533
|
+
process.on('SIGINT', cleanup);
|
|
534
|
+
process.on('exit', () => {
|
|
535
|
+
stopLockWatcher();
|
|
536
|
+
manager?.stopWatching();
|
|
537
|
+
detachLogger();
|
|
538
|
+
});
|
|
539
|
+
// Run forever.
|
|
540
|
+
await new Promise(() => { });
|
|
541
|
+
}
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// A disk-cache reader for the long-lived MCP server: loads the keeper-maintained
|
|
544
|
+
// index and reloads it when the cache file's mtime advances (the keeper rewrites
|
|
545
|
+
// it ~5s after edits settle). This is how the MCP server stays live WITHOUT its
|
|
546
|
+
// own watcher — the keeper is the single freshness authority.
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
function makeCacheReader(finalRoot, cacheDir, quiet) {
|
|
549
|
+
let cached = null;
|
|
550
|
+
let lastMtimeMs = 0;
|
|
551
|
+
const metaPath = join(getCacheDir(finalRoot, cacheDir), 'meta.json');
|
|
552
|
+
const currentMtime = () => {
|
|
553
|
+
try {
|
|
554
|
+
return statSync(metaPath).mtimeMs;
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
return async () => {
|
|
561
|
+
const m = currentMtime();
|
|
562
|
+
if (!cached || (m > 0 && m > lastMtimeMs)) {
|
|
563
|
+
// 'gs' profile: everything the MCP tools (find/gs/kb_search) read, without
|
|
564
|
+
// the keeper-only build segment. The keeper is the only 'full' loader.
|
|
565
|
+
const fresh = await loadCachedIndex(finalRoot, cacheDir, 'gs');
|
|
566
|
+
if (fresh) {
|
|
567
|
+
cached = fresh;
|
|
568
|
+
lastMtimeMs = m;
|
|
569
|
+
}
|
|
570
|
+
else if (!cached) {
|
|
571
|
+
// No cache yet (keeper still building its first index). Readers never
|
|
572
|
+
// build (B4) — wait for the keeper's first save; in-process build only
|
|
573
|
+
// if no keeper exists at all.
|
|
574
|
+
const wait = await waitForKeeperCache(finalRoot, cacheDir, 180_000, (msg) => log(quiet, msg));
|
|
575
|
+
if (wait === 'ready') {
|
|
576
|
+
for (let attempt = 0; attempt < 3 && !cached; attempt++) {
|
|
577
|
+
cached = await loadCachedIndex(finalRoot, cacheDir, 'gs');
|
|
578
|
+
if (!cached)
|
|
579
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (!cached) {
|
|
583
|
+
if (wait === 'timeout') {
|
|
584
|
+
throw new Error('coldstart: the keeper is still building the index for this repo — retry in a minute');
|
|
585
|
+
}
|
|
586
|
+
log(quiet, '[coldstart] no keeper available — building in-process (one-off)');
|
|
587
|
+
cached = await buildIndex(finalRoot, [], [], quiet);
|
|
588
|
+
try {
|
|
589
|
+
await saveCachedIndex(cached, cacheDir);
|
|
590
|
+
}
|
|
591
|
+
catch { /* ignore */ }
|
|
592
|
+
}
|
|
593
|
+
lastMtimeMs = currentMtime();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return { index: cached, isRebuilding: false };
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
// ---------------------------------------------------------------------------
|
|
600
|
+
// Main
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
async function main() {
|
|
603
|
+
if (process.argv[2] === 'init') {
|
|
604
|
+
const { runInit } = await import('./init.js');
|
|
605
|
+
await runInit();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (process.argv[2] === 'status') {
|
|
609
|
+
const { runStatus } = await import('./status.js');
|
|
610
|
+
await runStatus();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// `restart` — kill the keeper for this root (or --all) and clear its lock.
|
|
614
|
+
// The keeper respawns lazily on the next reader.
|
|
615
|
+
if (process.argv[2] === 'restart') {
|
|
616
|
+
const { runRestart } = await import('./restart.js');
|
|
617
|
+
await runRestart();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// CLI query surface — pure-reader `find`/`gs`, plus single-writer `index` prep.
|
|
621
|
+
// These read the on-disk cache directly (kept fresh by the keeper), run the
|
|
622
|
+
// same engine the MCP reader uses, print, exit. (docs/cli-skill-spec.md)
|
|
623
|
+
if (process.argv[2] === 'gs') {
|
|
624
|
+
const { runGs } = await import('./cli.js');
|
|
625
|
+
process.exit(await runGs(process.argv.slice(3), buildIndex));
|
|
626
|
+
}
|
|
627
|
+
if (process.argv[2] === 'find') {
|
|
628
|
+
const { runFind } = await import('./cli.js');
|
|
629
|
+
process.exit(await runFind(process.argv.slice(3), buildIndex));
|
|
630
|
+
}
|
|
631
|
+
if (process.argv[2] === 'index') {
|
|
632
|
+
const { runIndexPrep } = await import('./cli.js');
|
|
633
|
+
process.exit(await runIndexPrep(process.argv.slice(3), buildIndex));
|
|
634
|
+
}
|
|
635
|
+
// Notebook KB — `kb search|write|status|lint|render|init|migrate`.
|
|
636
|
+
if (process.argv[2] === 'kb') {
|
|
637
|
+
const { runKb } = await import('./kb/cli.js');
|
|
638
|
+
process.exit(await runKb(process.argv.slice(3)));
|
|
639
|
+
}
|
|
640
|
+
const { root: cliRoot, rootExplicit, excludes, includes, cacheDir, quiet, noCache, daemon, noDaemon, probe } = parseArgs(process.argv);
|
|
641
|
+
// --probe: one-shot stats dump for the validation harness, then exit.
|
|
642
|
+
if (probe) {
|
|
643
|
+
await runProbe(resolve(cliRoot), excludes, includes);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
// --daemon: background keeper that keeps the on-disk cache fresh (no serving).
|
|
647
|
+
// Lazy-spawned by readers via ensureKeeper().
|
|
648
|
+
if (daemon) {
|
|
649
|
+
const finalRoot = resolve(cliRoot);
|
|
650
|
+
await runKeeper(finalRoot, excludes, includes, cacheDir, quiet, noCache);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
// --no-daemon: self-contained single-process stdio MCP (builds + watches +
|
|
654
|
+
// serves in-process, no background keeper). Useful for debugging / one-off use.
|
|
655
|
+
if (noDaemon) {
|
|
656
|
+
let manager = null;
|
|
657
|
+
let managerReadyResolve;
|
|
658
|
+
let managerReadyReject;
|
|
659
|
+
const managerReady = new Promise((res, rej) => {
|
|
660
|
+
managerReadyResolve = res;
|
|
661
|
+
managerReadyReject = rej;
|
|
662
|
+
});
|
|
663
|
+
const buildAndSet = async (finalRoot) => {
|
|
664
|
+
log(quiet, `[coldstart] Starting — root: ${finalRoot}`);
|
|
665
|
+
try {
|
|
666
|
+
await migrateLegacyMcpConfig(finalRoot);
|
|
667
|
+
manager = await buildManager(finalRoot, excludes, includes, cacheDir, quiet, noCache);
|
|
668
|
+
managerReadyResolve();
|
|
669
|
+
log(quiet, '[coldstart] MCP server ready');
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
log(quiet, `[coldstart] Fatal: ${err}`);
|
|
673
|
+
managerReadyReject(err instanceof Error ? err : new Error(String(err)));
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
if (rootExplicit) {
|
|
678
|
+
buildAndSet(resolve(cliRoot)).catch(() => { });
|
|
679
|
+
}
|
|
680
|
+
await startMCPServer(async (clientRoots) => {
|
|
681
|
+
if (!rootExplicit) {
|
|
682
|
+
buildAndSet(resolveClientRoot(clientRoots, resolve(cliRoot))).catch(() => { });
|
|
683
|
+
}
|
|
684
|
+
}, async () => {
|
|
685
|
+
await managerReady;
|
|
686
|
+
return manager.getContext();
|
|
687
|
+
}, { cacheDir });
|
|
688
|
+
process.on('exit', () => manager?.stopWatching());
|
|
689
|
+
process.on('SIGINT', () => { manager?.stopWatching(); process.exit(0); });
|
|
690
|
+
process.on('SIGTERM', () => { manager?.stopWatching(); process.exit(0); });
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
// Default: stdio MCP server in READER mode. Serves find/gs off the on-disk
|
|
694
|
+
// cache that a background keeper keeps fresh; spawns the keeper if absent.
|
|
695
|
+
// This is the path for no-shell MCP clients (e.g. Claude Desktop).
|
|
696
|
+
{
|
|
697
|
+
let reader = null;
|
|
698
|
+
let readerReadyResolve;
|
|
699
|
+
const readerReady = new Promise(res => { readerReadyResolve = res; });
|
|
700
|
+
const initRoot = async (finalRoot) => {
|
|
701
|
+
log(quiet, `[coldstart] MCP reader — root: ${finalRoot}`);
|
|
702
|
+
await ensureKeeper(finalRoot);
|
|
703
|
+
reader = makeCacheReader(finalRoot, cacheDir, quiet);
|
|
704
|
+
readerReadyResolve();
|
|
705
|
+
};
|
|
706
|
+
if (rootExplicit) {
|
|
707
|
+
void initRoot(resolve(cliRoot));
|
|
708
|
+
}
|
|
709
|
+
await startMCPServer(async (clientRoots) => {
|
|
710
|
+
if (!rootExplicit) {
|
|
711
|
+
void initRoot(resolveClientRoot(clientRoots, resolve(cliRoot)));
|
|
712
|
+
}
|
|
713
|
+
}, async () => {
|
|
714
|
+
await readerReady;
|
|
715
|
+
return reader();
|
|
716
|
+
}, { cacheDir });
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
main().catch(err => {
|
|
721
|
+
process.stderr.write(`[coldstart] Fatal error: ${err}\n`);
|
|
722
|
+
process.exit(1);
|
|
723
|
+
});
|
|
724
|
+
//# sourceMappingURL=index.js.map
|