@optima-chat/browser-cli 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +123 -9
  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
  }
@@ -199,7 +200,7 @@ function conversationHeader() {
199
200
  const conv = conversationId();
200
201
  return conv ? { "X-Optima-Conversation": conv } : {};
201
202
  }
202
- async function request(path, options = {}) {
203
+ async function request(path, options = {}, originalTaskConversation) {
203
204
  const url = `${BROWSER_API_URL}${path}`;
204
205
  const auth = authHeader();
205
206
  let response;
@@ -209,7 +210,7 @@ async function request(path, options = {}) {
209
210
  headers: {
210
211
  "Content-Type": "application/json",
211
212
  ...auth,
212
- ...conversationHeader(),
213
+ ...originalTaskConversation === void 0 ? conversationHeader() : originalTaskConversation === null ? {} : { "X-Optima-Conversation": originalTaskConversation },
213
214
  ...options.headers
214
215
  }
215
216
  });
@@ -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
  }
@@ -443,8 +450,32 @@ var BrowserAPI = class {
443
450
  body: JSON.stringify(payload)
444
451
  });
445
452
  }
453
+ async settleScriptTaskSession(taskId) {
454
+ const task = await this.getScriptTask(taskId);
455
+ const proof = task.execution_evidence;
456
+ if (task.id !== taskId || !["failed", "cancelled", "needs_fallback", "completed"].includes(task.status) || !proof || proof.protocol !== "script-execution/v1" || proof.task_id !== taskId || proof.worker_ended !== true || !(proof.conversation === null || typeof proof.conversation === "string")) {
457
+ throw new Error("Original task execution/conversation unresolved; refusing session settlement");
458
+ }
459
+ return request(
460
+ `/api/script-tasks/${encodeURIComponent(taskId)}/settle-session`,
461
+ { method: "POST" },
462
+ proof.conversation
463
+ );
464
+ }
446
465
  async getScriptTask(taskId) {
447
- return request(`/api/script-tasks/${taskId}`);
466
+ return request(`/api/script-tasks/${encodeURIComponent(taskId)}`);
467
+ }
468
+ async sourceCapabilities() {
469
+ return request(`/api/script-source-capabilities`);
470
+ }
471
+ async observeSource(taskId) {
472
+ return request(`/api/script-tasks/${encodeURIComponent(taskId)}/source`);
473
+ }
474
+ async releaseSource(taskId) {
475
+ return request(`/api/script-tasks/${encodeURIComponent(taskId)}/source`, { method: "DELETE" });
476
+ }
477
+ async sourceScreenshot(taskId, observationId) {
478
+ return requestBinary(`/api/script-tasks/${encodeURIComponent(taskId)}/source/observations/${encodeURIComponent(observationId)}/screenshot`);
448
479
  }
