contextforge-cli-ai-prompt-pirates 0.4.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.
Files changed (72) hide show
  1. package/.contextforge/config.ai.example.json +28 -0
  2. package/.contextforge/config.example.json +29 -0
  3. package/.contextforge/prompts/README.md +50 -0
  4. package/.env.example +73 -0
  5. package/LICENSE +21 -0
  6. package/README.md +223 -0
  7. package/bin/contextforge.js +14 -0
  8. package/bin/postinstall-worker.js +14 -0
  9. package/bin/postinstall.js +26 -0
  10. package/package.json +65 -0
  11. package/prompts/README.md +50 -0
  12. package/prompts/response-schema.md +22 -0
  13. package/prompts/retry-addon.md +1 -0
  14. package/prompts/system.md +10 -0
  15. package/prompts/user-template.md +22 -0
  16. package/src/analyzers/ast-parser.js +139 -0
  17. package/src/analyzers/bugs-lite.js +118 -0
  18. package/src/analyzers/changelog.js +190 -0
  19. package/src/analyzers/code-insights.js +225 -0
  20. package/src/analyzers/dependencies.js +110 -0
  21. package/src/analyzers/detection-quality.js +106 -0
  22. package/src/analyzers/eslint-runner.js +56 -0
  23. package/src/analyzers/folder-tree.js +60 -0
  24. package/src/analyzers/git-insights.js +94 -0
  25. package/src/analyzers/project-meta.js +98 -0
  26. package/src/cli/commands/changes.js +36 -0
  27. package/src/cli/commands/doctor.js +152 -0
  28. package/src/cli/commands/generate.js +7 -0
  29. package/src/cli/commands/init.js +98 -0
  30. package/src/cli/commands/prompt.js +53 -0
  31. package/src/cli/commands/stop.js +10 -0
  32. package/src/cli/commands/summary.js +22 -0
  33. package/src/cli/commands/watch.js +9 -0
  34. package/src/cli/index.js +120 -0
  35. package/src/core/confidence.js +51 -0
  36. package/src/core/config.js +183 -0
  37. package/src/core/cursor-rules.js +293 -0
  38. package/src/core/ensure-setup.js +45 -0
  39. package/src/core/env.js +113 -0
  40. package/src/core/package-meta.js +35 -0
  41. package/src/core/paths.js +39 -0
  42. package/src/core/pipeline.js +256 -0
  43. package/src/core/postinstall.js +168 -0
  44. package/src/core/setup-env.js +86 -0
  45. package/src/core/setup-prompts.js +31 -0
  46. package/src/core/snapshot-hash.js +70 -0
  47. package/src/detectors/architecture.js +117 -0
  48. package/src/detectors/auth-deploy.js +51 -0
  49. package/src/detectors/database.js +127 -0
  50. package/src/detectors/tech-stack.js +180 -0
  51. package/src/generators/artifacts.js +44 -0
  52. package/src/generators/changes-markdown.js +72 -0
  53. package/src/generators/context-json.js +91 -0
  54. package/src/generators/context-markdown.js +571 -0
  55. package/src/generators/mermaid.js +59 -0
  56. package/src/index.js +13 -0
  57. package/src/scanner/change-detector.js +24 -0
  58. package/src/scanner/config-files.js +52 -0
  59. package/src/scanner/file-index.js +55 -0
  60. package/src/scanner/package-deps.js +32 -0
  61. package/src/scanner/repo-scanner.js +143 -0
  62. package/src/services/ai/background.js +87 -0
  63. package/src/services/ai/config.js +48 -0
  64. package/src/services/ai/enrich.js +87 -0
  65. package/src/services/ai/index.js +3 -0
  66. package/src/services/ai/prompt-loader.js +104 -0
  67. package/src/services/ai/prompt.js +42 -0
  68. package/src/services/ai/providers/groq.js +36 -0
  69. package/src/services/ai/providers/openai.js +32 -0
  70. package/src/services/ai/redact.js +33 -0
  71. package/src/services/ai/validate.js +70 -0
  72. package/src/watcher/watcher.js +128 -0
