@rubytech/create-maxy-code 0.1.45 → 0.1.47
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 +8 -16
- package/package.json +1 -1
- package/payload/platform/lib/admins-write/dist/index.d.ts +0 -1
- package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admins-write/dist/index.js +2 -5
- package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
- package/payload/platform/lib/admins-write/src/index.ts +2 -8
- package/payload/platform/lib/graph-mcp/dist/index.js +1 -4
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts +7 -6
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js +7 -6
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/index.ts +1 -4
- package/payload/platform/lib/graph-mcp/src/schema-cache.ts +7 -6
- package/payload/platform/lib/graph-search/dist/index.d.ts +5 -5
- package/payload/platform/lib/graph-search/dist/index.js +4 -4
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +6 -7
- package/payload/platform/lib/graph-search/src/index.ts +5 -5
- package/payload/platform/lib/graph-trash/dist/index.d.ts +8 -12
- package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-trash/dist/index.js +8 -12
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +8 -12
- package/payload/platform/lib/graph-write/dist/index.d.ts +8 -9
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +9 -10
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/index.ts +9 -10
- package/payload/platform/lib/mcp-eager/dist/index.d.ts +4 -4
- package/payload/platform/lib/mcp-eager/dist/index.js +4 -4
- package/payload/platform/lib/mcp-eager/src/index.ts +4 -4
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +56 -38
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +42 -40
- package/payload/platform/plugins/cloudflare/scripts/__tests__/tunnel-ingress.test.ts +2 -2
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +7 -9
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +10 -12
- package/payload/platform/plugins/cloudflare/scripts/tunnel-ingress.ts +3 -3
- package/payload/platform/plugins/docs/references/deployment.md +0 -14
- package/payload/platform/plugins/docs/references/platform.md +2 -2
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +5 -5
- package/payload/platform/plugins/loop/PLUGIN.md +67 -12
- package/payload/platform/plugins/loop/mcp/dist/index.js +42 -16
- package/payload/platform/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.d.ts +12 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.js +22 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts +0 -7
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.js +4 -38
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.d.ts +33 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.js +19 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.d.ts +0 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.js +1 -7
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.d.ts +9 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.js +19 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/src/__tests__/new-tool-shape.test.ts +99 -0
- package/payload/platform/plugins/loop/mcp/src/__tests__/swagger-write-coverage.test.ts +12 -11
- package/payload/platform/plugins/loop/mcp/src/index.ts +68 -17
- package/payload/platform/plugins/loop/mcp/src/tools/auto-responder.ts +80 -0
- package/payload/platform/plugins/loop/mcp/src/tools/marketing-enquiry.ts +14 -100
- package/payload/platform/plugins/loop/mcp/src/tools/property-intent.ts +84 -0
- package/payload/platform/plugins/loop/mcp/src/tools/property-listed.ts +1 -8
- package/payload/platform/plugins/loop/mcp/src/tools/property-sold.ts +67 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js +4 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +7 -12
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-not-applicable.test.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js +4 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.js +8 -9
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.js.map +1 -1
- package/payload/platform/plugins/waitlist/mcp/dist/tools/waitlist-heal.d.ts +2 -2
- package/payload/platform/plugins/waitlist/mcp/dist/tools/waitlist-heal.js +2 -2
- package/payload/platform/scripts/admin-persist-audit.ts +4 -10
- package/payload/platform/scripts/conversation-id-allowlist.txt +0 -1
- package/payload/platform/scripts/lib/resolve-account-dir.sh +0 -2
- package/payload/platform/scripts/resume-tunnel.sh +2 -9
- package/payload/platform/scripts/seed-neo4j.sh +26 -104
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +53 -7
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -5
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +13 -17
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +10 -2
- package/payload/platform/templates/agents/admin/SOUL.md +23 -1
- package/payload/premium-plugins/real-agent/agents/buyer-enquiry/SOUL.md +10 -0
- package/payload/premium-plugins/real-agent/agents/compliance.md +1 -1
- package/payload/premium-plugins/real-agent/agents/negotiator.md +1 -1
- package/payload/premium-plugins/real-agent/agents/valuer.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/PLUGIN.md +67 -12
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js +42 -16
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.d.ts +12 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.js +22 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts +0 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.js +4 -38
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.d.ts +33 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.js +19 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.d.ts +0 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.js +1 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.d.ts +9 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.js +19 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/new-tool-shape.test.ts +99 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/swagger-write-coverage.test.ts +12 -11
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/index.ts +68 -17
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/auto-responder.ts +80 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/marketing-enquiry.ts +14 -100
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-intent.ts +84 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-listed.ts +1 -8
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-sold.ts +67 -0
- package/payload/server/public/assets/admin-SyUHjvNs.js +216 -0
- package/payload/server/public/assets/{data-BxpOR0eM.js → data-DwQ7ZEyy.js} +1 -1
- package/payload/server/public/assets/{graph-CrmxRSeb.js → graph-CwZkD6GX.js} +1 -1
- package/payload/server/public/assets/graph-labels-CVkmOHhw.js +1 -0
- package/payload/server/public/assets/page-CQ7ELqGs.js +1 -0
- package/payload/server/public/assets/page-SOCqJGlV.js +50 -0
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +9 -46
- package/payload/platform/plugins/docs/references/migration-guide.md +0 -90
- package/payload/platform/plugins/loop/mcp/src/tools/property-request.ts +0 -42
- package/payload/platform/scripts/component-knowledgedoc-backfill.ts +0 -214
- package/payload/platform/scripts/migrate-import.sh +0 -437
- package/payload/platform/scripts/taskmaster-export.sh +0 -388
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-request.ts +0 -42
- package/payload/server/public/assets/admin-DVZcLuYu.js +0 -216
- package/payload/server/public/assets/graph-labels-C8A6BmtP.js +0 -1
- package/payload/server/public/assets/page-D5wAeFU0.js +0 -50
- package/payload/server/public/assets/page-DR5NU6S0.js +0 -1
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Stop hook — fires on every completed admin-agent turn and dispatches one
|
|
3
|
-
# headless
|
|
4
|
-
#
|
|
5
|
-
# the operator's request.
|
|
3
|
+
# headless database-operator session against the conversation transcript.
|
|
4
|
+
# The hidden recorder is the only writer to the Neo4j graph; the admin agent
|
|
5
|
+
# stays focused on the operator's request.
|
|
6
6
|
#
|
|
7
|
-
# Pipeline (Task 106
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
7
|
+
# Pipeline (Task 114 redesign — replaces Task 106's `--print` + stdin-EOT
|
|
8
|
+
# shape, which is incompatible with the OAuth subscription the manager runs
|
|
9
|
+
# on):
|
|
10
|
+
# 1. /spawn with `hidden=true, specialist=database-operator` — the manager
|
|
11
|
+
# produces a structurally normal admin specialist session (same argv,
|
|
12
|
+
# same OAuth path) and returns its sessionId.
|
|
13
|
+
# 2. POST /<sessionId>/input with `<instruction>\n\nConversation transcript:\n<filtered JSON>`.
|
|
14
|
+
# 3. The manager's recorder-auto-archive subscriber stops the PTY as soon
|
|
15
|
+
# as the recorder's JSONL contains an assistant message with
|
|
16
|
+
# `stop_reason === "end_turn"`. The hook does not need to follow up.
|
|
15
17
|
#
|
|
16
18
|
# Gating:
|
|
17
19
|
# - MAXY_SESSION_ROLE must equal "admin"
|
|
18
|
-
# - MAXY_HIDDEN_SPAWN must equal "0" (
|
|
19
|
-
#
|
|
20
|
-
# hooks itself, but the guard stays as a belt-and-braces backstop).
|
|
20
|
+
# - MAXY_HIDDEN_SPAWN must equal "0" (the recorder PTY itself runs with
|
|
21
|
+
# MAXY_HIDDEN_SPAWN=1; the guard prevents accidental recursion).
|
|
21
22
|
#
|
|
22
23
|
# Input: Claude Code's Stop hook stdin shape
|
|
23
24
|
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
|
|
@@ -33,10 +34,6 @@ if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
|
33
34
|
exit 0
|
|
34
35
|
fi
|
|
35
36
|
if [ "${MAXY_HIDDEN_SPAWN:-0}" = "1" ]; then
|
|
36
|
-
# Recorder PTY's own Stop hook would land here if claude --print ever
|
|
37
|
-
# emitted one. The lifecycle is now stdin-close → process-exit, so no
|
|
38
|
-
# self-stop POST is needed; exit silently to prevent accidental recursion
|
|
39
|
-
# into the admin branch below.
|
|
40
37
|
exit 0
|
|
41
38
|
fi
|
|
42
39
|
|
|
@@ -106,36 +103,17 @@ FILTERED_COUNT=$(printf '%s' "$FILTERED_TAIL" | python3 -c 'import sys,json; d=j
|
|
|
106
103
|
MANAGER_PORT="${CLAUDE_SESSION_MANAGER_PORT:-19400}"
|
|
107
104
|
MANAGER_BASE="http://127.0.0.1:${MANAGER_PORT}"
|
|
108
105
|
|
|
109
|
-
# Compose the recorder prompt + bundle it into the /spawn body's
|
|
110
|
-
# `stdinPayload`. The manager appends `--print` to the claude argv and
|
|
111
|
-
# writes (payload + EOT) to the PTY's stdin; claude consumes one turn and
|
|
112
|
-
# exits, the fs-watcher archives the row.
|
|
113
106
|
SPAWN_BODY=$(python3 -c '
|
|
114
|
-
import
|
|
115
|
-
sid = sys.argv[1]
|
|
116
|
-
tail_json = sys.argv[2]
|
|
117
|
-
prompt = (
|
|
118
|
-
"You are the database-operator specialist. Record the following completed turn "
|
|
119
|
-
"into the Neo4j graph. The transcript is a JSON array of filtered records "
|
|
120
|
-
"from the admin agent (user prompts, assistant responses, tool calls). "
|
|
121
|
-
f"Conversation id: {sid}.\n\nTranscript:\n{tail_json}\n"
|
|
122
|
-
)
|
|
107
|
+
import json
|
|
123
108
|
body = {
|
|
124
109
|
"senderId": "turn-recorder",
|
|
125
110
|
"role": "admin",
|
|
126
111
|
"channel": "browser",
|
|
127
112
|
"hidden": True,
|
|
128
113
|
"specialist": "database-operator",
|
|
129
|
-
"stdinPayload": prompt,
|
|
130
114
|
}
|
|
131
115
|
print(json.dumps(body))
|
|
132
|
-
'
|
|
133
|
-
|
|
134
|
-
if [ -z "$SPAWN_BODY" ]; then
|
|
135
|
-
exit 0
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
PAYLOAD_BYTES=$(printf '%s' "$SPAWN_BODY" | python3 -c 'import sys,json; b=json.load(sys.stdin); print(len(b.get("stdinPayload","").encode("utf-8")))' 2>/dev/null || echo 0)
|
|
116
|
+
' 2>/dev/null)
|
|
139
117
|
|
|
140
118
|
SPAWN_RES=$(curl -sS -X POST \
|
|
141
119
|
-H 'Content-Type: application/json' \
|
|
@@ -157,7 +135,31 @@ if [ -z "$RECORDER_SESSION_ID" ]; then
|
|
|
157
135
|
exit 0
|
|
158
136
|
fi
|
|
159
137
|
|
|
160
|
-
|
|
138
|
+
# Compose the input prompt and POST it to /<sessionId>/input. The instruction
|
|
139
|
+
# string is fixed (Task 114). The transcript is appended as a JSON array so
|
|
140
|
+
# the recorder reads it as a single user turn.
|
|
141
|
+
INPUT_BODY=$(python3 -c '
|
|
142
|
+
import sys, json
|
|
143
|
+
sid = sys.argv[1]
|
|
144
|
+
tail_json = sys.argv[2]
|
|
145
|
+
instruction = "update the graph with any missing information or intent that can be derived from this conversation"
|
|
146
|
+
text = (
|
|
147
|
+
f"{instruction}\n\n"
|
|
148
|
+
f"Conversation id: {sid}\n"
|
|
149
|
+
f"Conversation transcript:\n{tail_json}\n"
|
|
150
|
+
)
|
|
151
|
+
print(json.dumps({"text": text}))
|
|
152
|
+
' "$ADMIN_SESSION_ID" "$FILTERED_TAIL" 2>/dev/null)
|
|
153
|
+
|
|
154
|
+
INPUT_BYTES=$(printf '%s' "$INPUT_BODY" | python3 -c 'import sys,json; b=json.load(sys.stdin); print(len(b.get("text","").encode("utf-8")))' 2>/dev/null || echo 0)
|
|
155
|
+
|
|
156
|
+
INPUT_HTTP=$(curl -sS -o /tmp/turn-recorder-input.out -w '%{http_code}' -X POST \
|
|
157
|
+
-H 'Content-Type: application/json' \
|
|
158
|
+
--max-time 10 \
|
|
159
|
+
--data "$INPUT_BODY" \
|
|
160
|
+
"${MANAGER_BASE}/${RECORDER_SESSION_ID}/input" 2>/dev/null || echo 000)
|
|
161
|
+
|
|
162
|
+
echo "[turn-recorder] spawn-with-input sessionId=${RECORDER_SESSION_ID:0:8} bytes=${INPUT_BYTES} filtered-from-records=${FILTERED_COUNT} input-http=${INPUT_HTTP}" >&2
|
|
161
163
|
|
|
162
164
|
END_MS=$(python3 -c 'import time; print(int(time.time()*1000))')
|
|
163
165
|
ELAPSED=$((END_MS - START_MS))
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
// renderConfigYml
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
|
-
test("renderConfigYml: HTTPS-only matches
|
|
26
|
+
test("renderConfigYml: HTTPS-only matches expected shape byte-for-byte", () => {
|
|
27
27
|
const spec: IngressSpec = {
|
|
28
28
|
tunnelId: "abc-123",
|
|
29
29
|
credentialsPath: "/home/admin/.maxy/cloudflared/abc-123.json",
|
|
@@ -99,7 +99,7 @@ test("renderConfigYml: empty string hostnames are treated as absent", () => {
|
|
|
99
99
|
// renderTunnelState
|
|
100
100
|
// ---------------------------------------------------------------------------
|
|
101
101
|
|
|
102
|
-
test("renderTunnelState:
|
|
102
|
+
test("renderTunnelState: base fields preserved when ssh/smb absent", () => {
|
|
103
103
|
const state: TunnelState = {
|
|
104
104
|
tunnelId: "abc",
|
|
105
105
|
tunnelName: "maxy",
|
|
@@ -55,11 +55,10 @@ import { fileURLToPath } from "node:url";
|
|
|
55
55
|
// source-of-truth contract:
|
|
56
56
|
// Runtime: brand.json `cdpPort` at `${MAXY_PLATFORM_ROOT}/config/brand.json`
|
|
57
57
|
// is authoritative. Wrapper exports MAXY_PLATFORM_ROOT and BRAND. Missing
|
|
58
|
-
// env / file / field → loud-fail with one of three named reasons.
|
|
59
|
-
//
|
|
60
|
-
// `cdp-unreachable
|
|
61
|
-
//
|
|
62
|
-
// never falls back silently.
|
|
58
|
+
// env / file / field → loud-fail with one of three named reasons. A
|
|
59
|
+
// silent CDP-port default would make every non-default brand fail
|
|
60
|
+
// `cdp-unreachable`; runtime config never falls back silently
|
|
61
|
+
// (NEO4J_URI sets the same precedent).
|
|
63
62
|
//
|
|
64
63
|
// Test overrides: when BOTH `LIST_CF_DOMAINS_CDP_HOST` and
|
|
65
64
|
// `LIST_CF_DOMAINS_CDP_PORT` are set, they win over brand.json. The
|
|
@@ -531,10 +530,9 @@ async function dumpHtml(
|
|
|
531
530
|
// truncated mid-document. The false positive (outerHTML exactly at the
|
|
532
531
|
// ceiling) is a rounding coincidence on the order of 1-in-5M and is
|
|
533
532
|
// acceptable — a loud truncation signal with a ~10^-7 false-positive
|
|
534
|
-
// rate is strictly better than silent truncation
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
// can re-scrape with a larger cap or `outerHTML` unsliced.
|
|
533
|
+
// rate is strictly better than silent truncation. A downstream
|
|
534
|
+
// investigator seeing `truncated=true` can re-scrape with a larger
|
|
535
|
+
// cap or `outerHTML` unsliced.
|
|
538
536
|
const truncated = htmlStr.length >= OUTER_HTML_CAPTURE_MAX_CHARS;
|
|
539
537
|
return { path: dumpPath, truncated };
|
|
540
538
|
} catch (err) {
|
|
@@ -358,8 +358,8 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
358
358
|
SPAWN_STDERR="$(tr '\n' ' ' < "${CHROMIUM_SPAWN_ERR}" | head -c 300 || echo unavailable)"
|
|
359
359
|
rm -f "${CHROMIUM_SPAWN_ERR}"
|
|
360
360
|
# Loud-fail rather than fire-and-forget fallback: a systemd-run failure
|
|
361
|
-
#
|
|
362
|
-
#
|
|
361
|
+
# leaves no teardown handle. Operator should see the bus-not-running
|
|
362
|
+
# / linger-not-enabled cause.
|
|
363
363
|
phase_line setup-tunnel step=browser-spawn result=error \
|
|
364
364
|
reason=systemd-run-failed exit="${SPAWN_RC}" stderr="${SPAWN_STDERR}" \
|
|
365
365
|
unit="${CHROMIUM_UNIT}"
|
|
@@ -456,15 +456,13 @@ fi
|
|
|
456
456
|
# --------------------------------------------------------------------------
|
|
457
457
|
# Step 2+3: Resolve the tunnel identity from operator input.
|
|
458
458
|
#
|
|
459
|
-
#
|
|
460
|
-
# -
|
|
461
|
-
#
|
|
462
|
-
#
|
|
463
|
-
#
|
|
464
|
-
# said "Done. tunnel=maxy-maxytest" while the operator's `maxytest` hostname
|
|
465
|
-
# kept resolving via a stale orphan.
|
|
459
|
+
# The local hostname has no authority over which tunnel the operator's
|
|
460
|
+
# logged-in Cloudflare account holds. Deriving TUNNEL_NAME locally (e.g.
|
|
461
|
+
# "${BRAND}-$(hostname -s)") and reusing-or-creating it produces a new
|
|
462
|
+
# tunnel when the device is renamed while existing CNAMEs continue to
|
|
463
|
+
# point at the old one.
|
|
466
464
|
#
|
|
467
|
-
#
|
|
465
|
+
# Contract: the form (rendered via /api/admin/cloudflare/tunnels list) is
|
|
468
466
|
# the source of truth. The endpoint passes exactly one of:
|
|
469
467
|
# TUNNEL_ID — operator selected an existing tunnel from the list
|
|
470
468
|
# TUNNEL_NAME — operator typed a name to create a new tunnel
|
|
@@ -505,8 +503,8 @@ if [ -n "${TUNNEL_ID:-}" ]; then
|
|
|
505
503
|
TUNNEL_SOURCE="operator-selected"
|
|
506
504
|
else
|
|
507
505
|
# Operator-create branch. Refuse to silently reuse an existing tunnel of
|
|
508
|
-
# the same name —
|
|
509
|
-
# The operator should have picked it from the list.
|
|
506
|
+
# the same name — silent reuse hides which tunnel CNAMEs actually point
|
|
507
|
+
# at. The operator should have picked it from the list.
|
|
510
508
|
EXISTING_ID="$(cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel list --output json 2>/dev/null \
|
|
511
509
|
| jq -r --arg N "${TUNNEL_NAME}" '.[]? | select(.name == $N) | .id' | head -1)"
|
|
512
510
|
if [ -n "${EXISTING_ID}" ] && [ "${EXISTING_ID}" != "null" ]; then
|
|
@@ -111,9 +111,9 @@ export function renderConfigYml(spec: IngressSpec): string {
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* Render the tunnel.state JSON, preserving SSH/SMB hostnames as additive
|
|
114
|
-
* fields so re-runs can rehydrate them. The shape
|
|
115
|
-
*
|
|
116
|
-
*
|
|
114
|
+
* fields so re-runs can rehydrate them. The base shape (tunnelId,
|
|
115
|
+
* tunnelName, domain, configPath, credentialsPath) is a superset that
|
|
116
|
+
* consumers without ssh/smb knowledge continue to read correctly.
|
|
117
117
|
*/
|
|
118
118
|
export function renderTunnelState(state: TunnelState): string {
|
|
119
119
|
const out: Record<string, string> = {
|
|
@@ -163,24 +163,10 @@ Each installed brand runs two per-brand `--user` systemd units (earlier platform
|
|
|
163
163
|
- `{hostname}.service` — the admin + public HTTP server on `127.0.0.1:19201` (public port + 1). Restarted by the upgrade flow; short downtime is expected during steps 8→11 of an upgrade. An earlier fix: the unit carries two port env vars — `PORT=<public>` (canonical public port, read by the upgrade detector) and `MAXY_UI_INTERNAL_PORT=<public+1>` (the port maxy-ui actually binds).
|
|
164
164
|
- `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service and handles `/websockify` (VNC) WebSocket upgrades locally. An earlier fix: also hosts `/api/admin/actions/*` and `/api/admin/version*` — the Software Update modal's own routes — so the log stream survives the brand service's restart window. Does NOT restart during an upgrade — the browser WebSocket stays connected by construction.
|
|
165
165
|
|
|
166
|
-
**Port-drift recovery.** Devices upgraded between Tasks 647 and 666 may have drifted +1 on every upgrade because the pre-Task-666 installer wrote `Environment=PORT=<internal>` into `{hostname}.service` and the upgrade reader correctly treated `PORT=` as public. The first post-Task-666 install detects this (comparing maxy's PORT against the edge's EDGE_PORT) and emits a one-shot loud log: `[port-recovery] detected drift maxy=<X> edge=<Y> — pinning at <Y>`. Subsequent upgrades are silent. If your Cloudflare tunnel was pointing at a drifted port, the ingress `config.yml` still needs a one-time manual fix: `sed -i 's|localhost:<old>|localhost:<current>|' ~/.{configDir}/cloudflared/config.yml && cloudflared tunnel ingress validate`. {{productName}} never rewrites cloudflared config programmatically.
|
|
167
|
-
|
|
168
166
|
Upgrade and Cloudflare setup run as detached actions: `systemd-run --user` transient units per invocation with stdout+stderr persisted to `~/.maxy/logs/actions/<actionId>.log` and streamed to the UI via SSE. No boot-time service file exists for these.
|
|
169
167
|
|
|
170
168
|
If an action looks stuck, read `~/.maxy/logs/actions/<actionId>.log` directly for the full output, or `journalctl --user --identifier=maxy-action-<actionId>` for systemd's record.
|
|
171
169
|
|
|
172
|
-
**Pre-Task-662 / pre-Task-664 upgrade** — devices that ran an installer have legacy shared `maxy-edge.service` / `maxy-ttyd.service` units; devices that ran have per-brand `{hostname}-ttyd.service` units plus a pinned `/usr/local/bin/ttyd` binary. Neither is removed automatically — do this cleanup once per device before re-running any installer:
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
systemctl --user stop maxy-edge maxy-ttyd realagent-ttyd 2>/dev/null || true
|
|
176
|
-
systemctl --user disable maxy-edge maxy-ttyd realagent-ttyd 2>/dev/null || true
|
|
177
|
-
rm -f ~/.config/systemd/user/maxy-edge.service \
|
|
178
|
-
~/.config/systemd/user/maxy-ttyd.service \
|
|
179
|
-
~/.config/systemd/user/realagent-ttyd.service
|
|
180
|
-
sudo rm -f /usr/local/bin/ttyd
|
|
181
|
-
systemctl --user daemon-reload
|
|
182
|
-
```
|
|
183
|
-
|
|
184
170
|
## Linux laptops: snap-confined Chromium replacement
|
|
185
171
|
|
|
186
172
|
On Ubuntu 24.04 (Noble) the system Chromium binary at `/usr/bin/chromium` is a symlink into the snap. Snap's AppArmor profile denies writes to hidden directories under your home folder, so the per-brand Chromium profile at `~/.{brand}/chromium-profile/` is unwritable and the VNC browser never starts. Pi installs (Debian Bookworm) are unaffected because Bookworm ships a real `.deb` chromium.
|
|
@@ -70,11 +70,11 @@ There is no dashboard, no settings panel, no menus. Everything is done through c
|
|
|
70
70
|
|
|
71
71
|
The chat input auto-grows as you type — it expands to fit your message and shrinks back when you delete text. You can also drag the resize handle above the input to set a custom height.
|
|
72
72
|
|
|
73
|
-
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (the database-operator recorder, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with Maxy and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The chat and artefact divider is drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Open JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
73
|
+
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (the database-operator recorder, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with Maxy and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every admin page (Sessions root, Graph, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all three pages. The drag handle is mounted by each AdminShell consumer rather than by AdminShell itself, so any new admin route must include `<SidebarSplitter />` as a direct child of its `<AdminShell>` to pick up the shared width. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Open JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
74
74
|
|
|
75
75
|
Page titles are brand-aware: the browser tab shows your product name (e.g. `Real Agent` instead of `Maxy`) on every shell — chat, graph, and data — so a non-default brand never leaks the default name in tab strips or browser history.
|
|
76
76
|
|
|
77
|
-
**Session lifecycle and reconcile model.** The sidebar Sessions list is driven by a single Server-Sent Events feed at `/api/admin/claude-sessions/events`. The session manager watches the two directories Claude Code writes (`${CLAUDE_CONFIG_DIR}/sessions/<pid>.json` for live state, `${CLAUDE_CONFIG_DIR}/projects/<slug>/<sid>.jsonl` for transcripts) and emits `row-created`, `row-updated`, `row-archived`, or `row-removed` deltas to every connected browser tab. Three real delete shapes map to deltas — there is no fourth: PID file gone with JSONL surviving demotes the row to `row-archived`; PID file gone with no JSONL ever written (the per-turn recorder case) emits `row-removed` against the unindexed sessionId; a JSONL deletion against an already-unindexed row also emits `row-removed`. The recorder branch is what reconciles transient hidden spawns — without it, ghost rows persist after the recorder exits. On connect the manager replays the current row index so a freshly-opened tab catches up without polling, then streams deltas as files change on disk. Two open tabs see the same list within ~300ms of any spawn, status flip, or exit; no refresh button required for state to be current. The legacy `/list` fetch and `useAdminSessions` hook stay mounted to serve the ConversationsModal and the post-action reconcile path in `session-actions`, but the sidebar's visible rows come from the row store, not from `/list`. Each EventSource open emits `[admin-events] client-connected ip=<…> seeded-rows=<n>` server-side and `[admin-ui] session-row-store connected events-received=<n>` in the browser console; transport drops log `[admin-ui] session-row-store reconnect trigger=<auto|manual> attempt=<n> delay-ms=<n>` until the EventSource reattaches. The small dot at the right edge of the Active/Archived/All segmented control is the live-updates indicator: sage when the SSE feed is connected, grey when the feed has dropped. The grey state is an actionable button — clicking it cancels any pending backoff and re-opens the feed immediately, with the click logged as `trigger=manual` so manual retries are distinguishable from automatic ones in the console.
|
|
77
|
+
**Session lifecycle and reconcile model.** The sidebar Sessions list is driven by a single Server-Sent Events feed at `/api/admin/claude-sessions/events`. The session manager watches the two directories Claude Code writes (`${CLAUDE_CONFIG_DIR}/sessions/<pid>.json` for live state, `${CLAUDE_CONFIG_DIR}/projects/<slug>/<sid>.jsonl` for transcripts) and emits `row-created`, `row-updated`, `row-archived`, or `row-removed` deltas to every connected browser tab. Three real delete shapes map to deltas — there is no fourth: PID file gone with JSONL surviving demotes the row to `row-archived`; PID file gone with no JSONL ever written (the per-turn recorder case) emits `row-removed` against the unindexed sessionId; a JSONL deletion against an already-unindexed row also emits `row-removed`. The recorder branch is what reconciles transient hidden spawns — without it, ghost rows persist after the recorder exits. On connect the manager replays the current row index so a freshly-opened tab catches up without polling, then streams deltas as files change on disk. Two open tabs see the same list within ~300ms of any spawn, status flip, or exit; no refresh button required for state to be current. The legacy `/list` fetch and `useAdminSessions` hook stay mounted to serve the ConversationsModal and the post-action reconcile path in `session-actions`, but the sidebar's visible rows come from the row store, not from `/list`. Each EventSource open emits `[admin-events] client-connected ip=<…> seeded-rows=<n>` server-side and `[admin-ui] session-row-store connected events-received=<n>` in the browser console; transport drops log `[admin-ui] session-row-store reconnect trigger=<auto|manual> attempt=<n> delay-ms=<n>` until the EventSource reattaches. The small dot at the right edge of the Active/Archived/All segmented control is the live-updates indicator: sage when the SSE feed is connected, grey when the feed has dropped. The grey state is an actionable button — clicking it cancels any pending backoff and re-opens the feed immediately, with the click logged as `trigger=manual` so manual retries are distinguishable from automatic ones in the console. The refresh icon at the top of the Sessions list is the operator-recoverable reconcile path against any SSE gap: it fetches `/api/admin/claude-sessions` and passes the authoritative id set to the row store, which evicts any indexed row that the server no longer reports. SSE replay only re-asserts currently-indexed rows and never emits `row-removed` for a row that vanished while disconnected, so without this manual surface a stale row can persist until the operator reloads the tab. Each click logs `[admin-ui] session-row-store reconcile evicted=<n> kept=<n>` when at least one row is evicted, and is silent otherwise.
|
|
78
78
|
|
|
79
79
|
The row feed sits behind `requireAdminSession` like every other admin route, so the URL must carry `?session_key=<cacheKey>` — `EventSource` cannot send custom headers, so the query string is the only viable transport. Every admin URL (fetch and EventSource alike) routes through the shared `appendAdminSessionKey(url, cacheKey)` helper exported from `app/lib/useAdminFetch.ts`, which is the single source of truth for the convention; no caller constructs the query string by hand. On a 4xx rejection the browser-side store probes the same URL once per reconnect (suppressed after a successful `open`, capped at one fetch per attempt) and logs `[admin-ui] session-row-store sse-error status=<n> code=<code> attempt=<n>`. The `code` field uses the closed `AdminSessionRejectCode` taxonomy (`session-missing | session-not-registered | session-expired-age | grant-expired`, plus a default `unknown` bucket) that mirrors the server-side rejection emitted by `requireAdminSession`, so a single grep correlates client and server timelines on the same code.
|
|
80
80
|
|
|
@@ -73,7 +73,7 @@ Brand decides which premium plugins ship. Maxy installs ship only `platform/plug
|
|
|
73
73
|
| Plugin | Type | What it does | Public agent |
|
|
74
74
|
|--------|------|-------------|-------------|
|
|
75
75
|
| `teaching` | Skills | Interactive tutoring, lesson planning, and study pack generation from your knowledge base | Yes — all 3 skills serve students and parents |
|
|
76
|
-
| `real-agent` | Bundle (10 sub-plugins) | UK estate agency skills — sales, listings, vendor management, buyer management, lead generation, coaching, business operations, teaching, Loop CRM, and property brochures. 3 specialist roles (negotiator, valuer, compliance) | 4 sub-plugins (estate-sales, buyers, estate-coaching, estate-teaching) |
|
|
76
|
+
| `real-agent` | Bundle (10 sub-plugins) | UK estate agency skills — sales, listings, vendor management, buyer management, lead generation, coaching, business operations, teaching, Loop CRM (five value pillars: auto-respond, viewing lifecycle, pipeline mining, listings prospecting, maintenance & preferences), and property brochures. 3 specialist roles (negotiator, valuer, compliance) | 4 sub-plugins (estate-sales, buyers, estate-coaching, estate-teaching) |
|
|
77
77
|
| `writer-craft` | Skills + Agent | Manuscript review and writing craft — story architecture, reader engagement, prose craft, editorial practice, and multi-level review | No — writing craft serves the author |
|
|
78
78
|
|
|
79
79
|
**How it works:** Every boot {{productName}} delivers the brand's premium plugins from staging into `platform/plugins/` and stamps `enabledPlugins` against what is actually on disk. No conversation needed — the brand's full set is active from the first turn after install. Updates and reinstalls re-deliver from staging.
|
|
@@ -178,7 +178,7 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
|
|
|
178
178
|
|
|
179
179
|
**Continuous `err-class=econnrefused-coldstart` for >30 s past the last `[edge] listening` line** indicates the upstream never binds — the upgrade or boot has stalled. Recover via `sudo systemctl --user status maxy.service` and check the action runner log per the next section. Permanent-failure UI escalation (turning the holding page into an error after N seconds) is intentionally deferred.
|
|
180
180
|
|
|
181
|
-
**The literal string `maxy-ui` should never appear in `edge.log` or in any user-visible 502 body**, regardless of brand. If it does, the edge is running
|
|
181
|
+
**The literal string `maxy-ui` should never appear in `edge.log` or in any user-visible 502 body**, regardless of brand. If it does, the edge is running stale code — re-bundle and re-publish.
|
|
182
182
|
|
|
183
183
|
**Verifying the holding page locally:** `curl -sS -H 'Accept: text/html' http://127.0.0.1:<EDGE_PORT>/` while `maxy.service` is stopped should return HTML containing the brand `productName`. The `Accept: text/html` header is required — non-html clients (default `curl`, `fetch`, XHR) get the branded plain-text 502 instead, so the holding page's own `/api/health` polls don't break themselves during cold-start.
|
|
184
184
|
|
|
@@ -262,7 +262,7 @@ Failure modes:
|
|
|
262
262
|
|
|
263
263
|
## Software Update click shows an error instead of opening the terminal
|
|
264
264
|
|
|
265
|
-
> **
|
|
265
|
+
> **First-line diagnostic for the byte-stream xterm.js terminal surface:** `sudo systemctl --user status maxy-ttyd` plus `sudo grep 'ttyd-proxy' ~/.maxy/logs/edge-boot.log | tail -20`. Failure mode signals: `ttyd-ws-upgrade accepted` with no `ttyd-proxy-open` → `maxy-ttyd.service` is down; `ttyd-proxy-open` with no `ttyd-proxy-chunk dir=upstream→client` → ttyd/tmux is not attaching a PTY.
|
|
266
266
|
|
|
267
267
|
**Symptom:** You clicked **Upgrade** in the Software Update modal, but instead of the VNC terminal overlay appearing, the modal shows a red error row like:
|
|
268
268
|
|
|
@@ -337,7 +337,7 @@ sudo grep 'started.*reason=upgrade' ~/.maxy/logs/terminal-launch.log | tail -3
|
|
|
337
337
|
# Expected shape: started pid=<N> display=:99 cmd="/usr/bin/xterm... -e bash -c 'npx -y @rubytech/create-maxy@latest; exec bash'" transport=vnc windowPresent=true reason=upgrade
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
-
If the `cmd=` field does not contain `-e bash -c`, re-run the installer — the vnc.sh on the device is
|
|
340
|
+
If the `cmd=` field does not contain `-e bash -c`, re-run the installer — the vnc.sh on the device is stale. If the command IS logged correctly but nothing is running, open the VNC overlay and type `history | tail` inside the shell — if the npx line is there, it ran and exited (check `~/.maxy/logs/install-*.log` for the exit status).
|
|
341
341
|
|
|
342
342
|
---
|
|
343
343
|
|
|
@@ -533,9 +533,9 @@ sudo systemctl --user start maxy-ui
|
|
|
533
533
|
|
|
534
534
|
## Admin DevTools console floods with `onboarding-banner-mount` or `sessions-poll` lines
|
|
535
535
|
|
|
536
|
-
**
|
|
536
|
+
**Regression symptom.** Open DevTools on the admin shell at `/` with `onboardingComplete=false`, leave the page idle for a minute, then scroll back through the console. Thousands of `[admin-ui] onboarding-banner-mount onboardingComplete=false` lines (one per AdminShell render, ~40/min driven by the 3s sessions poll) with no per-tick poll telemetry indicates the banner-mount log has regressed back into the render body.
|
|
537
537
|
|
|
538
|
-
**Steady-state invariants at
|
|
538
|
+
**Steady-state invariants at `/`:**
|
|
539
539
|
|
|
540
540
|
- `grep -c '\[admin-ui\] onboarding-banner-mount' ~/.maxy/logs/admin-ui-console.log` equals page-load count plus onboarding-flip count, not the render count. Sustained climb at idle means the banner mount log regressed back into the render body (fix).
|
|
541
541
|
- `grep -c '\[admin-ui\] sessions-poll' ~/.maxy/logs/admin-ui-console.log` over a 60-minute idle window equals zero. The hook no longer installs a `setInterval`; every `sessions-poll` line is operator-triggered (initial mount, refresh button, post-mutation refetch). One or more lines during operator idle means `setInterval` was reinstated.
|
|
@@ -18,7 +18,13 @@ tools:
|
|
|
18
18
|
publicAllowlist: false
|
|
19
19
|
- name: loop-property-listed
|
|
20
20
|
publicAllowlist: false
|
|
21
|
-
- name: loop-property-
|
|
21
|
+
- name: loop-property-sold
|
|
22
|
+
publicAllowlist: false
|
|
23
|
+
- name: loop-property-viewing
|
|
24
|
+
publicAllowlist: false
|
|
25
|
+
- name: loop-property-callback
|
|
26
|
+
publicAllowlist: false
|
|
27
|
+
- name: loop-property-information
|
|
22
28
|
publicAllowlist: false
|
|
23
29
|
- name: loop-viewing-search
|
|
24
30
|
publicAllowlist: false
|
|
@@ -44,6 +50,8 @@ tools:
|
|
|
44
50
|
publicAllowlist: false
|
|
45
51
|
- name: loop-marketing-enquiry
|
|
46
52
|
publicAllowlist: false
|
|
53
|
+
- name: loop-auto-responder
|
|
54
|
+
publicAllowlist: false
|
|
47
55
|
- name: loop-customer-preferences
|
|
48
56
|
publicAllowlist: false
|
|
49
57
|
- name: loop-supplier
|
|
@@ -112,20 +120,67 @@ Never include API key values in responses. Identify teams by name only. When the
|
|
|
112
120
|
|
|
113
121
|
## When to use these tools
|
|
114
122
|
|
|
115
|
-
|
|
123
|
+
Loop covers five value pillars with API surface. Each pillar has both read and write triggers — use reads to surface what's already in Loop unprompted, use writes when the operator's intent is to record a decision. See `.docs/loop-value-extraction.md` for the verbatim Loop value claims and swagger endpoint citations behind each pillar.
|
|
124
|
+
|
|
125
|
+
### Pillar 1 — Auto-respond to portal enquiries (Loop's stated 40% efficiency lever)
|
|
126
|
+
|
|
127
|
+
**Read triggers** — operator says "what enquiries came in", "any new enquiries", "did we get anything from Rightmove":
|
|
128
|
+
- `loop-auto-responder` (action=get) — fetch enquiry questions + applicant details
|
|
129
|
+
|
|
130
|
+
**Write triggers** — operator says "answer the enquiry", "send the auto-responder reply", "refer it to <team>":
|
|
131
|
+
- `loop-auto-responder` (action=answers) — submit applicant answers
|
|
132
|
+
- `loop-auto-responder` (action=details) — update enquiry record
|
|
133
|
+
- `loop-auto-responder` (action=refer) — refer enquiry to another team
|
|
134
|
+
- `loop-marketing-enquiry` — submit a seller-side enquiry
|
|
135
|
+
|
|
136
|
+
### Pillar 2 — Viewing lifecycle (book → feedback → notes)
|
|
137
|
+
|
|
138
|
+
**Read triggers** — "what viewings are coming up", "any viewings without feedback yet", "did we hear back from <name>":
|
|
139
|
+
- `loop-viewing-search`, `loop-viewing-detail`, `loop-feedback-get`
|
|
140
|
+
|
|
141
|
+
**Write triggers** — "book a viewing", "the viewer said X", "the seller wants Y", "add a note":
|
|
142
|
+
- `loop-viewing-create` — book a viewing
|
|
143
|
+
- `loop-viewing-update` — record buyer/renter feedback, seller/landlord feedback, or a note
|
|
144
|
+
- `loop-feedback-submit` — explicit feedback submission against a viewing
|
|
145
|
+
|
|
146
|
+
### Pillar 3 — Pipeline mining (unconverted enquiries, stale matches)
|
|
147
|
+
|
|
148
|
+
**Read triggers** — "who haven't we followed up", "any matches we haven't actioned", "find people looking in <area>", "show me buyers":
|
|
149
|
+
- `loop-people-search`, `loop-people-detail`
|
|
150
|
+
- `loop-marketing-match`, `loop-marketing-match-batch`
|
|
151
|
+
|
|
152
|
+
**Write triggers** — "request a viewing for the match", "ask them to call", "send them more info":
|
|
153
|
+
- `loop-marketing-match-request`
|
|
154
|
+
|
|
155
|
+
### Pillar 4 — Listings prospecting (canvassing + competitive sweep)
|
|
156
|
+
|
|
157
|
+
**Read triggers** — "what have we sold on <street>", "what's listed on Rightmove", "give me canvassing material for <postcode>":
|
|
158
|
+
- `loop-property-sold` — exchanged/completed properties by channel (canvassing source)
|
|
159
|
+
- `loop-property-listed` — currently active listings by channel
|
|
160
|
+
- `loop-property-search`, `loop-property-detail`
|
|
161
|
+
|
|
162
|
+
**Write triggers** — "request a viewing", "ask them to call back", "request a brochure":
|
|
163
|
+
- `loop-property-viewing` — viewing request against a specific property
|
|
164
|
+
- `loop-property-callback` — callback request
|
|
165
|
+
- `loop-property-information` — information/brochure request
|
|
166
|
+
|
|
167
|
+
### Pillar 5 — Maintenance closeout and customer preferences
|
|
168
|
+
|
|
169
|
+
**Read triggers** — "what does <person> want", "any open maintenance jobs", "any quotes outstanding":
|
|
170
|
+
- `loop-customer-preferences` (action=read)
|
|
171
|
+
- `loop-supplier` (action=maintenance-jobs, maintenance-quotes, board-jobs)
|
|
172
|
+
|
|
173
|
+
**Write triggers** — "save what they're looking for", "mark the job complete", "submit a quote":
|
|
174
|
+
- `loop-customer-preferences` (action=write)
|
|
175
|
+
- `loop-supplier` (action=maintenance-complete, board-complete, maintenance-submit-quote)
|
|
176
|
+
|
|
177
|
+
### Team-key prerequisite
|
|
116
178
|
|
|
117
|
-
|
|
118
|
-
- "submit", "send", "log", "record" → `loop-feedback-submit`, `loop-marketing-enquiry`, `loop-property-request`
|
|
119
|
-
- "create", "book", "schedule", "set up" a viewing → `loop-viewing-create`
|
|
120
|
-
- "add a note", "leave feedback", "record what they said" → `loop-viewing-update`
|
|
121
|
-
- "request a callback", "request more info", "ask for a viewing" → `loop-property-request` or `loop-marketing-match-request`
|
|
122
|
-
- "update preferences", "save what they're looking for" → `loop-customer-preferences` (action=write)
|
|
123
|
-
- "mark complete", "close out", "finished the job" → `loop-supplier` (action=maintenance-complete or board-complete)
|
|
124
|
-
- "submit a quote" → `loop-supplier` (action=maintenance-submit-quote)
|
|
179
|
+
Every Loop tool routes through one or more team keys. If `loop-key-list` returns nothing for the account, surface the registration prompt before attempting any other Loop call — every downstream tool will fail otherwise. If a call returns `Team "X" does not have <group> permission`, the key was scoped to fewer than 8 endpoint groups; re-register with the full permissions array (`["properties","people","viewings","feedback","team","marketing","customer","supplier"]`).
|
|
125
180
|
|
|
126
|
-
|
|
181
|
+
### What Loop markets without an API surface (do not promise these)
|
|
127
182
|
|
|
128
|
-
|
|
183
|
+
Loop's marketing pages also list Tasks/Reminders, full Diary beyond `team/availability`, Market Data (distressed properties, instructed sellers), Inspections & Certificates, and Client Accounts. Loop's public V2 API does not expose these — Real Agent cannot do them. See `.docs/loop-api-gaps.md` for the full inventory.
|
|
129
184
|
|
|
130
185
|
## Cross-cutting skills hosted here
|
|
131
186
|
|
|
@@ -9,7 +9,8 @@ import { keyList } from "./tools/key-list.js";
|
|
|
9
9
|
import { propertySearch } from "./tools/property-search.js";
|
|
10
10
|
import { propertyDetail } from "./tools/property-detail.js";
|
|
11
11
|
import { propertyListed } from "./tools/property-listed.js";
|
|
12
|
-
import {
|
|
12
|
+
import { propertySold } from "./tools/property-sold.js";
|
|
13
|
+
import { propertyViewing, propertyCallback, propertyInformation } from "./tools/property-intent.js";
|
|
13
14
|
import { peopleSearch } from "./tools/people-search.js";
|
|
14
15
|
import { peopleDetail } from "./tools/people-detail.js";
|
|
15
16
|
import { viewingSearch } from "./tools/viewing-search.js";
|
|
@@ -23,6 +24,7 @@ import { marketingMatchDetail } from "./tools/marketing-match.js";
|
|
|
23
24
|
import { marketingMatchBatch } from "./tools/marketing-match-batch.js";
|
|
24
25
|
import { marketingMatchRequest } from "./tools/marketing-match-request.js";
|
|
25
26
|
import { marketingEnquiry } from "./tools/marketing-enquiry.js";
|
|
27
|
+
import { autoResponder } from "./tools/auto-responder.js";
|
|
26
28
|
import { customerPreferences } from "./tools/customer-preferences.js";
|
|
27
29
|
import { supplier } from "./tools/supplier.js";
|
|
28
30
|
import { closeDriver } from "./lib/neo4j.js";
|
|
@@ -144,23 +146,47 @@ server.tool("loop-property-detail", "Get full details for a specific property by
|
|
|
144
146
|
previewHash: z.number().int().optional().describe("Preview hash for public preview URL"),
|
|
145
147
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
146
148
|
}, toolHandler(async (p) => propertyDetail(p)));
|
|
147
|
-
server.tool("loop-property-listed", "Get properties listed on a specific channel (Rightmove, Zoopla, OnTheMarket, website).
|
|
149
|
+
server.tool("loop-property-listed", "Get properties currently listed on a specific channel (Rightmove, Zoopla, OnTheMarket, website). For exchanged/completed properties use loop-property-sold.", {
|
|
148
150
|
channel: z.enum(["rightmove", "onTheMarket", "zoopla", "website"]).describe("Listing channel"),
|
|
149
151
|
department: z.enum(["sales", "lettings", "both"]).optional().describe("Department (default: both)"),
|
|
150
|
-
includeSold: z.boolean().optional().describe("Include sold gallery (default: false)"),
|
|
151
152
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
152
153
|
limit: z.number().int().optional().describe("Max results (default 200)"),
|
|
153
154
|
}, toolHandler(async (p) => propertyListed(p)));
|
|
154
|
-
server.tool("loop-property-
|
|
155
|
+
server.tool("loop-property-sold", "Get exchanged or completed properties on a specific channel. Powers canvassing from recent sales and sold-gallery rendering.", {
|
|
156
|
+
channel: z.enum(["rightmove", "onTheMarket", "zoopla", "website"]).describe("Listing channel"),
|
|
157
|
+
teamName: z.string().optional().describe("Query a specific team only"),
|
|
158
|
+
limit: z.number().int().optional().describe("Max results (default 200)"),
|
|
159
|
+
}, toolHandler(async (p) => propertySold(p)));
|
|
160
|
+
// Property-level intent tools (Task 103 split — replaces loop-property-request).
|
|
161
|
+
// Each intent is a distinct operator action so the agent's tool surface
|
|
162
|
+
// preserves intent fidelity.
|
|
163
|
+
server.tool("loop-property-viewing", "Request a viewing for a property. Use when the operator says 'book a viewing', 'arrange a viewing', or 'ask to see the property'.", {
|
|
164
|
+
teamName: z.string().min(1).describe("Team to submit the request through"),
|
|
165
|
+
propertyId: z.number().int().describe("Property ID"),
|
|
166
|
+
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
167
|
+
name: z.string().optional().describe("Requester name"),
|
|
168
|
+
email: z.string().optional().describe("Requester email"),
|
|
169
|
+
phone: z.string().optional().describe("Requester phone"),
|
|
170
|
+
message: z.string().optional().describe("Additional message"),
|
|
171
|
+
}, toolHandler(async (p) => propertyViewing(p)));
|
|
172
|
+
server.tool("loop-property-callback", "Request a callback about a property. Use when the operator says 'ask them to call', 'request a callback', or 'have someone ring back'.", {
|
|
155
173
|
teamName: z.string().min(1).describe("Team to submit the request through"),
|
|
156
174
|
propertyId: z.number().int().describe("Property ID"),
|
|
157
175
|
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
158
|
-
action: z.enum(["viewing", "call-back", "information"]).describe("Request type"),
|
|
159
176
|
name: z.string().optional().describe("Requester name"),
|
|
160
177
|
email: z.string().optional().describe("Requester email"),
|
|
161
178
|
phone: z.string().optional().describe("Requester phone"),
|
|
162
179
|
message: z.string().optional().describe("Additional message"),
|
|
163
|
-
}, toolHandler(async (p) =>
|
|
180
|
+
}, toolHandler(async (p) => propertyCallback(p)));
|
|
181
|
+
server.tool("loop-property-information", "Request more information about a property. Use when the operator says 'ask for details', 'request the brochure', or 'find out more'.", {
|
|
182
|
+
teamName: z.string().min(1).describe("Team to submit the request through"),
|
|
183
|
+
propertyId: z.number().int().describe("Property ID"),
|
|
184
|
+
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
185
|
+
name: z.string().optional().describe("Requester name"),
|
|
186
|
+
email: z.string().optional().describe("Requester email"),
|
|
187
|
+
phone: z.string().optional().describe("Requester phone"),
|
|
188
|
+
message: z.string().optional().describe("Additional message"),
|
|
189
|
+
}, toolHandler(async (p) => propertyInformation(p)));
|
|
164
190
|
// ─────────────────────────────────────────────────────────────────
|
|
165
191
|
// Viewing Tools (4) — permission: viewings
|
|
166
192
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -245,18 +271,18 @@ server.tool("loop-marketing-match-request", "Submit a viewing, information, or c
|
|
|
245
271
|
phone: z.string().optional().describe("Requester phone"),
|
|
246
272
|
message: z.string().optional().describe("Additional message"),
|
|
247
273
|
}, toolHandler(async (p) => marketingMatchRequest(p)));
|
|
248
|
-
server.tool("loop-marketing-enquiry", "
|
|
274
|
+
server.tool("loop-marketing-enquiry", "Submit a seller-side marketing enquiry to Loop. For inbound applicant auto-responder workflows use loop-auto-responder.", {
|
|
249
275
|
teamName: z.string().min(1).describe("Team to submit through"),
|
|
250
|
-
|
|
251
|
-
"seller-enquiry", "autoresponder-get", "autoresponder-answers",
|
|
252
|
-
"autoresponder-details", "autoresponder-refer",
|
|
253
|
-
]).describe("Enquiry action"),
|
|
254
|
-
sellerEnquiryData: z.record(z.string(), z.unknown()).optional().describe("Seller enquiry request body (for seller-enquiry)"),
|
|
255
|
-
autoResponderId: z.number().int().optional().describe("Auto-responder enquiry ID"),
|
|
256
|
-
autoResponderKey: z.string().optional().describe("Auto-responder key (UUID)"),
|
|
257
|
-
answers: z.array(z.unknown()).optional().describe("Auto-responder answers array"),
|
|
258
|
-
details: z.record(z.string(), z.unknown()).optional().describe("Auto-responder details object"),
|
|
276
|
+
sellerEnquiryData: z.record(z.string(), z.unknown()).optional().describe("Seller enquiry request body"),
|
|
259
277
|
}, toolHandler(async (p) => marketingEnquiry(p)));
|
|
278
|
+
server.tool("loop-auto-responder", "Read or progress an inbound enquiry through Loop's auto-responder (the surface behind Loop's 40% enquiry-efficiency claim). action=get reads questions+details; answers submits applicant replies; details updates the enquiry record; refer hands it to another team.", {
|
|
279
|
+
teamName: z.string().min(1).describe("Team for this operation"),
|
|
280
|
+
action: z.enum(["get", "answers", "details", "refer"]).describe("Auto-responder action"),
|
|
281
|
+
autoResponderId: z.number().int().describe("Auto-responder enquiry ID"),
|
|
282
|
+
autoResponderKey: z.string().describe("Auto-responder key (UUID issued at enquiry creation)"),
|
|
283
|
+
answers: z.array(z.unknown()).optional().describe("Applicant answers (required for action=answers)"),
|
|
284
|
+
details: z.record(z.string(), z.unknown()).optional().describe("Enquiry details (for action=details)"),
|
|
285
|
+
}, toolHandler(async (p) => autoResponder(p)));
|
|
260
286
|
// ─────────────────────────────────────────────────────────────────
|
|
261
287
|
// Customer Tools (1) — permission: customer
|
|
262
288
|
// ─────────────────────────────────────────────────────────────────
|