codesearch-mcp 0.2.2-alpha → 0.3.0-alpha

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 (158) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +60 -33
  3. package/dist/app.js +46 -22
  4. package/dist/src/chunking/AstChunker.d.ts +12 -0
  5. package/dist/src/chunking/AstChunker.js +30 -0
  6. package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
  7. package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
  8. package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
  9. package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
  10. package/dist/src/chunking/FragmentMerger.d.ts +11 -0
  11. package/dist/src/chunking/FragmentMerger.js +61 -0
  12. package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
  13. package/dist/src/chunking/ImportOnlyFilter.js +14 -0
  14. package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
  15. package/dist/src/chunking/LangchainTextSplitter.js +133 -0
  16. package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
  17. package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
  18. package/dist/src/chunking/RunPacker.d.ts +12 -0
  19. package/dist/src/chunking/RunPacker.js +84 -0
  20. package/dist/src/chunking/create-chunker.d.ts +3 -0
  21. package/dist/src/chunking/create-chunker.js +15 -0
  22. package/dist/src/chunking/interfaces.d.ts +27 -0
  23. package/dist/src/chunking/interfaces.js +2 -0
  24. package/dist/src/commands/ConfigCommand.js +1 -9
  25. package/dist/src/commands/EvalCommand.d.ts +11 -0
  26. package/dist/src/commands/EvalCommand.js +264 -0
  27. package/dist/src/commands/IndexCommand.js +70 -66
  28. package/dist/src/commands/McpCommand.js +2 -8
  29. package/dist/src/commands/WebCommand.js +2 -5
  30. package/dist/src/config-error.d.ts +0 -4
  31. package/dist/src/config-error.js +0 -4
  32. package/dist/src/config-store.d.ts +1 -54
  33. package/dist/src/config-store.js +8 -69
  34. package/dist/src/config.d.ts +8 -17
  35. package/dist/src/config.js +35 -34
  36. package/dist/src/constants.d.ts +2 -28
  37. package/dist/src/constants.js +6 -28
  38. package/dist/src/container.d.ts +6 -0
  39. package/dist/src/container.js +12 -3
  40. package/dist/src/db/index.d.ts +1 -1
  41. package/dist/src/db/index.js +1 -2
  42. package/dist/src/db/schema.d.ts +22 -29
  43. package/dist/src/db/schema.js +3 -14
  44. package/dist/src/declarations/DeclarationTree.d.ts +19 -0
  45. package/dist/src/declarations/DeclarationTree.js +66 -0
  46. package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
  47. package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
  48. package/dist/src/declarations/SourceLines.d.ts +37 -0
  49. package/dist/src/declarations/SourceLines.js +415 -0
  50. package/dist/src/dependency-traversal.d.ts +10 -0
  51. package/dist/src/dependency-traversal.js +39 -0
  52. package/dist/src/enriched-prompt.d.ts +0 -5
  53. package/dist/src/enriched-prompt.js +0 -5
  54. package/dist/src/error-record.d.ts +12 -0
  55. package/dist/src/error-record.js +248 -0
  56. package/dist/src/eval-corpus.d.ts +16 -0
  57. package/dist/src/eval-corpus.js +169 -0
  58. package/dist/src/eval-results.d.ts +15 -0
  59. package/dist/src/eval-results.js +397 -0
  60. package/dist/src/file-utils.d.ts +10 -0
  61. package/dist/src/file-utils.js +19 -1
  62. package/dist/src/graph.d.ts +0 -25
  63. package/dist/src/graph.js +0 -28
  64. package/dist/src/help.js +4 -1
  65. package/dist/src/hnsw-settings.d.ts +3 -0
  66. package/dist/src/hnsw-settings.js +24 -0
  67. package/dist/src/interfaces/IAstParser.d.ts +5 -30
  68. package/dist/src/interfaces/IChunker.d.ts +3 -12
  69. package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
  70. package/dist/src/interfaces/ICorpusInspector.js +2 -0
  71. package/dist/src/interfaces/IDatabase.d.ts +2 -36
  72. package/dist/src/interfaces/IEmbedder.d.ts +0 -9
  73. package/dist/src/interfaces/ILogger.d.ts +1 -11
  74. package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
  75. package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
  76. package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
  77. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +216 -0
  78. package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
  79. package/dist/src/interfaces/ISearcher.d.ts +26 -20
  80. package/dist/src/interfaces/index.d.ts +1 -0
  81. package/dist/src/interrupt.js +0 -2
  82. package/dist/src/owner-path.d.ts +9 -10
  83. package/dist/src/owner-path.js +11 -23
  84. package/dist/src/project-scope.d.ts +7 -0
  85. package/dist/src/project-scope.js +37 -0
  86. package/dist/src/project-selector.d.ts +1 -0
  87. package/dist/src/project-selector.js +13 -6
  88. package/dist/src/retrieval-eval.d.ts +24 -0
  89. package/dist/src/retrieval-eval.js +500 -0
  90. package/dist/src/search/result-shaping.d.ts +4 -0
  91. package/dist/src/search/result-shaping.js +19 -0
  92. package/dist/src/services/AstParserService.d.ts +28 -23
  93. package/dist/src/services/AstParserService.js +391 -98
  94. package/dist/src/services/DatabaseService.d.ts +2 -19
  95. package/dist/src/services/DatabaseService.js +44 -81
  96. package/dist/src/services/EmbedderFactory.d.ts +0 -4
  97. package/dist/src/services/EmbedderFactory.js +0 -5
  98. package/dist/src/services/IndexerService.d.ts +2 -10
  99. package/dist/src/services/IndexerService.js +29 -45
  100. package/dist/src/services/InfinityEmbedder.d.ts +0 -21
  101. package/dist/src/services/InfinityEmbedder.js +0 -15
  102. package/dist/src/services/LoggerService.d.ts +7 -5
  103. package/dist/src/services/LoggerService.js +37 -41
  104. package/dist/src/services/McpServer.d.ts +25 -10
  105. package/dist/src/services/McpServer.js +101 -64
  106. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
  107. package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
  108. package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
  109. package/dist/src/services/NxMonorepoResolver.js +5 -56
  110. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
  111. package/dist/src/services/OpenAICompatibleEmbedder.js +1 -14
  112. package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
  113. package/dist/src/services/ProjectDiscoveryService.js +0 -4
  114. package/dist/src/services/SearcherService.d.ts +127 -70
  115. package/dist/src/services/SearcherService.js +526 -305
  116. package/dist/src/services/VoyageEmbedder.d.ts +0 -6
  117. package/dist/src/services/VoyageEmbedder.js +0 -6
  118. package/dist/src/services/WebServerService.d.ts +1 -53
  119. package/dist/src/services/WebServerService.js +11 -57
  120. package/dist/src/services/http-retry.d.ts +0 -7
  121. package/dist/src/services/http-retry.js +1 -10
  122. package/dist/src/services/infinity-provisioning.d.ts +0 -74
  123. package/dist/src/services/infinity-provisioning.js +5 -88
  124. package/dist/src/services/legacy-ollama-index.d.ts +0 -16
  125. package/dist/src/services/legacy-ollama-index.js +0 -23
  126. package/dist/src/services/reranker.d.ts +0 -9
  127. package/dist/src/services/reranker.js +46 -12
  128. package/dist/src/setup.d.ts +11 -23
  129. package/dist/src/setup.js +98 -71
  130. package/dist/src/symbol-name.d.ts +0 -12
  131. package/dist/src/symbol-name.js +0 -13
  132. package/dist/src/symbol-rows.d.ts +2 -11
  133. package/dist/src/symbol-rows.js +5 -13
  134. package/dist/src/tool-call-log.d.ts +2 -0
  135. package/dist/src/tool-call-log.js +24 -0
  136. package/dist/src/tools/FindSymbolTool.js +14 -5
  137. package/dist/src/tools/GetCallersTool.d.ts +3 -1
  138. package/dist/src/tools/GetCallersTool.js +16 -6
  139. package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
  140. package/dist/src/tools/GetDependenciesTool.js +15 -8
  141. package/dist/src/tools/GetDependentsTool.d.ts +3 -1
  142. package/dist/src/tools/GetDependentsTool.js +31 -16
  143. package/dist/src/tools/ListFilesTool.d.ts +3 -1
  144. package/dist/src/tools/ListFilesTool.js +14 -12
  145. package/dist/src/tools/ListProjectsTool.js +5 -1
  146. package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
  147. package/dist/src/tools/ReadFileRangeTool.js +12 -11
  148. package/dist/src/tools/ReadFileTool.d.ts +3 -1
  149. package/dist/src/tools/ReadFileTool.js +12 -10
  150. package/dist/src/tools/SearchCodebaseTool.js +7 -2
  151. package/dist/src/tools/format-helpers.d.ts +10 -1
  152. package/dist/src/tools/format-helpers.js +145 -8
  153. package/dist/src/web/dashboard.d.ts +0 -8
  154. package/dist/src/web/dashboard.js +7 -10
  155. package/package.json +10 -9
  156. package/dist/src/services/AstChunker.d.ts +0 -42
  157. package/dist/src/services/AstChunker.js +0 -158
  158. package/docs/web-dashboard.md +0 -206
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codesearch-mcp",
3
- "version": "0.2.2-alpha",
3
+ "version": "0.3.0-alpha",
4
4
  "description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -29,7 +29,7 @@
