codebrief 1.1.9 → 1.1.10

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/ai.js +216 -69
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebrief",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "Generate AI context files for your project in seconds",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/ai.js CHANGED
@@ -11,31 +11,57 @@ const IMPORTANT_PATTERNS = [
11
11
  "package.json",
12
12
  "tsconfig.json",
13
13
  "next.config.*",
14
+ "nuxt.config.*",
14
15
  "vite.config.*",
16
+ "webpack.config.*",
15
17
  "tailwind.config.*",
18
+ "postcss.config.*",
16
19
  "prisma/schema.prisma",
17
20
  "drizzle.config.*",
18
- "README.md",
21
+ ".env.example",
22
+ "docker-compose.*",
23
+ "Dockerfile",
19
24
  "src/index.*",
20
25
  "src/main.*",
21
26
  "src/app.*",
22
27
  "src/server.*",
28
+ "src/config.*",
29
+ "src/routes.*",
30
+ "src/middleware.*",
23
31
  "lib/db.*",
24
32
  "lib/auth.*",
33
+ "lib/utils.*",
25
34
  "app/layout.*",
26
35
  "app/page.*",
36
+ "app/api/**/route.*",
37
+ "pages/_app.*",
38
+ "pages/index.*",
39
+ "server/index.*",
40
+ "server/api/**",
41
+ "controllers/*",
42
+ "models/*",
43
+ "services/*",
27
44
  ];
28
45
 
