@0xdevabir/enhance 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -0
- package/bin/enhance.js +2 -0
- package/dist/index.js +918 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Enhance
|
|
2
|
+
|
|
3
|
+
> AI prompt middleware — upgrades vague developer prompts into production-quality instructions before sending to coding agents.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Instead of sending `build login page` directly to Claude/Codex/OpenCode:
|
|
8
|
+
|
|
9
|
+
1. Scans your project (stack, frameworks, folder structure)
|
|
10
|
+
2. Analyzes your intent
|
|
11
|
+
3. Finds relevant existing files for context
|
|
12
|
+
4. Rewrites the prompt with structure, best practices, and project awareness
|
|
13
|
+
5. Sends the enhanced prompt to the AI
|
|
14
|
+
|
|
15
|
+
**Same effort. Dramatically better output.**
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
### Prerequisites
|
|
22
|
+
|
|
23
|
+
- Node.js 20+
|
|
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
|
+
|
|
26
|
+
### Step 1 — Install globally
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @0xdevabir/enhance
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Step 2 — Run setup
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
enhance setup
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Detects which AI coding tools you have installed and automatically adds the `/enhance` slash command to each one.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Enhance — Setup
|
|
42
|
+
|
|
43
|
+
Detected: Claude Code, OpenCode
|
|
44
|
+
Not found: Codex CLI
|
|
45
|
+
|
|
46
|
+
✓ Claude Code → ~/.claude/commands/enhance.md
|
|
47
|
+
✓ OpenCode → ~/.opencode/commands/enhance.md
|
|
48
|
+
|
|
49
|
+
Ready. In any project, type:
|
|
50
|
+
|
|
51
|
+
/enhance build a login page
|
|
52
|
+
/enhance fix the auth bug
|
|
53
|
+
/enhance refactor the user service
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Options:**
|
|
57
|
+
```bash
|
|
58
|
+
enhance setup --force # overwrite existing installations
|
|
59
|
+
enhance setup --all # install for all tools even if not detected
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Step 3 — Use it
|
|
63
|
+
|
|
64
|
+
Open Claude Code, OpenCode, or Codex CLI in any project and type:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
/enhance build a login page with email and password
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### Inside Claude Code / OpenCode / Codex CLI (after `enhance setup`)
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
/enhance build a login page with email and password
|
|
78
|
+
/enhance fix the auth bug
|
|
79
|
+
/enhance create a payment form with Stripe
|
|
80
|
+
/enhance refactor the user service to use dependency injection
|
|
81
|
+
/enhance add dark mode support
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Standalone CLI (calls AI directly)
|
|
85
|
+
|
|
86
|
+
Requires API key:
|
|
87
|
+
```bash
|
|
88
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Basic — scans project, enhances prompt, calls Claude
|
|
93
|
+
enhance "build a login page"
|
|
94
|
+
|
|
95
|
+
# Dry run — see the enhanced prompt without calling AI
|
|
96
|
+
enhance "fix the auth bug" --dry-run
|
|
97
|
+
|
|
98
|
+
# Show enhanced prompt AND call AI
|
|
99
|
+
enhance "create dashboard component" --print-prompt
|
|
100
|
+
|
|
101
|
+
# See detected stack and intent
|
|
102
|
+
enhance "refactor user service" --verbose
|
|
103
|
+
|
|
104
|
+
# Use a different provider
|
|
105
|
+
enhance "add payment form" --provider codex
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Configuration
|
|
111
|
+
|
|
112
|
+
Create `.enhancerc.json` in your project root to customize behavior per-project:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"provider": "claude",
|
|
117
|
+
"model": "claude-sonnet-4-6",
|
|
118
|
+
"maxContextTokens": 3000,
|
|
119
|
+
"customInstructions": "Always use React Query. Prefer named exports."
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Key | Default | Description |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `provider` | `claude` | AI provider: `claude`, `codex`, `opencode` |
|
|
126
|
+
| `model` | `claude-sonnet-4-6` | Model ID |
|
|
127
|
+
| `maxContextTokens` | `3000` | Max characters of project files to include |
|
|
128
|
+
| `customInstructions` | — | Extra instructions always injected into every prompt |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Providers
|
|
133
|
+
|
|
134
|
+
### Claude (default)
|
|
135
|
+
|
|
136
|
+
Requires `ANTHROPIC_API_KEY`. Get one at [console.anthropic.com](https://console.anthropic.com/).
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
140
|
+
enhance "build login page"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Codex CLI
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm install -g @openai/codex
|
|
147
|
+
enhance "build login page" --provider codex
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### OpenCode
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npm install -g opencode
|
|
154
|
+
enhance "build login page" --provider opencode
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## What Enhance Detects
|
|
160
|
+
|
|
161
|
+
| Category | Detected |
|
|
162
|
+
|---|---|
|
|
163
|
+
| Frameworks | Next.js, React, Vue, Svelte, Nuxt, Remix, Astro, Express, Fastify, Hono, NestJS |
|
|
164
|
+
| Styling | TailwindCSS, shadcn/ui, Material UI, Chakra UI, Ant Design |
|
|
165
|
+
| ORMs | Prisma, Drizzle, TypeORM, Mongoose |
|
|
166
|
+
| Testing | Vitest, Jest, Playwright, Cypress |
|
|
167
|
+
| Language | TypeScript vs JavaScript |
|
|
168
|
+
| Router | Next.js App Router vs Pages Router |
|
|
169
|
+
| Package manager | npm, yarn, pnpm, bun |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Example Enhancement
|
|
174
|
+
|
|
175
|
+
**Input:**
|
|
176
|
+
```
|
|
177
|
+
enhance "build login page"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Enhanced prompt sent to AI:**
|
|
181
|
+
```
|
|
182
|
+
You are working inside a Next.js TypeScript project.
|
|
183
|
+
|
|
184
|
+
## Project Stack
|
|
185
|
+
- Language: TypeScript
|
|
186
|
+
- Frameworks: Next.js, React, TailwindCSS, shadcn/ui
|
|
187
|
+
- UI Library: shadcn/ui
|
|
188
|
+
- ORM: Prisma
|
|
189
|
+
- Next.js Router: App Router
|
|
190
|
+
|
|
191
|
+
## Task
|
|
192
|
+
Create: build login page
|
|
193
|
+
|
|
194
|
+
## Requirements
|
|
195
|
+
- Use TypeScript throughout — no `any`, prefer `unknown` with type guards
|
|
196
|
+
- Use Server Components by default — only add "use client" when needed
|
|
197
|
+
- Follow App Router conventions: page.tsx, layout.tsx, route.ts
|
|
198
|
+
- Use `next/navigation` for navigation
|
|
199
|
+
- Use Tailwind classes — no custom CSS unless unavoidable
|
|
200
|
+
- Use shadcn/ui components over building from scratch
|
|
201
|
+
- Never store plain text passwords — use bcrypt or argon2
|
|
202
|
+
- Use httpOnly, secure cookies for session tokens — never localStorage
|
|
203
|
+
- Validate and sanitize all auth inputs server-side
|
|
204
|
+
- Consider rate limiting on login/signup endpoints
|
|
205
|
+
- ...
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Development
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
git clone https://github.com/0xdevabir/agent-enhance.git
|
|
214
|
+
cd agent-enhance
|
|
215
|
+
npm install
|
|
216
|
+
|
|
217
|
+
# Run without building
|
|
218
|
+
npm run dev -- "build login page" --dry-run
|
|
219
|
+
|
|
220
|
+
# Run tests
|
|
221
|
+
npm test
|
|
222
|
+
|
|
223
|
+
# Build
|
|
224
|
+
npm run build
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT
|
package/bin/enhance.js
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
|
|
7
|
+
// src/cli/logger.ts
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
var error = (msg, err) => {
|
|
10
|
+
console.error(chalk.red(`\u2717 ${msg}`));
|
|
11
|
+
if (err instanceof Error && process.env["ENHANCE_DEBUG"]) {
|
|
12
|
+
console.error(chalk.red(err.stack));
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/cli/errors.ts
|
|
17
|
+
var EnhanceError = class extends Error {
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.name = "EnhanceError";
|
|
22
|
+
}
|
|
23
|
+
code;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/config/index.ts
|
|
27
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
28
|
+
var DEFAULT_CONFIG = {
|
|
29
|
+
provider: "claude",
|
|
30
|
+
model: "claude-sonnet-4-6",
|
|
31
|
+
maxContextTokens: 3e3
|
|
32
|
+
};
|
|
33
|
+
async function loadConfig(root) {
|
|
34
|
+
const explorer = cosmiconfig("enhance", {
|
|
35
|
+
searchPlaces: [
|
|
36
|
+
".enhancerc",
|
|
37
|
+
".enhancerc.json",
|
|
38
|
+
".enhancerc.yaml",
|
|
39
|
+
"enhance.config.js",
|
|
40
|
+
"enhance.config.ts",
|
|
41
|
+
"package.json"
|
|
42
|
+
]
|
|
43
|
+
});
|
|
44
|
+
let userConfig = {};
|
|
45
|
+
try {
|
|
46
|
+
const result = await explorer.search(root);
|
|
47
|
+
if (result?.config) {
|
|
48
|
+
userConfig = result.config;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
const merged = { ...DEFAULT_CONFIG, ...userConfig };
|
|
53
|
+
if (!merged.apiKey) {
|
|
54
|
+
merged.apiKey = process.env["ANTHROPIC_API_KEY"] ?? process.env["OPENAI_API_KEY"];
|
|
55
|
+
}
|
|
56
|
+
return merged;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/scanner/package.ts
|
|
60
|
+
import { readFile } from "fs/promises";
|
|
61
|
+
import { join } from "path";
|
|
62
|
+
async function readPackageJson(root) {
|
|
63
|
+
try {
|
|
64
|
+
const content = await readFile(join(root, "package.json"), "utf-8");
|
|
65
|
+
return JSON.parse(content);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/scanner/frameworks.ts
|
|
72
|
+
import { stat } from "fs/promises";
|
|
73
|
+
import { join as join2 } from "path";
|
|
74
|
+
async function detectStack(pkg, root) {
|
|
75
|
+
const deps = {
|
|
76
|
+
...pkg["dependencies"] ?? {},
|
|
77
|
+
...pkg["devDependencies"] ?? {}
|
|
78
|
+
};
|
|
79
|
+
const has = (name) => name in deps;
|
|
80
|
+
const frameworks = [];
|
|
81
|
+
if (has("next")) frameworks.push("nextjs");
|
|
82
|
+
if (has("react") && !frameworks.includes("nextjs")) frameworks.push("react");
|
|
83
|
+
if (has("vue")) frameworks.push("vue");
|
|
84
|
+
if (has("svelte") || has("@sveltejs/kit")) frameworks.push("svelte");
|
|
85
|
+
if (has("nuxt") || has("nuxt3")) frameworks.push("nuxt");
|
|
86
|
+
if (has("@remix-run/node") || has("@remix-run/react")) frameworks.push("remix");
|
|
87
|
+
if (has("astro")) frameworks.push("astro");
|
|
88
|
+
if (has("express")) frameworks.push("express");
|
|
89
|
+
if (has("fastify")) frameworks.push("fastify");
|
|
90
|
+
if (has("hono")) frameworks.push("hono");
|
|
91
|
+
if (has("@nestjs/core")) frameworks.push("nestjs");
|
|
92
|
+
if (has("tailwindcss")) frameworks.push("tailwind");
|
|
93
|
+
let uiLibrary;
|
|
94
|
+
if (has("@radix-ui/react-dialog") || has("shadcn") || has("@shadcn/ui")) {
|
|
95
|
+
frameworks.push("shadcn");
|
|
96
|
+
uiLibrary = "shadcn";
|
|
97
|
+
} else if (has("@mui/material")) {
|
|
98
|
+
frameworks.push("mui");
|
|
99
|
+
uiLibrary = "mui";
|
|
100
|
+
} else if (has("antd")) {
|
|
101
|
+
frameworks.push("antd");
|
|
102
|
+
uiLibrary = "antd";
|
|
103
|
+
} else if (has("@chakra-ui/react")) {
|
|
104
|
+
frameworks.push("chakra");
|
|
105
|
+
uiLibrary = "chakra";
|
|
106
|
+
}
|
|
107
|
+
let orm;
|
|
108
|
+
if (has("prisma") || has("@prisma/client")) orm = "prisma";
|
|
109
|
+
else if (has("drizzle-orm")) orm = "drizzle";
|
|
110
|
+
else if (has("typeorm")) orm = "typeorm";
|
|
111
|
+
else if (has("mongoose")) orm = "mongoose";
|
|
112
|
+
let testing;
|
|
113
|
+
if (has("vitest")) testing = "vitest";
|
|
114
|
+
else if (has("jest") || has("@jest/core")) testing = "jest";
|
|
115
|
+
else if (has("@playwright/test")) testing = "playwright";
|
|
116
|
+
else if (has("cypress")) testing = "cypress";
|
|
117
|
+
const language = has("typescript") ? "typescript" : "javascript";
|
|
118
|
+
const packageManager = await detectPackageManager(root);
|
|
119
|
+
let nextRouterType;
|
|
120
|
+
if (frameworks.includes("nextjs")) {
|
|
121
|
+
const hasApp = await pathExists(join2(root, "app")) || await pathExists(join2(root, "src", "app"));
|
|
122
|
+
const hasPages = await pathExists(join2(root, "pages")) || await pathExists(join2(root, "src", "pages"));
|
|
123
|
+
nextRouterType = hasApp ? "app" : hasPages ? "pages" : "app";
|
|
124
|
+
}
|
|
125
|
+
let runtime = "unknown";
|
|
126
|
+
if (frameworks.some((f) => ["express", "fastify", "hono", "nestjs"].includes(f))) runtime = "node";
|
|
127
|
+
else if (frameworks.some((f) => ["react", "vue", "svelte", "astro"].includes(f))) runtime = "browser";
|
|
128
|
+
else if (frameworks.includes("nextjs")) runtime = "browser";
|
|
129
|
+
return { language, frameworks, runtime, orm, testing, packageManager, nextRouterType, uiLibrary };
|
|
130
|
+
}
|
|
131
|
+
async function detectPackageManager(root) {
|
|
132
|
+
if (await pathExists(join2(root, "bun.lockb"))) return "bun";
|
|
133
|
+
if (await pathExists(join2(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
134
|
+
if (await pathExists(join2(root, "yarn.lock"))) return "yarn";
|
|
135
|
+
return "npm";
|
|
136
|
+
}
|
|
137
|
+
async function pathExists(p) {
|
|
138
|
+
try {
|
|
139
|
+
await stat(p);
|
|
140
|
+
return true;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/scanner/structure.ts
|
|
147
|
+
import { readdir, stat as stat2 } from "fs/promises";
|
|
148
|
+
import { join as join3 } from "path";
|
|
149
|
+
var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
150
|
+
"node_modules",
|
|
151
|
+
".git",
|
|
152
|
+
".next",
|
|
153
|
+
".nuxt",
|
|
154
|
+
"dist",
|
|
155
|
+
"build",
|
|
156
|
+
".cache",
|
|
157
|
+
"coverage",
|
|
158
|
+
".turbo",
|
|
159
|
+
"out",
|
|
160
|
+
".output"
|
|
161
|
+
]);
|
|
162
|
+
async function scanFolderStructure(root) {
|
|
163
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
164
|
+
const dirs = entries.filter((e) => e.isDirectory() && !EXCLUDE_DIRS.has(e.name)).map((e) => e.name);
|
|
165
|
+
const hasSrcDir = dirs.includes("src");
|
|
166
|
+
const hasAppDir = dirs.includes("app") || await dirExists(join3(root, "src", "app"));
|
|
167
|
+
const hasPagesDir = dirs.includes("pages") || await dirExists(join3(root, "src", "pages"));
|
|
168
|
+
let srcDirs = [];
|
|
169
|
+
if (hasSrcDir) {
|
|
170
|
+
try {
|
|
171
|
+
const srcEntries = await readdir(join3(root, "src"), { withFileTypes: true });
|
|
172
|
+
srcDirs = srcEntries.filter((e) => e.isDirectory() && !EXCLUDE_DIRS.has(e.name)).map((e) => e.name);
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { root, dirs, srcDirs, hasAppDir, hasPagesDir, hasSrcDir };
|
|
177
|
+
}
|
|
178
|
+
async function dirExists(path) {
|
|
179
|
+
try {
|
|
180
|
+
const s = await stat2(path);
|
|
181
|
+
return s.isDirectory();
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/scanner/tsconfig.ts
|
|
188
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
189
|
+
import { join as join4 } from "path";
|
|
190
|
+
async function readTsConfig(root) {
|
|
191
|
+
try {
|
|
192
|
+
const content = await readFile2(join4(root, "tsconfig.json"), "utf-8");
|
|
193
|
+
const parsed = JSON.parse(content);
|
|
194
|
+
return {
|
|
195
|
+
paths: parsed.compilerOptions?.paths,
|
|
196
|
+
baseUrl: parsed.compilerOptions?.baseUrl
|
|
197
|
+
};
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/scanner/index.ts
|
|
204
|
+
async function scanProject(root) {
|
|
205
|
+
const [pkg, structure] = await Promise.all([
|
|
206
|
+
readPackageJson(root),
|
|
207
|
+
scanFolderStructure(root)
|
|
208
|
+
]);
|
|
209
|
+
const safePkg = pkg ?? {};
|
|
210
|
+
const [stack] = await Promise.all([
|
|
211
|
+
detectStack(safePkg, root),
|
|
212
|
+
readTsConfig(root)
|
|
213
|
+
]);
|
|
214
|
+
return {
|
|
215
|
+
stack,
|
|
216
|
+
structure,
|
|
217
|
+
projectRoot: root,
|
|
218
|
+
scannedAt: Date.now()
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/cache/index.ts
|
|
223
|
+
import { readFile as readFile3, writeFile, stat as stat3 } from "fs/promises";
|
|
224
|
+
import { join as join5 } from "path";
|
|
225
|
+
var CACHE_FILE = ".enhance-cache.json";
|
|
226
|
+
async function getCached(root) {
|
|
227
|
+
const cachePath = join5(root, CACHE_FILE);
|
|
228
|
+
try {
|
|
229
|
+
const raw = await readFile3(cachePath, "utf-8");
|
|
230
|
+
const payload = JSON.parse(raw);
|
|
231
|
+
const pkgStat = await stat3(join5(root, "package.json"));
|
|
232
|
+
if (pkgStat.mtimeMs > payload.scannedAt) return null;
|
|
233
|
+
return payload.result;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function setCached(root, result) {
|
|
239
|
+
const cachePath = join5(root, CACHE_FILE);
|
|
240
|
+
const payload = { scannedAt: Date.now(), result };
|
|
241
|
+
await writeFile(cachePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/analyzer/intent.ts
|
|
245
|
+
var ACTION_KEYWORDS = {
|
|
246
|
+
create: ["build", "create", "make", "add", "implement", "write", "generate", "scaffold", "set up", "setup", "new"],
|
|
247
|
+
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"],
|
|
252
|
+
unknown: []
|
|
253
|
+
};
|
|
254
|
+
var ENTITY_KEYWORDS = {
|
|
255
|
+
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"],
|
|
257
|
+
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"],
|
|
262
|
+
unknown: []
|
|
263
|
+
};
|
|
264
|
+
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
|
+
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"]
|
|
276
|
+
};
|
|
277
|
+
function analyzeIntent(rawPrompt) {
|
|
278
|
+
const lower = rawPrompt.toLowerCase();
|
|
279
|
+
let action = "unknown";
|
|
280
|
+
for (const [act, keywords] of Object.entries(ACTION_KEYWORDS)) {
|
|
281
|
+
if (act === "unknown") continue;
|
|
282
|
+
if (keywords.some((k) => lower.includes(k))) {
|
|
283
|
+
action = act;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
let entity = "unknown";
|
|
288
|
+
for (const [ent, keywords] of Object.entries(ENTITY_KEYWORDS)) {
|
|
289
|
+
if (ent === "unknown") continue;
|
|
290
|
+
if (keywords.some((k) => lower.includes(k))) {
|
|
291
|
+
entity = ent;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
let feature = "general";
|
|
296
|
+
for (const [feat, keywords] of Object.entries(FEATURE_KEYWORDS)) {
|
|
297
|
+
if (keywords.some((k) => lower.includes(k))) {
|
|
298
|
+
feature = feat;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { action, entity, feature, rawPrompt };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/analyzer/context.ts
|
|
306
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
307
|
+
import { join as join6 } from "path";
|
|
308
|
+
import fg from "fast-glob";
|
|
309
|
+
var FEATURE_PATTERNS = {
|
|
310
|
+
auth: ["*auth*", "*login*", "*logout*", "*session*", "*jwt*", "*token*", "*credential*", "*middleware*", "*guard*"],
|
|
311
|
+
dashboard: ["*dashboard*", "*admin*", "*overview*", "*analytics*"],
|
|
312
|
+
payment: ["*payment*", "*checkout*", "*billing*", "*stripe*", "*invoice*"],
|
|
313
|
+
user: ["*user*", "*profile*", "*account*", "*avatar*"],
|
|
314
|
+
upload: ["*upload*", "*file*", "*storage*", "*media*"],
|
|
315
|
+
notification: ["*notification*", "*alert*", "*email*", "*toast*"],
|
|
316
|
+
navigation: ["*nav*", "*navbar*", "*sidebar*", "*header*", "*menu*"],
|
|
317
|
+
table: ["*table*", "*grid*", "*list*"],
|
|
318
|
+
form: ["*form*", "*field*", "*input*"],
|
|
319
|
+
search: ["*search*", "*filter*"]
|
|
320
|
+
};
|
|
321
|
+
var ALWAYS_INCLUDE_PATTERNS = [
|
|
322
|
+
"app/layout.tsx",
|
|
323
|
+
"app/layout.ts",
|
|
324
|
+
"src/app/layout.tsx",
|
|
325
|
+
"pages/_app.tsx",
|
|
326
|
+
"pages/_app.ts",
|
|
327
|
+
"src/pages/_app.tsx"
|
|
328
|
+
];
|
|
329
|
+
var MAX_CHARS = 12e3;
|
|
330
|
+
var MAX_FILE_LINES = 250;
|
|
331
|
+
async function findRelevantFiles(root, intent, structure) {
|
|
332
|
+
const sections = [];
|
|
333
|
+
let charBudget = MAX_CHARS;
|
|
334
|
+
for (const pattern of ALWAYS_INCLUDE_PATTERNS) {
|
|
335
|
+
const content = await tryRead(join6(root, pattern));
|
|
336
|
+
if (content) {
|
|
337
|
+
const snippet = truncateLines(content, MAX_FILE_LINES);
|
|
338
|
+
const section = `// FILE: ${pattern}
|
|
339
|
+
${snippet}`;
|
|
340
|
+
if (section.length < charBudget) {
|
|
341
|
+
sections.push(section);
|
|
342
|
+
charBudget -= section.length;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const featurePatterns = FEATURE_PATTERNS[intent.feature] ?? [];
|
|
347
|
+
if (featurePatterns.length > 0) {
|
|
348
|
+
const searchBase = structure.hasSrcDir ? join6(root, "src") : root;
|
|
349
|
+
const globs = featurePatterns.flatMap((p) => [
|
|
350
|
+
`**/${p}.{ts,tsx,js,jsx}`,
|
|
351
|
+
`**/${p}/{*.ts,*.tsx,*.js,*.jsx}`
|
|
352
|
+
]);
|
|
353
|
+
let files = [];
|
|
354
|
+
try {
|
|
355
|
+
files = await fg(globs, {
|
|
356
|
+
cwd: searchBase,
|
|
357
|
+
ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**"],
|
|
358
|
+
absolute: true,
|
|
359
|
+
onlyFiles: true
|
|
360
|
+
});
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
for (const file of files.slice(0, 5)) {
|
|
364
|
+
if (charBudget <= 0) break;
|
|
365
|
+
const content = await tryRead(file);
|
|
366
|
+
if (!content) continue;
|
|
367
|
+
const relPath = file.replace(root + "/", "");
|
|
368
|
+
const snippet = truncateLines(content, MAX_FILE_LINES);
|
|
369
|
+
const section = `// FILE: ${relPath}
|
|
370
|
+
${snippet}`;
|
|
371
|
+
if (section.length < charBudget) {
|
|
372
|
+
sections.push(section);
|
|
373
|
+
charBudget -= section.length;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (["component", "page"].includes(intent.entity) && charBudget > 1e3) {
|
|
378
|
+
const componentDir = structure.hasSrcDir ? join6(root, "src", "components") : join6(root, "components");
|
|
379
|
+
let sampleFiles = [];
|
|
380
|
+
try {
|
|
381
|
+
sampleFiles = await fg("**/*.{tsx,jsx}", {
|
|
382
|
+
cwd: componentDir,
|
|
383
|
+
ignore: ["**/node_modules/**"],
|
|
384
|
+
absolute: true,
|
|
385
|
+
onlyFiles: true,
|
|
386
|
+
deep: 2
|
|
387
|
+
});
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
for (const file of sampleFiles.slice(0, 2)) {
|
|
391
|
+
if (charBudget <= 500) break;
|
|
392
|
+
const content = await tryRead(file);
|
|
393
|
+
if (!content) continue;
|
|
394
|
+
const relPath = file.replace(root + "/", "");
|
|
395
|
+
const snippet = truncateLines(content, 80);
|
|
396
|
+
const section = `// FILE: ${relPath} (pattern reference)
|
|
397
|
+
${snippet}`;
|
|
398
|
+
if (section.length < charBudget) {
|
|
399
|
+
sections.push(section);
|
|
400
|
+
charBudget -= section.length;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return sections.join("\n\n");
|
|
405
|
+
}
|
|
406
|
+
async function tryRead(path) {
|
|
407
|
+
try {
|
|
408
|
+
return await readFile4(path, "utf-8");
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function truncateLines(content, maxLines) {
|
|
414
|
+
const lines = content.split("\n");
|
|
415
|
+
if (lines.length <= maxLines) return content;
|
|
416
|
+
return lines.slice(0, maxLines).join("\n") + `
|
|
417
|
+
// ... (truncated at ${maxLines} lines)`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/enhancer/injectors.ts
|
|
421
|
+
function getInjections(stack, intent) {
|
|
422
|
+
const rules = [];
|
|
423
|
+
if (stack.language === "typescript") {
|
|
424
|
+
rules.push("Use TypeScript throughout \u2014 no `any`, prefer `unknown` with type guards");
|
|
425
|
+
rules.push("Export all interfaces and types");
|
|
426
|
+
if (intent.action === "create" || intent.action === "add") {
|
|
427
|
+
rules.push("Add Zod validation at all external data boundaries (API input, form submissions)");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (stack.frameworks.includes("nextjs")) {
|
|
431
|
+
if (stack.nextRouterType === "app") {
|
|
432
|
+
rules.push('Use Server Components by default \u2014 only add "use client" when you need browser APIs or interactivity');
|
|
433
|
+
rules.push("Follow App Router conventions: page.tsx, layout.tsx, loading.tsx, error.tsx, route.ts");
|
|
434
|
+
rules.push("Use `next/navigation` (not `next/router`) for navigation in App Router");
|
|
435
|
+
rules.push("Prefer server actions for mutations over API route handlers when possible");
|
|
436
|
+
} else if (stack.nextRouterType === "pages") {
|
|
437
|
+
rules.push("Use `next/router` for navigation");
|
|
438
|
+
rules.push("Keep data fetching in `getServerSideProps` or `getStaticProps`");
|
|
439
|
+
}
|
|
440
|
+
rules.push("Never expose secrets in client-side code \u2014 use environment variables with NEXT_PUBLIC_ prefix only for public values");
|
|
441
|
+
}
|
|
442
|
+
if (stack.frameworks.includes("react") || stack.frameworks.includes("nextjs")) {
|
|
443
|
+
rules.push("Keep components small and focused on a single responsibility");
|
|
444
|
+
rules.push("Extract reusable logic into custom hooks");
|
|
445
|
+
rules.push("Use composition over prop drilling for shared state");
|
|
446
|
+
if (intent.entity === "component") {
|
|
447
|
+
rules.push("Make the component accessible \u2014 add aria labels, keyboard navigation, focus management");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (stack.frameworks.includes("tailwind")) {
|
|
451
|
+
rules.push("Use Tailwind classes \u2014 no custom CSS unless unavoidable");
|
|
452
|
+
rules.push("Design mobile-first: base styles for mobile, `md:` / `lg:` for larger screens");
|
|
453
|
+
}
|
|
454
|
+
if (stack.frameworks.includes("shadcn")) {
|
|
455
|
+
rules.push("Use shadcn/ui components (Button, Input, Dialog, etc.) over building from scratch");
|
|
456
|
+
rules.push("Follow shadcn component patterns: import from `@/components/ui/`");
|
|
457
|
+
}
|
|
458
|
+
if (stack.orm === "prisma") {
|
|
459
|
+
rules.push("Use Prisma client for all database operations \u2014 no raw SQL unless required");
|
|
460
|
+
rules.push("Always wrap multi-step DB operations in a transaction");
|
|
461
|
+
} else if (stack.orm === "drizzle") {
|
|
462
|
+
rules.push("Use Drizzle schema and query builder \u2014 avoid raw SQL");
|
|
463
|
+
}
|
|
464
|
+
if (intent.feature === "auth") {
|
|
465
|
+
rules.push("Never store plain text passwords \u2014 use bcrypt or argon2");
|
|
466
|
+
rules.push("Use httpOnly, secure cookies for session tokens \u2014 never localStorage");
|
|
467
|
+
rules.push("Validate and sanitize all auth inputs server-side");
|
|
468
|
+
rules.push("Consider rate limiting on login/signup endpoints");
|
|
469
|
+
rules.push("Implement CSRF protection for state-changing auth operations");
|
|
470
|
+
}
|
|
471
|
+
if (intent.feature === "payment") {
|
|
472
|
+
rules.push("Never log or store raw card data \u2014 let Stripe/payment processor handle it");
|
|
473
|
+
rules.push("Always verify webhook signatures server-side");
|
|
474
|
+
rules.push("Use idempotency keys for payment operations");
|
|
475
|
+
}
|
|
476
|
+
if (intent.feature === "upload") {
|
|
477
|
+
rules.push("Validate file type and size server-side, not just client-side");
|
|
478
|
+
rules.push("Sanitize filenames before storage");
|
|
479
|
+
rules.push("Use presigned URLs for direct-to-storage uploads");
|
|
480
|
+
}
|
|
481
|
+
if (intent.entity === "api") {
|
|
482
|
+
rules.push("Return consistent response shape: `{ data, error, status }`");
|
|
483
|
+
rules.push("Handle errors explicitly \u2014 never let unhandled rejections leak to the client");
|
|
484
|
+
rules.push("Validate all input with Zod before processing");
|
|
485
|
+
rules.push("Use appropriate HTTP status codes");
|
|
486
|
+
}
|
|
487
|
+
rules.push("Match the existing code style and file naming conventions");
|
|
488
|
+
rules.push("List every file you create or modify at the end of your response");
|
|
489
|
+
rules.push("Prefer reusing existing utilities over creating new ones");
|
|
490
|
+
return rules;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/enhancer/template.ts
|
|
494
|
+
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)}
|
|
508
|
+
|
|
509
|
+
## Project Structure
|
|
510
|
+
${structureLabel}
|
|
511
|
+
|
|
512
|
+
## Task
|
|
513
|
+
${taskDescription}
|
|
514
|
+
|
|
515
|
+
## Requirements
|
|
516
|
+
${requirementsList}
|
|
517
|
+
${contextSection}
|
|
518
|
+
|
|
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(", ")}`);
|
|
532
|
+
}
|
|
533
|
+
if (stack.uiLibrary) {
|
|
534
|
+
lines.push(`- **UI Library**: ${capitalize(stack.uiLibrary)}`);
|
|
535
|
+
}
|
|
536
|
+
if (stack.orm) {
|
|
537
|
+
lines.push(`- **ORM**: ${capitalize(stack.orm)}`);
|
|
538
|
+
}
|
|
539
|
+
if (stack.testing) {
|
|
540
|
+
lines.push(`- **Testing**: ${capitalize(stack.testing)}`);
|
|
541
|
+
}
|
|
542
|
+
if (stack.nextRouterType) {
|
|
543
|
+
lines.push(`- **Next.js Router**: ${stack.nextRouterType === "app" ? "App Router" : "Pages Router"}`);
|
|
544
|
+
}
|
|
545
|
+
lines.push(`- **Package Manager**: ${stack.packageManager}`);
|
|
546
|
+
return lines.join("\n");
|
|
547
|
+
}
|
|
548
|
+
function formatStructure(structure) {
|
|
549
|
+
const lines = [];
|
|
550
|
+
if (structure.dirs.length > 0) {
|
|
551
|
+
lines.push(`Top-level directories: \`${structure.dirs.join("/")}\``);
|
|
552
|
+
}
|
|
553
|
+
if (structure.hasSrcDir && structure.srcDirs.length > 0) {
|
|
554
|
+
lines.push(`Inside src/: \`${structure.srcDirs.join("/")}\``);
|
|
555
|
+
}
|
|
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
|
+
}
|
|
560
|
+
function buildTaskDescription(intent) {
|
|
561
|
+
const actionMap = {
|
|
562
|
+
create: "Create",
|
|
563
|
+
fix: "Fix",
|
|
564
|
+
refactor: "Refactor",
|
|
565
|
+
explain: "Explain",
|
|
566
|
+
add: "Add",
|
|
567
|
+
delete: "Remove",
|
|
568
|
+
unknown: "Handle"
|
|
569
|
+
};
|
|
570
|
+
const action = actionMap[intent.action] ?? "Handle";
|
|
571
|
+
return `${action}: ${intent.rawPrompt}`;
|
|
572
|
+
}
|
|
573
|
+
function capitalize(s) {
|
|
574
|
+
if (!s) return s;
|
|
575
|
+
const map = {
|
|
576
|
+
nextjs: "Next.js",
|
|
577
|
+
tailwind: "TailwindCSS",
|
|
578
|
+
shadcn: "shadcn/ui",
|
|
579
|
+
prisma: "Prisma",
|
|
580
|
+
drizzle: "Drizzle ORM",
|
|
581
|
+
nestjs: "NestJS",
|
|
582
|
+
typescript: "TypeScript",
|
|
583
|
+
javascript: "JavaScript",
|
|
584
|
+
vitest: "Vitest",
|
|
585
|
+
mui: "Material UI",
|
|
586
|
+
chakra: "Chakra UI"
|
|
587
|
+
};
|
|
588
|
+
return map[s] ?? s.charAt(0).toUpperCase() + s.slice(1);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/enhancer/index.ts
|
|
592
|
+
function enhance(rawPrompt, scan, contextFiles) {
|
|
593
|
+
const intent = analyzeIntent(rawPrompt);
|
|
594
|
+
const injections = getInjections(scan.stack, intent);
|
|
595
|
+
const enhanced = buildEnhancedPrompt({
|
|
596
|
+
stack: scan.stack,
|
|
597
|
+
structure: scan.structure,
|
|
598
|
+
intent,
|
|
599
|
+
contextFiles,
|
|
600
|
+
injections
|
|
601
|
+
});
|
|
602
|
+
return { original: rawPrompt, enhanced, intent, stack: scan.stack };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/providers/claude.ts
|
|
606
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
607
|
+
var ClaudeProvider = class {
|
|
608
|
+
name = "Claude";
|
|
609
|
+
client;
|
|
610
|
+
model;
|
|
611
|
+
constructor(config) {
|
|
612
|
+
if (!config.apiKey) {
|
|
613
|
+
throw new EnhanceError(
|
|
614
|
+
"MISSING_API_KEY",
|
|
615
|
+
"ANTHROPIC_API_KEY environment variable is not set."
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
this.client = new Anthropic({ apiKey: config.apiKey });
|
|
619
|
+
this.model = config.model;
|
|
620
|
+
}
|
|
621
|
+
async send(prompt) {
|
|
622
|
+
const stream = await this.client.messages.create({
|
|
623
|
+
model: this.model,
|
|
624
|
+
max_tokens: 8192,
|
|
625
|
+
system: "You are an expert software engineer. Follow instructions precisely. Write clean, idiomatic, production-ready code.",
|
|
626
|
+
messages: [{ role: "user", content: prompt }],
|
|
627
|
+
stream: true
|
|
628
|
+
});
|
|
629
|
+
for await (const event of stream) {
|
|
630
|
+
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
631
|
+
process.stdout.write(event.delta.text);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
process.stdout.write("\n");
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// src/providers/codex.ts
|
|
639
|
+
import { spawn } from "child_process";
|
|
640
|
+
var CodexProvider = class {
|
|
641
|
+
name = "Codex";
|
|
642
|
+
async send(prompt) {
|
|
643
|
+
await runSubprocess("codex", [prompt]);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
var OpenCodeProvider = class {
|
|
647
|
+
name = "OpenCode";
|
|
648
|
+
async send(prompt) {
|
|
649
|
+
await runSubprocess("opencode", ["run", prompt]);
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
function runSubprocess(cmd, args) {
|
|
653
|
+
return new Promise((resolve2, reject) => {
|
|
654
|
+
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
655
|
+
child.on("error", (err) => {
|
|
656
|
+
if (err.code === "ENOENT") {
|
|
657
|
+
reject(new EnhanceError(
|
|
658
|
+
"PROVIDER_NOT_FOUND",
|
|
659
|
+
`Provider "${cmd}" is not installed or not in PATH. Install it first.`
|
|
660
|
+
));
|
|
661
|
+
} else {
|
|
662
|
+
reject(err);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
child.on("close", (code) => {
|
|
666
|
+
if (code === 0 || code === null) resolve2();
|
|
667
|
+
else reject(new EnhanceError("PROVIDER_ERROR", `${cmd} exited with code ${code}`));
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/providers/index.ts
|
|
673
|
+
function getProvider(config) {
|
|
674
|
+
switch (config.provider) {
|
|
675
|
+
case "claude":
|
|
676
|
+
return new ClaudeProvider(config);
|
|
677
|
+
case "codex":
|
|
678
|
+
return new CodexProvider();
|
|
679
|
+
case "opencode":
|
|
680
|
+
return new OpenCodeProvider();
|
|
681
|
+
default:
|
|
682
|
+
throw new EnhanceError(
|
|
683
|
+
"UNKNOWN_PROVIDER",
|
|
684
|
+
`Unknown provider "${config.provider}". Use: claude, codex, opencode`
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/cli/setup.ts
|
|
690
|
+
import { access, mkdir, writeFile as writeFile2 } from "fs/promises";
|
|
691
|
+
import { join as join7 } from "path";
|
|
692
|
+
import { homedir } from "os";
|
|
693
|
+
import { execSync } from "child_process";
|
|
694
|
+
import chalk2 from "chalk";
|
|
695
|
+
var TOOLS = [
|
|
696
|
+
{
|
|
697
|
+
name: "Claude Code",
|
|
698
|
+
cliCommand: "claude",
|
|
699
|
+
commandsDir: join7(homedir(), ".claude", "commands"),
|
|
700
|
+
commandFile: "enhance.md"
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
name: "OpenCode",
|
|
704
|
+
cliCommand: "opencode",
|
|
705
|
+
commandsDir: join7(homedir(), ".opencode", "commands"),
|
|
706
|
+
commandFile: "enhance.md"
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
name: "Codex CLI",
|
|
710
|
+
cliCommand: "codex",
|
|
711
|
+
commandsDir: join7(homedir(), ".codex", "commands"),
|
|
712
|
+
commandFile: "enhance.md"
|
|
713
|
+
}
|
|
714
|
+
];
|
|
715
|
+
var ENHANCE_COMMAND_TEMPLATE = `Your job is to rewrite the following messy/vague prompt into a structured, detailed, production-quality prompt that will get dramatically better results from any AI coding agent.
|
|
716
|
+
|
|
717
|
+
**Original prompt:** $ARGUMENTS
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
**Step 1 \u2014 Scan the project silently:**
|
|
722
|
+
- Read \`package.json\` \u2192 detect frameworks, language, ORM, testing tools, UI library
|
|
723
|
+
- Check for \`tsconfig.json\` \u2192 note path aliases
|
|
724
|
+
- Scan folder structure \u2192 note \`app/\` vs \`pages/\` (Next.js router), \`src/\`, \`components/\`, \`lib/\`, \`hooks/\`
|
|
725
|
+
- Read 1-2 existing files most relevant to the prompt topic (for pattern/convention reference)
|
|
726
|
+
|
|
727
|
+
**Step 2 \u2014 Understand the intent:**
|
|
728
|
+
- What is the user actually trying to build/fix/refactor?
|
|
729
|
+
- What entity is involved? (page, component, API, hook, util)
|
|
730
|
+
- What feature domain? (auth, payment, dashboard, upload, etc.)
|
|
731
|
+
|
|
732
|
+
**Step 3 \u2014 Output the enhanced prompt in this exact format:**
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
## \u2726 Enhanced Prompt
|
|
736
|
+
|
|
737
|
+
**Context:**
|
|
738
|
+
[Project stack, folder conventions, existing patterns found]
|
|
739
|
+
|
|
740
|
+
**Task:**
|
|
741
|
+
[Rewrite the original request as a clear, specific, unambiguous instruction]
|
|
742
|
+
|
|
743
|
+
**Requirements:**
|
|
744
|
+
[8-15 specific requirements based on detected stack + task domain:]
|
|
745
|
+
- Language/type safety (TypeScript, Zod, no \`any\`)
|
|
746
|
+
- Framework rules (App Router, server components, next/navigation, etc.)
|
|
747
|
+
- UI/styling (Tailwind, shadcn components)
|
|
748
|
+
- Security if relevant (auth \u2192 bcrypt, httpOnly; API \u2192 input validation)
|
|
749
|
+
- Code quality (match existing conventions, reuse utils, no unnecessary deps)
|
|
750
|
+
- Output rule: list every file created/modified at the end
|
|
751
|
+
|
|
752
|
+
**Relevant existing code:**
|
|
753
|
+
[1-2 file snippets for reference, or "no relevant files found"]
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
**Step 4 \u2014 Ask the user:**
|
|
758
|
+
> Does this look right? Reply **yes** to execute, **no** to refine, or edit any part of the prompt above.
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
**Step 5 \u2014 Handle the response:**
|
|
763
|
+
|
|
764
|
+
If the user says **yes** \u2192 execute the enhanced prompt exactly as written above.
|
|
765
|
+
|
|
766
|
+
If the user says **no** or gives feedback \u2192 do all of the following:
|
|
767
|
+
1. Ask: "What's missing or wrong? (e.g. wrong framework assumed, missing requirement, task unclear)"
|
|
768
|
+
2. Wait for their answer
|
|
769
|
+
3. Revise the enhanced prompt incorporating their feedback
|
|
770
|
+
4. Show the updated prompt in the same format above
|
|
771
|
+
5. Ask again: "Better? Reply yes to execute or no to keep refining."
|
|
772
|
+
6. Repeat this loop until the user says yes.
|
|
773
|
+
|
|
774
|
+
If the user edits specific parts of the prompt \u2192 accept their edits, merge with the enhanced version, confirm, then execute.
|
|
775
|
+
|
|
776
|
+
The goal: never execute until the user is satisfied with the enhanced prompt.
|
|
777
|
+
`;
|
|
778
|
+
function isInstalled(cmd) {
|
|
779
|
+
try {
|
|
780
|
+
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
781
|
+
return true;
|
|
782
|
+
} catch {
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async function installForTool(tool) {
|
|
787
|
+
try {
|
|
788
|
+
await mkdir(tool.commandsDir, { recursive: true });
|
|
789
|
+
const dest = join7(tool.commandsDir, tool.commandFile);
|
|
790
|
+
await writeFile2(dest, ENHANCE_COMMAND_TEMPLATE, "utf-8");
|
|
791
|
+
return "installed";
|
|
792
|
+
} catch {
|
|
793
|
+
return "error";
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async function runSetup(opts) {
|
|
797
|
+
console.log(chalk2.bold("\nEnhance \u2014 Setup\n"));
|
|
798
|
+
const detected = [];
|
|
799
|
+
const notFound = [];
|
|
800
|
+
for (const tool of TOOLS) {
|
|
801
|
+
if (opts.all || isInstalled(tool.cliCommand)) {
|
|
802
|
+
detected.push(tool);
|
|
803
|
+
} else {
|
|
804
|
+
notFound.push(tool);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (detected.length === 0) {
|
|
808
|
+
console.log(chalk2.yellow("No supported AI coding tools detected in PATH.\n"));
|
|
809
|
+
console.log("Supported tools:", TOOLS.map((t) => t.cliCommand).join(", "));
|
|
810
|
+
console.log("\nTo install for all tools anyway:");
|
|
811
|
+
console.log(chalk2.gray(" enhance setup --all\n"));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
console.log(chalk2.gray("Detected:"), detected.map((t) => t.name).join(", "));
|
|
815
|
+
if (notFound.length > 0) {
|
|
816
|
+
console.log(chalk2.gray("Not found:"), notFound.map((t) => t.name).join(", "));
|
|
817
|
+
}
|
|
818
|
+
console.log();
|
|
819
|
+
let anyInstalled = false;
|
|
820
|
+
for (const tool of detected) {
|
|
821
|
+
const dest = join7(tool.commandsDir, tool.commandFile);
|
|
822
|
+
const exists = await fileExists(dest);
|
|
823
|
+
if (exists && !opts.force) {
|
|
824
|
+
console.log(chalk2.yellow(`\u26A0 ${tool.name}`), chalk2.gray(`already installed \u2014 use --force to overwrite`));
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const result = await installForTool(tool);
|
|
828
|
+
if (result === "installed") {
|
|
829
|
+
console.log(chalk2.green(`\u2713 ${tool.name}`), chalk2.gray(`\u2192 ${dest}`));
|
|
830
|
+
anyInstalled = true;
|
|
831
|
+
} else {
|
|
832
|
+
console.log(chalk2.red(`\u2717 ${tool.name}`), chalk2.gray(`failed to write to ${dest}`));
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (anyInstalled || detected.every((t) => fileExists(join7(t.commandsDir, t.commandFile)))) {
|
|
836
|
+
console.log(chalk2.bold("\nReady. In any project, type:\n"));
|
|
837
|
+
console.log(chalk2.cyan(" /enhance build a login page"));
|
|
838
|
+
console.log(chalk2.cyan(" /enhance fix the auth bug"));
|
|
839
|
+
console.log(chalk2.cyan(" /enhance refactor the user service\n"));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async function fileExists(path) {
|
|
843
|
+
try {
|
|
844
|
+
await access(path);
|
|
845
|
+
return true;
|
|
846
|
+
} catch {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/cli/index.ts
|
|
852
|
+
var program = new Command();
|
|
853
|
+
program.name("enhance").description("AI prompt middleware \u2014 upgrades vague prompts into production-quality AI instructions").version("0.1.0");
|
|
854
|
+
program.command("setup").description("Install the /enhance slash command for Claude Code, OpenCode, and Codex CLI").option("--force", "Overwrite existing installations").option("--all", "Install for all supported tools even if not detected in PATH").action(async (opts) => {
|
|
855
|
+
await runSetup(opts);
|
|
856
|
+
});
|
|
857
|
+
program.command("<prompt>", { isDefault: true }).description("Enhance a prompt and send to AI").addHelpText("after", `
|
|
858
|
+
Examples:
|
|
859
|
+
enhance "build a login page"
|
|
860
|
+
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) => {
|
|
863
|
+
if (!rawPrompt) {
|
|
864
|
+
program.help();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const cwd = process.cwd();
|
|
868
|
+
const spinner = ora();
|
|
869
|
+
try {
|
|
870
|
+
spinner.start("Loading config...");
|
|
871
|
+
const config = await loadConfig(cwd);
|
|
872
|
+
if (opts.provider) config.provider = opts.provider;
|
|
873
|
+
spinner.text = "Scanning project...";
|
|
874
|
+
let scan = await getCached(cwd);
|
|
875
|
+
if (!scan) {
|
|
876
|
+
scan = await scanProject(cwd);
|
|
877
|
+
await setCached(cwd, scan);
|
|
878
|
+
}
|
|
879
|
+
spinner.text = "Analyzing intent...";
|
|
880
|
+
const intent = analyzeIntent(rawPrompt);
|
|
881
|
+
if (opts.verbose) {
|
|
882
|
+
spinner.stop();
|
|
883
|
+
console.log("\nStack detected:", JSON.stringify(scan.stack, null, 2));
|
|
884
|
+
console.log("\nIntent:", JSON.stringify(intent, null, 2));
|
|
885
|
+
spinner.start("Optimizing context...");
|
|
886
|
+
}
|
|
887
|
+
spinner.text = "Optimizing context...";
|
|
888
|
+
const contextFiles = await findRelevantFiles(cwd, intent, scan.structure);
|
|
889
|
+
spinner.text = "Enhancing prompt...";
|
|
890
|
+
const enhanced = enhance(rawPrompt, scan, contextFiles);
|
|
891
|
+
spinner.stop();
|
|
892
|
+
if (opts.dryRun || opts.printPrompt) {
|
|
893
|
+
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
|
+
console.log(enhanced.enhanced);
|
|
895
|
+
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");
|
|
896
|
+
}
|
|
897
|
+
if (opts.dryRun) {
|
|
898
|
+
process.exit(0);
|
|
899
|
+
}
|
|
900
|
+
const provider = getProvider(config);
|
|
901
|
+
console.log(`
|
|
902
|
+
[${provider.name}]
|
|
903
|
+
`);
|
|
904
|
+
await provider.send(enhanced.enhanced);
|
|
905
|
+
} catch (err) {
|
|
906
|
+
spinner.stop();
|
|
907
|
+
if (err instanceof EnhanceError) {
|
|
908
|
+
error(err.message);
|
|
909
|
+
if (err.code === "MISSING_API_KEY") {
|
|
910
|
+
console.error("\nSet your API key: export ANTHROPIC_API_KEY=sk-...\n");
|
|
911
|
+
}
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
error("Unexpected error", err);
|
|
915
|
+
process.exit(1);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@0xdevabir/enhance",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI prompt middleware — intelligently rewrites developer prompts before sending to coding agents",
|
|
5
|
+
"bin": {
|
|
6
|
+
"enhance": "./bin/enhance.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "tsx src/cli/index.ts",
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"start": "node bin/enhance.js",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"prepublishOnly": "npm run build && npm test"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/0xdevabir/agent-enhance.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"ai",
|
|
26
|
+
"cli",
|
|
27
|
+
"prompt",
|
|
28
|
+
"developer-tools",
|
|
29
|
+
"claude",
|
|
30
|
+
"codex",
|
|
31
|
+
"opencode",
|
|
32
|
+
"prompt-engineering"
|
|
33
|
+
],
|
|
34
|
+
"author": "0xdevabir",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"type": "module",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/0xdevabir/agent-enhance/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/0xdevabir/agent-enhance#readme",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@anthropic-ai/sdk": "^0.98.0",
|
|
46
|
+
"chalk": "^5.6.2",
|
|
47
|
+
"commander": "^14.0.3",
|
|
48
|
+
"cosmiconfig": "^9.0.1",
|
|
49
|
+
"fast-glob": "^3.3.3",
|
|
50
|
+
"ora": "^9.4.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^25.9.1",
|
|
54
|
+
"tsup": "^8.5.1",
|
|
55
|
+
"tsx": "^4.22.3",
|
|
56
|
+
"typescript": "^6.0.3",
|
|
57
|
+
"vitest": "^4.1.7"
|
|
58
|
+
}
|
|
59
|
+
}
|