@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/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 };