@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.
Files changed (27) hide show
  1. package/dist/index.js +34 -0
  2. package/package.json +1 -1
  3. package/payload/platform/lib/graph-mcp/dist/index.d.ts +26 -0
  4. package/payload/platform/lib/graph-mcp/dist/index.d.ts.map +1 -0
  5. package/payload/platform/lib/graph-mcp/dist/index.js +193 -0
  6. package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -0
  7. package/payload/platform/lib/graph-mcp/src/index.ts +225 -0
  8. package/payload/platform/lib/graph-mcp/tsconfig.json +8 -0
  9. package/payload/platform/package.json +2 -2
  10. package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +124 -0
  11. package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +45 -3
  12. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +151 -10
  13. package/payload/platform/plugins/docs/references/memory-guide.md +8 -0
  14. package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
  15. package/payload/platform/plugins/memory/mcp/scripts/graph/accept.sh +129 -0
  16. package/payload/platform/plugins/memory/mcp/scripts/graph/fixture.cypher +59 -0
  17. package/payload/platform/plugins/memory/references/graph-primitives.md +195 -0
  18. package/payload/server/public/assets/admin-DirN63aF.js +352 -0
  19. package/payload/server/public/assets/public-Cizdj15i.js +5 -0
  20. package/payload/server/public/assets/useVoiceRecorder-DIV9KAk_.css +1 -0
  21. package/payload/server/public/assets/{useVoiceRecorder-CiYPZu3g.js → useVoiceRecorder-tbj4tUsl.js} +1 -1
  22. package/payload/server/public/index.html +3 -3
  23. package/payload/server/public/public.html +3 -3
  24. package/payload/server/server.js +481 -102
  25. package/payload/server/public/assets/admin-BxVuKRJZ.js +0 -352
  26. package/payload/server/public/assets/public-Bgm9WQFZ.js +0 -5
  27. package/payload/server/public/assets/useVoiceRecorder-BORuG_su.css +0 -1
@@ -19,6 +19,20 @@
19
19
 
20
20
  set -euo pipefail
21
21
 
22
+ # --------------------------------------------------------------------------
23
+ # Shared stream-log helpers (require STREAM_LOG_PATH, phase_line, tee_subprocess).
24
+ # Task 556: any cloudflared subprocess this script spawns is teed line-by-line
25
+ # into the per-conversation stream log so the chat UI tailer renders live
26
+ # progress — same contract as setup-tunnel.sh.
27
+ # --------------------------------------------------------------------------
28
+
29
+ # shellcheck source=_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"
34
+ require_stream_log_path reset-tunnel
35
+
22
36
  if [ "$#" -lt 1 ]; then
23
37
  echo "Usage: $0 <brand>" >&2
24
38
  exit 2
@@ -28,28 +42,54 @@ BRAND="$1"
28
42
  CFG_DIR="${HOME}/.${BRAND}/cloudflared"
29
43
  CERT="${CFG_DIR}/cert.pem"
30
44
 
45
+ phase_line reset-tunnel step=start brand="${BRAND}" cfg_dir="${CFG_DIR}"
46
+
31
47
  if [ ! -f "${CERT}" ]; then
48
+ phase_line reset-tunnel step=no-cert cfg_dir="${CFG_DIR}"
32
49
  echo "No cert.pem at ${CERT} — nothing to delete via CLI."
33
50
  echo "Removing ${CFG_DIR} if present."
34
51
  rm -rf "${CFG_DIR}"
52
+ phase_line reset-tunnel step=done result=wiped-no-cert
35
53
  exit 0
36
54
  fi
37
55
 
38
56
  # Delete every tunnel on the account (the cert is per-account for tunnel CRUD).