29
- function sampleSourceFiles(rootDir, fileTree, charBudget = 18000) {
46
+ function sampleSourceFiles(rootDir, fileTree, charBudget = 32000) {
30
47
  const samples = [];
31
48
  let budget = charBudget;
32
49
 
33
- // First pass: prioritised files
50
+ // Smart read: for large files, take head + tail to capture imports AND exports/env vars
51
+ function smartRead(filePath, maxChars) {
52
+ const raw = fs.readFileSync(filePath, "utf-8");
53
+ if (raw.length <= maxChars) return raw;
54
+ const head = Math.floor(maxChars * 0.6);
55
+ const tail = maxChars - head - 20; // 20 for separator
56
+ return raw.slice(0, head) + "\n// ... (truncated) ...\n" + raw.slice(-tail);
57
+ }
58
+
59
+ // First pass: prioritised files (generous budget — these are the most important)
34
60
  for (const pattern of IMPORTANT_PATTERNS) {
35
61
  const full = path.join(rootDir, pattern);
36
62
  if (fs.existsSync(full)) {
37
63
  try {
38
- const content = fs.readFileSync(full, "utf-8").slice(0, 3000);
64
+ const content = smartRead(full, 6000);
39
65
  samples.push({ file: pattern, content });
40
66
  budget -= content.length;
41
67
  if (budget <= 0) break;
@@ -68,18 +94,17 @@ function sampleSourceFiles(rootDir, fileTree, charBudget = 18000) {
68
94
  const ext = path.extname(entry.name).toLowerCase();
69
95
  if (!sourceExts.has(ext)) continue;
70
96
 
71
- const full = path.join(rootDir, entry.relativePath || entry.name);
97
+ const full = path.join(rootDir, entry.path || entry.name);
72
98
  // Skip already-read files
73
99
  if (samples.some((s) => full.endsWith(s.file))) continue;
74
100
 
75
101
  try {
76
- const raw = fs.readFileSync(full, "utf-8");
77
- const snippet = raw.slice(0, 1500);
102
+ const content = smartRead(full, 4000);
78
103
  samples.push({
79
- file: entry.relativePath || entry.name,
80
- content: snippet,
104
+ file: entry.path || entry.name,
105
+ content,
81
106
  });
82
- budget -= snippet.length;
107
+ budget -= content.length;
83
108
  } catch {
84
109
  /* skip */
85
110
  }
@@ -88,14 +113,80 @@ function sampleSourceFiles(rootDir, fileTree, charBudget = 18000) {
88
113
  return samples;
89
114
  }
90
115
 
116
+ // ── Env var extractor ────────────────────────────────────────
117
+ // Scans all source files for process.env.XXX references
118
+ function extractEnvVars(rootDir, fileTree) {
119
+ const envVars = new Map(); // name → [files]
120
+ const sourceExts = new Set([".js", ".ts", ".jsx", ".tsx", ".vue", ".svelte", ".py", ".go", ".rs", ".rb", ".php"]);
121
+ const envRegex = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
122
+ const dotenvRegex = /^([A-Z_][A-Z0-9_]*)=/gm;
123
+
124
+ for (const entry of fileTree) {
125
+ if (entry.type !== "file") continue;
126
+ const ext = path.extname(entry.name).toLowerCase();
127
+ const name = entry.name.toLowerCase();
128
+ if (!sourceExts.has(ext) && !name.startsWith(".env")) continue;
129
+
130
+ const full = path.join(rootDir, entry.path || entry.name);
131
+ try {
132
+ const raw = fs.readFileSync(full, "utf-8");
133
+ const regex = name.startsWith(".env") ? dotenvRegex : envRegex;
134
+ let match;
135
+ while ((match = regex.exec(raw)) !== null) {
136
+ const varName = match[1];
137
+ // Skip generic placeholder names
138
+ if (varName === "XXX" || varName.length < 3) continue;
139
+ if (!envVars.has(varName)) envVars.set(varName, []);
140
+ const file = entry.path || entry.name;
141
+ if (!envVars.get(varName).includes(file)) envVars.get(varName).push(file);
142
+ }
143
+ } catch { /* skip */ }
144
+ }
145
+
146
+ return envVars;
147
+ }
148
+
149
+ // ── Export extractor ─────────────────────────────────────────
150
+ // Scans all source files for key function/class exports
151
+ function extractExports(rootDir, fileTree) {
152
+ const exports = [];
153
+ const sourceExts = new Set([".js", ".ts", ".jsx", ".tsx"]);
154
+ const patterns = [
155
+ /(?:module\.exports\s*=\s*\{([^}]+)\})/g,
156
+ /(?:exports\.(\w+)\s*=)/g,
157
+ /(?:export\s+(?:default\s+)?(?:function|class|const|let|var)\s+(\w+))/g,
158
+ ];
159
+
160
+ for (const entry of fileTree) {
161
+ if (entry.type !== "file") continue;
162
+ const ext = path.extname(entry.name).toLowerCase();
163
+ if (!sourceExts.has(ext)) continue;
164
+
165
+ const full = path.join(rootDir, entry.path || entry.name);
166
+ try {
167
+ const raw = fs.readFileSync(full, "utf-8");
168
+ const file = entry.path || entry.name;
169
+ for (const regex of patterns) {
170
+ let match;
171
+ regex.lastIndex = 0;
172
+ while ((match = regex.exec(raw)) !== null) {
173
+ exports.push({ file, exports: match[1] || match[0] });
174
+ }
175
+ }
176
+ } catch { /* skip */ }
177
+ }
178
+
179
+ return exports;
180
+ }
181
+
91
182
  // ── Prompt builder ───────────────────────────────────────────
92
- function buildPrompt(analysis, fileTree, fileSamples) {
183
+ function buildPrompt(analysis, fileTree, fileSamples, envVars, fileExports) {
93
184
  const fileList = fileTree
94
- .slice(0, 80)
185
+ .slice(0, 120)
95
186
  .map((e) =>
96
187
  e.type === "dir"
97
- ? ` ${e.relativePath || e.name}/`
98
- : ` ${e.relativePath || e.name}`,
188
+ ? ` ${e.path || e.name}/`
189
+ : ` ${e.path || e.name}`,
99
190
  )
100
191
  .join("\n");
101
192
 
@@ -108,41 +199,71 @@ function buildPrompt(analysis, fileTree, fileSamples) {
108
199
  .map(([k, v]) => ` ${k}: ${v}`)
109
200
  .join("\n") || " (none)";
110
201
 
111
- return `You are a senior software architect performing a deep code review. A developer ran \`codebrief\` on their project. Your job is to write a CONTEXT.md that gives an AI coding assistant (Cursor, Copilot, etc.) enough knowledge to contribute code as if it had written the project itself.
202
+ const envVarsText = envVars.size > 0
203
+ ? Array.from(envVars.entries())
204
+ .map(([name, files]) => `- \`${name}\` — used in ${files.map((f) => "`" + f + "`").join(", ")}`)
205
+ .join("\n")
206
+ : "(none found)";
207
+
208
+ const exportsText = fileExports.length > 0
209
+ ? fileExports.slice(0, 30).map((e) => `- \`${e.file}\`: ${e.exports}`).join("\n")
210
+ : "(none found)";
211
+
212
+ const systemMessage = `You are a world-class software architect. You read source code and produce extremely precise, file-grounded documentation. You NEVER write generic advice. Every sentence you write must cite a real file path, function name, or pattern visible in the code you are given. If you cannot ground a claim in the actual source, you omit it entirely.`;
112
213
 
113
- CRITICAL RULES:
114
- - Every claim must be grounded in the actual files and code samples provided. No generic filler.
115
- - Reference specific file paths (e.g. \`src/lib/auth.ts\`, \`app/api/users/route.ts\`) wherever possible.
116
- - If you cannot infer something from the provided code, omit that bullet entirely — do NOT guess or write placeholders.
117
- - Architecture Notes must name real functions, classes, modules, or patterns you actually observed, not vague descriptions.
118
- - Rules for AI must reflect patterns actually present in the source — not generic best practices.
214
+ const userMessage = `I ran \`codebrief\` and need you to write a CONTEXT.md that lets an AI code assistant (Cursor, Copilot) understand this project so well it can write production code immediately.
119
215
 
216
+ ---
217
+ ## HARD RULES (violating these = failure)
218
+
219
+ 1. **File-path grounding**: Every bullet in Architecture Notes, Rules for AI, and Never Do MUST reference at least one real file path or function/export name from the code samples. No exceptions.
220
+ 2. **No negatives**: NEVER write "X is not used", "the project does not have Y", "no database detected". If something doesn't exist, simply don't mention it.
221
+ 3. **No generic advice**: NEVER write vague statements like "follow best practices", "maintain code quality", "adhere to coding standards", "ensure security". These are worthless.
222
+ 4. **Omit, don't guess**: If you can't infer something from the actual code samples, omit that section/bullet entirely. Empty sections should be removed.
223
+ 5. **Specific > exhaustive**: 5 deeply specific bullets beat 15 vague ones.
224
+
225
+ ## BAD (never write like this)
226
+ - "Authentication and session logic are not explicitly handled within the project"
227
+ - "Adhere to the project's coding standards and best practices"
228
+ - "Regular security audits are essential"
229
+ - "Error handling mechanisms are crucial for a robust application"
230
+ - "The project follows a modular structure, enhancing maintainability"
231
+
232
+ ## GOOD (write like this)
233
+ - "CLI entry point is \`src/index.js:main()\` — parses flags via \`hasFlag()\`/\`getFlagValue()\`, calls \`scanDirectory()\` → \`analyzeProject()\` → \`generateContextFile()\` in sequence"
234
+ - "AI enhancement in \`src/ai.js:enhanceWithAI()\` samples up to 32k chars of source via \`sampleSourceFiles()\`, builds a structured prompt, dispatches to the selected provider (Groq/OpenAI/Anthropic/Gemini/Grok/Ollama)"
235
+ - "Never add npm dependencies — this project uses zero deps (native \`https\`, \`fs\`, \`path\` only). See \`package.json\` dependencies field is empty."
236
+ - "All color output uses the \`c\` object from \`src/index.js\` (ANSI escape codes) — never use chalk or other color libraries"
237
+
238
+ ---
120
239
  ## Project metadata
121
240
  - Name: ${analysis.name}
122
241
  - Framework / Type: ${analysis.type}
123
242
  - Language: ${analysis.language}
124
243
  - Package manager: ${analysis.packageManager}
125
244
  - Stack: ${analysis.stack.join(", ") || "unknown"}
126
- - CSS framework: ${analysis.cssFramework || "none detected"}
127
- - UI library: ${analysis.uiLibrary || "none detected"}
128
- - State management: ${analysis.stateManagement || "none detected"}
129
- - Database / ORM: ${analysis.database || "none detected"}
130
- - Test framework: ${analysis.testFramework || "none detected"}
131
- - Deployment target: ${analysis.deployment || "unknown"}
245
+ - CSS: ${analysis.cssFramework || "none"} · UI: ${analysis.uiLibrary || "none"} · State: ${analysis.stateManagement || "none"}
246
+ - DB: ${analysis.database || "none"} · Tests: ${analysis.testFramework || "none"} · Deploy: ${analysis.deployment || "unknown"}
132
247
  - Monorepo: ${analysis.isMonorepo}
133
248
 
134
249
  ## Scripts
135
250
  ${scripts}
136
251
 
137
- ## File tree (up to 80 entries)
252
+ ## File tree
138
253
  ${fileList}
139
254
 
140
- ## Source file samples
255
+ ## Source code samples (READ CAREFULLY — this is your evidence)
141
256
  ${samplesText}
142
257
 
258
+ ## Environment variables found in code
259
+ ${envVarsText}
260
+
261
+ ## Module exports detected
262
+ ${exportsText}
263
+
143
264
  ---
144
265
 
145
- Produce the CONTEXT.md in exactly this structure:
266
+ Now produce the CONTEXT.md in EXACTLY this structure. Remove any section where you have nothing concrete to say. Keep the emoji in every section header EXACTLY as shown.
146
267
 
147
268
  # Project Context: ${analysis.name}
148
269
  > AI-enhanced by **codebrief** · ${new Date().toISOString().split("T")[0]}
@@ -150,44 +271,40 @@ Produce the CONTEXT.md in exactly this structure:
150
271
  ---
151
272
 
152
273
  ## 🧱 Tech Stack
153
- - List each technology and its specific role in this project (e.g. "Prisma — ORM used in \`src/db/\` for all database queries").
274
+ Bullet list. Each bullet: technology name + its specific role citing where it's used.
275
+ Example: "Node.js — runtime; entry point at \`src/index.js\`, all code is CommonJS with \`require()\`"
154
276
 
155
277
  ## 🚀 Key Files
156
- - List the 5–8 most important files a new developer should read first, with one sentence explaining what each does. Use exact paths from the file tree.
278
+ The 5–8 most important files to read first. Exact paths. One sentence each explaining what the file does and its key exports/functions.
157
279
 
158
280
  ## 📁 Folder Structure
159
- - Explain what each top-level folder is responsible for, inferred from the actual file tree.
281
+ One bullet per top-level directory, explaining its responsibility based on the actual files inside.
160
282
 
161
283
  ## 🔧 Scripts
162
- - Explain what each script actually does in the context of this project.
163
-
164
- ## 🗂️ Project Tree
165
- \`\`\`
166
- ${fileList}
167
- \`\`\`
284
+ One bullet per script. Say what it actually does, not just its command.
168
285
 
169
286
  ## 🏗️ Architecture Notes
170
- Write 8–15 bullet points. Each bullet must:
171
- - Name the specific file(s) or function(s) involved (e.g. "Auth flow starts in \`middleware.ts\`, validates JWT via \`src/lib/auth.ts:verifyToken()\`")
172
- - Describe a concrete data flow, pattern, or constraint — not a vague summary
173
- - Cover: request lifecycle, data access layer, auth/session, key abstractions, inter-module dependencies, anything surprising
287
+ 8–15 bullets. Each MUST:
288
+ - Name specific file(s), function(s), or export(s)
289
+ - Describe a concrete data flow, dependency, or design decision
290
+ - Be something useful for an AI about to write code in this project
174
291
 
175
292
  ## 🤖 Rules for AI
176
- Write 8–12 rules based on patterns you actually observed in the code. Example format:
177
- - "Always use the \`db\` instance from \`src/lib/db.ts\` never import Prisma directly"
178
- - "API routes live in \`app/api/\` and must use the response helper from \`src/lib/response.ts\`"
293
+ 8–12 rules extracted from the actual code patterns. Format:
294
+ - "Always/Never [specific action][file or pattern reference]"
179
295
 
180
296
  ## 🚫 Never Do
181
- Write 6–10 prohibitions inferred from the stack and code style. Be specific, not generic.
297
+ 6–10 prohibitions grounded in the codebase. Each must cite WHY (a file, pattern, or convention).
182
298
 
183
- ## 🔐 Environment & Security
184
- - List environment variables referenced in the code (exact names if found).
185
- - Note any auth patterns, token handling, or secrets management observed.
299
+ ## 🔐 Environment & Secrets
300
+ List actual env var names found in the code (e.g. \`GROQ_API_KEY\`, \`OPENAI_API_KEY\`). Describe how they're loaded and used. If none found, omit this section.
186
301
 
187
302
  ---
188
303
  *Re-run \`codebrief --ai\` after major refactors to keep this file current.*
189
304
 
190
- Respond with ONLY the Markdown. No preamble, no code fences wrapping the entire output.`;
305
+ Respond with ONLY the Markdown. No preamble, no wrapping code fences.`;
306
+
307
+ return { systemMessage, userMessage };
191
308
  }
192
309
 
193
310
  // ── HTTP helper (native, no deps) ────────────────────────────
@@ -233,6 +350,10 @@ async function callGroq(prompt, model) {
233
350
  " Get a free key in ~30s at https://console.groq.com",
234
351
  );
235
352
 
353
+ const messages = typeof prompt === "string"
354
+ ? [{ role: "user", content: prompt }]
355
+ : [{ role: "system", content: prompt.systemMessage }, { role: "user", content: prompt.userMessage }];
356
+
236
357
  const res = await httpsPost(
237
358
  "api.groq.com",
238
359
  "/openai/v1/chat/completions",
@@ -242,9 +363,9 @@ async function callGroq(prompt, model) {
242
363
  },
243
364
  {
244
365
  model,
245
- messages: [{ role: "user", content: prompt }],
246
- temperature: 0.3,
247
- max_tokens: 4096,
366
+ messages,
367
+ temperature: 0.2,
368
+ max_tokens: 8192,
248
369
  },
249
370
  );
250
371
  return res.choices?.[0]?.message?.content || "";
@@ -256,6 +377,10 @@ async function callOpenAI(prompt, model) {
256
377
  if (!apiKey)
257
378
  throw new Error("OPENAI_API_KEY environment variable is not set.");
258
379
 
380
+ const messages = typeof prompt === "string"
381
+ ? [{ role: "user", content: prompt }]
382
+ : [{ role: "system", content: prompt.systemMessage }, { role: "user", content: prompt.userMessage }];
383
+
259
384
  const res = await httpsPost(
260
385
  "api.openai.com",
261
386
  "/v1/chat/completions",
@@ -265,9 +390,9 @@ async function callOpenAI(prompt, model) {
265
390
  },
266
391
  {
267
392
  model,
268
- messages: [{ role: "user", content: prompt }],
269
- temperature: 0.3,
270
- max_tokens: 4096,
393
+ messages,
394
+ temperature: 0.2,
395
+ max_tokens: 8192,
271
396
  },
272
397
  );
273
398
  return res.choices?.[0]?.message?.content || "";
@@ -279,6 +404,19 @@ async function callAnthropic(prompt, model) {
279
404
  if (!apiKey)
280
405
  throw new Error("ANTHROPIC_API_KEY environment variable is not set.");
281
406
 
407
+ const messages = typeof prompt === "string"
408
+ ? [{ role: "user", content: prompt }]
409
+ : [{ role: "user", content: prompt.userMessage }];
410
+
411
+ const system = typeof prompt === "string" ? undefined : prompt.systemMessage;
412
+
413
+ const body = {
414
+ model,
415
+ max_tokens: 8192,
416
+ messages,
417
+ };
418
+ if (system) body.system = system;
419
+
282
420
  const res = await httpsPost(
283
421
  "api.anthropic.com",
284
422
  "/v1/messages",
@@ -287,11 +425,7 @@ async function callAnthropic(prompt, model) {
287
425
  "x-api-key": apiKey,
288
426
  "anthropic-version": "2023-06-01",
289
427
  },
290
- {
291
- model,
292
- max_tokens: 4096,
293
- messages: [{ role: "user", content: prompt }],
294
- },
428
+ body,
295
429
  );
296
430
  return res.content?.[0]?.text || "";
297
431
  }
@@ -305,13 +439,17 @@ async function callGemini(prompt, model) {
305
439
  " Get a free key at https://aistudio.google.com/app/apikey",
306
440
  );
307
441
 
442
+ const fullText = typeof prompt === "string"
443
+ ? prompt
444
+ : `${prompt.systemMessage}\n\n${prompt.userMessage}`;
445
+
308
446
  const res = await httpsPost(
309
447
  "generativelanguage.googleapis.com",
310
448
  `/v1beta/models/${model}:generateContent?key=${apiKey}`,
311
449
  { "Content-Type": "application/json" },
312
450
  {
313
- contents: [{ parts: [{ text: prompt }] }],
314
- generationConfig: { temperature: 0.3, maxOutputTokens: 4096 },
451
+ contents: [{ parts: [{ text: fullText }] }],
452
+ generationConfig: { temperature: 0.2, maxOutputTokens: 8192 },
315
453
  },
316
454
  );
317
455
  return res.candidates?.[0]?.content?.parts?.[0]?.text || "";
@@ -326,6 +464,10 @@ async function callGrok(prompt, model) {
326
464
  " Get a key at https://console.x.ai",
327
465
  );
328
466
 
467
+ const messages = typeof prompt === "string"
468
+ ? [{ role: "user", content: prompt }]
469
+ : [{ role: "system", content: prompt.systemMessage }, { role: "user", content: prompt.userMessage }];
470
+
329
471
  const res = await httpsPost(
330
472
  "api.x.ai",
331
473
  "/v1/chat/completions",
@@ -335,9 +477,9 @@ async function callGrok(prompt, model) {
335
477
  },
336
478
  {
337
479
  model,
338
- messages: [{ role: "user", content: prompt }],
339
- temperature: 0.3,
340
- max_tokens: 4096,
480
+ messages,
481
+ temperature: 0.2,
482
+ max_tokens: 8192,
341
483
  },
342
484
  );
343
485
  return res.choices?.[0]?.message?.content || "";
@@ -347,7 +489,10 @@ async function callOllama(prompt, model) {
347
489
  model = model || getDefaultModel("ollama");
348
490
  // Ollama runs locally on port 11434 — use http
349
491
  const http = require("http");
350
- const body = JSON.stringify({ model, prompt, stream: false });
492
+ const fullText = typeof prompt === "string"
493
+ ? prompt
494
+ : `${prompt.systemMessage}\n\n${prompt.userMessage}`;
495
+ const body = JSON.stringify({ model, prompt: fullText, stream: false });
351
496
 
352
497
  return new Promise((resolve, reject) => {
353
498
  const req = http.request(
@@ -396,7 +541,9 @@ async function enhanceWithAI(analysis, fileTree, rootDir, options = {}) {
396
541
  const { provider = "openai", model } = options;
397
542
 
398
543
  const fileSamples = sampleSourceFiles(rootDir, fileTree);
399
- const prompt = buildPrompt(analysis, fileTree, fileSamples);
544
+ const envVars = extractEnvVars(rootDir, fileTree);
545
+ const fileExports = extractExports(rootDir, fileTree);
546
+ const prompt = buildPrompt(analysis, fileTree, fileSamples, envVars, fileExports);
400
547
 
401
548
  switch (provider.toLowerCase()) {
402
549
  case "groq":