vibeoscore 1.0.2
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/.env.example +5 -0
- package/README.md +29 -0
- package/client.js +257 -0
- package/client.ts +334 -0
- package/dashboard/dist/assets/index-BnPt1Fii.js +1 -0
- package/dashboard/dist/assets/index-CfH00tOL.css +1 -0
- package/dashboard/dist/index.html +3 -0
- package/lib/blackbox-rf.js +1099 -0
- package/lib/blackbox.js +137 -0
- package/lib/compression.js +119 -0
- package/lib/db.js +106 -0
- package/lib/db.ts +113 -0
- package/lib/delegation.js +137 -0
- package/lib/meta-controller.js +418 -0
- package/lib/meta-controller.mjs +499 -0
- package/lib/patterns.js +150 -0
- package/lib/resolution-tracker.js +486 -0
- package/lib/stress.js +84 -0
- package/lib/tdd.js +218 -0
- package/lib/tier-routing.js +48 -0
- package/mcp-server.js +370 -0
- package/mcp-server.ts +364 -0
- package/middleware/auth.js +75 -0
- package/middleware/auth.ts +87 -0
- package/middleware/usage-logging.js +29 -0
- package/middleware/usage-logging.ts +41 -0
- package/nginx-vibetheog-api.conf +64 -0
- package/package.json +66 -0
- package/routes/admin.js +93 -0
- package/routes/admin.ts +107 -0
- package/routes/blackbox.js +463 -0
- package/routes/compression.js +12 -0
- package/routes/delegation.js +30 -0
- package/routes/patterns.js +53 -0
- package/routes/pricing.js +62 -0
- package/routes/stress.js +30 -0
- package/routes/tdd.js +68 -0
- package/routes/tier-routing.js +31 -0
- package/scripts/dashboard-server.mjs +246 -0
- package/scripts/deploy-zero-downtime.sh +77 -0
- package/scripts/deploy.sh +68 -0
- package/scripts/release.mjs +30 -0
- package/scripts/seed-master-token.js +29 -0
- package/scripts/start-all.mjs +34 -0
- package/server.js +88 -0
- package/vibeos-api.service +19 -0
package/lib/tdd.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
function extractExports(sourceContent, ext) {
|
|
2
|
+
if (!sourceContent || typeof sourceContent !== "string") return []
|
|
3
|
+
const exports = []
|
|
4
|
+
const seen = new Set()
|
|
5
|
+
const add = (name, type = "function") => {
|
|
6
|
+
if (name && !seen.has(name)) { seen.add(name); exports.push({ name, type }) }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
switch (ext) {
|
|
10
|
+
case "py": {
|
|
11
|
+
const defRe = /^def\s+([a-zA-Z_]\w*)\s*\(/gm
|
|
12
|
+
const classRe = /^class\s+([a-zA-Z_]\w*)/gm
|
|
13
|
+
let m
|
|
14
|
+
while ((m = defRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
15
|
+
while ((m = classRe.exec(sourceContent)) !== null) add(m[1], "class")
|
|
16
|
+
break
|
|
17
|
+
}
|
|
18
|
+
case "js": case "mjs": case "jsx": {
|
|
19
|
+
const funcRe = /^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$]\w*)\s*\(/gm
|
|
20
|
+
const constRe = /^(?:export\s+)?const\s+([a-zA-Z_$]\w*)\s*[:=]\s*(?:async\s+)?(?:\(|function)/gm
|
|
21
|
+
let m
|
|
22
|
+
while ((m = funcRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
23
|
+
while ((m = constRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
case "ts": case "tsx": {
|
|
27
|
+
const funcRe = /^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$]\w*)\s*\(/gm
|
|
28
|
+
const constRe = /^(?:export\s+)?const\s+([a-zA-Z_$]\w*)\s*[:=]\s*(?:async\s+)?(?:\(|function)/gm
|
|
29
|
+
const classRe = /^(?:export\s+)?class\s+([a-zA-Z_$]\w*)/gm
|
|
30
|
+
let m
|
|
31
|
+
while ((m = funcRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
32
|
+
while ((m = constRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
33
|
+
while ((m = classRe.exec(sourceContent)) !== null) add(m[1], "class")
|
|
34
|
+
break
|
|
35
|
+
}
|
|
36
|
+
case "go": {
|
|
37
|
+
const funcRe = /^func\s+(?:\([^)]+\)\s+)?([a-zA-Z_]\w*)\s*\(/gm
|
|
38
|
+
let m
|
|
39
|
+
while ((m = funcRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
case "rs": {
|
|
43
|
+
const fnRe = /^pub\s+(?:async\s+)?fn\s+([a-zA-Z_]\w*)\s*[<(]/gm
|
|
44
|
+
const structRe = /^pub\s+struct\s+([a-zA-Z_]\w*)/gm
|
|
45
|
+
let m
|
|
46
|
+
while ((m = fnRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
47
|
+
while ((m = structRe.exec(sourceContent)) !== null) add(m[1], "struct")
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
case "rb": {
|
|
51
|
+
const defRe = /^\s*def\s+(?:self\.)?([a-zA-Z_]\w*[!?=]?)/gm
|
|
52
|
+
const classRe = /^\s*class\s+([a-zA-Z_]\w*)/gm
|
|
53
|
+
let m
|
|
54
|
+
while ((m = defRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
55
|
+
while ((m = classRe.exec(sourceContent)) !== null) add(m[1], "class")
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
case "java": case "kt": {
|
|
59
|
+
const methodRe = /^\s*(?:public|protected|private|\s)+\s+(?:static\s+)?(?:final\s+)?\w+\s+([a-zA-Z_]\w*)\s*\(/gm
|
|
60
|
+
const funRe = /^\s*(?:fun)\s+([a-zA-Z_]\w*)\s*[<(]/gm
|
|
61
|
+
let m
|
|
62
|
+
while ((m = ext === "kt" ? funRe.exec(sourceContent) : methodRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
case "sh": {
|
|
66
|
+
const funcRe = /^(?:function\s+)?([a-zA-Z_]\w*)\s*\(\)/gm
|
|
67
|
+
let m
|
|
68
|
+
while ((m = funcRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return exports
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function inferFunctionParams(sourceContent, funcName) {
|
|
76
|
+
if (!sourceContent || !funcName) return []
|
|
77
|
+
const patterns = [
|
|
78
|
+
new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${escapeRegex(funcName)}\\s*\\(([^)]*)\\)`, "m"),
|
|
79
|
+
new RegExp(`(?:export\\s+)?const\\s+${escapeRegex(funcName)}\\s*[:=]\\s*(?:async\\s+)?\\(([^)]*)\\)`, "m"),
|
|
80
|
+
new RegExp(`def\\s+${escapeRegex(funcName)}\\s*\\(([^)]*)\\)`, "m"),
|
|
81
|
+
new RegExp(`func\\s+(?:\\([^)]+\\)\\s+)?${escapeRegex(funcName)}\\s*\\(([^)]*)\\)`, "m"),
|
|
82
|
+
new RegExp(`(?:pub\\s+)?fn\\s+${escapeRegex(funcName)}\\s*[<\\(]([^)]*)\\)`, "m"),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
for (const pat of patterns) {
|
|
86
|
+
const m = sourceContent.match(pat)
|
|
87
|
+
if (m) {
|
|
88
|
+
return m[1].split(",").map(s => {
|
|
89
|
+
const trimmed = s.trim()
|
|
90
|
+
if (!trimmed) return null
|
|
91
|
+
const parts = trimmed.split(":").map(p => p.split("="))
|
|
92
|
+
const name = parts[0][0]?.trim().replace(/^[*&]/, "")
|
|
93
|
+
const type = parts[0][1]?.trim() || inferTypeFromName(name, parts[0][0]?.split("=")[1]?.trim())
|
|
94
|
+
const defaultValue = parts[0][1]?.includes("=") ? parts[0][0].split("=")[1]?.trim() : (parts[1]?.join("=").trim())
|
|
95
|
+
return name ? { name, type: type || "any", defaultValue } : null
|
|
96
|
+
}).filter(Boolean)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function inferTypeFromName(paramName, defaultValue) {
|
|
103
|
+
if (!paramName) return "any"
|
|
104
|
+
const name = paramName.toLowerCase()
|
|
105
|
+
|
|
106
|
+
if (defaultValue !== undefined) {
|
|
107
|
+
if (typeof defaultValue === "string") {
|
|
108
|
+
if (defaultValue === "true" || defaultValue === "false") return "boolean"
|
|
109
|
+
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) return "number"
|
|
110
|
+
if (defaultValue.startsWith("[") || defaultValue.startsWith("{")) return defaultValue.startsWith("[") ? "array" : "object"
|
|
111
|
+
return "string"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (/^(is|has|can|should|will|did|enable|disable|visible|active|open|closed)/.test(name)) return "boolean"
|
|
116
|
+
if (/^(count|index|limit|size|length|max|min|width|height|depth|offset|duration|delay|timeout|interval|rate|threshold|level|score|priority|version|port|year|month|day|hour|minute|second)/.test(name)) return "number"
|
|
117
|
+
if (/^(name|title|label|text|content|body|message|description|summary|path|url|uri|host|port|file|filename|ext|extension|format|type|kind|mode|state|status|color|theme|lang|language|locale|timezone|currency|unit|prefix|suffix|key|token|secret|password|email|phone|address|id|uuid|slug)/.test(name)) return "string"
|
|
118
|
+
if (/^(items|list|array|elements|nodes|children|options|params|args|arguments|values|entries|records|rows|columns|fields|properties|attrs|attributes|headers|tags|categories|labels|classes|styles)/.test(name)) return "array"
|
|
119
|
+
if (/^(obj|config|opts|options|settings|prefs|preferences|context|state|data|info|metadata|meta|props|query|filter|sort|pagination|page|request|response|event|error|result|output|input)/.test(name)) return "object"
|
|
120
|
+
if (/^(fn|cb|callback|handler|listener|middleware|transform|map|reduce|filter|sort|forEach)/.test(name)) return "function"
|
|
121
|
+
|
|
122
|
+
return "any"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildTestSkeleton(language, fileName, exports, options = {}) {
|
|
126
|
+
const { strict = true, quality = true } = options
|
|
127
|
+
const testName = fileName.replace(/\.[^.]+$/, "")
|
|
128
|
+
|
|
129
|
+
const skeletons = {
|
|
130
|
+
py: () => {
|
|
131
|
+
const imports = `import unittest\nfrom ${testName} import ${exports.map(e => e.name).join(", ")}\n`
|
|
132
|
+
const tests = exports.map(exp => {
|
|
133
|
+
if (exp.type === "class") {
|
|
134
|
+
return `\nclass Test${exp.name}(unittest.TestCase):\n def test_init(self):\n """Test ${exp.name} initialization."""\n instance = ${exp.name}()\n self.assertIsNotNone(instance)\n`
|
|
135
|
+
}
|
|
136
|
+
const params = inferFunctionParams("", exp.name)
|
|
137
|
+
const paramStr = params.map(p => p.defaultValue !== undefined ? `${p.name}=${p.defaultValue}` : "None").join(", ") || ""
|
|
138
|
+
return `\n def test_${exp.name}_smoke(self):\n """Smoke test for ${exp.name}."""\n result = ${exp.name}(${paramStr})\n ${strict ? "self.assertIsNotNone(result)" : "pass"}\n`
|
|
139
|
+
}).join("")
|
|
140
|
+
return `${imports}\n${tests}`
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
js: () => {
|
|
144
|
+
const imports = `const { ${exports.map(e => e.name).join(", ")} } = require("./${testName}")\n`
|
|
145
|
+
const tests = exports.map(exp => {
|
|
146
|
+
const params = inferFunctionParams("", exp.name)
|
|
147
|
+
const paramStr = params.map(p => p.defaultValue !== undefined ? `${p.name}=${p.defaultValue}` : "undefined").join(", ") || ""
|
|
148
|
+
return `\ntest("${exp.name} smoke test", () => {\n const result = ${exp.name}(${paramStr})\n ${strict ? "expect(result).toBeDefined()" : "// TODO: add assertions"}\n})`
|
|
149
|
+
}).join("\n")
|
|
150
|
+
return `${imports}\n${tests}`
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
ts: () => {
|
|
154
|
+
const imports = `import { ${exports.map(e => e.name).join(", ")} } from "./${testName}"\n`
|
|
155
|
+
const tests = exports.map(exp => {
|
|
156
|
+
const params = inferFunctionParams("", exp.name)
|
|
157
|
+
const paramStr = params.map(p => p.defaultValue !== undefined ? `${p.name}=${p.defaultValue}` : "undefined").join(", ") || ""
|
|
158
|
+
return `\ntest("${exp.name} smoke test", () => {\n const result = ${exp.name}(${paramStr})\n ${strict ? "expect(result).toBeDefined()" : "// TODO: add assertions"}\n})`
|
|
159
|
+
}).join("\n")
|
|
160
|
+
return `${imports}\n${tests}`
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
go: () => {
|
|
164
|
+
const tests = exports.map(exp => {
|
|
165
|
+
return `\nfunc Test${capitalize(exp.name)}(t *testing.T) {\n\t// TODO: implement test for ${exp.name}\n\tt.Skip("not implemented")\n}`
|
|
166
|
+
}).join("\n")
|
|
167
|
+
return `package main\n\nimport "testing"\n${tests}`
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
sh: () => {
|
|
171
|
+
const tests = exports.map(exp => {
|
|
172
|
+
return `\ntest_${exp.name}() {\n # TODO: implement test for ${exp.name}\n echo "SKIP: test_${exp.name} not implemented"\n}`
|
|
173
|
+
}).join("\n")
|
|
174
|
+
return `#!/usr/bin/env bash\nset -euo pipefail\n\nsource "./${testName}.sh"\n${tests}`
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
rs: () => {
|
|
178
|
+
const tests = exports.map(exp => {
|
|
179
|
+
return `\n#[test]\nfn test_${exp.name}() {\n // TODO: implement test for ${exp.name}\n}`
|
|
180
|
+
}).join("\n")
|
|
181
|
+
return `#[cfg(test)]\nmod tests {\n use super::*;\n${tests}\n}`
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
rb: () => {
|
|
185
|
+
const tests = exports.map(exp => {
|
|
186
|
+
return `\n def test_${exp.name}\n # TODO: implement test for ${exp.name}\n skip "not implemented"\n end`
|
|
187
|
+
}).join("\n")
|
|
188
|
+
return `require "minitest/autorun"\nrequire_relative "${testName}"\n\nclass Test${capitalize(testName)} < Minitest::Test\n${tests}\nend`
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
java: () => {
|
|
192
|
+
const tests = exports.map(exp => {
|
|
193
|
+
return `\n @Test\n void test${capitalize(exp.name)}() {\n // TODO: implement test for ${exp.name}\n }`
|
|
194
|
+
}).join("\n")
|
|
195
|
+
return `import org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass ${capitalize(testName)}Test {\n${tests}\n}`
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
kt: () => {
|
|
199
|
+
const tests = exports.map(exp => {
|
|
200
|
+
return `\n @Test\n fun test${capitalize(exp.name)}() {\n // TODO: implement test for ${exp.name}\n }`
|
|
201
|
+
}).join("\n")
|
|
202
|
+
return `import org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.Assertions.*\n\nclass ${capitalize(testName)}Test {\n${tests}\n}`
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const generator = skeletons[language] || skeletons.js
|
|
207
|
+
return generator()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function escapeRegex(str) {
|
|
211
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function capitalize(str) {
|
|
215
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export { extractExports, inferFunctionParams, inferTypeFromName, buildTestSkeleton }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const FALLBACK_HIGH = /opus|gemini-.*-pro|deepseek\/deepseek-v4-pro|gpt-5|(^|\/)o[134]($|-|\/)/i
|
|
2
|
+
const FALLBACK_MID = /deepseek\/deepseek-v4-flash|claude.*sonnet|gemini-.*-flash|gpt-4o(?!-mini)/i
|
|
3
|
+
|
|
4
|
+
const BASE_EXPLORATORY = new Set([
|
|
5
|
+
"check", "find", "list", "search", "does", "verify", "look", "count",
|
|
6
|
+
"show", "get", "read", "grep", "scan", "detect", "inspect"
|
|
7
|
+
])
|
|
8
|
+
|
|
9
|
+
function classify(model, customRegex = null) {
|
|
10
|
+
const s = String(model || "").toLowerCase()
|
|
11
|
+
const highRe = customRegex?.high ? new RegExp(customRegex.high, "i") : FALLBACK_HIGH
|
|
12
|
+
const midRe = customRegex?.mid ? new RegExp(customRegex.mid, "i") : FALLBACK_MID
|
|
13
|
+
if (highRe.test(s)) return "high"
|
|
14
|
+
if (midRe.test(s)) return "mid"
|
|
15
|
+
return "budget"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function routeModel(prompt, currentTier, trinityCheap, trinityMedium, learnedExploratory = [], stressScore = 0) {
|
|
19
|
+
const firstWord = prompt.split(/\s+/)[0]?.toLowerCase() || ""
|
|
20
|
+
|
|
21
|
+
const exploratory = new Set([...BASE_EXPLORATORY, ...(learnedExploratory || [])])
|
|
22
|
+
const isExploratory = exploratory.has(firstWord)
|
|
23
|
+
|
|
24
|
+
if (isExploratory && trinityCheap) {
|
|
25
|
+
return { target: trinityCheap, reason: "exploratory_first_word", word: firstWord }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (currentTier === "high" && trinityMedium) {
|
|
29
|
+
let target = trinityMedium
|
|
30
|
+
|
|
31
|
+
if (trinityCheap && stressScore > 0.5) {
|
|
32
|
+
target = trinityCheap
|
|
33
|
+
return { target, reason: "stress_aware_downgrade", stress_score: stressScore }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { target, reason: "medium_fallback" }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { target: trinityCheap || null, reason: "default_cheap" }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isExploratoryPrompt(prompt, learnedExploratory = []) {
|
|
43
|
+
const firstWord = prompt.split(/\s+/)[0]?.toLowerCase() || ""
|
|
44
|
+
const exploratory = new Set([...BASE_EXPLORATORY, ...(learnedExploratory || [])])
|
|
45
|
+
return { is_exploratory: exploratory.has(firstWord), first_word: firstWord }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { classify, routeModel, isExploratoryPrompt, BASE_EXPLORATORY }
|
package/mcp-server.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 vibeOS <https://github.com/DrunkkToys/vibeOS>
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import { parse as parseUrl } from "node:url";
|
|
5
|
+
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
6
|
+
import { extname, join, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
const MIME_MAP = {
|
|
9
|
+
".html": "text/html; charset=utf-8",
|
|
10
|
+
".js": "application/javascript; charset=utf-8",
|
|
11
|
+
".css": "text/css; charset=utf-8",
|
|
12
|
+
".json": "application/json; charset=utf-8",
|
|
13
|
+
".png": "image/png",
|
|
14
|
+
".ico": "image/x-icon",
|
|
15
|
+
};
|
|
16
|
+
function json(res, statusCode, data) {
|
|
17
|
+
res.statusCode = statusCode;
|
|
18
|
+
res.setHeader("Content-Type", "application/json");
|
|
19
|
+
res.end(JSON.stringify(data));
|
|
20
|
+
}
|
|
21
|
+
function parseBody(req) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
let raw = "";
|
|
24
|
+
req.on("data", (chunk) => {
|
|
25
|
+
raw += String(chunk || "");
|
|
26
|
+
if (raw.length > 1024 * 1024) {
|
|
27
|
+
reject(new Error("payload too large"));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
req.on("end", () => {
|
|
31
|
+
if (!raw.trim()) {
|
|
32
|
+
resolve({});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
resolve(JSON.parse(raw));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
reject(new Error("invalid request"));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
req.on("error", reject);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const _MCP_FILENAME = fileURLToPath(import.meta.url);
|
|
46
|
+
const _MCP_DIR = dirname(_MCP_FILENAME);
|
|
47
|
+
function resolveDashboardDir() {
|
|
48
|
+
const c = [
|
|
49
|
+
join(_MCP_DIR, "dashboard", "dist"),
|
|
50
|
+
];
|
|
51
|
+
for (const p of c) {
|
|
52
|
+
if (existsSync(join(p, "index.html")))
|
|
53
|
+
return p;
|
|
54
|
+
}
|
|
55
|
+
return c[0];
|
|
56
|
+
}
|
|
57
|
+
const DASHBOARD_DIR = resolveDashboardDir();
|
|
58
|
+
|
|
59
|
+
const BACKEND_HEALTH_URL = process.env.VIBEOS_BACKEND_HEALTH_URL || "http://127.0.0.1:3000/health"
|
|
60
|
+
const BACKEND_HEALTH_TTL_MS = 5_000
|
|
61
|
+
|
|
62
|
+
let backendHealth = { ok: null, checkedAt: 0 }
|
|
63
|
+
|
|
64
|
+
async function probeBackendHealth(force = false) {
|
|
65
|
+
const now = Date.now()
|
|
66
|
+
if (!force && backendHealth.ok !== null && (now - backendHealth.checkedAt) < BACKEND_HEALTH_TTL_MS) {
|
|
67
|
+
return backendHealth.ok
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const ctl = new AbortController()
|
|
71
|
+
const timer = setTimeout(() => ctl.abort(), 1500)
|
|
72
|
+
const res = await fetch(BACKEND_HEALTH_URL, { signal: ctl.signal })
|
|
73
|
+
clearTimeout(timer)
|
|
74
|
+
backendHealth = { ok: res.ok, checkedAt: now }
|
|
75
|
+
return res.ok
|
|
76
|
+
} catch {
|
|
77
|
+
backendHealth = { ok: false, checkedAt: now }
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sendFile(res, fp) {
|
|
83
|
+
if (!existsSync(fp)) {
|
|
84
|
+
res.statusCode = 404;
|
|
85
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
86
|
+
res.end("not found");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const ext = extname(fp).toLowerCase();
|
|
90
|
+
const mime = MIME_MAP[ext] || "application/octet-stream";
|
|
91
|
+
const st = statSync(fp);
|
|
92
|
+
res.statusCode = 200;
|
|
93
|
+
res.setHeader("Content-Type", mime);
|
|
94
|
+
res.setHeader("Content-Length", st.size);
|
|
95
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
96
|
+
const s = createReadStream(fp);
|
|
97
|
+
s.pipe(res);
|
|
98
|
+
s.on("error", () => { res.statusCode = 500; res.end(); });
|
|
99
|
+
}
|
|
100
|
+
function serveDashboard(res, p) {
|
|
101
|
+
const idx = join(DASHBOARD_DIR, "index.html");
|
|
102
|
+
let fp = join(DASHBOARD_DIR, p === "/" ? "index.html" : p);
|
|
103
|
+
if (existsSync(fp) && statSync(fp).isFile()) {
|
|
104
|
+
sendFile(res, fp);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (existsSync(idx)) {
|
|
108
|
+
sendFile(res, idx);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
res.statusCode = 404;
|
|
112
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
113
|
+
res.end("not found");
|
|
114
|
+
}
|
|
115
|
+
export function createMcpServer(deps) {
|
|
116
|
+
let server = null;
|
|
117
|
+
let startPromise = null;
|
|
118
|
+
let closePromise = null;
|
|
119
|
+
const handler = async (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const method = (req.method || "GET").toUpperCase();
|
|
122
|
+
const parsed = parseUrl(req.url || "/", true);
|
|
123
|
+
const path = parsed.pathname || "/";
|
|
124
|
+
if (method === "GET" && path === "/status") {
|
|
125
|
+
const state = deps.getState()
|
|
126
|
+
const ok = await probeBackendHealth()
|
|
127
|
+
json(res, 200, { ...state, backend_connected: ok === true, backend_health_url: BACKEND_HEALTH_URL })
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (method === "GET" && path === "/savings") {
|
|
131
|
+
json(res, 200, deps.getSavings());
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (method === "GET" && path === "/sessions") {
|
|
135
|
+
const state = deps.getState();
|
|
136
|
+
const sessionsMap = state?.sessions_raw || {};
|
|
137
|
+
const sessions = Object.entries(sessionsMap).map(([id, ses]) => ({
|
|
138
|
+
id,
|
|
139
|
+
started: ses?.started || null,
|
|
140
|
+
cost_usd: Number(ses?.cost_usd ?? 0) || 0,
|
|
141
|
+
delegation_savings_usd: Array.isArray(ses?.warns)
|
|
142
|
+
? ses.warns.reduce((sum, w) => sum + (Number(w?.est_savings_usd ?? 0) || 0), 0)
|
|
143
|
+
: ses?.total_savings_usd || 0,
|
|
144
|
+
cache_savings_usd: Number(ses?.cache_savings_usd ?? 0) || 0,
|
|
145
|
+
warns_count: Array.isArray(ses?.warns) ? ses.warns.length : 0,
|
|
146
|
+
}));
|
|
147
|
+
json(res, 200, { sessions, total_sessions: sessions.length });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (method === "GET" && path === "/sessions/current") {
|
|
151
|
+
json(res, 200, deps.getSessionMetrics(deps.getCurrentSessionId()));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (method === "GET" && path === "/reports") {
|
|
155
|
+
try {
|
|
156
|
+
const query = parsed.query;
|
|
157
|
+
const type = typeof query.type === "string" ? query.type : undefined;
|
|
158
|
+
const project = typeof query.project === "string" ? query.project : undefined;
|
|
159
|
+
const hoursRaw = query.hours;
|
|
160
|
+
const hours = hoursRaw != null ? Number(hoursRaw) : undefined;
|
|
161
|
+
const fingerprint = typeof query.fingerprint === "string" ? query.fingerprint : undefined;
|
|
162
|
+
const reports = deps.listReports({ type, project, hours: Number.isFinite(hours) ? hours : undefined, fingerprint });
|
|
163
|
+
json(res, 200, reports);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
const error = err;
|
|
167
|
+
if (error?.status === 404) {
|
|
168
|
+
json(res, 404, { error: "not found", status: 404 });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (method === "GET" && path.startsWith("/reports/")) {
|
|
176
|
+
const id = decodeURIComponent(path.replace(/^\/reports\//, "")).trim();
|
|
177
|
+
const report = deps.readReport(id);
|
|
178
|
+
if (!report) {
|
|
179
|
+
json(res, 404, { error: "not found", status: 404 });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
json(res, 200, report);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (method === "GET" && path === "/diagnose") {
|
|
186
|
+
json(res, 200, deps.runDiagnose());
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (method === "GET" && path === "/project") {
|
|
190
|
+
json(res, 200, deps.runProject());
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (method === "POST" && path === "/trinity") {
|
|
194
|
+
let body;
|
|
195
|
+
try {
|
|
196
|
+
body = await parseBody(req);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
json(res, 400, { error: "invalid request", status: 400 });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const action = body?.action;
|
|
203
|
+
const slot = body?.slot;
|
|
204
|
+
const level = body?.level;
|
|
205
|
+
if (!action || typeof action !== "string") {
|
|
206
|
+
json(res, 400, { error: "invalid request", status: 400 });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const result = await deps.runTrinity(action, { slot, level });
|
|
210
|
+
const txt = typeof result === "string" ? result : JSON.stringify(result);
|
|
211
|
+
const ok = !(txt.startsWith("❌") || txt.toLowerCase().includes("unknown action"));
|
|
212
|
+
json(res, ok ? 200 : 400, ok ? { ok: true, result } : { ok: false, error: txt });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (method === "POST" && path === "/research-audit") {
|
|
216
|
+
let body;
|
|
217
|
+
try {
|
|
218
|
+
body = await parseBody(req);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
json(res, 400, { error: "invalid request", status: 400 });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const hours = Number(body?.hours ?? 24);
|
|
225
|
+
const report = deps.runResearchAudit(Number.isFinite(hours) ? hours : 24);
|
|
226
|
+
json(res, 200, report);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (method === "POST" && path === "/reports") {
|
|
230
|
+
let body;
|
|
231
|
+
try {
|
|
232
|
+
body = await parseBody(req);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
json(res, 400, { error: "invalid request", status: 400 });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (!body || typeof body !== "object") {
|
|
239
|
+
json(res, 400, { error: "invalid request", status: 400 });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const id = deps.saveReport({
|
|
243
|
+
type: "manual",
|
|
244
|
+
summary: body.summary || "",
|
|
245
|
+
findings: body.findings || [],
|
|
246
|
+
metrics: body.metrics || {},
|
|
247
|
+
narrative: body.narrative || "",
|
|
248
|
+
tags: Array.isArray(body.tags) ? body.tags : [],
|
|
249
|
+
});
|
|
250
|
+
if (!id) {
|
|
251
|
+
json(res, 500, { error: "failed to save report", status: 500 });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
json(res, 200, { ok: true, id });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (method === "POST" && path === "/sessions/checkout") {
|
|
258
|
+
const result = deps.generateSessionCheckout();
|
|
259
|
+
json(res, 200, result);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (method === "GET" && path === "/events") {
|
|
263
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*" });
|
|
264
|
+
const push = async () => { const state = deps.getState(); const ok = await probeBackendHealth(); res.write(`data: ${JSON.stringify({ status: { ...state, backend_connected: ok === true, backend_health_url: BACKEND_HEALTH_URL }, savings: deps.getSavings() })}\n\n`); };
|
|
265
|
+
push();
|
|
266
|
+
const iv = setInterval(push, 1500);
|
|
267
|
+
req.on("close", () => { clearInterval(iv); });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (existsSync(join(DASHBOARD_DIR, "index.html"))) {
|
|
271
|
+
serveDashboard(res, path);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
json(res, 404, { error: "not found", status: 404 });
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
const error = err;
|
|
278
|
+
json(res, 500, { error: error?.message || "internal error", status: 500 });
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
async start(port) {
|
|
283
|
+
if (closePromise)
|
|
284
|
+
await closePromise;
|
|
285
|
+
if (server)
|
|
286
|
+
return server;
|
|
287
|
+
if (startPromise)
|
|
288
|
+
return startPromise;
|
|
289
|
+
const listen = (listenPort) => new Promise((resolve, reject) => {
|
|
290
|
+
const nextServer = http.createServer((req, res) => {
|
|
291
|
+
void handler(req, res);
|
|
292
|
+
});
|
|
293
|
+
const onListening = () => resolve(nextServer);
|
|
294
|
+
const onError = (err) => {
|
|
295
|
+
try {
|
|
296
|
+
nextServer.close();
|
|
297
|
+
}
|
|
298
|
+
catch { }
|
|
299
|
+
reject(err);
|
|
300
|
+
};
|
|
301
|
+
nextServer.once("listening", onListening);
|
|
302
|
+
nextServer.once("error", onError);
|
|
303
|
+
try {
|
|
304
|
+
nextServer.listen(listenPort, "127.0.0.1");
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
onError(err);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
startPromise = (async () => {
|
|
311
|
+
try {
|
|
312
|
+
server = await listen(port);
|
|
313
|
+
return server;
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
const error = err;
|
|
317
|
+
if (error?.code !== "EADDRINUSE" || port === 0) {
|
|
318
|
+
startPromise = null;
|
|
319
|
+
server = null;
|
|
320
|
+
console.error(`[vibeOS] MCP server bind failed: ${error.message}`);
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const fallback = await listen(0);
|
|
325
|
+
server = fallback;
|
|
326
|
+
const bound = fallback.address();
|
|
327
|
+
const actualPort = typeof bound === "object" && bound ? bound.port : 0;
|
|
328
|
+
console.error(`[vibeOS] MCP server port ${port} busy; fell back to ${actualPort}`);
|
|
329
|
+
return fallback;
|
|
330
|
+
}
|
|
331
|
+
catch (fallbackErr) {
|
|
332
|
+
const fbError = fallbackErr;
|
|
333
|
+
startPromise = null;
|
|
334
|
+
server = null;
|
|
335
|
+
console.error(`[vibeOS] MCP server bind failed: ${fbError.message}`);
|
|
336
|
+
throw fallbackErr;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
startPromise = null;
|
|
341
|
+
}
|
|
342
|
+
})();
|
|
343
|
+
return startPromise;
|
|
344
|
+
},
|
|
345
|
+
close() {
|
|
346
|
+
if (!server)
|
|
347
|
+
return closePromise || Promise.resolve();
|
|
348
|
+
if (closePromise)
|
|
349
|
+
return closePromise;
|
|
350
|
+
const current = server;
|
|
351
|
+
closePromise = new Promise((resolve) => {
|
|
352
|
+
try {
|
|
353
|
+
current.close(() => {
|
|
354
|
+
if (server === current)
|
|
355
|
+
server = null;
|
|
356
|
+
closePromise = null;
|
|
357
|
+
resolve();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
if (server === current)
|
|
362
|
+
server = null;
|
|
363
|
+
closePromise = null;
|
|
364
|
+
resolve();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
return closePromise;
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|