levante 0.3.5 → 0.3.7
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 +263 -83
- package/dist/cli.js +64 -54
- package/dist/mcp.js +206 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Levante
|
|
2
2
|
|
|
3
|
-
AI-powered end-to-end test pipeline for Playwright. Record, transcribe, generate
|
|
3
|
+
AI-powered end-to-end test pipeline for Playwright. Record interactions, transcribe narration, generate Playwright tests, self-heal failures, and produce QA documentation — all driven by LLM agents.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install -g levante
|
|
9
|
+
# or
|
|
10
|
+
bun add -g levante
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
Levante requires Playwright as a peer dependency:
|
|
@@ -14,82 +16,109 @@ Levante requires Playwright as a peer dependency:
|
|
|
14
16
|
npm install -D @playwright/test
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
### Environment Variables
|
|
18
|
-
|
|
19
|
-
| Variable | Required | Description |
|
|
20
|
-
|----------|----------|-------------|
|
|
21
|
-
| `OPENAI_API_KEY` | Yes (if using OpenAI) | OpenAI API key |
|
|
22
|
-
| `ANTHROPIC_API_KEY` | Yes (if using Anthropic) | Anthropic API key |
|
|
23
|
-
| `E2E_AI_API_URL` | No | Remote API URL for QA map push |
|
|
24
|
-
| `E2E_AI_API_KEY` | No | API key for push authentication |
|
|
25
|
-
|
|
26
19
|
## Quick Start
|
|
27
20
|
|
|
28
21
|
```bash
|
|
29
|
-
# Initialize config and agents
|
|
22
|
+
# 1. Initialize config and agents
|
|
30
23
|
levante init
|
|
31
24
|
|
|
32
|
-
# Run full pipeline for a test case
|
|
25
|
+
# 2. Run full pipeline for a test case
|
|
33
26
|
levante run --key PROJ-101
|
|
34
27
|
|
|
35
|
-
# Or
|
|
28
|
+
# Or step by step
|
|
36
29
|
levante record --key PROJ-101
|
|
37
30
|
levante transcribe --key PROJ-101
|
|
38
31
|
levante scenario --key PROJ-101
|
|
39
32
|
levante generate --key PROJ-101
|
|
33
|
+
levante refine --key PROJ-101
|
|
40
34
|
levante test --key PROJ-101
|
|
35
|
+
levante heal --key PROJ-101 # only if test fails
|
|
36
|
+
levante qa --key PROJ-101
|
|
41
37
|
```
|
|
42
38
|
|
|
43
|
-
|
|
39
|
+
---
|
|
44
40
|
|
|
45
|
-
|
|
41
|
+
## Environment Variables
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
```
|
|
43
|
+
| Variable | Description |
|
|
44
|
+
|----------|-------------|
|
|
45
|
+
| `OPENAI_API_KEY` | OpenAI API key |
|
|
46
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key |
|
|
47
|
+
| `QAI_BASE_URL` | QA Intelligence API base URL |
|
|
48
|
+
| `QAI_API_URL` | Full push endpoint (overrides base URL) |
|
|
49
|
+
| `QAI_API_KEY` | API key for authenticated push |
|
|
55
50
|
|
|
56
|
-
|
|
51
|
+
---
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
## Commands
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
### `levante init`
|
|
56
|
+
|
|
57
|
+
Initialize levante in your project. Generates `.qai/levante/config.ts`, copies agent templates, and optionally connects to QA Intelligence.
|
|
61
58
|
|
|
62
59
|
```bash
|
|
63
60
|
levante init
|
|
64
|
-
levante init --non-interactive
|
|
61
|
+
levante init --non-interactive # skip prompts, use defaults
|
|
65
62
|
```
|
|
66
63
|
|
|
67
|
-
|
|
64
|
+
On re-run, preserves your existing config and context, and only updates agents.
|
|
65
|
+
|
|
66
|
+
**After init:**
|
|
67
|
+
1. Generate `.qai/levante/context.md` using the `init-agent` prompt in your AI tool, or via MCP (`levante_scan_codebase`)
|
|
68
|
+
2. Review the generated context
|
|
69
|
+
3. Start recording: `levante record --key PROJ-101`
|
|
70
|
+
|
|
71
|
+
---
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
### `levante record [session]`
|
|
74
|
+
|
|
75
|
+
Launch Playwright codegen with optional voice narration recording.
|
|
70
76
|
|
|
71
77
|
```bash
|
|
72
78
|
levante record --key PROJ-101
|
|
73
|
-
levante record --key PROJ-101 --no-voice
|
|
79
|
+
levante record --key PROJ-101 --no-companion # voice only, no companion UI
|
|
80
|
+
levante record --key PROJ-101 --no-voice # codegen only, no audio
|
|
74
81
|
```
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
| Option | Description |
|
|
84
|
+
|--------|-------------|
|
|
85
|
+
| `--no-companion` | Disable companion UI (voice recording only) |
|
|
86
|
+
| `--no-voice` | Disable voice recording entirely |
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
**Output:** codegen TypeScript file + `.wav` audio (if voice enabled)
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### `levante transcribe [session]`
|
|
93
|
+
|
|
94
|
+
Transcribe the `.wav` voice recording via OpenAI Whisper. If a live transcript from the recording session exists, Whisper is skipped.
|
|
79
95
|
|
|
80
96
|
```bash
|
|
81
97
|
levante transcribe --key PROJ-101
|
|
98
|
+
levante transcribe --key PROJ-101 --force # re-transcribe even if transcript exists
|
|
82
99
|
```
|
|
83
100
|
|
|
84
|
-
|
|
101
|
+
Merges voice annotations (with timestamps) back into the codegen file as inline comments.
|
|
102
|
+
|
|
103
|
+
**Output:** `*-transcript.json`, `*-transcript.md`, annotated codegen file
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### `levante scenario [session]`
|
|
85
108
|
|
|
86
|
-
Generate a structured YAML scenario from codegen output and transcript.
|
|
109
|
+
Generate a structured YAML scenario from codegen output and voice transcript.
|
|
87
110
|
|
|
88
111
|
```bash
|
|
89
112
|
levante scenario --key PROJ-101
|
|
90
113
|
```
|
|
91
114
|
|
|
92
|
-
|
|
115
|
+
Uses `transcript-agent` (if transcript available) to extract narrative and action intents, then `scenario-agent` to produce a YAML scenario with title, precondition, steps, and postcondition. Jira/Linear issue context is included automatically if `--key` is set.
|
|
116
|
+
|
|
117
|
+
**Output:** `.yaml` scenario file in `e2e/tests/[key]/`
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### `levante generate [scenario]`
|
|
93
122
|
|
|
94
123
|
Generate a Playwright `.test.ts` file from a YAML scenario.
|
|
95
124
|
|
|
@@ -97,118 +126,238 @@ Generate a Playwright `.test.ts` file from a YAML scenario.
|
|
|
97
126
|
levante generate --key PROJ-101
|
|
98
127
|
```
|
|
99
128
|
|
|
100
|
-
|
|
129
|
+
Uses `playwright-generator-agent` with scenario + project context. If Zephyr is configured, also generates a Zephyr test case export.
|
|
130
|
+
|
|
131
|
+
**Output:** `[key].test.ts` in `e2e/tests/[key]/`
|
|
132
|
+
|
|
133
|
+
---
|
|
101
134
|
|
|
102
|
-
|
|
135
|
+
### `levante refine [test]`
|
|
136
|
+
|
|
137
|
+
Refactor a generated test with AI — replaces raw selectors, improves structure, applies project patterns.
|
|
103
138
|
|
|
104
139
|
```bash
|
|
105
140
|
levante refine --key PROJ-101
|
|
106
141
|
```
|
|
107
142
|
|
|
108
|
-
|
|
143
|
+
Uses `refactor-agent`. Rewrites the test file in-place.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### `levante test [test]`
|
|
109
148
|
|
|
110
|
-
Run the Playwright test with trace
|
|
149
|
+
Run the Playwright test with trace, video, and screenshot capture.
|
|
111
150
|
|
|
112
151
|
```bash
|
|
113
152
|
levante test --key PROJ-101
|
|
153
|
+
levante test --key PROJ-101 --no-trace
|
|
114
154
|
```
|
|
115
155
|
|
|
116
|
-
|
|
156
|
+
**Output:** Test execution logs, trace files in `e2e/traces/`
|
|
117
157
|
|
|
118
|
-
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### `levante heal [test]`
|
|
161
|
+
|
|
162
|
+
Self-heal a failing test. Diagnoses the failure, patches the test, and re-runs — up to 3 retries.
|
|
119
163
|
|
|
120
164
|
```bash
|
|
121
165
|
levante heal --key PROJ-101
|
|
122
166
|
```
|
|
123
167
|
|
|
124
|
-
|
|
168
|
+
Each attempt uses `self-healing-agent` with test content + error output + trace data to produce:
|
|
169
|
+
- `diagnosis` (failure type, root cause, confidence)
|
|
170
|
+
- `patchedTest` (updated test file)
|
|
171
|
+
- `changes` (summary of what was fixed)
|
|
172
|
+
|
|
173
|
+
Exits with error if all 3 attempts fail.
|
|
125
174
|
|
|
126
|
-
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### `levante qa [test]`
|
|
178
|
+
|
|
179
|
+
Generate QA documentation from the test and scenario.
|
|
127
180
|
|
|
128
181
|
```bash
|
|
129
182
|
levante qa --key PROJ-101
|
|
130
183
|
```
|
|
131
184
|
|
|
132
|
-
|
|
185
|
+
Uses `qa-testcase-agent` with test + scenario + Jira context. Produces markdown and/or Zephyr export depending on `outputTarget`.
|
|
186
|
+
|
|
187
|
+
**Output:** `qa/[testId].md` and/or Zephyr JSON
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### `levante run [session]`
|
|
133
192
|
|
|
134
|
-
Run the full pipeline: record → transcribe → scenario → generate → refine → test → heal → qa
|
|
193
|
+
Run the full pipeline: `record → transcribe → scenario → generate → refine → test → heal → qa`.
|
|
135
194
|
|
|
136
195
|
```bash
|
|
137
196
|
levante run --key PROJ-101
|
|
138
|
-
levante run --key PROJ-101 --from generate
|
|
197
|
+
levante run --key PROJ-101 --from generate # resume from a specific step
|
|
139
198
|
levante run --key PROJ-101 --skip transcribe,heal
|
|
199
|
+
levante run --key PROJ-101 --no-voice # skip recording + transcription
|
|
140
200
|
```
|
|
141
201
|
|
|
142
202
|
| Option | Description |
|
|
143
203
|
|--------|-------------|
|
|
144
|
-
| `--from <step>` | Start from a specific step |
|
|
145
|
-
| `--skip <steps>` |
|
|
204
|
+
| `--from <step>` | Start from a specific step (skips earlier steps) |
|
|
205
|
+
| `--skip <steps>` | Comma-separated step names to skip |
|
|
206
|
+
| `--no-voice` | Disable voice recording (skips transcription too) |
|
|
207
|
+
| `--no-trace` | Disable trace capture |
|
|
146
208
|
|
|
147
|
-
|
|
209
|
+
**Steps (in order):** `record` → `transcribe` → `scenario` → `generate` → `refine` → `test` → `heal` → `qa`
|
|
148
210
|
|
|
149
|
-
|
|
211
|
+
---
|
|
150
212
|
|
|
151
|
-
###
|
|
213
|
+
### `levante auth`
|
|
214
|
+
|
|
215
|
+
Manage authentication with QA Intelligence.
|
|
152
216
|
|
|
153
217
|
```bash
|
|
154
|
-
|
|
218
|
+
levante auth login # open browser for OAuth sign-in
|
|
219
|
+
levante auth logout # revoke CLI token
|
|
220
|
+
levante auth status # show current auth status
|
|
221
|
+
levante auth switch # re-authenticate with a different org/project/app
|
|
155
222
|
```
|
|
156
223
|
|
|
157
|
-
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### `levante jira`
|
|
227
|
+
|
|
228
|
+
Interactive Jira integration for issue discovery and workflow launch.
|
|
158
229
|
|
|
159
|
-
|
|
230
|
+
#### `levante jira browse`
|
|
160
231
|
|
|
232
|
+
Interactive browser — search, view, and select Jira issues to start a test workflow.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
levante jira browse
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Select an issue to:
|
|
239
|
+
- **run** — save context and launch `levante run --key <KEY>`
|
|
240
|
+
- **save** — save issue context without running
|
|
241
|
+
- **view** — display full issue details
|
|
242
|
+
|
|
243
|
+
#### `levante jira search <query>`
|
|
244
|
+
|
|
245
|
+
Search Jira issues by text or raw JQL.
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
levante jira search "login flow"
|
|
249
|
+
levante jira search --jql "project = QA AND status = 'In Progress'"
|
|
250
|
+
levante jira search "dashboard" --max 10
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
| Option | Description |
|
|
254
|
+
|--------|-------------|
|
|
255
|
+
| `<query>` | Text search query |
|
|
256
|
+
| `--jql <jql>` | Use raw JQL instead of text search |
|
|
257
|
+
| `--max <n>` | Maximum results to return (default: 20) |
|
|
258
|
+
|
|
259
|
+
#### `levante jira show <issueKey>`
|
|
260
|
+
|
|
261
|
+
Display full details for a Jira issue.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
levante jira show PROJ-101
|
|
265
|
+
levante jira show PROJ-101 --save # also save context for pipeline use
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
| Option | Description |
|
|
269
|
+
|--------|-------------|
|
|
270
|
+
| `--save` | Save issue context to `.qai/levante/issues/[KEY].json` |
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
### `levante mcp`
|
|
275
|
+
|
|
276
|
+
Print MCP server setup instructions.
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
levante mcp
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Claude Code:**
|
|
283
|
+
```bash
|
|
284
|
+
claude mcp add levante -- levante-mcp # project-scoped
|
|
285
|
+
claude mcp add levante -s user -- levante-mcp # global
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Claude Desktop / other clients:**
|
|
161
289
|
```json
|
|
162
290
|
{
|
|
163
291
|
"mcpServers": {
|
|
164
|
-
"levante": {
|
|
165
|
-
"command": "npx",
|
|
166
|
-
"args": ["levante-mcp"]
|
|
167
|
-
}
|
|
292
|
+
"levante": { "command": "levante-mcp" }
|
|
168
293
|
}
|
|
169
294
|
}
|
|
170
295
|
```
|
|
171
296
|
|
|
172
|
-
|
|
297
|
+
**Available MCP tools:**
|
|
173
298
|
|
|
174
299
|
| Tool | Description |
|
|
175
300
|
|------|-------------|
|
|
176
301
|
| `levante_plan_workflow` | Get ordered step list with prerequisite checks |
|
|
177
302
|
| `levante_execute_step` | Execute a single pipeline step |
|
|
178
|
-
| `levante_get_workflow_guide` |
|
|
303
|
+
| `levante_get_workflow_guide` | Read the full workflow guide |
|
|
179
304
|
| `levante_scan_codebase` | Scan project for test infrastructure |
|
|
180
305
|
| `levante_validate_context` | Validate context.md completeness |
|
|
181
306
|
| `levante_read_agent` | Load an agent prompt by name |
|
|
182
|
-
| `levante_get_example` | Get example context.md template |
|
|
307
|
+
| `levante_get_example` | Get an example context.md template |
|
|
183
308
|
| `levante_scan_ast` | Run AST scanner |
|
|
184
309
|
| `levante_scan_ast_detail` | Drill into routes/components/hooks |
|
|
185
310
|
| `levante_build_qa_map` | Build and validate QA map |
|
|
186
311
|
| `levante_read_qa_map` | Load existing QA map |
|
|
187
312
|
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Global Options
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
-k, --key <KEY> Issue key (e.g. PROJ-101, LIN-42)
|
|
319
|
+
--provider <provider> LLM provider: openai | anthropic
|
|
320
|
+
--model <model> LLM model override
|
|
321
|
+
--verbose Verbose output
|
|
322
|
+
--no-voice Disable voice recording
|
|
323
|
+
--no-trace Disable trace capture
|
|
324
|
+
-v, --version Show version
|
|
325
|
+
-h, --help Show help
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
188
330
|
## Configuration
|
|
189
331
|
|
|
190
|
-
Configuration lives in `.qai/levante/config.ts
|
|
332
|
+
Configuration lives in `.qai/levante/config.ts` (generated by `levante init`):
|
|
191
333
|
|
|
192
334
|
```typescript
|
|
193
|
-
import { defineConfig } from 'levante
|
|
335
|
+
import { defineConfig } from 'levante';
|
|
194
336
|
|
|
195
337
|
export default defineConfig({
|
|
196
|
-
inputSource: 'jira',
|
|
197
|
-
outputTarget: 'both',
|
|
338
|
+
inputSource: 'jira', // 'none' | 'jira' | 'linear'
|
|
339
|
+
outputTarget: 'both', // 'markdown' | 'zephyr' | 'both'
|
|
198
340
|
baseUrl: 'http://localhost:3000',
|
|
199
341
|
|
|
200
342
|
llm: {
|
|
201
|
-
provider: 'openai',
|
|
202
|
-
model: 'gpt-4o',
|
|
203
|
-
agentModels: {
|
|
343
|
+
provider: 'openai', // 'openai' | 'anthropic'
|
|
344
|
+
model: 'gpt-4o',
|
|
345
|
+
agentModels: { // per-agent model overrides
|
|
204
346
|
'scenario-agent': 'claude-sonnet-4-20250514',
|
|
205
347
|
},
|
|
206
348
|
},
|
|
207
349
|
|
|
208
350
|
playwright: {
|
|
209
|
-
browser: 'chromium',
|
|
351
|
+
browser: 'chromium', // 'chromium' | 'firefox' | 'webkit'
|
|
210
352
|
timeout: 120_000,
|
|
211
|
-
|
|
353
|
+
retries: 0,
|
|
354
|
+
traceMode: 'on', // 'on' | 'off' | 'retain-on-failure'
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
voice: {
|
|
358
|
+
enabled: true,
|
|
359
|
+
engine: 'webspeech', // 'webspeech' | 'whisper'
|
|
360
|
+
language: 'en-US',
|
|
212
361
|
},
|
|
213
362
|
|
|
214
363
|
paths: {
|
|
@@ -221,36 +370,67 @@ export default defineConfig({
|
|
|
221
370
|
},
|
|
222
371
|
|
|
223
372
|
integrations: {
|
|
224
|
-
jira: { /*
|
|
373
|
+
jira: { /* Jira config */ },
|
|
225
374
|
zephyr: { titlePrefix: 'UI Automation' },
|
|
226
375
|
},
|
|
227
376
|
|
|
228
377
|
push: {
|
|
229
|
-
apiUrl: 'https://
|
|
378
|
+
apiUrl: 'https://qaligent.space/api',
|
|
230
379
|
apiKey: 'your-key',
|
|
231
380
|
},
|
|
232
381
|
});
|
|
233
382
|
```
|
|
234
383
|
|
|
235
|
-
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## AI Agents
|
|
387
|
+
|
|
388
|
+
Levante ships with agents copied to `.qai/levante/agents/` on `init`. Edit them to tune AI behavior for your project.
|
|
236
389
|
|
|
237
|
-
|
|
390
|
+
| Agent | Purpose |
|
|
391
|
+
|-------|---------|
|
|
392
|
+
| `0.init-agent` | Generate project `context.md` |
|
|
393
|
+
| `1_1.transcript-agent` | Analyze voice transcript + codegen into narrative |
|
|
394
|
+
| `1_2.scenario-agent` | Generate YAML scenario from narrative and actions |
|
|
395
|
+
| `2.playwright-generator-agent` | Generate Playwright test from YAML scenario |
|
|
396
|
+
| `3.refactor-agent` | Refactor test code for quality and conventions |
|
|
397
|
+
| `4.self-healing-agent` | Diagnose and patch failing tests |
|
|
398
|
+
| `5.qa-testcase-agent` | Generate QA documentation and Zephyr export |
|
|
399
|
+
|
|
400
|
+
Add `.qai/levante/context.md` to inject project-specific context into every agent call.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Project Structure
|
|
238
405
|
|
|
239
406
|
```
|
|
240
407
|
your-project/
|
|
241
408
|
.qai/levante/
|
|
242
|
-
config.ts
|
|
243
|
-
context.md
|
|
244
|
-
agents/
|
|
245
|
-
workflow.md
|
|
409
|
+
config.ts # Configuration
|
|
410
|
+
context.md # Project context for AI agents
|
|
411
|
+
agents/ # Agent prompts (customizable)
|
|
412
|
+
workflow.md # Pipeline workflow guide
|
|
413
|
+
issues/ # Saved Jira issue contexts
|
|
246
414
|
e2e/
|
|
247
|
-
tests/{key}/
|
|
248
|
-
recordings/
|
|
249
|
-
transcripts/
|
|
250
|
-
traces/
|
|
251
|
-
qa/
|
|
415
|
+
tests/{key}/ # Generated test files + scenario YAML
|
|
416
|
+
recordings/ # Codegen + voice recordings
|
|
417
|
+
transcripts/ # Transcription output
|
|
418
|
+
traces/ # Playwright traces
|
|
419
|
+
qa/ # QA documentation output
|
|
252
420
|
```
|
|
253
421
|
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Interactive TUI
|
|
425
|
+
|
|
426
|
+
Running `levante` with no arguments launches an interactive terminal UI (requires TTY, ≥ 60×20).
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
levante
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
254
434
|
## License
|
|
255
435
|
|
|
256
436
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -57497,19 +57497,22 @@ async function launchTui(program2) {
|
|
|
57497
57497
|
version: version2,
|
|
57498
57498
|
onRunCommand: (argv) => {
|
|
57499
57499
|
runningCommand = true;
|
|
57500
|
-
instance.waitUntilExit().then(
|
|
57501
|
-
|
|
57502
|
-
|
|
57500
|
+
instance.waitUntilExit().then(() => {
|
|
57501
|
+
setTimeout(async () => {
|
|
57502
|
+
process.stdin.resume();
|
|
57503
|
+
process.stdin.ref();
|
|
57504
|
+
process.stdout.write(`
|
|
57503
57505
|
$ levante ${argv.join(" ")}
|
|
57504
57506
|
|
|
57505
57507
|
`);
|
|
57506
|
-
|
|
57507
|
-
|
|
57508
|
-
|
|
57509
|
-
|
|
57510
|
-
|
|
57511
|
-
|
|
57512
|
-
|
|
57508
|
+
try {
|
|
57509
|
+
await program2.parseAsync(argv, { from: "user" });
|
|
57510
|
+
} catch (err) {
|
|
57511
|
+
if (err?.name !== "ExitPromptError")
|
|
57512
|
+
throw err;
|
|
57513
|
+
}
|
|
57514
|
+
done();
|
|
57515
|
+
}, 50);
|
|
57513
57516
|
});
|
|
57514
57517
|
},
|
|
57515
57518
|
onQuit: done
|
|
@@ -75507,54 +75510,61 @@ async function migrateFromLegacy(projectRoot, nonInteractive) {
|
|
|
75507
75510
|
// src/commands/init.ts
|
|
75508
75511
|
function registerInit(program2) {
|
|
75509
75512
|
program2.command("init").description("Initialize levante configuration for your project").option("--non-interactive", "Skip interactive prompts, use defaults").action(async (cmdOpts) => {
|
|
75510
|
-
|
|
75511
|
-
|
|
75512
|
-
|
|
75513
|
-
|
|
75514
|
-
|
|
75515
|
-
|
|
75516
|
-
|
|
75517
|
-
|
|
75518
|
-
|
|
75513
|
+
try {
|
|
75514
|
+
const projectRoot = getProjectRoot();
|
|
75515
|
+
const nonInteractive = !!cmdOpts?.nonInteractive;
|
|
75516
|
+
header("levante init");
|
|
75517
|
+
await migrateFromLegacy(projectRoot, nonInteractive);
|
|
75518
|
+
const qaiDir = join17(projectRoot, CONFIG_DIR);
|
|
75519
|
+
const configPath = join17(qaiDir, "config.ts");
|
|
75520
|
+
const isReInit = fileExists(configPath);
|
|
75521
|
+
if (isReInit) {
|
|
75522
|
+
info(`Existing ${CONFIG_DIR}/ detected — preserving config and context.
|
|
75519
75523
|
`);
|
|
75520
|
-
|
|
75521
|
-
|
|
75522
|
-
|
|
75523
|
-
|
|
75524
|
-
|
|
75525
|
-
|
|
75526
|
-
|
|
75527
|
-
|
|
75528
|
-
|
|
75529
|
-
|
|
75530
|
-
|
|
75531
|
-
|
|
75532
|
-
|
|
75533
|
-
|
|
75534
|
-
|
|
75535
|
-
|
|
75536
|
-
|
|
75537
|
-
|
|
75538
|
-
|
|
75539
|
-
|
|
75540
|
-
|
|
75541
|
-
|
|
75542
|
-
|
|
75543
|
-
|
|
75524
|
+
await copyAgentsToLocal(projectRoot, nonInteractive);
|
|
75525
|
+
await copyWorkflowGuide(projectRoot, nonInteractive);
|
|
75526
|
+
} else {
|
|
75527
|
+
const answers = nonInteractive ? getDefaultAnswers() : await askConfigQuestions();
|
|
75528
|
+
const config2 = buildConfigFromAnswers(answers);
|
|
75529
|
+
writeFile(configPath, generateConfigFile(config2));
|
|
75530
|
+
success2(`Config written: ${configPath}`);
|
|
75531
|
+
await copyAgentsToLocal(projectRoot, nonInteractive);
|
|
75532
|
+
await copyWorkflowGuide(projectRoot, nonInteractive);
|
|
75533
|
+
}
|
|
75534
|
+
if (!nonInteractive) {
|
|
75535
|
+
const { confirm: confirmPrompt } = await Promise.resolve().then(() => (init_dist9(), exports_dist));
|
|
75536
|
+
const connectAuth = await confirmPrompt({
|
|
75537
|
+
message: "Connect to QA Intelligence?",
|
|
75538
|
+
default: false
|
|
75539
|
+
});
|
|
75540
|
+
if (connectAuth) {
|
|
75541
|
+
try {
|
|
75542
|
+
const { login: login2 } = await Promise.resolve().then(() => (init_dist10(), exports_dist2));
|
|
75543
|
+
const session = await login2({ webappUrl: "https://qaligent.space" });
|
|
75544
|
+
success2(`Authenticated as ${session.user.email}`);
|
|
75545
|
+
success2(`Linked to: ${session.scope.orgSlug} / ${session.scope.projectSlug} / ${session.scope.appSlug}`);
|
|
75546
|
+
} catch (err) {
|
|
75547
|
+
warn(`Authentication skipped: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
75548
|
+
}
|
|
75544
75549
|
}
|
|
75545
75550
|
}
|
|
75546
|
-
|
|
75547
|
-
|
|
75548
|
-
success2(`Initialization complete!
|
|
75551
|
+
console.log("");
|
|
75552
|
+
success2(`Initialization complete!
|
|
75549
75553
|
`);
|
|
75550
|
-
|
|
75551
|
-
|
|
75552
|
-
|
|
75553
|
-
|
|
75554
|
-
|
|
75555
|
-
|
|
75556
|
-
|
|
75557
|
-
|
|
75554
|
+
if (!isReInit) {
|
|
75555
|
+
console.log(import_picocolors5.default.bold("Next steps:"));
|
|
75556
|
+
console.log(` 1. Use the ${import_picocolors5.default.cyan("init-agent")} in your AI tool to generate ${import_picocolors5.default.cyan(`${CONFIG_DIR}/context.md`)}`);
|
|
75557
|
+
console.log(` (or use the MCP server: ${import_picocolors5.default.cyan("levante_scan_codebase")} + ${import_picocolors5.default.cyan("levante_read_agent")})`);
|
|
75558
|
+
console.log(` 2. Review the generated ${import_picocolors5.default.cyan(`${CONFIG_DIR}/context.md`)}`);
|
|
75559
|
+
console.log(` 3. Run: ${import_picocolors5.default.cyan("levante run --key PROJ-101")}`);
|
|
75560
|
+
} else {
|
|
75561
|
+
console.log(import_picocolors5.default.dim("Config and context.md were preserved. Only agents and workflow were checked."));
|
|
75562
|
+
}
|
|
75563
|
+
} catch (err) {
|
|
75564
|
+
if (err?.name === "ExitPromptError") {
|
|
75565
|
+
process.exit(0);
|
|
75566
|
+
}
|
|
75567
|
+
throw err;
|
|
75558
75568
|
}
|
|
75559
75569
|
});
|
|
75560
75570
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -28540,7 +28540,7 @@ class StdioServerTransport {
|
|
|
28540
28540
|
// src/mcp.ts
|
|
28541
28541
|
import { execSync } from "node:child_process";
|
|
28542
28542
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
28543
|
-
import { join as
|
|
28543
|
+
import { join as join6 } from "node:path";
|
|
28544
28544
|
|
|
28545
28545
|
// src/agents/loadAgent.ts
|
|
28546
28546
|
import { readFileSync, existsSync as existsSync2, readdirSync } from "node:fs";
|
|
@@ -28726,6 +28726,44 @@ function parseSections(body) {
|
|
|
28726
28726
|
return sections;
|
|
28727
28727
|
}
|
|
28728
28728
|
|
|
28729
|
+
// src/config/paths.ts
|
|
28730
|
+
import { join as join3 } from "node:path";
|
|
28731
|
+
function resolvePaths(config2, key) {
|
|
28732
|
+
const root = getProjectRoot();
|
|
28733
|
+
const workingDir = join3(root, config2.paths.workingDir);
|
|
28734
|
+
const testsDir = join3(root, config2.paths.tests);
|
|
28735
|
+
const recordingsDir = join3(root, config2.paths.recordings);
|
|
28736
|
+
const transcriptsDir = join3(root, config2.paths.transcripts);
|
|
28737
|
+
const tracesDir = join3(root, config2.paths.traces);
|
|
28738
|
+
const qaDir = join3(root, config2.paths.qaOutput);
|
|
28739
|
+
const keyDir = key ? join3(workingDir, key) : null;
|
|
28740
|
+
const testDir = key ? join3(testsDir, key) : null;
|
|
28741
|
+
const testFile = key ? join3(testsDir, key, `${key}.test.ts`) : null;
|
|
28742
|
+
const scenarioDir = key ? join3(testsDir, key) : null;
|
|
28743
|
+
const scenarioFile = key ? join3(testsDir, key, `${key}.yaml`) : null;
|
|
28744
|
+
const qaFile = key ? join3(qaDir, `${key}.md`) : null;
|
|
28745
|
+
const needsZephyr = config2.outputTarget === "zephyr" || config2.outputTarget === "both";
|
|
28746
|
+
const zephyrJsonFile = needsZephyr && key ? join3(workingDir, key, `${key}-zephyr-test-case.json`) : null;
|
|
28747
|
+
const zephyrXmlFile = needsZephyr && key ? join3(testsDir, key, `${key}-zephyr-import.xml`) : null;
|
|
28748
|
+
return {
|
|
28749
|
+
projectRoot: root,
|
|
28750
|
+
workingDir,
|
|
28751
|
+
keyDir,
|
|
28752
|
+
testsDir,
|
|
28753
|
+
testDir,
|
|
28754
|
+
testFile,
|
|
28755
|
+
scenarioDir,
|
|
28756
|
+
scenarioFile,
|
|
28757
|
+
recordingsDir,
|
|
28758
|
+
transcriptsDir,
|
|
28759
|
+
tracesDir,
|
|
28760
|
+
qaDir,
|
|
28761
|
+
qaFile,
|
|
28762
|
+
zephyrJsonFile,
|
|
28763
|
+
zephyrXmlFile
|
|
28764
|
+
};
|
|
28765
|
+
}
|
|
28766
|
+
|
|
28729
28767
|
// src/scanner/scanner.ts
|
|
28730
28768
|
import { readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync, writeFileSync } from "node:fs";
|
|
28731
28769
|
import { resolve as resolve2, relative as relative2 } from "node:path";
|
|
@@ -32218,10 +32256,10 @@ class TypeScriptParser {
|
|
|
32218
32256
|
|
|
32219
32257
|
// src/scanner/extractors/routes.ts
|
|
32220
32258
|
import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync2 } from "node:fs";
|
|
32221
|
-
import { join as
|
|
32259
|
+
import { join as join4, relative, basename, dirname as dirname2 } from "node:path";
|
|
32222
32260
|
function extractRoutes(scanDir) {
|
|
32223
32261
|
const routes = [];
|
|
32224
|
-
const appDirs = ["app", "src/app"].map((d) =>
|
|
32262
|
+
const appDirs = ["app", "src/app"].map((d) => join4(scanDir, d));
|
|
32225
32263
|
for (const appDir of appDirs) {
|
|
32226
32264
|
try {
|
|
32227
32265
|
if (statSync(appDir).isDirectory()) {
|
|
@@ -32229,7 +32267,7 @@ function extractRoutes(scanDir) {
|
|
|
32229
32267
|
}
|
|
32230
32268
|
} catch {}
|
|
32231
32269
|
}
|
|
32232
|
-
const pagesDirs = ["pages", "src/pages"].map((d) =>
|
|
32270
|
+
const pagesDirs = ["pages", "src/pages"].map((d) => join4(scanDir, d));
|
|
32233
32271
|
for (const pagesDir of pagesDirs) {
|
|
32234
32272
|
try {
|
|
32235
32273
|
if (statSync(pagesDir).isDirectory()) {
|
|
@@ -32242,7 +32280,7 @@ function extractRoutes(scanDir) {
|
|
|
32242
32280
|
function extractAppRouterRoutes(dir, baseDir, routes) {
|
|
32243
32281
|
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
32244
32282
|
for (const entry of entries) {
|
|
32245
|
-
const fullPath =
|
|
32283
|
+
const fullPath = join4(dir, entry.name);
|
|
32246
32284
|
if (entry.isDirectory()) {
|
|
32247
32285
|
extractAppRouterRoutes(fullPath, baseDir, routes);
|
|
32248
32286
|
continue;
|
|
@@ -32278,7 +32316,7 @@ function extractAppRouterRoutes(dir, baseDir, routes) {
|
|
|
32278
32316
|
function extractPagesRouterRoutes(dir, baseDir, routes) {
|
|
32279
32317
|
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
32280
32318
|
for (const entry of entries) {
|
|
32281
|
-
const fullPath =
|
|
32319
|
+
const fullPath = join4(dir, entry.name);
|
|
32282
32320
|
if (entry.isDirectory()) {
|
|
32283
32321
|
if (entry.name === "api") {
|
|
32284
32322
|
extractPagesApiRoutes(fullPath, baseDir, routes);
|
|
@@ -32304,7 +32342,7 @@ function extractPagesRouterRoutes(dir, baseDir, routes) {
|
|
|
32304
32342
|
function extractPagesApiRoutes(dir, baseDir, routes) {
|
|
32305
32343
|
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
32306
32344
|
for (const entry of entries) {
|
|
32307
|
-
const fullPath =
|
|
32345
|
+
const fullPath = join4(dir, entry.name);
|
|
32308
32346
|
if (entry.isDirectory()) {
|
|
32309
32347
|
extractPagesApiRoutes(fullPath, baseDir, routes);
|
|
32310
32348
|
continue;
|
|
@@ -32332,7 +32370,7 @@ function findLayoutFile(dir, baseDir) {
|
|
|
32332
32370
|
let current = dir;
|
|
32333
32371
|
while (current.startsWith(baseDir)) {
|
|
32334
32372
|
for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
|
|
32335
|
-
const layoutPath =
|
|
32373
|
+
const layoutPath = join4(current, `layout${ext}`);
|
|
32336
32374
|
try {
|
|
32337
32375
|
statSync(layoutPath);
|
|
32338
32376
|
return layoutPath;
|
|
@@ -32793,7 +32831,7 @@ function requireEnum(obj, field, allowed, context, errors3) {
|
|
|
32793
32831
|
|
|
32794
32832
|
// src/utils/scan.ts
|
|
32795
32833
|
import { readdirSync as readdirSync3, existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
32796
|
-
import { join as
|
|
32834
|
+
import { join as join5, relative as relative3 } from "node:path";
|
|
32797
32835
|
async function scanCodebase(root) {
|
|
32798
32836
|
const scan = {
|
|
32799
32837
|
testFiles: [],
|
|
@@ -32812,7 +32850,7 @@ async function scanCodebase(root) {
|
|
|
32812
32850
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
32813
32851
|
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
|
|
32814
32852
|
continue;
|
|
32815
|
-
const full =
|
|
32853
|
+
const full = join5(dir, entry.name);
|
|
32816
32854
|
if (entry.isDirectory()) {
|
|
32817
32855
|
files.push(...walk(full, depth + 1));
|
|
32818
32856
|
} else {
|
|
@@ -32849,7 +32887,7 @@ async function scanCodebase(root) {
|
|
|
32849
32887
|
}
|
|
32850
32888
|
}
|
|
32851
32889
|
for (const name of ["playwright.config.ts", "vitest.config.ts", "jest.config.ts", "tsconfig.json", "package.json"]) {
|
|
32852
|
-
if (existsSync4(
|
|
32890
|
+
if (existsSync4(join5(root, name)))
|
|
32853
32891
|
scan.configFiles.push(name);
|
|
32854
32892
|
}
|
|
32855
32893
|
return scan;
|
|
@@ -32898,38 +32936,60 @@ You have access to Levante, an AI-powered E2E test automation tool. Follow this
|
|
|
32898
32936
|
|
|
32899
32937
|
NEVER run multiple pipeline steps at once. Each step is a separate job with its own context.
|
|
32900
32938
|
|
|
32939
|
+
## Two Modes of Execution
|
|
32940
|
+
|
|
32941
|
+
Steps fall into two categories:
|
|
32942
|
+
|
|
32943
|
+
### 1. Tool Steps — use \`levante_execute_step\`
|
|
32944
|
+
These steps do NOT require an LLM. Run them via \`levante_execute_step\`:
|
|
32945
|
+
- \`record\` — opens Playwright browser codegen (interactive)
|
|
32946
|
+
- \`test\` — runs Playwright tests
|
|
32947
|
+
- \`push\` — pushes QA map to remote API
|
|
32948
|
+
- \`scan\` — runs the filesystem/AST scan
|
|
32949
|
+
|
|
32950
|
+
### 2. Agent Steps — YOU are the AI, do these yourself
|
|
32951
|
+
These steps require LLM reasoning. Do NOT call \`levante_execute_step\` for them. Instead, load the agent prompt and act on it directly:
|
|
32952
|
+
|
|
32953
|
+
| Step | Agent to load | Input to read | Output to write |
|
|
32954
|
+
|------|--------------|---------------|-----------------|
|
|
32955
|
+
| \`transcribe\` | \`transcript-agent\` | codegen file via \`levante_read_pipeline_file(key, "codegen")\` | transcript JSON via \`levante_write_pipeline_file\` |
|
|
32956
|
+
| \`scenario\` | \`scenario-agent\` | codegen + optional transcript | scenario YAML via \`levante_write_pipeline_file(key, "scenario", ...)\` |
|
|
32957
|
+
| \`generate\` | \`playwright-generator-agent\` | scenario YAML | test file via \`levante_write_pipeline_file(key, "test", ...)\` |
|
|
32958
|
+
| \`refine\` | \`refactor-agent\` | test file | updated test file (overwrite via \`levante_write_pipeline_file\`) |
|
|
32959
|
+
| \`heal\` | \`self-healing-agent\` | test file + error output | patched test file |
|
|
32960
|
+
| \`qa\` | \`qa-testcase-agent\` | test file + scenario | QA markdown via \`levante_write_pipeline_file(key, "qa", ...)\` |
|
|
32961
|
+
| \`analyze\` | \`feature-analyzer-agent\` | AST via \`levante_scan_ast_detail\` | QA map via \`levante_build_qa_map\` |
|
|
32962
|
+
|
|
32963
|
+
**Workflow for each agent step:**
|
|
32964
|
+
1. Call \`levante_read_agent("<agent-name>")\` to load the system prompt and config.
|
|
32965
|
+
2. Call \`levante_read_pipeline_file(key, "<type>")\` to load the input artifact.
|
|
32966
|
+
3. Read \`.qai/levante/context.md\` if it exists — this is project context the agent needs.
|
|
32967
|
+
4. Apply the agent system prompt to your own reasoning and produce the output.
|
|
32968
|
+
5. Call \`levante_write_pipeline_file(key, "<type>", content)\` to save the output.
|
|
32969
|
+
|
|
32901
32970
|
## Protocol
|
|
32902
32971
|
|
|
32903
|
-
1. **Plan first.** Call \`levante_plan_workflow\` with the user's goal
|
|
32904
|
-
2. **
|
|
32905
|
-
3. **
|
|
32906
|
-
|
|
32907
|
-
|
|
32908
|
-
|
|
32909
|
-
|
|
32910
|
-
d. If the step fails, stop and discuss with the user before continuing.
|
|
32911
|
-
e. Move to the next step only after the current one succeeds.
|
|
32912
|
-
5. **Use subagents when available.** If your AI platform supports subagents (e.g., Claude Code Agent tool), dispatch each step as a dedicated subagent to preserve context. Each subagent should:
|
|
32913
|
-
- Receive only the context it needs (step name, key, relevant file paths)
|
|
32914
|
-
- Call \`levante_execute_step\` to do its work
|
|
32915
|
-
- Return the result to the orchestrator
|
|
32972
|
+
1. **Plan first.** Call \`levante_plan_workflow\` with the user's goal to get a structured step list.
|
|
32973
|
+
2. **Present the plan.** Show the user the ordered steps. Ask for confirmation before proceeding.
|
|
32974
|
+
3. **Execute one step at a time.** For each step:
|
|
32975
|
+
a. Announce which step you're running and why.
|
|
32976
|
+
b. Use **Tool Steps** via \`levante_execute_step\`, or handle **Agent Steps** yourself (see above).
|
|
32977
|
+
c. Report the result. If a step fails, stop and discuss before continuing.
|
|
32978
|
+
4. **Use subagents when available.** If your AI platform supports subagents (Claude Code Agent tool), dispatch each step as a dedicated subagent. Each subagent should receive only the context it needs.
|
|
32916
32979
|
|
|
32917
32980
|
## Step Dependencies
|
|
32918
32981
|
|
|
32919
|
-
Steps produce artifacts
|
|
32982
|
+
Steps produce artifacts consumed by later steps. Do not skip steps unless the plan says a step can be skipped.
|
|
32920
32983
|
|
|
32921
32984
|
## Interactive Steps
|
|
32922
32985
|
|
|
32923
|
-
The \`record\` step opens a browser
|
|
32924
|
-
- Tell the user they need to interact with the browser window
|
|
32925
|
-
- The step will block until they close the codegen window
|
|
32926
|
-
- After recording completes, proceed with the next step
|
|
32986
|
+
The \`record\` step opens a browser. Tell the user to interact with it — the step blocks until they close the codegen window.
|
|
32927
32987
|
|
|
32928
32988
|
## When Things Fail
|
|
32929
32989
|
|
|
32930
|
-
- If \`test\` fails and \`heal\` is in the plan, that's expected —
|
|
32931
|
-
- If
|
|
32932
|
-
- For any other failure, stop and ask the user how to proceed
|
|
32990
|
+
- If \`test\` fails and \`heal\` is in the plan, that's expected — do the \`heal\` agent step yourself.
|
|
32991
|
+
- If healing fails after multiple attempts, stop and show the user the last error.
|
|
32992
|
+
- For any other failure, stop and ask the user how to proceed.
|
|
32933
32993
|
|
|
32934
32994
|
## Available Workflows
|
|
32935
32995
|
|
|
@@ -32945,7 +33005,7 @@ Always use \`levante_plan_workflow\` to determine the right steps — don't gues
|
|
|
32945
33005
|
|
|
32946
33006
|
## Scanner Analysis (Interactive QA Map)
|
|
32947
33007
|
|
|
32948
|
-
For deep codebase analysis and QA map generation, use the interactive scanner workflow
|
|
33008
|
+
For deep codebase analysis and QA map generation, use the interactive scanner workflow:
|
|
32949
33009
|
|
|
32950
33010
|
1. **Load the protocol.** Call \`levante_read_agent("scanner-agent")\` — this returns the full interactive protocol.
|
|
32951
33011
|
2. **Scan.** Call \`levante_scan_ast()\` to run the AST scanner and get a compact summary.
|
|
@@ -32954,8 +33014,6 @@ For deep codebase analysis and QA map generation, use the interactive scanner wo
|
|
|
32954
33014
|
5. **Build.** Construct the QA map payload and validate with \`levante_build_qa_map({ dryRun: true })\`.
|
|
32955
33015
|
6. **Write.** Once validated and approved, call \`levante_build_qa_map({ dryRun: false })\` to save.
|
|
32956
33016
|
7. **Read existing.** Use \`levante_read_qa_map()\` to load a previously generated QA map for incremental updates.
|
|
32957
|
-
|
|
32958
|
-
This approach is preferred over \`scan → analyze\` CLI steps because it allows interactive refinement with the user.
|
|
32959
33017
|
`.trim();
|
|
32960
33018
|
var TEST_PIPELINE_STEPS = [
|
|
32961
33019
|
{
|
|
@@ -33043,41 +33101,20 @@ var SCANNER_PIPELINE_STEPS = [
|
|
|
33043
33101
|
var ALL_STEPS = [...TEST_PIPELINE_STEPS, ...SCANNER_PIPELINE_STEPS];
|
|
33044
33102
|
var STEP_REQUIREMENTS = {
|
|
33045
33103
|
record: { envVars: [] },
|
|
33046
|
-
transcribe: { envVars: [
|
|
33047
|
-
scenario: { envVars: [
|
|
33048
|
-
|
|
33049
|
-
|
|
33050
|
-
] },
|
|
33051
|
-
generate: { envVars: [
|
|
33052
|
-
{ name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
|
|
33053
|
-
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
33054
|
-
] },
|
|
33055
|
-
refine: { envVars: [
|
|
33056
|
-
{ name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
|
|
33057
|
-
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
33058
|
-
] },
|
|
33104
|
+
transcribe: { envVars: [] },
|
|
33105
|
+
scenario: { envVars: [] },
|
|
33106
|
+
generate: { envVars: [] },
|
|
33107
|
+
refine: { envVars: [] },
|
|
33059
33108
|
test: { envVars: [] },
|
|
33060
|
-
heal: { envVars: [
|
|
33061
|
-
|
|
33062
|
-
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
33063
|
-
] },
|
|
33064
|
-
qa: { envVars: [
|
|
33065
|
-
{ name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
|
|
33066
|
-
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
33067
|
-
] },
|
|
33109
|
+
heal: { envVars: [] },
|
|
33110
|
+
qa: { envVars: [] },
|
|
33068
33111
|
scan: { envVars: [] },
|
|
33069
|
-
analyze: { envVars: [
|
|
33070
|
-
{ name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
|
|
33071
|
-
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
33072
|
-
] },
|
|
33112
|
+
analyze: { envVars: [] },
|
|
33073
33113
|
push: { envVars: [
|
|
33074
33114
|
{ name: "E2E_AI_API_URL", reason: "Push requires API URL (set E2E_AI_API_URL or push.apiUrl in config)" },
|
|
33075
33115
|
{ name: "E2E_AI_API_KEY", reason: "Push requires API key (set E2E_AI_API_KEY or push.apiKey in config)" }
|
|
33076
33116
|
] }
|
|
33077
33117
|
};
|
|
33078
|
-
function getProvider() {
|
|
33079
|
-
return process.env.AI_PROVIDER ?? "openai";
|
|
33080
|
-
}
|
|
33081
33118
|
function checkPrerequisites(stepNames) {
|
|
33082
33119
|
const issueMap = new Map;
|
|
33083
33120
|
for (const stepName of stepNames) {
|
|
@@ -33085,8 +33122,6 @@ function checkPrerequisites(stepNames) {
|
|
|
33085
33122
|
if (!reqs)
|
|
33086
33123
|
continue;
|
|
33087
33124
|
for (const envReq of reqs.envVars) {
|
|
33088
|
-
if (envReq.onlyIf && !envReq.onlyIf())
|
|
33089
|
-
continue;
|
|
33090
33125
|
if (!process.env[envReq.name]) {
|
|
33091
33126
|
const key = `env:${envReq.name}`;
|
|
33092
33127
|
if (issueMap.has(key)) {
|
|
@@ -33230,7 +33265,7 @@ function executeStep(stepName, options) {
|
|
|
33230
33265
|
args.push(...options.extraArgs);
|
|
33231
33266
|
}
|
|
33232
33267
|
const pkgRoot = getPackageRoot();
|
|
33233
|
-
const cliBin =
|
|
33268
|
+
const cliBin = join6(pkgRoot, "dist", "cli.js");
|
|
33234
33269
|
const command = `node ${cliBin} ${args.join(" ")}`;
|
|
33235
33270
|
try {
|
|
33236
33271
|
const stdout = execSync(command, {
|
|
@@ -33257,7 +33292,7 @@ ${stderr}`,
|
|
|
33257
33292
|
};
|
|
33258
33293
|
}
|
|
33259
33294
|
}
|
|
33260
|
-
var server = new McpServer({ name: "levante", version: "1.
|
|
33295
|
+
var server = new McpServer({ name: "levante", version: "1.6.0" }, { instructions: SERVER_INSTRUCTIONS });
|
|
33261
33296
|
server.registerTool("levante_scan_codebase", {
|
|
33262
33297
|
title: "Scan Codebase",
|
|
33263
33298
|
description: "Scan a project directory for test files, configs, fixtures, path aliases, and sample test content. Use this during project setup or to understand test infrastructure.",
|
|
@@ -33309,13 +33344,108 @@ server.registerTool("levante_read_agent", {
|
|
|
33309
33344
|
};
|
|
33310
33345
|
}
|
|
33311
33346
|
});
|
|
33347
|
+
server.registerTool("levante_read_pipeline_file", {
|
|
33348
|
+
title: "Read Pipeline File",
|
|
33349
|
+
description: "Read a pipeline artifact for a given issue key. Use this before an agent step to load the input. " + 'Types: "codegen" (raw Playwright codegen), "transcript" (voice transcript JSON), ' + '"scenario" (YAML test scenario), "test" (Playwright .test.ts), "qa" (QA markdown), "context" (project context.md).',
|
|
33350
|
+
inputSchema: exports_external.object({
|
|
33351
|
+
key: exports_external.string().optional().describe('Issue key (e.g. PROJ-101). Not required for "context" type.'),
|
|
33352
|
+
type: exports_external.enum(["codegen", "transcript", "scenario", "test", "qa", "context"]).describe("Which artifact to read")
|
|
33353
|
+
})
|
|
33354
|
+
}, async ({ key, type }) => {
|
|
33355
|
+
try {
|
|
33356
|
+
let filePath = null;
|
|
33357
|
+
if (type === "context") {
|
|
33358
|
+
filePath = join6(process.cwd(), CONFIG_DIR, "context.md");
|
|
33359
|
+
} else {
|
|
33360
|
+
if (!key) {
|
|
33361
|
+
return {
|
|
33362
|
+
content: [{ type: "text", text: "Error: key is required for this file type" }],
|
|
33363
|
+
isError: true
|
|
33364
|
+
};
|
|
33365
|
+
}
|
|
33366
|
+
const config2 = await loadConfig();
|
|
33367
|
+
const paths = resolvePaths(config2, key);
|
|
33368
|
+
if (type === "codegen") {
|
|
33369
|
+
filePath = paths.keyDir ? join6(paths.keyDir, `${key}.codegen.ts`) : null;
|
|
33370
|
+
} else if (type === "transcript") {
|
|
33371
|
+
filePath = paths.keyDir ? join6(paths.keyDir, `${key}.transcript.json`) : null;
|
|
33372
|
+
} else if (type === "scenario") {
|
|
33373
|
+
filePath = paths.scenarioFile;
|
|
33374
|
+
} else if (type === "test") {
|
|
33375
|
+
filePath = paths.testFile;
|
|
33376
|
+
} else if (type === "qa") {
|
|
33377
|
+
filePath = paths.qaFile;
|
|
33378
|
+
}
|
|
33379
|
+
}
|
|
33380
|
+
if (!filePath) {
|
|
33381
|
+
return {
|
|
33382
|
+
content: [{ type: "text", text: JSON.stringify({ found: false, path: null, reason: "Could not resolve path" }) }]
|
|
33383
|
+
};
|
|
33384
|
+
}
|
|
33385
|
+
if (!existsSync5(filePath)) {
|
|
33386
|
+
return {
|
|
33387
|
+
content: [{ type: "text", text: JSON.stringify({ found: false, path: filePath }) }]
|
|
33388
|
+
};
|
|
33389
|
+
}
|
|
33390
|
+
const fileContent = readFileSync5(filePath, "utf-8");
|
|
33391
|
+
return {
|
|
33392
|
+
content: [{ type: "text", text: JSON.stringify({ found: true, path: filePath, content: fileContent }) }]
|
|
33393
|
+
};
|
|
33394
|
+
} catch (err) {
|
|
33395
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33396
|
+
return {
|
|
33397
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
33398
|
+
isError: true
|
|
33399
|
+
};
|
|
33400
|
+
}
|
|
33401
|
+
});
|
|
33402
|
+
server.registerTool("levante_write_pipeline_file", {
|
|
33403
|
+
title: "Write Pipeline File",
|
|
33404
|
+
description: "Write a pipeline artifact after completing an agent step. " + 'Types: "transcript" (voice transcript JSON), "scenario" (YAML test scenario), ' + '"test" (Playwright .test.ts), "qa" (QA markdown).',
|
|
33405
|
+
inputSchema: exports_external.object({
|
|
33406
|
+
key: exports_external.string().describe("Issue key (e.g. PROJ-101)"),
|
|
33407
|
+
type: exports_external.enum(["transcript", "scenario", "test", "qa"]).describe("Which artifact to write"),
|
|
33408
|
+
content: exports_external.string().describe("The file content to write")
|
|
33409
|
+
})
|
|
33410
|
+
}, async ({ key, type, content: fileContent }) => {
|
|
33411
|
+
try {
|
|
33412
|
+
const config2 = await loadConfig();
|
|
33413
|
+
const paths = resolvePaths(config2, key);
|
|
33414
|
+
const fileMap = {
|
|
33415
|
+
transcript: paths.keyDir ? join6(paths.keyDir, `${key}.transcript.json`) : null,
|
|
33416
|
+
scenario: paths.scenarioFile,
|
|
33417
|
+
test: paths.testFile,
|
|
33418
|
+
qa: paths.qaFile
|
|
33419
|
+
};
|
|
33420
|
+
const filePath = fileMap[type];
|
|
33421
|
+
if (!filePath) {
|
|
33422
|
+
return {
|
|
33423
|
+
content: [{ type: "text", text: `Error: Could not resolve path for type "${type}" with key "${key}"` }],
|
|
33424
|
+
isError: true
|
|
33425
|
+
};
|
|
33426
|
+
}
|
|
33427
|
+
const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
|
|
33428
|
+
const { dirname: dirname4 } = await import("node:path");
|
|
33429
|
+
mkdirSync2(dirname4(filePath), { recursive: true });
|
|
33430
|
+
writeFileSync2(filePath, fileContent, "utf-8");
|
|
33431
|
+
return {
|
|
33432
|
+
content: [{ type: "text", text: JSON.stringify({ written: true, path: filePath }) }]
|
|
33433
|
+
};
|
|
33434
|
+
} catch (err) {
|
|
33435
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33436
|
+
return {
|
|
33437
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
33438
|
+
isError: true
|
|
33439
|
+
};
|
|
33440
|
+
}
|
|
33441
|
+
});
|
|
33312
33442
|
server.registerTool("levante_get_example", {
|
|
33313
33443
|
title: "Get Example Context",
|
|
33314
33444
|
description: `Returns the full example context markdown file that shows the expected format for ${CONFIG_DIR}/context.md.`,
|
|
33315
33445
|
inputSchema: exports_external.object({})
|
|
33316
33446
|
}, async () => {
|
|
33317
33447
|
try {
|
|
33318
|
-
const examplePath =
|
|
33448
|
+
const examplePath = join6(getPackageRoot(), "templates", "e2e-ai.context.example.md");
|
|
33319
33449
|
const content = readFileSync5(examplePath, "utf-8");
|
|
33320
33450
|
return {
|
|
33321
33451
|
content: [{ type: "text", text: content }]
|
|
@@ -33411,7 +33541,7 @@ server.registerTool("levante_get_workflow_guide", {
|
|
|
33411
33541
|
inputSchema: exports_external.object({})
|
|
33412
33542
|
}, async () => {
|
|
33413
33543
|
try {
|
|
33414
|
-
const guidePath =
|
|
33544
|
+
const guidePath = join6(getPackageRoot(), "templates", "workflow.md");
|
|
33415
33545
|
if (!existsSync5(guidePath)) {
|
|
33416
33546
|
return {
|
|
33417
33547
|
content: [{ type: "text", text: "Error: workflow.md not found in templates" }],
|
|
@@ -33442,15 +33572,15 @@ server.registerTool("levante_scan_ast", {
|
|
|
33442
33572
|
const cwd = process.cwd();
|
|
33443
33573
|
const dir = scanDir ?? "src";
|
|
33444
33574
|
const scanConfig = {
|
|
33445
|
-
scanDir:
|
|
33575
|
+
scanDir: join6(cwd, dir),
|
|
33446
33576
|
include: include ?? ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
|
33447
33577
|
exclude: exclude ?? ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/*.test.*", "**/*.spec.*", "**/__tests__/**"],
|
|
33448
|
-
cacheDir:
|
|
33578
|
+
cacheDir: join6(cwd, CONFIG_DIR, "cache")
|
|
33449
33579
|
};
|
|
33450
33580
|
const ast = await runStage1(scanConfig);
|
|
33451
|
-
const astPath =
|
|
33581
|
+
const astPath = join6(cwd, CONFIG_DIR, "ast-scan.json");
|
|
33452
33582
|
const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
|
|
33453
|
-
mkdirSync2(
|
|
33583
|
+
mkdirSync2(join6(cwd, CONFIG_DIR), { recursive: true });
|
|
33454
33584
|
writeFileSync2(astPath, JSON.stringify(ast, null, 2));
|
|
33455
33585
|
const summary2 = summarizeAST(ast, astPath);
|
|
33456
33586
|
return {
|
|
@@ -33473,7 +33603,7 @@ server.registerTool("levante_scan_ast_detail", {
|
|
|
33473
33603
|
})
|
|
33474
33604
|
}, async ({ category, filter, limit }) => {
|
|
33475
33605
|
try {
|
|
33476
|
-
const astPath =
|
|
33606
|
+
const astPath = join6(process.cwd(), CONFIG_DIR, "ast-scan.json");
|
|
33477
33607
|
if (!existsSync5(astPath)) {
|
|
33478
33608
|
return {
|
|
33479
33609
|
content: [{ type: "text", text: "Error: No AST scan found. Run levante_scan_ast first." }],
|
|
@@ -33509,7 +33639,7 @@ server.registerTool("levante_build_qa_map", {
|
|
|
33509
33639
|
payload.commitSha = sha;
|
|
33510
33640
|
} catch {}
|
|
33511
33641
|
}
|
|
33512
|
-
const outputPath = output ??
|
|
33642
|
+
const outputPath = output ?? join6(process.cwd(), CONFIG_DIR, "qa-map.json");
|
|
33513
33643
|
let written = false;
|
|
33514
33644
|
if (validation.valid && !dryRun) {
|
|
33515
33645
|
const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
|
|
@@ -33553,7 +33683,7 @@ server.registerTool("levante_read_qa_map", {
|
|
|
33553
33683
|
})
|
|
33554
33684
|
}, async ({ path }) => {
|
|
33555
33685
|
try {
|
|
33556
|
-
const mapPath = path ??
|
|
33686
|
+
const mapPath = path ?? join6(process.cwd(), CONFIG_DIR, "qa-map.json");
|
|
33557
33687
|
if (!existsSync5(mapPath)) {
|
|
33558
33688
|
return {
|
|
33559
33689
|
content: [{ type: "text", text: JSON.stringify(null) }]
|