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.
Files changed (97) hide show
  1. package/.agent/CLAUDE.md +53 -0
  2. package/.agent/mindforge/auto.md +22 -0
  3. package/.agent/mindforge/browse.md +26 -0
  4. package/.agent/mindforge/costs.md +11 -0
  5. package/.agent/mindforge/cross-review.md +17 -0
  6. package/.agent/mindforge/execute-phase.md +5 -3
  7. package/.agent/mindforge/qa.md +16 -0
  8. package/.agent/mindforge/remember.md +14 -0
  9. package/.agent/mindforge/research.md +11 -0
  10. package/.agent/mindforge/steer.md +13 -0
  11. package/.agent/workflows/publish-release.md +36 -0
  12. package/.claude/CLAUDE.md +53 -0
  13. package/.claude/commands/mindforge/auto.md +22 -0
  14. package/.claude/commands/mindforge/browse.md +26 -0
  15. package/.claude/commands/mindforge/costs.md +11 -0
  16. package/.claude/commands/mindforge/cross-review.md +17 -0
  17. package/.claude/commands/mindforge/execute-phase.md +5 -3
  18. package/.claude/commands/mindforge/qa.md +16 -0
  19. package/.claude/commands/mindforge/remember.md +14 -0
  20. package/.claude/commands/mindforge/research.md +11 -0
  21. package/.claude/commands/mindforge/steer.md +13 -0
  22. package/.mindforge/MINDFORGE-V2-SCHEMA.json +47 -0
  23. package/.mindforge/browser/daemon-protocol.md +24 -0
  24. package/.mindforge/browser/qa-engine.md +16 -0
  25. package/.mindforge/browser/session-manager.md +18 -0
  26. package/.mindforge/browser/visual-verify-spec.md +31 -0
  27. package/.mindforge/engine/autonomous/auto-executor.md +266 -0
  28. package/.mindforge/engine/autonomous/headless-adapter.md +66 -0
  29. package/.mindforge/engine/autonomous/node-repair.md +190 -0
  30. package/.mindforge/engine/autonomous/progress-reporter.md +58 -0
  31. package/.mindforge/engine/autonomous/steering-manager.md +64 -0
  32. package/.mindforge/engine/autonomous/stuck-detector.md +89 -0
  33. package/.mindforge/memory/MEMORY-SCHEMA.md +155 -0
  34. package/.mindforge/memory/decision-library.jsonl +0 -0
  35. package/.mindforge/memory/engine/capture-protocol.md +36 -0
  36. package/.mindforge/memory/engine/global-sync-spec.md +42 -0
  37. package/.mindforge/memory/engine/retrieval-spec.md +44 -0
  38. package/.mindforge/memory/knowledge-base.jsonl +7 -0
  39. package/.mindforge/memory/pattern-library.jsonl +1 -0
  40. package/.mindforge/memory/team-preferences.jsonl +4 -0
  41. package/.mindforge/models/model-registry.md +48 -0
  42. package/.mindforge/models/model-router.md +30 -0
  43. package/.mindforge/personas/research-agent.md +24 -0
  44. package/.planning/browser-daemon.log +32 -0
  45. package/.planning/decisions/ADR-021-autonomy-boundary.md +17 -0
  46. package/.planning/decisions/ADR-022-node-repair-hierarchy.md +19 -0
  47. package/.planning/decisions/ADR-023-gate-3-timing.md +15 -0
  48. package/CHANGELOG.md +68 -0
  49. package/MINDFORGE.md +26 -3
  50. package/README.md +54 -18
  51. package/bin/autonomous/auto-runner.js +95 -0
  52. package/bin/autonomous/headless.js +36 -0
  53. package/bin/autonomous/progress-stream.js +49 -0
  54. package/bin/autonomous/repair-operator.js +213 -0
  55. package/bin/autonomous/steer.js +71 -0
  56. package/bin/autonomous/stuck-monitor.js +77 -0
  57. package/bin/browser/browser-daemon.js +139 -0
  58. package/bin/browser/daemon-manager.js +91 -0
  59. package/bin/browser/qa-engine.js +47 -0
  60. package/bin/browser/qa-report-writer.js +32 -0
  61. package/bin/browser/regression-writer.js +27 -0
  62. package/bin/browser/screenshot-store.js +49 -0
  63. package/bin/browser/session-manager.js +93 -0
  64. package/bin/browser/visual-verify-executor.js +89 -0
  65. package/bin/install.js +4 -4
  66. package/bin/installer-core.js +24 -24
  67. package/bin/memory/cli.js +99 -0
  68. package/bin/memory/global-sync.js +107 -0
  69. package/bin/memory/knowledge-capture.js +278 -0
  70. package/bin/memory/knowledge-indexer.js +172 -0
  71. package/bin/memory/knowledge-store.js +319 -0
  72. package/bin/memory/session-memory-loader.js +137 -0
  73. package/bin/migrations/0.1.0-to-0.5.0.js +2 -3
  74. package/bin/migrations/0.5.0-to-0.6.0.js +1 -1
  75. package/bin/migrations/0.6.0-to-1.0.0.js +3 -3
  76. package/bin/migrations/migrate.js +15 -11
  77. package/bin/models/anthropic-provider.js +77 -0
  78. package/bin/models/cost-tracker.js +118 -0
  79. package/bin/models/gemini-provider.js +79 -0
  80. package/bin/models/model-client.js +98 -0
  81. package/bin/models/model-router.js +111 -0
  82. package/bin/models/openai-provider.js +78 -0
  83. package/bin/research/research-engine.js +115 -0
  84. package/bin/review/cross-review-engine.js +81 -0
  85. package/bin/review/finding-synthesizer.js +116 -0
  86. package/bin/review/review-report-writer.js +49 -0
  87. package/bin/updater/self-update.js +13 -13
  88. package/docs/adr/ADR-024-browser-localhost-only.md +17 -0
  89. package/docs/adr/ADR-025-visual-verify-failure-treatment.md +19 -0
  90. package/docs/adr/ADR-026-session-persistence-security.md +20 -0
  91. package/docs/architecture/README.md +4 -2
  92. package/docs/publishing-guide.md +78 -0
  93. package/docs/reference/commands.md +17 -2
  94. package/docs/reference/sdk-api.md +6 -1
  95. package/docs/user-guide.md +93 -9
  96. package/docs/usp-features.md +56 -8
  97. 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(` ℹ️ No .planning/ directory found — skipping migration`);
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(` ✅ No migration needed`);
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(` ✅ No applicable migrations`);
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(` ℹ️ No files to migrate`);
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 { fs.rmSync(backupDir, { recursive: true, force: true }); } catch {}
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
- `Free disk space and retry.`
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(` ✅ Complete`);
113
+ console.log(' ✅ Complete');
110
114
  } catch (migErr) {
111
115
  console.error(` ❌ Failed: ${migErr.message}`);
112
- console.log(` Restoring from backup...`);
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(` ✅ Restored from backup. No changes applied.`);
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(` 🗑️ CI mode: backup auto-deleted (disk space)`);
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;