@agentxjs/devtools 2.0.0 → 3.0.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/bdd/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/bdd/cli.ts
4
4
  import { spawn } from "child_process";
5
- import { resolve, dirname, relative } from "path";
6
- import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
5
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
6
+ import { dirname, relative, resolve } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  var __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  function loadEnvFile(filePath) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bdd/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * BDD CLI wrapper for cucumber-js\n *\n * Usage:\n * bdd # Run all tests\n * bdd path/to/file.feature # Run specific feature file\n * bdd path/to/file.feature:10 # Run specific scenario by line\n * bdd --tags @contributor # Run specific tags\n * bdd --tags \"@dev and not @slow\" # Tag expression\n * bdd --name \"token usage\" # Filter by scenario name (regex)\n * bdd --dry-run # Validate without executing\n * bdd --config path # Custom config (default: bdd/cucumber.js)\n */\n\nimport { spawn } from \"node:child_process\";\nimport { resolve, dirname, relative } from \"node:path\";\nimport { existsSync, readFileSync, writeFileSync, unlinkSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Load .env files (like dotenv but zero dependencies)\nfunction loadEnvFile(filePath: string) {\n if (!existsSync(filePath)) return;\n const content = readFileSync(filePath, \"utf-8\");\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n const key = trimmed.slice(0, eqIndex).trim();\n let value = trimmed.slice(eqIndex + 1).trim();\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n}\n\n// Find monorepo root by walking up to find the root package.json with workspaces\nfunction findMonorepoRoot(startDir: string): string | null {\n let dir = startDir;\n while (true) {\n const pkgPath = resolve(dir, \"package.json\");\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n if (pkg.workspaces) return dir;\n } catch {\n // ignore parse errors\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nconst cwd = process.cwd();\n\n// Load .env files from cwd first, then monorepo root\nloadEnvFile(resolve(cwd, \".env\"));\nloadEnvFile(resolve(cwd, \".env.local\"));\n\nconst monorepoRoot = findMonorepoRoot(cwd);\nif (monorepoRoot && monorepoRoot !== cwd) {\n loadEnvFile(resolve(monorepoRoot, \".env\"));\n loadEnvFile(resolve(monorepoRoot, \".env.local\"));\n}\n\nconst args = process.argv.slice(2);\n\n// Extract --config\nlet configPath = \"bdd/cucumber.js\";\nconst configIndex = args.indexOf(\"--config\");\nif (configIndex !== -1 && args[configIndex + 1]) {\n configPath = args[configIndex + 1];\n args.splice(configIndex, 2);\n}\n\n// Check if config exists\nconst fullConfigPath = resolve(cwd, configPath);\nif (!existsSync(fullConfigPath)) {\n console.error(`Config not found: ${fullConfigPath}`);\n console.error(\"Create bdd/cucumber.js or specify --config path\");\n process.exit(1);\n}\n\n// Separate positional args (feature files/lines) from flags\nconst featurePaths: string[] = [];\nconst flags: string[] = [];\n\nfor (const arg of args) {\n if (arg.startsWith(\"-\")) {\n flags.push(arg);\n } else if (arg.endsWith(\".feature\") || arg.includes(\".feature:\")) {\n featurePaths.push(arg);\n } else {\n // Could be a flag value (e.g. after --tags), keep as-is\n flags.push(arg);\n }\n}\n\n// Find cucumber-js binary\nconst cucumberPaths = [\n resolve(cwd, \"node_modules/.bin/cucumber-js\"),\n resolve(__dirname, \"../../../.bin/cucumber-js\"),\n \"cucumber-js\",\n];\nconst cucumberBin =\n cucumberPaths.find((p) => p === \"cucumber-js\" || existsSync(p)) || \"cucumber-js\";\n\nconst rootNodeModules = resolve(cwd, \"node_modules\");\n\n// When feature paths are specified, generate a temp config that overrides\n// the original config's `paths` — cucumber-js config.paths takes precedence\n// over positional args, so we must override it in the config itself.\nlet effectiveConfig = configPath;\nlet tempConfigPath: string | null = null;\n\nif (featurePaths.length > 0) {\n const configRelPath = relative(\n dirname(resolve(cwd, \"bdd/.tmp-cucumber.js\")),\n fullConfigPath\n ).replace(/\\\\/g, \"/\");\n const pathsJson = JSON.stringify(featurePaths);\n const tempContent = [\n `import config from \"./${configRelPath}\";`,\n `export default { ...config.default ?? config, paths: ${pathsJson} };`,\n \"\",\n ].join(\"\\n\");\n\n tempConfigPath = resolve(cwd, \"bdd/.tmp-cucumber.js\");\n writeFileSync(tempConfigPath, tempContent);\n effectiveConfig = \"bdd/.tmp-cucumber.js\";\n}\n\n// Build cucumber args\nconst cucumberArgs = [\"--config\", effectiveConfig, ...flags];\n\nconst child = spawn(cucumberBin, cucumberArgs, {\n stdio: \"inherit\",\n env: {\n ...process.env,\n NODE_OPTIONS: \"--import tsx\",\n NODE_PATH: rootNodeModules,\n },\n});\n\nchild.on(\"close\", (code) => {\n // Clean up temp config\n if (tempConfigPath && existsSync(tempConfigPath)) {\n try {\n unlinkSync(tempConfigPath);\n } catch {\n // ignore cleanup errors\n }\n }\n process.exit(code ?? 0);\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/bdd/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * BDD CLI wrapper for cucumber-js\n *\n * Usage:\n * bdd # Run all tests\n * bdd path/to/file.feature # Run specific feature file\n * bdd path/to/file.feature:10 # Run specific scenario by line\n * bdd --tags @contributor # Run specific tags\n * bdd --tags \"@dev and not @slow\" # Tag expression\n * bdd --name \"token usage\" # Filter by scenario name (regex)\n * bdd --dry-run # Validate without executing\n * bdd --config path # Custom config (default: bdd/cucumber.js)\n */\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { dirname, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Load .env files (like dotenv but zero dependencies)\nfunction loadEnvFile(filePath: string) {\n if (!existsSync(filePath)) return;\n const content = readFileSync(filePath, \"utf-8\");\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n const key = trimmed.slice(0, eqIndex).trim();\n let value = trimmed.slice(eqIndex + 1).trim();\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n}\n\n// Find monorepo root by walking up to find the root package.json with workspaces\nfunction findMonorepoRoot(startDir: string): string | null {\n let dir = startDir;\n while (true) {\n const pkgPath = resolve(dir, \"package.json\");\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n if (pkg.workspaces) return dir;\n } catch {\n // ignore parse errors\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nconst cwd = process.cwd();\n\n// Load .env files from cwd first, then monorepo root\nloadEnvFile(resolve(cwd, \".env\"));\nloadEnvFile(resolve(cwd, \".env.local\"));\n\nconst monorepoRoot = findMonorepoRoot(cwd);\nif (monorepoRoot && monorepoRoot !== cwd) {\n loadEnvFile(resolve(monorepoRoot, \".env\"));\n loadEnvFile(resolve(monorepoRoot, \".env.local\"));\n}\n\nconst args = process.argv.slice(2);\n\n// Extract --config\nlet configPath = \"bdd/cucumber.js\";\nconst configIndex = args.indexOf(\"--config\");\nif (configIndex !== -1 && args[configIndex + 1]) {\n configPath = args[configIndex + 1];\n args.splice(configIndex, 2);\n}\n\n// Check if config exists\nconst fullConfigPath = resolve(cwd, configPath);\nif (!existsSync(fullConfigPath)) {\n console.error(`Config not found: ${fullConfigPath}`);\n console.error(\"Create bdd/cucumber.js or specify --config path\");\n process.exit(1);\n}\n\n// Separate positional args (feature files/lines) from flags\nconst featurePaths: string[] = [];\nconst flags: string[] = [];\n\nfor (const arg of args) {\n if (arg.startsWith(\"-\")) {\n flags.push(arg);\n } else if (arg.endsWith(\".feature\") || arg.includes(\".feature:\")) {\n featurePaths.push(arg);\n } else {\n // Could be a flag value (e.g. after --tags), keep as-is\n flags.push(arg);\n }\n}\n\n// Find cucumber-js binary\nconst cucumberPaths = [\n resolve(cwd, \"node_modules/.bin/cucumber-js\"),\n resolve(__dirname, \"../../../.bin/cucumber-js\"),\n \"cucumber-js\",\n];\nconst cucumberBin =\n cucumberPaths.find((p) => p === \"cucumber-js\" || existsSync(p)) || \"cucumber-js\";\n\nconst rootNodeModules = resolve(cwd, \"node_modules\");\n\n// When feature paths are specified, generate a temp config that overrides\n// the original config's `paths` — cucumber-js config.paths takes precedence\n// over positional args, so we must override it in the config itself.\nlet effectiveConfig = configPath;\nlet tempConfigPath: string | null = null;\n\nif (featurePaths.length > 0) {\n const configRelPath = relative(\n dirname(resolve(cwd, \"bdd/.tmp-cucumber.js\")),\n fullConfigPath\n ).replace(/\\\\/g, \"/\");\n const pathsJson = JSON.stringify(featurePaths);\n const tempContent = [\n `import config from \"./${configRelPath}\";`,\n `export default { ...config.default ?? config, paths: ${pathsJson} };`,\n \"\",\n ].join(\"\\n\");\n\n tempConfigPath = resolve(cwd, \"bdd/.tmp-cucumber.js\");\n writeFileSync(tempConfigPath, tempContent);\n effectiveConfig = \"bdd/.tmp-cucumber.js\";\n}\n\n// Build cucumber args\nconst cucumberArgs = [\"--config\", effectiveConfig, ...flags];\n\nconst child = spawn(cucumberBin, cucumberArgs, {\n stdio: \"inherit\",\n env: {\n ...process.env,\n NODE_OPTIONS: \"--import tsx\",\n NODE_PATH: rootNodeModules,\n },\n});\n\nchild.on(\"close\", (code) => {\n // Clean up temp config\n if (tempConfigPath && existsSync(tempConfigPath)) {\n try {\n unlinkSync(tempConfigPath);\n } catch {\n // ignore cleanup errors\n }\n }\n process.exit(code ?? 0);\n});\n"],"mappings":"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","names":[]}
@@ -1,5 +1,50 @@
1
- import { Browser, Page } from '@playwright/test';
2
1
  import { ChildProcess } from 'node:child_process';