39
- NAMES="$(cloudflared --origincert "${CERT}" tunnel list --output json 2>/dev/null \
40
- | jq -r '.[]?.name' | sort -u)"
57
+ # cloudflared's list/delete output is teed into STREAM_LOG_PATH; the `list`
58
+ # call uses tee_subprocess_capture so NAMES_TMP receives the JSON for jq to
59
+ # parse. Using the non-capture variant here would strand the JSON in stderr
60
+ # and leave NAMES_TMP empty — every tunnel would silently survive reset.
61
+ phase_line reset-tunnel step=list-tunnels
62
+ NAMES_TMP="$(mktemp -t maxy-reset-tunnel-list.XXXXXX)"
63
+ # shellcheck disable=SC2064
64
+ trap "rm -f '${NAMES_TMP}'" EXIT
65
+ if ! tee_subprocess_capture reset-tunnel:cloudflared -- \
66
+ cloudflared --origincert "${CERT}" tunnel list --output json \
67
+ > "${NAMES_TMP}"; then
68
+ phase_line reset-tunnel step=list-tunnels result=error reason=cloudflared-list-failed
69
+ echo "ERROR: cloudflared tunnel list failed. See stream log for detail." >&2
70
+ exit 1
71
+ fi
72
+ NAMES="$(jq -r '.[]?.name' "${NAMES_TMP}" 2>/dev/null | sort -u || true)"
41
73
  if [ -z "${NAMES}" ]; then
74
+ phase_line reset-tunnel step=list-tunnels result=empty
42
75
  echo "No tunnels to delete on this brand's account."
43
76
  else
44
77
  while IFS= read -r NAME; do
45
78
  [ -z "${NAME}" ] && continue
79
+ phase_line reset-tunnel step=delete-tunnel name="${NAME}"
46
80
  echo "Deleting tunnel: ${NAME}"
47
- cloudflared --origincert "${CERT}" tunnel delete "${NAME}"
81
+ if ! tee_subprocess reset-tunnel:cloudflared -- \
82
+ cloudflared --origincert "${CERT}" tunnel delete "${NAME}"; then
83
+ phase_line reset-tunnel step=delete-tunnel result=error name="${NAME}"
84
+ echo "ERROR: cloudflared tunnel delete failed for ${NAME}. See stream log." >&2
85
+ exit 1
86
+ fi
48
87
  done <<< "${NAMES}"
49
88
  fi
50
89
 
51
90
  # Wipe the brand-scoped cloudflared state directory.
52
91
  rm -rf "${CFG_DIR}"
92
+ phase_line reset-tunnel step=wipe-cfg-dir path="${CFG_DIR}"
53
93
  echo "Removed ${CFG_DIR}"
54
94
 
55
95
  echo ""
@@ -63,3 +103,5 @@ echo "The platform service (${BRAND}.service) was not touched. To release any"
63
103
  echo "running cloudflared connector started by ${BRAND}.service's ExecStartPre,"
64
104
  echo "either restart the service (which will no-op on resume since tunnel.state"
65
105
  echo "is gone) or leave it running until you re-run setup-tunnel.sh."
106
+
107
+ phase_line reset-tunnel step=done result=ok brand="${BRAND}"
@@ -13,9 +13,28 @@
13
13
  # via `cloudflared tunnel route dns` — see manual-setup.md §Step 4 Apex
14
14
  # hostnames. The script writes the ingress rule for them but prints an
15
15
  # explicit ACTION REQUIRED message naming the manual dashboard step.
16
+ #
17
+ # Task 556: Step 1 owns the browser-spawn deterministically. Instead of
18
+ # delegating to cloudflared's xdg-open (which silently degrades to "print
19
+ # URL and wait"), the script drives Chromium on :99 via CDP `PUT /json/new?<url>`
20
+ # on http://127.0.0.1:9222 — the same mechanism
21
+ # /api/admin/device-browser/navigate uses. cloudflared's stdout+stderr is
22
+ # teed line-by-line into STREAM_LOG_PATH so the chat UI's server-side
23
+ # tailer renders live progress in-turn.
16
24
 