@@ -0,0 +1,183 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { getConfigPath, getContextForgeRoot } from './paths.js';
4
+ import { DEFAULT_AI_CONFIG } from '../services/ai/config.js';
5
+ import {
6
+ loadProjectEnv,
7
+ getRefreshIntervalMinutesFromEnv,
8
+ getDebounceMsFromEnv,
9
+ isAiEnabledViaEnv,
10
+ hasAnyAiKey,
11
+ } from './env.js';
12
+ import { resolveAiConfig } from '../services/ai/config.js';
13
+
14
+ export const DEFAULT_CONFIG = {
15
+ ignore: [
16
+ 'node_modules',
17
+ 'dist',
18
+ 'build',
19
+ '.next',
20
+ 'coverage',
21
+ '.git',
22
+ 'test/fixtures',
23
+ 'test/tmp-mini-express',
24
+ '.contextforge/cache',
25
+ '.contextforge/logs',
26
+ '.contextforge/embeddings',
27
+ ],
28
+ watch: true,
29
+ autoGenerate: true,
30
+ includeGitHistory: true,
31
+ detectBugs: true,
32
+ debounceMs: 2000,
33
+ /** Auto-refresh context.md every N minutes during `watch` (0 = file changes only) */
34
+ refreshIntervalMinutes: 0,
35
+ /** Track and log file/stack changes between runs */
36
+ trackChanges: true,
37
+ maxTreeDepth: 4,
38
+ gitCommitLimit: 20,
39
+ maxFileSizeBytes: 524288,
40
+ installCursorRules: true,
41
+ /** Run generate after npm install (postinstall hook) */
42
+ postinstallGenerate: true,
43
+ /** Start `watch --daemon` in background after npm install */
44
+ postinstallWatch: true,
45
+ ai: { ...DEFAULT_AI_CONFIG },
46
+ };
47
+
48
+ /** Paths never read for content analysis */
49
+ export const SENSITIVE_PATTERNS = [
50
+ /^\.env(\.|$)/,
51
+ /\.pem$/i,
52
+ /credentials/i,
53
+ /id_rsa/i,
54
+ /\.key$/i,
55
+ /secrets?\./i,
56
+ ];
57
+
58
+ export function isSensitivePath(relativePath) {
59
+ const base = path.basename(relativePath);
60
+ return SENSITIVE_PATTERNS.some((re) => re.test(base) || re.test(relativePath));
61
+ }
62
+
63
+ /** Merge saved config with package defaults (adds new fields on upgrade). */
64
+ export function mergeConfigWithDefaults(userConfig = {}) {
65
+ const merged = { ...DEFAULT_CONFIG, ...userConfig };
66
+ if (userConfig.ignore) {
67
+ merged.ignore = [...new Set([...DEFAULT_CONFIG.ignore, ...userConfig.ignore])];
68
+ }
69
+ merged.ai = {
70
+ ...DEFAULT_AI_CONFIG,
71
+ ...(userConfig.ai || {}),
72
+ openai: { ...DEFAULT_AI_CONFIG.openai, ...(userConfig.ai?.openai || {}) },
73
+ groq: { ...DEFAULT_AI_CONFIG.groq, ...(userConfig.ai?.groq || {}) },
74
+ };
75
+ return merged;
76
+ }
77
+
78
+ /** Apply .env overrides (timer, debounce, AI provider) on top of config. */
79
+ export function applyEnvOverrides(config) {
80
+ const envInterval = getRefreshIntervalMinutesFromEnv();
81
+ if (envInterval !== null) {
82
+ config.refreshIntervalMinutes = envInterval;
83
+ }
84
+ const envDebounce = getDebounceMsFromEnv();
85
+ if (envDebounce !== null) {
86
+ config.debounceMs = envDebounce;
87
+ }
88
+ const resolvedAi = resolveAiConfig(config);
89
+ config.ai = { ...config.ai, ...resolvedAi };
90
+ return config;
91
+ }
92
+
93
+ /**
94
+ * Write/update config.json from defaults + .env (proper init/upgrade path).
95
+ * @param {string} projectRoot
96
+ * @param {{ enableAi?: boolean, upgrade?: boolean }} options
97
+ */
98
+ export async function syncConfigFile(projectRoot, options = {}) {
99
+ await loadProjectEnv(projectRoot);
100
+ const configPath = getConfigPath(projectRoot);
101
+ let userConfig = {};
102
+ if (await fs.pathExists(configPath)) {
103
+ userConfig = await fs.readJson(configPath);
104
+ }
105
+
106
+ const merged = mergeConfigWithDefaults(userConfig);
107
+
108
+ if (options.enableAi) {
109
+ merged.ai.enabled = true;
110
+ merged.ai.enrichContext = true;
111
+ merged.ai.background = true;
112
+ } else {
113
+ const envAi = isAiEnabledViaEnv();
114
+ if (envAi === true) {
115
+ merged.ai.enabled = true;
116
+ } else if (envAi === false) {
117
+ merged.ai.enabled = false;
118
+ } else if (options.upgrade && hasAnyAiKey() && userConfig.ai?.enabled === undefined) {
119
+ merged.ai.enabled = true;
120
+ }
121
+ }
122
+
123
+ applyEnvOverrides(merged);
124
+
125
+ await ensureContextForgeDirs(projectRoot);
126
+ await fs.writeJson(configPath, merged, { spaces: 2 });
127
+ return merged;
128
+ }
129
+
130
+ export async function loadConfig(projectRoot) {
131
+ await loadProjectEnv(projectRoot);
132
+
133
+ const configPath = getConfigPath(projectRoot);
134
+ let userConfig = {};
135
+
136
+ if (await fs.pathExists(configPath)) {
137
+ userConfig = await fs.readJson(configPath);
138
+ }
139
+
140
+ const merged = mergeConfigWithDefaults(userConfig);
141
+ applyEnvOverrides(merged);
142
+ return merged;
143
+ }
144
+
145
+ export async function ensureContextForgeDirs(projectRoot) {
146
+ const root = getContextForgeRoot(projectRoot);
147
+ await fs.ensureDir(root);
148
+ await fs.ensureDir(path.join(root, 'cache'));
149
+ await fs.ensureDir(path.join(root, 'logs'));
150
+ }
151
+
152
+ export async function writeDefaultConfig(projectRoot, options = {}) {
153
+ await ensureContextForgeDirs(projectRoot);
154
+ const configPath = getConfigPath(projectRoot);
155
+
156
+ if (!(await fs.pathExists(configPath))) {
157
+ const config = mergeConfigWithDefaults(options);
158
+ if (options.ai) {
159
+ config.ai = { ...config.ai, ...options.ai };
160
+ }
161
+ applyEnvOverrides(config);
162
+ if (options.enableAi) {
163
+ config.ai.enabled = true;
164
+ }
165
+ await fs.writeJson(configPath, config, { spaces: 2 });
166
+ return config;
167
+ }
168
+
169
+ if (options.ai) {
170
+ const existing = await fs.readJson(configPath);
171
+ await fs.writeJson(
172
+ configPath,
173
+ mergeConfigWithDefaults({ ...existing, ai: { ...existing.ai, ...options.ai } }),
174
+ { spaces: 2 }
175
+ );
176
+ }
177
+
178
+ return mergeConfigWithDefaults(await fs.readJson(configPath));
179
+ }
180
+
181
+ export async function isInitialized(projectRoot) {
182
+ return fs.pathExists(getConfigPath(projectRoot));
183
+ }
@@ -0,0 +1,293 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { getOutputPath, OUTPUT_FILES } from './paths.js';
4
+
5
+ const RULE_FILENAME = 'contextforge.mdc';
6
+
7
+ /**
8
+ * @param {{ description?: string, alwaysApply?: boolean }} meta
9
+ * @param {string} body
10
+ */
11
+ function formatMdc(meta, body) {
12
+ const lines = ['---'];
13
+ if (meta.description) lines.push(`description: ${meta.description}`);
14
+ lines.push(`alwaysApply: ${meta.alwaysApply === true}`);
15
+ lines.push('---', '', body.trim(), '');
16
+ return lines.join('\n');
17
+ }
18
+
19
+ function bullets(items, empty = '_None_') {
20
+ if (!items?.length) return empty;
21
+ return items.map((i) => `- ${i}`).join('\n');
22
+ }
23
+
24
+ function renderAtAGlanceTable(snapshot) {
25
+ const ts = snapshot.techStack || {};
26
+ const arch = snapshot.architecture || {};
27
+ const auth = snapshot.authDeploy || {};
28
+
29
+ const rows = [
30
+ ['Frontend', [...(ts.frontend || []), ...(ts.styling || [])].join(', ') || '—'],
31
+ ['Backend', (ts.backend || []).join(', ') || '—'],
32
+ ['Database / ORM', [...(ts.database || []), ...(ts.orm || [])].join(', ') || '—'],
33
+ ['State', (ts.state || []).join(', ') || '—'],
34
+ [
35
+ 'Auth',
36
+ [...new Set([...(auth.authentication || []), ...(ts.auth || [])])].join(', ') || '—',
37
+ ],
38
+ ['Deploy', [...(auth.deployment || []), ...(ts.deploy || [])].join(', ') || '—'],
39
+ ['Architecture', arch?.primary?.label || '—'],
40
+ ['API style', arch?.apiFlow || snapshot.codeInsights?.pipelineFlow || '—'],
41
+ ];
42
+
43
+ return [
44
+ '| Area | Detected |',
45
+ '|------|----------|',
46
+ ...rows.map(([a, b]) => `| **${a}** | ${b} |`),
47
+ ].join('\n');
48
+ }
49
+
50
+ function collectImportantRules(snapshot) {
51
+ const code = snapshot.codeInsights || {};
52
+ const ai = snapshot.aiEnrichment?.structured || snapshot.aiEnrichment || {};
53
+ const fromCode = code.importantRules || [];
54
+ const fromAi = Array.isArray(ai.importantRules)
55
+ ? ai.importantRules
56
+ : ai.importantRules
57
+ ? [String(ai.importantRules)]
58
+ : [];
59
+ return [...new Set([...fromCode, ...fromAi])].slice(0, 12);
60
+ }
61
+
62
+ function collectConventions(snapshot) {
63
+ const ai = snapshot.aiEnrichment?.structured || snapshot.aiEnrichment || {};
64
+ const fromAi = Array.isArray(ai.codingConventions)
65
+ ? ai.codingConventions
66
+ : ai.codingConventions
67
+ ? [String(ai.codingConventions)]
68
+ : [];
69
+ const patterns = snapshot.codeInsights?.patterns || [];
70
+ return [...new Set([...fromAi, ...patterns])].slice(0, 10);
71
+ }
72
+
73
+ function renderRoutesBrief(endpoints = []) {
74
+ if (!endpoints.length) return null;
75
+ const lines = [
76
+ '| Method | Path | File |',
77
+ '|--------|------|------|',
78
+ ...endpoints.slice(0, 20).map((e) => `| ${e.method} | ${e.path} | \`${e.file}\` |`),
79
+ ];
80
+ if (endpoints.length > 20) {
81
+ lines.push('', `_+ ${endpoints.length - 20} more in \`.contextforge/context.md\`_`);
82
+ }
83
+ return lines.join('\n');
84
+ }
85
+
86
+ function renderServicesBrief(businessLogic = []) {
87
+ if (!businessLogic.length) return null;
88
+ return businessLogic
89
+ .slice(0, 15)
90
+ .map((b) => `- \`${b.name}\` (${b.type}) — \`${b.file}\``)
91
+ .join('\n');
92
+ }
93
+
94
+ /**
95
+ * Build a single Cursor rule file from the pipeline snapshot (same source as context.md).
96
+ * @param {object} snapshot
97
+ * @returns {{ filename: string, content: string }[]}
98
+ */
99
+ export function buildCursorRuleFiles(snapshot) {
100
+ const projectName = snapshot.projectName || 'project';
101
+ const generatedAt = snapshot.generatedAt || new Date().toISOString();
102
+ const ts = snapshot.techStack || {};
103
+ const sections = [];
104
+
105
+ sections.push(
106
+ `# ContextForge — ${projectName}`,
107
+ '',
108
+ `_Synced from \`.contextforge/context.md\` (${generatedAt})_`,
109
+ '',
110
+ 'Before answering questions about this codebase, **read [`.contextforge/context.md`](.contextforge/context.md)** for full detail.',
111
+ '',
112
+ '## At a Glance',
113
+ '',
114
+ renderAtAGlanceTable(snapshot),
115
+ '',
116
+ '## Commands',
117
+ '',
118
+ '- Regenerate: `npx contextforge generate`',
119
+ '- Watch: `npx contextforge watch`',
120
+ '- Changes: `npx contextforge changes` or `.contextforge/CHANGES.md`',
121
+ '- Timer: `CONTEXTFORGE_REFRESH_INTERVAL_MINUTES` in `.env` (e.g. 15)'
122
+ );
123
+
124
+ const importantRules = collectImportantRules(snapshot);
125
+ if (importantRules.length) {
126
+ sections.push('', '## Important rules', '', bullets(importantRules));
127
+ }
128
+
129
+ const conventions = collectConventions(snapshot);
130
+ if (conventions.length) {
131
+ sections.push('', '## Patterns & conventions', '', bullets(conventions));
132
+ }
133
+
134
+ const topIssues = (snapshot.bugs?.issues || []).slice(0, 5);
135
+ if (topIssues.length) {
136
+ sections.push(
137
+ '',
138
+ '## Watch for (static analysis)',
139
+ '',
140
+ ...topIssues.map(
141
+ (i) => `- **[${i.severity}]** ${i.message}${i.file ? ` — \`${i.file}\`` : ''}`
142
+ )
143
+ );
144
+ }
145
+
146
+ const endpoints = snapshot.codeInsights?.apiEndpoints || [];
147
+ const routeTable = renderRoutesBrief(endpoints);
148
+ if (routeTable) {
149
+ sections.push(
150
+ '',
151
+ '## API routes',
152
+ '',
153
+ snapshot.architecture?.apiFlow ? `**Flow:** ${snapshot.architecture.apiFlow}` : '',
154
+ '',
155
+ routeTable
156
+ );
157
+ }
158
+
159
+ const businessLogic = snapshot.codeInsights?.businessLogic || [];
160
+ const serviceList = renderServicesBrief(businessLogic);
161
+ if (serviceList || snapshot.codeInsights?.summary) {
162
+ sections.push('', '## Business logic');
163
+ if (snapshot.codeInsights?.summary) {
164
+ sections.push('', snapshot.codeInsights.summary);
165
+ }
166
+ if (serviceList) {
167
+ sections.push('', serviceList);
168
+ }
169
+ }
170
+
171
+ if (ts.frontend?.length || ts.styling?.length) {
172
+ sections.push(
173
+ '',
174
+ '## Frontend',
175
+ '',
176
+ `**Stack:** ${[...(ts.frontend || []), ...(ts.styling || []), ...(ts.state || [])].join(', ') || '—'}`
177
+ );
178
+ }
179
+
180
+ sections.push(
181
+ '_More detail in `.contextforge/context.md` (architecture, git, database, full issue list)._'
182
+ );
183
+
184
+ return [
185
+ {
186
+ filename: RULE_FILENAME,
187
+ content: formatMdc(
188
+ {
189
+ description: 'ContextForge project memory — read before coding',
190
+ alwaysApply: true,
191
+ },
192
+ sections.join('\n\n')
193
+ ),
194
+ },
195
+ ];
196
+ }
197
+
198
+ /**
199
+ * @param {string} projectRoot
200
+ * @param {object} [snapshot]
201
+ */
202
+ export async function installCursorRules(projectRoot, snapshot = null) {
203
+ const cursorDir = path.join(projectRoot, '.cursor', 'rules');
204
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
205
+
206
+ if (!snapshot) {
207
+ const jsonPath = getOutputPath(projectRoot, OUTPUT_FILES.contextJson);
208
+ if (await fs.pathExists(jsonPath)) {
209
+ snapshot = await fs.readJson(jsonPath);
210
+ }
211
+ }
212
+
213
+ await fs.ensureDir(cursorDir);
214
+
215
+ const ruleFiles = snapshot ? buildCursorRuleFiles(snapshot) : [buildFallbackRule()];
216
+
217
+ const written = [];
218
+ for (const { filename, content } of ruleFiles) {
219
+ const rulePath = path.join(cursorDir, filename);
220
+ await fs.writeFile(rulePath, content, 'utf8');
221
+ written.push(rulePath);
222
+ }
223
+
224
+ await pruneStaleRules(cursorDir, ruleFiles.map((r) => r.filename));
225
+
226
+ await syncAgentsMd(agentsPath);
227
+
228
+ return { rulePaths: written, agentsPath };
229
+ }
230
+
231
+ function buildFallbackRule() {
232
+ return {
233
+ filename: RULE_FILENAME,
234
+ content: formatMdc(
235
+ {
236
+ description: 'ContextForge project context — read before coding',
237
+ alwaysApply: true,
238
+ },
239
+ `# ContextForge — Project Memory
240
+
241
+ Before answering questions about this codebase, **read \`.contextforge/context.md\`** in full.
242
+
243
+ Regenerate context: \`npx contextforge generate\`
244
+ Watch mode: \`npx contextforge watch\`
245
+ Change history: \`npx contextforge changes\` or \`.contextforge/CHANGES.md\``
246
+ ),
247
+ };
248
+ }
249
+
250
+ /** Remove old split contextforge-*.mdc files from previous versions. */
251
+ async function pruneStaleRules(cursorDir, keepFilenames) {
252
+ const keep = new Set(keepFilenames);
253
+ let entries = [];
254
+ try {
255
+ entries = await fs.readdir(cursorDir);
256
+ } catch {
257
+ return;
258
+ }
259
+ for (const name of entries) {
260
+ if (!name.startsWith('contextforge') || !name.endsWith('.mdc')) continue;
261
+ if (!keep.has(name)) {
262
+ await fs.remove(path.join(cursorDir, name));
263
+ }
264
+ }
265
+ }
266
+
267
+ async function syncAgentsMd(agentsPath) {
268
+ const agentsSnippet = `## ContextForge
269
+
270
+ Read [\`.contextforge/context.md\`](.contextforge/context.md) before working on this repository.
271
+ Cursor rule [\`.cursor/rules/contextforge.mdc\`](.cursor/rules/contextforge.mdc) is synced from the latest generate run.
272
+
273
+ `;
274
+
275
+ if (await fs.pathExists(agentsPath)) {
276
+ const content = await fs.readFile(agentsPath, 'utf8');
277
+ if (!content.includes('contextforge/context.md')) {
278
+ await fs.writeFile(agentsPath, agentsSnippet + content, 'utf8');
279
+ } else if (!content.includes('contextforge.mdc')) {
280
+ const updated = content.replace(
281
+ /Cursor rules in \[`\.cursor\/rules\/`\]\(\.cursor\/rules\/\) are synced from the latest generate run\./,
282
+ 'Cursor rule [`.cursor/rules/contextforge.mdc`](.cursor/rules/contextforge.mdc) is synced from the latest generate run.'
283
+ );
284
+ if (updated !== content) await fs.writeFile(agentsPath, updated, 'utf8');
285
+ }
286
+ } else {
287
+ await fs.writeFile(
288
+ agentsPath,
289
+ `# Agent Instructions\n\n${agentsSnippet}`,
290
+ 'utf8'
291
+ );
292
+ }
293
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ writeDefaultConfig,
3
+ isInitialized,
4
+ syncConfigFile,
5
+ ensureContextForgeDirs,
6
+ } from './config.js';
7
+ import { ensureEnvExample, ensureEnvFromExample } from './setup-env.js';
8
+ import { ensureProjectPrompts } from './setup-prompts.js';
9
+ import { getContextForgeRoot } from './paths.js';
10
+
11
+ /**
12
+ * Create `.contextforge/` and config when missing (used by generate/watch).
13
+ * @param {string} projectRoot
14
+ * @param {{ enableAi?: boolean, quiet?: boolean }} [options]
15
+ * @returns {Promise<boolean>} true if setup was created now
16
+ */
17
+ export async function ensureInitialized(projectRoot, options = {}) {
18
+ if (await isInitialized(projectRoot)) {
19
+ await ensureContextForgeDirs(projectRoot);
20
+ return false;
21
+ }
22
+
23
+ if (!options.quiet) {
24
+ console.log('ContextForge not set up — creating `.contextforge/` automatically...');
25
+ }
26
+
27
+ const initOptions = {};
28
+ if (options.enableAi) {
29
+ initOptions.ai = { enabled: true, enrichContext: true, background: true };
30
+ }
31
+
32
+ await writeDefaultConfig(projectRoot, initOptions);
33
+ await syncConfigFile(projectRoot, { enableAi: options.enableAi, upgrade: true });
34
+ await ensureProjectPrompts(projectRoot);
35
+ await ensureEnvExample(projectRoot);
36
+ await ensureEnvFromExample(projectRoot);
37
+ await ensureContextForgeDirs(projectRoot);
38
+
39
+ if (!options.quiet) {
40
+ console.log(` → ${getContextForgeRoot(projectRoot)}/`);
41
+ console.log(' Tip: run `npx contextforge watch` to keep context.md updated while you code');
42
+ }
43
+
44
+ return true;
45
+ }
@@ -0,0 +1,113 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import dotenv from 'dotenv';
4
+
5
+ /**
6
+ * Load environment variables from project .env (never committed).
7
+ * @param {string} projectRoot
8
+ */
9
+ export async function loadProjectEnv(projectRoot) {
10
+ const root = path.resolve(projectRoot);
11
+ const envPath = path.join(root, '.env');
12
+ const examplePath = path.join(root, '.env.example');
13
+
14
+ if (await fs.pathExists(envPath)) {
15
+ const result = dotenv.config({ path: envPath, override: true });
16
+ if (result.error) {
17
+ console.warn(`[contextforge] Warning: could not parse .env — ${result.error.message}`);
18
+ }
19
+ } else if (await fs.pathExists(examplePath)) {
20
+ dotenv.config({ path: examplePath, override: true });
21
+ }
22
+ }
23
+
24
+ export function getApiKey(provider) {
25
+ if (provider === 'groq') {
26
+ return (
27
+ process.env.GROQ_API_KEY ||
28
+ process.env.CONTEXTFORGE_GROQ_API_KEY ||
29
+ ''
30
+ ).trim();
31
+ }
32
+ return (
33
+ process.env.OPENAI_API_KEY ||
34
+ process.env.CONTEXTFORGE_OPENAI_API_KEY ||
35
+ ''
36
+ ).trim();
37
+ }
38
+
39
+ export function getEnvModel(provider, defaultModel) {
40
+ if (provider === 'groq') {
41
+ return (process.env.GROQ_MODEL || defaultModel).trim();
42
+ }
43
+ return (process.env.OPENAI_MODEL || defaultModel).trim();
44
+ }
45
+
46
+ function parseEnvBool(value) {
47
+ if (value === 'true' || value === '1') return true;
48
+ if (value === 'false' || value === '0') return false;
49
+ return null;
50
+ }
51
+
52
+ export function isAiEnabledViaEnv() {
53
+ return parseEnvBool(process.env.CONTEXTFORGE_AI_ENABLED);
54
+ }
55
+
56
+ /** Master switch: auto setup + background watch on `npm install` (default: on). */
57
+ export function isAutoStartEnabledViaEnv() {
58
+ if (process.env.CONTEXTFORGE_SKIP_POSTINSTALL === '1') return false;
59
+ const v = parseEnvBool(process.env.CONTEXTFORGE_AUTO_START);
60
+ if (v !== null) return v;
61
+ return true;
62
+ }
63
+
64
+ export function isPostinstallGenerateEnabledViaEnv() {
65
+ const v = parseEnvBool(process.env.CONTEXTFORGE_POSTINSTALL_GENERATE);
66
+ if (v !== null) return v;
67
+ return true;
68
+ }
69
+
70
+ export function isPostinstallWatchEnabledViaEnv() {
71
+ const v = parseEnvBool(process.env.CONTEXTFORGE_POSTINSTALL_WATCH);
72
+ if (v !== null) return v;
73
+ return true;
74
+ }
75
+
76
+ export function hasAnyAiKey() {
77
+ return !!(getApiKey('openai') || getApiKey('groq'));
78
+ }
79
+
80
+ function parseNonNegativeNumber(value) {
81
+ if (value === undefined || value === null || String(value).trim() === '') {
82
+ return null;
83
+ }
84
+ const n = parseFloat(String(value).trim());
85
+ if (Number.isNaN(n) || n < 0) return null;
86
+ return n;
87
+ }
88
+
89
+ /**
90
+ * Auto-refresh interval for `watch` (minutes). 0 = only on file changes.
91
+ * Env: CONTEXTFORGE_REFRESH_INTERVAL_MINUTES, CONTEXTFORGE_WATCH_INTERVAL_MINUTES
92
+ * Or seconds: CONTEXTFORGE_REFRESH_INTERVAL_SECONDS
93
+ */
94
+ export function getRefreshIntervalMinutesFromEnv() {
95
+ const minutes = parseNonNegativeNumber(
96
+ process.env.CONTEXTFORGE_REFRESH_INTERVAL_MINUTES ??
97
+ process.env.CONTEXTFORGE_WATCH_INTERVAL_MINUTES
98
+ );
99
+ if (minutes !== null) return minutes;
100
+
101
+ const seconds = parseNonNegativeNumber(
102
+ process.env.CONTEXTFORGE_REFRESH_INTERVAL_SECONDS
103
+ );
104
+ if (seconds !== null) return seconds / 60;
105
+
106
+ return null;
107
+ }
108
+
109
+ /** File-change debounce for watch (milliseconds). Env: CONTEXTFORGE_DEBOUNCE_MS */
110
+ export function getDebounceMsFromEnv() {
111
+ const ms = parseNonNegativeNumber(process.env.CONTEXTFORGE_DEBOUNCE_MS);
112
+ return ms !== null ? Math.round(ms) : null;
113
+ }
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const PACKAGE_ROOT = path.join(__dirname, '../..');
7
+
8
+ /** Published npm package name (must match package.json "name"). */
9
+ export const NPM_PACKAGE_NAME = 'contextforge-cli-ai-prompt-pirates';
10
+
11
+ /** Older / alternate names still recognized in consumer package.json. */
12
+ export const LEGACY_NPM_NAMES = ['contextforge', '@contextforge/core'];
13
+
14
+ let cachedOwnName = null;
15
+
16
+ export async function getOwnPackageName() {
17
+ if (cachedOwnName) return cachedOwnName;
18
+ try {
19
+ const pkg = await fs.readJson(path.join(PACKAGE_ROOT, 'package.json'));
20
+ cachedOwnName = pkg.name || NPM_PACKAGE_NAME;
21
+ } catch {
22
+ cachedOwnName = NPM_PACKAGE_NAME;
23
+ }
24
+ return cachedOwnName;
25
+ }
26
+
27
+ /**
28
+ * @param {Record<string, string>} deps merged dependencies from consumer package.json
29
+ * @param {string} consumerPackageName
30
+ */
31
+ export function hasContextForgeDependency(deps, consumerPackageName, ownName) {
32
+ const keys = [ownName, NPM_PACKAGE_NAME, ...LEGACY_NPM_NAMES];
33
+ if (keys.some((k) => deps[k])) return true;
34
+ return keys.includes(consumerPackageName);
35
+ }
@@ -0,0 +1,39 @@
1
+ import path from 'node:path';
2
+
3
+ export const CONTEXTFORGE_DIR = '.contextforge';
4
+
5
+ export function getContextForgeRoot(projectRoot) {
6
+ return path.join(projectRoot, CONTEXTFORGE_DIR);
7
+ }
8
+
9
+ export function getConfigPath(projectRoot) {
10
+ return path.join(getContextForgeRoot(projectRoot), 'config.json');
11
+ }
12
+
13
+ export function getCacheDir(projectRoot) {
14
+ return path.join(getContextForgeRoot(projectRoot), 'cache');
15
+ }
16
+
17
+ export function getLogsDir(projectRoot) {
18
+ return path.join(getContextForgeRoot(projectRoot), 'logs');
19
+ }
20
+
21
+ export function getFileIndexPath(projectRoot) {
22
+ return path.join(getCacheDir(projectRoot), 'file-index.json');
23
+ }
24
+
25
+ export function getPidPath(projectRoot) {
26
+ return path.join(getContextForgeRoot(projectRoot), '.watch.pid');
27
+ }
28
+
29
+ export const OUTPUT_FILES = {
30
+ contextMd: 'context.md',
31
+ contextJson: 'context.json',
32
+ architectureJson: 'architecture.json',
33
+ gitInsightsJson: 'git-insights.json',
34
+ bugsReportJson: 'bugs-report.json',
35
+ };
36
+
37
+ export function getOutputPath(projectRoot, filename) {
38
+ return path.join(getContextForgeRoot(projectRoot), filename);
39
+ }