@rubytech/create-realagent 1.0.647 → 1.0.649
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 +76 -3
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +1 -11
- package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -2
- package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize.mjs +274 -0
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +69 -0
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +401 -0
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +73 -3
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +4 -2
- package/payload/platform/plugins/docs/references/cloudflare.md +1 -0
- package/payload/platform/plugins/docs/references/deployment.md +11 -0
- package/payload/platform/plugins/docs/references/getting-started.md +2 -0
- package/payload/platform/plugins/docs/references/memory-guide.md +2 -2
- package/payload/platform/plugins/docs/references/platform.md +6 -0
- package/payload/platform/plugins/docs/references/troubleshooting.md +16 -0
- package/payload/platform/plugins/memory/references/graph-primitives.md +5 -5
- package/payload/platform/templates/dotfiles/.tmux.conf +1 -0
- package/payload/platform/templates/systemd/maxy-ttyd.service +20 -0
- package/payload/server/public/assets/admin-BtjYSo0M.js +362 -0
- package/payload/server/public/assets/admin-kHJ-D0s7.css +1 -0
- package/payload/server/public/assets/{arc-DcrodP5U.js → arc-BMhgytDB.js} +1 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-S9-oeq_x.js +1 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Cvo_8X6C.js → architectureDiagram-Q4EWVU46-BePoi8XC.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CEK06TEn.js → blockDiagram-DXYQGD6D-BkiwLTtq.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-QjYUSwTU.js → c4Diagram-AHTNJAMY-bpjPj2Ln.js} +1 -1
- package/payload/server/public/assets/channel-D3U0_a1j.js +1 -0
- package/payload/server/public/assets/{chunk-2KRD3SAO-BBifNfFc.js → chunk-2KRD3SAO-ZcHg_orY.js} +1 -1
- package/payload/server/public/assets/{chunk-336JU56O-B-N_zWuf.js → chunk-336JU56O-BpATJiGl.js} +2 -2
- package/payload/server/public/assets/chunk-426QAEUC-Wz6Bpsil.js +1 -0
- package/payload/server/public/assets/{chunk-4BX2VUAB-CtDQKj9B.js → chunk-4BX2VUAB-zJekz2NU.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-Dnu9n3p1.js → chunk-4TB4RGXK-CLXL19Wd.js} +1 -1
- package/payload/server/public/assets/{chunk-55IACEB6-DSLoJJSj.js → chunk-55IACEB6-CzqB8aoU.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-rx-IvMNE.js → chunk-5FUZZQ4R-BoTfWHuW.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-B9w9AKCS.js → chunk-5PVQY5BW-RhIfPCRB.js} +1 -1
- package/payload/server/public/assets/{chunk-67CJDMHE-C1yEjtiu.js → chunk-67CJDMHE-mM1sFmlz.js} +1 -1
- package/payload/server/public/assets/{chunk-7N4EOEYR-C2f3zeVH.js → chunk-7N4EOEYR-GUck0jv1.js} +1 -1
- package/payload/server/public/assets/{chunk-AA7GKIK3-B_U-NsDK.js → chunk-AA7GKIK3-BYhfUc1V.js} +1 -1
- package/payload/server/public/assets/{chunk-BSJP7CBP-DM6_wafW.js → chunk-BSJP7CBP-CTsYuARh.js} +1 -1
- package/payload/server/public/assets/{chunk-CIAEETIT-DG7WkfNj.js → chunk-CIAEETIT-CGsGmUze.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-aS3_rdwQ.js → chunk-EDXVE4YY-utELKGQK.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-r1I0uoCf.js → chunk-ENJZ2VHE-CNHjq5xK.js} +1 -1
- package/payload/server/public/assets/{chunk-FMBD7UC4-CTm3YRE5.js → chunk-FMBD7UC4-DaRrfk3s.js} +1 -1
- package/payload/server/public/assets/{chunk-FOC6F5B3-CAIttx3K.js → chunk-FOC6F5B3-BaeLcJVt.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-DQFV4c1l.js → chunk-ICPOFSXX-Di63NBur.js} +2 -2
- package/payload/server/public/assets/{chunk-K5T4RW27-B2WCPQBa.js → chunk-K5T4RW27-CTTOezMH.js} +1 -1
- package/payload/server/public/assets/{chunk-KGLVRYIC-C6w2sUOF.js → chunk-KGLVRYIC-DCkohKP2.js} +1 -1
- package/payload/server/public/assets/{chunk-LIHQZDEY-BT1hcDTK.js → chunk-LIHQZDEY-osQO30uB.js} +1 -1
- package/payload/server/public/assets/{chunk-ORNJ4GCN-Brl32BSe.js → chunk-ORNJ4GCN-DoLajOOe.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-BCO6n1CX.js → chunk-OYMX7WX6-BSPzqyxs.js} +1 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-BAQp1OEl.js +1 -0
- package/payload/server/public/assets/{chunk-U2HBQHQK-BvgC0fpb.js → chunk-U2HBQHQK-BZnA7c4T.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-3yLdcqYf.js → chunk-X2U36JSP-DpQ2OA_c.js} +1 -1
- package/payload/server/public/assets/{chunk-XPW4576I-D876RWxK.js → chunk-XPW4576I-BccP1mlQ.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-CiuA4hOC.js → chunk-YZCP3GAM-BAkNXu0G.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-COjEBPzv.js → chunk-ZZ45TVLE-DBSm41oP.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-6EGGLDD_.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DfAV4tgE.js +1 -0
- package/payload/server/public/assets/clone-BoV8noAi.js +1 -0
- package/payload/server/public/assets/{cose-bilkent-S5V4N54A-Dap0yL0o.js → cose-bilkent-S5V4N54A-Boeb8aWs.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-DEia9UJj.js → dagre-KV5264BT-BkvWofSp.js} +1 -1
- package/payload/server/public/assets/{dagre-DBbjK-Cf.js → dagre-nvPNAunb.js} +1 -1
- package/payload/server/public/assets/data-gtcFdXVv.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-CWSP9MzJ.js → diagram-5BDNPKRD-CMEgyt4E.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-DWRsfitL.js → diagram-G4DWMVQ6-ChorrAF0.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-n-jyzS4D.js → diagram-MMDJMWI5-D_iD27po.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-CLPTbfLq.js → diagram-TYMM5635-8qXI1ioG.js} +1 -1
- package/payload/server/public/assets/{dist-Tkw8EOuG.js → dist-CrzV1W3-.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-CmPC9Cnc.js → erDiagram-SMLLAGMA-BFjtKDSB.js} +1 -1
- package/payload/server/public/assets/file-DRa7iPfT.js +1 -0
- package/payload/server/public/assets/{flatten-Db2kUB5j.js → flatten-ya0TqRLc.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-DKMNmUbX.js → flowDiagram-DWJPFMVM-Bpd7IL9l.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-C5-y3w-l.js → ganttDiagram-T4ZO3ILL-CwOozU85.js} +1 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-BOC4CldZ.js +1 -0
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-D9hG5kTg.js → gitGraphDiagram-UUTBAWPF-CcPILiC9.js} +1 -1
- package/payload/server/public/assets/graph-ydtwufZX.js +49 -0
- package/payload/server/public/assets/{graphlib-StP6GUhM.js → graphlib-B_mcXEVr.js} +1 -1
- package/payload/server/public/assets/house-DEu5yOIQ.js +1 -0
- package/payload/server/public/assets/info-OMHHGYJF-BSCPTUIx.js +1 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-T2sn--WJ.js +2 -0
- package/payload/server/public/assets/{isEmpty-CXH_nKTs.js → isEmpty-h-wRi_o9.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A--K4KOS61.js → ishikawaDiagram-UXIWVN3A-DOP9-Q8H.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DE-28YrW.js → journeyDiagram-VCZTEJTY-DGATg0WC.js} +1 -1
- package/payload/server/public/assets/{jsx-runtime-DwoXvzmf.js → jsx-runtime-BrB6Lw5Y.js} +1 -1
- package/payload/server/public/assets/{jsx-runtime-BQmd8XDE.css → jsx-runtime-C8kAju5f.css} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CxSHjau2.js → kanban-definition-6JOO6SKY-C5PigmKg.js} +1 -1
- package/payload/server/public/assets/{line-DbcqYIG0.js → line-DlKKhwkO.js} +1 -1
- package/payload/server/public/assets/{linear-DXHoZSN3.js → linear-DD4JiB1l.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-CNGUA13J.js → mermaid-parser.core-C8xGCa9p.js} +2 -2
- package/payload/server/public/assets/{mermaid.core-IQgx_upQ.js → mermaid.core-CCUSwZB_.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-BT9Up6-C.js → mindmap-definition-QFDTVHPH-75k-IVhC.js} +1 -1
- package/payload/server/public/assets/{ordinal-CN3oz6oW.js → ordinal-Dwxksj1B.js} +1 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-pBa_ZhNI.js +1 -0
- package/payload/server/public/assets/pie-ZZUOXDRM-BzYOyiMb.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-BuewQTi6.js → pieDiagram-DEJITSTG-DN5RsDwZ.js} +1 -1
- package/payload/server/public/assets/public-D0ymcICW.js +5 -0
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-Cu8y2zQL.js → quadrantDiagram-34T5L4WZ-Sd9x6pNe.js} +1 -1
- package/payload/server/public/assets/radar-PYXPWWZC-CTVOaAq6.js +1 -0
- package/payload/server/public/assets/{reduce-SDh8_UdG.js → reduce-BUuWaDl2.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-DiT9bo27.js → requirementDiagram-MS252O5E-BDgifYzj.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-EkRnGTxM.js → sankeyDiagram-XADWPNL6-BX9VULNJ.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BUiQA3SI.js → sequenceDiagram-FGHM5R23-z3vMxhgE.js} +1 -1
- package/payload/server/public/assets/share-2-Cnd_9DYU.js +1 -0
- package/payload/server/public/assets/{src-DQQCRlaQ.js → src-Bo15iQ7w.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-Fij35Tic.js → stateDiagram-FHFEXIEX-DlP0hBxF.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DSddQStC.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BMsyr7wU.js → timeline-definition-GMOUNBTQ-DwQbhKCo.js} +1 -1
- package/payload/server/public/assets/treeView-SZITEDCU-OTnF4Qzw.js +1 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-DlIRmHFb.js +1 -0
- package/payload/server/public/assets/{useVoiceRecorder-C0Fvv_Bt.js → useVoiceRecorder-CFIpzGaB.js} +3 -3
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-6fegYFB3.js → vennDiagram-DHZGUBPP-WTqmZWWa.js} +1 -1
- package/payload/server/public/assets/wardley-RL74JXVD-DwMXAC4U.js +1 -0
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BtJ_B35h.js → wardleyDiagram-NUSXRM2D-BUY50x5T.js} +1 -1
- package/payload/server/public/assets/x-DitohdYO.js +1 -0
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DJ20B4NY.js → xychartDiagram-5P7HB3ND-Btdq-fDj.js} +1 -1
- package/payload/server/public/data.html +7 -5
- package/payload/server/public/graph.html +19 -0
- package/payload/server/public/index.html +10 -7
- package/payload/server/public/public.html +6 -6
- package/payload/server/server.js +965 -664
- package/payload/server/public/assets/admin-ITLuG4BN.js +0 -352
- package/payload/server/public/assets/architecture-YZFGNWBL-C38eyeNF.js +0 -1
- package/payload/server/public/assets/channel-D0dIwjlN.js +0 -1
- package/payload/server/public/assets/chunk-426QAEUC-C8oXXITm.js +0 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-BE_lylks.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DH37CWIF.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DNJ7bv8r.js +0 -1
- package/payload/server/public/assets/clone-rrGuX3ZR.js +0 -1
- package/payload/server/public/assets/data-D8kol1ed.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-CCjgA3FG.js +0 -1
- package/payload/server/public/assets/info-OMHHGYJF-B65K6dQJ.js +0 -1
- package/payload/server/public/assets/infoDiagram-42DDH7IO-DUJfTICr.js +0 -2
- package/payload/server/public/assets/packet-4T2RLAQJ-fp5ishAK.js +0 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-Bc3VMuuU.js +0 -1
- package/payload/server/public/assets/public-CWuf8cLU.js +0 -5
- package/payload/server/public/assets/radar-PYXPWWZC-D9jy5QAa.js +0 -1
- package/payload/server/public/assets/share-2-wGga_ldi.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DQzhSd8K.js +0 -1
- package/payload/server/public/assets/treeView-SZITEDCU-CHyRL9e4.js +0 -1
- package/payload/server/public/assets/treemap-W4RFUUIX-DpQ_FOO6.js +0 -1
- package/payload/server/public/assets/wardley-RL74JXVD-CWBIAatW.js +0 -1
- /package/payload/server/public/assets/{_baseFor-D71p92tl.js → _baseFor-Dn4GSmI6.js} +0 -0
- /package/payload/server/public/assets/{array-Bs_owIvv.js → array-DJN9YAVf.js} +0 -0
- /package/payload/server/public/assets/{chunk-lgnzUk6H.js → chunk-DD-I1_y5.js} +0 -0
- /package/payload/server/public/assets/{cytoscape.esm-DLG5qhup.js → cytoscape.esm-BcJTl1re.js} +0 -0
- /package/payload/server/public/assets/{defaultLocale-Du_2bjyv.js → defaultLocale-B4F_XsBB.js} +0 -0
- /package/payload/server/public/assets/{init-BYLBkHX_.js → init-DX0Y1qU4.js} +0 -0
- /package/payload/server/public/assets/{katex-lkho_UhZ.js → katex-CjHJ1D7d.js} +0 -0
- /package/payload/server/public/assets/{path-BO54iFkf.js → path-7vUsG-o2.js} +0 -0
- /package/payload/server/public/assets/{preload-helper-DWTEM3RW.js → preload-helper-qlgyTAkD.js} +0 -0
- /package/payload/server/public/assets/{rough.esm-BCiZEpQC.js → rough.esm-NLRoWnq-.js} +0 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// Raw CDP scrape of the operator's Cloudflare dashboard to discover the
|
|
2
|
+
// domains attached to the logged-in account. Output on stdout is a JSON
|
|
3
|
+
// `string[]`, sorted and deduped. Every failure mode exits non-zero with
|
|
4
|
+
// a `reason=<enum>` token on stderr tagged [list-cf-domains].
|
|
5
|
+
//
|
|
6
|
+
// Why raw CDP, not playwright:
|
|
7
|
+
// - `/json/new?about:blank` + WebSocket to the returned `webSocketDebuggerUrl`
|
|
8
|
+
// is ~120 LOC including retries and state machine.
|
|
9
|
+
// - Adds zero npm deps (Node 22 ships global `WebSocket`; no `ws`/`playwright`).
|
|
10
|
+
// - CDP is already bound to 127.0.0.1 by vnc.sh; the runtime surface is
|
|
11
|
+
// the same one setup-tunnel.sh uses for browser drive.
|
|
12
|
+
//
|
|
13
|
+
// Why URL-pattern extraction, not CSS selectors:
|
|
14
|
+
// - Cloudflare's SPA uses hashed, version-churning class names — a selector
|
|
15
|
+
// written today drifts within a dashboard redesign window.
|
|
16
|
+
// - The `/<accountId>/<domain>` URL shape is load-bearing for CF's own
|
|
17
|
+
// routing and cannot drift without breaking their entire dashboard; it
|
|
18
|
+
// is the most stable surface to key off.
|
|
19
|
+
//
|
|
20
|
+
// Contract with the caller (list-cf-domains.sh → route → form):
|
|
21
|
+
// stdout on exit 0: JSON `string[]` (empty array means signed-in-but-empty)
|
|
22
|
+
// stderr on any path: phase_line-formatted lines, `[list-cf-domains] …`
|
|
23
|
+
// exit 1: `reason=<enum>` on stderr; scrape-empty-but-no-error is exit 0
|
|
24
|
+
// with body dump (the enum exists so the caller can distinguish
|
|
25
|
+
// "selector drift" from "empty account" — both shapes arrive via
|
|
26
|
+
// stdout).
|
|
27
|
+
|
|
28
|
+
import { writeFile } from "node:fs/promises";
|
|
29
|
+
import { resolve } from "node:path";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
|
|
32
|
+
const CDP_HOST = "127.0.0.1";
|
|
33
|
+
const CDP_PORT = 9222;
|
|
34
|
+
|
|
35
|
+
// Phase budgets total 30s (enforced by the shell wrapper's `timeout`).
|
|
36
|
+
// Individual steps get sub-budgets so an early stall does not eat the
|
|
37
|
+
// whole envelope silently.
|
|
38
|
+
const CONNECT_TIMEOUT_MS = 2_000;
|
|
39
|
+
const NAVIGATE_TIMEOUT_MS = 15_000;
|
|
40
|
+
const SIGNED_IN_POLL_MS = 10_000;
|
|
41
|
+
const SCRAPE_POLL_MS = 10_000;
|
|
42
|
+
const POLL_INTERVAL_MS = 500;
|
|
43
|
+
|
|
44
|
+
type Reason =
|
|
45
|
+
| "cdp-unreachable"
|
|
46
|
+
| "target-create-failed"
|
|
47
|
+
| "ws-connect-failed"
|
|
48
|
+
| "not-signed-in"
|
|
49
|
+
| "navigate-failed"
|
|
50
|
+
| "table-wait-timeout"
|
|
51
|
+
| "runtime-error";
|
|
52
|
+
|
|
53
|
+
function logPhase(line: string): void {
|
|
54
|
+
// Written to stderr; shell wrapper tees to STREAM_LOG_PATH with timestamp.
|
|
55
|
+
// Keeping format parity with `_stream-log.sh phase_line` on the bash side
|
|
56
|
+
// means the tailer regex and grepping by `[list-cf-domains]` work uniformly.
|
|
57
|
+
process.stderr.write(`[list-cf-domains] ${line}\n`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function die(reason: Reason, detail = ""): never {
|
|
61
|
+
logPhase(`phase=error reason=${reason}${detail ? ` detail="${detail.replace(/"/g, "'").slice(0, 300)}"` : ""}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function cdpVersion(): Promise<void> {
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), CONNECT_TIMEOUT_MS);
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(`http://${CDP_HOST}:${CDP_PORT}/json/version`, { signal: controller.signal });
|
|
70
|
+
if (!res.ok) die("cdp-unreachable", `HTTP ${res.status}`);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
die("cdp-unreachable", err instanceof Error ? err.message : String(err));
|
|
73
|
+
} finally {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface CdpTarget {
|
|
79
|
+
id: string;
|
|
80
|
+
webSocketDebuggerUrl: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function createTarget(): Promise<CdpTarget> {
|
|
84
|
+
// `PUT /json/new?<url>` returns JSON describing the target. The URL goes
|
|
85
|
+
// as a query-string argument (not a ?url= key), matching Chromium's CDP
|
|
86
|
+
// server. about:blank avoids a wasted navigation — the real navigate
|
|
87
|
+
// happens over WS after we attach.
|
|
88
|
+
const endpoint = `http://${CDP_HOST}:${CDP_PORT}/json/new?about:blank`;
|
|
89
|
+
let res: Response;
|
|
90
|
+
try {
|
|
91
|
+
res = await fetch(endpoint, { method: "PUT", signal: AbortSignal.timeout(CONNECT_TIMEOUT_MS) });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
die("target-create-failed", err instanceof Error ? err.message : String(err));
|
|
94
|
+
}
|
|
95
|
+
if (!res.ok) die("target-create-failed", `HTTP ${res.status}`);
|
|
96
|
+
const target = (await res.json()) as Partial<CdpTarget>;
|
|
97
|
+
if (!target.id || !target.webSocketDebuggerUrl) {
|
|
98
|
+
die("target-create-failed", "response missing id or webSocketDebuggerUrl");
|
|
99
|
+
}
|
|
100
|
+
return target as CdpTarget;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function closeTarget(id: string): Promise<void> {
|
|
104
|
+
try {
|
|
105
|
+
await fetch(`http://${CDP_HOST}:${CDP_PORT}/json/close/${id}`, {
|
|
106
|
+
signal: AbortSignal.timeout(CONNECT_TIMEOUT_MS),
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
// Best-effort on cleanup — a leaked target is recovered by the next vnc.sh
|
|
110
|
+
// restart; surfacing it as a failure would mask the real scrape error.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Minimal CDP WebSocket client. We only need three commands: Page.enable,
|
|
115
|
+
// Runtime.enable, Page.navigate, Runtime.evaluate. A full client would
|
|
116
|
+
// manage sessions / targets / events — we just need request/response with
|
|
117
|
+
// per-id correlation and an optional event stream for Page.frameNavigated
|
|
118
|
+
// (which we do not actually need because Runtime.evaluate on location.href
|
|
119
|
+
// is the authoritative signal).
|
|
120
|
+
class CdpClient {
|
|
121
|
+
private ws: WebSocket;
|
|
122
|
+
private nextId = 1;
|
|
123
|
+
private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
124
|
+
|
|
125
|
+
private constructor(ws: WebSocket) {
|
|
126
|
+
this.ws = ws;
|
|
127
|
+
this.ws.addEventListener("message", (ev) => this.onMessage(ev.data as string));
|
|
128
|
+
this.ws.addEventListener("error", () => {
|
|
129
|
+
for (const { reject } of this.pending.values()) reject(new Error("WebSocket error"));
|
|
130
|
+
this.pending.clear();
|
|
131
|
+
});
|
|
132
|
+
this.ws.addEventListener("close", () => {
|
|
133
|
+
for (const { reject } of this.pending.values()) reject(new Error("WebSocket closed"));
|
|
134
|
+
this.pending.clear();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static async connect(url: string): Promise<CdpClient> {
|
|
139
|
+
const ws = new WebSocket(url);
|
|
140
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
141
|
+
const timer = setTimeout(() => {
|
|
142
|
+
ws.close();
|
|
143
|
+
rejectPromise(new Error(`WebSocket connect timeout (${CONNECT_TIMEOUT_MS}ms)`));
|
|
144
|
+
}, CONNECT_TIMEOUT_MS);
|
|
145
|
+
ws.addEventListener("open", () => {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
resolvePromise(new CdpClient(ws));
|
|
148
|
+
}, { once: true });
|
|
149
|
+
ws.addEventListener("error", (ev) => {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
rejectPromise(new Error(`WebSocket error: ${(ev as ErrorEvent).message ?? "unknown"}`));
|
|
152
|
+
}, { once: true });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private onMessage(raw: string): void {
|
|
157
|
+
let msg: { id?: number; result?: unknown; error?: { message?: string } };
|
|
158
|
+
try {
|
|
159
|
+
msg = JSON.parse(raw);
|
|
160
|
+
} catch {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (typeof msg.id !== "number") return;
|
|
164
|
+
const waiter = this.pending.get(msg.id);
|
|
165
|
+
if (!waiter) return;
|
|
166
|
+
this.pending.delete(msg.id);
|
|
167
|
+
if (msg.error) {
|
|
168
|
+
waiter.reject(new Error(msg.error.message ?? "CDP error"));
|
|
169
|
+
} else {
|
|
170
|
+
waiter.resolve(msg.result);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
send<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
|
|
175
|
+
const id = this.nextId++;
|
|
176
|
+
return new Promise<T>((resolvePromise, rejectPromise) => {
|
|
177
|
+
this.pending.set(id, {
|
|
178
|
+
resolve: resolvePromise as (v: unknown) => void,
|
|
179
|
+
reject: rejectPromise,
|
|
180
|
+
});
|
|
181
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
close(): void {
|
|
186
|
+
try { this.ws.close(); } catch { /* ws may already be closing */ }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface EvaluateResult {
|
|
191
|
+
result: { type: string; value?: unknown; description?: string };
|
|
192
|
+
exceptionDetails?: { text?: string; exception?: { description?: string } };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function evaluate<T = unknown>(cdp: CdpClient, expression: string): Promise<T> {
|
|
196
|
+
const res = await cdp.send<EvaluateResult>("Runtime.evaluate", {
|
|
197
|
+
expression,
|
|
198
|
+
returnByValue: true,
|
|
199
|
+
awaitPromise: false,
|
|
200
|
+
});
|
|
201
|
+
if (res.exceptionDetails) {
|
|
202
|
+
throw new Error(res.exceptionDetails.exception?.description ?? res.exceptionDetails.text ?? "evaluate threw");
|
|
203
|
+
}
|
|
204
|
+
return res.result.value as T;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Detect whether the dashboard has redirected to a signed-in account path
|
|
208
|
+
// (URL like `/<32-hex-accountId>/…`) or the unsigned-in `/login` flow.
|
|
209
|
+
// Returns the accountId if signed in, null if still loading, false if
|
|
210
|
+
// definitively signed-out.
|
|
211
|
+
const ACCOUNT_ID_REGEX = /^\/([a-f0-9]{32})(?:\/|$)/;
|
|
212
|
+
|
|
213
|
+
async function readAccountId(cdp: CdpClient): Promise<string | null | false> {
|
|
214
|
+
const pathname = await evaluate<string>(cdp, "location.pathname");
|
|
215
|
+
if (typeof pathname !== "string") return null;
|
|
216
|
+
if (pathname.startsWith("/login") || pathname.startsWith("/sign-in")) return false;
|
|
217
|
+
const match = pathname.match(ACCOUNT_ID_REGEX);
|
|
218
|
+
return match ? match[1] : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function waitForSignedIn(cdp: CdpClient): Promise<string> {
|
|
222
|
+
const deadline = Date.now() + SIGNED_IN_POLL_MS;
|
|
223
|
+
while (Date.now() < deadline) {
|
|
224
|
+
const state = await readAccountId(cdp);
|
|
225
|
+
if (state === false) die("not-signed-in", "dashboard redirected to login");
|
|
226
|
+
if (typeof state === "string") {
|
|
227
|
+
logPhase(`phase=dashboard-nav-complete accountId=${state.slice(0, 8)}…`);
|
|
228
|
+
return state;
|
|
229
|
+
}
|
|
230
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
231
|
+
}
|
|
232
|
+
// Timed out without seeing either pattern — treat as not-signed-in to
|
|
233
|
+
// prompt `tunnel-login`; a genuinely-slow dashboard that later resolves
|
|
234
|
+
// would be a false-negative we accept in exchange for deterministic UX.
|
|
235
|
+
die("not-signed-in", "no /<accountId>/… path reached within budget");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Extract every domain mentioned on the Domains Overview page via URL-
|
|
239
|
+
// pattern match, then merge with any FQDN-shaped text found in table cells.
|
|
240
|
+
// Two sources: (a) `/<accountId>/<hostname>` URL hrefs on the page; (b) any
|
|
241
|
+
// text node on the page matching a valid FQDN pattern — the overview page
|
|
242
|
+
// renders the zone name as both a link and a table cell, so either source
|
|
243
|
+
// catches it. The merge is conservative: we require the string look like a
|
|
244
|
+
// domain (2+ labels, last label 2-63 alpha) AND not be a cloudflare-owned
|
|
245
|
+
// marketing host.
|
|
246
|
+
const SCRAPE_EXPRESSION = `(function() {
|
|
247
|
+
const accountIdMatch = location.pathname.match(/^\\/([a-f0-9]{32})/);
|
|
248
|
+
if (!accountIdMatch) return { reason: 'no-account-id', domains: [] };
|
|
249
|
+
const accountId = accountIdMatch[1];
|
|
250
|
+
|
|
251
|
+
const out = new Set();
|
|
252
|
+
const pushIfDomain = (s) => {
|
|
253
|
+
if (typeof s !== 'string') return;
|
|
254
|
+
const t = s.trim().toLowerCase();
|
|
255
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/.test(t)) return;
|
|
256
|
+
if (t.endsWith('.cloudflare.com') || t === 'cloudflare.com') return;
|
|
257
|
+
out.add(t);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Source A: /<accountId>/<host> hrefs — the canonical CF routing pattern
|
|
261
|
+
// that the overview page uses to link each zone to its management screens.
|
|
262
|
+
const hrefPrefix = '/' + accountId + '/';
|
|
263
|
+
for (const a of document.querySelectorAll('a[href]')) {
|
|
264
|
+
const href = a.getAttribute('href') || '';
|
|
265
|
+
if (!href.startsWith(hrefPrefix)) continue;
|
|
266
|
+
const tail = href.slice(hrefPrefix.length).split('?')[0].split('#')[0];
|
|
267
|
+
const firstSegment = tail.split('/')[0];
|
|
268
|
+
pushIfDomain(firstSegment);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Source B: explicit FQDN-shaped text in the main content region. Guards
|
|
272
|
+
// against a hypothetical CF redesign that drops the link tree — the zone
|
|
273
|
+
// name still appears as rendered text in the table row.
|
|
274
|
+
const main = document.querySelector('main') || document.body;
|
|
275
|
+
if (main) {
|
|
276
|
+
const treeWalker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT);
|
|
277
|
+
let node;
|
|
278
|
+
while ((node = treeWalker.nextNode())) {
|
|
279
|
+
const text = (node.nodeValue || '').trim();
|
|
280
|
+
if (text.length < 4 || text.length > 253) continue;
|
|
281
|
+
pushIfDomain(text);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { reason: 'ok', domains: Array.from(out).sort() };
|
|
286
|
+
})()`;
|
|
287
|
+
|
|
288
|
+
interface ScrapeOutcome {
|
|
289
|
+
reason: "ok" | "no-account-id";
|
|
290
|
+
domains: string[];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function scrapeDomains(cdp: CdpClient): Promise<string[]> {
|
|
294
|
+
const deadline = Date.now() + SCRAPE_POLL_MS;
|
|
295
|
+
let lastOutcome: ScrapeOutcome | null = null;
|
|
296
|
+
while (Date.now() < deadline) {
|
|
297
|
+
try {
|
|
298
|
+
const outcome = await evaluate<ScrapeOutcome>(cdp, SCRAPE_EXPRESSION);
|
|
299
|
+
lastOutcome = outcome;
|
|
300
|
+
if (outcome.reason === "ok" && outcome.domains.length > 0) {
|
|
301
|
+
logPhase(`phase=dom-scrape-complete result=ok count=${outcome.domains.length}`);
|
|
302
|
+
return outcome.domains;
|
|
303
|
+
}
|
|
304
|
+
// `ok` with zero domains is ambiguous during SPA hydration — keep
|
|
305
|
+
// polling in case the table renders after a data fetch. The final
|
|
306
|
+
// iteration's zero result is the empty-account signal.
|
|
307
|
+
} catch (err) {
|
|
308
|
+
logPhase(`phase=scrape-retry err="${(err instanceof Error ? err.message : String(err)).slice(0, 120)}"`);
|
|
309
|
+
}
|
|
310
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
311
|
+
}
|
|
312
|
+
// Poll exhausted. Distinguish genuine empty account from selector drift by
|
|
313
|
+
// dumping the body HTML (bounded) for manual inspection, then reporting
|
|
314
|
+
// empty with the diagnostic enum. The script still exits 0 — the caller
|
|
315
|
+
// treats empty as a valid signal and shows the "add a domain" empty state.
|
|
316
|
+
try {
|
|
317
|
+
const html = await evaluate<string>(cdp, "document.documentElement.outerHTML.slice(0, 100000)");
|
|
318
|
+
// CONFIG_DIR is set by list-cf-domains.sh before the spawn. A silent
|
|
319
|
+
// fallback would dump logs into the wrong brand's directory on a Real
|
|
320
|
+
// Agent install — a silent-miswrite masking the wrapper-side break it
|
|
321
|
+
// came from. Per Task 473 doctrine: loud-fail on absent runtime-derived
|
|
322
|
+
// values.
|
|
323
|
+
const configDir = process.env.CONFIG_DIR;
|
|
324
|
+
if (!configDir) {
|
|
325
|
+
throw new Error("CONFIG_DIR env var not set by wrapper — refusing to guess brand log directory");
|
|
326
|
+
}
|
|
327
|
+
const logDir = resolve(homedir(), configDir, "logs");
|
|
328
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
329
|
+
const dumpPath = resolve(logDir, `list-cf-domains-${ts}.html`);
|
|
330
|
+
await writeFile(dumpPath, typeof html === "string" ? html : String(html), "utf-8");
|
|
331
|
+
logPhase(`phase=dom-scrape-complete result=empty-or-drift dump=${dumpPath} lastReason=${lastOutcome?.reason ?? "unknown"}`);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
logPhase(`phase=dom-scrape-complete result=empty-or-drift dump=failed lastReason=${lastOutcome?.reason ?? "unknown"} err="${(err instanceof Error ? err.message : String(err)).slice(0, 120)}"`);
|
|
334
|
+
}
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function navigate(cdp: CdpClient, url: string): Promise<void> {
|
|
339
|
+
const result = await cdp.send<{ errorText?: string }>("Page.navigate", { url });
|
|
340
|
+
if (result?.errorText) die("navigate-failed", `Page.navigate errorText="${result.errorText}"`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function waitForDocumentReady(cdp: CdpClient): Promise<void> {
|
|
344
|
+
const deadline = Date.now() + NAVIGATE_TIMEOUT_MS;
|
|
345
|
+
while (Date.now() < deadline) {
|
|
346
|
+
const state = await evaluate<string>(cdp, "document.readyState");
|
|
347
|
+
if (state === "complete" || state === "interactive") return;
|
|
348
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
349
|
+
}
|
|
350
|
+
die("navigate-failed", "document.readyState did not reach complete/interactive");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function main(): Promise<void> {
|
|
354
|
+
logPhase(`phase=script-start cdp=${CDP_HOST}:${CDP_PORT}`);
|
|
355
|
+
|
|
356
|
+
await cdpVersion();
|
|
357
|
+
logPhase(`phase=cdp-connect result=ok`);
|
|
358
|
+
|
|
359
|
+
const target = await createTarget();
|
|
360
|
+
logPhase(`phase=target-created targetId=${target.id.slice(0, 8)}…`);
|
|
361
|
+
|
|
362
|
+
let cdp: CdpClient;
|
|
363
|
+
try {
|
|
364
|
+
cdp = await CdpClient.connect(target.webSocketDebuggerUrl);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
await closeTarget(target.id);
|
|
367
|
+
die("ws-connect-failed", err instanceof Error ? err.message : String(err));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
await cdp.send("Page.enable");
|
|
372
|
+
await cdp.send("Runtime.enable");
|
|
373
|
+
|
|
374
|
+
logPhase(`phase=dashboard-nav-start url=https://dash.cloudflare.com/`);
|
|
375
|
+
await navigate(cdp, "https://dash.cloudflare.com/");
|
|
376
|
+
await waitForDocumentReady(cdp);
|
|
377
|
+
|
|
378
|
+
const accountId = await waitForSignedIn(cdp);
|
|
379
|
+
|
|
380
|
+
const overviewUrl = `https://dash.cloudflare.com/${accountId}/domains/overview`;
|
|
381
|
+
logPhase(`phase=table-wait url=${overviewUrl}`);
|
|
382
|
+
await navigate(cdp, overviewUrl);
|
|
383
|
+
await waitForDocumentReady(cdp);
|
|
384
|
+
|
|
385
|
+
const domains = await scrapeDomains(cdp);
|
|
386
|
+
|
|
387
|
+
logPhase(`phase=target-closed`);
|
|
388
|
+
process.stdout.write(JSON.stringify(domains) + "\n");
|
|
389
|
+
logPhase(`phase=script-exit code=0 count=${domains.length}`);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
// Only runtime errors that escaped the typed-reason paths land here.
|
|
392
|
+
die("runtime-error", err instanceof Error ? err.message : String(err));
|
|
393
|
+
} finally {
|
|
394
|
+
cdp.close();
|
|
395
|
+
await closeTarget(target.id);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
main().catch((err: unknown) => {
|
|
400
|
+
die("runtime-error", err instanceof Error ? err.message : String(err));
|
|
401
|
+
});
|
|
@@ -168,11 +168,73 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
168
168
|
echo "ERROR: CDP PUT /json/new?<url> returned empty — Chromium rejected the navigate." >&2
|
|
169
169
|
exit 1
|
|
170
170
|
fi
|
|
171
|
-
|
|
171
|
+
# Extract the CDP target id so _cdp-authorize.mjs can open a debugger
|
|
172
|
+
# WebSocket against the correct tab. The PUT response is a JSON object
|
|
173
|
+
# describing the newly-created target; `.id` is the only stable field
|
|
174
|
+
# across Chromium versions that works for `/json/list` lookup.
|
|
175
|
+
CDP_TARGET_ID="$(printf '%s' "${CDP_RESP}" | jq -r '.id // empty' 2>/dev/null || true)"
|
|
176
|
+
if [ -z "${CDP_TARGET_ID}" ]; then
|
|
177
|
+
kill "${CF_PIPELINE_PID}" 2>/dev/null || true
|
|
178
|
+
phase_line setup-tunnel step=browser-drive result=error reason=cdp-target-id-missing \
|
|
179
|
+
resp_head="$(printf '%s' "${CDP_RESP}" | head -c 200)"
|
|
180
|
+
echo "ERROR: CDP PUT /json/new?<url> returned a body without an .id field — Chromium protocol drift?" >&2
|
|
181
|
+
exit 1
|
|
182
|
+
fi
|
|
183
|
+
phase_line setup-tunnel step=browser-drive result=accepted target_id="${CDP_TARGET_ID}"
|
|
184
|
+
|
|
185
|
+
# Task 588: drive the Authorize click via CDP WebSocket instead of waiting
|
|
186
|
+
# for a human. The helper shares a directory with this script; same
|
|
187
|
+
# readlink-resolve pattern as _stream-log.sh so the symlink install path
|
|
188
|
+
# (packages/create-maxy installs ~/setup-tunnel.sh as a symlink) doesn't
|
|
189
|
+
# point dirname at $HOME.
|
|
190
|
+
AUTH_HELPER="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_cdp-authorize.mjs"
|
|
191
|
+
if [ ! -x "${AUTH_HELPER}" ]; then
|
|
192
|
+
kill "${CF_PIPELINE_PID}" 2>/dev/null || true
|
|
193
|
+
phase_line setup-tunnel step=browser-drive result=error reason=cdp-helper-missing \
|
|
194
|
+
path="${AUTH_HELPER}"
|
|
195
|
+
echo "ERROR: _cdp-authorize.mjs helper missing or not executable at ${AUTH_HELPER}" >&2
|
|
196
|
+
exit 1
|
|
197
|
+
fi
|
|
198
|
+
# tee_subprocess pipes the helper's cdp-authorize result= line into the
|
|
199
|
+
# stream log under the setup-tunnel:cdp-click tag so the chat UI renders
|
|
200
|
+
# the click outcome in real time. Exit code drives control flow.
|
|
201
|
+
if tee_subprocess setup-tunnel:cdp-click -- \
|
|
202
|
+
node "${AUTH_HELPER}" "${CDP_TARGET_ID}"; then
|
|
203
|
+
phase_line setup-tunnel step=oauth-login result=authorize-clicked \
|
|
204
|
+
target_id="${CDP_TARGET_ID}"
|
|
205
|
+
else
|
|
206
|
+
CLICK_RC=$?
|
|
207
|
+
kill "${CF_PIPELINE_PID}" 2>/dev/null || true
|
|
208
|
+
# Helper exit codes: 1=authorize-button-not-found (loud, expected failure
|
|
209
|
+
# mode when the VNC browser isn't signed into Cloudflare), 2=cdp-ws-unreachable,
|
|
210
|
+
# 3=target-not-found, 4=click-evaluate-threw, 5=protocol-error. All are
|
|
211
|
+
# final — no retry, no silent fallback. Map the exit code 1:1 to a reason
|
|
212
|
+
# string so the stream-log `reason=<…>` is greppable by exact cause; a
|
|
213
|
+
# single catch-all reason would defeat the observability contract.
|
|
214
|
+
case "${CLICK_RC}" in
|
|
215
|
+
1) CLICK_REASON=authorize-button-not-found ;;
|
|
216
|
+
2) CLICK_REASON=cdp-ws-unreachable ;;
|
|
217
|
+
3) CLICK_REASON=target-not-found ;;
|
|
218
|
+
4) CLICK_REASON=click-evaluate-threw ;;
|
|
219
|
+
5) CLICK_REASON=protocol-error ;;
|
|
220
|
+
*) CLICK_REASON="unknown-exit-${CLICK_RC}" ;;
|
|
221
|
+
esac
|
|
222
|
+
phase_line setup-tunnel step=browser-drive result=error \
|
|
223
|
+
reason="${CLICK_REASON}" click_rc="${CLICK_RC}"
|
|
224
|
+
echo "ERROR: CDP-driven Authorize click failed (helper exit=${CLICK_RC}, reason=${CLICK_REASON})." >&2
|
|
225
|
+
echo " If the VNC browser is not signed into Cloudflare, sign in manually," >&2
|
|
226
|
+
echo " close the sign-in tab, and re-run setup — or run ~/reset-tunnel.sh first." >&2
|
|
227
|
+
exit 1
|
|
228
|
+
fi
|
|
172
229
|
|
|
173
230
|
# Wait for cert.pem to land — cloudflared writes to ~/.cloudflared/cert.pem
|
|
174
|
-
# regardless of --origincert, so watch the canonical location.
|
|
175
|
-
|
|
231
|
+
# regardless of --origincert, so watch the canonical location. Task 588
|
|
232
|
+
# collapses the pre-click 180 s human-latency window to a 20 s deterministic
|
|
233
|
+
# round-trip window (Cloudflare → cloudflared webhook after consent). The
|
|
234
|
+
# 2-second heartbeat inside the loop is the observability contract for
|
|
235
|
+
# this bounded wait — no form-spawned script is allowed a silent poll of
|
|
236
|
+
# more than ~2 s per criterion 3 of Task 588.
|
|
237
|
+
LOGIN_TIMEOUT="${SETUP_TUNNEL_LOGIN_TIMEOUT:-20}"
|
|
176
238
|
LOGIN_WAIT=0
|
|
177
239
|
while [ ! -f "${HOME}/.cloudflared/cert.pem" ]; do
|
|
178
240
|
if ! kill -0 "${CF_PIPELINE_PID}" 2>/dev/null; then
|
|
@@ -192,6 +254,14 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
192
254
|
echo "ERROR: Timed out after ${LOGIN_WAIT}s waiting for cert.pem to land." >&2
|
|
193
255
|
exit 1
|
|
194
256
|
fi
|
|
257
|
+
# Heartbeat every 2 s after the click. t=0 is the authorize-clicked
|
|
258
|
+
# phase line above; the first heartbeat fires at t=2. Without this line
|
|
259
|
+
# the tailer sees silence for the full 1-20 s round-trip — the exact
|
|
260
|
+
# state Task 588 forbids.
|
|
261
|
+
if [ "${LOGIN_WAIT}" -gt 0 ] && [ $((LOGIN_WAIT % 2)) -eq 0 ]; then
|
|
262
|
+
phase_line setup-tunnel step=oauth-login result=awaiting-cert \
|
|
263
|
+
elapsed="${LOGIN_WAIT}s" timeout="${LOGIN_TIMEOUT}s"
|
|
264
|
+
fi
|
|
195
265
|
sleep 1
|
|
196
266
|
LOGIN_WAIT=$((LOGIN_WAIT + 1))
|
|
197
267
|
done
|
|
@@ -84,7 +84,9 @@ Example:
|
|
|
84
84
|
|
|
85
85
|
## 4. Dashboard guidance — `references/dashboard-guide.md`
|
|
86
86
|
|
|
87
|
-
Use this when the operator needs to do something only the Cloudflare dashboard can do: sign in, switch accounts, add a site, edit an apex CNAME, verify zone nameservers, delete a tunnel after stopping its replicas. The guide has one numbered click-path per operation. Quote the relevant click-path verbatim — the operator follows it in the browser
|
|
87
|
+
Use this when the operator needs to do something only the Cloudflare dashboard can do: sign in, switch accounts, add a site, edit an apex CNAME, verify zone nameservers, delete a tunnel after stopping its replicas. The guide has one numbered click-path per operation. Quote the relevant click-path verbatim — the operator follows it in the browser. The agent does not drive dashboard mutations via Playwright or Chrome DevTools.
|
|
88
|
+
|
|
89
|
+
The single exception is `list-cf-domains.sh` (Task 589), which reads the domains attached to the logged-in account to populate the `cloudflare-setup-form` dropdowns. That script is deterministic (bash + raw CDP, no LLM in the decision path), invoked only by the `/api/admin/cloudflare/domains` route, and produces only a JSON `string[]` on stdout; no dashboard state is changed. Any dashboard scrape that is not this exact script is forbidden — the agent does not extend this carve-out to new scripts it writes, hypothesises, or finds. Adding a new sanctioned scrape surface requires a code change reviewed as a doctrine change, not an inline agent decision.
|
|
88
90
|
|
|
89
91
|
---
|
|
90
92
|
|
|
@@ -96,6 +98,6 @@ When the operator's request touches Cloudflare, the agent's permitted actions ar
|
|
|
96
98
|
- Quote `references/manual-setup.md`, `references/reset-guide.md`, or `references/dashboard-guide.md`.
|
|
97
99
|
- Verify reachability via plain HTTP (`curl -I https://<hostname>`).
|
|
98
100
|
|
|
99
|
-
The agent does not drive Cloudflare
|
|
101
|
+
The agent does not drive Cloudflare dashboard mutations via Playwright or Chrome DevTools. The single sanctioned read-only scrape is `list-cf-domains.sh`, invoked only by the `/api/admin/cloudflare/domains` route — the LLM is not in its decision path. No other dashboard-automation surface is permitted; the agent does not generalise this exception to new scripts. The agent does not synthesise `cloudflared` flag combinations from web search or prior training. The agent does not call Cloudflare API or SDK from any language. The agent does not write or mutate `cert.pem`, `tunnel.state`, `config.yml`, or `alias-domains.json` directly — `setup-tunnel.sh` and the operator manage those files.
|
|
100
102
|
|
|
101
103
|
When a sanctioned surface fails, the agent reports the failure with the exact output, cites the recovery step from `references/reset-guide.md`, and stops. Improvisation — "let me try a different flag" or "let me check the dashboard myself" — is the behaviour this rule exists to prevent. See IDENTITY.md § Cloudflare operations for the unconditional form.
|
|
@@ -8,6 +8,7 @@ Each installation has its own Cloudflare account. Sign-in is OAuth in the device
|
|
|
8
8
|
|------|--------|
|
|
9
9
|
| **Product identity** (Maxy vs Real Agent) | `brand.json` (`productName`, `configDir`) — known at install. |
|
|
10
10
|
| **Cloudflare account identity** | `cert.pem` from OAuth. One account per brand per device. |
|
|
11
|
+
| **Domain scope** (which zones the operator can route) | Live Cloudflare dashboard at form-render time via `list-cf-domains.sh`, not `brand.json`. Brand identity has no authority over which domains the operator's CF account holds. |
|
|
11
12
|
| **Local tunnel state** | `~/{configDir}/cloudflared/` — `cert.pem`, `<UUID>.json`, `config.yml`, `tunnel.state`, `alias-domains.json`. |
|
|
12
13
|
|
|
13
14
|
There is no token-based auth for the operator-owned path (Mode A). To switch Cloudflare accounts, run `reset-tunnel.sh` (which deletes the cert and every tunnel on the current account), then run `setup-tunnel.sh` again — `cloudflared tunnel login` inside the setup script will pick a fresh account when you sign in.
|
|
@@ -66,6 +66,15 @@ The logs will show which service failed to start and why. Common causes:
|
|
|
66
66
|
- **Port 19200 already in use** — check for another process: `lsof -i :19200`
|
|
67
67
|
- **Claude OAuth expired** — the next admin session will prompt you to re-authenticate
|
|
68
68
|
|
|
69
|
+
## Systemd units on each device
|
|
70
|
+
|
|
71
|
+
Each Maxy device runs two independent `--user` systemd units:
|
|
72
|
+
|
|
73
|
+
- `maxy-ui.service` — the admin + public HTTP server (default port 19200). Restarted by the upgrade flow; short downtime is expected during steps 8→12 of an upgrade.
|
|
74
|
+
- `maxy-ttyd.service` — a persistent PTY over WebSocket on `127.0.0.1:7681` attached to a tmux session named `maxy-pty`. Independent of `maxy-ui` — restarting the UI does not interrupt shell work, which is exactly the property the upgrade flow relies on to survive its own admin-server restart.
|
|
75
|
+
|
|
76
|
+
If the Software Update window's terminal goes blank and does not reconnect, `sudo systemctl --user status maxy-ttyd` is the first thing to check. Restarting the unit (`sudo systemctl --user restart maxy-ttyd`) is safe: the tmux session survives because `tmux new-session -A -s maxy-pty` is idempotent.
|
|
77
|
+
|
|
69
78
|
## Upgrading
|
|
70
79
|
|
|
71
80
|
To upgrade Maxy to the latest version, ask Maxy: "Upgrade Maxy." The platform checks the current device identity (hostname and port via `system-status`), then re-runs the installer with explicit `--hostname` and `--port` flags to preserve them across the upgrade.
|
|
@@ -76,4 +85,6 @@ The docs plugin (this plugin) is upgraded in the same step — you always have t
|
|
|
76
85
|
|
|
77
86
|
Maxy checks for new releases on every admin session start — whenever you log in, reload the page, or return to the admin chat. When a newer version is available, the Software Update window opens automatically showing your current and the latest version, with a one-click Upgrade button. Dismissing the window (click outside or the close button) defers the alert until your next login or reload; no alert is shown when you are already on the latest version.
|
|
78
87
|
|
|
88
|
+
The upgrade runs inside a live terminal embedded in the Software Update window — you see each installation step stream as it happens, and any password prompts from `sudo` appear directly in the terminal for you to answer. Closing the window does not cancel the upgrade; re-opening it reattaches to the same shell so you can see what happened while disconnected.
|
|
89
|
+
|
|
79
90
|
The header menu's version indicator still reflects real-time status: a green dot means you are up to date, and an accent-coloured dot means an upgrade is available. Opening the menu refreshes the version check, so a long-lived session can still surface an upgrade that became available after login without reloading the page.
|
|
@@ -40,6 +40,8 @@ When you first open the admin interface:
|
|
|
40
40
|
|
|
41
41
|
This setup is resumable — if you close the browser mid-setup, Maxy picks up where you left off next time.
|
|
42
42
|
|
|
43
|
+
After install, a live admin terminal is available inside the Software Update window — your Pi's shell, accessible through the admin UI, for upgrades and any other shell work without needing to SSH.
|
|
44
|
+
|
|
43
45
|
## How to Use Maxy
|
|
44
46
|
|
|
45
47
|
Conversation is the only interface. Type or speak what you need:
|
|
@@ -84,9 +84,9 @@ Ask naturally:
|
|
|
84
84
|
|
|
85
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
86
|
|
|
87
|
-
You can also open
|
|
87
|
+
You can also open a visual view of your graph at any time from the burger menu → **Graph**. It renders up to 200 of your most recently updated nodes as a force-directed map, coloured by label (Person, Service, KnowledgeDocument, Task, …). Click a node to see its properties; type in the search box to highlight matches.
|
|
88
88
|
|
|
89
|
-
The
|
|
89
|
+
The page reads only your own brand's Neo4j — a Maxy device and a Real Agent device share no graph state even when on the same laptop. No credentials are required; the view inherits your admin session.
|
|
90
90
|
|
|
91
91
|
## Privacy
|
|
92
92
|
|
|
@@ -62,6 +62,12 @@ There is no dashboard, no settings panel, no menus. Everything is done through c
|
|
|
62
62
|
|
|
63
63
|
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.
|
|
64
64
|
|
|
65
|
+
## Admin Terminal
|
|
66
|
+
|
|
67
|
+
The admin UI includes a live terminal surface that opens a real shell on your Pi in the browser, reached via the Software Update modal. Under the hood it's a WebSocket (`/admin/terminal/ws`) attached through `@xterm/xterm` to `ttyd` on `127.0.0.1:7681`, backed by a persistent tmux session named `maxy-pty`.
|
|
68
|
+
|
|
69
|
+
The tmux session outlives admin-server restarts — running an upgrade inside this terminal means you see the live shell output continuously, even through the admin server's own restart mid-upgrade. Closing the browser tab does not kill the running work; re-opening the Software Update window reattaches to the same session and scrollback shows everything that happened in the meantime. Password-protected `sudo` prompts appear natively inside the terminal, and the password you type never leaves the Pi — the admin-server proxy is a raw byte pipe that never inspects frame payloads.
|
|
70
|
+
|
|
65
71
|
## AI Content Provenance
|
|
66
72
|
|
|
67
73
|
When your public agent sends a message to someone — via email, WhatsApp, Telegram, or SMS — the platform automatically includes a brief disclosure that the content was generated by AI. This is transparent and cannot be turned off.
|
|
@@ -90,3 +90,19 @@ If the tunnel won't reconnect, re-run the Cloudflare setup: ask Maxy "Reconnect
|
|
|
90
90
|
If the initial Cloudflare login fails during setup, Maxy will fall back to asking you for a connection key. You can create one in the Cloudflare dashboard (Maxy will guide you through this in the browser).
|
|
91
91
|
|
|
92
92
|
**If you switched Cloudflare accounts or are stuck on the wrong one:** ask Maxy "Reset my Cloudflare login and start over." This is a clean reset — Maxy clears every stored credential, then opens a fresh browser sign-in. The next sign-in binds to whichever Cloudflare account you choose, with no risk of the previous account's stored credentials silently coming back.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Admin Terminal Stuck Disconnected After Upgrade
|
|
97
|
+
|
|
98
|
+
**Symptom:** The Software Update window's terminal goes blank, says "server restart detected — reconnecting…" and never comes back, or the window sits empty after you open it.
|
|
99
|
+
|
|
100
|
+
**Check:** `sudo systemctl --user status maxy-ttyd` — the unit should be `active (running)`. If it is, the admin-server proxy should reattach automatically within a few seconds.
|
|
101
|
+
|
|
102
|
+
**Fix:** Restart the unit:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
sudo systemctl --user restart maxy-ttyd
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This is safe — the tmux session survives the ttyd restart because `tmux new-session -A -s maxy-pty` is idempotent. Any upgrade or other shell command still running inside the session continues uninterrupted; ttyd simply re-attaches to it. If the session itself is wedged, you can kill it explicitly with `tmux kill-session -t maxy-pty` and the next attach will create a fresh one.
|
|
@@ -17,11 +17,11 @@ filter in the query.
|
|
|
17
17
|
|
|
18
18
|
For visual exploration ("can I see this graphically?", "show me the graph"),
|
|
19
19
|
tell the user to open the **Graph** menu item in the burger menu — it opens
|
|
20
|
-
|
|
21
|
-
visualizations, do not start a static file server,
|
|
22
|
-
`.html` file and navigate Playwright to it. The one-sentence
|
|
23
|
-
("Open the Graph menu item in the top-right — it opens a visual
|
|
24
|
-
|
|
20
|
+
a Maxy-native force-directed view of their own brand's subgraph (Task 587).
|
|
21
|
+
Do not build ad-hoc HTML visualizations, do not start a static file server,
|
|
22
|
+
do not `Write` an `.html` file and navigate Playwright to it. The one-sentence
|
|
23
|
+
reply ("Open the Graph menu item in the top-right — it opens a visual view
|
|
24
|
+
of your graph") is the correct answer.
|
|
25
25
|
|
|
26
26
|
## When the graph tools are absent
|
|
27
27
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
set -g history-limit 50000
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Maxy admin terminal (ttyd + tmux) — persistent PTY for admin UI
|
|
3
|
+
After=default.target
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
Type=simple
|
|
7
|
+
# -p 7681 listen on 127.0.0.1:7681 (same-origin proxy in maxy-ui binds to it)
|
|
8
|
+
# -i 127.0.0.1 reject non-loopback connections at the ttyd layer as well
|
|
9
|
+
# -W writable (allow client → server input bytes)
|
|
10
|
+
# tmux new-session -A -s maxy-pty attach if session exists, create otherwise.
|
|
11
|
+
# Lifetime = user session / device lifetime. Outlives maxy-ui restarts because
|
|
12
|
+
# this unit has no After=maxy-ui.service and no Requires= — independent.
|
|
13
|
+
# -x 200 -y 50 initial geometry; xterm.js fit-addon drives runtime resizes.
|
|
14
|
+
ExecStart=/usr/bin/ttyd -p 7681 -i 127.0.0.1 -W tmux new-session -A -s maxy-pty -x 200 -y 50
|
|
15
|
+
Environment=TERM=xterm-256color
|
|
16
|
+
Restart=always
|
|
17
|
+
RestartSec=2
|
|
18
|
+
|
|
19
|
+
[Install]
|
|
20
|
+
WantedBy=default.target
|