writethevision 7.0.6 → 7.0.8
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 +27 -2
- package/package.json +1 -1
- package/src/cli.js +134 -64
- package/src/engine.js +109 -0
package/README.md
CHANGED
|
@@ -296,9 +296,34 @@ The Masterbuilder reads your vision document and coordinates the artisans.
|
|
|
296
296
|
|
|
297
297
|
## Vision Runner (Ralphy-Style)
|
|
298
298
|
|
|
299
|
-
`wtv run`
|
|
299
|
+
`wtv run` (with no arguments) launches the **Vision Runner**, an autonomous loop that executes a project vision until completion.
|
|
300
300
|
|
|
301
|
-
It
|
|
301
|
+
It is designed for "Ralphy-style" execution: give it a vision, and it runs until the vision is a reality.
|
|
302
|
+
|
|
303
|
+
### The Workflow
|
|
304
|
+
|
|
305
|
+
1. **Pick a Vision**: WTV finds `VISION.md` (root) or files in `vision/*.md` and asks you to select one.
|
|
306
|
+
2. **Generate Plan**: It prompts the engine (OpenCode, Codex, or Claude) to generate a detailed `PRD.md` with a task checklist.
|
|
307
|
+
3. **Execute Loop**:
|
|
308
|
+
- Reads `PRD.md` to find the next unchecked task.
|
|
309
|
+
- Creates a focused prompt for the engine.
|
|
310
|
+
- Runs the engine to implement the task.
|
|
311
|
+
- Verifies the task was marked completed in `PRD.md`.
|
|
312
|
+
- Appends a checkpoint to `progress.txt`.
|
|
313
|
+
- Repeats until all tasks are done.
|
|
314
|
+
4. **Finalize**: Creates a single git commit ("feat: complete <vision>") and pushes to remote (unless disabled).
|
|
315
|
+
|
|
316
|
+
### File Contracts
|
|
317
|
+
|
|
318
|
+
The Vision Runner relies on strict file conventions:
|
|
319
|
+
|
|
320
|
+
- **Input**:
|
|
321
|
+
- `VISION.md` (or `vision/*.md`): The source of truth for *what* to build.
|
|
322
|
+
- **Artifacts**:
|
|
323
|
+
- `PRD.md`: The execution checklist. Must use `- [ ]` for incomplete and `- [x]` for complete tasks. The runner stops when no `- [ ]` remain.
|
|
324
|
+
- `progress.txt`: An append-only log of every completed task with timestamps.
|
|
325
|
+
|
|
326
|
+
### Usage
|
|
302
327
|
|
|
303
328
|
```bash
|
|
304
329
|
wtv run
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "writethevision",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.8",
|
|
4
4
|
"description": "Write The Vision (WTV): vision-driven development with the Habakkuk workflow. 10 agents + 21 skills for Claude Code, Codex CLI, and OpenCode.",
|
|
5
5
|
"author": "Christopher Hogg",
|
|
6
6
|
"license": "MIT",
|
package/src/cli.js
CHANGED
|
@@ -1256,15 +1256,23 @@ async function checkForUpdates() {
|
|
|
1256
1256
|
|
|
1257
1257
|
try {
|
|
1258
1258
|
const packageName = getPackageName();
|
|
1259
|
+
const checkEveryRun = process.env.WTV_UPDATE_CHECK_EVERY_RUN === '1';
|
|
1259
1260
|
const notifier = updateNotifier({
|
|
1260
1261
|
pkg: {
|
|
1261
1262
|
name: packageName,
|
|
1262
1263
|
version: getVersion(),
|
|
1263
1264
|
},
|
|
1264
|
-
updateCheckInterval: 1000 * 60 * 60 * 24 * 7,
|
|
1265
|
+
updateCheckInterval: checkEveryRun ? Number.POSITIVE_INFINITY : 1000 * 60 * 60 * 24 * 7,
|
|
1265
1266
|
shouldNotifyInNpmScript: false,
|
|
1266
1267
|
});
|
|
1267
1268
|
|
|
1269
|
+
if (checkEveryRun) {
|
|
1270
|
+
try {
|
|
1271
|
+
notifier.update = await notifier.fetchInfo();
|
|
1272
|
+
} catch {
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1268
1276
|
const update = notifier.update;
|
|
1269
1277
|
if (!update) return;
|
|
1270
1278
|
|
|
@@ -1290,7 +1298,7 @@ Or run latest once:
|
|
|
1290
1298
|
},
|
|
1291
1299
|
});
|
|
1292
1300
|
|
|
1293
|
-
const shouldPrompt = process.stdin.isTTY && process.env.WTV_NO_AUTO_UPDATE !== '1' && !process.env.CI;
|
|
1301
|
+
const shouldPrompt = process.stdin.isTTY && process.env.WTV_NO_AUTO_UPDATE !== '1' && !process.env.CI && !checkEveryRun;
|
|
1294
1302
|
if (!shouldPrompt) return;
|
|
1295
1303
|
|
|
1296
1304
|
const isDevCheckout = existsSync(join(PACKAGE_ROOT, '.git'));
|
|
@@ -3265,8 +3273,14 @@ async function runEngine(engine, promptText) {
|
|
|
3265
3273
|
}
|
|
3266
3274
|
|
|
3267
3275
|
if (normalized === 'claude') {
|
|
3276
|
+
// Claude Code CLI: explicitly force text output so user config
|
|
3277
|
+
// can't switch this to stream-json (which has extra requirements).
|
|
3268
3278
|
return await new Promise((resolve, reject) => {
|
|
3269
|
-
const child = spawn(
|
|
3279
|
+
const child = spawn(
|
|
3280
|
+
'claude',
|
|
3281
|
+
['--dangerously-skip-permissions', '--print', '--output-format', 'text', promptText],
|
|
3282
|
+
{ stdio: 'inherit' },
|
|
3283
|
+
);
|
|
3270
3284
|
child.on('error', reject);
|
|
3271
3285
|
child.on('exit', (code) => resolve(code ?? 1));
|
|
3272
3286
|
});
|
|
@@ -3275,13 +3289,12 @@ async function runEngine(engine, promptText) {
|
|
|
3275
3289
|
throw new Error(`Unsupported engine: ${engine}`);
|
|
3276
3290
|
}
|
|
3277
3291
|
|
|
3278
|
-
function ensureProgressHeader(progressPath, { visionPath, engine, startSha }) {
|
|
3292
|
+
export function ensureProgressHeader(progressPath, { visionPath, engine, startSha }) {
|
|
3279
3293
|
if (!existsSync(progressPath)) {
|
|
3280
3294
|
writeFileSync(progressPath, '');
|
|
3281
3295
|
}
|
|
3282
3296
|
|
|
3283
3297
|
const existing = readFileSync(progressPath, 'utf8');
|
|
3284
|
-
if (existing.trim().length > 0) return;
|
|
3285
3298
|
|
|
3286
3299
|
const startedAt = new Date().toISOString();
|
|
3287
3300
|
const header = [
|
|
@@ -3290,16 +3303,19 @@ function ensureProgressHeader(progressPath, { visionPath, engine, startSha }) {
|
|
|
3290
3303
|
`Vision: ${visionPath}`,
|
|
3291
3304
|
`Engine: ${engine}`,
|
|
3292
3305
|
startSha ? `Start SHA: ${startSha}` : `Start SHA: (not a git repo)`,
|
|
3306
|
+
startSha ? `Rollback hint: git reset --hard ${startSha}` : '',
|
|
3293
3307
|
'',
|
|
3294
3308
|
].join('\n');
|
|
3295
3309
|
|
|
3296
|
-
|
|
3310
|
+
if (existing.trim().length > 0) {
|
|
3311
|
+
writeFileSync(progressPath, existing.trimEnd() + '\n\n' + header);
|
|
3312
|
+
} else {
|
|
3313
|
+
writeFileSync(progressPath, header);
|
|
3314
|
+
}
|
|
3297
3315
|
}
|
|
3298
3316
|
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
const promptText = `You are generating a PRD for a software project.
|
|
3317
|
+
export function generatePrdPrompt(visionContent) {
|
|
3318
|
+
return `You are generating a PRD for a software project.
|
|
3303
3319
|
|
|
3304
3320
|
Input vision document:
|
|
3305
3321
|
---
|
|
@@ -3316,6 +3332,12 @@ Requirements:
|
|
|
3316
3332
|
- Do NOT implement any code yet.
|
|
3317
3333
|
- Do NOT run git commit or git push.
|
|
3318
3334
|
- Output is the updated files on disk (PRD.md).`;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
async function generatePrdFromVision({ engine, visionPath, prdPath }) {
|
|
3338
|
+
const visionContent = readFileSync(visionPath, 'utf8');
|
|
3339
|
+
|
|
3340
|
+
const promptText = generatePrdPrompt(visionContent);
|
|
3319
3341
|
|
|
3320
3342
|
const code = await runEngine(engine, promptText);
|
|
3321
3343
|
if (code !== 0) {
|
|
@@ -3332,7 +3354,7 @@ Requirements:
|
|
|
3332
3354
|
}
|
|
3333
3355
|
}
|
|
3334
3356
|
|
|
3335
|
-
async function runVisionTaskIteration({ engine, prdPath, progressPath, taskText, noTests, noLint }) {
|
|
3357
|
+
export async function runVisionTaskIteration({ engine, prdPath, progressPath, taskText, noTests, noLint, _runEngine }) {
|
|
3336
3358
|
const promptText = `You are an autonomous coding agent executing a PRD.
|
|
3337
3359
|
|
|
3338
3360
|
Files:
|
|
@@ -3360,7 +3382,8 @@ Stop after completing this one task.`;
|
|
|
3360
3382
|
|
|
3361
3383
|
const progressBefore = existsSync(progressPath) ? readFileSync(progressPath, 'utf8') : '';
|
|
3362
3384
|
|
|
3363
|
-
const
|
|
3385
|
+
const runFn = _runEngine || runEngine;
|
|
3386
|
+
const code = await runFn(engine, promptText);
|
|
3364
3387
|
if (code !== 0) {
|
|
3365
3388
|
throw new Error(`Engine exited with code ${code}`);
|
|
3366
3389
|
}
|
|
@@ -3382,6 +3405,71 @@ Stop after completing this one task.`;
|
|
|
3382
3405
|
}
|
|
3383
3406
|
}
|
|
3384
3407
|
|
|
3408
|
+
export function finalizeVisionRun({
|
|
3409
|
+
prdPath,
|
|
3410
|
+
visionPath,
|
|
3411
|
+
hasGit,
|
|
3412
|
+
startSha,
|
|
3413
|
+
noPush,
|
|
3414
|
+
_runGit,
|
|
3415
|
+
_readFileSync,
|
|
3416
|
+
_countUncheckedPrdTasks,
|
|
3417
|
+
_getCurrentBranch
|
|
3418
|
+
}) {
|
|
3419
|
+
const runGitFn = _runGit || runGit;
|
|
3420
|
+
const readFileSyncFn = _readFileSync || readFileSync;
|
|
3421
|
+
const countUncheckedFn = _countUncheckedPrdTasks || countUncheckedPrdTasks;
|
|
3422
|
+
const getCurrentBranchFn = _getCurrentBranch || getCurrentBranch;
|
|
3423
|
+
|
|
3424
|
+
if (!hasGit) {
|
|
3425
|
+
console.log(`\n ${c.green}${sym.check}${c.reset} PRD complete (no git repo detected).\n`);
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
const prdFinal = readFileSyncFn(prdPath, 'utf8');
|
|
3430
|
+
if (countUncheckedFn(prdFinal) > 0) {
|
|
3431
|
+
console.log(`\n ${c.yellow}${sym.warn}${c.reset} PRD still has remaining tasks; skipping commit/push.\n`);
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
const visionName = basename(visionPath).replace(/\.md$/, '');
|
|
3436
|
+
const commitMessage = `feat: run vision (${visionName})`;
|
|
3437
|
+
|
|
3438
|
+
const addRes = runGitFn(['add', '-A']);
|
|
3439
|
+
if (addRes.status !== 0) {
|
|
3440
|
+
console.log(`\n ${c.red}${sym.cross}${c.reset} git add failed.\n`);
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
const statusRes = runGitFn(['status', '--porcelain']);
|
|
3445
|
+
if (statusRes.status === 0 && String(statusRes.stdout || '').trim().length === 0) {
|
|
3446
|
+
console.log(`\n ${c.green}${sym.check}${c.reset} Nothing to commit.\n`);
|
|
3447
|
+
return;
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
const commitRes = runGitFn(['commit', '-m', commitMessage]);
|
|
3451
|
+
if (commitRes.status !== 0) {
|
|
3452
|
+
console.log(`\n ${c.red}${sym.cross}${c.reset} git commit failed.\n`);
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
if (!noPush) {
|
|
3457
|
+
const pushRes = runGitFn(['push']);
|
|
3458
|
+
if (pushRes.status !== 0) {
|
|
3459
|
+
const branch = getCurrentBranchFn();
|
|
3460
|
+
if (branch) {
|
|
3461
|
+
runGitFn(['push', '-u', 'origin', branch]);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
console.log(`\n ${c.green}${sym.check}${c.reset} Vision complete.`);
|
|
3467
|
+
if (startSha) {
|
|
3468
|
+
console.log(` Started from: ${c.dim}${startSha}${c.reset}`);
|
|
3469
|
+
}
|
|
3470
|
+
console.log('');
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3385
3473
|
async function visionRunner(opts) {
|
|
3386
3474
|
const config = loadConfig();
|
|
3387
3475
|
const interactive = process.stdout.isTTY && process.stdin.isTTY;
|
|
@@ -3389,10 +3477,9 @@ async function visionRunner(opts) {
|
|
|
3389
3477
|
const prdPath = join(process.cwd(), 'PRD.md');
|
|
3390
3478
|
const progressPath = join(process.cwd(), 'progress.txt');
|
|
3391
3479
|
|
|
3392
|
-
|
|
3393
|
-
let engine = defaultEngine;
|
|
3480
|
+
let engine = normalizeVisionRunnerEngine(opts.engine || null);
|
|
3394
3481
|
|
|
3395
|
-
if (
|
|
3482
|
+
if (engine && !isSupportedVisionRunnerEngine(engine)) {
|
|
3396
3483
|
engine = null;
|
|
3397
3484
|
}
|
|
3398
3485
|
|
|
@@ -3453,6 +3540,12 @@ async function visionRunner(opts) {
|
|
|
3453
3540
|
const hasGit = isGitRepo();
|
|
3454
3541
|
const startSha = hasGit ? getGitHeadSha() : null;
|
|
3455
3542
|
|
|
3543
|
+
if (!hasGit) {
|
|
3544
|
+
console.log(`\n ${c.yellow}${sym.warn} Warning:${c.reset} Not a git repository. Version control features disabled.\n`);
|
|
3545
|
+
} else if (startSha) {
|
|
3546
|
+
console.log(`\n ${c.blue}${sym.info} Rollback hint:${c.reset} git reset --hard ${startSha}\n`);
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3456
3549
|
if (hasGit && !isWorkingTreeClean() && !opts.dryRun) {
|
|
3457
3550
|
if (!interactive) {
|
|
3458
3551
|
console.log(`\n ${c.red}Error:${c.reset} Working tree is not clean (non-interactive mode).\n`);
|
|
@@ -3549,53 +3642,13 @@ async function visionRunner(opts) {
|
|
|
3549
3642
|
}
|
|
3550
3643
|
}
|
|
3551
3644
|
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
console.log(`\n ${c.yellow}${sym.warn}${c.reset} PRD still has remaining tasks; skipping commit/push.\n`);
|
|
3560
|
-
return;
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
|
-
const visionName = basename(visionPath).replace(/\.md$/, '');
|
|
3564
|
-
const commitMessage = `feat: run vision (${visionName})`;
|
|
3565
|
-
|
|
3566
|
-
const addRes = runGit(['add', '-A']);
|
|
3567
|
-
if (addRes.status !== 0) {
|
|
3568
|
-
console.log(`\n ${c.red}${sym.cross}${c.reset} git add failed.\n`);
|
|
3569
|
-
return;
|
|
3570
|
-
}
|
|
3571
|
-
|
|
3572
|
-
const statusRes = runGit(['status', '--porcelain']);
|
|
3573
|
-
if (statusRes.status === 0 && String(statusRes.stdout || '').trim().length === 0) {
|
|
3574
|
-
console.log(`\n ${c.green}${sym.check}${c.reset} Nothing to commit.\n`);
|
|
3575
|
-
return;
|
|
3576
|
-
}
|
|
3577
|
-
|
|
3578
|
-
const commitRes = runGit(['commit', '-m', commitMessage]);
|
|
3579
|
-
if (commitRes.status !== 0) {
|
|
3580
|
-
console.log(`\n ${c.red}${sym.cross}${c.reset} git commit failed.\n`);
|
|
3581
|
-
return;
|
|
3582
|
-
}
|
|
3583
|
-
|
|
3584
|
-
if (!opts.noPush) {
|
|
3585
|
-
const pushRes = runGit(['push']);
|
|
3586
|
-
if (pushRes.status !== 0) {
|
|
3587
|
-
const branch = getCurrentBranch();
|
|
3588
|
-
if (branch) {
|
|
3589
|
-
runGit(['push', '-u', 'origin', branch]);
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
}
|
|
3593
|
-
|
|
3594
|
-
console.log(`\n ${c.green}${sym.check}${c.reset} Vision complete.`);
|
|
3595
|
-
if (startSha) {
|
|
3596
|
-
console.log(` Started from: ${c.dim}${startSha}${c.reset}`);
|
|
3597
|
-
}
|
|
3598
|
-
console.log('');
|
|
3645
|
+
finalizeVisionRun({
|
|
3646
|
+
prdPath,
|
|
3647
|
+
visionPath,
|
|
3648
|
+
hasGit,
|
|
3649
|
+
startSha,
|
|
3650
|
+
noPush: opts.noPush
|
|
3651
|
+
});
|
|
3599
3652
|
}
|
|
3600
3653
|
|
|
3601
3654
|
// ============================================================================
|
|
@@ -4565,7 +4618,7 @@ function showHelp() {
|
|
|
4565
4618
|
console.log(`${pad} ${c.cyan}vision${c.reset} Show VISION.md status`);
|
|
4566
4619
|
console.log(`${pad} ${c.cyan}log${c.reset} Show task logs`);
|
|
4567
4620
|
console.log(`${pad} ${c.cyan}meet${c.reset} Meet Paul and the Artisans`);
|
|
4568
|
-
console.log(`${pad} ${c.cyan}run${c.reset} Run the vision (
|
|
4621
|
+
console.log(`${pad} ${c.cyan}run${c.reset} Run the vision (Vision Runner)`);
|
|
4569
4622
|
console.log(`${pad} ${c.cyan}help${c.reset} Show this help\n`);
|
|
4570
4623
|
|
|
4571
4624
|
console.log(`${pad}${c.bold}Agent Commands${c.reset}`);
|
|
@@ -4576,12 +4629,17 @@ function showHelp() {
|
|
|
4576
4629
|
console.log(`${pad} ${c.cyan}agents fav${c.reset} <name> Toggle favorite`);
|
|
4577
4630
|
console.log(`${pad} ${c.cyan}agents rm${c.reset} <name> Remove agent\n`);
|
|
4578
4631
|
|
|
4632
|
+
console.log(`${pad}${c.bold}Vision Runner${c.reset} ${c.dim}"That he may run that readeth it"${c.reset}`);
|
|
4633
|
+
console.log(`${pad} ${c.cyan}run${c.reset} Launch Vision Runner (interactive)`);
|
|
4634
|
+
console.log(`${pad} ${c.cyan}run${c.reset} --vision <f> Execute specific vision file`);
|
|
4635
|
+
console.log(`${pad} ${c.cyan}run${c.reset} --resume Resume existing PRD.md\n`);
|
|
4636
|
+
|
|
4579
4637
|
console.log(`${pad}${c.bold}Habakkuk Workflow${c.reset} ${c.dim}"Write the vision, make it plain"${c.reset}`);
|
|
4580
4638
|
console.log(`${pad} ${c.cyan}board${c.reset} [--all] Show kanban board`);
|
|
4581
4639
|
console.log(`${pad} ${c.cyan}cry${c.reset} "desc" Enter a problem or need`);
|
|
4582
4640
|
console.log(`${pad} ${c.cyan}wait${c.reset} <id> Move to waiting (seeking)`);
|
|
4583
4641
|
console.log(`${pad} ${c.cyan}vision${c.reset} <id> Move to vision (answer received)`);
|
|
4584
|
-
console.log(`${pad} ${c.cyan}run${c.reset}
|
|
4642
|
+
console.log(`${pad} ${c.cyan}run${c.reset} <id> Move to run (execution started)`);
|
|
4585
4643
|
console.log(`${pad} ${c.cyan}worship${c.reset} <id> Move to worship (retrospective)`);
|
|
4586
4644
|
console.log(`${pad} ${c.cyan}note${c.reset} <id> "text" Add note to item`);
|
|
4587
4645
|
console.log(`${pad} ${c.cyan}item${c.reset} <id> Show item details`);
|
|
@@ -4599,6 +4657,18 @@ function showHelp() {
|
|
|
4599
4657
|
console.log(`${pad} ${c.dim}--gemini${c.reset} Target Gemini CLI`);
|
|
4600
4658
|
console.log(`${pad} ${c.dim}--antigravity${c.reset} Target Antigravity\n`);
|
|
4601
4659
|
|
|
4660
|
+
console.log(`${pad}${c.bold}Vision Runner Options${c.reset}`);
|
|
4661
|
+
console.log(`${pad} ${c.dim}--vision <file>${c.reset} Input vision file (default: discover)`);
|
|
4662
|
+
console.log(`${pad} ${c.dim}--engine <name>${c.reset} Execution engine (opencode|codex|claude)`);
|
|
4663
|
+
console.log(`${pad} ${c.dim}--resume${c.reset} Resume existing PRD.md`);
|
|
4664
|
+
console.log(`${pad} ${c.dim}--regenerate-prd${c.reset} Force regeneration of PRD.md from vision`);
|
|
4665
|
+
console.log(`${pad} ${c.dim}--max-iterations <n>${c.reset} Stop after N tasks`);
|
|
4666
|
+
console.log(`${pad} ${c.dim}--dry-run${c.reset} Show what would happen without running`);
|
|
4667
|
+
console.log(`${pad} ${c.dim}--fast${c.reset} Skip tests and linting`);
|
|
4668
|
+
console.log(`${pad} ${c.dim}--no-tests${c.reset} Skip tests`);
|
|
4669
|
+
console.log(`${pad} ${c.dim}--no-lint${c.reset} Skip linting`);
|
|
4670
|
+
console.log(`${pad} ${c.dim}--no-push${c.reset} Skip git push at end\n`);
|
|
4671
|
+
|
|
4602
4672
|
console.log(`${pad}${c.bold}Examples${c.reset}`);
|
|
4603
4673
|
console.log(`${pad} ${c.green}wtv${c.reset} ${c.dim}# Dashboard${c.reset}`);
|
|
4604
4674
|
console.log(`${pad} ${c.green}wtv agents${c.reset} ${c.dim}# List agents${c.reset}`);
|
package/src/engine.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for AI CLI engine adapters.
|
|
5
|
+
*/
|
|
6
|
+
class EngineAdapter {
|
|
7
|
+
constructor(cwd = process.cwd()) {
|
|
8
|
+
this.cwd = cwd;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get the command and arguments for the engine.
|
|
13
|
+
* @param {string} prompt - The prompt to execute.
|
|
14
|
+
* @returns {{ command: string, args: string[] }}
|
|
15
|
+
*/
|
|
16
|
+
getCommand(prompt) {
|
|
17
|
+
throw new Error('getCommand must be implemented by subclass');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run the engine with the given prompt.
|
|
22
|
+
* @param {string} prompt - The prompt to execute.
|
|
23
|
+
* @param {object} options - Options for spawn.
|
|
24
|
+
* @returns {Promise<number>} - Exit code.
|
|
25
|
+
*/
|
|
26
|
+
async run(prompt, options = {}) {
|
|
27
|
+
const { command, args } = this.getCommand(prompt);
|
|
28
|
+
|
|
29
|
+
console.log(`[Engine] Running: ${command} ${args.join(' ')}`);
|
|
30
|
+
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const child = spawn(command, args, {
|
|
33
|
+
cwd: this.cwd,
|
|
34
|
+
stdio: 'inherit', // Stream output to parent stdout/stderr
|
|
35
|
+
shell: true, // Use shell to resolve command paths
|
|
36
|
+
...options
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
child.on('error', (err) => {
|
|
40
|
+
reject(err);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
child.on('close', (code) => {
|
|
44
|
+
resolve(code);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adapter for OpenCode CLI.
|
|
52
|
+
*/
|
|
53
|
+
class OpenCodeEngine extends EngineAdapter {
|
|
54
|
+
getCommand(prompt) {
|
|
55
|
+
// Assumption: opencode takes the prompt as a single argument string
|
|
56
|
+
return {
|
|
57
|
+
command: 'opencode',
|
|
58
|
+
args: [prompt]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Adapter for Codex CLI.
|
|
65
|
+
*/
|
|
66
|
+
class CodexEngine extends EngineAdapter {
|
|
67
|
+
getCommand(prompt) {
|
|
68
|
+
// Assumption: codex takes the prompt as a single argument string
|
|
69
|
+
return {
|
|
70
|
+
command: 'codex',
|
|
71
|
+
args: [prompt]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Adapter for Claude Code CLI.
|
|
78
|
+
*/
|
|
79
|
+
class ClaudeEngine extends EngineAdapter {
|
|
80
|
+
getCommand(prompt) {
|
|
81
|
+
// Assumption: claude takes the prompt as a single argument string
|
|
82
|
+
// Note: Check if it needs -p or similar flag
|
|
83
|
+
return {
|
|
84
|
+
command: 'claude',
|
|
85
|
+
args: [prompt]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Factory to create the appropriate engine adapter.
|
|
92
|
+
* @param {string} name - The name of the engine ('opencode', 'codex', 'claude').
|
|
93
|
+
* @param {string} cwd - Current working directory.
|
|
94
|
+
* @returns {EngineAdapter}
|
|
95
|
+
*/
|
|
96
|
+
export function createEngine(name, cwd) {
|
|
97
|
+
switch (name.toLowerCase()) {
|
|
98
|
+
case 'opencode':
|
|
99
|
+
return new OpenCodeEngine(cwd);
|
|
100
|
+
case 'codex':
|
|
101
|
+
return new CodexEngine(cwd);
|
|
102
|
+
case 'claude':
|
|
103
|
+
return new ClaudeEngine(cwd);
|
|
104
|
+
default:
|
|
105
|
+
throw new Error(`Unsupported engine: ${name}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { EngineAdapter, OpenCodeEngine, CodexEngine, ClaudeEngine };
|