17
25
  set -euo pipefail
18
26
 
27
+ # --------------------------------------------------------------------------
28
+ # Shared stream-log helpers (require STREAM_LOG_PATH, phase_line, …).
29
+ # --------------------------------------------------------------------------
30
+
31
+ # shellcheck source=_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"
36
+ require_stream_log_path setup-tunnel
37
+
19
38
  # --------------------------------------------------------------------------
20
39
  # Args
21
40
  # --------------------------------------------------------------------------
@@ -31,6 +50,8 @@ PORT="$2"
31
50
  shift 2
32
51
  HOSTNAMES=("$@")
33
52
 
53
+ phase_line setup-tunnel step=start brand="${BRAND}" port="${PORT}" hostnames="${HOSTNAMES[*]}"
54
+
34
55
  # --------------------------------------------------------------------------
35
56
  # Step 0: Set brand context (paths + dirs). Corresponds to runbook Step 0.
36
57
  # --------------------------------------------------------------------------
@@ -40,24 +61,144 @@ mkdir -p "${CFG_DIR}"
40
61
 
41
62
  # --------------------------------------------------------------------------
42
63
  # Step 1: OAuth login (only if cert.pem missing). Corresponds to runbook Step 1.
43
- # cloudflared always writes cert.pem to ~/.cloudflared/cert.pem regardless
44
- # of --origincert mv it into the brand-scoped path on fresh login.
64
+ #
65
+ # Control flow, rewritten per Task 556:
66
+ # 1. CDP precheck on 127.0.0.1:9222 — loud failure if Chromium isn't up.
67
+ # 2. Spawn cloudflared with stdout+stderr teed line-by-line to
68
+ # $STREAM_LOG_PATH with prefix [setup-tunnel:cloudflared].
69
+ # 3. Extract the authorize URL with a tolerant regex as it streams.
70
+ # 4. Drive the VNC Chromium to that URL via CDP `PUT /json/new?<url>`.
71
+ # 5. Wait for ~/.cloudflared/cert.pem to land with bounded timeout.
72
+ # 6. Move cert.pem into the brand-scoped path.
73
+ #
74
+ # Every branch exits 1 loudly naming the failure — no silent retries,
75
+ # no xdg-open race, no cloudflared-internal browser-spawn path.
45
76
  # --------------------------------------------------------------------------
46
77
 
47
78
  if [ ! -f "${CFG_DIR}/cert.pem" ]; then
