@atlassian/chippy 0.0.0-stage → 0.0.1-beta.2

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.
Files changed (133) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +276 -2
  3. package/bin/chippy.mjs +92 -0
  4. package/bin/pack.mjs +204 -0
  5. package/bin/skill-dir.mjs +15 -0
  6. package/lib/avalanche.js +2601 -0
  7. package/lib/avalanche.js.map +7 -0
  8. package/lib/bootstrap.d.ts +1 -0
  9. package/lib/bootstrap.js +70 -0
  10. package/lib/bootstrap.js.map +7 -0
  11. package/lib/browser.js +15 -0
  12. package/lib/browser.js.map +7 -0
  13. package/lib/extension/avalanche.es.js +5535 -0
  14. package/lib/extension/avalanche.es.js.map +1 -0
  15. package/lib/extension/chippy-page-bridge.js +1953 -0
  16. package/lib/extension/inspect-react.js +216 -0
  17. package/lib/extension/layout-lab.js +1453 -0
  18. package/lib/extension/panel-dom.js +90 -0
  19. package/lib/extension/resolve-probes.js +342 -0
  20. package/lib/mcp-skill/LICENSE +201 -0
  21. package/lib/mcp-skill/README.md +148 -0
  22. package/lib/mcp-skill/SKILL.md +335 -0
  23. package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
  24. package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
  25. package/lib/mcp-skill/extension/armed-reload-startup.js +83 -0
  26. package/lib/mcp-skill/extension/avalanche.es.js +5535 -0
  27. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
  28. package/lib/mcp-skill/extension/avalanche.iife.js +5536 -0
  29. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
  30. package/lib/mcp-skill/extension/background.js +325 -0
  31. package/lib/mcp-skill/extension/chippy-page-bridge.js +1953 -0
  32. package/lib/mcp-skill/extension/content-script.js +190 -0
  33. package/lib/mcp-skill/extension/devtools-hook-shim.js +85 -0
  34. package/lib/mcp-skill/extension/early-bootstrap.js +49 -0
  35. package/lib/mcp-skill/extension/icon-128.png +0 -0
  36. package/lib/mcp-skill/extension/icon-16.png +0 -0
  37. package/lib/mcp-skill/extension/icon-32.png +0 -0
  38. package/lib/mcp-skill/extension/icon-48.png +0 -0
  39. package/lib/mcp-skill/extension/inspect-react.js +216 -0
  40. package/lib/mcp-skill/extension/layout-lab.js +1453 -0
  41. package/lib/mcp-skill/extension/logo.svg +14 -0
  42. package/lib/mcp-skill/extension/manifest.json +69 -0
  43. package/lib/mcp-skill/extension/panel-dom.js +90 -0
  44. package/lib/mcp-skill/extension/panel-settings.js +173 -0
  45. package/lib/mcp-skill/extension/popup.html +41 -0
  46. package/lib/mcp-skill/extension/popup.js +80 -0
  47. package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
  48. package/lib/mcp-skill/extension/resolve-probes.js +342 -0
  49. package/lib/mcp-skill/extension/startup-handoff.js +30 -0
  50. package/lib/mcp-skill/package.json +1 -0
  51. package/lib/mcp-skill/references/README.md +56 -0
  52. package/lib/mcp-skill/references/active-stimulus.md +85 -0
  53. package/lib/mcp-skill/references/avalanche-detector.md +731 -0
  54. package/lib/mcp-skill/references/bdd-router.md +310 -0
  55. package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
  56. package/lib/mcp-skill/references/command-output-formats.md +1085 -0
  57. package/lib/mcp-skill/references/command-surface.md +183 -0
  58. package/lib/mcp-skill/references/cynefin.md +99 -0
  59. package/lib/mcp-skill/references/debug-hook.md +161 -0
  60. package/lib/mcp-skill/references/fixing-performance.md +490 -0
  61. package/lib/mcp-skill/references/hook-metrics.md +199 -0
  62. package/lib/mcp-skill/references/hooks-inspection.md +61 -0
  63. package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
  64. package/lib/mcp-skill/references/mcp.md +122 -0
  65. package/lib/mcp-skill/references/mode-a-injection.md +102 -0
  66. package/lib/mcp-skill/references/operating-model.md +128 -0
  67. package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
  68. package/lib/mcp-skill/references/playwright-probes.md +205 -0
  69. package/lib/mcp-skill/references/procedures.md +545 -0
  70. package/lib/mcp-skill/references/recipes.txt +114 -0
  71. package/lib/mcp-skill/references/stability-render.md +428 -0
  72. package/lib/mcp-skill/references/test-mode-playbook.md +194 -0
  73. package/lib/mcp-skill/references/troubleshooting.md +59 -0
  74. package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
  75. package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
  76. package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
  77. package/lib/mcp-skill/scripts/chippy-helper-server.js +1768 -0
  78. package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
  79. package/lib/mcp-skill/scripts/chippy.sh +1252 -0
  80. package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
  81. package/lib/mcp-skill/scripts/helper-access.js +158 -0
  82. package/lib/mcp-skill/scripts/helper-proxy.js +87 -0
  83. package/lib/mcp-skill/scripts/page-socket-trust.js +81 -0
  84. package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
  85. package/lib/mcp-skill/scripts/search-recipes.js +248 -0
  86. package/lib/mcp-skill/snippets/README.md +89 -0
  87. package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
  88. package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
  89. package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
  90. package/lib/mcp-skill/snippets/find-prop.js +83 -0
  91. package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
  92. package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
  93. package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
  94. package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
  95. package/lib/package.json +1 -0
  96. package/lib/plugin.js +119 -0
  97. package/lib/plugin.js.map +7 -0
  98. package/lib/storybook/avalanche-panel-model.js +79 -0
  99. package/lib/storybook/brand.jsx +6 -0
  100. package/lib/storybook/connection-fallback.jsx +47 -0
  101. package/lib/storybook/constants.js +15 -0
  102. package/lib/storybook/context-card.jsx +171 -0
  103. package/lib/storybook/manager-owner.js +42 -0
  104. package/lib/storybook/manager.jsx +425 -0
  105. package/lib/storybook/preset.js +7 -0
  106. package/lib/storybook/preview.js +298 -0
  107. package/lib/storybook/probe-annotation-editor.jsx +52 -0
  108. package/lib/storybook/probe-intent.js +23 -0
  109. package/lib/storybook/setup.js +104 -0
  110. package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
  111. package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
  112. package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
  113. package/lib/types/avalanche/detector.d.ts +183 -0
  114. package/lib/types/avalanche/interaction-capture.d.ts +25 -0
  115. package/lib/types/avalanche/internals.d.ts +189 -0
  116. package/lib/types/avalanche/report-serializer.d.ts +283 -0
  117. package/lib/types/avalanche/reporter.d.ts +610 -0
  118. package/lib/types/chippy-bridge/fiber-source.d.ts +48 -0
  119. package/lib/types/chippy-bridge/react-renderer.d.ts +74 -0
  120. package/lib/types/chippy-bridge/recorded-source.d.ts +44 -0
  121. package/lib/types/chippy-bridge/source-map.d.ts +30 -0
  122. package/lib/types/debug-render/core.d.ts +78 -0
  123. package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
  124. package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
  125. package/lib/types/layout-lab/marker-context.d.ts +94 -0
  126. package/lib/types/package/avalanche.d.ts +6 -0
  127. package/lib/types/package/browser.d.ts +10 -0
  128. package/lib/types/package/plugin.d.ts +15 -0
  129. package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
  130. package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
  131. package/lib/types/plugins/index.d.ts +49 -0
  132. package/lib/types/plugins/jira/context-provider.d.ts +36 -0
  133. package/package.json +106 -4