29
29
  "files": [
30
30
  "dist/",
31
31
  "docker/",
32
- "docs/",
32
+ "docs/architecture.svg",
33
33
  "README.md",
34
34
  "CHANGELOG.md"
35
35
  ],
@@ -42,23 +42,24 @@
42
42
  "start:web": "ts-node app.ts web",
43
43
  "index:workspace": "ts-node app.ts index",
44
44
  "index:reindex": "ts-node app.ts index --reindex",
45
+ "index:project": "ts-node app.ts index --project",
45
46
  "typecheck": "tsc --noEmit",
46
47
  "test": "vitest run",
47
48
  "test:watch": "vitest",
48
49
  "verify": "tsc --noEmit && vitest run"
49
50
  },
50
51
  "dependencies": {
51
- "@inquirer/prompts": "^8.4.1",
52
- "@langchain/textsplitters": "^1.0.1",
53
- "@modelcontextprotocol/sdk": "^1.0.0",
52
+ "@inquirer/prompts": "^8.7.3",
53
+ "@langchain/textsplitters": "^1.0.2",
54
+ "@modelcontextprotocol/sdk": "1.32.0",
54
55
  "cli-progress": "^3.12.0",
55
56
  "drizzle-orm": "^0.45.2",
56
57
  "ignore": "^7.0.5",
57
- "pg": "^8.13.0",
58
- "pino": "^10.3.1",
58
+ "pg": "^8.23.1",
59
+ "pino": "^10.4.0",
59
60
  "pino-roll": "^4.0.0",
60
61
  "ts-morph": "^28.0.0",
61
- "zod": "^4.3.6"
62
+ "zod": "^4.6.7"
62
63
  },
