@rubytech/create-realagent 1.0.628 → 1.0.631
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +51 -4
- package/package.json +1 -1
- package/payload/platform/lib/graph-mcp/dist/index.d.ts +26 -0
- package/payload/platform/lib/graph-mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/index.js +193 -0
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-mcp/src/index.ts +225 -0
- package/payload/platform/lib/graph-mcp/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +12 -1
- package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +4 -1
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +70 -40
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/memory-guide.md +8 -0
- package/payload/platform/plugins/memory/mcp/scripts/graph/accept.sh +129 -0
- package/payload/platform/plugins/memory/mcp/scripts/graph/fixture.cypher +59 -0
- package/payload/platform/plugins/memory/references/graph-primitives.md +195 -0
- package/payload/server/public/assets/admin-BntwbBs-.js +352 -0
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +349 -31
- package/payload/server/public/assets/admin-CGIu9HnV.js +0 -352
|
@@ -29,7 +29,10 @@ set -euo pipefail
|
|
|
29
29
|
# --------------------------------------------------------------------------
|
|
30
30
|
|
|
31
31
|
# shellcheck source=_stream-log.sh
|
|
32
|
-
|
|
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
|
# --------------------------------------------------------------------------
|
|
@@ -303,7 +306,23 @@ echo "wrote ${CFG_DIR}/tunnel.state"
|
|
|
303
306
|
# --------------------------------------------------------------------------
|
|
304
307
|
# Restart the brand's user-space service so resume-tunnel.sh (its
|
|
305
308
|
# ExecStartPre) picks up the new tunnel.state + config.yml and spawns the
|
|
306
|
-
# connector.
|
|
309
|
+
# connector.
|
|
310
|
+
#
|
|
311
|
+
# CRITICAL: this script runs inside ${BRAND}.service's cgroup whenever the
|
|
312
|
+
# admin agent invokes it via the Bash tool. `systemctl --user restart
|
|
313
|
+
# ${BRAND}.service` from inside that cgroup SIGTERMs the whole cgroup —
|
|
314
|
+
# the node server, the claude subprocess, the Bash child, and this script
|
|
315
|
+
# itself (Task 558). Dispatching the restart to a transient systemd-run
|
|
316
|
+
# unit is the ONLY primitive that creates a new cgroup outside the service
|
|
317
|
+
# — setsid/nohup/disown/& all inherit cgroup membership, and
|
|
318
|
+
# `systemd-run --scope` runs in the caller's scope.
|
|
319
|
+
#
|
|
320
|
+
# The transient timer fires $RESTART_DELAY seconds after dispatch; the
|
|
321
|
+
# script exits 0 cleanly in microseconds, then the timer restarts the
|
|
322
|
+
# service from its own cgroup — semantically identical to an operator
|
|
323
|
+
# SSH-invoked `systemctl restart`. Post-restart verification (connector
|
|
324
|
+
# up + hostname probe) is out of scope here — the client reconnects and
|
|
325
|
+
# the next admin turn can verify via MCP tools.
|
|
307
326
|
# --------------------------------------------------------------------------
|
|
308
327
|
|
|
309
328
|
if ! systemctl --user list-unit-files "${BRAND}.service" --no-pager --no-legend | grep -q "${BRAND}.service"; then
|
|
@@ -312,48 +331,57 @@ if ! systemctl --user list-unit-files "${BRAND}.service" --no-pager --no-legend
|
|
|
312
331
|
exit 1
|
|
313
332
|
fi
|
|
314
333
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
334
|
+
if ! command -v systemd-run >/dev/null 2>&1; then
|
|
335
|
+
phase_line setup-tunnel step=service-restart-dispatched result=error \
|
|
336
|
+
reason=systemd-run-missing
|
|
337
|
+
echo "ERROR: systemd-run is not in PATH." >&2
|
|
338
|
+
echo " The script dispatches the ${BRAND}.service restart to a transient" >&2
|
|
339
|
+
echo " systemd user unit so it does not kill its own cgroup (Task 558)." >&2
|
|
340
|
+
echo " Install systemd userspace (standard on supported Maxy Pi images)." >&2
|
|
341
|
+
exit 1
|
|
342
|
+
fi
|
|
320
343
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
344
|
+
RESTART_DELAY=3
|
|
345
|
+
TRANSIENT_UNIT="maxy-tunnel-restart-$$-$(date +%s)"
|
|
346
|
+
phase_line setup-tunnel step=service-restart-dispatched \
|
|
347
|
+
unit="${TRANSIENT_UNIT}" delay="${RESTART_DELAY}s" \
|
|
348
|
+
cmd="systemctl --user restart ${BRAND}.service"
|
|
349
|
+
|
|
350
|
+
# Dispatch via systemd-run --user --on-active — creates a transient unit
|
|
351
|
+
# with its own cgroup that fires the restart after RESTART_DELAY seconds.
|
|
352
|
+
# --collect auto-GCs the unit after it terminates. The script exits before
|
|
353
|
+
# the timer fires; no race because the exit is microseconds and the timer
|
|
354
|
+
# is seconds. Capture stderr so the operator sees the actual systemd-run
|
|
355
|
+
# failure reason (e.g. "Failed to connect to bus" when linger is disabled).
|
|
356
|
+
SYSTEMD_RUN_ERR="$(mktemp -t maxy-systemd-run-err.XXXXXX)"
|
|
357
|
+
if systemd-run --user --unit="${TRANSIENT_UNIT}.service" \
|
|
358
|
+
--description="Detached restart of ${BRAND}.service (Task 558)" \
|
|
359
|
+
--on-active="${RESTART_DELAY}s" \
|
|
360
|
+
--collect \
|
|
361
|
+
/bin/systemctl --user restart "${BRAND}.service" 2>"${SYSTEMD_RUN_ERR}"; then
|
|
362
|
+
RESTART_RC=0
|
|
363
|
+
else
|
|
364
|
+
RESTART_RC=$?
|
|
365
|
+
fi
|
|
327
366
|
|
|
328
|
-
if
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
367
|
+
if [ "${RESTART_RC}" -ne 0 ]; then
|
|
368
|
+
STDERR_TEXT="$(cat "${SYSTEMD_RUN_ERR}" 2>/dev/null | tr '\n' ' ' | head -c 500 || echo 'unavailable')"
|
|
369
|
+
rm -f "${SYSTEMD_RUN_ERR}"
|
|
370
|
+
phase_line setup-tunnel step=service-restart-dispatched result=error \
|
|
371
|
+
reason=systemd-run-failed exit="${RESTART_RC}" unit="${TRANSIENT_UNIT}" \
|
|
372
|
+
stderr="${STDERR_TEXT}"
|
|
373
|
+
echo "ERROR: systemd-run failed to dispatch the transient restart (exit=${RESTART_RC})." >&2
|
|
374
|
+
echo " systemd-run stderr: ${STDERR_TEXT}" >&2
|
|
375
|
+
echo " If stderr mentions 'Failed to connect to bus', the user-scope systemd" >&2
|
|
376
|
+
echo " instance isn't running. Fix: 'loginctl enable-linger \$(whoami)' and retry." >&2
|
|
377
|
+
echo " The service was NOT restarted. Re-run the script or restart manually:" >&2
|
|
378
|
+
echo " systemctl --user restart ${BRAND}.service" >&2
|
|
333
379
|
exit 1
|
|
334
380
|
fi
|
|
381
|
+
rm -f "${SYSTEMD_RUN_ERR}"
|
|
335
382
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
echo "Connector running against ${CFG_DIR}/config.yml — verifying each subdomain hostname (up to ${VERIFY_TIMEOUT}s per host for DNS propagation):"
|
|
339
|
-
for H in "${HOSTNAMES[@]}"; do
|
|
340
|
-
if is_apex "$H"; then continue; fi
|
|
341
|
-
ELAPSED=0
|
|
342
|
-
STATUS="000"
|
|
343
|
-
while [ "${ELAPSED}" -lt "${VERIFY_TIMEOUT}" ]; do
|
|
344
|
-
STATUS=$(curl -o /dev/null -s -w '%{http_code}' --max-time 5 -I "https://${H}" || echo "000")
|
|
345
|
-
if [ "${STATUS}" != "530" ] && [ "${STATUS}" != "000" ]; then
|
|
346
|
-
break
|
|
347
|
-
fi
|
|
348
|
-
sleep "${POLL_INTERVAL}"
|
|
349
|
-
ELAPSED=$((ELAPSED + POLL_INTERVAL))
|
|
350
|
-
done
|
|
351
|
-
if [ "${STATUS}" = "530" ] || [ "${STATUS}" = "000" ]; then
|
|
352
|
-
echo " ${H}: HTTP ${STATUS} after ${VERIFY_TIMEOUT}s — DNS propagation slow or connector-to-edge issue"
|
|
353
|
-
else
|
|
354
|
-
echo " ${H}: HTTP ${STATUS} — live (after ${ELAPSED}s)"
|
|
355
|
-
fi
|
|
356
|
-
done
|
|
383
|
+
phase_line setup-tunnel step=service-restart-armed exit=0 unit="${TRANSIENT_UNIT}"
|
|
384
|
+
echo "${BRAND}.service restart armed via ${TRANSIENT_UNIT} (fires in ${RESTART_DELAY}s)."
|
|
357
385
|
|
|
358
386
|
# --------------------------------------------------------------------------
|
|
359
387
|
# Apex ACTION REQUIRED summary
|
|
@@ -377,6 +405,8 @@ if [ "${#APEX_HOSTNAMES[@]}" -gt 0 ]; then
|
|
|
377
405
|
echo "============================================================"
|
|
378
406
|
fi
|
|
379
407
|
|
|
408
|
+
phase_line setup-tunnel step=done tunnel="${TUNNEL_NAME}" id="${TUNNEL_ID}"
|
|
380
409
|
echo ""
|
|
381
410
|
echo "Done. tunnel=${TUNNEL_NAME} id=${TUNNEL_ID}"
|
|
382
|
-
echo "
|
|
411
|
+
echo "Service will restart in ~${RESTART_DELAY}s to load the new config."
|
|
412
|
+
echo "Verify hostnames with: curl -I https://${HOSTNAMES[0]}"
|
|
@@ -20,7 +20,7 @@ Any Cloudflare action outside these four surfaces is a discipline violation —
|
|
|
20
20
|
|
|
21
21
|
## 1. Autonomous path — `setup-tunnel.sh`
|
|
22
22
|
|
|
23
|
-
Use this when the operator wants Cloudflare set up (or re-set up) end-to-end on the device. The script handles OAuth login, tunnel creation, DNS routing for each subdomain, config.yml + tunnel.state, service restart
|
|
23
|
+
Use this when the operator wants Cloudflare set up (or re-set up) end-to-end on the device. The script handles OAuth login, tunnel creation, DNS routing for each subdomain, config.yml + tunnel.state, and dispatches the `${BRAND}.service` restart to a transient `systemd-run` unit (Task 558) — all in one invocation. The restart fires a few seconds after the script exits so the script does not kill its own cgroup when invoked via the Bash tool; the chat UI receives a `server_shutdown` SSE frame and reconnects automatically. Post-restart hostname verification is out of scope for the script (connector is not up when the script exits) — verify via the next admin turn or manually with `curl -I https://<hostname>`. Apex hostnames cannot be routed by the CLI; when one is passed, the script prints an `ACTION REQUIRED` block naming the exact dashboard record to edit.
|
|
24
24
|
|
|
25
25
|
### Inputs to collect before invoking
|
|
26
26
|
|
|
@@ -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.
|
|
@@ -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);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<!-- Injected into admin system prompt by claude-agent.ts when agentType === "admin". -->
|
|
2
|
+
<!-- Consumed by the upstream mcp-neo4j-cypher server via `maxy-graph_read_neo4j_cypher`. -->
|
|
3
|
+
<!-- Source of truth for node labels and properties: platform/neo4j/schema.cypher + plugins/memory/references/schema-*.md. -->
|
|
4
|
+
|
|
5
|
+
# Graph interrogation (read-only Cypher cookbook)
|
|
6
|
+
|
|
7
|
+
When a user asks a relational question about their own graph — *"list all my
|
|
8
|
+
people", "how many tasks do I have", "find the person with email X", "what's
|
|
9
|
+
linked to this business", "show me the 20 most recently created nodes" — use
|
|
10
|
+
`maxy-graph_read_neo4j_cypher` or `maxy-graph_get_neo4j_schema`, not
|
|
11
|
+
`memory-search`. Vector search is for "things like this"; Cypher is for "the
|
|
12
|
+
exact set where".
|
|
13
|
+
|
|
14
|
+
The connected Neo4j instance contains only this brand's data (per-brand
|
|
15
|
+
instance architecture — see `.docs/neo4j.md`). You never need an account
|
|
16
|
+
filter in the query.
|
|
17
|
+
|
|
18
|
+
## Non-negotiable: never return raw nodes
|
|
19
|
+
|
|
20
|
+
`RETURN n` dumps every property, including the 768-dim `embedding` float
|
|
21
|
+
array. This blows the context budget on the first row. **Always enumerate the
|
|
22
|
+
properties you want.**
|
|
23
|
+
|
|
24
|
+
Wrong: `MATCH (n:Person) RETURN n LIMIT 20`
|
|
25
|
+
Right: `MATCH (n:Person) RETURN elementId(n) AS id, n.givenName, n.familyName, n.email, n.telephone LIMIT 20`
|
|
26
|
+
|
|
27
|
+
Apply this convention to every query. Only project the fields you need to
|
|
28
|
+
answer the question. When in doubt, exclude `embedding`, `embeddingText`, and
|
|
29
|
+
any property whose name ends in `_vector`.
|
|
30
|
+
|
|
31
|
+
## Cookbook
|
|
32
|
+
|
|
33
|
+
Parameterise results with `ORDER BY` and `LIMIT` every time — even an
|
|
34
|
+
"inventory" query should bound itself. Default limit: 50.
|
|
35
|
+
|
|
36
|
+
### Enumerate
|
|
37
|
+
|
|
38
|
+
All nodes, grouped by label, with a human-readable name:
|
|
39
|
+
|
|
40
|
+
```cypher
|
|
41
|
+
MATCH (n)
|
|
42
|
+
RETURN labels(n)[0] AS type,
|
|
43
|
+
coalesce(n.name, n.title, n.givenName + ' ' + n.familyName, n.email, '(unnamed)') AS name,
|
|
44
|
+
elementId(n) AS id
|
|
45
|
+
ORDER BY type, name
|
|
46
|
+
LIMIT 200
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
One label only:
|
|
50
|
+
|
|
51
|
+
```cypher
|
|
52
|
+
MATCH (n:Person)
|
|
53
|
+
RETURN elementId(n) AS id, n.givenName, n.familyName, n.email, n.telephone
|
|
54
|
+
ORDER BY n.familyName, n.givenName
|
|
55
|
+
LIMIT 100
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Count
|
|
59
|
+
|
|
60
|
+
Total across the graph:
|
|
61
|
+
|
|
62
|
+
```cypher
|
|
63
|
+
MATCH (n)
|
|
64
|
+
RETURN count(n) AS total
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
By label:
|
|
68
|
+
|
|
69
|
+
```cypher
|
|
70
|
+
MATCH (n)
|
|
71
|
+
RETURN labels(n)[0] AS type, count(*) AS n
|
|
72
|
+
ORDER BY n DESC
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Find
|
|
76
|
+
|
|
77
|
+
By exact email:
|
|
78
|
+
|
|
79
|
+
```cypher
|
|
80
|
+
MATCH (p:Person {email: $email})
|
|
81
|
+
RETURN elementId(p) AS id, p.givenName, p.familyName, p.telephone, p.status
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Partial name, case-insensitive:
|
|
85
|
+
|
|
86
|
+
```cypher
|
|
87
|
+
MATCH (p:Person)
|
|
88
|
+
WHERE toLower(p.givenName) CONTAINS toLower($q)
|
|
89
|
+
OR toLower(p.familyName) CONTAINS toLower($q)
|
|
90
|
+
RETURN elementId(p) AS id, p.givenName, p.familyName, p.email
|
|
91
|
+
LIMIT 20
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Neighbours
|
|
95
|
+
|
|
96
|
+
What is linked to a node (1 hop out, undirected):
|
|
97
|
+
|
|
98
|
+
```cypher
|
|
99
|
+
MATCH (n)-[r]-(m)
|
|
100
|
+
WHERE elementId(n) = $id
|
|
101
|
+
RETURN type(r) AS rel,
|
|
102
|
+
labels(m)[0] AS neighbourType,
|
|
103
|
+
coalesce(m.name, m.title, m.email, '(unnamed)') AS neighbour,
|
|
104
|
+
elementId(m) AS neighbourId
|
|
105
|
+
LIMIT 50
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Recent
|
|
109
|
+
|
|
110
|
+
Most recently created nodes (any label):
|
|
111
|
+
|
|
112
|
+
```cypher
|
|
113
|
+
MATCH (n)
|
|
114
|
+
WHERE n.createdAt IS NOT NULL
|
|
115
|
+
RETURN labels(n)[0] AS type,
|
|
116
|
+
coalesce(n.name, n.title, n.givenName + ' ' + n.familyName, n.email, '(unnamed)') AS name,
|
|
117
|
+
toString(n.createdAt) AS createdAt,
|
|
118
|
+
elementId(n) AS id
|
|
119
|
+
ORDER BY n.createdAt DESC
|
|
120
|
+
LIMIT 20
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Most recently updated:
|
|
124
|
+
|
|
125
|
+
```cypher
|
|
126
|
+
MATCH (n)
|
|
127
|
+
WHERE n.updatedAt IS NOT NULL
|
|
128
|
+
RETURN labels(n)[0] AS type,
|
|
129
|
+
coalesce(n.name, n.title, n.email, '(unnamed)') AS name,
|
|
130
|
+
toString(n.updatedAt) AS updatedAt,
|
|
131
|
+
elementId(n) AS id
|
|
132
|
+
ORDER BY n.updatedAt DESC
|
|
133
|
+
LIMIT 20
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Schema
|
|
137
|
+
|
|
138
|
+
All labels present in the graph:
|
|
139
|
+
|
|
140
|
+
```cypher
|
|
141
|
+
CALL db.labels() YIELD label RETURN label ORDER BY label
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
All relationship types:
|
|
145
|
+
|
|
146
|
+
```cypher
|
|
147
|
+
CALL db.relationshipTypes() YIELD relationshipType RETURN relationshipType ORDER BY relationshipType
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Or use `maxy-graph_get_neo4j_schema` for a richer one-shot structural summary.
|
|
151
|
+
|
|
152
|
+
### Fulltext
|
|
153
|
+
|
|
154
|
+
Use the `knowledge_fulltext` index for keyword-style search across
|
|
155
|
+
KnowledgeDocument / Section / Chunk content:
|
|
156
|
+
|
|
157
|
+
```cypher
|
|
158
|
+
CALL db.index.fulltext.queryNodes('knowledge_fulltext', $query)
|
|
159
|
+
YIELD node, score
|
|
160
|
+
WHERE score > 0.5
|
|
161
|
+
RETURN labels(node)[0] AS type,
|
|
162
|
+
coalesce(node.name, node.title, node.heading, '(unnamed)') AS name,
|
|
163
|
+
score,
|
|
164
|
+
elementId(node) AS id
|
|
165
|
+
LIMIT 20
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Filter by status or category
|
|
169
|
+
|
|
170
|
+
Events that are cancelled:
|
|
171
|
+
|
|
172
|
+
```cypher
|
|
173
|
+
MATCH (e:Event {status: 'cancelled'})
|
|
174
|
+
RETURN elementId(e) AS id, e.name, toString(e.startDate) AS start
|
|
175
|
+
ORDER BY e.startDate DESC
|
|
176
|
+
LIMIT 50
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Tasks by status:
|
|
180
|
+
|
|
181
|
+
```cypher
|
|
182
|
+
MATCH (t:Task {status: $status})
|
|
183
|
+
RETURN elementId(t) AS id, t.title, toString(t.createdAt) AS created, t.priority
|
|
184
|
+
ORDER BY t.createdAt DESC
|
|
185
|
+
LIMIT 50
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## When to switch back to memory-search
|
|
189
|
+
|
|
190
|
+
- "Things similar to X" → `memory-search` (vector + BM25 hybrid)
|
|
191
|
+
- "Candidates to rank by criterion Y" → `memory-rank`
|
|
192
|
+
- "Conversations where we discussed Z" → `conversation-search`
|
|
193
|
+
|
|
194
|
+
Cypher is for relational certainty. Vector search is for similarity. They
|
|
195
|
+
answer different questions; don't use one to fake the other.
|