@@ -0,0 +1,1252 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ BASE_URL="http://127.0.0.1:7823/__chippy"
5
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
6
+ HELPER_ENTRY="$SCRIPT_DIR/chippy-helper-server.js"
7
+ HELPER_TOKEN_FILE="${CHIPPY_STATE_DIR:-$HOME/.chippy}/helper-7823.token"
8
+ AUTH_HEADER=()
9
+ CURL_OPTS=(-fsS)
10
+
11
+ AUTH_HEADER_FILE=""
12
+
13
+ # The helper writes its access token after it starts listening. Reload it after
14
+ # starting the helper; CHIPPY_HELPER_TOKEN overrides the file. curl reads the
15
+ # header from a private file so the token does not appear in the process list.
16
+ load_helper_token() {
17
+ local token="${CHIPPY_HELPER_TOKEN:-}"
18
+ if [[ -z "$token" && -r "$HELPER_TOKEN_FILE" ]]; then
19
+ token=$(tr -d '[:space:]' <"$HELPER_TOKEN_FILE")
20
+ fi
21
+ AUTH_HEADER=()
22
+ if [[ -n "$token" ]]; then
23
+ if [[ -z "$AUTH_HEADER_FILE" ]]; then
24
+ AUTH_HEADER_FILE=$(mktemp "${TMPDIR:-/tmp}/chippy-auth.XXXXXX")
25
+ trap 'rm -f "$AUTH_HEADER_FILE"' EXIT
26
+ fi
27
+ printf 'X-Chippy-Token: %s\n' "$token" >"$AUTH_HEADER_FILE"
28
+ AUTH_HEADER=(-H "@$AUTH_HEADER_FILE")
29
+ fi
30
+ CURL_OPTS=(-fsS ${AUTH_HEADER[@]+"${AUTH_HEADER[@]}"})
31
+ }
32
+ load_helper_token
33
+
34
+ usage() {
35
+ cat <<'EOF'
36
+ Usage:
37
+ ./scripts/chippy.sh start [--host <address>]
38
+ ./scripts/chippy.sh stop
39
+ ./scripts/chippy.sh health
40
+ ./scripts/chippy.sh inject-url
41
+ ./scripts/chippy.sh recipes <query...> [--limit N]
42
+ ./scripts/chippy.sh pages [slice]
43
+ ./scripts/chippy.sh recent-pages
44
+ ./scripts/chippy.sh page <clientId> [slice]
45
+ ./scripts/chippy.sh snapshot <clientId> [slice]
46
+ ./scripts/chippy.sh request-snapshot <clientId>
47
+ ./scripts/chippy.sh wait <clientId> [timeoutSeconds]
48
+ ./scripts/chippy.sh activate <clientId>
49
+ ./scripts/chippy.sh deactivate <clientId>
50
+ ./scripts/chippy.sh context-ui <clientId> <on|off>
51
+ ./scripts/chippy.sh ensure-probes <clientId>
52
+ ./scripts/chippy.sh clear-probes <clientId>
53
+ ./scripts/chippy.sh show-status <clientId> <text>
54
+ ./scripts/chippy.sh hide-status <clientId>
55
+ ./scripts/chippy.sh command <type> [clientId]
56
+ ./scripts/chippy.sh get-computed-style <clientId> <probeId> [parentLevel] [selector]
57
+ ./scripts/chippy.sh probe-dom <clientId> <probeId> [parentLevel]
58
+ ./scripts/chippy.sh probe-parents <clientId> <probeId> [max]
59
+ ./scripts/chippy.sh probe-boundaries <clientId> <probeId> [max]
60
+ ./scripts/chippy.sh probe-owners <clientId> <probeId>
61
+ ./scripts/chippy.sh probe-owner <clientId> <probeId> <ownerIndex>
62
+ ./scripts/chippy.sh probe-props <clientId> <probeId> [ownerIndex]
63
+ ./scripts/chippy.sh probe-prop-value <clientId> <probeId> <propName> [ownerIndex]
64
+ ./scripts/chippy.sh probe-hooks <clientId> <probeId>
65
+ ./scripts/chippy.sh probe-hooks-summary <clientId> <probeId>
66
+ ./scripts/chippy.sh page-click <clientId> <selector|probeId|fiberId>
67
+ ./scripts/chippy.sh fiber-prop-contract <clientId> <fiberId> <propName>
68
+ ./scripts/chippy.sh fiber-invoke <clientId> <fiberId> <propName> [--args JSON]
69
+ ./scripts/chippy.sh fiber-schedule-update <clientId> <fiberId> [--ownerIndex N]
70
+ ./scripts/chippy.sh find-fibers <clientId> [--name regex] [--file regex] [--prop name] [--testid regex] [--text regex] [--max N]
71
+ ./scripts/chippy.sh fiber-state-capture <clientId> [label] [maxFibers]
72
+ ./scripts/chippy.sh fiber-state-list
73
+ ./scripts/chippy.sh fiber-state-compare <beforeCaptureId> <afterCaptureId> [maxChanges]
74
+ ./scripts/chippy.sh page-boundaries <clientId> [max]
75
+ ./scripts/chippy.sh inspect-fiber-hooks <clientId> <fiberId>
76
+ ./scripts/chippy.sh inspect-fiber-hooks-summary <clientId> <fiberId>
77
+ ./scripts/chippy.sh inspect-fiber-props <clientId> <fiberId>
78
+ ./scripts/chippy.sh inspect-fiber-owners <clientId> <fiberId>
79
+ ./scripts/chippy.sh inspect-fiber-parents <clientId> <fiberId> [max]
80
+ ./scripts/chippy.sh inspect-fiber-boundaries <clientId> <fiberId> [max]
81
+ ./scripts/chippy.sh inspect-fiber-structure <clientId> <fiberId> <hookIndex> [windowSize]
82
+ ./scripts/chippy.sh describe-component <clientId> <fiberIdOrName> [--text|--json]
83
+ ./scripts/chippy.sh explain-component <clientId> <probeId|fiberId|name> [--text|--json|--complexity]
84
+ ./scripts/chippy.sh avalanche-clear <clientId>
85
+ ./scripts/chippy.sh avalanche-status <clientId>
86
+ ./scripts/chippy.sh avalanche-export <clientId> [lastN]
87
+ ./scripts/chippy.sh avalanche-hot-sources <clientId> [limit]
88
+ ./scripts/chippy.sh avalanche-commit-list <clientId> [top] [by]
89
+ ./scripts/chippy.sh avalanche-commit-peek <clientId> <commitId> [include] [depth] [maxItems]
90
+ ./scripts/chippy.sh avalanche-describe-action <clientId> <actionId>
91
+ ./scripts/chippy.sh avalanche-install <clientId> [detectionMode]
92
+ ./scripts/chippy.sh avalanche-recapture <clientId> [detectionMode]
93
+ ./scripts/chippy.sh avalanche-debug-hook <clientId> <sourceId> <hookIndex> [window]
94
+ ./scripts/chippy.sh avalanche-save <clientId> <file> [count]
95
+ ./scripts/chippy.sh avalanche-offline <file> <summary|commit-list|commit-peek|hot-sources|describe-action|source-search> [args...]
96
+ ./scripts/chippy.sh eval-page <clientId> <snippet-file-or-name> [--key value ...]
97
+ ./scripts/chippy.sh eval-snippets
98
+ ./scripts/chippy.sh avalanche-arm <clientId> <origin> [maxCommits] [captureFibers=true|false] [detectionMode=sources|full|stats|amplifiers]
99
+ ./scripts/chippy.sh avalanche-disarm <clientId> <origin>
100
+ ./scripts/chippy.sh page-reload <clientId>
101
+ ./scripts/chippy.sh page-wait-reload <clientId> [timeoutMs]
102
+ ./scripts/chippy.sh reload-with-avalanche <clientId> <origin> [maxCommits] [timeoutMs] [settleMs] [captureFibers=true|false] [detectionMode=sources|full|stats|amplifiers]
103
+ ./scripts/chippy.sh handoff-to-playwright <clientId> [url] [probeIdsCsv] [browserName] [headless]
104
+ ./scripts/chippy.sh proxy-state
105
+ ./scripts/chippy.sh proxy-start <targetPort> [originalUrl]
106
+ ./scripts/chippy.sh proxy-stop
107
+
108
+ Notes:
109
+ - Base URL is fixed to http://127.0.0.1:7823/__chippy.
110
+ - recipes searches the local task corpus and does not contact the helper.
111
+ - slice is one of: minimal, standard, full.
112
+ - wait performs one cancellable long poll; it does not install or modify host hooks.
113
+ - Prefer the explicit subcommands over the generic 'command' form.
114
+ EOF
115
+ }
116
+
117
+ post_avalanche() {
118
+ local client_id="$1"
119
+ local body="$2"
120
+ post_json "/api/pages/$client_id/avalanche" "$body"
121
+ }
122
+
123
+ json_escape() {
124
+ node -e 'process.stdout.write(JSON.stringify(process.argv[1]))' "$1"
125
+ }
126
+
127
+ helper_is_up() {
128
+ curl "${CURL_OPTS[@]}" "$BASE_URL/health" >/dev/null 2>&1
129
+ }
130
+
131
+ # Binds 127.0.0.1 unless --host or CHIPPY_HELPER_HOST asks for a wider address.
132
+ start_helper() {
133
+ local bind_host="${CHIPPY_HELPER_HOST:-127.0.0.1}"
134
+ if [[ "${1:-}" == "--host" ]]; then
135
+ bind_host="${2:?--host needs an address, for example 0.0.0.0}"
136
+ fi
137
+ if helper_is_up; then
138
+ echo "Chippy helper already up at $BASE_URL; stop it first to change the bind address" >&2
139
+ curl "${CURL_OPTS[@]}" "$BASE_URL/health"
140
+ return 0
141
+ fi
142
+
143
+ mkdir -p .agents
144
+ nohup node "$HELPER_ENTRY" --host "$bind_host" --port 7823 >/dev/null 2>&1 &
145
+ local pid=$!
146
+
147
+ for _ in $(seq 1 40); do
148
+ if helper_is_up; then
149
+ load_helper_token
150
+ echo "Chippy helper started (pid $pid)" >&2
151
+ curl "${CURL_OPTS[@]}" "$BASE_URL/health"
152
+ return 0
153
+ fi
154
+ sleep 0.25
155
+ done
156
+
157
+ echo "Failed to start Chippy helper." >&2
158
+ return 1
159
+ }
160
+
161
+ stop_helper() {
162
+ local helper_pattern="chippy-helper-server.js .*--port 7823"
163
+ if ! pgrep -f "$helper_pattern" >/dev/null 2>&1; then
164
+ echo "Chippy helper is not running" >&2
165
+ return 0
166
+ fi
167
+
168
+ pkill -f "$helper_pattern" >/dev/null 2>&1 || true
169
+
170
+ for _ in $(seq 1 20); do
171
+ if ! helper_is_up; then
172
+ echo "Chippy helper stopped" >&2
173
+ return 0
174
+ fi
175
+ sleep 0.25
176
+ done
177
+
178
+ echo "Timed out waiting for Chippy helper to stop" >&2
179
+ return 1
180
+ }
181
+
182
+ # The helper explains failures in its JSON body (`error`, `hint`); show them
183
+ # instead of leaving only the HTTP status.
184
+ print_helper_error_body() {
185
+ [[ -s "$1" ]] || return 0
186
+ node -e '
187
+ const fs = require("fs");
188
+ const raw = fs.readFileSync(process.argv[1], "utf8");
189
+ let body;
190
+ try { body = JSON.parse(raw); } catch { process.stdout.write(` helper: ${raw.trim().slice(0, 500)}\n`); process.exit(0); }
191
+ for (const key of ["error", "hint", "next"]) {
192
+ const value = body && body[key];
193
+ if (value) process.stdout.write(` ${key}: ${typeof value === "string" ? value : JSON.stringify(value)}\n`);
194
+ }
195
+ ' "$1" 2>/dev/null || true
196
+ }
197
+
198
+ run_curl() {
199
+ local method="$1"
200
+ local url="$2"
201
+ local body="${3:-}"
202
+ local response_file stderr_file http_code code stderr_text
203
+ response_file=$(mktemp)
204
+ stderr_file=$(mktemp)
205
+ set +e
206
+ if [[ "$method" == "POST" ]]; then
207
+ http_code=$(curl -sS -o "$response_file" -w '%{http_code}' ${AUTH_HEADER[@]+"${AUTH_HEADER[@]}"} -X POST "$url" -H 'Content-Type: application/json' -d "$body" 2>"$stderr_file")
208
+ else
209
+ http_code=$(curl -sS -o "$response_file" -w '%{http_code}' ${AUTH_HEADER[@]+"${AUTH_HEADER[@]}"} "$url" 2>"$stderr_file")
210
+ fi
211
+ code=$?
212
+ set -e
213
+ stderr_text=$(cat "$stderr_file" 2>/dev/null || true)
214
+
215
+ if [[ $code -eq 0 && "$http_code" -lt 400 ]]; then
216
+ cat "$response_file"
217
+ rm -f "$response_file" "$stderr_file"
218
+ return 0
219
+ fi
220
+
221
+ case "$code" in
222
+ 7)
223
+ echo "Chippy helper request failed." >&2
224
+ echo " observed: curl_exit=7 request='$method $url'" >&2
225
+ echo " meaning: could not connect to helper at $BASE_URL" >&2
226
+ echo " next: start helper with ./.agents/skills/chippy/scripts/chippy.sh start" >&2
227
+ ;;
228
+ 28)
229
+ echo "Chippy helper request failed." >&2
230
+ echo " observed: curl_exit=28 request='$method $url'" >&2
231
+ echo " meaning: HTTP request itself timed out before a response completed" >&2
232
+ echo " next: retry, then check helper and page responsiveness with pages / request-snapshot" >&2
233
+ ;;
234
+ *)
235
+ if [[ "$http_code" == "504" ]]; then
236
+ echo "Chippy helper request failed." >&2
237
+ echo " observed: http_status=504 request='$method $url'" >&2
238
+ echo " meaning: helper answered the HTTP request, but timed out while waiting for an upstream result" >&2
239
+ echo " upstream in this setup is usually the connected page/runtime" >&2
240
+ echo " next: verify lighter round-trips first (pages, request-snapshot, avalanche-status) before retrying this command" >&2
241
+ elif [[ "$http_code" =~ ^5 ]]; then
242
+ echo "Chippy helper request failed." >&2
243
+ echo " observed: http_status=$http_code request='$method $url'" >&2
244
+ echo " meaning: helper returned a server error" >&2
245
+ elif [[ "$http_code" == "401" ]]; then
246
+ echo "Chippy helper request failed." >&2
247
+ echo " observed: http_status=401 request='$method $url'" >&2
248
+ echo " meaning: helper requires its access token and none matched" >&2
249
+ echo " next: the running helper writes its token to $HELPER_TOKEN_FILE; restart it with stop/start if that file is missing or stale, or set CHIPPY_HELPER_TOKEN" >&2
250
+ elif [[ "$http_code" =~ ^4 ]]; then
251
+ echo "Chippy helper request failed." >&2
252
+ echo " observed: http_status=$http_code request='$method $url'" >&2
253
+ echo " meaning: helper rejected the request" >&2
254
+ else
255
+ echo "Chippy helper request failed." >&2
256
+ echo " observed: curl_exit=$code request='$method $url'" >&2
257
+ echo " meaning: curl failed before a successful helper response was received" >&2
258
+ fi
259
+ ;;
260
+ esac
261
+ if [[ -n "$stderr_text" ]]; then
262
+ echo " → curl: $stderr_text" >&2
263
+ fi
264
+ print_helper_error_body "$response_file" >&2
265
+ rm -f "$response_file" "$stderr_file"
266
+ if [[ "$http_code" =~ ^[0-9]+$ && "$http_code" -ge 400 ]]; then
267
+ return 22
268
+ fi
269
+ return "$code"
270
+ }
271
+
272
+ require_detection_mode() {
273
+ case "$2" in
274
+ sources|full|stats|amplifiers) ;;
275
+ *)
276
+ echo "$1 detectionMode must be sources, full, stats, or amplifiers (got '$2')" >&2
277
+ exit 1
278
+ ;;
279
+ esac
280
+ }
281
+
282
+ post_json() {
283
+ local path="$1"
284
+ local body="$2"
285
+ run_curl POST "$BASE_URL$path" "$body"
286
+ }
287
+
288
+ print_probe_result_text() {
289
+ node "$SCRIPT_DIR/format-probe-result.js" "$1"
290
+ }
291
+
292
+ get_json() {
293
+ local path="$1"
294
+ run_curl GET "$BASE_URL$path"
295
+ }
296
+
297
+ send_command() {
298
+ local type="$1"
299
+ local client_id="${2:-}"
300
+ local extra_json="${3:-}"
301
+ local body
302
+
303
+ body="{"
304
+ if [[ -n "$client_id" ]]; then
305
+ body+="\"clientId\":$(json_escape "$client_id"),"
306
+ fi
307
+ body+="\"command\":{\"type\":$(json_escape "$type")"
308
+
309
+ if [[ -n "$extra_json" ]]; then
310
+ body+="${extra_json}"
311
+ fi
312
+
313
+ body+="}}"
314
+ post_json "/api/command" "$body"
315
+ }
316
+
317
+ cmd="${1:-}"
318
+ case "$cmd" in
319
+ ""|-h|--help|help)
320
+ usage
321
+ ;;
322
+ start)
323
+ start_helper "${@:2}"
324
+ ;;
325
+ stop)
326
+ stop_helper
327
+ ;;
328
+ health)
329
+ curl "${CURL_OPTS[@]}" "$BASE_URL/health"
330
+ ;;
331
+ inject-url)
332
+ echo "Import integration is document-scoped: a full reload/navigation removes Chippy and requires the user or automation to inject it again." >&2
333
+ echo "The classic inject.js installs Chippy only; load $BASE_URL/avalanche.iife.js after it for bridge and Avalanche commands, or import $BASE_URL/inject.mjs instead." >&2
334
+ printf '%s/inject.js\n' "$BASE_URL"
335
+ ;;
336
+ recipes)
337
+ shift
338
+ node "$SCRIPT_DIR/search-recipes.js" "$SCRIPT_DIR/../references/recipes.txt" "$@"
339
+ ;;
340
+ pages)
341
+ slice="${2:-minimal}"
342
+ curl "${CURL_OPTS[@]}" "$BASE_URL/api/pages?slice=$slice"
343
+ ;;
344
+ recent-pages)
345
+ curl "${CURL_OPTS[@]}" "$BASE_URL/api/recent-pages"
346
+ ;;
347
+ page)
348
+ client_id="${2:-}"
349
+ slice="${3:-standard}"
350
+ if [[ -z "$client_id" ]]; then
351
+ echo "page requires <clientId>" >&2
352
+ exit 1
353
+ fi
354
+ curl "${CURL_OPTS[@]}" "$BASE_URL/api/pages/$client_id?slice=$slice"
355
+ ;;
356
+ snapshot)
357
+ client_id="${2:-}"
358
+ slice="${3:-standard}"
359
+ if [[ -z "$client_id" ]]; then
360
+ echo "snapshot requires <clientId> [slice]" >&2
361
+ exit 1
362
+ fi
363
+ get_json "/api/pages/$client_id/snapshot?slice=$slice"
364
+ ;;
365
+ request-snapshot)
366
+ client_id="${2:-}"
367
+ if [[ -z "$client_id" ]]; then
368
+ echo "request-snapshot requires <clientId>" >&2
369
+ exit 1
370
+ fi
371
+ post_json "/api/pages/$client_id/snapshot" '{}'
372
+ ;;
373
+ wait)
374
+ client_id="${2:-}"
375
+ timeout_seconds="${3:-300}"
376
+ if [[ -z "$client_id" ]]; then
377
+ echo "wait requires <clientId> [timeoutSeconds]" >&2
378
+ exit 1
379
+ fi
380
+ if [[ ! "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || (( timeout_seconds > 1800 )); then
381
+ echo "timeoutSeconds must be an integer from 1 through 1800" >&2
382
+ exit 1
383
+ fi
384
+ get_json "/api/pages/$client_id/probe-annotations/wait?timeoutMs=$((timeout_seconds * 1000))"
385
+ ;;
386
+ activate)
387
+ client_id="${2:-}"
388
+ if [[ -z "$client_id" ]]; then
389
+ echo "activate requires <clientId>" >&2
390
+ exit 1
391
+ fi
392
+ send_command "activate" "$client_id"
393
+ ;;
394
+ deactivate)
395
+ client_id="${2:-}"
396
+ if [[ -z "$client_id" ]]; then
397
+ echo "deactivate requires <clientId>" >&2
398
+ exit 1
399
+ fi
400
+ send_command "deactivate" "$client_id"
401
+ ;;
402
+ context-ui)
403
+ command_name="$1"
404
+ client_id="${2:-}"
405
+ mode="${3:-}"
406
+ if [[ -z "$client_id" || -z "$mode" ]]; then
407
+ echo "$command_name requires <clientId> <on|off>" >&2
408
+ exit 1
409
+ fi
410
+ case "$mode" in
411
+ on|true|1) enabled=true ;;
412
+ off|false|0) enabled=false ;;
413
+ *)
414
+ echo "$command_name mode must be on or off" >&2
415
+ exit 1
416
+ ;;
417
+ esac
418
+ send_command "set-context-ui" "$client_id" ",\"enabled\":$enabled"
419
+ ;;
420
+ ensure-probes)
421
+ client_id="${2:-}"
422
+ if [[ -z "$client_id" ]]; then
423
+ echo "ensure-probes requires <clientId>" >&2
424
+ exit 1
425
+ fi
426
+ send_command "ensure-probes" "$client_id"
427
+ ;;
428
+ clear-probes)
429
+ client_id="${2:-}"
430
+ if [[ -z "$client_id" ]]; then
431
+ echo "clear-probes requires <clientId>" >&2
432
+ exit 1
433
+ fi
434
+ send_command "clear-probes" "$client_id"
435
+ ;;
436
+ show-status)
437
+ client_id="${2:-}"
438
+ text="${3:-}"
439
+ if [[ -z "$client_id" || -z "$text" ]]; then
440
+ echo "show-status requires <clientId> <text>" >&2
441
+ exit 1
442
+ fi
443
+ send_command "show-status" "$client_id" ",\"text\":$(json_escape "$text")"
444
+ ;;
445
+ hide-status)
446
+ client_id="${2:-}"
447
+ if [[ -z "$client_id" ]]; then
448
+ echo "hide-status requires <clientId>" >&2
449
+ exit 1
450
+ fi
451
+ send_command "hide-status" "$client_id"
452
+ ;;
453
+ command)
454
+ type="${2:-}"
455
+ client_id="${3:-}"
456
+ if [[ -z "$type" ]]; then
457
+ echo "command requires <type> [clientId]" >&2
458
+ exit 1
459
+ fi
460
+ send_command "$type" "$client_id"
461
+ ;;
462
+ get-computed-style)
463
+ client_id="${2:-}"
464
+ probe_id="${3:-}"
465
+ parent_level="${4:-0}"
466
+ selector="${5:-}"
467
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
468
+ echo "get-computed-style requires <clientId> <probeId> [parentLevel] [selector]" >&2
469
+ exit 1
470
+ fi
471
+ if [[ -n "$selector" ]]; then
472
+ body="{\"probeId\":$(json_escape "$probe_id"),\"parentLevel\":$parent_level,\"selector\":$(json_escape "$selector")}"
473
+ else
474
+ body="{\"probeId\":$(json_escape "$probe_id"),\"parentLevel\":$parent_level}"
475
+ fi
476
+ post_json "/api/pages/$client_id/computed-style" "$body"
477
+ ;;
478
+ probe-dom)
479
+ # Usage: probe-dom <clientId> <probeId> [parentLevel]
480
+ client_id="${2:-}"
481
+ probe_id="${3:-}"
482
+ parent_level="${4:-0}"
483
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
484
+ echo "probe-dom requires <clientId> <probeId> [parentLevel]" >&2
485
+ exit 1
486
+ fi
487
+ body="{\"command\":\"eval-probe-dom\",\"probeId\":$(json_escape "$probe_id"),\"parentLevel\":$parent_level}"
488
+ post_json "/api/pages/$client_id/eval-probe" "$body"
489
+ ;;
490
+ probe-parents)
491
+ client_id="${2:-}"
492
+ probe_id="${3:-}"
493
+ max="${4:-50}"
494
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
495
+ echo "probe-parents requires <clientId> <probeId> [max]" >&2
496
+ exit 1
497
+ fi
498
+ if [[ ! "$max" =~ ^[0-9]+$ ]]; then
499
+ echo "max must be a non-negative integer" >&2
500
+ exit 1
501
+ fi
502
+ body="{\"command\":\"eval-probe-parents\",\"probeId\":$(json_escape "$probe_id"),\"max\":$max}"
503
+ post_json "/api/pages/$client_id/eval-probe" "$body"
504
+ ;;
505
+ probe-boundaries)
506
+ client_id="${2:-}"
507
+ probe_id="${3:-}"
508
+ max="${4:-100}"
509
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
510
+ echo "probe-boundaries requires <clientId> <probeId> [max]" >&2
511
+ exit 1
512
+ fi
513
+ if [[ ! "$max" =~ ^[1-9][0-9]*$ ]]; then
514
+ echo "max must be a positive integer" >&2
515
+ exit 1
516
+ fi
517
+ body="{\"command\":\"eval-probe-boundaries\",\"probeId\":$(json_escape "$probe_id"),\"max\":$max}"
518
+ post_json "/api/pages/$client_id/eval-probe" "$body"
519
+ ;;
520
+ probe-owners)
521
+ client_id="${2:-}"
522
+ probe_id="${3:-}"
523
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
524
+ echo "probe-owners requires <clientId> <probeId>" >&2
525
+ exit 1
526
+ fi
527
+ get_json "/api/pages/$client_id/probe/$probe_id/owners"
528
+ ;;
529
+ probe-owner)
530
+ client_id="${2:-}"
531
+ probe_id="${3:-}"
532
+ owner_index="${4:-}"
533
+ if [[ -z "$client_id" || -z "$probe_id" || -z "$owner_index" ]]; then
534
+ echo "probe-owner requires <clientId> <probeId> <ownerIndex>" >&2
535
+ exit 1
536
+ fi
537
+ get_json "/api/pages/$client_id/probe/$probe_id/owners/$owner_index"
538
+ ;;
539
+ probe-props)
540
+ # Usage: probe-props <clientId> <probeId> [ownerIndex]
541
+ # ownerIndex: 0 = immediate owner, 1 = next owner up, etc.
542
+ client_id="${2:-}"
543
+ probe_id="${3:-}"
544
+ owner_index="${4:-}"
545
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
546
+ echo "probe-props requires <clientId> <probeId> [ownerIndex]" >&2
547
+ exit 1
548
+ fi
549
+ if [[ -n "$owner_index" && ! "$owner_index" =~ ^-?[0-9]+$ ]]; then
550
+ echo "ownerIndex must be an integer" >&2
551
+ exit 1
552
+ fi
553
+ if [[ -n "$owner_index" ]]; then
554
+ body="{\"command\":\"eval-probe-props\",\"probeId\":$(json_escape "$probe_id"),\"ownerIndex\":$owner_index}"
555
+ else
556
+ body="{\"command\":\"eval-probe-props\",\"probeId\":$(json_escape "$probe_id")}"
557
+ fi
558
+ post_json "/api/pages/$client_id/eval-probe" "$body"
559
+ ;;
560
+ probe-prop-value)
561
+ # Usage: probe-prop-value <clientId> <probeId> <propName> [ownerIndex]
562
+ # ownerIndex: 0 = immediate owner, 1 = next owner up, etc.
563
+ client_id="${2:-}"
564
+ probe_id="${3:-}"
565
+ prop_name="${4:-}"
566
+ owner_index="${5:-}"
567
+ if [[ -z "$client_id" || -z "$probe_id" || -z "$prop_name" ]]; then
568
+ echo "probe-prop-value requires <clientId> <probeId> <propName> [ownerIndex]" >&2
569
+ exit 1
570
+ fi
571
+ if [[ -n "$owner_index" && ! "$owner_index" =~ ^-?[0-9]+$ ]]; then
572
+ echo "ownerIndex must be an integer" >&2
573
+ exit 1
574
+ fi
575
+ if [[ -n "$owner_index" ]]; then
576
+ body="{\"command\":\"eval-prop-value\",\"probeId\":$(json_escape "$probe_id"),\"prop\":$(json_escape "$prop_name"),\"ownerIndex\":$owner_index}"
577
+ else
578
+ body="{\"command\":\"eval-prop-value\",\"probeId\":$(json_escape "$probe_id"),\"prop\":$(json_escape "$prop_name")}"
579
+ fi
580
+ post_json "/api/pages/$client_id/eval-probe" "$body"
581
+ ;;
582
+ probe-hooks)
583
+ client_id="${2:-}"
584
+ probe_id="${3:-}"
585
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
586
+ echo "probe-hooks requires <clientId> <probeId>" >&2
587
+ exit 1
588
+ fi
589
+ body="{\"command\":\"eval-probe-hooks\",\"probeId\":$(json_escape "$probe_id")}"
590
+ post_json "/api/pages/$client_id/eval-probe" "$body"
591
+ ;;
592
+ probe-hooks-summary)
593
+ client_id="${2:-}"
594
+ probe_id="${3:-}"
595
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
596
+ echo "probe-hooks-summary requires <clientId> <probeId>" >&2
597
+ exit 1
598
+ fi
599
+ body="{\"command\":\"eval-probe-hooks-summary\",\"probeId\":$(json_escape "$probe_id")}"
600
+ post_json "/api/pages/$client_id/eval-probe" "$body"
601
+ ;;
602
+ page-click)
603
+ client_id="${2:-}"
604
+ target_raw="${3:-}"
605
+ shift 3 || true
606
+ if [[ -z "$client_id" || -z "$target_raw" ]]; then
607
+ echo "page-click requires <clientId> <selector|probeId|fiberId>" >&2
608
+ exit 1
609
+ fi
610
+ if [[ $# -gt 0 ]]; then
611
+ echo "page-click does not support extra options; selector must resolve to exactly one target" >&2
612
+ exit 1
613
+ fi
614
+ if [[ "$target_raw" =~ ^[Pp]?[123]$ ]]; then
615
+ probe_id="${target_raw#[Pp]}"
616
+ body='{"probeId":'"$(json_escape "$probe_id")"'}'
617
+ elif [[ "$target_raw" =~ ^fiber:[0-9]+$ ]]; then
618
+ body='{"fiberId":'"${target_raw#fiber:}"'}'
619
+ elif [[ "$target_raw" =~ ^[0-9]+$ ]]; then
620
+ body='{"fiberId":'"$target_raw"'}'
621
+ else
622
+ body='{"selector":'"$(json_escape "$target_raw")"'}'
623
+ fi
624
+ post_json "/api/pages/$client_id/page-click" "$body"
625
+ ;;
626
+ fiber-prop-contract)
627
+ client_id="${2:-}"
628
+ fiber_id="${3:-}"
629
+ fiber_id="${fiber_id#fiber:}"
630
+ prop_name="${4:-}"
631
+ shift 4 || true
632
+ if [[ -z "$client_id" || -z "$fiber_id" || -z "$prop_name" ]]; then
633
+ echo "fiber-prop-contract requires <clientId> <fiberId> <propName>" >&2
634
+ exit 1
635
+ fi
636
+ if [[ $# -gt 0 ]]; then
637
+ echo "fiber-prop-contract does not support extra options" >&2
638
+ exit 1
639
+ fi
640
+ post_json "/api/pages/$client_id/fiber-prop-contract" '{"fiberId":'"$fiber_id"',"propName":'"$(json_escape "$prop_name")"'}'
641
+ ;;
642
+ fiber-invoke)
643
+ client_id="${2:-}"
644
+ fiber_id="${3:-}"
645
+ fiber_id="${fiber_id#fiber:}"
646
+ prop_name="${4:-}"
647
+ shift 4 || true
648
+ if [[ -z "$client_id" || -z "$fiber_id" || -z "$prop_name" ]]; then
649
+ echo "fiber-invoke requires <clientId> <fiberId> <propName> [--args JSON]" >&2
650
+ exit 1
651
+ fi
652
+ args='[]'
653
+ while [[ $# -gt 0 ]]; do
654
+ case "$1" in
655
+ --args)
656
+ if [[ $# -lt 2 ]]; then
657
+ echo "missing value for --args" >&2
658
+ exit 1
659
+ fi
660
+ args="$2"
661
+ shift
662
+ ;;
663
+ *) echo "unknown option for fiber-invoke: $1" >&2; exit 1 ;;
664
+ esac
665
+ shift
666
+ done
667
+ post_json "/api/pages/$client_id/fiber-invoke" '{"fiberId":'"$fiber_id"',"propName":'"$(json_escape "$prop_name")"',"args":'"$args"'}'
668
+ ;;
669
+ fiber-schedule-update)
670
+ client_id="${2:-}"
671
+ fiber_id="${3:-}"
672
+ fiber_id="${fiber_id#fiber:}"
673
+ shift 3 || true
674
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
675
+ echo "fiber-schedule-update requires <clientId> <fiberId> [--ownerIndex N]" >&2
676
+ exit 1
677
+ fi
678
+ owner_index=0
679
+ while [[ $# -gt 0 ]]; do
680
+ case "$1" in
681
+ --ownerIndex)
682
+ if [[ $# -lt 2 ]]; then
683
+ echo "missing value for --ownerIndex" >&2
684
+ exit 1
685
+ fi
686
+ owner_index="$2"
687
+ shift
688
+ ;;
689
+ *) echo "unknown option for fiber-schedule-update: $1" >&2; exit 1 ;;
690
+ esac
691
+ shift
692
+ done
693
+ post_json "/api/pages/$client_id/fiber-schedule-update" '{"fiberId":'"$(json_escape "$fiber_id")"',"ownerIndex":'"$owner_index"'}'
694
+ ;;
695
+ find-fibers)
696
+ client_id="${2:-}"
697
+ shift 2 || true
698
+ if [[ -z "$client_id" ]]; then
699
+ echo "find-fibers requires <clientId> [--name regex] [--file regex] [--prop name] [--testid regex] [--text regex] [--max N]" >&2
700
+ exit 1
701
+ fi
702
+ filters='{}'
703
+ while [[ $# -gt 0 ]]; do
704
+ if [[ "$1" == --* ]]; then
705
+ k="${1#--}"; v="${2:-}"; shift 2
706
+ filters=$(node -e 'const o=JSON.parse(process.argv[1]);o[process.argv[2]]=process.argv[3];process.stdout.write(JSON.stringify(o));' "$filters" "$k" "$v") || exit 1
707
+ else shift; fi
708
+ done
709
+ body=$(node -e 'process.stdout.write(JSON.stringify({command:"find-fibers",filters:JSON.parse(process.argv[1])}))' "$filters") || exit 1
710
+ post_json "/api/pages/$client_id/eval-probe" "$body"
711
+ ;;
712
+ fiber-state-capture)
713
+ client_id="${2:-}"
714
+ label="${3:-}"
715
+ max_fibers="${4:-100000}"
716
+ if [[ -z "$client_id" ]]; then
717
+ echo "fiber-state-capture requires <clientId> [label] [maxFibers]" >&2
718
+ exit 1
719
+ fi
720
+ if [[ ! "$max_fibers" =~ ^[1-9][0-9]*$ ]]; then
721
+ echo "maxFibers must be a positive integer" >&2
722
+ exit 1
723
+ fi
724
+ body="{\"label\":$(json_escape "$label"),\"maxFibers\":$max_fibers}"
725
+ post_json "/api/pages/$client_id/fiber-state-captures" "$body"
726
+ ;;
727
+ fiber-state-list)
728
+ get_json "/api/fiber-state-captures"
729
+ ;;
730
+ fiber-state-compare)
731
+ before_capture_id="${2:-}"
732
+ after_capture_id="${3:-}"
733
+ max_changes="${4:-200}"
734
+ if [[ -z "$before_capture_id" || -z "$after_capture_id" ]]; then
735
+ echo "fiber-state-compare requires <beforeCaptureId> <afterCaptureId> [maxChanges]" >&2
736
+ exit 1
737
+ fi
738
+ if [[ ! "$max_changes" =~ ^[1-9][0-9]*$ ]]; then
739
+ echo "maxChanges must be a positive integer" >&2
740
+ exit 1
741
+ fi
742
+ body="{\"before\":$(json_escape "$before_capture_id"),\"after\":$(json_escape "$after_capture_id"),\"maxChanges\":$max_changes}"
743
+ post_json "/api/fiber-state-comparisons" "$body"
744
+ ;;
745
+ page-boundaries)
746
+ client_id="${2:-}"
747
+ max="${3:-1000}"
748
+ if [[ -z "$client_id" ]]; then
749
+ echo "page-boundaries requires <clientId> [max]" >&2
750
+ exit 1
751
+ fi
752
+ if [[ ! "$max" =~ ^[1-9][0-9]*$ ]]; then
753
+ echo "max must be a positive integer" >&2
754
+ exit 1
755
+ fi
756
+ body="{\"command\":\"page-boundaries\",\"max\":$max}"
757
+ post_json "/api/pages/$client_id/eval-probe" "$body"
758
+ ;;
759
+ inspect-fiber-hooks)
760
+ client_id="${2:-}"
761
+ fiber_id="${3:-}"
762
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
763
+ echo "inspect-fiber-hooks requires <clientId> <fiberId>" >&2
764
+ exit 1
765
+ fi
766
+ body="{\"command\":\"inspect-fiber-hooks\",\"fiberId\":$(json_escape "$fiber_id")}"
767
+ post_json "/api/pages/$client_id/eval-probe" "$body"
768
+ ;;
769
+ inspect-fiber-hooks-summary)
770
+ client_id="${2:-}"
771
+ fiber_id="${3:-}"
772
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
773
+ echo "inspect-fiber-hooks-summary requires <clientId> <fiberId>" >&2
774
+ exit 1
775
+ fi
776
+ body="{\"command\":\"inspect-fiber-hooks-summary\",\"fiberId\":$(json_escape "$fiber_id")}"
777
+ post_json "/api/pages/$client_id/eval-probe" "$body"
778
+ ;;
779
+ inspect-fiber-props)
780
+ client_id="${2:-}"
781
+ fiber_id="${3:-}"
782
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
783
+ echo "inspect-fiber-props requires <clientId> <fiberId>" >&2
784
+ exit 1
785
+ fi
786
+ body="{\"command\":\"inspect-fiber-props\",\"fiberId\":$(json_escape "$fiber_id")}"
787
+ post_json "/api/pages/$client_id/eval-probe" "$body"
788
+ ;;
789
+ inspect-fiber-owners)
790
+ client_id="${2:-}"
791
+ fiber_id="${3:-}"
792
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
793
+ echo "inspect-fiber-owners requires <clientId> <fiberId>" >&2
794
+ exit 1
795
+ fi
796
+ body="{\"command\":\"inspect-fiber-owners\",\"fiberId\":$(json_escape "$fiber_id")}"
797
+ post_json "/api/pages/$client_id/eval-probe" "$body"
798
+ ;;
799
+ inspect-fiber-parents)
800
+ client_id="${2:-}"
801
+ fiber_id="${3:-}"
802
+ max="${4:-50}"
803
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
804
+ echo "inspect-fiber-parents requires <clientId> <fiberId> [max]" >&2
805
+ exit 1
806
+ fi
807
+ if [[ ! "$max" =~ ^[0-9]+$ ]]; then
808
+ echo "max must be a non-negative integer" >&2
809
+ exit 1
810
+ fi
811
+ body="{\"command\":\"inspect-fiber-parents\",\"fiberId\":$(json_escape "$fiber_id"),\"max\":$max}"
812
+ post_json "/api/pages/$client_id/eval-probe" "$body"
813
+ ;;
814
+ inspect-fiber-boundaries)
815
+ client_id="${2:-}"
816
+ fiber_id="${3:-}"
817
+ max="${4:-100}"
818
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
819
+ echo "inspect-fiber-boundaries requires <clientId> <fiberId> [max]" >&2
820
+ exit 1
821
+ fi
822
+ if [[ ! "$max" =~ ^[1-9][0-9]*$ ]]; then
823
+ echo "max must be a positive integer" >&2
824
+ exit 1
825
+ fi
826
+ body="{\"command\":\"inspect-fiber-boundaries\",\"fiberId\":$(json_escape "$fiber_id"),\"max\":$max}"
827
+ post_json "/api/pages/$client_id/eval-probe" "$body"
828
+ ;;
829
+ inspect-fiber-structure)
830
+ client_id="${2:-}"
831
+ fiber_id="${3:-}"
832
+ hook_index="${4:-0}"
833
+ window_size="${5:-3}"
834
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
835
+ echo "inspect-fiber-structure requires <clientId> <fiberId> <hookIndex> [windowSize]" >&2
836
+ exit 1
837
+ fi
838
+ body="{\"command\":\"inspect-fiber-structure\",\"fiberId\":$(json_escape "$fiber_id"),\"hookIndex\":$hook_index,\"windowSize\":$window_size}"
839
+ post_json "/api/pages/$client_id/eval-probe" "$body"
840
+ ;;
841
+ describe-component)
842
+ # Usage: describe-component <clientId> <fiberIdOrName> [--text|--json]
843
+ # If arg starts with "fiber:" → resolve from registry; else treat as component name.
844
+ client_id="${2:-}"
845
+ target="${3:-}"
846
+ view="${4:---text}"
847
+ if [[ -z "$client_id" || -z "$target" ]]; then
848
+ echo "describe-component requires <clientId> <fiberIdOrName> [--text|--json]" >&2
849
+ exit 1
850
+ fi
851
+ if [[ "$view" != "--text" && "$view" != "--json" ]]; then
852
+ echo "describe-component view must be --text or --json" >&2
853
+ exit 1
854
+ fi
855
+ if [[ "$target" == fiber:* ]]; then
856
+ body="{\"command\":\"describe-component\",\"fiberId\":$(json_escape "$target")}"
857
+ else
858
+ body="{\"command\":\"describe-component\",\"name\":$(json_escape "$target")}"
859
+ fi
860
+ response=$(post_json "/api/pages/$client_id/eval-probe" "$body")
861
+ print_probe_result_text "${view#--}" <<<"$response"
862
+ ;;
863
+ explain-component)
864
+ # Usage: explain-component <clientId> <probeId|fiberId|name> [--text|--json|--complexity]
865
+ # Default --text prints semantic sections as plain text; --json preserves the full raw payload.
866
+ client_id="${2:-}"
867
+ target="${3:-}"
868
+ view="${4:---text}"
869
+ if [[ -z "$client_id" || -z "$target" ]]; then
870
+ echo "explain-component requires <clientId> <probeId|fiberId|name> [--text|--json|--complexity]" >&2
871
+ exit 1
872
+ fi
873
+ if [[ "$view" != "--text" && "$view" != "--json" && "$view" != "--complexity" ]]; then
874
+ echo "explain-component view must be --text, --json, or --complexity" >&2
875
+ exit 1
876
+ fi
877
+ if [[ "$target" =~ ^[0-9]+$ ]]; then
878
+ body="{\"command\":\"explain-component\",\"probeId\":$(json_escape "$target")}"
879
+ elif [[ "$target" == fiber:* ]]; then
880
+ body="{\"command\":\"explain-component\",\"fiberId\":$(json_escape "$target")}"
881
+ else
882
+ body="{\"command\":\"explain-component\",\"name\":$(json_escape "$target")}"
883
+ fi
884
+ response=$(post_json "/api/pages/$client_id/eval-probe" "$body")
885
+ print_probe_result_text "${view#--}" <<<"$response"
886
+ ;;
887
+ # NOTE: avalanche-start / avalanche-stop are removed in the upstream-shape bridge.
888
+ # The detector auto-installs on bundle eval; there is no pause/resume API. Use
889
+ # avalanche-clear (alias to upstream reset) to drop history.
890
+ avalanche-clear)
891
+ client_id="${2:-}"
892
+ if [[ -z "$client_id" ]]; then
893
+ echo "avalanche-clear requires <clientId>" >&2
894
+ exit 1
895
+ fi
896
+ post_avalanche "$client_id" '{"action":"clear"}'
897
+ ;;
898
+ avalanche-status)
899
+ client_id="${2:-}"
900
+ if [[ -z "$client_id" ]]; then
901
+ echo "avalanche-status requires <clientId>" >&2
902
+ exit 1
903
+ fi
904
+ post_avalanche "$client_id" '{"action":"status"}'
905
+ ;;
906
+ avalanche-export)
907
+ client_id="${2:-}"
908
+ last_n="${3:-}"
909
+ out_file="${4:-}"
910
+ if [[ -z "$client_id" ]]; then
911
+ echo "avalanche-export requires <clientId> [lastN] [outFile]" >&2
912
+ exit 1
913
+ fi
914
+ if [[ -n "$last_n" ]]; then
915
+ payload=$(post_avalanche "$client_id" "{\"action\":\"export-last\",\"count\":$last_n}")
916
+ else
917
+ payload=$(post_avalanche "$client_id" '{"action":"export"}')
918
+ fi
919
+ if [[ -n "$out_file" ]]; then
920
+ printf '%s' "$payload" > "$out_file"
921
+ echo "avalanche export written to $out_file" >&2
922
+ else
923
+ printf '%s' "$payload"
924
+ fi
925
+ ;;
926
+ # NOTE: avalanche-metrics / avalanche-help are removed. Upstream surface does
927
+ # not have these. Use avalanche-status for installed/commitCount overview.
928
+ avalanche-hot-sources)
929
+ client_id="${2:-}"
930
+ limit="${3:-}"
931
+ if [[ -z "$client_id" ]]; then
932
+ echo "avalanche-hot-sources requires <clientId> [limit]" >&2
933
+ exit 1
934
+ fi
935
+ if [[ -n "$limit" ]]; then
936
+ post_avalanche "$client_id" "{\"action\":\"hot-sources\",\"limit\":$limit}"
937
+ else
938
+ post_avalanche "$client_id" '{"action":"hot-sources"}'
939
+ fi
940
+ ;;
941
+ avalanche-commit-list)
942
+ client_id="${2:-}"
943
+ top="${3:-20}"
944
+ by="${4:-collateral}"
945
+ if [[ -z "$client_id" ]]; then
946
+ echo "avalanche-commit-list requires <clientId> [top] [by]" >&2
947
+ echo " live by ∈ collateral | sources | restarts | performedWork | effects | memos | callbacks | contextUpdates" >&2
948
+ echo " offline by additionally supports restartProxy | amplifiers | componentRenders | externalStoreRestarts | priorityInterruptions | fiberInterruptions | repeats" >&2
949
+ exit 1
950
+ fi
951
+ post_avalanche "$client_id" "{\"action\":\"commit-list\",\"top\":$top,\"by\":\"$by\"}"
952
+ ;;
953
+ avalanche-commit-peek)
954
+ client_id="${2:-}"
955
+ commit_id="${3:-}"
956
+ include="${4:-stats,sources,timing,metrics}"
957
+ depth="${5:-3}"
958
+ max_items="${6:-20}"
959
+ if [[ -z "$client_id" || -z "$commit_id" ]]; then
960
+ echo "avalanche-commit-peek requires <clientId> <commitId> [include] [depth] [maxItems]" >&2
961
+ echo " live include csv ⊆ stats,sources,timing,metrics,unmounts,actions,rendered (atl: alias of metrics)" >&2
962
+ echo " note: \"rendered\" requires arming with detectionMode=full" >&2
963
+ echo " offline include csv ⊆ stats,sources,collaterals,context,amplifiers,metrics" >&2
964
+ exit 1
965
+ fi
966
+ include_json="["
967
+ first=1
968
+ IFS=',' read -ra parts <<< "$include"
969
+ for p in "${parts[@]}"; do
970
+ p_trim=$(echo "$p" | tr -d ' ')
971
+ [[ -z "$p_trim" ]] && continue
972
+ if [[ $first -eq 0 ]]; then include_json+=","; fi
973
+ include_json+="\"$p_trim\""
974
+ first=0
975
+ done
976
+ include_json+="]"
977
+ post_avalanche "$client_id" "{\"action\":\"commit-peek\",\"commitId\":$commit_id,\"include\":$include_json,\"depth\":$depth,\"maxItems\":$max_items}"
978
+ ;;
979
+ avalanche-describe-action)
980
+ client_id="${2:-}"
981
+ action_id="${3:-}"
982
+ if [[ -z "$client_id" || -z "$action_id" ]]; then
983
+ echo "avalanche-describe-action requires <clientId> <actionId>" >&2
984
+ exit 1
985
+ fi
986
+ post_avalanche "$client_id" "{\"action\":\"describe-action\",\"actionId\":$action_id}"
987
+ ;;
988
+ avalanche-install)
989
+ client_id="${2:-}"
990
+ detection_mode="${3:-sources}"
991
+ if [[ -z "$client_id" ]]; then
992
+ echo "avalanche-install requires <clientId> [detectionMode]" >&2
993
+ exit 1
994
+ fi
995
+ require_detection_mode avalanche-install "$detection_mode"
996
+ body="{\"action\":\"install\",\"options\":{\"captureFibers\":true,\"detectionMode\":\"$detection_mode\",\"historySize\":1000}}"
997
+ post_avalanche "$client_id" "$body"
998
+ ;;
999
+ avalanche-recapture)
1000
+ client_id="${2:-}"
1001
+ detection_mode="${3:-sources}"
1002
+ if [[ -z "$client_id" ]]; then
1003
+ echo "avalanche-recapture requires <clientId> [detectionMode]" >&2
1004
+ exit 1
1005
+ fi
1006
+ require_detection_mode avalanche-recapture "$detection_mode"
1007
+ body="{\"action\":\"install\",\"options\":{\"captureFibers\":true,\"detectionMode\":\"$detection_mode\",\"historySize\":1000}}"
1008
+ post_avalanche "$client_id" "$body" >/dev/null
1009
+ post_avalanche "$client_id" '{"action":"clear"}' >/dev/null
1010
+ post_avalanche "$client_id" '{"action":"status"}'
1011
+ ;;
1012
+ avalanche-debug-hook)
1013
+ client_id="${2:-}"
1014
+ source_id="${3:-}"
1015
+ hook_index="${4:-}"
1016
+ window_size="${5:-3}"
1017
+ if [[ -z "$client_id" || -z "$source_id" || -z "$hook_index" ]]; then
1018
+ echo "avalanche-debug-hook requires <clientId> <sourceId> <hookIndex> [window]" >&2
1019
+ exit 1
1020
+ fi
1021
+ source_json=$(json_escape "$source_id")
1022
+ post_avalanche "$client_id" "{\"action\":\"debug-hook\",\"sourceId\":$source_json,\"hookIndex\":$hook_index,\"window\":$window_size}"
1023
+ ;;
1024
+ avalanche-save)
1025
+ client_id="${2:-}"
1026
+ out_file="${3:-}"
1027
+ count="${4:-0}"
1028
+ if [[ -z "$client_id" || -z "$out_file" ]]; then
1029
+ echo "avalanche-save requires <clientId> <file> [count]" >&2
1030
+ echo " count=0 → full export, count=N → last N commits" >&2
1031
+ exit 1
1032
+ fi
1033
+ if [[ "$count" -gt 0 ]]; then
1034
+ body="{\"action\":\"export-last\",\"count\":$count}"
1035
+ else
1036
+ body='{"action":"export"}'
1037
+ fi
1038
+ response=$(post_avalanche "$client_id" "$body")
1039
+ if [[ -z "$response" ]]; then
1040
+ echo "avalanche-save: empty response from helper" >&2
1041
+ exit 1
1042
+ fi
1043
+ payload=$(node "$SCRIPT_DIR/avalanche-analyze.cjs" normalize "$count" <<<"$response") || exit 1
1044
+ printf '%s' "$payload" > "$out_file"
1045
+ bytes=$(wc -c < "$out_file" | tr -d ' ')
1046
+ commits=$(node "$SCRIPT_DIR/avalanche-analyze.cjs" "$out_file" summary | node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync(0,"utf8")).commits))')
1047
+ echo "{\"ok\":true,\"file\":\"$out_file\",\"bytes\":$bytes,\"commits\":$commits}"
1048
+ ;;
1049
+ avalanche-offline)
1050
+ shift
1051
+ if [[ $# -lt 2 ]]; then
1052
+ echo "avalanche-offline requires <file> <summary|commit-list|commit-peek|hot-sources|describe-action|source-search> [args...]" >&2
1053
+ exit 1
1054
+ fi
1055
+ node "$SCRIPT_DIR/avalanche-analyze.cjs" "$@"
1056
+ ;;
1057
+ eval-snippets)
1058
+ snip_dir="$SCRIPT_DIR/../snippets"
1059
+ if [[ ! -d "$snip_dir" ]]; then echo "no snippets dir at $snip_dir" >&2; exit 1; fi
1060
+ ( cd "$snip_dir" && ls -1 *.js 2>/dev/null )
1061
+ ;;
1062
+ eval-page)
1063
+ client_id="${2:-}"
1064
+ snippet="${3:-}"
1065
+ shift 3 || true
1066
+ if [[ -z "$client_id" || -z "$snippet" ]]; then
1067
+ echo "eval-page requires <clientId> <snippet-file-or-name> [--key value ...]" >&2
1068
+ echo " snippet may be an absolute path, a path relative to cwd, or a name resolved against $SCRIPT_DIR/../snippets/<name>.js" >&2
1069
+ exit 1
1070
+ fi
1071
+ snip_path=""
1072
+ if [[ -f "$snippet" ]]; then snip_path="$snippet"; fi
1073
+ if [[ -z "$snip_path" && -f "$SCRIPT_DIR/../snippets/$snippet" ]]; then snip_path="$SCRIPT_DIR/../snippets/$snippet"; fi
1074
+ if [[ -z "$snip_path" && -f "$SCRIPT_DIR/../snippets/$snippet.js" ]]; then snip_path="$SCRIPT_DIR/../snippets/$snippet.js"; fi
1075
+ if [[ -z "$snip_path" ]]; then echo "snippet not found: $snippet" >&2; exit 1; fi
1076
+ # Build args object from --key value pairs.
1077
+ args_json='{}'
1078
+ while [[ $# -gt 0 ]]; do
1079
+ if [[ "$1" == --* ]]; then
1080
+ k="${1#--}"; v="${2:-}"; shift 2
1081
+ args_json=$(node -e '
1082
+ const o = JSON.parse(process.argv[1]);
1083
+ const k = process.argv[2];
1084
+ let v = process.argv[3];
1085
+ if (v === "true") v = true;
1086
+ else if (v === "false") v = false;
1087
+ else if (!Number.isNaN(Number(v)) && v !== "") v = Number(v);
1088
+ o[k] = v;
1089
+ process.stdout.write(JSON.stringify(o));
1090
+ ' "$args_json" "$k" "$v") || exit 1
1091
+ else shift; fi
1092
+ done
1093
+ src=$(cat "$snip_path")
1094
+ body=$(node -e '
1095
+ const src = require("fs").readFileSync(process.argv[1], "utf8");
1096
+ const args = JSON.parse(process.argv[2]);
1097
+ process.stdout.write(JSON.stringify({ source: src, args }));
1098
+ ' "$snip_path" "$args_json") || exit 1
1099
+ run_curl POST "$BASE_URL/api/pages/$client_id/eval-script" "$body"
1100
+ ;;
1101
+ avalanche-arm)
1102
+ client_id="${2:-}"
1103
+ origin="${3:-}"
1104
+ max_commits="${4:-}"
1105
+ capture_fibers="${5:-true}"
1106
+ detection_mode="${6:-sources}"
1107
+ if [[ -z "$client_id" || -z "$origin" ]]; then
1108
+ echo "avalanche-arm requires <clientId> <origin> [maxCommits] [captureFibers=true|false] [detectionMode=sources|full|stats|amplifiers]" >&2
1109
+ exit 1
1110
+ fi
1111
+ max_commits="${max_commits:-0}"
1112
+ if [[ "$capture_fibers" != "true" && "$capture_fibers" != "false" ]]; then
1113
+ echo "avalanche-arm captureFibers must be true or false" >&2
1114
+ exit 1
1115
+ fi
1116
+ require_detection_mode avalanche-arm "$detection_mode"
1117
+ body="{\"clientId\":$(json_escape "$client_id"),\"origin\":$(json_escape "$origin"),\"autoStart\":true,\"max\":$max_commits,\"options\":{\"captureFibers\":$capture_fibers,\"detectionMode\":\"$detection_mode\"}}"
1118
+ post_json "/api/avalanche-arm" "$body" >/dev/null
1119
+ # Reset first (clear prior capture), then install detector — no reload needed
1120
+ reset_body="{\"action\":\"reset\"}"
1121
+ post_json "/api/pages/$client_id/avalanche" "$reset_body" >/dev/null
1122
+ install_body="{\"action\":\"install\",\"options\":{\"captureFibers\":$capture_fibers,\"detectionMode\":\"$detection_mode\"}}"
1123
+ post_json "/api/pages/$client_id/avalanche" "$install_body"
1124
+ ;;
1125
+ avalanche-disarm)
1126
+ client_id="${2:-}"
1127
+ origin="${3:-}"
1128
+ if [[ -z "$client_id" || -z "$origin" ]]; then
1129
+ echo "avalanche-disarm requires <clientId> <origin>" >&2
1130
+ exit 1
1131
+ fi
1132
+ body="{\"clientId\":$(json_escape "$client_id"),\"origin\":$(json_escape "$origin"),\"autoStart\":false}"
1133
+ post_json "/api/avalanche-arm" "$body"
1134
+ ;;
1135
+ page-reload)
1136
+ client_id="${2:-}"
1137
+ if [[ -z "$client_id" ]]; then
1138
+ echo "page-reload requires <clientId>" >&2
1139
+ exit 1
1140
+ fi
1141
+ post_json "/api/pages/$client_id/reload" '{}'
1142
+ ;;
1143
+ page-wait-reload)
1144
+ client_id="${2:-}"
1145
+ timeout_ms="${3:-5000}"
1146
+ if [[ -z "$client_id" ]]; then
1147
+ echo "page-wait-reload requires <clientId> [timeoutMs]" >&2
1148
+ exit 1
1149
+ fi
1150
+ body="{\"timeoutMs\":$timeout_ms}"
1151
+ post_json "/api/pages/$client_id/wait-reload" "$body"
1152
+ ;;
1153
+ reload-with-avalanche)
1154
+ client_id="${2:-}"
1155
+ origin="${3:-}"
1156
+ max_commits="${4:-0}"
1157
+ timeout_ms="${5:-5000}"
1158
+ settle_ms="${6:-3000}"
1159
+ capture_fibers="${7:-true}"
1160
+ detection_mode="${8:-sources}"
1161
+ if [[ -z "$client_id" || -z "$origin" ]]; then
1162
+ echo "reload-with-avalanche requires <clientId> <origin> [maxCommits] [timeoutMs] [settleMs] [captureFibers=true|false] [detectionMode=sources|full|stats|amplifiers]" >&2
1163
+ exit 1
1164
+ fi
1165
+ echo "== arm ==" >&2
1166
+ "$0" avalanche-arm "$client_id" "$origin" "$max_commits" "$capture_fibers" "$detection_mode"
1167
+ echo >&2
1168
+ echo "== reload ==" >&2
1169
+ "$0" page-reload "$client_id"
1170
+ echo >&2
1171
+ echo "== wait ==" >&2
1172
+ wait_result=$("$0" page-wait-reload "$client_id" "$timeout_ms")
1173
+ printf '%s\n' "$wait_result"
1174
+ if ! printf '%s' "$wait_result" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.exit(JSON.parse(s).reloaded===true?0:1)}catch{process.exit(1)}})'; then
1175
+ echo "reload-with-avalanche: the page did not reconnect within ${timeout_ms}ms after the reload." >&2
1176
+ echo " meaning: Chippy was not loaded again in the new document, so nothing captured its commits" >&2
1177
+ echo " next: with Mode A, load the runtimes as init scripts (installHelpers(page, { init: true })) so they return on reload," >&2
1178
+ echo " or skip the reload: avalanche-install, then trigger the interaction and read avalanche-status" >&2
1179
+ exit 1
1180
+ fi
1181
+ if [[ "$settle_ms" -gt 0 ]]; then
1182
+ echo >&2
1183
+ echo "== settle (${settle_ms}ms) ==" >&2
1184
+ sleep "$(awk "BEGIN { printf \"%.3f\", ${settle_ms}/1000 }")"
1185
+ fi
1186
+ # Upstream IIFE has no stop/pause; capture continues. We just sample status
1187
+ # and hot-sources after the settle window.
1188
+ echo >&2
1189
+ echo "== status ==" >&2
1190
+ "$0" avalanche-status "$client_id"
1191
+ echo >&2
1192
+ echo "== hot sources ==" >&2
1193
+ "$0" avalanche-hot-sources "$client_id" 12
1194
+ ;;
1195
+ handoff-to-playwright)
1196
+ client_id="${2:-}"
1197
+ url="${3:-}"
1198
+ probe_ids_csv="${4:-1,2}"
1199
+ browser_name="${5:-chromium}"
1200
+ headless="${6:-true}"
1201
+ if [[ -z "$client_id" ]]; then
1202
+ echo "handoff-to-playwright requires <clientId> [url] [probeIdsCsv] [browserName] [headless]" >&2
1203
+ exit 1
1204
+ fi
1205
+ IFS=',' read -ra probe_parts <<< "$probe_ids_csv"
1206
+ probe_json="["
1207
+ first=1
1208
+ for probe_id in "${probe_parts[@]}"; do
1209
+ probe_trim=$(echo "$probe_id" | tr -d ' ')
1210
+ [[ -z "$probe_trim" ]] && continue
1211
+ if [[ $first -eq 0 ]]; then probe_json+=","; fi
1212
+ probe_json+="\"$probe_trim\""
1213
+ first=0
1214
+ done
1215
+ probe_json+="]"
1216
+ body=$(node -e '
1217
+ const body = {
1218
+ url: process.argv[1] || undefined,
1219
+ probeIds: JSON.parse(process.argv[2]),
1220
+ browserName: process.argv[3] || "chromium",
1221
+ headless: process.argv[4] !== "false",
1222
+ };
1223
+ process.stdout.write(JSON.stringify(body));
1224
+ ' "$url" "$probe_json" "$browser_name" "$headless") || exit 1
1225
+ post_json "/api/pages/$client_id/handoff-to-playwright" "$body"
1226
+ ;;
1227
+ proxy-state)
1228
+ curl "${CURL_OPTS[@]}" "$BASE_URL/api/proxy/state"
1229
+ ;;
1230
+ proxy-start)
1231
+ target_port="${2:-}"
1232
+ original_url="${3:-}"
1233
+ if [[ -z "$target_port" ]]; then
1234
+ echo "proxy-start requires <targetPort> [originalUrl]" >&2
1235
+ exit 1
1236
+ fi
1237
+ if [[ -n "$original_url" ]]; then
1238
+ body="{\"targetPort\":$target_port,\"originalUrl\":$(json_escape "$original_url")}"
1239
+ else
1240
+ body="{\"targetPort\":$target_port}"
1241
+ fi
1242
+ post_json "/api/proxy/start" "$body"
1243
+ ;;
1244
+ proxy-stop)
1245
+ post_json "/api/proxy/stop" '{}'
1246
+ ;;
1247
+ *)
1248
+ echo "Unknown command: $cmd" >&2
1249
+ usage >&2
1250
+ exit 1
1251
+ ;;
1252
+ esac