pi-cursor-sdk 0.1.19 → 0.1.21
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 +52 -0
- package/README.md +72 -11
- package/docs/cursor-dogfood-checklist.md +57 -0
- package/docs/cursor-live-smoke-checklist.md +116 -10
- package/docs/cursor-model-ux-spec.md +60 -19
- package/docs/cursor-native-tool-replay.md +21 -11
- package/docs/cursor-native-tool-visual-audit.md +104 -59
- package/docs/cursor-testing-lessons.md +10 -5
- package/docs/cursor-tool-surfaces.md +69 -0
- package/package.json +37 -11
- package/scripts/debug-provider-events.d.mts +59 -0
- package/scripts/debug-provider-events.mjs +70 -175
- package/scripts/debug-sdk-events.d.mts +90 -0
- package/scripts/debug-sdk-events.mjs +36 -98
- package/scripts/fixtures/plan-strip-shim/index.ts +12 -0
- package/scripts/isolated-cursor-smoke.sh +264 -102
- package/scripts/lib/cursor-child-process.d.mts +10 -0
- package/scripts/lib/cursor-child-process.mjs +50 -0
- package/scripts/lib/cursor-cli-args.d.mts +63 -0
- package/scripts/lib/cursor-cli-args.mjs +129 -0
- package/scripts/lib/cursor-script-fail.d.mts +1 -0
- package/scripts/lib/cursor-script-fail.mjs +13 -0
- package/scripts/lib/cursor-sdk-output-filter.d.mts +5 -0
- package/scripts/lib/cursor-smoke-env.d.mts +38 -0
- package/scripts/lib/cursor-smoke-env.mjs +81 -0
- package/scripts/lib/cursor-smoke-shell.sh +174 -0
- package/scripts/lib/cursor-visual-render.d.mts +15 -0
- package/scripts/lib/cursor-visual-render.mjs +131 -0
- package/scripts/probe-mcp-coldstart.mjs +226 -0
- package/scripts/refresh-cursor-model-snapshots.mjs +29 -65
- package/scripts/steering-rpc-smoke.mjs +170 -65
- package/scripts/tmux-live-smoke.sh +152 -98
- package/scripts/visual-tui-smoke.mjs +659 -0
- package/shared/cursor-sdk-event-debug-env.d.mts +12 -0
- package/shared/cursor-sdk-event-debug-env.mjs +13 -0
- package/shared/cursor-sensitive-text.d.mts +1 -0
- package/{scripts/lib/cursor-probe-utils.mjs → shared/cursor-sensitive-text.mjs} +1 -13
- package/shared/cursor-setting-sources.d.mts +5 -0
- package/shared/cursor-setting-sources.mjs +22 -0
- package/src/context.ts +21 -12
- package/src/cursor-bridge-contract.ts +1 -3
- package/src/cursor-incomplete-tool-visibility.ts +72 -49
- package/src/cursor-mcp-timeout-override.ts +66 -11
- package/src/cursor-native-tool-display-registration.ts +63 -27
- package/src/cursor-native-tool-display-replay.ts +246 -143
- package/src/cursor-native-tool-display-state.ts +2 -0
- package/src/cursor-native-tool-display-tools.ts +149 -41
- package/src/cursor-provider-live-run-drain.ts +1 -52
- package/src/cursor-provider-run-finalizer.ts +235 -0
- package/src/cursor-provider-run-outcome.ts +149 -0
- package/src/cursor-provider-turn-api-key.ts +8 -0
- package/src/cursor-provider-turn-coordinator.ts +113 -440
- package/src/cursor-provider-turn-display-router.ts +216 -0
- package/src/cursor-provider-turn-emit.ts +59 -0
- package/src/cursor-provider-turn-finalize.ts +119 -0
- package/src/cursor-provider-turn-lifecycle-emitter.ts +97 -0
- package/src/cursor-provider-turn-message-offset.ts +15 -0
- package/src/cursor-provider-turn-prepare.ts +216 -0
- package/src/cursor-provider-turn-runner.ts +138 -0
- package/src/cursor-provider-turn-sdk-normalizer.ts +88 -0
- package/src/cursor-provider-turn-send.ts +103 -0
- package/src/cursor-provider-turn-shell-output.ts +107 -0
- package/src/cursor-provider-turn-tool-ledger.ts +126 -0
- package/src/cursor-provider-turn-types.ts +87 -0
- package/src/cursor-provider.ts +16 -482
- package/src/cursor-replay-activity-builders.ts +276 -0
- package/src/cursor-replay-source-names.ts +33 -0
- package/src/cursor-replay-summary-args.ts +191 -0
- package/src/cursor-replay-tool-details.ts +464 -0
- package/src/cursor-run-final-text.ts +56 -0
- package/src/cursor-sdk-abort-error-guard.ts +4 -0
- package/src/cursor-sdk-event-debug-constants.ts +14 -5
- package/src/cursor-sdk-event-debug.ts +8 -2
- package/src/cursor-sensitive-text.ts +3 -36
- package/src/cursor-session-agent.ts +265 -88
- package/src/cursor-setting-sources.ts +7 -10
- package/src/cursor-state.ts +232 -28
- package/src/cursor-tool-lifecycle.ts +17 -42
- package/src/cursor-tool-manifest.ts +41 -0
- package/src/cursor-tool-names.ts +18 -79
- package/src/cursor-tool-presentation-registry.ts +556 -0
- package/src/cursor-tool-transcript.ts +1 -1
- package/src/cursor-tool-visibility.ts +39 -0
- package/src/cursor-transcript-tool-formatters.ts +0 -59
- package/src/cursor-transcript-tool-specs.ts +169 -232
- package/src/cursor-transcript-utils.ts +0 -44
- package/src/cursor-web-tool-activity.ts +10 -60
- package/src/cursor-web-tool-args.ts +39 -0
- package/src/index.ts +4 -10
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { CURSOR_SETTING_SOURCES_ENV, resolveCursorSettingSources } from "../../shared/cursor-setting-sources.mjs";
|
|
3
|
+
|
|
4
|
+
export function readArgvValue(argv, index, flagName, fail, options = {}) {
|
|
5
|
+
const current = argv[index];
|
|
6
|
+
if (!current || (!options.allowDashValue && current.startsWith("--"))) {
|
|
7
|
+
fail(`${flagName} requires a value`);
|
|
8
|
+
}
|
|
9
|
+
return current;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function assignParsedArg(args, key, spec, raw, flagName) {
|
|
13
|
+
const value = spec.assign ? spec.assign(raw, flagName) : raw;
|
|
14
|
+
if (spec.repeat) {
|
|
15
|
+
args[key] = [...(Array.isArray(args[key]) ? args[key] : []), value];
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
args[key] = value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseArgv(argv, { defaults, flags, fail }) {
|
|
22
|
+
const args = { ...defaults, help: false };
|
|
23
|
+
for (let index = 0; index < argv.length; index++) {
|
|
24
|
+
const arg = argv[index];
|
|
25
|
+
if (arg === "-h" || arg === "--help") {
|
|
26
|
+
args.help = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let matched = false;
|
|
31
|
+
for (const [key, spec] of Object.entries(flags)) {
|
|
32
|
+
for (const flagName of spec.names) {
|
|
33
|
+
if (arg === flagName) {
|
|
34
|
+
if (spec.takesValue === false) {
|
|
35
|
+
assignParsedArg(args, key, spec, true, flagName);
|
|
36
|
+
} else {
|
|
37
|
+
const raw = readArgvValue(argv, ++index, flagName, fail, { allowDashValue: spec.allowDashValue === true });
|
|
38
|
+
assignParsedArg(args, key, spec, raw, flagName);
|
|
39
|
+
}
|
|
40
|
+
matched = true;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
if (arg.startsWith(`${flagName}=`)) {
|
|
44
|
+
if (spec.takesValue === false) fail(`${flagName} does not accept a value`);
|
|
45
|
+
const raw = arg.slice(flagName.length + 1);
|
|
46
|
+
assignParsedArg(args, key, spec, raw, flagName);
|
|
47
|
+
matched = true;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (matched) break;
|
|
52
|
+
}
|
|
53
|
+
if (!matched) fail(`unknown argument: ${arg}`);
|
|
54
|
+
}
|
|
55
|
+
return args;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function defaultSettingSourcesFromEnv(env = process.env) {
|
|
59
|
+
return resolveCursorSettingSources(env[CURSOR_SETTING_SOURCES_ENV]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function defaultApiKeyFromEnv(env = process.env) {
|
|
63
|
+
return env.CURSOR_API_KEY?.trim() || undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function readArgvApiKey(argv) {
|
|
67
|
+
for (let index = 0; index < argv.length; index++) {
|
|
68
|
+
const arg = argv[index];
|
|
69
|
+
if (arg === "--api-key") {
|
|
70
|
+
const value = argv[index + 1];
|
|
71
|
+
return typeof value === "string" ? value.trim() : undefined;
|
|
72
|
+
}
|
|
73
|
+
if (arg.startsWith("--api-key=")) return arg.slice("--api-key=".length).trim();
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function apiKeySecretsFromProcess(argv = process.argv.slice(2), env = process.env) {
|
|
79
|
+
return [defaultApiKeyFromEnv(env), readArgvApiKey(argv)];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function requireApiKey(args, env, fail) {
|
|
83
|
+
const apiKey = args.apiKey ?? defaultApiKeyFromEnv(env);
|
|
84
|
+
if (!apiKey) {
|
|
85
|
+
fail("Cursor API key is required. Set CURSOR_API_KEY or pass --api-key.");
|
|
86
|
+
}
|
|
87
|
+
return apiKey;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function defaultTimestampedDir(prefix, baseDir = "/tmp") {
|
|
91
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
92
|
+
return resolve(baseDir, `${prefix}-${stamp}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const commonProbePathFlag = (key) => ({
|
|
96
|
+
names: [`--${key}`],
|
|
97
|
+
assign: (value) => resolve(value),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export const commonProbeStringFlag = (key) => ({
|
|
101
|
+
names: [`--${key}`],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const commonBooleanFlag = (...names) => ({
|
|
105
|
+
names,
|
|
106
|
+
takesValue: false,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const commonRepeatStringFlag = (...names) => ({
|
|
110
|
+
names,
|
|
111
|
+
repeat: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const commonProbeFlags = {
|
|
115
|
+
cwd: commonProbePathFlag("cwd"),
|
|
116
|
+
model: commonProbeStringFlag("model"),
|
|
117
|
+
prompt: commonProbeStringFlag("prompt"),
|
|
118
|
+
out: commonProbePathFlag("out"),
|
|
119
|
+
sessionDir: { names: ["--session-dir"], assign: (value) => resolve(value) },
|
|
120
|
+
promptFile: { names: ["--prompt-file"], assign: (value) => resolve(value) },
|
|
121
|
+
apiKey: {
|
|
122
|
+
names: ["--api-key"],
|
|
123
|
+
assign: (value) => value.trim(),
|
|
124
|
+
},
|
|
125
|
+
settingSources: {
|
|
126
|
+
names: ["--setting-sources"],
|
|
127
|
+
assign: (value) => resolveCursorSettingSources(value),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createScriptFail(prefix: string): (message: string, secrets?: string | string[]) => never;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { scrubSensitiveText } from "../../shared/cursor-sensitive-text.mjs";
|
|
2
|
+
|
|
3
|
+
export function createScriptFail(prefix) {
|
|
4
|
+
return (message, secrets = []) => {
|
|
5
|
+
const secretList = Array.isArray(secrets) ? secrets : [secrets];
|
|
6
|
+
let scrubbed = scrubSensitiveText(message);
|
|
7
|
+
for (const secret of secretList) {
|
|
8
|
+
if (secret) scrubbed = scrubSensitiveText(scrubbed, secret);
|
|
9
|
+
}
|
|
10
|
+
console.error(`${prefix}: ${scrubbed}`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const CURSOR_SDK_STARTUP_NOISE_PATTERNS: readonly string[];
|
|
2
|
+
export declare function isCursorSdkOutputSuppressed(): boolean;
|
|
3
|
+
export declare function suppressCursorSdkOutput<T>(operation: () => T): T;
|
|
4
|
+
export declare function isCursorSdkStartupNoise(text: string): boolean;
|
|
5
|
+
export declare function installCursorSdkOutputFilter(): () => void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export {
|
|
2
|
+
CURSOR_SDK_EVENT_DEBUG_DIR_ENV,
|
|
3
|
+
CURSOR_SDK_EVENT_DEBUG_ENV,
|
|
4
|
+
CURSOR_SDK_EVENT_DEBUG_ENV_NAMES,
|
|
5
|
+
CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV,
|
|
6
|
+
CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV,
|
|
7
|
+
CURSOR_SDK_EVENT_DEBUG_STDERR_ENV,
|
|
8
|
+
} from "../../shared/cursor-sdk-event-debug-env.mjs";
|
|
9
|
+
|
|
10
|
+
export declare function sealedNodePath(nodePath?: string, envPath?: string): string;
|
|
11
|
+
export declare function clearCursorSdkEventDebugEnv<TEnv extends Record<string, string | undefined>>(env: TEnv): TEnv;
|
|
12
|
+
export declare function buildCursorSmokeEnv(options?: {
|
|
13
|
+
baseEnv?: Record<string, string | undefined>;
|
|
14
|
+
nodePath?: string;
|
|
15
|
+
settingSources?: string | null;
|
|
16
|
+
nativeToolDisplay?: boolean;
|
|
17
|
+
registerNativeTools?: boolean;
|
|
18
|
+
bridge?: boolean;
|
|
19
|
+
exposeBuiltinTools?: boolean;
|
|
20
|
+
term?: string;
|
|
21
|
+
eventDebugDir?: string;
|
|
22
|
+
}): Record<string, string | undefined>;
|
|
23
|
+
export declare function buildCursorSmokeEnvPlan(options?: {
|
|
24
|
+
baseEnv?: Record<string, string | undefined>;
|
|
25
|
+
nodePath?: string;
|
|
26
|
+
settingSources?: string | null;
|
|
27
|
+
nativeToolDisplay?: boolean;
|
|
28
|
+
registerNativeTools?: boolean;
|
|
29
|
+
bridge?: boolean;
|
|
30
|
+
exposeBuiltinTools?: boolean;
|
|
31
|
+
term?: string;
|
|
32
|
+
eventDebugDir?: string;
|
|
33
|
+
}): {
|
|
34
|
+
env: Record<string, string | undefined>;
|
|
35
|
+
sealedPath: string;
|
|
36
|
+
clearEnvNames: string[];
|
|
37
|
+
envEntries: Array<[string, string]>;
|
|
38
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { delimiter, dirname } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
CURSOR_SDK_EVENT_DEBUG_DIR_ENV,
|
|
4
|
+
CURSOR_SDK_EVENT_DEBUG_ENV,
|
|
5
|
+
CURSOR_SDK_EVENT_DEBUG_ENV_NAMES,
|
|
6
|
+
CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV,
|
|
7
|
+
CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV,
|
|
8
|
+
CURSOR_SDK_EVENT_DEBUG_STDERR_ENV,
|
|
9
|
+
} from "../../shared/cursor-sdk-event-debug-env.mjs";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
CURSOR_SDK_EVENT_DEBUG_DIR_ENV,
|
|
13
|
+
CURSOR_SDK_EVENT_DEBUG_ENV,
|
|
14
|
+
CURSOR_SDK_EVENT_DEBUG_ENV_NAMES,
|
|
15
|
+
CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV,
|
|
16
|
+
CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV,
|
|
17
|
+
CURSOR_SDK_EVENT_DEBUG_STDERR_ENV,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function sealedNodePath(nodePath = process.execPath, envPath = process.env.PATH ?? "") {
|
|
21
|
+
return [dirname(nodePath), envPath].filter(Boolean).join(delimiter);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function clearCursorSdkEventDebugEnv(env) {
|
|
25
|
+
for (const name of CURSOR_SDK_EVENT_DEBUG_ENV_NAMES) delete env[name];
|
|
26
|
+
return env;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function boolEnv(value) {
|
|
30
|
+
return value ? "1" : "0";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pushIfDefined(entries, name, value) {
|
|
34
|
+
if (value !== undefined) entries.push([name, value]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildCursorSmokeEnv({
|
|
38
|
+
baseEnv = process.env,
|
|
39
|
+
nodePath = process.execPath,
|
|
40
|
+
settingSources,
|
|
41
|
+
nativeToolDisplay,
|
|
42
|
+
registerNativeTools,
|
|
43
|
+
bridge,
|
|
44
|
+
exposeBuiltinTools,
|
|
45
|
+
term,
|
|
46
|
+
eventDebugDir,
|
|
47
|
+
} = {}) {
|
|
48
|
+
const env = clearCursorSdkEventDebugEnv({ ...baseEnv });
|
|
49
|
+
env.PATH = sealedNodePath(nodePath, baseEnv.PATH ?? "");
|
|
50
|
+
if (settingSources === null) delete env.PI_CURSOR_SETTING_SOURCES;
|
|
51
|
+
else if (settingSources !== undefined) env.PI_CURSOR_SETTING_SOURCES = settingSources;
|
|
52
|
+
if (nativeToolDisplay !== undefined) env.PI_CURSOR_NATIVE_TOOL_DISPLAY = boolEnv(nativeToolDisplay);
|
|
53
|
+
if (registerNativeTools !== undefined) env.PI_CURSOR_REGISTER_NATIVE_TOOLS = boolEnv(registerNativeTools);
|
|
54
|
+
if (bridge !== undefined) env.PI_CURSOR_PI_TOOL_BRIDGE = boolEnv(bridge);
|
|
55
|
+
if (exposeBuiltinTools !== undefined) env.PI_CURSOR_EXPOSE_BUILTIN_TOOLS = boolEnv(exposeBuiltinTools);
|
|
56
|
+
if (term !== undefined) env.TERM = term;
|
|
57
|
+
if (eventDebugDir !== undefined) {
|
|
58
|
+
env[CURSOR_SDK_EVENT_DEBUG_ENV] = "1";
|
|
59
|
+
env[CURSOR_SDK_EVENT_DEBUG_DIR_ENV] = eventDebugDir;
|
|
60
|
+
}
|
|
61
|
+
return env;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildCursorSmokeEnvPlan(options = {}) {
|
|
65
|
+
const env = buildCursorSmokeEnv(options);
|
|
66
|
+
const envEntries = [];
|
|
67
|
+
pushIfDefined(envEntries, "PI_CURSOR_SETTING_SOURCES", options.settingSources === null ? undefined : options.settingSources);
|
|
68
|
+
pushIfDefined(envEntries, "PI_CURSOR_NATIVE_TOOL_DISPLAY", options.nativeToolDisplay === undefined ? undefined : boolEnv(options.nativeToolDisplay));
|
|
69
|
+
pushIfDefined(envEntries, "PI_CURSOR_REGISTER_NATIVE_TOOLS", options.registerNativeTools === undefined ? undefined : boolEnv(options.registerNativeTools));
|
|
70
|
+
pushIfDefined(envEntries, "PI_CURSOR_PI_TOOL_BRIDGE", options.bridge === undefined ? undefined : boolEnv(options.bridge));
|
|
71
|
+
pushIfDefined(envEntries, "PI_CURSOR_EXPOSE_BUILTIN_TOOLS", options.exposeBuiltinTools === undefined ? undefined : boolEnv(options.exposeBuiltinTools));
|
|
72
|
+
pushIfDefined(envEntries, "TERM", options.term);
|
|
73
|
+
pushIfDefined(envEntries, CURSOR_SDK_EVENT_DEBUG_ENV, options.eventDebugDir === undefined ? undefined : "1");
|
|
74
|
+
pushIfDefined(envEntries, CURSOR_SDK_EVENT_DEBUG_DIR_ENV, options.eventDebugDir);
|
|
75
|
+
return {
|
|
76
|
+
env,
|
|
77
|
+
sealedPath: env.PATH,
|
|
78
|
+
clearEnvNames: [...CURSOR_SDK_EVENT_DEBUG_ENV_NAMES],
|
|
79
|
+
envEntries,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Shared maintainer smoke shell helpers (timeout, logging, auth seeding).
|
|
2
|
+
# Source from top-level smoke scripts: . "$(dirname "$0")/lib/cursor-smoke-shell.sh"
|
|
3
|
+
|
|
4
|
+
: "${SMOKE_LOG_PREFIX:=smoke}"
|
|
5
|
+
SMOKE_KILL_GRACE_SECS="${SMOKE_KILL_GRACE_SECS:-2}"
|
|
6
|
+
SMOKE_CURSOR_SDK_EVENT_DEBUG_ENV_NAMES=()
|
|
7
|
+
SMOKE_CURSOR_SDK_EVENT_DEBUG_ENV_UNSETS=()
|
|
8
|
+
|
|
9
|
+
smoke_log() {
|
|
10
|
+
printf '[%s] %s\n' "$SMOKE_LOG_PREFIX" "$*"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
smoke_fail() {
|
|
14
|
+
printf '[%s] FAIL: %s\n' "$SMOKE_LOG_PREFIX" "$*" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
smoke_require_cmd() {
|
|
19
|
+
command -v "$1" >/dev/null 2>&1 || smoke_fail "missing required command: $1"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
smoke_resolve_cmd() {
|
|
23
|
+
local name="$1"
|
|
24
|
+
local path
|
|
25
|
+
if ! path="$(command -v -- "$name" 2>/dev/null)" || [[ -z "$path" ]]; then
|
|
26
|
+
smoke_fail "missing required command: $name"
|
|
27
|
+
fi
|
|
28
|
+
if [[ "$path" != /* ]]; then
|
|
29
|
+
smoke_fail "required command $name did not resolve to an absolute path: $path"
|
|
30
|
+
fi
|
|
31
|
+
printf '%s\n' "$path"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
smoke_build_sealed_node_path() {
|
|
35
|
+
local node_bin="$1"
|
|
36
|
+
local base_path
|
|
37
|
+
if (( $# >= 2 )); then
|
|
38
|
+
base_path="$2"
|
|
39
|
+
else
|
|
40
|
+
base_path="$PATH"
|
|
41
|
+
fi
|
|
42
|
+
if [[ -n "$base_path" ]]; then
|
|
43
|
+
printf '%s:%s\n' "$(dirname "$node_bin")" "$base_path"
|
|
44
|
+
else
|
|
45
|
+
printf '%s\n' "$(dirname "$node_bin")"
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
smoke_load_cursor_sdk_event_debug_env_names() {
|
|
50
|
+
local node_bin="$1"
|
|
51
|
+
local module_path="$2"
|
|
52
|
+
local name
|
|
53
|
+
SMOKE_CURSOR_SDK_EVENT_DEBUG_ENV_NAMES=()
|
|
54
|
+
while IFS= read -r name; do
|
|
55
|
+
[[ -n "$name" ]] || continue
|
|
56
|
+
SMOKE_CURSOR_SDK_EVENT_DEBUG_ENV_NAMES+=( "$name" )
|
|
57
|
+
done < <("$node_bin" --input-type=module -e 'import { pathToFileURL } from "node:url"; const mod = await import(pathToFileURL(process.argv[1]).href); for (const name of mod.CURSOR_SDK_EVENT_DEBUG_ENV_NAMES) console.log(name);' "$module_path")
|
|
58
|
+
if [[ "${#SMOKE_CURSOR_SDK_EVENT_DEBUG_ENV_NAMES[@]}" -eq 0 ]]; then
|
|
59
|
+
smoke_fail "failed to load Cursor SDK event-debug env names from $module_path"
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
smoke_build_cursor_sdk_event_debug_unsets() {
|
|
64
|
+
local name
|
|
65
|
+
SMOKE_CURSOR_SDK_EVENT_DEBUG_ENV_UNSETS=()
|
|
66
|
+
for name in "${SMOKE_CURSOR_SDK_EVENT_DEBUG_ENV_NAMES[@]}"; do
|
|
67
|
+
SMOKE_CURSOR_SDK_EVENT_DEBUG_ENV_UNSETS+=( -u "$name" )
|
|
68
|
+
done
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Run a command with a wall-clock timeout. Prefer GNU/BSD timeout; fall back to a
|
|
72
|
+
# process-group kill watcher with TERM then KILL (same semantics as tmux live smoke).
|
|
73
|
+
smoke_run_with_timeout() {
|
|
74
|
+
local timeout_secs="$1"
|
|
75
|
+
shift
|
|
76
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
77
|
+
timeout "$timeout_secs" "$@"
|
|
78
|
+
return $?
|
|
79
|
+
fi
|
|
80
|
+
if command -v gtimeout >/dev/null 2>&1; then
|
|
81
|
+
gtimeout "$timeout_secs" "$@"
|
|
82
|
+
return $?
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
local restore_monitor=0
|
|
86
|
+
case $- in
|
|
87
|
+
*m*) ;;
|
|
88
|
+
*)
|
|
89
|
+
restore_monitor=1
|
|
90
|
+
set -m
|
|
91
|
+
;;
|
|
92
|
+
esac
|
|
93
|
+
|
|
94
|
+
"$@" &
|
|
95
|
+
local pid=$!
|
|
96
|
+
(
|
|
97
|
+
sleep "$timeout_secs"
|
|
98
|
+
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
|
99
|
+
sleep "$SMOKE_KILL_GRACE_SECS"
|
|
100
|
+
kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
|
|
101
|
+
) &
|
|
102
|
+
local watcher=$!
|
|
103
|
+
local code=0
|
|
104
|
+
if wait "$pid"; then
|
|
105
|
+
code=0
|
|
106
|
+
else
|
|
107
|
+
code=$?
|
|
108
|
+
fi
|
|
109
|
+
kill "$watcher" 2>/dev/null || true
|
|
110
|
+
wait "$watcher" 2>/dev/null || true
|
|
111
|
+
if (( restore_monitor )); then
|
|
112
|
+
set +m
|
|
113
|
+
fi
|
|
114
|
+
return "$code"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Run with timeout; map exit 124/137/143 to a smoke_fail timeout message.
|
|
118
|
+
smoke_run_with_timeout_or_fail() {
|
|
119
|
+
local label="$1"
|
|
120
|
+
local timeout_secs="$2"
|
|
121
|
+
shift 2
|
|
122
|
+
smoke_log "$label (timeout ${timeout_secs}s)"
|
|
123
|
+
local restore_errexit=0
|
|
124
|
+
case $- in
|
|
125
|
+
*e*)
|
|
126
|
+
restore_errexit=1
|
|
127
|
+
set +e
|
|
128
|
+
;;
|
|
129
|
+
esac
|
|
130
|
+
local rc=0
|
|
131
|
+
smoke_run_with_timeout "$timeout_secs" "$@"
|
|
132
|
+
rc=$?
|
|
133
|
+
if (( restore_errexit )); then
|
|
134
|
+
set -e
|
|
135
|
+
fi
|
|
136
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
case "$rc" in
|
|
140
|
+
124|137|143) smoke_fail "$label timed out after ${timeout_secs}s" ;;
|
|
141
|
+
*) smoke_fail "$label exited $rc" ;;
|
|
142
|
+
esac
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
smoke_seed_pi_agent_home() {
|
|
146
|
+
local home="$1"
|
|
147
|
+
local auth_json="${2:-${AUTH_JSON:-${REAL_HOME:-$HOME}/.pi/agent/auth.json}}"
|
|
148
|
+
local models_src="${3:-${PI_AGENT_DIR:-${REAL_HOME:-$HOME}/.pi/agent}/models.json}"
|
|
149
|
+
mkdir -p "$home/.pi/agent"
|
|
150
|
+
if [[ -f "$auth_json" ]]; then
|
|
151
|
+
cp "$auth_json" "$home/.pi/agent/auth.json"
|
|
152
|
+
chmod 600 "$home/.pi/agent/auth.json"
|
|
153
|
+
smoke_log "seeded $home/.pi/agent/auth.json"
|
|
154
|
+
else
|
|
155
|
+
smoke_log "WARN: no auth.json at $auth_json"
|
|
156
|
+
fi
|
|
157
|
+
if [[ -f "$models_src" ]]; then
|
|
158
|
+
cp "$models_src" "$home/.pi/agent/models.json"
|
|
159
|
+
fi
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
smoke_has_auth_provider() {
|
|
163
|
+
local provider="$1"
|
|
164
|
+
local auth_path="$2"
|
|
165
|
+
python3 - "$provider" "$auth_path" <<'PY'
|
|
166
|
+
import json, sys
|
|
167
|
+
provider, path = sys.argv[1], sys.argv[2]
|
|
168
|
+
try:
|
|
169
|
+
data = json.load(open(path))
|
|
170
|
+
except FileNotFoundError:
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
sys.exit(0 if provider in data and data[provider] else 1)
|
|
173
|
+
PY
|
|
174
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function buildTerminalHtml(args: {
|
|
2
|
+
ansi: string;
|
|
3
|
+
plain: string;
|
|
4
|
+
options: {
|
|
5
|
+
label: string;
|
|
6
|
+
model: string;
|
|
7
|
+
mode: string;
|
|
8
|
+
cwd: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
historyLines: number;
|
|
13
|
+
};
|
|
14
|
+
}): string;
|
|
15
|
+
export declare function writeTerminalScreenshot(htmlPath: string, pngPath: string, width: number, height: number): Promise<void>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
function escapeHtml(text) {
|
|
6
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function htmlJson(value) {
|
|
10
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function loadXtermAssets() {
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
try {
|
|
16
|
+
return {
|
|
17
|
+
css: readFileSync(require.resolve("@xterm/xterm/css/xterm.css"), "utf8"),
|
|
18
|
+
js: readFileSync(require.resolve("@xterm/xterm/lib/xterm.js"), "utf8"),
|
|
19
|
+
};
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new Error(`failed to load @xterm/xterm assets; run npm install: ${error instanceof Error ? error.message : String(error)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildTerminalHtml({ ansi, plain, options }) {
|
|
26
|
+
const assets = loadXtermAssets();
|
|
27
|
+
return `<!doctype html>
|
|
28
|
+
<html lang="en">
|
|
29
|
+
<head>
|
|
30
|
+
<meta charset="utf-8">
|
|
31
|
+
<title>pi-cursor-sdk visual smoke: ${escapeHtml(options.label)}</title>
|
|
32
|
+
<style>
|
|
33
|
+
${assets.css}
|
|
34
|
+
:root { color-scheme: dark; }
|
|
35
|
+
body {
|
|
36
|
+
margin: 0;
|
|
37
|
+
padding: 16px;
|
|
38
|
+
background: #0b0f14;
|
|
39
|
+
color: #d8dee9;
|
|
40
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
41
|
+
}
|
|
42
|
+
header {
|
|
43
|
+
margin: 0 0 12px;
|
|
44
|
+
font-size: 13px;
|
|
45
|
+
line-height: 1.4;
|
|
46
|
+
color: #9aa4b2;
|
|
47
|
+
}
|
|
48
|
+
header code { color: #d8dee9; }
|
|
49
|
+
#terminal {
|
|
50
|
+
display: inline-block;
|
|
51
|
+
padding: 12px;
|
|
52
|
+
border: 1px solid #303846;
|
|
53
|
+
border-radius: 8px;
|
|
54
|
+
background: #0b0f14;
|
|
55
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
|
56
|
+
}
|
|
57
|
+
.fallback {
|
|
58
|
+
white-space: pre-wrap;
|
|
59
|
+
font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
63
|
+
<script>${assets.js}</script>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<header>
|
|
67
|
+
<div><strong>pi-cursor-sdk visual smoke</strong> <code>${escapeHtml(options.label)}</code></div>
|
|
68
|
+
<div>model <code>${escapeHtml(options.model)}</code> · mode <code>${escapeHtml(options.mode)}</code> · cwd <code>${escapeHtml(options.cwd)}</code></div>
|
|
69
|
+
<div>session <code>${escapeHtml(options.sessionId)}</code> · captured ${new Date().toISOString()}</div>
|
|
70
|
+
</header>
|
|
71
|
+
<div id="terminal"></div>
|
|
72
|
+
<noscript><pre class="fallback">${escapeHtml(plain)}</pre></noscript>
|
|
73
|
+
<script>
|
|
74
|
+
const ansi = ${htmlJson(ansi)};
|
|
75
|
+
const fallbackText = ${htmlJson(plain)};
|
|
76
|
+
const terminalElement = document.getElementById("terminal");
|
|
77
|
+
try {
|
|
78
|
+
const term = new Terminal({
|
|
79
|
+
cols: ${options.width},
|
|
80
|
+
rows: ${options.height},
|
|
81
|
+
convertEol: true,
|
|
82
|
+
fontFamily: 'Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
83
|
+
fontSize: 13,
|
|
84
|
+
lineHeight: 1.18,
|
|
85
|
+
scrollback: ${options.historyLines},
|
|
86
|
+
theme: {
|
|
87
|
+
background: '#0b0f14',
|
|
88
|
+
foreground: '#d8dee9',
|
|
89
|
+
cursor: '#d8dee9'
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
term.open(terminalElement);
|
|
93
|
+
term.resize(${options.width}, ${options.height});
|
|
94
|
+
term.write(ansi, () => {
|
|
95
|
+
document.body.setAttribute("data-render-ready", "true");
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const pre = document.createElement("pre");
|
|
99
|
+
pre.className = "fallback";
|
|
100
|
+
pre.textContent = fallbackText + "\\n\\n[xterm render failed: " + String(error) + "]";
|
|
101
|
+
terminalElement.replaceChildren(pre);
|
|
102
|
+
document.body.setAttribute("data-render-ready", "true");
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
</body>
|
|
106
|
+
</html>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function writeTerminalScreenshot(htmlPath, pngPath, width, height) {
|
|
111
|
+
let browser;
|
|
112
|
+
try {
|
|
113
|
+
const { chromium } = await import("playwright");
|
|
114
|
+
browser = await chromium.launch();
|
|
115
|
+
const page = await browser.newPage({
|
|
116
|
+
viewport: {
|
|
117
|
+
width: Math.max(1_200, width * 10),
|
|
118
|
+
height: Math.max(800, height * 22),
|
|
119
|
+
},
|
|
120
|
+
deviceScaleFactor: 1,
|
|
121
|
+
});
|
|
122
|
+
await page.goto(pathToFileURL(htmlPath).href);
|
|
123
|
+
await page.waitForSelector('body[data-render-ready="true"]', { timeout: 30_000 });
|
|
124
|
+
await page.locator("#terminal").screenshot({ path: pngPath });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
throw new Error(`failed to capture PNG with Playwright: ${message}\nInstall Chromium with: npx playwright install chromium\nOr rerun with --no-screenshot and capture ${htmlPath} with agent_browser.`);
|
|
128
|
+
} finally {
|
|
129
|
+
if (browser) await browser.close();
|
|
130
|
+
}
|
|
131
|
+
}
|