pi-cursor-sdk 0.1.28 → 0.1.29
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 +19 -0
- package/README.md +38 -35
- package/docs/crabbox-platform-testing-lessons.md +508 -0
- package/docs/cursor-dogfood-checklist.md +4 -3
- package/docs/cursor-live-smoke-checklist.md +22 -20
- package/docs/cursor-model-ux-spec.md +12 -12
- package/docs/cursor-native-tool-replay.md +10 -10
- package/docs/cursor-native-tool-visual-audit.md +9 -7
- package/docs/cursor-testing-lessons.md +20 -15
- package/docs/cursor-tool-surfaces.md +3 -3
- package/docs/platform-smoke.md +994 -0
- package/package.json +32 -3
- package/platform-smoke.config.mjs +21 -0
- package/scripts/debug-provider-events.mjs +10 -3
- package/scripts/debug-sdk-events.mjs +10 -2
- package/scripts/isolated-cursor-smoke.sh +4 -4
- package/scripts/lib/cursor-visual-render.mjs +1 -0
- package/scripts/platform-smoke/artifacts.mjs +124 -0
- package/scripts/platform-smoke/assertions.mjs +101 -0
- package/scripts/platform-smoke/card-detect.mjs +96 -0
- package/scripts/platform-smoke/crabbox-runner.mjs +215 -0
- package/scripts/platform-smoke/doctor.mjs +446 -0
- package/scripts/platform-smoke/jsonl-text.mjs +31 -0
- package/scripts/platform-smoke/live-suite-runner.mjs +677 -0
- package/scripts/platform-smoke/platform-build-windows.ps1 +187 -0
- package/scripts/platform-smoke/pty-capture.mjs +131 -0
- package/scripts/platform-smoke/render-ansi.mjs +65 -0
- package/scripts/platform-smoke/scenarios.mjs +186 -0
- package/scripts/platform-smoke/targets.mjs +900 -0
- package/scripts/platform-smoke/visual-evidence.mjs +139 -0
- package/scripts/platform-smoke.mjs +193 -0
- package/scripts/probe-mcp-coldstart.mjs +8 -1
- package/scripts/steering-rpc-smoke.mjs +1 -1
- package/scripts/tmux-live-smoke.sh +3 -3
- package/scripts/visual-tui-smoke.mjs +1 -1
- package/src/cursor-pi-tool-bridge-abort.ts +1 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +12 -1
- package/src/cursor-pi-tool-bridge.ts +46 -1
- package/src/cursor-provider-turn-lifecycle-emitter.ts +65 -8
- package/src/cursor-provider-turn-tool-ledger.ts +2 -3
- package/src/cursor-run-final-text.ts +11 -1
- package/src/cursor-state.ts +38 -19
- package/src/cursor-tool-lifecycle.ts +1 -1
- package/src/cursor-tool-manifest.ts +1 -1
- package/src/cursor-transcript-utils.ts +7 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cursor-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "pi provider extension backed by @cursor/sdk local agents",
|
|
5
5
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,21 @@
|
|
|
36
36
|
"scripts/debug-sdk-events.d.mts",
|
|
37
37
|
"scripts/debug-provider-events.mjs",
|
|
38
38
|
"scripts/debug-provider-events.d.mts",
|
|
39
|
+
"platform-smoke.config.mjs",
|
|
40
|
+
"scripts/platform-smoke.mjs",
|
|
41
|
+
"scripts/platform-smoke/assertions.mjs",
|
|
42
|
+
"scripts/platform-smoke/artifacts.mjs",
|
|
43
|
+
"scripts/platform-smoke/card-detect.mjs",
|
|
44
|
+
"scripts/platform-smoke/crabbox-runner.mjs",
|
|
45
|
+
"scripts/platform-smoke/doctor.mjs",
|
|
46
|
+
"scripts/platform-smoke/live-suite-runner.mjs",
|
|
47
|
+
"scripts/platform-smoke/jsonl-text.mjs",
|
|
48
|
+
"scripts/platform-smoke/platform-build-windows.ps1",
|
|
49
|
+
"scripts/platform-smoke/pty-capture.mjs",
|
|
50
|
+
"scripts/platform-smoke/render-ansi.mjs",
|
|
51
|
+
"scripts/platform-smoke/scenarios.mjs",
|
|
52
|
+
"scripts/platform-smoke/targets.mjs",
|
|
53
|
+
"scripts/platform-smoke/visual-evidence.mjs",
|
|
39
54
|
"scripts/lib/cursor-cli-args.mjs",
|
|
40
55
|
"scripts/lib/cursor-cli-args.d.mts",
|
|
41
56
|
"scripts/lib/cursor-child-process.mjs",
|
|
@@ -54,9 +69,11 @@
|
|
|
54
69
|
"docs/cursor-tool-surfaces.md",
|
|
55
70
|
"docs/cursor-live-smoke-checklist.md",
|
|
56
71
|
"docs/cursor-testing-lessons.md",
|
|
72
|
+
"docs/crabbox-platform-testing-lessons.md",
|
|
57
73
|
"docs/cursor-dogfood-checklist.md",
|
|
58
74
|
"docs/cursor-native-tool-replay.md",
|
|
59
75
|
"docs/cursor-native-tool-visual-audit.md",
|
|
76
|
+
"docs/platform-smoke.md",
|
|
60
77
|
"LICENSE",
|
|
61
78
|
"CHANGELOG.md"
|
|
62
79
|
],
|
|
@@ -69,6 +86,7 @@
|
|
|
69
86
|
"typecheck:src": "tsc --noEmit",
|
|
70
87
|
"typecheck:tests": "tsc -p tsconfig.test.json --noEmit",
|
|
71
88
|
"typecheck:replay-compile": "tsc --noEmit -p test/tsconfig.json",
|
|
89
|
+
"verify": "npm run check:platform-smoke && npm run typecheck && npm test",
|
|
72
90
|
"test": "vitest run",
|
|
73
91
|
"test:watch": "vitest",
|
|
74
92
|
"refresh:cursor-snapshots": "node scripts/refresh-cursor-model-snapshots.mjs",
|
|
@@ -79,10 +97,17 @@
|
|
|
79
97
|
"smoke:jsonl": "node scripts/validate-smoke-jsonl.mjs",
|
|
80
98
|
"debug:sdk-events": "node scripts/debug-sdk-events.mjs",
|
|
81
99
|
"debug:provider-events": "node scripts/debug-provider-events.mjs",
|
|
82
|
-
"debug:mcp-coldstart": "node scripts/probe-mcp-coldstart.mjs"
|
|
100
|
+
"debug:mcp-coldstart": "node scripts/probe-mcp-coldstart.mjs",
|
|
101
|
+
"check:platform-smoke": "node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/assertions.mjs && node --check scripts/platform-smoke/artifacts.mjs && node --check scripts/platform-smoke/card-detect.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/jsonl-text.mjs && node --check scripts/platform-smoke/live-suite-runner.mjs && node --check scripts/platform-smoke/pty-capture.mjs && node --check scripts/platform-smoke/render-ansi.mjs && node --check scripts/platform-smoke/scenarios.mjs && node --check scripts/platform-smoke/targets.mjs && node --check scripts/platform-smoke/visual-evidence.mjs && vitest run test/smoke-tooling.test.ts",
|
|
102
|
+
"smoke:platform": "node scripts/platform-smoke.mjs",
|
|
103
|
+
"smoke:platform:doctor": "node scripts/platform-smoke.mjs doctor",
|
|
104
|
+
"smoke:platform:macos": "node scripts/platform-smoke.mjs run --target macos",
|
|
105
|
+
"smoke:platform:ubuntu": "node scripts/platform-smoke.mjs run --target ubuntu",
|
|
106
|
+
"smoke:platform:windows-native": "node scripts/platform-smoke.mjs run --target windows-native",
|
|
107
|
+
"smoke:platform:all": "npm run smoke:platform:doctor && node scripts/platform-smoke.mjs run --target macos,ubuntu,windows-native"
|
|
83
108
|
},
|
|
84
109
|
"dependencies": {
|
|
85
|
-
"@cursor/sdk": "1.0.
|
|
110
|
+
"@cursor/sdk": "1.0.17",
|
|
86
111
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
87
112
|
},
|
|
88
113
|
"peerDependencies": {
|
|
@@ -96,6 +121,7 @@
|
|
|
96
121
|
"@earendil-works/pi-coding-agent": "0.78.0",
|
|
97
122
|
"@earendil-works/pi-tui": "0.78.0",
|
|
98
123
|
"@xterm/xterm": "^6.0.0",
|
|
124
|
+
"node-pty": "^1.1.0",
|
|
99
125
|
"playwright": "^1.60.0",
|
|
100
126
|
"typebox": "^1.1.38",
|
|
101
127
|
"typescript": "^6.0.3",
|
|
@@ -109,5 +135,8 @@
|
|
|
109
135
|
"overrides": {
|
|
110
136
|
"undici": "7.25.0",
|
|
111
137
|
"sqlite3": "6.0.1"
|
|
138
|
+
},
|
|
139
|
+
"allowScripts": {
|
|
140
|
+
"node-pty@1.1.0": true
|
|
112
141
|
}
|
|
113
142
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Platform smoke configuration for pi-cursor-sdk.
|
|
2
|
+
// Reusable across pi extensions: change package name, model IDs, scenarios, and card matrix only.
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
packageName: "pi-cursor-sdk",
|
|
6
|
+
cursorModel: "cursor/composer-2-5",
|
|
7
|
+
artifactRoot: ".artifacts/platform-smoke",
|
|
8
|
+
requiredTargets: ["macos", "ubuntu", "windows-native"],
|
|
9
|
+
requiredSuites: [
|
|
10
|
+
"platform-build",
|
|
11
|
+
"cursor-native-visual-matrix",
|
|
12
|
+
"cursor-bridge-visual-matrix",
|
|
13
|
+
"cursor-abort-cleanup",
|
|
14
|
+
],
|
|
15
|
+
requiredCrabbox: {
|
|
16
|
+
install: "brew install crabbox",
|
|
17
|
+
version: "0.24.0",
|
|
18
|
+
},
|
|
19
|
+
ubuntuContainerImage: "cimg/node:24.16",
|
|
20
|
+
nodeValidationMajor: 24,
|
|
21
|
+
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
|
-
import { dirname, join } from "node:path";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import {
|
|
11
11
|
apiKeySecretsFromProcess,
|
|
@@ -20,10 +20,17 @@ import { scrubSensitiveText } from "../shared/cursor-sensitive-text.mjs";
|
|
|
20
20
|
import { createScriptFail } from "./lib/cursor-script-fail.mjs";
|
|
21
21
|
import { serializeCursorSettingSources } from "../shared/cursor-setting-sources.mjs";
|
|
22
22
|
|
|
23
|
+
function isMainModule() {
|
|
24
|
+
if (!process.argv[1]) return false;
|
|
25
|
+
const current = fileURLToPath(import.meta.url);
|
|
26
|
+
const invoked = resolve(process.argv[1]);
|
|
27
|
+
return process.platform === "win32" ? current.toLowerCase() === invoked.toLowerCase() : current === invoked;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
const require = createRequire(import.meta.url);
|
|
24
31
|
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
25
32
|
const packageJson = require("../package.json");
|
|
26
|
-
const DEFAULT_MODEL = "cursor/composer-2
|
|
33
|
+
const DEFAULT_MODEL = "cursor/composer-2-5";
|
|
27
34
|
const DEFAULT_OUT_BASE = ".debug/cursor-sdk-events";
|
|
28
35
|
const SDK_EVENT_DEBUG_LOG_PREFIX = "[pi-cursor-sdk:sdk-events]";
|
|
29
36
|
const PI_SESSION_SNAPSHOT_ARTIFACT = "pi-session-snapshot.jsonl";
|
|
@@ -291,7 +298,7 @@ async function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
291
298
|
console.log(JSON.stringify(await runDebugProviderEvents(args, env)));
|
|
292
299
|
}
|
|
293
300
|
|
|
294
|
-
if (
|
|
301
|
+
if (isMainModule()) {
|
|
295
302
|
main().catch((error) => {
|
|
296
303
|
fail(error instanceof Error ? error.message : String(error), apiKeySecretsFromProcess());
|
|
297
304
|
});
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
|
-
import {
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
10
|
import {
|
|
10
11
|
apiKeySecretsFromProcess,
|
|
11
12
|
commonProbeFlags,
|
|
@@ -18,6 +19,13 @@ import {
|
|
|
18
19
|
import { createScriptFail } from "./lib/cursor-script-fail.mjs";
|
|
19
20
|
import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./lib/cursor-sdk-output-filter.mjs";
|
|
20
21
|
|
|
22
|
+
function isMainModule() {
|
|
23
|
+
if (!process.argv[1]) return false;
|
|
24
|
+
const current = fileURLToPath(import.meta.url);
|
|
25
|
+
const invoked = resolve(process.argv[1]);
|
|
26
|
+
return process.platform === "win32" ? current.toLowerCase() === invoked.toLowerCase() : current === invoked;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
const require = createRequire(import.meta.url);
|
|
22
30
|
const packageJson = require("../package.json");
|
|
23
31
|
|
|
@@ -343,7 +351,7 @@ async function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
343
351
|
await captureEvents(args);
|
|
344
352
|
}
|
|
345
353
|
|
|
346
|
-
if (
|
|
354
|
+
if (isMainModule()) {
|
|
347
355
|
main().catch((error) => {
|
|
348
356
|
const message = error instanceof Error ? error.message : String(error);
|
|
349
357
|
fail(message, apiKeySecretsFromProcess());
|
|
@@ -361,13 +361,13 @@ log "check: list-models"
|
|
|
361
361
|
LIST_OUT="$ISOLATED/list-models.txt"
|
|
362
362
|
run_in_dir_capture_combined "list-models" 30 "$PROJECT_DIR" "$LIST_OUT" "${PI_CURSOR_ENV[@]}" \
|
|
363
363
|
"$PI_BIN" --cursor-no-fast --list-models cursor
|
|
364
|
-
"$RG_BIN" -q "composer-2\\.5|composer-2-5" "$LIST_OUT" || fail "composer-2
|
|
364
|
+
"$RG_BIN" -q "composer-2\\.5|composer-2-5" "$LIST_OUT" || fail "composer-2-5 not listed (see $LIST_OUT)"
|
|
365
365
|
|
|
366
366
|
log "check: basic provider prompt"
|
|
367
367
|
BASIC_DIR="$SESSION_ROOT/basic"
|
|
368
368
|
mkdir -p "$BASIC_DIR"
|
|
369
369
|
run_in_dir_capture_split "basic prompt" "$PI_LIVE_TIMEOUT" "$PROJECT_DIR" "$ISOLATED/basic.stdout.txt" "$ISOLATED/basic.stderr.txt" "${PI_CURSOR_ENV[@]}" \
|
|
370
|
-
"$PI_BIN" --cursor-no-fast --model cursor/composer-2
|
|
370
|
+
"$PI_BIN" --cursor-no-fast --model cursor/composer-2-5 --session-dir "$BASIC_DIR" --no-tools -p 'Reply exactly: PI_CURSOR_ISOLATED_OK'
|
|
371
371
|
"$RG_BIN" -q "PI_CURSOR_ISOLATED_OK" "$ISOLATED/basic.stdout.txt" || fail "basic prompt missing PI_CURSOR_ISOLATED_OK"
|
|
372
372
|
validate_replay_jsonl "$BASIC_DIR"
|
|
373
373
|
|
|
@@ -375,14 +375,14 @@ log "check: native replay"
|
|
|
375
375
|
REPLAY_DIR="$SESSION_ROOT/native-replay"
|
|
376
376
|
mkdir -p "$REPLAY_DIR"
|
|
377
377
|
run_in_dir_capture_split "native replay" "$PI_LIVE_TIMEOUT" "$PROJECT_DIR" "$ISOLATED/replay.stdout.txt" "$ISOLATED/replay.stderr.txt" "${PI_CURSOR_ENV[@]}" PI_CURSOR_NATIVE_TOOL_DISPLAY=1 \
|
|
378
|
-
"$PI_BIN" --cursor-no-fast --model cursor/composer-2
|
|
378
|
+
"$PI_BIN" --cursor-no-fast --model cursor/composer-2-5 --session-dir "$REPLAY_DIR" -p 'Read ./README.md briefly, then answer README_SEEN=yes if it mentions pi-cursor-sdk.'
|
|
379
379
|
validate_replay_jsonl "$REPLAY_DIR"
|
|
380
380
|
|
|
381
381
|
log "check: plan-strip shim (plan-mode execute reset)"
|
|
382
382
|
PLAN_DIR="$SESSION_ROOT/plan-strip"
|
|
383
383
|
mkdir -p "$PLAN_DIR"
|
|
384
384
|
run_in_dir_capture_split "plan-strip replay" "$PI_LIVE_TIMEOUT" "$PROJECT_DIR" "$ISOLATED/plan.stdout.txt" "$ISOLATED/plan.stderr.txt" "${PI_CURSOR_ENV[@]}" PI_CURSOR_NATIVE_TOOL_DISPLAY=1 \
|
|
385
|
-
"$PI_BIN" -e "$SHIM_DIR" --cursor-no-fast --model cursor/composer-2
|
|
385
|
+
"$PI_BIN" -e "$SHIM_DIR" --cursor-no-fast --model cursor/composer-2-5 --session-dir "$PLAN_DIR" -p 'After reset, read README.md and answer PLAN_STRIP_OK=yes.'
|
|
386
386
|
validate_replay_jsonl "$PLAN_DIR"
|
|
387
387
|
|
|
388
388
|
log "PASS isolated install smoke: $ISOLATED"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact management — directory layout, manifest, redaction scanning, packaging.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
6
|
+
import { resolve, relative, basename } from "node:path";
|
|
7
|
+
|
|
8
|
+
/** Create a suite artifact directory. */
|
|
9
|
+
export function createSuiteDir(artifactRoot, runId, targetName, suiteName) {
|
|
10
|
+
const dir = resolve(process.cwd(), artifactRoot, runId, targetName, suiteName);
|
|
11
|
+
mkdirSync(dir, { recursive: true });
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Write artifact-manifest.json. */
|
|
16
|
+
export function writeManifest(dir, expectedFiles) {
|
|
17
|
+
const actual = [];
|
|
18
|
+
function walk(d) {
|
|
19
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
20
|
+
const fp = resolve(d, entry.name);
|
|
21
|
+
if (entry.isDirectory()) walk(fp);
|
|
22
|
+
else if (entry.isFile()) actual.push(relative(dir, fp));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (existsSync(dir)) walk(dir);
|
|
26
|
+
|
|
27
|
+
const manifest = {
|
|
28
|
+
expected: expectedFiles ?? [],
|
|
29
|
+
present: actual,
|
|
30
|
+
missing: (expectedFiles ?? []).filter(f => !actual.includes(f)),
|
|
31
|
+
writtenAt: new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
writeFileSync(resolve(dir, "artifact-manifest.json"), JSON.stringify(manifest, null, 2));
|
|
34
|
+
return manifest;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SECRET_PATTERNS = [
|
|
38
|
+
[/Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}=*/gi, "Authorization header", "Authorization: Bearer [REDACTED_BEARER_TOKEN]"],
|
|
39
|
+
[/(bearer\s+)[A-Za-z0-9\-._~+/]{20,}=*/gi, "bearer token", "$1[REDACTED_BEARER_TOKEN]"],
|
|
40
|
+
[/connect\.sid=[A-Za-z0-9%]+/gi, "session cookie", "connect.sid=[REDACTED_SESSION_COOKIE]"],
|
|
41
|
+
[/https?:\/\/[^/\s]*\/cursor-pi-tool-bridge\/[A-Za-z0-9_.:-]+\/mcp/gi, "bridge endpoint URL", "[REDACTED_BRIDGE_ENDPOINT_URL]"],
|
|
42
|
+
[/"(apiKey|accessToken|refreshToken|session|cookie)"\s*:\s*"[^"\s]{12,}"/gi, "auth/token JSON field", '"$1":"[REDACTED_SECRET]"'],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Redact known secret material before writing logs/artifacts. */
|
|
46
|
+
export function redactSecrets(text) {
|
|
47
|
+
let redacted = String(text ?? "");
|
|
48
|
+
const cursorKey = process.env.CURSOR_API_KEY;
|
|
49
|
+
if (cursorKey && cursorKey.length > 10) {
|
|
50
|
+
redacted = redacted.split(cursorKey).join("[REDACTED_CURSOR_API_KEY]");
|
|
51
|
+
}
|
|
52
|
+
for (const [pattern, , replacement] of SECRET_PATTERNS) {
|
|
53
|
+
redacted = redacted.replace(pattern, replacement);
|
|
54
|
+
}
|
|
55
|
+
return redacted;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Scan text content for secrets. Returns array of violation descriptions. */
|
|
59
|
+
export function scanForSecrets(text) {
|
|
60
|
+
const violations = [];
|
|
61
|
+
const cursorKey = process.env.CURSOR_API_KEY;
|
|
62
|
+
if (cursorKey && cursorKey.length > 10 && String(text ?? "").includes(cursorKey)) {
|
|
63
|
+
violations.push("CURSOR_API_KEY literal found");
|
|
64
|
+
}
|
|
65
|
+
for (const [pattern, label] of SECRET_PATTERNS) {
|
|
66
|
+
pattern.lastIndex = 0;
|
|
67
|
+
if (pattern.test(String(text ?? ""))) violations.push(`potential ${label}`);
|
|
68
|
+
}
|
|
69
|
+
return violations;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Scan all text files in a directory for secrets. */
|
|
73
|
+
export function scanArtifacts(dir) {
|
|
74
|
+
const findings = [];
|
|
75
|
+
function walk(d) {
|
|
76
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
77
|
+
const fp = resolve(d, entry.name);
|
|
78
|
+
if (entry.isDirectory()) { walk(fp); continue; }
|
|
79
|
+
if (!entry.isFile()) continue;
|
|
80
|
+
const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
81
|
+
if (!["txt", "json", "jsonl", "md", "log", "ansi", "html", "yml", "yaml", "js", "mjs", "ts"].includes(ext)) continue;
|
|
82
|
+
try {
|
|
83
|
+
const content = readFileSync(fp, "utf-8");
|
|
84
|
+
const violations = scanForSecrets(content);
|
|
85
|
+
for (const v of violations) {
|
|
86
|
+
findings.push({ file: relative(dir, fp), violation: v });
|
|
87
|
+
}
|
|
88
|
+
} catch { /* binary or unreadable */ }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
walk(dir);
|
|
92
|
+
return findings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Write summary.json for a suite. */
|
|
96
|
+
export function writeSummary(dir, data) {
|
|
97
|
+
writeFileSync(resolve(dir, "summary.json"), JSON.stringify({
|
|
98
|
+
...data,
|
|
99
|
+
writtenAt: new Date().toISOString(),
|
|
100
|
+
}, null, 2));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Write command.txt recording the command that was executed. */
|
|
104
|
+
export function writeCommand(dir, cmd) {
|
|
105
|
+
writeFileSync(resolve(dir, "command.txt"), Array.isArray(cmd) ? cmd.join(" ") + "\n" : cmd + "\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Write exit-code.txt. */
|
|
109
|
+
export function writeExitCode(dir, code, signal) {
|
|
110
|
+
writeFileSync(resolve(dir, "exit-code.txt"), `code=${code}\nsignal=${signal ?? "none"}\n`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Package a directory as tar.gz (posix) or zip (powershell). */
|
|
114
|
+
export async function packageArtifacts(dir, archivePath) {
|
|
115
|
+
const { execSync } = await import("node:child_process");
|
|
116
|
+
const dirName = basename(dir);
|
|
117
|
+
const parentDir = resolve(dir, "..");
|
|
118
|
+
if (archivePath.endsWith(".tar.gz")) {
|
|
119
|
+
execSync(`tar -czf "${archivePath}" -C "${parentDir}" "${dirName}"`, { stdio: "pipe" });
|
|
120
|
+
} else if (archivePath.endsWith(".zip")) {
|
|
121
|
+
execSync(`cd "${parentDir}" && zip -r "${archivePath}" "${dirName}"`, { stdio: "pipe" });
|
|
122
|
+
}
|
|
123
|
+
return archivePath;
|
|
124
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assertion engine — assertions.json, failures.md, JSONL parsing.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
6
|
+
import { resolve, basename } from "node:path";
|
|
7
|
+
|
|
8
|
+
/** Run a set of checks and write assertions.json. */
|
|
9
|
+
export function runAssertions(dir, checks) {
|
|
10
|
+
const results = [];
|
|
11
|
+
let allOk = true;
|
|
12
|
+
for (const check of checks) {
|
|
13
|
+
try {
|
|
14
|
+
const ok = check.fn();
|
|
15
|
+
results.push({ id: check.id, ok, ...(ok ? {} : { error: check.error }) });
|
|
16
|
+
if (!ok) allOk = false;
|
|
17
|
+
} catch (e) {
|
|
18
|
+
results.push({ id: check.id, ok: false, error: e.message });
|
|
19
|
+
allOk = false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const assertions = {
|
|
24
|
+
ok: allOk,
|
|
25
|
+
checks: results,
|
|
26
|
+
writtenAt: new Date().toISOString(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
writeFileSync(resolve(dir, "assertions.json"), JSON.stringify(assertions, null, 2));
|
|
30
|
+
|
|
31
|
+
if (!allOk) {
|
|
32
|
+
const failures = results.filter(r => !r.ok);
|
|
33
|
+
const md = [
|
|
34
|
+
"# Assertion Failures",
|
|
35
|
+
"",
|
|
36
|
+
...failures.map(f => `- **${f.id}**: ${f.error ?? "failed"}`),
|
|
37
|
+
"",
|
|
38
|
+
`Total: ${failures.length} failure(s)`,
|
|
39
|
+
];
|
|
40
|
+
writeFileSync(resolve(dir, "failures.md"), md.join("\n") + "\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return assertions;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Parse a JSONL file into an array of objects. */
|
|
47
|
+
export function parseJSONL(path) {
|
|
48
|
+
if (!existsSync(path)) return [];
|
|
49
|
+
try {
|
|
50
|
+
const text = readFileSync(path, "utf-8");
|
|
51
|
+
return text.split("\n").filter(Boolean).map(line => {
|
|
52
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
53
|
+
}).filter(Boolean);
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Find a session JSONL file in a session directory. */
|
|
60
|
+
export function findSessionJSONL(sessionDir) {
|
|
61
|
+
if (!existsSync(sessionDir)) return null;
|
|
62
|
+
for (const entry of readdirSync(sessionDir, { withFileTypes: true })) {
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
const found = findSessionJSONL(resolve(sessionDir, entry.name));
|
|
65
|
+
if (found) return found;
|
|
66
|
+
}
|
|
67
|
+
if (entry.name.endsWith(".jsonl") && !entry.name.includes("events")) {
|
|
68
|
+
return resolve(sessionDir, entry.name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Simple check: does text contain a required substring? */
|
|
75
|
+
export function assertContains(text, substring, id) {
|
|
76
|
+
return { id, fn: () => text.includes(substring) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Simple check: does a file exist? */
|
|
80
|
+
export function assertFileExists(path, id) {
|
|
81
|
+
return { id, fn: () => existsSync(path) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Check JSONL for required tool calls. */
|
|
85
|
+
export function assertJSONLToolCalls(jsonlPath, expectedTools) {
|
|
86
|
+
return {
|
|
87
|
+
id: "jsonl-tool-calls",
|
|
88
|
+
fn: () => {
|
|
89
|
+
const events = parseJSONL(jsonlPath);
|
|
90
|
+
const toolCalls = events.filter(e => e.type === "tool_use" || e.toolCall);
|
|
91
|
+
for (const expected of expectedTools) {
|
|
92
|
+
const found = toolCalls.some(tc => {
|
|
93
|
+
const name = tc.toolCall?.name ?? tc.name ?? tc.tool_name ?? "";
|
|
94
|
+
return name === expected.name;
|
|
95
|
+
});
|
|
96
|
+
if (!found) return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card detector — records stable visible evidence regions from rendered terminal output.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally stricter than a raw text-marker search: prompt lines such as
|
|
5
|
+
* "call pi__read" must not satisfy card assertions. Per-card screenshots come from
|
|
6
|
+
* visual-evidence.mjs; this module writes the legacy cards.json/index.html inventory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
const CARD_PATTERNS = [
|
|
13
|
+
{ id: "read", pattern: /^\s*read (?:\.\/)?package\.json\s*$/i },
|
|
14
|
+
{ id: "grep", pattern: /^\s*grep \/pi-cursor-sdk\/ in README\.md\s*$/i },
|
|
15
|
+
{ id: "find", pattern: /^\s*find README\.md in\s+\S+/i },
|
|
16
|
+
{ id: "list", pattern: /^\s*(?:find \* in src|find src\/\* in \.|Get-ChildItem -Name \.\/src)\s*/i },
|
|
17
|
+
{ id: "shell-success", pattern: /^\s*cursor visual smoke\s*$/i },
|
|
18
|
+
{ id: "write", pattern: /^\s*\+.*beta\s*$/i },
|
|
19
|
+
{ id: "edit-diff", pattern: /^\s*\+.*gamma\s*$/i },
|
|
20
|
+
{ id: "shell-failure", pattern: /^\s*(?:native shell failure|Command exited with code 7)\s*$/i },
|
|
21
|
+
{ id: "bridge-read-success", pattern: /^\s*read \.\/package\.json\s*$/i },
|
|
22
|
+
{ id: "bridge-read-failure", pattern: /^\s*(?:read \.\/definitely-missing-platform-smoke-file\.txt|ENOENT: no such file)\s*/i },
|
|
23
|
+
{ id: "bridge-shell-success", pattern: /^\s*bridge visual smoke\s*$/i },
|
|
24
|
+
{ id: "footer-status", pattern: /\bcomposer-2-5\b|\bcomposer-2\.5\b/i },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function cleanLine(line) {
|
|
28
|
+
return line
|
|
29
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
30
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
31
|
+
.replace(/\r/g, "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect stable rendered evidence regions in terminal text.
|
|
36
|
+
*
|
|
37
|
+
* Returns an array of { id, label, startLine, endLine }.
|
|
38
|
+
*/
|
|
39
|
+
export function detectCards(txtContent) {
|
|
40
|
+
const lines = txtContent.split("\n").map(cleanLine);
|
|
41
|
+
const cards = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
for (const card of CARD_PATTERNS) {
|
|
46
|
+
if (seen.has(card.id)) continue;
|
|
47
|
+
card.pattern.lastIndex = 0;
|
|
48
|
+
if (!card.pattern.test(lines[i])) continue;
|
|
49
|
+
seen.add(card.id);
|
|
50
|
+
cards.push({
|
|
51
|
+
id: card.id,
|
|
52
|
+
label: card.id,
|
|
53
|
+
startLine: i,
|
|
54
|
+
endLine: i,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return cards;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Write cards.json and cards/index.html gallery. */
|
|
63
|
+
export function writeCardArtifacts(dir, cards) {
|
|
64
|
+
mkdirSync(resolve(dir, "cards"), { recursive: true });
|
|
65
|
+
|
|
66
|
+
const cardsData = cards.map(c => ({
|
|
67
|
+
id: c.id,
|
|
68
|
+
label: c.label,
|
|
69
|
+
startLine: c.startLine,
|
|
70
|
+
endLine: c.endLine,
|
|
71
|
+
lineCount: c.endLine - c.startLine + 1,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
writeFileSync(resolve(dir, "cards", "cards.json"), JSON.stringify(cardsData, null, 2));
|
|
75
|
+
|
|
76
|
+
const html = `<!DOCTYPE html>
|
|
77
|
+
<html lang="en">
|
|
78
|
+
<head><meta charset="utf-8"><title>Cards Gallery</title>
|
|
79
|
+
<style>
|
|
80
|
+
body { font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; }
|
|
81
|
+
.card { border: 1px solid #444; margin: 8px 0; padding: 8px; }
|
|
82
|
+
.card h3 { margin: 0 0 4px 0; color: #569cd6; }
|
|
83
|
+
.card .meta { color: #888; font-size: 12px; }
|
|
84
|
+
</style></head><body>
|
|
85
|
+
<h1>Cards Gallery</h1>
|
|
86
|
+
${cardsData.map(c => `<div class="card"><h3>${c.label}</h3><div class="meta">line ${c.startLine}</div></div>`).join("\n")}
|
|
87
|
+
</body></html>`;
|
|
88
|
+
|
|
89
|
+
writeFileSync(resolve(dir, "cards", "index.html"), html);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Assert that required cards are present as distinct exact evidence ids. */
|
|
93
|
+
export function assertRequiredCards(_dir, detectedCards, requiredCards) {
|
|
94
|
+
const detectedIds = new Set(detectedCards.map(c => c.id));
|
|
95
|
+
return requiredCards.map((req) => ({ id: `card-${req}`, ok: detectedIds.has(req) }));
|
|
96
|
+
}
|