63
64
  "devDependencies": {
64
65
  "@types/cli-progress": "^3.11.6",
@@ -66,6 +67,6 @@
66
67
  "@types/pg": "^8.20.0",
67
68
  "ts-node": "^10.9.2",
68
69
  "typescript": "^5.6.0",
69
- "vitest": "^4.1.4"
70
+ "vitest": "^5.0.5"
70
71
  }
71
72
  }
@@ -1,42 +0,0 @@
1
- import type { Config } from '../config';
2
- import type { IChunker, ChunkPiece, ChunkOpts } from '../interfaces/IChunker';
3
- /**
4
- * Splits TS/JS source into embeddable pieces aligned to top-level declaration
5
- * ranges (AST strategy). Falls back to the character-based recursive splitter
6
- * for schema files, files without symbol analysis, or when strategy is
7
- * 'character' — that path is byte-for-byte the previous behavior.
8
- */
9
- export declare class AstChunker implements IChunker {
10
- private config;
11
- private readonly jsSplitter;
12
- private readonly schemaSplitter;
13
- private readonly chunkSize;
14
- constructor(config: Config);
15
- chunk(code: string, opts: ChunkOpts): Promise<ChunkPiece[]>;
16
- private produce;
17
- /**
18
- * True iff every significant line (blank and comment lines stripped) belongs
19
- * to an `import` / re-export statement and at least one is an import or
20
- * re-export keyword — so the chunk carries no value/type declaration and is
21
- * never a search answer. Any `export const|let|var|function|class|interface|
22
- * enum|namespace|default|declare|async function|type X = …` disqualifies
23
- * exclusion. Pure. Distinguishes `export { … } from` / `export type { … }`
24
- * (re-export → drop) from `export type X = …` (declaration → keep).
25
- */
26
- static isImportOrReexportOnly(content: string): boolean;
27
- /** Character-splitter path for a whole file; line numbers from splitter metadata. */
28
- private splitWholeFile;
29
- private chunkByAst;
30
- /**
31
- * Build disjoint, sorted line intervals from symbol ranges. Overlapping /
32
- * nested ranges merge into the outermost (a merged region covers any symbol
33
- * inside it). Ranges are clamped to the file's line count.
34
- */
35
- private static buildIntervals;
36
- /**
37
- * Emit pieces for a 1-based inclusive line region. Whitespace-only gaps are
38
- * dropped; regions longer than chunkSize are sub-split with the char splitter,
39
- * with sub-piece line numbers offset back to absolute file lines.
40
- */
41
- private regionPieces;
42
- }
@@ -1,158 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AstChunker = void 0;
4
- const textsplitters_1 = require("@langchain/textsplitters");
5
- /**
6
- * Splits TS/JS source into embeddable pieces aligned to top-level declaration
7
- * ranges (AST strategy). Falls back to the character-based recursive splitter
8
- * for schema files, files without symbol analysis, or when strategy is
9
- * 'character' — that path is byte-for-byte the previous behavior.
10
- */
11
- class AstChunker {
12
- config;
13
- jsSplitter;
14
- schemaSplitter;
15
- chunkSize;
16
- constructor(config) {
17
- this.config = config;
18
- this.chunkSize = config.indexer.chunkSize;
19
- this.jsSplitter = textsplitters_1.RecursiveCharacterTextSplitter.fromLanguage('js', {
20
- chunkSize: config.indexer.chunkSize,
21
- chunkOverlap: config.indexer.chunkOverlap,
22
- });
23
- this.schemaSplitter = new textsplitters_1.RecursiveCharacterTextSplitter({
24
- chunkSize: config.indexer.chunkSize,
25
- chunkOverlap: config.indexer.chunkOverlap,
26
- separators: ['\n\n', '\n', ' '],
27
- });
28
- }
29
- async chunk(code, opts) {
30
- const pieces = await this.produce(code, opts);
31
- // Single post-chunk pass: drop pieces whose only significant content is
32
- // import / re-export statements. Both chunk sources — AST "gap" regions and
33
- // the whole-file character path for barrel files — can emit these, so we
34
- // filter once over the final pieces. Dependency edges are untouched (they
35
- // come from analyzeProject → code_dependency, not from chunk text).
36
- return pieces.filter(p => !AstChunker.isImportOrReexportOnly(p.content));
37
- }
38
- async produce(code, opts) {
39
- if (opts.isSchemaFile) {
40
- return this.splitWholeFile(code, this.schemaSplitter);
41
- }
42
- if (opts.strategy === 'character' || opts.symbols.length === 0) {
43
- return this.splitWholeFile(code, this.jsSplitter);
44
- }
45
- return this.chunkByAst(code, opts.symbols);
46
- }
47
- /**
48
- * True iff every significant line (blank and comment lines stripped) belongs
49
- * to an `import` / re-export statement and at least one is an import or
50
- * re-export keyword — so the chunk carries no value/type declaration and is
51
- * never a search answer. Any `export const|let|var|function|class|interface|
52
- * enum|namespace|default|declare|async function|type X = …` disqualifies
53
- * exclusion. Pure. Distinguishes `export { … } from` / `export type { … }`
54
- * (re-export → drop) from `export type X = …` (declaration → keep).
55
- */
56
- static isImportOrReexportOnly(content) {
57
- const lines = content
58
- .replace(/\/\*[\s\S]*?\*\//g, '') // strip block comments
59
- .split('\n')
60
- .map(l => l.trim())
61
- .filter(l => l !== '' && !l.startsWith('//') && !l.startsWith('*'));
62
- if (lines.length === 0)
63
- return false;
64
- const IMPORT = /^import\b/;
65
- const REEXPORT = /^export\s+(\*|type\s+\{|\{)/; // export * | export type { | export {
66
- const DISQUALIFY = /^export\s+(default|async\b|function\b|const\b|let\b|var\b|class\b|abstract\b|interface\b|enum\b|namespace\b|declare\b|type\s+[A-Za-z_$])/;
67
- const CONTINUATION = /^(\}?\s*from\s+['"][^'"]+['"];?|[{}]|(type\s+)?[A-Za-z_$][\w$]*(\s+as\s+[A-Za-z_$][\w$]*)?,?|['"][^'"]*['"];?)$/;
68
- let keywords = 0;
69
- for (const line of lines) {
70
- if (DISQUALIFY.test(line))
71
- return false;
72
- if (IMPORT.test(line) || REEXPORT.test(line)) {
73
- keywords++;
74
- continue;
75
- }
76
- if (CONTINUATION.test(line))
77
- continue;
78
- return false; // unrecognized line → real code, keep the chunk
79
- }
80
- return keywords >= 1;
81
- }
82
- /** Character-splitter path for a whole file; line numbers from splitter metadata. */
83
- async splitWholeFile(code, splitter) {
84
- const docs = await splitter.createDocuments([code]);
85
- return docs.map((d) => {
86
- const from = (d.metadata?.loc?.lines?.from ?? 1);
87
- const to = (d.metadata?.loc?.lines?.to ?? from);
88
- return { content: d.pageContent, startLine: from, endLine: to };
89
- });
90
- }
91
- async chunkByAst(code, symbols) {
92
- const lines = code.split('\n');
93
- const total = lines.length;
94
- const intervals = AstChunker.buildIntervals(symbols, total);
95
- const pieces = [];
96
- let cursor = 1;
97
- for (const iv of intervals) {
98
- if (cursor < iv.start) {
99
- pieces.push(...await this.regionPieces(lines, cursor, iv.start - 1, true));
100
- }
101
- pieces.push(...await this.regionPieces(lines, iv.start, iv.end, false));
102
- cursor = iv.end + 1;
103
- }
104
- if (cursor <= total) {
105
- pieces.push(...await this.regionPieces(lines, cursor, total, true));
106
- }
107
- return pieces;
108
- }
109
- /**
110
- * Build disjoint, sorted line intervals from symbol ranges. Overlapping /
111
- * nested ranges merge into the outermost (a merged region covers any symbol
112
- * inside it). Ranges are clamped to the file's line count.
113
- */
114
- static buildIntervals(symbols, total) {
115
- const raw = symbols
116
- .filter(s => s.line != null && s.line >= 1 && s.line <= total)
117
- .map(s => ({
118
- start: s.line,
119
- end: Math.min(Math.max(s.endLine ?? s.line, s.line), total),
120
- }))
121
- .sort((a, b) => a.start - b.start || b.end - a.end);
122
- const merged = [];
123
- for (const iv of raw) {
124
- const last = merged[merged.length - 1];
125
- if (last && iv.start <= last.end) {
126
- last.end = Math.max(last.end, iv.end);
127
- }
128
- else {
129
- merged.push({ ...iv });
130
- }
131
- }
132
- return merged;
133
- }
134
- /**
135
- * Emit pieces for a 1-based inclusive line region. Whitespace-only gaps are
136
- * dropped; regions longer than chunkSize are sub-split with the char splitter,
137
- * with sub-piece line numbers offset back to absolute file lines.
138
- */
139
- async regionPieces(lines, start, end, isGap) {
140
- const text = lines.slice(start - 1, end).join('\n');
141
- if (isGap && text.trim() === '')
142
- return [];
143
- if (text.length <= this.chunkSize) {
144
- return [{ content: text, startLine: start, endLine: end }];
145
- }
146
- const docs = await this.jsSplitter.createDocuments([text]);
147
- return docs.map((d) => {
148
- const from = (d.metadata?.loc?.lines?.from ?? 1);
149
- const to = (d.metadata?.loc?.lines?.to ?? from);
150
- return {
151
- content: d.pageContent,
152
- startLine: start + from - 1,
153
- endLine: start + to - 1,
154
- };
155
- });
156
- }
157
- }
158
- exports.AstChunker = AstChunker;
@@ -1,206 +0,0 @@
1
- # Web dashboard (`rag-web`)
2
-
3
- A local web UI for configuring and inspecting the RAG server. It provides a
4
- schema-driven config editor, an overview of indexed projects, and an interactive
5
- project-dependency graph — all served from a small built-in HTTP server with no
6
- external framework and no new dependencies (`node:http`).
7
-
8
- The dashboard is a thin presentation layer over primitives that already exist:
9
- config I/O and validation (`src/config-store.ts`), project data
10
- (`SearcherService.listProjects`), and the graph builder (`src/graph.ts`). It adds
11
- no business logic of its own beyond request routing and a password-preservation
12
- merge.
13
-
14
- ---
15
-
16
- ## Running
17
-
18
- ```bash
19
- rag-web # http://127.0.0.1:4319
20
- rag-web --port 8080 # custom port
21
- npm run start:web # dev mode (ts-node, no build)
22
- ```
23
-
24
- If the `rag-web` binary is not on your `PATH`, either run it from the project
25
- (`node dist/app.js web`) or register it once with `npm link`.
26
-
27
- Keep the terminal open — the server runs until you press `Ctrl+C`. The browser
28
- can only reach the dashboard while the process is alive.
29
-
30
- ---
31
-
32
- ## Security model
33
-
34
- - **Binds to `127.0.0.1` only.** The server is never exposed on the network;
35
- it is a single-user local dev tool, so there is no authentication.
36
- - **The DB password is never sent to the browser.** `GET /api/config` returns it
37
- masked as `********`. On save, if the field is still the mask, the stored
38
- password is preserved (`mergeConfigForSave`); a genuinely changed value is
39
- written through.
40
- - **No arbitrary file serving.** Only the known routes below respond; there is no
41
- static file handler, so no path-traversal surface.
42
-
43
- ---
44
-
45
- ## Requirements & failure modes
46
-
47
- - Requires a valid `config.json` — `rag-web` goes through the normal container
48
- bootstrap and `workspaceRoot` guard. To repair a broken config, use
49
- `rag-config edit` (CLI) instead.
50
- - A failed **DB** connection is non-fatal: the config editor still works; the
51
- Projects and Graph tabs show an in-page error.
52
- - A failed **embedding backend** connection only affects model suggestions (see
53
- *Field options*); everything else works.
54
-
55
- ---
56
-
57
- ## UI tabs
58
-
59
- ### Config
60
- A form generated from the config JSON Schema (`buildJsonSchema()` — the same
61
- schema that powers editor autocomplete on `config.json`). Fields are grouped by
62
- section and typed automatically:
63
-
64
- | Schema shape | Rendered as |
65
- |---|---|
66
- | `boolean` | checkbox |
67
- | `enum` (e.g. `logging.level`) | `<select>` |
68
- | `integer` / `number` | number input (with sensible `min`/`max`/`step`) |
69
- | `string` | text input |
70
- | field in `/api/field-options`, `strict` | `<select>` limited to the list |
71
- | field in `/api/field-options`, non-strict | text input + `<datalist>` (combobox) |
72
-
73
- Two affordances make the effective configuration obvious:
74
-
75
- - **Defaults.** Any key left unset shows `↪ using default: <value>` (amber) and
76
- the applied value as the input placeholder. A key that *is* set shows a muted
77
- `default: <value>` for reference. Source: `GET /api/defaults`
78
- (`CONFIG_DEFAULTS`).
79
- - **Field options.** Fields whose valid values are constrained get a dropdown
80
- instead of free text. Source: `GET /api/field-options`. See below.
81
-
82
- Saving validates through `parseConfig` and, on failure, shows the validation
83
- message and highlights the offending fields; on success the config is written
84
- (with the `$schema` reference preserved) and the form reloads.
85
-
86
- ### Projects
87
- Cards for each indexed project from `SearcherService.listProjects()` — name,
88
- chunk count, and any `.ragmeta.json` metadata (stack, description, relations).
89
-
90
- ### Graph
91
- The **full standalone graph page embedded in an `<iframe>`** (`src="/graph"`),
92
- loaded lazily on first open. `GET /graph` serves the exact same HTML as the
93
- generated `project-graph.html` (`buildHtml()` in `src/graph.ts`), so the whole
94
- sidebar comes for free: the node-color legend, the edge-type filter buttons, and
95
- the project toggle list, plus hover highlighting and auto-fit. The dashboard
96
- does **not** re-implement vis-network — reusing one page keeps the two graph
97
- views identical. `GET /api/graph` still returns the raw `{ nodes, edges }` JSON
98
- (`buildGraphData()`) for programmatic use.
99
-
100
- ---
101
-
102
- ## Field options (model dropdowns)
103
-
104
- Some settings have a constrained set of valid values, so the form offers a list
105
- rather than free text. `GET /api/field-options` returns
106
- `{ [dottedKey]: { values: string[], strict: boolean } }`:
107
-
108
- - **`embedding.model`, `reranker.model` — non-strict.** `embedding.model`
109
- suggests `MODEL_REGISTRY` (`src/constants.ts`) entries plus any model the
110
- backend currently serves; `reranker.model` suggests only served models. Both
111
- allow free text — a model outside the registry still works (its dimension is
112
- probed at index time). Served models come from `IEmbedder.listModels()`,
113
- implemented only by `InfinityEmbedder` (2-second-limited `/models` listing);
114
- if the backend is unreachable, or the provider doesn't implement `listModels`
115
- (`openai-compatible`, `voyage`), the served-model list is empty and the field
116
- degrades to registry suggestions only (or plain text for `reranker.model`).
117
-
118
- A strict `<select>` preserves an unknown current value as an extra option, so
119
- saving never silently drops a value that isn't in the list.
120
-
121
- ---
122
-
123
- ## HTTP API
124
-
125
- All routes are served from `127.0.0.1:<port>`.
126
-
127
- | Method | Path | Response |
128
- |---|---|---|
129
- | `GET` | `/` | Dashboard HTML (self-contained) |
130
- | `GET` | `/api/schema` | JSON Schema of the config (`buildJsonSchema()`) |
131
- | `GET` | `/api/defaults` | `CONFIG_DEFAULTS` — dotted key → default value |
132
- | `GET` | `/api/field-options` | dotted key → `{ values, strict }` |
133
- | `GET` | `/api/config` | Current config, `db.password` masked |
134
- | `PUT` | `/api/config` | Save. Body = config JSON. `200 {ok:true, backend:{action,message}}` or `400 {error}`. For `embedding.provider: infinity`, `backend.action` reports whether the Infinity container needed a background recreate (`started`/`already-running`/`not-needed`/`manual-required`/`failed`) — see *Infinity backend recreate* below |
135
- | `GET` | `/api/backend-status` | `{ state, message, startedAt, finishedAt }` — progress of the most recent background Infinity recreate this server process started |
136
- | `GET` | `/api/projects` | `SearcherService.listProjects()` |
137
- | `GET` | `/api/graph` | `{ nodes, edges }` JSON (`buildGraphData()`) |
138
- | `GET` | `/graph` | Full interactive graph page (`buildHtml()`), embedded by the Graph tab's iframe |
139
-
140
- ---
141
-
142
- ## Infinity backend recreate
143
-
144
- When a save's `embedding.provider` is `infinity`, the server computes the
145
- required model set from the **saved** config (freshly re-read from disk, not
146
- the embedder the process started with) and compares it against what the
147
- backend currently serves. If a managed container needs to be started or
148
- recreated to serve the new set, that runs as an in-process background task —
149
- the `PUT` responds immediately (`action: "started"`) without waiting for it.
150
- Poll `GET /api/backend-status` for progress (`state: "recreating"` →
151
- `"ready"`/`"failed"`). Only one `docker compose` run is active at a time; a
152
- save that arrives while a recreate is already in flight for a managed backend
153
- responds `already-running`, and at most one follow-up run applies the latest
154
- such save once the in-flight one finishes. An external (unmanaged) backend
155
- missing a required model never triggers Docker — the response's `message`
156
- carries manual `--model-id`/launch instructions instead.
157
-
158
- ---
159
-
160
- ## Architecture
161
-
162
- | File | Responsibility |
163
- |---|---|
164
- | `src/commands/WebCommand.ts` | CLI entry. `db.initDb()`, starts the server, keeps `execute()` pending until `SIGINT`/`SIGTERM` (mirrors `McpCommand`), then `server.stop()`. Parses `--port` (default `4319`). |
165
- | `src/services/WebServerService.ts` | `WebServer(searcher, config, logger, embedder, deps?)` — `node:http` server, routing, the API handlers, and the background Infinity recreate + `/api/backend-status` state. Exports the pure `mergeConfigForSave()` and the `FieldOption` type. |
166
- | `src/services/infinity-provisioning.ts` | The readiness/recreate primitive the background task delegates to (shared with `InfinityEmbedder` and `rag-setup`) — classification, compose-file rewrite, `docker compose` invocation, readiness polling. |
167
- | `src/web/dashboard.ts` | `renderDashboardHtml()` — the entire page (HTML + CSS + JS) as one self-contained string, mirroring `graph.ts:buildHtml`. |
168
-
169
- Reused primitives (no duplication):
170
-
171
- - `readConfigJson` / `parseConfig` / `writeConfigJson` / `buildJsonSchema` /
172
- `CONFIG_DEFAULTS` — `src/config-store.ts`
173
- - `buildGraphData` — `src/graph.ts`
174
- - `listProjects` — `src/services/SearcherService.ts`
175
- - `MODEL_REGISTRY` — `src/constants.ts`
176
- - `evaluateBackend` / `ensureModelSetServed` / `requiredModelSet` — `src/services/infinity-provisioning.ts`
177
-
178
- Wiring: `app.ts` detects the `web` mode and dispatches
179
- `new WebCommand(container, effectiveArgs)`. The command uses the shared
180
- `Container` services (`db`, `searcher`, `config`, `logger`) — no container
181
- changes were needed.
182
-
183
- ---
184
-
185
- ## Extending
186
-
187
- - **Constrain another field to a list** — add an entry to `fieldOptions()` in
188
- `WebServerService.ts` (`{ values, strict }`). The form renders it automatically
189
- (strict → dropdown, non-strict → combobox).
190
- - **Add a tab / endpoint** — add a route in `WebServer.handle`, a fetch + render
191
- in `dashboard.ts`, and a tab button. Keep handlers thin — delegate to a service.
192
- - **Actions (e.g. trigger reindex)** — deferred by design. This is heavier (a
193
- long-running process with live progress, likely via SSE, plus concurrency
194
- control) and belongs in its own change rather than the read/config-edit shell.
195
-
196
- ---
197
-
198
- ## Keep in sync
199
-
200
- - `CONFIG_DEFAULTS` (`config-store.ts`) is the **single source of truth** for
201
- built-in defaults: `resolvedSchema` in `config.ts` references it in its
202
- `.default(...)` calls, so the runtime default and the dashboard-displayed
203
- default cannot diverge — no manual sync needed. (A unit test still asserts
204
- every key is a valid config key.)
205
- - `MODEL_REGISTRY` (`constants.ts`) is advisory, not exhaustive — a model
206
- outside it still works via the runtime dimension probe at index time.