48
- echo "No cert.pem at ${CFG_DIR}/cert.pem — starting OAuth login (DISPLAY=${DISPLAY:-:99})"
49
- DISPLAY="${DISPLAY:-:99}" cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel login &
50
- LOGIN_PID=$!
51
- # cloudflared login writes to ~/.cloudflared/cert.pem regardless of --origincert
79
+ phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem" display="${DISPLAY:-:99}"
80
+
81
+ # CDP precheck — fail loudly if Chromium DevTools is not answering.
82
+ if ! curl -sf --max-time 2 "http://127.0.0.1:9222/json/version" > /dev/null 2>&1; then
83
+ phase_line setup-tunnel step=oauth-login result=error reason=cdp-unreachable \
84
+ endpoint=http://127.0.0.1:9222 hint="run ~/vnc.sh restart"
85
+ echo "ERROR: Chromium CDP on 127.0.0.1:9222 is not reachable." >&2
86
+ echo " The script needs CDP to drive the authorize URL on the VNC browser." >&2
87
+ echo " Fix: run 'vnc.sh restart' to bring Chromium up on :99." >&2
88
+ exit 1
89
+ fi
90
+ phase_line setup-tunnel step=oauth-login cdp=ok
91
+
92
+ URL_FILE="$(mktemp -t maxy-setup-tunnel-url.XXXXXX)"
93
+ LAST_LINE_FILE="$(mktemp -t maxy-setup-tunnel-last.XXXXXX)"
94
+ : > "${URL_FILE}"
95
+ : > "${LAST_LINE_FILE}"
96
+ # Track the cloudflared pipeline PID so the EXIT trap can kill it on any
97
+ # failure path — including ones that `exit 1` without an explicit kill.
98
+ # Missing this trap leaks a cloudflared subshell waiting for the OAuth
99
+ # callback forever; subsequent setup-tunnel runs see a stale cert.pem
100
+ # landing asynchronously and race against the new URL-extraction pass.
101
+ CF_PIPELINE_PID=""
102
+ cleanup_oauth() {
103
+ [ -n "${CF_PIPELINE_PID}" ] && kill "${CF_PIPELINE_PID}" 2>/dev/null || true
104
+ rm -f "${URL_FILE}" "${LAST_LINE_FILE}"
105
+ }
106
+ trap cleanup_oauth EXIT
107
+
108
+ # cloudflared is line-buffered (stdbuf), teed to the stream log, URL
109
+ # extracted as it streams. The subshell holds the whole pipeline so
110
+ # PIPESTATUS[0] (cloudflared's exit code) is reachable later.
111
+ (
112
+ DISPLAY="${DISPLAY:-:99}" stdbuf -oL -eL cloudflared \
113
+ --origincert "${CFG_DIR}/cert.pem" tunnel login 2>&1 |
114
+ while IFS= read -r line; do
115
+ ts="$(stream_log_ts)"
116
+ printf '[%s] [setup-tunnel:cloudflared] %s\n' "${ts}" "${line}" >> "${STREAM_LOG_PATH}"
117
+ printf '%s\n' "${line}" >&2
118
+ printf '%s\n' "${line}" > "${LAST_LINE_FILE}"
119
+ if [ ! -s "${URL_FILE}" ]; then
120
+ url="$(printf '%s' "${line}" | grep -oE 'https://dash\.cloudflare\.com/argotunnel\?[^ ]+' | head -1 || true)"
121
+ if [ -n "${url}" ]; then
122
+ printf '%s' "${url}" > "${URL_FILE}"
123
+ fi
124
+ fi
125
+ done
126
+ ) &
127
+ CF_PIPELINE_PID=$!
128
+
129
+ # Wait up to ~15s for the URL to surface in cloudflared's output.
130
+ URL_WAIT=0
131
+ while [ ! -s "${URL_FILE}" ] && [ "${URL_WAIT}" -lt 30 ]; do
132
+ if ! kill -0 "${CF_PIPELINE_PID}" 2>/dev/null; then
133
+ phase_line setup-tunnel step=oauth-login result=error \
134
+ reason=cloudflared-exited-before-url \
135
+ last_line="$(cat "${LAST_LINE_FILE}" 2>/dev/null || echo none)"
136
+ echo "ERROR: cloudflared exited before printing the authorize URL." >&2
137
+ exit 1
138
+ fi
139
+ sleep 0.5
140
+ URL_WAIT=$((URL_WAIT + 1))
141
+ done
142
+
143
+ if [ ! -s "${URL_FILE}" ]; then
144
+ kill "${CF_PIPELINE_PID}" 2>/dev/null || true
145
+ phase_line setup-tunnel step=oauth-login result=error \
146
+ reason=url-not-extracted waited=15s \
147
+ last_line="$(cat "${LAST_LINE_FILE}" 2>/dev/null || echo none)"
148
+ echo "ERROR: cloudflared ran for ~15s without emitting a dash.cloudflare.com/argotunnel URL." >&2
149
+ echo " cloudflared output-format may have changed. Check the stream log tail for the raw lines." >&2
150
+ exit 1
151
+ fi
152
+
153
+ AUTH_URL="$(cat "${URL_FILE}")"
154
+ phase_line setup-tunnel step=browser-drive url_extracted=1
155
+
156
+ # Drive CDP. Same PUT /json/new?<url> contract as
157
+ # platform/ui/app/lib/cdp-client.ts (which uses encodeURIComponent on
158
+ # the URL). Without percent-encoding, CDP's URL parser splits on the
159
+ # inner `?` and `&` in the argotunnel URL and drops the callback/token
160
+ # query params — Chromium lands on a bare argotunnel page and the
161
+ # consent screen is never reached. `jq -sRr @uri` percent-encodes a raw
162
+ # string, matching the TS client exactly.
163
+ AUTH_URL_ENC="$(printf '%s' "${AUTH_URL}" | jq -sRr @uri)"
164
+ CDP_RESP="$(curl -sf --max-time 5 -X PUT "http://127.0.0.1:9222/json/new?${AUTH_URL_ENC}" 2>&1 || true)"
165
+ if [ -z "${CDP_RESP}" ]; then
166
+ kill "${CF_PIPELINE_PID}" 2>/dev/null || true
167
+ phase_line setup-tunnel step=browser-drive result=error reason=cdp-put-failed
168
+ echo "ERROR: CDP PUT /json/new?<url> returned empty — Chromium rejected the navigate." >&2
169
+ exit 1
170
+ fi
171
+ phase_line setup-tunnel step=browser-drive result=accepted
172
+
173
+ # Wait for cert.pem to land — cloudflared writes to ~/.cloudflared/cert.pem
174
+ # regardless of --origincert, so watch the canonical location.
175
+ LOGIN_TIMEOUT="${SETUP_TUNNEL_LOGIN_TIMEOUT:-180}"
176
+ LOGIN_WAIT=0
52
177
  while [ ! -f "${HOME}/.cloudflared/cert.pem" ]; do
