contextforge-cli-ai-prompt-pirates 0.4.0 → 0.5.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/package.json +3 -2
- package/src/core/cursor-rules.js +84 -23
- package/src/core/pipeline.js +8 -2
- package/src/services/ai/enrich.js +15 -0
- package/src/services/ai/prompt.js +4 -1
- package/src/services/ai/providers/openai.js +32 -5
- package/.contextforge/config.ai.example.json +0 -28
- package/.contextforge/config.example.json +0 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contextforge-cli-ai-prompt-pirates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Background AI context engine — auto-generates context.md on npm install for Cursor, Claude, and ChatGPT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -52,14 +52,15 @@
|
|
|
52
52
|
"@babel/parser": "^7.26.3",
|
|
53
53
|
"chokidar": "^4.0.3",
|
|
54
54
|
"commander": "^13.1.0",
|
|
55
|
+
"dotenv": "^16.4.7",
|
|
55
56
|
"fs-extra": "^11.3.0",
|
|
56
57
|
"glob": "^11.0.1",
|
|
57
58
|
"ignore": "^7.0.3",
|
|
58
|
-
"dotenv": "^16.4.7",
|
|
59
59
|
"openai": "^4.77.0",
|
|
60
60
|
"simple-git": "^3.27.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
+
"contextforge-cli-ai-prompt-pirates": "^0.4.0",
|
|
63
64
|
"vitest": "^3.0.5"
|
|
64
65
|
}
|
|
65
66
|
}
|
package/src/core/cursor-rules.js
CHANGED
|
@@ -3,6 +3,20 @@ import path from 'node:path';
|
|
|
3
3
|
import { getOutputPath, OUTPUT_FILES } from './paths.js';
|
|
4
4
|
|
|
5
5
|
const RULE_FILENAME = 'contextforge.mdc';
|
|
6
|
+
/** Max chars of context.md embedded in .mdc (Cursor rule size limits). */
|
|
7
|
+
const MAX_EMBEDDED_CONTEXT_CHARS = 14000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create `.cursor/` and `.cursor/rules/` if they do not exist.
|
|
11
|
+
* @param {string} projectRoot
|
|
12
|
+
* @returns {Promise<{ cursorDir: string, created: boolean }>}
|
|
13
|
+
*/
|
|
14
|
+
export async function ensureCursorRulesDir(projectRoot) {
|
|
15
|
+
const cursorDir = path.join(projectRoot, '.cursor', 'rules');
|
|
16
|
+
const existed = await fs.pathExists(cursorDir);
|
|
17
|
+
await fs.ensureDir(cursorDir);
|
|
18
|
+
return { cursorDir, created: !existed };
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
/**
|
|
8
22
|
* @param {{ description?: string, alwaysApply?: boolean }} meta
|
|
@@ -78,7 +92,7 @@ function renderRoutesBrief(endpoints = []) {
|
|
|
78
92
|
...endpoints.slice(0, 20).map((e) => `| ${e.method} | ${e.path} | \`${e.file}\` |`),
|
|
79
93
|
];
|
|
80
94
|
if (endpoints.length > 20) {
|
|
81
|
-
lines.push('', `_+ ${endpoints.length - 20} more in
|
|
95
|
+
lines.push('', `_+ ${endpoints.length - 20} more in full context below._`);
|
|
82
96
|
}
|
|
83
97
|
return lines.join('\n');
|
|
84
98
|
}
|
|
@@ -92,11 +106,30 @@ function renderServicesBrief(businessLogic = []) {
|
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
/**
|
|
95
|
-
*
|
|
109
|
+
* @param {string} projectRoot
|
|
110
|
+
* @param {{ maxChars?: number }} [options]
|
|
111
|
+
*/
|
|
112
|
+
export async function readContextMarkdownForRule(projectRoot, options = {}) {
|
|
113
|
+
const maxChars = options.maxChars ?? MAX_EMBEDDED_CONTEXT_CHARS;
|
|
114
|
+
const mdPath = getOutputPath(projectRoot, OUTPUT_FILES.contextMd);
|
|
115
|
+
if (!(await fs.pathExists(mdPath))) return null;
|
|
116
|
+
|
|
117
|
+
let md = await fs.readFile(mdPath, 'utf8');
|
|
118
|
+
const truncated = md.length > maxChars;
|
|
119
|
+
if (truncated) {
|
|
120
|
+
md =
|
|
121
|
+
md.slice(0, maxChars) +
|
|
122
|
+
'\n\n---\n\n_…truncated. Full file: `.contextforge/context.md`._';
|
|
123
|
+
}
|
|
124
|
+
return { content: md, truncated, sourcePath: '.contextforge/context.md' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build rule body (markdown inside .mdc).
|
|
96
129
|
* @param {object} snapshot
|
|
97
|
-
* @
|
|
130
|
+
* @param {string|null} embeddedContextMd
|
|
98
131
|
*/
|
|
99
|
-
export function
|
|
132
|
+
export function buildCursorRuleBody(snapshot, embeddedContextMd = null) {
|
|
100
133
|
const projectName = snapshot.projectName || 'project';
|
|
101
134
|
const generatedAt = snapshot.generatedAt || new Date().toISOString();
|
|
102
135
|
const ts = snapshot.techStack || {};
|
|
@@ -107,18 +140,15 @@ export function buildCursorRuleFiles(snapshot) {
|
|
|
107
140
|
'',
|
|
108
141
|
`_Synced from \`.contextforge/context.md\` (${generatedAt})_`,
|
|
109
142
|
'',
|
|
110
|
-
'Before answering questions about this codebase, **read [`.contextforge/context.md`](.contextforge/context.md)** for full detail.',
|
|
111
|
-
'',
|
|
112
143
|
'## At a Glance',
|
|
113
144
|
'',
|
|
114
145
|
renderAtAGlanceTable(snapshot),
|
|
115
146
|
'',
|
|
116
147
|
'## Commands',
|
|
117
148
|
'',
|
|
118
|
-
'- Regenerate: `npx contextforge generate`',
|
|
119
|
-
'- Watch: `npx
|
|
120
|
-
'- Changes: `npx
|
|
121
|
-
'- Timer: `CONTEXTFORGE_REFRESH_INTERVAL_MINUTES` in `.env` (e.g. 15)'
|
|
149
|
+
'- Regenerate: `npx cfpirates generate` or `npx contextforge-cli-ai-prompt-pirates generate`',
|
|
150
|
+
'- Watch: `npx cfpirates watch`',
|
|
151
|
+
'- Changes: `npx cfpirates changes`'
|
|
122
152
|
);
|
|
123
153
|
|
|
124
154
|
const importantRules = collectImportantRules(snapshot);
|
|
@@ -177,10 +207,33 @@ export function buildCursorRuleFiles(snapshot) {
|
|
|
177
207
|
);
|
|
178
208
|
}
|
|
179
209
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
210
|
+
if (embeddedContextMd) {
|
|
211
|
+
sections.push(
|
|
212
|
+
'',
|
|
213
|
+
'---',
|
|
214
|
+
'',
|
|
215
|
+
'## Full project context (from context.md)',
|
|
216
|
+
'',
|
|
217
|
+
embeddedContextMd
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
sections.push(
|
|
221
|
+
'',
|
|
222
|
+
'_Full detail: [`.contextforge/context.md`](.contextforge/context.md)._'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return sections.join('\n\n');
|
|
227
|
+
}
|
|
183
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Build a single Cursor rule file from the pipeline snapshot.
|
|
231
|
+
* @param {object} snapshot
|
|
232
|
+
* @param {string|null} [embeddedContextMd]
|
|
233
|
+
* @returns {{ filename: string, content: string }[]}
|
|
234
|
+
*/
|
|
235
|
+
export function buildCursorRuleFiles(snapshot, embeddedContextMd = null) {
|
|
236
|
+
const body = buildCursorRuleBody(snapshot, embeddedContextMd);
|
|
184
237
|
return [
|
|
185
238
|
{
|
|
186
239
|
filename: RULE_FILENAME,
|
|
@@ -189,7 +242,7 @@ export function buildCursorRuleFiles(snapshot) {
|
|
|
189
242
|
description: 'ContextForge project memory — read before coding',
|
|
190
243
|
alwaysApply: true,
|
|
191
244
|
},
|
|
192
|
-
|
|
245
|
+
body
|
|
193
246
|
),
|
|
194
247
|
},
|
|
195
248
|
];
|
|
@@ -198,9 +251,10 @@ export function buildCursorRuleFiles(snapshot) {
|
|
|
198
251
|
/**
|
|
199
252
|
* @param {string} projectRoot
|
|
200
253
|
* @param {object} [snapshot]
|
|
254
|
+
* @param {{ embedContextMd?: boolean }} [options]
|
|
201
255
|
*/
|
|
202
|
-
export async function installCursorRules(projectRoot, snapshot = null) {
|
|
203
|
-
const
|
|
256
|
+
export async function installCursorRules(projectRoot, snapshot = null, options = {}) {
|
|
257
|
+
const embedContextMd = options.embedContextMd !== false;
|
|
204
258
|
const agentsPath = path.join(projectRoot, 'AGENTS.md');
|
|
205
259
|
|
|
206
260
|
if (!snapshot) {
|
|
@@ -210,9 +264,17 @@ export async function installCursorRules(projectRoot, snapshot = null) {
|
|
|
210
264
|
}
|
|
211
265
|
}
|
|
212
266
|
|
|
213
|
-
await
|
|
267
|
+
const { cursorDir, created } = await ensureCursorRulesDir(projectRoot);
|
|
268
|
+
|
|
269
|
+
let embeddedMd = null;
|
|
270
|
+
if (embedContextMd && snapshot) {
|
|
271
|
+
const loaded = await readContextMarkdownForRule(projectRoot);
|
|
272
|
+
if (loaded) embeddedMd = loaded.content;
|
|
273
|
+
}
|
|
214
274
|
|
|
215
|
-
const ruleFiles = snapshot
|
|
275
|
+
const ruleFiles = snapshot
|
|
276
|
+
? buildCursorRuleFiles(snapshot, embeddedMd)
|
|
277
|
+
: [buildFallbackRule()];
|
|
216
278
|
|
|
217
279
|
const written = [];
|
|
218
280
|
for (const { filename, content } of ruleFiles) {
|
|
@@ -225,7 +287,7 @@ export async function installCursorRules(projectRoot, snapshot = null) {
|
|
|
225
287
|
|
|
226
288
|
await syncAgentsMd(agentsPath);
|
|
227
289
|
|
|
228
|
-
return { rulePaths: written, agentsPath };
|
|
290
|
+
return { rulePaths: written, agentsPath, createdCursorDir: created, cursorDir };
|
|
229
291
|
}
|
|
230
292
|
|
|
231
293
|
function buildFallbackRule() {
|
|
@@ -238,11 +300,10 @@ function buildFallbackRule() {
|
|
|
238
300
|
},
|
|
239
301
|
`# ContextForge — Project Memory
|
|
240
302
|
|
|
241
|
-
|
|
303
|
+
Run \`npx cfpirates generate\` to create \`.contextforge/context.md\`, then re-run to sync this rule.
|
|
242
304
|
|
|
243
|
-
Regenerate
|
|
244
|
-
Watch
|
|
245
|
-
Change history: \`npx contextforge changes\` or \`.contextforge/CHANGES.md\``
|
|
305
|
+
Regenerate: \`npx cfpirates generate\`
|
|
306
|
+
Watch: \`npx cfpirates watch\``
|
|
246
307
|
),
|
|
247
308
|
};
|
|
248
309
|
}
|
package/src/core/pipeline.js
CHANGED
|
@@ -203,10 +203,16 @@ export async function runPipeline(projectRoot, options = {}) {
|
|
|
203
203
|
await renderContextMarkdown(projectRootResolved, snapshot);
|
|
204
204
|
|
|
205
205
|
if (config.installCursorRules !== false) {
|
|
206
|
-
const { rulePaths } = await installCursorRules(
|
|
206
|
+
const { rulePaths, createdCursorDir, cursorDir } = await installCursorRules(
|
|
207
|
+
projectRootResolved,
|
|
208
|
+
snapshot
|
|
209
|
+
);
|
|
210
|
+
if (createdCursorDir) {
|
|
211
|
+
log(projectRootResolved, `Created ${cursorDir}/`, verbose);
|
|
212
|
+
}
|
|
207
213
|
log(
|
|
208
214
|
projectRootResolved,
|
|
209
|
-
`Synced ${rulePaths.length} Cursor rule(s)
|
|
215
|
+
`Synced ${rulePaths.length} Cursor rule(s) with embedded context.md`,
|
|
210
216
|
verbose
|
|
211
217
|
);
|
|
212
218
|
}
|
|
@@ -12,6 +12,12 @@ const PROVIDERS = {
|
|
|
12
12
|
|
|
13
13
|
const MAX_RETRIES = 2;
|
|
14
14
|
|
|
15
|
+
function isRateLimitError(err) {
|
|
16
|
+
const status = err?.status ?? err?.response?.status;
|
|
17
|
+
const msg = err?.message || String(err);
|
|
18
|
+
return status === 429 || /429|rate limit|tokens per day/i.test(msg);
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
22
|
* @param {object} snapshot
|
|
17
23
|
* @param {object} config
|
|
@@ -72,11 +78,20 @@ export async function enrichWithAI(snapshot, config) {
|
|
|
72
78
|
};
|
|
73
79
|
} catch (err) {
|
|
74
80
|
lastError = err;
|
|
81
|
+
if (isRateLimitError(err)) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
75
84
|
if (attempt >= MAX_RETRIES) break;
|
|
76
85
|
}
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
if (lastError && isRateLimitError(lastError)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`AI rate limit (${lastError.message}). Wait and retry, or set CONTEXTFORGE_AI_PROVIDER=openai in .env.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
throw lastError || new Error('No AI provider available. Set OPENAI_API_KEY or GROQ_API_KEY in .env');
|
|
81
96
|
}
|
|
82
97
|
|
|
@@ -33,7 +33,10 @@ export function buildEnrichmentPrompt(snapshot, isRetry = false) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function parseAiResponse(text) {
|
|
36
|
-
const trimmed = text.trim();
|
|
36
|
+
const trimmed = (text ?? '').trim();
|
|
37
|
+
if (!trimmed) {
|
|
38
|
+
throw new Error('AI response was empty');
|
|
39
|
+
}
|
|
37
40
|
const jsonMatch = trimmed.match(/\{[\s\S]*\}/);
|
|
38
41
|
if (!jsonMatch) {
|
|
39
42
|
throw new Error('AI response did not contain JSON');
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import { getApiKey } from '../../../core/env.js';
|
|
3
3
|
|
|
4
|
+
/** GPT-5 / o-series models use max_completion_tokens instead of max_tokens. */
|
|
5
|
+
function usesCompletionTokensParam(model) {
|
|
6
|
+
return /^(gpt-5|o[0-9](-|$))/i.test(model || '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function buildCompletionParams(model, maxTokens) {
|
|
10
|
+
if (usesCompletionTokensParam(model)) {
|
|
11
|
+
return { max_completion_tokens: maxTokens };
|
|
12
|
+
}
|
|
13
|
+
return { max_tokens: maxTokens };
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
/**
|
|
5
17
|
* @param {{ model: string, maxTokens: number, timeoutMs: number }} options
|
|
6
18
|
*/
|
|
@@ -15,16 +27,31 @@ export async function callOpenAI(messages, options) {
|
|
|
15
27
|
timeout: options.timeoutMs,
|
|
16
28
|
});
|
|
17
29
|
|
|
18
|
-
const
|
|
30
|
+
const request = {
|
|
19
31
|
model: options.model,
|
|
20
32
|
messages,
|
|
21
|
-
|
|
22
|
-
temperature: 0.3,
|
|
33
|
+
...buildCompletionParams(options.model, options.maxTokens),
|
|
23
34
|
response_format: { type: 'json_object' },
|
|
24
|
-
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (!usesCompletionTokensParam(options.model)) {
|
|
38
|
+
request.temperature = 0.3;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const response = await client.chat.completions.create(request);
|
|
42
|
+
const choice = response.choices[0];
|
|
43
|
+
const content = choice?.message?.content ?? '';
|
|
44
|
+
|
|
45
|
+
if (!content.trim()) {
|
|
46
|
+
const finish = choice?.finish_reason ?? 'unknown';
|
|
47
|
+
throw new Error(
|
|
48
|
+
`OpenAI returned empty content (finish_reason=${finish}). ` +
|
|
49
|
+
'For gpt-5/o-series models, raise maxTokens — reasoning uses completion budget.'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
25
52
|
|
|
26
53
|
return {
|
|
27
|
-
content
|
|
54
|
+
content,
|
|
28
55
|
model: response.model,
|
|
29
56
|
provider: 'openai',
|
|
30
57
|
usage: response.usage,
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"ignore": ["node_modules", "dist", ".next", "coverage", ".git"],
|
|
3
|
-
"watch": true,
|
|
4
|
-
"autoGenerate": true,
|
|
5
|
-
"includeGitHistory": true,
|
|
6
|
-
"detectBugs": true,
|
|
7
|
-
"debounceMs": 2000,
|
|
8
|
-
"refreshIntervalMinutes": 15,
|
|
9
|
-
"trackChanges": true,
|
|
10
|
-
"maxTreeDepth": 4,
|
|
11
|
-
"gitCommitLimit": 20,
|
|
12
|
-
"ai": {
|
|
13
|
-
"enabled": true,
|
|
14
|
-
"provider": "openai",
|
|
15
|
-
"fallbackProvider": "groq",
|
|
16
|
-
"enrichContext": true,
|
|
17
|
-
"background": true,
|
|
18
|
-
"openai": {
|
|
19
|
-
"model": "gpt-4o-mini"
|
|
20
|
-
},
|
|
21
|
-
"groq": {
|
|
22
|
-
"model": "llama-3.3-70b-versatile"
|
|
23
|
-
},
|
|
24
|
-
"maxTokens": 4096,
|
|
25
|
-
"timeoutMs": 120000,
|
|
26
|
-
"promptsDir": ".contextforge/prompts"
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"ignore": ["node_modules", "dist", "build", ".next", "coverage", ".git"],
|
|
3
|
-
"watch": true,
|
|
4
|
-
"autoGenerate": true,
|
|
5
|
-
"includeGitHistory": true,
|
|
6
|
-
"detectBugs": true,
|
|
7
|
-
"debounceMs": 2000,
|
|
8
|
-
"refreshIntervalMinutes": 0,
|
|
9
|
-
"trackChanges": true,
|
|
10
|
-
"maxTreeDepth": 4,
|
|
11
|
-
"gitCommitLimit": 20,
|
|
12
|
-
"maxFileSizeBytes": 524288,
|
|
13
|
-
"installCursorRules": true,
|
|
14
|
-
"postinstallGenerate": true,
|
|
15
|
-
"postinstallWatch": true,
|
|
16
|
-
"postinstallOnNpmInstall": true,
|
|
17
|
-
"ai": {
|
|
18
|
-
"enabled": false,
|
|
19
|
-
"provider": "openai",
|
|
20
|
-
"fallbackProvider": "groq",
|
|
21
|
-
"enrichContext": true,
|
|
22
|
-
"background": true,
|
|
23
|
-
"openai": { "model": "gpt-4o-mini" },
|
|
24
|
-
"groq": { "model": "llama-3.3-70b-versatile" },
|
|
25
|
-
"maxTokens": 4096,
|
|
26
|
-
"timeoutMs": 120000,
|
|
27
|
-
"promptsDir": ".contextforge/prompts"
|
|
28
|
-
}
|
|
29
|
-
}
|