@optima-chat/browser-cli 0.1.5 → 0.2.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 +478 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,29 +1,82 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/config.ts
|
|
7
8
|
import { readFileSync, existsSync } from "fs";
|
|
8
9
|
import { join } from "path";
|
|
9
10
|
import { homedir } from "os";
|
|
10
|
-
var
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
var ENV_CONFIG = {
|
|
12
|
+
production: {
|
|
13
|
+
apiUrl: "https://browser-api.optima.onl"
|
|
14
|
+
},
|
|
15
|
+
stage: {
|
|
16
|
+
apiUrl: "https://browser-api.stage.optima.onl"
|
|
17
|
+
},
|
|
18
|
+
development: {
|
|
19
|
+
apiUrl: "http://localhost:7292"
|
|
20
|
+
}
|
|
15
21
|
};
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
var OPTIMA_TOKEN_FILE = join(homedir(), ".optima", "token.json");
|
|
23
|
+
function tokenEnvToBrowserEnv(tokenEnv) {
|
|
24
|
+
switch (tokenEnv) {
|
|
25
|
+
case "prod":
|
|
26
|
+
case "production":
|
|
27
|
+
return "production";
|
|
28
|
+
case "stage":
|
|
29
|
+
return "stage";
|
|
30
|
+
case "ci":
|
|
31
|
+
case "development":
|
|
32
|
+
return "development";
|
|
33
|
+
default:
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function readOptimaToken() {
|
|
38
|
+
if (!existsSync(OPTIMA_TOKEN_FILE)) return null;
|
|
18
39
|
try {
|
|
19
|
-
return JSON.parse(readFileSync(
|
|
40
|
+
return JSON.parse(readFileSync(OPTIMA_TOKEN_FILE, "utf-8"));
|
|
20
41
|
} catch {
|
|
21
42
|
return null;
|
|
22
43
|
}
|
|
23
44
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
45
|
+
function getCurrentEnv() {
|
|
46
|
+
const browserEnv = process.env.BROWSER_ENV;
|
|
47
|
+
if (browserEnv && ["production", "stage", "development"].includes(browserEnv)) {
|
|
48
|
+
return browserEnv;
|
|
49
|
+
}
|
|
50
|
+
const optimaEnv = process.env.OPTIMA_ENV;
|
|
51
|
+
if (optimaEnv && ["production", "stage", "development"].includes(optimaEnv)) {
|
|
52
|
+
return optimaEnv;
|
|
53
|
+
}
|
|
54
|
+
const optimaToken = readOptimaToken();
|
|
55
|
+
if (optimaToken?.env) {
|
|
56
|
+
const mapped = tokenEnvToBrowserEnv(optimaToken.env);
|
|
57
|
+
if (mapped) return mapped;
|
|
58
|
+
}
|
|
59
|
+
return "development";
|
|
60
|
+
}
|
|
61
|
+
function loadConfig() {
|
|
62
|
+
const env = getCurrentEnv();
|
|
63
|
+
const envConfig = ENV_CONFIG[env];
|
|
64
|
+
let token = process.env.BROWSER_TOKEN || null;
|
|
65
|
+
if (!token) {
|
|
66
|
+
const optimaToken = readOptimaToken();
|
|
67
|
+
if (optimaToken?.access_token) {
|
|
68
|
+
token = optimaToken.access_token;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
apiUrl: process.env.BROWSER_API_URL || envConfig.apiUrl,
|
|
73
|
+
token,
|
|
74
|
+
env
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
var config = loadConfig();
|
|
78
|
+
var BROWSER_API_URL = config.apiUrl;
|
|
79
|
+
var ACCESS_TOKEN = config.token;
|
|
27
80
|
|
|
28
81
|
// src/api.ts
|
|
29
82
|
var BrowserAPIError = class extends Error {
|
|
@@ -62,7 +115,11 @@ async function request(path, options = {}) {
|
|
|
62
115
|
`HTTP ${response.status}: ${response.statusText}`
|
|
63
116
|
);
|
|
64
117
|
}
|
|
65
|
-
|
|
118
|
+
const detail = body.detail || body;
|
|
119
|
+
throw new BrowserAPIError(response.status, detail.error, detail.message);
|
|
120
|
+
}
|
|
121
|
+
if (response.status === 204) {
|
|
122
|
+
return void 0;
|
|
66
123
|
}
|
|
67
124
|
return await response.json();
|
|
68
125
|
}
|
|
@@ -91,7 +148,8 @@ async function requestBinary(path) {
|
|
|
91
148
|
`HTTP ${response.status}: ${response.statusText}`
|
|
92
149
|
);
|
|
93
150
|
}
|
|
94
|
-
|
|
151
|
+
const detail = body.detail || body;
|
|
152
|
+
throw new BrowserAPIError(response.status, detail.error, detail.message);
|
|
95
153
|
}
|
|
96
154
|
const arrayBuffer = await response.arrayBuffer();
|
|
97
155
|
return Buffer.from(arrayBuffer);
|
|
@@ -191,6 +249,50 @@ var BrowserAPI = class {
|
|
|
191
249
|
{ method: "DELETE" }
|
|
192
250
|
);
|
|
193
251
|
}
|
|
252
|
+
// Workflow methods
|
|
253
|
+
async saveWorkflow(sessionId, name, description) {
|
|
254
|
+
return request("/api/workflows", {
|
|
255
|
+
method: "POST",
|
|
256
|
+
body: JSON.stringify({ session_id: sessionId, name, description })
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async listWorkflows(name) {
|
|
260
|
+
const query = name ? `?name=${encodeURIComponent(name)}` : "";
|
|
261
|
+
return request(`/api/workflows${query}`);
|
|
262
|
+
}
|
|
263
|
+
async getWorkflow(workflowId) {
|
|
264
|
+
return request(`/api/workflows/${workflowId}`);
|
|
265
|
+
}
|
|
266
|
+
async updateWorkflow(workflowId, updates) {
|
|
267
|
+
return request(`/api/workflows/${workflowId}`, {
|
|
268
|
+
method: "PUT",
|
|
269
|
+
body: JSON.stringify(updates)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async deleteWorkflow(workflowId) {
|
|
273
|
+
await request(`/api/workflows/${workflowId}`, { method: "DELETE" });
|
|
274
|
+
}
|
|
275
|
+
async runWorkflow(workflowId, params = {}, force) {
|
|
276
|
+
return request(`/api/workflows/${workflowId}/run`, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
body: JSON.stringify({ params, force: !!force })
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
async getTaskStatus(taskId) {
|
|
282
|
+
return request(`/api/workflow-tasks/${taskId}`);
|
|
283
|
+
}
|
|
284
|
+
async provideInput(taskId, values, confirm) {
|
|
285
|
+
return request(`/api/workflow-tasks/${taskId}/input`, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
body: JSON.stringify({ values: values ?? null, confirm: confirm ?? null })
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
async cancelTask(taskId) {
|
|
291
|
+
return request(`/api/workflow-tasks/${taskId}/cancel`, {
|
|
292
|
+
method: "POST",
|
|
293
|
+
body: JSON.stringify({})
|
|
294
|
+
});
|
|
295
|
+
}
|
|
194
296
|
};
|
|
195
297
|
var api = new BrowserAPI();
|
|
196
298
|
|
|
@@ -200,11 +302,18 @@ import { join as join2 } from "path";
|
|
|
200
302
|
import { homedir as homedir2 } from "os";
|
|
201
303
|
var OPTIMA_DIR = join2(homedir2(), ".optima");
|
|
202
304
|
var SESSION_FILE = join2(OPTIMA_DIR, "browser-session-id");
|
|
305
|
+
var _sessionOverride = null;
|
|
306
|
+
function setSessionOverride(id) {
|
|
307
|
+
_sessionOverride = id;
|
|
308
|
+
}
|
|
203
309
|
function saveSessionId(id) {
|
|
204
310
|
mkdirSync(OPTIMA_DIR, { recursive: true });
|
|
205
311
|
writeFileSync(SESSION_FILE, id, "utf-8");
|
|
206
312
|
}
|
|
207
313
|
function loadSessionId() {
|
|
314
|
+
if (_sessionOverride) {
|
|
315
|
+
return _sessionOverride;
|
|
316
|
+
}
|
|
208
317
|
if (!existsSync2(SESSION_FILE)) {
|
|
209
318
|
throw new Error(
|
|
210
319
|
"No active browser session. Run 'browser-cli launch' first."
|
|
@@ -286,6 +395,67 @@ function formatError(err) {
|
|
|
286
395
|
function formatTabsInline(tabs) {
|
|
287
396
|
return tabs.map((t) => `[${t.tab_id}] ${t.title}`).join(", ");
|
|
288
397
|
}
|
|
398
|
+
function formatWorkflowSaved(workflow) {
|
|
399
|
+
return `\u2713 Workflow '${workflow.name}' saved (id: ${workflow.id}, ${workflow.steps.length} steps)`;
|
|
400
|
+
}
|
|
401
|
+
function formatWorkflowList(workflows) {
|
|
402
|
+
if (workflows.length === 0) {
|
|
403
|
+
return "No workflows found.";
|
|
404
|
+
}
|
|
405
|
+
const lines = ["Workflows:", ""];
|
|
406
|
+
for (const wf of workflows) {
|
|
407
|
+
const desc = wf.description ? ` \u2014 ${wf.description}` : "";
|
|
408
|
+
lines.push(` ${wf.name} (${wf.step_count} steps)${desc}`);
|
|
409
|
+
}
|
|
410
|
+
return lines.join("\n");
|
|
411
|
+
}
|
|
412
|
+
function formatWorkflowInfo(workflow) {
|
|
413
|
+
const lines = [];
|
|
414
|
+
lines.push(`Workflow: ${workflow.name}`);
|
|
415
|
+
lines.push(`ID: ${workflow.id}`);
|
|
416
|
+
if (workflow.description) lines.push(`Description: ${workflow.description}`);
|
|
417
|
+
lines.push(`Steps: ${workflow.steps.length}`);
|
|
418
|
+
lines.push(`Created: ${workflow.created_at}`);
|
|
419
|
+
lines.push("");
|
|
420
|
+
if (workflow.input_schema.length > 0) {
|
|
421
|
+
lines.push("Input variables:");
|
|
422
|
+
for (const v of workflow.input_schema) {
|
|
423
|
+
const req = v.required ? " (required)" : "";
|
|
424
|
+
lines.push(` ${v.name}: ${v.type}${req}`);
|
|
425
|
+
}
|
|
426
|
+
lines.push("");
|
|
427
|
+
}
|
|
428
|
+
lines.push("Steps:");
|
|
429
|
+
for (let i = 0; i < workflow.steps.length; i++) {
|
|
430
|
+
const s = workflow.steps[i];
|
|
431
|
+
lines.push(` ${i + 1}. [${s.step_type}] ${s.description}`);
|
|
432
|
+
}
|
|
433
|
+
return lines.join("\n");
|
|
434
|
+
}
|
|
435
|
+
function formatWorkflowTask(task) {
|
|
436
|
+
const lines = [];
|
|
437
|
+
const name = task.workflow_name ? ` (${task.workflow_name})` : "";
|
|
438
|
+
lines.push(`Task: ${task.id}${name}`);
|
|
439
|
+
lines.push(`Status: ${task.status}`);
|
|
440
|
+
lines.push(`Progress: ${task.current_step}/${task.total_steps}`);
|
|
441
|
+
if (task.session_id) lines.push(`Session: ${task.session_id}`);
|
|
442
|
+
if (task.missing_vars && task.missing_vars.length > 0) {
|
|
443
|
+
lines.push(`Missing: ${task.missing_vars.join(", ")}`);
|
|
444
|
+
}
|
|
445
|
+
if (task.pending_confirmation) {
|
|
446
|
+
lines.push(`Confirmation: ${task.pending_confirmation}`);
|
|
447
|
+
}
|
|
448
|
+
if (task.error) lines.push(`Error: ${task.error}`);
|
|
449
|
+
if (task.output) {
|
|
450
|
+
const entries = Object.entries(task.output).filter(([k]) => !k.startsWith("_")).map(([k, v]) => `${k}: ${v}`).join(", ");
|
|
451
|
+
if (entries) lines.push(`Output: ${entries}`);
|
|
452
|
+
}
|
|
453
|
+
if (task.completed_at) lines.push(`Completed: ${task.completed_at}`);
|
|
454
|
+
return lines.join("\n");
|
|
455
|
+
}
|
|
456
|
+
function formatWorkflowDeleted(name) {
|
|
457
|
+
return `\u2713 Workflow '${name}' deleted`;
|
|
458
|
+
}
|
|
289
459
|
|
|
290
460
|
// src/commands/lifecycle.ts
|
|
291
461
|
function registerLifecycleCommands(program2) {
|
|
@@ -506,12 +676,306 @@ function registerTabCommands(program2) {
|
|
|
506
676
|
});
|
|
507
677
|
}
|
|
508
678
|
|
|
679
|
+
// src/commands/workflow.ts
|
|
680
|
+
function registerWorkflowCommands(program2) {
|
|
681
|
+
program2.command("save-workflow").argument("<name>", "Workflow name").description("Save current session actions as a workflow").option("--description <desc>", "Workflow description").action(async (name, opts) => {
|
|
682
|
+
try {
|
|
683
|
+
const sessionId = loadSessionId();
|
|
684
|
+
const workflow = await api.saveWorkflow(sessionId, name, opts.description);
|
|
685
|
+
console.log(formatWorkflowSaved(workflow));
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.error(formatError(err));
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
program2.command("list-workflows").description("List saved workflows").action(async () => {
|
|
692
|
+
try {
|
|
693
|
+
const workflows = await api.listWorkflows();
|
|
694
|
+
console.log(formatWorkflowList(workflows));
|
|
695
|
+
} catch (err) {
|
|
696
|
+
console.error(formatError(err));
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
program2.command("workflow-info").argument("<name>", "Workflow name").description("Show workflow details").action(async (name) => {
|
|
701
|
+
try {
|
|
702
|
+
const workflows = await api.listWorkflows(name);
|
|
703
|
+
if (workflows.length === 0) {
|
|
704
|
+
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
const workflow = await api.getWorkflow(workflows[0].id);
|
|
708
|
+
console.log(formatWorkflowInfo(workflow));
|
|
709
|
+
} catch (err) {
|
|
710
|
+
console.error(formatError(err));
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
program2.command("run-workflow").argument("<name>", "Workflow name").description("Execute a saved workflow").option("--param <params...>", "Parameters as key=value").option("--force", "Force run even if another task is active").action(async (name, opts) => {
|
|
715
|
+
try {
|
|
716
|
+
const workflows = await api.listWorkflows(name);
|
|
717
|
+
if (workflows.length === 0) {
|
|
718
|
+
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
const params = {};
|
|
722
|
+
if (opts.param) {
|
|
723
|
+
for (const p of opts.param) {
|
|
724
|
+
const [k, ...rest] = p.split("=");
|
|
725
|
+
params[k] = rest.join("=");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const task = await api.runWorkflow(workflows[0].id, params, opts.force);
|
|
729
|
+
console.log(formatWorkflowTask(task));
|
|
730
|
+
} catch (err) {
|
|
731
|
+
console.error(formatError(err));
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
program2.command("workflow-status").argument("<task_id>", "Task ID").description("Check workflow task status").action(async (taskId) => {
|
|
736
|
+
try {
|
|
737
|
+
const task = await api.getTaskStatus(taskId);
|
|
738
|
+
console.log(formatWorkflowTask(task));
|
|
739
|
+
} catch (err) {
|
|
740
|
+
console.error(formatError(err));
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
program2.command("workflow-cancel").argument("<task_id>", "Task ID").description("Cancel a running workflow task").action(async (taskId) => {
|
|
745
|
+
try {
|
|
746
|
+
const task = await api.cancelTask(taskId);
|
|
747
|
+
console.log(formatWorkflowTask(task));
|
|
748
|
+
} catch (err) {
|
|
749
|
+
console.error(formatError(err));
|
|
750
|
+
process.exit(1);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
program2.command("workflow-input").argument("<task_id>", "Task ID").description("Provide input or confirmation to a waiting workflow task").option("--value <value>", "Provide a single value (only when one variable is missing)").option("--var <vars...>", "Provide named variables as key=value").option("--confirm", "Confirm a sensitive operation").action(async (taskId, opts) => {
|
|
754
|
+
try {
|
|
755
|
+
const values = {};
|
|
756
|
+
if (opts.value) {
|
|
757
|
+
const task = await api.getTaskStatus(taskId);
|
|
758
|
+
if (!task.missing_vars || task.missing_vars.length !== 1) {
|
|
759
|
+
console.error(formatError(new Error("--value can only be used when exactly one variable is missing. Use --var key=value instead.")));
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
762
|
+
values[task.missing_vars[0]] = opts.value;
|
|
763
|
+
}
|
|
764
|
+
if (opts.var) {
|
|
765
|
+
for (const v of opts.var) {
|
|
766
|
+
const [k, ...rest] = v.split("=");
|
|
767
|
+
values[k] = rest.join("=");
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const hasValues = Object.keys(values).length > 0;
|
|
771
|
+
const result = await api.provideInput(taskId, hasValues ? values : void 0, opts.confirm);
|
|
772
|
+
console.log(formatWorkflowTask(result));
|
|
773
|
+
} catch (err) {
|
|
774
|
+
console.error(formatError(err));
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
program2.command("delete-workflow").argument("<name>", "Workflow name").description("Delete a saved workflow").action(async (name) => {
|
|
779
|
+
try {
|
|
780
|
+
const workflows = await api.listWorkflows(name);
|
|
781
|
+
if (workflows.length === 0) {
|
|
782
|
+
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
783
|
+
process.exit(1);
|
|
784
|
+
}
|
|
785
|
+
await api.deleteWorkflow(workflows[0].id);
|
|
786
|
+
console.log(formatWorkflowDeleted(name));
|
|
787
|
+
} catch (err) {
|
|
788
|
+
console.error(formatError(err));
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
program2.command("edit-workflow").argument("<name>", "Workflow name").description("Edit a workflow").option("--set-var", "Set a step value as variable").option("--step <n>", "Step number (1-based)", parseInt).option("--name <varName>", "Variable name").option("--add-input <varName>", "Add an input variable").option("--type <type>", "Variable type", "string").option("--required", "Mark variable as required").option("--add-step <type>", "Add a step (extract, wait_for_element)").option("--selectors <json>", "CSS selectors for extract step").option("--screenshot", "Include screenshot in extract step").option("--selector <css>", "CSS selector for wait_for_element step").option("--timeout <seconds>", "Timeout for wait_for_element step", parseInt).option("--remove-step <n>", "Remove step N (1-based)", parseInt).option("--update-step <n>", "Update step N (1-based)", parseInt).option("--requires-confirmation", "Mark step as requiring confirmation").option("--no-requires-confirmation", "Unmark step confirmation requirement").option("--target-text <text>", "Update step target text").option("--auto-clean", "Auto-clean redundant steps").action(async (name, opts) => {
|
|
793
|
+
try {
|
|
794
|
+
const workflows = await api.listWorkflows(name);
|
|
795
|
+
if (workflows.length === 0) {
|
|
796
|
+
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
const workflow = await api.getWorkflow(workflows[0].id);
|
|
800
|
+
if (opts.setVar && opts.step && opts.name) {
|
|
801
|
+
const stepIdx = opts.step - 1;
|
|
802
|
+
if (stepIdx < 0 || stepIdx >= workflow.steps.length) {
|
|
803
|
+
console.error(formatError(new Error(`Step ${opts.step} out of range`)));
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
const step = workflow.steps[stepIdx];
|
|
807
|
+
if (step.step_type === "navigate" && step.url) {
|
|
808
|
+
step.url = `{${opts.name}}`;
|
|
809
|
+
} else if (step.value !== void 0 && step.value !== null) {
|
|
810
|
+
step.value = `{${opts.name}}`;
|
|
811
|
+
} else {
|
|
812
|
+
console.error(formatError(new Error(`Step ${opts.step} ([${step.step_type}]) has no value or url to parameterize`)));
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
816
|
+
console.log(`\u2713 Step ${opts.step} value set to {${opts.name}}`);
|
|
817
|
+
} else if (opts.addInput) {
|
|
818
|
+
const inputVar = {
|
|
819
|
+
name: opts.addInput,
|
|
820
|
+
type: opts.type || "string",
|
|
821
|
+
description: "",
|
|
822
|
+
required: !!opts.required,
|
|
823
|
+
default: null,
|
|
824
|
+
sensitive: false
|
|
825
|
+
};
|
|
826
|
+
const schema = workflow.input_schema || [];
|
|
827
|
+
schema.push(inputVar);
|
|
828
|
+
await api.updateWorkflow(workflow.id, { input_schema: schema });
|
|
829
|
+
console.log(`\u2713 Input variable '${opts.addInput}' added`);
|
|
830
|
+
} else if (opts.addStep === "extract") {
|
|
831
|
+
const extractStep = {
|
|
832
|
+
step_type: "extract",
|
|
833
|
+
description: "Extract data",
|
|
834
|
+
extract_selectors: opts.selectors ? JSON.parse(opts.selectors) : {},
|
|
835
|
+
extract_screenshot: !!opts.screenshot
|
|
836
|
+
};
|
|
837
|
+
workflow.steps.push(extractStep);
|
|
838
|
+
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
839
|
+
console.log(`\u2713 Extract step added`);
|
|
840
|
+
} else if (opts.addStep === "wait_for_element") {
|
|
841
|
+
const waitStep = {
|
|
842
|
+
step_type: "wait_for_element",
|
|
843
|
+
description: `Wait for ${opts.selector || "element"}`,
|
|
844
|
+
wait_selector: opts.selector || "",
|
|
845
|
+
wait_timeout: opts.timeout || 30
|
|
846
|
+
};
|
|
847
|
+
workflow.steps.push(waitStep);
|
|
848
|
+
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
849
|
+
console.log(`\u2713 wait_for_element step added`);
|
|
850
|
+
} else if (opts.removeStep) {
|
|
851
|
+
const stepIdx = opts.removeStep - 1;
|
|
852
|
+
if (stepIdx < 0 || stepIdx >= workflow.steps.length) {
|
|
853
|
+
console.error(formatError(new Error(`Step ${opts.removeStep} out of range`)));
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
const removed = workflow.steps.splice(stepIdx, 1)[0];
|
|
857
|
+
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
858
|
+
console.log(`\u2713 Step ${opts.removeStep} removed ([${removed.step_type}] ${removed.description})`);
|
|
859
|
+
} else if (opts.updateStep) {
|
|
860
|
+
const stepIdx = opts.updateStep - 1;
|
|
861
|
+
if (stepIdx < 0 || stepIdx >= workflow.steps.length) {
|
|
862
|
+
console.error(formatError(new Error(`Step ${opts.updateStep} out of range`)));
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}
|
|
865
|
+
const step = workflow.steps[stepIdx];
|
|
866
|
+
let changed = false;
|
|
867
|
+
if (opts.requiresConfirmation === true) {
|
|
868
|
+
step.requires_confirmation = true;
|
|
869
|
+
changed = true;
|
|
870
|
+
} else if (opts.requiresConfirmation === false) {
|
|
871
|
+
step.requires_confirmation = false;
|
|
872
|
+
changed = true;
|
|
873
|
+
}
|
|
874
|
+
if (opts.targetText) {
|
|
875
|
+
step.target_text = opts.targetText;
|
|
876
|
+
changed = true;
|
|
877
|
+
}
|
|
878
|
+
if (!changed) {
|
|
879
|
+
console.log("Specify what to update: --requires-confirmation, --target-text <text>");
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
883
|
+
console.log(`\u2713 Step ${opts.updateStep} updated`);
|
|
884
|
+
} else if (opts.autoClean) {
|
|
885
|
+
const before = workflow.steps.length;
|
|
886
|
+
const cleaned = autoCleanSteps(workflow.steps);
|
|
887
|
+
await api.updateWorkflow(workflow.id, { steps: cleaned });
|
|
888
|
+
const removed = before - cleaned.length;
|
|
889
|
+
console.log(`\u2713 Auto-clean: ${removed} step(s) removed (${before} \u2192 ${cleaned.length})`);
|
|
890
|
+
} else {
|
|
891
|
+
console.log("Usage:");
|
|
892
|
+
console.log(" edit-workflow <name> --set-var --step N --name varName");
|
|
893
|
+
console.log(" edit-workflow <name> --add-input varName --type T [--required]");
|
|
894
|
+
console.log(" edit-workflow <name> --add-step extract --selectors '{...}' [--screenshot]");
|
|
895
|
+
console.log(" edit-workflow <name> --add-step wait_for_element --selector '.css' [--timeout 30]");
|
|
896
|
+
console.log(" edit-workflow <name> --remove-step N");
|
|
897
|
+
console.log(" edit-workflow <name> --update-step N --requires-confirmation");
|
|
898
|
+
console.log(" edit-workflow <name> --update-step N --target-text 'new text'");
|
|
899
|
+
console.log(" edit-workflow <name> --auto-clean");
|
|
900
|
+
}
|
|
901
|
+
} catch (err) {
|
|
902
|
+
console.error(formatError(err));
|
|
903
|
+
process.exit(1);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
function autoCleanSteps(steps) {
|
|
908
|
+
let result = [...steps];
|
|
909
|
+
result = cleanNavigateBackNavigate(result);
|
|
910
|
+
result = cleanConsecutiveInput(result);
|
|
911
|
+
result = cleanConsecutiveScroll(result);
|
|
912
|
+
result = cleanConsecutiveClick(result);
|
|
913
|
+
return result;
|
|
914
|
+
}
|
|
915
|
+
function cleanNavigateBackNavigate(steps) {
|
|
916
|
+
const result = [];
|
|
917
|
+
let i = 0;
|
|
918
|
+
while (i < steps.length) {
|
|
919
|
+
if (i + 2 < steps.length && steps[i].step_type === "navigate" && steps[i + 1].step_type === "go_back" && steps[i + 2].step_type === "navigate") {
|
|
920
|
+
i += 2;
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
result.push(steps[i]);
|
|
924
|
+
i++;
|
|
925
|
+
}
|
|
926
|
+
return result;
|
|
927
|
+
}
|
|
928
|
+
function sameElement(a, b) {
|
|
929
|
+
return !!a.target_text && a.target_text === b.target_text && a.element_tag === b.element_tag;
|
|
930
|
+
}
|
|
931
|
+
function cleanConsecutiveInput(steps) {
|
|
932
|
+
const result = [];
|
|
933
|
+
for (let i = 0; i < steps.length; i++) {
|
|
934
|
+
if (steps[i].step_type === "input" && i + 1 < steps.length && steps[i + 1].step_type === "input" && sameElement(steps[i], steps[i + 1])) {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
result.push(steps[i]);
|
|
938
|
+
}
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
941
|
+
function cleanConsecutiveScroll(steps) {
|
|
942
|
+
const result = [];
|
|
943
|
+
for (let i = 0; i < steps.length; i++) {
|
|
944
|
+
if (steps[i].step_type === "scroll" && i + 1 < steps.length && steps[i + 1].step_type === "scroll" && steps[i].scroll_down === steps[i + 1].scroll_down) {
|
|
945
|
+
const amount = (steps[i].scroll_amount || 1) + (steps[i + 1].scroll_amount || 1);
|
|
946
|
+
steps[i + 1] = { ...steps[i + 1], scroll_amount: amount };
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
result.push(steps[i]);
|
|
950
|
+
}
|
|
951
|
+
return result;
|
|
952
|
+
}
|
|
953
|
+
function cleanConsecutiveClick(steps) {
|
|
954
|
+
const result = [];
|
|
955
|
+
for (let i = 0; i < steps.length; i++) {
|
|
956
|
+
if (steps[i].step_type === "click" && i + 1 < steps.length && steps[i + 1].step_type === "click" && sameElement(steps[i], steps[i + 1])) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
result.push(steps[i]);
|
|
960
|
+
}
|
|
961
|
+
return result;
|
|
962
|
+
}
|
|
963
|
+
|
|
509
964
|
// src/index.ts
|
|
965
|
+
var require2 = createRequire(import.meta.url);
|
|
966
|
+
var { version } = require2("../package.json");
|
|
510
967
|
var program = new Command();
|
|
511
|
-
program.name("browser-cli").description("CLI client for browser-backend").version(
|
|
968
|
+
program.name("browser-cli").description("CLI client for browser-backend").version(version).option("--session <id>", "Override session ID");
|
|
969
|
+
program.hook("preAction", () => {
|
|
970
|
+
const opts = program.opts();
|
|
971
|
+
if (opts.session) {
|
|
972
|
+
setSessionOverride(opts.session);
|
|
973
|
+
}
|
|
974
|
+
});
|
|
512
975
|
registerLifecycleCommands(program);
|
|
513
976
|
registerNavigationCommands(program);
|
|
514
977
|
registerInteractionCommands(program);
|
|
515
978
|
registerInfoCommands(program);
|
|
516
979
|
registerTabCommands(program);
|
|
980
|
+
registerWorkflowCommands(program);
|
|
517
981
|
program.parse();
|