@optima-chat/browser-cli 0.23.0 → 0.25.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/index.js +116 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -160,10 +160,11 @@ function clearSessionId(onlyIfEquals) {
|
|
|
160
160
|
|
|
161
161
|
// src/api.ts
|
|
162
162
|
var BrowserAPIError = class extends Error {
|
|
163
|
-
constructor(statusCode, errorCode, message) {
|
|
163
|
+
constructor(statusCode, errorCode, message, details) {
|
|
164
164
|
super(message);
|
|
165
165
|
this.statusCode = statusCode;
|
|
166
166
|
this.errorCode = errorCode;
|
|
167
|
+
this.details = details;
|
|
167
168
|
this.name = "BrowserAPIError";
|
|
168
169
|
}
|
|
169
170
|
};
|
|
@@ -173,7 +174,7 @@ function toAPIError(status, statusText, body) {
|
|
|
173
174
|
return new BrowserAPIError(status, "http_error", `HTTP ${status}: ${detail}`);
|
|
174
175
|
}
|
|
175
176
|
if (detail?.message) {
|
|
176
|
-
return new BrowserAPIError(status, detail.error ?? "unknown", detail.message);
|
|
177
|
+
return new BrowserAPIError(status, detail.error ?? "unknown", detail.message, detail);
|
|
177
178
|
}
|
|
178
179
|
return new BrowserAPIError(status, "unknown", `HTTP ${status}: ${statusText}`);
|
|
179
180
|
}
|
|
@@ -322,6 +323,12 @@ var BrowserAPI = class {
|
|
|
322
323
|
throw err;
|
|
323
324
|
}
|
|
324
325
|
}
|
|
326
|
+
async handoff(sessionId, timeoutSeconds) {
|
|
327
|
+
return request(`/api/sessions/${sessionId}/handoff`, { method: "POST", body: JSON.stringify({ timeout_seconds: timeoutSeconds }) });
|
|
328
|
+
}
|
|
329
|
+
async resumeHandoff(sessionId) {
|
|
330
|
+
return request(`/api/sessions/${sessionId}/handoff`, { method: "DELETE" });
|
|
331
|
+
}
|
|
325
332
|
async getStatus(sessionId) {
|
|
326
333
|
return request(`/api/sessions/${sessionId}/status`);
|
|
327
334
|
}
|
|
@@ -446,6 +453,18 @@ var BrowserAPI = class {
|
|
|
446
453
|
async getScriptTask(taskId) {
|
|
447
454
|
return request(`/api/script-tasks/${taskId}`);
|
|
448
455
|
}
|
|
456
|
+
async sourceCapabilities() {
|
|
457
|
+
return request(`/api/script-source-capabilities`);
|
|
458
|
+
}
|
|
459
|
+
async observeSource(taskId) {
|
|
460
|
+
return request(`/api/script-tasks/${encodeURIComponent(taskId)}/source`);
|
|
461
|
+
}
|
|
462
|
+
async releaseSource(taskId) {
|
|
463
|
+
return request(`/api/script-tasks/${encodeURIComponent(taskId)}/source`, { method: "DELETE" });
|
|
464
|
+
}
|
|
465
|
+
async sourceScreenshot(taskId, observationId) {
|
|
466
|
+
return requestBinary(`/api/script-tasks/${encodeURIComponent(taskId)}/source/observations/${encodeURIComponent(observationId)}/screenshot`);
|
|
467
|
+
}
|
|
449
468
|
async sendFile(src, dest) {
|
|
450
469
|
return request("/api/files/send", {
|
|
451
470
|
method: "POST",
|
|
@@ -491,6 +510,12 @@ function formatStatus(status) {
|
|
|
491
510
|
lines.push(`Status: ${status.status}`);
|
|
492
511
|
lines.push(`Created: ${status.created_at}`);
|
|
493
512
|
lines.push(`Idle: ${status.idle_seconds}s`);
|
|
513
|
+
if (status.handoff) {
|
|
514
|
+
lines.push(`Handoff: ${status.handoff.state}`);
|
|
515
|
+
lines.push(`Expires: ${status.handoff.expires_at}`);
|
|
516
|
+
lines.push(`Remaining: ${status.handoff.remaining_seconds}s`);
|
|
517
|
+
lines.push(`Recovery: ${status.handoff.recovery}`);
|
|
518
|
+
}
|
|
494
519
|
if (status.execution) {
|
|
495
520
|
const where = status.execution === "sidecar" ? "user's machine" : "server-driven";
|
|
496
521
|
lines.push(`Execution: ${status.execution} (${where})`);
|
|
@@ -572,6 +597,33 @@ function registerLifecycleCommands(program2) {
|
|
|
572
597
|
process.exit(1);
|
|
573
598
|
}
|
|
574
599
|
});
|
|
600
|
+
program2.command("handoff").description("Reserve the current session/tab for human review (bounded, no submission)").option("--timeout-seconds <seconds>", "Retention from now, 60\u20133600 seconds; retries keep the original deadline", "1800").action(async (opts) => {
|
|
601
|
+
try {
|
|
602
|
+
const seconds = Number(opts.timeoutSeconds);
|
|
603
|
+
if (!Number.isInteger(seconds) || seconds < 60 || seconds > 3600) {
|
|
604
|
+
throw new Error("timeout-seconds must be an integer between 60 and 3600");
|
|
605
|
+
}
|
|
606
|
+
const result = await api.handoff(loadSessionId(), seconds);
|
|
607
|
+
console.log(`Session: ${result.session_id}
|
|
608
|
+
Handoff: ${result.handoff.state}
|
|
609
|
+
Expires: ${result.handoff.expires_at}
|
|
610
|
+
Remaining: ${result.handoff.remaining_seconds}s
|
|
611
|
+
Recovery: ${result.handoff.recovery}`);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
console.error(formatError(err));
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
program2.command("handoff-resume").description("Release human retention without navigating, submitting, or closing the tab").action(async () => {
|
|
618
|
+
try {
|
|
619
|
+
const result = await api.resumeHandoff(loadSessionId());
|
|
620
|
+
console.log(`Session: ${result.session_id}
|
|
621
|
+
Handoff released. Inspect the current page and verify submission before restoring or retrying.`);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.error(formatError(err));
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
575
627
|
program2.command("status").description("Show session status").action(async () => {
|
|
576
628
|
try {
|
|
577
629
|
const sessionId = loadSessionId();
|
|
@@ -833,8 +885,18 @@ function parseWaitTimeout(raw) {
|
|
|
833
885
|
return n;
|
|
834
886
|
}
|
|
835
887
|
function stillRunningMessage(task, waitedS, nowMs = Date.now()) {
|
|
836
|
-
const
|
|
837
|
-
|
|
888
|
+
const secondsSince = (iso) => {
|
|
889
|
+
const t = iso ? Date.parse(iso) : Number.NaN;
|
|
890
|
+
return Number.isFinite(t) ? Math.max(0, Math.round((nowMs - t) / 1e3)) : null;
|
|
891
|
+
};
|
|
892
|
+
const created = secondsSince(task.created_at);
|
|
893
|
+
let elapsed;
|
|
894
|
+
if (task.queue?.queued) {
|
|
895
|
+
elapsed = `queued, ${task.queue.ahead} task(s) of this user ahead` + (created !== null ? `, created ${created}s ago` : "");
|
|
896
|
+
} else {
|
|
897
|
+
const ran = secondsSince(task.queue?.started_at) ?? created;
|
|
898
|
+
elapsed = ran !== null ? `running for ${ran}s` : `waited ${waitedS}s`;
|
|
899
|
+
}
|
|
838
900
|
const step = task.total_steps ? `, step ${task.current_step}/${task.total_steps}` : "";
|
|
839
901
|
const waiting = waitingHumanLine(task);
|
|
840
902
|
return `Still running (${elapsed}${step}). Call again as a separate command: browser-cli script-task ${task.id} --wait` + (waiting ? ` --through-human
|
|
@@ -985,7 +1047,12 @@ function formatScriptTask(task) {
|
|
|
985
1047
|
lines.push("");
|
|
986
1048
|
}
|
|
987
1049
|
const sid = task.session_id;
|
|
988
|
-
|
|
1050
|
+
const source = output.source_handoff;
|
|
1051
|
+
if (source) {
|
|
1052
|
+
lines.push(JSON.stringify(source));
|
|
1053
|
+
lines.push(`Submission remains unknown. Read only: browser-cli observe-source ${task.id}`);
|
|
1054
|
+
lines.push(`Release without closing a page: browser-cli release-source ${task.id}`);
|
|
1055
|
+
} else lines.push(
|
|
989
1056
|
sid ? `Session ${sid} may still be open \u2014 continue with browser-cli --session ${sid} dom/click/input, or free it with browser-cli --session ${sid} close` : "Session id not reported \u2014 if one remains, close it by id: browser-cli --session <id> close"
|
|
990
1057
|
);
|
|
991
1058
|
} else if (task.status === "failed") {
|
|
@@ -1025,6 +1092,50 @@ function resolveSubmitSource(opts) {
|
|
|
1025
1092
|
throw new Error("\u5FC5\u987B\u7ED9 --code-file \u6216 --code-dir");
|
|
1026
1093
|
}
|
|
1027
1094
|
function registerScriptCommands(program2) {
|
|
1095
|
+
program2.command("observe-source <task-id>").description("Read the exact original task page without navigation or replay; does not renew retention").option("--out <directory>", "Save text/JSON evidence before attempting screenshot download").action(async (taskId, opts) => {
|
|
1096
|
+
try {
|
|
1097
|
+
const capability = await api.sourceCapabilities();
|
|
1098
|
+
if (capability.protocol !== "source-observation/v1" || !capability.task_bound_observation) {
|
|
1099
|
+
throw new Error("source_observation_unsupported: backend capability unavailable; do not launch a replacement page");
|
|
1100
|
+
}
|
|
1101
|
+
const result = await api.observeSource(taskId);
|
|
1102
|
+
if (opts.out) {
|
|
1103
|
+
const directory = resolve(opts.out);
|
|
1104
|
+
mkdirSync2(directory, { recursive: true });
|
|
1105
|
+
const evidence = join3(directory, `${result.observation.id}.json`);
|
|
1106
|
+
writeFileSync3(evidence, JSON.stringify(result, null, 2));
|
|
1107
|
+
if (result.observation.screenshot_path) {
|
|
1108
|
+
try {
|
|
1109
|
+
writeFileSync3(
|
|
1110
|
+
join3(directory, `${result.observation.id}.png`),
|
|
1111
|
+
await api.sourceScreenshot(taskId, result.observation.id)
|
|
1112
|
+
);
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
result.observation.screenshot_error = String(err);
|
|
1115
|
+
writeFileSync3(evidence, JSON.stringify(result, null, 2));
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
const details = err && typeof err === "object" && "details" in err ? err.details : void 0;
|
|
1122
|
+
if (opts.out && details) {
|
|
1123
|
+
const directory = resolve(opts.out);
|
|
1124
|
+
mkdirSync2(directory, { recursive: true });
|
|
1125
|
+
writeFileSync3(join3(directory, "source-refused.json"), JSON.stringify(details, null, 2));
|
|
1126
|
+
}
|
|
1127
|
+
console.error(formatError(err));
|
|
1128
|
+
process.exitCode = 1;
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
program2.command("release-source <task-id>").description("Release a task's bounded source lease; never closes or navigates a page").action(async (taskId) => {
|
|
1132
|
+
try {
|
|
1133
|
+
console.log(JSON.stringify(await api.releaseSource(taskId), null, 2));
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
console.error(formatError(err));
|
|
1136
|
+
process.exitCode = 1;
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1028
1139
|
program2.command("submit-script").description("Submit a script (write to EFS)").requiredOption("--name <name>", "Script name").option("--code-file <path>", "Path to Python script file (\u7B49\u4EF7\u4E8E\u300C\u53EA\u6709 script.py\u300D)").option("--code-dir <dir>", "Path to a skill directory (script.py + _*.py \u4E00\u8D77\u9001)").action(async (opts) => {
|
|
1029
1140
|
try {
|
|
1030
1141
|
const files = resolveSubmitSource(opts);
|