codesearch-mcp 0.0.17-alpha.1 → 0.1.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 (134) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +95 -6
  3. package/dist/app.js +70 -10
  4. package/dist/src/commands/ConfigCommand.d.ts +1 -0
  5. package/dist/src/commands/ConfigCommand.js +84 -4
  6. package/dist/src/commands/IndexCommand.js +35 -7
  7. package/dist/src/commands/McpCommand.js +9 -4
  8. package/dist/src/commands/{SetupCommand.d.ts → WebCommand.d.ts} +3 -2
  9. package/dist/src/commands/WebCommand.js +56 -0
  10. package/dist/src/config-store.d.ts +85 -4
  11. package/dist/src/config-store.js +157 -32
  12. package/dist/src/config.d.ts +32 -8
  13. package/dist/src/config.js +93 -30
  14. package/dist/src/constants.d.ts +35 -0
  15. package/dist/src/constants.js +57 -4
  16. package/dist/src/container.d.ts +10 -6
  17. package/dist/src/container.js +10 -5
  18. package/dist/src/db/index.d.ts +1 -1
  19. package/dist/src/db/index.js +2 -1
  20. package/dist/src/db/schema.d.ts +111 -0
  21. package/dist/src/db/schema.js +15 -1
  22. package/dist/src/graph.d.ts +60 -0
  23. package/dist/src/graph.js +239 -120
  24. package/dist/src/help.js +20 -5
  25. package/dist/src/interfaces/IAstParser.d.ts +20 -3
  26. package/dist/src/interfaces/IChunker.d.ts +24 -0
  27. package/dist/src/interfaces/IChunker.js +2 -0
  28. package/dist/src/interfaces/IDatabase.d.ts +27 -0
  29. package/dist/src/interfaces/IDatabase.js +2 -0
  30. package/dist/src/interfaces/IEmbedder.d.ts +15 -2
  31. package/dist/src/interfaces/ILogger.d.ts +10 -0
  32. package/dist/src/interfaces/ILogger.js +2 -0
  33. package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
  34. package/dist/src/interfaces/IMcpToolHandler.js +2 -0
  35. package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
  36. package/dist/src/interfaces/IMonorepoResolver.js +2 -0
  37. package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
  38. package/dist/src/interfaces/IProjectDiscovery.js +2 -0
  39. package/dist/src/interfaces/ISearcher.d.ts +37 -0
  40. package/dist/src/interfaces/ISearcher.js +2 -0
  41. package/dist/src/interfaces/index.d.ts +8 -0
  42. package/dist/src/interfaces/index.js +2 -0
  43. package/dist/src/owner-path.d.ts +10 -0
  44. package/dist/src/owner-path.js +27 -0
  45. package/dist/src/project-selector.d.ts +1 -1
  46. package/dist/src/services/AstChunker.d.ts +31 -0
  47. package/dist/src/services/AstChunker.js +114 -0
  48. package/dist/src/services/AstParserService.d.ts +10 -6
  49. package/dist/src/services/AstParserService.js +60 -15
  50. package/dist/src/services/DatabaseService.d.ts +14 -1
  51. package/dist/src/services/DatabaseService.js +70 -25
  52. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  53. package/dist/src/services/EmbedderFactory.js +29 -0
  54. package/dist/src/services/IndexerService.d.ts +22 -12
  55. package/dist/src/services/IndexerService.js +106 -46
  56. package/dist/src/services/InfinityEmbedder.d.ts +79 -0
  57. package/dist/src/services/InfinityEmbedder.js +199 -0
  58. package/dist/src/services/LoggerService.d.ts +2 -1
  59. package/dist/src/services/McpServer.d.ts +16 -7
  60. package/dist/src/services/McpServer.js +54 -367
  61. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
  62. package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
  63. package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
  64. package/dist/src/services/NxMonorepoResolver.js +308 -0
  65. package/dist/src/services/OllamaEmbedder.d.ts +19 -5
  66. package/dist/src/services/OllamaEmbedder.js +91 -36
  67. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  68. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  69. package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
  70. package/dist/src/{env-loader.js → services/ProjectDiscoveryService.js} +16 -5
  71. package/dist/src/services/SearcherService.d.ts +46 -7
  72. package/dist/src/services/SearcherService.js +183 -53
  73. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  74. package/dist/src/services/VoyageEmbedder.js +72 -0
  75. package/dist/src/services/WebServerService.d.ts +77 -0
  76. package/dist/src/services/WebServerService.js +267 -0
  77. package/dist/src/services/http-retry.d.ts +19 -0
  78. package/dist/src/services/http-retry.js +54 -0
  79. package/dist/src/services/reranker.d.ts +15 -0
  80. package/dist/src/services/reranker.js +44 -0
  81. package/dist/src/setup.d.ts +6 -0
  82. package/dist/src/setup.js +249 -31
  83. package/dist/src/tools/FindSymbolTool.d.ts +8 -0
  84. package/dist/src/tools/FindSymbolTool.js +45 -0
  85. package/dist/src/tools/GetCallersTool.d.ts +8 -0
  86. package/dist/src/tools/GetCallersTool.js +42 -0
  87. package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
  88. package/dist/src/tools/GetDependenciesTool.js +38 -0
  89. package/dist/src/tools/GetDependentsTool.d.ts +8 -0
  90. package/dist/src/tools/GetDependentsTool.js +57 -0
  91. package/dist/src/tools/ListFilesTool.d.ts +8 -0
  92. package/dist/src/tools/ListFilesTool.js +39 -0
  93. package/dist/src/tools/ListProjectsTool.d.ts +8 -0
  94. package/dist/src/tools/ListProjectsTool.js +44 -0
  95. package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
  96. package/dist/src/tools/ReadFileRangeTool.js +44 -0
  97. package/dist/src/tools/ReadFileTool.d.ts +8 -0
  98. package/dist/src/tools/ReadFileTool.js +42 -0
  99. package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
  100. package/dist/src/tools/SearchCodebaseTool.js +81 -0
  101. package/dist/src/tools/format-helpers.d.ts +8 -0
  102. package/dist/src/tools/format-helpers.js +58 -0
  103. package/dist/src/tools/index.d.ts +9 -0
  104. package/dist/src/tools/index.js +21 -0
  105. package/dist/src/web/dashboard.d.ts +9 -0
  106. package/dist/src/web/dashboard.js +342 -0
  107. package/docker/docker-compose.infinity-gpu.yml +19 -0
  108. package/docker/docker-compose.yml +25 -0
  109. package/docs/web-dashboard.md +187 -0
  110. package/package.json +25 -3
  111. package/dist/src/ast.d.ts +0 -2
  112. package/dist/src/ast.js +0 -110
  113. package/dist/src/commands/SetupCommand.js +0 -14
  114. package/dist/src/db/data-source.d.ts +0 -2
  115. package/dist/src/db/data-source.js +0 -13
  116. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  117. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  118. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  119. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  120. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  121. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  122. package/dist/src/db/init.d.ts +0 -2
  123. package/dist/src/db/init.js +0 -62
  124. package/dist/src/env-loader.d.ts +0 -1
  125. package/dist/src/indexer.d.ts +0 -17
  126. package/dist/src/indexer.js +0 -242
  127. package/dist/src/mcp.d.ts +0 -1
  128. package/dist/src/mcp.js +0 -403
  129. package/dist/src/ollama.d.ts +0 -2
  130. package/dist/src/ollama.js +0 -6
  131. package/dist/src/searcher.d.ts +0 -74
  132. package/dist/src/searcher.js +0 -446
  133. package/dist/vitest.config.d.ts +0 -2
  134. package/dist/vitest.config.js +0 -9
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VoyageEmbedder = void 0;
4
+ const LoggerService_1 = require("./LoggerService");
5
+ const config_1 = require("../config");
6
+ const reranker_1 = require("./reranker");
7
+ const http_retry_1 = require("./http-retry");
8
+ const constants_1 = require("../constants");
9
+ /**
10
+ * Voyage AI embedder. Voyage's schema differs from OpenAI's: `input` is an array
11
+ * of strings, `input_type` (`query` | `document`) is required for best quality,
12
+ * and the optional `output_dimension` requests Matryoshka truncation. Response
13
+ * is `{ data: [{ embedding, index }] }`.
14
+ */
15
+ class VoyageEmbedder {
16
+ config;
17
+ logger;
18
+ dimensions;
19
+ rerank;
20
+ url;
21
+ doFetch;
22
+ baseDelayMs;
23
+ constructor(config, logger = LoggerService_1.LoggerService.silent(), deps = {}) {
24
+ this.config = config;
25
+ this.logger = logger;
26
+ const base = config.embedding.baseURL.replace(/\/+$/, '');
27
+ this.url = /\/embeddings$/.test(base) ? base : `${base}/embeddings`;
28
+ this.doFetch = deps.fetchImpl ?? fetch;
29
+ this.baseDelayMs = deps.baseDelayMs;
30
+ this.dimensions = config.embedding.dimensions ?? (0, constants_1.registryDimension)(config.embedding.model) ?? 0;
31
+ this.rerank = (0, reranker_1.makeReranker)(config, logger, deps);
32
+ }
33
+ async embed(text, inputType) {
34
+ const [v] = await this.embedBatch([text], inputType);
35
+ return v;
36
+ }
37
+ async embedBatch(texts, inputType = 'document') {
38
+ if (texts.length === 0)
39
+ return [];
40
+ return (0, http_retry_1.withRetry)(async () => {
41
+ const headers = { 'Content-Type': 'application/json' };
42
+ const apiKey = (0, config_1.resolveEnvRef)(this.config.embedding.apiKey);
43
+ if (apiKey)
44
+ headers['Authorization'] = `Bearer ${apiKey}`;
45
+ const body = {
46
+ model: this.config.embedding.model,
47
+ input: texts,
48
+ input_type: inputType,
49
+ };
50
+ if (this.config.embedding.dimensions != null)
51
+ body.output_dimension = this.config.embedding.dimensions;
52
+ const res = await this.doFetch(this.url, {
53
+ method: 'POST',
54
+ headers,
55
+ signal: AbortSignal.timeout(120_000),
56
+ body: JSON.stringify(body),
57
+ });
58
+ if (!res.ok) {
59
+ const errBody = (await res.text().catch(() => res.statusText)).slice(0, 200);
60
+ throw new http_retry_1.HttpError(res.status, `Voyage embeddings request failed: ${res.status} ${errBody}`);
61
+ }
62
+ const data = await res.json();
63
+ if (!Array.isArray(data.data) || data.data.length !== texts.length) {
64
+ throw new Error(`Voyage embeddings response returned ${data.data?.length ?? 0} vectors for ${texts.length} inputs`);
65
+ }
66
+ return [...data.data]
67
+ .sort((a, b) => (a.index ?? 0) - (b.index ?? 0))
68
+ .map(d => d.embedding);
69
+ }, { logger: this.logger, baseDelayMs: this.baseDelayMs, label: 'voyage-embed' });
70
+ }
71
+ }
72
+ exports.VoyageEmbedder = VoyageEmbedder;
@@ -0,0 +1,77 @@
1
+ import type { Config } from '../config';
2
+ import type { ISearcher } from '../interfaces';
3
+ import type { IEmbedder } from '../interfaces';
4
+ import type { ILogger } from '../interfaces';
5
+ import { type JsonConfig } from '../config-store';
6
+ /**
7
+ * Per-field choice lists for the config form. `strict` → render a dropdown that
8
+ * only accepts a listed value (e.g. the embedding model, tied to the vector
9
+ * dimension). Non-strict → a combobox: the values are suggestions but free text
10
+ * is still allowed (e.g. any Ollama model the user has pulled).
11
+ */
12
+ export interface FieldOption {
13
+ values: string[];
14
+ strict: boolean;
15
+ }
16
+ /** Placeholder shown to the browser instead of a real secret. */
17
+ export declare const PASSWORD_MASK = "********";
18
+ /**
19
+ * Merge a config submitted from the browser with the stored one. Any secret
20
+ * field (DB password, embedding/reranker API keys) still holding the mask means
21
+ * the user did not change it — restore the stored value rather than overwrite it
22
+ * with the placeholder. `env:` references are not masked and round-trip as-is.
23
+ * Pure — no I/O. Exported for unit testing.
24
+ */
25
+ export declare function mergeConfigForSave(incoming: JsonConfig, existing: JsonConfig | null): JsonConfig;
26
+ /**
27
+ * Return a copy of the config with raw secrets (DB password, raw API keys)
28
+ * replaced by the mask, so real secrets never reach the browser. `env:`
29
+ * references are left verbatim — they are not secret and the dashboard shows
30
+ * them as "set via <VAR>". The masked value round-trips through
31
+ * `mergeConfigForSave`. Pure — no I/O. Exported for unit testing.
32
+ */
33
+ export declare function maskConfig(cfg: JsonConfig): JsonConfig;
34
+ /**
35
+ * Local HTTP dashboard. Serves a schema-driven config editor, a project
36
+ * overview, and the dependency graph. Bind to 127.0.0.1 only — no auth.
37
+ */
38
+ export declare class WebServer {
39
+ private readonly searcher;
40
+ private readonly config;
41
+ private readonly logger;
42
+ private readonly embedder;
43
+ private server?;
44
+ constructor(searcher: ISearcher, config: Config, logger: ILogger, embedder: IEmbedder);
45
+ start(port: number, host: string): Promise<void>;
46
+ stop(): Promise<void>;
47
+ /** The bound address once started (e.g. `.port` after listen(0)); null otherwise. */
48
+ address(): {
49
+ port: number;
50
+ } | null;
51
+ private handle;
52
+ private maskedConfig;
53
+ private saveConfig;
54
+ /**
55
+ * Choice lists for model fields. Both are non-strict comboboxes (free text
56
+ * allowed): the embedding model suggests the known registry models plus any
57
+ * locally-installed models; the reranker model suggests installed models.
58
+ * Backends beyond the registry (e.g. arbitrary HuggingFace models on Infinity)
59
+ * still work — their dimension is probed at index time.
60
+ */
61
+ private fieldOptions;
62
+ private installedModels;
63
+ /** Tabbed graph views (Workspace + one per nested Nx repo). */
64
+ private graphViews;
65
+ /** `/api/graph` — the Workspace view's raw `{ nodes, edges }` (back-compat). */
66
+ private graphData;
67
+ /**
68
+ * The full interactive graph page — the same HTML as the standalone
69
+ * `project-graph.html` (inner tabs, filters, legend, project toggles). The
70
+ * dashboard's Graph tab embeds this via an iframe rather than re-implementing
71
+ * vis-network.
72
+ */
73
+ private graphPage;
74
+ private readBody;
75
+ private json;
76
+ private html;
77
+ }
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.WebServer = exports.PASSWORD_MASK = void 0;
37
+ exports.mergeConfigForSave = mergeConfigForSave;
38
+ exports.maskConfig = maskConfig;
39
+ const http = __importStar(require("http"));
40
+ const path = __importStar(require("path"));
41
+ const config_store_1 = require("../config-store");
42
+ const constants_1 = require("../constants");
43
+ const graph_1 = require("../graph");
44
+ const dashboard_1 = require("../web/dashboard");
45
+ /** Placeholder shown to the browser instead of a real secret. */
46
+ exports.PASSWORD_MASK = '********';
47
+ const MAX_BODY = 1_000_000; // 1 MB — config payloads are tiny
48
+ /** A secret worth masking: a non-empty string that is not an `env:` reference. */
49
+ function isRawSecret(v) {
50
+ return typeof v === 'string' && v !== '' && !v.startsWith('env:');
51
+ }
52
+ /**
53
+ * Merge a config submitted from the browser with the stored one. Any secret
54
+ * field (DB password, embedding/reranker API keys) still holding the mask means
55
+ * the user did not change it — restore the stored value rather than overwrite it
56
+ * with the placeholder. `env:` references are not masked and round-trip as-is.
57
+ * Pure — no I/O. Exported for unit testing.
58
+ */
59
+ function mergeConfigForSave(incoming, existing) {
60
+ const merged = JSON.parse(JSON.stringify(incoming ?? {}));
61
+ const restore = (grp, field) => {
62
+ const g = merged[grp];
63
+ if (g && g[field] === exports.PASSWORD_MASK) {
64
+ const prev = existing?.[grp]?.[field];
65
+ if (prev !== undefined)
66
+ g[field] = prev;
67
+ else
68
+ delete g[field];
69
+ }
70
+ };
71
+ restore('db', 'password');
72
+ restore('embedding', 'apiKey');
73
+ restore('reranker', 'apiKey');
74
+ return merged;
75
+ }
76
+ /**
77
+ * Return a copy of the config with raw secrets (DB password, raw API keys)
78
+ * replaced by the mask, so real secrets never reach the browser. `env:`
79
+ * references are left verbatim — they are not secret and the dashboard shows
80
+ * them as "set via <VAR>". The masked value round-trips through
81
+ * `mergeConfigForSave`. Pure — no I/O. Exported for unit testing.
82
+ */
83
+ function maskConfig(cfg) {
84
+ const clone = JSON.parse(JSON.stringify(cfg ?? {}));
85
+ if (clone.db?.password)
86
+ clone.db.password = exports.PASSWORD_MASK;
87
+ if (isRawSecret(clone.embedding?.apiKey))
88
+ clone.embedding.apiKey = exports.PASSWORD_MASK;
89
+ if (isRawSecret(clone.reranker?.apiKey))
90
+ clone.reranker.apiKey = exports.PASSWORD_MASK;
91
+ return clone;
92
+ }
93
+ /**
94
+ * Local HTTP dashboard. Serves a schema-driven config editor, a project
95
+ * overview, and the dependency graph. Bind to 127.0.0.1 only — no auth.
96
+ */
97
+ class WebServer {
98
+ searcher;
99
+ config;
100
+ logger;
101
+ embedder;
102
+ server;
103
+ constructor(searcher, config, logger, embedder) {
104
+ this.searcher = searcher;
105
+ this.config = config;
106
+ this.logger = logger;
107
+ this.embedder = embedder;
108
+ }
109
+ async start(port, host) {
110
+ this.server = http.createServer((req, res) => {
111
+ this.handle(req, res).catch(err => {
112
+ this.logger.error('WebServer: request failed', { reason: err instanceof Error ? err.message : String(err) });
113
+ if (!res.headersSent) {
114
+ res.writeHead(500);
115
+ res.end('Internal error');
116
+ }
117
+ });
118
+ });
119
+ await new Promise((resolve, reject) => {
120
+ this.server.once('error', reject);
121
+ this.server.listen(port, host, () => resolve());
122
+ });
123
+ this.logger.info('WebServer: listening', { host, port });
124
+ }
125
+ async stop() {
126
+ if (!this.server)
127
+ return;
128
+ await new Promise(resolve => this.server.close(() => resolve()));
129
+ this.server = undefined;
130
+ }
131
+ /** The bound address once started (e.g. `.port` after listen(0)); null otherwise. */
132
+ address() {
133
+ const a = this.server?.address();
134
+ return a && typeof a === 'object' ? { port: a.port } : null;
135
+ }
136
+ async handle(req, res) {
137
+ const url = new URL(req.url ?? '/', 'http://localhost');
138
+ const p = url.pathname;
139
+ const m = req.method ?? 'GET';
140
+ if (m === 'GET' && p === '/')
141
+ return this.html(res, (0, dashboard_1.renderDashboardHtml)());
142
+ if (m === 'GET' && p === '/api/schema')
143
+ return this.json(res, 200, (0, config_store_1.buildJsonSchema)());
144
+ if (m === 'GET' && p === '/api/defaults')
145
+ return this.json(res, 200, config_store_1.CONFIG_DEFAULTS);
146
+ if (m === 'GET' && p === '/api/field-options')
147
+ return this.json(res, 200, await this.fieldOptions());
148
+ if (m === 'GET' && p === '/api/config')
149
+ return this.json(res, 200, this.maskedConfig());
150
+ if (m === 'PUT' && p === '/api/config')
151
+ return this.saveConfig(req, res);
152
+ if (m === 'GET' && p === '/api/projects')
153
+ return this.json(res, 200, await this.searcher.listProjects());
154
+ if (m === 'GET' && p === '/api/graph')
155
+ return this.json(res, 200, await this.graphData());
156
+ if (m === 'GET' && p === '/graph')
157
+ return this.html(res, await this.graphPage());
158
+ this.json(res, 404, { error: 'Not found' });
159
+ }
160
+ maskedConfig() {
161
+ return maskConfig((0, config_store_1.readConfigJson)() ?? {});
162
+ }
163
+ async saveConfig(req, res) {
164
+ let body;
165
+ try {
166
+ body = await this.readBody(req);
167
+ }
168
+ catch (e) {
169
+ // Distinguish the size limit from other stream errors.
170
+ return this.json(res, 413, { error: e instanceof Error ? e.message : 'Request body too large' });
171
+ }
172
+ let incoming;
173
+ try {
174
+ incoming = JSON.parse(body);
175
+ }
176
+ catch {
177
+ return this.json(res, 400, { error: 'Invalid JSON body' });
178
+ }
179
+ const merged = mergeConfigForSave(incoming, (0, config_store_1.readConfigJson)());
180
+ let validated;
181
+ try {
182
+ validated = (0, config_store_1.parseConfig)(merged);
183
+ }
184
+ catch (e) {
185
+ return this.json(res, 400, { error: e instanceof Error ? e.message : String(e) });
186
+ }
187
+ (0, config_store_1.writeConfigJson)(validated);
188
+ this.logger.info('WebServer: config updated');
189
+ this.json(res, 200, { ok: true });
190
+ }
191
+ /**
192
+ * Choice lists for model fields. Both are non-strict comboboxes (free text
193
+ * allowed): the embedding model suggests the known registry models plus any
194
+ * locally-installed models; the reranker model suggests installed models.
195
+ * Backends beyond the registry (e.g. arbitrary HuggingFace models on Infinity)
196
+ * still work — their dimension is probed at index time.
197
+ */
198
+ async fieldOptions() {
199
+ const installed = await this.installedModels();
200
+ const registryModels = Object.keys(constants_1.MODEL_REGISTRY);
201
+ return {
202
+ 'embedding.model': { values: [...new Set([...registryModels, ...installed])], strict: false },
203
+ 'reranker.model': { values: installed, strict: false },
204
+ };
205
+ }
206
+ async installedModels() {
207
+ try {
208
+ return (await this.embedder.listModels?.()) ?? [];
209
+ }
210
+ catch (err) {
211
+ this.logger.warn('WebServer: could not list models', { reason: err instanceof Error ? err.message : String(err) });
212
+ return [];
213
+ }
214
+ }
215
+ /** Tabbed graph views (Workspace + one per nested Nx repo). */
216
+ async graphViews() {
217
+ const projects = (await this.searcher.listProjects()).map(p => ({
218
+ name: p.serviceName,
219
+ // Real project root (resolved via discovery — handles nested-monorepo
220
+ // names). Fall back to the flat assumption if listProjects omits it.
221
+ path: p.path ?? path.join(this.config.workspaceRoot, p.serviceName),
222
+ }));
223
+ const projectEdges = await this.searcher.getProjectEdges();
224
+ return (0, graph_1.buildGraphViews)(projects, projectEdges, this.config.workspaceRoot);
225
+ }
226
+ /** `/api/graph` — the Workspace view's raw `{ nodes, edges }` (back-compat). */
227
+ async graphData() {
228
+ const views = await this.graphViews();
229
+ return { nodes: views[0].nodes, edges: views[0].edges };
230
+ }
231
+ /**
232
+ * The full interactive graph page — the same HTML as the standalone
233
+ * `project-graph.html` (inner tabs, filters, legend, project toggles). The
234
+ * dashboard's Graph tab embeds this via an iframe rather than re-implementing
235
+ * vis-network.
236
+ */
237
+ async graphPage() {
238
+ return (0, graph_1.buildHtml)(await this.graphViews());
239
+ }
240
+ readBody(req) {
241
+ return new Promise((resolve, reject) => {
242
+ let data = '';
243
+ let tooLarge = false;
244
+ req.on('data', chunk => {
245
+ if (tooLarge)
246
+ return; // stop growing `data`; keep the socket alive so the 413 flushes
247
+ data += chunk;
248
+ if (data.length > MAX_BODY) {
249
+ tooLarge = true;
250
+ reject(new Error('Request body too large (max 1 MB)'));
251
+ }
252
+ });
253
+ req.on('end', () => resolve(data));
254
+ req.on('error', reject);
255
+ });
256
+ }
257
+ json(res, status, body) {
258
+ const s = JSON.stringify(body);
259
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(s) });
260
+ res.end(s);
261
+ }
262
+ html(res, body) {
263
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
264
+ res.end(body);
265
+ }
266
+ }
267
+ exports.WebServer = WebServer;
@@ -0,0 +1,19 @@
1
+ import type { ILogger } from '../interfaces/ILogger';
2
+ export declare class HttpError extends Error {
3
+ status: number;
4
+ constructor(status: number, message: string);
5
+ }
6
+ export interface RetryOpts {
7
+ /** Total attempts including the first (default 4). */
8
+ attempts?: number;
9
+ /** Base backoff in ms; grows exponentially (default 500). Set 0 in tests. */
10
+ baseDelayMs?: number;
11
+ logger?: ILogger;
12
+ label?: string;
13
+ }
14
+ /**
15
+ * Run `fn` with bounded exponential backoff on HTTP 429/5xx and network errors.
16
+ * Re-throws the last error once attempts are exhausted or the error is not
17
+ * transient.
18
+ */
19
+ export declare function withRetry<T>(fn: () => Promise<T>, opts?: RetryOpts): Promise<T>;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpError = void 0;
4
+ exports.withRetry = withRetry;
5
+ class HttpError extends Error {
6
+ status;
7
+ constructor(status, message) {
8
+ super(message);
9
+ this.status = status;
10
+ this.name = 'HttpError';
11
+ }
12
+ }
13
+ exports.HttpError = HttpError;
14
+ function sleep(ms) {
15
+ return ms > 0 ? new Promise(r => setTimeout(r, ms)) : Promise.resolve();
16
+ }
17
+ /** True for transient failures worth retrying (rate limit / server error / network / timeout). */
18
+ function isRetryable(err) {
19
+ if (err instanceof HttpError)
20
+ return err.status === 429 || (err.status >= 500 && err.status <= 599);
21
+ // A request timeout (AbortSignal.timeout) rejects with a DOMException named
22
+ // 'TimeoutError' — the most common transient cloud failure, so retry it.
23
+ if (err != null && typeof err === 'object' && err.name === 'TimeoutError')
24
+ return true;
25
+ // Network-level errors (fetch throws) are transient too.
26
+ return err instanceof TypeError;
27
+ }
28
+ /**
29
+ * Run `fn` with bounded exponential backoff on HTTP 429/5xx and network errors.
30
+ * Re-throws the last error once attempts are exhausted or the error is not
31
+ * transient.
32
+ */
33
+ async function withRetry(fn, opts = {}) {
34
+ const attempts = opts.attempts ?? 4;
35
+ const base = opts.baseDelayMs ?? 500;
36
+ let lastErr;
37
+ for (let attempt = 1; attempt <= attempts; attempt++) {
38
+ try {
39
+ return await fn();
40
+ }
41
+ catch (err) {
42
+ lastErr = err;
43
+ if (attempt >= attempts || !isRetryable(err))
44
+ break;
45
+ const delay = base * Math.pow(2, attempt - 1);
46
+ opts.logger?.warn('http-retry: transient failure, retrying', {
47
+ label: opts.label, attempt, attempts, delayMs: delay,
48
+ reason: err instanceof Error ? err.message : String(err),
49
+ });
50
+ await sleep(delay);
51
+ }
52
+ }
53
+ throw lastErr;
54
+ }
@@ -0,0 +1,15 @@
1
+ import type { Config } from '../config';
2
+ import type { ILogger } from '../interfaces/ILogger';
3
+ /**
4
+ * Build a rerank function from `config.reranker`, or return `undefined` when
5
+ * reranking is disabled / unconfigured. Targets an OpenAI-compatible `/rerank`
6
+ * endpoint returning `{ results: [{ index }] }` (Infinity, TEI, vLLM,
7
+ * Jina/Cohere-shape). Ollama has NO rerank API, so it is not a valid backend
8
+ * here — the embedding provider is independent of the rerank server
9
+ * (`reranker.baseURL`), so Ollama embeddings + an Infinity reranker is fine.
10
+ * Returns original indices into `documents`, best-first.
11
+ */
12
+ export declare function makeReranker(config: Config, logger: ILogger, deps?: {
13
+ fetchImpl?: typeof fetch;
14
+ baseDelayMs?: number;
15
+ }): ((query: string, documents: string[], topN: number) => Promise<number[]>) | undefined;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeReranker = makeReranker;
4
+ const config_1 = require("../config");
5
+ const http_retry_1 = require("./http-retry");
6
+ /**
7
+ * Build a rerank function from `config.reranker`, or return `undefined` when
8
+ * reranking is disabled / unconfigured. Targets an OpenAI-compatible `/rerank`
9
+ * endpoint returning `{ results: [{ index }] }` (Infinity, TEI, vLLM,
10
+ * Jina/Cohere-shape). Ollama has NO rerank API, so it is not a valid backend
11
+ * here — the embedding provider is independent of the rerank server
12
+ * (`reranker.baseURL`), so Ollama embeddings + an Infinity reranker is fine.
13
+ * Returns original indices into `documents`, best-first.
14
+ */
15
+ function makeReranker(config, logger, deps = {}) {
16
+ const r = config.reranker;
17
+ if (!r.enabled)
18
+ return undefined;
19
+ if (!r.model) {
20
+ logger.warn('reranker: enabled=true but reranker.model is not set — reranking disabled');
21
+ return undefined;
22
+ }
23
+ const doFetch = deps.fetchImpl ?? fetch;
24
+ return async (query, documents, topN) => {
25
+ return (0, http_retry_1.withRetry)(async () => {
26
+ const headers = { 'Content-Type': 'application/json' };
27
+ const apiKey = (0, config_1.resolveEnvRef)(r.apiKey);
28
+ if (apiKey)
29
+ headers['Authorization'] = `Bearer ${apiKey}`;
30
+ const res = await doFetch(r.url, {
31
+ method: 'POST',
32
+ headers,
33
+ signal: AbortSignal.timeout(30_000),
34
+ body: JSON.stringify({ model: r.model, query, documents, top_n: topN, truncate: true }),
35
+ });
36
+ if (!res.ok) {
37
+ const body = (await res.text().catch(() => res.statusText)).slice(0, 200);
38
+ throw new http_retry_1.HttpError(res.status, `rerank failed: ${res.status} ${body}`);
39
+ }
40
+ const data = await res.json();
41
+ return data.results.map(x => x.index);
42
+ }, { logger, baseDelayMs: deps.baseDelayMs, label: 'rerank' });
43
+ };
44
+ }
@@ -1,2 +1,8 @@
1
1
  import type { LoggerService } from './services/LoggerService';
2
+ /**
3
+ * Default answer for the "start Docker containers?" prompt: no only when both
4
+ * the database and the embedding backend are already reachable. Pure — exported
5
+ * for unit testing.
6
+ */
7
+ export declare function dockerPromptDefault(dbReachable: boolean, embReachable: boolean): 'y' | 'n';
2
8
  export declare function runSetup(logger?: LoggerService): Promise<void>;