mindforge-cc 1.0.5 → 2.0.0-alpha.4
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/.agent/CLAUDE.md +53 -0
- package/.agent/mindforge/auto.md +22 -0
- package/.agent/mindforge/browse.md +26 -0
- package/.agent/mindforge/costs.md +11 -0
- package/.agent/mindforge/cross-review.md +17 -0
- package/.agent/mindforge/execute-phase.md +5 -3
- package/.agent/mindforge/qa.md +16 -0
- package/.agent/mindforge/remember.md +14 -0
- package/.agent/mindforge/research.md +11 -0
- package/.agent/mindforge/steer.md +13 -0
- package/.agent/workflows/publish-release.md +36 -0
- package/.claude/CLAUDE.md +53 -0
- package/.claude/commands/mindforge/auto.md +22 -0
- package/.claude/commands/mindforge/browse.md +26 -0
- package/.claude/commands/mindforge/costs.md +11 -0
- package/.claude/commands/mindforge/cross-review.md +17 -0
- package/.claude/commands/mindforge/execute-phase.md +5 -3
- package/.claude/commands/mindforge/qa.md +16 -0
- package/.claude/commands/mindforge/remember.md +14 -0
- package/.claude/commands/mindforge/research.md +11 -0
- package/.claude/commands/mindforge/steer.md +13 -0
- package/.mindforge/MINDFORGE-V2-SCHEMA.json +47 -0
- package/.mindforge/browser/daemon-protocol.md +24 -0
- package/.mindforge/browser/qa-engine.md +16 -0
- package/.mindforge/browser/session-manager.md +18 -0
- package/.mindforge/browser/visual-verify-spec.md +31 -0
- package/.mindforge/engine/autonomous/auto-executor.md +266 -0
- package/.mindforge/engine/autonomous/headless-adapter.md +66 -0
- package/.mindforge/engine/autonomous/node-repair.md +190 -0
- package/.mindforge/engine/autonomous/progress-reporter.md +58 -0
- package/.mindforge/engine/autonomous/steering-manager.md +64 -0
- package/.mindforge/engine/autonomous/stuck-detector.md +89 -0
- package/.mindforge/memory/MEMORY-SCHEMA.md +155 -0
- package/.mindforge/memory/decision-library.jsonl +0 -0
- package/.mindforge/memory/engine/capture-protocol.md +36 -0
- package/.mindforge/memory/engine/global-sync-spec.md +42 -0
- package/.mindforge/memory/engine/retrieval-spec.md +44 -0
- package/.mindforge/memory/knowledge-base.jsonl +7 -0
- package/.mindforge/memory/pattern-library.jsonl +1 -0
- package/.mindforge/memory/team-preferences.jsonl +4 -0
- package/.mindforge/models/model-registry.md +48 -0
- package/.mindforge/models/model-router.md +30 -0
- package/.mindforge/personas/research-agent.md +24 -0
- package/.planning/browser-daemon.log +32 -0
- package/.planning/decisions/ADR-021-autonomy-boundary.md +17 -0
- package/.planning/decisions/ADR-022-node-repair-hierarchy.md +19 -0
- package/.planning/decisions/ADR-023-gate-3-timing.md +15 -0
- package/CHANGELOG.md +68 -0
- package/MINDFORGE.md +26 -3
- package/README.md +54 -18
- package/bin/autonomous/auto-runner.js +95 -0
- package/bin/autonomous/headless.js +36 -0
- package/bin/autonomous/progress-stream.js +49 -0
- package/bin/autonomous/repair-operator.js +213 -0
- package/bin/autonomous/steer.js +71 -0
- package/bin/autonomous/stuck-monitor.js +77 -0
- package/bin/browser/browser-daemon.js +139 -0
- package/bin/browser/daemon-manager.js +91 -0
- package/bin/browser/qa-engine.js +47 -0
- package/bin/browser/qa-report-writer.js +32 -0
- package/bin/browser/regression-writer.js +27 -0
- package/bin/browser/screenshot-store.js +49 -0
- package/bin/browser/session-manager.js +93 -0
- package/bin/browser/visual-verify-executor.js +89 -0
- package/bin/install.js +4 -4
- package/bin/installer-core.js +24 -24
- package/bin/memory/cli.js +99 -0
- package/bin/memory/global-sync.js +107 -0
- package/bin/memory/knowledge-capture.js +278 -0
- package/bin/memory/knowledge-indexer.js +172 -0
- package/bin/memory/knowledge-store.js +319 -0
- package/bin/memory/session-memory-loader.js +137 -0
- package/bin/migrations/0.1.0-to-0.5.0.js +2 -3
- package/bin/migrations/0.5.0-to-0.6.0.js +1 -1
- package/bin/migrations/0.6.0-to-1.0.0.js +3 -3
- package/bin/migrations/migrate.js +15 -11
- package/bin/models/anthropic-provider.js +77 -0
- package/bin/models/cost-tracker.js +118 -0
- package/bin/models/gemini-provider.js +79 -0
- package/bin/models/model-client.js +98 -0
- package/bin/models/model-router.js +111 -0
- package/bin/models/openai-provider.js +78 -0
- package/bin/research/research-engine.js +115 -0
- package/bin/review/cross-review-engine.js +81 -0
- package/bin/review/finding-synthesizer.js +116 -0
- package/bin/review/review-report-writer.js +49 -0
- package/bin/updater/self-update.js +13 -13
- package/docs/adr/ADR-024-browser-localhost-only.md +17 -0
- package/docs/adr/ADR-025-visual-verify-failure-treatment.md +19 -0
- package/docs/adr/ADR-026-session-persistence-security.md +20 -0
- package/docs/architecture/README.md +4 -2
- package/docs/publishing-guide.md +78 -0
- package/docs/reference/commands.md +17 -2
- package/docs/reference/sdk-api.md +6 -1
- package/docs/user-guide.md +93 -9
- package/docs/usp-features.md +56 -8
- package/package.json +3 -2
|
@@ -30,12 +30,12 @@ async function runMigrations(fromVersion, toVersion) {
|
|
|
30
30
|
console.log(`\n Migration: v${fromVersion} → v${toVersion}`);
|
|
31
31
|
|
|
32
32
|
if (!fs.existsSync(PLANNING_DIR)) {
|
|
33
|
-
console.log(
|
|
33
|
+
console.log(' ℹ️ No .planning/ directory found — skipping migration');
|
|
34
34
|
return { status: 'no-planning-dir' };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (compareSemver(fromVersion, toVersion) >= 0) {
|
|
38
|
-
console.log(
|
|
38
|
+
console.log(' ✅ No migration needed');
|
|
39
39
|
return { status: 'no-migration-needed' };
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -54,7 +54,7 @@ async function runMigrations(fromVersion, toVersion) {
|
|
|
54
54
|
);
|
|
55
55
|
|
|
56
56
|
if (migrationsToRun.length === 0) {
|
|
57
|
-
console.log(
|
|
57
|
+
console.log(' ✅ No applicable migrations');
|
|
58
58
|
return { status: 'no-migrations' };
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -69,7 +69,7 @@ async function runMigrations(fromVersion, toVersion) {
|
|
|
69
69
|
const filesToBackup = Object.values(PATHS).filter(p => fs.existsSync(p));
|
|
70
70
|
|
|
71
71
|
if (filesToBackup.length === 0) {
|
|
72
|
-
console.log(
|
|
72
|
+
console.log(' ℹ️ No files to migrate');
|
|
73
73
|
fs.rmdirSync(backupDir);
|
|
74
74
|
return { status: 'no-files' };
|
|
75
75
|
}
|
|
@@ -93,11 +93,15 @@ async function runMigrations(fromVersion, toVersion) {
|
|
|
93
93
|
} catch (backupErr) {
|
|
94
94
|
// Abort cleanly — no migration is safer than a migration without backup
|
|
95
95
|
if (fs.existsSync(backupDir)) {
|
|
96
|
-
try {
|
|
96
|
+
try {
|
|
97
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Ignore backup cleanup failures if backup creation already failed
|
|
100
|
+
}
|
|
97
101
|
}
|
|
98
102
|
throw new Error(
|
|
99
103
|
`Migration aborted: cannot create backup (${backupErr.message}). ` +
|
|
100
|
-
|
|
104
|
+
'Free disk space and retry.'
|
|
101
105
|
);
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -106,10 +110,10 @@ async function runMigrations(fromVersion, toVersion) {
|
|
|
106
110
|
console.log(`\n Running: v${migration.fromVersion} → v${migration.toVersion}...`);
|
|
107
111
|
try {
|
|
108
112
|
await migration.run(PATHS);
|
|
109
|
-
console.log(
|
|
113
|
+
console.log(' ✅ Complete');
|
|
110
114
|
} catch (migErr) {
|
|
111
115
|
console.error(` ❌ Failed: ${migErr.message}`);
|
|
112
|
-
console.log(
|
|
116
|
+
console.log(' Restoring from backup...');
|
|
113
117
|
|
|
114
118
|
// Restore all files from backup
|
|
115
119
|
for (const f of fs.readdirSync(backupDir)) {
|
|
@@ -117,7 +121,7 @@ async function runMigrations(fromVersion, toVersion) {
|
|
|
117
121
|
path.join(PLANNING_DIR, f);
|
|
118
122
|
fs.copyFileSync(path.join(backupDir, f), dst);
|
|
119
123
|
}
|
|
120
|
-
console.log(
|
|
124
|
+
console.log(' ✅ Restored from backup. No changes applied.');
|
|
121
125
|
throw new Error(`Migration failed at v${migration.toVersion}: ${migErr.message}`);
|
|
122
126
|
}
|
|
123
127
|
}
|
|
@@ -136,8 +140,8 @@ async function runMigrations(fromVersion, toVersion) {
|
|
|
136
140
|
if (process.env.CI === 'true') {
|
|
137
141
|
try {
|
|
138
142
|
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
139
|
-
console.log(
|
|
140
|
-
} catch {
|
|
143
|
+
console.log(' 🗑️ CI mode: backup auto-deleted (disk space)');
|
|
144
|
+
} catch (err) {
|
|
141
145
|
// Silent failure on cleanup — migration succeeded, cleanup is optional
|
|
142
146
|
}
|
|
143
147
|
} else {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Anthropic Provider
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
|
|
8
|
+
class AnthropicProvider {
|
|
9
|
+
constructor(apiKey) {
|
|
10
|
+
this.apiKey = apiKey;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async complete(params) {
|
|
14
|
+
const { model, systemPrompt, userMessage, maxTokens = 4096, temperature = 0.7 } = params;
|
|
15
|
+
|
|
16
|
+
const data = JSON.stringify({
|
|
17
|
+
model,
|
|
18
|
+
system: systemPrompt,
|
|
19
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
20
|
+
max_tokens: maxTokens,
|
|
21
|
+
temperature,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const req = https.request({
|
|
26
|
+
hostname: 'api.anthropic.com',
|
|
27
|
+
path: '/v1/messages',
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
'x-api-key': this.apiKey,
|
|
32
|
+
'anthropic-version': '2023-06-01',
|
|
33
|
+
'Content-Length': Buffer.byteLength(data),
|
|
34
|
+
},
|
|
35
|
+
timeout: 120_000,
|
|
36
|
+
}, res => {
|
|
37
|
+
let body = '';
|
|
38
|
+
res.on('data', chunk => body += chunk);
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
try {
|
|
41
|
+
const json = JSON.parse(body);
|
|
42
|
+
if (res.statusCode !== 200) {
|
|
43
|
+
return reject(Object.assign(new Error(json.error?.message || 'Anthropic API error'), { status: res.statusCode }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const inputTokens = json.usage.input_tokens;
|
|
47
|
+
const outputTokens = json.usage.output_tokens;
|
|
48
|
+
|
|
49
|
+
// Basic cost calculation (Sonnet 3.5 prices)
|
|
50
|
+
const cost = (inputTokens * 0.000003) + (outputTokens * 0.000015);
|
|
51
|
+
|
|
52
|
+
resolve({
|
|
53
|
+
model: json.model,
|
|
54
|
+
content: json.content[0].text,
|
|
55
|
+
input_tokens: inputTokens,
|
|
56
|
+
output_tokens: outputTokens,
|
|
57
|
+
cost_usd: cost,
|
|
58
|
+
provider: 'anthropic'
|
|
59
|
+
});
|
|
60
|
+
} catch (e) {
|
|
61
|
+
reject(new Error('Failed to parse Anthropic response: ' + e.message));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
req.on('error', reject);
|
|
67
|
+
req.on('timeout', () => {
|
|
68
|
+
req.destroy();
|
|
69
|
+
reject(Object.assign(new Error('Anthropic timeout'), { status: 408 }));
|
|
70
|
+
});
|
|
71
|
+
req.write(data);
|
|
72
|
+
req.end();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = AnthropicProvider;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Cost Tracker
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const METRICS_DIR = path.join(process.cwd(), '.mindforge', 'metrics');
|
|
10
|
+
const USAGE_LOG = path.join(METRICS_DIR, 'token-usage.jsonl');
|
|
11
|
+
|
|
12
|
+
function ensureDir() {
|
|
13
|
+
if (!fs.existsSync(METRICS_DIR)) {
|
|
14
|
+
fs.mkdirSync(METRICS_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let _dailyCache = { value: 0, computed_at: 0 };
|
|
19
|
+
|
|
20
|
+
function getTodaySpend() {
|
|
21
|
+
if (!fs.existsSync(USAGE_LOG)) return 0;
|
|
22
|
+
|
|
23
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
24
|
+
const content = fs.readFileSync(USAGE_LOG, 'utf8');
|
|
25
|
+
const lines = content.trim().split('\n');
|
|
26
|
+
|
|
27
|
+
let total = 0;
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
try {
|
|
30
|
+
const entry = JSON.parse(line);
|
|
31
|
+
if (entry.date === today) {
|
|
32
|
+
total += entry.cost_usd || 0;
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
process.stderr.write('[cost-tracker] Skipped malformed entry\n');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return total;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getTodaySpendCached() {
|
|
42
|
+
const AGE_MS = Date.now() - _dailyCache.computed_at;
|
|
43
|
+
if (AGE_MS > 60_000) {
|
|
44
|
+
_dailyCache.value = getTodaySpend();
|
|
45
|
+
_dailyCache.computed_at = Date.now();
|
|
46
|
+
}
|
|
47
|
+
return _dailyCache.value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function preflight(estimatedCost = 0) {
|
|
51
|
+
const settings = require('./model-router').getAllSettings();
|
|
52
|
+
const hardLimit = parseFloat(settings.MODEL_COST_HARD_LIMIT_USD || '0.0');
|
|
53
|
+
|
|
54
|
+
if (hardLimit <= 0) return;
|
|
55
|
+
|
|
56
|
+
const todaySpend = getTodaySpendCached();
|
|
57
|
+
const projected = todaySpend + estimatedCost;
|
|
58
|
+
|
|
59
|
+
if (projected >= hardLimit) {
|
|
60
|
+
throw Object.assign(
|
|
61
|
+
new Error(`Daily cost limit $${hardLimit} reached (Today: $${todaySpend.toFixed(4)})`),
|
|
62
|
+
{ code: 'COST_LIMIT_REACHED', spend: todaySpend, limit: hardLimit }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function record(entry) {
|
|
68
|
+
ensureDir();
|
|
69
|
+
const enriched = {
|
|
70
|
+
...entry,
|
|
71
|
+
date: new Date().toISOString().slice(0, 10),
|
|
72
|
+
timestamp: new Date().toISOString()
|
|
73
|
+
};
|
|
74
|
+
fs.appendFileSync(USAGE_LOG, JSON.stringify(enriched) + '\n');
|
|
75
|
+
_dailyCache.computed_at = 0; // Invalidate cache
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getSummary(params = { days: 7 }) {
|
|
79
|
+
if (!fs.existsSync(USAGE_LOG)) return { total_usd: 0, by_model: {} };
|
|
80
|
+
|
|
81
|
+
const cutoffDate = new Date();
|
|
82
|
+
cutoffDate.setDate(cutoffDate.getDate() - params.days);
|
|
83
|
+
const cutoffStr = cutoffDate.toISOString().slice(0, 10);
|
|
84
|
+
|
|
85
|
+
const content = fs.readFileSync(USAGE_LOG, 'utf8');
|
|
86
|
+
const lines = content.trim().split('\n');
|
|
87
|
+
|
|
88
|
+
const result = {
|
|
89
|
+
total_usd: 0,
|
|
90
|
+
by_model: {},
|
|
91
|
+
by_phase: {},
|
|
92
|
+
calls: 0
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
try {
|
|
97
|
+
const entry = JSON.parse(line);
|
|
98
|
+
if (entry.date >= cutoffStr) {
|
|
99
|
+
const cost = entry.cost_usd || 0;
|
|
100
|
+
result.total_usd += cost;
|
|
101
|
+
result.calls++;
|
|
102
|
+
|
|
103
|
+
const model = entry.model || 'unknown';
|
|
104
|
+
if (!result.by_model[model]) result.by_model[model] = { cost: 0, calls: 0, tokens: 0 };
|
|
105
|
+
result.by_model[model].cost += cost;
|
|
106
|
+
result.by_model[model].calls++;
|
|
107
|
+
result.by_model[model].tokens += (entry.input_tokens || 0) + (entry.output_tokens || 0);
|
|
108
|
+
|
|
109
|
+
const phase = entry.phase || 'unknown';
|
|
110
|
+
if (!result.by_phase[phase]) result.by_phase[phase] = 0;
|
|
111
|
+
result.by_phase[phase] += cost;
|
|
112
|
+
}
|
|
113
|
+
} catch (e) { /* ignore parse errors for summary */ }
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { record, preflight, getTodaySpend, getTodaySpendCached, getSummary };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Gemini Provider
|
|
3
|
+
* Using header-based auth for security.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const https = require('https');
|
|
8
|
+
|
|
9
|
+
class GeminiProvider {
|
|
10
|
+
constructor(apiKey) {
|
|
11
|
+
this.apiKey = apiKey;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async complete(params) {
|
|
15
|
+
const { model, systemPrompt, userMessage, maxTokens = 8192, temperature = 0.2 } = params;
|
|
16
|
+
|
|
17
|
+
const data = JSON.stringify({
|
|
18
|
+
system_instruction: { parts: [{ text: systemPrompt }] },
|
|
19
|
+
contents: [{ parts: [{ text: userMessage }] }],
|
|
20
|
+
generationConfig: {
|
|
21
|
+
maxOutputTokens: maxTokens,
|
|
22
|
+
temperature,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const modelId = model.startsWith('models/') ? model : `models/${model}`;
|
|
27
|
+
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const req = https.request({
|
|
30
|
+
hostname: 'generativelanguage.googleapis.com',
|
|
31
|
+
path: `/v1beta/${modelId}:generateContent`,
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'x-goog-api-key': this.apiKey, // Header auth
|
|
36
|
+
'Content-Length': Buffer.byteLength(data),
|
|
37
|
+
},
|
|
38
|
+
timeout: 180_000,
|
|
39
|
+
}, res => {
|
|
40
|
+
let body = '';
|
|
41
|
+
res.on('data', chunk => body += chunk);
|
|
42
|
+
res.on('end', () => {
|
|
43
|
+
try {
|
|
44
|
+
const json = JSON.parse(body);
|
|
45
|
+
if (res.statusCode !== 200) {
|
|
46
|
+
return reject(Object.assign(new Error(json.error?.message || 'Gemini API error'), { status: res.statusCode }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Gemini 1.5 Pro billing is complex; using $1.25 / 1M input as baseline
|
|
50
|
+
const inputTokens = json.usageMetadata.promptTokenCount;
|
|
51
|
+
const outputTokens = json.usageMetadata.candidatesTokenCount;
|
|
52
|
+
const cost = (inputTokens * 0.00000125) + (outputTokens * 0.00000375);
|
|
53
|
+
|
|
54
|
+
resolve({
|
|
55
|
+
model: modelId,
|
|
56
|
+
content: json.candidates[0].content.parts[0].text,
|
|
57
|
+
input_tokens: inputTokens,
|
|
58
|
+
output_tokens: outputTokens,
|
|
59
|
+
cost_usd: cost,
|
|
60
|
+
provider: 'google'
|
|
61
|
+
});
|
|
62
|
+
} catch (e) {
|
|
63
|
+
reject(new Error('Failed to parse Gemini response: ' + e.message));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
req.on('error', reject);
|
|
69
|
+
req.on('timeout', () => {
|
|
70
|
+
req.destroy();
|
|
71
|
+
reject(Object.assign(new Error('Gemini timeout'), { status: 408 }));
|
|
72
|
+
});
|
|
73
|
+
req.write(data);
|
|
74
|
+
req.end();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = GeminiProvider;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Model Client
|
|
3
|
+
* Unified client with routing, fallbacks, and cost tracking.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const Router = require('./model-router');
|
|
8
|
+
const CostTracker = require('./cost-tracker');
|
|
9
|
+
const AnthropicProvider = require('./anthropic-provider');
|
|
10
|
+
const OpenAIProvider = require('./openai-provider');
|
|
11
|
+
const GeminiProvider = require('./gemini-provider');
|
|
12
|
+
|
|
13
|
+
const FALLBACK_CHAINS = {
|
|
14
|
+
'claude-3-opus-20240229': ['gpt-4o', 'claude-3-5-sonnet-20240620'],
|
|
15
|
+
'gpt-4o': ['claude-3-5-sonnet-20240620'],
|
|
16
|
+
'gemini-1.5-pro': ['claude-3-5-sonnet-20240620'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
class ModelClient {
|
|
20
|
+
static async complete(params) {
|
|
21
|
+
const {
|
|
22
|
+
persona = 'developer',
|
|
23
|
+
tier = 1,
|
|
24
|
+
maxTokens,
|
|
25
|
+
temperature,
|
|
26
|
+
taskName = 'unknown',
|
|
27
|
+
sessionId = 'unknown',
|
|
28
|
+
phaseNum = 0
|
|
29
|
+
} = params;
|
|
30
|
+
|
|
31
|
+
// 1. Route to model
|
|
32
|
+
const routing = Router.route(persona, tier);
|
|
33
|
+
let modelId = routing.model;
|
|
34
|
+
|
|
35
|
+
// 2. Pre-flight cost check
|
|
36
|
+
try {
|
|
37
|
+
await CostTracker.preflight(0.05); // Conservative estimate
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (e.code === 'COST_LIMIT_REACHED') throw e;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3. Execute with fallbacks
|
|
43
|
+
let result = null;
|
|
44
|
+
let attempts = [modelId, ...(FALLBACK_CHAINS[modelId] || [])];
|
|
45
|
+
|
|
46
|
+
for (const currentModel of attempts) {
|
|
47
|
+
try {
|
|
48
|
+
const provider = this._getProvider(currentModel);
|
|
49
|
+
if (!provider) continue;
|
|
50
|
+
|
|
51
|
+
result = await provider.complete({
|
|
52
|
+
model: currentModel,
|
|
53
|
+
systemPrompt: params.systemPrompt,
|
|
54
|
+
userMessage: params.userMessage,
|
|
55
|
+
maxTokens,
|
|
56
|
+
temperature
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Add metadata
|
|
60
|
+
result.task_name = taskName;
|
|
61
|
+
result.session_id = sessionId;
|
|
62
|
+
result.phase = phaseNum;
|
|
63
|
+
|
|
64
|
+
if (currentModel !== modelId) {
|
|
65
|
+
result.content = `[FALLBACK NOTICE: ${modelId} unavailable — used ${currentModel} instead.]\n\n${result.content}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4. Record cost
|
|
69
|
+
await CostTracker.record(result);
|
|
70
|
+
return result;
|
|
71
|
+
|
|
72
|
+
} catch (err) {
|
|
73
|
+
process.stderr.write(`[model-client] ${currentModel} failed: ${err.message}\n`);
|
|
74
|
+
if (attempts.indexOf(currentModel) === attempts.length - 1) {
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static _getProvider(modelId) {
|
|
82
|
+
if (modelId.includes('claude')) {
|
|
83
|
+
if (!process.env.ANTHROPIC_API_KEY) return null;
|
|
84
|
+
return new AnthropicProvider(process.env.ANTHROPIC_API_KEY);
|
|
85
|
+
}
|
|
86
|
+
if (modelId.includes('gpt')) {
|
|
87
|
+
if (!process.env.OPENAI_API_KEY) return null;
|
|
88
|
+
return new OpenAIProvider(process.env.OPENAI_API_KEY);
|
|
89
|
+
}
|
|
90
|
+
if (modelId.includes('gemini')) {
|
|
91
|
+
if (!process.env.GOOGLE_API_KEY) return null;
|
|
92
|
+
return new GeminiProvider(process.env.GOOGLE_API_KEY);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = ModelClient;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Model Router
|
|
3
|
+
* Resolves persona and tier to a specific model ID based on settings and context.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// Default model assignments
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
PLANNER_MODEL: 'claude-3-opus-20240229',
|
|
13
|
+
EXECUTOR_MODEL: 'claude-3-5-sonnet-20240620',
|
|
14
|
+
REVIEWER_MODEL: 'gpt-4o',
|
|
15
|
+
SECURITY_MODEL: 'claude-3-opus-20240229',
|
|
16
|
+
RESEARCH_MODEL: 'gemini-1.5-pro',
|
|
17
|
+
QA_MODEL: 'claude-3-5-sonnet-20240620',
|
|
18
|
+
DEBUG_MODEL: 'claude-3-opus-20240229',
|
|
19
|
+
QUICK_MODEL: 'claude-3-5-haiku-20241022',
|
|
20
|
+
CROSS_REVIEW_SECONDARY: 'gpt-4o',
|
|
21
|
+
CROSS_REVIEW_TERTIARY: 'gemini-1.5-pro',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Persona to setting key mapping
|
|
25
|
+
const PERSONA_MAP = {
|
|
26
|
+
'developer': 'EXECUTOR_MODEL',
|
|
27
|
+
'architect': 'PLANNER_MODEL',
|
|
28
|
+
'planner': 'PLANNER_MODEL',
|
|
29
|
+
'security-reviewer': 'SECURITY_MODEL',
|
|
30
|
+
'qa-engineer': 'QA_MODEL',
|
|
31
|
+
'research-agent': 'RESEARCH_MODEL',
|
|
32
|
+
'debug-specialist': 'DEBUG_MODEL',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let _settingsCache = null;
|
|
36
|
+
|
|
37
|
+
function readMindforgeSettings() {
|
|
38
|
+
if (_settingsCache) return _settingsCache;
|
|
39
|
+
const configPath = path.join(process.cwd(), 'MINDFORGE.md');
|
|
40
|
+
if (!fs.existsSync(configPath)) return DEFAULTS;
|
|
41
|
+
|
|
42
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
43
|
+
const settings = { ...DEFAULTS };
|
|
44
|
+
|
|
45
|
+
const lines = content.split('\n');
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
48
|
+
if (match) {
|
|
49
|
+
settings[match[1]] = match[2].trim();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
_settingsCache = settings;
|
|
53
|
+
return settings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function route(persona = 'developer', tier = 1) {
|
|
57
|
+
const settings = readMindforgeSettings();
|
|
58
|
+
|
|
59
|
+
// 1. Tier 3 override (Security/Privacy always uses SECURITY_MODEL)
|
|
60
|
+
if (tier === 3) {
|
|
61
|
+
return {
|
|
62
|
+
model: settings.SECURITY_MODEL,
|
|
63
|
+
setting: 'SECURITY_MODEL',
|
|
64
|
+
reason: 'Tier 3 (Security/Privacy) override'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Persona mapping (Specific personas like research, debug, qa)
|
|
69
|
+
if (persona !== 'developer' && PERSONA_MAP[persona]) {
|
|
70
|
+
const settingKey = PERSONA_MAP[persona];
|
|
71
|
+
return {
|
|
72
|
+
model: settings[settingKey],
|
|
73
|
+
setting: settingKey,
|
|
74
|
+
reason: `Mapped from specific persona "${persona}"`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Budget Bias (Tier 1 uses QUICK_MODEL for default developer tasks)
|
|
79
|
+
if (tier === 1) {
|
|
80
|
+
return {
|
|
81
|
+
model: settings.QUICK_MODEL,
|
|
82
|
+
setting: 'QUICK_MODEL',
|
|
83
|
+
reason: 'Tier 1 Budget Bias (efficiency mode)'
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 4. Default mapping
|
|
88
|
+
const settingKey = 'EXECUTOR_MODEL';
|
|
89
|
+
const model = settings[settingKey];
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
model,
|
|
93
|
+
setting: settingKey,
|
|
94
|
+
reason: `Default EXECUTOR_MODEL for tier ${tier}`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getModel(settingKey) {
|
|
99
|
+
const settings = readMindforgeSettings();
|
|
100
|
+
return settings[settingKey] || DEFAULTS[settingKey];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function clearCache() {
|
|
104
|
+
_settingsCache = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getAllSettings() {
|
|
108
|
+
return readMindforgeSettings();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { route, getModel, clearCache, getAllSettings };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — OpenAI Provider
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
|
|
8
|
+
class OpenAIProvider {
|
|
9
|
+
constructor(apiKey) {
|
|
10
|
+
this.apiKey = apiKey;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async complete(params) {
|
|
14
|
+
const { model, systemPrompt, userMessage, maxTokens = 4096, temperature = 0.7 } = params;
|
|
15
|
+
|
|
16
|
+
const data = JSON.stringify({
|
|
17
|
+
model,
|
|
18
|
+
messages: [
|
|
19
|
+
{ role: 'system', content: systemPrompt },
|
|
20
|
+
{ role: 'user', content: userMessage }
|
|
21
|
+
],
|
|
22
|
+
max_tokens: maxTokens,
|
|
23
|
+
temperature,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const req = https.request({
|
|
28
|
+
hostname: 'api.openai.com',
|
|
29
|
+
path: '/v1/chat/completions',
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
34
|
+
'Content-Length': Buffer.byteLength(data),
|
|
35
|
+
},
|
|
36
|
+
timeout: 120_000,
|
|
37
|
+
}, res => {
|
|
38
|
+
let body = '';
|
|
39
|
+
res.on('data', chunk => body += chunk);
|
|
40
|
+
res.on('end', () => {
|
|
41
|
+
try {
|
|
42
|
+
const json = JSON.parse(body);
|
|
43
|
+
if (res.statusCode !== 200) {
|
|
44
|
+
return reject(Object.assign(new Error(json.error?.message || 'OpenAI API error'), { status: res.statusCode }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const inputTokens = json.usage.prompt_tokens;
|
|
48
|
+
const outputTokens = json.usage.completion_tokens;
|
|
49
|
+
|
|
50
|
+
// Basic cost calculation (GPT-4o prices)
|
|
51
|
+
const cost = (inputTokens * 0.000005) + (outputTokens * 0.000015);
|
|
52
|
+
|
|
53
|
+
resolve({
|
|
54
|
+
model: json.model,
|
|
55
|
+
content: json.choices[0].message.content,
|
|
56
|
+
input_tokens: inputTokens,
|
|
57
|
+
output_tokens: outputTokens,
|
|
58
|
+
cost_usd: cost,
|
|
59
|
+
provider: 'openai'
|
|
60
|
+
});
|
|
61
|
+
} catch (e) {
|
|
62
|
+
reject(new Error('Failed to parse OpenAI response: ' + e.message));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
req.on('error', reject);
|
|
68
|
+
req.on('timeout', () => {
|
|
69
|
+
req.destroy();
|
|
70
|
+
reject(Object.assign(new Error('OpenAI timeout'), { status: 408 }));
|
|
71
|
+
});
|
|
72
|
+
req.write(data);
|
|
73
|
+
req.end();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = OpenAIProvider;
|