53
- if ! kill -0 "${LOGIN_PID}" 2>/dev/null; then
54
- echo "ERROR: cloudflared tunnel login exited before cert.pem landed" >&2
178
+ if ! kill -0 "${CF_PIPELINE_PID}" 2>/dev/null; then
179
+ # Pipeline exited one more cert.pem probe in case it landed right before exit.
180
+ if [ -f "${HOME}/.cloudflared/cert.pem" ]; then break; fi
181
+ phase_line setup-tunnel step=oauth-login result=error \
182
+ reason=cloudflared-exited-no-cert \
183
+ last_line="$(cat "${LAST_LINE_FILE}" 2>/dev/null || echo none)"
184
+ echo "ERROR: cloudflared exited before cert.pem landed." >&2
55
185
  exit 1
56
186
  fi
57
- sleep 2
187
+ if [ "${LOGIN_WAIT}" -ge "${LOGIN_TIMEOUT}" ]; then
188
+ kill "${CF_PIPELINE_PID}" 2>/dev/null || true
189
+ phase_line setup-tunnel step=oauth-login result=error \
190
+ reason=timeout-waiting-cert waited="${LOGIN_WAIT}s" \
191
+ last_line="$(cat "${LAST_LINE_FILE}" 2>/dev/null || echo none)"
192
+ echo "ERROR: Timed out after ${LOGIN_WAIT}s waiting for cert.pem to land." >&2
193
+ exit 1
194
+ fi
195
+ sleep 1
196
+ LOGIN_WAIT=$((LOGIN_WAIT + 1))
58
197
  done
198
+
59
199
  mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem"
60
- echo "cert.pem moved to ${CFG_DIR}/cert.pem"
200
+ phase_line setup-tunnel step=oauth-login result=cert-received \
201
+ path="${CFG_DIR}/cert.pem" waited="${LOGIN_WAIT}s"
61
202
  fi
62
203
 
63
204
  # --------------------------------------------------------------------------
@@ -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.
@@ -103,6 +103,8 @@ After this, every `console.error("[your-tool] ...")` from any tool in the plugin
103
103
 