449
480
  async sendFile(src, dest) {
450
481
  return request("/api/files/send", {
@@ -491,6 +522,12 @@ function formatStatus(status) {
491
522
  lines.push(`Status: ${status.status}`);
492
523
  lines.push(`Created: ${status.created_at}`);
493
524
  lines.push(`Idle: ${status.idle_seconds}s`);
525
+ if (status.handoff) {
526
+ lines.push(`Handoff: ${status.handoff.state}`);
527
+ lines.push(`Expires: ${status.handoff.expires_at}`);
528
+ lines.push(`Remaining: ${status.handoff.remaining_seconds}s`);
529
+ lines.push(`Recovery: ${status.handoff.recovery}`);
530
+ }
494
531
  if (status.execution) {
495
532
  const where = status.execution === "sidecar" ? "user's machine" : "server-driven";
496
533
  lines.push(`Execution: ${status.execution} (${where})`);
@@ -572,6 +609,33 @@ function registerLifecycleCommands(program2) {
572
609
  process.exit(1);
573
610
  }
574
611
  });
612
+ 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) => {
613
+ try {
614
+ const seconds = Number(opts.timeoutSeconds);
615
+ if (!Number.isInteger(seconds) || seconds < 60 || seconds > 3600) {
616
+ throw new Error("timeout-seconds must be an integer between 60 and 3600");
617
+ }
618
+ const result = await api.handoff(loadSessionId(), seconds);
619
+ console.log(`Session: ${result.session_id}
620
+ Handoff: ${result.handoff.state}
621
+ Expires: ${result.handoff.expires_at}
622
+ Remaining: ${result.handoff.remaining_seconds}s
623
+ Recovery: ${result.handoff.recovery}`);
624
+ } catch (err) {
625
+ console.error(formatError(err));
626
+ process.exit(1);
627
+ }
628
+ });
629
+ program2.command("handoff-resume").description("Release human retention without navigating, submitting, or closing the tab").action(async () => {
630
+ try {
631
+ const result = await api.resumeHandoff(loadSessionId());
632
+ console.log(`Session: ${result.session_id}
633
+ Handoff released. Inspect the current page and verify submission before restoring or retrying.`);
634
+ } catch (err) {
635
+ console.error(formatError(err));
636
+ process.exit(1);
637
+ }
638
+ });
575
639
  program2.command("status").description("Show session status").action(async () => {
576
640
  try {
577
641
  const sessionId = loadSessionId();
@@ -995,7 +1059,12 @@ function formatScriptTask(task) {
995
1059
  lines.push("");
996
1060
  }
997
1061
  const sid = task.session_id;
998
- lines.push(
1062
+ const source = output.source_handoff;
1063
+ if (source) {
1064
+ lines.push(JSON.stringify(source));
1065
+ lines.push(`Submission remains unknown. Read only: browser-cli observe-source ${task.id}`);
1066
+ lines.push(`Release without closing a page: browser-cli release-source ${task.id}`);
1067
+ } else lines.push(
999
1068
  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"
1000
1069
  );
1001
1070
  } else if (task.status === "failed") {
@@ -1035,6 +1104,50 @@ function resolveSubmitSource(opts) {
1035
1104
  throw new Error("\u5FC5\u987B\u7ED9 --code-file \u6216 --code-dir");
1036
1105
  }
1037
1106
  function registerScriptCommands(program2) {
1107
+ 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) => {
1108
+ try {
1109
+ const capability = await api.sourceCapabilities();
1110
+ if (capability.protocol !== "source-observation/v1" || !capability.task_bound_observation) {
1111
+ throw new Error("source_observation_unsupported: backend capability unavailable; do not launch a replacement page");
1112
+ }
1113
+ const result = await api.observeSource(taskId);
1114
+ if (opts.out) {
1115
+ const directory = resolve(opts.out);
1116
+ mkdirSync2(directory, { recursive: true });
1117
+ const evidence = join3(directory, `${result.observation.id}.json`);
1118
+ writeFileSync3(evidence, JSON.stringify(result, null, 2));
1119
+ if (result.observation.screenshot_path) {
1120
+ try {
1121
+ writeFileSync3(
1122
+ join3(directory, `${result.observation.id}.png`),
1123
+ await api.sourceScreenshot(taskId, result.observation.id)
1124
+ );
1125
+ } catch (err) {
1126
+ result.observation.screenshot_error = String(err);
1127
+ writeFileSync3(evidence, JSON.stringify(result, null, 2));
1128
+ }
1129
+ }
1130
+ }
1131
+ console.log(JSON.stringify(result, null, 2));
1132
+ } catch (err) {
1133
+ const details = err && typeof err === "object" && "details" in err ? err.details : void 0;
1134
+ if (opts.out && details) {
1135
+ const directory = resolve(opts.out);
1136
+ mkdirSync2(directory, { recursive: true });
1137
+ writeFileSync3(join3(directory, "source-refused.json"), JSON.stringify(details, null, 2));
1138
+ }
1139
+ console.error(formatError(err));
1140
+ process.exitCode = 1;
1141
+ }
1142
+ });
1143
+ program2.command("release-source <task-id>").description("Release a task's bounded source lease; never closes or navigates a page").action(async (taskId) => {
1144
+ try {
1145
+ console.log(JSON.stringify(await api.releaseSource(taskId), null, 2));
1146
+ } catch (err) {
1147
+ console.error(formatError(err));
1148
+ process.exitCode = 1;
1149
+ }
1150
+ });
1038
1151
  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) => {
1039
1152
  try {
1040
1153
  const files = resolveSubmitSource(opts);
@@ -1126,7 +1239,7 @@ ${downloadHint(result)}`);
1126
1239
  process.exit(1);
1127
1240
  }
1128
1241
  });
1129
- program2.command("script-task").argument("<task-id>", "Task ID").description("Check script task status").option("--download [path]", "Save the task's exported file (default: ./<filename>)").option("--json", "Print the raw task (status / error / full output) as JSON").option(
1242
+ program2.command("script-task").argument("<task-id>", "Task ID").description("Check script task status").option("--settle-session", "Read the original task, then settle its ended channel under its server-recorded conversation; keeps the current session and never classifies publication").option("--download [path]", "Save the task's exported file (default: ./<filename>)").option("--json", "Print the raw task (status / error / full output) as JSON").option(
1130
1243
  "--wait",
1131
1244
  `Block until the task finishes (completed / failed / needs_fallback), at most --timeout seconds. Exit code: 0 completed, 2 failed / needs_fallback, 3 still running (call again), 4 waiting for the store owner (e.g. captcha; tell them, then --wait --through-human), 1 error`
1132
1245
  ).option("--through-human", "With --wait: keep waiting while the script waits for the store owner (exit 4 otherwise)").option("--timeout <seconds>", `With --wait: max seconds to wait (default ${WAIT_DEFAULT_TIMEOUT_S}, max ${WAIT_MAX_TIMEOUT_S})`).action(async (taskId, opts) => {
@@ -1136,6 +1249,7 @@ ${downloadHint(result)}`);
1136
1249
  }
1137
1250
  if (opts.timeout !== void 0 && !opts.wait) throw new Error("--timeout \u53EA\u80FD\u548C --wait \u4E00\u8D77\u7528");
1138
1251
  if (opts.throughHuman && !opts.wait) throw new Error("--through-human \u53EA\u80FD\u548C --wait \u4E00\u8D77\u7528");
1252
+ if (opts.settleSession && (opts.wait || opts.download !== void 0)) throw new Error("--settle-session cannot combine with --wait/--download");
1139
1253
  const timeoutS = opts.timeout === void 0 ? WAIT_DEFAULT_TIMEOUT_S : parseWaitTimeout(opts.timeout);
1140
1254
  let task;
1141
1255
  if (opts.wait) {
@@ -1154,12 +1268,12 @@ ${downloadHint(result)}`);
1154
1268
  return;
1155
1269
  }
1156
1270
  } else {
1157
- task = await api.getScriptTask(taskId);
1271
+ task = opts.settleSession ? await api.settleScriptTaskSession(taskId) : await api.getScriptTask(taskId);
1158
1272
  }
1159
1273
  const stoppedExit = () => {
1160
1274
  if (opts.wait && task.status !== "completed") process.exit(EXIT_TASK_STOPPED);
1161
1275
  };
1162
- if (task.status === "needs_fallback" && task.session_id) saveSessionId(task.session_id);
1276
+ if (!opts.settleSession && task.status === "needs_fallback" && task.session_id) saveSessionId(task.session_id);
1163
1277
  if (opts.json) {
1164
1278
  console.log(JSON.stringify(task, null, 2));
1165
1279
  stoppedExit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/browser-cli",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {