opensteer 0.8.9 → 0.8.11

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/bin.js CHANGED
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { assertProviderSupportsEngine, createOpensteerSemanticRuntime, OpensteerBrowserManager, dispatchSemanticOperation, loadEnvironment, normalizeOpensteerProviderMode, discoverLocalCdpBrowsers, inspectCdpEndpoint, resolveOpensteerRuntimeConfig, resolveOpensteerEngineName, resolveOpensteerProvider, resolveFilesystemWorkspacePath, pathExists, readPersistedLocalBrowserSessionRecord, readPersistedCloudSessionRecord, OpensteerCloudClient, isProcessRunning } from '../chunk-RO6WAWWG.js';
2
+ import { assertProviderSupportsEngine, createOpensteerSemanticRuntime, OpensteerBrowserManager, dispatchSemanticOperation, loadEnvironment, normalizeOpensteerProviderMode, discoverLocalCdpBrowsers, inspectCdpEndpoint, resolveOpensteerRuntimeConfig, resolveOpensteerEngineName, resolveOpensteerProvider, resolveFilesystemWorkspacePath, CloudSessionProxy, FlowRecorderCollector, generateReplayScript, pathExists, readPersistedLocalBrowserSessionRecord, readPersistedCloudSessionRecord, OpensteerCloudClient, isProcessRunning } from '../chunk-33FDEOQY.js';
3
3
  import process2 from 'process';
4
4
  import { spawn } from 'child_process';
5
5
  import { existsSync } from 'fs';
6
6
  import path from 'path';
7
7
  import { createRequire } from 'module';
8
8
  import { fileURLToPath } from 'url';
9
+ import { mkdir, writeFile } from 'fs/promises';
9
10
 
10
11
  // package.json
11
12
  var package_default = {
12
- version: "0.8.8"};
13
+ version: "0.8.10"};
13
14
 
14
15
  // src/cli/env-loader.ts
