@rubytech/create-realagent 1.0.627 → 1.0.630
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 +34 -0
- 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/scripts/_stream-log.sh +124 -0
- package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +45 -3
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +151 -10
- package/payload/platform/plugins/docs/references/memory-guide.md +8 -0
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -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-DirN63aF.js +352 -0
- package/payload/server/public/assets/public-Cizdj15i.js +5 -0
- package/payload/server/public/assets/useVoiceRecorder-DIV9KAk_.css +1 -0
- package/payload/server/public/assets/{useVoiceRecorder-CiYPZu3g.js → useVoiceRecorder-tbj4tUsl.js} +1 -1
- package/payload/server/public/index.html +3 -3
- package/payload/server/public/public.html +3 -3
- package/payload/server/server.js +481 -102
- package/payload/server/public/assets/admin-BxVuKRJZ.js +0 -352
- package/payload/server/public/assets/public-Bgm9WQFZ.js +0 -5
- package/payload/server/public/assets/useVoiceRecorder-BORuG_su.css +0 -1
package/dist/index.js
CHANGED
|
@@ -813,6 +813,39 @@ function installOllama(embedModel) {
|
|
|
813
813
|
logFile(` Pulling embedding model: ${embedModel}`);
|
|
814
814
|
shellRetry("ollama", ["pull", embedModel], { timeout: 600_000 }, 3, 15);
|
|
815
815
|
}
|
|
816
|
+
// Task 557 — uv/uvx bootstrap. Required by the `graph` MCP server which
|
|
817
|
+
// spawns `uvx mcp-neo4j-cypher@0.6.0 --transport stdio`. Idempotent: when
|
|
818
|
+
// uvx is already on PATH (or under $HOME/.local/bin) we skip. Non-fatal on
|
|
819
|
+
// failure — the installer continues; the graph server will loudly fail
|
|
820
|
+
// at session start with a clear "uvx not found" error, which is retriable
|
|
821
|
+
// via a second installer run with network access.
|
|
822
|
+
function installUv() {
|
|
823
|
+
if (commandExists("uvx")) {
|
|
824
|
+
logFile(" uv: already installed");
|
|
825
|
+
console.log(" uv/uvx already installed.");
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
console.log(" Installing uv (Python tool runner — required by Neo4j MCP server)...");
|
|
829
|
+
logFile(" uv: installing via astral.sh installer");
|
|
830
|
+
const result = spawnSync("bash", ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh -s -- -y"], { stdio: "inherit" });
|
|
831
|
+
if (result.status !== 0) {
|
|
832
|
+
console.error(` WARNING: uv install exited ${result.status} — graph MCP server will fail at session start until this is retried`);
|
|
833
|
+
logFile(` WARNING: uv install failed with status ${result.status}`);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
// The installer writes uvx to $HOME/.local/bin — add it to PATH for the
|
|
837
|
+
// remainder of this install so commandExists("uvx") works downstream.
|
|
838
|
+
const localBin = `${process.env.HOME}/.local/bin`;
|
|
839
|
+
if (process.env.PATH && !process.env.PATH.includes(localBin)) {
|
|
840
|
+
process.env.PATH = `${localBin}:${process.env.PATH}`;
|
|
841
|
+
}
|
|
842
|
+
if (commandExists("uvx")) {
|
|
843
|
+
logFile(" uv: install succeeded; uvx on PATH");
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
console.error(" WARNING: uv installed but uvx not on PATH — check $HOME/.local/bin");
|
|
847
|
+
}
|
|
848
|
+
}
|
|
816
849
|
function installCloudflared() {
|
|
817
850
|
if (commandExists("cloudflared")) {
|
|
818
851
|
log("6", TOTAL, "Cloudflared already installed.");
|
|
@@ -1937,6 +1970,7 @@ try {
|
|
|
1937
1970
|
installNeo4j();
|
|
1938
1971
|
setupDedicatedNeo4j();
|
|
1939
1972
|
installOllama(EMBED_MODEL);
|
|
1973
|
+
installUv();
|
|
1940
1974
|
installCloudflared();
|
|
1941
1975
|
installWhisperCpp();
|
|
1942
1976
|
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",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared stream-log helpers sourced by setup-tunnel.sh and reset-tunnel.sh.
|
|
3
|
+
#
|
|
4
|
+
# The platform exposes STREAM_LOG_PATH in the `claude` spawn env (Task 556);
|
|
5
|
+
# the Bash-tool subprocess inherits it, and opt-in scripts call these
|
|
6
|
+
# helpers to emit phase lines and tee subprocess output into the same
|
|
7
|
+
# per-conversation file the chat UI's server-side tailer reads.
|
|
8
|
+
#
|
|
9
|
+
# Contract (read by platform/ui/app/api/admin/chat/route.ts tailer and
|
|
10
|
+
# .docs/platform.md):
|
|
11
|
+
# [<ISO-ts>] [<scope>] <kv …>
|
|
12
|
+
# [<ISO-ts>] [<scope>:<subprocess-tag>] <raw line>
|
|
13
|
+
# where <scope> ∈ {setup-tunnel, reset-tunnel}. The tailer regex is
|
|
14
|
+
# ^\[[^]]+\] \[(setup-tunnel|reset-tunnel)(:[^]]+)?\]
|
|
15
|
+
# so any prefix change must be made on both sides atomically.
|
|
16
|
+
|
|
17
|
+
# Exit 1 loudly with the variable name and the invoking scope so direct-SSH
|
|
18
|
+
# invocations fail fast and the operator reads exactly what to set. No
|
|
19
|
+
# silent fallback — criterion 4 of Task 556.
|
|
20
|
+
require_stream_log_path() {
|
|
21
|
+
local scope="$1"
|
|
22
|
+
if [ -z "${STREAM_LOG_PATH:-}" ]; then
|
|
23
|
+
echo "ERROR [${scope}]: STREAM_LOG_PATH environment variable is unset." >&2
|
|
24
|
+
echo " This script tees subprocess output into a per-conversation" >&2
|
|
25
|
+
echo " stream log; it is meaningless without a target path." >&2
|
|
26
|
+
echo " The platform sets STREAM_LOG_PATH automatically for every" >&2
|
|
27
|
+
echo " \`claude\` spawn (Task 556). If you are invoking this" >&2
|
|
28
|
+
echo " script by hand, export it yourself, for example:" >&2
|
|
29
|
+
echo " export STREAM_LOG_PATH=\"\${HOME}/.maxy/logs/manual-invocation.log\"" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
mkdir -p "$(dirname "${STREAM_LOG_PATH}")"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# ISO-8601 UTC timestamp with millisecond precision.
|
|
36
|
+
stream_log_ts() {
|
|
37
|
+
date -u +"%Y-%m-%dT%H:%M:%S.%3NZ"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Append one phase line to STREAM_LOG_PATH AND echo to stderr so the Bash
|
|
41
|
+
# tool's stderr capture carries it. Both paths matter: the stream log is
|
|
42
|
+
# the live tailer surface; stderr is the exit-time Bash-tool surface.
|
|
43
|
+
#
|
|
44
|
+
# Usage: phase_line <scope> <key=value …>
|
|
45
|
+
phase_line() {
|
|
46
|
+
local scope="$1"; shift
|
|
47
|
+
local ts
|
|
48
|
+
ts="$(stream_log_ts)"
|
|
49
|
+
printf '[%s] [%s] %s\n' "${ts}" "${scope}" "$*" >> "${STREAM_LOG_PATH}"
|
|
50
|
+
printf '[%s] %s\n' "${scope}" "$*" >&2
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Tee a subprocess's combined stdout+stderr into STREAM_LOG_PATH line-by-line
|
|
54
|
+
# with the given tag, mirroring each line to stderr for the Bash tool.
|
|
55
|
+
# Returns the subprocess's exit code.
|
|
56
|
+
#
|
|
57
|
+
# Use for fire-and-forget subprocesses whose stdout does NOT need to be
|
|
58
|
+
# captured by the caller (the caller only cares about the exit code).
|
|
59
|
+
# For subprocesses whose stdout the caller must parse (e.g. JSON output
|
|
60
|
+
# feeding `jq`), use `tee_subprocess_capture` instead.
|
|
61
|
+
#
|
|
62
|
+
# Usage: tee_subprocess <tag> -- <cmd> <args …>
|
|
63
|
+
# Example: tee_subprocess reset-tunnel:cloudflared -- cloudflared --origincert … tunnel delete my-tunnel
|
|
64
|
+
#
|
|
65
|
+
# stdbuf forces line buffering; without it cloudflared holds its stdout in
|
|
66
|
+
# libc's 8 KB buffer until exit and the ≤ 1 s latency criterion fails.
|
|
67
|
+
tee_subprocess() {
|
|
68
|
+
local tag="$1"; shift
|
|
69
|
+
if [ "${1:-}" != "--" ]; then
|
|
70
|
+
echo "tee_subprocess: expected -- before command" >&2
|
|
71
|
+
return 2
|
|
72
|
+
fi
|
|
73
|
+
shift
|
|
74
|
+
# Prefer `stdbuf -oL -eL` to force line buffering on the subprocess; fall
|
|
75
|
+
# back to the bare command when stdbuf is unavailable (macOS dev). On the
|
|
76
|
+
# Pi (Linux), stdbuf is present via coreutils and cloudflared's output
|
|
77
|
+
# reaches the tee as soon as each line is written.
|
|
78
|
+
local -a buf_prefix=()
|
|
79
|
+
if command -v stdbuf >/dev/null 2>&1; then
|
|
80
|
+
buf_prefix=(stdbuf -oL -eL)
|
|
81
|
+
fi
|
|
82
|
+
"${buf_prefix[@]}" "$@" 2>&1 | while IFS= read -r line; do
|
|
83
|
+
local ts
|
|
84
|
+
ts="$(stream_log_ts)"
|
|
85
|
+
printf '[%s] [%s] %s\n' "${ts}" "${tag}" "${line}" >> "${STREAM_LOG_PATH}"
|
|
86
|
+
printf '%s\n' "${line}" >&2
|
|
87
|
+
done
|
|
88
|
+
return "${PIPESTATUS[0]}"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Tee a subprocess's combined stdout+stderr into STREAM_LOG_PATH line-by-line
|
|
92
|
+
# with the given tag, passing each line through on stdout so the caller can
|
|
93
|
+
# capture it with `> file` or `$( … )`. Stderr mirroring is dropped so the
|
|
94
|
+
# caller's stdout is exactly the subprocess's output — `jq` and friends
|
|
95
|
+
# remain parseable.
|
|
96
|
+
#
|
|
97
|
+
# Usage: tee_subprocess_capture <tag> -- <cmd> <args …> > captured.file
|
|
98
|
+
# Example:
|
|
99
|
+
# tee_subprocess_capture reset-tunnel:cloudflared -- \
|
|
100
|
+
# cloudflared --origincert "${CERT}" tunnel list --output json \
|
|
101
|
+
# > "${NAMES_TMP}"
|
|
102
|
+
tee_subprocess_capture() {
|
|
103
|
+
local tag="$1"; shift
|
|
104
|
+
if [ "${1:-}" != "--" ]; then
|
|
105
|
+
echo "tee_subprocess_capture: expected -- before command" >&2
|
|
106
|
+
return 2
|
|
107
|
+
fi
|
|
108
|
+
shift
|
|
109
|
+
# Prefer `stdbuf -oL -eL` to force line buffering on the subprocess; fall
|
|
110
|
+
# back to the bare command when stdbuf is unavailable (macOS dev). On the
|
|
111
|
+
# Pi (Linux), stdbuf is present via coreutils and cloudflared's output
|
|
112
|
+
# reaches the tee as soon as each line is written.
|
|
113
|
+
local -a buf_prefix=()
|
|
114
|
+
if command -v stdbuf >/dev/null 2>&1; then
|
|
115
|
+
buf_prefix=(stdbuf -oL -eL)
|
|
116
|
+
fi
|
|
117
|
+
"${buf_prefix[@]}" "$@" 2>&1 | while IFS= read -r line; do
|
|
118
|
+
local ts
|
|
119
|
+
ts="$(stream_log_ts)"
|
|
120
|
+
printf '[%s] [%s] %s\n' "${ts}" "${tag}" "${line}" >> "${STREAM_LOG_PATH}"
|
|
121
|
+
printf '%s\n' "${line}"
|
|
122
|
+
done
|
|
123
|
+
return "${PIPESTATUS[0]}"
|
|
124
|
+
}
|