@optima-chat/browser-cli 0.1.6 → 0.3.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 +606 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -118,6 +118,9 @@ async function request(path, options = {}) {
|
|
|
118
118
|
const detail = body.detail || body;
|
|
119
119
|
throw new BrowserAPIError(response.status, detail.error, detail.message);
|
|
120
120
|
}
|
|
121
|
+
if (response.status === 204) {
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
121
124
|
return await response.json();
|
|
122
125
|
}
|
|
123
126
|
async function requestBinary(path) {
|
|
@@ -246,6 +249,87 @@ var BrowserAPI = class {
|
|
|
246
249
|
{ method: "DELETE" }
|
|
247
250
|
);
|
|
248
251
|
}
|
|
252
|
+
async uploadFiles(sessionId, index, filePaths) {
|
|
253
|
+
return request(`/api/sessions/${sessionId}/upload`, {
|
|
254
|
+
method: "POST",
|
|
255
|
+
body: JSON.stringify({ index, file_paths: filePaths })
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// Workflow methods
|
|
259
|
+
async saveWorkflow(sessionId, name, description) {
|
|
260
|
+
return request("/api/workflows", {
|
|
261
|
+
method: "POST",
|
|
262
|
+
body: JSON.stringify({ session_id: sessionId, name, description })
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async listWorkflows(name) {
|
|
266
|
+
const query = name ? `?name=${encodeURIComponent(name)}` : "";
|
|
267
|
+
return request(`/api/workflows${query}`);
|
|
268
|
+
}
|
|
269
|
+
async getWorkflow(workflowId) {
|
|
270
|
+
return request(`/api/workflows/${workflowId}`);
|
|
271
|
+
}
|
|
272
|
+
async updateWorkflow(workflowId, updates) {
|
|
273
|
+
return request(`/api/workflows/${workflowId}`, {
|
|
274
|
+
method: "PUT",
|
|
275
|
+
body: JSON.stringify(updates)
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
async deleteWorkflow(workflowId) {
|
|
279
|
+
await request(`/api/workflows/${workflowId}`, { method: "DELETE" });
|
|
280
|
+
}
|
|
281
|
+
async runWorkflow(workflowId, params = {}, force) {
|
|
282
|
+
return request(`/api/workflows/${workflowId}/run`, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
body: JSON.stringify({ params, force: !!force })
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async getTaskStatus(taskId) {
|
|
288
|
+
return request(`/api/workflow-tasks/${taskId}`);
|
|
289
|
+
}
|
|
290
|
+
async provideInput(taskId, values, confirm) {
|
|
291
|
+
return request(`/api/workflow-tasks/${taskId}/input`, {
|
|
292
|
+
method: "POST",
|
|
293
|
+
body: JSON.stringify({ values: values ?? null, confirm: confirm ?? null })
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async cancelTask(taskId) {
|
|
297
|
+
return request(`/api/workflow-tasks/${taskId}/cancel`, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
body: JSON.stringify({})
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
// Script methods
|
|
303
|
+
async submitScript(name, code) {
|
|
304
|
+
return request(`/api/scripts`, {
|
|
305
|
+
method: "POST",
|
|
306
|
+
body: JSON.stringify({ name, code })
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async updateScript(name, code) {
|
|
310
|
+
return request(`/api/scripts/${encodeURIComponent(name)}`, {
|
|
311
|
+
method: "PUT",
|
|
312
|
+
body: JSON.stringify({ code })
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async listScripts() {
|
|
316
|
+
return request("/api/scripts");
|
|
317
|
+
}
|
|
318
|
+
async getScript(name) {
|
|
319
|
+
return request(`/api/scripts/${encodeURIComponent(name)}`);
|
|
320
|
+
}
|
|
321
|
+
async deleteScript(name) {
|
|
322
|
+
await request(`/api/scripts/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
323
|
+
}
|
|
324
|
+
async runScript(scriptName, params = {}) {
|
|
325
|
+
return request(`/api/scripts/${encodeURIComponent(scriptName)}/run`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
body: JSON.stringify({ params })
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
async getScriptTask(taskId) {
|
|
331
|
+
return request(`/api/script-tasks/${taskId}`);
|
|
332
|
+
}
|
|
249
333
|
};
|
|
250
334
|
var api = new BrowserAPI();
|
|
251
335
|
|
|
@@ -255,11 +339,18 @@ import { join as join2 } from "path";
|
|
|
255
339
|
import { homedir as homedir2 } from "os";
|
|
256
340
|
var OPTIMA_DIR = join2(homedir2(), ".optima");
|
|
257
341
|
var SESSION_FILE = join2(OPTIMA_DIR, "browser-session-id");
|
|
342
|
+
var _sessionOverride = null;
|
|
343
|
+
function setSessionOverride(id) {
|
|
344
|
+
_sessionOverride = id;
|
|
345
|
+
}
|
|
258
346
|
function saveSessionId(id) {
|
|
259
347
|
mkdirSync(OPTIMA_DIR, { recursive: true });
|
|
260
348
|
writeFileSync(SESSION_FILE, id, "utf-8");
|
|
261
349
|
}
|
|
262
350
|
function loadSessionId() {
|
|
351
|
+
if (_sessionOverride) {
|
|
352
|
+
return _sessionOverride;
|
|
353
|
+
}
|
|
263
354
|
if (!existsSync2(SESSION_FILE)) {
|
|
264
355
|
throw new Error(
|
|
265
356
|
"No active browser session. Run 'browser-cli launch' first."
|
|
@@ -341,6 +432,67 @@ function formatError(err) {
|
|
|
341
432
|
function formatTabsInline(tabs) {
|
|
342
433
|
return tabs.map((t) => `[${t.tab_id}] ${t.title}`).join(", ");
|
|
343
434
|
}
|
|
435
|
+
function formatWorkflowSaved(workflow) {
|
|
436
|
+
return `\u2713 Workflow '${workflow.name}' saved (id: ${workflow.id}, ${workflow.steps.length} steps)`;
|
|
437
|
+
}
|
|
438
|
+
function formatWorkflowList(workflows) {
|
|
439
|
+
if (workflows.length === 0) {
|
|
440
|
+
return "No workflows found.";
|
|
441
|
+
}
|
|
442
|
+
const lines = ["Workflows:", ""];
|
|
443
|
+
for (const wf of workflows) {
|
|
444
|
+
const desc = wf.description ? ` \u2014 ${wf.description}` : "";
|
|
445
|
+
lines.push(` ${wf.name} (${wf.step_count} steps)${desc}`);
|
|
446
|
+
}
|
|
447
|
+
return lines.join("\n");
|
|
448
|
+
}
|
|
449
|
+
function formatWorkflowInfo(workflow) {
|
|
450
|
+
const lines = [];
|
|
451
|
+
lines.push(`Workflow: ${workflow.name}`);
|
|
452
|
+
lines.push(`ID: ${workflow.id}`);
|
|
453
|
+
if (workflow.description) lines.push(`Description: ${workflow.description}`);
|
|
454
|
+
lines.push(`Steps: ${workflow.steps.length}`);
|
|
455
|
+
lines.push(`Created: ${workflow.created_at}`);
|
|
456
|
+
lines.push("");
|
|
457
|
+
if (workflow.input_schema.length > 0) {
|
|
458
|
+
lines.push("Input variables:");
|
|
459
|
+
for (const v of workflow.input_schema) {
|
|
460
|
+
const req = v.required ? " (required)" : "";
|
|
461
|
+
lines.push(` ${v.name}: ${v.type}${req}`);
|
|
462
|
+
}
|
|
463
|
+
lines.push("");
|
|
464
|
+
}
|
|
465
|
+
lines.push("Steps:");
|
|
466
|
+
for (let i = 0; i < workflow.steps.length; i++) {
|
|
467
|
+
const s = workflow.steps[i];
|
|
468
|
+
lines.push(` ${i + 1}. [${s.step_type}] ${s.description}`);
|
|
469
|
+
}
|
|
470
|
+
return lines.join("\n");
|
|
471
|
+
}
|
|
472
|
+
function formatWorkflowTask(task) {
|
|
473
|
+
const lines = [];
|
|
474
|
+
const name = task.workflow_name ? ` (${task.workflow_name})` : "";
|
|
475
|
+
lines.push(`Task: ${task.id}${name}`);
|
|
476
|
+
lines.push(`Status: ${task.status}`);
|
|
477
|
+
lines.push(`Progress: ${task.current_step}/${task.total_steps}`);
|
|
478
|
+
if (task.session_id) lines.push(`Session: ${task.session_id}`);
|
|
479
|
+
if (task.missing_vars && task.missing_vars.length > 0) {
|
|
480
|
+
lines.push(`Missing: ${task.missing_vars.join(", ")}`);
|
|
481
|
+
}
|
|
482
|
+
if (task.pending_confirmation) {
|
|
483
|
+
lines.push(`Confirmation: ${task.pending_confirmation}`);
|
|
484
|
+
}
|
|
485
|
+
if (task.error) lines.push(`Error: ${task.error}`);
|
|
486
|
+
if (task.output) {
|
|
487
|
+
const entries = Object.entries(task.output).filter(([k]) => !k.startsWith("_")).map(([k, v]) => `${k}: ${v}`).join(", ");
|
|
488
|
+
if (entries) lines.push(`Output: ${entries}`);
|
|
489
|
+
}
|
|
490
|
+
if (task.completed_at) lines.push(`Completed: ${task.completed_at}`);
|
|
491
|
+
return lines.join("\n");
|
|
492
|
+
}
|
|
493
|
+
function formatWorkflowDeleted(name) {
|
|
494
|
+
return `\u2713 Workflow '${name}' deleted`;
|
|
495
|
+
}
|
|
344
496
|
|
|
345
497
|
// src/commands/lifecycle.ts
|
|
346
498
|
function registerLifecycleCommands(program2) {
|
|
@@ -479,6 +631,16 @@ function registerInteractionCommands(program2) {
|
|
|
479
631
|
process.exit(1);
|
|
480
632
|
}
|
|
481
633
|
});
|
|
634
|
+
program2.command("upload").description("Upload file(s) to a file input element").argument("<index>", "File input element index from DOM").argument("<files...>", "File path(s) to upload").action(async (index, files) => {
|
|
635
|
+
try {
|
|
636
|
+
const sessionId = loadSessionId();
|
|
637
|
+
const state = await api.uploadFiles(sessionId, parseInt(index, 10), files);
|
|
638
|
+
console.log(formatPageState(state));
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.error(formatError(err));
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
482
644
|
}
|
|
483
645
|
|
|
484
646
|
// src/commands/info.ts
|
|
@@ -561,14 +723,457 @@ function registerTabCommands(program2) {
|
|
|
561
723
|
});
|
|
562
724
|
}
|
|
563
725
|
|
|
726
|
+
// src/commands/workflow.ts
|
|
727
|
+
function registerWorkflowCommands(program2) {
|
|
728
|
+
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) => {
|
|
729
|
+
try {
|
|
730
|
+
const sessionId = loadSessionId();
|
|
731
|
+
const workflow = await api.saveWorkflow(sessionId, name, opts.description);
|
|
732
|
+
console.log(formatWorkflowSaved(workflow));
|
|
733
|
+
} catch (err) {
|
|
734
|
+
console.error(formatError(err));
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
program2.command("list-workflows").description("List saved workflows").action(async () => {
|
|
739
|
+
try {
|
|
740
|
+
const workflows = await api.listWorkflows();
|
|
741
|
+
console.log(formatWorkflowList(workflows));
|
|
742
|
+
} catch (err) {
|
|
743
|
+
console.error(formatError(err));
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
program2.command("workflow-info").argument("<name>", "Workflow name").description("Show workflow details").action(async (name) => {
|
|
748
|
+
try {
|
|
749
|
+
const workflows = await api.listWorkflows(name);
|
|
750
|
+
if (workflows.length === 0) {
|
|
751
|
+
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
const workflow = await api.getWorkflow(workflows[0].id);
|
|
755
|
+
console.log(formatWorkflowInfo(workflow));
|
|
756
|
+
} catch (err) {
|
|
757
|
+
console.error(formatError(err));
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
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) => {
|
|
762
|
+
try {
|
|
763
|
+
const workflows = await api.listWorkflows(name);
|
|
764
|
+
if (workflows.length === 0) {
|
|
765
|
+
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
const params = {};
|
|
769
|
+
if (opts.param) {
|
|
770
|
+
for (const p of opts.param) {
|
|
771
|
+
const [k, ...rest] = p.split("=");
|
|
772
|
+
params[k] = rest.join("=");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const task = await api.runWorkflow(workflows[0].id, params, opts.force);
|
|
776
|
+
console.log(formatWorkflowTask(task));
|
|
777
|
+
} catch (err) {
|
|
778
|
+
console.error(formatError(err));
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
program2.command("workflow-status").argument("<task_id>", "Task ID").description("Check workflow task status").action(async (taskId) => {
|
|
783
|
+
try {
|
|
784
|
+
const task = await api.getTaskStatus(taskId);
|
|
785
|
+
console.log(formatWorkflowTask(task));
|
|
786
|
+
} catch (err) {
|
|
787
|
+
console.error(formatError(err));
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
program2.command("workflow-cancel").argument("<task_id>", "Task ID").description("Cancel a running workflow task").action(async (taskId) => {
|
|
792
|
+
try {
|
|
793
|
+
const task = await api.cancelTask(taskId);
|
|
794
|
+
console.log(formatWorkflowTask(task));
|
|
795
|
+
} catch (err) {
|
|
796
|
+
console.error(formatError(err));
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
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) => {
|
|
801
|
+
try {
|
|
802
|
+
const values = {};
|
|
803
|
+
if (opts.value) {
|
|
804
|
+
const task = await api.getTaskStatus(taskId);
|
|
805
|
+
if (!task.missing_vars || task.missing_vars.length !== 1) {
|
|
806
|
+
console.error(formatError(new Error("--value can only be used when exactly one variable is missing. Use --var key=value instead.")));
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
values[task.missing_vars[0]] = opts.value;
|
|
810
|
+
}
|
|
811
|
+
if (opts.var) {
|
|
812
|
+
for (const v of opts.var) {
|
|
813
|
+
const [k, ...rest] = v.split("=");
|
|
814
|
+
values[k] = rest.join("=");
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const hasValues = Object.keys(values).length > 0;
|
|
818
|
+
const result = await api.provideInput(taskId, hasValues ? values : void 0, opts.confirm);
|
|
819
|
+
console.log(formatWorkflowTask(result));
|
|
820
|
+
} catch (err) {
|
|
821
|
+
console.error(formatError(err));
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
program2.command("delete-workflow").argument("<name>", "Workflow name").description("Delete a saved workflow").action(async (name) => {
|
|
826
|
+
try {
|
|
827
|
+
const workflows = await api.listWorkflows(name);
|
|
828
|
+
if (workflows.length === 0) {
|
|
829
|
+
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
830
|
+
process.exit(1);
|
|
831
|
+
}
|
|
832
|
+
await api.deleteWorkflow(workflows[0].id);
|
|
833
|
+
console.log(formatWorkflowDeleted(name));
|
|
834
|
+
} catch (err) {
|
|
835
|
+
console.error(formatError(err));
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
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) => {
|
|
840
|
+
try {
|
|
841
|
+
const workflows = await api.listWorkflows(name);
|
|
842
|
+
if (workflows.length === 0) {
|
|
843
|
+
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
const workflow = await api.getWorkflow(workflows[0].id);
|
|
847
|
+
if (opts.setVar && opts.step && opts.name) {
|
|
848
|
+
const stepIdx = opts.step - 1;
|
|
849
|
+
if (stepIdx < 0 || stepIdx >= workflow.steps.length) {
|
|
850
|
+
console.error(formatError(new Error(`Step ${opts.step} out of range`)));
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
const step = workflow.steps[stepIdx];
|
|
854
|
+
if (step.step_type === "navigate" && step.url) {
|
|
855
|
+
step.url = `{${opts.name}}`;
|
|
856
|
+
} else if (step.value !== void 0 && step.value !== null) {
|
|
857
|
+
step.value = `{${opts.name}}`;
|
|
858
|
+
} else {
|
|
859
|
+
console.error(formatError(new Error(`Step ${opts.step} ([${step.step_type}]) has no value or url to parameterize`)));
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
863
|
+
console.log(`\u2713 Step ${opts.step} value set to {${opts.name}}`);
|
|
864
|
+
} else if (opts.addInput) {
|
|
865
|
+
const inputVar = {
|
|
866
|
+
name: opts.addInput,
|
|
867
|
+
type: opts.type || "string",
|
|
868
|
+
description: "",
|
|
869
|
+
required: !!opts.required,
|
|
870
|
+
default: null,
|
|
871
|
+
sensitive: false
|
|
872
|
+
};
|
|
873
|
+
const schema = workflow.input_schema || [];
|
|
874
|
+
schema.push(inputVar);
|
|
875
|
+
await api.updateWorkflow(workflow.id, { input_schema: schema });
|
|
876
|
+
console.log(`\u2713 Input variable '${opts.addInput}' added`);
|
|
877
|
+
} else if (opts.addStep === "extract") {
|
|
878
|
+
const extractStep = {
|
|
879
|
+
step_type: "extract",
|
|
880
|
+
description: "Extract data",
|
|
881
|
+
extract_selectors: opts.selectors ? JSON.parse(opts.selectors) : {},
|
|
882
|
+
extract_screenshot: !!opts.screenshot
|
|
883
|
+
};
|
|
884
|
+
workflow.steps.push(extractStep);
|
|
885
|
+
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
886
|
+
console.log(`\u2713 Extract step added`);
|
|
887
|
+
} else if (opts.addStep === "wait_for_element") {
|
|
888
|
+
const waitStep = {
|
|
889
|
+
step_type: "wait_for_element",
|
|
890
|
+
description: `Wait for ${opts.selector || "element"}`,
|
|
891
|
+
wait_selector: opts.selector || "",
|
|
892
|
+
wait_timeout: opts.timeout || 30
|
|
893
|
+
};
|
|
894
|
+
workflow.steps.push(waitStep);
|
|
895
|
+
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
896
|
+
console.log(`\u2713 wait_for_element step added`);
|
|
897
|
+
} else if (opts.removeStep) {
|
|
898
|
+
const stepIdx = opts.removeStep - 1;
|
|
899
|
+
if (stepIdx < 0 || stepIdx >= workflow.steps.length) {
|
|
900
|
+
console.error(formatError(new Error(`Step ${opts.removeStep} out of range`)));
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
const removed = workflow.steps.splice(stepIdx, 1)[0];
|
|
904
|
+
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
905
|
+
console.log(`\u2713 Step ${opts.removeStep} removed ([${removed.step_type}] ${removed.description})`);
|
|
906
|
+
} else if (opts.updateStep) {
|
|
907
|
+
const stepIdx = opts.updateStep - 1;
|
|
908
|
+
if (stepIdx < 0 || stepIdx >= workflow.steps.length) {
|
|
909
|
+
console.error(formatError(new Error(`Step ${opts.updateStep} out of range`)));
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
const step = workflow.steps[stepIdx];
|
|
913
|
+
let changed = false;
|
|
914
|
+
if (opts.requiresConfirmation === true) {
|
|
915
|
+
step.requires_confirmation = true;
|
|
916
|
+
changed = true;
|
|
917
|
+
} else if (opts.requiresConfirmation === false) {
|
|
918
|
+
step.requires_confirmation = false;
|
|
919
|
+
changed = true;
|
|
920
|
+
}
|
|
921
|
+
if (opts.targetText) {
|
|
922
|
+
step.target_text = opts.targetText;
|
|
923
|
+
changed = true;
|
|
924
|
+
}
|
|
925
|
+
if (!changed) {
|
|
926
|
+
console.log("Specify what to update: --requires-confirmation, --target-text <text>");
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
929
|
+
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
930
|
+
console.log(`\u2713 Step ${opts.updateStep} updated`);
|
|
931
|
+
} else if (opts.autoClean) {
|
|
932
|
+
const before = workflow.steps.length;
|
|
933
|
+
const cleaned = autoCleanSteps(workflow.steps);
|
|
934
|
+
await api.updateWorkflow(workflow.id, { steps: cleaned });
|
|
935
|
+
const removed = before - cleaned.length;
|
|
936
|
+
console.log(`\u2713 Auto-clean: ${removed} step(s) removed (${before} \u2192 ${cleaned.length})`);
|
|
937
|
+
} else {
|
|
938
|
+
console.log("Usage:");
|
|
939
|
+
console.log(" edit-workflow <name> --set-var --step N --name varName");
|
|
940
|
+
console.log(" edit-workflow <name> --add-input varName --type T [--required]");
|
|
941
|
+
console.log(" edit-workflow <name> --add-step extract --selectors '{...}' [--screenshot]");
|
|
942
|
+
console.log(" edit-workflow <name> --add-step wait_for_element --selector '.css' [--timeout 30]");
|
|
943
|
+
console.log(" edit-workflow <name> --remove-step N");
|
|
944
|
+
console.log(" edit-workflow <name> --update-step N --requires-confirmation");
|
|
945
|
+
console.log(" edit-workflow <name> --update-step N --target-text 'new text'");
|
|
946
|
+
console.log(" edit-workflow <name> --auto-clean");
|
|
947
|
+
}
|
|
948
|
+
} catch (err) {
|
|
949
|
+
console.error(formatError(err));
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
function autoCleanSteps(steps) {
|
|
955
|
+
let result = [...steps];
|
|
956
|
+
result = cleanNavigateBackNavigate(result);
|
|
957
|
+
result = cleanConsecutiveInput(result);
|
|
958
|
+
result = cleanConsecutiveScroll(result);
|
|
959
|
+
result = cleanConsecutiveClick(result);
|
|
960
|
+
return result;
|
|
961
|
+
}
|
|
962
|
+
function cleanNavigateBackNavigate(steps) {
|
|
963
|
+
const result = [];
|
|
964
|
+
let i = 0;
|
|
965
|
+
while (i < steps.length) {
|
|
966
|
+
if (i + 2 < steps.length && steps[i].step_type === "navigate" && steps[i + 1].step_type === "go_back" && steps[i + 2].step_type === "navigate") {
|
|
967
|
+
i += 2;
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
result.push(steps[i]);
|
|
971
|
+
i++;
|
|
972
|
+
}
|
|
973
|
+
return result;
|
|
974
|
+
}
|
|
975
|
+
function sameElement(a, b) {
|
|
976
|
+
return !!a.target_text && a.target_text === b.target_text && a.element_tag === b.element_tag;
|
|
977
|
+
}
|
|
978
|
+
function cleanConsecutiveInput(steps) {
|
|
979
|
+
const result = [];
|
|
980
|
+
for (let i = 0; i < steps.length; i++) {
|
|
981
|
+
if (steps[i].step_type === "input" && i + 1 < steps.length && steps[i + 1].step_type === "input" && sameElement(steps[i], steps[i + 1])) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
result.push(steps[i]);
|
|
985
|
+
}
|
|
986
|
+
return result;
|
|
987
|
+
}
|
|
988
|
+
function cleanConsecutiveScroll(steps) {
|
|
989
|
+
const result = [];
|
|
990
|
+
for (let i = 0; i < steps.length; i++) {
|
|
991
|
+
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) {
|
|
992
|
+
const amount = (steps[i].scroll_amount || 1) + (steps[i + 1].scroll_amount || 1);
|
|
993
|
+
steps[i + 1] = { ...steps[i + 1], scroll_amount: amount };
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
result.push(steps[i]);
|
|
997
|
+
}
|
|
998
|
+
return result;
|
|
999
|
+
}
|
|
1000
|
+
function cleanConsecutiveClick(steps) {
|
|
1001
|
+
const result = [];
|
|
1002
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1003
|
+
if (steps[i].step_type === "click" && i + 1 < steps.length && steps[i + 1].step_type === "click" && sameElement(steps[i], steps[i + 1])) {
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
result.push(steps[i]);
|
|
1007
|
+
}
|
|
1008
|
+
return result;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/commands/script.ts
|
|
1012
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1013
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
1014
|
+
var POLL_TIMEOUT_MS = 18e4;
|
|
1015
|
+
function formatScriptList(scripts) {
|
|
1016
|
+
if (scripts.length === 0) return "No scripts registered.";
|
|
1017
|
+
const lines = [];
|
|
1018
|
+
for (const s of scripts) {
|
|
1019
|
+
const tag = s.source ? ` [${s.source}]` : "";
|
|
1020
|
+
lines.push(`${s.name} \u2014 ${s.description}${tag}`);
|
|
1021
|
+
for (let i = 0; i < s.steps.length; i++) {
|
|
1022
|
+
lines.push(` [${i + 1}] ${s.steps[i].id} \u2014 ${s.steps[i].description}`);
|
|
1023
|
+
}
|
|
1024
|
+
lines.push("");
|
|
1025
|
+
}
|
|
1026
|
+
return lines.join("\n").trimEnd();
|
|
1027
|
+
}
|
|
1028
|
+
function formatScriptTask(task) {
|
|
1029
|
+
const lines = [];
|
|
1030
|
+
if (task.status === "completed") {
|
|
1031
|
+
lines.push("\u2713 Script completed");
|
|
1032
|
+
if (task.output) {
|
|
1033
|
+
for (const [k, v] of Object.entries(task.output)) {
|
|
1034
|
+
lines.push(`${k}: ${v}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
} else if (task.status === "needs_fallback") {
|
|
1038
|
+
lines.push("\u26A0 Script needs fallback");
|
|
1039
|
+
lines.push("");
|
|
1040
|
+
const output = task.output ?? {};
|
|
1041
|
+
const completed = output.completed_steps ?? [];
|
|
1042
|
+
const remaining = output.remaining_steps ?? [];
|
|
1043
|
+
const error = output.error;
|
|
1044
|
+
if (completed.length > 0) {
|
|
1045
|
+
lines.push("\u2713 Completed:");
|
|
1046
|
+
for (let i = 0; i < completed.length; i++) {
|
|
1047
|
+
lines.push(` [${i + 1}] ${completed[i].id} \u2014 ${completed[i].description}`);
|
|
1048
|
+
}
|
|
1049
|
+
lines.push("");
|
|
1050
|
+
}
|
|
1051
|
+
if (error) {
|
|
1052
|
+
const failedStep = completed.length + 1;
|
|
1053
|
+
const failedId = remaining.length > 0 ? remaining[0].id : "unknown";
|
|
1054
|
+
lines.push(`\u2717 Failed at step [${failedStep}] ${failedId}: ${error}`);
|
|
1055
|
+
lines.push("");
|
|
1056
|
+
}
|
|
1057
|
+
if (remaining.length > 0) {
|
|
1058
|
+
lines.push("\u2192 Remaining steps:");
|
|
1059
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
1060
|
+
const stepNum = completed.length + i + 1;
|
|
1061
|
+
lines.push(` [${stepNum}] ${remaining[i].id} \u2014 ${remaining[i].description}`);
|
|
1062
|
+
}
|
|
1063
|
+
lines.push("");
|
|
1064
|
+
}
|
|
1065
|
+
lines.push("Session is still open \u2014 use browser-cli dom/click/input to continue");
|
|
1066
|
+
} else if (task.status === "failed") {
|
|
1067
|
+
lines.push(`\u2717 Script failed: ${task.error ?? "unknown error"}`);
|
|
1068
|
+
} else {
|
|
1069
|
+
lines.push(`Script status: ${task.status} (step ${task.current_step}/${task.total_steps})`);
|
|
1070
|
+
}
|
|
1071
|
+
return lines.join("\n");
|
|
1072
|
+
}
|
|
1073
|
+
function registerScriptCommands(program2) {
|
|
1074
|
+
program2.command("submit-script").description("Submit a script (write to EFS)").requiredOption("--name <name>", "Script name").requiredOption("--code-file <path>", "Path to Python script file").action(async (opts) => {
|
|
1075
|
+
try {
|
|
1076
|
+
const code = readFileSync3(opts.codeFile, "utf-8");
|
|
1077
|
+
const result = await api.submitScript(opts.name, code);
|
|
1078
|
+
console.log(`\u2713 Script ${result.name} ${result.created ? "created" : "updated"}`);
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
console.error(formatError(err));
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
program2.command("script-code").argument("<name>", "Script name").description("Show script source code").action(async (name) => {
|
|
1085
|
+
try {
|
|
1086
|
+
const detail = await api.getScript(name);
|
|
1087
|
+
console.log(detail.code);
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
console.error(formatError(err));
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
program2.command("delete-script").argument("<name>", "Script name").description("Delete a script").action(async (name) => {
|
|
1094
|
+
try {
|
|
1095
|
+
await api.deleteScript(name);
|
|
1096
|
+
console.log(`\u2713 Script ${name} deleted`);
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
console.error(formatError(err));
|
|
1099
|
+
process.exit(1);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
program2.command("scripts").description("List scripts").action(async () => {
|
|
1103
|
+
try {
|
|
1104
|
+
const resp = await api.listScripts();
|
|
1105
|
+
console.log(formatScriptList(resp.scripts));
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
console.error(formatError(err));
|
|
1108
|
+
process.exit(1);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
program2.command("runscript").argument("<name>", "Script name").description("Run a script (polls until complete)").option("--param <params...>", "Parameters as key=value").option("--no-poll", "Return immediately without polling").action(async (name, opts) => {
|
|
1112
|
+
try {
|
|
1113
|
+
const params = {};
|
|
1114
|
+
if (opts.param) {
|
|
1115
|
+
for (const p of opts.param) {
|
|
1116
|
+
const [k, ...rest] = p.split("=");
|
|
1117
|
+
params[k] = rest.join("=");
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
clearSessionId();
|
|
1121
|
+
const task = await api.runScript(name, params);
|
|
1122
|
+
console.log(`Script started (task: ${task.id})`);
|
|
1123
|
+
if (!opts.poll) {
|
|
1124
|
+
console.log(`Poll with: browser-cli script-task ${task.id}`);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
1128
|
+
let result = task;
|
|
1129
|
+
while (result.status === "running" && Date.now() < deadline) {
|
|
1130
|
+
await sleep(POLL_INTERVAL_MS);
|
|
1131
|
+
result = await api.getScriptTask(task.id);
|
|
1132
|
+
}
|
|
1133
|
+
if (result.status === "running") {
|
|
1134
|
+
console.log(`Polling timed out. Task ${task.id} still running.`);
|
|
1135
|
+
console.log(`Poll with: browser-cli script-task ${task.id}`);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (result.status === "needs_fallback" && result.session_id) {
|
|
1139
|
+
saveSessionId(result.session_id);
|
|
1140
|
+
}
|
|
1141
|
+
console.log(formatScriptTask(result));
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
console.error(formatError(err));
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
program2.command("script-task").argument("<task-id>", "Task ID").description("Check script task status").action(async (taskId) => {
|
|
1148
|
+
try {
|
|
1149
|
+
const task = await api.getScriptTask(taskId);
|
|
1150
|
+
console.log(formatScriptTask(task));
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
console.error(formatError(err));
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
function sleep(ms) {
|
|
1158
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1159
|
+
}
|
|
1160
|
+
|
|
564
1161
|
// src/index.ts
|
|
565
1162
|
var require2 = createRequire(import.meta.url);
|
|
566
1163
|
var { version } = require2("../package.json");
|
|
567
1164
|
var program = new Command();
|
|
568
|
-
program.name("browser-cli").description("CLI client for browser-backend").version(version);
|
|
1165
|
+
program.name("browser-cli").description("CLI client for browser-backend").version(version).option("--session <id>", "Override session ID");
|
|
1166
|
+
program.hook("preAction", () => {
|
|
1167
|
+
const opts = program.opts();
|
|
1168
|
+
if (opts.session) {
|
|
1169
|
+
setSessionOverride(opts.session);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
569
1172
|
registerLifecycleCommands(program);
|
|
570
1173
|
registerNavigationCommands(program);
|
|
571
1174
|
registerInteractionCommands(program);
|
|
572
1175
|
registerInfoCommands(program);
|
|
573
1176
|
registerTabCommands(program);
|
|
1177
|
+
registerWorkflowCommands(program);
|
|
1178
|
+
registerScriptCommands(program);
|
|
574
1179
|
program.parse();
|