@rubytech/create-maxy 1.0.627 → 1.0.629

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.627",
3
+ "version": "1.0.629",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env bash
2
+ # Shared stream-log helpers sourced by setup-tunnel.sh and reset-tunnel.sh.
3
+ #
4
+ # The platform exposes STREAM_LOG_PATH in the `claude` spawn env (Task 556);
5
+ # the Bash-tool subprocess inherits it, and opt-in scripts call these
6
+ # helpers to emit phase lines and tee subprocess output into the same
7
+ # per-conversation file the chat UI's server-side tailer reads.
8
+ #
9
+ # Contract (read by platform/ui/app/api/admin/chat/route.ts tailer and
10
+ # .docs/platform.md):
11
+ # [<ISO-ts>] [<scope>] <kv …>
12
+ # [<ISO-ts>] [<scope>:<subprocess-tag>] <raw line>
13
+ # where <scope> ∈ {setup-tunnel, reset-tunnel}. The tailer regex is
14
+ # ^\[[^]]+\] \[(setup-tunnel|reset-tunnel)(:[^]]+)?\]
15
+ # so any prefix change must be made on both sides atomically.
16
+
17
+ # Exit 1 loudly with the variable name and the invoking scope so direct-SSH
18
+ # invocations fail fast and the operator reads exactly what to set. No
19
+ # silent fallback — criterion 4 of Task 556.
20
+ require_stream_log_path() {
21
+ local scope="$1"
22
+ if [ -z "${STREAM_LOG_PATH:-}" ]; then
23
+ echo "ERROR [${scope}]: STREAM_LOG_PATH environment variable is unset." >&2
24
+ echo " This script tees subprocess output into a per-conversation" >&2
25
+ echo " stream log; it is meaningless without a target path." >&2
26
+ echo " The platform sets STREAM_LOG_PATH automatically for every" >&2
27
+ echo " \`claude\` spawn (Task 556). If you are invoking this" >&2
28
+ echo " script by hand, export it yourself, for example:" >&2
29
+ echo " export STREAM_LOG_PATH=\"\${HOME}/.maxy/logs/manual-invocation.log\"" >&2
30
+ exit 1
31
+ fi
32
+ mkdir -p "$(dirname "${STREAM_LOG_PATH}")"
33
+ }
34
+
35
+ # ISO-8601 UTC timestamp with millisecond precision.
36
+ stream_log_ts() {
37
+ date -u +"%Y-%m-%dT%H:%M:%S.%3NZ"
38
+ }
39
+
40
+ # Append one phase line to STREAM_LOG_PATH AND echo to stderr so the Bash
41
+ # tool's stderr capture carries it. Both paths matter: the stream log is
42
+ # the live tailer surface; stderr is the exit-time Bash-tool surface.
43
+ #
44
+ # Usage: phase_line <scope> <key=value …>
45
+ phase_line() {
46
+ local scope="$1"; shift
47
+ local ts
48
+ ts="$(stream_log_ts)"
49
+ printf '[%s] [%s] %s\n' "${ts}" "${scope}" "$*" >> "${STREAM_LOG_PATH}"
50
+ printf '[%s] %s\n' "${scope}" "$*" >&2
51
+ }
52
+
53
+ # Tee a subprocess's combined stdout+stderr into STREAM_LOG_PATH line-by-line
54
+ # with the given tag, mirroring each line to stderr for the Bash tool.
55
+ # Returns the subprocess's exit code.
56
+ #
57
+ # Use for fire-and-forget subprocesses whose stdout does NOT need to be
58
+ # captured by the caller (the caller only cares about the exit code).
59
+ # For subprocesses whose stdout the caller must parse (e.g. JSON output
60
+ # feeding `jq`), use `tee_subprocess_capture` instead.
61
+ #
62
+ # Usage: tee_subprocess <tag> -- <cmd> <args …>
63
+ # Example: tee_subprocess reset-tunnel:cloudflared -- cloudflared --origincert … tunnel delete my-tunnel
64
+ #
65
+ # stdbuf forces line buffering; without it cloudflared holds its stdout in
66
+ # libc's 8 KB buffer until exit and the ≤ 1 s latency criterion fails.
67
+ tee_subprocess() {
68
+ local tag="$1"; shift
69
+ if [ "${1:-}" != "--" ]; then
70
+ echo "tee_subprocess: expected -- before command" >&2
71
+ return 2
72
+ fi
73
+ shift
74
+ # Prefer `stdbuf -oL -eL` to force line buffering on the subprocess; fall
75
+ # back to the bare command when stdbuf is unavailable (macOS dev). On the
76
+ # Pi (Linux), stdbuf is present via coreutils and cloudflared's output
77
+ # reaches the tee as soon as each line is written.
78
+ local -a buf_prefix=()
79
+ if command -v stdbuf >/dev/null 2>&1; then
80
+ buf_prefix=(stdbuf -oL -eL)
81
+ fi
82
+ "${buf_prefix[@]}" "$@" 2>&1 | while IFS= read -r line; do
83
+ local ts
84
+ ts="$(stream_log_ts)"
85
+ printf '[%s] [%s] %s\n' "${ts}" "${tag}" "${line}" >> "${STREAM_LOG_PATH}"
86
+ printf '%s\n' "${line}" >&2
87
+ done
88
+ return "${PIPESTATUS[0]}"
89
+ }
90
+
91
+ # Tee a subprocess's combined stdout+stderr into STREAM_LOG_PATH line-by-line
92
+ # with the given tag, passing each line through on stdout so the caller can
93
+ # capture it with `> file` or `$( … )`. Stderr mirroring is dropped so the
94
+ # caller's stdout is exactly the subprocess's output — `jq` and friends
95
+ # remain parseable.
96
+ #
97
+ # Usage: tee_subprocess_capture <tag> -- <cmd> <args …> > captured.file
98
+ # Example:
99
+ # tee_subprocess_capture reset-tunnel:cloudflared -- \
100
+ # cloudflared --origincert "${CERT}" tunnel list --output json \
101
+ # > "${NAMES_TMP}"
102
+ tee_subprocess_capture() {
103
+ local tag="$1"; shift
104
+ if [ "${1:-}" != "--" ]; then
105
+ echo "tee_subprocess_capture: expected -- before command" >&2
106
+ return 2
107
+ fi
108
+ shift
109
+ # Prefer `stdbuf -oL -eL` to force line buffering on the subprocess; fall
110
+ # back to the bare command when stdbuf is unavailable (macOS dev). On the
111
+ # Pi (Linux), stdbuf is present via coreutils and cloudflared's output
112
+ # reaches the tee as soon as each line is written.
113
+ local -a buf_prefix=()
114
+ if command -v stdbuf >/dev/null 2>&1; then
115
+ buf_prefix=(stdbuf -oL -eL)
116
+ fi
117
+ "${buf_prefix[@]}" "$@" 2>&1 | while IFS= read -r line; do
118
+ local ts
119
+ ts="$(stream_log_ts)"
120
+ printf '[%s] [%s] %s\n' "${ts}" "${tag}" "${line}" >> "${STREAM_LOG_PATH}"
121
+ printf '%s\n' "${line}"
122
+ done
123
+ return "${PIPESTATUS[0]}"
124
+ }
@@ -19,6 +19,20 @@
19
19
 
