pi-cursor-sdk 0.1.16 → 0.1.17

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 (35) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +1 -1
  3. package/docs/cursor-live-smoke-checklist.md +35 -39
  4. package/docs/cursor-model-ux-spec.md +3 -2
  5. package/package.json +11 -5
  6. package/scripts/steering-rpc-smoke.mjs +238 -0
  7. package/scripts/tmux-live-smoke.sh +418 -0
  8. package/scripts/validate-smoke-jsonl.mjs +152 -0
  9. package/src/cursor-edit-diff.ts +11 -0
  10. package/src/cursor-env-boolean.ts +22 -0
  11. package/src/cursor-live-run-coordinator.ts +483 -0
  12. package/src/cursor-native-tool-display-registration.ts +93 -0
  13. package/src/cursor-native-tool-display-replay.ts +465 -0
  14. package/src/cursor-native-tool-display-state.ts +78 -0
  15. package/src/cursor-native-tool-display-tools.ts +102 -0
  16. package/src/cursor-native-tool-display.ts +10 -648
  17. package/src/cursor-partial-content-emitter.ts +121 -0
  18. package/src/cursor-pi-tool-bridge-abort.ts +133 -0
  19. package/src/cursor-pi-tool-bridge-diagnostics.ts +179 -0
  20. package/src/cursor-pi-tool-bridge-mcp.ts +118 -0
  21. package/src/cursor-pi-tool-bridge-run.ts +384 -0
  22. package/src/cursor-pi-tool-bridge-server.ts +182 -0
  23. package/src/cursor-pi-tool-bridge-snapshot.ts +88 -0
  24. package/src/cursor-pi-tool-bridge-types.ts +80 -0
  25. package/src/cursor-pi-tool-bridge.ts +42 -1104
  26. package/src/cursor-provider-live-run-drain.ts +379 -0
  27. package/src/cursor-provider-turn-coordinator.ts +456 -0
  28. package/src/cursor-provider.ts +72 -1103
  29. package/src/cursor-record-utils.ts +26 -0
  30. package/src/cursor-sdk-output-filter.ts +100 -0
  31. package/src/cursor-sensitive-text.ts +37 -0
  32. package/src/cursor-tool-transcript.ts +28 -1229
  33. package/src/cursor-transcript-tool-formatters.ts +641 -0
  34. package/src/cursor-transcript-tool-specs.ts +441 -0
  35. 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,152 @@
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
+ function printHelp() {
9
+ console.log(`Validate assistant presence and usage metadata in pi smoke session JSONL files.
10
+
11
+ Usage:
12
+ node scripts/validate-smoke-jsonl.mjs <smoke-dir>
13
+ SMOKE_DIR=/tmp/pi-cursor-smoke node scripts/validate-smoke-jsonl.mjs
14
+
15
+ Arguments:
16
+ smoke-dir Directory containing smoke session subdirs and JSONL files.
17
+ Defaults to SMOKE_DIR when the positional arg is omitted.
18
+
19
+ Options:
20
+ -h, --help Show this help.
21
+
22
+ Exit codes:
23
+ 0 every scanned JSONL file has at least one assistant message and valid assistant usage metadata
24
+ 1 invalid arguments, unreadable directory, invalid JSONL, empty/no-assistant files, or usage validation failures
25
+ 2 no JSONL files found under the smoke directory
26
+
27
+ Enforced invariants:
28
+ - each scanned JSONL file contains parseable JSONL records
29
+ - each scanned JSONL file contains at least one persisted assistant message
30
+ - every persisted assistant message has usage metadata
31
+ - assistant usage input/output/totalTokens are non-negative numbers
32
+ - assistant usage cacheRead/cacheWrite are exactly 0
33
+
34
+ Notes:
35
+ - Prints one JSON summary line per scanned session file.
36
+ - Does not print session message contents or secrets.`);
37
+ }
38
+
39
+ function fail(message) {
40
+ console.error(`validate-smoke-jsonl: ${message}`);
41
+ process.exit(1);
42
+ }
43
+
44
+ function collectJsonlFiles(root) {
45
+ const files = [];
46
+ function walk(dir) {
47
+ for (const name of readdirSync(dir)) {
48
+ const path = join(dir, name);
49
+ const st = statSync(path);
50
+ if (st.isDirectory()) walk(path);
51
+ else if (path.endsWith(".jsonl")) files.push(path);
52
+ }
53
+ }
54
+ walk(root);
55
+ return files.sort();
56
+ }
57
+
58
+ function isNonNegativeNumber(value) {
59
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
60
+ }
61
+
62
+ function isBadUsage(usage) {
63
+ return (
64
+ !usage ||
65
+ typeof usage !== "object" ||
66
+ !isNonNegativeNumber(usage.input) ||
67
+ !isNonNegativeNumber(usage.output) ||
68
+ !isNonNegativeNumber(usage.totalTokens) ||
69
+ usage.cacheRead !== 0 ||
70
+ usage.cacheWrite !== 0
71
+ );
72
+ }
73
+
74
+ function parseJsonlFile(file) {
75
+ const lines = readFileSync(file, "utf8")
76
+ .split(/\r?\n/)
77
+ .map((line) => line.trim())
78
+ .filter(Boolean);
79
+ const records = [];
80
+ let parseErrorCount = 0;
81
+ for (const line of lines) {
82
+ try {
83
+ records.push(JSON.parse(line));
84
+ } catch {
85
+ parseErrorCount += 1;
86
+ }
87
+ }
88
+ return { lineCount: lines.length, records, parseErrorCount };
89
+ }
90
+
91
+ function main() {
92
+ const args = process.argv.slice(2);
93
+ if (args.includes("-h") || args.includes("--help")) {
94
+ printHelp();
95
+ return;
96
+ }
97
+
98
+ if (args.length > 1) {
99
+ fail("too many arguments; pass only the smoke directory");
100
+ }
101
+
102
+ const smokeDir = args[0] ?? process.env.SMOKE_DIR;
103
+ if (!smokeDir) {
104
+ fail("missing smoke directory; pass a path or set SMOKE_DIR");
105
+ }
106
+
107
+ let files;
108
+ try {
109
+ files = collectJsonlFiles(smokeDir);
110
+ } catch (error) {
111
+ fail(error instanceof Error ? error.message : String(error));
112
+ }
113
+
114
+ if (files.length === 0) {
115
+ console.error(`validate-smoke-jsonl: no JSONL files under ${smokeDir}`);
116
+ process.exit(2);
117
+ }
118
+
119
+ let failures = 0;
120
+ for (const file of files) {
121
+ let summary;
122
+ try {
123
+ const { lineCount, records, parseErrorCount } = parseJsonlFile(file);
124
+ const messages = records.filter((record) => record.type === "message").map((record) => record.message);
125
+ const assistants = messages.filter((message) => message?.role === "assistant");
126
+ const usage = assistants.map((message) => message.usage).filter(Boolean);
127
+ const badUsage = assistants.map((message) => message.usage).filter(isBadUsage);
128
+ const fileFailure = lineCount === 0 || parseErrorCount > 0 || assistants.length === 0 || usage.length !== assistants.length || badUsage.length > 0;
129
+ if (fileFailure) failures += 1;
130
+ summary = {
131
+ file: relative(smokeDir, file),
132
+ lineCount,
133
+ parseErrorCount,
134
+ messageCount: messages.length,
135
+ assistantCount: assistants.length,
136
+ usageCount: usage.length,
137
+ badUsageCount: badUsage.length,
138
+ };
139
+ } catch (error) {
140
+ failures += 1;
141
+ summary = {
142
+ file: relative(smokeDir, file),
143
+ readError: error instanceof Error ? error.message : String(error),
144
+ };
145
+ }
146
+ console.log(JSON.stringify(summary));
147
+ }
148
+
149
+ process.exit(failures === 0 ? 0 : 1);
150
+ }
151
+
152
+ main();
@@ -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
+ }