mindforge-cc 1.0.5 → 2.0.0-alpha.6

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 (119) hide show
  1. package/.agent/CLAUDE.md +83 -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/dashboard.md +98 -0
  7. package/.agent/mindforge/execute-phase.md +5 -3
  8. package/.agent/mindforge/init-project.md +12 -0
  9. package/.agent/mindforge/qa.md +16 -0
  10. package/.agent/mindforge/remember.md +14 -0
  11. package/.agent/mindforge/research.md +11 -0
  12. package/.agent/mindforge/steer.md +13 -0
  13. package/.agent/workflows/publish-release.md +36 -0
  14. package/.claude/CLAUDE.md +83 -0
  15. package/.claude/commands/mindforge/auto.md +22 -0
  16. package/.claude/commands/mindforge/browse.md +26 -0
  17. package/.claude/commands/mindforge/costs.md +11 -0
  18. package/.claude/commands/mindforge/cross-review.md +17 -0
  19. package/.claude/commands/mindforge/dashboard.md +98 -0
  20. package/.claude/commands/mindforge/execute-phase.md +5 -3
  21. package/.claude/commands/mindforge/qa.md +16 -0
  22. package/.claude/commands/mindforge/remember.md +14 -0
  23. package/.claude/commands/mindforge/research.md +11 -0
  24. package/.claude/commands/mindforge/steer.md +13 -0
  25. package/.mindforge/MINDFORGE-V2-SCHEMA.json +47 -0
  26. package/.mindforge/browser/daemon-protocol.md +24 -0
  27. package/.mindforge/browser/qa-engine.md +16 -0
  28. package/.mindforge/browser/session-manager.md +18 -0
  29. package/.mindforge/browser/visual-verify-spec.md +31 -0
  30. package/.mindforge/dashboard/api-reference.md +122 -0
  31. package/.mindforge/dashboard/dashboard-spec.md +96 -0
  32. package/.mindforge/engine/autonomous/auto-executor.md +266 -0
  33. package/.mindforge/engine/autonomous/headless-adapter.md +66 -0
  34. package/.mindforge/engine/autonomous/node-repair.md +190 -0
  35. package/.mindforge/engine/autonomous/progress-reporter.md +58 -0
  36. package/.mindforge/engine/autonomous/steering-manager.md +64 -0
  37. package/.mindforge/engine/autonomous/stuck-detector.md +89 -0
  38. package/.mindforge/memory/MEMORY-SCHEMA.md +155 -0
  39. package/.mindforge/memory/decision-library.jsonl +0 -0
  40. package/.mindforge/memory/engine/capture-protocol.md +36 -0
  41. package/.mindforge/memory/engine/global-sync-spec.md +42 -0
  42. package/.mindforge/memory/engine/retrieval-spec.md +44 -0
  43. package/.mindforge/memory/knowledge-base.jsonl +7 -0
  44. package/.mindforge/memory/pattern-library.jsonl +1 -0
  45. package/.mindforge/memory/team-preferences.jsonl +4 -0
  46. package/.mindforge/models/model-registry.md +48 -0
  47. package/.mindforge/models/model-router.md +30 -0
  48. package/.mindforge/personas/research-agent.md +24 -0
  49. package/.planning/approvals/v2-architecture-approval.json +15 -0
  50. package/.planning/browser-daemon.log +32 -0
  51. package/.planning/decisions/ADR-021-autonomy-boundary.md +17 -0
  52. package/.planning/decisions/ADR-022-node-repair-hierarchy.md +19 -0
  53. package/.planning/decisions/ADR-023-gate-3-timing.md +15 -0
  54. package/CHANGELOG.md +81 -0
  55. package/MINDFORGE.md +26 -3
  56. package/README.md +70 -18
  57. package/bin/autonomous/auto-runner.js +95 -0
  58. package/bin/autonomous/headless.js +36 -0
  59. package/bin/autonomous/progress-stream.js +49 -0
  60. package/bin/autonomous/repair-operator.js +213 -0
  61. package/bin/autonomous/steer.js +71 -0
  62. package/bin/autonomous/stuck-monitor.js +77 -0
  63. package/bin/browser/browser-daemon.js +139 -0
  64. package/bin/browser/daemon-manager.js +91 -0
  65. package/bin/browser/qa-engine.js +47 -0
  66. package/bin/browser/qa-report-writer.js +32 -0
  67. package/bin/browser/regression-writer.js +27 -0
  68. package/bin/browser/screenshot-store.js +49 -0
  69. package/bin/browser/session-manager.js +93 -0
  70. package/bin/browser/visual-verify-executor.js +89 -0
  71. package/bin/change-classifier.js +86 -0
  72. package/bin/dashboard/api-router.js +198 -0
  73. package/bin/dashboard/approval-handler.js +134 -0
  74. package/bin/dashboard/frontend/index.html +511 -0
  75. package/bin/dashboard/metrics-aggregator.js +296 -0
  76. package/bin/dashboard/server.js +135 -0
  77. package/bin/dashboard/sse-bridge.js +178 -0
  78. package/bin/dashboard/team-tracker.js +0 -0
  79. package/bin/governance/approve.js +60 -0
  80. package/bin/install.js +4 -4
  81. package/bin/installer-core.js +91 -35
  82. package/bin/memory/cli.js +99 -0
  83. package/bin/memory/global-sync.js +107 -0
  84. package/bin/memory/knowledge-capture.js +278 -0
  85. package/bin/memory/knowledge-indexer.js +172 -0
  86. package/bin/memory/knowledge-store.js +319 -0
  87. package/bin/memory/session-memory-loader.js +137 -0
  88. package/bin/migrations/0.1.0-to-0.5.0.js +2 -3
  89. package/bin/migrations/0.5.0-to-0.6.0.js +1 -1
  90. package/bin/migrations/0.6.0-to-1.0.0.js +3 -3
  91. package/bin/migrations/migrate.js +15 -11
  92. package/bin/mindforge-cli.js +87 -0
  93. package/bin/models/anthropic-provider.js +77 -0
  94. package/bin/models/cost-tracker.js +118 -0
  95. package/bin/models/gemini-provider.js +79 -0
  96. package/bin/models/model-client.js +98 -0
  97. package/bin/models/model-router.js +111 -0
  98. package/bin/models/openai-provider.js +78 -0
  99. package/bin/research/research-engine.js +115 -0
  100. package/bin/review/cross-review-engine.js +81 -0
  101. package/bin/review/finding-synthesizer.js +116 -0
  102. package/bin/review/review-report-writer.js +49 -0
  103. package/bin/updater/self-update.js +13 -13
  104. package/bin/wizard/setup-wizard.js +5 -1
  105. package/docs/adr/ADR-024-browser-localhost-only.md +17 -0
  106. package/docs/adr/ADR-025-visual-verify-failure-treatment.md +19 -0
  107. package/docs/adr/ADR-026-session-persistence-security.md +20 -0
  108. package/docs/architecture/README.md +6 -2
  109. package/docs/ci-cd.md +92 -0
  110. package/docs/commands-reference.md +1 -0
  111. package/docs/feature-dashboard.md +52 -0
  112. package/docs/publishing-guide.md +43 -0
  113. package/docs/reference/commands.md +17 -2
  114. package/docs/reference/sdk-api.md +6 -1
  115. package/docs/testing-current-version.md +130 -0
  116. package/docs/user-guide.md +115 -9
  117. package/docs/usp-features.md +70 -8
  118. package/docs/workflow-atlas.md +57 -0
  119. package/package.json +7 -3