20
20
  set -euo pipefail
21
21
 
22
+ # --------------------------------------------------------------------------
23
+ # Shared stream-log helpers (require STREAM_LOG_PATH, phase_line, tee_subprocess).
24
+ # Task 556: any cloudflared subprocess this script spawns is teed line-by-line
25
+ # into the per-conversation stream log so the chat UI tailer renders live
26
+ # progress — same contract as setup-tunnel.sh.
27
+ # --------------------------------------------------------------------------
28
+
29
+ # shellcheck source=_stream-log.sh
30
+ # Resolve symlinks before dirname — ~/reset-tunnel.sh is installed as a symlink
31
+ # (see packages/create-maxy/src/index.ts:installTunnelScripts), so the raw
32
+ # BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
33
+ source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
34
+ require_stream_log_path reset-tunnel
35
+
22
36
  if [ "$#" -lt 1 ]; then
23
37
  echo "Usage: $0 <brand>" >&2
24
38
  exit 2
@@ -28,28 +42,54 @@ BRAND="$1"
28
42
  CFG_DIR="${HOME}/.${BRAND}/cloudflared"
29
43
  CERT="${CFG_DIR}/cert.pem"
30
44
 
45
+ phase_line reset-tunnel step=start brand="${BRAND}" cfg_dir="${CFG_DIR}"
46
+
31
47
  if [ ! -f "${CERT}" ]; then
48
+ phase_line reset-tunnel step=no-cert cfg_dir="${CFG_DIR}"
32
49
  echo "No cert.pem at ${CERT} — nothing to delete via CLI."
