@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.
Files changed (3) hide show
  1. package/README.md +20 -1
  2. package/dist/index.js +775 -123
  3. 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
- ### Step 1Install globally
26
+ ### Option ANo 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: 3e3
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
- refactor: ["refactor", "clean", "improve", "optimize", "restructure", "simplify", "rewrite", "reorganize"],
249
- explain: ["explain", "describe", "what", "how does", "why", "show me", "understand", "walkthrough"],
250
- add: ["add", "integrate", "install", "include", "plug in", "connect"],
251
- delete: ["delete", "remove", "drop", "clean up", "uninstall", "get rid"],
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", "element", "button", "form", "modal", "card", "dialog", "input", "dropdown", "table", "list", "nav", "navbar", "sidebar", "header", "footer", "menu"],
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", "usecallback", "useeffect", "usestate", "usememo", "custom hook"],
259
- util: ["util", "utility", "helper", "function", "service", "lib", "library"],
260
- config: ["config", "configuration", "setting", "env", "environment"],
261
- style: ["style", "css", "theme", "design", "color", "spacing", "typography"],
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", "authentication", "login", "logout", "signup", "sign up", "sign in", "register", "password", "session", "oauth", "jwt", "credentials"],
266
- dashboard: ["dashboard", "admin", "panel", "overview", "analytics", "stats", "metrics"],
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", "settings", "preferences"],
269
- search: ["search", "filter", "query", "find", "lookup"],
270
- upload: ["upload", "file", "image", "media", "attachment", "storage"],
271
- notification: ["notification", "alert", "email", "push", "toast", "banner"],
272
- navigation: ["nav", "navbar", "navigation", "menu", "breadcrumb", "sidebar", "header"],
273
- table: ["table", "grid", "list", "data table", "datagrid"],
274
- form: ["form", "input", "field", "validation", "submit"],
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
- if (keywords.some((k) => lower.includes(k))) {
312
+ const matches = keywords.filter((k) => lower.includes(k)).length;
313
+ if (matches > actionMatchCount) {
283
314
  action = act;
284
- break;
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
- if (keywords.some((k) => lower.includes(k))) {
322
+ const matches = keywords.filter((k) => lower.includes(k)).length;
323
+ if (matches > entityMatchCount) {
291
324
  entity = ent;
292
- break;
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
- if (keywords.some((k) => lower.includes(k))) {
331
+ const matches = keywords.filter((k) => lower.includes(k)).length;
332
+ if (matches > featureMatchCount) {
298
333
  feature = feat;
299
- break;
334
+ featureMatchCount = matches;
300
335
  }
301
336
  }
302
- return { action, entity, feature, rawPrompt };
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 MAX_CHARS = 12e3;
330
- var MAX_FILE_LINES = 250;
331
- async function findRelevantFiles(root, intent, structure) {
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 charBudget = MAX_CHARS;
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
- const snippet = truncateLines(content, MAX_FILE_LINES);
338
- const section = `// FILE: ${pattern}
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
- if (section.length < charBudget) {
341
- sections.push(section);
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
- for (const file of files.slice(0, 5)) {
364
- if (charBudget <= 0) break;
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 snippet = truncateLines(content, MAX_FILE_LINES);
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
- if (section.length < charBudget) {
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
- charBudget -= section.length;
533
+ tokenBudget -= estimateTokens(section);
374
534
  }
375
535
  }
376
536
  }
377
- if (["component", "page"].includes(intent.entity) && charBudget > 1e3) {
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 (charBudget <= 500) break;
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 = truncateLines(content, 80);
555
+ const snippet = smartTruncate(content, Math.min(200, tokenBudget - 50));
396
556
  const section = `// FILE: ${relPath} (pattern reference)
397
557
  ${snippet}`;
398
- if (section.length < charBudget) {
399
- sections.push(section);
400
- charBudget -= section.length;
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 sections.join("\n\n");
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 getInjections(stack, intent) {
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 { stack, structure, intent, contextFiles, injections } = opts;
496
- const stackLabel = formatStack(stack);
497
- const structureLabel = formatStructure(structure);
498
- const taskDescription = buildTaskDescription(intent);
499
- const requirementsList = injections.map((r) => `- ${r}`).join("\n");
500
- const contextSection = contextFiles ? `
501
- ## Existing Code Context
502
-
503
- ${contextFiles}` : "";
504
- return `You are working inside a ${stackLabel} project.
505
-
506
- ## Project Stack
507
- ${formatStackDetails(stack)}
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
- ## Project Structure
510
- ${structureLabel}
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
- ## Task
513
- ${taskDescription}
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
- ## Requirements
516
- ${requirementsList}
517
- ${contextSection}
747
+ ${projectInstructions}`);
748
+ }
749
+ sections.push(`## Context
750
+ **Stack**: ${formatStack(stack)}
751
+ ${formatStackDetails(stack)}`);
752
+ sections.push(`## Task
518
753
 
519
- ---
520
- Complete the task above. Follow every requirement. At the end, list every file you created or modified.`;
521
- }
522
- function formatStack(stack) {
523
- const primary = stack.frameworks[0] ?? "Node.js";
524
- const lang = stack.language === "typescript" ? "TypeScript" : "JavaScript";
525
- return `${capitalize(primary)} ${lang}`;
526
- }
527
- function formatStackDetails(stack) {
528
- const lines = [];
529
- lines.push(`- **Language**: ${stack.language === "typescript" ? "TypeScript" : "JavaScript"}`);
530
- if (stack.frameworks.length > 0) {
531
- lines.push(`- **Frameworks**: ${stack.frameworks.map(capitalize).join(", ")}`);
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 (stack.uiLibrary) {
534
- lines.push(`- **UI Library**: ${capitalize(stack.uiLibrary)}`);
768
+ if (contextFiles) {
769
+ sections.push(`## Code
770
+
771
+ ${contextFiles}`);
535
772
  }
536
- if (stack.orm) {
537
- lines.push(`- **ORM**: ${capitalize(stack.orm)}`);
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
- if (stack.testing) {
540
- lines.push(`- **Testing**: ${capitalize(stack.testing)}`);
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 (stack.nextRouterType) {
543
- lines.push(`- **Next.js Router**: ${stack.nextRouterType === "app" ? "App Router" : "Pages Router"}`);
816
+ if (contextFiles) {
817
+ sections.push(`## Relevant Code
818
+
819
+ ${contextFiles}`);
544
820
  }
545
- lines.push(`- **Package Manager**: ${stack.packageManager}`);
546
- return lines.join("\n");
821
+ sections.push(`## Expected Output
822
+
823
+ ${buildOutputFormat(intent)}`);
824
+ return sections.join("\n\n---\n\n");
547
825
  }
548
- function formatStructure(structure) {
549
- const lines = [];
550
- if (structure.dirs.length > 0) {
551
- lines.push(`Top-level directories: \`${structure.dirs.join("/")}\``);
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
- if (structure.hasSrcDir && structure.srcDirs.length > 0) {
554
- lines.push(`Inside src/: \`${structure.srcDirs.join("/")}\``);
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 `${action}: ${intent.rawPrompt}`;
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 injections = getInjections(scan.stack, intent);
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 Anthropic from "@anthropic-ai/sdk";
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 Anthropic({ apiKey: config.apiKey });
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" --print-prompt --verbose
862
- enhance "refactor user service" --provider codex`).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("--verbose", "Show detected stack and intent").action(async (rawPrompt, opts) => {
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
- const intent = analyzeIntent(rawPrompt);
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 findRelevantFiles(cwd, intent, scan.structure);
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 enhanced = enhance(rawPrompt, scan, contextFiles);
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 (opts.dryRun || opts.printPrompt) {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xdevabir/enhance",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "AI prompt middleware — intelligently rewrites developer prompts before sending to coding agents",
5
5
  "bin": {
6
6
  "enhance": "./bin/enhance.js"