2
+ import { Page, Browser } from '@playwright/test';
3
+
4
+ interface DocTestResult {
5
+ passed: boolean;
6
+ output: string;
7
+ }
8
+ interface DocTesterOptions {
9
+ /** LLM provider (default: "anthropic") */
10
+ provider?: string;
11
+ /** Model name */
12
+ model?: string;
13
+ /** API key (reads from env if not provided) */
14
+ apiKey?: string;
15
+ /** Base URL (reads from env if not provided) */
16
+ baseUrl?: string;
17
+ /** Timeout in ms */
18
+ timeout?: number;
19
+ }
20
+ /**
21
+ * Evaluate a document against requirements using AgentX.
22
+ *
23
+ * Uses agentxjs local mode — no subprocess, no CLI, no auth issues.
24
+ * Requires `agentxjs` as a peer dependency.
25
+ */
26
+ declare function agentDocTester(options: {
27
+ files: string[];
28
+ requirements: string;
29
+ }, testerOptions?: DocTesterOptions): Promise<DocTestResult>;
30
+
31
+ interface UiTestResult {
32
+ passed: boolean;
33
+ output: string;
34
+ }
35
+ interface UiTesterOptions {
36
+ model?: string;
37
+ baseUrl?: string;
38
+ timeout?: number;
39
+ /** Show browser window (default: false) */
40
+ headed?: boolean;
41
+ }
42
+ /**
43
+ * Run a UI test scenario using Claude Code CLI + agent-browser.
44
+ *
45
+ * BDD scripts must run under Node.js (not Bun) to avoid claude CLI auth bug.
46
+ */
47
+ declare function agentUiTester(prompt: string, options?: UiTesterOptions): UiTestResult;
3
48
 