33
50
  echo "Removing ${CFG_DIR} if present."
34
51
  rm -rf "${CFG_DIR}"
52
+ phase_line reset-tunnel step=done result=wiped-no-cert
35
53
  exit 0
36
54
  fi
37
55
 
38
56
  # Delete every tunnel on the account (the cert is per-account for tunnel CRUD).
39
- NAMES="$(cloudflared --origincert "${CERT}" tunnel list --output json 2>/dev/null \
40
- | jq -r '.[]?.name' | sort -u)"
57
+ # cloudflared's list/delete output is teed into STREAM_LOG_PATH; the `list`
58
+ # call uses tee_subprocess_capture so NAMES_TMP receives the JSON for jq to
59
+ # parse. Using the non-capture variant here would strand the JSON in stderr
60
+ # and leave NAMES_TMP empty — every tunnel would silently survive reset.
61
+ phase_line reset-tunnel step=list-tunnels
62
+ NAMES_TMP="$(mktemp -t maxy-reset-tunnel-list.XXXXXX)"
63
+ # shellcheck disable=SC2064
64
+ trap "rm -f '${NAMES_TMP}'" EXIT
65
+ if ! tee_subprocess_capture reset-tunnel:cloudflared -- \
66
+ cloudflared --origincert "${CERT}" tunnel list --output json \
67
+ > "${NAMES_TMP}"; then
68
+ phase_line reset-tunnel step=list-tunnels result=error reason=cloudflared-list-failed
69
+ echo "ERROR: cloudflared tunnel list failed. See stream log for detail." >&2
70
+ exit 1
71
+ fi
72
+ NAMES="$(jq -r '.[]?.name' "${NAMES_TMP}" 2>/dev/null | sort -u || true)"
41
73
  if [ -z "${NAMES}" ]; then
74
+ phase_line reset-tunnel step=list-tunnels result=empty
42
75
  echo "No tunnels to delete on this brand's account."
43
76
  else
44
77
  while IFS= read -r NAME; do
45
78
  [ -z "${NAME}" ] && continue
79
+ phase_line reset-tunnel step=delete-tunnel name="${NAME}"
46
80
  echo "Deleting tunnel: ${NAME}"
47
- cloudflared --origincert "${CERT}" tunnel delete "${NAME}"
81
+ if ! tee_subprocess reset-tunnel:cloudflared -- \
82
+ cloudflared --origincert "${CERT}" tunnel delete "${NAME}"; then
83
+ phase_line reset-tunnel step=delete-tunnel result=error name="${NAME}"
84
+ echo "ERROR: cloudflared tunnel delete failed for ${NAME}. See stream log." >&2
85
+ exit 1
86
+ fi
48
87
  done <<< "${NAMES}"
49
88
  fi
50
89
 
51
90
  # Wipe the brand-scoped cloudflared state directory.
52
91
  rm -rf "${CFG_DIR}"
92
+ phase_line reset-tunnel step=wipe-cfg-dir path="${CFG_DIR}"
53
93
  echo "Removed ${CFG_DIR}"
54
94
 
55
95
  echo ""
@@ -63,3 +103,5 @@ echo "The platform service (${BRAND}.service) was not touched. To release any"
63
103
  echo "running cloudflared connector started by ${BRAND}.service's ExecStartPre,"
64
104
  echo "either restart the service (which will no-op on resume since tunnel.state"
65
105
  echo "is gone) or leave it running until you re-run setup-tunnel.sh."
106
+
107
+ phase_line reset-tunnel step=done result=ok brand="${BRAND}"
@@ -13,9 +13,28 @@
13
13
  # via `cloudflared tunnel route dns` — see manual-setup.md §Step 4 Apex
14
14
  # hostnames. The script writes the ingress rule for them but prints an
15
15
  # explicit ACTION REQUIRED message naming the manual dashboard step.
16
+ #
17
+ # Task 556: Step 1 owns the browser-spawn deterministically. Instead of
18
+ # delegating to cloudflared's xdg-open (which silently degrades to "print
19
+ # URL and wait"), the script drives Chromium on :99 via CDP `PUT /json/new?<url>`
20
+ # on http://127.0.0.1:9222 — the same mechanism
21
+ # /api/admin/device-browser/navigate uses. cloudflared's stdout+stderr is
22
+ # teed line-by-line into STREAM_LOG_PATH so the chat UI's server-side
23
+ # tailer renders live progress in-turn.
16
24
 
