@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.
Files changed (2) hide show
  1. package/dist/index.js +116 -5
  2. 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 started = task.created_at ? Date.parse(task.created_at) : Number.NaN;
837
- const elapsed = Number.isFinite(started) ? `running for ${Math.max(0, Math.round((nowMs - started) / 1e3))}s` : `waited ${waitedS}s`;
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
- lines.push(
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/browser-cli",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {