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.
Files changed (42) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/README.md +2 -2
  3. package/docs/cursor-live-smoke-checklist.md +54 -41
  4. package/docs/cursor-model-ux-spec.md +4 -3
  5. package/docs/cursor-testing-lessons.md +199 -0
  6. package/package.json +14 -5
  7. package/scripts/isolated-cursor-smoke.sh +226 -0
  8. package/scripts/steering-rpc-smoke.mjs +238 -0
  9. package/scripts/tmux-live-smoke.sh +418 -0
  10. package/scripts/validate-smoke-jsonl.mjs +207 -0
  11. package/src/cursor-context-tools.ts +6 -0
  12. package/src/cursor-display-text.ts +10 -0
  13. package/src/cursor-edit-diff.ts +11 -0
  14. package/src/cursor-env-boolean.ts +22 -0
  15. package/src/cursor-live-run-coordinator.ts +483 -0
  16. package/src/cursor-native-replay-routing.ts +48 -0
  17. package/src/cursor-native-replay-trace.ts +29 -0
  18. package/src/cursor-native-tool-display-registration.ts +103 -0
  19. package/src/cursor-native-tool-display-replay.ts +465 -0
  20. package/src/cursor-native-tool-display-state.ts +78 -0
  21. package/src/cursor-native-tool-display-tools.ts +102 -0
  22. package/src/cursor-native-tool-display.ts +10 -648
  23. package/src/cursor-partial-content-emitter.ts +121 -0
  24. package/src/cursor-pi-tool-bridge-abort.ts +133 -0
  25. package/src/cursor-pi-tool-bridge-diagnostics.ts +179 -0
  26. package/src/cursor-pi-tool-bridge-mcp.ts +118 -0
  27. package/src/cursor-pi-tool-bridge-run.ts +384 -0
  28. package/src/cursor-pi-tool-bridge-server.ts +182 -0
  29. package/src/cursor-pi-tool-bridge-snapshot.ts +88 -0
  30. package/src/cursor-pi-tool-bridge-types.ts +80 -0
  31. package/src/cursor-pi-tool-bridge.ts +42 -1104
  32. package/src/cursor-provider-live-run-drain.ts +405 -0
  33. package/src/cursor-provider-turn-coordinator.ts +460 -0
  34. package/src/cursor-provider.ts +77 -1103
  35. package/src/cursor-question-tool.ts +9 -1
  36. package/src/cursor-record-utils.ts +26 -0
  37. package/src/cursor-sdk-output-filter.ts +100 -0
  38. package/src/cursor-sensitive-text.ts +37 -0
  39. package/src/cursor-tool-transcript.ts +28 -1229
  40. package/src/cursor-transcript-tool-formatters.ts +641 -0
  41. package/src/cursor-transcript-tool-specs.ts +441 -0
  42. 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
+ }