17
25
  set -euo pipefail
18
26
 
27
+ # --------------------------------------------------------------------------
28
+ # Shared stream-log helpers (require STREAM_LOG_PATH, phase_line, …).
29
+ # --------------------------------------------------------------------------
30
+
31
+ # shellcheck source=_stream-log.sh
32
+ # Resolve symlinks before dirname — ~/setup-tunnel.sh is installed as a symlink
33
+ # (see packages/create-maxy/src/index.ts:installTunnelScripts), so the raw
34
+ # BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
35
+ source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
36
+ require_stream_log_path setup-tunnel
37
+
19
38
  # --------------------------------------------------------------------------
20
39
  # Args
21
40
  # --------------------------------------------------------------------------
@@ -31,6 +50,8 @@ PORT="$2"
31
50
  shift 2
32
51
  HOSTNAMES=("$@")
33
52
 
53
+ phase_line setup-tunnel step=start brand="${BRAND}" port="${PORT}" hostnames="${HOSTNAMES[*]}"
54
+
34
55
  # --------------------------------------------------------------------------
35
56
  # Step 0: Set brand context (paths + dirs). Corresponds to runbook Step 0.
36
57
  # --------------------------------------------------------------------------
@@ -40,24 +61,144 @@ mkdir -p "${CFG_DIR}"
40
61
 
41
62
  # --------------------------------------------------------------------------
42
63
  # Step 1: OAuth login (only if cert.pem missing). Corresponds to runbook Step 1.
43
- # cloudflared always writes cert.pem to ~/.cloudflared/cert.pem regardless
44
- # of --origincert mv it into the brand-scoped path on fresh login.
64
+ #
65
+ # Control flow, rewritten per Task 556:
66
+ # 1. CDP precheck on 127.0.0.1:9222 — loud failure if Chromium isn't up.
67
+ # 2. Spawn cloudflared with stdout+stderr teed line-by-line to
68
+ # $STREAM_LOG_PATH with prefix [setup-tunnel:cloudflared].
69
+ # 3. Extract the authorize URL with a tolerant regex as it streams.
70
+ # 4. Drive the VNC Chromium to that URL via CDP `PUT /json/new?<url>`.
71
+ # 5. Wait for ~/.cloudflared/cert.pem to land with bounded timeout.
72
+ # 6. Move cert.pem into the brand-scoped path.
73
+ #
74
+ # Every branch exits 1 loudly naming the failure — no silent retries,
75
+ # no xdg-open race, no cloudflared-internal browser-spawn path.
45
76
  # --------------------------------------------------------------------------
46
77
 
47
78
  if [ ! -f "${CFG_DIR}/cert.pem" ]; then
