@flrande/browserctl 0.4.0-dev.15.1 → 0.5.0-dev.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/apps/browserctl/src/commands/act.test.ts +71 -0
  2. package/apps/browserctl/src/commands/act.ts +45 -1
  3. package/apps/browserctl/src/commands/command-wrappers.test.ts +302 -0
  4. package/apps/browserctl/src/commands/console-list.test.ts +102 -0
  5. package/apps/browserctl/src/commands/console-list.ts +89 -1
  6. package/apps/browserctl/src/commands/har-export.test.ts +112 -0
  7. package/apps/browserctl/src/commands/har-export.ts +120 -0
  8. package/apps/browserctl/src/commands/memory-delete.ts +20 -0
  9. package/apps/browserctl/src/commands/memory-inspect.ts +20 -0
  10. package/apps/browserctl/src/commands/memory-list.ts +90 -0
  11. package/apps/browserctl/src/commands/memory-mode-set.ts +29 -0
  12. package/apps/browserctl/src/commands/memory-purge.ts +16 -0
  13. package/apps/browserctl/src/commands/memory-resolve.ts +56 -0
  14. package/apps/browserctl/src/commands/memory-status.ts +16 -0
  15. package/apps/browserctl/src/commands/memory-ttl-set.ts +28 -0
  16. package/apps/browserctl/src/commands/memory-upsert.ts +142 -0
  17. package/apps/browserctl/src/commands/network-list.test.ts +110 -0
  18. package/apps/browserctl/src/commands/network-list.ts +112 -0
  19. package/apps/browserctl/src/commands/session-drop.test.ts +36 -0
  20. package/apps/browserctl/src/commands/session-drop.ts +16 -0
  21. package/apps/browserctl/src/commands/session-list.test.ts +81 -0
  22. package/apps/browserctl/src/commands/session-list.ts +70 -0
  23. package/apps/browserctl/src/commands/trace-get.test.ts +61 -0
  24. package/apps/browserctl/src/commands/trace-get.ts +62 -0
  25. package/apps/browserctl/src/commands/wait-element.test.ts +80 -0
  26. package/apps/browserctl/src/commands/wait-element.ts +76 -0
  27. package/apps/browserctl/src/commands/wait-text.test.ts +110 -0
  28. package/apps/browserctl/src/commands/wait-text.ts +93 -0
  29. package/apps/browserctl/src/commands/wait-url.test.ts +80 -0
  30. package/apps/browserctl/src/commands/wait-url.ts +76 -0
  31. package/apps/browserctl/src/main.dispatch.test.ts +206 -1
  32. package/apps/browserctl/src/main.test.ts +30 -0
  33. package/apps/browserctl/src/main.ts +246 -4
  34. package/apps/browserd/src/container.ts +1603 -48
  35. package/apps/browserd/src/main.test.ts +538 -1
  36. package/apps/browserd/src/tool-matrix.test.ts +492 -3
  37. package/package.json +5 -1
  38. package/packages/core/src/driver.ts +1 -1
  39. package/packages/core/src/index.ts +1 -0
  40. package/packages/core/src/navigation-memory.test.ts +259 -0
  41. package/packages/core/src/navigation-memory.ts +360 -0
  42. package/packages/core/src/session-store.test.ts +33 -0
  43. package/packages/core/src/session-store.ts +111 -6
  44. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +112 -2
  45. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +233 -10
  46. package/packages/driver-managed/src/managed-driver.test.ts +124 -0
  47. package/packages/driver-managed/src/managed-driver.ts +233 -17
  48. package/packages/driver-managed/src/managed-local-driver.test.ts +104 -2
  49. package/packages/driver-managed/src/managed-local-driver.ts +232 -10
  50. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +112 -2
  51. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +232 -10
  52. package/packages/transport-mcp-stdio/src/tool-map.ts +18 -1
