opensteer 0.8.8 → 0.8.10

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,77 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { assertProviderSupportsEngine, createOpensteerSemanticRuntime, OpensteerBrowserManager, dispatchSemanticOperation, pathExists, normalizeOpensteerProviderMode, discoverLocalCdpBrowsers, inspectCdpEndpoint, resolveOpensteerRuntimeConfig, resolveOpensteerEngineName, resolveOpensteerProvider, resolveFilesystemWorkspacePath, readPersistedLocalBrowserSessionRecord, readPersistedCloudSessionRecord, OpensteerCloudClient, isProcessRunning } from '../chunk-3QJGBVWT.js';
2
+ import { assertProviderSupportsEngine, createOpensteerSemanticRuntime, OpensteerBrowserManager, dispatchSemanticOperation, loadEnvironment, normalizeOpensteerProviderMode, discoverLocalCdpBrowsers, inspectCdpEndpoint, resolveOpensteerRuntimeConfig, resolveOpensteerEngineName, resolveOpensteerProvider, resolveFilesystemWorkspacePath, FlowRecorderCollector, generateReplayScript, pathExists, readPersistedLocalBrowserSessionRecord, readPersistedCloudSessionRecord, OpensteerCloudClient, isProcessRunning } from '../chunk-F3X6UOEN.js';
3
3
  import process2 from 'process';
4
- import { readFile } from 'fs/promises';
5
- import path2 from 'path';
6
4
  import { spawn } from 'child_process';
7
5
  import { existsSync } from 'fs';
6
+ import path from 'path';
8
7
  import { createRequire } from 'module';
9
8
  import { fileURLToPath } from 'url';
9
+ import { mkdir, writeFile } from 'fs/promises';
10
10
 
11
11
  // package.json