4
49
  /**
5
50
  * Shared Cucumber configuration for BDD tests
@@ -40,49 +85,6 @@ declare function createCucumberConfig(options: CucumberConfigOptions): {
40
85
  };
41
86
  };
42
87
 
43
- /**
44
- * Playwright utilities for BDD testing
45
- *
46
- * Uses system Chrome to avoid downloading Chromium.
47
- * Install: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 bun add -d @playwright/test
48
- *
49
- * Browser lifecycle:
50
- * - Single browser instance for all tests
51
- * - Single page (tab) reused across scenarios
52
- * - resetPage() clears state between scenarios
53
- */
54
-
55
- interface BrowserOptions {
56
- headless?: boolean;
57
- slowMo?: number;
58
- }
59
- /**
60
- * Launch browser using system Chrome (singleton)
61
- */
62
- declare function launchBrowser(options?: BrowserOptions): Promise<Browser>;
63
- /**
64
- * Get or create a page (singleton, reused across scenarios)
65
- */
66
- declare function getPage(): Promise<Page>;
67
- /**
68
- * Reset page state between scenarios (without closing)
69
- * Use this instead of closePage() for faster tests
70
- */
71
- declare function resetPage(): Promise<void>;
72
- /**
73
- * Close current page
74
- * @deprecated Use resetPage() for faster tests. Only use closePage() if you need full isolation.
75
- */
76
- declare function closePage(): Promise<void>;
77
- /**
78
- * Close browser and cleanup
79
- */
80
- declare function closeBrowser(): Promise<void>;
81
- /**
82
- * Wait for a URL to be accessible
83
- */
84
- declare function waitForUrl(url: string, timeout?: number): Promise<boolean>;
85
-
86
88
  /**
87
89
  * Dev server utilities for BDD testing
88
90
  *
@@ -154,49 +156,47 @@ declare const paths: {
154
156
  reset: typeof resetPaths;
155
157
  };
156
158
 
157
- interface UiTestResult {
158
- passed: boolean;
159
- output: string;
160
- }
161
- interface UiTesterOptions {
162
- model?: string;
163
- baseUrl?: string;
164
- timeout?: number;
165
- /** Show browser window (default: false) */
166
- headed?: boolean;
167
- }
168
159
  /**
169
- * Run a UI test scenario using Claude Code CLI + agent-browser.
160
+ * Playwright utilities for BDD testing
170
161
  *
171
- * BDD scripts must run under Node.js (not Bun) to avoid claude CLI auth bug.
162
+ * Uses system Chrome to avoid downloading Chromium.
163
+ * Install: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 bun add -d @playwright/test
164
+ *
165
+ * Browser lifecycle:
166
+ * - Single browser instance for all tests
167
+ * - Single page (tab) reused across scenarios
168
+ * - resetPage() clears state between scenarios
172
169
  */
