@onkernel/cua-cli 0.1.0
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/README.md +170 -0
- package/dist/cli.js +1758 -0
- package/dist/harness-models-GT8Ke1vt.js +106 -0
- package/dist/main-Bphx_zOj.js +899 -0
- package/package.json +48 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1758 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as createKernelClient, i as captureScreenshot, n as listSupportedModels, o as provisionBrowser, r as resolveCuaModelRef, s as resolveProfileId, t as DEFAULT_CUA_MODEL_REF } from "./harness-models-GT8Ke1vt.js";
|
|
3
|
+
import { stderr, stdout } from "node:process";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { CuaAgentHarness, InMemorySessionRepo, JsonlSessionRepo, NodeExecutionEnv, formatSkillsForSystemPrompt, loadSkills } from "@onkernel/cua-agent";
|
|
6
|
+
import { getCuaEnvApiKey, getCuaModel, parseCuaModelRef, requireCuaEnvApiKey, resolveCuaRuntimeSpec } from "@onkernel/cua-ai";
|
|
7
|
+
import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
8
|
+
import { createCodingTools } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
//#region src/action/prompts.ts
|
|
13
|
+
function buildPrompt(req) {
|
|
14
|
+
switch (req.action) {
|
|
15
|
+
case "click":
|
|
16
|
+
if (!req.target) throw new Error("click action requires a target description");
|
|
17
|
+
return clickPrompt(req.target);
|
|
18
|
+
case "type":
|
|
19
|
+
if (!req.target) throw new Error("type action requires a target description");
|
|
20
|
+
if (!req.text) throw new Error("type action requires text to type");
|
|
21
|
+
return typePrompt(req.target, req.text);
|
|
22
|
+
case "open": {
|
|
23
|
+
const url = req.text || req.target;
|
|
24
|
+
if (!url) throw new Error("open action requires a URL");
|
|
25
|
+
return openPrompt(url);
|
|
26
|
+
}
|
|
27
|
+
case "press":
|
|
28
|
+
if (!req.keys || req.keys.length === 0) throw new Error("press action requires at least one key");
|
|
29
|
+
return pressPrompt(req.keys);
|
|
30
|
+
case "observe":
|
|
31
|
+
if (req.text) return observeWithQuestionPrompt(req.text);
|
|
32
|
+
return observePrompt();
|
|
33
|
+
case "url": return urlPrompt();
|
|
34
|
+
case "do": {
|
|
35
|
+
const instruction = req.text || req.target;
|
|
36
|
+
if (!instruction) throw new Error("do action requires an instruction");
|
|
37
|
+
return instruction;
|
|
38
|
+
}
|
|
39
|
+
case "screenshot": throw new Error("screenshot action does not use a prompt");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function clickPrompt(target) {
|
|
43
|
+
return `Look at the current screen. Locate and click the element that best matches this description: ${JSON.stringify(target)}.
|
|
44
|
+
Perform exactly ONE click on the best matching element, then stop.
|
|
45
|
+
If no matching element is visible on screen, respond with the text: NOT_FOUND: followed by a brief explanation.
|
|
46
|
+
Do not perform any other actions.`;
|
|
47
|
+
}
|
|
48
|
+
function typePrompt(target, text) {
|
|
49
|
+
return `Look at the current screen. Locate the input/text field that best matches this description: ${JSON.stringify(target)}.
|
|
50
|
+
Click on it to focus it, then type exactly this text: ${JSON.stringify(text)}
|
|
51
|
+
Perform only the click and type actions, then stop.
|
|
52
|
+
If no matching element is visible on screen, respond with the text: NOT_FOUND: followed by a brief explanation.
|
|
53
|
+
Do not perform any other actions.`;
|
|
54
|
+
}
|
|
55
|
+
function openPrompt(url) {
|
|
56
|
+
return `Navigate the browser to this URL: ${url}
|
|
57
|
+
Use the goto action. Perform only this navigation, then stop.`;
|
|
58
|
+
}
|
|
59
|
+
function pressPrompt(keys) {
|
|
60
|
+
return `Press the following key(s): ${keys.join("+")}
|
|
61
|
+
Perform exactly this key press, then stop. Do not perform any other actions.`;
|
|
62
|
+
}
|
|
63
|
+
function observePrompt() {
|
|
64
|
+
return `Look at the current screen and describe what you see. Be concise and factual.
|
|
65
|
+
Do NOT perform any actions. Only observe and describe.`;
|
|
66
|
+
}
|
|
67
|
+
function observeWithQuestionPrompt(question) {
|
|
68
|
+
return `Look at the current screen and answer this question: ${JSON.stringify(question)}
|
|
69
|
+
Be concise and factual. Do NOT perform any actions. Only observe and respond.`;
|
|
70
|
+
}
|
|
71
|
+
function urlPrompt() {
|
|
72
|
+
return `Report the current page URL. Use the url action to read it. Do not perform any other actions.`;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/action/result.ts
|
|
76
|
+
/**
|
|
77
|
+
* Build a structured ActionResult from the agent's final assistant text
|
|
78
|
+
* and any action events captured during the run.
|
|
79
|
+
*/
|
|
80
|
+
function parseResult(action, textOutput, actionEvents, elapsedMs, toolError) {
|
|
81
|
+
const trimmed = textOutput.trim();
|
|
82
|
+
const result = {
|
|
83
|
+
action,
|
|
84
|
+
status: "ok",
|
|
85
|
+
elapsedMs,
|
|
86
|
+
timestamp: Date.now()
|
|
87
|
+
};
|
|
88
|
+
if (toolError && toolError.trim().length > 0) {
|
|
89
|
+
result.status = "error";
|
|
90
|
+
result.text = toolError.trim();
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
if (trimmed.startsWith("NOT_FOUND:")) {
|
|
94
|
+
result.status = "not_found";
|
|
95
|
+
result.text = trimmed.slice(10).trim();
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
for (let i = actionEvents.length - 1; i >= 0; i--) {
|
|
99
|
+
const ev = actionEvents[i];
|
|
100
|
+
if (ev.x !== void 0 && ev.y !== void 0 && (ev.actionType === "click" || ev.actionType === "double_click" || ev.actionType === "click_mouse" || ev.actionType === "left_click" || ev.actionType === "right_click" || ev.actionType === "middle_click" || ev.actionType === "triple_click" || ev.actionType === "click_at")) {
|
|
101
|
+
result.coordinates = [ev.x, ev.y];
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
switch (action) {
|
|
106
|
+
case "observe":
|
|
107
|
+
result.text = trimmed;
|
|
108
|
+
break;
|
|
109
|
+
case "url": {
|
|
110
|
+
const url = extractFirstUrl(trimmed);
|
|
111
|
+
if (url) result.url = url;
|
|
112
|
+
else if (trimmed) {
|
|
113
|
+
result.status = "error";
|
|
114
|
+
result.text = trimmed;
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
default: break;
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function extractFirstUrl(text) {
|
|
123
|
+
const matches = text.match(/(?:https?:\/\/\S+|about:blank|file:\/\/\S+|chrome:\/\/\S+|chrome-extension:\/\/\S+|edge:\/\/\S+|brave:\/\/\S+)/gi);
|
|
124
|
+
if (!matches || matches.length === 0) return void 0;
|
|
125
|
+
return matches[matches.length - 1].replace(/[),.;!?]+$/, "");
|
|
126
|
+
}
|
|
127
|
+
function formatCompact(r) {
|
|
128
|
+
switch (r.status) {
|
|
129
|
+
case "not_found": return r.text ? `not_found ${r.text}` : "not_found";
|
|
130
|
+
case "error": return r.text ? `error ${r.text}` : "error";
|
|
131
|
+
case "timeout": return "timeout";
|
|
132
|
+
}
|
|
133
|
+
switch (r.action) {
|
|
134
|
+
case "click":
|
|
135
|
+
if (r.coordinates) return `ok clicked (${r.coordinates[0]}, ${r.coordinates[1]})`;
|
|
136
|
+
return "ok clicked";
|
|
137
|
+
case "type": return "ok typed";
|
|
138
|
+
case "open": return "ok";
|
|
139
|
+
case "press": return "ok pressed";
|
|
140
|
+
case "observe": return r.text ?? "";
|
|
141
|
+
case "url": return r.url ?? r.text ?? "";
|
|
142
|
+
case "screenshot": return "ok";
|
|
143
|
+
case "do": return r.text ?? "ok";
|
|
144
|
+
default: return "ok";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function exitCodeFor(r) {
|
|
148
|
+
switch (r.status) {
|
|
149
|
+
case "ok": return 0;
|
|
150
|
+
case "not_found": return 1;
|
|
151
|
+
default: return 2;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/action/harness-runner.ts
|
|
156
|
+
/**
|
|
157
|
+
* Run a single action subcommand against an existing harness + browser and
|
|
158
|
+
* return the parsed result plus exit code. The `screenshot` action is
|
|
159
|
+
* model-free — it captures directly through the SDK. All other actions
|
|
160
|
+
* drive the harness for at most `maxTurns` turns.
|
|
161
|
+
*/
|
|
162
|
+
async function runAction(req, opts, screenshot) {
|
|
163
|
+
const startedAt = Date.now();
|
|
164
|
+
if (req.action === "screenshot") {
|
|
165
|
+
const out = screenshot ?? { out: "screenshot.png" };
|
|
166
|
+
const png = await captureScreenshot(opts.browserHandle.client, opts.browserHandle.browser.session_id);
|
|
167
|
+
if (!png) {
|
|
168
|
+
const result = {
|
|
169
|
+
action: "screenshot",
|
|
170
|
+
status: "error",
|
|
171
|
+
text: "failed to capture screenshot",
|
|
172
|
+
elapsedMs: Date.now() - startedAt,
|
|
173
|
+
timestamp: Date.now()
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
result,
|
|
177
|
+
exitCode: exitCodeFor(result)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (out.out === "-") stdout.write(png);
|
|
181
|
+
else await writeFile(out.out, png);
|
|
182
|
+
const result = parseResult("screenshot", "", [], Date.now() - startedAt);
|
|
183
|
+
result.text = out.out === "-" ? "(stdout)" : out.out;
|
|
184
|
+
return {
|
|
185
|
+
result,
|
|
186
|
+
exitCode: 0
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const prompt = buildPrompt(req);
|
|
190
|
+
const maxTurns = req.maxTurns ?? opts.maxTurns ?? 3;
|
|
191
|
+
const events = [];
|
|
192
|
+
let assistantText = "";
|
|
193
|
+
let turns = 0;
|
|
194
|
+
let aborted = false;
|
|
195
|
+
let lastToolError;
|
|
196
|
+
let lastToolErrorDetail;
|
|
197
|
+
const unsubscribe = opts.harness.subscribe((event) => {
|
|
198
|
+
switch (event.type) {
|
|
199
|
+
case "tool_execution_start":
|
|
200
|
+
collectActionEvent(event.toolName, event.args, events);
|
|
201
|
+
return;
|
|
202
|
+
case "tool_execution_end":
|
|
203
|
+
if (event.isError) {
|
|
204
|
+
const { text, detail } = inspectToolError(event.result);
|
|
205
|
+
lastToolError = text ?? "tool execution failed";
|
|
206
|
+
lastToolErrorDetail = detail;
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
case "message_update":
|
|
210
|
+
if (event.assistantMessageEvent.type === "text_delta") assistantText += event.assistantMessageEvent.delta;
|
|
211
|
+
return;
|
|
212
|
+
case "turn_end":
|
|
213
|
+
turns += 1;
|
|
214
|
+
if (turns >= maxTurns && !aborted) {
|
|
215
|
+
aborted = true;
|
|
216
|
+
opts.harness.abort();
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
default: return;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
let runError;
|
|
223
|
+
let assistant;
|
|
224
|
+
try {
|
|
225
|
+
const images = await maybeInitialScreenshot$1(opts);
|
|
226
|
+
assistant = await opts.harness.prompt(prompt, images ? { images } : void 0);
|
|
227
|
+
if (assistant.stopReason === "error") runError = new Error(assistant.errorMessage ?? "agent stopped with error");
|
|
228
|
+
} catch (err) {
|
|
229
|
+
runError = err instanceof Error ? err : new Error(String(err));
|
|
230
|
+
} finally {
|
|
231
|
+
unsubscribe();
|
|
232
|
+
}
|
|
233
|
+
const elapsed = Date.now() - startedAt;
|
|
234
|
+
if (runError) {
|
|
235
|
+
const result = {
|
|
236
|
+
action: req.action,
|
|
237
|
+
status: "error",
|
|
238
|
+
text: runError.message,
|
|
239
|
+
elapsedMs: elapsed,
|
|
240
|
+
timestamp: Date.now()
|
|
241
|
+
};
|
|
242
|
+
return {
|
|
243
|
+
result,
|
|
244
|
+
exitCode: exitCodeFor(result)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (!assistantText.trim() && assistant) assistantText = textFromAssistant(assistant);
|
|
248
|
+
const toolError = lastToolErrorDetail ?? lastToolError;
|
|
249
|
+
const result = parseResult(req.action, assistantText, events, elapsed, toolError);
|
|
250
|
+
return {
|
|
251
|
+
result,
|
|
252
|
+
exitCode: exitCodeFor(result)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function maybeInitialScreenshot$1(opts) {
|
|
256
|
+
if (opts.skipInitialScreenshot) return void 0;
|
|
257
|
+
if (await sessionHasPriorTurn$1(opts.session)) return void 0;
|
|
258
|
+
const png = await captureScreenshot(opts.browserHandle.client, opts.browserHandle.browser.session_id);
|
|
259
|
+
if (!png) return void 0;
|
|
260
|
+
return [{
|
|
261
|
+
type: "image",
|
|
262
|
+
data: png.toString("base64"),
|
|
263
|
+
mimeType: "image/png"
|
|
264
|
+
}];
|
|
265
|
+
}
|
|
266
|
+
async function sessionHasPriorTurn$1(session) {
|
|
267
|
+
const entries = await session.getBranch();
|
|
268
|
+
for (const entry of entries) if (entry.type === "message" && (entry.message.role === "user" || entry.message.role === "assistant")) return true;
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
function textFromAssistant(message) {
|
|
272
|
+
const parts = [];
|
|
273
|
+
for (const block of message.content) if (block && block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
274
|
+
return parts.join("");
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Collect click coordinates from canonical CUA tool calls. The harness
|
|
278
|
+
* dispatches batched calls via `computer_batch` (args: { actions: [...] })
|
|
279
|
+
* and single-action calls via per-action tools (args: cua action without
|
|
280
|
+
* the `type` field, which we recover from the tool name).
|
|
281
|
+
*/
|
|
282
|
+
function collectActionEvent(toolName, args, events) {
|
|
283
|
+
if (toolName === "computer_batch") {
|
|
284
|
+
const actions = args.actions;
|
|
285
|
+
if (Array.isArray(actions)) {
|
|
286
|
+
for (const action of actions) if (action && typeof action === "object") addClickEvent(action.type, action.x, action.y, events);
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (args && typeof args === "object") {
|
|
291
|
+
const x = args.x;
|
|
292
|
+
const y = args.y;
|
|
293
|
+
addClickEvent(toolName, x, y, events);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function addClickEvent(type, x, y, events) {
|
|
297
|
+
if (typeof type !== "string") return;
|
|
298
|
+
if (type !== "click" && type !== "double_click") return;
|
|
299
|
+
if (typeof x !== "number" || typeof y !== "number") return;
|
|
300
|
+
events.push({
|
|
301
|
+
actionType: type,
|
|
302
|
+
x,
|
|
303
|
+
y
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function inspectToolError(result) {
|
|
307
|
+
if (!result || typeof result !== "object") return {};
|
|
308
|
+
const detailsError = result.details?.error;
|
|
309
|
+
const detail = typeof detailsError === "string" ? detailsError.trim() : void 0;
|
|
310
|
+
const content = result.content;
|
|
311
|
+
if (!Array.isArray(content)) return { detail };
|
|
312
|
+
const parts = [];
|
|
313
|
+
for (const block of content) if (block && typeof block === "object" && block.type === "text") {
|
|
314
|
+
const text = block.text;
|
|
315
|
+
if (typeof text === "string" && text.trim().length > 0) parts.push(text.trim());
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
text: parts.length > 0 ? parts.join("\n") : void 0,
|
|
319
|
+
detail
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/** Print a compact result line and return its exit code. */
|
|
323
|
+
function emitCompact(res) {
|
|
324
|
+
const text = formatCompact(res.result);
|
|
325
|
+
if (text) stdout.write(`${text}\n`);
|
|
326
|
+
if (res.exitCode !== 0 && !text.startsWith("error") && res.result.status === "error") stderr.write(`error ${res.result.text ?? ""}\n`);
|
|
327
|
+
return res.exitCode;
|
|
328
|
+
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/harness.ts
|
|
331
|
+
/**
|
|
332
|
+
* Build a `CuaAgentHarness` wired with cua-cli's defaults: pi `NodeExecutionEnv`,
|
|
333
|
+
* caller-supplied jsonl `Session`, pi-coding-agent's `createCodingTools` as
|
|
334
|
+
* `extraTools`, env-var API-key resolution (via cua-ai conventions), and a
|
|
335
|
+
* `systemPrompt` that composes the runtime spec's default prompt with the
|
|
336
|
+
* formatted skill block.
|
|
337
|
+
*/
|
|
338
|
+
function buildCuaHarness(opts) {
|
|
339
|
+
const skills = opts.skills ?? [];
|
|
340
|
+
const extraTools = opts.extraTools ?? createCodingTools(opts.cwd);
|
|
341
|
+
const model = opts.modelBaseUrl ? {
|
|
342
|
+
...getCuaModel(opts.model),
|
|
343
|
+
baseUrl: opts.modelBaseUrl
|
|
344
|
+
} : opts.model;
|
|
345
|
+
return new CuaAgentHarness({
|
|
346
|
+
env: new NodeExecutionEnv({ cwd: opts.cwd }),
|
|
347
|
+
session: opts.session,
|
|
348
|
+
model,
|
|
349
|
+
browser: opts.browser,
|
|
350
|
+
client: opts.client,
|
|
351
|
+
extraTools,
|
|
352
|
+
resources: { skills },
|
|
353
|
+
thinkingLevel: opts.thinkingLevel,
|
|
354
|
+
systemPrompt: ({ model: activeModel, resources }) => {
|
|
355
|
+
return composeSystemPrompt(resolveCuaRuntimeSpec(activeModel).defaultSystemPrompt, resources.skills ?? []);
|
|
356
|
+
},
|
|
357
|
+
getApiKeyAndHeaders: opts.getApiKeyAndHeaders ?? (async (resolvedModel) => {
|
|
358
|
+
const apiKey = getCuaEnvApiKey(resolvedModel.provider);
|
|
359
|
+
return apiKey ? { apiKey } : void 0;
|
|
360
|
+
})
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function composeSystemPrompt(base, skills) {
|
|
364
|
+
const skillBlock = formatSkillsForSystemPrompt(skills).trim();
|
|
365
|
+
if (!skillBlock) return base;
|
|
366
|
+
return `${base.trim()}\n\n${skillBlock}\n`;
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/harness-named-sessions.ts
|
|
370
|
+
const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
371
|
+
function namedSessionsDir() {
|
|
372
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
373
|
+
if (xdg) return join(xdg, "cua", "named-sessions");
|
|
374
|
+
return join(homedir(), ".local", "share", "cua", "named-sessions");
|
|
375
|
+
}
|
|
376
|
+
function sessionFilePath(name) {
|
|
377
|
+
return join(namedSessionsDir(), `${name}.json`);
|
|
378
|
+
}
|
|
379
|
+
function validateSlug(name) {
|
|
380
|
+
if (!SLUG_PATTERN.test(name)) throw new Error(`invalid session name "${name}": must match ${SLUG_PATTERN} (lowercase a-z, 0-9, hyphens; 1-63 chars; cannot start with a hyphen)`);
|
|
381
|
+
}
|
|
382
|
+
async function fileExists(path) {
|
|
383
|
+
try {
|
|
384
|
+
await stat(path);
|
|
385
|
+
return true;
|
|
386
|
+
} catch {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function readNamedSession(name) {
|
|
391
|
+
const path = sessionFilePath(name);
|
|
392
|
+
if (!await fileExists(path)) return void 0;
|
|
393
|
+
const raw = await readFile(path, "utf8");
|
|
394
|
+
return JSON.parse(raw);
|
|
395
|
+
}
|
|
396
|
+
async function writeNamedSession(meta) {
|
|
397
|
+
validateSlug(meta.name);
|
|
398
|
+
const path = sessionFilePath(meta.name);
|
|
399
|
+
await mkdir(namedSessionsDir(), { recursive: true });
|
|
400
|
+
await writeFile(path, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
|
|
401
|
+
return path;
|
|
402
|
+
}
|
|
403
|
+
async function deleteNamedSession(name) {
|
|
404
|
+
const path = sessionFilePath(name);
|
|
405
|
+
if (!await fileExists(path)) return false;
|
|
406
|
+
await unlink(path);
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
async function listNamedSessions() {
|
|
410
|
+
const dir = namedSessionsDir();
|
|
411
|
+
if (!await fileExists(dir)) return [];
|
|
412
|
+
const entries = await readdir(dir);
|
|
413
|
+
const out = [];
|
|
414
|
+
for (const entry of entries) {
|
|
415
|
+
if (!entry.endsWith(".json")) continue;
|
|
416
|
+
try {
|
|
417
|
+
const raw = await readFile(join(dir, entry), "utf8");
|
|
418
|
+
out.push(JSON.parse(raw));
|
|
419
|
+
} catch {}
|
|
420
|
+
}
|
|
421
|
+
out.sort((a, b) => b.created_at - a.created_at);
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
/** Provision a fresh Kernel browser and persist a named-session metadata file. */
|
|
425
|
+
async function startNamedSession(opts) {
|
|
426
|
+
validateSlug(opts.name);
|
|
427
|
+
const existing = await readNamedSession(opts.name);
|
|
428
|
+
if (existing) throw new Error(`named session "${opts.name}" already exists (kernel_session_id=${existing.kernel_session_id}). Run \`cua session stop ${opts.name}\` first.`);
|
|
429
|
+
const client = createKernelClient(opts.apiKey, opts.baseUrl);
|
|
430
|
+
const timeoutSeconds = opts.browserTimeoutSeconds && opts.browserTimeoutSeconds > 0 ? opts.browserTimeoutSeconds : 300;
|
|
431
|
+
let profileId;
|
|
432
|
+
if (opts.profileSelector && opts.profileSelector.trim()) profileId = await resolveProfileId(client, opts.profileSelector);
|
|
433
|
+
const params = {
|
|
434
|
+
stealth: true,
|
|
435
|
+
timeout_seconds: timeoutSeconds
|
|
436
|
+
};
|
|
437
|
+
if (profileId) params.profile = {
|
|
438
|
+
id: profileId,
|
|
439
|
+
save_changes: opts.saveProfileChanges ?? false
|
|
440
|
+
};
|
|
441
|
+
const browser = await client.browsers.create(params);
|
|
442
|
+
const meta = {
|
|
443
|
+
name: opts.name,
|
|
444
|
+
kernel_session_id: browser.session_id,
|
|
445
|
+
live_url: browser.browser_live_view_url,
|
|
446
|
+
profile_id: profileId,
|
|
447
|
+
created_at: Date.now()
|
|
448
|
+
};
|
|
449
|
+
return {
|
|
450
|
+
meta,
|
|
451
|
+
metadataPath: await writeNamedSession(meta),
|
|
452
|
+
client,
|
|
453
|
+
browser
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Attach to a previously-started named session. Performs a liveness check
|
|
458
|
+
* via `client.browsers.retrieve` so the caller can fail fast when the
|
|
459
|
+
* server-side session has timed out or been deleted.
|
|
460
|
+
*/
|
|
461
|
+
async function attachNamedSession(opts) {
|
|
462
|
+
const meta = await readNamedSession(opts.name);
|
|
463
|
+
if (!meta) throw new Error(`unknown named session "${opts.name}". Run \`cua session list\` to see available sessions, or \`cua session start ${opts.name}\` to create one.`);
|
|
464
|
+
const client = createKernelClient(opts.apiKey, opts.baseUrl);
|
|
465
|
+
let browser;
|
|
466
|
+
try {
|
|
467
|
+
browser = await client.browsers.retrieve(meta.kernel_session_id);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
if (err.status === 404) throw new Error(`named session "${opts.name}" is no longer alive on Kernel (browser timed out or was deleted). Run \`cua session stop ${opts.name} && cua session start ${opts.name}\` to provision a fresh one.`);
|
|
470
|
+
throw new Error(`liveness check for named session "${opts.name}" failed: ${err.message}`, { cause: err });
|
|
471
|
+
}
|
|
472
|
+
if (browser.deleted_at) throw new Error(`named session "${opts.name}" is no longer alive on Kernel (browser timed out or was deleted). Run \`cua session stop ${opts.name} && cua session start ${opts.name}\` to provision a fresh one.`);
|
|
473
|
+
return {
|
|
474
|
+
meta,
|
|
475
|
+
client,
|
|
476
|
+
browser
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/** Tear down a named session: delete the Kernel browser and remove the metadata file. */
|
|
480
|
+
async function stopNamedSession(opts) {
|
|
481
|
+
const meta = await readNamedSession(opts.name);
|
|
482
|
+
if (!meta) return {
|
|
483
|
+
existed: false,
|
|
484
|
+
kernelDeleted: false
|
|
485
|
+
};
|
|
486
|
+
const client = createKernelClient(opts.apiKey, opts.baseUrl);
|
|
487
|
+
let kernelDeleted = false;
|
|
488
|
+
try {
|
|
489
|
+
await client.browsers.deleteByID(meta.kernel_session_id);
|
|
490
|
+
kernelDeleted = true;
|
|
491
|
+
} catch (err) {
|
|
492
|
+
if (err.status !== 404) throw new Error(`failed to delete Kernel browser ${meta.kernel_session_id} for named session "${opts.name}": ${err.message}`, { cause: err });
|
|
493
|
+
}
|
|
494
|
+
await deleteNamedSession(opts.name);
|
|
495
|
+
return {
|
|
496
|
+
existed: true,
|
|
497
|
+
kernelDeleted
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
/** Update the persisted `transcript_path` on a named session. */
|
|
501
|
+
async function recordTranscriptPath(name, transcriptPath) {
|
|
502
|
+
const meta = await readNamedSession(name);
|
|
503
|
+
if (!meta) return;
|
|
504
|
+
if (meta.transcript_path === transcriptPath) return;
|
|
505
|
+
meta.transcript_path = transcriptPath;
|
|
506
|
+
await writeNamedSession(meta);
|
|
507
|
+
}
|
|
508
|
+
function shortKernelId(id) {
|
|
509
|
+
return id.length > 10 ? `${id.slice(0, 8)}…` : id;
|
|
510
|
+
}
|
|
511
|
+
function formatRelativeAge(createdAt) {
|
|
512
|
+
const diff = Date.now() - createdAt;
|
|
513
|
+
const sec = Math.max(0, Math.floor(diff / 1e3));
|
|
514
|
+
if (sec < 60) return `${sec}s`;
|
|
515
|
+
const min = Math.floor(sec / 60);
|
|
516
|
+
if (min < 60) return `${min}m`;
|
|
517
|
+
const hr = Math.floor(min / 60);
|
|
518
|
+
if (hr < 24) return `${hr}h`;
|
|
519
|
+
return `${Math.floor(hr / 24)}d`;
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/harness-sessions.ts
|
|
523
|
+
/**
|
|
524
|
+
* Resolve the default sessions directory: `$XDG_DATA_HOME/cua/sessions`
|
|
525
|
+
* (or `~/.local/share/cua/sessions`).
|
|
526
|
+
*/
|
|
527
|
+
function defaultSessionsRoot() {
|
|
528
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
529
|
+
if (xdg) return join(xdg, "cua", "sessions");
|
|
530
|
+
return join(homedir(), ".local", "share", "cua", "sessions");
|
|
531
|
+
}
|
|
532
|
+
/** Build a `JsonlSessionRepo` rooted at the resolved sessions directory. */
|
|
533
|
+
function createSessionRepo(sessionsRoot) {
|
|
534
|
+
const root = sessionsRoot ?? defaultSessionsRoot();
|
|
535
|
+
return new JsonlSessionRepo({
|
|
536
|
+
fs: new NodeExecutionEnv({ cwd: process.cwd() }),
|
|
537
|
+
sessionsRoot: root
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/** List sessions for a cwd; legacy / malformed files are skipped. */
|
|
541
|
+
async function listSessionsForCwd(repo, cwd) {
|
|
542
|
+
return await repo.list({ cwd });
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Find the most recent session metadata for cwd. The pi `JsonlSessionRepo`
|
|
546
|
+
* already orders by `createdAt` descending, but legacy `-c` semantics
|
|
547
|
+
* resumed by last *modified* time so a session that was reopened and
|
|
548
|
+
* appended to comes back first. We stat each file and prefer the newest
|
|
549
|
+
* mtime; results that fail to stat fall back to `createdAt`.
|
|
550
|
+
*/
|
|
551
|
+
async function findLatestSession(repo, cwd) {
|
|
552
|
+
const sessions = await listSessionsForCwd(repo, cwd);
|
|
553
|
+
if (sessions.length === 0) return void 0;
|
|
554
|
+
const ranked = await Promise.all(sessions.map(async (meta) => {
|
|
555
|
+
try {
|
|
556
|
+
return {
|
|
557
|
+
meta,
|
|
558
|
+
mtime: (await stat(meta.path)).mtimeMs
|
|
559
|
+
};
|
|
560
|
+
} catch {
|
|
561
|
+
return {
|
|
562
|
+
meta,
|
|
563
|
+
mtime: NaN
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
}));
|
|
567
|
+
ranked.sort((a, b) => {
|
|
568
|
+
const am = Number.isFinite(a.mtime) ? a.mtime : -Infinity;
|
|
569
|
+
const bm = Number.isFinite(b.mtime) ? b.mtime : -Infinity;
|
|
570
|
+
if (am !== bm) return bm - am;
|
|
571
|
+
return b.meta.createdAt.localeCompare(a.meta.createdAt);
|
|
572
|
+
});
|
|
573
|
+
return ranked[0]?.meta;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Resolve a `--session <ref>` argument. Accepts:
|
|
577
|
+
* - an absolute or relative path to an existing session file
|
|
578
|
+
* - `latest` for the most recent session for cwd
|
|
579
|
+
* - any other string as a prefix matched against session ids
|
|
580
|
+
*/
|
|
581
|
+
async function resolveSessionRef(repo, cwd, ref) {
|
|
582
|
+
const trimmed = ref.trim();
|
|
583
|
+
if (!trimmed) throw new Error("session reference is empty");
|
|
584
|
+
if (trimmed.includes("/") || trimmed.endsWith(".jsonl")) {
|
|
585
|
+
const absolute = isAbsolute(trimmed) ? trimmed : resolve(cwd, trimmed);
|
|
586
|
+
const direct = await readMetadataFromFile(absolute);
|
|
587
|
+
if (direct) return direct;
|
|
588
|
+
const match = (await repo.list()).find((m) => m.path === absolute);
|
|
589
|
+
if (match) return match;
|
|
590
|
+
throw new Error(`no session at "${trimmed}"`);
|
|
591
|
+
}
|
|
592
|
+
if (trimmed === "latest") {
|
|
593
|
+
const latest = await findLatestSession(repo, cwd);
|
|
594
|
+
if (!latest) throw new Error("no sessions found");
|
|
595
|
+
return latest;
|
|
596
|
+
}
|
|
597
|
+
const matches = (await listSessionsForCwd(repo, cwd)).filter((s) => s.id.startsWith(trimmed));
|
|
598
|
+
if (matches.length === 0) throw new Error(`no session matches "${trimmed}"`);
|
|
599
|
+
if (matches.length > 1) throw new Error(`ambiguous session prefix "${trimmed}" (${matches.length} matches)`);
|
|
600
|
+
return matches[0];
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Load the header line of a jsonl session file from disk and return its
|
|
604
|
+
* metadata, or undefined when the file is missing/empty/legacy. Used to
|
|
605
|
+
* resolve `--session <path>` and named transcript_path entries that may
|
|
606
|
+
* have been created from a different cwd (so the repo's per-cwd listing
|
|
607
|
+
* wouldn't see them).
|
|
608
|
+
*/
|
|
609
|
+
async function readMetadataFromFile(absolutePath) {
|
|
610
|
+
try {
|
|
611
|
+
const firstLine = (await readFile(absolutePath, "utf8")).split("\n", 1)[0]?.trim();
|
|
612
|
+
if (!firstLine) return void 0;
|
|
613
|
+
const header = JSON.parse(firstLine);
|
|
614
|
+
if (header.type !== "session") return void 0;
|
|
615
|
+
if (typeof header.id !== "string" || typeof header.timestamp !== "string" || typeof header.cwd !== "string") return;
|
|
616
|
+
return {
|
|
617
|
+
id: header.id,
|
|
618
|
+
createdAt: header.timestamp,
|
|
619
|
+
cwd: header.cwd,
|
|
620
|
+
path: absolutePath,
|
|
621
|
+
...typeof header.parentSession === "string" ? { parentSessionPath: header.parentSession } : {}
|
|
622
|
+
};
|
|
623
|
+
} catch {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/** Open (resume) a session by metadata. */
|
|
628
|
+
function openSession(repo, metadata) {
|
|
629
|
+
return repo.open(metadata);
|
|
630
|
+
}
|
|
631
|
+
/** Create a brand-new session for cwd. */
|
|
632
|
+
function createSession(repo, cwd) {
|
|
633
|
+
return repo.create({ cwd });
|
|
634
|
+
}
|
|
635
|
+
/** Custom entry type used to record the Kernel browser the session ran against. */
|
|
636
|
+
const CUA_BROWSER_ENTRY = "cua-browser";
|
|
637
|
+
/** Append a browser-metadata custom entry to the session. */
|
|
638
|
+
async function appendBrowserEntry(session, data) {
|
|
639
|
+
try {
|
|
640
|
+
await session.appendCustomEntry(CUA_BROWSER_ENTRY, data);
|
|
641
|
+
} catch {}
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/harness-skills.ts
|
|
645
|
+
/**
|
|
646
|
+
* Discover skills following the cross-agent `~/.agents/skills/` standard.
|
|
647
|
+
*
|
|
648
|
+
* Discovery order: explicit `--skill` paths, then `~/.agents/skills/`,
|
|
649
|
+
* then `<cwd>/.agents/skills/`. Missing paths are skipped silently.
|
|
650
|
+
*/
|
|
651
|
+
async function discoverCuaSkills(opts) {
|
|
652
|
+
if (opts.disabled) return {
|
|
653
|
+
skills: [],
|
|
654
|
+
sources: [],
|
|
655
|
+
diagnostics: []
|
|
656
|
+
};
|
|
657
|
+
const extras = (opts.extraPaths ?? []).filter((p) => p && p.trim().length > 0);
|
|
658
|
+
const userAgentsDir = join(homedir(), ".agents", "skills");
|
|
659
|
+
const projectAgentsDir = join(opts.cwd, ".agents", "skills");
|
|
660
|
+
const sources = [
|
|
661
|
+
...extras,
|
|
662
|
+
userAgentsDir,
|
|
663
|
+
projectAgentsDir
|
|
664
|
+
].filter((p) => existsSync(p));
|
|
665
|
+
if (sources.length === 0) return {
|
|
666
|
+
skills: [],
|
|
667
|
+
sources: [],
|
|
668
|
+
diagnostics: []
|
|
669
|
+
};
|
|
670
|
+
const result = await loadSkills(opts.env, sources);
|
|
671
|
+
return {
|
|
672
|
+
skills: result.skills,
|
|
673
|
+
sources,
|
|
674
|
+
diagnostics: result.diagnostics
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Resolve a `/skill:<name>` invocation. Returns the matched skill (so the
|
|
679
|
+
* caller can use `harness.skill(name)`) plus any remainder text the user
|
|
680
|
+
* typed after the skill name, which the caller can append as an additional
|
|
681
|
+
* instruction.
|
|
682
|
+
*/
|
|
683
|
+
function parseSkillInvocation(text, skills) {
|
|
684
|
+
const match = text.trim().match(/^\/skill:([A-Za-z0-9_\-.]+)\s*(.*)$/);
|
|
685
|
+
if (!match) return void 0;
|
|
686
|
+
const [, name, rest] = match;
|
|
687
|
+
return {
|
|
688
|
+
skill: skills.find((s) => s.name === name),
|
|
689
|
+
remainder: (rest ?? "").trim()
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Subscribe to a harness and emit one JSON object per line for downstream
|
|
694
|
+
* tooling. The event schema mirrors the legacy `output/jsonl.ts`: only the
|
|
695
|
+
* source of each field changes.
|
|
696
|
+
*/
|
|
697
|
+
function attachHarnessJsonlSink(opts) {
|
|
698
|
+
const write = opts.write ?? ((line) => process.stdout.write(line + "\n"));
|
|
699
|
+
const emit = (obj) => {
|
|
700
|
+
try {
|
|
701
|
+
write(JSON.stringify(obj));
|
|
702
|
+
} catch {
|
|
703
|
+
write(JSON.stringify({
|
|
704
|
+
type: "error",
|
|
705
|
+
code: "serialize_failed",
|
|
706
|
+
message: "could not serialize event",
|
|
707
|
+
ts: Date.now()
|
|
708
|
+
}));
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
emit({
|
|
712
|
+
type: "session_created",
|
|
713
|
+
schema_version: 1,
|
|
714
|
+
model: opts.modelRef,
|
|
715
|
+
provider: opts.provider,
|
|
716
|
+
ts: Date.now()
|
|
717
|
+
});
|
|
718
|
+
emit({
|
|
719
|
+
type: "browser_created",
|
|
720
|
+
browser_session_id: opts.browser.session_id,
|
|
721
|
+
live_url: opts.browser.browser_live_view_url,
|
|
722
|
+
...opts.profileId ? { profile_id: opts.profileId } : {},
|
|
723
|
+
ts: Date.now()
|
|
724
|
+
});
|
|
725
|
+
let turn = 0;
|
|
726
|
+
const includeDeltas = opts.includeDeltas === true;
|
|
727
|
+
const includeImages = opts.includeImages === true;
|
|
728
|
+
return opts.harness.subscribe((event) => {
|
|
729
|
+
switch (event.type) {
|
|
730
|
+
case "turn_start":
|
|
731
|
+
turn += 1;
|
|
732
|
+
return;
|
|
733
|
+
case "turn_end":
|
|
734
|
+
emit({
|
|
735
|
+
type: "turn_done",
|
|
736
|
+
turn,
|
|
737
|
+
ts: Date.now()
|
|
738
|
+
});
|
|
739
|
+
return;
|
|
740
|
+
case "agent_end":
|
|
741
|
+
emit({
|
|
742
|
+
type: "run_complete",
|
|
743
|
+
turns: turn,
|
|
744
|
+
ts: Date.now()
|
|
745
|
+
});
|
|
746
|
+
return;
|
|
747
|
+
case "message_end": {
|
|
748
|
+
const msg = event.message;
|
|
749
|
+
if (msg.role === "user") emit({
|
|
750
|
+
type: "user_message",
|
|
751
|
+
text: textOf(msg.content),
|
|
752
|
+
ts: Date.now()
|
|
753
|
+
});
|
|
754
|
+
else if (msg.role === "assistant") {
|
|
755
|
+
const text = textOf(msg.content);
|
|
756
|
+
if (text) emit({
|
|
757
|
+
type: "assistant_text_done",
|
|
758
|
+
text,
|
|
759
|
+
ts: Date.now()
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
case "message_update":
|
|
765
|
+
if (!includeDeltas) return;
|
|
766
|
+
if (event.assistantMessageEvent.type === "text_delta") emit({
|
|
767
|
+
type: "assistant_text_delta",
|
|
768
|
+
delta: event.assistantMessageEvent.delta,
|
|
769
|
+
ts: Date.now()
|
|
770
|
+
});
|
|
771
|
+
return;
|
|
772
|
+
case "tool_execution_start":
|
|
773
|
+
emit({
|
|
774
|
+
type: "tool_call",
|
|
775
|
+
tool_name: event.toolName,
|
|
776
|
+
call_id: event.toolCallId,
|
|
777
|
+
args: event.args,
|
|
778
|
+
ts: Date.now()
|
|
779
|
+
});
|
|
780
|
+
return;
|
|
781
|
+
case "tool_execution_end": {
|
|
782
|
+
const result = event.result;
|
|
783
|
+
const ok = !event.isError;
|
|
784
|
+
let contentText;
|
|
785
|
+
let screenshotBytes;
|
|
786
|
+
const screenshotsB64 = [];
|
|
787
|
+
if (result?.content) {
|
|
788
|
+
const textParts = [];
|
|
789
|
+
for (const c of result.content) {
|
|
790
|
+
if (c?.type === "text" && typeof c.text === "string") textParts.push(c.text);
|
|
791
|
+
if (c?.type === "image" && typeof c.data === "string") {
|
|
792
|
+
const len = c.data.length;
|
|
793
|
+
screenshotBytes = (screenshotBytes ?? 0) + len;
|
|
794
|
+
if (includeImages) screenshotsB64.push(c.data);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
contentText = textParts.join("\n").trim() || void 0;
|
|
798
|
+
}
|
|
799
|
+
emit({
|
|
800
|
+
type: "tool_result",
|
|
801
|
+
tool_name: event.toolName,
|
|
802
|
+
call_id: event.toolCallId,
|
|
803
|
+
ok,
|
|
804
|
+
content_text: contentText,
|
|
805
|
+
screenshot_bytes: screenshotBytes,
|
|
806
|
+
...includeImages && screenshotsB64.length ? { screenshots_b64: screenshotsB64 } : {},
|
|
807
|
+
details: result?.details,
|
|
808
|
+
ts: Date.now()
|
|
809
|
+
});
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
default: return;
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function textOf(content) {
|
|
817
|
+
if (typeof content === "string") return content;
|
|
818
|
+
if (!Array.isArray(content)) return "";
|
|
819
|
+
const parts = [];
|
|
820
|
+
for (const c of content) if (c && typeof c === "object" && c.type === "text" && typeof c.text === "string") parts.push(c.text);
|
|
821
|
+
return parts.join("\n");
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/print.ts
|
|
825
|
+
/**
|
|
826
|
+
* Run a single prompt through the harness and stream output to stdout
|
|
827
|
+
* (text mode) or as jsonl events. Returns the process exit code (0 ok,
|
|
828
|
+
* 1 on failure).
|
|
829
|
+
*/
|
|
830
|
+
async function runPrint(opts) {
|
|
831
|
+
const jsonlMode = opts.jsonlMode === true;
|
|
832
|
+
let unsubscribeJsonl;
|
|
833
|
+
if (jsonlMode) unsubscribeJsonl = attachHarnessJsonlSink({
|
|
834
|
+
harness: opts.harness,
|
|
835
|
+
browser: opts.browserHandle.browser,
|
|
836
|
+
profileId: opts.browserHandle.profileId,
|
|
837
|
+
modelRef: opts.modelRef,
|
|
838
|
+
provider: opts.provider,
|
|
839
|
+
includeDeltas: opts.jsonlIncludeDeltas,
|
|
840
|
+
includeImages: opts.jsonlIncludeImages
|
|
841
|
+
});
|
|
842
|
+
const unsubscribeText = opts.harness.subscribe((event) => {
|
|
843
|
+
if (jsonlMode) return;
|
|
844
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
845
|
+
stdout.write(event.assistantMessageEvent.delta);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (opts.verbose && event.type === "tool_execution_start") stderr.write(`\n[cua] tool ${event.toolName} ${JSON.stringify(event.args)}\n`);
|
|
849
|
+
if (opts.verbose && event.type === "tool_execution_end") stderr.write(`[cua] tool ${event.toolName} done\n`);
|
|
850
|
+
});
|
|
851
|
+
let exitCode = 0;
|
|
852
|
+
try {
|
|
853
|
+
const invocation = parseSkillInvocation(opts.prompt, opts.skills ?? []);
|
|
854
|
+
let assistant;
|
|
855
|
+
if (invocation?.skill) {
|
|
856
|
+
if (opts.verbose) stderr.write(`[cua] expanded /skill:${invocation.skill.name}\n`);
|
|
857
|
+
assistant = await opts.harness.skill(invocation.skill.name, invocation.remainder || void 0);
|
|
858
|
+
} else {
|
|
859
|
+
const images = await maybeInitialScreenshot(opts);
|
|
860
|
+
assistant = await opts.harness.prompt(opts.prompt, images ? { images } : void 0);
|
|
861
|
+
}
|
|
862
|
+
if (assistant.stopReason === "error" || assistant.stopReason === "aborted") throw new Error(assistant.errorMessage ?? `agent stopped with ${assistant.stopReason}`);
|
|
863
|
+
if (!jsonlMode) stdout.write("\n");
|
|
864
|
+
} catch (err) {
|
|
865
|
+
if (jsonlMode) stdout.write(JSON.stringify({
|
|
866
|
+
type: "error",
|
|
867
|
+
code: "run_failed",
|
|
868
|
+
message: err.message,
|
|
869
|
+
ts: Date.now()
|
|
870
|
+
}) + "\n");
|
|
871
|
+
else stderr.write(`\n[cua] error: ${err.message}\n`);
|
|
872
|
+
exitCode = 1;
|
|
873
|
+
} finally {
|
|
874
|
+
unsubscribeText();
|
|
875
|
+
unsubscribeJsonl?.();
|
|
876
|
+
}
|
|
877
|
+
return exitCode;
|
|
878
|
+
}
|
|
879
|
+
async function maybeInitialScreenshot(opts) {
|
|
880
|
+
if (opts.skipInitialScreenshot) return void 0;
|
|
881
|
+
if (await sessionHasPriorTurn(opts.session)) return void 0;
|
|
882
|
+
const png = await captureScreenshot(opts.browserHandle.client, opts.browserHandle.browser.session_id);
|
|
883
|
+
if (!png) return void 0;
|
|
884
|
+
return [{
|
|
885
|
+
type: "image",
|
|
886
|
+
data: png.toString("base64"),
|
|
887
|
+
mimeType: "image/png"
|
|
888
|
+
}];
|
|
889
|
+
}
|
|
890
|
+
async function sessionHasPriorTurn(session) {
|
|
891
|
+
const entries = await session.getBranch();
|
|
892
|
+
for (const entry of entries) if (entry.type === "message" && (entry.message.role === "user" || entry.message.role === "assistant")) return true;
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region src/cli-harness.ts
|
|
897
|
+
const MODELS_HELP = `cua models — list supported -m/--model values
|
|
898
|
+
|
|
899
|
+
Usage:
|
|
900
|
+
cua models
|
|
901
|
+
cua models -p openai
|
|
902
|
+
cua models --provider anthropic
|
|
903
|
+
cua models --json
|
|
904
|
+
|
|
905
|
+
Options:
|
|
906
|
+
-p, --provider <id> Filter by provider: openai | anthropic | google | gemini | tzafon | yutori
|
|
907
|
+
--json Output JSON
|
|
908
|
+
-h, --help Show this help
|
|
909
|
+
`;
|
|
910
|
+
function parseModelsArgs(argv) {
|
|
911
|
+
const parsed = parseArgs({
|
|
912
|
+
args: argv,
|
|
913
|
+
options: {
|
|
914
|
+
provider: {
|
|
915
|
+
type: "string",
|
|
916
|
+
short: "p"
|
|
917
|
+
},
|
|
918
|
+
json: {
|
|
919
|
+
type: "boolean",
|
|
920
|
+
default: false
|
|
921
|
+
},
|
|
922
|
+
help: {
|
|
923
|
+
type: "boolean",
|
|
924
|
+
short: "h",
|
|
925
|
+
default: false
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
allowPositionals: true,
|
|
929
|
+
strict: true
|
|
930
|
+
});
|
|
931
|
+
const positionalProvider = parsed.positionals[0];
|
|
932
|
+
if (parsed.positionals.length > 1) throw new Error(`unexpected arguments: ${parsed.positionals.slice(1).join(" ")}`);
|
|
933
|
+
return {
|
|
934
|
+
provider: parsed.values.provider ?? positionalProvider,
|
|
935
|
+
json: !!parsed.values.json,
|
|
936
|
+
help: !!parsed.values.help
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
/** `cua models` subcommand backed by cua-ai's `listCuaModels()`. */
|
|
940
|
+
async function runModelsSubcommand(argv) {
|
|
941
|
+
let flags;
|
|
942
|
+
try {
|
|
943
|
+
flags = parseModelsArgs(argv);
|
|
944
|
+
} catch (err) {
|
|
945
|
+
stderr.write(`${err.message}\n\n${MODELS_HELP}`);
|
|
946
|
+
return 2;
|
|
947
|
+
}
|
|
948
|
+
if (flags.help) {
|
|
949
|
+
stdout.write(MODELS_HELP);
|
|
950
|
+
return 0;
|
|
951
|
+
}
|
|
952
|
+
let models;
|
|
953
|
+
try {
|
|
954
|
+
models = listSupportedModels(flags.provider);
|
|
955
|
+
} catch (err) {
|
|
956
|
+
stderr.write(`${err.message}\n`);
|
|
957
|
+
return 2;
|
|
958
|
+
}
|
|
959
|
+
if (flags.json) {
|
|
960
|
+
stdout.write(`${JSON.stringify(models, null, 2)}\n`);
|
|
961
|
+
return 0;
|
|
962
|
+
}
|
|
963
|
+
stdout.write(formatModelsTable(models));
|
|
964
|
+
return 0;
|
|
965
|
+
}
|
|
966
|
+
function formatModelsTable(models) {
|
|
967
|
+
const rows = models.map((entry) => ({
|
|
968
|
+
ref: entry.ref,
|
|
969
|
+
provider: entry.provider,
|
|
970
|
+
model: entry.model,
|
|
971
|
+
default: entry.ref === "openai:gpt-5.5" ? "yes" : "",
|
|
972
|
+
name: entry.name
|
|
973
|
+
}));
|
|
974
|
+
const headers = {
|
|
975
|
+
ref: "REF",
|
|
976
|
+
provider: "PROVIDER",
|
|
977
|
+
model: "MODEL",
|
|
978
|
+
default: "DEFAULT",
|
|
979
|
+
name: "NAME"
|
|
980
|
+
};
|
|
981
|
+
const widths = {
|
|
982
|
+
ref: columnWidth(headers.ref, rows.map((r) => r.ref)),
|
|
983
|
+
provider: columnWidth(headers.provider, rows.map((r) => r.provider)),
|
|
984
|
+
model: columnWidth(headers.model, rows.map((r) => r.model)),
|
|
985
|
+
default: columnWidth(headers.default, rows.map((r) => r.default)),
|
|
986
|
+
name: columnWidth(headers.name, rows.map((r) => r.name))
|
|
987
|
+
};
|
|
988
|
+
const lines = [[
|
|
989
|
+
headers.ref.padEnd(widths.ref),
|
|
990
|
+
headers.provider.padEnd(widths.provider),
|
|
991
|
+
headers.model.padEnd(widths.model),
|
|
992
|
+
headers.default.padEnd(widths.default),
|
|
993
|
+
headers.name
|
|
994
|
+
].join(" "), [
|
|
995
|
+
"-".repeat(widths.ref),
|
|
996
|
+
"-".repeat(widths.provider),
|
|
997
|
+
"-".repeat(widths.model),
|
|
998
|
+
"-".repeat(widths.default),
|
|
999
|
+
"-".repeat(widths.name)
|
|
1000
|
+
].join(" ")];
|
|
1001
|
+
for (const row of rows) lines.push([
|
|
1002
|
+
row.ref.padEnd(widths.ref),
|
|
1003
|
+
row.provider.padEnd(widths.provider),
|
|
1004
|
+
row.model.padEnd(widths.model),
|
|
1005
|
+
row.default.padEnd(widths.default),
|
|
1006
|
+
row.name
|
|
1007
|
+
].join(" "));
|
|
1008
|
+
return `${lines.join("\n")}\n`;
|
|
1009
|
+
}
|
|
1010
|
+
function columnWidth(header, values) {
|
|
1011
|
+
return Math.max(header.length, ...values.map((value) => value.length));
|
|
1012
|
+
}
|
|
1013
|
+
function requireKernelApiKey() {
|
|
1014
|
+
const apiKey = process.env.KERNEL_API_KEY?.trim();
|
|
1015
|
+
if (!apiKey) throw new Error("missing Kernel API key (set KERNEL_API_KEY)");
|
|
1016
|
+
return {
|
|
1017
|
+
apiKey,
|
|
1018
|
+
baseUrl: process.env.KERNEL_BASE_URL?.trim() || void 0
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
function resolveAuth(flags) {
|
|
1022
|
+
const { apiKey, baseUrl } = requireKernelApiKey();
|
|
1023
|
+
const modelRef = resolveCuaModelRef(flags.model);
|
|
1024
|
+
const { provider } = parseCuaModelRef(modelRef);
|
|
1025
|
+
requireCuaEnvApiKey(provider);
|
|
1026
|
+
return {
|
|
1027
|
+
kernelApiKey: apiKey,
|
|
1028
|
+
kernelBaseUrl: baseUrl,
|
|
1029
|
+
modelRef
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
async function provisionForFlags(flags, auth) {
|
|
1033
|
+
if (flags.namedSession) {
|
|
1034
|
+
const { client, browser, meta } = await attachNamedSession({
|
|
1035
|
+
name: flags.namedSession,
|
|
1036
|
+
apiKey: auth.kernelApiKey,
|
|
1037
|
+
baseUrl: auth.kernelBaseUrl
|
|
1038
|
+
});
|
|
1039
|
+
if (flags.verbose) {
|
|
1040
|
+
stderr.write(`[cua] attached named session "${meta.name}" (browser=${browser.session_id})\n`);
|
|
1041
|
+
if (browser.browser_live_view_url) stderr.write(`[cua] live view=${browser.browser_live_view_url}\n`);
|
|
1042
|
+
}
|
|
1043
|
+
return {
|
|
1044
|
+
handle: {
|
|
1045
|
+
client,
|
|
1046
|
+
browser,
|
|
1047
|
+
profileId: meta.profile_id,
|
|
1048
|
+
async close() {}
|
|
1049
|
+
},
|
|
1050
|
+
named: meta
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
if (flags.verbose) stderr.write("[cua] provisioning Kernel browser...\n");
|
|
1054
|
+
const handle = await provisionBrowser({
|
|
1055
|
+
apiKey: auth.kernelApiKey,
|
|
1056
|
+
baseUrl: auth.kernelBaseUrl,
|
|
1057
|
+
timeoutSeconds: flags.browserTimeout,
|
|
1058
|
+
profileSelector: flags.browserProfile,
|
|
1059
|
+
saveChanges: flags.profileSaveChanges
|
|
1060
|
+
});
|
|
1061
|
+
if (flags.verbose) {
|
|
1062
|
+
stderr.write(`[cua] browser session=${handle.browser.session_id}\n`);
|
|
1063
|
+
if (handle.browser.browser_live_view_url) stderr.write(`[cua] live view=${handle.browser.browser_live_view_url}\n`);
|
|
1064
|
+
}
|
|
1065
|
+
return { handle };
|
|
1066
|
+
}
|
|
1067
|
+
async function resolveSession(repo, cwd, flags, namedMeta) {
|
|
1068
|
+
if (flags.noSession) return void 0;
|
|
1069
|
+
if (flags.sessionRef) {
|
|
1070
|
+
const metadata = await resolveSessionRef(repo, cwd, flags.sessionRef);
|
|
1071
|
+
return {
|
|
1072
|
+
session: await openSession(repo, metadata),
|
|
1073
|
+
transcriptPath: metadata.path,
|
|
1074
|
+
resumed: true
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
if (flags.continueLatest) {
|
|
1078
|
+
const latest = await findLatestSession(repo, cwd);
|
|
1079
|
+
if (!latest) {
|
|
1080
|
+
stderr.write("[cua] no previous session for this cwd; starting fresh\n");
|
|
1081
|
+
const fresh = await createSession(repo, cwd);
|
|
1082
|
+
return {
|
|
1083
|
+
session: fresh,
|
|
1084
|
+
transcriptPath: (await fresh.getMetadata()).path,
|
|
1085
|
+
resumed: false
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
session: await openSession(repo, latest),
|
|
1090
|
+
transcriptPath: latest.path,
|
|
1091
|
+
resumed: true
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
if (flags.resumePicker) {
|
|
1095
|
+
const sessions = await listSessionsForCwd(repo, cwd);
|
|
1096
|
+
if (sessions.length === 0) {
|
|
1097
|
+
stderr.write("[cua] no previous sessions for this cwd; starting fresh\n");
|
|
1098
|
+
const fresh = await createSession(repo, cwd);
|
|
1099
|
+
return {
|
|
1100
|
+
session: fresh,
|
|
1101
|
+
transcriptPath: (await fresh.getMetadata()).path,
|
|
1102
|
+
resumed: false
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
const picked = await pickSession(sessions);
|
|
1106
|
+
if (!picked) {
|
|
1107
|
+
const fresh = await createSession(repo, cwd);
|
|
1108
|
+
return {
|
|
1109
|
+
session: fresh,
|
|
1110
|
+
transcriptPath: (await fresh.getMetadata()).path,
|
|
1111
|
+
resumed: false
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
return {
|
|
1115
|
+
session: await openSession(repo, picked),
|
|
1116
|
+
transcriptPath: picked.path,
|
|
1117
|
+
resumed: true
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
if (namedMeta?.transcript_path) {
|
|
1121
|
+
const direct = await readMetadataFromFile(namedMeta.transcript_path);
|
|
1122
|
+
if (direct) return {
|
|
1123
|
+
session: await openSession(repo, direct),
|
|
1124
|
+
transcriptPath: direct.path,
|
|
1125
|
+
resumed: true
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
const fresh = await createSession(repo, cwd);
|
|
1129
|
+
return {
|
|
1130
|
+
session: fresh,
|
|
1131
|
+
transcriptPath: (await fresh.getMetadata()).path,
|
|
1132
|
+
resumed: false
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
async function pickSession(sessions) {
|
|
1136
|
+
const sorted = [...sessions].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
1137
|
+
stderr.write("\nResume which session?\n");
|
|
1138
|
+
const limit = Math.min(sorted.length, 20);
|
|
1139
|
+
for (let i = 0; i < limit; i++) {
|
|
1140
|
+
const s = sorted[i];
|
|
1141
|
+
stderr.write(` [${i + 1}] ${s.id.slice(0, 8)} · ${s.createdAt}\n`);
|
|
1142
|
+
}
|
|
1143
|
+
if (sorted.length > limit) stderr.write(` (${sorted.length - limit} more not shown; use --session <prefix> to select directly)\n`);
|
|
1144
|
+
const { createInterface } = await import("node:readline/promises");
|
|
1145
|
+
const rl = createInterface({
|
|
1146
|
+
input: process.stdin,
|
|
1147
|
+
output: process.stderr
|
|
1148
|
+
});
|
|
1149
|
+
try {
|
|
1150
|
+
const answer = (await rl.question("Pick a number (or blank to skip): ")).trim();
|
|
1151
|
+
if (!answer) return void 0;
|
|
1152
|
+
const n = Number(answer);
|
|
1153
|
+
if (!Number.isFinite(n) || n < 1 || n > limit) {
|
|
1154
|
+
stderr.write("[cua] invalid selection; starting fresh\n");
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
return sorted[n - 1];
|
|
1158
|
+
} finally {
|
|
1159
|
+
rl.close();
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
async function setupHarnessRuntime(flags, opts = {}) {
|
|
1163
|
+
const auth = resolveAuth(flags);
|
|
1164
|
+
const cwd = process.cwd();
|
|
1165
|
+
const { skills } = await discoverCuaSkills({
|
|
1166
|
+
cwd,
|
|
1167
|
+
env: new NodeExecutionEnv({ cwd }),
|
|
1168
|
+
extraPaths: flags.skillPaths,
|
|
1169
|
+
disabled: flags.noSkills
|
|
1170
|
+
});
|
|
1171
|
+
const provisioned = await provisionForFlags(flags, auth);
|
|
1172
|
+
const repo = createSessionRepo(flags.sessionDir);
|
|
1173
|
+
const resolved = opts.skipDiskSession === true && !hasExplicitSessionFlag(flags) ? void 0 : await resolveSession(repo, cwd, flags, provisioned.named);
|
|
1174
|
+
let inMemorySession;
|
|
1175
|
+
if (!resolved) inMemorySession = await new InMemorySessionRepo().create();
|
|
1176
|
+
const session = resolved?.session ?? inMemorySession;
|
|
1177
|
+
const { provider } = parseCuaModelRef(auth.modelRef);
|
|
1178
|
+
if (resolved) {
|
|
1179
|
+
await appendBrowserEntry(session, {
|
|
1180
|
+
sessionId: provisioned.handle.browser.session_id,
|
|
1181
|
+
liveUrl: provisioned.handle.browser.browser_live_view_url,
|
|
1182
|
+
profileId: provisioned.handle.profileId,
|
|
1183
|
+
createdAt: Date.now()
|
|
1184
|
+
});
|
|
1185
|
+
if (provisioned.named) await recordTranscriptPath(provisioned.named.name, resolved.transcriptPath);
|
|
1186
|
+
if (flags.verbose) {
|
|
1187
|
+
stderr.write(`[cua] session=${resolved.transcriptPath}\n`);
|
|
1188
|
+
if (resolved.resumed) stderr.write("[cua] resumed prior session into fresh browser\n");
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const thinkingLevel = mapThinkingLevel(flags.thinking);
|
|
1192
|
+
const baseUrlOverride = providerBaseUrlOverride(provider);
|
|
1193
|
+
const harness = buildCuaHarness({
|
|
1194
|
+
cwd,
|
|
1195
|
+
client: provisioned.handle.client,
|
|
1196
|
+
browser: provisioned.handle.browser,
|
|
1197
|
+
session,
|
|
1198
|
+
model: auth.modelRef,
|
|
1199
|
+
skills,
|
|
1200
|
+
thinkingLevel,
|
|
1201
|
+
modelBaseUrl: baseUrlOverride
|
|
1202
|
+
});
|
|
1203
|
+
return {
|
|
1204
|
+
handle: provisioned.handle,
|
|
1205
|
+
resolved,
|
|
1206
|
+
session,
|
|
1207
|
+
skills,
|
|
1208
|
+
harness,
|
|
1209
|
+
provider,
|
|
1210
|
+
modelRef: auth.modelRef
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
function hasExplicitSessionFlag(flags) {
|
|
1214
|
+
return !!flags.sessionRef || flags.continueLatest || flags.resumePicker || !!flags.namedSession;
|
|
1215
|
+
}
|
|
1216
|
+
function providerBaseUrlOverride(provider) {
|
|
1217
|
+
const envName = `${provider.toUpperCase()}_BASE_URL`;
|
|
1218
|
+
const value = process.env[envName]?.trim();
|
|
1219
|
+
return value && value.length > 0 ? value : void 0;
|
|
1220
|
+
}
|
|
1221
|
+
function mapThinkingLevel(raw) {
|
|
1222
|
+
switch ((raw ?? "low").trim().toLowerCase()) {
|
|
1223
|
+
case "off":
|
|
1224
|
+
case "none": return "off";
|
|
1225
|
+
case "minimal": return "minimal";
|
|
1226
|
+
case "medium": return "medium";
|
|
1227
|
+
case "high": return "high";
|
|
1228
|
+
case "xhigh": return "xhigh";
|
|
1229
|
+
case "low":
|
|
1230
|
+
case "": return "low";
|
|
1231
|
+
default: throw new Error(`invalid --thinking value "${raw}"; expected one of: off | minimal | low | medium | high | xhigh`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
/** Run a single prompt through the new harness wiring (`--print`). */
|
|
1235
|
+
async function runPrintCommand(prompt, flags) {
|
|
1236
|
+
const runtime = await setupHarnessRuntime(flags);
|
|
1237
|
+
const jsonlMode = (flags.output ?? "text").toLowerCase() === "jsonl";
|
|
1238
|
+
try {
|
|
1239
|
+
return await runPrint({
|
|
1240
|
+
harness: runtime.harness,
|
|
1241
|
+
browserHandle: runtime.handle,
|
|
1242
|
+
session: runtime.session,
|
|
1243
|
+
modelRef: runtime.modelRef,
|
|
1244
|
+
provider: runtime.provider,
|
|
1245
|
+
prompt,
|
|
1246
|
+
skills: runtime.skills,
|
|
1247
|
+
skipInitialScreenshot: runtime.resolved?.resumed === true,
|
|
1248
|
+
verbose: flags.verbose,
|
|
1249
|
+
jsonlMode,
|
|
1250
|
+
jsonlIncludeDeltas: flags.jsonlIncludeDeltas,
|
|
1251
|
+
jsonlIncludeImages: flags.jsonlIncludeImages
|
|
1252
|
+
});
|
|
1253
|
+
} finally {
|
|
1254
|
+
try {
|
|
1255
|
+
await runtime.handle.close();
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
stderr.write(`[cua] cleanup warning: ${err.message}\n`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
/** Run the interactive TUI through the new harness wiring. */
|
|
1262
|
+
async function runInteractiveCommand(initialPrompt, flags) {
|
|
1263
|
+
const runtime = await setupHarnessRuntime(flags);
|
|
1264
|
+
const { runInteractive } = await import("./main-Bphx_zOj.js");
|
|
1265
|
+
try {
|
|
1266
|
+
return await runInteractive({
|
|
1267
|
+
cwd: process.cwd(),
|
|
1268
|
+
harness: runtime.harness,
|
|
1269
|
+
browserHandle: runtime.handle,
|
|
1270
|
+
session: runtime.session,
|
|
1271
|
+
skills: runtime.skills,
|
|
1272
|
+
modelRef: runtime.modelRef,
|
|
1273
|
+
provider: runtime.provider,
|
|
1274
|
+
initialPrompt: initialPrompt || void 0,
|
|
1275
|
+
imageProtocol: flags.imageProtocol,
|
|
1276
|
+
debugTui: flags.debugTui,
|
|
1277
|
+
resumed: runtime.resolved?.resumed === true,
|
|
1278
|
+
transcriptPath: runtime.resolved?.transcriptPath,
|
|
1279
|
+
skipInitialScreenshot: runtime.resolved?.resumed === true
|
|
1280
|
+
});
|
|
1281
|
+
} finally {
|
|
1282
|
+
try {
|
|
1283
|
+
await runtime.handle.close();
|
|
1284
|
+
} catch (err) {
|
|
1285
|
+
stderr.write(`[cua] cleanup warning: ${err.message}\n`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/** Run a one-shot action subcommand through the new harness wiring. */
|
|
1290
|
+
async function runActionCommand(action, rest, flags) {
|
|
1291
|
+
const runtime = await setupHarnessRuntime(flags, { skipDiskSession: true });
|
|
1292
|
+
const req = buildActionRequest(action, rest);
|
|
1293
|
+
if (flags.maxSteps !== void 0) req.maxTurns = flags.maxSteps;
|
|
1294
|
+
const screenshotOut = flags.out ? { out: flags.out } : action === "screenshot" ? { out: "screenshot.png" } : void 0;
|
|
1295
|
+
try {
|
|
1296
|
+
return emitCompact(await runAction(req, {
|
|
1297
|
+
harness: runtime.harness,
|
|
1298
|
+
browserHandle: runtime.handle,
|
|
1299
|
+
session: runtime.session,
|
|
1300
|
+
skipInitialScreenshot: runtime.resolved?.resumed === true
|
|
1301
|
+
}, screenshotOut));
|
|
1302
|
+
} finally {
|
|
1303
|
+
try {
|
|
1304
|
+
await runtime.handle.close();
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
stderr.write(`[cua] cleanup warning: ${err.message}\n`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function buildActionRequest(action, rest) {
|
|
1311
|
+
switch (action) {
|
|
1312
|
+
case "open": return {
|
|
1313
|
+
action,
|
|
1314
|
+
text: rest[0]
|
|
1315
|
+
};
|
|
1316
|
+
case "click": return {
|
|
1317
|
+
action,
|
|
1318
|
+
target: rest.join(" ")
|
|
1319
|
+
};
|
|
1320
|
+
case "type": return {
|
|
1321
|
+
action,
|
|
1322
|
+
target: rest[0],
|
|
1323
|
+
text: rest[1]
|
|
1324
|
+
};
|
|
1325
|
+
case "press": return {
|
|
1326
|
+
action,
|
|
1327
|
+
keys: rest
|
|
1328
|
+
};
|
|
1329
|
+
case "observe": return {
|
|
1330
|
+
action,
|
|
1331
|
+
text: rest.join(" ")
|
|
1332
|
+
};
|
|
1333
|
+
case "url": return { action };
|
|
1334
|
+
case "screenshot": return { action };
|
|
1335
|
+
case "do": return {
|
|
1336
|
+
action,
|
|
1337
|
+
text: rest.join(" ")
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
/** Named-session subcommand handlers wired to the new SDK-backed implementation. */
|
|
1342
|
+
async function runSessionSubcommand(args, flags) {
|
|
1343
|
+
const sub = args[0];
|
|
1344
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
1345
|
+
stdout.write(`${sessionHelp()}\n`);
|
|
1346
|
+
return 0;
|
|
1347
|
+
}
|
|
1348
|
+
const auth = resolveAuthOrFail();
|
|
1349
|
+
switch (sub) {
|
|
1350
|
+
case "start": {
|
|
1351
|
+
const name = (args[1] ?? "").trim() || generateSessionSlug();
|
|
1352
|
+
validateSlug(name);
|
|
1353
|
+
const { meta, metadataPath, browser } = await startNamedSession({
|
|
1354
|
+
name,
|
|
1355
|
+
apiKey: auth.kernelApiKey,
|
|
1356
|
+
baseUrl: auth.kernelBaseUrl,
|
|
1357
|
+
browserTimeoutSeconds: flags.browserTimeout,
|
|
1358
|
+
profileSelector: flags.browserProfile,
|
|
1359
|
+
saveProfileChanges: flags.profileSaveChanges
|
|
1360
|
+
});
|
|
1361
|
+
stdout.write(`name=${meta.name}\n`);
|
|
1362
|
+
stdout.write(`kernel_session_id=${browser.session_id}\n`);
|
|
1363
|
+
if (browser.browser_live_view_url) stdout.write(`live_url=${browser.browser_live_view_url}\n`);
|
|
1364
|
+
stdout.write(`metadata=${metadataPath}\n`);
|
|
1365
|
+
stdout.write(`\nUse: cua -s ${meta.name} <subcommand>...\n`);
|
|
1366
|
+
return 0;
|
|
1367
|
+
}
|
|
1368
|
+
case "stop": {
|
|
1369
|
+
const name = (args[1] ?? "").trim();
|
|
1370
|
+
if (!name) {
|
|
1371
|
+
stderr.write("usage: cua session stop <name>\n");
|
|
1372
|
+
return 2;
|
|
1373
|
+
}
|
|
1374
|
+
validateSlug(name);
|
|
1375
|
+
const result = await stopNamedSession({
|
|
1376
|
+
name,
|
|
1377
|
+
apiKey: auth.kernelApiKey,
|
|
1378
|
+
baseUrl: auth.kernelBaseUrl
|
|
1379
|
+
});
|
|
1380
|
+
if (!result.existed) {
|
|
1381
|
+
stderr.write(`no named session "${name}"\n`);
|
|
1382
|
+
return 1;
|
|
1383
|
+
}
|
|
1384
|
+
stdout.write(result.kernelDeleted ? `stopped ${name} (kernel browser deleted)\n` : `stopped ${name} (kernel browser was already gone)\n`);
|
|
1385
|
+
return 0;
|
|
1386
|
+
}
|
|
1387
|
+
case "list": {
|
|
1388
|
+
const sessions = await listNamedSessions();
|
|
1389
|
+
if (sessions.length === 0) {
|
|
1390
|
+
stdout.write("(no named sessions; run `cua session start [name]`)\n");
|
|
1391
|
+
return 0;
|
|
1392
|
+
}
|
|
1393
|
+
const header = [
|
|
1394
|
+
"NAME",
|
|
1395
|
+
"KERNEL_ID",
|
|
1396
|
+
"AGE",
|
|
1397
|
+
"LIVE_URL"
|
|
1398
|
+
].join(" ");
|
|
1399
|
+
stdout.write(`${header}\n`);
|
|
1400
|
+
for (const s of sessions) stdout.write([
|
|
1401
|
+
s.name,
|
|
1402
|
+
shortKernelId(s.kernel_session_id),
|
|
1403
|
+
formatRelativeAge(s.created_at),
|
|
1404
|
+
s.live_url ?? "-"
|
|
1405
|
+
].join(" ") + "\n");
|
|
1406
|
+
return 0;
|
|
1407
|
+
}
|
|
1408
|
+
case "show": {
|
|
1409
|
+
const name = (args[1] ?? "").trim();
|
|
1410
|
+
if (!name) {
|
|
1411
|
+
stderr.write("usage: cua session show <name>\n");
|
|
1412
|
+
return 2;
|
|
1413
|
+
}
|
|
1414
|
+
validateSlug(name);
|
|
1415
|
+
const meta = (await listNamedSessions()).find((s) => s.name === name);
|
|
1416
|
+
if (!meta) {
|
|
1417
|
+
stderr.write(`no named session "${name}"\n`);
|
|
1418
|
+
return 1;
|
|
1419
|
+
}
|
|
1420
|
+
stdout.write(`${JSON.stringify(meta, null, 2)}\n`);
|
|
1421
|
+
return 0;
|
|
1422
|
+
}
|
|
1423
|
+
default:
|
|
1424
|
+
stderr.write(`unknown session subcommand: ${sub}\n${sessionHelp()}\n`);
|
|
1425
|
+
return 2;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function resolveAuthOrFail() {
|
|
1429
|
+
const { apiKey, baseUrl } = requireKernelApiKey();
|
|
1430
|
+
return {
|
|
1431
|
+
kernelApiKey: apiKey,
|
|
1432
|
+
kernelBaseUrl: baseUrl
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
function generateSessionSlug() {
|
|
1436
|
+
const adjectives = [
|
|
1437
|
+
"calm",
|
|
1438
|
+
"brisk",
|
|
1439
|
+
"swift",
|
|
1440
|
+
"quiet",
|
|
1441
|
+
"bright",
|
|
1442
|
+
"sharp"
|
|
1443
|
+
];
|
|
1444
|
+
const nouns = [
|
|
1445
|
+
"fox",
|
|
1446
|
+
"owl",
|
|
1447
|
+
"lynx",
|
|
1448
|
+
"hawk",
|
|
1449
|
+
"wolf",
|
|
1450
|
+
"moth"
|
|
1451
|
+
];
|
|
1452
|
+
return `${adjectives[Math.floor(Math.random() * adjectives.length)] ?? "calm"}-${nouns[Math.floor(Math.random() * nouns.length)] ?? "fox"}-${Date.now().toString(36).slice(-4)}`;
|
|
1453
|
+
}
|
|
1454
|
+
function sessionHelp() {
|
|
1455
|
+
return [
|
|
1456
|
+
"cua session start [name] Start a new named browser session.",
|
|
1457
|
+
"cua session stop <name> Tear down a named session.",
|
|
1458
|
+
"cua session list List existing named sessions.",
|
|
1459
|
+
"cua session show <name> Print full metadata for a named session.",
|
|
1460
|
+
"",
|
|
1461
|
+
"Use `-s <name>` on any other command to reuse the named session's",
|
|
1462
|
+
"browser (e.g. `cua -s login open https://...`)."
|
|
1463
|
+
].join("\n");
|
|
1464
|
+
}
|
|
1465
|
+
//#endregion
|
|
1466
|
+
//#region src/cli.ts
|
|
1467
|
+
const HELP = `cua — Kernel-cloud-browser computer-use agent
|
|
1468
|
+
|
|
1469
|
+
Usage:
|
|
1470
|
+
cua [options] [prompt...]
|
|
1471
|
+
cua --print "go to news.ycombinator.com and summarize"
|
|
1472
|
+
cua open <url>
|
|
1473
|
+
cua click "<description>"
|
|
1474
|
+
cua type "<target>" "<text>"
|
|
1475
|
+
cua press <key> [key...]
|
|
1476
|
+
cua observe ["<question>"]
|
|
1477
|
+
cua url
|
|
1478
|
+
cua screenshot [--out file|-]
|
|
1479
|
+
cua do "<instruction>"
|
|
1480
|
+
cua models [-p provider]
|
|
1481
|
+
cua session start [name] | stop <name> | list | show <name>
|
|
1482
|
+
|
|
1483
|
+
Options:
|
|
1484
|
+
-p, --print Run a single prompt and exit
|
|
1485
|
+
-m, --model <ref> Model ref (default: ${DEFAULT_CUA_MODEL_REF})
|
|
1486
|
+
Accepts \`provider:model\` refs or bare ids that
|
|
1487
|
+
match exactly one entry in \`cua models\`.
|
|
1488
|
+
Recommended:
|
|
1489
|
+
openai: openai:gpt-5.5
|
|
1490
|
+
anthropic: anthropic:claude-opus-4-7
|
|
1491
|
+
google: google:gemini-3-flash-preview
|
|
1492
|
+
tzafon: tzafon:tzafon.northstar-cua-fast
|
|
1493
|
+
yutori: yutori:n1.5-latest
|
|
1494
|
+
--thinking <level> Thinking level: off | minimal | low | medium | high | xhigh
|
|
1495
|
+
(default: low; applies to providers that support it)
|
|
1496
|
+
--profile <name|id> Kernel browser profile to load
|
|
1497
|
+
--profile-no-save-changes Do not persist changes back to the profile
|
|
1498
|
+
--browser-timeout <s> Browser inactivity timeout in seconds (default 300)
|
|
1499
|
+
--max-steps <n> Max turns for action subcommands (default 3)
|
|
1500
|
+
--out <file|-> Output file for screenshot subcommand
|
|
1501
|
+
-o, --output <fmt> Output format for --print: text (default) | jsonl
|
|
1502
|
+
--jsonl-include-deltas Include assistant_text_delta events (default off)
|
|
1503
|
+
--jsonl-include-images Include base64 screenshots (default off, only sizes)
|
|
1504
|
+
--image-protocol <p> Force terminal image protocol: \`kitty\` | \`iterm2\` | \`none\` | \`auto\`
|
|
1505
|
+
(Ghostty / WezTerm are auto-detected as \`kitty\`.)
|
|
1506
|
+
Also via CUA_IMAGE_PROTOCOL env var.
|
|
1507
|
+
-s, --session-name <name> Reuse a named browser session (see \`cua session start\`)
|
|
1508
|
+
-c, --continue Resume the most recent session for cwd (fresh browser)
|
|
1509
|
+
-r, --resume Pick a previous session to resume from a list
|
|
1510
|
+
--session <ref> Resume a specific session: path | partial id | latest
|
|
1511
|
+
--session-dir <dir> Override the sessions directory
|
|
1512
|
+
--no-session Don't persist this session to disk
|
|
1513
|
+
--skill <path> Load a skill file or directory (repeatable).
|
|
1514
|
+
Defaults: ~/.agents/skills/, <cwd>/.agents/skills/
|
|
1515
|
+
-ns, --no-skills Disable skill discovery entirely
|
|
1516
|
+
--debug-tui Enable TUI render diagnostics for manual repros
|
|
1517
|
+
-v, --verbose Verbose progress output to stderr
|
|
1518
|
+
-h, --help Show this help
|
|
1519
|
+
|
|
1520
|
+
Environment:
|
|
1521
|
+
KERNEL_API_KEY Kernel API key (required)
|
|
1522
|
+
OPENAI_API_KEY OpenAI API key (required when -m openai:…)
|
|
1523
|
+
ANTHROPIC_API_KEY Anthropic API key (required when -m anthropic:…)
|
|
1524
|
+
GOOGLE_API_KEY Google API key (required when -m google:…)
|
|
1525
|
+
GEMINI_API_KEY Alias for GOOGLE_API_KEY
|
|
1526
|
+
TZAFON_API_KEY Tzafon API key (required when -m tzafon:…)
|
|
1527
|
+
YUTORI_API_KEY Yutori API key (required when -m yutori:…)
|
|
1528
|
+
KERNEL_BASE_URL Override Kernel base URL
|
|
1529
|
+
OPENAI_BASE_URL Override OpenAI base URL
|
|
1530
|
+
ANTHROPIC_BASE_URL Override Anthropic base URL
|
|
1531
|
+
GOOGLE_BASE_URL Override Google base URL
|
|
1532
|
+
TZAFON_BASE_URL Override Tzafon base URL
|
|
1533
|
+
YUTORI_BASE_URL Override Yutori base URL
|
|
1534
|
+
XDG_DATA_HOME Sessions are stored under \$XDG_DATA_HOME/cua/sessions
|
|
1535
|
+
(defaults to ~/.local/share/cua/sessions)
|
|
1536
|
+
CUA_IMAGE_PROTOCOL Force inline image protocol (\`kitty\`|\`iterm2\`|\`none\`|\`auto\`)
|
|
1537
|
+
`;
|
|
1538
|
+
function parseCliArgs(argv) {
|
|
1539
|
+
const preprocessed = argv.map((arg) => arg === "-ns" ? "--no-skills" : arg);
|
|
1540
|
+
let parsed;
|
|
1541
|
+
try {
|
|
1542
|
+
parsed = parseArgs({
|
|
1543
|
+
args: preprocessed,
|
|
1544
|
+
options: {
|
|
1545
|
+
help: {
|
|
1546
|
+
type: "boolean",
|
|
1547
|
+
short: "h",
|
|
1548
|
+
default: false
|
|
1549
|
+
},
|
|
1550
|
+
print: {
|
|
1551
|
+
type: "boolean",
|
|
1552
|
+
short: "p",
|
|
1553
|
+
default: false
|
|
1554
|
+
},
|
|
1555
|
+
verbose: {
|
|
1556
|
+
type: "boolean",
|
|
1557
|
+
short: "v",
|
|
1558
|
+
default: false
|
|
1559
|
+
},
|
|
1560
|
+
model: {
|
|
1561
|
+
type: "string",
|
|
1562
|
+
short: "m"
|
|
1563
|
+
},
|
|
1564
|
+
thinking: { type: "string" },
|
|
1565
|
+
profile: { type: "string" },
|
|
1566
|
+
"profile-no-save-changes": {
|
|
1567
|
+
type: "boolean",
|
|
1568
|
+
default: false
|
|
1569
|
+
},
|
|
1570
|
+
"browser-timeout": { type: "string" },
|
|
1571
|
+
"max-steps": { type: "string" },
|
|
1572
|
+
out: { type: "string" },
|
|
1573
|
+
"image-protocol": { type: "string" },
|
|
1574
|
+
"session-name": {
|
|
1575
|
+
type: "string",
|
|
1576
|
+
short: "s"
|
|
1577
|
+
},
|
|
1578
|
+
continue: {
|
|
1579
|
+
type: "boolean",
|
|
1580
|
+
short: "c",
|
|
1581
|
+
default: false
|
|
1582
|
+
},
|
|
1583
|
+
resume: {
|
|
1584
|
+
type: "boolean",
|
|
1585
|
+
short: "r",
|
|
1586
|
+
default: false
|
|
1587
|
+
},
|
|
1588
|
+
session: { type: "string" },
|
|
1589
|
+
"session-dir": { type: "string" },
|
|
1590
|
+
"no-session": {
|
|
1591
|
+
type: "boolean",
|
|
1592
|
+
default: false
|
|
1593
|
+
},
|
|
1594
|
+
skill: {
|
|
1595
|
+
type: "string",
|
|
1596
|
+
multiple: true,
|
|
1597
|
+
default: []
|
|
1598
|
+
},
|
|
1599
|
+
"no-skills": {
|
|
1600
|
+
type: "boolean",
|
|
1601
|
+
default: false
|
|
1602
|
+
},
|
|
1603
|
+
"debug-tui": {
|
|
1604
|
+
type: "boolean",
|
|
1605
|
+
default: false
|
|
1606
|
+
},
|
|
1607
|
+
output: {
|
|
1608
|
+
type: "string",
|
|
1609
|
+
short: "o"
|
|
1610
|
+
},
|
|
1611
|
+
"jsonl-include-deltas": {
|
|
1612
|
+
type: "boolean",
|
|
1613
|
+
default: false
|
|
1614
|
+
},
|
|
1615
|
+
"jsonl-include-images": {
|
|
1616
|
+
type: "boolean",
|
|
1617
|
+
default: false
|
|
1618
|
+
}
|
|
1619
|
+
},
|
|
1620
|
+
allowPositionals: true,
|
|
1621
|
+
strict: true
|
|
1622
|
+
});
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
throw new Error(`invalid arguments: ${err.message}`);
|
|
1625
|
+
}
|
|
1626
|
+
const browserTimeoutRaw = parsed.values["browser-timeout"];
|
|
1627
|
+
const browserTimeout = browserTimeoutRaw ? Number(browserTimeoutRaw) : void 0;
|
|
1628
|
+
const maxStepsRaw = parsed.values["max-steps"];
|
|
1629
|
+
const maxSteps = maxStepsRaw ? Number(maxStepsRaw) : void 0;
|
|
1630
|
+
const thinkingRaw = parsed.values.thinking;
|
|
1631
|
+
if (thinkingRaw !== void 0) {
|
|
1632
|
+
if (!new Set([
|
|
1633
|
+
"off",
|
|
1634
|
+
"none",
|
|
1635
|
+
"minimal",
|
|
1636
|
+
"low",
|
|
1637
|
+
"medium",
|
|
1638
|
+
"high",
|
|
1639
|
+
"xhigh"
|
|
1640
|
+
]).has(thinkingRaw.trim().toLowerCase())) throw new Error(`invalid --thinking value "${thinkingRaw}"; expected one of: off | minimal | low | medium | high | xhigh`);
|
|
1641
|
+
}
|
|
1642
|
+
return {
|
|
1643
|
+
help: !!parsed.values.help,
|
|
1644
|
+
print: !!parsed.values.print,
|
|
1645
|
+
verbose: !!parsed.values.verbose,
|
|
1646
|
+
profileSaveChanges: !parsed.values["profile-no-save-changes"],
|
|
1647
|
+
continueLatest: !!parsed.values.continue,
|
|
1648
|
+
resumePicker: !!parsed.values.resume,
|
|
1649
|
+
noSession: !!parsed.values["no-session"],
|
|
1650
|
+
noSkills: !!parsed.values["no-skills"],
|
|
1651
|
+
debugTui: !!parsed.values["debug-tui"],
|
|
1652
|
+
model: parsed.values.model,
|
|
1653
|
+
thinking: parsed.values.thinking,
|
|
1654
|
+
browserProfile: parsed.values.profile,
|
|
1655
|
+
browserTimeout: Number.isFinite(browserTimeout) ? browserTimeout : void 0,
|
|
1656
|
+
maxSteps: Number.isFinite(maxSteps) ? maxSteps : void 0,
|
|
1657
|
+
out: parsed.values.out,
|
|
1658
|
+
imageProtocol: parsed.values["image-protocol"],
|
|
1659
|
+
namedSession: parsed.values["session-name"],
|
|
1660
|
+
sessionRef: parsed.values.session,
|
|
1661
|
+
sessionDir: parsed.values["session-dir"],
|
|
1662
|
+
skillPaths: (parsed.values.skill ?? []).filter((p) => p && p.trim().length > 0),
|
|
1663
|
+
output: parsed.values.output,
|
|
1664
|
+
jsonlIncludeDeltas: !!parsed.values["jsonl-include-deltas"],
|
|
1665
|
+
jsonlIncludeImages: !!parsed.values["jsonl-include-images"],
|
|
1666
|
+
positionals: parsed.positionals
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
function toHarnessFlags(flags) {
|
|
1670
|
+
return {
|
|
1671
|
+
verbose: flags.verbose,
|
|
1672
|
+
profileSaveChanges: flags.profileSaveChanges,
|
|
1673
|
+
continueLatest: flags.continueLatest,
|
|
1674
|
+
resumePicker: flags.resumePicker,
|
|
1675
|
+
noSession: flags.noSession,
|
|
1676
|
+
noSkills: flags.noSkills,
|
|
1677
|
+
debugTui: flags.debugTui,
|
|
1678
|
+
jsonlIncludeDeltas: flags.jsonlIncludeDeltas,
|
|
1679
|
+
jsonlIncludeImages: flags.jsonlIncludeImages,
|
|
1680
|
+
model: flags.model,
|
|
1681
|
+
thinking: flags.thinking,
|
|
1682
|
+
browserProfile: flags.browserProfile,
|
|
1683
|
+
browserTimeout: flags.browserTimeout,
|
|
1684
|
+
maxSteps: flags.maxSteps,
|
|
1685
|
+
out: flags.out,
|
|
1686
|
+
output: flags.output,
|
|
1687
|
+
imageProtocol: flags.imageProtocol,
|
|
1688
|
+
namedSession: flags.namedSession,
|
|
1689
|
+
sessionRef: flags.sessionRef,
|
|
1690
|
+
sessionDir: flags.sessionDir,
|
|
1691
|
+
skillPaths: flags.skillPaths
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
const SUBCOMMANDS = new Set([
|
|
1695
|
+
"open",
|
|
1696
|
+
"click",
|
|
1697
|
+
"type",
|
|
1698
|
+
"press",
|
|
1699
|
+
"observe",
|
|
1700
|
+
"url",
|
|
1701
|
+
"screenshot",
|
|
1702
|
+
"do"
|
|
1703
|
+
]);
|
|
1704
|
+
async function main(argv) {
|
|
1705
|
+
if (argv[0] === "models") return await runModelsSubcommand(argv.slice(1));
|
|
1706
|
+
let flags;
|
|
1707
|
+
try {
|
|
1708
|
+
flags = parseCliArgs(argv);
|
|
1709
|
+
} catch (err) {
|
|
1710
|
+
stderr.write(`${err.message}\n\n${HELP}`);
|
|
1711
|
+
return 2;
|
|
1712
|
+
}
|
|
1713
|
+
if (flags.help) {
|
|
1714
|
+
stdout.write(HELP);
|
|
1715
|
+
return 0;
|
|
1716
|
+
}
|
|
1717
|
+
const positionals = flags.positionals;
|
|
1718
|
+
const first = positionals[0];
|
|
1719
|
+
if (first === "session") try {
|
|
1720
|
+
return await runSessionSubcommand(positionals.slice(1), toHarnessFlags(flags));
|
|
1721
|
+
} catch (err) {
|
|
1722
|
+
stderr.write(`session error: ${err.message}\n`);
|
|
1723
|
+
return 2;
|
|
1724
|
+
}
|
|
1725
|
+
if (first && SUBCOMMANDS.has(first)) try {
|
|
1726
|
+
return await runActionCommand(first, positionals.slice(1), toHarnessFlags(flags));
|
|
1727
|
+
} catch (err) {
|
|
1728
|
+
stderr.write(`error: ${err.message}\n`);
|
|
1729
|
+
return 2;
|
|
1730
|
+
}
|
|
1731
|
+
const prompt = positionals.join(" ").trim();
|
|
1732
|
+
if (flags.print) {
|
|
1733
|
+
if (!prompt) {
|
|
1734
|
+
stderr.write("error: --print requires a prompt\n");
|
|
1735
|
+
return 2;
|
|
1736
|
+
}
|
|
1737
|
+
try {
|
|
1738
|
+
return await runPrintCommand(prompt, toHarnessFlags(flags));
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
stderr.write(`error: ${err.message}\n`);
|
|
1741
|
+
return 1;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
return await runInteractiveCommand(prompt, toHarnessFlags(flags));
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
stderr.write(`error: ${err.message}\n`);
|
|
1748
|
+
return 1;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
main(process.argv.slice(2)).then((code) => {
|
|
1752
|
+
process.exit(code);
|
|
1753
|
+
}, (err) => {
|
|
1754
|
+
stderr.write(`fatal: ${err.message}\n`);
|
|
1755
|
+
process.exit(1);
|
|
1756
|
+
});
|
|
1757
|
+
//#endregion
|
|
1758
|
+
export { main };
|