@@ -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;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * MindForge v2 — Research Engine
3
+ * Leverages Gemini 1M context for deep research.
4
+ */
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const https = require('https');
10
+ const http = require('http');
11
+ const { URL } = require('url');
12
+ const dns = require('dns').promises;
13
+ const ModelClient = require('../models/model-client');
14
+ const Router = require('../models/model-router');
15
+
16
+ const PRIVATE_RANGES = [
17
+ /^127\./,
18
+ /^10\./,
19
+ /^172\.(1[6-9]|2\d|3[01])\./,
20
+ /^192\.168\./,
21
+ /^169\.254\./,
22
+ /^::1$/,
23
+ /^fc00:/,
24
+ /^fe80:/,
25
+ ];
26
+
27
+ const SYSTEM_PROMPTS = {
28
+ general: 'You are a thorough technical researcher. Analyze the provided context deeply and answer the question comprehensively. Cite specific evidence.',
29
+ library_comparison: 'You are a senior engineer evaluating libraries. Analyze: API design, maintenance, performance, security, and community.',
30
+ codebase_analysis: 'You are a senior architect auditing a codebase. Identify patterns, debt, security issues, and bottlenecks.',
31
+ compliance: 'You are a compliance engineer reviewing implementation against regulations. Identify gaps and remediation steps.',
32
+ };
33
+
34
+ async function isSafeUrl(url) {
35
+ try {
36
+ const parsed = new URL(url);
37
+ if (!['http:', 'https:'].includes(parsed.protocol)) return false;
38
+ const { address } = await dns.lookup(parsed.hostname);
39
+ return !PRIVATE_RANGES.some(r => r.test(address));
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ async function fetchUrl(url) {
46
+ if (!await isSafeUrl(url)) throw new Error(`URL blocked: ${url}`);
47
+
48
+ return new Promise((resolve, reject) => {
49
+ const protocol = url.startsWith('https') ? https : http;
50
+ const timeout = setTimeout(() => { req.destroy(); reject(new Error('Timeout')); }, 30_000);
51
+
52
+ const req = protocol.get(url, res => {
53
+ let body = '';
54
+ res.on('data', c => body += c);
55
+ res.on('end', () => { clearTimeout(timeout); resolve(body.slice(0, 500_000)); });
56
+ });
57
+ req.on('error', e => { clearTimeout(timeout); reject(e); });
58
+ });
59
+ }
60
+
61
+ function packageLocalContext(paths) {
62
+ let content = '';
63
+ for (const p of paths) {
64
+ const fullPath = path.resolve(process.cwd(), p);
65
+ if (!fs.existsSync(fullPath)) continue;
66
+
67
+ if (fs.statSync(fullPath).isDirectory()) {
68
+ content += walkDir(fullPath);
69
+ } else {
70
+ content += `\n\n### File: ${p}\n${fs.readFileSync(fullPath, 'utf8')}\n`;
71
+ }
72
+ }
73
+ return content;
74
+ }
75
+
76
+ function walkDir(dir) {
77
+ let content = '';
78
+ const files = fs.readdirSync(dir);
79
+ for (const f of files) {
80
+ const full = path.join(dir, f);
81
+ if (f.startsWith('.') || f === 'node_modules') continue;
82
+ if (fs.statSync(full).isDirectory()) content += walkDir(full);
83
+ else if (['.js', '.ts', '.md', '.json'].some(ext => f.endsWith(ext))) {
84
+ content += `\n\n### File: ${full}\n${fs.readFileSync(full, 'utf8')}\n`;
85
+ }
86
+ }
87
+ return content;
88
+ }
89
+
90
+ async function research(params) {
91
+ const { topic, question, type = 'general', contextPaths = [], urls = [] } = params;
92
+
93
+ let context = packageLocalContext(contextPaths);
94
+ for (const url of urls) {
95
+ try {
96
+ const html = await fetchUrl(url);
97
+ const text = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
98
+ context += `\n\n### URL: ${url}\n${text.slice(0, 100_000)}`;
99
+ } catch (e) {
100
+ context += `\n\n### URL: ${url}\n[Failed to fetch: ${e.message}]`;
101
+ }
102
+ }
103
+
104
+ const result = await ModelClient.complete({
105
+ persona: 'research-agent',
106
+ systemPrompt: SYSTEM_PROMPTS[type] || SYSTEM_PROMPTS.general,
107
+ userMessage: `Topic: ${topic}\n\nContext:\n${context}\n\nQuestion: ${question}`,
108
+ taskName: `research-${topic}`,
109
+ maxTokens: 8192
110
+ });
111
+
112
+ return { ...result, context_length: context.length };
113
+ }
114
+
115
+ module.exports = { research, packageLocalContext, fetchUrl };