@@ -0,0 +1,76 @@
1
+ import { callDaemonTool } from "../daemon-client";
2
+ import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
3
+
4
+ export type WaitElementCommandResult = Record<string, unknown>;
5
+
6
+ function parsePositiveInteger(value: string, optionName: string): number {
7
+ const normalized = value.trim();
8
+ if (!/^[1-9]\d*$/.test(normalized)) {
9
+ throw new Error(`${optionName} must be a positive integer.`);
10
+ }
11
+
12
+ return Number.parseInt(normalized, 10);
13
+ }
14
+
15
+ function parseWaitOptions(tokens: string[]): {
16
+ timeoutMs?: number;
17
+ pollMs?: number;
18
+ } {
19
+ let timeoutMs: number | undefined;
20
+ let pollMs: number | undefined;
21
+
22
+ for (let index = 0; index < tokens.length; index += 1) {
23
+ const token = tokens[index];
24
+ if (token === "--timeout-ms") {
25
+ const value = tokens[index + 1];
26
+ if (value === undefined) {
27
+ throw new Error("Missing value for --timeout-ms.");
28
+ }
29
+ timeoutMs = parsePositiveInteger(value, "--timeout-ms");
30
+ index += 1;
31
+ continue;
32
+ }
33
+
34
+ if (token === "--poll-ms") {
35
+ const value = tokens[index + 1];
36
+ if (value === undefined) {
37
+ throw new Error("Missing value for --poll-ms.");
38
+ }
39
+ pollMs = parsePositiveInteger(value, "--poll-ms");
40
+ index += 1;
41
+ continue;
42
+ }
43
+
44
+ if (token.startsWith("--")) {
45
+ throw new Error(`Unknown wait-element option: ${token}`);
46
+ }
47
+
48
+ if (timeoutMs === undefined) {
49
+ timeoutMs = parsePositiveInteger(token, "timeoutMs");
50
+ continue;
51
+ }
52
+
53
+ throw new Error(`Unknown wait-element option: ${token}`);
54
+ }
55
+
56
+ return {
57
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
58
+ ...(pollMs !== undefined ? { pollMs } : {})
59
+ };
60
+ }
61
+
62
+ export async function runWaitElementCommand(args: string[]): Promise<WaitElementCommandResult> {
63
+ const context = parseCommandContext(args);
64
+ const targetId = requirePositionalArg(context, 0, "targetId");
65
+ const selector = requirePositionalArg(context, 1, "selector");
66
+ const options = parseWaitOptions(context.positional.slice(2));
67
+
68
+ return await callDaemonTool<WaitElementCommandResult>(
69
+ "browser.wait.element",
70
+ buildToolArguments(context, {
71
+ targetId,
72
+ selector,
73
+ ...options
74
+ })
75
+ );
76
+ }
@@ -0,0 +1,110 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const { callDaemonToolMock } = vi.hoisted(() => ({
4
+ callDaemonToolMock: vi.fn()
5
+ }));
6
+
7
+ vi.mock("../daemon-client", () => ({
8
+ callDaemonTool: callDaemonToolMock
9
+ }));
10
+
11
+ import { runWaitTextCommand } from "./wait-text";
12
+
13
+ afterEach(() => {
14
+ callDaemonToolMock.mockReset();
15
+ });
16
+
17
+ describe("runWaitTextCommand", () => {
18
+ it("supports selector and timeout options", async () => {
19
+ callDaemonToolMock.mockResolvedValue({ ok: true });
20
+
21
+ await runWaitTextCommand([
22
+ "target:1",
23
+ "Order Submitted",
24
+ "--selector",
25
+ "#status",
26
+ "--timeout-ms",
27
+ "4000",
28
+ "--poll-ms",
29
+ "50"
30
+ ]);
31
+
32
+ expect(callDaemonToolMock).toHaveBeenCalledTimes(1);
33
+ expect(callDaemonToolMock).toHaveBeenCalledWith("browser.wait.text", {
34
+ sessionId: "cli:local",
35
+ targetId: "target:1",
36
+ text: "Order Submitted",
37
+ selector: "#status",
38
+ timeoutMs: 4000,
39
+ pollMs: 50
40
+ });
41
+ });
42
+
43
+ it("supports positional timeout shorthand", async () => {
44
+ callDaemonToolMock.mockResolvedValue({ ok: true });
45
+
46
+ await runWaitTextCommand(["target:1", "ready", "1500"]);
47
+
48
+ expect(callDaemonToolMock).toHaveBeenCalledTimes(1);
49
+ expect(callDaemonToolMock).toHaveBeenCalledWith("browser.wait.text", {
50
+ sessionId: "cli:local",
51
+ targetId: "target:1",
52
+ text: "ready",
53
+ timeoutMs: 1500
54
+ });
55
+ });
56
+
57
+ it.each([
58
+ {
59
+ label: "--selector",
60
+ args: ["target:1", "ready", "--selector"],
61
+ message: "Missing value for --selector."
62
+ },
63
+ {
64
+ label: "--timeout-ms",
65
+ args: ["target:1", "ready", "--timeout-ms"],
66
+ message: "Missing value for --timeout-ms."
67
+ },
68
+ {
69
+ label: "--poll-ms",
70
+ args: ["target:1", "ready", "--poll-ms"],
71
+ message: "Missing value for --poll-ms."
72
+ }
73
+ ])("throws when $label is missing a value", async ({ args, message }) => {
74
+ await expect(runWaitTextCommand(args)).rejects.toThrow(message);
75
+ expect(callDaemonToolMock).not.toHaveBeenCalled();
76
+ });
77
+
78
+ it.each([
79
+ {
80
+ label: "selector empty",
81
+ args: ["target:1", "ready", "--selector", " "],
82
+ message: "--selector must be a non-empty string."
83
+ },
84
+ {
85
+ label: "timeout <= 0",
86
+ args: ["target:1", "ready", "--timeout-ms", "0"],
87
+ message: "--timeout-ms must be a positive integer."
88
+ },
89
+ {
90
+ label: "poll <= 0",
91
+ args: ["target:1", "ready", "--poll-ms", "0"],
92
+ message: "--poll-ms must be a positive integer."
93
+ },
94
+ {
95
+ label: "non-strict timeout text",
96
+ args: ["target:1", "ready", "--timeout-ms", "1500abc"],
97
+ message: "--timeout-ms must be a positive integer."
98
+ }
99
+ ])("throws on invalid option value: $label", async ({ args, message }) => {
100
+ await expect(runWaitTextCommand(args)).rejects.toThrow(message);
101
+ expect(callDaemonToolMock).not.toHaveBeenCalled();
102
+ });
103
+
104
+ it("throws on unknown option token", async () => {
105
+ await expect(runWaitTextCommand(["target:1", "ready", "--unknown"]))
106
+ .rejects.toThrow("Unknown wait-text option: --unknown");
107
+
108
+ expect(callDaemonToolMock).not.toHaveBeenCalled();
109
+ });
110
+ });
@@ -0,0 +1,93 @@
1
+ import { callDaemonTool } from "../daemon-client";
2
+ import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
3
+
4
+ export type WaitTextCommandResult = Record<string, unknown>;
5
+
6
+ function parsePositiveInteger(value: string, optionName: string): number {
7
+ const normalized = value.trim();
8
+ if (!/^[1-9]\d*$/.test(normalized)) {
9
+ throw new Error(`${optionName} must be a positive integer.`);
10
+ }
11
+
12
+ return Number.parseInt(normalized, 10);
13
+ }
14
+
15
+ function parseWaitOptions(tokens: string[]): {
16
+ selector?: string;
17
+ timeoutMs?: number;
18
+ pollMs?: number;
19
+ } {
20
+ let selector: string | undefined;
21
+ let timeoutMs: number | undefined;
22
+ let pollMs: number | undefined;
23
+
24
+ for (let index = 0; index < tokens.length; index += 1) {
25
+ const token = tokens[index];
26
+ if (token === "--selector") {
27
+ const value = tokens[index + 1];
28
+ if (value === undefined) {
29
+ throw new Error("Missing value for --selector.");
30
+ }
31
+ const trimmed = value.trim();
32
+ if (trimmed.length === 0) {
33
+ throw new Error("--selector must be a non-empty string.");
34
+ }
35
+ selector = trimmed;
36
+ index += 1;
37
+ continue;
38
+ }
39
+
40
+ if (token === "--timeout-ms") {
41
+ const value = tokens[index + 1];
42
+ if (value === undefined) {
43
+ throw new Error("Missing value for --timeout-ms.");
44
+ }
45
+ timeoutMs = parsePositiveInteger(value, "--timeout-ms");
46
+ index += 1;
47
+ continue;
48
+ }
49
+
50
+ if (token === "--poll-ms") {
51
+ const value = tokens[index + 1];
52
+ if (value === undefined) {
53
+ throw new Error("Missing value for --poll-ms.");
54
+ }
55
+ pollMs = parsePositiveInteger(value, "--poll-ms");
56
+ index += 1;
57
+ continue;
58
+ }
59
+
60
+ if (token.startsWith("--")) {
61
+ throw new Error(`Unknown wait-text option: ${token}`);
62
+ }
63
+
64
+ if (timeoutMs === undefined) {
65
+ timeoutMs = parsePositiveInteger(token, "timeoutMs");
66
+ continue;
67
+ }
68
+
69
+ throw new Error(`Unknown wait-text option: ${token}`);
70
+ }
71
+
72
+ return {
73
+ ...(selector !== undefined ? { selector } : {}),
74
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
75
+ ...(pollMs !== undefined ? { pollMs } : {})
76
+ };
77
+ }
78
+
79
+ export async function runWaitTextCommand(args: string[]): Promise<WaitTextCommandResult> {
80
+ const context = parseCommandContext(args);
81
+ const targetId = requirePositionalArg(context, 0, "targetId");
82
+ const text = requirePositionalArg(context, 1, "text");
83
+ const options = parseWaitOptions(context.positional.slice(2));
84
+
85
+ return await callDaemonTool<WaitTextCommandResult>(
86
+ "browser.wait.text",
87
+ buildToolArguments(context, {
88
+ targetId,
89
+ text,
90
+ ...options
91
+ })
92
+ );
93
+ }
@@ -0,0 +1,80 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const { callDaemonToolMock } = vi.hoisted(() => ({
4
+ callDaemonToolMock: vi.fn()
5
+ }));
6
+
7
+ vi.mock("../daemon-client", () => ({
8
+ callDaemonTool: callDaemonToolMock
9
+ }));
10
+
11
+ import { runWaitUrlCommand } from "./wait-url";
12
+
13
+ afterEach(() => {
14
+ callDaemonToolMock.mockReset();
15
+ });
16
+
17
+ describe("runWaitUrlCommand", () => {
18
+ it("supports positional timeout shorthand", async () => {
19
+ callDaemonToolMock.mockResolvedValue({ ok: true });
20
+
21
+ await runWaitUrlCommand(["target:1", "/dashboard", "2500"]);
22
+
23
+ expect(callDaemonToolMock).toHaveBeenCalledTimes(1);
24
+ expect(callDaemonToolMock).toHaveBeenCalledWith("browser.wait.url", {
25
+ sessionId: "cli:local",
26
+ targetId: "target:1",
27
+ urlPattern: "/dashboard",
28
+ timeoutMs: 2500
29
+ });
30
+ });
31
+
32
+ it.each([
33
+ {
34
+ label: "--timeout-ms",
35
+ args: ["target:1", "/dashboard", "--timeout-ms"],
36
+ message: "Missing value for --timeout-ms."
37
+ },
38
+ {
39
+ label: "--poll-ms",
40
+ args: ["target:1", "/dashboard", "--poll-ms"],
41
+ message: "Missing value for --poll-ms."
42
+ }
43
+ ])("throws when $label is missing a value", async ({ args, message }) => {
44
+ await expect(runWaitUrlCommand(args)).rejects.toThrow(message);
45
+ expect(callDaemonToolMock).not.toHaveBeenCalled();
46
+ });
47
+
48
+ it.each([
49
+ {
50
+ label: "timeout <= 0",
51
+ args: ["target:1", "/dashboard", "--timeout-ms", "0"],
52
+ message: "--timeout-ms must be a positive integer."
53
+ },
54
+ {
55
+ label: "poll <= 0",
56
+ args: ["target:1", "/dashboard", "--poll-ms", "-5"],
57
+ message: "--poll-ms must be a positive integer."
58
+ },
59
+ {
60
+ label: "positional timeout <= 0",
61
+ args: ["target:1", "/dashboard", "0"],
62
+ message: "timeoutMs must be a positive integer."
63
+ },
64
+ {
65
+ label: "non-strict timeout text",
66
+ args: ["target:1", "/dashboard", "--timeout-ms", "2500abc"],
67
+ message: "--timeout-ms must be a positive integer."
68
+ }
69
+ ])("throws on non-positive numeric option: $label", async ({ args, message }) => {
70
+ await expect(runWaitUrlCommand(args)).rejects.toThrow(message);
71
+ expect(callDaemonToolMock).not.toHaveBeenCalled();
72
+ });
73
+
74
+ it("throws on unknown option token", async () => {
75
+ await expect(runWaitUrlCommand(["target:1", "/dashboard", "--unknown"]))
76
+ .rejects.toThrow("Unknown wait-url option: --unknown");
77
+
78
+ expect(callDaemonToolMock).not.toHaveBeenCalled();
79
+ });
80
+ });
@@ -0,0 +1,76 @@
1
+ import { callDaemonTool } from "../daemon-client";
2
+ import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
3
+
4
+ export type WaitUrlCommandResult = Record<string, unknown>;
5
+
6
+ function parsePositiveInteger(value: string, optionName: string): number {
7
+ const normalized = value.trim();
8
+ if (!/^[1-9]\d*$/.test(normalized)) {
9
+ throw new Error(`${optionName} must be a positive integer.`);
10
+ }
11
+
12
+ return Number.parseInt(normalized, 10);
13
+ }
14
+
15
+ function parseWaitOptions(tokens: string[]): {
16
+ timeoutMs?: number;
17
+ pollMs?: number;
18
+ } {
19
+ let timeoutMs: number | undefined;
20
+ let pollMs: number | undefined;
21
+
22
+ for (let index = 0; index < tokens.length; index += 1) {
23
+ const token = tokens[index];
24
+ if (token === "--timeout-ms") {
25
+ const value = tokens[index + 1];
26
+ if (value === undefined) {
27
+ throw new Error("Missing value for --timeout-ms.");
28
+ }
29
+ timeoutMs = parsePositiveInteger(value, "--timeout-ms");
30
+ index += 1;
31
+ continue;
32
+ }
33
+
34
+ if (token === "--poll-ms") {
35
+ const value = tokens[index + 1];
36
+ if (value === undefined) {
37
+ throw new Error("Missing value for --poll-ms.");
38
+ }
39
+ pollMs = parsePositiveInteger(value, "--poll-ms");
40
+ index += 1;
41
+ continue;
42
+ }
43
+
44
+ if (token.startsWith("--")) {
45
+ throw new Error(`Unknown wait-url option: ${token}`);
46
+ }
47
+
48
+ if (timeoutMs === undefined) {
49
+ timeoutMs = parsePositiveInteger(token, "timeoutMs");
50
+ continue;
51
+ }
52
+
53
+ throw new Error(`Unknown wait-url option: ${token}`);
54
+ }
55
+
56
+ return {
57
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
58
+ ...(pollMs !== undefined ? { pollMs } : {})
59
+ };
60
+ }
61
+
62
+ export async function runWaitUrlCommand(args: string[]): Promise<WaitUrlCommandResult> {
63
+ const context = parseCommandContext(args);
64
+ const targetId = requirePositionalArg(context, 0, "targetId");
65
+ const urlPattern = requirePositionalArg(context, 1, "urlPattern");
66
+ const options = parseWaitOptions(context.positional.slice(2));
67
+
68
+ return await callDaemonTool<WaitUrlCommandResult>(
69
+ "browser.wait.url",
70
+ buildToolArguments(context, {
71
+ targetId,
72
+ urlPattern,
73
+ ...options
74
+ })
75
+ );
76
+ }