notex-companion 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +875 -0
- package/dist/context.d.ts +6 -0
- package/dist/cors.d.ts +9 -0
- package/dist/footer.d.ts +12 -0
- package/dist/graph.d.ts +39 -0
- package/dist/http.d.ts +21 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +769 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/net.d.ts +14 -0
- package/dist/ops.d.ts +75 -0
- package/dist/pairing.d.ts +6 -0
- package/dist/scoring.d.ts +17 -0
- package/dist/serve.d.ts +26 -0
- package/dist/traversal.d.ts +16 -0
- package/dist/types.d.ts +68 -0
- package/package.json +43 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { resolve as resolve2 } from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/graph.ts
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
import { readFileSync, statSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
// src/scoring.ts
|
|
14
|
+
var STOPWORDS = new Set(("a an the and or but if of to in on for with from by at as is are was were " + "do does did how what where when which who why can could should would we " + "our it its this that these those there here about into over under not no " + "you your i me my be been being have has had will shall may might must " + "get got make made use used using does happen happens work works").split(" "));
|
|
15
|
+
function shatter(text) {
|
|
16
|
+
return String(text).replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).map((t) => t.toLowerCase()).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
function terms(question) {
|
|
19
|
+
return [...new Set(shatter(question).filter((t) => t.length >= 3 && !STOPWORDS.has(t)))];
|
|
20
|
+
}
|
|
21
|
+
function scoreNodes(index, queryTerms) {
|
|
22
|
+
const scored = [];
|
|
23
|
+
for (const entry of index) {
|
|
24
|
+
let score = 0;
|
|
25
|
+
let exact = false;
|
|
26
|
+
for (const term of queryTerms) {
|
|
27
|
+
let best = 0;
|
|
28
|
+
if (entry.labelLower === term) {
|
|
29
|
+
best = 6;
|
|
30
|
+
exact = true;
|
|
31
|
+
} else if (entry.labelTokens.has(term)) {
|
|
32
|
+
best = 3;
|
|
33
|
+
exact = true;
|
|
34
|
+
} else {
|
|
35
|
+
for (const tok of entry.labelTokens) {
|
|
36
|
+
if (tok.length >= 4 && (tok.startsWith(term) || term.startsWith(tok))) {
|
|
37
|
+
best = Math.max(best, 1.5);
|
|
38
|
+
} else if (term.length >= 4 && tok.includes(term)) {
|
|
39
|
+
best = Math.max(best, 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (best === 0) {
|
|
44
|
+
if (entry.pathTokens.has(term)) {
|
|
45
|
+
best = 1.2;
|
|
46
|
+
} else {
|
|
47
|
+
for (const tok of entry.pathTokens) {
|
|
48
|
+
if (tok.length >= 4 && term.length >= 4 && tok.startsWith(term))
|
|
49
|
+
best = 0.6;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
score += best;
|
|
54
|
+
}
|
|
55
|
+
if (score > 0)
|
|
56
|
+
scored.push({ id: entry.id, score: Math.round(score * 100) / 100, exact });
|
|
57
|
+
}
|
|
58
|
+
return scored.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/types.ts
|
|
62
|
+
var ERROR_CODES = {
|
|
63
|
+
unauthorized: "unauthorized",
|
|
64
|
+
notFound: "not_found",
|
|
65
|
+
graphUnreadable: "graph_unreadable",
|
|
66
|
+
invalidRequest: "invalid_request",
|
|
67
|
+
graphLoading: "graph_loading"
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
class OpError extends Error {
|
|
71
|
+
code;
|
|
72
|
+
detail;
|
|
73
|
+
constructor(code, message, detail) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "OpError";
|
|
76
|
+
this.code = code;
|
|
77
|
+
this.detail = detail;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/graph.ts
|
|
82
|
+
function rootPrefixFor(checkoutPath, graphRoot) {
|
|
83
|
+
if (graphRoot === checkoutPath)
|
|
84
|
+
return "";
|
|
85
|
+
const boundary = checkoutPath.endsWith("/") ? checkoutPath : `${checkoutPath}/`;
|
|
86
|
+
if (!graphRoot.startsWith(boundary))
|
|
87
|
+
return "";
|
|
88
|
+
return graphRoot.slice(boundary.length);
|
|
89
|
+
}
|
|
90
|
+
function readGraphRoot(checkoutPath) {
|
|
91
|
+
try {
|
|
92
|
+
return readFileSync(resolve(checkoutPath, "graphify-out/.graphify_root"), "utf8").trim();
|
|
93
|
+
} catch {
|
|
94
|
+
return checkoutPath;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function readHeadSha(checkoutPath) {
|
|
98
|
+
try {
|
|
99
|
+
return execSync("git rev-parse HEAD", { cwd: checkoutPath, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function loadGraph(checkoutPath) {
|
|
105
|
+
const graphPath = resolve(checkoutPath, "graphify-out/graph.json");
|
|
106
|
+
let raw;
|
|
107
|
+
try {
|
|
108
|
+
raw = readFileSync(graphPath, "utf8");
|
|
109
|
+
} catch (err) {
|
|
110
|
+
throw new OpError("graph_unreadable", `graph.json not found at ${graphPath}`, err);
|
|
111
|
+
}
|
|
112
|
+
let doc;
|
|
113
|
+
try {
|
|
114
|
+
doc = JSON.parse(raw);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
throw new OpError("graph_unreadable", `graph.json at ${graphPath} is not valid JSON`, err);
|
|
117
|
+
}
|
|
118
|
+
const graphRoot = readGraphRoot(checkoutPath);
|
|
119
|
+
const rootPrefix = rootPrefixFor(checkoutPath, graphRoot);
|
|
120
|
+
const checkoutRelative = (sourceFile) => rootPrefix ? `${rootPrefix}/${sourceFile}` : sourceFile;
|
|
121
|
+
const stamp = {
|
|
122
|
+
builtAt: statSync(graphPath).mtime.toISOString(),
|
|
123
|
+
graphHash: createHash("sha256").update(raw).digest("hex").slice(0, 16),
|
|
124
|
+
nodeCount: doc.nodes.length,
|
|
125
|
+
edgeCount: doc.links.length,
|
|
126
|
+
communityCount: new Set(doc.nodes.map((n) => n.community)).size,
|
|
127
|
+
checkoutPath,
|
|
128
|
+
headSha: readHeadSha(checkoutPath),
|
|
129
|
+
graphRoot,
|
|
130
|
+
rootPrefix
|
|
131
|
+
};
|
|
132
|
+
const nodesById = new Map(doc.nodes.map((n) => [n.id, n]));
|
|
133
|
+
const adjacency = new Map;
|
|
134
|
+
for (const e of doc.links) {
|
|
135
|
+
if (!adjacency.has(e.source))
|
|
136
|
+
adjacency.set(e.source, []);
|
|
137
|
+
if (!adjacency.has(e.target))
|
|
138
|
+
adjacency.set(e.target, []);
|
|
139
|
+
adjacency.get(e.source).push({ other: e.target, edge: e });
|
|
140
|
+
adjacency.get(e.target).push({ other: e.source, edge: e });
|
|
141
|
+
}
|
|
142
|
+
const scoreIndex = doc.nodes.map((n) => ({
|
|
143
|
+
id: n.id,
|
|
144
|
+
labelTokens: new Set([...shatter(n.label), ...shatter(n.norm_label ?? "")]),
|
|
145
|
+
pathTokens: new Set(shatter(n.source_file ?? "")),
|
|
146
|
+
labelLower: String(n.label).toLowerCase()
|
|
147
|
+
}));
|
|
148
|
+
const project = (n) => ({
|
|
149
|
+
id: n.id,
|
|
150
|
+
label: n.label,
|
|
151
|
+
sourceFile: checkoutRelative(n.source_file),
|
|
152
|
+
sourceLocation: n.source_location,
|
|
153
|
+
fileType: n.file_type,
|
|
154
|
+
community: n.community == null ? null : { id: n.community, name: n.community_name ?? "" }
|
|
155
|
+
});
|
|
156
|
+
const projectEdge = (e) => ({
|
|
157
|
+
source: e.source,
|
|
158
|
+
target: e.target,
|
|
159
|
+
relation: e.relation,
|
|
160
|
+
weight: e.weight,
|
|
161
|
+
confidence: e.confidence,
|
|
162
|
+
sourceFile: checkoutRelative(e.source_file),
|
|
163
|
+
sourceLocation: e.source_location
|
|
164
|
+
});
|
|
165
|
+
return { stamp, nodesById, edges: doc.links, adjacency, scoreIndex, project, projectEdge };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/http.ts
|
|
169
|
+
import { timingSafeEqual } from "node:crypto";
|
|
170
|
+
|
|
171
|
+
// src/cors.ts
|
|
172
|
+
var DEV_ORIGIN = "http://localhost:3000";
|
|
173
|
+
function resolveOrigins(configured, nodeEnv, productionOrigin) {
|
|
174
|
+
const origins = [...configured];
|
|
175
|
+
if (productionOrigin && !origins.includes(productionOrigin))
|
|
176
|
+
origins.push(productionOrigin);
|
|
177
|
+
if (nodeEnv !== "production" && !origins.includes(DEV_ORIGIN))
|
|
178
|
+
origins.push(DEV_ORIGIN);
|
|
179
|
+
return origins;
|
|
180
|
+
}
|
|
181
|
+
function corsHeaders(origins, requestOrigin) {
|
|
182
|
+
if (requestOrigin === undefined || !origins.includes(requestOrigin))
|
|
183
|
+
return {};
|
|
184
|
+
return { "Access-Control-Allow-Origin": requestOrigin, Vary: "Origin" };
|
|
185
|
+
}
|
|
186
|
+
function preflightHeaders(origins, requestOrigin) {
|
|
187
|
+
const base = corsHeaders(origins, requestOrigin);
|
|
188
|
+
if (Object.keys(base).length === 0)
|
|
189
|
+
return {};
|
|
190
|
+
return {
|
|
191
|
+
...base,
|
|
192
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
193
|
+
"Access-Control-Allow-Headers": "content-type, authorization",
|
|
194
|
+
"Access-Control-Max-Age": "600",
|
|
195
|
+
"Access-Control-Allow-Private-Network": "true"
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/context.ts
|
|
200
|
+
function buildContext(question, stamp, nodes, edges, opts = {}) {
|
|
201
|
+
const lines = [];
|
|
202
|
+
const builtDate = stamp.builtAt.slice(0, 10);
|
|
203
|
+
lines.push(`# ${question}`, "");
|
|
204
|
+
lines.push(`built ${builtDate} · ${nodes.length} nodes`, "");
|
|
205
|
+
const groups = new Map;
|
|
206
|
+
for (const n of nodes) {
|
|
207
|
+
const key = n.community?.name ? n.community.name : "Ungrouped";
|
|
208
|
+
if (!groups.has(key))
|
|
209
|
+
groups.set(key, []);
|
|
210
|
+
groups.get(key).push(n);
|
|
211
|
+
}
|
|
212
|
+
const groupNames = [...groups.keys()].sort((a, b) => a === "Ungrouped" ? 1 : b === "Ungrouped" ? -1 : a.localeCompare(b));
|
|
213
|
+
for (const name of groupNames) {
|
|
214
|
+
lines.push(`## ${name}`);
|
|
215
|
+
const members = [...groups.get(name)].sort((a, b) => a.label.localeCompare(b.label) || a.id.localeCompare(b.id));
|
|
216
|
+
for (const n of members)
|
|
217
|
+
lines.push(`- ${n.label} — ${n.sourceFile}:${n.sourceLocation}`);
|
|
218
|
+
lines.push("");
|
|
219
|
+
}
|
|
220
|
+
if (edges.length > 0) {
|
|
221
|
+
const labelById = new Map(nodes.map((n) => [n.id, n.label]));
|
|
222
|
+
lines.push("## Relations");
|
|
223
|
+
for (const e of edges) {
|
|
224
|
+
const tag = e.confidence === "EXTRACTED" ? "" : ` [${e.confidence}]`;
|
|
225
|
+
lines.push(`- ${labelById.get(e.source) ?? e.source} —${e.relation}→ ${labelById.get(e.target) ?? e.target}${tag}`);
|
|
226
|
+
}
|
|
227
|
+
lines.push("");
|
|
228
|
+
}
|
|
229
|
+
if (opts.truncated) {
|
|
230
|
+
lines.push(`> Retrieval was truncated (${opts.truncated.reason}); ${opts.truncated.omittedCount} related nodes were omitted.`, "");
|
|
231
|
+
}
|
|
232
|
+
if (opts.degraded) {
|
|
233
|
+
lines.push("> Matched literally — no vocabulary expansion was applied to the question.", "");
|
|
234
|
+
}
|
|
235
|
+
while (lines.length > 0 && lines[lines.length - 1] === "")
|
|
236
|
+
lines.pop();
|
|
237
|
+
return lines.join(`
|
|
238
|
+
`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/footer.ts
|
|
242
|
+
function isoDate(date) {
|
|
243
|
+
return date.toISOString().slice(0, 10);
|
|
244
|
+
}
|
|
245
|
+
function buildFooter(stamp, sources, opts = {}) {
|
|
246
|
+
const draftDate = opts.draftDate ?? isoDate(new Date);
|
|
247
|
+
const builtDate = stamp.builtAt.slice(0, 10);
|
|
248
|
+
const graphLine = stamp.headSha ? `Graph built ${builtDate} (${stamp.graphHash}) at commit ${stamp.headSha.slice(0, 7)}.` : `Graph built ${builtDate} (${stamp.graphHash}).`;
|
|
249
|
+
const noteClauses = [];
|
|
250
|
+
if (opts.truncated) {
|
|
251
|
+
noteClauses.push(`Retrieval was truncated (${opts.truncated.reason}); some related code may be missing.`);
|
|
252
|
+
}
|
|
253
|
+
if (opts.degraded) {
|
|
254
|
+
noteClauses.push("Retrieval matched literally; vocabulary was not expanded.");
|
|
255
|
+
}
|
|
256
|
+
const sortedSources = [...new Set(sources.map((s) => `${s.file}:${s.location}`))].sort();
|
|
257
|
+
const lines = [
|
|
258
|
+
"---",
|
|
259
|
+
`Drafted from the code graph on ${draftDate}.`,
|
|
260
|
+
graphLine,
|
|
261
|
+
...noteClauses.length > 0 ? [noteClauses.join(" ")] : [],
|
|
262
|
+
"Sources:",
|
|
263
|
+
...sortedSources.map((s) => `- ${s}`)
|
|
264
|
+
];
|
|
265
|
+
return `
|
|
266
|
+
` + lines.join(`
|
|
267
|
+
`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/traversal.ts
|
|
271
|
+
function traverse(adjacency, allEdges, seedIds, depth, maxNodes) {
|
|
272
|
+
const kept = new Set;
|
|
273
|
+
const overflow = new Set;
|
|
274
|
+
let frontier = [];
|
|
275
|
+
for (const s of seedIds) {
|
|
276
|
+
if (kept.size < maxNodes) {
|
|
277
|
+
kept.add(s);
|
|
278
|
+
frontier.push(s);
|
|
279
|
+
} else {
|
|
280
|
+
overflow.add(s);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
let d = 0;
|
|
284
|
+
for (;d < depth; d++) {
|
|
285
|
+
const candidates = [];
|
|
286
|
+
for (const id of frontier) {
|
|
287
|
+
for (const { other, edge } of adjacency.get(id) ?? []) {
|
|
288
|
+
if (!kept.has(other))
|
|
289
|
+
candidates.push({ other, weight: edge.weight ?? 1 });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
candidates.sort((a, b) => b.weight - a.weight);
|
|
293
|
+
const next = [];
|
|
294
|
+
for (const c of candidates) {
|
|
295
|
+
if (kept.has(c.other))
|
|
296
|
+
continue;
|
|
297
|
+
if (kept.size >= maxNodes) {
|
|
298
|
+
overflow.add(c.other);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
kept.add(c.other);
|
|
302
|
+
next.push(c.other);
|
|
303
|
+
}
|
|
304
|
+
frontier = next;
|
|
305
|
+
if (frontier.length === 0)
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
let truncated = null;
|
|
309
|
+
if (overflow.size > 0) {
|
|
310
|
+
truncated = { reason: "maxNodes", omittedCount: overflow.size };
|
|
311
|
+
} else if (d === depth && frontier.length > 0) {
|
|
312
|
+
const beyond = new Set;
|
|
313
|
+
for (const id of frontier) {
|
|
314
|
+
for (const { other } of adjacency.get(id) ?? []) {
|
|
315
|
+
if (!kept.has(other))
|
|
316
|
+
beyond.add(other);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (beyond.size > 0)
|
|
320
|
+
truncated = { reason: "depth", omittedCount: beyond.size };
|
|
321
|
+
}
|
|
322
|
+
const edges = allEdges.filter((e) => kept.has(e.source) && kept.has(e.target));
|
|
323
|
+
return { nodeIds: [...kept], edges, truncated };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/ops.ts
|
|
327
|
+
var API_VERSION = "0.1.0";
|
|
328
|
+
var CAPABILITIES = ["search", "query", "path", "node"];
|
|
329
|
+
var MAX_NODES_CEILING = 1000;
|
|
330
|
+
var MAX_DEPTH_CEILING = 3;
|
|
331
|
+
var DEFAULT_DEPTH = 1;
|
|
332
|
+
var DEFAULT_MAX_NODES = 60;
|
|
333
|
+
var DEFAULT_SEED_COUNT = 5;
|
|
334
|
+
var DEFAULT_SEARCH_LIMIT = 20;
|
|
335
|
+
var MAX_SEARCH_LIMIT = 100;
|
|
336
|
+
function status(index) {
|
|
337
|
+
return {
|
|
338
|
+
graph: index.stamp,
|
|
339
|
+
apiVersion: API_VERSION,
|
|
340
|
+
capabilities: [...CAPABILITIES],
|
|
341
|
+
limits: { maxNodes: MAX_NODES_CEILING, maxDepth: MAX_DEPTH_CEILING }
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function search(index, req) {
|
|
345
|
+
const limit = Math.max(0, Math.min(req.limit ?? DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT));
|
|
346
|
+
const scored = scoreNodes(index.scoreIndex, terms(req.q)).slice(0, limit);
|
|
347
|
+
return {
|
|
348
|
+
graph: index.stamp,
|
|
349
|
+
results: scored.map((s) => ({ ...index.project(index.nodesById.get(s.id)), score: s.score }))
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function query(index, req) {
|
|
353
|
+
const depth = Math.min(req.depth ?? DEFAULT_DEPTH, MAX_DEPTH_CEILING);
|
|
354
|
+
const maxNodes = Math.min(req.maxNodes ?? DEFAULT_MAX_NODES, MAX_NODES_CEILING);
|
|
355
|
+
const seedCount = Math.max(0, req.seeds ?? DEFAULT_SEED_COUNT);
|
|
356
|
+
const include = req.include ?? ["subgraph"];
|
|
357
|
+
const queryTerms = req.terms?.length ? req.terms.map((t) => t.toLowerCase()) : terms(req.question);
|
|
358
|
+
const degraded = req.terms?.length ? undefined : { expansion: "none" };
|
|
359
|
+
const scored = scoreNodes(index.scoreIndex, queryTerms);
|
|
360
|
+
const scoredById = new Map(scored.map((s) => [s.id, s]));
|
|
361
|
+
const seedIds = scored.slice(0, seedCount).map((s) => s.id);
|
|
362
|
+
const topScore = scored[0]?.score ?? 0;
|
|
363
|
+
const clearsFloor = seedIds.some((id) => scoredById.get(id)?.exact);
|
|
364
|
+
const lowConfidence = clearsFloor ? undefined : { topScore };
|
|
365
|
+
if (seedIds.length === 0) {
|
|
366
|
+
const empty = {
|
|
367
|
+
graph: index.stamp,
|
|
368
|
+
...degraded ? { degraded } : {},
|
|
369
|
+
...lowConfidence ? { lowConfidence } : {},
|
|
370
|
+
subgraph: { nodes: [], edges: [], seeds: [] }
|
|
371
|
+
};
|
|
372
|
+
if (include.includes("context")) {
|
|
373
|
+
empty.context = { markdown: buildContext(req.question, index.stamp, [], [], { degraded }), sources: [] };
|
|
374
|
+
}
|
|
375
|
+
if (include.includes("footer")) {
|
|
376
|
+
empty.footer = buildFooter(index.stamp, [], { degraded });
|
|
377
|
+
}
|
|
378
|
+
return empty;
|
|
379
|
+
}
|
|
380
|
+
const { nodeIds, edges: rawEdges, truncated } = traverse(index.adjacency, index.edges, seedIds, depth, maxNodes);
|
|
381
|
+
const nodes = nodeIds.map((id) => index.project(index.nodesById.get(id)));
|
|
382
|
+
const edges = rawEdges.map((e) => index.projectEdge(e));
|
|
383
|
+
const response = {
|
|
384
|
+
graph: index.stamp,
|
|
385
|
+
...degraded ? { degraded } : {},
|
|
386
|
+
...truncated ? { truncated } : {},
|
|
387
|
+
...lowConfidence ? { lowConfidence } : {},
|
|
388
|
+
subgraph: { nodes, edges, seeds: seedIds }
|
|
389
|
+
};
|
|
390
|
+
if (include.includes("context")) {
|
|
391
|
+
response.context = {
|
|
392
|
+
markdown: buildContext(req.question, index.stamp, nodes, edges, { truncated: truncated ?? undefined, degraded }),
|
|
393
|
+
sources: sourcesFrom(nodes)
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (include.includes("footer")) {
|
|
397
|
+
response.footer = buildFooter(index.stamp, sourcesFrom(nodes), { truncated: truncated ?? undefined, degraded });
|
|
398
|
+
}
|
|
399
|
+
return response;
|
|
400
|
+
}
|
|
401
|
+
function sourcesFrom(nodes) {
|
|
402
|
+
const keys = [...new Set(nodes.map((n) => `${n.sourceFile}:${n.sourceLocation}`))].sort();
|
|
403
|
+
return keys.map((k) => {
|
|
404
|
+
const i = k.lastIndexOf(":");
|
|
405
|
+
return { file: k.slice(0, i), location: k.slice(i + 1) };
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function path(index, req) {
|
|
409
|
+
const { from, to, maxDepth } = req;
|
|
410
|
+
if (!index.nodesById.has(from))
|
|
411
|
+
throw new OpError("not_found", `Unknown node id: ${from}`);
|
|
412
|
+
if (!index.nodesById.has(to))
|
|
413
|
+
throw new OpError("not_found", `Unknown node id: ${to}`);
|
|
414
|
+
if (from === to) {
|
|
415
|
+
return {
|
|
416
|
+
graph: index.stamp,
|
|
417
|
+
found: true,
|
|
418
|
+
nodes: [index.project(index.nodesById.get(from))],
|
|
419
|
+
edges: []
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const cameFrom = new Map;
|
|
423
|
+
const depthOf = new Map([[from, 0]]);
|
|
424
|
+
const queue = [from];
|
|
425
|
+
let qi = 0;
|
|
426
|
+
let found = false;
|
|
427
|
+
outer:
|
|
428
|
+
while (qi < queue.length) {
|
|
429
|
+
const id = queue[qi++];
|
|
430
|
+
const d = depthOf.get(id);
|
|
431
|
+
if (maxDepth !== undefined && d >= maxDepth)
|
|
432
|
+
continue;
|
|
433
|
+
for (const { other, edge } of index.adjacency.get(id) ?? []) {
|
|
434
|
+
if (depthOf.has(other))
|
|
435
|
+
continue;
|
|
436
|
+
depthOf.set(other, d + 1);
|
|
437
|
+
cameFrom.set(other, { prev: id, edge });
|
|
438
|
+
if (other === to) {
|
|
439
|
+
found = true;
|
|
440
|
+
break outer;
|
|
441
|
+
}
|
|
442
|
+
queue.push(other);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!found)
|
|
446
|
+
return { graph: index.stamp, found: false, nodes: [], edges: [] };
|
|
447
|
+
const nodeIds = [to];
|
|
448
|
+
const rawEdges = [];
|
|
449
|
+
let cur = to;
|
|
450
|
+
while (cur !== from) {
|
|
451
|
+
const step = cameFrom.get(cur);
|
|
452
|
+
rawEdges.push(step.edge);
|
|
453
|
+
cur = step.prev;
|
|
454
|
+
nodeIds.push(cur);
|
|
455
|
+
}
|
|
456
|
+
nodeIds.reverse();
|
|
457
|
+
rawEdges.reverse();
|
|
458
|
+
return {
|
|
459
|
+
graph: index.stamp,
|
|
460
|
+
found: true,
|
|
461
|
+
nodes: nodeIds.map((id) => index.project(index.nodesById.get(id))),
|
|
462
|
+
edges: rawEdges.map((e) => index.projectEdge(e))
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function node(index, req) {
|
|
466
|
+
const raw = index.nodesById.get(req.id);
|
|
467
|
+
if (!raw)
|
|
468
|
+
throw new OpError("not_found", `Unknown node id: ${req.id}`);
|
|
469
|
+
const neighbours = (index.adjacency.get(req.id) ?? []).map(({ other, edge }) => ({
|
|
470
|
+
node: index.project(index.nodesById.get(other)),
|
|
471
|
+
edge: index.projectEdge(edge)
|
|
472
|
+
}));
|
|
473
|
+
return { graph: index.stamp, node: index.project(raw), neighbours };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/http.ts
|
|
477
|
+
var ERROR_STATUS = {
|
|
478
|
+
[ERROR_CODES.unauthorized]: 401,
|
|
479
|
+
[ERROR_CODES.notFound]: 404,
|
|
480
|
+
[ERROR_CODES.graphUnreadable]: 409,
|
|
481
|
+
[ERROR_CODES.invalidRequest]: 422,
|
|
482
|
+
[ERROR_CODES.graphLoading]: 503
|
|
483
|
+
};
|
|
484
|
+
function createHandler(opts) {
|
|
485
|
+
return async (req) => {
|
|
486
|
+
const url = new URL(req.url);
|
|
487
|
+
const origin = req.headers.get("origin") ?? undefined;
|
|
488
|
+
if (req.method === "OPTIONS") {
|
|
489
|
+
return new Response(null, { status: 204, headers: preflightHeaders(opts.origins, origin) });
|
|
490
|
+
}
|
|
491
|
+
const cors = corsHeaders(opts.origins, origin);
|
|
492
|
+
try {
|
|
493
|
+
const body = await dispatch(req, url, opts);
|
|
494
|
+
return jsonResponse(200, body, cors);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
if (err instanceof OpError)
|
|
497
|
+
return jsonResponse(ERROR_STATUS[err.code], errorBody(err), cors);
|
|
498
|
+
return jsonResponse(500, { error: { code: "internal_error", message: "Internal server error" } }, cors);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
async function dispatch(req, url, opts) {
|
|
503
|
+
const { pathname } = url;
|
|
504
|
+
const method = req.method;
|
|
505
|
+
if (method === "GET" && pathname === "/v1/ping")
|
|
506
|
+
return { ok: true, apiVersion: API_VERSION };
|
|
507
|
+
requireAuth(req, opts.token);
|
|
508
|
+
const index = requireGraph(opts.getGraphState());
|
|
509
|
+
if (method === "GET" && pathname === "/v1/status")
|
|
510
|
+
return status(index);
|
|
511
|
+
if (method === "POST" && pathname === "/v1/search")
|
|
512
|
+
return search(index, parseSearchRequest(await readJsonBody(req)));
|
|
513
|
+
if (method === "POST" && pathname === "/v1/query")
|
|
514
|
+
return query(index, parseQueryRequest(await readJsonBody(req)));
|
|
515
|
+
if (method === "POST" && pathname === "/v1/path")
|
|
516
|
+
return path(index, parsePathRequest(await readJsonBody(req)));
|
|
517
|
+
if (method === "GET" && pathname.startsWith("/v1/node/")) {
|
|
518
|
+
return node(index, { id: decodeNodeId(pathname.slice("/v1/node/".length)) });
|
|
519
|
+
}
|
|
520
|
+
throw new OpError("not_found", `No such route: ${method} ${pathname}`);
|
|
521
|
+
}
|
|
522
|
+
function decodeNodeId(raw) {
|
|
523
|
+
let id;
|
|
524
|
+
try {
|
|
525
|
+
id = decodeURIComponent(raw);
|
|
526
|
+
} catch {
|
|
527
|
+
throw new OpError("invalid_request", "Malformed node id");
|
|
528
|
+
}
|
|
529
|
+
if (!id)
|
|
530
|
+
throw new OpError("not_found", "Missing node id");
|
|
531
|
+
return id;
|
|
532
|
+
}
|
|
533
|
+
function requireAuth(req, token) {
|
|
534
|
+
const header = req.headers.get("authorization");
|
|
535
|
+
const provided = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : undefined;
|
|
536
|
+
if (!provided || !safeEqual(provided, token))
|
|
537
|
+
throw new OpError("unauthorized", "Missing or invalid bearer token");
|
|
538
|
+
}
|
|
539
|
+
function safeEqual(a, b) {
|
|
540
|
+
const bufA = Buffer.from(a);
|
|
541
|
+
const bufB = Buffer.from(b);
|
|
542
|
+
return bufA.length === bufB.length && timingSafeEqual(bufA, bufB);
|
|
543
|
+
}
|
|
544
|
+
function requireGraph(state) {
|
|
545
|
+
if (state.kind === "loading")
|
|
546
|
+
throw new OpError("graph_loading", "Graph not yet loaded");
|
|
547
|
+
if (state.kind === "error")
|
|
548
|
+
throw state.error;
|
|
549
|
+
return state.index;
|
|
550
|
+
}
|
|
551
|
+
async function readJsonBody(req) {
|
|
552
|
+
let parsed;
|
|
553
|
+
try {
|
|
554
|
+
parsed = await req.json();
|
|
555
|
+
} catch {
|
|
556
|
+
throw new OpError("invalid_request", "Request body must be valid JSON");
|
|
557
|
+
}
|
|
558
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
559
|
+
throw new OpError("invalid_request", "Request body must be a JSON object");
|
|
560
|
+
}
|
|
561
|
+
return parsed;
|
|
562
|
+
}
|
|
563
|
+
function requireString(body, key) {
|
|
564
|
+
const v = body[key];
|
|
565
|
+
if (typeof v !== "string" || v.length === 0)
|
|
566
|
+
throw new OpError("invalid_request", `"${key}" must be a non-empty string`);
|
|
567
|
+
return v;
|
|
568
|
+
}
|
|
569
|
+
function optionalCount(body, key) {
|
|
570
|
+
const v = body[key];
|
|
571
|
+
if (v === undefined)
|
|
572
|
+
return;
|
|
573
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0) {
|
|
574
|
+
throw new OpError("invalid_request", `"${key}" must be a non-negative integer`);
|
|
575
|
+
}
|
|
576
|
+
return v;
|
|
577
|
+
}
|
|
578
|
+
function optionalStringArray(body, key) {
|
|
579
|
+
const v = body[key];
|
|
580
|
+
if (v === undefined)
|
|
581
|
+
return;
|
|
582
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
|
|
583
|
+
throw new OpError("invalid_request", `"${key}" must be an array of strings`);
|
|
584
|
+
}
|
|
585
|
+
return v;
|
|
586
|
+
}
|
|
587
|
+
var INCLUDE_VALUES = new Set(["subgraph", "context", "footer"]);
|
|
588
|
+
function optionalInclude(body) {
|
|
589
|
+
const v = body.include;
|
|
590
|
+
if (v === undefined)
|
|
591
|
+
return;
|
|
592
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === "string" && INCLUDE_VALUES.has(x))) {
|
|
593
|
+
throw new OpError("invalid_request", '"include" must be an array of "subgraph" | "context" | "footer"');
|
|
594
|
+
}
|
|
595
|
+
return v;
|
|
596
|
+
}
|
|
597
|
+
function parseSearchRequest(body) {
|
|
598
|
+
return { q: requireString(body, "q"), limit: optionalCount(body, "limit") };
|
|
599
|
+
}
|
|
600
|
+
function parseQueryRequest(body) {
|
|
601
|
+
return {
|
|
602
|
+
question: requireString(body, "question"),
|
|
603
|
+
terms: optionalStringArray(body, "terms"),
|
|
604
|
+
depth: optionalCount(body, "depth"),
|
|
605
|
+
maxNodes: optionalCount(body, "maxNodes"),
|
|
606
|
+
seeds: optionalCount(body, "seeds"),
|
|
607
|
+
include: optionalInclude(body)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function parsePathRequest(body) {
|
|
611
|
+
return { from: requireString(body, "from"), to: requireString(body, "to"), maxDepth: optionalCount(body, "maxDepth") };
|
|
612
|
+
}
|
|
613
|
+
function errorBody(err) {
|
|
614
|
+
const detail = err.detail instanceof Error ? { name: err.detail.name, message: err.detail.message } : err.detail;
|
|
615
|
+
return { error: { code: err.code, message: err.message, ...detail !== undefined ? { detail } : {} } };
|
|
616
|
+
}
|
|
617
|
+
function jsonResponse(statusCode, body, cors) {
|
|
618
|
+
return new Response(JSON.stringify(body), { status: statusCode, headers: { "Content-Type": "application/json", ...cors } });
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/net.ts
|
|
622
|
+
import { createServer } from "node:http";
|
|
623
|
+
function detectRuntime() {
|
|
624
|
+
return typeof Bun !== "undefined" ? "bun" : "node";
|
|
625
|
+
}
|
|
626
|
+
function startServer(opts, runtime = detectRuntime()) {
|
|
627
|
+
return runtime === "bun" ? Bun.serve(opts) : startNodeServer(opts);
|
|
628
|
+
}
|
|
629
|
+
function startNodeServer(opts) {
|
|
630
|
+
if (opts.port === 0) {
|
|
631
|
+
throw new Error("net.startNodeServer: a dynamic port (0) is not supported on the Node fallback — pass a fixed port");
|
|
632
|
+
}
|
|
633
|
+
const server = createServer((req, res) => {
|
|
634
|
+
res.on("error", () => {});
|
|
635
|
+
handleRequest(req, res, opts.fetch);
|
|
636
|
+
});
|
|
637
|
+
server.listen(opts.port, opts.hostname);
|
|
638
|
+
return {
|
|
639
|
+
hostname: opts.hostname,
|
|
640
|
+
port: opts.port,
|
|
641
|
+
stop(closeActiveConnections) {
|
|
642
|
+
if (closeActiveConnections)
|
|
643
|
+
server.closeAllConnections?.();
|
|
644
|
+
server.close();
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
async function handleRequest(req, res, fetch) {
|
|
649
|
+
try {
|
|
650
|
+
const request = await toWebRequest(req);
|
|
651
|
+
const response = await fetch(request);
|
|
652
|
+
res.statusCode = response.status;
|
|
653
|
+
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
654
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
655
|
+
} catch {
|
|
656
|
+
if (res.headersSent || res.writableEnded)
|
|
657
|
+
return;
|
|
658
|
+
res.statusCode = 500;
|
|
659
|
+
res.setHeader("content-type", "application/json");
|
|
660
|
+
res.end(JSON.stringify({ error: { code: "internal_error", message: "Internal server error" } }));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function toWebRequest(req) {
|
|
664
|
+
const url = `http://${req.headers.host ?? "127.0.0.1"}${req.url ?? "/"}`;
|
|
665
|
+
const headers = new Headers;
|
|
666
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
667
|
+
if (value === undefined)
|
|
668
|
+
continue;
|
|
669
|
+
if (Array.isArray(value))
|
|
670
|
+
for (const v of value)
|
|
671
|
+
headers.append(key, v);
|
|
672
|
+
else
|
|
673
|
+
headers.set(key, value);
|
|
674
|
+
}
|
|
675
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
676
|
+
let body;
|
|
677
|
+
if (hasBody) {
|
|
678
|
+
const chunks = [];
|
|
679
|
+
for await (const chunk of req)
|
|
680
|
+
chunks.push(chunk);
|
|
681
|
+
body = Buffer.concat(chunks);
|
|
682
|
+
}
|
|
683
|
+
return new Request(url, { method: req.method, headers, body });
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/pairing.ts
|
|
687
|
+
import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
|
|
688
|
+
import { randomBytes } from "node:crypto";
|
|
689
|
+
import { dirname, join } from "node:path";
|
|
690
|
+
function pairingFilePath(checkoutPath) {
|
|
691
|
+
return join(checkoutPath, ".notex", "companion.json");
|
|
692
|
+
}
|
|
693
|
+
function generateToken() {
|
|
694
|
+
return randomBytes(32).toString("base64url");
|
|
695
|
+
}
|
|
696
|
+
function readExistingToken(path2) {
|
|
697
|
+
try {
|
|
698
|
+
const parsed = JSON.parse(readFileSync2(path2, "utf8"));
|
|
699
|
+
return typeof parsed.token === "string" ? parsed.token : undefined;
|
|
700
|
+
} catch {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function tokenContents(token) {
|
|
705
|
+
return JSON.stringify({ token }, null, 2);
|
|
706
|
+
}
|
|
707
|
+
function createTokenFileExclusive(path2, token) {
|
|
708
|
+
mkdirSync(dirname(path2), { recursive: true });
|
|
709
|
+
writeFileSync(path2, tokenContents(token), { mode: 384, flag: "wx" });
|
|
710
|
+
}
|
|
711
|
+
function rewriteTokenFile(path2, token) {
|
|
712
|
+
mkdirSync(dirname(path2), { recursive: true });
|
|
713
|
+
const tmpPath = `${path2}.${process.pid}.tmp`;
|
|
714
|
+
writeFileSync(tmpPath, tokenContents(token), { mode: 384, flag: "wx" });
|
|
715
|
+
renameSync(tmpPath, path2);
|
|
716
|
+
}
|
|
717
|
+
function loadOrCreateToken(checkoutPath, opts = {}) {
|
|
718
|
+
const path2 = pairingFilePath(checkoutPath);
|
|
719
|
+
if (opts.rotate) {
|
|
720
|
+
const token2 = generateToken();
|
|
721
|
+
rewriteTokenFile(path2, token2);
|
|
722
|
+
return token2;
|
|
723
|
+
}
|
|
724
|
+
const existing = readExistingToken(path2);
|
|
725
|
+
if (existing !== undefined)
|
|
726
|
+
return existing;
|
|
727
|
+
const token = generateToken();
|
|
728
|
+
try {
|
|
729
|
+
createTokenFileExclusive(path2, token);
|
|
730
|
+
return token;
|
|
731
|
+
} catch (err) {
|
|
732
|
+
if (err.code !== "EEXIST")
|
|
733
|
+
throw err;
|
|
734
|
+
const winner = readExistingToken(path2);
|
|
735
|
+
if (winner !== undefined)
|
|
736
|
+
return winner;
|
|
737
|
+
rewriteTokenFile(path2, token);
|
|
738
|
+
return token;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function pairingLine(baseUrl, token) {
|
|
742
|
+
return `${baseUrl}/#token=${token}`;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/serve.ts
|
|
746
|
+
var DEFAULT_PORT = 7717;
|
|
747
|
+
function serve(opts) {
|
|
748
|
+
const origins = resolveOrigins(opts.origins ?? [], opts.nodeEnv ?? "development", process.env.VITE_APP_URL);
|
|
749
|
+
const token = loadOrCreateToken(opts.checkoutPath, { rotate: opts.rotateToken });
|
|
750
|
+
let graphState = { kind: "loading" };
|
|
751
|
+
const handler = createHandler({ token, origins, getGraphState: () => graphState });
|
|
752
|
+
const server = startServer({
|
|
753
|
+
hostname: "127.0.0.1",
|
|
754
|
+
port: opts.port ?? DEFAULT_PORT,
|
|
755
|
+
fetch: handler
|
|
756
|
+
});
|
|
757
|
+
queueMicrotask(() => {
|
|
758
|
+
try {
|
|
759
|
+
graphState = { kind: "ready", index: loadGraph(opts.checkoutPath) };
|
|
760
|
+
} catch (err) {
|
|
761
|
+
graphState = { kind: "error", error: err instanceof OpError ? err : new OpError("graph_unreadable", String(err)) };
|
|
762
|
+
}
|
|
763
|
+
opts.onGraphState?.(graphState);
|
|
764
|
+
});
|
|
765
|
+
const baseUrl = `http://127.0.0.1:${server.port}`;
|
|
766
|
+
return { server, token, baseUrl, pairingLine: pairingLine(baseUrl, token) };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/mcp.ts
|
|
770
|
+
function startMcpStub() {
|
|
771
|
+
console.error("notex-companion mcp: stdio server not yet implemented — see TBR-69");
|
|
772
|
+
process.stdin.resume();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/cli.ts
|
|
776
|
+
var HELP = `notex-companion — local retrieval companion over a checkout's graphify-out/graph.json
|
|
777
|
+
|
|
778
|
+
Usage:
|
|
779
|
+
notex-companion [serve] [options] Start the loopback HTTP server (default command)
|
|
780
|
+
notex-companion mcp Start the stdio MCP server
|
|
781
|
+
|
|
782
|
+
Options for serve:
|
|
783
|
+
--port <n> Port to bind (default 7717)
|
|
784
|
+
--origin <url> Additional allowed CORS origin, beyond the built-in defaults. Repeatable.
|
|
785
|
+
--rotate-token Generate a new pairing token, invalidating the old one
|
|
786
|
+
-h, --help Show this message
|
|
787
|
+
`;
|
|
788
|
+
|
|
789
|
+
class CliUsageError extends Error {
|
|
790
|
+
}
|
|
791
|
+
function parseServeArgs(args) {
|
|
792
|
+
const origins = [];
|
|
793
|
+
let port;
|
|
794
|
+
let rotateToken = false;
|
|
795
|
+
for (let i = 0;i < args.length; i++) {
|
|
796
|
+
const arg = args[i];
|
|
797
|
+
switch (arg) {
|
|
798
|
+
case "--port": {
|
|
799
|
+
const value = args[++i];
|
|
800
|
+
const parsed = value === undefined ? NaN : Number(value);
|
|
801
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
802
|
+
throw new CliUsageError(`--port needs an integer between 0 and 65535, got ${value ?? "(nothing)"}`);
|
|
803
|
+
}
|
|
804
|
+
port = parsed;
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
case "--origin": {
|
|
808
|
+
const value = args[++i];
|
|
809
|
+
if (!value)
|
|
810
|
+
throw new CliUsageError("--origin needs a value");
|
|
811
|
+
origins.push(value);
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case "--rotate-token":
|
|
815
|
+
rotateToken = true;
|
|
816
|
+
break;
|
|
817
|
+
default:
|
|
818
|
+
throw new CliUsageError(`unrecognised option "${arg}"`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return { port, origins, rotateToken };
|
|
822
|
+
}
|
|
823
|
+
function runServe(args) {
|
|
824
|
+
const { port, origins, rotateToken } = parseServeArgs(args);
|
|
825
|
+
const checkoutPath = process.cwd();
|
|
826
|
+
const graphPath = resolve2(checkoutPath, "graphify-out/graph.json");
|
|
827
|
+
if (!existsSync(graphPath)) {
|
|
828
|
+
console.error(`notex-companion: no graphify-out/graph.json found in ${checkoutPath}
|
|
829
|
+
` + `Generate a graph for this checkout before running notex-companion.`);
|
|
830
|
+
process.exit(1);
|
|
831
|
+
}
|
|
832
|
+
const handle = serve({
|
|
833
|
+
checkoutPath,
|
|
834
|
+
port,
|
|
835
|
+
origins,
|
|
836
|
+
rotateToken,
|
|
837
|
+
onGraphState: (state) => {
|
|
838
|
+
if (state.kind === "ready") {
|
|
839
|
+
const { stamp } = state.index;
|
|
840
|
+
console.log(`graph: ${stamp.nodeCount} nodes, ${stamp.edgeCount} edges, ${stamp.communityCount} communities (built ${stamp.builtAt})`);
|
|
841
|
+
} else if (state.kind === "error") {
|
|
842
|
+
console.error(`notex-companion: graph.json could not be read: ${state.error.message}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
console.log(`notex-companion serving ${checkoutPath}`);
|
|
847
|
+
console.log(handle.pairingLine);
|
|
848
|
+
}
|
|
849
|
+
function main(argv = process.argv.slice(2)) {
|
|
850
|
+
const [command, ...rest] = argv;
|
|
851
|
+
if (command === "-h" || command === "--help") {
|
|
852
|
+
console.log(HELP);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
if (command === "mcp") {
|
|
857
|
+
startMcpStub();
|
|
858
|
+
} else if (command === undefined || command === "serve" || command.startsWith("-")) {
|
|
859
|
+
runServe(command === "serve" ? rest : argv);
|
|
860
|
+
} else {
|
|
861
|
+
throw new CliUsageError(`unknown command "${command}"`);
|
|
862
|
+
}
|
|
863
|
+
} catch (err) {
|
|
864
|
+
if (err instanceof CliUsageError) {
|
|
865
|
+
console.error(`notex-companion: ${err.message}
|
|
866
|
+
|
|
867
|
+
${HELP}`);
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
throw err;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/bin.ts
|
|
875
|
+
main();
|