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.
- package/CHANGELOG.md +48 -0
- package/README.md +95 -6
- package/dist/app.js +70 -10
- package/dist/src/commands/ConfigCommand.d.ts +1 -0
- package/dist/src/commands/ConfigCommand.js +84 -4
- package/dist/src/commands/IndexCommand.js +35 -7
- package/dist/src/commands/McpCommand.js +9 -4
- package/dist/src/commands/{SetupCommand.d.ts → WebCommand.d.ts} +3 -2
- package/dist/src/commands/WebCommand.js +56 -0
- package/dist/src/config-store.d.ts +85 -4
- package/dist/src/config-store.js +157 -32
- package/dist/src/config.d.ts +32 -8
- package/dist/src/config.js +93 -30
- package/dist/src/constants.d.ts +35 -0
- package/dist/src/constants.js +57 -4
- package/dist/src/container.d.ts +10 -6
- package/dist/src/container.js +10 -5
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +111 -0
- package/dist/src/db/schema.js +15 -1
- package/dist/src/graph.d.ts +60 -0
- package/dist/src/graph.js +239 -120
- package/dist/src/help.js +20 -5
- package/dist/src/interfaces/IAstParser.d.ts +20 -3
- package/dist/src/interfaces/IChunker.d.ts +24 -0
- package/dist/src/interfaces/IChunker.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +27 -0
- package/dist/src/interfaces/IDatabase.js +2 -0
- package/dist/src/interfaces/IEmbedder.d.ts +15 -2
- package/dist/src/interfaces/ILogger.d.ts +10 -0
- package/dist/src/interfaces/ILogger.js +2 -0
- package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
- package/dist/src/interfaces/IMcpToolHandler.js +2 -0
- package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
- package/dist/src/interfaces/IMonorepoResolver.js +2 -0
- package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
- package/dist/src/interfaces/IProjectDiscovery.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +37 -0
- package/dist/src/interfaces/ISearcher.js +2 -0
- package/dist/src/interfaces/index.d.ts +8 -0
- package/dist/src/interfaces/index.js +2 -0
- package/dist/src/owner-path.d.ts +10 -0
- package/dist/src/owner-path.js +27 -0
- package/dist/src/project-selector.d.ts +1 -1
- package/dist/src/services/AstChunker.d.ts +31 -0
- package/dist/src/services/AstChunker.js +114 -0
- package/dist/src/services/AstParserService.d.ts +10 -6
- package/dist/src/services/AstParserService.js +60 -15
- package/dist/src/services/DatabaseService.d.ts +14 -1
- package/dist/src/services/DatabaseService.js +70 -25
- package/dist/src/services/EmbedderFactory.d.ts +9 -0
- package/dist/src/services/EmbedderFactory.js +29 -0
- package/dist/src/services/IndexerService.d.ts +22 -12
- package/dist/src/services/IndexerService.js +106 -46
- package/dist/src/services/InfinityEmbedder.d.ts +79 -0
- package/dist/src/services/InfinityEmbedder.js +199 -0
- package/dist/src/services/LoggerService.d.ts +2 -1
- package/dist/src/services/McpServer.d.ts +16 -7
- package/dist/src/services/McpServer.js +54 -367
- package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
- package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
- package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
- package/dist/src/services/NxMonorepoResolver.js +308 -0
- package/dist/src/services/OllamaEmbedder.d.ts +19 -5
- package/dist/src/services/OllamaEmbedder.js +91 -36
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
- package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
- package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
- package/dist/src/{env-loader.js → services/ProjectDiscoveryService.js} +16 -5
- package/dist/src/services/SearcherService.d.ts +46 -7
- package/dist/src/services/SearcherService.js +183 -53
- package/dist/src/services/VoyageEmbedder.d.ts +22 -0
- package/dist/src/services/VoyageEmbedder.js +72 -0
- package/dist/src/services/WebServerService.d.ts +77 -0
- package/dist/src/services/WebServerService.js +267 -0
- package/dist/src/services/http-retry.d.ts +19 -0
- package/dist/src/services/http-retry.js +54 -0
- package/dist/src/services/reranker.d.ts +15 -0
- package/dist/src/services/reranker.js +44 -0
- package/dist/src/setup.d.ts +6 -0
- package/dist/src/setup.js +249 -31
- package/dist/src/tools/FindSymbolTool.d.ts +8 -0
- package/dist/src/tools/FindSymbolTool.js +45 -0
- package/dist/src/tools/GetCallersTool.d.ts +8 -0
- package/dist/src/tools/GetCallersTool.js +42 -0
- package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
- package/dist/src/tools/GetDependenciesTool.js +38 -0
- package/dist/src/tools/GetDependentsTool.d.ts +8 -0
- package/dist/src/tools/GetDependentsTool.js +57 -0
- package/dist/src/tools/ListFilesTool.d.ts +8 -0
- package/dist/src/tools/ListFilesTool.js +39 -0
- package/dist/src/tools/ListProjectsTool.d.ts +8 -0
- package/dist/src/tools/ListProjectsTool.js +44 -0
- package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
- package/dist/src/tools/ReadFileRangeTool.js +44 -0
- package/dist/src/tools/ReadFileTool.d.ts +8 -0
- package/dist/src/tools/ReadFileTool.js +42 -0
- package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
- package/dist/src/tools/SearchCodebaseTool.js +81 -0
- package/dist/src/tools/format-helpers.d.ts +8 -0
- package/dist/src/tools/format-helpers.js +58 -0
- package/dist/src/tools/index.d.ts +9 -0
- package/dist/src/tools/index.js +21 -0
- package/dist/src/web/dashboard.d.ts +9 -0
- package/dist/src/web/dashboard.js +342 -0
- package/docker/docker-compose.infinity-gpu.yml +19 -0
- package/docker/docker-compose.yml +25 -0
- package/docs/web-dashboard.md +187 -0
- package/package.json +25 -3
- package/dist/src/ast.d.ts +0 -2
- package/dist/src/ast.js +0 -110
- package/dist/src/commands/SetupCommand.js +0 -14
- package/dist/src/db/data-source.d.ts +0 -2
- package/dist/src/db/data-source.js +0 -13
- package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
- package/dist/src/db/entities/code-chunk.entity.js +0 -54
- package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
- package/dist/src/db/entities/code-dependency.entity.js +0 -54
- package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
- package/dist/src/db/entities/code-symbol.entity.js +0 -59
- package/dist/src/db/init.d.ts +0 -2
- package/dist/src/db/init.js +0 -62
- package/dist/src/env-loader.d.ts +0 -1
- package/dist/src/indexer.d.ts +0 -17
- package/dist/src/indexer.js +0 -242
- package/dist/src/mcp.d.ts +0 -1
- package/dist/src/mcp.js +0 -403
- package/dist/src/ollama.d.ts +0 -2
- package/dist/src/ollama.js +0 -6
- package/dist/src/searcher.d.ts +0 -74
- package/dist/src/searcher.js +0 -446
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -9
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderDashboardHtml = renderDashboardHtml;
|
|
4
|
+
/**
|
|
5
|
+
* Renders the RAG dashboard as a single self-contained HTML page (inline CSS +
|
|
6
|
+
* JS), mirroring the approach in graph.ts. The page is schema-driven: it fetches
|
|
7
|
+
* /api/schema (from buildJsonSchema — the same source as Step 1's editor support)
|
|
8
|
+
* and /api/config, then renders a typed form. The Graph tab embeds the full
|
|
9
|
+
* standalone graph page (GET /graph — same buildHtml as project-graph.html) in
|
|
10
|
+
* an iframe rather than re-implementing the network here.
|
|
11
|
+
*/
|
|
12
|
+
function renderDashboardHtml() {
|
|
13
|
+
return `<!DOCTYPE html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="UTF-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18
|
+
<title>RAG Dashboard</title>
|
|
19
|
+
<style>
|
|
20
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
21
|
+
body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #eee; height: 100vh; display: flex; flex-direction: column; }
|
|
22
|
+
#header { padding: 12px 20px; background: #16213e; border-bottom: 1px solid #0f3460; display: flex; align-items: center; gap: 24px; flex-shrink: 0; }
|
|
23
|
+
#header h1 { font-size: 16px; font-weight: 600; }
|
|
24
|
+
#tabs { display: flex; gap: 6px; }
|
|
25
|
+
.tab { padding: 6px 14px; border-radius: 8px; border: 1px solid transparent; background: transparent; color: #88aabb; font-size: 13px; cursor: pointer; }
|
|
26
|
+
.tab.active { background: #4A90D9; color: #fff; }
|
|
27
|
+
#main { flex: 1; overflow: auto; padding: 24px; }
|
|
28
|
+
.panel { display: none; max-width: 900px; margin: 0 auto; }
|
|
29
|
+
.panel.active { display: block; }
|
|
30
|
+
/* Graph panel uses the full available width (not the 900px form column). */
|
|
31
|
+
#graph-panel { max-width: none; }
|
|
32
|
+
|
|
33
|
+
/* Config form */
|
|
34
|
+
.section { background: #16213e; border: 1px solid #0f3460; border-radius: 10px; padding: 16px 18px; margin-bottom: 16px; }
|
|
35
|
+
.section h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #7aa; margin-bottom: 14px; }
|
|
36
|
+
.field { display: grid; grid-template-columns: 220px 1fr; align-items: center; gap: 14px; margin-bottom: 12px; }
|
|
37
|
+
.field:last-child { margin-bottom: 0; }
|
|
38
|
+
.field label { font-size: 13px; color: #cde; }
|
|
39
|
+
.field .desc { display: block; font-size: 11px; color: #667; margin-top: 2px; }
|
|
40
|
+
.field .defval { display: block; font-size: 11px; color: #667; margin-top: 2px; }
|
|
41
|
+
.field .defval.using { color: #c9a24a; } /* amber — this field is currently using the default */
|
|
42
|
+
.field input[type=text], .field input[type=password], .field input[type=number], .field select {
|
|
43
|
+
width: 100%; padding: 7px 9px; border-radius: 6px; border: 1px solid #2a3f6a;
|
|
44
|
+
background: #0f1a33; color: #eee; font-size: 13px; font-family: inherit;
|
|
45
|
+
}
|
|
46
|
+
.field input:focus, .field select:focus { outline: none; border-color: #4A90D9; }
|
|
47
|
+
.field.invalid input, .field.invalid select { border-color: #e0555a; }
|
|
48
|
+
.field input[type=checkbox] { width: 16px; height: 16px; accent-color: #4A90D9; }
|
|
49
|
+
#actions { display: flex; align-items: center; gap: 14px; margin-top: 4px; }
|
|
50
|
+
button.primary { padding: 9px 20px; border-radius: 8px; border: none; background: #4A90D9; color: #fff; font-size: 13px; cursor: pointer; }
|
|
51
|
+
button.primary:hover { background: #5a9fe0; }
|
|
52
|
+
button.primary:disabled { opacity: .5; cursor: default; }
|
|
53
|
+
#banner { display: none; padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-bottom: 16px; white-space: pre-wrap; }
|
|
54
|
+
#banner.err { display: block; background: #3a1a1f; border: 1px solid #e0555a; color: #f0b3b6; }
|
|
55
|
+
#banner.ok { display: block; background: #16332a; border: 1px solid #3fae7a; color: #a8e6c4; }
|
|
56
|
+
|
|
57
|
+
/* Projects */
|
|
58
|
+
#cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
|
|
59
|
+
.card { background: #16213e; border: 1px solid #0f3460; border-radius: 10px; padding: 14px 16px; }
|
|
60
|
+
.card h3 { font-size: 14px; margin-bottom: 4px; }
|
|
61
|
+
.card .chunks { font-size: 11px; color: #4A90D9; margin-bottom: 8px; }
|
|
62
|
+
.card .stack { font-size: 12px; color: #8ab; margin-bottom: 6px; }
|
|
63
|
+
.card .descr { font-size: 12px; color: #99a; line-height: 1.4; }
|
|
64
|
+
.card .rel { font-size: 11px; color: #778; margin-top: 8px; }
|
|
65
|
+
.empty { color: #667; font-size: 13px; padding: 20px 0; }
|
|
66
|
+
|
|
67
|
+
/* Graph — embeds the full standalone graph page (/graph) in an iframe, so
|
|
68
|
+
the filters/legend/project-list sidebar comes for free. A definite height
|
|
69
|
+
is required; a flex/percentage height would collapse the iframe. */
|
|
70
|
+
#graph-frame { width: 100%; height: calc(100vh - 130px); border: 1px solid #0f3460; border-radius: 10px; background: #131a30; display: block; }
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<div id="header">
|
|
75
|
+
<h1>RAG Dashboard</h1>
|
|
76
|
+
<div id="tabs">
|
|
77
|
+
<button class="tab active" data-tab="config">Config</button>
|
|
78
|
+
<button class="tab" data-tab="projects">Projects</button>
|
|
79
|
+
<button class="tab" data-tab="graph">Graph</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div id="main">
|
|
83
|
+
<div class="panel active" id="config-panel">
|
|
84
|
+
<div id="banner"></div>
|
|
85
|
+
<form id="config-form"></form>
|
|
86
|
+
<div id="actions">
|
|
87
|
+
<button class="primary" id="save-btn" type="button">Save</button>
|
|
88
|
+
<span id="save-hint" style="font-size:12px;color:#667"></span>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="panel" id="projects-panel">
|
|
92
|
+
<div id="cards"></div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="panel" id="graph-panel">
|
|
95
|
+
<iframe id="graph-frame" title="Project dependency graph"></iframe>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<script>
|
|
100
|
+
const MASK = '********';
|
|
101
|
+
let schema = null;
|
|
102
|
+
let config = null;
|
|
103
|
+
let defaults = {};
|
|
104
|
+
let fieldOptions = {};
|
|
105
|
+
let graphLoaded = false;
|
|
106
|
+
|
|
107
|
+
// ---- tabs ----
|
|
108
|
+
document.querySelectorAll('.tab').forEach(btn => {
|
|
109
|
+
btn.addEventListener('click', () => {
|
|
110
|
+
document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'));
|
|
111
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
112
|
+
btn.classList.add('active');
|
|
113
|
+
const tab = btn.dataset.tab;
|
|
114
|
+
document.getElementById(tab + '-panel').classList.add('active');
|
|
115
|
+
if (tab === 'projects') loadProjects();
|
|
116
|
+
if (tab === 'graph') loadGraph();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---- config form ----
|
|
121
|
+
function nested(obj, path) {
|
|
122
|
+
return path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function fmtDefault(v) {
|
|
126
|
+
if (v === '') return "'' (empty)";
|
|
127
|
+
return String(v);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// A dropdown limited to the given values. Preserves an unknown current value
|
|
131
|
+
// as an extra option so saving never silently drops it.
|
|
132
|
+
function selectEl(key, values, value, def) {
|
|
133
|
+
const sel = document.createElement('select');
|
|
134
|
+
sel.dataset.key = key; sel.dataset.type = 'string';
|
|
135
|
+
const blank = document.createElement('option');
|
|
136
|
+
blank.value = ''; blank.textContent = def !== undefined ? '(default: ' + def + ')' : '(default)';
|
|
137
|
+
sel.appendChild(blank);
|
|
138
|
+
const all = values.slice();
|
|
139
|
+
if (value && !all.includes(value)) all.unshift(value);
|
|
140
|
+
all.forEach(v => {
|
|
141
|
+
const o = document.createElement('option'); o.value = v; o.textContent = v;
|
|
142
|
+
if (value === v) o.selected = true;
|
|
143
|
+
sel.appendChild(o);
|
|
144
|
+
});
|
|
145
|
+
return sel;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Free-text input with a datalist of suggestions (combobox).
|
|
149
|
+
function comboEl(key, values, value, def) {
|
|
150
|
+
const frag = document.createDocumentFragment();
|
|
151
|
+
const inp = document.createElement('input');
|
|
152
|
+
inp.type = 'text'; inp.dataset.key = key; inp.dataset.type = 'string';
|
|
153
|
+
const listId = 'dl-' + key.replace(/\\./g, '-');
|
|
154
|
+
inp.setAttribute('list', listId);
|
|
155
|
+
if (value !== undefined && value !== null) inp.value = value;
|
|
156
|
+
else if (def !== undefined && def !== '') inp.placeholder = String(def);
|
|
157
|
+
const dl = document.createElement('datalist'); dl.id = listId;
|
|
158
|
+
values.forEach(v => { const o = document.createElement('option'); o.value = v; dl.appendChild(o); });
|
|
159
|
+
frag.appendChild(inp); frag.appendChild(dl);
|
|
160
|
+
return frag;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function fieldInput(key, spec, value, def) {
|
|
164
|
+
const isUnset = value === undefined || value === null;
|
|
165
|
+
const opt = fieldOptions[key];
|
|
166
|
+
if (opt) return opt.strict ? selectEl(key, opt.values, value, def) : comboEl(key, opt.values, value, def);
|
|
167
|
+
const enumVals = spec.enum;
|
|
168
|
+
if (spec.type === 'boolean') {
|
|
169
|
+
const cb = document.createElement('input');
|
|
170
|
+
cb.type = 'checkbox'; cb.dataset.key = key; cb.dataset.type = 'boolean';
|
|
171
|
+
cb.checked = value === true;
|
|
172
|
+
return cb;
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(enumVals)) return selectEl(key, enumVals, value, def);
|
|
175
|
+
const inp = document.createElement('input');
|
|
176
|
+
const isNum = spec.type === 'number' || spec.type === 'integer';
|
|
177
|
+
inp.dataset.key = key;
|
|
178
|
+
inp.dataset.type = isNum ? 'number' : 'string';
|
|
179
|
+
const isEnvRef = typeof value === 'string' && value.startsWith('env:');
|
|
180
|
+
if (key === 'db.password') inp.type = 'password';
|
|
181
|
+
else if (key.endsWith('.apiKey')) inp.type = isEnvRef ? 'text' : 'password';
|
|
182
|
+
else if (isNum) {
|
|
183
|
+
inp.type = 'number';
|
|
184
|
+
// Only apply small, meaningful bounds (skip zod's safe-integer noise).
|
|
185
|
+
if (typeof spec.minimum === 'number' && Math.abs(spec.minimum) < 1e6) inp.min = spec.minimum;
|
|
186
|
+
if (typeof spec.maximum === 'number' && Math.abs(spec.maximum) < 1e6) inp.max = spec.maximum;
|
|
187
|
+
if (spec.maximum === 1) inp.step = '0.05';
|
|
188
|
+
} else inp.type = 'text';
|
|
189
|
+
if (!isUnset) inp.value = value;
|
|
190
|
+
// Show the applied default as placeholder when the field is left unset.
|
|
191
|
+
else if (def !== undefined && def !== '') inp.placeholder = String(def);
|
|
192
|
+
return inp;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderField(container, key, spec, value, def) {
|
|
196
|
+
const isUnset = value === undefined || value === null;
|
|
197
|
+
const wrap = document.createElement('div');
|
|
198
|
+
wrap.className = 'field'; wrap.dataset.fieldKey = key;
|
|
199
|
+
const label = document.createElement('label');
|
|
200
|
+
let html = key.split('.').pop();
|
|
201
|
+
if (spec.description) html += '<span class="desc">' + spec.description + '</span>';
|
|
202
|
+
if (key.endsWith('.apiKey') && typeof value === 'string' && value.startsWith('env:')) {
|
|
203
|
+
html += '<span class="defval using">✔ set via ' + value.slice(4) + '</span>';
|
|
204
|
+
}
|
|
205
|
+
if (def !== undefined) {
|
|
206
|
+
html += '<span class="defval' + (isUnset ? ' using' : '') + '">'
|
|
207
|
+
+ (isUnset ? '↪ using default: ' : 'default: ') + fmtDefault(def) + '</span>';
|
|
208
|
+
}
|
|
209
|
+
label.innerHTML = html;
|
|
210
|
+
wrap.appendChild(label);
|
|
211
|
+
wrap.appendChild(fieldInput(key, spec, value, def));
|
|
212
|
+
container.appendChild(wrap);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderForm() {
|
|
216
|
+
const form = document.getElementById('config-form');
|
|
217
|
+
form.innerHTML = '';
|
|
218
|
+
const props = schema.properties || {};
|
|
219
|
+
for (const [group, gspec] of Object.entries(props)) {
|
|
220
|
+
const sec = document.createElement('div'); sec.className = 'section';
|
|
221
|
+
const h = document.createElement('h2'); h.textContent = group; sec.appendChild(h);
|
|
222
|
+
if (gspec.type === 'object' && gspec.properties) {
|
|
223
|
+
for (const [field, fspec] of Object.entries(gspec.properties)) {
|
|
224
|
+
const key = group + '.' + field;
|
|
225
|
+
renderField(sec, key, fspec, nested(config, key), defaults[key]);
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
renderField(sec, group, gspec, nested(config, group), defaults[group]);
|
|
229
|
+
}
|
|
230
|
+
form.appendChild(sec);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function collectForm() {
|
|
235
|
+
const out = {};
|
|
236
|
+
document.querySelectorAll('[data-key]').forEach(el => {
|
|
237
|
+
const key = el.dataset.key, type = el.dataset.type;
|
|
238
|
+
let val;
|
|
239
|
+
if (type === 'boolean') val = el.checked;
|
|
240
|
+
else if (el.value === '') return; // unset → omit (stay minimal)
|
|
241
|
+
else if (type === 'number') { const n = Number(el.value); if (Number.isNaN(n)) return; val = n; }
|
|
242
|
+
else val = el.value;
|
|
243
|
+
const parts = key.split('.');
|
|
244
|
+
if (parts.length === 1) out[parts[0]] = val;
|
|
245
|
+
else { (out[parts[0]] = out[parts[0]] || {})[parts[1]] = val; }
|
|
246
|
+
});
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function showBanner(cls, msg) {
|
|
251
|
+
const b = document.getElementById('banner');
|
|
252
|
+
b.className = cls; b.textContent = msg;
|
|
253
|
+
}
|
|
254
|
+
function clearInvalid() {
|
|
255
|
+
document.querySelectorAll('.field.invalid').forEach(f => f.classList.remove('invalid'));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function save() {
|
|
259
|
+
clearInvalid();
|
|
260
|
+
const btn = document.getElementById('save-btn');
|
|
261
|
+
btn.disabled = true;
|
|
262
|
+
try {
|
|
263
|
+
const res = await fetch('/api/config', {
|
|
264
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify(collectForm()),
|
|
266
|
+
});
|
|
267
|
+
const data = await res.json();
|
|
268
|
+
if (res.ok) {
|
|
269
|
+
showBanner('ok', 'Saved.');
|
|
270
|
+
await loadConfig(); // refresh (re-mask password, normalize)
|
|
271
|
+
} else {
|
|
272
|
+
showBanner('err', data.error || 'Validation failed');
|
|
273
|
+
// best-effort inline highlight: mark fields whose key appears in the message
|
|
274
|
+
document.querySelectorAll('[data-field-key]').forEach(f => {
|
|
275
|
+
if ((data.error || '').includes(f.dataset.fieldKey)) f.classList.add('invalid');
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
} catch (e) {
|
|
279
|
+
showBanner('err', 'Request failed: ' + e.message);
|
|
280
|
+
} finally {
|
|
281
|
+
btn.disabled = false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
document.getElementById('save-btn').addEventListener('click', save);
|
|
285
|
+
|
|
286
|
+
async function loadConfig() {
|
|
287
|
+
[schema, config, defaults, fieldOptions] = await Promise.all([
|
|
288
|
+
fetch('/api/schema').then(r => r.json()),
|
|
289
|
+
fetch('/api/config').then(r => r.json()),
|
|
290
|
+
fetch('/api/defaults').then(r => r.json()),
|
|
291
|
+
fetch('/api/field-options').then(r => r.json()),
|
|
292
|
+
]);
|
|
293
|
+
config = config || {};
|
|
294
|
+
defaults = defaults || {};
|
|
295
|
+
fieldOptions = fieldOptions || {};
|
|
296
|
+
renderForm();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---- projects ----
|
|
300
|
+
async function loadProjects() {
|
|
301
|
+
const cards = document.getElementById('cards');
|
|
302
|
+
cards.innerHTML = '<div class="empty">Loading…</div>';
|
|
303
|
+
try {
|
|
304
|
+
const projects = await fetch('/api/projects').then(r => r.json());
|
|
305
|
+
if (!projects.length) { cards.innerHTML = '<div class="empty">No indexed projects.</div>'; return; }
|
|
306
|
+
cards.innerHTML = '';
|
|
307
|
+
projects.forEach(p => {
|
|
308
|
+
const rel = p.relations ? Object.entries(p.relations)
|
|
309
|
+
.map(([k, v]) => k + ': ' + (Array.isArray(v) ? v.join(', ') : v)).join(' · ') : '';
|
|
310
|
+
const card = document.createElement('div'); card.className = 'card';
|
|
311
|
+
card.innerHTML =
|
|
312
|
+
'<h3></h3><div class="chunks"></div>' +
|
|
313
|
+
(p.stack ? '<div class="stack"></div>' : '') +
|
|
314
|
+
(p.description ? '<div class="descr"></div>' : '') +
|
|
315
|
+
(rel ? '<div class="rel"></div>' : '');
|
|
316
|
+
card.querySelector('h3').textContent = p.serviceName;
|
|
317
|
+
card.querySelector('.chunks').textContent = p.chunks + ' chunks';
|
|
318
|
+
if (p.stack) card.querySelector('.stack').textContent = p.stack;
|
|
319
|
+
if (p.description) card.querySelector('.descr').textContent = p.description;
|
|
320
|
+
if (rel) card.querySelector('.rel').textContent = rel;
|
|
321
|
+
cards.appendChild(card);
|
|
322
|
+
});
|
|
323
|
+
} catch (e) {
|
|
324
|
+
cards.innerHTML = '<div class="empty">Failed to load projects: ' + e.message + '</div>';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---- graph ----
|
|
329
|
+
// Reuse the full standalone graph page (same HTML as project-graph.html) via
|
|
330
|
+
// an iframe instead of re-implementing the network here. Loaded lazily on the
|
|
331
|
+
// first Graph-tab open so the graph page isn't fetched until needed.
|
|
332
|
+
function loadGraph() {
|
|
333
|
+
if (graphLoaded) return;
|
|
334
|
+
graphLoaded = true;
|
|
335
|
+
document.getElementById('graph-frame').src = '/graph';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
loadConfig().catch(e => showBanner('err', 'Failed to load config: ' + e.message));
|
|
339
|
+
</script>
|
|
340
|
+
</body>
|
|
341
|
+
</html>`;
|
|
342
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# GPU override for the Infinity service. Layered on top of docker-compose.yml
|
|
2
|
+
# (docker compose -f docker-compose.yml -f docker-compose.infinity-gpu.yml …)
|
|
3
|
+
# only when an NVIDIA GPU + container runtime is detected. Kept separate because
|
|
4
|
+
# a `deploy.reservations.devices` GPU request aborts startup on GPU-less hosts;
|
|
5
|
+
# the base file therefore stays CPU-only and always starts.
|
|
6
|
+
services:
|
|
7
|
+
infinity:
|
|
8
|
+
# Warmup is cheap on GPU (seconds) — re-enable it so the first real request
|
|
9
|
+
# is already fast. On CPU (base file only) it stays off.
|
|
10
|
+
environment:
|
|
11
|
+
INFINITY_MODEL_WARMUP: "true"
|
|
12
|
+
deploy:
|
|
13
|
+
resources:
|
|
14
|
+
reservations:
|
|
15
|
+
devices:
|
|
16
|
+
- driver: nvidia
|
|
17
|
+
count: all
|
|
18
|
+
capabilities:
|
|
19
|
+
- gpu
|
|
@@ -19,6 +19,31 @@ services:
|
|
|
19
19
|
volumes:
|
|
20
20
|
- ollama_data:/root/.ollama
|
|
21
21
|
|
|
22
|
+
# One Infinity container serves both /embeddings and /rerank. The model set is
|
|
23
|
+
# bound at startup via INFINITY_MODEL_ID (";"-separated for multiple models,
|
|
24
|
+
# e.g. "BAAI/bge-m3;BAAI/bge-reranker-v2-m3"). Defaults to CPU so it starts on
|
|
25
|
+
# any host; GPU is opt-in via docker-compose.infinity-gpu.yml (see rag-setup).
|
|
26
|
+
# Behind an "infinity" profile so `docker compose up -d` (ollama path) doesn't
|
|
27
|
+
# pull this multi-GB image — it starts only when explicitly targeted.
|
|
28
|
+
# Model warmup defaults OFF here: on CPU it runs multi-minute benchmark
|
|
29
|
+
# batches at every boot (a pure perf measurement, not required to serve). The
|
|
30
|
+
# GPU override turns it back on, where it costs seconds.
|
|
31
|
+
infinity:
|
|
32
|
+
image: michaelf34/infinity:latest
|
|
33
|
+
restart: unless-stopped
|
|
34
|
+
ports:
|
|
35
|
+
- "7997:7997"
|
|
36
|
+
environment:
|
|
37
|
+
INFINITY_MODEL_ID: ${INFINITY_MODEL_ID:-jinaai/jina-embeddings-v2-base-code}
|
|
38
|
+
INFINITY_MODEL_WARMUP: ${INFINITY_MODEL_WARMUP:-false}
|
|
39
|
+
INFINITY_PORT: "7997"
|
|
40
|
+
command: ["v2"]
|
|
41
|
+
volumes:
|
|
42
|
+
- infinity_cache:/app/.cache
|
|
43
|
+
profiles:
|
|
44
|
+
- infinity
|
|
45
|
+
|
|
22
46
|
volumes:
|
|
23
47
|
pgdata:
|
|
24
48
|
ollama_data:
|
|
49
|
+
infinity_cache:
|
|
@@ -0,0 +1,187 @@
|
|
|
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 **Ollama** 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
|
+
- **`ollama.model` — strict.** The embedding model is tied to the vector column
|
|
109
|
+
dimension (`bge-m3` → 1024). Values come from `SUPPORTED_EMBED_MODELS`
|
|
110
|
+
(`src/constants.ts`); the dropdown does **not** allow free text, so a
|
|
111
|
+
dimension-incompatible model cannot be set. To add a model, confirm its
|
|
112
|
+
dimension matches and append it to `SUPPORTED_EMBED_MODELS`.
|
|
113
|
+
- **`search.translateModel`, `reranker.model` — non-strict.** Suggestions are the
|
|
114
|
+
user's installed Ollama models (`ollama.list()`), but free text is allowed
|
|
115
|
+
because these are not dimension-bound and the user may pull a new model later.
|
|
116
|
+
If Ollama is unreachable the list is empty and the field degrades to plain
|
|
117
|
+
text.
|
|
118
|
+
|
|
119
|
+
A strict `<select>` preserves an unknown current value as an extra option, so
|
|
120
|
+
saving never silently drops a value that isn't in the list.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## HTTP API
|
|
125
|
+
|
|
126
|
+
All routes are served from `127.0.0.1:<port>`.
|
|
127
|
+
|
|
128
|
+
| Method | Path | Response |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `GET` | `/` | Dashboard HTML (self-contained) |
|
|
131
|
+
| `GET` | `/api/schema` | JSON Schema of the config (`buildJsonSchema()`) |
|
|
132
|
+
| `GET` | `/api/defaults` | `CONFIG_DEFAULTS` — dotted key → default value |
|
|
133
|
+
| `GET` | `/api/field-options` | dotted key → `{ values, strict }` |
|
|
134
|
+
| `GET` | `/api/config` | Current config, `db.password` masked |
|
|
135
|
+
| `PUT` | `/api/config` | Save. Body = config JSON. `200 {ok:true}` or `400 {error}` |
|
|
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
|
+
## Architecture
|
|
143
|
+
|
|
144
|
+
| File | Responsibility |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `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`). |
|
|
147
|
+
| `src/services/WebServerService.ts` | `WebServer(searcher, config, logger)` — `node:http` server, routing, and the API handlers. Exports the pure `mergeConfigForSave()` and the `FieldOption` type. |
|
|
148
|
+
| `src/web/dashboard.ts` | `renderDashboardHtml()` — the entire page (HTML + CSS + JS) as one self-contained string, mirroring `graph.ts:buildHtml`. |
|
|
149
|
+
|
|
150
|
+
Reused primitives (no duplication):
|
|
151
|
+
|
|
152
|
+
- `readConfigJson` / `parseConfig` / `writeConfigJson` / `buildJsonSchema` /
|
|
153
|
+
`CONFIG_DEFAULTS` — `src/config-store.ts`
|
|
154
|
+
- `buildGraphData` — `src/graph.ts`
|
|
155
|
+
- `listProjects` — `src/services/SearcherService.ts`
|
|
156
|
+
- `SUPPORTED_EMBED_MODELS` — `src/constants.ts`
|
|
157
|
+
|
|
158
|
+
Wiring: `app.ts` detects the `web` mode and dispatches
|
|
159
|
+
`new WebCommand(container, effectiveArgs)`. The command uses the shared
|
|
160
|
+
`Container` services (`db`, `searcher`, `config`, `logger`) — no container
|
|
161
|
+
changes were needed.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Extending
|
|
166
|
+
|
|
167
|
+
- **Constrain another field to a list** — add an entry to `fieldOptions()` in
|
|
168
|
+
`WebServerService.ts` (`{ values, strict }`). The form renders it automatically
|
|
169
|
+
(strict → dropdown, non-strict → combobox).
|
|
170
|
+
- **Add a tab / endpoint** — add a route in `WebServer.handle`, a fetch + render
|
|
171
|
+
in `dashboard.ts`, and a tab button. Keep handlers thin — delegate to a service.
|
|
172
|
+
- **Actions (e.g. trigger reindex)** — deferred by design. This is heavier (a
|
|
173
|
+
long-running process with live progress, likely via SSE, plus concurrency
|
|
174
|
+
control) and belongs in its own change rather than the read/config-edit shell.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Keep in sync
|
|
179
|
+
|
|
180
|
+
- `CONFIG_DEFAULTS` (`config-store.ts`) is the **single source of truth** for
|
|
181
|
+
built-in defaults: `resolvedSchema` in `config.ts` references it in its
|
|
182
|
+
`.default(...)` calls, so the runtime default and the dashboard-displayed
|
|
183
|
+
default cannot diverge — no manual sync needed. (A unit test still asserts
|
|
184
|
+
every key is a valid config key.)
|
|
185
|
+
- `SUPPORTED_EMBED_MODELS` (`constants.ts`) must contain only models whose
|
|
186
|
+
embedding dimension matches the `vector(...)` column — this one is still a
|
|
187
|
+
manual invariant, guarded only at the membership level by a unit test.
|
package/package.json
CHANGED
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codesearch-mcp",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0-alpha",
|
|
4
4
|
"description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"rag",
|
|
10
|
+
"semantic-search",
|
|
11
|
+
"code-search",
|
|
12
|
+
"pgvector",
|
|
13
|
+
"embeddings",
|
|
14
|
+
"claude",
|
|
15
|
+
"ollama",
|
|
16
|
+
"infinity"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"tag": "alpha"
|
|
23
|
+
},
|
|
6
24
|
"main": "dist/app.js",
|
|
7
25
|
"bin": {
|
|
8
26
|
"rag-index": "dist/app.js",
|
|
9
27
|
"rag-mcp": "dist/app.js",
|
|
10
28
|
"rag-setup": "dist/app.js",
|
|
11
|
-
"rag-config": "dist/app.js"
|
|
29
|
+
"rag-config": "dist/app.js",
|
|
30
|
+
"rag-web": "dist/app.js"
|
|
12
31
|
},
|
|
13
32
|
"files": [
|
|
14
33
|
"dist/",
|
|
15
34
|
"docker/",
|
|
35
|
+
"docs/",
|
|
16
36
|
"README.md",
|
|
17
37
|
"CHANGELOG.md"
|
|
18
38
|
],
|
|
19
39
|
"scripts": {
|
|
40
|
+
"prebuild": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
20
41
|
"build": "tsc",
|
|
21
42
|
"postbuild": "node -e \"const fs=require('fs');const f='dist/app.js';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'));fs.chmodSync(f,0o755);\"",
|
|
22
|
-
"prepublishOnly": "npm run build && npm run typecheck",
|
|
43
|
+
"prepublishOnly": "npm run build && npm run typecheck && npm test",
|
|
23
44
|
"start:mcp": "ts-node app.ts mcp",
|
|
45
|
+
"start:web": "ts-node app.ts web",
|
|
24
46
|
"index:workspace": "ts-node app.ts index",
|
|
25
47
|
"index:reindex": "ts-node app.ts index --reindex",
|
|
26
48
|
"index:backfill": "ts-node app.ts backfill",
|
package/dist/src/ast.d.ts
DELETED