@rubytech/create-realagent 1.0.652 → 1.0.653
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/package.json +1 -1
- package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +19 -4
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +162 -63
- package/payload/platform/plugins/docs/references/platform.md +3 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +16 -0
- package/payload/server/public/assets/{admin-DQxieG3v.js → admin-DLp3geZN.js} +4 -4
- package/payload/server/public/assets/{data-CAKMrPTQ.js → data-DPqrIvgB.js} +1 -1
- package/payload/server/public/assets/{file-dBmvpAuH.js → file-Buaz89w8.js} +1 -1
- package/payload/server/public/assets/{graph-3snSy3WW.js → graph-B_TxtKJP.js} +1 -1
- package/payload/server/public/assets/{house-IMEjNkQf.js → house-B5wS-2kc.js} +1 -1
- package/payload/server/public/assets/jsx-runtime-ChVPhhAG.css +1 -0
- package/payload/server/public/assets/{public-b43rEAhq.js → public-BNEciseE.js} +1 -1
- package/payload/server/public/assets/{share-2-ARoCxH5K.js → share-2-Z5v9aWZ2.js} +1 -1
- package/payload/server/public/assets/{trash-2-D_Rm8z21.js → trash-2-BaLFnigq.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-Cw8gxj1L.js → useVoiceRecorder-CgMo3FDt.js} +1 -1
- package/payload/server/public/assets/x-DYxtrMFK.js +1 -0
- package/payload/server/public/data.html +7 -7
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +15 -1
- package/payload/server/public/assets/jsx-runtime-BJhXEiL3.css +0 -1
- package/payload/server/public/assets/x-CFPIrGuL.js +0 -1
- /package/payload/server/public/assets/{jsx-runtime-CXoJCO3U.js → jsx-runtime-WYScGBOd.js} +0 -0
package/package.json
CHANGED
|
@@ -6,13 +6,28 @@
|
|
|
6
6
|
# helpers to emit phase lines and tee subprocess output into the same
|
|
7
7
|
# per-conversation file the chat UI's server-side tailer reads.
|
|
8
8
|
#
|
|
9
|
-
# Contract (read by platform/ui/app/
|
|
9
|
+
# Contract (read by platform/ui/app/lib/script-stream-tailer.ts tailer and
|
|
10
10
|
# .docs/platform.md):
|
|
11
11
|
# [<ISO-ts>] [<scope>] <kv …>
|
|
12
12
|
# [<ISO-ts>] [<scope>:<subprocess-tag>] <raw line>
|
|
13
|
-
#
|
|
14
|
-
# ^\[[
|
|
15
|
-
#
|
|
13
|
+
# Canonical regex — SCRIPT_STREAM_RE at platform/ui/app/lib/script-stream-tailer.ts:51:
|
|
14
|
+
# ^\[([^\]]+)\] \[([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] (.*)$
|
|
15
|
+
# <scope> is any `[a-z][a-z0-9-]*` token (Task 592 generalised from the
|
|
16
|
+
# pre-592 enum `setup-tunnel|reset-tunnel`, which silently filtered out the
|
|
17
|
+
# `[list-cf-domains]` lines Task 589 emitted). <subprocess-tag> may contain
|
|
18
|
+
# lowercase, digits, `-`, `_`, `:`. Adding a new <scope> requires no edit to
|
|
19
|
+
# the regex — the shape is the only contract. Any prefix-shape change must
|
|
20
|
+
# be made on both sides (this helper + script-stream-tailer.ts) atomically.
|
|
21
|
+
#
|
|
22
|
+
# Inner layers (e.g. a node/python helper a .sh wrapper spawns — Task 598's
|
|
23
|
+
# `list-cf-domains.sh` → `list-cf-domains.ts` pattern) must write phase lines
|
|
24
|
+
# directly to STREAM_LOG_PATH with the same prefix shape: stderr alone is
|
|
25
|
+
# silently discarded by runFormSpawn on exit 0. The build-gate
|
|
26
|
+
# `platform/ui/scripts/check-stream-log-contract.mjs` (Task 600) enforces this
|
|
27
|
+
# by rejecting any .sh under `platform/plugins/*/scripts/` that sources this
|
|
28
|
+
# helper and invokes an interpreter subprocess whose target does not pair a
|
|
29
|
+
# STREAM_LOG_PATH env read with an append/write call. Opt out per invocation
|
|
30
|
+
# with `# stream-log-contract: stderr-only (reason: <prose>)`.
|
|
16
31
|
|
|
17
32
|
# Exit 1 loudly with the variable name and the invoking scope so direct-SSH
|
|
18
33
|
# invocations fail fast and the operator reads exactly what to set. No
|
|
@@ -17,6 +17,23 @@
|
|
|
17
17
|
// routing and cannot drift without breaking their entire dashboard; it
|
|
18
18
|
// is the most stable surface to key off.
|
|
19
19
|
//
|
|
20
|
+
// Why href-only (no page-text fallback): Task 599 removed an earlier
|
|
21
|
+
// <main>-wide FQDN text walker ("Source B") after a reproduction returned
|
|
22
|
+
// `count=1` with a bogus string (`leg.interest` — a static footer FQDN-shape
|
|
23
|
+
// that hit the regex). The walker's non-empty result short-circuited the
|
|
24
|
+
// poll-to-success branch before zones hydrated 500 ms later, silently
|
|
25
|
+
// returning wrong data with `reason=ok`. The walker's intended role —
|
|
26
|
+
// "defend against CF redesign removing href links" — is already served by
|
|
27
|
+
// the existing `empty-or-drift` body-dump branch when href scraping returns
|
|
28
|
+
// nothing; a silent fallback that produces wrong answers is strictly worse
|
|
29
|
+
// than a loud drift signal with a snapshotted HTML dump for diagnosis.
|
|
30
|
+
//
|
|
31
|
+
// Why poll-to-stable: the CF dashboard lazy-loads its zone list after the
|
|
32
|
+
// initial document-ready signal. A naive "return on first non-empty poll"
|
|
33
|
+
// can race into a partial result (e.g. first zone rendered at t=500 ms, all
|
|
34
|
+
// zones at t=1000 ms). The `scrapeDomains` poll waits for the observed count
|
|
35
|
+
// to stabilise across two consecutive iterations before returning.
|
|
36
|
+
//
|
|
20
37
|
// Contract with the caller (list-cf-domains.sh → route → form):
|
|
21
38
|
// stdout on exit 0: JSON `string[]` (empty array means signed-in-but-empty)
|
|
22
39
|
// stderr on any path: phase_line-formatted lines, `[list-cf-domains] …`
|
|
@@ -29,6 +46,7 @@ import { appendFileSync } from "node:fs";
|
|
|
29
46
|
import { writeFile } from "node:fs/promises";
|
|
30
47
|
import { resolve } from "node:path";
|
|
31
48
|
import { homedir } from "node:os";
|
|
49
|
+
import { fileURLToPath } from "node:url";
|
|
32
50
|
|
|
33
51
|
// CDP host/port default to the VNC Chromium's localhost bind; env overrides
|
|
34
52
|
// exist solely so the vitest regression under platform/ui/__tests__ can force
|
|
@@ -272,86 +290,154 @@ async function waitForSignedIn(cdp: CdpClient): Promise<string> {
|
|
|
272
290
|
die("not-signed-in", "no /<accountId>/… path reached within budget");
|
|
273
291
|
}
|
|
274
292
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
293
|
+
export interface ScrapeOutcome {
|
|
294
|
+
reason: "ok" | "no-account-id";
|
|
295
|
+
domains: string[];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Pure scrape logic — extracts the zones on the current Cloudflare dashboard
|
|
299
|
+
// page by matching `<a href="/<accountId>/<zone>…">` links. Exported for
|
|
300
|
+
// direct in-process testing under JSDOM (see `list-cf-domains-scrape.test.ts`),
|
|
301
|
+
// and serialised into `SCRAPE_EXPRESSION` below for execution inside the
|
|
302
|
+
// operator's VNC Chromium via CDP `Runtime.evaluate`. The function takes its
|
|
303
|
+
// document and location as arguments (rather than reading globals) so the
|
|
304
|
+
// JSDOM test can pass in its window's document+location directly — test and
|
|
305
|
+
// prod share the same implementation, with no duplication and no eval.
|
|
306
|
+
//
|
|
307
|
+
// Note: this function body is serialised via `Function.prototype.toString()`,
|
|
308
|
+
// so it must be fully self-contained — no imports, no captures from outer
|
|
309
|
+
// scope. Node-side helpers belong outside.
|
|
310
|
+
export function scrapeCurrentPage(
|
|
311
|
+
document: Document,
|
|
312
|
+
location: Location,
|
|
313
|
+
): ScrapeOutcome {
|
|
314
|
+
const accountIdMatch = location.pathname.match(/^\/([a-f0-9]{32})/);
|
|
315
|
+
if (!accountIdMatch) return { reason: "no-account-id", domains: [] };
|
|
286
316
|
const accountId = accountIdMatch[1];
|
|
287
317
|
|
|
288
|
-
const out = new Set();
|
|
289
|
-
const pushIfDomain = (s) => {
|
|
290
|
-
if (typeof s !==
|
|
318
|
+
const out = new Set<string>();
|
|
319
|
+
const pushIfDomain = (s: unknown): void => {
|
|
320
|
+
if (typeof s !== "string") return;
|
|
291
321
|
const t = s.trim().toLowerCase();
|
|
292
|
-
if (
|
|
293
|
-
|
|
322
|
+
if (
|
|
323
|
+
!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/.test(
|
|
324
|
+
t,
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
return;
|
|
328
|
+
if (t.endsWith(".cloudflare.com") || t === "cloudflare.com") return;
|
|
294
329
|
out.add(t);
|
|
295
330
|
};
|
|
296
331
|
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
332
|
+
// The only scrape source: `/<accountId>/<host>` hrefs — the canonical CF
|
|
333
|
+
// routing pattern the overview page uses to link each zone to its
|
|
334
|
+
// management screens. Task 599 removed a full-<main> FQDN text walker
|
|
335
|
+
// after it returned a bogus FQDN-shaped string from static page copy
|
|
336
|
+
// (see module header). When this source returns empty, the caller's
|
|
337
|
+
// `empty-or-drift` branch snapshots the page HTML for diagnosis — loud
|
|
338
|
+
// drift signal, not silent wrong data.
|
|
339
|
+
const hrefPrefix = "/" + accountId + "/";
|
|
340
|
+
for (const a of document.querySelectorAll("a[href]")) {
|
|
341
|
+
const href = a.getAttribute("href") || "";
|
|
302
342
|
if (!href.startsWith(hrefPrefix)) continue;
|
|
303
|
-
const tail = href.slice(hrefPrefix.length).split(
|
|
304
|
-
const firstSegment = tail.split(
|
|
343
|
+
const tail = href.slice(hrefPrefix.length).split("?")[0].split("#")[0];
|
|
344
|
+
const firstSegment = tail.split("/")[0];
|
|
305
345
|
pushIfDomain(firstSegment);
|
|
306
346
|
}
|
|
307
347
|
|
|
308
|
-
|
|
309
|
-
// against a hypothetical CF redesign that drops the link tree — the zone
|
|
310
|
-
// name still appears as rendered text in the table row.
|
|
311
|
-
const main = document.querySelector('main') || document.body;
|
|
312
|
-
if (main) {
|
|
313
|
-
const treeWalker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT);
|
|
314
|
-
let node;
|
|
315
|
-
while ((node = treeWalker.nextNode())) {
|
|
316
|
-
const text = (node.nodeValue || '').trim();
|
|
317
|
-
if (text.length < 4 || text.length > 253) continue;
|
|
318
|
-
pushIfDomain(text);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return { reason: 'ok', domains: Array.from(out).sort() };
|
|
323
|
-
})()`;
|
|
324
|
-
|
|
325
|
-
interface ScrapeOutcome {
|
|
326
|
-
reason: "ok" | "no-account-id";
|
|
327
|
-
domains: string[];
|
|
348
|
+
return { reason: "ok", domains: Array.from(out).sort() };
|
|
328
349
|
}
|
|
329
350
|
|
|
330
|
-
|
|
351
|
+
// The serialised form the CDP Runtime.evaluate executes inside the operator's
|
|
352
|
+
// VNC Chromium. Deriving it from `scrapeCurrentPage.toString()` means the
|
|
353
|
+
// browser-side and the test-side share one implementation — drift between
|
|
354
|
+
// them is impossible by construction.
|
|
355
|
+
export const SCRAPE_EXPRESSION = `(${scrapeCurrentPage.toString()})(document, location)`;
|
|
356
|
+
|
|
357
|
+
// Minimum surface `scrapeDomains` needs from the CDP client: execute a JS
|
|
358
|
+
// expression in the target page and return the serialised value. Typing the
|
|
359
|
+
// dependency at this width lets the vitest regression inject a mock without
|
|
360
|
+
// constructing a full CdpClient + WebSocket.
|
|
361
|
+
export type CdpEvaluator = (expression: string) => Promise<unknown>;
|
|
362
|
+
|
|
363
|
+
// Number of consecutive polls returning an identical non-empty count that
|
|
364
|
+
// proves the SPA zone-list has finished hydrating. Two iterations at 500 ms
|
|
365
|
+
// each = 1 s of observed stability, which is the empirical ceiling on the
|
|
366
|
+
// CF dashboard's lazy zone-list fetch post-document-ready. Shorter thresholds
|
|
367
|
+
// risk returning a partial list; longer thresholds add user-visible latency
|
|
368
|
+
// without proportional robustness.
|
|
369
|
+
const STABLE_POLL_THRESHOLD = 2;
|
|
370
|
+
|
|
371
|
+
export async function scrapeDomains(evaluator: CdpEvaluator): Promise<string[]> {
|
|
331
372
|
const deadline = Date.now() + SCRAPE_POLL_MS;
|
|
332
373
|
let lastOutcome: ScrapeOutcome | null = null;
|
|
374
|
+
// Track the most recent non-empty observation so deadline-reached without
|
|
375
|
+
// stability returns the caller's best evidence rather than falling through
|
|
376
|
+
// to the drift-dump (which is reserved for the always-empty case).
|
|
377
|
+
let lastNonEmptyDomains: string[] = [];
|
|
378
|
+
let stableCount = -1;
|
|
379
|
+
let stableIterations = 0;
|
|
380
|
+
let polls = 0;
|
|
381
|
+
|
|
333
382
|
while (Date.now() < deadline) {
|
|
383
|
+
polls += 1;
|
|
334
384
|
try {
|
|
335
|
-
const outcome = await
|
|
385
|
+
const outcome = (await evaluator(SCRAPE_EXPRESSION)) as ScrapeOutcome;
|
|
336
386
|
lastOutcome = outcome;
|
|
387
|
+
|
|
337
388
|
if (outcome.reason === "ok" && outcome.domains.length > 0) {
|
|
338
|
-
|
|
339
|
-
|
|
389
|
+
lastNonEmptyDomains = outcome.domains;
|
|
390
|
+
if (outcome.domains.length === stableCount) {
|
|
391
|
+
stableIterations += 1;
|
|
392
|
+
if (stableIterations >= STABLE_POLL_THRESHOLD) {
|
|
393
|
+
logPhase(
|
|
394
|
+
`phase=dom-scrape-complete result=ok count=${outcome.domains.length} polls=${polls} stable_polls=${stableIterations} unstable=false`,
|
|
395
|
+
);
|
|
396
|
+
return outcome.domains;
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
stableCount = outcome.domains.length;
|
|
400
|
+
stableIterations = 1;
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
// `ok` with zero domains OR a non-ok reason: treat as still-hydrating.
|
|
404
|
+
// Keep polling; the final iteration's zero result is the empty-account
|
|
405
|
+
// signal. Reset stability tracking so a 0 → N → 0 sequence doesn't
|
|
406
|
+
// falsely satisfy the threshold.
|
|
407
|
+
stableCount = -1;
|
|
408
|
+
stableIterations = 0;
|
|
340
409
|
}
|
|
341
|
-
// `ok` with zero domains is ambiguous during SPA hydration — keep
|
|
342
|
-
// polling in case the table renders after a data fetch. The final
|
|
343
|
-
// iteration's zero result is the empty-account signal.
|
|
344
410
|
} catch (err) {
|
|
345
|
-
logPhase(
|
|
411
|
+
logPhase(
|
|
412
|
+
`phase=scrape-retry err="${(err instanceof Error ? err.message : String(err)).slice(0, 120)}"`,
|
|
413
|
+
);
|
|
346
414
|
}
|
|
347
415
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
348
416
|
}
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
417
|
+
|
|
418
|
+
// Deadline reached. Two distinct branches:
|
|
419
|
+
//
|
|
420
|
+
// (i) We saw non-empty results but they never stabilised across two
|
|
421
|
+
// consecutive polls — the page is either paginating or re-rendering
|
|
422
|
+
// the zone list. Return the last non-empty observation and flag
|
|
423
|
+
// `unstable=true` on the phase line so the operator can see the race
|
|
424
|
+
// in the stream log. This is NOT drift — the HTML dump would be
|
|
425
|
+
// misleading noise, so we suppress it.
|
|
426
|
+
//
|
|
427
|
+
// (ii) We never saw non-empty results — this is either a genuinely empty
|
|
428
|
+
// account OR CF has drifted the href URL shape so Source A yields
|
|
429
|
+
// nothing. The `empty-or-drift` body-dump branch snapshots the page
|
|
430
|
+
// HTML so the operator can distinguish the two by inspecting the
|
|
431
|
+
// dump file.
|
|
432
|
+
if (lastNonEmptyDomains.length > 0) {
|
|
433
|
+
logPhase(
|
|
434
|
+
`phase=dom-scrape-complete result=ok count=${lastNonEmptyDomains.length} polls=${polls} stable_polls=${stableIterations} unstable=true`,
|
|
435
|
+
);
|
|
436
|
+
return lastNonEmptyDomains;
|
|
437
|
+
}
|
|
438
|
+
|
|
353
439
|
try {
|
|
354
|
-
const html = await
|
|
440
|
+
const html = (await evaluator("document.documentElement.outerHTML.slice(0, 100000)")) as string;
|
|
355
441
|
// CONFIG_DIR is set by list-cf-domains.sh before the spawn. A silent
|
|
356
442
|
// fallback would dump logs into the wrong brand's directory on a Real
|
|
357
443
|
// Agent install — a silent-miswrite masking the wrapper-side break it
|
|
@@ -359,15 +445,21 @@ async function scrapeDomains(cdp: CdpClient): Promise<string[]> {
|
|
|
359
445
|
// values.
|
|
360
446
|
const configDir = process.env.CONFIG_DIR;
|
|
361
447
|
if (!configDir) {
|
|
362
|
-
throw new Error(
|
|
448
|
+
throw new Error(
|
|
449
|
+
"CONFIG_DIR env var not set by wrapper — refusing to guess brand log directory",
|
|
450
|
+
);
|
|
363
451
|
}
|
|
364
452
|
const logDir = resolve(homedir(), configDir, "logs");
|
|
365
453
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
366
454
|
const dumpPath = resolve(logDir, `list-cf-domains-${ts}.html`);
|
|
367
455
|
await writeFile(dumpPath, typeof html === "string" ? html : String(html), "utf-8");
|
|
368
|
-
logPhase(
|
|
456
|
+
logPhase(
|
|
457
|
+
`phase=dom-scrape-complete result=empty-or-drift dump=${dumpPath} lastReason=${lastOutcome?.reason ?? "unknown"} polls=${polls}`,
|
|
458
|
+
);
|
|
369
459
|
} catch (err) {
|
|
370
|
-
logPhase(
|
|
460
|
+
logPhase(
|
|
461
|
+
`phase=dom-scrape-complete result=empty-or-drift dump=failed lastReason=${lastOutcome?.reason ?? "unknown"} polls=${polls} err="${(err instanceof Error ? err.message : String(err)).slice(0, 120)}"`,
|
|
462
|
+
);
|
|
371
463
|
}
|
|
372
464
|
return [];
|
|
373
465
|
}
|
|
@@ -419,7 +511,7 @@ async function main(): Promise<void> {
|
|
|
419
511
|
await navigate(cdp, overviewUrl);
|
|
420
512
|
await waitForDocumentReady(cdp);
|
|
421
513
|
|
|
422
|
-
const domains = await scrapeDomains(cdp);
|
|
514
|
+
const domains = await scrapeDomains((expr) => evaluate(cdp, expr));
|
|
423
515
|
|
|
424
516
|
logPhase(`phase=target-closed`);
|
|
425
517
|
process.stdout.write(JSON.stringify(domains) + "\n");
|
|
@@ -433,6 +525,13 @@ async function main(): Promise<void> {
|
|
|
433
525
|
}
|
|
434
526
|
}
|
|
435
527
|
|
|
436
|
-
main()
|
|
437
|
-
|
|
438
|
-
|
|
528
|
+
// Entry-point gate: only run `main()` when this module is the script Node was
|
|
529
|
+
// invoked with, not when imported by the vitest regressions that exercise
|
|
530
|
+
// `scrapeCurrentPage` / `scrapeDomains` directly. Without the gate, importing
|
|
531
|
+
// this module would trigger a live CDP connection attempt and process.exit(1)
|
|
532
|
+
// inside the test process.
|
|
533
|
+
if (process.argv[1] && process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
534
|
+
main().catch((err: unknown) => {
|
|
535
|
+
die("runtime-error", err instanceof Error ? err.message : String(err));
|
|
536
|
+
});
|
|
537
|
+
}
|
|
@@ -66,7 +66,9 @@ The chat input auto-grows as you type — it expands to fit your message and shr
|
|
|
66
66
|
|
|
67
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
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.
|
|
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 during an active upgrade 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
|
+
|
|
71
|
+
The Software Update window mounts the terminal lazily: the WebSocket is opened on the first Upgrade click, not when the window opens. Until you click Upgrade, the terminal area shows "Ready to upgrade." and no network traffic flows. If the admin server cannot reach `ttyd`, the window renders an inline "Admin terminal not available" message with the exact re-install command and a Try again button — no silent reconnect loops, no empty black rectangle. The scrollback-across-reopen behaviour above still applies during an active upgrade (a sessionStorage flag remembers that an upgrade is in flight so reopening the window re-mounts the terminal and reattaches).
|
|
70
72
|
|
|
71
73
|
## AI Content Provenance
|
|
72
74
|
|
|
@@ -103,7 +103,7 @@ After this, every `console.error("[your-tool] ...")` from any tool in the plugin
|
|
|
103
103
|
|
|
104
104
|
**How the tee decides which file to write to (Task 532):** the platform sets `STREAM_LOG_PATH` as an environment variable on every MCP server spawn, pointing to the conversation-scoped stream log. The MCP server does not know about conversations — it just trusts `STREAM_LOG_PATH`. Multiple concurrent conversations produce multiple concurrent MCP server processes, each teeing to its own file; no cross-conversation leakage.
|
|
105
105
|
|
|
106
|
-
**`STREAM_LOG_PATH` reaches every Claude Code child (Task 556).** The platform now sets `STREAM_LOG_PATH` on the parent `claude` spawn env itself (not only on MCP server envs), so the bundled Bun runtime inherits it and every Bash-tool subprocess the CLI spawns sees it too. Opt-in shell scripts — currently `setup-tunnel.sh` and `
|
|
106
|
+
**`STREAM_LOG_PATH` reaches every Claude Code child (Task 556).** The platform now sets `STREAM_LOG_PATH` on the parent `claude` spawn env itself (not only on MCP server envs), so the bundled Bun runtime inherits it and every Bash-tool subprocess the CLI spawns sees it too. Opt-in shell scripts — currently `setup-tunnel.sh`, `reset-tunnel.sh`, and `list-cf-domains.sh` under `platform/plugins/cloudflare/scripts/` — read the variable, guard against a missing value with a loud exit, and tee subprocess output line-by-line into the same per-conversation file. Each spawn writes one `[spawn-env] STREAM_LOG_PATH=set pid=… conversationId=… site=…` line so the env-propagation is auditable per session. The chat UI tails the same file for lines matching `^\[([^\]]+)\] \[([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] ` — any lowercase scope shape participates on first write (Task 592 generalised from the pre-592 enum `setup-tunnel|reset-tunnel`) — and emits them as `script_stream` SSE events; see `.docs/web-chat.md` for the contract. Inner-layer helpers that a .sh wrapper spawns (e.g. `list-cf-domains.ts` via `node --experimental-strip-types`) must write phase lines directly to `STREAM_LOG_PATH` rather than relying on stderr propagation (Task 598); the build-gate `platform/ui/scripts/check-stream-log-contract.mjs` (Task 600) enforces this and is the definitive reference for the three allowed patterns (tee-wrapped, direct-write, or explicit stderr-only marker).
|
|
107
107
|
|
|
108
108
|
**Retrieve MCP diagnostic lines for a conversation:**
|
|
109
109
|
|
|
@@ -109,6 +109,22 @@ This is safe — the tmux session survives the ttyd restart because `tmux new-se
|
|
|
109
109
|
|
|
110
110
|
**Terminal unit missing entirely?** If `sudo systemctl --user status maxy-ttyd` reports `unit not found`, the installer's ttyd provisioning step failed — typically on a Bookworm device where `ttyd` is not available via apt and the upstream download (or SHA256 check) failed at install time. Re-run `npx -y @rubytech/create-maxy@latest`; the operator-visible remediation command is also printed at install time in `~/.maxy/logs/install-*.log`.
|
|
111
111
|
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## "Admin terminal not available" in the Software Update window
|
|
115
|
+
|
|
116
|
+
**Symptom:** The Software Update window displays "Admin terminal not available. Re-run the installer from a shell: `npx -y @rubytech/create-maxy@latest`" with a Try again button, instead of the usual terminal area.
|
|
117
|
+
|
|
118
|
+
**What it means:** The admin server could not reach `ttyd` on `127.0.0.1:7681`. Either `maxy-ttyd.service` is not running, or it failed to install during setup.
|
|
119
|
+
|
|
120
|
+
**Fix:** Run the exact command shown in the error message from a shell on the device:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx -y @rubytech/create-maxy@latest
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Then return to the upgrade window and click **Try again**. The window re-probes `/api/health` and, once ttyd is listening, the terminal area mounts as normal. If the problem persists, check the boot log for `[ttyd] upstream NOT reachable on 127.0.0.1:7681` and follow the `maxy-ttyd` restart steps above.
|
|
127
|
+
|
|
112
128
|
## Orphan Account Directory Archived to `.trash/`
|
|
113
129
|
|
|
114
130
|
**What happened:** During upgrade, the installer detected multiple account directories under `~/maxy/data/accounts/` and identified one as live (its `admins` list matches the device's `users.json`). Non-matching siblings are archived — not deleted — under `~/maxy/data/accounts/.trash/<uuid>-<ISO8601-ts>/`.
|