173
- declare function agentUiTester(prompt: string, options?: UiTesterOptions): UiTestResult;
174
170
 
175
- interface DocTestResult {
176
- passed: boolean;
177
- output: string;
178
- }
179
- interface DocTesterOptions {
180
- /** LLM provider (default: "anthropic") */
181
- provider?: string;
182
- /** Model name */
183
- model?: string;
184
- /** API key (reads from env if not provided) */
185
- apiKey?: string;
186
- /** Base URL (reads from env if not provided) */
187
- baseUrl?: string;
188
- /** Timeout in ms */
189
- timeout?: number;
171
+ interface BrowserOptions {
172
+ headless?: boolean;
173
+ slowMo?: number;
190
174
  }
191
175
  /**
192
- * Evaluate a document against requirements using AgentX.
193
- *
194
- * Uses agentxjs local mode — no subprocess, no CLI, no auth issues.
195
- * Requires `agentxjs` as a peer dependency.
176
+ * Launch browser using system Chrome (singleton)
196
177
  */
197
- declare function agentDocTester(options: {
198
- files: string[];
199
- requirements: string;
200
- }, testerOptions?: DocTesterOptions): Promise<DocTestResult>;
178
+ declare function launchBrowser(options?: BrowserOptions): Promise<Browser>;
179
+ /**
180
+ * Get or create a page (singleton, reused across scenarios)
181
+ */
182
+ declare function getPage(): Promise<Page>;
183
+ /**
184
+ * Reset page state between scenarios (without closing)
185
+ * Use this instead of closePage() for faster tests
186
+ */
187
+ declare function resetPage(): Promise<void>;
188
+ /**
189
+ * Close current page
190
+ * @deprecated Use resetPage() for faster tests. Only use closePage() if you need full isolation.
191
+ */
192
+ declare function closePage(): Promise<void>;
193
+ /**
194
+ * Close browser and cleanup
195
+ */
196
+ declare function closeBrowser(): Promise<void>;
197
+ /**
198
+ * Wait for a URL to be accessible
199
+ */
200
+ declare function waitForUrl(url: string, timeout?: number): Promise<boolean>;
201
201
 
202
202
  export { type BrowserOptions, type CucumberConfigOptions, type DevServerOptions, type DocTestResult, type DocTesterOptions, type UiTestResult, type UiTesterOptions, agentDocTester, agentUiTester, closeBrowser, closePage, createCucumberConfig, ensureDir, getBddPath, getDevServer, getFixturesPath, getMonorepoPath, getPackagePath, getPage, getTempPath, launchBrowser, paths, resetPage, resetPaths, startDevServer, stopDevServer, waitForUrl };
package/dist/bdd/index.js CHANGED
@@ -1,10 +1,159 @@
1
1
  import {
2
2
  env
3
- } from "../chunk-S7J75AXG.js";
3
+ } from "../chunk-Y6RXZINS.js";
4
4
  import {
5
5
  __require
6
6
  } from "../chunk-DGUM43GV.js";
