@0xdevabir/enhance 0.1.0 → 0.1.1

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 +615 -122
  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 getInjections(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,160 @@ 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
 
493
683
  // src/enhancer/template.ts
494
684
  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)}
685
+ const { intent } = opts;
686
+ switch (intent.complexity) {
687
+ case "simple":
688
+ return buildSimplePrompt(opts);
689
+ case "system":
690
+ return buildSystemPrompt(opts);
691
+ default:
692
+ return buildFeaturePrompt(opts);
693
+ }
694
+ }
695
+ function buildSimplePrompt(opts) {
696
+ const { stack, intent, contextFiles, gitContext, injections, projectInstructions } = opts;
697
+ const sections = [];
698
+ if (projectInstructions) {
699
+ sections.push(`## Project Instructions
508
700
 
509
- ## Project Structure
510
- ${structureLabel}
701
+ ${projectInstructions}`);
702
+ }
703
+ sections.push(`## Context
704
+ **Stack**: ${formatStack(stack)}
705
+ ${formatStackDetails(stack)}`);
706
+ sections.push(`## Task
511
707
 
512
- ## Task
513
- ${taskDescription}
708
+ ${buildTaskDescription(intent)}`);
709
+ const rules = [...injections.slice(0, 3), ...injections.slice(-3)];
710
+ sections.push(`## Requirements
514
711
 
515
- ## Requirements
516
- ${requirementsList}
517
- ${contextSection}
712
+ ${rules.map((r) => `- ${r}`).join("\n")}`);
713
+ if (gitContext) {
714
+ sections.push(`## Recent Changes
518
715
 
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(", ")}`);
716
+ ${gitContext}`);
532
717
  }
533
- if (stack.uiLibrary) {
534
- lines.push(`- **UI Library**: ${capitalize(stack.uiLibrary)}`);
718
+ if (contextFiles) {
719
+ sections.push(`## Code
720
+
721
+ ${contextFiles}`);
535
722
  }
536
- if (stack.orm) {
537
- lines.push(`- **ORM**: ${capitalize(stack.orm)}`);
723
+ sections.push(`## Expected Output
724
+
725
+ ${buildOutputFormat(intent)}`);
726
+ return sections.join("\n\n---\n\n");
727
+ }
728
+ function buildFeaturePrompt(opts) {
729
+ const { stack, structure, intent, contextFiles, gitContext, injections, projectInstructions } = opts;
730
+ const sections = [];
731
+ if (projectInstructions) {
732
+ sections.push(`## Project Instructions
733
+
734
+ ${projectInstructions}`);
538
735
  }
539
- if (stack.testing) {
540
- lines.push(`- **Testing**: ${capitalize(stack.testing)}`);
736
+ sections.push(`## Project Context
737
+
738
+ **Stack**: ${formatStack(stack)}
739
+ ${formatStackDetails(stack)}
740
+
741
+ **Structure**: ${formatStructure(structure)}`);
742
+ sections.push(`## Task
743
+
744
+ ${buildTaskDescription(intent)}`);
745
+ const specific = injections.slice(0, -3);
746
+ const universal = injections.slice(-3);
747
+ const requirementLines = [
748
+ ...specific.map((r) => `- ${r}`),
749
+ ...specific.length > 0 ? ["", "**Always:**"] : ["**Always:**"],
750
+ ...universal.map((r) => `- ${r}`)
751
+ ].join("\n");
752
+ sections.push(`## Requirements
753
+
754
+ ${requirementLines}`);
755
+ if (gitContext) {
756
+ sections.push(`## Recent Changes
757
+
758
+ ${gitContext}`);
541
759
  }
542
- if (stack.nextRouterType) {
543
- lines.push(`- **Next.js Router**: ${stack.nextRouterType === "app" ? "App Router" : "Pages Router"}`);
760
+ if (contextFiles) {
761
+ sections.push(`## Relevant Code
762
+
763
+ ${contextFiles}`);
544
764
  }
545
- lines.push(`- **Package Manager**: ${stack.packageManager}`);
546
- return lines.join("\n");
765
+ sections.push(`## Expected Output
766
+
767
+ ${buildOutputFormat(intent)}`);
768
+ return sections.join("\n\n---\n\n");
547
769
  }
548
- function formatStructure(structure) {
549
- const lines = [];
550
- if (structure.dirs.length > 0) {
551
- lines.push(`Top-level directories: \`${structure.dirs.join("/")}\``);
770
+ function buildSystemPrompt(opts) {
771
+ const { stack, structure, intent, contextFiles, gitContext, injections, projectInstructions } = opts;
772
+ const sections = [];
773
+ if (projectInstructions) {
774
+ sections.push(`## Project Instructions
775
+
776
+ ${projectInstructions}`);
552
777
  }
553
- if (structure.hasSrcDir && structure.srcDirs.length > 0) {
554
- lines.push(`Inside src/: \`${structure.srcDirs.join("/")}\``);
778
+ sections.push(`## Project Context
779
+
780
+ **Stack**: ${formatStack(stack)}
781
+ ${formatStackDetails(stack)}
782
+
783
+ **Structure**: ${formatStructure(structure)}`);
784
+ sections.push(`## Task
785
+
786
+ ${buildTaskDescription(intent)}
787
+
788
+ > This is a system-level change. Before writing any code:
789
+ > 1. Analyze the current state and identify all affected areas
790
+ > 2. Define your implementation plan step by step
791
+ > 3. Highlight any breaking changes or migration steps
792
+ > 4. Then implement each step`);
793
+ const requirementLines = injections.map((r) => `- ${r}`).join("\n");
794
+ sections.push(`## Requirements
795
+
796
+ ${requirementLines}`);
797
+ if (gitContext) {
798
+ sections.push(`## Recent Changes
799
+
800
+ ${gitContext}`);
801
+ }
802
+ if (contextFiles) {
803
+ sections.push(`## Relevant Code
804
+
805
+ ${contextFiles}`);
806
+ }
807
+ sections.push(`## Expected Output
808
+
809
+ 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.`);
810
+ return sections.join("\n\n---\n\n");
811
+ }
812
+ function buildOutputFormat(intent) {
813
+ switch (intent.action) {
814
+ case "fix":
815
+ return "Show exactly what changed. Explain in one sentence why it was broken. Provide the corrected code. List every file modified at the end.";
816
+ case "create":
817
+ case "add":
818
+ 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.";
819
+ case "refactor":
820
+ 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.";
821
+ case "explain":
822
+ return "Walk through step by step. Reference specific function names, line numbers, or variable names from the existing code. Use concrete examples.";
823
+ case "delete":
824
+ return "List exactly what you will remove and why. Show the cleaned-up result. Confirm nothing else depends on the removed code.";
825
+ default:
826
+ return "Be thorough and precise. Show all changes. List every file created or modified at the end of your response.";
555
827
  }
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
828
  }
560
829
  function buildTaskDescription(intent) {
561
830
  const actionMap = {
@@ -568,7 +837,36 @@ function buildTaskDescription(intent) {
568
837
  unknown: "Handle"
569
838
  };
570
839
  const action = actionMap[intent.action] ?? "Handle";
571
- return `${action}: ${intent.rawPrompt}`;
840
+ return `**${action}**: ${intent.rawPrompt}`;
841
+ }
842
+ function formatStack(stack) {
843
+ const primary = stack.frameworks[0] ?? "Node.js";
844
+ const lang = stack.language === "typescript" ? "TypeScript" : "JavaScript";
845
+ return `${capitalize(primary)} / ${lang}`;
846
+ }
847
+ function formatStackDetails(stack) {
848
+ const lines = [];
849
+ if (stack.frameworks.length > 0) {
850
+ lines.push(`- **Frameworks**: ${stack.frameworks.map(capitalize).join(", ")}`);
851
+ }
852
+ if (stack.uiLibrary) lines.push(`- **UI**: ${capitalize(stack.uiLibrary)}`);
853
+ if (stack.orm) lines.push(`- **ORM**: ${capitalize(stack.orm)}`);
854
+ if (stack.testing) lines.push(`- **Testing**: ${capitalize(stack.testing)}`);
855
+ if (stack.nextRouterType) {
856
+ lines.push(`- **Router**: ${stack.nextRouterType === "app" ? "App Router" : "Pages Router"}`);
857
+ }
858
+ lines.push(`- **Package Manager**: ${stack.packageManager}`);
859
+ return lines.join("\n");
860
+ }
861
+ function formatStructure(structure) {
862
+ const parts = [];
863
+ if (structure.dirs.length > 0) parts.push(`dirs: \`${structure.dirs.join(", ")}\``);
864
+ if (structure.hasSrcDir && structure.srcDirs.length > 0) {
865
+ parts.push(`src/: \`${structure.srcDirs.join(", ")}\``);
866
+ }
867
+ if (structure.hasAppDir) parts.push("App Router (`app/`)");
868
+ if (structure.hasPagesDir) parts.push("Pages Router (`pages/`)");
869
+ return parts.join(" | ") || "Standard layout";
572
870
  }
573
871
  function capitalize(s) {
574
872
  if (!s) return s;
@@ -589,21 +887,77 @@ function capitalize(s) {
589
887
  }
590
888
 
591
889
  // src/enhancer/index.ts
592
- function enhance(rawPrompt, scan, contextFiles) {
593
- const intent = analyzeIntent(rawPrompt);
594
- const injections = getInjections(scan.stack, intent);
890
+ function enhance(rawPrompt, scan, contextFiles, projectInstructions = "", gitContext = "", intentOverride, config) {
891
+ const intent = intentOverride ?? analyzeIntent(rawPrompt);
892
+ const injections = getInjections(scan.stack, intent, config);
595
893
  const enhanced = buildEnhancedPrompt({
596
894
  stack: scan.stack,
597
895
  structure: scan.structure,
598
896
  intent,
599
897
  contextFiles,
600
- injections
898
+ injections,
899
+ projectInstructions,
900
+ gitContext
601
901
  });
602
902
  return { original: rawPrompt, enhanced, intent, stack: scan.stack };
603
903
  }
604
904
 
605
- // src/providers/claude.ts
905
+ // src/enhancer/planner.ts
606
906
  import Anthropic from "@anthropic-ai/sdk";
907
+ async function decomposeToPlan(rawPrompt, intent, stack, apiKey) {
908
+ const client = new Anthropic({ apiKey });
909
+ const stackSummary = `${stack.frameworks.join(", ")} / ${stack.language}`;
910
+ const systemPrompt = `You are a software architect. Break complex development tasks into sequential, focused implementation steps.
911
+ Each step should be small enough to implement in a single AI session.
912
+ Return valid JSON only \u2014 no prose, no markdown fences.`;
913
+ const userPrompt = `Task: "${rawPrompt}"
914
+ Stack: ${stackSummary}
915
+
916
+ Break this into ordered implementation steps. Each step must be self-contained and reference the previous step's outputs when needed.
917
+
918
+ Return JSON with this exact shape:
919
+ {
920
+ "steps": [
921
+ {
922
+ "step": 1,
923
+ "title": "short title",
924
+ "prompt": "the full focused prompt for this step, including what was done in previous steps",
925
+ "dependsOn": []
926
+ }
927
+ ]
928
+ }
929
+
930
+ Rules:
931
+ - 2\u20136 steps maximum
932
+ - Each prompt should be specific and actionable
933
+ - Later steps must mention what the earlier steps created
934
+ - Do not include setup/install steps unless strictly necessary`;
935
+ const response = await client.messages.create({
936
+ model: "claude-haiku-4-5-20251001",
937
+ max_tokens: 2048,
938
+ system: systemPrompt,
939
+ messages: [{ role: "user", content: userPrompt }]
940
+ });
941
+ const text = response.content[0]?.type === "text" ? response.content[0].text : "{}";
942
+ try {
943
+ const parsed = JSON.parse(text);
944
+ return parsed;
945
+ } catch {
946
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
947
+ if (jsonMatch) {
948
+ return JSON.parse(jsonMatch[0]);
949
+ }
950
+ throw new Error("Failed to parse plan JSON from AI response");
951
+ }
952
+ }
953
+
954
+ // src/providers/claude.ts
955
+ import Anthropic2 from "@anthropic-ai/sdk";
956
+ var MODEL_BY_COMPLEXITY = {
957
+ simple: "claude-haiku-4-5-20251001",
958
+ feature: "claude-sonnet-4-6",
959
+ system: "claude-opus-4-7"
960
+ };
607
961
  var ClaudeProvider = class {
608
962
  name = "Claude";
609
963
  client;
@@ -615,10 +969,16 @@ var ClaudeProvider = class {
615
969
  "ANTHROPIC_API_KEY environment variable is not set."
616
970
  );
617
971
  }
618
- this.client = new Anthropic({ apiKey: config.apiKey });
972
+ this.client = new Anthropic2({ apiKey: config.apiKey });
619
973
  this.model = config.model;
620
974
  }
621
- async send(prompt) {
975
+ async send(prompt, options) {
976
+ const complexity = options?.["complexity"];
977
+ if (complexity && MODEL_BY_COMPLEXITY[complexity] && this.model === "claude-sonnet-4-6") {
978
+ this.model = MODEL_BY_COMPLEXITY[complexity];
979
+ }
980
+ console.log(` Model: ${this.model}
981
+ `);
622
982
  const stream = await this.client.messages.create({
623
983
  model: this.model,
624
984
  max_tokens: 8192,
@@ -626,12 +986,22 @@ var ClaudeProvider = class {
626
986
  messages: [{ role: "user", content: prompt }],
627
987
  stream: true
628
988
  });
989
+ let inputTokens = 0;
990
+ let outputTokens = 0;
629
991
  for await (const event of stream) {
630
992
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
631
993
  process.stdout.write(event.delta.text);
632
994
  }
995
+ if (event.type === "message_start" && event.message.usage) {
996
+ inputTokens = event.message.usage.input_tokens;
997
+ }
998
+ if (event.type === "message_delta" && event.usage) {
999
+ outputTokens = event.usage.output_tokens;
1000
+ }
633
1001
  }
634
1002
  process.stdout.write("\n");
1003
+ const onUsage = options?.["onUsage"];
1004
+ onUsage?.({ inputTokens, outputTokens, model: this.model });
635
1005
  }
636
1006
  };
637
1007
 
@@ -858,8 +1228,9 @@ program.command("<prompt>", { isDefault: true }).description("Enhance a prompt a
858
1228
  Examples:
859
1229
  enhance "build a login page"
860
1230
  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) => {
1231
+ enhance "create dashboard" --confirm --verbose
1232
+ enhance "refactor user service" --provider codex
1233
+ 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("--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
1234
  if (!rawPrompt) {
864
1235
  program.help();
865
1236
  return;
@@ -877,7 +1248,41 @@ Examples:
877
1248
  await setCached(cwd, scan);
878
1249
  }
879
1250
  spinner.text = "Analyzing intent...";
880
- const intent = analyzeIntent(rawPrompt);
1251
+ let intent = analyzeIntent(rawPrompt);
1252
+ if (opts.action) intent = { ...intent, action: opts.action };
1253
+ if (opts.entity) intent = { ...intent, entity: opts.entity };
1254
+ if (opts.feature) intent = { ...intent, feature: opts.feature };
1255
+ if (opts.plan) {
1256
+ if (!config.apiKey) {
1257
+ error("ANTHROPIC_API_KEY required for --plan");
1258
+ process.exit(1);
1259
+ }
1260
+ spinner.text = "Decomposing task into steps...";
1261
+ const plan = await decomposeToPlan(rawPrompt, intent, scan.stack, config.apiKey);
1262
+ spinner.stop();
1263
+ console.log("\n" + chalk3.bold(`Implementation Plan (${plan.steps.length} steps)
1264
+ `));
1265
+ for (const step of plan.steps) {
1266
+ console.log(chalk3.cyan(`Step ${step.step}: ${step.title}`));
1267
+ if (step.dependsOn.length > 0) {
1268
+ console.log(chalk3.dim(` Depends on: Step ${step.dependsOn.join(", ")}`));
1269
+ }
1270
+ console.log(chalk3.dim("\u2500".repeat(60)));
1271
+ console.log(step.prompt);
1272
+ console.log();
1273
+ }
1274
+ process.exit(0);
1275
+ }
1276
+ if (opts.confirm) {
1277
+ spinner.stop();
1278
+ printIntentSummary(intent);
1279
+ const approved = await confirmPrompt("Proceed with this intent? [Y/n] ");
1280
+ if (!approved) {
1281
+ console.log(chalk3.yellow("\nAborted. Use --action / --entity / --feature to override detection.\n"));
1282
+ process.exit(0);
1283
+ }
1284
+ spinner.start("Optimizing context...");
1285
+ }
881
1286
  if (opts.verbose) {
882
1287
  spinner.stop();
883
1288
  console.log("\nStack detected:", JSON.stringify(scan.stack, null, 2));
@@ -885,11 +1290,26 @@ Examples:
885
1290
  spinner.start("Optimizing context...");
886
1291
  }
887
1292
  spinner.text = "Optimizing context...";
888
- const contextFiles = await findRelevantFiles(cwd, intent, scan.structure);
1293
+ const [{ contextFiles, gitContext }, projectInstructions] = await Promise.all([
1294
+ findRelevantFiles(cwd, intent, scan.structure, config.maxContextTokens, config.alwaysInclude ?? []),
1295
+ findProjectInstructions(cwd)
1296
+ ]);
1297
+ if (opts.verbose) {
1298
+ spinner.stop();
1299
+ if (projectInstructions) console.log(chalk3.dim(" Project instructions: found (CLAUDE.md / AGENTS.md)"));
1300
+ if (gitContext) console.log(chalk3.dim(" Git context: recent changes detected"));
1301
+ spinner.start("Enhancing prompt...");
1302
+ }
889
1303
  spinner.text = "Enhancing prompt...";
890
- const enhanced = enhance(rawPrompt, scan, contextFiles);
1304
+ const enhanced = enhance(rawPrompt, scan, contextFiles, projectInstructions, gitContext, intent, config);
891
1305
  spinner.stop();
892
- if (opts.dryRun || opts.printPrompt) {
1306
+ if (opts.preview) {
1307
+ printColoredPreview(enhanced.enhanced);
1308
+ if (opts.dryRun) process.exit(0);
1309
+ const go = await confirmPrompt("Send to AI? [Y/n] ");
1310
+ if (!go) process.exit(0);
1311
+ spinner.start(`Sending to AI...`);
1312
+ } else if (opts.dryRun || opts.printPrompt) {
893
1313
  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
1314
  console.log(enhanced.enhanced);
895
1315
  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 +1321,14 @@ Examples:
901
1321
  console.log(`
902
1322
  [${provider.name}]
903
1323
  `);
904
- await provider.send(enhanced.enhanced);
1324
+ await provider.send(enhanced.enhanced, {
1325
+ complexity: enhanced.intent.complexity,
1326
+ onUsage: (usage) => {
1327
+ const cost = estimateCost(usage.model, usage.inputTokens, usage.outputTokens);
1328
+ console.log(chalk3.dim(`
1329
+ Tokens: ${usage.inputTokens} in / ${usage.outputTokens} out \xB7 ~$${cost}`));
1330
+ }
1331
+ });
905
1332
  } catch (err) {
906
1333
  spinner.stop();
907
1334
  if (err instanceof EnhanceError) {
@@ -916,3 +1343,69 @@ Examples:
916
1343
  }
917
1344
  });
918
1345
  program.parse();
1346
+ function printIntentSummary(intent) {
1347
+ const confidencePct = Math.round(intent.confidence * 100);
1348
+ const confidenceColor = intent.confidence >= 0.6 ? chalk3.green : intent.confidence >= 0.3 ? chalk3.yellow : chalk3.red;
1349
+ console.log("\n" + chalk3.bold("Detected Intent:"));
1350
+ console.log(` ${chalk3.cyan("Action")}: ${intent.action}`);
1351
+ console.log(` ${chalk3.cyan("Entity")}: ${intent.entity}`);
1352
+ console.log(` ${chalk3.cyan("Feature")}: ${intent.feature}`);
1353
+ console.log(` ${chalk3.cyan("Scope")}: ${intent.scope}`);
1354
+ console.log(` ${chalk3.cyan("Complexity")}: ${intent.complexity}`);
1355
+ console.log(` ${chalk3.cyan("Confidence")}: ${confidenceColor(`${confidencePct}%`)}`);
1356
+ console.log();
1357
+ }
1358
+ async function confirmPrompt(question) {
1359
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1360
+ return new Promise((resolve2) => {
1361
+ rl.question(question, (answer) => {
1362
+ rl.close();
1363
+ resolve2(answer.toLowerCase() !== "n" && answer.toLowerCase() !== "no");
1364
+ });
1365
+ });
1366
+ }
1367
+ function printColoredPreview(prompt) {
1368
+ const SECTION_COLORS = {
1369
+ "Project Instructions": chalk3.magenta,
1370
+ "Project Context": chalk3.blue,
1371
+ "Task": chalk3.yellow,
1372
+ "Requirements": chalk3.green,
1373
+ "Recent Changes": chalk3.cyan,
1374
+ "Relevant Code": chalk3.dim,
1375
+ "Code": chalk3.dim,
1376
+ "Expected Output": chalk3.white
1377
+ };
1378
+ const CHARS_PER_TOKEN2 = 4;
1379
+ const sections = prompt.split(/\n---\n/);
1380
+ const totalTokens = Math.ceil(prompt.length / CHARS_PER_TOKEN2);
1381
+ 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"));
1382
+ console.log(chalk3.dim(`Total: ~${totalTokens} tokens
1383
+ `));
1384
+ for (const section of sections) {
1385
+ const headerMatch = section.match(/^## (.+)/m);
1386
+ const headerName = headerMatch?.[1]?.trim() ?? "Section";
1387
+ const colorFn = SECTION_COLORS[headerName] ?? chalk3.white;
1388
+ const sectionTokens = Math.ceil(section.length / CHARS_PER_TOKEN2);
1389
+ console.log(colorFn(chalk3.bold(`## ${headerName}`)) + chalk3.dim(` (~${sectionTokens} tok)`));
1390
+ const body = section.replace(/^## .+\n?/, "").trim();
1391
+ if (body) {
1392
+ const lines = body.split("\n");
1393
+ const preview = lines.slice(0, 8).join("\n");
1394
+ const rest = lines.length > 8 ? chalk3.dim(`
1395
+ ... (${lines.length - 8} more lines)`) : "";
1396
+ console.log(colorFn(preview) + rest);
1397
+ }
1398
+ console.log();
1399
+ }
1400
+ 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"));
1401
+ }
1402
+ var MODEL_PRICING = {
1403
+ "claude-haiku-4-5-20251001": { input: 0.8, output: 4 },
1404
+ "claude-sonnet-4-6": { input: 3, output: 15 },
1405
+ "claude-opus-4-7": { input: 15, output: 75 }
1406
+ };
1407
+ function estimateCost(model, inputTokens, outputTokens) {
1408
+ const pricing = MODEL_PRICING[model] ?? MODEL_PRICING["claude-sonnet-4-6"];
1409
+ const cost = (inputTokens * pricing.input + outputTokens * pricing.output) / 1e6;
1410
+ return cost < 1e-3 ? "<0.001" : cost.toFixed(4);
1411
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xdevabir/enhance",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "AI prompt middleware — intelligently rewrites developer prompts before sending to coding agents",
5
5
  "bin": {
6
6
  "enhance": "./bin/enhance.js"