104
104
  **How the tee decides which file to write to (Task 532):** the platform sets `STREAM_LOG_PATH` as an environment variable on every MCP server spawn, pointing to the conversation-scoped stream log. The MCP server does not know about conversations — it just trusts `STREAM_LOG_PATH`. Multiple concurrent conversations produce multiple concurrent MCP server processes, each teeing to its own file; no cross-conversation leakage.
105
105
 
106
+ **`STREAM_LOG_PATH` reaches every Claude Code child (Task 556).** The platform now sets `STREAM_LOG_PATH` on the parent `claude` spawn env itself (not only on MCP server envs), so the bundled Bun runtime inherits it and every Bash-tool subprocess the CLI spawns sees it too. Opt-in shell scripts — currently `setup-tunnel.sh` and `reset-tunnel.sh` under `platform/plugins/cloudflare/scripts/` — read the variable, guard against a missing value with a loud exit, and tee subprocess output line-by-line into the same per-conversation file. Each spawn writes one `[spawn-env] STREAM_LOG_PATH=set pid=… conversationId=… site=…` line so the env-propagation is auditable per session. The chat UI tails the same file for lines matching `^\[[^]]+\] \[(setup-tunnel|reset-tunnel)(:[^]]+)?\] ` and emits them as `script_stream` SSE events; see `.docs/web-chat.md` for the contract.
107
+
106
108
  **Retrieve MCP diagnostic lines for a conversation:**
107
109
 
