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 +83 -7
- package/agents/feature-analyzer-agent.md +0 -4
- package/agents/init-agent.md +29 -21
- package/agents/playwright-generator-agent.md +0 -4
- package/agents/qa-testcase-agent.md +0 -4
- package/agents/refactor-agent.md +0 -4
- package/agents/scenario-agent.md +0 -4
- package/agents/scenario-planner-agent.md +0 -4
- package/agents/self-healing-agent.md +0 -4
- package/agents/transcript-agent.md +0 -4
- package/dist/cli-98db6h2q.js +101 -0
- package/dist/cli-kx32qnf3.js +67 -0
- package/dist/cli.js +10 -145
- package/dist/index.js +1 -1
- package/dist/mcp.js +71 -8
- package/package.json +1 -1
- package/scripts/codegen-env.mjs +74 -42
- package/scripts/voice/merger.mjs +44 -13
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 +
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
package/agents/init-agent.md
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
```
|
package/agents/refactor-agent.md
CHANGED
package/agents/scenario-agent.md
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
package/dist/mcp.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
loadAgent
|
|
4
|
-
|
|
5
|
-
} from "./cli-6c0wsk32.js";
|
|
3
|
+
loadAgent
|
|
4
|
+
} from "./cli-98db6h2q.js";
|
|
6
5
|
import {
|
|
7
6
|
getPackageRoot
|
|
8
|
-
} from "./cli-
|
|
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 =
|
|
14959
|
-
const content =
|
|
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
package/scripts/codegen-env.mjs
CHANGED
|
@@ -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
|
-
// ---
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
}
|
package/scripts/voice/merger.mjs
CHANGED
|
@@ -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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
//
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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(
|