15
16
  async function loadCliEnvironment(cwd) {
@@ -214,9 +215,11 @@ async function describeCloudLane(input) {
214
215
  status: "connected",
215
216
  current: input.current,
216
217
  summary: input.record.sessionId,
217
- detail: input.record.baseUrl,
218
218
  sessionId: input.record.sessionId,
219
- baseUrl: input.record.baseUrl
219
+ ...input.cloudConfig === void 0 ? {} : {
220
+ detail: input.cloudConfig.baseUrl,
221
+ baseUrl: input.cloudConfig.baseUrl
222
+ }
220
223
  };
221
224
  if (input.cloudConfig === void 0) {
222
225
  return base;
@@ -260,6 +263,214 @@ function formatLaneRow(input) {
260
263
  const summary = input.summary.padEnd(16, " ");
261
264
  return `${input.marker} ${provider} ${status} ${summary}${input.detail ?? ""}`.trimEnd();
262
265
  }
266
+ async function runOpensteerRecordCommand(input) {
267
+ const stdout = input.stdout ?? process.stdout;
268
+ const stderr = input.stderr ?? process.stderr;
269
+ const outputPath = resolveRecordOutputPath({
270
+ rootDir: input.rootDir,
271
+ workspace: input.workspace,
272
+ ...input.outputPath === void 0 ? {} : { outputPath: input.outputPath }
273
+ });
274
+ const runtime = input.runtime;
275
+ const collector = new FlowRecorderCollector(createRecorderRuntimeAdapter(runtime), {
276
+ ...input.pollIntervalMs === void 0 ? {} : { pollIntervalMs: input.pollIntervalMs },
277
+ onAction: (action) => {
278
+ stderr.write(`${formatRecordedAction(action)}
279
+ `);
280
+ }
281
+ });
282
+ stderr.write(
283
+ `Recording browser actions for workspace "${input.workspace}". Click "Stop recording" in the browser when you're done.
284
+ `
285
+ );
286
+ let closed = false;
287
+ try {
288
+ const opened = await runtime.open({
289
+ url: input.url
290
+ });
291
+ await collector.install();
292
+ collector.start();
293
+ await collector.waitForStop();
294
+ const actions = await collector.stop();
295
+ const script = generateReplayScript({
296
+ actions,
297
+ workspace: input.workspace,
298
+ startUrl: opened.url
299
+ });
300
+ await mkdir(path.dirname(outputPath), { recursive: true });
301
+ await writeFile(outputPath, script, "utf8");
302
+ if (input.closeSession !== void 0) {
303
+ await input.closeSession();
304
+ closed = true;
305
+ }
306
+ stdout.write(`${outputPath}
307
+ `);
308
+ stderr.write(`Wrote replay script to ${outputPath}
309
+ `);
310
+ } finally {
311
+ if (!closed) {
312
+ await runtime.disconnect().catch(() => void 0);
313
+ }
314
+ }
315
+ }
316
+ async function runOpensteerCloudRecordCommand(input) {
317
+ const stdout = input.stdout ?? process.stdout;
318
+ const stderr = input.stderr ?? process.stderr;
319
+ const outputPath = resolveRecordOutputPath({
320
+ rootDir: input.rootDir,
321
+ workspace: input.workspace,
322
+ ...input.outputPath === void 0 ? {} : { outputPath: input.outputPath }
323
+ });
324
+ let cloud;
325
+ const resolveCloud = () => {
326
+ cloud ??= new OpensteerCloudClient(input.cloudConfig);
327
+ return cloud;
328
+ };
329
+ const runtime = input.runtime ?? new CloudSessionProxy(resolveCloud(), {
330
+ rootDir: input.rootDir,
331
+ workspace: input.workspace
332
+ });
333
+ const client = input.client ?? resolveCloud();
334
+ const sleep = input.sleep ?? delay;
335
+ let closed = false;
336
+ try {
337
+ await runtime.open({
338
+ url: input.url,
339
+ ...input.browser === void 0 ? {} : { browser: input.browser },
340
+ ...input.launch === void 0 ? {} : { launch: input.launch },
341
+ ...input.context === void 0 ? {} : { context: input.context }
342
+ });
343
+ const sessionId = await resolveCloudRecordingSessionId(runtime);
344
+ const sessionUrl = buildCloudRecordingSessionUrl(input.cloudConfig, sessionId);
345
+ await client.startSessionRecording(sessionId);
346
+ stderr.write(
347
+ `Recording browser actions for workspace "${input.workspace}". Open ${sessionUrl} and click "Stop recording" in the browser session toolbar when you're done.
348
+ `
349
+ );
350
+ const completed = await waitForCloudRecordingCompletion({
351
+ client,
352
+ sessionId,
353
+ ...input.pollIntervalMs === void 0 ? {} : { pollIntervalMs: input.pollIntervalMs },
354
+ sleep
355
+ });
356
+ if (completed.result === void 0) {
357
+ throw new Error("Cloud recording completed without a replay script.");
358
+ }
359
+ await mkdir(path.dirname(outputPath), { recursive: true });
360
+ await writeFile(outputPath, completed.result.script, "utf8");
361
+ await runtime.close();
362
+ closed = true;
363
+ stdout.write(`${outputPath}
364
+ `);
365
+ stderr.write(`Cloud browser session: ${sessionUrl}
366
+ `);
367
+ stderr.write(`Wrote replay script to ${outputPath}
368
+ `);
369
+ } finally {
370
+ if (!closed) {
371
+ await runtime.close().catch(() => void 0);
372
+ }
373
+ }
374
+ }
375
+ function resolveRecordOutputPath(input) {
376
+ if (input.outputPath !== void 0) {
377
+ return path.resolve(input.rootDir, input.outputPath);
378
+ }
379
+ return path.join(
380
+ resolveFilesystemWorkspacePath({
381
+ rootDir: input.rootDir,
382
+ workspace: input.workspace
383
+ }),
384
+ "recorded-flow.ts"
385
+ );
386
+ }
387
+ function createRecorderRuntimeAdapter(runtime) {
388
+ return {
389
+ addInitScript: (input) => runtime.addInitScript(input),
390
+ evaluate: async (input) => {
391
+ const output = await runtime.evaluate({
392
+ script: input.script,
393
+ ...input.pageRef === void 0 ? {} : { pageRef: input.pageRef }
394
+ });
395
+ return output.value;
396
+ },
397
+ listPages: async () => {
398
+ const output = await runtime.listPages();
399
+ return {
400
+ pages: output.pages.map((page) => ({
401
+ pageRef: page.pageRef,
402
+ url: page.url,
403
+ ...page.openerPageRef === void 0 ? {} : { openerPageRef: page.openerPageRef }
404
+ }))
405
+ };
406
+ }
407
+ };
408
+ }
409
+ function buildCloudRecordingSessionUrl(cloudConfig, sessionId) {
410
+ const baseUrl = cloudConfig.appBaseUrl;
411
+ if (!baseUrl || baseUrl.trim().length === 0) {
412
+ throw new Error(
413
+ 'record with provider=cloud requires OPENSTEER_CLOUD_APP_BASE_URL or "--cloud-app-base-url".'
414
+ );
415
+ }
416
+ return `${baseUrl.replace(/\/+$/, "")}/browsers/${encodeURIComponent(sessionId)}`;
417
+ }
418
+ async function resolveCloudRecordingSessionId(runtime) {
419
+ const info = await runtime.info();
420
+ if (typeof info.sessionId !== "string" || info.sessionId.length === 0) {
421
+ throw new Error("Cloud recording could not resolve the created session id.");
422
+ }
423
+ return info.sessionId;
424
+ }
425
+ async function waitForCloudRecordingCompletion(input) {
426
+ const pollIntervalMs = input.pollIntervalMs ?? 1e3;
427
+ for (; ; ) {
428
+ const state = await input.client.getSessionRecording(input.sessionId);
429
+ if (state.status === "completed") {
430
+ return state;
431
+ }
432
+ if (state.status === "failed") {
433
+ throw new Error(state.error ?? "Cloud recording failed.");
434
+ }
435
+ await input.sleep(pollIntervalMs);
436
+ }
437
+ }
438
+ function formatRecordedAction(action) {
439
+ const time = new Date(action.timestamp).toISOString().slice(11, 19);
440
+ switch (action.kind) {
441
+ case "click":
442
+ return `[${time}] click ${action.pageId} -> ${action.selector ?? "<unknown>"}`;
443
+ case "dblclick":
444
+ return `[${time}] dblclick ${action.pageId} -> ${action.selector ?? "<unknown>"}`;
445
+ case "type":
446
+ return `[${time}] type ${action.pageId} -> ${action.selector ?? "<unknown>"} -> ${JSON.stringify(action.detail.text)}`;
447
+ case "keypress":
448
+ return `[${time}] keypress ${action.pageId} -> ${action.detail.key}`;
449
+ case "scroll":
450
+ return `[${time}] scroll ${action.pageId} -> (${String(action.detail.deltaX)}, ${String(action.detail.deltaY)})`;
451
+ case "select-option":
452
+ return `[${time}] select ${action.pageId} -> ${action.selector ?? "<unknown>"} -> ${JSON.stringify(action.detail.value)}`;
453
+ case "navigate":
454
+ return `[${time}] navigate ${action.pageId} -> ${action.detail.url}`;
455
+ case "new-tab":
456
+ return `[${time}] new-tab ${action.pageId} -> ${action.detail.initialUrl}`;
457
+ case "close-tab":
458
+ return `[${time}] close-tab ${action.pageId}`;
459
+ case "switch-tab":
460
+ return `[${time}] switch-tab -> ${action.detail.toPageId}`;
461
+ case "go-back":
462
+ return `[${time}] go-back ${action.pageId}`;
463
+ case "go-forward":
464
+ return `[${time}] go-forward ${action.pageId}`;
465
+ case "reload":
466
+ return `[${time}] reload ${action.pageId}`;
467
+ }
468
+ }
469
+ function delay(ms) {
470
+ return new Promise((resolve) => {
471
+ setTimeout(resolve, ms);
472
+ });
473
+ }
263
474
 
264
475
  // src/cli/bin.ts
265
476
  var OPERATION_ALIASES = /* @__PURE__ */ new Map([
@@ -351,6 +562,10 @@ async function main() {
351
562
  await handleStatusCommand(parsed);
352
563
  return;
353
564
  }
565
+ if (parsed.command[0] === "record") {
566
+ await handleRecordCommandEntry(parsed);
567
+ return;
568
+ }
354
569
  const operation = parsed.command[0] === "run" ? parsed.rest[0] : resolveOperation(parsed.command);
355
570
  if (!operation) {
356
571
  throw new Error(`Unknown command: ${parsed.command.join(" ")}`);
@@ -495,6 +710,100 @@ async function handleBrowserCommand(parsed) {
495
710
  throw new Error(`Unknown browser command: ${parsed.command.join(" ")}`);
496
711
  }
497
712
  }
713
+ async function handleRecordCommandEntry(parsed) {
714
+ if (parsed.options.workspace === void 0) {
715
+ throw new Error('record requires "--workspace <id>".');
716
+ }
717
+ const url = parsed.options.url ?? parsed.rest[0];
718
+ if (url === void 0) {
719
+ throw new Error('record requires "--url <value>" or a positional URL.');
720
+ }
721
+ const provider = resolveCliProvider(parsed);
722
+ assertCloudCliOptionsMatchProvider(parsed, provider.mode);
723
+ const engineName = resolveCliEngineName(parsed);
724
+ if (engineName !== "playwright") {
725
+ throw new Error("record requires engine=playwright.");
726
+ }
727
+ const rootDir = process2.cwd();
728
+ const recordBrowser = parsed.options.browser;
729
+ if (provider.mode === "cloud") {
730
+ if (typeof recordBrowser === "object") {
731
+ throw new Error('record does not support browser.mode="attach".');
732
+ }
733
+ const runtimeProvider = buildCliRuntimeProvider(parsed, provider.mode);
734
+ const runtimeConfig = resolveOpensteerRuntimeConfig({
735
+ ...runtimeProvider === void 0 ? {} : { provider: runtimeProvider },
736
+ environment: process2.env
737
+ });
738
+ await runOpensteerCloudRecordCommand({
739
+ cloudConfig: runtimeConfig.cloud,
740
+ workspace: parsed.options.workspace,
741
+ url,
742
+ rootDir,
743
+ ...recordBrowser === void 0 ? {} : { browser: recordBrowser },
744
+ ...parsed.options.launch === void 0 ? {} : { launch: parsed.options.launch },
745
+ ...parsed.options.context === void 0 ? {} : { context: parsed.options.context },
746
+ ...parsed.options.output === void 0 ? {} : { outputPath: parsed.options.output }
747
+ });
748
+ return;
749
+ }
750
+ if (parsed.options.launch?.headless === true) {
751
+ throw new Error('record requires a headed browser. Remove "--headless true".');
752
+ }
753
+ if (recordBrowser !== void 0 && recordBrowser !== "persistent") {
754
+ throw new Error('record only supports "--browser persistent".');
755
+ }
756
+ const launch = {
757
+ ...parsed.options.launch ?? {},
758
+ headless: false
759
+ };
760
+ const browserManager = new OpensteerBrowserManager({
761
+ rootDir,
762
+ workspace: parsed.options.workspace,
763
+ engineName,
764
+ browser: "persistent",
765
+ launch,
766
+ ...parsed.options.context === void 0 ? {} : { context: parsed.options.context }
767
+ });
768
+ const runtime = createOpensteerSemanticRuntime({
769
+ provider: {
770
+ mode: "local"
771
+ },
772
+ engine: engineName,
773
+ runtimeOptions: {
774
+ rootPath: browserManager.rootPath,
775
+ cleanupRootOnClose: browserManager.cleanupRootOnDisconnect,
776
+ workspace: parsed.options.workspace,
777
+ browser: "persistent",
778
+ launch,
779
+ ...parsed.options.context === void 0 ? {} : { context: parsed.options.context }
780
+ }
781
+ });
782
+ await runOpensteerRecordCommand({
783
+ runtime,
784
+ closeSession: () => closeOwnedLocalBrowserSession(runtime, browserManager),
785
+ workspace: parsed.options.workspace,
786
+ url,
787
+ rootDir,
788
+ ...parsed.options.output === void 0 ? {} : { outputPath: parsed.options.output }
789
+ });
790
+ }
791
+ async function closeOwnedLocalBrowserSession(runtime, browserManager) {
792
+ let closeError;
793
+ try {
794
+ await runtime.close();
795
+ } catch (error) {
796
+ closeError = error;
797
+ }
798
+ try {
799
+ await browserManager.close();
800
+ } catch (error) {
801
+ closeError ??= error;
802
+ }
803
+ if (closeError !== void 0) {
804
+ throw closeError;
805
+ }
806
+ }
498
807
  function buildOperationInput(operation, parsed) {
499
808
  if (parsed.options.inputJson !== void 0) {
500
809
  return parsed.options.inputJson;
@@ -575,6 +884,46 @@ function resolveOperation(command) {
575
884
  }
576
885
  return void 0;
577
886
  }
887
+ var CLI_OPTION_SPECS = {
888
+ workspace: { kind: "value" },
889
+ url: { kind: "value" },
890
+ output: { kind: "value" },
891
+ engine: { kind: "value" },
892
+ provider: { kind: "value" },
893
+ "cloud-base-url": { kind: "value" },
894
+ "cloud-api-key": { kind: "value" },
895
+ "cloud-app-base-url": { kind: "value" },
896
+ "cloud-profile-id": { kind: "value" },
897
+ "cloud-profile-reuse-if-active": { kind: "boolean" },
898
+ json: { kind: "boolean" },
899
+ agent: { kind: "value", multiple: true },
900
+ skill: { kind: "value", multiple: true },
901
+ global: { kind: "boolean" },
902
+ yes: { kind: "boolean" },
903
+ copy: { kind: "boolean" },
904
+ all: { kind: "boolean" },
905
+ list: { kind: "boolean" },
906
+ browser: { kind: "value" },
907
+ "attach-endpoint": { kind: "value" },
908
+ "attach-header": { kind: "value", multiple: true },
909
+ "fresh-tab": { kind: "boolean" },
910
+ headless: { kind: "boolean" },
911
+ "executable-path": { kind: "value" },
912
+ arg: { kind: "value", multiple: true },
913
+ "timeout-ms": { kind: "value" },
914
+ "context-json": { kind: "value" },
915
+ "input-json": { kind: "value" },
916
+ "schema-json": { kind: "value" },
917
+ "source-user-data-dir": { kind: "value" },
918
+ "source-profile-directory": { kind: "value" },
919
+ selector: { kind: "value" },
920
+ description: { kind: "value" },
921
+ element: { kind: "value" },
922
+ text: { kind: "value" },
923
+ "press-enter": { kind: "boolean" },
924
+ direction: { kind: "value" },
925
+ amount: { kind: "value" }
926
+ };
578
927
  function parseCommandLine(argv) {
579
928
  const leadingTokens = [];
580
929
  let index = 0;
@@ -588,18 +937,43 @@ function parseCommandLine(argv) {
588
937
  const rawOptions = /* @__PURE__ */ new Map();
589
938
  while (index < argv.length) {
590
939
  const token = argv[index];
940
+ if (token === "--") {
941
+ rest.push(...argv.slice(index + 1));
942
+ break;
943
+ }
591
944
  if (!token.startsWith("--")) {
592
945
  rest.push(token);
593
946
  index += 1;
594
947
  continue;
595
948
  }
596
- const key = token.slice(2);
597
- const next = argv[index + 1];
598
- if (next === void 0 || next.startsWith("--")) {
599
- rawOptions.set(key, [...rawOptions.get(key) ?? [], "true"]);
949
+ const separator = token.indexOf("=");
950
+ const key = token.slice(2, separator === -1 ? void 0 : separator);
951
+ const spec = CLI_OPTION_SPECS[key];
952
+ if (spec === void 0) {
953
+ throw new Error(`Unknown option: --${key}.`);
954
+ }
955
+ if (separator !== -1) {
956
+ const value = token.slice(separator + 1);
957
+ rawOptions.set(key, [...rawOptions.get(key) ?? [], value]);
600
958
  index += 1;
601
959
  continue;
602
960
  }
961
+ const next = argv[index + 1];
962
+ if (spec.kind === "boolean") {
963
+ if (next === void 0 || next.startsWith("--")) {
964
+ rawOptions.set(key, [...rawOptions.get(key) ?? [], "true"]);
965
+ index += 1;
966
+ continue;
967
+ }
968
+ rawOptions.set(key, [...rawOptions.get(key) ?? [], next]);
969
+ index += 2;
970
+ continue;
971
+ }
972
+ if (next === void 0 || next.startsWith("--")) {
973
+ throw new Error(
974
+ `Option "--${key}" requires a value.${next?.startsWith("--") === true ? ` Use "--${key}=<value>" when the value begins with "--".` : ``}`
975
+ );
976
+ }
603
977
  rawOptions.set(key, [...rawOptions.get(key) ?? [], next]);
604
978
  index += 2;
605
979
  }
@@ -630,6 +1004,8 @@ function parseCommandLine(argv) {
630
1004
  ...timeoutMs === void 0 ? {} : { timeoutMs }
631
1005
  };
632
1006
  const workspace = readSingle(rawOptions, "workspace");
1007
+ const url = readSingle(rawOptions, "url");
1008
+ const output = readSingle(rawOptions, "output");
633
1009
  const sourceUserDataDir = readSingle(rawOptions, "source-user-data-dir");
634
1010
  const sourceProfileDirectory = readSingle(rawOptions, "source-profile-directory");
635
1011
  const selector = readSingle(rawOptions, "selector");
@@ -646,6 +1022,7 @@ function parseCommandLine(argv) {
646
1022
  const provider = providerValue === void 0 ? void 0 : normalizeOpensteerProviderMode(providerValue, "--provider");
647
1023
  const cloudBaseUrl = readSingle(rawOptions, "cloud-base-url");
648
1024
  const cloudApiKey = readSingle(rawOptions, "cloud-api-key");
1025
+ const cloudAppBaseUrl = readSingle(rawOptions, "cloud-app-base-url");
649
1026
  const cloudProfileId = readSingle(rawOptions, "cloud-profile-id");
650
1027
  const cloudProfileReuseIfActive = readOptionalBoolean(
651
1028
  rawOptions,
@@ -661,10 +1038,13 @@ function parseCommandLine(argv) {
661
1038
  const list = readOptionalBoolean(rawOptions, "list");
662
1039
  const options = {
663
1040
  ...workspace === void 0 ? {} : { workspace },
1041
+ ...url === void 0 ? {} : { url },
1042
+ ...output === void 0 ? {} : { output },
664
1043
  ...requestedEngineName === void 0 ? {} : { requestedEngineName },
665
1044
  ...provider === void 0 ? {} : { provider },
666
1045
  ...cloudBaseUrl === void 0 ? {} : { cloudBaseUrl },
667
1046
  ...cloudApiKey === void 0 ? {} : { cloudApiKey },
1047
+ ...cloudAppBaseUrl === void 0 ? {} : { cloudAppBaseUrl },
668
1048
  ...cloudProfileId === void 0 ? {} : { cloudProfileId },
669
1049
  ...cloudProfileReuseIfActive === void 0 ? {} : { cloudProfileReuseIfActive },
670
1050
  ...json === void 0 ? {} : { json },
@@ -749,7 +1129,7 @@ function buildCliRuntimeProvider(parsed, providerMode) {
749
1129
  return explicitProvider?.mode === "local" ? explicitProvider : void 0;
750
1130
  }
751
1131
  const browserProfile = buildCliBrowserProfile(parsed);
752
- const hasCloudOverrides = parsed.options.cloudBaseUrl !== void 0 || parsed.options.cloudApiKey !== void 0 || browserProfile !== void 0;
1132
+ const hasCloudOverrides = parsed.options.cloudBaseUrl !== void 0 || parsed.options.cloudApiKey !== void 0 || parsed.options.cloudAppBaseUrl !== void 0 || browserProfile !== void 0;
753
1133
  if (!hasCloudOverrides && explicitProvider?.mode !== "cloud") {
754
1134
  return void 0;
755
1135
  }
@@ -757,11 +1137,12 @@ function buildCliRuntimeProvider(parsed, providerMode) {
757
1137
  mode: "cloud",
758
1138
  ...parsed.options.cloudBaseUrl === void 0 ? {} : { baseUrl: parsed.options.cloudBaseUrl },
759
1139
  ...parsed.options.cloudApiKey === void 0 ? {} : { apiKey: parsed.options.cloudApiKey },
1140
+ ...parsed.options.cloudAppBaseUrl === void 0 ? {} : { appBaseUrl: parsed.options.cloudAppBaseUrl },
760
1141
  ...browserProfile === void 0 ? {} : { browserProfile }
761
1142
  };
762
1143
  }
763
1144
  function assertCloudCliOptionsMatchProvider(parsed, providerMode) {
764
- if (providerMode !== "cloud" && (parsed.options.cloudBaseUrl !== void 0 || parsed.options.cloudApiKey !== void 0 || parsed.options.cloudProfileId !== void 0 || parsed.options.cloudProfileReuseIfActive === true)) {
1145
+ if (providerMode !== "cloud" && (parsed.options.cloudBaseUrl !== void 0 || parsed.options.cloudApiKey !== void 0 || parsed.options.cloudAppBaseUrl !== void 0 || parsed.options.cloudProfileId !== void 0 || parsed.options.cloudProfileReuseIfActive === true)) {
765
1146
  throw new Error(
766
1147
  'Cloud-specific options require provider=cloud. Set "--provider cloud" or OPENSTEER_PROVIDER=cloud.'
767
1148
  );
@@ -860,6 +1241,9 @@ function resolveCommandLength(tokens) {
860
1241
  if (tokens[0] === "status") {
861
1242
  return 1;
862
1243
  }
1244
+ if (tokens[0] === "record") {
1245
+ return 1;
1246
+ }
863
1247
  for (let length = Math.min(3, tokens.length); length >= 1; length -= 1) {
864
1248
  if (OPERATION_ALIASES.has(tokens.slice(0, length).join(" "))) {
865
1249
  return length;
@@ -877,6 +1261,7 @@ Usage:
877
1261
  opensteer click --workspace <id> (--element <n> | --selector <css> | --description <text>)
878
1262
  opensteer input --workspace <id> --text <value> (--element <n> | --selector <css> | --description <text>)
879
1263
  opensteer extract --workspace <id> --description <text> [--schema-json <json>]
1264
+ opensteer record --workspace <id> --url <url> [--output <path>]
880
1265
  opensteer close --workspace <id>
881
1266
  opensteer status [--workspace <id>] [--json]
882
1267
 
@@ -894,9 +1279,12 @@ Common options:
894
1279
  --help
895
1280
  --version
896
1281
  --workspace <id>
1282
+ --url <url>
1283
+ --output <path>
897
1284
  --provider local|cloud
898
1285
  --cloud-base-url <url>
899
1286
  --cloud-api-key <key>
1287
+ --cloud-app-base-url <url>
900
1288
  --cloud-profile-id <id>
901
1289
  --cloud-profile-reuse-if-active <true|false>
902
1290
  --json <true|false>