@mikenguyen69/harness 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/build-identity.json +1 -0
- package/dist/cancellation.js +138 -0
- package/dist/cli.js +47 -0
- package/dist/dispatch.js +70 -0
- package/dist/help.js +47 -0
- package/dist/internal/agent/adapter/conformance.js +53 -0
- package/dist/internal/agent/adapter/hook.js +113 -0
- package/dist/internal/agent/adapter/install.js +30 -0
- package/dist/internal/agent/adapter/scope.js +55 -0
- package/dist/internal/agent/adapter/session.js +63 -0
- package/dist/internal/agent/adapter/settings.js +51 -0
- package/dist/internal/agent/cli.js +79 -0
- package/dist/internal/agent/evals/index.js +111 -0
- package/dist/internal/agent/index.js +15 -0
- package/dist/internal/agent/ledger-read.js +27 -0
- package/dist/internal/agent/routing/cli.js +104 -0
- package/dist/internal/agent/routing/index.js +123 -0
- package/dist/internal/agent/telemetry/index.js +97 -0
- package/dist/internal/orchestration/activity/index.js +111 -0
- package/dist/internal/orchestration/analyze/index.js +237 -0
- package/dist/internal/orchestration/cli.js +475 -0
- package/dist/internal/orchestration/config/index.js +66 -0
- package/dist/internal/orchestration/doctor/index.js +202 -0
- package/dist/internal/orchestration/forge/index.js +121 -0
- package/dist/internal/orchestration/git/index.js +181 -0
- package/dist/internal/orchestration/harness/cli.js +378 -0
- package/dist/internal/orchestration/harness/index.js +186 -0
- package/dist/internal/orchestration/harness/shim.js +182 -0
- package/dist/internal/orchestration/index.js +29 -0
- package/dist/internal/orchestration/loop/index.js +1107 -0
- package/dist/internal/orchestration/prompts/index.js +199 -0
- package/dist/internal/orchestration/report/index.js +125 -0
- package/dist/internal/orchestration/routes/index.js +85 -0
- package/dist/internal/orchestration/run/index.js +221 -0
- package/dist/internal/orchestration/runner/index.js +90 -0
- package/dist/internal/orchestration/runners/claude/index.js +246 -0
- package/dist/internal/orchestration/runners/claude/sdk.js +198 -0
- package/dist/internal/orchestration/runners/claude/wiring.js +98 -0
- package/dist/internal/orchestration/runners/codex/exec.js +114 -0
- package/dist/internal/orchestration/runners/codex/index.js +72 -0
- package/dist/internal/orchestration/runners/conformance/index.js +169 -0
- package/dist/internal/orchestration/runners/cursor/exec.js +531 -0
- package/dist/internal/orchestration/runners/cursor/index.js +184 -0
- package/dist/internal/orchestration/runners/stub/index.js +168 -0
- package/dist/internal/orchestration/runners/telemetry.js +36 -0
- package/dist/internal/orchestration/runstate/index.js +213 -0
- package/dist/internal/orchestration/runstate/publish.js +115 -0
- package/dist/internal/orchestration/worktree/index.js +89 -0
- package/dist/internal/system/adapters/index.js +19 -0
- package/dist/internal/system/agentops/index.js +16 -0
- package/dist/internal/system/agentops/scope.js +67 -0
- package/dist/internal/system/assets.js +45 -0
- package/dist/internal/system/board/cli.js +56 -0
- package/dist/internal/system/board/index.js +276 -0
- package/dist/internal/system/board/live.js +277 -0
- package/dist/internal/system/calibration/cli.js +45 -0
- package/dist/internal/system/calibration/index.js +141 -0
- package/dist/internal/system/cli.js +174 -0
- package/dist/internal/system/corpus/replay.js +79 -0
- package/dist/internal/system/digest/cli.js +23 -0
- package/dist/internal/system/digest/index.js +145 -0
- package/dist/internal/system/doors/classify.js +94 -0
- package/dist/internal/system/doors/cli.js +184 -0
- package/dist/internal/system/doors/index.js +249 -0
- package/dist/internal/system/doors/metrics.js +47 -0
- package/dist/internal/system/escapes/cli.js +45 -0
- package/dist/internal/system/escapes/index.js +81 -0
- package/dist/internal/system/explore/cli.js +77 -0
- package/dist/internal/system/explore/index.js +91 -0
- package/dist/internal/system/gates/cli.js +43 -0
- package/dist/internal/system/gates/detectors.js +55 -0
- package/dist/internal/system/gates/pre.js +51 -0
- package/dist/internal/system/hub/cli.js +51 -0
- package/dist/internal/system/hub/index.js +146 -0
- package/dist/internal/system/init/index.js +83 -0
- package/dist/internal/system/ledger/cli.js +54 -0
- package/dist/internal/system/ledger/index.js +176 -0
- package/dist/internal/system/manifest/index.js +207 -0
- package/dist/internal/system/provenance/index.js +13 -0
- package/dist/internal/system/report/index.js +15 -0
- package/dist/internal/system/runner/index.js +74 -0
- package/dist/internal/system/runstatus/cli.js +86 -0
- package/dist/internal/system/runstatus/index.js +168 -0
- package/dist/internal/system/sequencer/briefing.js +75 -0
- package/dist/internal/system/sequencer/cli.js +170 -0
- package/dist/internal/system/sequencer/index.js +156 -0
- package/dist/internal/system/spec/cli.js +110 -0
- package/dist/internal/system/spec/index.js +212 -0
- package/dist/internal/system/spec/materialize.js +111 -0
- package/dist/internal/system/spec/openspec-parse.js +83 -0
- package/dist/internal/system/spec/project.js +208 -0
- package/dist/internal/system/substrate/index.js +14 -0
- package/dist/internal/system/system/index.js +159 -0
- package/dist/internal/system/units/index.js +87 -0
- package/dist/internal/system/verify/cli.js +103 -0
- package/dist/internal/system/verify/declined-reasons.js +61 -0
- package/dist/internal/system/verify/index.js +188 -0
- package/dist/internal/system/verify/signing.js +79 -0
- package/dist/paths.js +50 -0
- package/dist/routes.js +93 -0
- package/dist/version.js +30 -0
- package/dist/win32-process-group.js +360 -0
- package/package.json +41 -0
- package/profiles/python/bindings.toml +59 -0
- package/profiles/terraform/bindings.toml +66 -0
- package/profiles/typescript/bindings.toml +64 -0
- package/profiles/typescript/smoke/README.md +8 -0
- package/schema/baseline.schema.json +26 -0
- package/schema/decision-classes.yaml +45 -0
- package/schema/envelope.schema.json +186 -0
- package/schema/ledger-event.schema.json +235 -0
- package/schema/manifest.schema.json +119 -0
- package/schema/routes.schema.json +55 -0
- package/schema/run-status.schema.json +162 -0
- package/schema/session-event.schema.json +74 -0
- package/schema/spec.schema.json +114 -0
- package/schema/system.schema.json +142 -0
- package/templates/ci/verify.yml +23 -0
- package/templates/target-kit/README.md +78 -0
- package/templates/target-kit/component.toml +20 -0
- package/templates/target-kit/orchestration.json +9 -0
- package/templates/target-kit/profiles/target/bindings.toml +6 -0
- package/templates/target-kit/routes.toml +5 -0
- package/templates/target-kit/specs/first-unit.yaml +11 -0
- package/templates/target-kit/system.toml +7 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exec — the ONLY module that shells `cursor-agent`. Everything else in
|
|
3
|
+
* `src/runners/cursor/` is plain glue over the agent-neutral `Runner` contract.
|
|
4
|
+
* If the Cursor Agent CLI's flag surface shifts, this file is the blast radius.
|
|
5
|
+
*
|
|
6
|
+
* `cursor-agent -p` runs one non-interactive turn. Surface used (verify against
|
|
7
|
+
* the installed `cursor-agent` at integration time — do not guess further):
|
|
8
|
+
* cursor-agent -p --output-format stream-json --trust --workspace <cwd>
|
|
9
|
+
* --model <m> [--plan | --force] "<prompt>"
|
|
10
|
+
* `--plan` is read-only (verifier); `--force` auto-approves writes (implementer).
|
|
11
|
+
* `--output-format stream-json` emits NDJSON events on stdout as the turn
|
|
12
|
+
* progresses (`system`, `thinking`, `tool_call`, `assistant`, `result`). Exit 0
|
|
13
|
+
* = the turn completed. We fold as lines arrive so an optional `onActivity`
|
|
14
|
+
* sink can observe mid-turn (execFile would buffer until exit and stay blind).
|
|
15
|
+
*
|
|
16
|
+
* `execImpl` is injectable so the runner's tests never spawn `cursor-agent`.
|
|
17
|
+
* On Windows, PATH often resolves to a `.cmd` shim — bare `spawn`/`execFile`
|
|
18
|
+
* cannot run that (ENOENT); resolve via `where` and run through `cmd.exe /c`.
|
|
19
|
+
*/
|
|
20
|
+
import { execFile, execFileSync, spawn, } from "node:child_process";
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { isAbsolute, posix as posixPath, resolve, win32 as win32Path } from "node:path";
|
|
23
|
+
const LINE_CLAMP = 400;
|
|
24
|
+
/** Board wire max is 160; watch sink keeps a longer line so stderr isn't clipped mid-thought. */
|
|
25
|
+
/** Locate `bin` on PATH (or as an absolute/relative path). */
|
|
26
|
+
export function locateCursorBin(bin, cwd) {
|
|
27
|
+
const candidate = isAbsolute(bin) ? bin : resolve(cwd, bin);
|
|
28
|
+
if (existsSync(candidate))
|
|
29
|
+
return candidate;
|
|
30
|
+
try {
|
|
31
|
+
const locator = process.platform === "win32" ? "where.exe" : "which";
|
|
32
|
+
const found = execFileSync(locator, [bin], { cwd, encoding: "utf8" })
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.find(Boolean);
|
|
36
|
+
if (found)
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// fall through — let spawn report ENOENT
|
|
41
|
+
}
|
|
42
|
+
return bin;
|
|
43
|
+
}
|
|
44
|
+
function windowsCommandLine(command, args) {
|
|
45
|
+
const quote = (value) => `"${value.replaceAll("%", "%%").replaceAll("\"", "\"\"")}"`;
|
|
46
|
+
return `"${[command, ...args].map(quote).join(" ")}"`;
|
|
47
|
+
}
|
|
48
|
+
function resolveSpawn(bin, args, cwd) {
|
|
49
|
+
const resolved = locateCursorBin(bin, cwd);
|
|
50
|
+
const isWindowsShim = process.platform === "win32" && /\.(?:cmd|bat)$/i.test(resolved);
|
|
51
|
+
if (isWindowsShim) {
|
|
52
|
+
return {
|
|
53
|
+
command: process.env.ComSpec ?? "cmd.exe",
|
|
54
|
+
args: ["/d", "/s", "/c", windowsCommandLine(resolved, args)],
|
|
55
|
+
windowsVerbatimArguments: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { command: resolved, args };
|
|
59
|
+
}
|
|
60
|
+
/** Buffered spawn (tests / callers that only need the final stdout). */
|
|
61
|
+
export async function execCursorAgentProcess(bin, args, opts) {
|
|
62
|
+
const spawnSpec = resolveSpawn(bin, args, opts.cwd);
|
|
63
|
+
return new Promise((resolvePromise) => {
|
|
64
|
+
const child = execFile(spawnSpec.command, spawnSpec.args, {
|
|
65
|
+
cwd: opts.cwd,
|
|
66
|
+
env: opts.env,
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
69
|
+
windowsHide: true,
|
|
70
|
+
...(spawnSpec.windowsVerbatimArguments
|
|
71
|
+
? { windowsVerbatimArguments: true }
|
|
72
|
+
: {}),
|
|
73
|
+
...(opts.timeoutMs !== undefined ? { timeout: opts.timeoutMs } : {}),
|
|
74
|
+
}, (error, stdout, stderr) => resolvePromise(foldProcessExit(error, stdout, stderr, opts.timeoutMs)));
|
|
75
|
+
child.stdin?.end();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Live spawn: stdout is delivered line-by-line so `onLine` can fold activity
|
|
80
|
+
* before the process exits.
|
|
81
|
+
*/
|
|
82
|
+
export async function streamCursorAgentProcess(bin, args, opts) {
|
|
83
|
+
const spawnSpec = resolveSpawn(bin, args, opts.cwd);
|
|
84
|
+
return new Promise((resolvePromise) => {
|
|
85
|
+
let stdout = "";
|
|
86
|
+
let stderr = "";
|
|
87
|
+
let settled = false;
|
|
88
|
+
let lineBuf = "";
|
|
89
|
+
const finish = (result) => {
|
|
90
|
+
if (settled)
|
|
91
|
+
return;
|
|
92
|
+
settled = true;
|
|
93
|
+
resolvePromise(result);
|
|
94
|
+
};
|
|
95
|
+
// Stay in the parent's process group / console (no `detached`) so the
|
|
96
|
+
// facade's process-group interrupt reaches this child.
|
|
97
|
+
const child = spawn(spawnSpec.command, spawnSpec.args, {
|
|
98
|
+
cwd: opts.cwd,
|
|
99
|
+
env: opts.env,
|
|
100
|
+
windowsHide: true,
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
102
|
+
...(spawnSpec.windowsVerbatimArguments
|
|
103
|
+
? { windowsVerbatimArguments: true }
|
|
104
|
+
: {}),
|
|
105
|
+
});
|
|
106
|
+
let killer;
|
|
107
|
+
if (opts.timeoutMs !== undefined) {
|
|
108
|
+
killer = setTimeout(() => {
|
|
109
|
+
child.kill();
|
|
110
|
+
finish({
|
|
111
|
+
code: 1,
|
|
112
|
+
stdout,
|
|
113
|
+
stderr: [stderr.trim(), `cursor-agent timed out after ${opts.timeoutMs}ms`]
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.join("\n"),
|
|
116
|
+
});
|
|
117
|
+
}, opts.timeoutMs);
|
|
118
|
+
}
|
|
119
|
+
const flushLines = (chunk, final = false) => {
|
|
120
|
+
lineBuf += chunk;
|
|
121
|
+
const parts = lineBuf.split(/\r?\n/);
|
|
122
|
+
lineBuf = final ? "" : (parts.pop() ?? "");
|
|
123
|
+
for (const part of parts) {
|
|
124
|
+
if (!part)
|
|
125
|
+
continue;
|
|
126
|
+
opts.onLine?.(part);
|
|
127
|
+
}
|
|
128
|
+
if (final && lineBuf) {
|
|
129
|
+
opts.onLine?.(lineBuf);
|
|
130
|
+
lineBuf = "";
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
child.stdout.setEncoding("utf8");
|
|
134
|
+
child.stderr.setEncoding("utf8");
|
|
135
|
+
child.stdout.on("data", (chunk) => {
|
|
136
|
+
stdout += chunk;
|
|
137
|
+
flushLines(chunk);
|
|
138
|
+
});
|
|
139
|
+
child.stderr.on("data", (chunk) => {
|
|
140
|
+
stderr += chunk;
|
|
141
|
+
});
|
|
142
|
+
child.on("error", (error) => {
|
|
143
|
+
if (killer)
|
|
144
|
+
clearTimeout(killer);
|
|
145
|
+
finish({
|
|
146
|
+
code: 1,
|
|
147
|
+
stdout,
|
|
148
|
+
stderr: [stderr.trim(), `spawn failed: ${error.message}`]
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.join("\n"),
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
child.on("close", (code) => {
|
|
154
|
+
if (killer)
|
|
155
|
+
clearTimeout(killer);
|
|
156
|
+
flushLines("", true);
|
|
157
|
+
finish({ code: code ?? 1, stdout, stderr });
|
|
158
|
+
});
|
|
159
|
+
// `close` waits for every stdio pipe to hit EOF. If cursor-agent spawned a
|
|
160
|
+
// background process that outlives it (e.g. an un-reaped `harness board
|
|
161
|
+
// serve` from a verifier smoke test), that descendant keeps the stdout
|
|
162
|
+
// handle open and `close` never fires — hanging the orchestrator on the
|
|
163
|
+
// review step. `exit` fires when the agent process itself ends: reap its
|
|
164
|
+
// process tree so the inherited pipes close and `close` can fire naturally
|
|
165
|
+
// with the *complete* buffer (a bare timeout race truncates the final
|
|
166
|
+
// stream-json `result` event and loses the verifier verdict). A long
|
|
167
|
+
// backstop timer covers the case where even that does not release them.
|
|
168
|
+
child.on("exit", (code) => {
|
|
169
|
+
if (settled || child.pid === undefined)
|
|
170
|
+
return;
|
|
171
|
+
try {
|
|
172
|
+
if (process.platform === "win32") {
|
|
173
|
+
spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
|
|
174
|
+
windowsHide: true,
|
|
175
|
+
stdio: "ignore",
|
|
176
|
+
}).on("error", () => undefined);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
try {
|
|
180
|
+
process.kill(-child.pid, "SIGKILL");
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
process.kill(child.pid, "SIGKILL");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// best effort — the backstop below still resolves the promise
|
|
189
|
+
}
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
if (settled)
|
|
192
|
+
return;
|
|
193
|
+
if (killer)
|
|
194
|
+
clearTimeout(killer);
|
|
195
|
+
flushLines("", true);
|
|
196
|
+
finish({ code: code ?? 0, stdout, stderr });
|
|
197
|
+
}, 20_000).unref?.();
|
|
198
|
+
});
|
|
199
|
+
child.stdin?.on("error", () => undefined); // child may exit before draining
|
|
200
|
+
child.stdin?.end(opts.stdin ?? "");
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function foldProcessExit(error, stdout, stderr, timeoutMs) {
|
|
204
|
+
if (!error)
|
|
205
|
+
return { code: 0, stdout, stderr };
|
|
206
|
+
const timedOut = timeoutMs !== undefined &&
|
|
207
|
+
(error.killed === true || error.signal != null || error.code === "ETIMEDOUT");
|
|
208
|
+
const timeoutError = timedOut ? `cursor-agent timed out after ${timeoutMs}ms` : "";
|
|
209
|
+
const spawnError = typeof error.code === "string" ? `spawn failed: ${error.code}` : "";
|
|
210
|
+
return {
|
|
211
|
+
code: typeof error.code === "number" ? error.code : 1,
|
|
212
|
+
stdout,
|
|
213
|
+
stderr: [stderr.trim(), timeoutError, spawnError].filter(Boolean).join("\n"),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export async function runCursorAgent(params) {
|
|
217
|
+
const bin = params.bin ?? process.env.CURSOR_AGENT_BIN ?? "cursor-agent";
|
|
218
|
+
// `cursor-agent -p` takes the prompt as a positional arg OR on stdin. A
|
|
219
|
+
// verifier prompt embeds the full branch diff; as an argv element that
|
|
220
|
+
// overflows the OS command-line limit (Windows: spawn ENAMETOOLONG, which
|
|
221
|
+
// crashes the orchestrator). Feed it on stdin for the real spawn; the
|
|
222
|
+
// injected `execImpl` (tests) still receives it as the last arg.
|
|
223
|
+
const baseArgs = [
|
|
224
|
+
"-p",
|
|
225
|
+
"--output-format",
|
|
226
|
+
"stream-json",
|
|
227
|
+
"--trust",
|
|
228
|
+
"--workspace",
|
|
229
|
+
params.cwd,
|
|
230
|
+
"--model",
|
|
231
|
+
params.model,
|
|
232
|
+
...(params.resumeSessionId ? ["--resume", params.resumeSessionId] : []),
|
|
233
|
+
...(params.posture === "read-only" ? ["--plan"] : ["--force"]),
|
|
234
|
+
];
|
|
235
|
+
const fold = createStreamFolder(params.onActivity, params.cwd);
|
|
236
|
+
if (params.execImpl) {
|
|
237
|
+
const { code, stdout, stderr } = await params.execImpl([...baseArgs, params.prompt], {
|
|
238
|
+
cwd: params.cwd,
|
|
239
|
+
env: params.env,
|
|
240
|
+
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs } : {}),
|
|
241
|
+
});
|
|
242
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
243
|
+
if (line.trim())
|
|
244
|
+
fold.consumeLine(line);
|
|
245
|
+
}
|
|
246
|
+
return fold.finish(code, stderr);
|
|
247
|
+
}
|
|
248
|
+
const { code, stderr } = await streamCursorAgentProcess(bin, baseArgs, {
|
|
249
|
+
cwd: params.cwd,
|
|
250
|
+
env: params.env,
|
|
251
|
+
stdin: params.prompt,
|
|
252
|
+
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs } : {}),
|
|
253
|
+
onLine: (line) => fold.consumeLine(line),
|
|
254
|
+
});
|
|
255
|
+
return fold.finish(code, stderr);
|
|
256
|
+
}
|
|
257
|
+
/** Parse buffered stdout (legacy json or stream-json) into a flat result. */
|
|
258
|
+
export function foldCursorOutput(stdout, code, stderr, onActivity, cwd) {
|
|
259
|
+
const fold = createStreamFolder(onActivity, cwd);
|
|
260
|
+
const trimmed = stdout.trim();
|
|
261
|
+
if (trimmed) {
|
|
262
|
+
try {
|
|
263
|
+
const ev = JSON.parse(trimmed);
|
|
264
|
+
fold.consumeEvent(ev);
|
|
265
|
+
return fold.finish(code, stderr);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// NDJSON / plain text below
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
272
|
+
if (line.trim())
|
|
273
|
+
fold.consumeLine(line);
|
|
274
|
+
}
|
|
275
|
+
return fold.finish(code, stderr);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* A worktree-absolute path (`<cwd>\repos\...\file.ts`) is unreadable on a
|
|
279
|
+
* board that shows five of them stacked — every activity line pays the same
|
|
280
|
+
* `.harness\worktrees\<unit>-<hash>\` prefix. Show it relative to the
|
|
281
|
+
* workspace root instead; leave anything outside the workspace alone.
|
|
282
|
+
*
|
|
283
|
+
* cursor-agent emits host-native paths (Windows paths on Windows), but the
|
|
284
|
+
* board and the tests may run on Linux — so pick the path flavour from the
|
|
285
|
+
* string rather than from `process.platform`.
|
|
286
|
+
*/
|
|
287
|
+
function shortenPath(path, cwd) {
|
|
288
|
+
if (!path || !cwd)
|
|
289
|
+
return path;
|
|
290
|
+
const p = /^[a-zA-Z]:[\\/]|^\\\\/.test(path) ? win32Path : posixPath;
|
|
291
|
+
if (!p.isAbsolute(path))
|
|
292
|
+
return path;
|
|
293
|
+
const rel = p.relative(cwd, path);
|
|
294
|
+
if (!rel || rel.startsWith(".."))
|
|
295
|
+
return path;
|
|
296
|
+
return rel.split(/[\\/]/).join("/");
|
|
297
|
+
}
|
|
298
|
+
function createStreamFolder(onActivity, cwd) {
|
|
299
|
+
let resultText = "";
|
|
300
|
+
let costUsd = 0;
|
|
301
|
+
let turn = 0;
|
|
302
|
+
let sessionId = "";
|
|
303
|
+
let tokens = { in: 0, out: 0 };
|
|
304
|
+
const toolNames = new Map();
|
|
305
|
+
const emit = (line, kind, tool, target) => {
|
|
306
|
+
if (!onActivity)
|
|
307
|
+
return;
|
|
308
|
+
try {
|
|
309
|
+
onActivity({
|
|
310
|
+
line: clamp(line, LINE_CLAMP),
|
|
311
|
+
turn,
|
|
312
|
+
tokens: { ...tokens },
|
|
313
|
+
kind,
|
|
314
|
+
...(tool ? { tool } : {}),
|
|
315
|
+
...(target ? { target } : {}),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// broken sink must not fail the session
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const consumeEvent = (ev) => {
|
|
323
|
+
if (!sessionId && typeof ev.session_id === "string")
|
|
324
|
+
sessionId = ev.session_id;
|
|
325
|
+
const usage = usageTokens(ev.usage);
|
|
326
|
+
if (usage)
|
|
327
|
+
tokens = usage;
|
|
328
|
+
costUsd += usageToCostUsd(ev.usage);
|
|
329
|
+
const type = typeof ev.type === "string" ? ev.type : "";
|
|
330
|
+
if (type === "tool_call") {
|
|
331
|
+
const subtype = typeof ev.subtype === "string" ? ev.subtype : "";
|
|
332
|
+
const callId = typeof ev.call_id === "string" ? ev.call_id : "";
|
|
333
|
+
const { name, target } = describeToolCall(ev.tool_call, cwd);
|
|
334
|
+
if (callId && name)
|
|
335
|
+
toolNames.set(callId, name);
|
|
336
|
+
if (subtype === "started") {
|
|
337
|
+
turn += 1;
|
|
338
|
+
emit(target ? `${name}: ${target}` : name, "tool", name, target || undefined);
|
|
339
|
+
}
|
|
340
|
+
else if (subtype === "completed") {
|
|
341
|
+
// Success completes are redundant with the started line; only surface failures.
|
|
342
|
+
if (!toolCallSucceeded(ev.tool_call)) {
|
|
343
|
+
const detail = toolCallErrorDetail(ev.tool_call);
|
|
344
|
+
emit(detail ? `${name} -> error: ${detail}` : `${name} -> error`, "tool-result", name, target || undefined);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (type === "assistant") {
|
|
350
|
+
turn += 1;
|
|
351
|
+
const text = assistantText(ev);
|
|
352
|
+
emit(text ? `assistant: ${text}` : "assistant", "text");
|
|
353
|
+
const fromMsg = pickResultText(ev);
|
|
354
|
+
if (fromMsg)
|
|
355
|
+
resultText = fromMsg;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (type === "result" || type === "" || type === "message") {
|
|
359
|
+
const text = pickResultText(ev);
|
|
360
|
+
if (text)
|
|
361
|
+
resultText = text;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
return {
|
|
365
|
+
consumeLine(line) {
|
|
366
|
+
const t = line.trim();
|
|
367
|
+
if (!t)
|
|
368
|
+
return;
|
|
369
|
+
try {
|
|
370
|
+
consumeEvent(JSON.parse(t));
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
if (!resultText)
|
|
374
|
+
resultText = t;
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
consumeEvent,
|
|
378
|
+
finish(code, stderr) {
|
|
379
|
+
const sid = sessionId ? { sessionId } : {};
|
|
380
|
+
return code === 0
|
|
381
|
+
? { ok: true, resultText, costUsd, ...sid }
|
|
382
|
+
: {
|
|
383
|
+
ok: false,
|
|
384
|
+
resultText,
|
|
385
|
+
costUsd,
|
|
386
|
+
...sid,
|
|
387
|
+
error: stderr.trim() || resultText || `cursor-agent exit ${code}`,
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function describeToolCall(toolCall, cwd) {
|
|
393
|
+
if (!toolCall || typeof toolCall !== "object") {
|
|
394
|
+
return { name: "tool", target: "" };
|
|
395
|
+
}
|
|
396
|
+
const tc = toolCall;
|
|
397
|
+
for (const [key, value] of Object.entries(tc)) {
|
|
398
|
+
if (!key.endsWith("ToolCall") || !value || typeof value !== "object")
|
|
399
|
+
continue;
|
|
400
|
+
const name = key.replace(/ToolCall$/, "") || "tool";
|
|
401
|
+
const body = value;
|
|
402
|
+
const args = (body.args ?? {});
|
|
403
|
+
const description = (typeof body.description === "string" && body.description) ||
|
|
404
|
+
(typeof args.description === "string" && args.description) ||
|
|
405
|
+
"";
|
|
406
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
407
|
+
const path = (typeof args.path === "string" && args.path) ||
|
|
408
|
+
(typeof args.targetDirectory === "string" && args.targetDirectory) ||
|
|
409
|
+
(typeof args.filePath === "string" && args.filePath) ||
|
|
410
|
+
"";
|
|
411
|
+
const target = description || command || shortenPath(path, cwd) || "";
|
|
412
|
+
return { name, target };
|
|
413
|
+
}
|
|
414
|
+
return { name: "tool", target: "" };
|
|
415
|
+
}
|
|
416
|
+
function toolCallSucceeded(toolCall) {
|
|
417
|
+
if (!toolCall || typeof toolCall !== "object")
|
|
418
|
+
return true;
|
|
419
|
+
for (const value of Object.values(toolCall)) {
|
|
420
|
+
if (!value || typeof value !== "object")
|
|
421
|
+
continue;
|
|
422
|
+
const result = value.result;
|
|
423
|
+
if (!result || typeof result !== "object")
|
|
424
|
+
continue;
|
|
425
|
+
if ("success" in result)
|
|
426
|
+
return true;
|
|
427
|
+
if ("error" in result || "failure" in result)
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
/** One-line error excerpt from a failed tool_call / completed payload. */
|
|
433
|
+
function toolCallErrorDetail(toolCall) {
|
|
434
|
+
if (!toolCall || typeof toolCall !== "object")
|
|
435
|
+
return "";
|
|
436
|
+
for (const value of Object.values(toolCall)) {
|
|
437
|
+
if (!value || typeof value !== "object")
|
|
438
|
+
continue;
|
|
439
|
+
const result = value.result;
|
|
440
|
+
if (!result || typeof result !== "object")
|
|
441
|
+
continue;
|
|
442
|
+
const r = result;
|
|
443
|
+
if (typeof r.error === "string")
|
|
444
|
+
return normalizeErrorExcerpt(r.error);
|
|
445
|
+
if (r.error && typeof r.error === "object") {
|
|
446
|
+
const err = r.error;
|
|
447
|
+
const msg = (typeof err.message === "string" && err.message) ||
|
|
448
|
+
(typeof err.stderr === "string" && err.stderr) ||
|
|
449
|
+
(typeof err.stdout === "string" && err.stdout) ||
|
|
450
|
+
"";
|
|
451
|
+
if (msg)
|
|
452
|
+
return normalizeErrorExcerpt(msg);
|
|
453
|
+
}
|
|
454
|
+
if (typeof r.failure === "string")
|
|
455
|
+
return normalizeErrorExcerpt(r.failure);
|
|
456
|
+
if (r.failure && typeof r.failure === "object") {
|
|
457
|
+
const fail = r.failure;
|
|
458
|
+
if (typeof fail.message === "string")
|
|
459
|
+
return normalizeErrorExcerpt(fail.message);
|
|
460
|
+
}
|
|
461
|
+
const success = r.success;
|
|
462
|
+
if (success && typeof success === "object") {
|
|
463
|
+
const s = success;
|
|
464
|
+
if (typeof s.exitCode === "number" && s.exitCode !== 0) {
|
|
465
|
+
return `exit ${s.exitCode}`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return "";
|
|
470
|
+
}
|
|
471
|
+
function normalizeErrorExcerpt(text) {
|
|
472
|
+
return text.replace(/\s+/g, " ").trim();
|
|
473
|
+
}
|
|
474
|
+
function assistantText(ev) {
|
|
475
|
+
const message = ev.message;
|
|
476
|
+
if (!message || typeof message !== "object")
|
|
477
|
+
return "";
|
|
478
|
+
const content = message.content;
|
|
479
|
+
if (typeof content === "string")
|
|
480
|
+
return content;
|
|
481
|
+
if (!Array.isArray(content))
|
|
482
|
+
return "";
|
|
483
|
+
const parts = [];
|
|
484
|
+
for (const block of content) {
|
|
485
|
+
if (!block || typeof block !== "object")
|
|
486
|
+
continue;
|
|
487
|
+
const b = block;
|
|
488
|
+
if (b.type === "text" && typeof b.text === "string")
|
|
489
|
+
parts.push(b.text);
|
|
490
|
+
}
|
|
491
|
+
return parts.join(" ").trim();
|
|
492
|
+
}
|
|
493
|
+
function pickResultText(ev) {
|
|
494
|
+
if (typeof ev.result === "string")
|
|
495
|
+
return ev.result;
|
|
496
|
+
const fromAssistant = assistantText(ev);
|
|
497
|
+
if (fromAssistant)
|
|
498
|
+
return fromAssistant;
|
|
499
|
+
return (pickString(ev, ["message", "text", "content", "last_message"]) ??
|
|
500
|
+
(ev.type === "message" ? pickString(ev, ["data"]) : undefined));
|
|
501
|
+
}
|
|
502
|
+
function usageTokens(usage) {
|
|
503
|
+
if (!usage || typeof usage !== "object")
|
|
504
|
+
return undefined;
|
|
505
|
+
const u = usage;
|
|
506
|
+
const input = (typeof u.inputTokens === "number" ? u.inputTokens : undefined) ??
|
|
507
|
+
(typeof u.input_tokens === "number" ? u.input_tokens : undefined);
|
|
508
|
+
const output = (typeof u.outputTokens === "number" ? u.outputTokens : undefined) ??
|
|
509
|
+
(typeof u.output_tokens === "number" ? u.output_tokens : undefined);
|
|
510
|
+
if (input === undefined && output === undefined)
|
|
511
|
+
return undefined;
|
|
512
|
+
return { in: input ?? 0, out: output ?? 0 };
|
|
513
|
+
}
|
|
514
|
+
function usageToCostUsd(usage) {
|
|
515
|
+
const t = usageTokens(usage);
|
|
516
|
+
if (!t)
|
|
517
|
+
return 0;
|
|
518
|
+
return (t.in / 1e6) * 3 + (t.out / 1e6) * 15;
|
|
519
|
+
}
|
|
520
|
+
function pickString(o, keys) {
|
|
521
|
+
for (const k of keys)
|
|
522
|
+
if (typeof o[k] === "string")
|
|
523
|
+
return o[k];
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
function clamp(s, max) {
|
|
527
|
+
if (s.length <= max)
|
|
528
|
+
return s;
|
|
529
|
+
return s.slice(0, Math.max(0, max - 3)) + "...";
|
|
530
|
+
}
|
|
531
|
+
//# sourceMappingURL=exec.js.map
|