e2e-ai 1.3.0 → 1.4.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/README.md CHANGED
@@ -10,9 +10,12 @@ Includes a **codebase scanner** that builds a QA map of your application's featu
10
10
  # Install
11
11
  npm install e2e-ai
12
12
 
13
- # Initialize config + project context
13
+ # Initialize config + copy agents
14
14
  npx e2e-ai init
15
15
 
16
+ # Generate project context (use init-agent in your AI tool)
17
+ # → produces .e2e-ai/context.md
18
+
16
19
  # Show all commands
17
20
  npx e2e-ai --help
18
21
 
@@ -57,7 +60,7 @@ Scans your codebase structure (routes, components, hooks), uses AI to identify f
57
60
 
58
61
  ### `init` - Project Setup
59
62
 
60
- Interactive wizard that scans your codebase and generates configuration + project context.
63
+ Interactive wizard that creates `.e2e-ai/config.ts` and copies agent prompts to `.e2e-ai/agents/`.
61
64
 
62
65
  ```bash
63
66
  npx e2e-ai init
@@ -66,6 +69,8 @@ npx e2e-ai init
66
69
  npx e2e-ai init --non-interactive
67
70
  ```
68
71
 
72
+ After init, use the **init-agent** in your AI tool (Claude Code, Cursor, etc.) to generate `.e2e-ai/context.md`. This context file teaches all downstream agents about your project's test conventions. If you have the MCP server configured, the AI tool can call `e2e_ai_scan_codebase` to analyze your project automatically.
73
+
69
74
  ---
70
75
 
71
76
  ### `record` - Browser Recording
@@ -409,7 +414,7 @@ npx e2e-ai analyze
409
414
 
410
415
  ## Configuration
411
416
 
412
- Run `npx e2e-ai init` to generate `e2e-ai.config.ts`:
417
+ Run `npx e2e-ai init` to generate `.e2e-ai/config.ts`:
413
418
 
414
419
  ```typescript
415
420
  import { defineConfig } from 'e2e-ai/config';
@@ -463,12 +468,10 @@ export default defineConfig({
463
468
  retries: 0,
464
469
  traceMode: 'on',
465
470
  },
466
-
467
- contextFile: '.e2e-ai/context.md',
468
471
  });
469
472
  ```
470
473
 
471
- See `templates/e2e-ai.context.example.md` for the project context file format.
474
+ All configuration lives inside the `.e2e-ai/` directory no files at project root.
472
475
 
473
476
  ## Global Options
474
477
 
@@ -524,7 +527,7 @@ You can customize agent behavior by editing the `.md` files directly. The frontm
524
527
 
525
528
  ## Output Directory Structure
526
529
 
527
- Default paths (configurable via `e2e-ai.config.ts`):
530
+ Default paths (configurable via `.e2e-ai/config.ts`):
528
531
 
529
532
  ```
530
533
  e2e/
@@ -534,12 +537,85 @@ e2e/
534
537
  qa/ # QA documentation .md files
535
538
 
536
539
  .e2e-ai/
540
+ config.ts # project configuration
541
+ context.md # project context (generated by init-agent)
542
+ agents/ # agent prompt definitions (.md files)
537
543
  <KEY>/ # per-issue working dir: codegen, recordings/, intermediate files
538
544
  ast-scan.json # scan command output
539
545
  qa-map.json # analyze command output
540
546
  scan-cache/ # file-level parse cache (gitignored)
541
547
  ```
542
548
 
549
+ ## MCP Server
550
+
551
+ e2e-ai ships an MCP (Model Context Protocol) server that lets AI assistants interact with your project's test infrastructure directly. The server binary is `e2e-ai-mcp`.
552
+
553
+ ### Setup
554
+
555
+ Add to your MCP client configuration:
556
+
557
+ **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
558
+
559
+ ```json
560
+ {
561
+ "mcpServers": {
562
+ "e2e-ai": {
563
+ "command": "npx",
564
+ "args": ["e2e-ai-mcp"],
565
+ "cwd": "/path/to/your/project"
566
+ }
567
+ }
568
+ }
569
+ ```
570
+
571
+ **Claude Code:**
572
+
573
+ ```bash
574
+ claude mcp add e2e-ai -- npx e2e-ai-mcp
575
+ ```
576
+
577
+ **Cursor** (`.cursor/mcp.json`):
578
+
579
+ ```json
580
+ {
581
+ "mcpServers": {
582
+ "e2e-ai": {
583
+ "command": "npx",
584
+ "args": ["e2e-ai-mcp"]
585
+ }
586
+ }
587
+ }
588
+ ```
589
+
590
+ If e2e-ai is installed globally or as a project dependency, you can use the binary path directly instead of `npx`:
591
+
592
+ ```json
593
+ {
594
+ "command": "node",
595
+ "args": ["node_modules/.bin/e2e-ai-mcp"]
596
+ }
597
+ ```
598
+
599
+ ### Available Tools
600
+
601
+ | Tool | Description | Input |
602
+ |------|-------------|-------|
603
+ | `e2e_ai_scan_codebase` | Scan project for test files, configs, fixtures, path aliases, and sample test content | `projectRoot?` (defaults to cwd) |
604
+ | `e2e_ai_validate_context` | Validate that a context markdown file has all required sections | `content` (markdown string) |
605
+ | `e2e_ai_read_agent` | Load an agent prompt by name — returns system prompt + config | `agentName` (e.g. `scenario-agent`) |
606
+ | `e2e_ai_get_example` | Get the example context markdown template | (none) |
607
+
608
+ ### Usage with AI Assistants
609
+
610
+ Once configured, an AI assistant can:
611
+
612
+ 1. **Scan your project** to understand its test structure, fixtures, and conventions
613
+ 2. **Read agent prompts** to understand how each pipeline step works
614
+ 3. **Validate context files** to ensure they have the right format before running commands
615
+ 4. **Get the example template** as a starting point for writing `e2e-ai.context.md`
616
+
617
+ This enables AI assistants to help you set up e2e-ai, debug pipeline issues, and generate better project context files.
618
+
543
619
  ## Library API