48
- echo "No cert.pem at ${CFG_DIR}/cert.pem — starting OAuth login (DISPLAY=${DISPLAY:-:99})"
49
- DISPLAY="${DISPLAY:-:99}" cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel login &
50
- LOGIN_PID=$!
51
- # cloudflared login writes to ~/.cloudflared/cert.pem regardless of --origincert
79
+ phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem" display="${DISPLAY:-:99}"
80
+
81
+ # CDP precheck — fail loudly if Chromium DevTools is not answering.
82
+ if ! curl -sf --max-time 2 "http://127.0.0.1:9222/json/version" > /dev/null 2>&1; then
83
+ phase_line setup-tunnel step=oauth-login result=error reason=cdp-unreachable \
84
+ endpoint=http://127.0.0.1:9222 hint="run ~/vnc.sh restart"
85
+ echo "ERROR: Chromium CDP on 127.0.0.1:9222 is not reachable." >&2
86
+ echo " The script needs CDP to drive the authorize URL on the VNC browser." >&2
87
+ echo " Fix: run 'vnc.sh restart' to bring Chromium up on :99." >&2
88
+ exit 1
89
+ fi
90
+ phase_line setup-tunnel step=oauth-login cdp=ok
91
+
92
+ URL_FILE="$(mktemp -t maxy-setup-tunnel-url.XXXXXX)"
93
+ LAST_LINE_FILE="$(mktemp -t maxy-setup-tunnel-last.XXXXXX)"
94
+ : > "${URL_FILE}"
95
+ : > "${LAST_LINE_FILE}"
96
+ # Track the cloudflared pipeline PID so the EXIT trap can kill it on any
97
+ # failure path — including ones that `exit 1` without an explicit kill.
98
+ # Missing this trap leaks a cloudflared subshell waiting for the OAuth
99
+ # callback forever; subsequent setup-tunnel runs see a stale cert.pem
100
+ # landing asynchronously and race against the new URL-extraction pass.
101
+ CF_PIPELINE_PID=""
102
+ cleanup_oauth() {
103
+ [ -n "${CF_PIPELINE_PID}" ] && kill "${CF_PIPELINE_PID}" 2>/dev/null || true
104
+ rm -f "${URL_FILE}" "${LAST_LINE_FILE}"
105
+ }
106
+ trap cleanup_oauth EXIT
107
+
108
+ # cloudflared is line-buffered (stdbuf), teed to the stream log, URL
109
+ # extracted as it streams. The subshell holds the whole pipeline so
110
+ # PIPESTATUS[0] (cloudflared's exit code) is reachable later.
111
+ (
112
+ DISPLAY="${DISPLAY:-:99}" stdbuf -oL -eL cloudflared \
113
+ --origincert "${CFG_DIR}/cert.pem" tunnel login 2>&1 |
114
+ while IFS= read -r line; do
115
+ ts="$(stream_log_ts)"
116
+ printf '[%s] [setup-tunnel:cloudflared] %s\n' "${ts}" "${line}" >> "${STREAM_LOG_PATH}"
117
+ printf '%s\n' "${line}" >&2
118
+ printf '%s\n' "${line}" > "${LAST_LINE_FILE}"
119
+ if [ ! -s "${URL_FILE}" ]; then
120
+ url="$(printf '%s' "${line}" | grep -oE 'https://dash\.cloudflare\.com/argotunnel\?[^ ]+' | head -1 || true)"
121
+ if [ -n "${url}" ]; then
122
+ printf '%s' "${url}" > "${URL_FILE}"
123
+ fi
124
+ fi
125
+ done
126
+ ) &
127
+ CF_PIPELINE_PID=$!
128
+
129
+ # Wait up to ~15s for the URL to surface in cloudflared's output.
130
+ URL_WAIT=0
131
+ while [ ! -s "${URL_FILE}" ] && [ "${URL_WAIT}" -lt 30 ]; do
132
+ if ! kill -0 "${CF_PIPELINE_PID}" 2>/dev/null; then
133
+ phase_line setup-tunnel step=oauth-login result=error \
134
+ reason=cloudflared-exited-before-url \
135
+ last_line="$(cat "${LAST_LINE_FILE}" 2>/dev/null || echo none)"
136
+ echo "ERROR: cloudflared exited before printing the authorize URL." >&2
137
+ exit 1
138
+ fi
139
+ sleep 0.5
140
+ URL_WAIT=$((URL_WAIT + 1))
141
+ done
142
+
143
+ if [ ! -s "${URL_FILE}" ]; then
144
+ kill "${CF_PIPELINE_PID}" 2>/dev/null || true
145
+ phase_line setup-tunnel step=oauth-login result=error \
146
+ reason=url-not-extracted waited=15s \
147
+ last_line="$(cat "${LAST_LINE_FILE}" 2>/dev/null || echo none)"
148
+ echo "ERROR: cloudflared ran for ~15s without emitting a dash.cloudflare.com/argotunnel URL." >&2
149
+ echo " cloudflared output-format may have changed. Check the stream log tail for the raw lines." >&2
150
+ exit 1
151
+ fi
152
+
153
+ AUTH_URL="$(cat "${URL_FILE}")"
154
+ phase_line setup-tunnel step=browser-drive url_extracted=1
155
+
156
+ # Drive CDP. Same PUT /json/new?<url> contract as
157
+ # platform/ui/app/lib/cdp-client.ts (which uses encodeURIComponent on
158
+ # the URL). Without percent-encoding, CDP's URL parser splits on the
159
+ # inner `?` and `&` in the argotunnel URL and drops the callback/token
160
+ # query params — Chromium lands on a bare argotunnel page and the
161
+ # consent screen is never reached. `jq -sRr @uri` percent-encodes a raw
162
+ # string, matching the TS client exactly.
163
+ AUTH_URL_ENC="$(printf '%s' "${AUTH_URL}" | jq -sRr @uri)"
164
+ CDP_RESP="$(curl -sf --max-time 5 -X PUT "http://127.0.0.1:9222/json/new?${AUTH_URL_ENC}" 2>&1 || true)"
165
+ if [ -z "${CDP_RESP}" ]; then
166
+ kill "${CF_PIPELINE_PID}" 2>/dev/null || true
167
+ phase_line setup-tunnel step=browser-drive result=error reason=cdp-put-failed
168
+ echo "ERROR: CDP PUT /json/new?<url> returned empty — Chromium rejected the navigate." >&2
169
+ exit 1
170
+ fi
171
+ phase_line setup-tunnel step=browser-drive result=accepted
172
+
173
+ # Wait for cert.pem to land — cloudflared writes to ~/.cloudflared/cert.pem
174
+ # regardless of --origincert, so watch the canonical location.
175
+ LOGIN_TIMEOUT="${SETUP_TUNNEL_LOGIN_TIMEOUT:-180}"
176
+ LOGIN_WAIT=0
52
177
  while [ ! -f "${HOME}/.cloudflared/cert.pem" ]; do