108
110
  - All servers: `logs-read { type: "system", conversationId: "..." }` → grep `[mcp:<name>]` on the returned stream log.
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bash
2
+ # Task 557 acceptance harness for the graph MCP integration.
3
+ #
4
+ # Spawns the graph shim, drives it as a JSON-RPC client over stdio, runs
5
+ # one query per acceptance class, prints PASS/FAIL per check, and exits
6
+ # non-zero if any check fails. Assumes the brand's Neo4j is running and
7
+ # the fixture in fixture.cypher has been seeded.
8
+ #
9
+ # Usage:
10
+ # PLATFORM_ROOT=~/.maxy/platform bash accept.sh # infers NEO4J_URI from env
11
+ # PLATFORM_ROOT=~/.maxy/platform NEO4J_URI=bolt://localhost:7687 bash accept.sh
12
+ #
13
+ # Dependencies: node, python3 (for JSON-RPC framing), uvx (via the shim).
14
+ #
15
+ # Exit codes:
16
+ # 0 all PASS
17
+ # 1 one or more FAIL
18
+ # 2 setup error (shim missing, uvx missing, etc.)
19
+
20
+ set -euo pipefail
21
+
22
+ PLATFORM_ROOT="${PLATFORM_ROOT:-$(cd "$(dirname "$0")/../../../../.." && pwd)}"
23
+ SHIM="${PLATFORM_ROOT}/lib/graph-mcp/dist/index.js"
24
+
25
+ if [[ ! -f "$SHIM" ]]; then
26
+ echo "FAIL: shim not built at $SHIM — run 'npm run build:lib' in $PLATFORM_ROOT" >&2
27
+ exit 2
28
+ fi
29
+
30
+ if ! command -v uvx >/dev/null 2>&1; then
31
+ echo "FAIL: uvx not on PATH — run: curl -LsSf https://astral.sh/uv/install.sh | sh -s -- -y" >&2
32
+ exit 2
33
+ fi
34
+
35
+ PASS=0
36
+ FAIL=0
37
+ TOTAL=0
38
+
39
+ run_check() {
40
+ local label="$1"
41
+ local query="$2"
42
+ local tool="${3:-maxy-graph_read_neo4j_cypher}"
43
+ TOTAL=$(( TOTAL + 1 ))
44
+
45
+ # Drive the shim via a short Node inline client. Exits 0 with the response
46
+ # body on stdout when the tool call succeeds; exits 1 on any RPC error.
47
+ if out=$(
48
+ node -e "
49
+ const { spawn } = require('node:child_process');
50
+ const shim = process.env.SHIM;
51
+ const tool = process.env.TOOL;
52
+ const query = process.env.QUERY;
53
+ const proc = spawn('node', [shim], { stdio: ['pipe', 'pipe', 'inherit'] });
54
+ let buf = '';
55
+ proc.stdout.on('data', (chunk) => { buf += chunk.toString('utf8'); });
56
+ const send = (obj) => proc.stdin.write(JSON.stringify(obj) + '\n');
57
+ send({ jsonrpc: '2.0', id: 1, method: 'initialize',
58
+ params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'accept.sh', version: '1.0' } } });
59
+ setTimeout(() => {
60
+ send({ jsonrpc: '2.0', method: 'notifications/initialized' });
61
+ const params = tool === 'maxy-graph_get_neo4j_schema' ? {} : { query };
62
+ send({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: tool, arguments: params } });
63
+ }, 1500);
64
+ setTimeout(() => {
65
+ proc.kill('SIGTERM');
66
+ // Look for id:2 response in the buffer
67
+ const lines = buf.split('\n').filter(Boolean);
68
+ for (const line of lines) {
69
+ try { const m = JSON.parse(line); if (m.id === 2) {
70
+ if (m.error) { console.error('RPC_ERROR: ' + JSON.stringify(m.error)); process.exit(1); }
71
+ const text = m.result && m.result.content && m.result.content[0] && m.result.content[0].text || '';
72
+ process.stdout.write(text);
73
+ process.exit(0);
74
+ } } catch (_) {}
75
+ }
76
+ console.error('NO_RESPONSE');
77
+ process.exit(1);
78
+ }, 10000);
79
+ " 2>/dev/null
80
+ ); then
81
+ if [[ -n "$out" ]]; then
82
+ echo "PASS: ${label} (response ${#out}b)"
83
+ PASS=$(( PASS + 1 ))
84
+ else
85
+ echo "FAIL: ${label} (empty response)"
86
+ FAIL=$(( FAIL + 1 ))
87
+ fi
88
+ else
89
+ echo "FAIL: ${label} (RPC error or timeout)"
90
+ FAIL=$(( FAIL + 1 ))
91
+ fi
92
+ }
93
+
94
+ # Exported env used by the inline Node client above.
95
+ export SHIM
96
+ export PLATFORM_ROOT
97
+
98
+ export TOOL="maxy-graph_get_neo4j_schema"
99
+ export QUERY=""
100
+ run_check "get_neo4j_schema returns labels" ""
101
+
102
+ export TOOL="maxy-graph_read_neo4j_cypher"
103
+
104
+ export QUERY="MATCH (n) RETURN labels(n)[0] AS type, count(*) AS n ORDER BY n DESC LIMIT 20"
105
+ run_check "enumerate by label" "$QUERY"
106
+
107
+ export QUERY="MATCH (n:Task) RETURN count(n) AS total"
108
+ run_check "count Tasks" "$QUERY"
109
+
110
+ export QUERY="MATCH (p:Person {email: 'graph-accept@test.maxy.local'}) RETURN elementId(p) AS id, p.givenName, p.email LIMIT 1"
111
+ run_check "find person by email" "$QUERY"
112
+
113
+ export QUERY="MATCH (p:Person {email: 'graph-accept@test.maxy.local'})-[r]-(m) RETURN type(r) AS rel, labels(m)[0] AS neighbourType LIMIT 10"
114
+ run_check "neighbours of fixture person" "$QUERY"
115
+
116
+ export QUERY="MATCH (n) WHERE n.createdAt IS NOT NULL RETURN labels(n)[0] AS type, toString(n.createdAt) AS createdAt ORDER BY n.createdAt DESC LIMIT 5"
117
+ run_check "recent nodes projection" "$QUERY"
118
+
119
+ export QUERY="CALL db.labels() YIELD label RETURN label ORDER BY label"
120
+ run_check "db.labels() schema introspection" "$QUERY"
121
+
122
+ echo ""
123
+ if [[ $FAIL -eq 0 ]]; then
124
+ echo "ALL PASS — ${PASS}/${TOTAL}"
125
+ exit 0
126
+ else
127
+ echo "FAIL — ${PASS}/${TOTAL} passed, ${FAIL} failed"
128
+ exit 1
129
+ fi
@@ -0,0 +1,59 @@
1
+ // Task 557 — acceptance fixture for the graph MCP integration.
2
+ // Seeds one node per primary label so accept.sh can verify enumerate,
3
+ // count, find, neighbours, recent, and schema queries end-to-end.
4
+ // Safe to re-run: MERGE on a synthetic accountId prefix + deterministic ids.
5
+
6
+ MERGE (p:Person {email: 'graph-accept@test.maxy.local'})
7
+ ON CREATE SET p.givenName = 'Graph',
8
+ p.familyName = 'Accept',
9
+ p.telephone = '+440000557001',
10
+ p.status = 'active',
11
+ p.accountId = 'accept-557',
12
+ p.createdAt = datetime()
13
+ MERGE (b:LocalBusiness {accountId: 'accept-557'})
14
+ ON CREATE SET b.name = 'Accept Corp',
15
+ b.businessType = 'test-fixture',
16
+ b.createdAt = datetime()
17
+ MERGE (s:Service {serviceId: 'accept-557-service-1'})
18
+ ON CREATE SET s.name = 'Test Service',
19
+ s.accountId = 'accept-557',
20
+ s.createdAt = datetime()
21
+ MERGE (t:Task {taskId: 'accept-557-task-1'})
22
+ ON CREATE SET t.title = 'Acceptance task',
23
+ t.status = 'open',
24
+ t.priority = 'P2',
25
+ t.accountId = 'accept-557',
26
+ t.createdAt = datetime()
27
+ MERGE (e:Event {eventId: 'accept-557-event-1'})
28
+ ON CREATE SET e.name = 'Acceptance event',
29
+ e.status = 'scheduled',
30
+ e.startDate = datetime(),
31
+ e.accountId = 'accept-557',
32
+ e.createdAt = datetime()
33
+ MERGE (c:Conversation {conversationId: 'accept-557-conv-1'})
34
+ ON CREATE SET c.name = 'Acceptance conversation',
35
+ c.agentType = 'admin',
36
+ c.accountId = 'accept-557',
37
+ c.createdAt = datetime()
38
+ MERGE (m:Message {messageId: 'accept-557-msg-1'})
39
+ ON CREATE SET m.role = 'user',
40
+ m.content = 'Acceptance test message',
41
+ m.accountId = 'accept-557',
42
+ m.createdAt = datetime()
43
+ MERGE (kd:KnowledgeDocument {attachmentId: 'accept-557-doc-1'})
44
+ ON CREATE SET kd.name = 'Acceptance document',
45
+ kd.accountId = 'accept-557',
46
+ kd.createdAt = datetime()
47
+ MERGE (sec:Section {sectionId: 'accept-557-sec-1'})
48
+ ON CREATE SET sec.title = 'Section one',
49
+ sec.accountId = 'accept-557',
50
+ sec.createdAt = datetime()
51
+ MERGE (ch:Chunk {chunkId: 'accept-557-chk-1'})
52
+ ON CREATE SET ch.text = 'Chunk text for acceptance.',
53
+ ch.accountId = 'accept-557',
54
+ ch.createdAt = datetime()
55
+ MERGE (p)-[:WORKS_FOR]->(b)
56
+ MERGE (b)-[:OFFERS]->(s)
57
+ MERGE (kd)-[:HAS_SECTION]->(sec)
58
+ MERGE (sec)-[:HAS_CHUNK]->(ch)
59
+ MERGE (c)-[:HAS_MESSAGE]->(m);