544
620
 
545
621
  e2e-ai also exports types and config helpers for programmatic use:
@@ -1,9 +1,5 @@
1
1
  ---
2
2
  agent: feature-analyzer-agent
3
- version: "1.0"
4
- model: gpt-4o
5
- max_tokens: 8192
6
- temperature: 0.1
7
3
  ---
8
4
 
9
5
  # System Prompt
@@ -1,20 +1,24 @@
1
1
  ---
2
2
  agent: init-agent
3
- version: "1.0"
4
- model: gpt-4o
5
- max_tokens: 8192
6
- temperature: 0.3
7
3
  ---
8
4
 
9
5
  # System Prompt
10
6
 
11
- You are a codebase analysis assistant for the e2e-ai test automation tool. Your job is to analyze a project's test infrastructure and produce a well-structured context document (`e2e-ai.context.md`) that will guide AI agents when generating, refining, and healing Playwright tests for this specific project.
7
+ You are a codebase analysis assistant for the e2e-ai test automation tool. Your job is to analyze a project's test infrastructure and produce a well-structured context document (`.e2e-ai/context.md`) that will guide AI agents when generating, refining, and healing Playwright tests for this specific project.
12
8
 
13
- You will receive scan results from the target codebase and engage in a conversation to clarify patterns you're uncertain about.
9
+ ## How to Use This Agent
10
+
11
+ This agent is designed to be used directly in your AI tool (Claude Code, Cursor, Gemini CLI, etc.). Start a conversation and ask it to generate your project context.
12
+
13
+ **If the e2e-ai MCP server is configured**, call `e2e_ai_scan_codebase` to get scan results, then follow this agent's instructions to produce the context file.
14
+
15
+ **If no MCP server**, manually explore the codebase: look at test files, fixtures, playwright config, tsconfig paths, and helper modules.
14
16
 
15
17
  ## Your Task
16
18
 
17
- Analyze the provided codebase scan and produce a context document covering:
19
+ Analyze the project codebase and produce a file at `.e2e-ai/context.md` that documents the project's test infrastructure, conventions, and patterns. This context file is consumed by downstream AI agents (scenario, generator, refiner, healer, QA) to produce Playwright tests that match the project's existing style.
20
+
21
+ Cover these areas:
18
22
 
19
23
  1. **Application Overview**: What the app does, tech stack, key pages/routes
20
24
  2. **Test Infrastructure**: Fixtures, custom test helpers, step counters, auth patterns
@@ -26,13 +30,13 @@ Analyze the provided codebase scan and produce a context document covering:
26
30
 
27
31
  ## Output Format
28
32
 
29
- When you have enough information, produce the final context as a markdown document with these sections:
33
+ Produce the context document with these sections and save it to `.e2e-ai/context.md`:
30
34
 
31
35
  ```markdown
32
36
  # Project Context for e2e-ai
33
37
 
34
38
  ## Application
35
- <name, description, tech stack>
39
+ <name, description, tech stack, base URL>
36
40
 
37
41
  ## Test Infrastructure
38
42
  <fixtures, helpers, auth pattern>
@@ -53,6 +57,22 @@ When you have enough information, produce the final context as a markdown docume
53
57
  <timeouts, waits, assertion patterns>
54
58
  ```
55
59
 
60
+ All sections are required. The file should be 100-300 lines, self-contained, and use actual code from the project (not generic Playwright examples).
61
+
62
+ ## How Context is Used
63
+
64
+ Each pipeline agent reads `.e2e-ai/context.md` to understand project conventions:
65
+
66
+ | Agent | Uses context for |
67
+ |-------|-----------------|
68
+ | **scenario-agent** | Structuring test steps to match project patterns |
69
+ | **playwright-generator-agent** | Generating code with correct imports, fixtures, selectors |
70
+ | **refactor-agent** | Applying project-specific refactoring patterns |
71
+ | **self-healing-agent** | Understanding expected test structure when fixing failures |
72
+ | **qa-testcase-agent** | Formatting QA documentation to match conventions |
73
+ | **feature-analyzer-agent** | Understanding app structure for QA map generation |
74
+ | **scenario-planner-agent** | Generating realistic test scenarios from codebase analysis |
75
+
56
76
  ## Rules
57
77
 
58
78
  1. Ask clarifying questions if the scan data is ambiguous — do NOT guess
@@ -61,15 +81,3 @@ When you have enough information, produce the final context as a markdown docume
61
81
  4. The context file should be self-contained — an AI agent reading only this file should understand all project conventions
62
82
  5. Keep the document concise but complete — aim for 100-300 lines
63
83
  6. If you need to see specific files to complete the analysis, list them explicitly
64
-
65
- ## Conversation Flow
66
-
67
- 1. **First turn**: Receive scan results, analyze them, ask clarifying questions if needed
68
- 2. **Middle turns**: Receive answers, refine understanding
69
- 3. **Final turn**: When you have enough info, produce the complete context document wrapped in a `<context>` tag:
70
- ```
71
- <context>
72
- # Project Context for e2e-ai
73
- ...
74
- </context>
75
- ```
@@ -1,9 +1,5 @@
1
1
  ---
2
2
  agent: playwright-generator-agent
3
- version: "1.0"
4
- model: gpt-4o
5
- max_tokens: 8192
6
- temperature: 0.2
7
3
  ---
8
4
 
9
5
  # System Prompt
@@ -1,9 +1,5 @@
1
1
  ---
2
2
  agent: qa-testcase-agent
3
- version: "1.0"
4
- model: gpt-4o
5
- max_tokens: 8192
6
- temperature: 0.2
7
3
  ---
8
4
 
9
5
  # System Prompt
@@ -1,9 +1,5 @@
1
1
  ---
2
2
  agent: refactor-agent
3
- version: "1.0"
4
- model: gpt-4o
5
- max_tokens: 8192
6
- temperature: 0.2
7
3
  ---
8
4
 
9
5
  # System Prompt
@@ -1,9 +1,5 @@
1
1
  ---
2
2
  agent: scenario-agent
