@rubytech/create-realagent 1.0.628 → 1.0.631
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/dist/index.js +51 -4
- package/package.json +1 -1
- package/payload/platform/lib/graph-mcp/dist/index.d.ts +26 -0
- package/payload/platform/lib/graph-mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/index.js +193 -0
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-mcp/src/index.ts +225 -0
- package/payload/platform/lib/graph-mcp/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +12 -1
- package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +4 -1
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +70 -40
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/memory-guide.md +8 -0
- package/payload/platform/plugins/memory/mcp/scripts/graph/accept.sh +129 -0
- package/payload/platform/plugins/memory/mcp/scripts/graph/fixture.cypher +59 -0
- package/payload/platform/plugins/memory/references/graph-primitives.md +195 -0
- package/payload/server/public/assets/admin-BntwbBs-.js +352 -0
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +349 -31
- package/payload/server/public/assets/admin-CGIu9HnV.js +0 -352
package/dist/index.js
CHANGED
|
@@ -384,7 +384,10 @@ function installClaudeCode() {
|
|
|
384
384
|
let needsInstall = true;
|
|
385
385
|
if (commandExists("claude")) {
|
|
386
386
|
try {
|
|
387
|
-
|
|
387
|
+
// `claude --version` prints "2.1.114 (Claude Code)" — extract the semver so
|
|
388
|
+
// the equality check against `npm view` (which returns bare "2.1.114") works.
|
|
389
|
+
const rawVersion = execFileSync("claude", ["--version"], { encoding: "utf-8", timeout: 10_000 }).trim();
|
|
390
|
+
const installed = rawVersion.match(/^(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/)?.[1] ?? rawVersion;
|
|
388
391
|
let latest = null;
|
|
389
392
|
try {
|
|
390
393
|
latest = execFileSync("npm", ["view", "@anthropic-ai/claude-code", "version"], { encoding: "utf-8", timeout: 30_000 }).trim();
|
|
@@ -408,9 +411,19 @@ function installClaudeCode() {
|
|
|
408
411
|
log("3", TOTAL, "Installing Claude Code...");
|
|
409
412
|
}
|
|
410
413
|
if (needsInstall) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
+
// `npm install -g` needs write access to the global prefix, which on Linux is
|
|
415
|
+
// root-owned by default — so we run it under sudo. When sudo requires a password
|
|
416
|
+
// and the installer is running non-interactively (e.g. systemd-run --scope on
|
|
417
|
+
// upgrade), sudo fails instantly. Skip the upgrade in that case; the running
|
|
418
|
+
// installation is assumed adequate. Matches the apt-get skip in step 1.
|
|
419
|
+
if (isLinux() && !canSudo()) {
|
|
420
|
+
console.log(" Skipping Claude Code upgrade (sudo unavailable non-interactively — keeping installed version)");
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
console.log(" This may take 15–30 minutes on Raspberry Pi...");
|
|
424
|
+
shellRetry("npm", ["install", "-g", ...NPM_NET_FLAGS, "--loglevel", "verbose", "@anthropic-ai/claude-code@latest"], { sudo: true, timeout: 2_400_000 }, // 40 min — Pi downloads can take 25+ min
|
|
425
|
+
3, 30);
|
|
426
|
+
}
|
|
414
427
|
}
|
|
415
428
|
console.log(" Registering claude-plugins-official marketplace...");
|
|
416
429
|
const marketplaceList = spawnSync("claude", ["plugin", "marketplace", "list"], { stdio: "pipe", encoding: "utf-8" });
|
|
@@ -813,6 +826,39 @@ function installOllama(embedModel) {
|
|
|
813
826
|
logFile(` Pulling embedding model: ${embedModel}`);
|
|
814
827
|
shellRetry("ollama", ["pull", embedModel], { timeout: 600_000 }, 3, 15);
|
|
815
828
|
}
|
|
829
|
+
// Task 557 — uv/uvx bootstrap. Required by the `graph` MCP server which
|
|
830
|
+
// spawns `uvx mcp-neo4j-cypher@0.6.0 --transport stdio`. Idempotent: when
|
|
831
|
+
// uvx is already on PATH (or under $HOME/.local/bin) we skip. Non-fatal on
|
|
832
|
+
// failure — the installer continues; the graph server will loudly fail
|
|
833
|
+
// at session start with a clear "uvx not found" error, which is retriable
|
|
834
|
+
// via a second installer run with network access.
|
|
835
|
+
function installUv() {
|
|
836
|
+
if (commandExists("uvx")) {
|
|
837
|
+
logFile(" uv: already installed");
|
|
838
|
+
console.log(" uv/uvx already installed.");
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
console.log(" Installing uv (Python tool runner — required by Neo4j MCP server)...");
|
|
842
|
+
logFile(" uv: installing via astral.sh installer");
|
|
843
|
+
const result = spawnSync("bash", ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh -s -- -y"], { stdio: "inherit" });
|
|
844
|
+
if (result.status !== 0) {
|
|
845
|
+
console.error(` WARNING: uv install exited ${result.status} — graph MCP server will fail at session start until this is retried`);
|
|
846
|
+
logFile(` WARNING: uv install failed with status ${result.status}`);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
// The installer writes uvx to $HOME/.local/bin — add it to PATH for the
|
|
850
|
+
// remainder of this install so commandExists("uvx") works downstream.
|
|
851
|
+
const localBin = `${process.env.HOME}/.local/bin`;
|
|
852
|
+
if (process.env.PATH && !process.env.PATH.includes(localBin)) {
|
|
853
|
+
process.env.PATH = `${localBin}:${process.env.PATH}`;
|
|
854
|
+
}
|
|
855
|
+
if (commandExists("uvx")) {
|
|
856
|
+
logFile(" uv: install succeeded; uvx on PATH");
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
console.error(" WARNING: uv installed but uvx not on PATH — check $HOME/.local/bin");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
816
862
|
function installCloudflared() {
|
|
817
863
|
if (commandExists("cloudflared")) {
|
|
818
864
|
log("6", TOTAL, "Cloudflared already installed.");
|
|
@@ -1937,6 +1983,7 @@ try {
|
|
|
1937
1983
|
installNeo4j();
|
|
1938
1984
|
setupDedicatedNeo4j();
|
|
1939
1985
|
installOllama(EMBED_MODEL);
|
|
1986
|
+
installUv();
|
|
1940
1987
|
installCloudflared();
|
|
1941
1988
|
installWhisperCpp();
|
|
1942
1989
|
deployPayload(); // Must happen before ensureNeo4jPassword — restores config backup
|
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Graph MCP shim — adopts upstream `mcp-neo4j-cypher` per brand.
|
|
4
|
+
*
|
|
5
|
+
* Task 557. Claude Code spawns this file as the `graph` MCP server; it
|
|
6
|
+
* starts the upstream Python server under `uvx` and proxies the JSON-RPC
|
|
7
|
+
* stdio stream byte-for-byte. While proxying, it:
|
|
8
|
+
* 1. Resolves `NEO4J_URI/USERNAME/PASSWORD` from the brand's own env
|
|
9
|
+
* (falls back to `${PLATFORM_ROOT}/config/.neo4j-password`, matching
|
|
10
|
+
* plugins/memory/mcp/src/lib/neo4j.ts:10-21).
|
|
11
|
+
* 2. Installs the Maxy stderr tee so upstream stderr lands in the
|
|
12
|
+
* per-conversation stream log alongside every other plugin.
|
|
13
|
+
* 3. Locks the upstream server into read-only mode (`NEO4J_READ_ONLY=true`)
|
|
14
|
+
* and a stable Maxy namespace (`NEO4J_NAMESPACE=maxy-graph`).
|
|
15
|
+
* 4. Parses JSON-RPC lines on the request path to record the cypher and
|
|
16
|
+
* start time per `id`, and on the response path to emit one
|
|
17
|
+
* `[graph-query] op=... brand=... port=... cypher="..." rows=... ms=...`
|
|
18
|
+
* line per tool call. Forwarding is byte-accurate — the parser
|
|
19
|
+
* observes, never mutates.
|
|
20
|
+
*
|
|
21
|
+
* Isolation model per MAXY-PRD.md:627 and create-maxy src/index.ts:504-824:
|
|
22
|
+
* each brand already owns its own Neo4j instance on its own port with its
|
|
23
|
+
* own password. This shim trusts that — no query-layer tenant filtering.
|
|
24
|
+
*/
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Graph MCP shim — adopts upstream `mcp-neo4j-cypher` per brand.
|
|
5
|
+
*
|
|
6
|
+
* Task 557. Claude Code spawns this file as the `graph` MCP server; it
|
|
7
|
+
* starts the upstream Python server under `uvx` and proxies the JSON-RPC
|
|
8
|
+
* stdio stream byte-for-byte. While proxying, it:
|
|
9
|
+
* 1. Resolves `NEO4J_URI/USERNAME/PASSWORD` from the brand's own env
|
|
10
|
+
* (falls back to `${PLATFORM_ROOT}/config/.neo4j-password`, matching
|
|
11
|
+
* plugins/memory/mcp/src/lib/neo4j.ts:10-21).
|
|
12
|
+
* 2. Installs the Maxy stderr tee so upstream stderr lands in the
|
|
13
|
+
* per-conversation stream log alongside every other plugin.
|
|
14
|
+
* 3. Locks the upstream server into read-only mode (`NEO4J_READ_ONLY=true`)
|
|
15
|
+
* and a stable Maxy namespace (`NEO4J_NAMESPACE=maxy-graph`).
|
|
16
|
+
* 4. Parses JSON-RPC lines on the request path to record the cypher and
|
|
17
|
+
* start time per `id`, and on the response path to emit one
|
|
18
|
+
* `[graph-query] op=... brand=... port=... cypher="..." rows=... ms=...`
|
|
19
|
+
* line per tool call. Forwarding is byte-accurate — the parser
|
|
20
|
+
* observes, never mutates.
|
|
21
|
+
*
|
|
22
|
+
* Isolation model per MAXY-PRD.md:627 and create-maxy src/index.ts:504-824:
|
|
23
|
+
* each brand already owns its own Neo4j instance on its own port with its
|
|
24
|
+
* own password. This shim trusts that — no query-layer tenant filtering.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
const node_child_process_1 = require("node:child_process");
|
|
28
|
+
const node_fs_1 = require("node:fs");
|
|
29
|
+
const node_path_1 = require("node:path");
|
|
30
|
+
const node_string_decoder_1 = require("node:string_decoder");
|
|
31
|
+
const index_js_1 = require("../../mcp-stderr-tee/dist/index.js");
|
|
32
|
+
const SERVER_NAME = "graph";
|
|
33
|
+
const UPSTREAM_PACKAGE = "mcp-neo4j-cypher@0.6.0";
|
|
34
|
+
(0, index_js_1.initStderrTee)(SERVER_NAME);
|
|
35
|
+
function resolvePassword() {
|
|
36
|
+
if (process.env.NEO4J_PASSWORD)
|
|
37
|
+
return process.env.NEO4J_PASSWORD;
|
|
38
|
+
// __dirname points at `lib/graph-mcp/dist` after compilation; the platform
|
|
39
|
+
// root is three levels up (lib/graph-mcp/dist -> lib/graph-mcp -> lib -> platform).
|
|
40
|
+
const root = process.env.PLATFORM_ROOT ?? (0, node_path_1.resolve)(__dirname, "../../..");
|
|
41
|
+
const file = (0, node_path_1.resolve)(root, "config/.neo4j-password");
|
|
42
|
+
try {
|
|
43
|
+
return (0, node_fs_1.readFileSync)(file, "utf-8").trim();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.error(`[graph-mcp] password unavailable — file=${file} env=NEO4J_PASSWORD (neither set)`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const brand = process.env.BRAND ?? "maxy";
|
|
51
|
+
const neo4jUri = process.env.NEO4J_URI ?? "bolt://localhost:7687";
|
|
52
|
+
const neo4jUser = process.env.NEO4J_USERNAME ?? process.env.NEO4J_USER ?? "neo4j";
|
|
53
|
+
const neo4jPassword = resolvePassword();
|
|
54
|
+
const portMatch = /:(\d+)$/.exec(neo4jUri);
|
|
55
|
+
const neo4jPort = portMatch ? portMatch[1] : "?";
|
|
56
|
+
const namespace = process.env.NEO4J_NAMESPACE ?? "maxy-graph";
|
|
57
|
+
const readOnly = process.env.NEO4J_READ_ONLY ?? "true";
|
|
58
|
+
const responseTokenLimit = process.env.NEO4J_RESPONSE_TOKEN_LIMIT ?? "20000";
|
|
59
|
+
const childEnv = {
|
|
60
|
+
...process.env,
|
|
61
|
+
NEO4J_URI: neo4jUri,
|
|
62
|
+
NEO4J_URL: neo4jUri,
|
|
63
|
+
NEO4J_USERNAME: neo4jUser,
|
|
64
|
+
NEO4J_PASSWORD: neo4jPassword,
|
|
65
|
+
NEO4J_READ_ONLY: readOnly,
|
|
66
|
+
NEO4J_NAMESPACE: namespace,
|
|
67
|
+
NEO4J_RESPONSE_TOKEN_LIMIT: responseTokenLimit,
|
|
68
|
+
};
|
|
69
|
+
console.error(`[graph-mcp] boot brand=${brand} uri=${neo4jUri} user=${neo4jUser} ` +
|
|
70
|
+
`namespace=${namespace} readOnly=${readOnly} tokenLimit=${responseTokenLimit}`);
|
|
71
|
+
const child = (0, node_child_process_1.spawn)("uvx", [UPSTREAM_PACKAGE, "--transport", "stdio"], {
|
|
72
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
73
|
+
env: childEnv,
|
|
74
|
+
});
|
|
75
|
+
child.on("spawn", () => {
|
|
76
|
+
console.error(`[graph-mcp] spawned ${UPSTREAM_PACKAGE} pid=${child.pid}`);
|
|
77
|
+
});
|
|
78
|
+
child.on("error", (err) => {
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
console.error(`[graph-mcp] spawn error: ${msg}`);
|
|
81
|
+
if (err.code === "ENOENT") {
|
|
82
|
+
console.error("[graph-mcp] uvx not found on PATH — install uv: " +
|
|
83
|
+
"curl -LsSf https://astral.sh/uv/install.sh | sh -s -- -y");
|
|
84
|
+
}
|
|
85
|
+
process.exit(127);
|
|
86
|
+
});
|
|
87
|
+
child.on("exit", (code, signal) => {
|
|
88
|
+
console.error(`[graph-mcp] uvx exited code=${code ?? "null"} signal=${signal ?? "null"}`);
|
|
89
|
+
process.exit(code ?? (signal ? 1 : 0));
|
|
90
|
+
});
|
|
91
|
+
// Forward child stderr to our own stderr so the stderr tee picks it up.
|
|
92
|
+
child.stderr.on("data", (chunk) => {
|
|
93
|
+
process.stderr.write(chunk);
|
|
94
|
+
});
|
|
95
|
+
const pending = new Map();
|
|
96
|
+
function truncate(s, n) {
|
|
97
|
+
return s.length <= n ? s : `${s.slice(0, n)}...`;
|
|
98
|
+
}
|
|
99
|
+
function stripNamespace(toolName) {
|
|
100
|
+
if (!toolName)
|
|
101
|
+
return "(unknown)";
|
|
102
|
+
const prefix = `${namespace}_`;
|
|
103
|
+
return toolName.startsWith(prefix) ? toolName.slice(prefix.length) : toolName;
|
|
104
|
+
}
|
|
105
|
+
function extractCypher(args) {
|
|
106
|
+
if (!args)
|
|
107
|
+
return null;
|
|
108
|
+
const q = args["query"] ?? args["cypher"];
|
|
109
|
+
return typeof q === "string" ? truncate(q.replace(/\s+/g, " ").trim(), 80) : null;
|
|
110
|
+
}
|
|
111
|
+
function countRows(result) {
|
|
112
|
+
if (!result?.content || !Array.isArray(result.content))
|
|
113
|
+
return "?";
|
|
114
|
+
const text = result.content[0]?.text ?? "";
|
|
115
|
+
const rowsMatch = /(\d+)\s+(?:rows?|records?)/i.exec(text);
|
|
116
|
+
if (rowsMatch)
|
|
117
|
+
return rowsMatch[1];
|
|
118
|
+
return String(result.content.length);
|
|
119
|
+
}
|
|
120
|
+
function onRequestLine(line) {
|
|
121
|
+
try {
|
|
122
|
+
const msg = JSON.parse(line);
|
|
123
|
+
if (msg.method === "tools/call" && msg.id !== undefined) {
|
|
124
|
+
pending.set(msg.id, {
|
|
125
|
+
method: stripNamespace(msg.params?.name),
|
|
126
|
+
cypherPrefix: extractCypher(msg.params?.arguments),
|
|
127
|
+
startMs: Date.now(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Non-JSON / partial line — forward untouched, don't log.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function onResponseLine(line) {
|
|
136
|
+
try {
|
|
137
|
+
const msg = JSON.parse(line);
|
|
138
|
+
if (msg.id === undefined || !pending.has(msg.id))
|
|
139
|
+
return;
|
|
140
|
+
const p = pending.get(msg.id);
|
|
141
|
+
pending.delete(msg.id);
|
|
142
|
+
const elapsed = Date.now() - p.startMs;
|
|
143
|
+
const cypherField = p.cypherPrefix ? `cypher="${p.cypherPrefix.replace(/"/g, "'")}"` : "";
|
|
144
|
+
if (msg.error) {
|
|
145
|
+
const errText = (msg.error.message ?? JSON.stringify(msg.error)).replace(/"/g, "'");
|
|
146
|
+
console.error(`[graph-query] op=${p.method} brand=${brand} port=${neo4jPort} ${cypherField} error="${errText}" ms=${elapsed}`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const rows = countRows(msg.result);
|
|
150
|
+
console.error(`[graph-query] op=${p.method} brand=${brand} port=${neo4jPort} ${cypherField} rows=${rows} ms=${elapsed}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Non-JSON — forward untouched.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function makeLineSplitter(onLine) {
|
|
158
|
+
const decoder = new node_string_decoder_1.StringDecoder("utf8");
|
|
159
|
+
let buf = "";
|
|
160
|
+
return (chunk) => {
|
|
161
|
+
buf += decoder.write(chunk);
|
|
162
|
+
let idx;
|
|
163
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
164
|
+
const line = buf.slice(0, idx);
|
|
165
|
+
buf = buf.slice(idx + 1);
|
|
166
|
+
if (line.length > 0)
|
|
167
|
+
onLine(line);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const splitRequest = makeLineSplitter(onRequestLine);
|
|
172
|
+
process.stdin.on("data", (chunk) => {
|
|
173
|
+
splitRequest(chunk);
|
|
174
|
+
child.stdin.write(chunk);
|
|
175
|
+
});
|
|
176
|
+
process.stdin.on("end", () => {
|
|
177
|
+
child.stdin.end();
|
|
178
|
+
});
|
|
179
|
+
const splitResponse = makeLineSplitter(onResponseLine);
|
|
180
|
+
child.stdout.on("data", (chunk) => {
|
|
181
|
+
splitResponse(chunk);
|
|
182
|
+
process.stdout.write(chunk);
|
|
183
|
+
});
|
|
184
|
+
child.stdout.on("end", () => {
|
|
185
|
+
process.stdout.end();
|
|
186
|
+
});
|
|
187
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
188
|
+
process.on(sig, () => {
|
|
189
|
+
console.error(`[graph-mcp] ${sig} received — forwarding to child`);
|
|
190
|
+
child.kill(sig);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Graph MCP shim — adopts upstream `mcp-neo4j-cypher` per brand.
|
|
4
|
+
*
|
|
5
|
+
* Task 557. Claude Code spawns this file as the `graph` MCP server; it
|
|
6
|
+
* starts the upstream Python server under `uvx` and proxies the JSON-RPC
|
|
7
|
+
* stdio stream byte-for-byte. While proxying, it:
|
|
8
|
+
* 1. Resolves `NEO4J_URI/USERNAME/PASSWORD` from the brand's own env
|
|
9
|
+
* (falls back to `${PLATFORM_ROOT}/config/.neo4j-password`, matching
|
|
10
|
+
* plugins/memory/mcp/src/lib/neo4j.ts:10-21).
|
|
11
|
+
* 2. Installs the Maxy stderr tee so upstream stderr lands in the
|
|
12
|
+
* per-conversation stream log alongside every other plugin.
|
|
13
|
+
* 3. Locks the upstream server into read-only mode (`NEO4J_READ_ONLY=true`)
|
|
14
|
+
* and a stable Maxy namespace (`NEO4J_NAMESPACE=maxy-graph`).
|
|
15
|
+
* 4. Parses JSON-RPC lines on the request path to record the cypher and
|
|
16
|
+
* start time per `id`, and on the response path to emit one
|
|
17
|
+
* `[graph-query] op=... brand=... port=... cypher="..." rows=... ms=...`
|
|
18
|
+
* line per tool call. Forwarding is byte-accurate — the parser
|
|
19
|
+
* observes, never mutates.
|
|
20
|
+
*
|
|
21
|
+
* Isolation model per MAXY-PRD.md:627 and create-maxy src/index.ts:504-824:
|
|
22
|
+
* each brand already owns its own Neo4j instance on its own port with its
|
|
23
|
+
* own password. This shim trusts that — no query-layer tenant filtering.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { spawn } from "node:child_process";
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
|
+
import { resolve } from "node:path";
|
|
29
|
+
import { StringDecoder } from "node:string_decoder";
|
|
30
|
+
import { initStderrTee } from "../../mcp-stderr-tee/dist/index.js";
|
|
31
|
+
|
|
32
|
+
const SERVER_NAME = "graph";
|
|
33
|
+
const UPSTREAM_PACKAGE = "mcp-neo4j-cypher@0.6.0";
|
|
34
|
+
|
|
35
|
+
initStderrTee(SERVER_NAME);
|
|
36
|
+
|
|
37
|
+
function resolvePassword(): string {
|
|
38
|
+
if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
|
|
39
|
+
// __dirname points at `lib/graph-mcp/dist` after compilation; the platform
|
|
40
|
+
// root is three levels up (lib/graph-mcp/dist -> lib/graph-mcp -> lib -> platform).
|
|
41
|
+
const root = process.env.PLATFORM_ROOT ?? resolve(__dirname, "../../..");
|
|
42
|
+
const file = resolve(root, "config/.neo4j-password");
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(file, "utf-8").trim();
|
|
45
|
+
} catch {
|
|
46
|
+
console.error(`[graph-mcp] password unavailable — file=${file} env=NEO4J_PASSWORD (neither set)`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const brand = process.env.BRAND ?? "maxy";
|
|
52
|
+
const neo4jUri = process.env.NEO4J_URI ?? "bolt://localhost:7687";
|
|
53
|
+
const neo4jUser = process.env.NEO4J_USERNAME ?? process.env.NEO4J_USER ?? "neo4j";
|
|
54
|
+
const neo4jPassword = resolvePassword();
|
|
55
|
+
const portMatch = /:(\d+)$/.exec(neo4jUri);
|
|
56
|
+
const neo4jPort = portMatch ? portMatch[1] : "?";
|
|
57
|
+
const namespace = process.env.NEO4J_NAMESPACE ?? "maxy-graph";
|
|
58
|
+
const readOnly = process.env.NEO4J_READ_ONLY ?? "true";
|
|
59
|
+
const responseTokenLimit = process.env.NEO4J_RESPONSE_TOKEN_LIMIT ?? "20000";
|
|
60
|
+
|
|
61
|
+
const childEnv: NodeJS.ProcessEnv = {
|
|
62
|
+
...process.env,
|
|
63
|
+
NEO4J_URI: neo4jUri,
|
|
64
|
+
NEO4J_URL: neo4jUri,
|
|
65
|
+
NEO4J_USERNAME: neo4jUser,
|
|
66
|
+
NEO4J_PASSWORD: neo4jPassword,
|
|
67
|
+
NEO4J_READ_ONLY: readOnly,
|
|
68
|
+
NEO4J_NAMESPACE: namespace,
|
|
69
|
+
NEO4J_RESPONSE_TOKEN_LIMIT: responseTokenLimit,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
console.error(
|
|
73
|
+
`[graph-mcp] boot brand=${brand} uri=${neo4jUri} user=${neo4jUser} ` +
|
|
74
|
+
`namespace=${namespace} readOnly=${readOnly} tokenLimit=${responseTokenLimit}`,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const child = spawn("uvx", [UPSTREAM_PACKAGE, "--transport", "stdio"], {
|
|
78
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
79
|
+
env: childEnv,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
child.on("spawn", () => {
|
|
83
|
+
console.error(`[graph-mcp] spawned ${UPSTREAM_PACKAGE} pid=${child.pid}`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
child.on("error", (err) => {
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
console.error(`[graph-mcp] spawn error: ${msg}`);
|
|
89
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
90
|
+
console.error(
|
|
91
|
+
"[graph-mcp] uvx not found on PATH — install uv: " +
|
|
92
|
+
"curl -LsSf https://astral.sh/uv/install.sh | sh -s -- -y",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
process.exit(127);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
child.on("exit", (code, signal) => {
|
|
99
|
+
console.error(`[graph-mcp] uvx exited code=${code ?? "null"} signal=${signal ?? "null"}`);
|
|
100
|
+
process.exit(code ?? (signal ? 1 : 0));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Forward child stderr to our own stderr so the stderr tee picks it up.
|
|
104
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
105
|
+
process.stderr.write(chunk);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// --- JSON-RPC call correlation ---
|
|
109
|
+
// tools/call is the only method we time; initialize and tools/list are noise.
|
|
110
|
+
interface PendingCall {
|
|
111
|
+
method: string;
|
|
112
|
+
cypherPrefix: string | null;
|
|
113
|
+
startMs: number;
|
|
114
|
+
}
|
|
115
|
+
const pending = new Map<string | number, PendingCall>();
|
|
116
|
+
|
|
117
|
+
function truncate(s: string, n: number): string {
|
|
118
|
+
return s.length <= n ? s : `${s.slice(0, n)}...`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function stripNamespace(toolName: string | undefined): string {
|
|
122
|
+
if (!toolName) return "(unknown)";
|
|
123
|
+
const prefix = `${namespace}_`;
|
|
124
|
+
return toolName.startsWith(prefix) ? toolName.slice(prefix.length) : toolName;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type JsonRpcMessage = {
|
|
128
|
+
id?: string | number;
|
|
129
|
+
method?: string;
|
|
130
|
+
params?: { name?: string; arguments?: Record<string, unknown> };
|
|
131
|
+
result?: { content?: Array<{ text?: string; type?: string }> };
|
|
132
|
+
error?: { message?: string; code?: number };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function extractCypher(args: Record<string, unknown> | undefined): string | null {
|
|
136
|
+
if (!args) return null;
|
|
137
|
+
const q = args["query"] ?? args["cypher"];
|
|
138
|
+
return typeof q === "string" ? truncate(q.replace(/\s+/g, " ").trim(), 80) : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function countRows(result: JsonRpcMessage["result"]): string {
|
|
142
|
+
if (!result?.content || !Array.isArray(result.content)) return "?";
|
|
143
|
+
const text = result.content[0]?.text ?? "";
|
|
144
|
+
const rowsMatch = /(\d+)\s+(?:rows?|records?)/i.exec(text);
|
|
145
|
+
if (rowsMatch) return rowsMatch[1];
|
|
146
|
+
return String(result.content.length);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function onRequestLine(line: string): void {
|
|
150
|
+
try {
|
|
151
|
+
const msg = JSON.parse(line) as JsonRpcMessage;
|
|
152
|
+
if (msg.method === "tools/call" && msg.id !== undefined) {
|
|
153
|
+
pending.set(msg.id, {
|
|
154
|
+
method: stripNamespace(msg.params?.name),
|
|
155
|
+
cypherPrefix: extractCypher(msg.params?.arguments),
|
|
156
|
+
startMs: Date.now(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Non-JSON / partial line — forward untouched, don't log.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function onResponseLine(line: string): void {
|
|
165
|
+
try {
|
|
166
|
+
const msg = JSON.parse(line) as JsonRpcMessage;
|
|
167
|
+
if (msg.id === undefined || !pending.has(msg.id)) return;
|
|
168
|
+
const p = pending.get(msg.id)!;
|
|
169
|
+
pending.delete(msg.id);
|
|
170
|
+
const elapsed = Date.now() - p.startMs;
|
|
171
|
+
const cypherField = p.cypherPrefix ? `cypher="${p.cypherPrefix.replace(/"/g, "'")}"` : "";
|
|
172
|
+
if (msg.error) {
|
|
173
|
+
const errText = (msg.error.message ?? JSON.stringify(msg.error)).replace(/"/g, "'");
|
|
174
|
+
console.error(
|
|
175
|
+
`[graph-query] op=${p.method} brand=${brand} port=${neo4jPort} ${cypherField} error="${errText}" ms=${elapsed}`,
|
|
176
|
+
);
|
|
177
|
+
} else {
|
|
178
|
+
const rows = countRows(msg.result);
|
|
179
|
+
console.error(
|
|
180
|
+
`[graph-query] op=${p.method} brand=${brand} port=${neo4jPort} ${cypherField} rows=${rows} ms=${elapsed}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Non-JSON — forward untouched.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function makeLineSplitter(onLine: (line: string) => void): (chunk: Buffer) => void {
|
|
189
|
+
const decoder = new StringDecoder("utf8");
|
|
190
|
+
let buf = "";
|
|
191
|
+
return (chunk: Buffer) => {
|
|
192
|
+
buf += decoder.write(chunk);
|
|
193
|
+
let idx: number;
|
|
194
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
195
|
+
const line = buf.slice(0, idx);
|
|
196
|
+
buf = buf.slice(idx + 1);
|
|
197
|
+
if (line.length > 0) onLine(line);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const splitRequest = makeLineSplitter(onRequestLine);
|
|
203
|
+
process.stdin.on("data", (chunk: Buffer) => {
|
|
204
|
+
splitRequest(chunk);
|
|
205
|
+
child.stdin.write(chunk);
|
|
206
|
+
});
|
|
207
|
+
process.stdin.on("end", () => {
|
|
208
|
+
child.stdin.end();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const splitResponse = makeLineSplitter(onResponseLine);
|
|
212
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
213
|
+
splitResponse(chunk);
|
|
214
|
+
process.stdout.write(chunk);
|
|
215
|
+
});
|
|
216
|
+
child.stdout.on("end", () => {
|
|
217
|
+
process.stdout.end();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
|
|
221
|
+
process.on(sig, () => {
|
|
222
|
+
console.error(`[graph-mcp] ${sig} received — forwarding to child`);
|
|
223
|
+
child.kill(sig);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"plugins/*/mcp"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
-
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
|
|
9
|
+
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
+
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
|
|
11
11
|
"build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
|
|
12
12
|
"build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
|
|
13
13
|
"build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
|
|
@@ -298,13 +298,24 @@ Prints the UUID from Step 3. If it prints empty or null, the heredoc's env expan
|
|
|
298
298
|
|
|
299
299
|
You do **not** run `cloudflared` manually. The brand's existing user-space systemd unit (`~/.config/systemd/user/${BRAND}.service`) declares `ExecStartPre=/home/<user>/${BRAND}/platform/scripts/resume-tunnel.sh`, and that pre-start script reads `${CFG_DIR}/tunnel.state` and `${CFG_DIR}/config.yml` (the files Steps 5 and 5b just wrote) and spawns the connector in the user's cgroup. Restarting the brand service is what picks up the new config.
|
|
300
300
|
|
|
301
|
-
> **Note:** When walking through by hand you run this step yourself. The automation script `platform/plugins/cloudflare/scripts/setup-tunnel.sh` runs it for you —
|
|
301
|
+
> **Note:** When walking through by hand you run this step yourself. The automation script `platform/plugins/cloudflare/scripts/setup-tunnel.sh` runs it for you — with a critical twist documented below. If you used the script, this step is already done and the service will restart a few seconds after the script exits.
|
|
302
302
|
|
|
303
303
|
|
|
304
304
|
```
|
|
305
305
|
systemctl --user restart "${BRAND}.service"
|
|
306
306
|
```
|
|
307
307
|
|
|
308
|
+
**Why the script dispatches the restart via `systemd-run` instead of a direct `systemctl restart` (Task 558):** when the admin agent invokes `setup-tunnel.sh` via the Bash tool, the script runs *inside* `${BRAND}.service`'s cgroup. A direct `systemctl --user restart ${BRAND}.service` from that cgroup tells systemd to SIGTERM the entire cgroup — the node server, the claude subprocess, the Bash child, and the script itself all die simultaneously. cgroup membership is inherited: `setsid`, `nohup`, `disown`, and `&` all stay in the caller's cgroup, and `systemd-run --scope` runs in the caller's scope. Only `systemd-run --user --unit=<name> --on-active=<N>s` creates a genuinely new transient unit with its own cgroup. The script uses that primitive to arm the restart a few seconds after its own exit:
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
systemd-run --user --unit=maxy-tunnel-restart-<nonce>.service --on-active=3s --collect \
|
|
312
|
+
/bin/systemctl --user restart "${BRAND}.service"
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The script then emits `[setup-tunnel] step=service-restart-dispatched` and `step=service-restart-armed exit=0` in the per-conversation stream log so operators see exactly when the restart was scheduled, exits 0, and the transient timer fires from outside the service's cgroup — semantically identical to this manual runbook's `systemctl --user restart`.
|
|
316
|
+
|
|
317
|
+
When walking through manually you do **not** need `systemd-run` — your SSH shell already lives in a separate user-scope cgroup (`user@<uid>.service`), so the direct `systemctl restart` does not kill the caller. The script's extra indirection only matters when the caller *is* the service being restarted.
|
|
318
|
+
|
|
308
319
|
**Why:** `resume-tunnel.sh` is the deterministic, brand-scoped spawner. Running `cloudflared` manually duplicates the connector (two processes for one tunnel) and races the brand service on every service restart. The service path is the only correct production path.
|
|
309
320
|
|
|
310
321
|
**Success:**
|
|
@@ -27,7 +27,10 @@ set -euo pipefail
|
|
|
27
27
|
# --------------------------------------------------------------------------
|
|
28
28
|
|
|
29
29
|
# shellcheck source=_stream-log.sh
|
|
30
|
-
|
|
30
|
+
# Resolve symlinks before dirname — ~/reset-tunnel.sh is installed as a symlink
|
|
31
|
+
# (see packages/create-maxy/src/index.ts:installTunnelScripts), so the raw
|
|
32
|
+
# BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
|
|
33
|
+
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
|
|
31
34
|
require_stream_log_path reset-tunnel
|
|
32
35
|
|
|
33
36
|
if [ "$#" -lt 1 ]; then
|