7
7
 
8
+ // src/bdd/agent-doc-tester.ts
9
+ import { existsSync, readFileSync } from "fs";
10
+ var SYSTEM_PROMPT = `You are a documentation reviewer evaluating documents from the reader's experience.
11
+
12
+ EVALUATION DIMENSIONS:
13
+ 1. Completeness \u2014 All required information is present. Nothing critical is missing.
14
+ 2. Logic \u2014 Structure flows naturally. Concepts build on each other without jumps.
15
+ 3. Readability \u2014 A newcomer can follow without confusion. No unexplained jargon.
16
+
17
+ RULES:
18
+ - Read the provided document carefully
19
+ - Evaluate each requirement listed in the prompt against ALL three dimensions
20
+ - Be strict but fair \u2014 the document should genuinely help the reader achieve the stated goal
21
+ - Output your result as a single line: PASS or FAIL followed by a brief reason
22
+ - If FAIL, list which specific requirements are not met and which dimension they violate`;
23
+ async function agentDocTester(options, testerOptions = {}) {
24
+ const {
25
+ provider = process.env.AGENTX_PROVIDER || "anthropic",
26
+ model = env.model,
27
+ apiKey = env.apiKey || "",
28
+ baseUrl = env.baseUrl,
29
+ timeout = 12e4
30
+ } = testerOptions;
31
+ const docContents = options.files.map((filePath) => {
32
+ if (!existsSync(filePath)) {
33
+ return `--- ${filePath} ---
34
+ [FILE NOT FOUND]`;
35
+ }
36
+ return `--- ${filePath} ---
37
+ ${readFileSync(filePath, "utf-8")}`;
38
+ }).join("\n\n");
39
+ const userPrompt = [
40
+ "Evaluate the following document(s) against the requirements below.",
41
+ "",
42
+ "DOCUMENTS:",
43
+ docContents,
44
+ "",
45
+ "REQUIREMENTS:",
46
+ options.requirements,
47
+ "",
48
+ "Evaluate each requirement. Output PASS if all are met, FAIL if any are not."
49
+ ].join("\n");
50
+ const moduleName = "agentxjs";
51
+ const agentxjs = await import(
52
+ /* @vite-ignore */
53
+ moduleName
54
+ );
55
+ const createAgentX = agentxjs.createAgentX;
56
+ let agentx = null;
57
+ try {
58
+ agentx = await createAgentX({
59
+ apiKey,
60
+ provider,
61
+ model,
62
+ baseUrl,
63
+ logLevel: "silent"
64
+ });
65
+ await agentx.containers.create("doc-tester");
66
+ const { record: image } = await agentx.images.create({
67
+ containerId: "doc-tester",
68
+ systemPrompt: SYSTEM_PROMPT
69
+ });
70
+ const { agentId } = await agentx.agents.create({ imageId: image.imageId });
71
+ let output = "";
72
+ agentx.on("text_delta", (e) => {
73
+ output += e.data.text;
74
+ });
75
+ await Promise.race([
76
+ agentx.sessions.send(agentId, userPrompt),
77
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout))
78
+ ]);
79
+ output = output.trim();
80
+ const passed = /\*{0,2}PASS\*{0,2}\b/m.test(output);
81
+ return { passed, output };
82
+ } catch (error) {
83
+ return { passed: false, output: error.message || "Unknown error" };
84
+ } finally {
85
+ if (agentx) {
86
+ try {
87
+ await agentx.shutdown();
88
+ } catch {
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ // src/bdd/agent-ui-tester.ts
95
+ import { execFileSync } from "child_process";
96
+ import { readFileSync as readFileSync2 } from "fs";
97
+ import { dirname, resolve } from "path";
98
+ import { fileURLToPath } from "url";
99
+ var __dirname = dirname(fileURLToPath(import.meta.url));
100
+ var SKILL_PATH = resolve(__dirname, "../../../../.claude/skills/agent-browser/SKILL.md");
101
+ function loadSystemPrompt(headed = false) {
102
+ let skillContent = "";
103
+ try {
104
+ skillContent = readFileSync2(SKILL_PATH, "utf-8");
105
+ } catch {
106
+ }
107
+ return `You are a UI tester. You test web application scenarios using the agent-browser CLI.
108
+
109
+ RULES:
110
+ - ONLY use agent-browser commands via Bash tool
111
+ - Use ${headed ? "--headed " : ""}--executable-path "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" for all commands
112
+ - After each navigation or click, run: agent-browser snapshot -i
113
+ - Refs (@e1, @e2) are invalidated after page changes \u2014 always re-snapshot
114
+ - At the end, close the browser with: agent-browser close
115
+ - Output your result as a single line: PASS or FAIL followed by a brief reason
116
+
117
+ ${skillContent ? `AGENT-BROWSER REFERENCE:
118
+ ${skillContent}` : ""}`;
119
+ }
120
+ function agentUiTester(prompt, options = {}) {
121
+ const { model = "haiku", baseUrl, timeout = 3e5, headed = false } = options;
122
+ const fullPrompt = baseUrl ? `Base URL: ${baseUrl}
123
+
124
+ ${prompt}` : prompt;
125
+ const systemPrompt = loadSystemPrompt(headed);
126
+ const cleanEnv = Object.fromEntries(
127
+ Object.entries(process.env).filter(([k]) => !k.startsWith("CLAUDE"))
128
+ );
129
+ try {
130
+ const output = execFileSync(
131
+ "claude",
132
+ [
133
+ "-p",
134
+ fullPrompt,
135
+ "--model",
136
+ model,
137
+ "--append-system-prompt",
138
+ systemPrompt,
139
+ "--allowedTools",
140
+ "Bash(agent-browser:*)"
141
+ ],
142
+ {
143
+ encoding: "utf-8",
144
+ timeout,
145
+ env: cleanEnv,
146
+ maxBuffer: 10 * 1024 * 1024
147
+ }
148
+ ).trim();
149
+ const passed = /\*{0,2}PASS\*{0,2}\b/m.test(output);
150
+ return { passed, output };
151
+ } catch (error) {
152
+ const output = error.stdout || error.stderr || error.message || "Unknown error";
153
+ return { passed: false, output: output.trim() };
154
+ }
155
+ }
156
+
8
157
  // src/bdd/cucumber.config.ts
9
158
  function createCucumberConfig(options) {
10
159
  return {
@@ -19,6 +168,9 @@ function createCucumberConfig(options) {
19
168
  };
20
169
  }
21
170
 
171
+ // src/bdd/dev-server.ts
172
+ import { spawn } from "child_process";
173
+
22
174
  // src/bdd/playwright.ts
23
175
  import { chromium } from "@playwright/test";
24
176
  var browser = null;
@@ -82,7 +234,6 @@ async function waitForUrl(url, timeout = 3e4) {
82
234
  }
83
235
 
84
236
  // src/bdd/dev-server.ts
85
- import { spawn } from "child_process";
86
237
  var devServer = null;
87
238
  async function startDevServer(options) {
88
239
  if (devServer) return;
@@ -125,19 +276,19 @@ function getDevServer() {
125
276
  }
126
277
 
127
278
  // src/bdd/paths.ts
128
- import { resolve, dirname } from "path";
129
- import { existsSync, mkdtempSync, mkdirSync } from "fs";
279
+ import { existsSync as existsSync2, mkdirSync, mkdtempSync } from "fs";
130
280
  import { tmpdir } from "os";
281
+ import { dirname as dirname2, resolve as resolve2 } from "path";
131
282
  function findMonorepoRoot(startDir = process.cwd()) {
132
283
  let dir = startDir;
133
284
  while (dir !== "/") {
134
- const pkgPath = resolve(dir, "package.json");
135
- if (existsSync(pkgPath)) {
285
+ const pkgPath = resolve2(dir, "package.json");
286
+ if (existsSync2(pkgPath)) {
136
287
  try {
137
288
  const pkg = __require(pkgPath);
138
289
  if (pkg.workspaces || pkg.private === true) {
139
- const hasPackages = existsSync(resolve(dir, "packages"));
140
- const hasApps = existsSync(resolve(dir, "apps"));
290
+ const hasPackages = existsSync2(resolve2(dir, "packages"));
291
+ const hasApps = existsSync2(resolve2(dir, "apps"));
141
292
  if (hasPackages || hasApps) {
142
293
  return dir;
143
294
  }
@@ -145,17 +296,17 @@ function findMonorepoRoot(startDir = process.cwd()) {
145
296
  } catch {
146
297
  }
147
298
  }
148
- dir = dirname(dir);
299
+ dir = dirname2(dir);
149
300
  }
150
301
  return startDir;
151
302
  }
152
303
  function getPackageRoot(startDir = process.cwd()) {
153
304
  let dir = startDir;
154
305
  while (dir !== "/") {
155
- if (existsSync(resolve(dir, "package.json"))) {
306
+ if (existsSync2(resolve2(dir, "package.json"))) {
156
307
  return dir;
157
308
  }
158
- dir = dirname(dir);
309
+ dir = dirname2(dir);
159
310
  }
160
311
  return startDir;
161
312
  }
@@ -175,20 +326,20 @@ function getPackagePath() {
175
326
  return _packageRoot;
176
327
  }
177
328
  function getBddPath() {
178
- return resolve(getPackagePath(), "bdd");
329
+ return resolve2(getPackagePath(), "bdd");
179
330
  }
180
331
  function getFixturesPath(subdir) {
181
- const base = resolve(getBddPath(), "fixtures");
182
- return subdir ? resolve(base, subdir) : base;
332
+ const base = resolve2(getBddPath(), "fixtures");
333
+ return subdir ? resolve2(base, subdir) : base;
183
334
  }
184
335
  function getTempPath(prefix = "bdd-") {
185
336
  if (!_tempDir) {
186
- _tempDir = mkdtempSync(resolve(tmpdir(), prefix));
337
+ _tempDir = mkdtempSync(resolve2(tmpdir(), prefix));
187
338
  }
188
339
  return _tempDir;
189
340
  }
190
341
  function ensureDir(path) {
191
- if (!existsSync(path)) {
342
+ if (!existsSync2(path)) {
192
343
  mkdirSync(path, { recursive: true });
193
344
  }
194
345
  return path;
@@ -207,155 +358,6 @@ var paths = {
207
358
  ensure: ensureDir,
208
359
  reset: resetPaths
209
360
  };
210
-
211
- // src/bdd/agent-ui-tester.ts
212
- import { execFileSync } from "child_process";
213
- import { readFileSync } from "fs";
214
- import { resolve as resolve2, dirname as dirname2 } from "path";
215
- import { fileURLToPath } from "url";
216
- var __dirname = dirname2(fileURLToPath(import.meta.url));
217
- var SKILL_PATH = resolve2(__dirname, "../../../../.claude/skills/agent-browser/SKILL.md");
218
- function loadSystemPrompt(headed = false) {
219
- let skillContent = "";
220
- try {
221
- skillContent = readFileSync(SKILL_PATH, "utf-8");
222
- } catch {
223
- }
224
- return `You are a UI tester. You test web application scenarios using the agent-browser CLI.
225
-
226
- RULES:
227
- - ONLY use agent-browser commands via Bash tool
228
- - Use ${headed ? "--headed " : ""}--executable-path "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" for all commands
229
- - After each navigation or click, run: agent-browser snapshot -i
230
- - Refs (@e1, @e2) are invalidated after page changes \u2014 always re-snapshot
231
- - At the end, close the browser with: agent-browser close
232
- - Output your result as a single line: PASS or FAIL followed by a brief reason
233
-
234
- ${skillContent ? `AGENT-BROWSER REFERENCE:
235
- ${skillContent}` : ""}`;
236
- }
237
- function agentUiTester(prompt, options = {}) {
238
- const { model = "haiku", baseUrl, timeout = 3e5, headed = false } = options;
239
- const fullPrompt = baseUrl ? `Base URL: ${baseUrl}
240
-
241
- ${prompt}` : prompt;
242
- const systemPrompt = loadSystemPrompt(headed);
243
- const cleanEnv = Object.fromEntries(
244
- Object.entries(process.env).filter(([k]) => !k.startsWith("CLAUDE"))
245
- );
246
- try {
247
- const output = execFileSync(
248
- "claude",
249
- [
250
- "-p",
251
- fullPrompt,
252
- "--model",
253
- model,
254
- "--append-system-prompt",
255
- systemPrompt,
256
- "--allowedTools",
257
- "Bash(agent-browser:*)"
258
- ],
259
- {
260
- encoding: "utf-8",
261
- timeout,
262
- env: cleanEnv,
263
- maxBuffer: 10 * 1024 * 1024
264
- }
265
- ).trim();
266
- const passed = /\*{0,2}PASS\*{0,2}\b/m.test(output);
267
- return { passed, output };
268
- } catch (error) {
269
- const output = error.stdout || error.stderr || error.message || "Unknown error";
270
- return { passed: false, output: output.trim() };
271
- }
272
- }
273
-
274
- // src/bdd/agent-doc-tester.ts
275
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
276
- var SYSTEM_PROMPT = `You are a documentation reviewer evaluating documents from the reader's experience.
277
-
278
- EVALUATION DIMENSIONS:
279
- 1. Completeness \u2014 All required information is present. Nothing critical is missing.
280
- 2. Logic \u2014 Structure flows naturally. Concepts build on each other without jumps.
281
- 3. Readability \u2014 A newcomer can follow without confusion. No unexplained jargon.
282
-
283
- RULES:
284
- - Read the provided document carefully
285
- - Evaluate each requirement listed in the prompt against ALL three dimensions
286
- - Be strict but fair \u2014 the document should genuinely help the reader achieve the stated goal
287
- - Output your result as a single line: PASS or FAIL followed by a brief reason
288
- - If FAIL, list which specific requirements are not met and which dimension they violate`;
289
- async function agentDocTester(options, testerOptions = {}) {
290
- const {
291
- provider = process.env.AGENTX_PROVIDER || "anthropic",
292
- model = env.model,
293
- apiKey = env.apiKey || "",
294
- baseUrl = env.baseUrl,
295
- timeout = 12e4
296
- } = testerOptions;
297
- const docContents = options.files.map((filePath) => {
298
- if (!existsSync2(filePath)) {
299
- return `--- ${filePath} ---
300
- [FILE NOT FOUND]`;
301
- }
302
- return `--- ${filePath} ---
303
- ${readFileSync2(filePath, "utf-8")}`;
304
- }).join("\n\n");
305
- const userPrompt = [
306
- "Evaluate the following document(s) against the requirements below.",
307
- "",
308
- "DOCUMENTS:",
309
- docContents,
310
- "",
311
- "REQUIREMENTS:",
312
- options.requirements,
313
- "",
314
- "Evaluate each requirement. Output PASS if all are met, FAIL if any are not."
315
- ].join("\n");
316
- const moduleName = "agentxjs";
317
- const agentxjs = await import(
318
- /* @vite-ignore */
319
- moduleName
320
- );
321
- const createAgentX = agentxjs.createAgentX;
322
- let agentx = null;
323
- try {
324
- agentx = await createAgentX({
325
- apiKey,
326
- provider,
327
- model,
328
- baseUrl,
329
- logLevel: "silent"
330
- });
331
- await agentx.containers.create("doc-tester");
332
- const { record: image } = await agentx.images.create({
333
- containerId: "doc-tester",
334
- systemPrompt: SYSTEM_PROMPT
335
- });
336
- const { agentId } = await agentx.agents.create({ imageId: image.imageId });
337
- let output = "";
338
- agentx.on("text_delta", (e) => {
339
- output += e.data.text;
340
- });
341
- await Promise.race([
342
- agentx.sessions.send(agentId, userPrompt),
343
- new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout))
344
- ]);
345
- output = output.trim();
346
- const passed = /\*{0,2}PASS\*{0,2}\b/m.test(output);
347
- return { passed, output };
348
- } catch (error) {
349
- return { passed: false, output: error.message || "Unknown error" };
350
- } finally {
351
- if (agentx) {
352
- try {
353
- await agentx.shutdown();
354
- } catch {
355
- }
356
- }
357
- }
358
- }
359
361
  export {
360
362
  agentDocTester,
361
363
  agentUiTester,