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.
Files changed (46) hide show
  1. package/.env.example +5 -0
  2. package/README.md +29 -0
  3. package/client.js +257 -0
  4. package/client.ts +334 -0
  5. package/dashboard/dist/assets/index-BnPt1Fii.js +1 -0
  6. package/dashboard/dist/assets/index-CfH00tOL.css +1 -0
  7. package/dashboard/dist/index.html +3 -0
  8. package/lib/blackbox-rf.js +1099 -0
  9. package/lib/blackbox.js +137 -0
  10. package/lib/compression.js +119 -0
  11. package/lib/db.js +106 -0
  12. package/lib/db.ts +113 -0
  13. package/lib/delegation.js +137 -0
  14. package/lib/meta-controller.js +418 -0
  15. package/lib/meta-controller.mjs +499 -0
  16. package/lib/patterns.js +150 -0
  17. package/lib/resolution-tracker.js +486 -0
  18. package/lib/stress.js +84 -0
  19. package/lib/tdd.js +218 -0
  20. package/lib/tier-routing.js +48 -0
  21. package/mcp-server.js +370 -0
  22. package/mcp-server.ts +364 -0
  23. package/middleware/auth.js +75 -0
  24. package/middleware/auth.ts +87 -0
  25. package/middleware/usage-logging.js +29 -0
  26. package/middleware/usage-logging.ts +41 -0
  27. package/nginx-vibetheog-api.conf +64 -0
  28. package/package.json +66 -0
  29. package/routes/admin.js +93 -0
  30. package/routes/admin.ts +107 -0
  31. package/routes/blackbox.js +463 -0
  32. package/routes/compression.js +12 -0
  33. package/routes/delegation.js +30 -0
  34. package/routes/patterns.js +53 -0
  35. package/routes/pricing.js +62 -0
  36. package/routes/stress.js +30 -0
  37. package/routes/tdd.js +68 -0
  38. package/routes/tier-routing.js +31 -0
  39. package/scripts/dashboard-server.mjs +246 -0
  40. package/scripts/deploy-zero-downtime.sh +77 -0
  41. package/scripts/deploy.sh +68 -0
  42. package/scripts/release.mjs +30 -0
  43. package/scripts/seed-master-token.js +29 -0
  44. package/scripts/start-all.mjs +34 -0
  45. package/server.js +88 -0
  46. 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
+ }