12
12
  var package_default = {
13
- version: "0.8.8"};
14
- var ENV_FILENAMES = [".env", ".env.local"];
13
+ version: "0.8.9"};
14
+
15
+ // src/cli/env-loader.ts
15
16
  async function loadCliEnvironment(cwd) {
16
- const protectedKeys = new Set(Object.keys(process.env));
17
- const directories = collectDirectories(cwd);
18
- for (const directory of directories) {
19
- for (const filename of ENV_FILENAMES) {
20
- const filePath = path2.join(directory, filename);
21
- if (!await pathExists(filePath)) {
22
- continue;
23
- }
24
- const parsed = parseEnvFile(await readFile(filePath, "utf8"));
25
- for (const [key, value] of Object.entries(parsed)) {
26
- if (protectedKeys.has(key)) {
27
- continue;
28
- }
29
- process.env[key] = value;
30
- }
31
- }
32
- }
33
- }
34
- function collectDirectories(cwd) {
35
- const directories = [];
36
- let current = path2.resolve(cwd);
37
- for (; ; ) {
38
- directories.unshift(current);
39
- const parent = path2.dirname(current);
40
- if (parent === current) {
41
- return directories;
42
- }
43
- current = parent;
44
- }
45
- }
46
- function parseEnvFile(contents) {
47
- const parsed = {};
48
- for (const rawLine of contents.split(/\r?\n/u)) {
49
- const trimmed = rawLine.trim();
50
- if (!trimmed || trimmed.startsWith("#")) {
51
- continue;
52
- }
53
- const line = trimmed.startsWith("export ") ? trimmed.slice("export ".length) : trimmed;
54
- const separatorIndex = line.indexOf("=");
55
- if (separatorIndex <= 0) {
56
- continue;
57
- }
58
- const key = line.slice(0, separatorIndex).trim();
59
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) {
60
- continue;
61
- }
62
- const rawValue = line.slice(separatorIndex + 1).trim();
63
- parsed[key] = parseEnvValue(rawValue);
64
- }
65
- return parsed;
66
- }
67
- function parseEnvValue(rawValue) {
68
- if (rawValue.length >= 2 && rawValue.startsWith('"') && rawValue.endsWith('"')) {
69
- return rawValue.slice(1, -1).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"');
70
- }
71
- if (rawValue.length >= 2 && rawValue.startsWith("'") && rawValue.endsWith("'")) {
72
- return rawValue.slice(1, -1);
73
- }
74
- return rawValue.replace(/\s+#.*$/u, "").trimEnd();
17
+ loadEnvironment(cwd);
75
18
  }
76
19
  function createOpensteerSkillsInvocation(input) {
77
20
  const cliArgs = ["add", input.skillSourcePath];
@@ -106,22 +49,22 @@ function createOpensteerSkillsInvocation(input) {
106
49
  function resolveOpensteerSkillsCliPath() {
107
50
  const require2 = createRequire(import.meta.url);
108
51
  const skillsPackagePath = require2.resolve("skills/package.json");
109
- const skillsPackageDir = path2.dirname(skillsPackagePath);
110
- const cliPath = path2.join(skillsPackageDir, "bin", "cli.mjs");
52
+ const skillsPackageDir = path.dirname(skillsPackagePath);
53
+ const cliPath = path.join(skillsPackageDir, "bin", "cli.mjs");
111
54
  if (!existsSync(cliPath)) {
112
55
  throw new Error(`skills CLI entrypoint was not found at "${cliPath}".`);
113
56
  }
114
57
  return cliPath;
115
58
  }
116
59
  function resolveOpensteerSkillSourcePath() {
117
- let ancestor = path2.dirname(fileURLToPath(import.meta.url));
60
+ let ancestor = path.dirname(fileURLToPath(import.meta.url));
118
61
  for (let index = 0; index < 6; index += 1) {
119
- const candidate = path2.join(ancestor, "skills");
120
- const skillManifest = path2.join(candidate, "opensteer", "SKILL.md");
62
+ const candidate = path.join(ancestor, "skills");
63
+ const skillManifest = path.join(candidate, "opensteer", "SKILL.md");
121
64
  if (existsSync(skillManifest)) {
122
65
  return candidate;
123
66
  }
124
- ancestor = path2.resolve(ancestor, "..");
67
+ ancestor = path.resolve(ancestor, "..");
125
68
  }
126
69
  throw new Error("Unable to find the packaged Opensteer skill source directory.");
127
70
  }
@@ -246,7 +189,7 @@ function describeLocalLane(record, current) {
246
189
  summary: "none"
247
190
  };
248
191
  }
249
- const browser = record.executablePath ? path2.basename(record.executablePath).replace(/\.[A-Za-z0-9]+$/u, "") : void 0;
192
+ const browser = record.executablePath ? path.basename(record.executablePath).replace(/\.[A-Za-z0-9]+$/u, "") : void 0;
250
193
  return {
251
194
  provider: "local",
252
195
  status: "active",
@@ -272,9 +215,11 @@ async function describeCloudLane(input) {
272
215
  status: "connected",
273
216
  current: input.current,
274
217
  summary: input.record.sessionId,
275
- detail: input.record.baseUrl,
276
218
  sessionId: input.record.sessionId,
277
- baseUrl: input.record.baseUrl
219
+ ...input.cloudConfig === void 0 ? {} : {
220
+ detail: input.cloudConfig.baseUrl,
221
+ baseUrl: input.cloudConfig.baseUrl
222
+ }
278
223
  };
279
224
  if (input.cloudConfig === void 0) {
280
225
  return base;
@@ -318,6 +263,121 @@ function formatLaneRow(input) {
318
263
  const summary = input.summary.padEnd(16, " ");
319
264
  return `${input.marker} ${provider} ${status} ${summary}${input.detail ?? ""}`.trimEnd();
320
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
+ function resolveRecordOutputPath(input) {
317
+ if (input.outputPath !== void 0) {
318
+ return path.resolve(input.rootDir, input.outputPath);
319
+ }
320
+ return path.join(
321
+ resolveFilesystemWorkspacePath({
322
+ rootDir: input.rootDir,
323
+ workspace: input.workspace
324
+ }),
325
+ "recorded-flow.ts"
326
+ );
327
+ }
328
+ function createRecorderRuntimeAdapter(runtime) {
329
+ return {
330
+ addInitScript: (input) => runtime.addInitScript(input),
331
+ evaluate: async (input) => {
332
+ const output = await runtime.evaluate({
333
+ script: input.script,
334
+ ...input.pageRef === void 0 ? {} : { pageRef: input.pageRef }
335
+ });
336
+ return output.value;
337
+ },
338
+ listPages: async () => {
339
+ const output = await runtime.listPages();
340
+ return {
341
+ pages: output.pages.map((page) => ({
342
+ pageRef: page.pageRef,
343
+ url: page.url,
344
+ ...page.openerPageRef === void 0 ? {} : { openerPageRef: page.openerPageRef }
345
+ }))
346
+ };
347
+ }
348
+ };
349
+ }
350
+ function formatRecordedAction(action) {
351
+ const time = new Date(action.timestamp).toISOString().slice(11, 19);
352
+ switch (action.kind) {
353
+ case "click":
354
+ return `[${time}] click ${action.pageId} -> ${action.selector ?? "<unknown>"}`;
355
+ case "dblclick":
356
+ return `[${time}] dblclick ${action.pageId} -> ${action.selector ?? "<unknown>"}`;
357
+ case "type":
358
+ return `[${time}] type ${action.pageId} -> ${action.selector ?? "<unknown>"} -> ${JSON.stringify(action.detail.text)}`;
359
+ case "keypress":
360
+ return `[${time}] keypress ${action.pageId} -> ${action.detail.key}`;
361
+ case "scroll":
362
+ return `[${time}] scroll ${action.pageId} -> (${String(action.detail.deltaX)}, ${String(action.detail.deltaY)})`;
363
+ case "select-option":
364
+ return `[${time}] select ${action.pageId} -> ${action.selector ?? "<unknown>"} -> ${JSON.stringify(action.detail.value)}`;
365
+ case "navigate":
366
+ return `[${time}] navigate ${action.pageId} -> ${action.detail.url}`;
367
+ case "new-tab":
368
+ return `[${time}] new-tab ${action.pageId} -> ${action.detail.initialUrl}`;
369
+ case "close-tab":
370
+ return `[${time}] close-tab ${action.pageId}`;
371
+ case "switch-tab":
372
+ return `[${time}] switch-tab -> ${action.detail.toPageId}`;
373
+ case "go-back":
374
+ return `[${time}] go-back ${action.pageId}`;
375
+ case "go-forward":
376
+ return `[${time}] go-forward ${action.pageId}`;
377
+ case "reload":
378
+ return `[${time}] reload ${action.pageId}`;
379
+ }
380
+ }
321
381
 
322
382
  // src/cli/bin.ts
323
383
  var OPERATION_ALIASES = /* @__PURE__ */ new Map([
@@ -409,6 +469,10 @@ async function main() {
409
469
  await handleStatusCommand(parsed);
410
470
  return;
411
471
  }
472
+ if (parsed.command[0] === "record") {
473
+ await handleRecordCommandEntry(parsed);
474
+ return;
475
+ }
412
476
  const operation = parsed.command[0] === "run" ? parsed.rest[0] : resolveOperation(parsed.command);
413
477
  if (!operation) {
414
478
  throw new Error(`Unknown command: ${parsed.command.join(" ")}`);
@@ -553,6 +617,83 @@ async function handleBrowserCommand(parsed) {
553
617
  throw new Error(`Unknown browser command: ${parsed.command.join(" ")}`);
554
618
  }
555
619
  }
620
+ async function handleRecordCommandEntry(parsed) {
621
+ if (parsed.options.workspace === void 0) {
622
+ throw new Error('record requires "--workspace <id>".');
623
+ }
624
+ const url = parsed.options.url ?? parsed.rest[0];
625
+ if (url === void 0) {
626
+ throw new Error('record requires "--url <value>" or a positional URL.');
627
+ }
628
+ const provider = resolveCliProvider(parsed);
629
+ assertCloudCliOptionsMatchProvider(parsed, provider.mode);
630
+ if (provider.mode !== "local") {
631
+ throw new Error(
632
+ 'record requires provider=local. Set "--provider local" or clear OPENSTEER_PROVIDER.'
633
+ );
634
+ }
635
+ const engineName = resolveCliEngineName(parsed);
636
+ if (engineName !== "playwright") {
637
+ throw new Error("record requires engine=playwright.");
638
+ }
639
+ if (parsed.options.browser !== void 0 && parsed.options.browser !== "persistent") {
640
+ throw new Error('record only supports "--browser persistent".');
641
+ }
642
+ if (parsed.options.launch?.headless === true) {
643
+ throw new Error('record requires a headed browser. Remove "--headless true".');
644
+ }
645
+ const rootDir = process2.cwd();
646
+ const launch = {
647
+ ...parsed.options.launch ?? {},
648
+ headless: false
649
+ };
650
+ const browserManager = new OpensteerBrowserManager({
651
+ rootDir,
652
+ workspace: parsed.options.workspace,
653
+ engineName,
654
+ browser: "persistent",
655
+ launch,
656
+ ...parsed.options.context === void 0 ? {} : { context: parsed.options.context }
657
+ });
658
+ const runtime = createOpensteerSemanticRuntime({
659
+ provider: {
660
+ mode: "local"
661
+ },
662
+ engine: engineName,
663
+ runtimeOptions: {
664
+ rootPath: browserManager.rootPath,
665
+ cleanupRootOnClose: browserManager.cleanupRootOnDisconnect,
666
+ workspace: parsed.options.workspace,
667
+ browser: "persistent",
668
+ launch,
669
+ ...parsed.options.context === void 0 ? {} : { context: parsed.options.context }
670
+ }
671
+ });
672
+ await runOpensteerRecordCommand({
673
+ runtime,
674
+ closeSession: () => closeOwnedLocalBrowserSession(runtime, browserManager),
675
+ workspace: parsed.options.workspace,
676
+ url,
677
+ rootDir,
678
+ ...parsed.options.output === void 0 ? {} : { outputPath: parsed.options.output }
679
+ });
680
+ }
681
+ async function closeOwnedLocalBrowserSession(runtime, browserManager) {
682
+ let closeError;
683
+ try {
684
+ await runtime.close();
685
+ } catch (error) {
686
+ closeError = error;
687
+ }
688
+ try {
689
+ await browserManager.close();
690
+ } catch (error) {
691
+ closeError ??= error;
692
+ }
693
+ if (closeError !== void 0) {
694
+ throw closeError;
695
+ }
696
+ }
556
697
  function buildOperationInput(operation, parsed) {
557
698
  if (parsed.options.inputJson !== void 0) {
558
699
  return parsed.options.inputJson;
@@ -633,6 +774,45 @@ function resolveOperation(command) {
633
774
  }
634
775
  return void 0;
635
776
  }
777
+ var CLI_OPTION_SPECS = {
778
+ workspace: { kind: "value" },
779
+ url: { kind: "value" },
780
+ output: { kind: "value" },
781
+ engine: { kind: "value" },
782
+ provider: { kind: "value" },
783
+ "cloud-base-url": { kind: "value" },
784
+ "cloud-api-key": { kind: "value" },
785
+ "cloud-profile-id": { kind: "value" },
786
+ "cloud-profile-reuse-if-active": { kind: "boolean" },
787
+ json: { kind: "boolean" },
788
+ agent: { kind: "value", multiple: true },
789
+ skill: { kind: "value", multiple: true },
790
+ global: { kind: "boolean" },
791
+ yes: { kind: "boolean" },
792
+ copy: { kind: "boolean" },
793
+ all: { kind: "boolean" },
794
+ list: { kind: "boolean" },
795
+ browser: { kind: "value" },
796
+ "attach-endpoint": { kind: "value" },
797
+ "attach-header": { kind: "value", multiple: true },
798
+ "fresh-tab": { kind: "boolean" },
799
+ headless: { kind: "boolean" },
800
+ "executable-path": { kind: "value" },
801
+ arg: { kind: "value", multiple: true },
802
+ "timeout-ms": { kind: "value" },
803
+ "context-json": { kind: "value" },
804
+ "input-json": { kind: "value" },
805
+ "schema-json": { kind: "value" },
806
+ "source-user-data-dir": { kind: "value" },
807
+ "source-profile-directory": { kind: "value" },
808
+ selector: { kind: "value" },
809
+ description: { kind: "value" },
810
+ element: { kind: "value" },
811
+ text: { kind: "value" },
812
+ "press-enter": { kind: "boolean" },
813
+ direction: { kind: "value" },
814
+ amount: { kind: "value" }
815
+ };
636
816
  function parseCommandLine(argv) {
637
817
  const leadingTokens = [];
638
818
  let index = 0;
@@ -646,18 +826,43 @@ function parseCommandLine(argv) {
646
826
  const rawOptions = /* @__PURE__ */ new Map();
647
827
  while (index < argv.length) {
648
828
  const token = argv[index];
829
+ if (token === "--") {
830
+ rest.push(...argv.slice(index + 1));
831
+ break;
832
+ }
649
833
  if (!token.startsWith("--")) {
650
834
  rest.push(token);
651
835
  index += 1;
652
836
  continue;
653
837
  }
654
- const key = token.slice(2);
655
- const next = argv[index + 1];
656
- if (next === void 0 || next.startsWith("--")) {
657
- rawOptions.set(key, [...rawOptions.get(key) ?? [], "true"]);
838
+ const separator = token.indexOf("=");
839
+ const key = token.slice(2, separator === -1 ? void 0 : separator);
840
+ const spec = CLI_OPTION_SPECS[key];
841
+ if (spec === void 0) {
842
+ throw new Error(`Unknown option: --${key}.`);
843
+ }
844
+ if (separator !== -1) {
845
+ const value = token.slice(separator + 1);
846
+ rawOptions.set(key, [...rawOptions.get(key) ?? [], value]);
658
847
  index += 1;
659
848
  continue;
660
849
  }
850
+ const next = argv[index + 1];
851
+ if (spec.kind === "boolean") {
852
+ if (next === void 0 || next.startsWith("--")) {
853
+ rawOptions.set(key, [...rawOptions.get(key) ?? [], "true"]);
854
+ index += 1;
855
+ continue;
856
+ }
857
+ rawOptions.set(key, [...rawOptions.get(key) ?? [], next]);
858
+ index += 2;
859
+ continue;
860
+ }
861
+ if (next === void 0 || next.startsWith("--")) {
862
+ throw new Error(
863
+ `Option "--${key}" requires a value.${next?.startsWith("--") === true ? ` Use "--${key}=<value>" when the value begins with "--".` : ``}`
864
+ );
865
+ }
661
866
  rawOptions.set(key, [...rawOptions.get(key) ?? [], next]);
662
867
  index += 2;
663
868
  }
@@ -688,6 +893,8 @@ function parseCommandLine(argv) {
688
893
  ...timeoutMs === void 0 ? {} : { timeoutMs }
689
894
  };
690
895
  const workspace = readSingle(rawOptions, "workspace");
896
+ const url = readSingle(rawOptions, "url");
897
+ const output = readSingle(rawOptions, "output");
691
898
  const sourceUserDataDir = readSingle(rawOptions, "source-user-data-dir");
692
899
  const sourceProfileDirectory = readSingle(rawOptions, "source-profile-directory");
693
900
  const selector = readSingle(rawOptions, "selector");
@@ -719,6 +926,8 @@ function parseCommandLine(argv) {
719
926
  const list = readOptionalBoolean(rawOptions, "list");
720
927
  const options = {
721
928
  ...workspace === void 0 ? {} : { workspace },
929
+ ...url === void 0 ? {} : { url },
930
+ ...output === void 0 ? {} : { output },
722
931
  ...requestedEngineName === void 0 ? {} : { requestedEngineName },
723
932
  ...provider === void 0 ? {} : { provider },
724
933
  ...cloudBaseUrl === void 0 ? {} : { cloudBaseUrl },
@@ -831,7 +1040,7 @@ async function handleStatusCommand(parsed) {
831
1040
  const runtimeProvider = buildCliRuntimeProvider(parsed, provider.mode);
832
1041
  const runtimeConfig = resolveOpensteerRuntimeConfig({
833
1042
  ...runtimeProvider === void 0 ? {} : { provider: runtimeProvider },
834
- ...process2.env.OPENSTEER_PROVIDER === void 0 ? {} : { environmentProvider: process2.env.OPENSTEER_PROVIDER }
1043
+ environment: process2.env
835
1044
  });
836
1045
  const status = await collectOpensteerStatus({
837
1046
  rootDir: process2.cwd(),
@@ -918,6 +1127,9 @@ function resolveCommandLength(tokens) {
918
1127
  if (tokens[0] === "status") {
919
1128
  return 1;
920
1129
  }
1130
+ if (tokens[0] === "record") {
1131
+ return 1;
1132
+ }
921
1133
  for (let length = Math.min(3, tokens.length); length >= 1; length -= 1) {
922
1134
  if (OPERATION_ALIASES.has(tokens.slice(0, length).join(" "))) {
923
1135
  return length;
@@ -935,6 +1147,7 @@ Usage:
935
1147
  opensteer click --workspace <id> (--element <n> | --selector <css> | --description <text>)
936
1148
  opensteer input --workspace <id> --text <value> (--element <n> | --selector <css> | --description <text>)
937
1149
  opensteer extract --workspace <id> --description <text> [--schema-json <json>]
1150
+ opensteer record --workspace <id> --url <url> [--output <path>]
938
1151
  opensteer close --workspace <id>
939
1152
  opensteer status [--workspace <id>] [--json]
940
1153
 
@@ -952,6 +1165,8 @@ Common options:
952
1165
  --help
953
1166
  --version
954
1167
  --workspace <id>
1168
+ --url <url>
1169
+ --output <path>
955
1170
  --provider local|cloud
956
1171
  --cloud-base-url <url>
957
1172
  --cloud-api-key <key>