53
- if ! kill -0 "${LOGIN_PID}" 2>/dev/null; then
54
- echo "ERROR: cloudflared tunnel login exited before cert.pem landed" >&2
178
+ if ! kill -0 "${CF_PIPELINE_PID}" 2>/dev/null; then
179
+ # Pipeline exited one more cert.pem probe in case it landed right before exit.
180
+ if [ -f "${HOME}/.cloudflared/cert.pem" ]; then break; fi
181
+ phase_line setup-tunnel step=oauth-login result=error \
182
+ reason=cloudflared-exited-no-cert \
183
+ last_line="$(cat "${LAST_LINE_FILE}" 2>/dev/null || echo none)"
184
+ echo "ERROR: cloudflared exited before cert.pem landed." >&2
55
185
  exit 1
56
186
  fi
57
- sleep 2
187
+ if [ "${LOGIN_WAIT}" -ge "${LOGIN_TIMEOUT}" ]; then
188
+ kill "${CF_PIPELINE_PID}" 2>/dev/null || true
189
+ phase_line setup-tunnel step=oauth-login result=error \
190
+ reason=timeout-waiting-cert waited="${LOGIN_WAIT}s" \
191
+ last_line="$(cat "${LAST_LINE_FILE}" 2>/dev/null || echo none)"
192
+ echo "ERROR: Timed out after ${LOGIN_WAIT}s waiting for cert.pem to land." >&2
193
+ exit 1
194
+ fi
195
+ sleep 1
196
+ LOGIN_WAIT=$((LOGIN_WAIT + 1))
58
197
  done
198
+
59
199
  mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem"
60
- echo "cert.pem moved to ${CFG_DIR}/cert.pem"
200
+ phase_line setup-tunnel step=oauth-login result=cert-received \
201
+ path="${CFG_DIR}/cert.pem" waited="${LOGIN_WAIT}s"
61
202
  fi
62
203
 
63
204
  # --------------------------------------------------------------------------
@@ -103,6 +103,8 @@ After this, every `console.error("[your-tool] ...")` from any tool in the plugin
103
103
 
104
104
  **How the tee decides which file to write to (Task 532):** the platform sets `STREAM_LOG_PATH` as an environment variable on every MCP server spawn, pointing to the conversation-scoped stream log. The MCP server does not know about conversations — it just trusts `STREAM_LOG_PATH`. Multiple concurrent conversations produce multiple concurrent MCP server processes, each teeing to its own file; no cross-conversation leakage.
105
105
 
106
+ **`STREAM_LOG_PATH` reaches every Claude Code child (Task 556).** The platform now sets `STREAM_LOG_PATH` on the parent `claude` spawn env itself (not only on MCP server envs), so the bundled Bun runtime inherits it and every Bash-tool subprocess the CLI spawns sees it too. Opt-in shell scripts — currently `setup-tunnel.sh` and `reset-tunnel.sh` under `platform/plugins/cloudflare/scripts/` — read the variable, guard against a missing value with a loud exit, and tee subprocess output line-by-line into the same per-conversation file. Each spawn writes one `[spawn-env] STREAM_LOG_PATH=set pid=… conversationId=… site=…` line so the env-propagation is auditable per session. The chat UI tails the same file for lines matching `^\[[^]]+\] \[(setup-tunnel|reset-tunnel)(:[^]]+)?\] ` and emits them as `script_stream` SSE events; see `.docs/web-chat.md` for the contract.
107
+
106
108
  **Retrieve MCP diagnostic lines for a conversation:**
107
109
 
108
110
  - All servers: `logs-read { type: "system", conversationId: "..." }` → grep `[mcp:<name>]` on the returned stream log.