@rubytech/create-realagent 1.0.628 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.628",
3
+ "version": "1.0.630",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -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",
@@ -27,7 +27,10 @@ set -euo pipefail
27
27
  # --------------------------------------------------------------------------
28
28
 
29
29
  # shellcheck source=_stream-log.sh
30
- source "$(dirname "${BASH_SOURCE[0]}")/_stream-log.sh"
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
@@ -29,7 +29,10 @@ set -euo pipefail
29
29
  # --------------------------------------------------------------------------
30
30
 
31
31
  # shellcheck source=_stream-log.sh
32
- source "$(dirname "${BASH_SOURCE[0]}")/_stream-log.sh"
32
+ # Resolve symlinks before dirname — ~/setup-tunnel.sh is installed as a symlink
33
+ # (see packages/create-maxy/src/index.ts:installTunnelScripts), so the raw
34
+ # BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
35
+ source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
33
36
  require_stream_log_path setup-tunnel
34
37
 
35
38
  # --------------------------------------------------------------------------
@@ -80,6 +80,14 @@ Ask naturally:
80
80
  - "What did I last discuss about the Acme proposal?"
81
81
  - "Who have I met from the fintech conference?"
82
82
 
83
+ ## Listing and counting (Task 557)
84
+
85
+ Maxy answers relational questions — "list all my people", "how many tasks do I have", "find the person with email X", "show me the 20 most recently created nodes" — via direct read-only Cypher against your Neo4j. This is faster and more precise than semantic search when the question is "the exact set where", not "things similar to".
86
+
87
+ You can also open the Neo4j Browser at any time from the burger menu → **Graph**. Sign in with your Neo4j username (`neo4j`) and password (stored in `config/.neo4j-password` on the device). Run `MATCH (n) RETURN n LIMIT 25` for a visual overview of your graph, or write your own Cypher for ad-hoc exploration.
88
+
89
+ The browser reaches only your own brand's Neo4j — a Maxy device and a Real Agent device share no graph state even when on the same laptop.
90
+
83
91
  ## Privacy
84
92
 
85
93
  All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. Maxy does not sync memory to any cloud service or third party.