3
- version: "1.0"
4
- model: gpt-4o
5
- max_tokens: 4096
6
- temperature: 0.2
7
3
  ---
8
4
 
9
5
  # System Prompt
@@ -1,9 +1,5 @@
1
1
  ---
2
2
  agent: scenario-planner-agent
3
- version: "1.0"
4
- model: gpt-4o
5
- max_tokens: 8192
6
- temperature: 0.2
7
3
  ---
8
4
 
9
5
  # System Prompt
@@ -1,9 +1,5 @@
1
1
  ---
2
2
  agent: self-healing-agent
3
- version: "1.0"
4
- model: gpt-4o
5
- max_tokens: 8192
6
- temperature: 0.2
7
3
  ---
8
4
 
9
5
  # System Prompt
@@ -1,9 +1,5 @@
1
1
  ---
2
2
  agent: transcript-agent
3
- version: "1.0"
4
- model: gpt-4o
5
- max_tokens: 4096
6
- temperature: 0.2
7
3
  ---
8
4
 
9
5
  # System Prompt
@@ -0,0 +1,101 @@
1
+ import {
2
+ getPackageRoot,
3
+ getProjectRoot
4
+ } from "./cli-kx32qnf3.js";
5
+
6
+ // src/agents/loadAgent.ts
7
+ import { readFileSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ function loadAgent(agentName, config) {
10
+ const localPath = join(getProjectRoot(), ".e2e-ai", "agents", `${agentName}.md`);
11
+ const packagePath = join(getPackageRoot(), "agents", `${agentName}.md`);
12
+ const filePath = existsSync(localPath) ? localPath : packagePath;
13
+ let content;
14
+ try {
15
+ content = readFileSync(filePath, "utf-8");
16
+ } catch {
17
+ throw new Error(`Agent file not found: ${filePath}`);
18
+ }
19
+ const { frontmatter, body } = parseFrontmatter(content);
20
+ const agentConfig = extractConfig(frontmatter);
21
+ let systemPrompt = body;
22
+ if (config) {
23
+ const contextPath = join(getProjectRoot(), ".e2e-ai", "context.md");
24
+ if (existsSync(contextPath)) {
25
+ const projectContext = readFileSync(contextPath, "utf-8").trim();
26
+ if (projectContext) {
27
+ systemPrompt = `${body}
28
+
29
+ ## Project Context
30
+
31
+ ${projectContext}`;
32
+ }
33
+ }
34
+ if (config.llm.agentModels[agentName]) {
35
+ agentConfig.model = config.llm.agentModels[agentName];
36
+ }
37
+ }
38
+ const sections = parseSections(body);
39
+ return {
40
+ name: frontmatter.agent ?? agentName,
41
+ systemPrompt,
42
+ inputSchema: sections["Input Schema"],
43
+ outputSchema: sections["Output Schema"],
44
+ rules: sections["Rules"],
45
+ example: sections["Example"],
46
+ config: agentConfig
47
+ };
48
+ }
49
+ function parseFrontmatter(content) {
50
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
51
+ if (!match)
52
+ return { frontmatter: {}, body: content };
53
+ const frontmatter = {};
54
+ for (const line of match[1].split(`
55
+ `)) {
56
+ const colonIdx = line.indexOf(":");
57
+ if (colonIdx === -1)
58
+ continue;
59
+ const key = line.slice(0, colonIdx).trim();
60
+ let value = line.slice(colonIdx + 1).trim();
61
+ if (value.startsWith('"') && value.endsWith('"'))
62
+ value = value.slice(1, -1);
63
+ if (value === "true")
64
+ value = true;
65
+ if (value === "false")
66
+ value = false;
67
+ if (!isNaN(Number(value)) && value !== "")
68
+ value = Number(value);
69
+ frontmatter[key] = value;
70
+ }
71
+ return { frontmatter, body: match[2] };
72
+ }
73
+ function extractConfig(frontmatter) {
74
+ return {
75
+ model: frontmatter.model,
76
+ maxTokens: frontmatter.max_tokens ?? 4096,
77
+ temperature: frontmatter.temperature ?? 0.2
78
+ };
79
+ }
80
+ function parseSections(body) {
81
+ const sections = {};
82
+ const headingRegex = /^##\s+(.+)$/gm;
83
+ const headings = [];
84
+ let match;
85
+ while ((match = headingRegex.exec(body)) !== null) {
86
+ headings.push({ title: match[1].trim(), index: match.index });
87
+ }
88
+ const systemMatch = body.match(/^#\s+System Prompt\n([\s\S]*?)(?=\n##\s|$)/m);
89
+ if (systemMatch) {
90
+ sections["System Prompt"] = systemMatch[1].trim();
91
+ }
92
+ for (let i = 0;i < headings.length; i++) {
93
+ const start = headings[i].index + body.slice(headings[i].index).indexOf(`
94
+ `) + 1;
95
+ const end = i + 1 < headings.length ? headings[i + 1].index : body.length;
96
+ sections[headings[i].title] = body.slice(start, end).trim();
97
+ }
98
+ return sections;
99
+ }
100
+
101
+ export { loadAgent };
@@ -0,0 +1,67 @@
1
+ import {
2
+ E2eAiConfigSchema
3
+ } from "./cli-fgp618yt.js";
4
+
5
+ // src/config/loader.ts
6
+ import { existsSync } from "node:fs";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ var CONFIG_DIR = ".e2e-ai";
10
+ var CONFIG_FILENAMES = ["config.ts", "config.js", "config.mjs"];
11
+ var cachedConfig = null;
12
+ var cachedProjectRoot = null;
13
+ function findConfigDir(startDir) {
14
+ let dir = resolve(startDir);
15
+ const root = dirname(dir) === dir ? dir : undefined;
16
+ while (true) {
17
+ const e2eDir = join(dir, CONFIG_DIR);
18
+ for (const name of CONFIG_FILENAMES) {
19
+ if (existsSync(join(e2eDir, name))) {
20
+ return dir;
21
+ }
22
+ }
23
+ const parent = dirname(dir);
24
+ if (parent === dir || dir === root)
25
+ return null;
26
+ dir = parent;
27
+ }
28
+ }
29
+ function getProjectRoot() {
30
+ if (cachedProjectRoot)
31
+ return cachedProjectRoot;
32
+ const found = findConfigDir(process.cwd());
33
+ cachedProjectRoot = found ?? process.cwd();
34
+ return cachedProjectRoot;
35
+ }
36
+ function getPackageRoot() {
37
+ let dir = import.meta.dirname;
38
+ while (!existsSync(join(dir, "package.json"))) {
39
+ const parent = dirname(dir);
40
+ if (parent === dir)
41
+ return dir;
42
+ dir = parent;
43
+ }
44
+ return dir;
45
+ }
46
+ async function loadConfig() {
47
+ if (cachedConfig)
48
+ return cachedConfig;
49
+ const projectRoot = getProjectRoot();
50
+ const e2eDir = join(projectRoot, CONFIG_DIR);
51
+ let userConfig = {};
52
+ for (const name of CONFIG_FILENAMES) {
53
+ const configPath = join(e2eDir, name);
54
+ if (existsSync(configPath)) {
55
+ try {
56
+ const fileUrl = pathToFileURL(configPath).href;
57
+ const mod = await import(fileUrl);
58
+ userConfig = mod.default ?? mod;
59
+ break;
60
+ } catch {}
61
+ }
62
+ }
63
+ cachedConfig = E2eAiConfigSchema.parse(userConfig);
64
+ return cachedConfig;
65
+ }
66
+
67
+ export { getProjectRoot, getPackageRoot, loadConfig };
package/dist/cli.js CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- loadAgent,
4
- scanCodebase
5
- } from "./cli-6c0wsk32.js";
3
+ loadAgent
4
+ } from "./cli-98db6h2q.js";
6
5
  import {
7
6
  getPackageRoot,
8
7
  getProjectRoot,
9
8
  loadConfig
10
- } from "./cli-cqabyzv3.js";
9
+ } from "./cli-kx32qnf3.js";
11
10
  import"./cli-fgp618yt.js";
