@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
|
@@ -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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
44
|
-
#
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 "${
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|