agex 0.1.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.
@@ -0,0 +1,419 @@
1
+ import {
2
+ buildEnv,
3
+ getAgentConfig,
4
+ getRequiredApiKey,
5
+ runClaude,
6
+ runCodex,
7
+ runCursor
8
+ } from "./chunk-3FGK7LUI.js";
9
+
10
+ // ../run/dist/run.js
11
+ function buildDebugOutput(output) {
12
+ const lines = [
13
+ "agex-run",
14
+ `agent=${output.agent}`,
15
+ `model=${output.model ?? ""}`,
16
+ `status=${output.status}`,
17
+ `duration_ms=${output.durationMs}`,
18
+ `exit_code=${output.exitCode}`,
19
+ "---stdout---",
20
+ output.stdout.trimEnd(),
21
+ "---stderr---",
22
+ output.stderr.trimEnd()
23
+ ];
24
+ return lines.join("\n");
25
+ }
26
+ function normalizeOutput(output, formattedText) {
27
+ return {
28
+ json: output,
29
+ text: buildDebugOutput(output),
30
+ formattedText
31
+ };
32
+ }
33
+ async function runAgent(request) {
34
+ const config = getAgentConfig(request.agent);
35
+ const apiKeyEnv = getRequiredApiKey(request.agent);
36
+ const apiKey = request.env?.[apiKeyEnv] ?? process.env[apiKeyEnv];
37
+ if (!apiKey && request.requireApiKey) {
38
+ throw new Error(`Missing required API key: ${apiKeyEnv}`);
39
+ }
40
+ if (request.install) {
41
+ const { ensureAgentInstalled: ensureAgentInstalled2 } = await import("./agents-2OHWEGTE.js");
42
+ await ensureAgentInstalled2(request.agent, void 0, request.installCommand);
43
+ }
44
+ const args = config.args({
45
+ prompt: request.prompt,
46
+ model: request.model,
47
+ approveMcps: request.approveMcps,
48
+ browser: request.browser,
49
+ streamOutput: request.streamOutput ?? true,
50
+ approvalPolicy: request.approvalPolicy,
51
+ mcpConfigPath: request.mcpConfigPath,
52
+ workspacePath: request.workspacePath
53
+ });
54
+ if ((request.streamOutput ?? true) && !request.quietOutput) {
55
+ process.stdout.write("[agex-run] starting agent...\n");
56
+ }
57
+ const env = buildEnv({ apiKeyEnv, apiKey, request });
58
+ let result;
59
+ if (request.agent === "cursor") {
60
+ result = await runCursor(request, args, env);
61
+ } else if (request.agent === "claude") {
62
+ result = await runClaude(request, args, env);
63
+ } else {
64
+ result = await runCodex(request, args, env);
65
+ }
66
+ return normalizeOutput(result.output, result.formattedText);
67
+ }
68
+
69
+ // ../proov/dist/proov.js
70
+ import * as fs2 from "fs";
71
+ import * as os2 from "os";
72
+ import * as path2 from "path";
73
+
74
+ // ../browse/dist/index.js
75
+ import * as fs from "fs";
76
+ import * as os from "os";
77
+ import * as path from "path";
78
+ import { fileURLToPath } from "url";
79
+ function getCursorScriptPath() {
80
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
81
+ return path.resolve(__dirname, "..", "assets", "cursor.js");
82
+ }
83
+ function buildMcpArgs(config) {
84
+ const args = ["@playwright/mcp@latest"];
85
+ const outputDir = path.resolve(config.output.dir);
86
+ if (config.storageStatePath && fs.existsSync(config.storageStatePath)) {
87
+ args.push("--storage-state", path.resolve(config.storageStatePath));
88
+ }
89
+ if (config.initScriptPath && fs.existsSync(config.initScriptPath)) {
90
+ args.push("--init-script", path.resolve(config.initScriptPath));
91
+ }
92
+ const showCursor = config.output.showCursor ?? config.output.video ?? true;
93
+ if (showCursor) {
94
+ const cursorScript = getCursorScriptPath();
95
+ if (fs.existsSync(cursorScript)) {
96
+ args.push("--init-script", cursorScript);
97
+ }
98
+ }
99
+ if (config.output.video) {
100
+ const viewport = config.output.viewport ?? { width: 1920, height: 1080 };
101
+ args.push(`--save-video=${viewport.width}x${viewport.height}`);
102
+ }
103
+ args.push("--output-dir", outputDir);
104
+ if (config.output.viewport) {
105
+ args.push("--viewport-size", `${config.output.viewport.width}x${config.output.viewport.height}`);
106
+ }
107
+ return args;
108
+ }
109
+ function createMcpJsonFile(config, agent = "cursor") {
110
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agex-"));
111
+ let mcpJsonPath;
112
+ if (agent === "cursor") {
113
+ const cursorDir = path.join(tempDir, ".cursor");
114
+ fs.mkdirSync(cursorDir, { recursive: true });
115
+ mcpJsonPath = path.join(cursorDir, "mcp.json");
116
+ } else {
117
+ mcpJsonPath = path.join(tempDir, "mcp.json");
118
+ }
119
+ const mcpJson = {
120
+ mcpServers: {
121
+ playwright: {
122
+ command: "npx",
123
+ args: buildMcpArgs(config)
124
+ }
125
+ }
126
+ };
127
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2));
128
+ return {
129
+ path: mcpJsonPath,
130
+ workspacePath: tempDir,
131
+ cleanup: () => {
132
+ try {
133
+ fs.rmSync(tempDir, { recursive: true, force: true });
134
+ } catch {
135
+ return;
136
+ }
137
+ }
138
+ };
139
+ }
140
+
141
+ // ../proov/dist/prompt.js
142
+ function buildProovPrompt(assertion, options) {
143
+ const recordingInfo = options.video !== false ? "Video recording is enabled." : "Screenshots only.";
144
+ const screenshotInfo = options.screenshots !== false ? "Take screenshots at key moments using 3-digit zero-padded numbering (e.g., 001-login-button.png). Only capture when it adds new evidence." : "";
145
+ const parts = [
146
+ "You have the Playwright MCP for browser automation.",
147
+ recordingInfo,
148
+ "",
149
+ `YOUR TASK: ${assertion}`
150
+ ];
151
+ if (options.url) {
152
+ parts.push(`Navigate to: ${options.url}`);
153
+ }
154
+ const screenshotInstruction = screenshotInfo ? `2. ${screenshotInfo}` : "2. Document key observations";
155
+ parts.push(`
156
+ INSTRUCTIONS:
157
+ 1. Use the browser tools to navigate and interact
158
+ ${screenshotInstruction}
159
+ 3. Add brief waits between actions for video clarity
160
+ 4. Narrate what you observe
161
+
162
+ FINAL VERDICT (required):
163
+ End your response with exactly one of these lines (copy-paste exactly):
164
+ [PROOV_VERDICT:PASS] - <brief explanation>
165
+ [PROOV_VERDICT:FAIL] - <brief explanation>
166
+ [PROOV_VERDICT:SKIP] - <reason why it cannot be proven>`);
167
+ return parts.join("\n");
168
+ }
169
+
170
+ // ../proov/dist/verdict.js
171
+ var VERDICT_REGEX = /\[PROOV_VERDICT:(PASS|FAIL|SKIP)\]\s*-?\s*([^"\n\r}]*)/i;
172
+ function parseVerdict(output) {
173
+ const matches = [...output.matchAll(new RegExp(VERDICT_REGEX, "gi"))];
174
+ if (matches.length === 0) {
175
+ return { verdict: "fail", reason: "No verdict found in output" };
176
+ }
177
+ const lastMatch = matches[matches.length - 1];
178
+ return {
179
+ verdict: lastMatch[1].toLowerCase(),
180
+ reason: lastMatch[2]?.trim() || ""
181
+ };
182
+ }
183
+
184
+ // ../proov/dist/proov.js
185
+ var DEFAULT_OPTIONS = {
186
+ agent: "cursor",
187
+ outputDir: "./proov-results",
188
+ video: true,
189
+ screenshots: true,
190
+ viewport: { width: 1920, height: 1080 },
191
+ headless: true
192
+ };
193
+ function createOutputDir(baseDir, assertion) {
194
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
195
+ const slug = assertion.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
196
+ const dir = path2.resolve(baseDir, `${timestamp}_${slug}`);
197
+ fs2.mkdirSync(dir, { recursive: true });
198
+ return dir;
199
+ }
200
+ function copyPlaywrightArtifacts(outputDir) {
201
+ const tmpDir = os2.tmpdir();
202
+ const playwrightOutputBase = path2.join(tmpDir, "playwright-mcp-output");
203
+ if (!fs2.existsSync(playwrightOutputBase)) {
204
+ return;
205
+ }
206
+ const subdirs = fs2.readdirSync(playwrightOutputBase).map((name) => ({ name, path: path2.join(playwrightOutputBase, name) })).filter((item) => fs2.statSync(item.path).isDirectory()).sort((a, b) => {
207
+ const statA = fs2.statSync(a.path);
208
+ const statB = fs2.statSync(b.path);
209
+ return statB.mtime.getTime() - statA.mtime.getTime();
210
+ });
211
+ if (subdirs.length === 0) {
212
+ return;
213
+ }
214
+ const latestDir = subdirs[0].path;
215
+ copyFilesRecursively(latestDir, outputDir);
216
+ }
217
+ function copyFilesRecursively(srcDir, destDir) {
218
+ if (!fs2.existsSync(srcDir)) {
219
+ return;
220
+ }
221
+ const entries = fs2.readdirSync(srcDir, { withFileTypes: true });
222
+ for (const entry of entries) {
223
+ const srcPath = path2.join(srcDir, entry.name);
224
+ if (entry.isDirectory()) {
225
+ copyFilesRecursively(srcPath, destDir);
226
+ } else if (entry.isFile() && /\.(png|jpg|jpeg|webm|mp4)$/i.test(entry.name)) {
227
+ const destPath = path2.join(destDir, entry.name);
228
+ fs2.copyFileSync(srcPath, destPath);
229
+ }
230
+ }
231
+ }
232
+ async function proov(assertion, options = {}) {
233
+ const opts = { ...DEFAULT_OPTIONS, ...options };
234
+ const agent = opts.agent;
235
+ const outputDir = createOutputDir(opts.outputDir, assertion);
236
+ const browserConfig = {
237
+ output: {
238
+ dir: outputDir,
239
+ video: opts.video,
240
+ screenshots: opts.screenshots,
241
+ viewport: opts.viewport,
242
+ showCursor: true
243
+ }
244
+ };
245
+ const mcpFile = createMcpJsonFile(browserConfig, agent);
246
+ const prompt = buildProovPrompt(assertion, opts);
247
+ const startTime = Date.now();
248
+ try {
249
+ const result = await runAgent({
250
+ agent,
251
+ prompt,
252
+ model: opts.model,
253
+ approveMcps: true,
254
+ browser: true,
255
+ streamOutput: true,
256
+ approvalPolicy: "never",
257
+ mcpConfigPath: mcpFile.path,
258
+ workspacePath: mcpFile.workspacePath,
259
+ timeoutMs: opts.timeout
260
+ });
261
+ const durationMs = Date.now() - startTime;
262
+ const { verdict, reason } = parseVerdict(result.json.stdout);
263
+ copyPlaywrightArtifacts(outputDir);
264
+ return {
265
+ verdict,
266
+ reason,
267
+ agent,
268
+ durationMs,
269
+ outputDir,
270
+ exitCode: result.json.exitCode,
271
+ stdout: result.json.stdout,
272
+ stderr: result.json.stderr
273
+ };
274
+ } finally {
275
+ mcpFile.cleanup();
276
+ }
277
+ }
278
+
279
+ // ../review/dist/git.js
280
+ import { execa } from "execa";
281
+ async function buildGitContext(baseRef, includeWorktree, cwd) {
282
+ const exec = (command) => execa("bash", ["-lc", command], { cwd, reject: false }).then((result) => result.stdout.trim());
283
+ const currentBranch = await exec('git branch --show-current 2>/dev/null || echo "HEAD"');
284
+ const diffStat = await exec(`git diff ${baseRef}...HEAD --stat 2>/dev/null | head -100`);
285
+ const changedFiles = await exec(`git diff ${baseRef}...HEAD --name-only 2>/dev/null | head -60`);
286
+ const commitMessages = await exec(`git log ${baseRef}..HEAD --oneline 2>/dev/null | head -30`);
287
+ let worktreeUnstaged = "";
288
+ let worktreeStaged = "";
289
+ let worktreeFilesUnstaged = "";
290
+ let worktreeFilesStaged = "";
291
+ if (includeWorktree) {
292
+ worktreeUnstaged = await exec("git diff --stat 2>/dev/null | head -100");
293
+ worktreeStaged = await exec("git diff --cached --stat 2>/dev/null | head -100");
294
+ worktreeFilesUnstaged = await exec("git diff --name-only 2>/dev/null | head -60");
295
+ worktreeFilesStaged = await exec("git diff --cached --name-only 2>/dev/null | head -60");
296
+ }
297
+ return [
298
+ `BASE: ${baseRef}`,
299
+ `HEAD: ${currentBranch}`,
300
+ "",
301
+ "COMMITS (base..HEAD):",
302
+ commitMessages,
303
+ "",
304
+ "CHANGED FILES (base...HEAD):",
305
+ changedFiles,
306
+ "",
307
+ "DIFF STATS (base...HEAD):",
308
+ diffStat,
309
+ "",
310
+ "UNCOMMITTED (unstaged) DIFF STATS:",
311
+ worktreeUnstaged,
312
+ "",
313
+ "UNCOMMITTED (staged) DIFF STATS:",
314
+ worktreeStaged,
315
+ "",
316
+ "UNCOMMITTED FILES (unstaged):",
317
+ worktreeFilesUnstaged,
318
+ "",
319
+ "UNCOMMITTED FILES (staged):",
320
+ worktreeFilesStaged
321
+ ].join("\n");
322
+ }
323
+
324
+ // ../review/dist/prompt.js
325
+ function buildPlanPrompt(gitContext, hypothesesCount, userHint) {
326
+ const hint = userHint && userHint.trim().length > 0 ? userHint : "NONE";
327
+ return `You are generating a detailed test specification based on git changes.
328
+
329
+ RULES:
330
+ - Do NOT use any tools. Do NOT browse. Do NOT run commands.
331
+ - Output MUST be a complete, actionable proof spec for a human/agent to execute later.
332
+ - Include Hypotheses H1.. up to H${hypothesesCount}. Each hypothesis should be short and testable.
333
+ - Prefer UI/UX observable outcomes. Avoid implementation details unless necessary for test setup.
334
+
335
+ CONTEXT (git summary):
336
+ ${gitContext}
337
+
338
+ USER HINT (optional):
339
+ ${hint}
340
+
341
+ OUTPUT FORMAT (exact headings, in this order):
342
+ ---GOAL---
343
+ [1-3 sentences]
344
+ ---SCOPE---
345
+ [bullets]
346
+ ---NON_GOALS---
347
+ [bullets]
348
+ ---PRECONDITIONS---
349
+ [bullets]
350
+ ---HYPOTHESES---
351
+ H1: ...
352
+ ...
353
+ H${hypothesesCount}: ...
354
+ ---PROOF_PLAN---
355
+ 1. ...
356
+ 2. ...
357
+ [each step includes expected observation]
358
+ ---TEST_DATA---
359
+ [bullets or NONE]
360
+ ---RISKS_AND_FLAKINESS---
361
+ [bullets]
362
+ ---PASS_FAIL---
363
+ PASS: ...
364
+ FAIL: ...
365
+ ---END---`;
366
+ }
367
+
368
+ // ../review/dist/review.js
369
+ function buildTextOutput(output) {
370
+ const lines = [
371
+ "agex-review",
372
+ `agent=${output.agent}`,
373
+ `model=${output.model ?? ""}`,
374
+ `base_ref=${output.baseRef}`,
375
+ "---plan---",
376
+ output.plan.stdout.trimEnd(),
377
+ "---execution---",
378
+ output.execution.stdout.trimEnd()
379
+ ];
380
+ return lines.join("\n");
381
+ }
382
+ function normalizeReviewOutput(output) {
383
+ return {
384
+ json: output,
385
+ text: buildTextOutput(output)
386
+ };
387
+ }
388
+ async function runReview(request) {
389
+ const hypothesesCount = request.hypotheses ?? 5;
390
+ const includeWorktree = request.includeWorktree ?? true;
391
+ const gitContext = await buildGitContext(request.baseRef, includeWorktree, request.cwd);
392
+ const planPrompt = buildPlanPrompt(gitContext, hypothesesCount, request.promptHint);
393
+ const plan = await runAgent({
394
+ agent: request.agent,
395
+ model: request.model,
396
+ prompt: planPrompt,
397
+ cwd: request.cwd
398
+ });
399
+ const execution = await runAgent({
400
+ agent: request.agent,
401
+ model: request.model,
402
+ prompt: plan.json.stdout,
403
+ cwd: request.cwd
404
+ });
405
+ const outputJson = {
406
+ baseRef: request.baseRef,
407
+ agent: request.agent,
408
+ model: request.model,
409
+ plan: plan.json,
410
+ execution: execution.json
411
+ };
412
+ return normalizeReviewOutput(outputJson);
413
+ }
414
+
415
+ export {
416
+ runAgent,
417
+ proov,
418
+ runReview
419
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import 'agex-proov';
3
+ import 'agex-run';
4
+ import 'agex-review';