12
11
  import {
13
12
  __commonJS,
@@ -9057,10 +9056,11 @@ var import_picocolors2 = __toESM(require_picocolors(), 1);
9057
9056
  function registerInit(program2) {
9058
9057
  program2.command("init").description("Initialize e2e-ai configuration for your project").option("--non-interactive", "Skip interactive prompts, use defaults").action(async (cmdOpts) => {
9059
9058
  const projectRoot = getProjectRoot();
9059
+ const e2eDir = join13(projectRoot, ".e2e-ai");
9060
9060
  header("e2e-ai init");
9061
9061
  const answers = cmdOpts?.nonInteractive ? getDefaultAnswers() : await askConfigQuestions();
9062
9062
  const config = buildConfigFromAnswers(answers);
9063
- const configPath = join13(projectRoot, "e2e-ai.config.ts");
9063
+ const configPath = join13(e2eDir, "config.ts");
9064
9064
  if (fileExists(configPath)) {
9065
9065
  warn(`Config already exists: ${configPath}`);
9066
9066
  const overwrite = cmdOpts?.nonInteractive ? false : await dist_default4({ message: "Overwrite existing config?", default: false });
@@ -9074,28 +9074,15 @@ function registerInit(program2) {
9074
9074
  writeFile(configPath, generateConfigFile(config));
9075
9075
  success(`Config written: ${configPath}`);
9076
9076
  }
9077
- const spinner = createSpinner();
9078
- spinner.start("Scanning codebase for test patterns...");
9079
- const scan = await scanCodebase(projectRoot);
9080
- spinner.stop();
9081
- if (scan.testFiles.length === 0 && scan.configFiles.length === 0) {
9082
- warn("No test files found. Scan results will be minimal.");
9083
- } else {
9084
- info(`Found ${scan.testFiles.length} test files, ${scan.configFiles.length} config files`);
9085
- }
9086
- const instructionsContent = generateInstructionsFile(scan);
9087
- const instructionsPath = join13(projectRoot, "e2e-ai.instructions.md");
9088
- writeFile(instructionsPath, instructionsContent);
9089
- success(`Instructions written: ${instructionsPath}`);
9090
- const copiedCount = await copyAgentsToLocal(projectRoot, !!cmdOpts?.nonInteractive);
9077
+ await copyAgentsToLocal(projectRoot, !!cmdOpts?.nonInteractive);
9091
9078
  console.log("");
9092
9079
  success(`Initialization complete!
9093
9080
  `);
9094
9081
  console.log(import_picocolors2.default.bold("Next steps:"));
9095
- console.log(` 1. Open ${import_picocolors2.default.cyan("e2e-ai.instructions.md")} with your AI tool`);
9082
+ console.log(` 1. Use the ${import_picocolors2.default.cyan("init-agent")} in your AI tool to generate ${import_picocolors2.default.cyan(".e2e-ai/context.md")}`);
9083
+ console.log(` (or use the MCP server: ${import_picocolors2.default.cyan("e2e_ai_scan_codebase")} + ${import_picocolors2.default.cyan("e2e_ai_read_agent")})`);
9096
9084
  console.log(` 2. Review the generated ${import_picocolors2.default.cyan(".e2e-ai/context.md")}`);
9097
- console.log(` 3. Customize agents in ${import_picocolors2.default.cyan(".e2e-ai/agents/")} if needed`);
9098
- console.log(` 4. Run: ${import_picocolors2.default.cyan("e2e-ai run --key PROJ-101")}`);
9085
+ console.log(` 3. Run: ${import_picocolors2.default.cyan("e2e-ai run --key PROJ-101")}`);
9099
9086
  });
9100
9087
  }
9101
9088
  function getDefaultAnswers() {
@@ -9157,8 +9144,7 @@ function buildConfigFromAnswers(answers) {
9157
9144
  inputSource: answers.inputSource,
9158
9145
  outputTarget: answers.outputTarget,
9159
9146
  voice: { enabled: answers.voiceEnabled },
9160
- llm: { provider: answers.provider },
9161
- contextFile: ".e2e-ai/context.md"
9147
+ llm: { provider: answers.provider }
9162
9148
  };
9163
9149
  if (answers.baseUrl) {
9164
9150
  config.baseUrl = answers.baseUrl;
@@ -9190,127 +9176,6 @@ function generateConfigFile(config) {
9190
9176
  return lines.join(`
9191
9177
  `);
9192
9178
  }
9193
- function generateInstructionsFile(scan) {
9194
- const packageRoot = getPackageRoot();
9195
- const sections = [];
9196
- sections.push(`# e2e-ai: Context Generation Instructions
9197
-
9198
- This file was generated by \`e2e-ai init\`. It contains everything an AI tool needs to generate \`.e2e-ai/context.md\` for your project.
9199
-
9200
- ## How to Use
9201
-
9202
- 1. Open this file in your AI tool (Claude Code, Cursor, Gemini CLI, etc.)
9203
- 2. Ask it to follow these instructions to generate \`.e2e-ai/context.md\`
9204
- 3. Review the generated file and adjust as needed
9205
-
9206
- Alternatively, if the e2e-ai MCP server is configured, your AI tool can call \`e2e_ai_scan_codebase\` and \`e2e_ai_validate_context\` directly.
9207
-
9208
- ---`);
9209
- sections.push(`## Task
9210
-
9211
- Scan this codebase and generate a file at \`.e2e-ai/context.md\` that documents the project's test infrastructure, conventions, and patterns. This context file is consumed by downstream AI agents (scenario, generator, refiner, healer, QA) to produce Playwright tests that match the project's existing style.`);
9212
- sections.push(`## Codebase Scan Results
9213
-
9214
- The following was pre-computed during \`e2e-ai init\`:
9215
-
9216
- ### Test Files (${scan.testFiles.length} found)
9217
- ${scan.testFiles.length > 0 ? scan.testFiles.slice(0, 20).map((f) => `- \`${f}\``).join(`
9218
- `) : "_No test files found_"}
9219
- ${scan.testFiles.length > 20 ? `
9220
- _(${scan.testFiles.length - 20} more not shown)_` : ""}
9221
-
9222
- ### Config Files
9223
- ${scan.configFiles.length > 0 ? scan.configFiles.map((f) => `- \`${f}\``).join(`
9224
- `) : "_None found_"}
9225
-
9226
- ### Fixture Files
9227
- ${scan.fixtureFiles.length > 0 ? scan.fixtureFiles.slice(0, 10).map((f) => `- \`${f}\``).join(`
9228
- `) : "_None found_"}
9229
-
9230
- ### Feature Files
9231
- ${scan.featureFiles.length > 0 ? scan.featureFiles.slice(0, 20).map((f) => `- \`${f}\``).join(`
9232
- `) : "_None found_"}
9233
-
9234
- ### Path Aliases (from tsconfig.json)
9235
- ${Object.keys(scan.tsconfigPaths).length > 0 ? Object.entries(scan.tsconfigPaths).map(([alias, targets]) => `- \`${alias}\` -> \`${targets.join(", ")}\``).join(`
9236
- `) : "_None configured_"}
9237
-
9238
- ### Playwright Config
9239
- ${scan.playwrightConfig ? `Found: \`${scan.playwrightConfig}\`` : "_Not found_"}
9240
-
9241
- ### Sample Test Content
9242
- ${scan.sampleTestContent ? "```typescript\n" + scan.sampleTestContent + "\n```" : "_No sample available_"}`);
9243
- let agentChecklist = "";
9244
- try {
9245
- const agentContent = readFileSync2(join13(packageRoot, "agents", "init-agent.md"), "utf-8");
9246
- const bodyMatch = agentContent.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
9247
- if (bodyMatch) {
9248
- agentChecklist = bodyMatch[1].trim();
9249
- }
9250
- } catch {}
9251
- if (agentChecklist) {
9252
- sections.push(`## What to Look For
9253
-
9254
- The following guidance comes from the e2e-ai init agent:
9255
-
9256
- ${agentChecklist}`);
9257
- }
9258
- sections.push(`## Output Format
9259
-
9260
- The generated \`.e2e-ai/context.md\` MUST contain these sections:
9261
-
9262
- \`\`\`markdown
9263
- # Project Context for e2e-ai
9264
-
9265
- ## Application
9266
- <name, description, tech stack, base URL>
9267
-
9268
- ## Test Infrastructure
9269
- <fixtures, helpers, auth pattern>
9270
-
9271
- ## Feature Methods
9272
- <method signatures grouped by module>
9273
-
9274
- ## Import Conventions
9275
- <path aliases, standard imports>
9276
-
9277
- ## Selector Conventions
9278
- <preferred selector strategies, patterns>
9279
-
9280
- ## Test Structure Template
9281
- <code template showing standard test layout>
9282
-
9283
- ## Utility Patterns
9284
- <timeouts, waits, assertion patterns>
9285
- \`\`\`
9286
-
9287
- All sections are required. The file should be 100-300 lines, self-contained, and use actual code from the project (not generic Playwright examples).`);
9288
- sections.push(`## How Context is Used
9289
-
9290
- Each pipeline agent reads \`.e2e-ai/context.md\` to understand project conventions:
9291
-
9292
- | Agent | Uses context for |
9293
- |-------|-----------------|
9294
- | **scenario-agent** | Structuring test steps to match project patterns |
9295
- | **playwright-generator-agent** | Generating code with correct imports, fixtures, selectors |
9296
- | **refactor-agent** | Applying project-specific refactoring patterns |
9297
- | **self-healing-agent** | Understanding expected test structure when fixing failures |
9298
- | **qa-testcase-agent** | Formatting QA documentation to match conventions |`);
9299
- let exampleContent = "";
9300
- try {
9301
- exampleContent = readFileSync2(join13(packageRoot, "templates", "e2e-ai.context.example.md"), "utf-8");
9302
- } catch {}
9303
- if (exampleContent) {
9304
- sections.push(`## Complete Example
9305
-
9306
- Below is a full example of a well-structured context file:
9307
-
9308
- ${exampleContent}`);
9309
- }
9310
- return sections.join(`
9311
-
9312
- `);
9313
- }
9314
9179
  async function copyAgentsToLocal(projectRoot, nonInteractive) {
9315
9180
  const packageRoot = getPackageRoot();
9316
9181
  const sourceDir = join13(packageRoot, "agents");
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  getPackageRoot,
3
3
  getProjectRoot,
4
4
  loadConfig
5
- } from "./cli-cqabyzv3.js";
5
+ } from "./cli-kx32qnf3.js";
6
6
  import {
7
7
  defineConfig
8
8
  } from "./cli-fgp618yt.js";
package/dist/mcp.js CHANGED
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- loadAgent,
4
- scanCodebase
5
- } from "./cli-6c0wsk32.js";
3
+ loadAgent
4
+ } from "./cli-98db6h2q.js";
6
5
  import {
7
6
  getPackageRoot
8
- } from "./cli-cqabyzv3.js";
7
+ } from "./cli-kx32qnf3.js";
9
8
  import {
10
9
  $ZodObject,
11
10
  $ZodType,
@@ -14857,8 +14856,72 @@ class StdioServerTransport {
14857
14856
  }
14858
14857
 
14859
14858
  // src/mcp.ts
14860
- import { readFileSync } from "node:fs";
14861
- import { join } from "node:path";
14859
+ import { readFileSync as readFileSync2 } from "node:fs";
14860
+ import { join as join2 } from "node:path";
14861
+
14862
+ // src/utils/scan.ts
14863
+ import { readdirSync, existsSync, readFileSync } from "node:fs";
14864
+ import { join, relative } from "node:path";
14865
+ async function scanCodebase(root) {
14866
+ const scan = {
14867
+ testFiles: [],
14868
+ configFiles: [],
14869
+ fixtureFiles: [],
14870
+ featureFiles: [],
14871
+ tsconfigPaths: {},
14872
+ playwrightConfig: null,
14873
+ sampleTestContent: null
14874
+ };
14875
+ function walk(dir, depth = 0) {
14876
+ if (depth > 5)
14877
+ return [];
14878
+ const files = [];
14879
+ try {
14880
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
14881
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
14882
+ continue;
14883
+ const full = join(dir, entry.name);
14884
+ if (entry.isDirectory()) {
14885
+ files.push(...walk(full, depth + 1));
14886
+ } else {
14887
+ files.push(full);
14888
+ }
14889
+ }
14890
+ } catch {}
14891
+ return files;
14892
+ }
14893
+ const allFiles = walk(root);
14894
+ for (const file of allFiles) {
14895
+ const rel = relative(root, file);
14896
+ if (rel.endsWith(".test.ts") || rel.endsWith(".spec.ts")) {
14897
+ scan.testFiles.push(rel);
14898
+ if (!scan.sampleTestContent && scan.testFiles.length <= 3) {
14899
+ try {
14900
+ scan.sampleTestContent = readFileSync(file, "utf-8").slice(0, 3000);
14901
+ } catch {}
14902
+ }
14903
+ }
14904
+ if (rel.endsWith(".feature.ts"))
14905
+ scan.featureFiles.push(rel);
14906
+ if (rel.includes("fixture") && rel.endsWith(".ts"))
14907
+ scan.fixtureFiles.push(rel);
14908
+ if (rel === "playwright.config.ts" || rel === "playwright.config.js")
14909
+ scan.playwrightConfig = rel;
14910
+ if (rel === "tsconfig.json" || rel.endsWith("/tsconfig.json")) {
14911
+ try {
14912
+ const tsconfig = JSON.parse(readFileSync(file, "utf-8"));
14913
+ if (tsconfig.compilerOptions?.paths) {
14914
+ scan.tsconfigPaths = { ...scan.tsconfigPaths, ...tsconfig.compilerOptions.paths };
14915
+ }
14916
+ } catch {}
14917
+ }
14918
+ }
14919
+ for (const name of ["playwright.config.ts", "vitest.config.ts", "jest.config.ts", "tsconfig.json", "package.json"]) {
14920
+ if (existsSync(join(root, name)))
14921
+ scan.configFiles.push(name);
14922
+ }
14923
+ return scan;
14924
+ }
14862
14925
 
