mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Imports the CodeAtlasWeb companion frontend verbatim from BernhardJackiewicz/CodeAtlasWeb at commit 1825f9c850ac794066e02606012508b9d393c6aa as the new graph-ui and removes the previous React frontend. This is Bernhard Jackiewicz's work, contributed with his agreement and squashed here from his development history (41 commits, cycles W0 through W15). The maintainer only relocates it: every product decision, line of code and test below graph-ui/ is his. The seven galaxy files that originate in this repository's previous graph-ui, and the pinned 3D libraries, are listed in graph-ui/THIRD_PARTY.md. What it is: a local reading IDE on the CBM read surface. A read-only Monaco source reader, a semantic twin that follows the caret, entry, bug-hunt, change-scope and flow reading modes, deterministic source-cited explanations with an optional local-model wording pass (llama-server sidecar, off unless the reader starts one; no model or binary ships), a persistent galaxy with focus following in both directions, a live-agent overlay fed by a loopback bridge, explicit air-gap, provenance and index-limit messaging, and an offline acceptance and browser verification harness. It consumes POST /rpc (the read-only tool allowlist), GET /api/layout, and the atlas routes /api/tree, /api/trace, /api/flows and /api/flow, plus the real ingest_traces store for observed calls; all of these are on feat/atlas-r1 (#1860), which this change stacks on. Not imported: CLAUDE.md (an agent-instruction file, which stays local-only in this repository), and the recorded proof artifacts under graph-ui/verification/, which follow in a separate commit so they can be reviewed and dropped on their own. Build wiring, the dev-proxy port contract, the CSP allowance for the sidecar and bridge ports, and the control surfaces the previous frontend had (project indexing, ADR editor, logs) follow in separate maintainer commits. Design and tracking: #1964. Co-authored-by: Bernhard Jackiewicz <bj@techport.io> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
174 lines
6.2 KiB
JavaScript
174 lines
6.2 KiB
JavaScript
/**
|
|
* Den gebauten C-Server aus PR 1860 starten, benutzen und wieder loswerden.
|
|
*
|
|
* Herausgeloest aus tools/smoke-spike.mjs, damit der zweite Beweislauf nicht
|
|
* eine zweite Fassung derselben Lebenszyklus-Regeln bekommt. Die Regeln sind
|
|
* kurz und jede einzelne ist teuer erkauft:
|
|
*
|
|
* 1. **Isoliertes HOME.** Der Server legt persistierte Einstellungen und den
|
|
* Index unter HOME ab. In das echte HOME zu schreiben waere ein Beweislauf,
|
|
* der die Maschine des Nutzers veraendert.
|
|
* 2. **MCP-Handshake auf stdin, und stdin bleibt offen.** Das HTTP-UI oeffnet
|
|
* erst nach initialize, und ein geschlossenes stdin beendet den
|
|
* stdio-Server sofort.
|
|
* 3. **Auf GET / warten, nicht auf eine Wartezeit.** Ein Schlaf ist eine
|
|
* Vermutung ueber die Maschine, auf der das laeuft.
|
|
* 4. **Am Ende zaehlen, was noch lauscht.** Ein Beweislauf, der einen Prozess
|
|
* zuruecklaesst, hat den naechsten schon verdorben.
|
|
*
|
|
* smoke-spike.mjs benutzt diese Datei bewusst nicht: es ist ausdruecklich
|
|
* Wegwerf-Beweiscode fuer W0 und soll unberuehrt gruen bleiben, statt wegen
|
|
* einer Aufraeumarbeit noch einmal durch Playwright zu muessen.
|
|
*/
|
|
|
|
import { execFile, spawn } from 'node:child_process';
|
|
import net from 'node:net';
|
|
import { promisify } from 'node:util';
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
/** Wartezeit, bis der Server auf GET / antworten muss. */
|
|
export const SERVER_READY_TIMEOUT_MS = 15000;
|
|
|
|
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
function isPortFree(port) {
|
|
return new Promise((resolve) => {
|
|
const probe = net.createServer();
|
|
probe.once('error', () => resolve(false));
|
|
probe.once('listening', () => probe.close(() => resolve(true)));
|
|
probe.listen(port, '127.0.0.1');
|
|
});
|
|
}
|
|
|
|
/** Erster freier Port ab `start`. */
|
|
export async function findFreePort(start, taken = []) {
|
|
for (let port = start; port < start + 200; port += 1) {
|
|
if (taken.includes(port)) { continue; }
|
|
if (await isPortFree(port)) { return port; }
|
|
}
|
|
throw new Error(`kein freier Port ab ${start} gefunden`);
|
|
}
|
|
|
|
/** Wie viele Prozesse auf dem Port lauschen. Null ist das Ziel nach dem Lauf. */
|
|
export async function countListeners(port) {
|
|
try {
|
|
const { stdout } = await execFileAsync('lsof', ['-ti', `tcp:${port}`]);
|
|
return stdout.split('\n').map((line) => line.trim()).filter(Boolean).length;
|
|
} catch (err) {
|
|
// lsof endet mit 1, wenn es nichts gefunden hat. Das ist der gute Fall.
|
|
if (err && err.code === 1) { return 0; }
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ein Repository indizieren, ueber die CLI und nicht ueber /rpc.
|
|
*
|
|
* Der Weg ist keine Bequemlichkeit: die Read-only-Allowlist des Servers bietet
|
|
* index_repository auf /rpc gar nicht an. Wer indizieren will, ruft die CLI,
|
|
* und genau das haelt der Beweislauf hier fest.
|
|
*/
|
|
export async function indexRepository(binary, { home, repoPath, project, mode = 'full' }) {
|
|
const started = Date.now();
|
|
const payload = JSON.stringify({ repo_path: repoPath, name: project, mode });
|
|
const result = await new Promise((resolve, reject) => {
|
|
const child = spawn(binary, ['cli', 'index_repository'], {
|
|
env: { ...process.env, HOME: home },
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
let out = '';
|
|
let err = '';
|
|
child.stdout.on('data', (d) => { out += d.toString(); });
|
|
child.stderr.on('data', (d) => { err += d.toString(); });
|
|
child.on('error', reject);
|
|
child.on('close', (code) => resolve({ code, out, err }));
|
|
child.stdin.end(payload + '\n');
|
|
});
|
|
|
|
const line = result.out.split('\n').map((l) => l.trim()).filter(Boolean).pop();
|
|
if (!line) {
|
|
throw new Error(
|
|
`index_repository lieferte keine Ausgabe (exit ${result.code}): ${result.err.trim()}`,
|
|
);
|
|
}
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(line);
|
|
} catch {
|
|
throw new Error(`index_repository lieferte kein JSON: ${line.slice(0, 400)}`);
|
|
}
|
|
if (parsed.status !== 'indexed' && parsed.nodes === undefined) {
|
|
throw new Error(`index_repository status=${parsed.status}: ${line.slice(0, 400)}`);
|
|
}
|
|
return { ...parsed, durationMs: Date.now() - started, stderr: result.err.trim() };
|
|
}
|
|
|
|
const INITIALIZE = JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
method: 'initialize',
|
|
params: {
|
|
protocolVersion: '2025-06-18',
|
|
capabilities: {},
|
|
clientInfo: { name: 'codeatlasweb-smoke', version: '0.0.1' },
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Den Server starten und warten, bis er auf 127.0.0.1:port antwortet.
|
|
*
|
|
* `log` sammelt die Ausgaben des Prozesses, damit ein vorzeitiges Ende nicht
|
|
* als Zeitueberschreitung ohne Begruendung endet.
|
|
*/
|
|
export async function startServer(binary, { home, port, log = [] }) {
|
|
const started = Date.now();
|
|
const child = spawn(binary, ['--ui=true', `--port=${port}`], {
|
|
env: { ...process.env, HOME: home },
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
child.stdout.on('data', (d) => { log.push(`[server stdout] ${d.toString().trim()}`); });
|
|
child.stderr.on('data', (d) => { log.push(`[server stderr] ${d.toString().trim()}`); });
|
|
|
|
let exited = null;
|
|
child.on('exit', (code, signal) => { exited = { code, signal }; });
|
|
child.stdin.write(INITIALIZE + '\n');
|
|
|
|
const deadline = Date.now() + SERVER_READY_TIMEOUT_MS;
|
|
while (Date.now() < deadline) {
|
|
if (exited) {
|
|
throw new Error(
|
|
`C-Server beendete sich vorzeitig (code=${exited.code}, signal=${exited.signal})\n` +
|
|
log.slice(-20).join('\n'),
|
|
);
|
|
}
|
|
try {
|
|
const res = await fetch(`http://127.0.0.1:${port}/`, { redirect: 'manual' });
|
|
await res.arrayBuffer();
|
|
if (res.status === 200) {
|
|
return { child, durationMs: Date.now() - started };
|
|
}
|
|
} catch {
|
|
// noch nicht da
|
|
}
|
|
await sleep(200);
|
|
}
|
|
throw new Error(
|
|
`C-Server wurde binnen ${SERVER_READY_TIMEOUT_MS} ms nicht auf 127.0.0.1:${port} erreichbar\n` +
|
|
log.slice(-20).join('\n'),
|
|
);
|
|
}
|
|
|
|
/** Den Prozess beenden, erst hoeflich und dann nicht mehr. */
|
|
export async function stopServer(child, label = 'C-Server') {
|
|
if (!child || child.exitCode !== null || child.killed) { return; }
|
|
child.kill('SIGTERM');
|
|
for (let i = 0; i < 50; i += 1) {
|
|
if (child.exitCode !== null || child.signalCode !== null) { return; }
|
|
await sleep(100);
|
|
}
|
|
console.error(`[smoke] ${label} reagierte nicht auf SIGTERM, sende SIGKILL`);
|
|
child.kill('SIGKILL');
|
|
await sleep(300);
|
|
}
|