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,32 @@
1
+ import OpenAI from 'openai';
2
+ import { getApiKey } from '../../../core/env.js';
3
+
4
+ /**
5
+ * @param {{ model: string, maxTokens: number, timeoutMs: number }} options
6
+ */
7
+ export async function callOpenAI(messages, options) {
8
+ const apiKey = getApiKey('openai');
9
+ if (!apiKey) {
10
+ throw new Error('OPENAI_API_KEY is not set in .env');
11
+ }
12
+
13
+ const client = new OpenAI({
14
+ apiKey,
15
+ timeout: options.timeoutMs,
16
+ });
17
+
18
+ const response = await client.chat.completions.create({
19
+ model: options.model,
20
+ messages,
21
+ max_tokens: options.maxTokens,
22
+ temperature: 0.3,
23
+ response_format: { type: 'json_object' },
24
+ });
25
+
26
+ return {
27
+ content: response.choices[0]?.message?.content || '',
28
+ model: response.model,
29
+ provider: 'openai',
30
+ usage: response.usage,
31
+ };
32
+ }
@@ -0,0 +1,33 @@
1
+ const SECRET_PATTERNS = [
2
+ /sk-[a-zA-Z0-9]{20,}/g,
3
+ /gsk_[a-zA-Z0-9]{20,}/g,
4
+ /api[_-]?key\s*[:=]\s*['"]?[\w-]+/gi,
5
+ /password\s*[:=]\s*['"]?[^\s'"]+/gi,
6
+ /Bearer\s+[a-zA-Z0-9._-]+/g,
7
+ ];
8
+
9
+ /**
10
+ * Redact secrets from text before sending to AI.
11
+ * @param {string} text
12
+ */
13
+ export function redactSecrets(text) {
14
+ let out = text;
15
+ for (const re of SECRET_PATTERNS) {
16
+ out = out.replace(re, '[REDACTED]');
17
+ }
18
+ return out;
19
+ }
20
+
21
+ /**
22
+ * Sanitize snapshot clone for AI — no paths with .env etc.
23
+ */
24
+ export function sanitizeSnapshotForAi(snapshot) {
25
+ const clone = JSON.parse(JSON.stringify(snapshot));
26
+ delete clone.projectRoot;
27
+ if (clone.configFiles) {
28
+ clone.configFiles = clone.configFiles.filter(
29
+ (p) => !/\.env(?!\.example)/i.test(p) && !/secret|credential|\.pem/i.test(p)
30
+ );
31
+ }
32
+ return redactSecrets(JSON.stringify(clone));
33
+ }
@@ -0,0 +1,70 @@
1
+ const REQUIRED_KEYS = [
2
+ 'executiveSummary',
3
+ 'architectureDeepDive',
4
+ 'apiAndDataFlow',
5
+ 'businessLogicAnalysis',
6
+ 'importantRules',
7
+ ];
8
+
9
+ const OPTIONAL_KEYS = [
10
+ 'techStackNarrative',
11
+ 'databaseAnalysis',
12
+ 'authenticationNotes',
13
+ 'deploymentNotes',
14
+ 'gitEvolution',
15
+ 'codingConventions',
16
+ 'possibleIssues',
17
+ 'suggestedImprovements',
18
+ ];
19
+
20
+ /**
21
+ * Validate AI JSON response structure and content sanity.
22
+ * @param {object} parsed
23
+ * @param {object} snapshot
24
+ */
25
+ export function validateAiSections(parsed, snapshot) {
26
+ const errors = [];
27
+
28
+ if (!parsed || typeof parsed !== 'object') {
29
+ return { valid: false, errors: ['Response is not an object'] };
30
+ }
31
+
32
+ for (const key of REQUIRED_KEYS) {
33
+ if (!parsed[key] || String(parsed[key]).trim().length < 10) {
34
+ errors.push(`Missing or too short: ${key}`);
35
+ }
36
+ }
37
+
38
+ const scanTech = [
39
+ ...(snapshot.techStack?.frontend || []),
40
+ ...(snapshot.techStack?.backend || []),
41
+ ].map((t) => t.toLowerCase());
42
+
43
+ const narrative = JSON.stringify(parsed).toLowerCase();
44
+ const hallucinations = ['django', 'laravel', 'spring boot', 'ruby on rails'].filter(
45
+ (fw) => narrative.includes(fw) && !scanTech.some((t) => t.includes(fw.split(' ')[0]))
46
+ );
47
+
48
+ if (hallucinations.length) {
49
+ errors.push(`Possible hallucinated stack: ${hallucinations.join(', ')}`);
50
+ }
51
+
52
+ if (parsed.importantRules && !Array.isArray(parsed.importantRules)) {
53
+ parsed.importantRules = [String(parsed.importantRules)];
54
+ }
55
+ if (parsed.suggestedImprovements && !Array.isArray(parsed.suggestedImprovements)) {
56
+ parsed.suggestedImprovements = [String(parsed.suggestedImprovements)];
57
+ }
58
+ if (parsed.codingConventions && !Array.isArray(parsed.codingConventions)) {
59
+ parsed.codingConventions = [String(parsed.codingConventions)];
60
+ }
61
+ if (parsed.possibleIssues && !Array.isArray(parsed.possibleIssues)) {
62
+ parsed.possibleIssues = [String(parsed.possibleIssues)];
63
+ }
64
+
65
+ return {
66
+ valid: errors.length === 0,
67
+ errors,
68
+ parsed,
69
+ };
70
+ }
@@ -0,0 +1,128 @@
1
+ import chokidar from 'chokidar';
2
+ import path from 'node:path';
3
+ import fs from 'fs-extra';
4
+ import { runPipeline } from '../core/pipeline.js';
5
+ import { loadConfig } from '../core/config.js';
6
+ import { getPidPath } from '../core/paths.js';
7
+ import { detectFileChanges } from '../scanner/change-detector.js';
8
+
9
+ let debounceTimer = null;
10
+ let intervalTimer = null;
11
+ let running = false;
12
+
13
+ export async function startWatcher(projectRoot, options = {}) {
14
+ const { verbose = false } = options;
15
+ const config = await loadConfig(projectRoot);
16
+
17
+ const intervalMinutes =
18
+ options.intervalMinutes ??
19
+ config.refreshIntervalMinutes ??
20
+ 0;
21
+
22
+ const watchPaths = ['.'];
23
+ const ignored = config.ignore.map((p) => {
24
+ if (p.startsWith('.')) return `**/${p}/**`;
25
+ return `**/${p}/**`;
26
+ });
27
+
28
+ if (options.daemon) {
29
+ await fs.writeFile(getPidPath(projectRoot), String(process.pid));
30
+ }
31
+
32
+ const run = async (reason = 'change') => {
33
+ if (running) return;
34
+ running = true;
35
+ try {
36
+ if (reason === 'scheduled') {
37
+ const check = await detectFileChanges(projectRoot, config);
38
+ if (!check.hasChanges) {
39
+ console.log(' [timer] No file/code changes — context is up to date');
40
+ return;
41
+ }
42
+ console.log(
43
+ ` [timer] File/code changes detected (+${check.counts.added} ~${check.counts.modified} -${check.counts.removed}) — updating...`
44
+ );
45
+ } else if (reason === 'file-change') {
46
+ console.log(' File saved — scanning for code changes...');
47
+ }
48
+
49
+ if (verbose) console.log(`[contextforge] Regenerating (${reason})...`);
50
+ const snapshot = await runPipeline(projectRoot, { verbose });
51
+ const ch = snapshot.changelog;
52
+ if (ch) {
53
+ if (ch.hasChanges) {
54
+ console.log(` Changes: ${ch.summary}`);
55
+ if (ch.addedFiles?.length) console.log(` + ${ch.addedFiles.length} new file(s)`);
56
+ if (ch.modifiedFiles?.length) console.log(` ~ ${ch.modifiedFiles.length} modified file(s)`);
57
+ } else {
58
+ console.log(' No changes since last run.');
59
+ }
60
+ }
61
+ } catch (err) {
62
+ console.error('Regeneration failed:', err.message);
63
+ } finally {
64
+ running = false;
65
+ }
66
+ };
67
+
68
+ console.log('Watching for changes... (Ctrl+C to stop)');
69
+ console.log(` File debounce: ${config.debounceMs || 2000}ms`);
70
+ if (intervalMinutes > 0) {
71
+ console.log(` Auto-refresh every: ${intervalMinutes} minute(s)`);
72
+ await run('initial');
73
+ intervalTimer = setInterval(
74
+ () => run('scheduled'),
75
+ intervalMinutes * 60 * 1000
76
+ );
77
+ }
78
+
79
+ const watcher = chokidar.watch(watchPaths, {
80
+ cwd: projectRoot,
81
+ ignored: [...ignored, '**/.contextforge/cache/**', '**/.contextforge/logs/**'],
82
+ persistent: true,
83
+ ignoreInitial: true,
84
+ awaitWriteFinish: {
85
+ stabilityThreshold: 400,
86
+ pollInterval: 100,
87
+ },
88
+ });
89
+
90
+ const schedule = () => {
91
+ clearTimeout(debounceTimer);
92
+ debounceTimer = setTimeout(() => run('file-change'), config.debounceMs || 2000);
93
+ };
94
+
95
+ watcher.on('add', schedule);
96
+ watcher.on('change', schedule);
97
+ watcher.on('unlink', schedule);
98
+
99
+ const cleanup = async () => {
100
+ clearTimeout(debounceTimer);
101
+ if (intervalTimer) clearInterval(intervalTimer);
102
+ await watcher.close();
103
+ await fs.remove(getPidPath(projectRoot)).catch(() => {});
104
+ };
105
+
106
+ process.on('SIGINT', async () => {
107
+ await cleanup();
108
+ process.exit(0);
109
+ });
110
+
111
+ return { watcher, cleanup };
112
+ }
113
+
114
+ export async function stopWatcher(projectRoot) {
115
+ const pidPath = getPidPath(projectRoot);
116
+ if (!(await fs.pathExists(pidPath))) {
117
+ throw new Error('No watcher PID file found. Is watch running with --daemon?');
118
+ }
119
+ const pid = parseInt(await fs.readFile(pidPath, 'utf8'), 10);
120
+ try {
121
+ process.kill(pid, 'SIGTERM');
122
+ await fs.remove(pidPath);
123
+ console.log(`Stopped watcher (PID ${pid})`);
124
+ } catch (err) {
125
+ await fs.remove(pidPath);
126
+ throw new Error(`Could not stop process ${pid}: ${err.message}`);
127
+ }
128
+ }