14863
14926
  // src/utils/validateContext.ts
14864
14927
  var REQUIRED_SECTIONS = [
@@ -14955,8 +15018,8 @@ server.registerTool("e2e_ai_get_example", {
14955
15018
  inputSchema: exports_external.object({})
14956
15019
  }, async () => {
14957
15020
  try {
14958
- const examplePath = join(getPackageRoot(), "templates", "e2e-ai.context.example.md");
14959
- const content = readFileSync(examplePath, "utf-8");
15021
+ const examplePath = join2(getPackageRoot(), "templates", "e2e-ai.context.example.md");
15022
+ const content = readFileSync2(examplePath, "utf-8");
14960
15023
  return {
14961
15024
  content: [{ type: "text", text: content }]
14962
15025
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "e2e-ai",
3
3
  "description": "AI-powered test automation pipeline — record, transcribe, generate, heal and ship Playwright tests from a single CLI",
4
- "version": "1.3.0",
4
+ "version": "1.4.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "bin": {
@@ -129,8 +129,54 @@ console.error(`Voice recording: ${voiceEnabled ? 'ENABLED' : 'disabled (--no-voi
129
129
  console.error(`Trace replay: ${traceEnabled ? 'ENABLED' : 'disabled (--no-trace)'}`);
130
130
  console.error('(When you close the Playwright Inspector, the file is written there.)\n');
131
131
 
132
+ // --- Session timing (always track, used for action timestamps) ---
133
+ const sessionStartTime = Date.now();
134
+
135
+ // --- Action timestamp tracking via file polling ---
136
+ const actionPattern = /^\s*(await\s+page\.|await\s+expect\()/;
137
+ let prevActionCount = 0;
138
+ const actionElapsedSec = []; // elapsed seconds for each action, in order
139
+ let pollInterval = null;
140
+
141
+ function startActionPolling() {
142
+ pollInterval = setInterval(() => {
143
+ if (!existsSync(outputPath)) return;
144
+ try {
145
+ const content = readFileSync(outputPath, 'utf-8');
146
+ const actionLines = content.split('\n').filter((l) => actionPattern.test(l));
147
+ const newCount = actionLines.length;
148
+ if (newCount > prevActionCount) {
149
+ const elapsed = (Date.now() - sessionStartTime) / 1000;
150
+ for (let i = prevActionCount; i < newCount; i++) {
151
+ actionElapsedSec.push(elapsed);
152
+ }
153
+ prevActionCount = newCount;
154
+ }
155
+ } catch {
156
+ // File might be mid-write — ignore
157
+ }
158
+ }, 500);
159
+ }
160
+
161
+ /** Inject // @t:<seconds>s comments above each action line in the codegen file. */
162
+ function injectActionTimestamps(filePath) {
163
+ if (actionElapsedSec.length === 0) return;
164
+ const content = readFileSync(filePath, 'utf-8');
165
+ const lines = content.split('\n');
166
+ const result = [];
167
+ let idx = 0;
168
+ for (const line of lines) {
169
+ if (actionPattern.test(line) && idx < actionElapsedSec.length) {
170
+ const indent = line.match(/^(\s*)/)[1];
171
+ result.push(`${indent}// @t:${actionElapsedSec[idx].toFixed(1)}s`);
172
+ idx++;
173
+ }
174
+ result.push(line);
175
+ }
176
+ writeFileSync(filePath, result.join('\n'));
177
+ }
178
+
132
179
  // --- Voice setup ---
133
- let sessionStartTime = null;
134
180
  let recording = false;
135
181
  let currentRecProcess = null;
136
182
  let segmentIndex = 0;
@@ -147,8 +193,6 @@ if (voiceEnabled) {
147
193
  mkdirSync(recordingsDir, { recursive: true });
148
194
  }
149
195
 
150
- sessionStartTime = Date.now();
151
-
152
196
  const segPath = resolve(recordingsDir, `seg-${String(segmentIndex).padStart(3, '0')}.wav`);
153
197
  segmentPaths.push(segPath);
154
198
  const rec = startRecording(segPath);
@@ -195,6 +239,9 @@ const child = spawn('npx', codegenArgs, {
195
239
  shell: true,
196
240
  });
197
241
 
242
+ // Start polling the output file for new actions to capture timestamps
243
+ startActionPolling();
244
+
198
245
  // --- Keyboard listener for pause/resume ---
199
246
  if (voiceEnabled && process.stdin.isTTY) {
200
247
  process.stdin.setRawMode(true);
@@ -238,16 +285,20 @@ function cleanupTerminal() {
238
285
 
239
286
  child.on('exit', async (code) => {
240
287
  cleanupTerminal();
288
+ if (pollInterval) clearInterval(pollInterval);
241
289
 
242
290
  if (code === 0) {
243
291
  console.error(`\nSaved: ${outputRelative}`);
244
292
  }
245
293
 
246
- // --- Voice post-processing ---
247
- if (voiceEnabled && segmentPaths.length > 0) {
248
- const sessionEndTime = Date.now();
249
- const durationSec = (sessionEndTime - sessionStartTime) / 1000;
294
+ // --- Inject action timestamps into codegen output ---
295
+ if (existsSync(outputPath) && actionElapsedSec.length > 0) {
296
+ injectActionTimestamps(outputPath);
297
+ console.error(`Injected ${actionElapsedSec.length} action timestamp(s) into: ${outputRelative}`);
298
+ }
250
299
 
300
+ // --- Voice post-processing: merge WAV segments only (transcription deferred to transcribe command) ---
301
+ if (voiceEnabled && segmentPaths.length > 0) {
251
302
  try {
252
303
  if (recording && currentRecProcess) {
253
304
  const { stopRecording } = await import(resolve(__dirname, 'voice', 'recorder.mjs'));
@@ -260,45 +311,26 @@ child.on('exit', async (code) => {
260
311
 
261
312
  if (existingSegments.length === 0) {
262
313
  console.error('No audio segments recorded.');
263
- process.exit(code ?? 0);
264
- return;
265
- }
266
-
267
- const mergedWavPath = resolve(recordingsDir, `voice-${timestamp}.wav`);
268
-
269
- if (existingSegments.length === 1) {
270
- renameSync(existingSegments[0], mergedWavPath);
271
314
  } else {
272
- console.error(`Merging ${existingSegments.length} audio segments...`);
273
- const args = ['--combine', 'concatenate', ...existingSegments, mergedWavPath];
274
- execSync(`sox ${args.map((a) => `"${a}"`).join(' ')}`, { stdio: 'ignore' });
275
-
276
- for (const seg of existingSegments) {
277
- try { unlinkSync(seg); } catch {}
315
+ const mergedWavPath = resolve(recordingsDir, `voice-${timestamp}.wav`);
316
+
317
+ if (existingSegments.length === 1) {
318
+ renameSync(existingSegments[0], mergedWavPath);
319
+ } else {
320
+ console.error(`Merging ${existingSegments.length} audio segments...`);
321
+ const args = ['--combine', 'concatenate', ...existingSegments, mergedWavPath];
322
+ execSync(`sox ${args.map((a) => `"${a}"`).join(' ')}`, { stdio: 'ignore' });
323
+
324
+ for (const seg of existingSegments) {
325
+ try { unlinkSync(seg); } catch {}
326
+ }
278
327
  }
279
- }
280
-
281
- console.error(`Audio saved: ${relative(root, mergedWavPath)}`);
282
328
 
283
- const { transcribe } = await import(resolve(__dirname, 'voice', 'transcriber.mjs'));
284
- const segments = await transcribe(mergedWavPath);
285
-
286
- const transcriptPath = resolve(recordingsDir, `voice-${timestamp}.json`);
287
- writeFileSync(transcriptPath, JSON.stringify(segments, null, 2));
288
- console.error(`Transcript saved: ${relative(root, transcriptPath)}`);
289
-
290
- if (segments.length > 0 && existsSync(outputPath)) {
291
- const { merge } = await import(resolve(__dirname, 'voice', 'merger.mjs'));
292
- const codegenContent = readFileSync(outputPath, 'utf-8');
293
- const annotated = merge(codegenContent, segments, durationSec);
294
- writeFileSync(outputPath, annotated);
295
- console.error(`Merged ${segments.length} voice segment(s) into: ${outputRelative}`);
329
+ console.error(`\nVoice recording summary:`);
330
+ console.error(` Audio: ${relative(root, mergedWavPath)}`);
331
+ console.error(` Codegen: ${outputRelative}`);
332
+ console.error(` (Run 'transcribe' to process voice → merge into codegen)`);
296
333
  }
297
-
298
- console.error('\nVoice recording summary:');
299
- console.error(` Audio: ${relative(root, mergedWavPath)}`);
300
- console.error(` Transcript: ${relative(root, transcriptPath)}`);
301
- console.error(` Codegen: ${outputRelative}`);
302
334
  } catch (err) {
303
335
  console.error(`\nVoice processing error: ${err.message}`);
304
336
  }
@@ -9,16 +9,42 @@ function formatTime(sec) {
9
9
  return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
10
10
  }
11
11
 
12
+ /**
13
+ * Extract real action timestamps from // @t:<seconds>s comments injected during codegen.
14
+ * Returns an array of { lineIndex, elapsed } or null if no timestamps found.
15
+ *
16
+ * @param {string[]} lines
17
+ * @param {number[]} actionIndices - line indices of action lines
18
+ * @returns {number[] | null} elapsed seconds per action, or null
19
+ */
20
+ function extractEmbeddedTimestamps(lines, actionIndices) {
21
+ const tsPattern = /^\s*\/\/\s*@t:([\d.]+)s\s*$/;
22
+ const timestamps = [];
23
+
24
+ for (const actionIdx of actionIndices) {
25
+ // Look at the line immediately before the action for a @t: comment
26
+ if (actionIdx > 0 && tsPattern.test(lines[actionIdx - 1])) {
27
+ const match = lines[actionIdx - 1].match(tsPattern);
28
+ timestamps.push(parseFloat(match[1]));
29
+ } else {
30
+ // Missing timestamp for this action — can't use embedded timestamps
31
+ return null;
32
+ }
33
+ }
34
+
35
+ return timestamps;
36
+ }
37
+
12
38
  /**
13
39
  * Merge codegen output with voice transcript segments.
14
40
  *
15
- * Action lines (await page.* / await expect(*) are identified and distributed
16
- * linearly across the session duration. Each speech segment is inserted as a
17
- * comment before the nearest action line.
41
+ * If the codegen contains // @t:<seconds>s comments (injected during recording),
42
+ * those real timestamps are used for alignment. Otherwise, action timestamps are
43
+ * distributed linearly across the session duration (fallback).
18
44
  *
19
45
  * @param {string} codegenContent - The original codegen .ts file content
20
46
  * @param {Array<{ start: number, end: number, text: string }>} segments - Whisper transcript segments
21
- * @param {number} durationSec - Total session duration in seconds
47
+ * @param {number} durationSec - Total session duration in seconds (used only for linear fallback)
22
48
  * @returns {string} Annotated codegen content
23
49
  */
24
50
  export function merge(codegenContent, segments, durationSec) {
@@ -26,8 +52,9 @@ export function merge(codegenContent, segments, durationSec) {
26
52
 
27
53
  const lines = codegenContent.split('\n');
28
54
  const actionPattern = /^\s*(await\s+page\.|await\s+expect\()/;
55
+ const tsCommentPattern = /^\s*\/\/\s*@t:[\d.]+s\s*$/;
29
56
 
30
- // Find indices of action lines
57
+ // Find indices of action lines (skip @t: comment lines)
31
58
  const actionIndices = [];
32
59
  for (let i = 0; i < lines.length; i++) {
33
60
  if (actionPattern.test(lines[i])) {
@@ -37,14 +64,16 @@ export function merge(codegenContent, segments, durationSec) {
37
64
 
38
65
  if (actionIndices.length === 0) return codegenContent;
39
66
 
40
- // Estimate timestamp for each action: distribute linearly across the session
41
- const actionTimestamps = actionIndices.map((_, idx) => {
42
- if (actionIndices.length === 1) return durationSec / 2;
43
- return (idx / (actionIndices.length - 1)) * durationSec;
44
- });
67
+ // Try to use embedded timestamps, fall back to linear distribution
68
+ const embeddedTs = extractEmbeddedTimestamps(lines, actionIndices);
69
+ const actionTimestamps = embeddedTs
70
+ ? embeddedTs
71
+ : actionIndices.map((_, idx) => {
72
+ if (actionIndices.length === 1) return durationSec / 2;
73
+ return (idx / (actionIndices.length - 1)) * durationSec;
74
+ });
45
75
 
46
76
  // For each segment, find the nearest action by timestamp
47
- // Map: actionIndex → list of segments to insert before it
48
77
  /** @type {Map<number, Array<{ start: number, end: number, text: string }>>} */
49
78
  const insertions = new Map();
50
79
 
@@ -68,12 +97,14 @@ export function merge(codegenContent, segments, durationSec) {
68
97
  insertions.get(actionLineIdx).push(seg);
69
98
  }
70
99
 
71
- // Build result: insert comment lines before action lines
100
+ // Build result: strip old @t: comments and insert voice comments before action lines
72
101
  const result = [];
73
102
  for (let i = 0; i < lines.length; i++) {
103
+ // Skip @t: timestamp comments — they're consumed, not preserved
104
+ if (tsCommentPattern.test(lines[i])) continue;
105
+
74
106
  const segs = insertions.get(i);
75
107
  if (segs) {
76
- // Detect indentation of the action line
77
108
  const indent = lines[i].match(/^(\s*)/)[1];
78
109
  for (const seg of segs) {
79
110
  result.push(