pi-cursor-sdk 0.1.16 → 0.1.18
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/CHANGELOG.md +53 -1
- package/README.md +2 -2
- package/docs/cursor-live-smoke-checklist.md +54 -41
- package/docs/cursor-model-ux-spec.md +4 -3
- package/docs/cursor-testing-lessons.md +199 -0
- package/package.json +14 -5
- package/scripts/isolated-cursor-smoke.sh +226 -0
- package/scripts/steering-rpc-smoke.mjs +238 -0
- package/scripts/tmux-live-smoke.sh +418 -0
- package/scripts/validate-smoke-jsonl.mjs +207 -0
- package/src/cursor-context-tools.ts +6 -0
- package/src/cursor-display-text.ts +10 -0
- package/src/cursor-edit-diff.ts +11 -0
- package/src/cursor-env-boolean.ts +22 -0
- package/src/cursor-live-run-coordinator.ts +483 -0
- package/src/cursor-native-replay-routing.ts +48 -0
- package/src/cursor-native-replay-trace.ts +29 -0
- package/src/cursor-native-tool-display-registration.ts +103 -0
- package/src/cursor-native-tool-display-replay.ts +465 -0
- package/src/cursor-native-tool-display-state.ts +78 -0
- package/src/cursor-native-tool-display-tools.ts +102 -0
- package/src/cursor-native-tool-display.ts +10 -648
- package/src/cursor-partial-content-emitter.ts +121 -0
- package/src/cursor-pi-tool-bridge-abort.ts +133 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +179 -0
- package/src/cursor-pi-tool-bridge-mcp.ts +118 -0
- package/src/cursor-pi-tool-bridge-run.ts +384 -0
- package/src/cursor-pi-tool-bridge-server.ts +182 -0
- package/src/cursor-pi-tool-bridge-snapshot.ts +88 -0
- package/src/cursor-pi-tool-bridge-types.ts +80 -0
- package/src/cursor-pi-tool-bridge.ts +42 -1104
- package/src/cursor-provider-live-run-drain.ts +405 -0
- package/src/cursor-provider-turn-coordinator.ts +460 -0
- package/src/cursor-provider.ts +77 -1103
- package/src/cursor-question-tool.ts +9 -1
- package/src/cursor-record-utils.ts +26 -0
- package/src/cursor-sdk-output-filter.ts +100 -0
- package/src/cursor-sensitive-text.ts +37 -0
- package/src/cursor-tool-transcript.ts +28 -1229
- package/src/cursor-transcript-tool-formatters.ts +641 -0
- package/src/cursor-transcript-tool-specs.ts +441 -0
- package/src/cursor-transcript-utils.ts +276 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
SMOKE_DIR="${SMOKE_DIR:-/tmp/pi-cursor-sdk-live-smoke-$(date +%Y%m%dT%H%M%S)}"
|
|
6
|
+
SHELL_BIN="${SHELL:-/bin/bash}"
|
|
7
|
+
|
|
8
|
+
PI_BASE=(
|
|
9
|
+
pi -e "$ROOT"
|
|
10
|
+
--cursor-no-fast
|
|
11
|
+
--model cursor/composer-2.5
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
TMUX_SESSIONS=()
|
|
15
|
+
|
|
16
|
+
cleanup() {
|
|
17
|
+
local session
|
|
18
|
+
for session in "${TMUX_SESSIONS[@]:-}"; do
|
|
19
|
+
tmux kill-session -t "$session" 2>/dev/null || true
|
|
20
|
+
done
|
|
21
|
+
}
|
|
22
|
+
trap cleanup EXIT
|
|
23
|
+
|
|
24
|
+
print_help() {
|
|
25
|
+
cat <<EOF
|
|
26
|
+
Partial live smoke runner for pi-cursor-sdk (subset of docs/cursor-live-smoke-checklist.md).
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
./scripts/tmux-live-smoke.sh
|
|
30
|
+
SMOKE_DIR=/tmp/pi-cursor-smoke ./scripts/tmux-live-smoke.sh
|
|
31
|
+
|
|
32
|
+
Environment:
|
|
33
|
+
SMOKE_DIR Artifact directory. Defaults to /tmp/pi-cursor-sdk-live-smoke-<timestamp>.
|
|
34
|
+
CURSOR_API_KEY Required for live Cursor runs.
|
|
35
|
+
|
|
36
|
+
Prerequisites:
|
|
37
|
+
pi, node, rg, tmux on PATH
|
|
38
|
+
timeout or gtimeout optional; bash process-group kill fallback is used when absent
|
|
39
|
+
|
|
40
|
+
Coverage:
|
|
41
|
+
- prereq model listing
|
|
42
|
+
- basic non-interactive prompt (retry-empty-output; strict output assertion)
|
|
43
|
+
- default ambient settings prompt (strict; no retry)
|
|
44
|
+
- simple non-interactive math prompt (strict; no retry)
|
|
45
|
+
- interactive TUI math/footer polling with cleanup
|
|
46
|
+
- RPC steering after native replay tool execution (tmux-isolated)
|
|
47
|
+
- diagnostics safety scan
|
|
48
|
+
- JSONL assistant usage validation
|
|
49
|
+
|
|
50
|
+
Not covered here:
|
|
51
|
+
bridge MCP, standalone native replay, abort/cancel, packaging, full checklist sections 4-9
|
|
52
|
+
|
|
53
|
+
Options:
|
|
54
|
+
-h, --help Show this help.
|
|
55
|
+
|
|
56
|
+
Exit codes:
|
|
57
|
+
0 all partial checks passed
|
|
58
|
+
1 prerequisite, smoke, safety, or JSONL validation failure
|
|
59
|
+
EOF
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
log() {
|
|
63
|
+
printf '[smoke] %s\n' "$*"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fail() {
|
|
67
|
+
printf '[smoke] FAIL: %s\n' "$*" >&2
|
|
68
|
+
exit 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
require_cmd() {
|
|
72
|
+
command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
run_with_timeout() {
|
|
76
|
+
local timeout_secs="$1"
|
|
77
|
+
shift
|
|
78
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
79
|
+
timeout "$timeout_secs" "$@"
|
|
80
|
+
return $?
|
|
81
|
+
fi
|
|
82
|
+
if command -v gtimeout >/dev/null 2>&1; then
|
|
83
|
+
gtimeout "$timeout_secs" "$@"
|
|
84
|
+
return $?
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
local restore_monitor=0
|
|
88
|
+
case $- in
|
|
89
|
+
*m*) ;;
|
|
90
|
+
*)
|
|
91
|
+
restore_monitor=1
|
|
92
|
+
set -m
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
|
|
96
|
+
"$@" &
|
|
97
|
+
local pid=$!
|
|
98
|
+
(
|
|
99
|
+
sleep "$timeout_secs"
|
|
100
|
+
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
|
101
|
+
sleep 2
|
|
102
|
+
kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
|
|
103
|
+
) &
|
|
104
|
+
local watcher=$!
|
|
105
|
+
local code=0
|
|
106
|
+
if wait "$pid"; then
|
|
107
|
+
code=0
|
|
108
|
+
else
|
|
109
|
+
code=$?
|
|
110
|
+
fi
|
|
111
|
+
kill "$watcher" 2>/dev/null || true
|
|
112
|
+
wait "$watcher" 2>/dev/null || true
|
|
113
|
+
if (( restore_monitor )); then
|
|
114
|
+
set +m
|
|
115
|
+
fi
|
|
116
|
+
return "$code"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
tail_file() {
|
|
120
|
+
local file="$1"
|
|
121
|
+
local lines="${2:-80}"
|
|
122
|
+
if [[ -s "$file" ]]; then
|
|
123
|
+
tail -n "$lines" "$file" || true
|
|
124
|
+
else
|
|
125
|
+
printf '<empty: %s>\n' "$file"
|
|
126
|
+
fi
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
assert_file_contains() {
|
|
130
|
+
local name="$1"
|
|
131
|
+
local file="$2"
|
|
132
|
+
local pattern="$3"
|
|
133
|
+
local label="$4"
|
|
134
|
+
if ! rg -q "$pattern" "$file"; then
|
|
135
|
+
printf '[smoke] %s missing %s in %s\n' "$name" "$label" "$file" >&2
|
|
136
|
+
printf '[smoke] %s transcript tail:\n' "$name" >&2
|
|
137
|
+
tail_file "$file" 120 >&2
|
|
138
|
+
fail "$name missing ${label}"
|
|
139
|
+
fi
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
is_empty_retryable_exit() {
|
|
143
|
+
local code="$1"
|
|
144
|
+
local stdout="$2"
|
|
145
|
+
[[ ! -s "$stdout" && ( "$code" == "0" || "$code" == "124" || "$code" == "137" || "$code" == "143" ) ]]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
run_direct_attempt() {
|
|
149
|
+
local name="$1"
|
|
150
|
+
local timeout_secs="$2"
|
|
151
|
+
local stdout="$3"
|
|
152
|
+
local stderr="$4"
|
|
153
|
+
shift 4
|
|
154
|
+
rm -f "$stdout" "$stderr"
|
|
155
|
+
|
|
156
|
+
if run_with_timeout "$timeout_secs" "$@" >"$stdout" 2>"$stderr"; then
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
return $?
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
run_direct_fail() {
|
|
163
|
+
local name="$1"
|
|
164
|
+
local code="$2"
|
|
165
|
+
local stdout="$3"
|
|
166
|
+
local stderr="$4"
|
|
167
|
+
local label="$5"
|
|
168
|
+
if [[ "$code" != "0" ]]; then
|
|
169
|
+
cat "$stderr" >&2 || true
|
|
170
|
+
fail "$name exited $code"
|
|
171
|
+
fi
|
|
172
|
+
printf '[smoke] %s missing %s in %s\n' "$name" "$label" "$stdout" >&2
|
|
173
|
+
printf '[smoke] %s stdout tail:\n' "$name" >&2
|
|
174
|
+
tail_file "$stdout" 120 >&2
|
|
175
|
+
printf '[smoke] %s stderr tail:\n' "$name" >&2
|
|
176
|
+
tail_file "$stderr" 80 >&2
|
|
177
|
+
fail "$name missing ${label}"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
run_direct() {
|
|
181
|
+
local name="$1"
|
|
182
|
+
local timeout_secs="$2"
|
|
183
|
+
local policy="$3"
|
|
184
|
+
local expected_pattern="$4"
|
|
185
|
+
local expected_label="$5"
|
|
186
|
+
shift 5
|
|
187
|
+
local stdout="$SMOKE_DIR/${name}.stdout.txt"
|
|
188
|
+
local stderr="$SMOKE_DIR/${name}.stderr.txt"
|
|
189
|
+
local code=0
|
|
190
|
+
|
|
191
|
+
if run_direct_attempt "$name" "$timeout_secs" "$stdout" "$stderr" "$@"; then
|
|
192
|
+
code=0
|
|
193
|
+
else
|
|
194
|
+
code=$?
|
|
195
|
+
fi
|
|
196
|
+
if [[ "$code" == "0" ]] && rg -q "$expected_pattern" "$stdout"; then
|
|
197
|
+
log "$name PASS"
|
|
198
|
+
return 0
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
case "$policy" in
|
|
202
|
+
strict)
|
|
203
|
+
run_direct_fail "$name" "$code" "$stdout" "$stderr" "$expected_label"
|
|
204
|
+
;;
|
|
205
|
+
retry-empty-output)
|
|
206
|
+
local first_stdout="$SMOKE_DIR/${name}.attempt1.stdout.txt"
|
|
207
|
+
local first_stderr="$SMOKE_DIR/${name}.attempt1.stderr.txt"
|
|
208
|
+
if ! is_empty_retryable_exit "$code" "$stdout"; then
|
|
209
|
+
run_direct_fail "$name" "$code" "$stdout" "$stderr" "$expected_label"
|
|
210
|
+
fi
|
|
211
|
+
mv "$stdout" "$first_stdout" 2>/dev/null || true
|
|
212
|
+
mv "$stderr" "$first_stderr" 2>/dev/null || true
|
|
213
|
+
log "$name retrying once after empty output with exit $code"
|
|
214
|
+
if run_direct_attempt "$name" "$timeout_secs" "$stdout" "$stderr" "$@"; then
|
|
215
|
+
local retry_code=0
|
|
216
|
+
if rg -q "$expected_pattern" "$stdout"; then
|
|
217
|
+
log "$name PASS after retry (first exit $code; first stderr: $first_stderr)"
|
|
218
|
+
return 0
|
|
219
|
+
fi
|
|
220
|
+
printf '[smoke] %s retry exited %s but still missed %s\n' "$name" "$retry_code" "$expected_label" >&2
|
|
221
|
+
else
|
|
222
|
+
local retry_code=$?
|
|
223
|
+
printf '[smoke] %s retry exited %s after first empty output exit %s\n' "$name" "$retry_code" "$code" >&2
|
|
224
|
+
fi
|
|
225
|
+
printf '[smoke] %s first stdout tail:\n' "$name" >&2
|
|
226
|
+
tail_file "$first_stdout" 80 >&2
|
|
227
|
+
printf '[smoke] %s first stderr tail:\n' "$name" >&2
|
|
228
|
+
tail_file "$first_stderr" 80 >&2
|
|
229
|
+
printf '[smoke] %s retry stdout tail:\n' "$name" >&2
|
|
230
|
+
tail_file "$stdout" 120 >&2
|
|
231
|
+
printf '[smoke] %s retry stderr tail:\n' "$name" >&2
|
|
232
|
+
tail_file "$stderr" 80 >&2
|
|
233
|
+
fail "$name retry failed after empty output"
|
|
234
|
+
;;
|
|
235
|
+
*)
|
|
236
|
+
fail "$name unknown run_direct policy: $policy (expected strict or retry-empty-output)"
|
|
237
|
+
;;
|
|
238
|
+
esac
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
quote_command() {
|
|
242
|
+
local quoted=()
|
|
243
|
+
local arg
|
|
244
|
+
for arg in "$@"; do
|
|
245
|
+
printf -v arg '%q' "$arg"
|
|
246
|
+
quoted+=("$arg")
|
|
247
|
+
done
|
|
248
|
+
printf '%s ' "${quoted[@]}"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
run_tui_math_footer_poll() {
|
|
252
|
+
local name="$1"
|
|
253
|
+
local timeout_secs="$2"
|
|
254
|
+
shift 2
|
|
255
|
+
local session="pi-cursor-smoke-${name}-$$"
|
|
256
|
+
local capture="$SMOKE_DIR/${name}.capture.txt"
|
|
257
|
+
local script
|
|
258
|
+
local command
|
|
259
|
+
command="$(quote_command "$@")"
|
|
260
|
+
rm -f "$capture"
|
|
261
|
+
|
|
262
|
+
printf -v script 'cd %q || exit 97
|
|
263
|
+
exec %s
|
|
264
|
+
' "$ROOT" "$command"
|
|
265
|
+
tmux new-session -d -s "$session" -x 120 -y 40 -- "$SHELL_BIN" -lc "$script"
|
|
266
|
+
TMUX_SESSIONS+=("$session")
|
|
267
|
+
|
|
268
|
+
local elapsed=0
|
|
269
|
+
local missing=""
|
|
270
|
+
while true; do
|
|
271
|
+
tmux capture-pane -pt "$session" >"$capture" 2>/dev/null || true
|
|
272
|
+
missing=""
|
|
273
|
+
rg -q "SUM=42" "$capture" || missing="${missing} SUM=42"
|
|
274
|
+
rg -q "\\(cursor\\) composer-2\\.5" "$capture" || missing="${missing} footer (cursor) composer-2.5"
|
|
275
|
+
if [[ -z "$missing" ]]; then
|
|
276
|
+
tmux kill-session -t "$session" 2>/dev/null || true
|
|
277
|
+
log "$name PASS"
|
|
278
|
+
return 0
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
sleep 2
|
|
282
|
+
elapsed=$((elapsed + 2))
|
|
283
|
+
if (( elapsed >= timeout_secs )); then
|
|
284
|
+
tmux kill-session -t "$session" 2>/dev/null || true
|
|
285
|
+
printf '[smoke] %s timed out after %ss; missing:%s\n' "$name" "$timeout_secs" "$missing" >&2
|
|
286
|
+
printf '[smoke] %s capture tail:\n' "$name" >&2
|
|
287
|
+
tail_file "$capture" 120 >&2
|
|
288
|
+
fail "$name timed out waiting for TUI evidence"
|
|
289
|
+
fi
|
|
290
|
+
done
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
run_tmux() {
|
|
294
|
+
local name="$1"
|
|
295
|
+
local timeout_secs="$2"
|
|
296
|
+
local dump_stderr_on_fail="$3"
|
|
297
|
+
shift 3
|
|
298
|
+
local session="pi-cursor-smoke-${name}-$$"
|
|
299
|
+
local marker="$SMOKE_DIR/${name}.done"
|
|
300
|
+
local stdout="$SMOKE_DIR/${name}.stdout.txt"
|
|
301
|
+
local stderr="$SMOKE_DIR/${name}.stderr.txt"
|
|
302
|
+
local command
|
|
303
|
+
local script
|
|
304
|
+
command="$(quote_command "$@")"
|
|
305
|
+
rm -f "$marker" "$stdout" "$stderr"
|
|
306
|
+
|
|
307
|
+
printf -v script 'cd %q || exit 97
|
|
308
|
+
%s> %q 2> %q
|
|
309
|
+
code=$?
|
|
310
|
+
printf '\''%%s\n'\'' "$code" > %q
|
|
311
|
+
' "$ROOT" "$command" "$stdout" "$stderr" "$marker"
|
|
312
|
+
tmux new-session -d -s "$session" -- "$SHELL_BIN" -lc "$script"
|
|
313
|
+
TMUX_SESSIONS+=("$session")
|
|
314
|
+
|
|
315
|
+
local elapsed=0
|
|
316
|
+
while [[ ! -f "$marker" ]]; do
|
|
317
|
+
sleep 2
|
|
318
|
+
elapsed=$((elapsed + 2))
|
|
319
|
+
if (( elapsed >= timeout_secs )); then
|
|
320
|
+
tmux capture-pane -pt "$session" >"$SMOKE_DIR/${name}.capture.txt" || true
|
|
321
|
+
tmux kill-session -t "$session" 2>/dev/null || true
|
|
322
|
+
fail "$name timed out after ${timeout_secs}s (see ${name}.capture.txt)"
|
|
323
|
+
fi
|
|
324
|
+
done
|
|
325
|
+
|
|
326
|
+
local code
|
|
327
|
+
code="$(cat "$marker")"
|
|
328
|
+
tmux kill-session -t "$session" 2>/dev/null || true
|
|
329
|
+
if [[ "$code" != "0" ]]; then
|
|
330
|
+
if [[ "$dump_stderr_on_fail" == "1" ]]; then
|
|
331
|
+
cat "$stderr" >&2 || true
|
|
332
|
+
fi
|
|
333
|
+
fail "$name exited $code"
|
|
334
|
+
fi
|
|
335
|
+
log "$name PASS"
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
model_listed() {
|
|
339
|
+
local file="$1"
|
|
340
|
+
rg -q "composer-2\\.5" "$file"
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
344
|
+
print_help
|
|
345
|
+
exit 0
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
require_cmd pi
|
|
349
|
+
require_cmd node
|
|
350
|
+
require_cmd rg
|
|
351
|
+
require_cmd tmux
|
|
352
|
+
|
|
353
|
+
if [[ -z "${CURSOR_API_KEY:-}" ]]; then
|
|
354
|
+
fail "CURSOR_API_KEY is required"
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
mkdir -p "$SMOKE_DIR"
|
|
358
|
+
printf '%s\n' "$SMOKE_DIR" >"$SMOKE_DIR/smoke-dir.txt"
|
|
359
|
+
|
|
360
|
+
log "SMOKE_DIR=$SMOKE_DIR"
|
|
361
|
+
log "partial live smoke: prereq, basic, default-settings, noninteractive-math, tui, steering, diagnostics, jsonl"
|
|
362
|
+
|
|
363
|
+
if ! PI_CURSOR_SETTING_SOURCES=none "${PI_BASE[@]}" --list-models cursor 2>"$SMOKE_DIR/prereq.stderr.txt" | tee "$SMOKE_DIR/prereq.models.txt" | rg -q "composer-2\\.5"; then
|
|
364
|
+
if ! model_listed "$SMOKE_DIR/prereq.stderr.txt"; then
|
|
365
|
+
fail "cursor/composer-2.5 not listed"
|
|
366
|
+
fi
|
|
367
|
+
fi
|
|
368
|
+
log "prereq PASS"
|
|
369
|
+
|
|
370
|
+
run_direct basic 600 retry-empty-output "PI_CURSOR_SMOKE_OK" "PI_CURSOR_SMOKE_OK" \
|
|
371
|
+
env PI_CURSOR_SETTING_SOURCES=none "${PI_BASE[@]}" \
|
|
372
|
+
--session-dir "$SMOKE_DIR/basic" \
|
|
373
|
+
--no-tools \
|
|
374
|
+
-p 'Live smoke. Reply exactly: PI_CURSOR_SMOKE_OK'
|
|
375
|
+
|
|
376
|
+
run_direct default-settings 300 strict "PRODUCT=42" "PRODUCT=42" \
|
|
377
|
+
"${PI_BASE[@]}" \
|
|
378
|
+
--session-dir "$SMOKE_DIR/default-settings" \
|
|
379
|
+
--no-tools \
|
|
380
|
+
-p 'Default settings smoke. Include PRODUCT=42 in the final answer.'
|
|
381
|
+
|
|
382
|
+
run_direct noninteractive-math 300 strict "SUM=42" "SUM=42" \
|
|
383
|
+
env PI_CURSOR_SETTING_SOURCES=none "${PI_BASE[@]}" \
|
|
384
|
+
--session-dir "$SMOKE_DIR/noninteractive-math" \
|
|
385
|
+
--no-tools \
|
|
386
|
+
-p 'Noninteractive math smoke. Compute 19 + 23. Reply only with SUM=42.'
|
|
387
|
+
|
|
388
|
+
run_tui_math_footer_poll tui 420 \
|
|
389
|
+
env PI_CURSOR_SETTING_SOURCES=none "${PI_BASE[@]}" \
|
|
390
|
+
--session-dir "$SMOKE_DIR/tui" \
|
|
391
|
+
--no-tools \
|
|
392
|
+
'TUI smoke. Compute 19 + 23. Reply only with SUM=<number>.'
|
|
393
|
+
|
|
394
|
+
run_tmux steering 420 1 \
|
|
395
|
+
env "SMOKE_SESSION_DIR=$SMOKE_DIR/steering" node "$ROOT/scripts/steering-rpc-smoke.mjs"
|
|
396
|
+
rg -q '"steerOk":true' "$SMOKE_DIR/steering.stdout.txt" || fail "steering missing steerOk"
|
|
397
|
+
rg -q '"steerChain":true' "$SMOKE_DIR/steering.stdout.txt" || fail "steering missing steerChain"
|
|
398
|
+
rg -q "already has active run|AgentBusyError" "$SMOKE_DIR/steering.stdout.txt" "$SMOKE_DIR/steering.stderr.txt" && fail "steering hit AgentBusyError" || true
|
|
399
|
+
|
|
400
|
+
forbidden_files="$(find "$SMOKE_DIR" -type f \( -name '*stderr.txt' -o -name '*capture*.txt' \) -print0 |
|
|
401
|
+
xargs -0 grep -IlE 'CURSOR_API_KEY|Bearer [A-Za-z0-9._-]+|/cursor-pi-tool-bridge/[^ ]+/mcp|127\.0\.0\.1:[0-9]+/cursor-pi-tool-bridge|apiKey|cookie|session-cookie|secret-token' || true)"
|
|
402
|
+
if [[ -n "$forbidden_files" ]]; then
|
|
403
|
+
printf '[smoke] diagnostics safety scan found forbidden material in:\n' >&2
|
|
404
|
+
while IFS= read -r file; do
|
|
405
|
+
[[ -z "$file" ]] && continue
|
|
406
|
+
if [[ "$file" == "$SMOKE_DIR/"* ]]; then
|
|
407
|
+
printf '[smoke] %s\n' "${file#"$SMOKE_DIR/"}" >&2
|
|
408
|
+
else
|
|
409
|
+
printf '[smoke] %s\n' "$file" >&2
|
|
410
|
+
fi
|
|
411
|
+
done <<<"$forbidden_files"
|
|
412
|
+
fail "diagnostics safety scan found forbidden material"
|
|
413
|
+
fi
|
|
414
|
+
log "diagnostics safety PASS"
|
|
415
|
+
|
|
416
|
+
node "$ROOT/scripts/validate-smoke-jsonl.mjs" "$SMOKE_DIR"
|
|
417
|
+
log "jsonl structural scan PASS"
|
|
418
|
+
log "partial live smoke checks passed (see --help for uncovered checklist sections)"
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate assistant presence and usage fields in pi session JSONL files under a smoke directory.
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { join, relative } from "node:path";
|
|
7
|
+
|
|
8
|
+
const REPLAY_TOOL_NOT_FOUND = [
|
|
9
|
+
"Tool grep not found",
|
|
10
|
+
"Tool cursor not found",
|
|
11
|
+
"Tool find not found",
|
|
12
|
+
"Tool ls not found",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function printHelp() {
|
|
16
|
+
console.log(`Validate assistant presence and usage metadata in pi smoke session JSONL files.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
node scripts/validate-smoke-jsonl.mjs <smoke-dir>
|
|
20
|
+
SMOKE_DIR=/tmp/pi-cursor-smoke node scripts/validate-smoke-jsonl.mjs
|
|
21
|
+
|
|
22
|
+
Arguments:
|
|
23
|
+
smoke-dir Directory containing smoke session subdirs and JSONL files.
|
|
24
|
+
Defaults to SMOKE_DIR when the positional arg is omitted.
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-h, --help Show this help.
|
|
28
|
+
--replay-errors Also fail when JSONL contains native replay "Tool * not found" errors.
|
|
29
|
+
--replay-errors-only Scan only for native replay "Tool * not found" errors (skip usage checks).
|
|
30
|
+
|
|
31
|
+
Exit codes:
|
|
32
|
+
0 every enforced invariant passed for the selected mode(s)
|
|
33
|
+
1 invalid arguments, unreadable directory, invalid JSONL, empty/no-assistant files, usage validation failures, or replay tool errors
|
|
34
|
+
2 no JSONL files found under the smoke directory
|
|
35
|
+
|
|
36
|
+
Enforced invariants (default mode):
|
|
37
|
+
- each scanned JSONL file contains parseable JSONL records
|
|
38
|
+
- each scanned JSONL file contains at least one persisted assistant message
|
|
39
|
+
- every persisted assistant message has usage metadata
|
|
40
|
+
- assistant usage input/output/totalTokens are non-negative numbers
|
|
41
|
+
- assistant usage cacheRead/cacheWrite are exactly 0
|
|
42
|
+
|
|
43
|
+
Replay error scan (--replay-errors / --replay-errors-only):
|
|
44
|
+
- no JSONL record contains "Tool grep/cursor/find/ls not found"
|
|
45
|
+
|
|
46
|
+
Notes:
|
|
47
|
+
- Prints one JSON summary line per scanned session file (usage mode) or one replay summary line (replay-only mode).
|
|
48
|
+
- Does not print session message contents or secrets.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fail(message) {
|
|
52
|
+
console.error(`validate-smoke-jsonl: ${message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function collectJsonlFiles(root) {
|
|
57
|
+
const files = [];
|
|
58
|
+
function walk(dir) {
|
|
59
|
+
for (const name of readdirSync(dir)) {
|
|
60
|
+
const path = join(dir, name);
|
|
61
|
+
const st = statSync(path);
|
|
62
|
+
if (st.isDirectory()) walk(path);
|
|
63
|
+
else if (path.endsWith(".jsonl")) files.push(path);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
walk(root);
|
|
67
|
+
return files.sort();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isNonNegativeNumber(value) {
|
|
71
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isBadUsage(usage) {
|
|
75
|
+
return (
|
|
76
|
+
!usage ||
|
|
77
|
+
typeof usage !== "object" ||
|
|
78
|
+
!isNonNegativeNumber(usage.input) ||
|
|
79
|
+
!isNonNegativeNumber(usage.output) ||
|
|
80
|
+
!isNonNegativeNumber(usage.totalTokens) ||
|
|
81
|
+
usage.cacheRead !== 0 ||
|
|
82
|
+
usage.cacheWrite !== 0
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseJsonlFile(file) {
|
|
87
|
+
const lines = readFileSync(file, "utf8")
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.map((line) => line.trim())
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
const records = [];
|
|
92
|
+
let parseErrorCount = 0;
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
try {
|
|
95
|
+
records.push(JSON.parse(line));
|
|
96
|
+
} catch {
|
|
97
|
+
parseErrorCount += 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { lineCount: lines.length, records, parseErrorCount };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function scanReplayErrors(file, records) {
|
|
104
|
+
const hits = [];
|
|
105
|
+
for (const [index, record] of records.entries()) {
|
|
106
|
+
const blob = JSON.stringify(record);
|
|
107
|
+
for (const needle of REPLAY_TOOL_NOT_FOUND) {
|
|
108
|
+
if (blob.includes(needle)) {
|
|
109
|
+
hits.push({ line: index + 1, needle });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return hits;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function main() {
|
|
117
|
+
const args = process.argv.slice(2);
|
|
118
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
119
|
+
printHelp();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const replayErrorsOnly = args.includes("--replay-errors-only");
|
|
124
|
+
const replayErrors = replayErrorsOnly || args.includes("--replay-errors");
|
|
125
|
+
const positional = args.filter((arg) => !arg.startsWith("-"));
|
|
126
|
+
|
|
127
|
+
if (positional.length > 1) {
|
|
128
|
+
fail("too many arguments; pass only the smoke directory");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const smokeDir = positional[0] ?? process.env.SMOKE_DIR;
|
|
132
|
+
if (!smokeDir) {
|
|
133
|
+
fail("missing smoke directory; pass a path or set SMOKE_DIR");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let files;
|
|
137
|
+
try {
|
|
138
|
+
files = collectJsonlFiles(smokeDir);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (files.length === 0) {
|
|
144
|
+
console.error(`validate-smoke-jsonl: no JSONL files under ${smokeDir}`);
|
|
145
|
+
process.exit(2);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let failures = 0;
|
|
149
|
+
if (replayErrorsOnly) {
|
|
150
|
+
let replayHitCount = 0;
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
const { records } = parseJsonlFile(file);
|
|
153
|
+
const hits = scanReplayErrors(file, records);
|
|
154
|
+
replayHitCount += hits.length;
|
|
155
|
+
if (hits.length > 0) failures += 1;
|
|
156
|
+
console.log(
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
file: relative(smokeDir, file),
|
|
159
|
+
replayErrorCount: hits.length,
|
|
160
|
+
replayErrors: hits.slice(0, 5),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
let summary;
|
|
169
|
+
try {
|
|
170
|
+
const { lineCount, records, parseErrorCount } = parseJsonlFile(file);
|
|
171
|
+
const messages = records.filter((record) => record.type === "message").map((record) => record.message);
|
|
172
|
+
const assistants = messages.filter((message) => message?.role === "assistant");
|
|
173
|
+
const usage = assistants.map((message) => message.usage).filter(Boolean);
|
|
174
|
+
const badUsage = assistants.map((message) => message.usage).filter(isBadUsage);
|
|
175
|
+
const replayHits = replayErrors ? scanReplayErrors(file, records) : [];
|
|
176
|
+
const fileFailure =
|
|
177
|
+
lineCount === 0 ||
|
|
178
|
+
parseErrorCount > 0 ||
|
|
179
|
+
assistants.length === 0 ||
|
|
180
|
+
usage.length !== assistants.length ||
|
|
181
|
+
badUsage.length > 0 ||
|
|
182
|
+
replayHits.length > 0;
|
|
183
|
+
if (fileFailure) failures += 1;
|
|
184
|
+
summary = {
|
|
185
|
+
file: relative(smokeDir, file),
|
|
186
|
+
lineCount,
|
|
187
|
+
parseErrorCount,
|
|
188
|
+
messageCount: messages.length,
|
|
189
|
+
assistantCount: assistants.length,
|
|
190
|
+
usageCount: usage.length,
|
|
191
|
+
badUsageCount: badUsage.length,
|
|
192
|
+
replayErrorCount: replayHits.length,
|
|
193
|
+
};
|
|
194
|
+
} catch (error) {
|
|
195
|
+
failures += 1;
|
|
196
|
+
summary = {
|
|
197
|
+
file: relative(smokeDir, file),
|
|
198
|
+
readError: error instanceof Error ? error.message : String(error),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
console.log(JSON.stringify(summary));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
main();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Context } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
/** Tool names from the provider context snapshot at stream start (not live pi.getActiveTools()). */
|
|
4
|
+
export function getActiveContextToolNames(context: Context): ReadonlySet<string> | undefined {
|
|
5
|
+
return context.tools ? new Set(context.tools.map((tool) => tool.name)) : undefined;
|
|
6
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Canonical single-line sanitization and truncation for Cursor replay/trace display. */
|
|
2
|
+
export function sanitizeCursorDisplayLine(value: string): string {
|
|
3
|
+
return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function truncateCursorDisplayLine(value: string, maxLength = 240): string {
|
|
7
|
+
const sanitized = sanitizeCursorDisplayLine(value);
|
|
8
|
+
if (sanitized.length <= maxLength) return sanitized;
|
|
9
|
+
return `${sanitized.slice(0, maxLength - 1)}…`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const CURSOR_EDIT_DIFF_FIELD_ORDER = ["diffString", "diff", "unifiedDiff", "patch"] as const;
|
|
2
|
+
|
|
3
|
+
export function resolveCursorEditDiff(source: unknown): string | undefined {
|
|
4
|
+
if (!source || typeof source !== "object") return undefined;
|
|
5
|
+
const record = source as Record<string, unknown>;
|
|
6
|
+
for (const key of CURSOR_EDIT_DIFF_FIELD_ORDER) {
|
|
7
|
+
const value = record[key];
|
|
8
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const DISABLED_ENV_VALUES = new Set(["0", "false", "off", "none", "no", "disabled"]);
|
|
2
|
+
const ENABLED_ENV_VALUES = new Set(["1", "true", "on", "yes", "enabled"]);
|
|
3
|
+
|
|
4
|
+
function normalizeEnvBoolean(raw: string | undefined): string | undefined {
|
|
5
|
+
const normalized = raw?.trim().toLowerCase();
|
|
6
|
+
return normalized || undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseOptionalEnvBoolean(raw: string | undefined): boolean | undefined {
|
|
10
|
+
const normalized = normalizeEnvBoolean(raw);
|
|
11
|
+
if (!normalized) return undefined;
|
|
12
|
+
if (DISABLED_ENV_VALUES.has(normalized)) return false;
|
|
13
|
+
if (ENABLED_ENV_VALUES.has(normalized)) return true;
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseEnvBoolean(
|
|
18
|
+
raw: string | undefined,
|
|
19
|
+
defaultValue: boolean,
|
|
20
|
+
): boolean {
|
|
21
|
+
return parseOptionalEnvBoolean(raw) ?? defaultValue;
|
|
22
|
+
}
|