contextforge-cli-harshil 1.0.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/.env.example ADDED
@@ -0,0 +1,37 @@
1
+ # ============================================================
2
+ # ContextForge Configuration
3
+ # Copy this file to .env and fill in your values.
4
+ # Change ONLY this file — no code changes needed.
5
+ # ============================================================
6
+
7
+
8
+ # ── Provider (groq | openai) ─────────────────────────────────
9
+ # Which AI provider to use.
10
+ # Leave blank to auto-detect from whichever API key is present.
11
+ # If both keys are set, Groq is preferred (faster + free tier).
12
+ AI_PROVIDER=groq
13
+
14
+
15
+ # ── Model ────────────────────────────────────────────────────
16
+ # Which model to use. Leave blank to use each provider's default.
17
+ #
18
+ # Groq models:
19
+ # llama-3.3-70b-versatile ← default (best quality)
20
+ # llama-3.1-8b-instant ← fastest
21
+ # mixtral-8x7b-32768
22
+ # gemma2-9b-it
23
+ #
24
+ # OpenAI models:
25
+ # gpt-4o-mini ← default (cheap + fast)
26
+ # gpt-4o
27
+ # gpt-4-turbo
28
+ #
29
+ AI_MODEL=llama-3.3-70b-versatile
30
+
31
+
32
+ # ── API Keys ─────────────────────────────────────────────────
33
+ # Get a free Groq key at: https://console.groq.com
34
+ GROQ_API_KEY=gsk_your-groq-key-here
35
+
36
+ # Get an OpenAI key at: https://platform.openai.com/api-keys
37
+ # OPENAI_API_KEY=sk-your-openai-key-here
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # ⚡ ContextForge
2
+
3
+ > **Automatic AI memory generation for repositories.**
4
+
5
+ Run one command → automatically generate reusable AI-ready project context.
6
+
7
+ ---
8
+
9
+ ## What It Does
10
+
11
+ ContextForge scans your repository, analyzes its architecture and stack, and generates a `context.md` file that you can paste into any AI assistant (ChatGPT, Claude, Gemini, Cursor, etc.) to give it deep, accurate knowledge of your codebase — instantly.
12
+
13
+ **No backend. No cloud. No magic. Just fast, local analysis.**
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install -g contextforge
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Usage
26
+
27
+ ### 1. Add your OpenAI API key
28
+
29
+ Create a `.env` file in your **project root** (the repo you want to analyze):
30
+
31
+ ```env
32
+ OPENAI_API_KEY=sk-your-api-key-here
33
+ ```
34
+
35
+ ### 2. Run ContextForge
36
+
37
+ ```bash
38
+ contextforge init
39
+ ```
40
+
41
+ That's it. ContextForge will:
42
+
43
+ 1. 🔍 **Scan** your repository (using fast-glob + .gitignore rules)
44
+ 2. 🧠 **Analyze** your architecture, stack, patterns, and auth
45
+ 3. 📁 **Identify** important files with heuristic scoring
46
+ 4. ✨ **Generate** AI-ready context using GPT-4o-mini
47
+ 5. 📝 **Write** `context.md` to your project root
48
+
49
+ ---
50
+
51
+ ## CLI Options
52
+
53
+ ```
54
+ contextforge init [options]
55
+
56
+ Options:
57
+ -o, --output <path> Output file path (default: "context.md")
58
+ --model <model> OpenAI model to use (default: "gpt-4o-mini")
59
+ --no-ai Skip OpenAI, generate structural context only
60
+ -v, --version Output the current version
61
+ -h, --help Display help
62
+ ```
63
+
64
+ ### Examples
65
+
66
+ ```bash
67
+ # Standard usage
68
+ contextforge init
69
+
70
+ # Use a more powerful model
71
+ contextforge init --model gpt-4o
72
+
73
+ # Offline / no API key — structural context only
74
+ contextforge init --no-ai
75
+
76
+ # Custom output path
77
+ contextforge init --output docs/ai-context.md
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Output Format
83
+
84
+ ```markdown
85
+ # Project Context
86
+
87
+ > Express.js REST API with Prisma ORM and JWT authentication.
88
+
89
+ ## Stack
90
+ - Express.js
91
+ - Node.js
92
+ - Prisma ORM
93
+ - JWT (jsonwebtoken)
94
+ - Zod
95
+
96
+ ## Architecture
97
+ - Route-based API structure
98
+ - MVC / Controller pattern
99
+ - Service layer pattern
100
+ - Middleware-based request handling
101
+
102
+ ## Important Modules
103
+ ### `src/`
104
+ - `src/app.js`
105
+ - `src/routes/`
106
+ - `src/controllers/`
107
+ - `src/services/`
108
+ - `src/middleware/auth.js`
109
+
110
+ ## Coding Patterns
111
+ - async/await pattern
112
+ - ES Modules (import/export)
113
+ - try/catch error handling
114
+ - Schema validation (Zod)
115
+
116
+ ## AI Instructions
117
+ - This is a JavaScript project using ES Modules
118
+ - Backend: Express.js — follow route/controller/service layering
119
+ - Database: Prisma ORM — use existing query patterns
120
+ - Auth: JWT — never bypass the auth middleware
121
+ - Validate all inputs with Zod schemas
122
+ - Use async/await for all async operations
123
+ ```
124
+
125
+ ---
126
+
127
+ ## How File Importance Is Scored
128
+
129
+ ContextForge uses **lightweight heuristic scoring** — no embeddings, no vectors:
130
+
131
+ | Signal | Score |
132
+ |---|---|
133
+ | Filename matches known important files | +10 |
134
+ | Directory matches (auth, routes, services, etc.) | +7 |
135
+ | Filename contains keywords (auth, route, controller...) | +5 |
136
+ | Content patterns (router.get, prisma., jwt.sign...) | +2 each |
137
+ | File extension (JS/TS > JSON/YAML > MD) | +1–3 |
138
+
139
+ ---
140
+
141
+ ## Architecture Detection
142
+
143
+ ContextForge detects:
144
+
145
+ - **Frameworks**: Express, Fastify, NestJS, Next.js, React, Vue, Svelte, Astro, Remix...
146
+ - **Databases**: Prisma, Mongoose, TypeORM, Drizzle, Sequelize, PostgreSQL, MySQL, Redis...
147
+ - **Auth**: JWT, Passport.js, NextAuth, Clerk, Supabase, Firebase, bcrypt...
148
+ - **State**: Zustand, Jotai, Redux Toolkit, TanStack Query, SWR...
149
+ - **Validation**: Zod, Joi, Yup, class-validator...
150
+ - **Testing**: Jest, Vitest, Playwright, Cypress...
151
+ - **Patterns**: Service layer, Repository, MVC, Middleware, RBAC, File-system routing...
152
+
153
+ ---
154
+
155
+ ## Privacy
156
+
157
+ Everything runs **locally** on your machine. Your code is only sent to OpenAI when AI generation is enabled (the default). Use `--no-ai` to keep everything fully local.
158
+
159
+ ---
160
+
161
+ ## Requirements
162
+
163
+ - Node.js ≥ 18
164
+ - OpenAI API key (optional with `--no-ai`)
165
+
166
+ ---
167
+
168
+ ## License
169
+
170
+ MIT
package/bin/index.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import dotenv from 'dotenv';
5
+ import { readFileSync, existsSync } from 'fs';
6
+ import { resolve } from 'path';
7
+ import chalk from 'chalk';
8
+
9
+ // Load .env from the directory where contextforge is being run
10
+ const envPath = resolve(process.cwd(), '.env');
11
+ if (existsSync(envPath)) {
12
+ dotenv.config({ path: envPath });
13
+ }
14
+
15
+ // Read package.json for version
16
+ const __dirname = new URL('.', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
17
+ const pkgPath = resolve(__dirname, '..', 'package.json');
18
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
19
+
20
+ program
21
+ .name('contextforge')
22
+ .description(
23
+ chalk.bold.cyan('⚡ ContextForge') +
24
+ ' — Automatic AI memory generation for repositories.'
25
+ )
26
+ .version(pkg.version, '-v, --version', 'Output the current version');
27
+
28
+ // ── contextforge init ──────────────────────────────────────────────────────
29
+ program
30
+ .command('init')
31
+ .description('Scan repository and generate AI-ready context.md')
32
+ .option('-o, --output <path>', 'Output file path', 'context.md')
33
+ .option(
34
+ '-p, --provider <provider>',
35
+ 'AI provider to use: groq | openai | auto (default: auto — prefers Groq)',
36
+ 'auto'
37
+ )
38
+ .option(
39
+ '--model <model>',
40
+ 'Model override (e.g. llama-3.3-70b-versatile, gpt-4o). Defaults to provider\'s recommended model.'
41
+ )
42
+ .option('--no-ai', 'Skip AI and generate a structural context only')
43
+ .option('--force', 'Regenerate even if context.md is already up to date')
44
+ .action(async (options) => {
45
+ const { runInit } = await import('../src/commands/init.js');
46
+ await runInit(options);
47
+ });
48
+
49
+ // Show help with clean exit if no command given
50
+ if (!process.argv.slice(2).length) {
51
+ program.outputHelp();
52
+ process.exit(0); // explicit 0 — not a failure
53
+ }
54
+
55
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "contextforge-cli-harshil",
3
+ "version": "1.0.0",
4
+ "description": "Automatically scan a repository and generate AI-ready project context in context.md",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "contextforge": "./bin/index.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md",
13
+ ".env.example"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/index.js",
17
+ "dev": "node bin/index.js"
18
+ },
19
+ "keywords": [
20
+ "ai",
21
+ "context",
22
+ "cli",
23
+ "openai",
24
+ "repository",
25
+ "codegen",
26
+ "developer-tools"
27
+ ],
28
+ "author": "",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "chalk": "^5.3.0",
32
+ "commander": "^12.1.0",
33
+ "dotenv": "^16.4.5",
34
+ "fast-glob": "^3.3.2",
35
+ "groq-sdk": "^1.2.0",
36
+ "ignore": "^5.3.1",
37
+ "openai": "^4.52.7",
38
+ "ora": "^8.1.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "type": "module"
44
+ }
package/src/ai.js ADDED
@@ -0,0 +1,399 @@
1
+ /**
2
+ * ai.js
3
+ * Unified AI provider — supports OpenAI and Groq.
4
+ *
5
+ * Configuration priority (highest → lowest):
6
+ * 1. CLI flags --provider groq --model llama-3.3-70b-versatile
7
+ * 2. .env vars AI_PROVIDER=groq AI_MODEL=llama-3.3-70b-versatile
8
+ * 3. Auto-detect reads GROQ_API_KEY / OPENAI_API_KEY from .env
9
+ * 4. Defaults Groq → llama-3.3-70b-versatile | OpenAI → gpt-4o-mini
10
+ *
11
+ * Changing ONLY your .env file is enough to switch provider and model.
12
+ */
13
+
14
+ import OpenAI from 'openai';
15
+ import Groq from 'groq-sdk';
16
+ import { readFileSafe } from './scan.js';
17
+
18
+ // ── Provider registry ─────────────────────────────────────────────────────────
19
+
20
+ export const PROVIDERS = {
21
+ openai: {
22
+ name: 'OpenAI',
23
+ envKey: 'OPENAI_API_KEY',
24
+ defaultModel: 'gpt-4o-mini',
25
+ models: ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'],
26
+ },
27
+ groq: {
28
+ name: 'Groq',
29
+ envKey: 'GROQ_API_KEY',
30
+ defaultModel: 'llama-3.3-70b-versatile',
31
+ models: [
32
+ 'llama-3.3-70b-versatile',
33
+ 'llama-3.1-70b-versatile',
34
+ 'llama-3.1-8b-instant',
35
+ 'llama3-70b-8192',
36
+ 'llama3-8b-8192',
37
+ 'mixtral-8x7b-32768',
38
+ 'gemma2-9b-it',
39
+ ],
40
+ },
41
+ };
42
+
43
+ // ── Provider detection ────────────────────────────────────────────────────────
44
+
45
+ /**
46
+ * Detect which AI provider to use.
47
+ *
48
+ * Resolution order:
49
+ * 1. Explicit CLI --provider flag (hint !== 'auto')
50
+ * 2. AI_PROVIDER env var (set in .env)
51
+ * 3. Auto-detect from API keys (GROQ preferred over OpenAI)
52
+ *
53
+ * @param {'openai'|'groq'|'auto'} hint - value from --provider flag
54
+ * @returns {'openai'|'groq'|null}
55
+ */
56
+ export function detectProvider(hint = 'auto') {
57
+ // 1. Explicit CLI flag overrides everything
58
+ if (hint === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : null;
59
+ if (hint === 'groq') return process.env.GROQ_API_KEY ? 'groq' : null;
60
+
61
+ // 2. AI_PROVIDER set in .env
62
+ const envProvider = (process.env.AI_PROVIDER || '').toLowerCase().trim();
63
+ if (envProvider === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : null;
64
+ if (envProvider === 'groq') return process.env.GROQ_API_KEY ? 'groq' : null;
65
+
66
+ // 3. Auto-detect: prefer Groq (faster, generous free tier)
67
+ if (process.env.GROQ_API_KEY) return 'groq';
68
+ if (process.env.OPENAI_API_KEY) return 'openai';
69
+
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * Validate that the API key for the selected provider is present and looks valid.
75
+ *
76
+ * @param {'openai'|'groq'} provider
77
+ * @returns {{ valid: boolean, message: string }}
78
+ */
79
+ export function validateProviderKey(provider) {
80
+ const cfg = PROVIDERS[provider];
81
+ if (!cfg) {
82
+ return { valid: false, message: `Unknown provider: "${provider}"` };
83
+ }
84
+
85
+ const key = process.env[cfg.envKey];
86
+ if (!key) {
87
+ return {
88
+ valid: false,
89
+ message:
90
+ `${cfg.envKey} is not set.\n` +
91
+ ` Add it to a .env file in your project root:\n\n` +
92
+ ` ${cfg.envKey}=your-key-here\n`,
93
+ };
94
+ }
95
+
96
+ // Basic format checks
97
+ if (provider === 'openai' && !key.startsWith('sk-')) {
98
+ return {
99
+ valid: false,
100
+ message: 'OPENAI_API_KEY looks invalid (should start with "sk-"). Check your .env file.',
101
+ };
102
+ }
103
+ if (provider === 'groq' && !key.startsWith('gsk_')) {
104
+ return {
105
+ valid: false,
106
+ message: 'GROQ_API_KEY looks invalid (should start with "gsk_"). Check your .env file.',
107
+ };
108
+ }
109
+
110
+ return { valid: true, message: '' };
111
+ }
112
+
113
+ // ── Client creation ───────────────────────────────────────────────────────────
114
+
115
+ /**
116
+ * Create and return the AI client for the given provider.
117
+ *
118
+ * @param {'openai'|'groq'} provider
119
+ * @returns {OpenAI|Groq}
120
+ */
121
+ export function createClient(provider) {
122
+ if (provider === 'groq') {
123
+ return new Groq({ apiKey: process.env.GROQ_API_KEY });
124
+ }
125
+ return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
126
+ }
127
+
128
+ /**
129
+ * Resolve which model to use.
130
+ *
131
+ * Resolution order:
132
+ * 1. --model CLI flag
133
+ * 2. AI_MODEL env var (set in .env)
134
+ * 3. Provider's built-in default
135
+ *
136
+ * @param {string|undefined} modelFlag - value from --model CLI flag
137
+ * @param {'openai'|'groq'} provider
138
+ * @returns {string}
139
+ */
140
+ export function resolveModel(modelFlag, provider) {
141
+ // 1. Explicit CLI --model flag
142
+ if (modelFlag) return modelFlag;
143
+
144
+ // 2. AI_MODEL in .env
145
+ const envModel = (process.env.AI_MODEL || '').trim();
146
+ if (envModel) return envModel;
147
+
148
+ // 3. Provider default
149
+ return PROVIDERS[provider].defaultModel;
150
+ }
151
+
152
+ // ── Prompt builder ────────────────────────────────────────────────────────────
153
+
154
+ /**
155
+ * Build the analysis prompt to send to the AI model.
156
+ *
157
+ * @param {Object} analysis - Result from analyzeRepository()
158
+ * @param {string} folderTree
159
+ * @returns {string}
160
+ */
161
+ export function buildPrompt(analysis, folderTree) {
162
+ const {
163
+ projectName,
164
+ projectVersion,
165
+ projectDescription,
166
+ language,
167
+ moduleSystem,
168
+ stack,
169
+ byCategory,
170
+ architecturePatterns,
171
+ authApproach,
172
+ codingConventions,
173
+ scripts,
174
+ importantFiles,
175
+ } = analysis;
176
+
177
+ // Collect snippets from important files (budget-capped for free-tier models)
178
+ const snippetParts = [];
179
+ let totalSnippetLength = 0;
180
+ const MAX_TOTAL = 18_000; // ~4500 tokens — leaves room for prompt + response
181
+ const MAX_PER_FILE = 1_200; // ~300 tokens per file
182
+
183
+ for (const file of importantFiles) {
184
+ if (totalSnippetLength >= MAX_TOTAL) break;
185
+ const content = readFileSafe(file.absolutePath, MAX_PER_FILE);
186
+ if (!content) continue;
187
+ const snippet = `### ${file.path}\n\`\`\`\n${content}\n\`\`\``;
188
+ snippetParts.push(snippet);
189
+ totalSnippetLength += snippet.length;
190
+ }
191
+
192
+ // package.json summary (capped)
193
+ const pkgSummary = analysis.pkgJson
194
+ ? JSON.stringify(
195
+ {
196
+ name: analysis.pkgJson.name,
197
+ version: analysis.pkgJson.version,
198
+ description: analysis.pkgJson.description,
199
+ scripts: analysis.pkgJson.scripts,
200
+ dependencies: analysis.pkgJson.dependencies,
201
+ devDependencies: analysis.pkgJson.devDependencies,
202
+ },
203
+ null,
204
+ 2
205
+ ).slice(0, 4_000)
206
+ : 'Not available';
207
+
208
+ const stackBlock =
209
+ stack.length > 0
210
+ ? stack.map((s) => `- ${s}`).join('\n')
211
+ : '- No external dependencies detected';
212
+
213
+ const categoryBlock =
214
+ Object.entries(byCategory)
215
+ .map(([cat, items]) => `**${capitalize(cat)}:** ${items.join(', ')}`)
216
+ .join('\n') || 'N/A';
217
+
218
+ const archBlock =
219
+ architecturePatterns.length > 0
220
+ ? architecturePatterns.map((p) => `- ${p}`).join('\n')
221
+ : '- Unable to determine';
222
+
223
+ const authBlock =
224
+ authApproach.length > 0
225
+ ? authApproach.map((a) => `- ${a}`).join('\n')
226
+ : '- None detected';
227
+
228
+ const conventionBlock =
229
+ codingConventions.length > 0
230
+ ? codingConventions.map((c) => `- ${c}`).join('\n')
231
+ : '- None detected';
232
+
233
+ const scriptBlock =
234
+ scripts.length > 0
235
+ ? scripts.map((s) => `- \`${s}\``).join('\n')
236
+ : '- None';
237
+
238
+ return `You are an expert software architect. Analyze the following repository and generate a comprehensive, AI-ready project context document.
239
+
240
+ ## Repository Overview
241
+
242
+ **Project Name:** ${projectName}
243
+ **Version:** ${projectVersion || 'N/A'}
244
+ **Description:** ${projectDescription || 'N/A'}
245
+ **Language:** ${language}
246
+ **Module System:** ${moduleSystem}
247
+
248
+ ## Detected Technology Stack
249
+
250
+ ${stackBlock}
251
+
252
+ ### By Category
253
+ ${categoryBlock}
254
+
255
+ ## Detected Architecture Patterns
256
+
257
+ ${archBlock}
258
+
259
+ ## Detected Auth Approach
260
+
261
+ ${authBlock}
262
+
263
+ ## Detected Coding Conventions
264
+
265
+ ${conventionBlock}
266
+
267
+ ## Available Scripts
268
+
269
+ ${scriptBlock}
270
+
271
+ ## Folder Structure
272
+
273
+ \`\`\`
274
+ ${folderTree}
275
+ \`\`\`
276
+
277
+ ## package.json
278
+
279
+ \`\`\`json
280
+ ${pkgSummary}
281
+ \`\`\`
282
+
283
+ ## Important File Snippets
284
+
285
+ ${snippetParts.join('\n\n')}
286
+
287
+ ---
288
+
289
+ ## Your Task
290
+
291
+ Based on all of the above, generate a well-structured **context.md** document in the following exact format:
292
+
293
+ \`\`\`markdown
294
+ # Project Context
295
+
296
+ > [One-sentence summary of what this project does]
297
+
298
+ ## Stack
299
+
300
+ [List every confirmed technology: language, runtime, frameworks, databases, auth, styling, testing, build tools]
301
+
302
+ ## Architecture
303
+
304
+ [Explain the high-level design: patterns used, folder layout rationale, component interaction, data flow]
305
+
306
+ ## Important Modules
307
+
308
+ [For each major area — auth, database, routing, services, etc. — list relevant paths and explain their role]
309
+
310
+ ## Coding Patterns
311
+
312
+ [List observed conventions: async style, error handling, module system, typing, etc.]
313
+
314
+ ## API Structure
315
+
316
+ [If applicable: REST/GraphQL/tRPC style, endpoint organization, auth middleware flow]
317
+
318
+ ## Database & Data Layer
319
+
320
+ [If applicable: ORM/query approach, schema structure, migration strategy]
321
+
322
+ ## Environment & Configuration
323
+
324
+ [List important environment variables and how config is managed]
325
+
326
+ ## Development Workflow
327
+
328
+ [List npm scripts and what they do]
329
+
330
+ ## AI Instructions
331
+
332
+ [Specific, actionable rules for an AI assistant working in this codebase. Be concrete and prescriptive.]
333
+ \`\`\`
334
+
335
+ Rules:
336
+ 1. Omit any section that is not relevant to this project.
337
+ 2. Be specific and concrete — avoid generic advice.
338
+ 3. The **AI Instructions** section is the most important: write precise rules an AI must follow when editing this repo.
339
+ 4. Return ONLY the markdown content — no preamble, no commentary, no wrapping fences.
340
+ 5. Use actual file paths, module names, and library names from the data above.
341
+ `;
342
+ }
343
+
344
+ // ── AI call ───────────────────────────────────────────────────────────────────
345
+
346
+ /**
347
+ * Call the selected provider and return the generated markdown context.
348
+ * Both OpenAI and Groq implement the same chat completions interface.
349
+ *
350
+ * @param {OpenAI|Groq} client
351
+ * @param {string} prompt
352
+ * @param {string} model
353
+ * @param {'openai'|'groq'} provider
354
+ * @returns {Promise<string>}
355
+ */
356
+ export async function generateContextWithAI(client, prompt, model, provider) {
357
+ const TIMEOUT_MS = 60_000; // 60 seconds
358
+
359
+ const apiCall = client.chat.completions.create({
360
+ model,
361
+ messages: [
362
+ {
363
+ role: 'system',
364
+ content:
365
+ 'You are a senior software architect who specializes in analyzing codebases ' +
366
+ 'and producing precise, AI-ready documentation. Write concise, actionable ' +
367
+ 'context documents that help AI assistants understand and contribute to projects.',
368
+ },
369
+ {
370
+ role: 'user',
371
+ content: prompt,
372
+ },
373
+ ],
374
+ temperature: 0.3,
375
+ max_tokens: 3000,
376
+ });
377
+
378
+ const timeoutPromise = new Promise((_, reject) =>
379
+ setTimeout(
380
+ () => reject(new Error(`AI request timed out after ${TIMEOUT_MS / 1000}s. Try again or use --no-ai.`)),
381
+ TIMEOUT_MS
382
+ )
383
+ );
384
+
385
+ const response = await Promise.race([apiCall, timeoutPromise]);
386
+
387
+ const content = response.choices?.[0]?.message?.content;
388
+ if (!content) {
389
+ const name = PROVIDERS[provider]?.name ?? provider;
390
+ throw new Error(`${name} returned an empty response.`);
391
+ }
392
+ return content.trim();
393
+ }
394
+
395
+ // ── Helpers ───────────────────────────────────────────────────────────────────
396
+
397
+ function capitalize(str) {
398
+ return str.charAt(0).toUpperCase() + str.slice(1);
399
+ }