@0xdevabir/enhance 0.1.0 → 0.1.2
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 +20 -1
- package/dist/index.js +775 -123
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,12 +23,31 @@ Instead of sending `build login page` directly to Claude/Codex/OpenCode:
|
|
|
23
23
|
- Node.js 20+
|
|
24
24
|
- At least one AI coding tool: [Claude Code](https://claude.ai/code), [OpenCode](https://opencode.ai), or [Codex CLI](https://github.com/openai/codex)
|
|
25
25
|
|
|
26
|
-
###
|
|
26
|
+
### Option A — No install (npx)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @0xdevabir/enhance@latest "build a login page"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Runs directly without installing anything globally. Great for trying it out or if you hit permission errors.
|
|
33
|
+
|
|
34
|
+
### Option B — Global install
|
|
27
35
|
|
|
28
36
|
```bash
|
|
29
37
|
npm install -g @0xdevabir/enhance
|
|
30
38
|
```
|
|
31
39
|
|
|
40
|
+
> **Permission error (EACCES)?** Fix npm's global prefix first, then retry:
|
|
41
|
+
> ```bash
|
|
42
|
+
> mkdir -p ~/.npm-global
|
|
43
|
+
> npm config set prefix '~/.npm-global'
|
|
44
|
+
> echo 'export PATH=$HOME/.npm-global/bin:$PATH' >> ~/.zshrc
|
|
45
|
+
> source ~/.zshrc
|
|
46
|
+
> npm install -g @0xdevabir/enhance
|
|
47
|
+
> ```
|
|
48
|
+
|
|
49
|
+
### Step 2 — Run setup
|
|
50
|
+
|
|
32
51
|
### Step 2 — Run setup
|
|
33
52
|
|
|
34
53
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
import { createInterface } from "readline";
|
|
5
6
|
import ora from "ora";
|
|
7
|
+
import chalk3 from "chalk";
|
|
6
8
|
|
|
7
9
|
// src/cli/logger.ts
|
|
8
10
|
import chalk from "chalk";
|
|
@@ -28,7 +30,7 @@ import { cosmiconfig } from "cosmiconfig";
|
|
|
28
30
|
var DEFAULT_CONFIG = {
|
|
29
31
|
provider: "claude",
|
|
30
32
|
model: "claude-sonnet-4-6",
|
|
31
|
-
maxContextTokens:
|
|
33
|
+
maxContextTokens: 8e3
|
|
32
34
|
};
|
|
33
35
|
async function loadConfig(root) {
|
|
34
36
|
const explorer = cosmiconfig("enhance", {
|
|
@@ -243,69 +245,178 @@ async function setCached(root, result) {
|
|
|
243
245
|
|
|
244
246
|
// src/analyzer/intent.ts
|
|
245
247
|
var ACTION_KEYWORDS = {
|
|
246
|
-
create: ["build", "create", "make", "add", "implement", "write", "generate", "scaffold", "set up", "setup", "new"],
|
|
247
248
|
fix: ["fix", "debug", "resolve", "repair", "broken", "failing", "crash", "error", "bug", "issue", "problem"],
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
create: ["create", "build", "make", "generate", "scaffold", "new", "write"],
|
|
250
|
+
add: ["add", "implement", "integrate", "include", "append", "enable"],
|
|
251
|
+
refactor: ["refactor", "restructure", "reorganize", "clean up", "cleanup", "simplify", "rewrite", "move", "extract"],
|
|
252
|
+
explain: ["explain", "describe", "how does", "what is", "why does", "understand", "document", "walkthrough"],
|
|
253
|
+
delete: ["delete", "remove", "drop", "destroy", "uninstall", "clean"],
|
|
252
254
|
unknown: []
|
|
253
255
|
};
|
|
254
256
|
var ENTITY_KEYWORDS = {
|
|
255
257
|
page: ["page", "route", "view", "screen", "layout"],
|
|
256
|
-
component: ["component", "widget", "
|
|
258
|
+
component: ["component", "widget", "card", "modal", "dialog", "dropdown", "button", "input", "form"],
|
|
257
259
|
api: ["api", "endpoint", "route handler", "server action", "action", "mutation", "query", "rest", "graphql"],
|
|
258
|
-
hook: ["hook", "
|
|
259
|
-
util: ["util", "
|
|
260
|
-
config: ["config", "configuration", "
|
|
261
|
-
style: ["style", "css", "theme", "design", "
|
|
260
|
+
hook: ["hook", "use", "custom hook"],
|
|
261
|
+
util: ["util", "helper", "function", "service", "lib", "library", "module"],
|
|
262
|
+
config: ["config", "configuration", "settings", "env", "environment"],
|
|
263
|
+
style: ["style", "css", "theme", "color", "design", "ui", "ux", "animation"],
|
|
262
264
|
unknown: []
|
|
263
265
|
};
|
|
264
266
|
var FEATURE_KEYWORDS = {
|
|
265
|
-
auth: ["auth", "
|
|
266
|
-
dashboard: ["dashboard", "admin", "
|
|
267
|
+
auth: ["auth", "login", "logout", "signup", "register", "session", "jwt", "token", "oauth", "credential", "password", "permission", "role"],
|
|
268
|
+
dashboard: ["dashboard", "admin", "overview", "analytics", "metrics", "stats"],
|
|
267
269
|
payment: ["payment", "checkout", "billing", "stripe", "invoice", "subscription", "pricing"],
|
|
268
|
-
user: ["user", "profile", "account", "avatar", "
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
chart: ["chart", "graph", "visualization", "plot", "diagram"]
|
|
270
|
+
user: ["user", "profile", "account", "avatar", "member"],
|
|
271
|
+
upload: ["upload", "file", "image", "storage", "media", "s3", "cdn"],
|
|
272
|
+
notification: ["notification", "alert", "email", "toast", "push", "sms"],
|
|
273
|
+
navigation: ["nav", "navbar", "sidebar", "header", "menu", "breadcrumb"],
|
|
274
|
+
table: ["table", "grid", "list", "pagination", "sort", "filter"],
|
|
275
|
+
form: ["form", "field", "input", "validation", "submit"],
|
|
276
|
+
search: ["search", "filter", "query", "autocomplete"]
|
|
276
277
|
};
|
|
278
|
+
var SYSTEM_SCOPE_SIGNALS = [
|
|
279
|
+
"entire",
|
|
280
|
+
"whole",
|
|
281
|
+
"all",
|
|
282
|
+
"system",
|
|
283
|
+
"everywhere",
|
|
284
|
+
"global",
|
|
285
|
+
"across",
|
|
286
|
+
"throughout",
|
|
287
|
+
"full",
|
|
288
|
+
"complete",
|
|
289
|
+
"migrate",
|
|
290
|
+
"overhaul"
|
|
291
|
+
];
|
|
292
|
+
var SIMPLE_SCOPE_SIGNALS = [
|
|
293
|
+
"this file",
|
|
294
|
+
"this component",
|
|
295
|
+
"this function",
|
|
296
|
+
"line",
|
|
297
|
+
"typo",
|
|
298
|
+
"typos",
|
|
299
|
+
"small",
|
|
300
|
+
"quick",
|
|
301
|
+
"minor",
|
|
302
|
+
"just change",
|
|
303
|
+
"rename"
|
|
304
|
+
];
|
|
277
305
|
function analyzeIntent(rawPrompt) {
|
|
278
306
|
const lower = rawPrompt.toLowerCase();
|
|
307
|
+
const words = lower.split(/\s+/);
|
|
279
308
|
let action = "unknown";
|
|
309
|
+
let actionMatchCount = 0;
|
|
280
310
|
for (const [act, keywords] of Object.entries(ACTION_KEYWORDS)) {
|
|
281
311
|
if (act === "unknown") continue;
|
|
282
|
-
|
|
312
|
+
const matches = keywords.filter((k) => lower.includes(k)).length;
|
|
313
|
+
if (matches > actionMatchCount) {
|
|
283
314
|
action = act;
|
|
284
|
-
|
|
315
|
+
actionMatchCount = matches;
|
|
285
316
|
}
|
|
286
317
|
}
|
|
287
318
|
let entity = "unknown";
|
|
319
|
+
let entityMatchCount = 0;
|
|
288
320
|
for (const [ent, keywords] of Object.entries(ENTITY_KEYWORDS)) {
|
|
289
321
|
if (ent === "unknown") continue;
|
|
290
|
-
|
|
322
|
+
const matches = keywords.filter((k) => lower.includes(k)).length;
|
|
323
|
+
if (matches > entityMatchCount) {
|
|
291
324
|
entity = ent;
|
|
292
|
-
|
|
325
|
+
entityMatchCount = matches;
|
|
293
326
|
}
|
|
294
327
|
}
|
|
295
328
|
let feature = "general";
|
|
329
|
+
let featureMatchCount = 0;
|
|
296
330
|
for (const [feat, keywords] of Object.entries(FEATURE_KEYWORDS)) {
|
|
297
|
-
|
|
331
|
+
const matches = keywords.filter((k) => lower.includes(k)).length;
|
|
332
|
+
if (matches > featureMatchCount) {
|
|
298
333
|
feature = feat;
|
|
299
|
-
|
|
334
|
+
featureMatchCount = matches;
|
|
300
335
|
}
|
|
301
336
|
}
|
|
302
|
-
|
|
337
|
+
let scope = "feature";
|
|
338
|
+
if (SYSTEM_SCOPE_SIGNALS.some((s) => lower.includes(s))) {
|
|
339
|
+
scope = "system";
|
|
340
|
+
} else if (SIMPLE_SCOPE_SIGNALS.some((s) => lower.includes(s))) {
|
|
341
|
+
scope = "file";
|
|
342
|
+
} else if (action === "fix" && words.length <= 8) {
|
|
343
|
+
scope = "file";
|
|
344
|
+
}
|
|
345
|
+
let complexity = "feature";
|
|
346
|
+
if (scope === "system" || action === "refactor" && scope !== "file") {
|
|
347
|
+
complexity = "system";
|
|
348
|
+
} else if (scope === "file" || action === "fix" || action === "explain") {
|
|
349
|
+
complexity = "simple";
|
|
350
|
+
}
|
|
351
|
+
const totalMatches = actionMatchCount + entityMatchCount + featureMatchCount;
|
|
352
|
+
const confidence = Math.min(1, totalMatches / Math.max(words.length * 0.3, 1));
|
|
353
|
+
return { action, entity, feature, scope, complexity, confidence, rawPrompt };
|
|
303
354
|
}
|
|
304
355
|
|
|
305
356
|
// src/analyzer/context.ts
|
|
306
357
|
import { readFile as readFile4 } from "fs/promises";
|
|
307
|
-
import { join as join6 } from "path";
|
|
358
|
+
import { join as join6, dirname } from "path";
|
|
359
|
+
import { exec } from "child_process";
|
|
360
|
+
import { promisify } from "util";
|
|
308
361
|
import fg from "fast-glob";
|
|
362
|
+
var execAsync = promisify(exec);
|
|
363
|
+
var PROJECT_INSTRUCTION_FILES = [
|
|
364
|
+
".claude/CLAUDE.md",
|
|
365
|
+
"CLAUDE.md",
|
|
366
|
+
"AGENTS.md",
|
|
367
|
+
".cursorrules"
|
|
368
|
+
];
|
|
369
|
+
async function findProjectInstructions(root) {
|
|
370
|
+
const parts = [];
|
|
371
|
+
const searchRoots = [root, dirname(root)];
|
|
372
|
+
for (const searchRoot of searchRoots) {
|
|
373
|
+
for (const fileName of PROJECT_INSTRUCTION_FILES) {
|
|
374
|
+
const content = await tryRead(join6(searchRoot, fileName));
|
|
375
|
+
if (content) {
|
|
376
|
+
const relPath = fileName;
|
|
377
|
+
parts.push(`<!-- Source: ${relPath} -->
|
|
378
|
+
${truncateLines(content, 300)}`);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return parts.join("\n\n");
|
|
384
|
+
}
|
|
385
|
+
async function findGitContext(root, charBudget) {
|
|
386
|
+
const parts = [];
|
|
387
|
+
let used = 0;
|
|
388
|
+
try {
|
|
389
|
+
const { stdout: diffOutput } = await execAsync("git diff HEAD", { cwd: root, timeout: 5e3 });
|
|
390
|
+
if (diffOutput.trim()) {
|
|
391
|
+
const truncated = diffOutput.slice(0, Math.min(3e3, charBudget * 0.4));
|
|
392
|
+
const section = `// GIT: uncommitted changes
|
|
393
|
+
${truncated}${diffOutput.length > truncated.length ? "\n// ... (diff truncated)" : ""}`;
|
|
394
|
+
parts.push(section);
|
|
395
|
+
used += section.length;
|
|
396
|
+
}
|
|
397
|
+
if (used < charBudget * 0.6) {
|
|
398
|
+
const { stdout: logOutput } = await execAsync(
|
|
399
|
+
"git diff HEAD~3..HEAD --name-only 2>/dev/null || git diff HEAD~1..HEAD --name-only",
|
|
400
|
+
{ cwd: root, timeout: 5e3 }
|
|
401
|
+
);
|
|
402
|
+
const changedFiles = logOutput.trim().split("\n").filter(Boolean).slice(0, 8);
|
|
403
|
+
for (const relPath of changedFiles) {
|
|
404
|
+
if (used >= charBudget * 0.7) break;
|
|
405
|
+
const content = await tryRead(join6(root, relPath));
|
|
406
|
+
if (!content) continue;
|
|
407
|
+
const snippet = truncateLines(content, 100);
|
|
408
|
+
const section = `// FILE (recently changed): ${relPath}
|
|
409
|
+
${snippet}`;
|
|
410
|
+
if (used + section.length < charBudget * 0.7) {
|
|
411
|
+
parts.push(section);
|
|
412
|
+
used += section.length;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
return { content: parts.join("\n\n"), charsUsed: used };
|
|
419
|
+
}
|
|
309
420
|
var FEATURE_PATTERNS = {
|
|
310
421
|
auth: ["*auth*", "*login*", "*logout*", "*session*", "*jwt*", "*token*", "*credential*", "*middleware*", "*guard*"],
|
|
311
422
|
dashboard: ["*dashboard*", "*admin*", "*overview*", "*analytics*"],
|
|
@@ -326,25 +437,57 @@ var ALWAYS_INCLUDE_PATTERNS = [
|
|
|
326
437
|
"pages/_app.ts",
|
|
327
438
|
"src/pages/_app.tsx"
|
|
328
439
|
];
|
|
329
|
-
var
|
|
330
|
-
var
|
|
331
|
-
|
|
440
|
+
var DEFAULT_TOKEN_BUDGET = 8e3;
|
|
441
|
+
var CHARS_PER_TOKEN = 4;
|
|
442
|
+
function estimateTokens(text) {
|
|
443
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
444
|
+
}
|
|
445
|
+
function smartTruncate(content, maxTokens) {
|
|
446
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN;
|
|
447
|
+
if (content.length <= maxChars) return content;
|
|
448
|
+
const lines = content.split("\n");
|
|
449
|
+
const result = [];
|
|
450
|
+
let chars = 0;
|
|
451
|
+
for (const line of lines) {
|
|
452
|
+
if (chars + line.length + 1 > maxChars) {
|
|
453
|
+
result.push(`// ... (truncated, ${lines.length - result.length} lines omitted)`);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
result.push(line);
|
|
457
|
+
chars += line.length + 1;
|
|
458
|
+
}
|
|
459
|
+
return result.join("\n");
|
|
460
|
+
}
|
|
461
|
+
async function findRelevantFiles(root, intent, structure, maxTokens = DEFAULT_TOKEN_BUDGET, alwaysInclude = []) {
|
|
332
462
|
const sections = [];
|
|
333
|
-
let
|
|
463
|
+
let tokenBudget = maxTokens;
|
|
464
|
+
const { content: gitContent, charsUsed: gitUsed } = await findGitContext(root, Math.floor(tokenBudget * 0.35) * CHARS_PER_TOKEN);
|
|
465
|
+
tokenBudget -= Math.ceil(gitUsed / CHARS_PER_TOKEN);
|
|
466
|
+
for (const relPath of alwaysInclude) {
|
|
467
|
+
if (tokenBudget <= 100) break;
|
|
468
|
+
const content = await tryRead(join6(root, relPath));
|
|
469
|
+
if (!content) continue;
|
|
470
|
+
const allowedTokens = Math.min(estimateTokens(content), Math.floor(tokenBudget * 0.2));
|
|
471
|
+
const snippet = smartTruncate(content, allowedTokens);
|
|
472
|
+
const section = `// FILE: ${relPath} (pinned)
|
|
473
|
+
${snippet}`;
|
|
474
|
+
sections.push(section);
|
|
475
|
+
tokenBudget -= estimateTokens(section);
|
|
476
|
+
}
|
|
334
477
|
for (const pattern of ALWAYS_INCLUDE_PATTERNS) {
|
|
478
|
+
if (tokenBudget <= 100) break;
|
|
335
479
|
const content = await tryRead(join6(root, pattern));
|
|
336
|
-
if (content)
|
|
337
|
-
|
|
338
|
-
|
|
480
|
+
if (!content) continue;
|
|
481
|
+
const fileTokens = estimateTokens(content);
|
|
482
|
+
const allowedTokens = Math.min(fileTokens, Math.floor(tokenBudget * 0.25));
|
|
483
|
+
const snippet = smartTruncate(content, allowedTokens);
|
|
484
|
+
const section = `// FILE: ${pattern}
|
|
339
485
|
${snippet}`;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
charBudget -= section.length;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
486
|
+
sections.push(section);
|
|
487
|
+
tokenBudget -= estimateTokens(section);
|
|
345
488
|
}
|
|
346
489
|
const featurePatterns = FEATURE_PATTERNS[intent.feature] ?? [];
|
|
347
|
-
if (featurePatterns.length > 0) {
|
|
490
|
+
if (featurePatterns.length > 0 && tokenBudget > 200) {
|
|
348
491
|
const searchBase = structure.hasSrcDir ? join6(root, "src") : root;
|
|
349
492
|
const globs = featurePatterns.flatMap((p) => [
|
|
350
493
|
`**/${p}.{ts,tsx,js,jsx}`,
|
|
@@ -360,21 +503,38 @@ ${snippet}`;
|
|
|
360
503
|
});
|
|
361
504
|
} catch {
|
|
362
505
|
}
|
|
363
|
-
|
|
364
|
-
|
|
506
|
+
const foundFeatureFiles = [];
|
|
507
|
+
for (const file of files.slice(0, 6)) {
|
|
508
|
+
if (tokenBudget <= 100) break;
|
|
365
509
|
const content = await tryRead(file);
|
|
366
510
|
if (!content) continue;
|
|
367
511
|
const relPath = file.replace(root + "/", "");
|
|
368
|
-
const
|
|
512
|
+
const allowedTokens = Math.min(estimateTokens(content), Math.floor(tokenBudget * 0.4));
|
|
513
|
+
const snippet = smartTruncate(content, allowedTokens);
|
|
369
514
|
const section = `// FILE: ${relPath}
|
|
370
515
|
${snippet}`;
|
|
371
|
-
|
|
516
|
+
sections.push(section);
|
|
517
|
+
tokenBudget -= estimateTokens(section);
|
|
518
|
+
foundFeatureFiles.push(file);
|
|
519
|
+
}
|
|
520
|
+
if (foundFeatureFiles.length > 0 && tokenBudget > 200) {
|
|
521
|
+
const importedFiles = await crawlImports(foundFeatureFiles, root);
|
|
522
|
+
const alreadyIncluded = new Set(foundFeatureFiles.map((f) => f));
|
|
523
|
+
for (const file of importedFiles.filter((f) => !alreadyIncluded.has(f)).slice(0, 4)) {
|
|
524
|
+
if (tokenBudget <= 150) break;
|
|
525
|
+
const content = await tryRead(file);
|
|
526
|
+
if (!content) continue;
|
|
527
|
+
const relPath = file.replace(root + "/", "");
|
|
528
|
+
const allowedTokens = Math.min(estimateTokens(content), Math.floor(tokenBudget * 0.2));
|
|
529
|
+
const snippet = smartTruncate(content, allowedTokens);
|
|
530
|
+
const section = `// FILE: ${relPath} (imported)
|
|
531
|
+
${snippet}`;
|
|
372
532
|
sections.push(section);
|
|
373
|
-
|
|
533
|
+
tokenBudget -= estimateTokens(section);
|
|
374
534
|
}
|
|
375
535
|
}
|
|
376
536
|
}
|
|
377
|
-
if (["component", "page"].includes(intent.entity) &&
|
|
537
|
+
if (["component", "page"].includes(intent.entity) && tokenBudget > 300) {
|
|
378
538
|
const componentDir = structure.hasSrcDir ? join6(root, "src", "components") : join6(root, "components");
|
|
379
539
|
let sampleFiles = [];
|
|
380
540
|
try {
|
|
@@ -388,20 +548,44 @@ ${snippet}`;
|
|
|
388
548
|
} catch {
|
|
389
549
|
}
|
|
390
550
|
for (const file of sampleFiles.slice(0, 2)) {
|
|
391
|
-
if (
|
|
551
|
+
if (tokenBudget <= 150) break;
|
|
392
552
|
const content = await tryRead(file);
|
|
393
553
|
if (!content) continue;
|
|
394
554
|
const relPath = file.replace(root + "/", "");
|
|
395
|
-
const snippet =
|
|
555
|
+
const snippet = smartTruncate(content, Math.min(200, tokenBudget - 50));
|
|
396
556
|
const section = `// FILE: ${relPath} (pattern reference)
|
|
397
557
|
${snippet}`;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
558
|
+
sections.push(section);
|
|
559
|
+
tokenBudget -= estimateTokens(section);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return { contextFiles: sections.join("\n\n"), gitContext: gitContent };
|
|
563
|
+
}
|
|
564
|
+
async function crawlImports(filePaths, root) {
|
|
565
|
+
const IMPORT_RE = /(?:import|from)\s+['"](\.[^'"]+)['"]/g;
|
|
566
|
+
const TS_EXTS = [".ts", ".tsx", ".js", ".jsx"];
|
|
567
|
+
const found = /* @__PURE__ */ new Set();
|
|
568
|
+
for (const filePath of filePaths) {
|
|
569
|
+
const content = await tryRead(filePath);
|
|
570
|
+
if (!content) continue;
|
|
571
|
+
const dir = filePath.replace(/\/[^/]+$/, "");
|
|
572
|
+
let match;
|
|
573
|
+
IMPORT_RE.lastIndex = 0;
|
|
574
|
+
while ((match = IMPORT_RE.exec(content)) !== null) {
|
|
575
|
+
const importPath = match[1];
|
|
576
|
+
if (!importPath) continue;
|
|
577
|
+
const base = join6(dir, importPath);
|
|
578
|
+
for (const ext of TS_EXTS) {
|
|
579
|
+
const candidate = base.endsWith(ext) ? base : `${base}${ext}`;
|
|
580
|
+
if (candidate.includes("node_modules") || candidate.includes(".next")) continue;
|
|
581
|
+
if (candidate.startsWith(root)) {
|
|
582
|
+
found.add(candidate);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
401
585
|
}
|
|
402
586
|
}
|
|
403
587
|
}
|
|
404
|
-
return
|
|
588
|
+
return [...found];
|
|
405
589
|
}
|
|
406
590
|
async function tryRead(path) {
|
|
407
591
|
try {
|
|
@@ -418,7 +602,7 @@ function truncateLines(content, maxLines) {
|
|
|
418
602
|
}
|
|
419
603
|
|
|
420
604
|
// src/enhancer/injectors.ts
|
|
421
|
-
function
|
|
605
|
+
function getFallbackInjections(stack, intent, config) {
|
|
422
606
|
const rules = [];
|
|
423
607
|
if (stack.language === "typescript") {
|
|
424
608
|
rules.push("Use TypeScript throughout \u2014 no `any`, prefer `unknown` with type guards");
|
|
@@ -487,75 +671,222 @@ function getInjections(stack, intent) {
|
|
|
487
671
|
rules.push("Match the existing code style and file naming conventions");
|
|
488
672
|
rules.push("List every file you create or modify at the end of your response");
|
|
489
673
|
rules.push("Prefer reusing existing utilities over creating new ones");
|
|
674
|
+
if (config?.customRules?.length) {
|
|
675
|
+
rules.unshift(...config.customRules);
|
|
676
|
+
}
|
|
677
|
+
if (config?.featureRules?.[intent.feature]?.length) {
|
|
678
|
+
rules.unshift(...config.featureRules[intent.feature]);
|
|
679
|
+
}
|
|
490
680
|
return rules;
|
|
491
681
|
}
|
|
492
682
|
|
|
683
|
+
// src/enhancer/angles.ts
|
|
684
|
+
var ANGLE_LABELS = {
|
|
685
|
+
completeness: "Completeness",
|
|
686
|
+
production: "Production Safety",
|
|
687
|
+
dx: "Developer Experience",
|
|
688
|
+
alternative: "Alternative Approach"
|
|
689
|
+
};
|
|
690
|
+
var ANGLE_ORDER = ["completeness", "production", "dx", "alternative"];
|
|
691
|
+
function getAngle(iteration) {
|
|
692
|
+
const idx = Math.min(iteration, ANGLE_ORDER.length - 1);
|
|
693
|
+
return ANGLE_ORDER[idx];
|
|
694
|
+
}
|
|
695
|
+
function getAngleFocus(angle) {
|
|
696
|
+
switch (angle) {
|
|
697
|
+
case "completeness":
|
|
698
|
+
return 'Cover every feature, sub-case, and edge case. Assume the reviewer will ask "what about X?" for every possible X. Nothing should be missing.';
|
|
699
|
+
case "production":
|
|
700
|
+
return "Focus on what breaks at 3am: failure modes, error handling, observability, security vulnerabilities, race conditions, data consistency, and retry semantics.";
|
|
701
|
+
case "dx":
|
|
702
|
+
return "Focus on implementation guidance: the correct step-by-step order, what confuses junior developers, integration pitfalls, what to verify first, and common mistakes.";
|
|
703
|
+
case "alternative":
|
|
704
|
+
return "Propose a meaningfully different architecture or pattern than the obvious approach. Explain the trade-off. Challenge the default solution.";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
493
708
|
// src/enhancer/template.ts
|
|
494
709
|
function buildEnhancedPrompt(opts) {
|
|
495
|
-
const {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
710
|
+
const { intent } = opts;
|
|
711
|
+
switch (intent.complexity) {
|
|
712
|
+
case "simple":
|
|
713
|
+
return buildSimplePrompt(opts);
|
|
714
|
+
case "system":
|
|
715
|
+
return buildSystemPrompt(opts);
|
|
716
|
+
default:
|
|
717
|
+
return buildFeaturePrompt(opts);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function buildVersionHeader(opts) {
|
|
721
|
+
const { angle, iteration } = opts;
|
|
722
|
+
if (angle === void 0) return null;
|
|
723
|
+
const v = (iteration ?? 0) + 1;
|
|
724
|
+
const label = ANGLE_LABELS[angle];
|
|
725
|
+
return `\u{1F4CD} v${v} \u2014 ${label} angle`;
|
|
726
|
+
}
|
|
727
|
+
function buildAssumptionsSection(assumptions) {
|
|
728
|
+
if (!assumptions.length) return null;
|
|
729
|
+
return `## Assumptions
|
|
508
730
|
|
|
509
|
-
|
|
510
|
-
|
|
731
|
+
${assumptions.map((a) => `- ${a}`).join("\n")}`;
|
|
732
|
+
}
|
|
733
|
+
function buildGotchasSection(gotchas) {
|
|
734
|
+
if (!gotchas.length) return null;
|
|
735
|
+
return `## Watch out for
|
|
511
736
|
|
|
512
|
-
|
|
513
|
-
|
|
737
|
+
${gotchas.map((g) => `- ${g}`).join("\n")}`;
|
|
738
|
+
}
|
|
739
|
+
function buildSimplePrompt(opts) {
|
|
740
|
+
const { stack, intent, contextFiles, gitContext, injections, projectInstructions, assumptions = [], gotchas = [] } = opts;
|
|
741
|
+
const sections = [];
|
|
742
|
+
const versionHeader = buildVersionHeader(opts);
|
|
743
|
+
if (versionHeader) sections.push(versionHeader);
|
|
744
|
+
if (projectInstructions) {
|
|
745
|
+
sections.push(`## Project Instructions
|
|
514
746
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
747
|
+
${projectInstructions}`);
|
|
748
|
+
}
|
|
749
|
+
sections.push(`## Context
|
|
750
|
+
**Stack**: ${formatStack(stack)}
|
|
751
|
+
${formatStackDetails(stack)}`);
|
|
752
|
+
sections.push(`## Task
|
|
518
753
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
754
|
+
${buildTaskDescription(intent)}`);
|
|
755
|
+
const rules = [...injections.slice(0, 3), ...injections.slice(-3)];
|
|
756
|
+
sections.push(`## Requirements
|
|
757
|
+
|
|
758
|
+
${rules.map((r) => `- ${r}`).join("\n")}`);
|
|
759
|
+
const assumptionsSection = buildAssumptionsSection(assumptions);
|
|
760
|
+
if (assumptionsSection) sections.push(assumptionsSection);
|
|
761
|
+
const gotchasSection = buildGotchasSection(gotchas);
|
|
762
|
+
if (gotchasSection) sections.push(gotchasSection);
|
|
763
|
+
if (gitContext) {
|
|
764
|
+
sections.push(`## Recent Changes
|
|
765
|
+
|
|
766
|
+
${gitContext}`);
|
|
532
767
|
}
|
|
533
|
-
if (
|
|
534
|
-
|
|
768
|
+
if (contextFiles) {
|
|
769
|
+
sections.push(`## Code
|
|
770
|
+
|
|
771
|
+
${contextFiles}`);
|
|
535
772
|
}
|
|
536
|
-
|
|
537
|
-
|
|
773
|
+
sections.push(`## Expected Output
|
|
774
|
+
|
|
775
|
+
${buildOutputFormat(intent)}`);
|
|
776
|
+
return sections.join("\n\n---\n\n");
|
|
777
|
+
}
|
|
778
|
+
function buildFeaturePrompt(opts) {
|
|
779
|
+
const { stack, structure, intent, contextFiles, gitContext, injections, projectInstructions, assumptions = [], gotchas = [] } = opts;
|
|
780
|
+
const sections = [];
|
|
781
|
+
const versionHeader = buildVersionHeader(opts);
|
|
782
|
+
if (versionHeader) sections.push(versionHeader);
|
|
783
|
+
if (projectInstructions) {
|
|
784
|
+
sections.push(`## Project Instructions
|
|
785
|
+
|
|
786
|
+
${projectInstructions}`);
|
|
538
787
|
}
|
|
539
|
-
|
|
540
|
-
|
|
788
|
+
sections.push(`## Project Context
|
|
789
|
+
|
|
790
|
+
**Stack**: ${formatStack(stack)}
|
|
791
|
+
${formatStackDetails(stack)}
|
|
792
|
+
|
|
793
|
+
**Structure**: ${formatStructure(structure)}`);
|
|
794
|
+
sections.push(`## Task
|
|
795
|
+
|
|
796
|
+
${buildTaskDescription(intent)}`);
|
|
797
|
+
const specific = injections.slice(0, -3);
|
|
798
|
+
const universal = injections.slice(-3);
|
|
799
|
+
const requirementLines = [
|
|
800
|
+
...specific.map((r) => `- ${r}`),
|
|
801
|
+
...specific.length > 0 ? ["", "**Always:**"] : ["**Always:**"],
|
|
802
|
+
...universal.map((r) => `- ${r}`)
|
|
803
|
+
].join("\n");
|
|
804
|
+
sections.push(`## Requirements
|
|
805
|
+
|
|
806
|
+
${requirementLines}`);
|
|
807
|
+
const assumptionsSection = buildAssumptionsSection(assumptions);
|
|
808
|
+
if (assumptionsSection) sections.push(assumptionsSection);
|
|
809
|
+
const gotchasSection = buildGotchasSection(gotchas);
|
|
810
|
+
if (gotchasSection) sections.push(gotchasSection);
|
|
811
|
+
if (gitContext) {
|
|
812
|
+
sections.push(`## Recent Changes
|
|
813
|
+
|
|
814
|
+
${gitContext}`);
|
|
541
815
|
}
|
|
542
|
-
if (
|
|
543
|
-
|
|
816
|
+
if (contextFiles) {
|
|
817
|
+
sections.push(`## Relevant Code
|
|
818
|
+
|
|
819
|
+
${contextFiles}`);
|
|
544
820
|
}
|
|
545
|
-
|
|
546
|
-
|
|
821
|
+
sections.push(`## Expected Output
|
|
822
|
+
|
|
823
|
+
${buildOutputFormat(intent)}`);
|
|
824
|
+
return sections.join("\n\n---\n\n");
|
|
547
825
|
}
|
|
548
|
-
function
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
826
|
+
function buildSystemPrompt(opts) {
|
|
827
|
+
const { stack, structure, intent, contextFiles, gitContext, injections, projectInstructions, assumptions = [], gotchas = [] } = opts;
|
|
828
|
+
const sections = [];
|
|
829
|
+
const versionHeader = buildVersionHeader(opts);
|
|
830
|
+
if (versionHeader) sections.push(versionHeader);
|
|
831
|
+
if (projectInstructions) {
|
|
832
|
+
sections.push(`## Project Instructions
|
|
833
|
+
|
|
834
|
+
${projectInstructions}`);
|
|
552
835
|
}
|
|
553
|
-
|
|
554
|
-
|
|
836
|
+
sections.push(`## Project Context
|
|
837
|
+
|
|
838
|
+
**Stack**: ${formatStack(stack)}
|
|
839
|
+
${formatStackDetails(stack)}
|
|
840
|
+
|
|
841
|
+
**Structure**: ${formatStructure(structure)}`);
|
|
842
|
+
sections.push(`## Task
|
|
843
|
+
|
|
844
|
+
${buildTaskDescription(intent)}
|
|
845
|
+
|
|
846
|
+
> This is a system-level change. Before writing any code:
|
|
847
|
+
> 1. Analyze the current state and identify all affected areas
|
|
848
|
+
> 2. Define your implementation plan step by step
|
|
849
|
+
> 3. Highlight any breaking changes or migration steps
|
|
850
|
+
> 4. Then implement each step`);
|
|
851
|
+
const requirementLines = injections.map((r) => `- ${r}`).join("\n");
|
|
852
|
+
sections.push(`## Requirements
|
|
853
|
+
|
|
854
|
+
${requirementLines}`);
|
|
855
|
+
const assumptionsSection = buildAssumptionsSection(assumptions);
|
|
856
|
+
if (assumptionsSection) sections.push(assumptionsSection);
|
|
857
|
+
const gotchasSection = buildGotchasSection(gotchas);
|
|
858
|
+
if (gotchasSection) sections.push(gotchasSection);
|
|
859
|
+
if (gitContext) {
|
|
860
|
+
sections.push(`## Recent Changes
|
|
861
|
+
|
|
862
|
+
${gitContext}`);
|
|
863
|
+
}
|
|
864
|
+
if (contextFiles) {
|
|
865
|
+
sections.push(`## Relevant Code
|
|
866
|
+
|
|
867
|
+
${contextFiles}`);
|
|
868
|
+
}
|
|
869
|
+
sections.push(`## Expected Output
|
|
870
|
+
|
|
871
|
+
Start with a numbered implementation plan. Then implement each step. Mark breaking changes with \u26A0\uFE0F. List every file created or modified at the end.`);
|
|
872
|
+
return sections.join("\n\n---\n\n");
|
|
873
|
+
}
|
|
874
|
+
function buildOutputFormat(intent) {
|
|
875
|
+
switch (intent.action) {
|
|
876
|
+
case "fix":
|
|
877
|
+
return "Show exactly what changed. Explain in one sentence why it was broken. Provide the corrected code. List every file modified at the end.";
|
|
878
|
+
case "create":
|
|
879
|
+
case "add":
|
|
880
|
+
return "First list every file you will create or modify and why. Then implement each file completely. End with a summary list of all created/modified files.";
|
|
881
|
+
case "refactor":
|
|
882
|
+
return "For each change, show before and after. Explain the improvement in one line. Do not change behavior \u2014 only structure. List every file modified at the end.";
|
|
883
|
+
case "explain":
|
|
884
|
+
return "Walk through step by step. Reference specific function names, line numbers, or variable names from the existing code. Use concrete examples.";
|
|
885
|
+
case "delete":
|
|
886
|
+
return "List exactly what you will remove and why. Show the cleaned-up result. Confirm nothing else depends on the removed code.";
|
|
887
|
+
default:
|
|
888
|
+
return "Be thorough and precise. Show all changes. List every file created or modified at the end of your response.";
|
|
555
889
|
}
|
|
556
|
-
if (structure.hasAppDir) lines.push("Uses Next.js App Router (`app/` directory)");
|
|
557
|
-
if (structure.hasPagesDir) lines.push("Uses Next.js Pages Router (`pages/` directory)");
|
|
558
|
-
return lines.join("\n") || "Standard project structure";
|
|
559
890
|
}
|
|
560
891
|
function buildTaskDescription(intent) {
|
|
561
892
|
const actionMap = {
|
|
@@ -568,7 +899,36 @@ function buildTaskDescription(intent) {
|
|
|
568
899
|
unknown: "Handle"
|
|
569
900
|
};
|
|
570
901
|
const action = actionMap[intent.action] ?? "Handle";
|
|
571
|
-
return
|
|
902
|
+
return `**${action}**: ${intent.rawPrompt}`;
|
|
903
|
+
}
|
|
904
|
+
function formatStack(stack) {
|
|
905
|
+
const primary = stack.frameworks[0] ?? "Node.js";
|
|
906
|
+
const lang = stack.language === "typescript" ? "TypeScript" : "JavaScript";
|
|
907
|
+
return `${capitalize(primary)} / ${lang}`;
|
|
908
|
+
}
|
|
909
|
+
function formatStackDetails(stack) {
|
|
910
|
+
const lines = [];
|
|
911
|
+
if (stack.frameworks.length > 0) {
|
|
912
|
+
lines.push(`- **Frameworks**: ${stack.frameworks.map(capitalize).join(", ")}`);
|
|
913
|
+
}
|
|
914
|
+
if (stack.uiLibrary) lines.push(`- **UI**: ${capitalize(stack.uiLibrary)}`);
|
|
915
|
+
if (stack.orm) lines.push(`- **ORM**: ${capitalize(stack.orm)}`);
|
|
916
|
+
if (stack.testing) lines.push(`- **Testing**: ${capitalize(stack.testing)}`);
|
|
917
|
+
if (stack.nextRouterType) {
|
|
918
|
+
lines.push(`- **Router**: ${stack.nextRouterType === "app" ? "App Router" : "Pages Router"}`);
|
|
919
|
+
}
|
|
920
|
+
lines.push(`- **Package Manager**: ${stack.packageManager}`);
|
|
921
|
+
return lines.join("\n");
|
|
922
|
+
}
|
|
923
|
+
function formatStructure(structure) {
|
|
924
|
+
const parts = [];
|
|
925
|
+
if (structure.dirs.length > 0) parts.push(`dirs: \`${structure.dirs.join(", ")}\``);
|
|
926
|
+
if (structure.hasSrcDir && structure.srcDirs.length > 0) {
|
|
927
|
+
parts.push(`src/: \`${structure.srcDirs.join(", ")}\``);
|
|
928
|
+
}
|
|
929
|
+
if (structure.hasAppDir) parts.push("App Router (`app/`)");
|
|
930
|
+
if (structure.hasPagesDir) parts.push("Pages Router (`pages/`)");
|
|
931
|
+
return parts.join(" | ") || "Standard layout";
|
|
572
932
|
}
|
|
573
933
|
function capitalize(s) {
|
|
574
934
|
if (!s) return s;
|
|
@@ -588,22 +948,169 @@ function capitalize(s) {
|
|
|
588
948
|
return map[s] ?? s.charAt(0).toUpperCase() + s.slice(1);
|
|
589
949
|
}
|
|
590
950
|
|
|
951
|
+
// src/enhancer/domain-requirements.ts
|
|
952
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
953
|
+
async function generateDomainEnhancements(rawPrompt, intent, stack, angle, apiKey) {
|
|
954
|
+
const client = new Anthropic({ apiKey });
|
|
955
|
+
const stackSummary = [
|
|
956
|
+
stack.language === "typescript" ? "TypeScript" : "JavaScript",
|
|
957
|
+
...stack.frameworks,
|
|
958
|
+
stack.orm ? `ORM:${stack.orm}` : null,
|
|
959
|
+
stack.uiLibrary ? `UI:${stack.uiLibrary}` : null
|
|
960
|
+
].filter(Boolean).join(", ");
|
|
961
|
+
const system = `You are a senior software engineer specializing in ${intent.feature} systems.
|
|
962
|
+
Generate implementation requirements, assumptions, and gotchas for a specific task.
|
|
963
|
+
|
|
964
|
+
CRITICAL RULES for requirements:
|
|
965
|
+
1. Every requirement must be SPECIFIC to this exact task \u2014 not generic boilerplate
|
|
966
|
+
2. Ask: "Would this sentence appear in a prompt about a completely different feature?" If yes \u2192 discard
|
|
967
|
+
3. Angle focus: ${getAngleFocus(angle)}
|
|
968
|
+
|
|
969
|
+
REJECTED examples (too generic \u2014 never include):
|
|
970
|
+
- "Use TypeScript throughout \u2014 no any"
|
|
971
|
+
- "Handle errors explicitly"
|
|
972
|
+
- "Keep components small and focused"
|
|
973
|
+
- "Follow best practices"
|
|
974
|
+
|
|
975
|
+
GOOD examples (specific, expert-level):
|
|
976
|
+
- "Stripe webhooks fire multiple times \u2014 use idempotency keys keyed on event.id to prevent double-processing"
|
|
977
|
+
- "Store payment_intent_id not charge_id \u2014 charge IDs change on retries"
|
|
978
|
+
- "JWT refresh tokens must be rotated on every use to prevent token theft via stolen refresh token reuse"
|
|
979
|
+
|
|
980
|
+
Return ONLY valid JSON \u2014 no markdown, no prose:
|
|
981
|
+
{
|
|
982
|
+
"requirements": ["...", "..."], // 10-13 specific items
|
|
983
|
+
"assumptions": ["Assumed ...", "Assumed ..."], // 3-5 items starting with "Assumed"
|
|
984
|
+
"gotchas": ["gotcha description", "..."] // 2-3 non-obvious things that WILL bite
|
|
985
|
+
}`;
|
|
986
|
+
const user = `Task: "${rawPrompt}"
|
|
987
|
+
Domain: ${intent.feature}
|
|
988
|
+
Stack: ${stackSummary}
|
|
989
|
+
Action: ${intent.action} | Entity: ${intent.entity}
|
|
990
|
+
|
|
991
|
+
Generate requirements, assumptions, and gotchas. JSON only.`;
|
|
992
|
+
const response = await client.messages.create({
|
|
993
|
+
model: "claude-haiku-4-5-20251001",
|
|
994
|
+
max_tokens: 1536,
|
|
995
|
+
system,
|
|
996
|
+
messages: [{ role: "user", content: user }]
|
|
997
|
+
});
|
|
998
|
+
const text = response.content[0]?.type === "text" ? response.content[0].text : "{}";
|
|
999
|
+
try {
|
|
1000
|
+
const parsed = JSON.parse(text);
|
|
1001
|
+
if (Array.isArray(parsed.requirements) && parsed.requirements.length > 0) {
|
|
1002
|
+
return {
|
|
1003
|
+
requirements: parsed.requirements,
|
|
1004
|
+
assumptions: Array.isArray(parsed.assumptions) ? parsed.assumptions : [],
|
|
1005
|
+
gotchas: Array.isArray(parsed.gotchas) ? parsed.gotchas : []
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
throw new Error("empty requirements");
|
|
1009
|
+
} catch {
|
|
1010
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
1011
|
+
if (jsonMatch) {
|
|
1012
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1013
|
+
return {
|
|
1014
|
+
requirements: Array.isArray(parsed.requirements) ? parsed.requirements : [],
|
|
1015
|
+
assumptions: Array.isArray(parsed.assumptions) ? parsed.assumptions : [],
|
|
1016
|
+
gotchas: Array.isArray(parsed.gotchas) ? parsed.gotchas : []
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
throw new Error("Failed to parse domain enhancements from AI response");
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
591
1023
|
// src/enhancer/index.ts
|
|
592
|
-
function enhance(rawPrompt, scan, contextFiles) {
|
|
593
|
-
const intent = analyzeIntent(rawPrompt);
|
|
594
|
-
const
|
|
1024
|
+
async function enhance(rawPrompt, scan, contextFiles, projectInstructions = "", gitContext = "", intentOverride, config, iteration = 0, _previousPrompt) {
|
|
1025
|
+
const intent = intentOverride ?? analyzeIntent(rawPrompt);
|
|
1026
|
+
const angle = getAngle(iteration);
|
|
1027
|
+
let injections;
|
|
1028
|
+
let assumptions = [];
|
|
1029
|
+
let gotchas = [];
|
|
1030
|
+
if (config?.apiKey) {
|
|
1031
|
+
try {
|
|
1032
|
+
const domain = await generateDomainEnhancements(rawPrompt, intent, scan.stack, angle, config.apiKey);
|
|
1033
|
+
injections = domain.requirements;
|
|
1034
|
+
assumptions = domain.assumptions;
|
|
1035
|
+
gotchas = domain.gotchas;
|
|
1036
|
+
} catch {
|
|
1037
|
+
injections = getFallbackInjections(scan.stack, intent, config);
|
|
1038
|
+
}
|
|
1039
|
+
} else {
|
|
1040
|
+
injections = getFallbackInjections(scan.stack, intent, config);
|
|
1041
|
+
}
|
|
595
1042
|
const enhanced = buildEnhancedPrompt({
|
|
596
1043
|
stack: scan.stack,
|
|
597
1044
|
structure: scan.structure,
|
|
598
1045
|
intent,
|
|
599
1046
|
contextFiles,
|
|
600
|
-
injections
|
|
1047
|
+
injections,
|
|
1048
|
+
projectInstructions,
|
|
1049
|
+
gitContext,
|
|
1050
|
+
angle,
|
|
1051
|
+
assumptions,
|
|
1052
|
+
gotchas,
|
|
1053
|
+
iteration
|
|
601
1054
|
});
|
|
602
|
-
return { original: rawPrompt, enhanced, intent, stack: scan.stack };
|
|
1055
|
+
return { original: rawPrompt, enhanced, intent, stack: scan.stack, iteration, angle: ANGLE_LABELS[angle] };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/enhancer/planner.ts
|
|
1059
|
+
import Anthropic2 from "@anthropic-ai/sdk";
|
|
1060
|
+
async function decomposeToPlan(rawPrompt, intent, stack, apiKey) {
|
|
1061
|
+
const client = new Anthropic2({ apiKey });
|
|
1062
|
+
const stackSummary = `${stack.frameworks.join(", ")} / ${stack.language}`;
|
|
1063
|
+
const systemPrompt = `You are a software architect. Break complex development tasks into sequential, focused implementation steps.
|
|
1064
|
+
Each step should be small enough to implement in a single AI session.
|
|
1065
|
+
Return valid JSON only \u2014 no prose, no markdown fences.`;
|
|
1066
|
+
const userPrompt = `Task: "${rawPrompt}"
|
|
1067
|
+
Stack: ${stackSummary}
|
|
1068
|
+
|
|
1069
|
+
Break this into ordered implementation steps. Each step must be self-contained and reference the previous step's outputs when needed.
|
|
1070
|
+
|
|
1071
|
+
Return JSON with this exact shape:
|
|
1072
|
+
{
|
|
1073
|
+
"steps": [
|
|
1074
|
+
{
|
|
1075
|
+
"step": 1,
|
|
1076
|
+
"title": "short title",
|
|
1077
|
+
"prompt": "the full focused prompt for this step, including what was done in previous steps",
|
|
1078
|
+
"dependsOn": []
|
|
1079
|
+
}
|
|
1080
|
+
]
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
Rules:
|
|
1084
|
+
- 2\u20136 steps maximum
|
|
1085
|
+
- Each prompt should be specific and actionable
|
|
1086
|
+
- Later steps must mention what the earlier steps created
|
|
1087
|
+
- Do not include setup/install steps unless strictly necessary`;
|
|
1088
|
+
const response = await client.messages.create({
|
|
1089
|
+
model: "claude-haiku-4-5-20251001",
|
|
1090
|
+
max_tokens: 2048,
|
|
1091
|
+
system: systemPrompt,
|
|
1092
|
+
messages: [{ role: "user", content: userPrompt }]
|
|
1093
|
+
});
|
|
1094
|
+
const text = response.content[0]?.type === "text" ? response.content[0].text : "{}";
|
|
1095
|
+
try {
|
|
1096
|
+
const parsed = JSON.parse(text);
|
|
1097
|
+
return parsed;
|
|
1098
|
+
} catch {
|
|
1099
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
1100
|
+
if (jsonMatch) {
|
|
1101
|
+
return JSON.parse(jsonMatch[0]);
|
|
1102
|
+
}
|
|
1103
|
+
throw new Error("Failed to parse plan JSON from AI response");
|
|
1104
|
+
}
|
|
603
1105
|
}
|
|
604
1106
|
|
|
605
1107
|
// src/providers/claude.ts
|
|
606
|
-
import
|
|
1108
|
+
import Anthropic3 from "@anthropic-ai/sdk";
|
|
1109
|
+
var MODEL_BY_COMPLEXITY = {
|
|
1110
|
+
simple: "claude-haiku-4-5-20251001",
|
|
1111
|
+
feature: "claude-sonnet-4-6",
|
|
1112
|
+
system: "claude-opus-4-7"
|
|
1113
|
+
};
|
|
607
1114
|
var ClaudeProvider = class {
|
|
608
1115
|
name = "Claude";
|
|
609
1116
|
client;
|
|
@@ -615,10 +1122,16 @@ var ClaudeProvider = class {
|
|
|
615
1122
|
"ANTHROPIC_API_KEY environment variable is not set."
|
|
616
1123
|
);
|
|
617
1124
|
}
|
|
618
|
-
this.client = new
|
|
1125
|
+
this.client = new Anthropic3({ apiKey: config.apiKey });
|
|
619
1126
|
this.model = config.model;
|
|
620
1127
|
}
|
|
621
|
-
async send(prompt) {
|
|
1128
|
+
async send(prompt, options) {
|
|
1129
|
+
const complexity = options?.["complexity"];
|
|
1130
|
+
if (complexity && MODEL_BY_COMPLEXITY[complexity] && this.model === "claude-sonnet-4-6") {
|
|
1131
|
+
this.model = MODEL_BY_COMPLEXITY[complexity];
|
|
1132
|
+
}
|
|
1133
|
+
console.log(` Model: ${this.model}
|
|
1134
|
+
`);
|
|
622
1135
|
const stream = await this.client.messages.create({
|
|
623
1136
|
model: this.model,
|
|
624
1137
|
max_tokens: 8192,
|
|
@@ -626,12 +1139,22 @@ var ClaudeProvider = class {
|
|
|
626
1139
|
messages: [{ role: "user", content: prompt }],
|
|
627
1140
|
stream: true
|
|
628
1141
|
});
|
|
1142
|
+
let inputTokens = 0;
|
|
1143
|
+
let outputTokens = 0;
|
|
629
1144
|
for await (const event of stream) {
|
|
630
1145
|
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
631
1146
|
process.stdout.write(event.delta.text);
|
|
632
1147
|
}
|
|
1148
|
+
if (event.type === "message_start" && event.message.usage) {
|
|
1149
|
+
inputTokens = event.message.usage.input_tokens;
|
|
1150
|
+
}
|
|
1151
|
+
if (event.type === "message_delta" && event.usage) {
|
|
1152
|
+
outputTokens = event.usage.output_tokens;
|
|
1153
|
+
}
|
|
633
1154
|
}
|
|
634
1155
|
process.stdout.write("\n");
|
|
1156
|
+
const onUsage = options?.["onUsage"];
|
|
1157
|
+
onUsage?.({ inputTokens, outputTokens, model: this.model });
|
|
635
1158
|
}
|
|
636
1159
|
};
|
|
637
1160
|
|
|
@@ -858,8 +1381,9 @@ program.command("<prompt>", { isDefault: true }).description("Enhance a prompt a
|
|
|
858
1381
|
Examples:
|
|
859
1382
|
enhance "build a login page"
|
|
860
1383
|
enhance "fix the auth bug" --dry-run
|
|
861
|
-
enhance "create dashboard" --
|
|
862
|
-
enhance "refactor user service" --provider codex
|
|
1384
|
+
enhance "create dashboard" --confirm --verbose
|
|
1385
|
+
enhance "refactor user service" --provider codex
|
|
1386
|
+
enhance "add auth" --action add --feature auth`).option("-p, --provider <name>", "AI provider: claude | codex | opencode").option("--dry-run", "Print enhanced prompt without sending to AI").option("--print-prompt", "Print enhanced prompt before sending to AI").option("--preview", "Color-coded prompt preview with per-section token counts").option("--verbose", "Show detected stack, intent, and context stats").option("--confirm", "Show detected intent and ask for approval before enhancing").option("--plan", "Decompose complex task into sequential sub-prompts (uses AI)").option("--iteration <n>", "Angle iteration: 0=completeness, 1=production, 2=dx, 3=alternative", "0").option("--action <action>", "Override detected action (create|fix|refactor|explain|add|delete)").option("--entity <entity>", "Override detected entity (page|component|api|hook|util|config|style)").option("--feature <feature>", "Override detected feature (auth|payment|user|upload|...)").action(async (rawPrompt, opts) => {
|
|
863
1387
|
if (!rawPrompt) {
|
|
864
1388
|
program.help();
|
|
865
1389
|
return;
|
|
@@ -877,7 +1401,41 @@ Examples:
|
|
|
877
1401
|
await setCached(cwd, scan);
|
|
878
1402
|
}
|
|
879
1403
|
spinner.text = "Analyzing intent...";
|
|
880
|
-
|
|
1404
|
+
let intent = analyzeIntent(rawPrompt);
|
|
1405
|
+
if (opts.action) intent = { ...intent, action: opts.action };
|
|
1406
|
+
if (opts.entity) intent = { ...intent, entity: opts.entity };
|
|
1407
|
+
if (opts.feature) intent = { ...intent, feature: opts.feature };
|
|
1408
|
+
if (opts.plan) {
|
|
1409
|
+
if (!config.apiKey) {
|
|
1410
|
+
error("ANTHROPIC_API_KEY required for --plan");
|
|
1411
|
+
process.exit(1);
|
|
1412
|
+
}
|
|
1413
|
+
spinner.text = "Decomposing task into steps...";
|
|
1414
|
+
const plan = await decomposeToPlan(rawPrompt, intent, scan.stack, config.apiKey);
|
|
1415
|
+
spinner.stop();
|
|
1416
|
+
console.log("\n" + chalk3.bold(`Implementation Plan (${plan.steps.length} steps)
|
|
1417
|
+
`));
|
|
1418
|
+
for (const step of plan.steps) {
|
|
1419
|
+
console.log(chalk3.cyan(`Step ${step.step}: ${step.title}`));
|
|
1420
|
+
if (step.dependsOn.length > 0) {
|
|
1421
|
+
console.log(chalk3.dim(` Depends on: Step ${step.dependsOn.join(", ")}`));
|
|
1422
|
+
}
|
|
1423
|
+
console.log(chalk3.dim("\u2500".repeat(60)));
|
|
1424
|
+
console.log(step.prompt);
|
|
1425
|
+
console.log();
|
|
1426
|
+
}
|
|
1427
|
+
process.exit(0);
|
|
1428
|
+
}
|
|
1429
|
+
if (opts.confirm) {
|
|
1430
|
+
spinner.stop();
|
|
1431
|
+
printIntentSummary(intent);
|
|
1432
|
+
const approved = await confirmPrompt("Proceed with this intent? [Y/n] ");
|
|
1433
|
+
if (!approved) {
|
|
1434
|
+
console.log(chalk3.yellow("\nAborted. Use --action / --entity / --feature to override detection.\n"));
|
|
1435
|
+
process.exit(0);
|
|
1436
|
+
}
|
|
1437
|
+
spinner.start("Optimizing context...");
|
|
1438
|
+
}
|
|
881
1439
|
if (opts.verbose) {
|
|
882
1440
|
spinner.stop();
|
|
883
1441
|
console.log("\nStack detected:", JSON.stringify(scan.stack, null, 2));
|
|
@@ -885,11 +1443,32 @@ Examples:
|
|
|
885
1443
|
spinner.start("Optimizing context...");
|
|
886
1444
|
}
|
|
887
1445
|
spinner.text = "Optimizing context...";
|
|
888
|
-
const contextFiles = await
|
|
1446
|
+
const [{ contextFiles, gitContext }, projectInstructions] = await Promise.all([
|
|
1447
|
+
findRelevantFiles(cwd, intent, scan.structure, config.maxContextTokens, config.alwaysInclude ?? []),
|
|
1448
|
+
findProjectInstructions(cwd)
|
|
1449
|
+
]);
|
|
1450
|
+
if (opts.verbose) {
|
|
1451
|
+
spinner.stop();
|
|
1452
|
+
if (projectInstructions) console.log(chalk3.dim(" Project instructions: found (CLAUDE.md / AGENTS.md)"));
|
|
1453
|
+
if (gitContext) console.log(chalk3.dim(" Git context: recent changes detected"));
|
|
1454
|
+
spinner.start("Enhancing prompt...");
|
|
1455
|
+
}
|
|
889
1456
|
spinner.text = "Enhancing prompt...";
|
|
890
|
-
const
|
|
1457
|
+
const iteration = Math.max(0, parseInt(opts.iteration ?? "0", 10) || 0);
|
|
1458
|
+
const enhanced = await enhance(rawPrompt, scan, contextFiles, projectInstructions, gitContext, intent, config, iteration);
|
|
891
1459
|
spinner.stop();
|
|
892
|
-
if (
|
|
1460
|
+
if (enhanced.angle) {
|
|
1461
|
+
console.log(chalk3.dim(`
|
|
1462
|
+
\u{1F4CD} v${iteration + 1} \u2014 ${enhanced.angle} angle
|
|
1463
|
+
`));
|
|
1464
|
+
}
|
|
1465
|
+
if (opts.preview) {
|
|
1466
|
+
printColoredPreview(enhanced.enhanced);
|
|
1467
|
+
if (opts.dryRun) process.exit(0);
|
|
1468
|
+
const go = await confirmPrompt("Send to AI? [Y/n] ");
|
|
1469
|
+
if (!go) process.exit(0);
|
|
1470
|
+
spinner.start(`Sending to AI...`);
|
|
1471
|
+
} else if (opts.dryRun || opts.printPrompt) {
|
|
893
1472
|
console.log("\n\u2500\u2500\u2500 Enhanced Prompt \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
894
1473
|
console.log(enhanced.enhanced);
|
|
895
1474
|
console.log("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
@@ -901,7 +1480,14 @@ Examples:
|
|
|
901
1480
|
console.log(`
|
|
902
1481
|
[${provider.name}]
|
|
903
1482
|
`);
|
|
904
|
-
await provider.send(enhanced.enhanced
|
|
1483
|
+
await provider.send(enhanced.enhanced, {
|
|
1484
|
+
complexity: enhanced.intent.complexity,
|
|
1485
|
+
onUsage: (usage) => {
|
|
1486
|
+
const cost = estimateCost(usage.model, usage.inputTokens, usage.outputTokens);
|
|
1487
|
+
console.log(chalk3.dim(`
|
|
1488
|
+
Tokens: ${usage.inputTokens} in / ${usage.outputTokens} out \xB7 ~$${cost}`));
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
905
1491
|
} catch (err) {
|
|
906
1492
|
spinner.stop();
|
|
907
1493
|
if (err instanceof EnhanceError) {
|
|
@@ -916,3 +1502,69 @@ Examples:
|
|
|
916
1502
|
}
|
|
917
1503
|
});
|
|
918
1504
|
program.parse();
|
|
1505
|
+
function printIntentSummary(intent) {
|
|
1506
|
+
const confidencePct = Math.round(intent.confidence * 100);
|
|
1507
|
+
const confidenceColor = intent.confidence >= 0.6 ? chalk3.green : intent.confidence >= 0.3 ? chalk3.yellow : chalk3.red;
|
|
1508
|
+
console.log("\n" + chalk3.bold("Detected Intent:"));
|
|
1509
|
+
console.log(` ${chalk3.cyan("Action")}: ${intent.action}`);
|
|
1510
|
+
console.log(` ${chalk3.cyan("Entity")}: ${intent.entity}`);
|
|
1511
|
+
console.log(` ${chalk3.cyan("Feature")}: ${intent.feature}`);
|
|
1512
|
+
console.log(` ${chalk3.cyan("Scope")}: ${intent.scope}`);
|
|
1513
|
+
console.log(` ${chalk3.cyan("Complexity")}: ${intent.complexity}`);
|
|
1514
|
+
console.log(` ${chalk3.cyan("Confidence")}: ${confidenceColor(`${confidencePct}%`)}`);
|
|
1515
|
+
console.log();
|
|
1516
|
+
}
|
|
1517
|
+
async function confirmPrompt(question) {
|
|
1518
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1519
|
+
return new Promise((resolve2) => {
|
|
1520
|
+
rl.question(question, (answer) => {
|
|
1521
|
+
rl.close();
|
|
1522
|
+
resolve2(answer.toLowerCase() !== "n" && answer.toLowerCase() !== "no");
|
|
1523
|
+
});
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
function printColoredPreview(prompt) {
|
|
1527
|
+
const SECTION_COLORS = {
|
|
1528
|
+
"Project Instructions": chalk3.magenta,
|
|
1529
|
+
"Project Context": chalk3.blue,
|
|
1530
|
+
"Task": chalk3.yellow,
|
|
1531
|
+
"Requirements": chalk3.green,
|
|
1532
|
+
"Recent Changes": chalk3.cyan,
|
|
1533
|
+
"Relevant Code": chalk3.dim,
|
|
1534
|
+
"Code": chalk3.dim,
|
|
1535
|
+
"Expected Output": chalk3.white
|
|
1536
|
+
};
|
|
1537
|
+
const CHARS_PER_TOKEN2 = 4;
|
|
1538
|
+
const sections = prompt.split(/\n---\n/);
|
|
1539
|
+
const totalTokens = Math.ceil(prompt.length / CHARS_PER_TOKEN2);
|
|
1540
|
+
console.log("\n" + chalk3.bold("\u2500\u2500\u2500 Enhanced Prompt Preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1541
|
+
console.log(chalk3.dim(`Total: ~${totalTokens} tokens
|
|
1542
|
+
`));
|
|
1543
|
+
for (const section of sections) {
|
|
1544
|
+
const headerMatch = section.match(/^## (.+)/m);
|
|
1545
|
+
const headerName = headerMatch?.[1]?.trim() ?? "Section";
|
|
1546
|
+
const colorFn = SECTION_COLORS[headerName] ?? chalk3.white;
|
|
1547
|
+
const sectionTokens = Math.ceil(section.length / CHARS_PER_TOKEN2);
|
|
1548
|
+
console.log(colorFn(chalk3.bold(`## ${headerName}`)) + chalk3.dim(` (~${sectionTokens} tok)`));
|
|
1549
|
+
const body = section.replace(/^## .+\n?/, "").trim();
|
|
1550
|
+
if (body) {
|
|
1551
|
+
const lines = body.split("\n");
|
|
1552
|
+
const preview = lines.slice(0, 8).join("\n");
|
|
1553
|
+
const rest = lines.length > 8 ? chalk3.dim(`
|
|
1554
|
+
... (${lines.length - 8} more lines)`) : "";
|
|
1555
|
+
console.log(colorFn(preview) + rest);
|
|
1556
|
+
}
|
|
1557
|
+
console.log();
|
|
1558
|
+
}
|
|
1559
|
+
console.log(chalk3.bold("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
1560
|
+
}
|
|
1561
|
+
var MODEL_PRICING = {
|
|
1562
|
+
"claude-haiku-4-5-20251001": { input: 0.8, output: 4 },
|
|
1563
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
1564
|
+
"claude-opus-4-7": { input: 15, output: 75 }
|
|
1565
|
+
};
|
|
1566
|
+
function estimateCost(model, inputTokens, outputTokens) {
|
|
1567
|
+
const pricing = MODEL_PRICING[model] ?? MODEL_PRICING["claude-sonnet-4-6"];
|
|
1568
|
+
const cost = (inputTokens * pricing.input + outputTokens * pricing.output) / 1e6;
|
|
1569
|
+
return cost < 1e-3 ? "<0.001" : cost.toFixed(4);
|
|
1570
|
+
}
|