@uinaf/skillcheck 0.5.1 → 0.5.2
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/dist/cursor-process.js +71 -0
- package/dist/cursor-provider.js +95 -33
- package/docs/usage.md +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
//#region src/cursor-process.ts
|
|
3
|
+
let child;
|
|
4
|
+
let terminal = false;
|
|
5
|
+
let cleaning = false;
|
|
6
|
+
let watchdog = setTimeout(cleanup, 5e3);
|
|
7
|
+
function cleanup() {
|
|
8
|
+
if (cleaning) return;
|
|
9
|
+
cleaning = true;
|
|
10
|
+
clearTimeout(watchdog);
|
|
11
|
+
if (process.platform !== "win32") process.kill(-process.pid, "SIGKILL");
|
|
12
|
+
child?.kill("SIGKILL");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
function fail(error) {
|
|
16
|
+
if (terminal) return;
|
|
17
|
+
terminal = true;
|
|
18
|
+
process.send?.({
|
|
19
|
+
type: "failure",
|
|
20
|
+
error
|
|
21
|
+
}, cleanup);
|
|
22
|
+
clearTimeout(watchdog);
|
|
23
|
+
watchdog = setTimeout(cleanup, 1e3);
|
|
24
|
+
}
|
|
25
|
+
if (!process.send) process.exit(1);
|
|
26
|
+
process.on("disconnect", cleanup);
|
|
27
|
+
process.on("message", (message) => {
|
|
28
|
+
if (message === "stop") return cleanup();
|
|
29
|
+
if (message === "cleanup" && terminal) {
|
|
30
|
+
process.send?.({ type: "cleanup" }, cleanup);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (child !== void 0 || terminal) return;
|
|
34
|
+
if (typeof message !== "object" || message === null || !("command" in message) || typeof message.command !== "string" || !("cwd" in message) || typeof message.cwd !== "string" || !("args" in message) || !Array.isArray(message.args) || !message.args.every((arg) => typeof arg === "string") || !("timeoutMs" in message) || typeof message.timeoutMs !== "number" || !Number.isFinite(message.timeoutMs) || message.timeoutMs <= 0) return fail("cursor-agent supervisor received invalid configuration");
|
|
35
|
+
clearTimeout(watchdog);
|
|
36
|
+
watchdog = setTimeout(() => fail("cursor-agent supervisor watchdog timed out"), message.timeoutMs + 1e3);
|
|
37
|
+
const command = message.command;
|
|
38
|
+
try {
|
|
39
|
+
child = spawn(command, message.args, {
|
|
40
|
+
cwd: message.cwd,
|
|
41
|
+
stdio: [
|
|
42
|
+
"pipe",
|
|
43
|
+
"inherit",
|
|
44
|
+
"inherit"
|
|
45
|
+
]
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return fail(`failed to spawn ${command}: ${error instanceof Error ? error.message : String(error)}`);
|
|
49
|
+
}
|
|
50
|
+
child.on("error", (error) => fail(`failed to spawn ${command}: ${error.message}`));
|
|
51
|
+
child.stdin?.on("error", (error) => fail(`cursor-agent stdin failed: ${error.message}`));
|
|
52
|
+
process.stdin.on("error", (error) => fail(`cursor-agent prompt stream failed: ${error.message}`));
|
|
53
|
+
if (child.stdin) process.stdin.pipe(child.stdin);
|
|
54
|
+
child.on("exit", (code, signal) => {
|
|
55
|
+
if (terminal) return;
|
|
56
|
+
if (!child?.stdin?.writableFinished) return fail("cursor-agent stdin closed before prompt delivery");
|
|
57
|
+
terminal = true;
|
|
58
|
+
process.stdin.unpipe();
|
|
59
|
+
clearTimeout(watchdog);
|
|
60
|
+
watchdog = setTimeout(cleanup, 1e3);
|
|
61
|
+
process.send?.({
|
|
62
|
+
type: "terminal",
|
|
63
|
+
code,
|
|
64
|
+
signal
|
|
65
|
+
}, (error) => {
|
|
66
|
+
if (error) cleanup();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
//#endregion
|
|
71
|
+
export {};
|
package/dist/cursor-provider.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
//#region src/cursor-provider.ts
|
|
3
4
|
function newStreamState() {
|
|
4
5
|
return {
|
|
@@ -65,53 +66,114 @@ var CursorAgentProvider = class {
|
|
|
65
66
|
const stderr = [];
|
|
66
67
|
let stdoutBuf = "";
|
|
67
68
|
return new Promise((resolve) => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const timeoutMs = this.config.timeout_ms ?? 9e5;
|
|
70
|
+
const supervisor = fork(new URL(`./cursor-process${path.extname(import.meta.url)}`, import.meta.url), {
|
|
71
|
+
execArgv: [],
|
|
72
|
+
detached: process.platform !== "win32",
|
|
71
73
|
stdio: [
|
|
72
74
|
"pipe",
|
|
73
75
|
"pipe",
|
|
74
|
-
"pipe"
|
|
76
|
+
"pipe",
|
|
77
|
+
"ipc"
|
|
75
78
|
]
|
|
76
79
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
child.stdout.on("data", (chunk) => {
|
|
87
|
-
stdoutBuf += chunk.toString("utf8");
|
|
88
|
-
const lines = stdoutBuf.split("\n");
|
|
89
|
-
stdoutBuf = lines.pop() ?? "";
|
|
90
|
-
for (const line of lines) foldLine(state, line);
|
|
91
|
-
});
|
|
92
|
-
child.stderr.on("data", (chunk) => {
|
|
93
|
-
stderr.push(chunk.toString("utf8"));
|
|
94
|
-
});
|
|
95
|
-
child.on("close", (code) => {
|
|
80
|
+
let failure;
|
|
81
|
+
let terminal;
|
|
82
|
+
let cleanupConfirmed = false;
|
|
83
|
+
let settled = false;
|
|
84
|
+
let cleanupTimer;
|
|
85
|
+
const finish = (closed, signal = null) => {
|
|
86
|
+
if (settled) return;
|
|
87
|
+
settled = true;
|
|
96
88
|
clearTimeout(timer);
|
|
89
|
+
clearTimeout(cleanupTimer);
|
|
90
|
+
supervisor.stdin?.destroy();
|
|
91
|
+
supervisor.stdout?.destroy();
|
|
92
|
+
supervisor.stderr?.destroy();
|
|
93
|
+
if (!closed) {
|
|
94
|
+
supervisor.kill("SIGKILL");
|
|
95
|
+
supervisor.unref();
|
|
96
|
+
if (supervisor.connected) supervisor.disconnect();
|
|
97
|
+
}
|
|
97
98
|
if (stdoutBuf !== "") foldLine(state, stdoutBuf);
|
|
98
|
-
|
|
99
|
+
const metadata = { skillCalls: state.skillCalls };
|
|
100
|
+
const fail = (error) => resolve({
|
|
101
|
+
error,
|
|
102
|
+
tokenUsage: state.tokenUsage,
|
|
103
|
+
metadata
|
|
104
|
+
});
|
|
105
|
+
if (failure !== void 0) {
|
|
99
106
|
const tail = stderr.join("").trim().slice(-2e3);
|
|
100
|
-
|
|
101
|
-
return;
|
|
107
|
+
const missingResult = terminal !== void 0 && state.result === void 0 ? " without a result event" : "";
|
|
108
|
+
return fail(`${failure}${missingResult}${tail === "" ? "" : `: ${tail}`}`);
|
|
102
109
|
}
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
if (!closed || !cleanupConfirmed || terminal === void 0 || process.platform !== "win32" && signal !== "SIGKILL") return fail("cursor-agent supervisor ended without confirmed cleanup");
|
|
111
|
+
if (terminal.code !== 0 || terminal.signal !== null || state.result === void 0) {
|
|
112
|
+
const tail = stderr.join("").trim().slice(-2e3);
|
|
113
|
+
return fail(`cursor-agent exited ${terminal.signal ?? terminal.code ?? "without status"}${state.result === void 0 ? " without a result event" : ""}${tail === "" ? "" : `: ${tail}`}`);
|
|
106
114
|
}
|
|
115
|
+
if (state.isError) return fail(state.result || "cursor-agent reported an error result");
|
|
107
116
|
resolve({
|
|
108
117
|
output: state.result,
|
|
109
118
|
tokenUsage: state.tokenUsage,
|
|
110
|
-
metadata
|
|
119
|
+
metadata
|
|
111
120
|
});
|
|
121
|
+
};
|
|
122
|
+
const boundCleanup = () => {
|
|
123
|
+
cleanupTimer ??= setTimeout(() => {
|
|
124
|
+
failure = failure === void 0 ? "cursor-agent cleanup or pipe draining timed out" : `${failure}; cleanup or pipe draining timed out`;
|
|
125
|
+
finish(false);
|
|
126
|
+
}, 2e3);
|
|
127
|
+
};
|
|
128
|
+
const stop = (error) => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
failure ??= error;
|
|
131
|
+
boundCleanup();
|
|
132
|
+
if (supervisor.connected) supervisor.send("stop", () => {});
|
|
133
|
+
};
|
|
134
|
+
const timer = setTimeout(() => stop(`cursor-agent timed out after ${timeoutMs}ms`), timeoutMs);
|
|
135
|
+
supervisor.on("error", (err) => stop(`cursor-agent supervisor failed: ${err.message}`));
|
|
136
|
+
supervisor.on("disconnect", () => {
|
|
137
|
+
if (!cleanupConfirmed && !settled) stop("cursor-agent supervisor disconnected before cleanup");
|
|
138
|
+
});
|
|
139
|
+
supervisor.on("message", (message) => {
|
|
140
|
+
if (settled) return;
|
|
141
|
+
if (typeof message !== "object" || message === null) return;
|
|
142
|
+
if (!("type" in message)) return;
|
|
143
|
+
if (message.type === "terminal" && "code" in message && "signal" in message && (message.code === null || typeof message.code === "number") && (message.signal === null || typeof message.signal === "string")) {
|
|
144
|
+
terminal = {
|
|
145
|
+
code: message.code,
|
|
146
|
+
signal: message.signal
|
|
147
|
+
};
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
if (terminal.code !== 0 || terminal.signal !== null) failure ??= `cursor-agent exited ${terminal.signal ?? terminal.code ?? "without status"}`;
|
|
150
|
+
boundCleanup();
|
|
151
|
+
supervisor.send("cleanup", (err) => {
|
|
152
|
+
if (err) stop(`cursor-agent cleanup request failed: ${err.message}`);
|
|
153
|
+
});
|
|
154
|
+
} else if (message.type === "failure" && "error" in message && typeof message.error === "string") stop(message.error);
|
|
155
|
+
else if (message.type === "cleanup" && terminal !== void 0) cleanupConfirmed = true;
|
|
156
|
+
});
|
|
157
|
+
supervisor.stdout?.on("data", (chunk) => {
|
|
158
|
+
stdoutBuf += chunk.toString("utf8");
|
|
159
|
+
const lines = stdoutBuf.split("\n");
|
|
160
|
+
stdoutBuf = lines.pop() ?? "";
|
|
161
|
+
for (const line of lines) foldLine(state, line);
|
|
162
|
+
});
|
|
163
|
+
supervisor.stderr?.on("data", (chunk) => stderr.push(chunk.toString("utf8")));
|
|
164
|
+
supervisor.stdout?.on("error", (err) => stop(`cursor-agent stdout failed: ${err.message}`));
|
|
165
|
+
supervisor.stderr?.on("error", (err) => stop(`cursor-agent stderr failed: ${err.message}`));
|
|
166
|
+
supervisor.stdin?.on("error", (err) => stop(`cursor-agent stdin failed: ${err.message}`));
|
|
167
|
+
supervisor.on("close", (_code, signal) => finish(true, signal));
|
|
168
|
+
supervisor.send({
|
|
169
|
+
command,
|
|
170
|
+
args,
|
|
171
|
+
cwd: this.config.working_dir,
|
|
172
|
+
timeoutMs
|
|
173
|
+
}, (err) => {
|
|
174
|
+
if (err) stop(`cursor-agent supervisor initialization failed: ${err.message}`);
|
|
112
175
|
});
|
|
113
|
-
|
|
114
|
-
child.stdin.end();
|
|
176
|
+
supervisor.stdin?.end(prompt);
|
|
115
177
|
});
|
|
116
178
|
}
|
|
117
179
|
};
|
package/docs/usage.md
CHANGED
|
@@ -56,7 +56,13 @@ the model to that CLI's own default.
|
|
|
56
56
|
cursor provider, so the run uses this package's own provider module, which
|
|
57
57
|
replays the CLI's `stream-json` output: the `result` event becomes the graded
|
|
58
58
|
output and `SKILL.md` reads under `.cursor/skills/` become the `skill-used`
|
|
59
|
-
evidence.
|
|
59
|
+
evidence. Grading requires both a successful result event and harness exit code
|
|
60
|
+
zero. On macOS and Linux, a supervisor owns the process group and kills remaining
|
|
61
|
+
helpers when the harness exits, times out, or the provider disconnects. Output
|
|
62
|
+
pipes have a separate two-second cleanup/drain deadline; incomplete cleanup is
|
|
63
|
+
an error. Helpers that detach into another process group are outside this cleanup
|
|
64
|
+
boundary; retained output pipes still cause a bounded error. Windows retains
|
|
65
|
+
only direct-child cleanup. The judge leg is unchanged.
|
|
60
66
|
|
|
61
67
|
`--judge` takes either a bare Claude model (graded through the Anthropic
|
|
62
68
|
selection in [auth](#auth)) or a provider-qualified promptfoo id, passed
|