ccqa 0.1.5 → 0.3.2
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/README.md +12 -7
- package/dist/bin/ccqa.d.mts +1 -0
- package/dist/bin/ccqa.mjs +1702 -0
- package/dist/package.json +48 -0
- package/dist/runtime/test-helpers.d.mts +22 -0
- package/dist/runtime/test-helpers.mjs +146 -0
- package/dist/runtime/vitest.config.d.mts +9837 -0
- package/dist/runtime/vitest.config.mjs +8 -0
- package/package.json +32 -11
- package/bin/ccqa.ts +0 -2
- package/src/claude/invoke.test.ts +0 -167
- package/src/claude/invoke.ts +0 -238
- package/src/cli/generate-setup.ts +0 -215
- package/src/cli/generate.ts +0 -224
- package/src/cli/index.ts +0 -25
- package/src/cli/logger.ts +0 -45
- package/src/cli/run.ts +0 -258
- package/src/cli/trace-setup.ts +0 -124
- package/src/cli/trace.test.ts +0 -233
- package/src/cli/trace.ts +0 -244
- package/src/codegen/actions-to-script.ts +0 -185
- package/src/prompts/codegen.ts +0 -73
- package/src/prompts/trace.ts +0 -278
- package/src/runtime/test-helpers.ts +0 -127
- package/src/spec/parser.test.ts +0 -135
- package/src/spec/parser.ts +0 -96
- package/src/store/index.test.ts +0 -107
- package/src/store/index.ts +0 -193
- package/src/types.test.ts +0 -96
- package/src/types.ts +0 -91
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccqa",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Browser test recorder powered by Claude Code and agent-browser",
|
|
6
6
|
"repository": {
|
|
@@ -12,14 +12,16 @@
|
|
|
12
12
|
"url": "https://github.com/shibukazu/ccqa/issues"
|
|
13
13
|
},
|
|
14
14
|
"bin": {
|
|
15
|
-
"ccqa": "./bin/ccqa.
|
|
15
|
+
"ccqa": "./dist/bin/ccqa.mjs"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
|
-
"./test-helpers":
|
|
18
|
+
"./test-helpers": {
|
|
19
|
+
"types": "./dist/runtime/test-helpers.d.mts",
|
|
20
|
+
"import": "./dist/runtime/test-helpers.mjs"
|
|
21
|
+
}
|
|
19
22
|
},
|
|
20
23
|
"files": [
|
|
21
|
-
"
|
|
22
|
-
"src/"
|
|
24
|
+
"dist/"
|
|
23
25
|
],
|
|
24
26
|
"dependencies": {
|
|
25
27
|
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
|
|
@@ -29,8 +31,8 @@
|
|
|
29
31
|
"zod": "^4.3.6"
|
|
30
32
|
},
|
|
31
33
|
"peerDependencies": {
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
+
"agent-browser": "*",
|
|
35
|
+
"vitest": ">=2.0.0"
|
|
34
36
|
},
|
|
35
37
|
"peerDependenciesMeta": {
|
|
36
38
|
"agent-browser": {
|
|
@@ -41,9 +43,28 @@
|
|
|
41
43
|
}
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
|
-
"@types/
|
|
46
|
+
"@types/node": "^22",
|
|
47
|
+
"tsdown": "^0.21.9",
|
|
48
|
+
"typescript": "^5",
|
|
49
|
+
"vitest": "^4.1.2"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsdown",
|
|
53
|
+
"dev": "node --experimental-strip-types ./bin/ccqa.ts",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:unit": "vitest run src/",
|
|
57
|
+
"test:e2e": "vitest run tests/e2e",
|
|
58
|
+
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=20"
|
|
62
|
+
},
|
|
63
|
+
"devEngines": {
|
|
64
|
+
"runtime": {
|
|
65
|
+
"name": "node",
|
|
66
|
+
"version": ">=22.6"
|
|
67
|
+
}
|
|
45
68
|
},
|
|
46
|
-
"
|
|
47
|
-
"agent-browser"
|
|
48
|
-
]
|
|
69
|
+
"packageManager": "pnpm@10.1.0"
|
|
49
70
|
}
|
package/bin/ccqa.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { extractAbActionFromBashCommand, isBlockedAbSubcommand, hasRefSelector, shellTokenize } from "./invoke.ts";
|
|
3
|
-
|
|
4
|
-
describe("extractAbActionFromBashCommand", () => {
|
|
5
|
-
test("returns null for non-agent-browser commands", () => {
|
|
6
|
-
expect(extractAbActionFromBashCommand("ls -la")).toBeNull();
|
|
7
|
-
expect(extractAbActionFromBashCommand("echo hello")).toBeNull();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test("parses cookies clear", () => {
|
|
11
|
-
expect(
|
|
12
|
-
extractAbActionFromBashCommand("agent-browser --session s1 cookies clear"),
|
|
13
|
-
).toBe("AB_ACTION|cookies_clear");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("parses open", () => {
|
|
17
|
-
expect(
|
|
18
|
-
extractAbActionFromBashCommand("agent-browser --session s1 open https://example.com"),
|
|
19
|
-
).toBe("AB_ACTION|open|https://example.com");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("parses click with quoted selector", () => {
|
|
23
|
-
expect(
|
|
24
|
-
extractAbActionFromBashCommand(`agent-browser --session s1 click "[aria-label='Submit']" "Submit"`),
|
|
25
|
-
).toBe("AB_ACTION|click|[aria-label='Submit']|Submit");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("parses fill", () => {
|
|
29
|
-
expect(
|
|
30
|
-
extractAbActionFromBashCommand(`agent-browser --session s1 fill "[placeholder='Email']" "test@example.com" "Email"`),
|
|
31
|
-
).toBe("AB_ACTION|fill|[placeholder='Email']|test@example.com|Email");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("parses snapshot as null", () => {
|
|
35
|
-
expect(
|
|
36
|
-
extractAbActionFromBashCommand("agent-browser --session s1 snapshot"),
|
|
37
|
-
).toBeNull();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("parses press", () => {
|
|
41
|
-
expect(
|
|
42
|
-
extractAbActionFromBashCommand("agent-browser --session s1 press Enter"),
|
|
43
|
-
).toBe("AB_ACTION|press|Enter");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("parses wait", () => {
|
|
47
|
-
expect(
|
|
48
|
-
extractAbActionFromBashCommand(`agent-browser --session s1 wait --text "Done"`),
|
|
49
|
-
).toBe("AB_ACTION|wait|--text|Done");
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("isBlockedAbSubcommand", () => {
|
|
54
|
-
test("blocks eval with session", () => {
|
|
55
|
-
expect(isBlockedAbSubcommand(`agent-browser --session s1 eval "document.querySelector('.btn').click()"`)).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("blocks js with session", () => {
|
|
59
|
-
expect(isBlockedAbSubcommand(`agent-browser --session s1 js "window.scrollTo(0, 100)"`)).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("blocks eval without session flag", () => {
|
|
63
|
-
expect(isBlockedAbSubcommand(`agent-browser eval "document.click()"`)).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("does not block click", () => {
|
|
67
|
-
expect(isBlockedAbSubcommand(`agent-browser --session s1 click "[aria-label='Submit']"`)).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("does not block snapshot", () => {
|
|
71
|
-
expect(isBlockedAbSubcommand("agent-browser --session s1 snapshot")).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("does not block fill", () => {
|
|
75
|
-
expect(isBlockedAbSubcommand(`agent-browser --session s1 fill "[placeholder='Email']" "test@example.com"`)).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("blocks find command", () => {
|
|
79
|
-
expect(isBlockedAbSubcommand(`agent-browser --session s1 find text "Select category" click`)).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("blocks label command", () => {
|
|
83
|
-
expect(isBlockedAbSubcommand(`agent-browser --session s1 label "Settings" click`)).toBe(true);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("blocks textbox command", () => {
|
|
87
|
-
expect(isBlockedAbSubcommand(`agent-browser --session s1 textbox "Search"`)).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("does not block non-agent-browser commands", () => {
|
|
91
|
-
expect(isBlockedAbSubcommand("ls -la")).toBe(false);
|
|
92
|
-
expect(isBlockedAbSubcommand("echo hello")).toBe(false);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe("hasRefSelector", () => {
|
|
97
|
-
test("detects @ref in click", () => {
|
|
98
|
-
expect(hasRefSelector(`agent-browser --session s1 click "@e14"`)).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("detects @ref in check", () => {
|
|
102
|
-
expect(hasRefSelector(`agent-browser --session s1 check @e7`)).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("detects @ref in fill", () => {
|
|
106
|
-
expect(hasRefSelector(`agent-browser --session s1 fill "@e6" "value"`)).toBe(true);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("does not flag aria-label selector", () => {
|
|
110
|
-
expect(hasRefSelector(`agent-browser --session s1 click "[aria-label='Submit']"`)).toBe(false);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("does not flag text= selector", () => {
|
|
114
|
-
expect(hasRefSelector(`agent-browser --session s1 click "text=Select category"`)).toBe(false);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("does not flag non-agent-browser commands", () => {
|
|
118
|
-
expect(hasRefSelector("ls -la")).toBe(false);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("does not flag snapshot (no args)", () => {
|
|
122
|
-
expect(hasRefSelector("agent-browser --session s1 snapshot")).toBe(false);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe("shellTokenize", () => {
|
|
127
|
-
test("splits simple tokens", () => {
|
|
128
|
-
expect(shellTokenize("click foo bar")).toEqual(["click", "foo", "bar"]);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test("preserves spaces inside double quotes", () => {
|
|
132
|
-
expect(shellTokenize(`click "[role='dialog'] button:last-child"`)).toEqual([
|
|
133
|
-
"click",
|
|
134
|
-
"[role='dialog'] button:last-child",
|
|
135
|
-
]);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("preserves spaces inside single quotes", () => {
|
|
139
|
-
expect(shellTokenize("fill 'text=hello world' value")).toEqual([
|
|
140
|
-
"fill",
|
|
141
|
-
"text=hello world",
|
|
142
|
-
"value",
|
|
143
|
-
]);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("handles empty string", () => {
|
|
147
|
-
expect(shellTokenize("")).toEqual([]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("handles multiple spaces", () => {
|
|
151
|
-
expect(shellTokenize("click foo")).toEqual(["click", "foo"]);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe("extractAbActionFromBashCommand with compound selectors", () => {
|
|
156
|
-
test("parses click with compound selector containing spaces", () => {
|
|
157
|
-
expect(
|
|
158
|
-
extractAbActionFromBashCommand(`agent-browser --session s1 click "[role='dialog'] button:last-child"`),
|
|
159
|
-
).toBe("AB_ACTION|click|[role='dialog'] button:last-child|");
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("parses fill with placeholder containing spaces", () => {
|
|
163
|
-
expect(
|
|
164
|
-
extractAbActionFromBashCommand(`agent-browser --session s1 fill ".modal-footer button" "OK"`),
|
|
165
|
-
).toBe("AB_ACTION|fill|.modal-footer button|OK|");
|
|
166
|
-
});
|
|
167
|
-
});
|
package/src/claude/invoke.ts
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
import type { SDKMessage, Options, HookInput } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
-
import * as log from "../cli/logger.ts";
|
|
4
|
-
|
|
5
|
-
export interface ClaudeInvokeOptions {
|
|
6
|
-
prompt: string;
|
|
7
|
-
systemPrompt?: string;
|
|
8
|
-
allowedTools?: string[];
|
|
9
|
-
disableBuiltinTools?: boolean;
|
|
10
|
-
mcpConfigPath?: string;
|
|
11
|
-
maxTurns?: number;
|
|
12
|
-
env?: Record<string, string>;
|
|
13
|
-
/** Called when an agent-browser command is intercepted; receives the AB_ACTION line. */
|
|
14
|
-
onAbAction?: (abAction: string) => void;
|
|
15
|
-
/** Called when an agent-browser command fails (exit non-zero); allows rolling back the last AB_ACTION. */
|
|
16
|
-
onAbActionFailed?: () => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function invokeClaudeStreaming(
|
|
20
|
-
options: ClaudeInvokeOptions,
|
|
21
|
-
onEvent: (msg: SDKMessage) => void,
|
|
22
|
-
): Promise<{ result: string; isError: boolean }> {
|
|
23
|
-
const {
|
|
24
|
-
prompt,
|
|
25
|
-
systemPrompt,
|
|
26
|
-
allowedTools,
|
|
27
|
-
disableBuiltinTools = false,
|
|
28
|
-
maxTurns,
|
|
29
|
-
env,
|
|
30
|
-
onAbAction,
|
|
31
|
-
onAbActionFailed,
|
|
32
|
-
} = options;
|
|
33
|
-
|
|
34
|
-
// Track the last agent-browser tool_use_id so PostToolUseFailure can roll back
|
|
35
|
-
let lastAbToolUseId: string | null = null;
|
|
36
|
-
|
|
37
|
-
const sdkOptions: Options = {
|
|
38
|
-
systemPrompt,
|
|
39
|
-
maxTurns,
|
|
40
|
-
allowedTools: allowedTools ?? ["Bash(*)"],
|
|
41
|
-
permissionMode: "bypassPermissions",
|
|
42
|
-
allowDangerouslySkipPermissions: true,
|
|
43
|
-
...(env ? { env: { ...process.env, ...env } as Record<string, string | undefined> } : {}),
|
|
44
|
-
...(disableBuiltinTools ? { tools: [] } : {}),
|
|
45
|
-
hooks:
|
|
46
|
-
onAbAction || onAbActionFailed
|
|
47
|
-
? {
|
|
48
|
-
PreToolUse: [
|
|
49
|
-
{
|
|
50
|
-
hooks: [
|
|
51
|
-
async (input: HookInput) => {
|
|
52
|
-
if (input.hook_event_name !== "PreToolUse") return {};
|
|
53
|
-
if (input.tool_name !== "Bash") return {};
|
|
54
|
-
const cmd = (input.tool_input as Record<string, unknown>)?.["command"];
|
|
55
|
-
if (typeof cmd !== "string") return {};
|
|
56
|
-
|
|
57
|
-
// Block eval/js/find/etc — they bypass structured action recording
|
|
58
|
-
if (isBlockedAbSubcommand(cmd)) {
|
|
59
|
-
return {
|
|
60
|
-
decision: "block",
|
|
61
|
-
reason: "This agent-browser subcommand is not allowed because it cannot be recorded as a structured test action. Use only the standard commands: click, check, fill, select, hover, press, wait. Take a fresh snapshot to find the correct selector.",
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Block @ref selectors — they are session-specific and not replayable
|
|
66
|
-
if (hasRefSelector(cmd)) {
|
|
67
|
-
return {
|
|
68
|
-
decision: "block",
|
|
69
|
-
reason: "@ref selectors (like @e14) are session-specific and change every run. They cannot be used in generated tests. Use one of the allowed selector formats instead: [aria-label='...'], text=..., [placeholder='...'], or [type='password']. Take a fresh snapshot and find the element's aria-label or visible text.",
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const ab = extractAbActionFromBashCommand(cmd);
|
|
74
|
-
if (ab && onAbAction) {
|
|
75
|
-
lastAbToolUseId = input.tool_use_id;
|
|
76
|
-
onAbAction(ab);
|
|
77
|
-
} else {
|
|
78
|
-
lastAbToolUseId = null;
|
|
79
|
-
}
|
|
80
|
-
return {};
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
PostToolUseFailure: [
|
|
86
|
-
{
|
|
87
|
-
hooks: [
|
|
88
|
-
async (input: HookInput) => {
|
|
89
|
-
if (input.hook_event_name !== "PostToolUseFailure") return {};
|
|
90
|
-
if (input.tool_name !== "Bash") return {};
|
|
91
|
-
// If the failed Bash command was the one that emitted an AB_ACTION, roll it back
|
|
92
|
-
if (input.tool_use_id === lastAbToolUseId && onAbActionFailed) {
|
|
93
|
-
onAbActionFailed();
|
|
94
|
-
lastAbToolUseId = null;
|
|
95
|
-
}
|
|
96
|
-
return {};
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
}
|
|
102
|
-
: undefined,
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
let result = "";
|
|
106
|
-
let isError = false;
|
|
107
|
-
|
|
108
|
-
const q = query({ prompt, options: sdkOptions });
|
|
109
|
-
|
|
110
|
-
for await (const msg of q) {
|
|
111
|
-
onEvent(msg);
|
|
112
|
-
|
|
113
|
-
if (msg.type === "assistant") {
|
|
114
|
-
for (const block of msg.message.content ?? []) {
|
|
115
|
-
if (block.type === "tool_use" && block.name === "Bash") {
|
|
116
|
-
const cmd = (block.input as Record<string, unknown>)?.["command"];
|
|
117
|
-
if (typeof cmd === "string") log.bash(cmd);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (msg.type === "result") {
|
|
123
|
-
result = msg.subtype === "success" ? msg.result : "";
|
|
124
|
-
isError = msg.is_error ?? false;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return { result, isError };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const BLOCKED_AB_SUBCOMMANDS = new Set(["eval", "js", "find", "label", "textbox"]);
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Shell-aware tokenizer: splits a command string into tokens respecting single/double quotes.
|
|
135
|
-
* e.g. `click "[role='dialog'] button:last-child"` → ["click", "[role='dialog'] button:last-child"]
|
|
136
|
-
*/
|
|
137
|
-
export function shellTokenize(s: string): string[] {
|
|
138
|
-
const tokens: string[] = [];
|
|
139
|
-
let cur = "";
|
|
140
|
-
let quote: '"' | "'" | null = null;
|
|
141
|
-
for (let i = 0; i < s.length; i++) {
|
|
142
|
-
const ch = s[i]!;
|
|
143
|
-
if (quote) {
|
|
144
|
-
if (ch === quote) { quote = null; }
|
|
145
|
-
else { cur += ch; }
|
|
146
|
-
} else if (ch === '"' || ch === "'") {
|
|
147
|
-
quote = ch;
|
|
148
|
-
} else if (ch === " " || ch === "\t") {
|
|
149
|
-
if (cur) { tokens.push(cur); cur = ""; }
|
|
150
|
-
} else {
|
|
151
|
-
cur += ch;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (cur) tokens.push(cur);
|
|
155
|
-
return tokens;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Extracts the subcommand from an `agent-browser [flags] <subcommand> [args...]` command string. */
|
|
159
|
-
export function extractAbSubcommand(cmd: string): string | null {
|
|
160
|
-
const abIdx = cmd.indexOf("agent-browser");
|
|
161
|
-
if (abIdx === -1) return null;
|
|
162
|
-
const rest = cmd.slice(abIdx + "agent-browser".length).trim();
|
|
163
|
-
const parts = shellTokenize(rest);
|
|
164
|
-
let i = 0;
|
|
165
|
-
while (i < parts.length && parts[i]!.startsWith("-")) { i += 2; }
|
|
166
|
-
return parts[i] ?? null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Returns true if the agent-browser subcommand is blocked (eval/js/find/etc). */
|
|
170
|
-
export function isBlockedAbSubcommand(cmd: string): boolean {
|
|
171
|
-
const sub = extractAbSubcommand(cmd);
|
|
172
|
-
return sub !== null && BLOCKED_AB_SUBCOMMANDS.has(sub);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Returns true if any argument to an agent-browser command uses a @ref selector (e.g. @e14). */
|
|
176
|
-
export function hasRefSelector(cmd: string): boolean {
|
|
177
|
-
const abIdx = cmd.indexOf("agent-browser");
|
|
178
|
-
if (abIdx === -1) return false;
|
|
179
|
-
const rest = cmd.slice(abIdx + "agent-browser".length).trim();
|
|
180
|
-
const parts = shellTokenize(rest);
|
|
181
|
-
// Skip flags and subcommand, check remaining args
|
|
182
|
-
let i = 0;
|
|
183
|
-
while (i < parts.length && parts[i]!.startsWith("-")) { i += 2; }
|
|
184
|
-
i++; // skip subcommand
|
|
185
|
-
for (; i < parts.length; i++) {
|
|
186
|
-
if (/^@/.test(parts[i]!)) return true;
|
|
187
|
-
}
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Parse an `agent-browser --session <name> <cmd> [args...]` bash command
|
|
193
|
-
* and return the corresponding AB_ACTION line, or null if not an agent-browser call.
|
|
194
|
-
*/
|
|
195
|
-
export function extractAbActionFromBashCommand(cmd: string): string | null {
|
|
196
|
-
const subCmd = extractAbSubcommand(cmd);
|
|
197
|
-
if (!subCmd) return null;
|
|
198
|
-
|
|
199
|
-
// Extract everything after "agent-browser" to get args (shell-aware tokenization)
|
|
200
|
-
const abIdx = cmd.indexOf("agent-browser");
|
|
201
|
-
const rest = cmd.slice(abIdx + "agent-browser".length).trim();
|
|
202
|
-
// Filter out shell redirects/pipes (2>&1, >&1, |, >file) that are not agent-browser args
|
|
203
|
-
const parts = shellTokenize(rest).filter(t => !/^(2?>|[|&>])/.test(t));
|
|
204
|
-
let i = 0;
|
|
205
|
-
while (i < parts.length && parts[i]!.startsWith("-")) { i += 2; }
|
|
206
|
-
const args = parts.slice(i + 1);
|
|
207
|
-
|
|
208
|
-
switch (subCmd) {
|
|
209
|
-
case "cookies":
|
|
210
|
-
if (args[0] === "clear") return "AB_ACTION|cookies_clear";
|
|
211
|
-
return null;
|
|
212
|
-
case "open":
|
|
213
|
-
return `AB_ACTION|open|${args[0] ?? ""}`;
|
|
214
|
-
case "press":
|
|
215
|
-
return `AB_ACTION|press|${args[0] ?? ""}`;
|
|
216
|
-
case "scroll":
|
|
217
|
-
return `AB_ACTION|scroll|${args.join("|")}`;
|
|
218
|
-
case "click":
|
|
219
|
-
case "dblclick":
|
|
220
|
-
case "check":
|
|
221
|
-
case "uncheck":
|
|
222
|
-
case "hover":
|
|
223
|
-
case "wait":
|
|
224
|
-
return `AB_ACTION|${subCmd}|${args[0] ?? ""}|${args[1] ?? ""}`;
|
|
225
|
-
case "fill":
|
|
226
|
-
case "type":
|
|
227
|
-
case "select":
|
|
228
|
-
return `AB_ACTION|${subCmd}|${args[0] ?? ""}|${args[1] ?? ""}|${args[2] ?? ""}`;
|
|
229
|
-
case "drag":
|
|
230
|
-
return `AB_ACTION|drag|${args[0] ?? ""}|${args[1] ?? ""}|${args[2] ?? ""}`;
|
|
231
|
-
case "snapshot":
|
|
232
|
-
// snapshot AB_ACTION is emitted by LLM with its own observation
|
|
233
|
-
return null;
|
|
234
|
-
default:
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|