@sugar-crash-studios/vibe-forge 0.4.0
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/.claude/commands/clear-attention.md +63 -0
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +102 -0
- package/.claude/commands/forge.md +171 -0
- package/.claude/commands/need-help.md +77 -0
- package/.claude/commands/update-status.md +64 -0
- package/.claude/commands/worker-loop.md +106 -0
- package/.claude/hooks/worker-loop.js +198 -0
- package/.claude/scripts/setup-worker-loop.sh +45 -0
- package/.claude/settings.local.json +46 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/agents/aegis/personality.md +294 -0
- package/agents/anvil/personality.md +276 -0
- package/agents/architect/personality.md +258 -0
- package/agents/crucible/personality.md +360 -0
- package/agents/ember/personality.md +291 -0
- package/agents/forge-master/capabilities.md +144 -0
- package/agents/forge-master/context-template.md +128 -0
- package/agents/forge-master/personality.md +138 -0
- package/agents/furnace/personality.md +340 -0
- package/agents/herald/personality.md +247 -0
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +283 -0
- package/agents/pixel/personality.md +113 -0
- package/agents/planning-hub/personality.md +320 -0
- package/agents/scribe/personality.md +251 -0
- package/agents/temper/personality.md +218 -0
- package/bin/cli.js +375 -0
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +483 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/frontend/index.html +13 -0
- package/bin/dashboard/frontend/package.json +16 -0
- package/bin/dashboard/frontend/src/App.svelte +222 -0
- package/bin/dashboard/frontend/src/app.css +1777 -0
- package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
- package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
- package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
- package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
- package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
- package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
- package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
- package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
- package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
- package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
- package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
- package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
- package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
- package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
- package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
- package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
- package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
- package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
- package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
- package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
- package/bin/dashboard/frontend/src/main.js +9 -0
- package/bin/dashboard/frontend/svelte.config.js +5 -0
- package/bin/dashboard/frontend/vite.config.js +20 -0
- package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
- package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +566 -0
- package/bin/forge-daemon.sh +463 -0
- package/bin/forge-setup.sh +645 -0
- package/bin/forge-spawn.sh +164 -0
- package/bin/forge.cmd +83 -0
- package/bin/forge.sh +533 -0
- package/bin/lib/agents.sh +177 -0
- package/bin/lib/colors.sh +44 -0
- package/bin/lib/config.sh +347 -0
- package/bin/lib/constants.sh +241 -0
- package/bin/lib/daemon/display.sh +128 -0
- package/bin/lib/daemon/notifications.sh +263 -0
- package/bin/lib/daemon/routing.sh +77 -0
- package/bin/lib/daemon/state.sh +115 -0
- package/bin/lib/daemon/sync.sh +95 -0
- package/bin/lib/database.sh +310 -0
- package/bin/lib/heimdall-setup.js +113 -0
- package/bin/lib/heimdall.js +265 -0
- package/bin/lib/json.sh +264 -0
- package/bin/lib/terminal.js +451 -0
- package/bin/lib/util.sh +126 -0
- package/bin/lib/vcs.js +349 -0
- package/config/agent-manifest.yaml +203 -0
- package/config/agents.json +168 -0
- package/config/task-template.md +159 -0
- package/config/task-types.yaml +106 -0
- package/context/agent-status/aegis.json +7 -0
- package/context/agent-status/anvil.json +7 -0
- package/context/agent-status/architect.json +7 -0
- package/context/agent-status/crucible.json +7 -0
- package/context/agent-status/ember.json +7 -0
- package/context/agent-status/furnace.json +7 -0
- package/context/agent-status/loki.json +7 -0
- package/context/agent-status/oracle.json +7 -0
- package/context/agent-status/pixel.json +7 -0
- package/context/agent-status/planning-hub.json +7 -0
- package/context/agent-status/scribe.json +7 -0
- package/context/agent-status/temper.json +7 -0
- package/context/feature-brainstorm.md +426 -0
- package/context/forge-state.yaml +19 -0
- package/context/modern-conventions.md +129 -0
- package/context/project-context-template.md +122 -0
- package/context/project-context.md +122 -0
- package/docs/TODO.md +150 -0
- package/docs/agents.md +409 -0
- package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
- package/docs/architecture/vibe-lab-integration.md +684 -0
- package/docs/architecture.md +194 -0
- package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
- package/docs/cleanup-workflow.md +329 -0
- package/docs/commands.md +451 -0
- package/docs/dashboard-mockup.html +989 -0
- package/docs/getting-started.md +261 -0
- package/docs/integration/forge-ownership-policy.md +112 -0
- package/docs/npm-publishing.md +132 -0
- package/docs/roadmap-2026.md +519 -0
- package/docs/security.md +144 -0
- package/docs/wireframes/dashboard-mvp.md +1164 -0
- package/docs/workflows/README.md +32 -0
- package/docs/workflows/azure-devops.md +108 -0
- package/docs/workflows/bitbucket.md +104 -0
- package/docs/workflows/git-only.md +130 -0
- package/docs/workflows/gitea.md +168 -0
- package/docs/workflows/github.md +103 -0
- package/docs/workflows/gitlab.md +105 -0
- package/docs/workflows.md +454 -0
- package/package.json +73 -0
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
- package/tasks/completed/ARCH-009-test-organization.md +78 -0
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
- package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
- package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
- package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
- package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
- package/tasks/completed/CLEAN-001.md +38 -0
- package/tasks/completed/CLEAN-002.md +43 -0
- package/tasks/completed/CLEAN-003.md +47 -0
- package/tasks/completed/CLEAN-004.md +56 -0
- package/tasks/completed/CLEAN-005.md +75 -0
- package/tasks/completed/CLEAN-006.md +47 -0
- package/tasks/completed/CLEAN-007.md +34 -0
- package/tasks/completed/CLEAN-008.md +49 -0
- package/tasks/completed/CLEAN-012.md +58 -0
- package/tasks/completed/CLEAN-013.md +45 -0
- package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
- package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
- package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
- package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
- package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
- package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
- package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
- package/tasks/completed/PLAT-1-heimdall.md +420 -0
- package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
- package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
- package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
- package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
- package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
- package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
- package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
- package/tasks/completed/review-bmad-aegis.md +89 -0
- package/tasks/completed/review-bmad-anvil.md +80 -0
- package/tasks/completed/review-bmad-crucible.md +81 -0
- package/tasks/completed/review-bmad-ember.md +90 -0
- package/tasks/completed/review-bmad-furnace.md +79 -0
- package/tasks/completed/review-bmad-pixel.md +82 -0
- package/tasks/completed/review-bmad-scribe.md +92 -0
- package/tasks/completed/review-bmad-sentinel.md +83 -0
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
- package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
- package/tasks/pending/ARCH-006-task-template-location.md +64 -0
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
- package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
- package/tasks/pending/CLEAN-009.md +31 -0
- package/tasks/pending/CLEAN-010.md +30 -0
- package/tasks/pending/CLEAN-011.md +30 -0
- package/tasks/pending/CLEAN-014.md +32 -0
- package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
- package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
- package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
- package/tasks/review/bmad-review-aegis.md +349 -0
- package/tasks/review/bmad-review-anvil.md +259 -0
- package/tasks/review/bmad-review-crucible.md +277 -0
- package/tasks/review/bmad-review-ember.md +307 -0
- package/tasks/review/bmad-review-furnace.md +285 -0
- package/tasks/review/bmad-review-pixel.md +329 -0
- package/tasks/review/bmad-review-scribe.md +361 -0
- package/tasks/review/bmad-review-sentinel.md +242 -0
- package/tasks/review/task-001.md +78 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Vibe Forge Dashboard Server
|
|
4
|
+
*
|
|
5
|
+
* HTTP + WebSocket server for the dashboard web UI.
|
|
6
|
+
* Serves static files, REST API, and real-time WebSocket updates.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node bin/dashboard/server.js [--port PORT] [--host HOST]
|
|
10
|
+
* DASHBOARD_PORT=5555 node bin/dashboard/server.js
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const url = require('url');
|
|
17
|
+
|
|
18
|
+
// Configuration
|
|
19
|
+
const DEFAULT_PORT = 2800; // Forge temperature in °F 🔥
|
|
20
|
+
const DEFAULT_HOST = 'localhost';
|
|
21
|
+
|
|
22
|
+
// Resolve paths relative to project root
|
|
23
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
24
|
+
const PUBLIC_DIR = path.join(__dirname, 'public');
|
|
25
|
+
|
|
26
|
+
// Import API handlers
|
|
27
|
+
const tasksApi = require('./api/tasks');
|
|
28
|
+
const agentsApi = require('./api/agents');
|
|
29
|
+
const dispatchApi = require('./api/dispatch');
|
|
30
|
+
|
|
31
|
+
// TTS - lazy-loaded so server still starts if msedge-tts is absent
|
|
32
|
+
let MsEdgeTTS, TTS_OUTPUT_FORMAT;
|
|
33
|
+
try {
|
|
34
|
+
({ MsEdgeTTS, OUTPUT_FORMAT: TTS_OUTPUT_FORMAT } = require('msedge-tts'));
|
|
35
|
+
} catch (_) {
|
|
36
|
+
console.warn('[TTS] msedge-tts not installed — /api/tts will return 503');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Agent → Edge TTS voice mapping
|
|
40
|
+
const AGENT_VOICES = {
|
|
41
|
+
'planning-hub': 'en-US-GuyNeural',
|
|
42
|
+
'forge-master': 'en-US-GuyNeural',
|
|
43
|
+
'oracle': 'en-US-AriaNeural',
|
|
44
|
+
'architect': 'en-GB-RyanNeural',
|
|
45
|
+
'aegis': 'en-US-JennyNeural',
|
|
46
|
+
'pixel': 'en-US-MichelleNeural',
|
|
47
|
+
'ember': 'en-US-ChristopherNeural',
|
|
48
|
+
'anvil': 'en-US-EricNeural',
|
|
49
|
+
'furnace': 'en-US-RogerNeural',
|
|
50
|
+
'crucible': 'en-US-MonicaNeural',
|
|
51
|
+
'temper': 'en-GB-ThomasNeural',
|
|
52
|
+
'scribe': 'en-AU-NatashaNeural',
|
|
53
|
+
'herald': 'en-US-SteffanNeural',
|
|
54
|
+
'loki': 'en-IE-ConnorNeural',
|
|
55
|
+
'system': 'en-US-AriaNeural',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// WebSocket Setup (lazy-loaded)
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
let WebSocketServer = null;
|
|
63
|
+
let wss = null;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Initialize WebSocket server
|
|
67
|
+
* @param {http.Server} server - HTTP server to attach to
|
|
68
|
+
*/
|
|
69
|
+
function initWebSocket(server) {
|
|
70
|
+
try {
|
|
71
|
+
const { WebSocketServer: WSServer } = require('ws');
|
|
72
|
+
WebSocketServer = WSServer;
|
|
73
|
+
|
|
74
|
+
wss = new WebSocketServer({ server, path: '/ws' });
|
|
75
|
+
|
|
76
|
+
wss.on('connection', (ws, req) => {
|
|
77
|
+
const clientIp = req.socket.remoteAddress;
|
|
78
|
+
console.log(`[WS] Client connected: ${clientIp}`);
|
|
79
|
+
|
|
80
|
+
// Send initial connection confirmation
|
|
81
|
+
ws.send(JSON.stringify({
|
|
82
|
+
type: 'connected',
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
message: 'Connected to Vibe Forge Dashboard'
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
ws.on('message', (message) => {
|
|
88
|
+
try {
|
|
89
|
+
const data = JSON.parse(message);
|
|
90
|
+
handleWebSocketMessage(ws, data);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
ws.send(JSON.stringify({
|
|
93
|
+
type: 'error',
|
|
94
|
+
message: 'Invalid JSON message'
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
ws.on('close', () => {
|
|
100
|
+
console.log(`[WS] Client disconnected: ${clientIp}`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
ws.on('error', (err) => {
|
|
104
|
+
console.error(`[WS] Error: ${err.message}`);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.log('[WS] WebSocket server initialized at /ws');
|
|
109
|
+
return true;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.warn('[WS] WebSocket disabled - ws package not installed');
|
|
112
|
+
console.warn('[WS] Run: npm install ws --save-dev');
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle incoming WebSocket messages
|
|
119
|
+
* @param {WebSocket} ws - WebSocket connection
|
|
120
|
+
* @param {Object} data - Parsed message data
|
|
121
|
+
*/
|
|
122
|
+
function handleWebSocketMessage(ws, data) {
|
|
123
|
+
switch (data.type) {
|
|
124
|
+
case 'ping':
|
|
125
|
+
ws.send(JSON.stringify({ type: 'pong', timestamp: new Date().toISOString() }));
|
|
126
|
+
break;
|
|
127
|
+
case 'subscribe':
|
|
128
|
+
// Future: subscribe to specific events
|
|
129
|
+
ws.send(JSON.stringify({
|
|
130
|
+
type: 'subscribed',
|
|
131
|
+
channel: data.channel || 'all'
|
|
132
|
+
}));
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
ws.send(JSON.stringify({
|
|
136
|
+
type: 'unknown',
|
|
137
|
+
message: `Unknown message type: ${data.type}`
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Broadcast message to all connected WebSocket clients
|
|
144
|
+
* @param {Object} data - Data to broadcast
|
|
145
|
+
*/
|
|
146
|
+
function broadcast(data) {
|
|
147
|
+
if (!wss) return;
|
|
148
|
+
|
|
149
|
+
const message = JSON.stringify(data);
|
|
150
|
+
wss.clients.forEach((client) => {
|
|
151
|
+
if (client.readyState === 1) { // WebSocket.OPEN
|
|
152
|
+
client.send(message);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Export broadcast for use by API handlers
|
|
158
|
+
module.exports = { broadcast };
|
|
159
|
+
|
|
160
|
+
// =============================================================================
|
|
161
|
+
// Static File Server
|
|
162
|
+
// =============================================================================
|
|
163
|
+
|
|
164
|
+
const MIME_TYPES = {
|
|
165
|
+
'.html': 'text/html',
|
|
166
|
+
'.css': 'text/css',
|
|
167
|
+
'.js': 'application/javascript',
|
|
168
|
+
'.json': 'application/json',
|
|
169
|
+
'.png': 'image/png',
|
|
170
|
+
'.jpg': 'image/jpeg',
|
|
171
|
+
'.jpeg': 'image/jpeg',
|
|
172
|
+
'.gif': 'image/gif',
|
|
173
|
+
'.svg': 'image/svg+xml',
|
|
174
|
+
'.ico': 'image/x-icon',
|
|
175
|
+
'.woff': 'font/woff',
|
|
176
|
+
'.woff2': 'font/woff2',
|
|
177
|
+
'.ttf': 'font/ttf'
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Serve static file from public directory
|
|
182
|
+
* @param {string} reqPath - Requested path
|
|
183
|
+
* @param {http.ServerResponse} res - Response object
|
|
184
|
+
*/
|
|
185
|
+
function serveStatic(reqPath, res) {
|
|
186
|
+
// Security: prevent directory traversal
|
|
187
|
+
const safePath = path.normalize(reqPath).replace(/^(\.\.[\/\\])+/, '');
|
|
188
|
+
let filePath = path.join(PUBLIC_DIR, safePath);
|
|
189
|
+
|
|
190
|
+
// Default to index.html for root
|
|
191
|
+
if (safePath === '/' || safePath === '') {
|
|
192
|
+
filePath = path.join(PUBLIC_DIR, 'index.html');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Verify file is within PUBLIC_DIR
|
|
196
|
+
if (!filePath.startsWith(PUBLIC_DIR)) {
|
|
197
|
+
sendError(res, 403, 'Forbidden');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fs.stat(filePath, (err, stats) => {
|
|
202
|
+
if (err || !stats.isFile()) {
|
|
203
|
+
// Try index.html for SPA routing
|
|
204
|
+
if (!filePath.endsWith('.html') && !path.extname(filePath)) {
|
|
205
|
+
serveStatic('/index.html', res);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
sendError(res, 404, 'Not Found');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
213
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
214
|
+
|
|
215
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
216
|
+
fs.createReadStream(filePath).pipe(res);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// =============================================================================
|
|
221
|
+
// API Router
|
|
222
|
+
// =============================================================================
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Parse JSON body from request
|
|
226
|
+
* @param {http.IncomingMessage} req - Request object
|
|
227
|
+
* @returns {Promise<Object>} Parsed JSON body
|
|
228
|
+
*/
|
|
229
|
+
function parseBody(req) {
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
let body = '';
|
|
232
|
+
req.on('data', chunk => {
|
|
233
|
+
body += chunk;
|
|
234
|
+
// Limit body size to 1MB
|
|
235
|
+
if (body.length > 1024 * 1024) {
|
|
236
|
+
reject(new Error('Request body too large'));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
req.on('end', () => {
|
|
240
|
+
if (!body) {
|
|
241
|
+
resolve({});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
resolve(JSON.parse(body));
|
|
246
|
+
} catch (err) {
|
|
247
|
+
reject(new Error('Invalid JSON'));
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
req.on('error', reject);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Send JSON response
|
|
256
|
+
* @param {http.ServerResponse} res - Response object
|
|
257
|
+
* @param {number} status - HTTP status code
|
|
258
|
+
* @param {Object} data - Response data
|
|
259
|
+
*/
|
|
260
|
+
function sendJson(res, status, data) {
|
|
261
|
+
res.writeHead(status, {
|
|
262
|
+
'Content-Type': 'application/json',
|
|
263
|
+
'Access-Control-Allow-Origin': '*',
|
|
264
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
265
|
+
'Access-Control-Allow-Headers': 'Content-Type'
|
|
266
|
+
});
|
|
267
|
+
res.end(JSON.stringify(data));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Send error response
|
|
272
|
+
* @param {http.ServerResponse} res - Response object
|
|
273
|
+
* @param {number} status - HTTP status code
|
|
274
|
+
* @param {string} message - Error message
|
|
275
|
+
*/
|
|
276
|
+
function sendError(res, status, message) {
|
|
277
|
+
sendJson(res, status, { error: message });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Route API requests
|
|
282
|
+
* @param {http.IncomingMessage} req - Request object
|
|
283
|
+
* @param {http.ServerResponse} res - Response object
|
|
284
|
+
* @param {string} pathname - Request pathname
|
|
285
|
+
*/
|
|
286
|
+
async function routeApi(req, res, pathname) {
|
|
287
|
+
const method = req.method.toUpperCase();
|
|
288
|
+
|
|
289
|
+
// Handle CORS preflight
|
|
290
|
+
if (method === 'OPTIONS') {
|
|
291
|
+
res.writeHead(204, {
|
|
292
|
+
'Access-Control-Allow-Origin': '*',
|
|
293
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
294
|
+
'Access-Control-Allow-Headers': 'Content-Type'
|
|
295
|
+
});
|
|
296
|
+
res.end();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// Parse request body for POST/PUT
|
|
302
|
+
let body = {};
|
|
303
|
+
if (method === 'POST' || method === 'PUT') {
|
|
304
|
+
body = await parseBody(req);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Tasks API
|
|
308
|
+
if (pathname === '/api/tasks') {
|
|
309
|
+
if (method === 'GET') {
|
|
310
|
+
const result = await tasksApi.listTasks(PROJECT_ROOT);
|
|
311
|
+
sendJson(res, 200, result);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (method === 'POST') {
|
|
315
|
+
const result = await tasksApi.createTask(PROJECT_ROOT, body);
|
|
316
|
+
broadcast({ type: 'task-created', task: result });
|
|
317
|
+
sendJson(res, 201, result);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Single task by ID
|
|
323
|
+
const taskMatch = pathname.match(/^\/api\/tasks\/([a-zA-Z0-9_-]+)$/);
|
|
324
|
+
if (taskMatch) {
|
|
325
|
+
const taskId = taskMatch[1];
|
|
326
|
+
if (method === 'GET') {
|
|
327
|
+
const result = await tasksApi.getTask(PROJECT_ROOT, taskId);
|
|
328
|
+
if (!result) {
|
|
329
|
+
sendError(res, 404, `Task not found: ${taskId}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
sendJson(res, 200, result);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Agents API
|
|
338
|
+
if (pathname === '/api/agents') {
|
|
339
|
+
if (method === 'GET') {
|
|
340
|
+
const result = await agentsApi.listAgents(PROJECT_ROOT);
|
|
341
|
+
sendJson(res, 200, result);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Dispatch API
|
|
347
|
+
if (pathname === '/api/dispatch') {
|
|
348
|
+
if (method === 'POST') {
|
|
349
|
+
try {
|
|
350
|
+
const result = await dispatchApi.dispatch(PROJECT_ROOT, body, broadcast);
|
|
351
|
+
sendJson(res, 201, result);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
// Validation errors → 400; runtime errors bubble to outer catch → 500
|
|
354
|
+
if (err.statusCode === 400) {
|
|
355
|
+
sendError(res, 400, err.message);
|
|
356
|
+
} else {
|
|
357
|
+
throw err;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// TTS API — synthesize speech via msedge-tts and stream MP3
|
|
365
|
+
if (pathname === '/api/tts') {
|
|
366
|
+
if (method === 'GET') {
|
|
367
|
+
if (!MsEdgeTTS) {
|
|
368
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
369
|
+
res.end(JSON.stringify({ error: 'TTS not available — install msedge-tts' }));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const parsedQuery = new url.URL(req.url, `http://${req.headers.host}`).searchParams;
|
|
373
|
+
const text = parsedQuery.get('text') || '';
|
|
374
|
+
const agent = (parsedQuery.get('agent') || 'system').toLowerCase();
|
|
375
|
+
if (!text.trim()) {
|
|
376
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
377
|
+
res.end(JSON.stringify({ error: 'text is required' }));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const voiceName = AGENT_VOICES[agent] || AGENT_VOICES['system'];
|
|
381
|
+
try {
|
|
382
|
+
const tts = new MsEdgeTTS();
|
|
383
|
+
await tts.setMetadata(voiceName, TTS_OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
|
|
384
|
+
const { audioStream } = tts.toStream(text);
|
|
385
|
+
res.writeHead(200, {
|
|
386
|
+
'Content-Type': 'audio/mpeg',
|
|
387
|
+
'Cache-Control': 'no-store',
|
|
388
|
+
'Access-Control-Allow-Origin': '*',
|
|
389
|
+
});
|
|
390
|
+
audioStream.pipe(res);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.error('[TTS] Synthesis error:', err.message);
|
|
393
|
+
if (!res.headersSent) {
|
|
394
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
395
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Issues API — placeholder, returns empty list (detection not yet implemented)
|
|
403
|
+
if (pathname === '/api/issues') {
|
|
404
|
+
if (method === 'GET') {
|
|
405
|
+
sendJson(res, 200, { issues: [], summary: { total: 0 } });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Config endpoint — exposes safe, UI-relevant config fields
|
|
411
|
+
if (pathname === '/api/config') {
|
|
412
|
+
if (method === 'GET') {
|
|
413
|
+
const configPath = path.join(PROJECT_ROOT, '.forge', 'config.json');
|
|
414
|
+
let cfg = {};
|
|
415
|
+
try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (_) {}
|
|
416
|
+
sendJson(res, 200, {
|
|
417
|
+
dashboard_enabled: cfg.dashboard_enabled ?? false,
|
|
418
|
+
dashboard_voice: cfg.dashboard_voice ?? false,
|
|
419
|
+
dashboard_port: cfg.dashboard_port ?? 2800,
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Health check
|
|
426
|
+
if (pathname === '/api/health') {
|
|
427
|
+
sendJson(res, 200, {
|
|
428
|
+
status: 'ok',
|
|
429
|
+
timestamp: new Date().toISOString(),
|
|
430
|
+
version: require(path.join(PROJECT_ROOT, 'package.json')).version
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Not found
|
|
436
|
+
sendError(res, 404, `API endpoint not found: ${pathname}`);
|
|
437
|
+
|
|
438
|
+
} catch (err) {
|
|
439
|
+
console.error(`[API] Error: ${err.message}`);
|
|
440
|
+
sendError(res, 500, err.message);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// =============================================================================
|
|
445
|
+
// HTTP Server
|
|
446
|
+
// =============================================================================
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Main request handler
|
|
450
|
+
* @param {http.IncomingMessage} req - Request object
|
|
451
|
+
* @param {http.ServerResponse} res - Response object
|
|
452
|
+
*/
|
|
453
|
+
function requestHandler(req, res) {
|
|
454
|
+
const parsedUrl = url.parse(req.url, true);
|
|
455
|
+
const pathname = parsedUrl.pathname;
|
|
456
|
+
|
|
457
|
+
// Log request
|
|
458
|
+
console.log(`[HTTP] ${req.method} ${pathname}`);
|
|
459
|
+
|
|
460
|
+
// Route to API or static files
|
|
461
|
+
if (pathname.startsWith('/api/')) {
|
|
462
|
+
routeApi(req, res, pathname);
|
|
463
|
+
} else if (pathname === '/ws') {
|
|
464
|
+
// WebSocket handled by ws library, ignore here
|
|
465
|
+
res.writeHead(426, { 'Content-Type': 'text/plain' });
|
|
466
|
+
res.end('Upgrade Required');
|
|
467
|
+
} else {
|
|
468
|
+
serveStatic(pathname, res);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Start the dashboard server
|
|
474
|
+
* @param {Object} options - Server options
|
|
475
|
+
* @param {number} options.port - Port to listen on
|
|
476
|
+
* @param {string} options.host - Host to bind to
|
|
477
|
+
*/
|
|
478
|
+
function startServer(options = {}) {
|
|
479
|
+
const port = options.port || process.env.DASHBOARD_PORT || DEFAULT_PORT;
|
|
480
|
+
const host = options.host || process.env.DASHBOARD_HOST || DEFAULT_HOST;
|
|
481
|
+
|
|
482
|
+
const server = http.createServer(requestHandler);
|
|
483
|
+
|
|
484
|
+
// Initialize WebSocket
|
|
485
|
+
initWebSocket(server);
|
|
486
|
+
|
|
487
|
+
server.listen(port, host, () => {
|
|
488
|
+
console.log('');
|
|
489
|
+
console.log('='.repeat(50));
|
|
490
|
+
console.log(' 🔥 VIBE FORGE DASHBOARD');
|
|
491
|
+
console.log('='.repeat(50));
|
|
492
|
+
console.log(` URL: http://${host}:${port}`);
|
|
493
|
+
console.log(` API: http://${host}:${port}/api/`);
|
|
494
|
+
console.log(` WebSocket: ws://${host}:${port}/ws`);
|
|
495
|
+
console.log('');
|
|
496
|
+
console.log(` Forge temperature: ${port}°F`);
|
|
497
|
+
console.log('='.repeat(50));
|
|
498
|
+
console.log('');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
server.on('error', (err) => {
|
|
502
|
+
if (err.code === 'EADDRINUSE') {
|
|
503
|
+
console.error(`[ERROR] Port ${port} is already in use`);
|
|
504
|
+
console.error(`[ERROR] Try: DASHBOARD_PORT=${parseInt(port) + 1} node bin/dashboard/server.js`);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
throw err;
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Graceful shutdown
|
|
511
|
+
process.on('SIGTERM', () => {
|
|
512
|
+
console.log('[Server] Shutting down...');
|
|
513
|
+
server.close(() => {
|
|
514
|
+
console.log('[Server] Closed');
|
|
515
|
+
process.exit(0);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
process.on('SIGINT', () => {
|
|
520
|
+
console.log('\n[Server] Interrupted, shutting down...');
|
|
521
|
+
server.close(() => {
|
|
522
|
+
console.log('[Server] Closed');
|
|
523
|
+
process.exit(0);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
return server;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// =============================================================================
|
|
531
|
+
// CLI
|
|
532
|
+
// =============================================================================
|
|
533
|
+
|
|
534
|
+
if (require.main === module) {
|
|
535
|
+
// Parse CLI arguments
|
|
536
|
+
const args = process.argv.slice(2);
|
|
537
|
+
const options = {};
|
|
538
|
+
|
|
539
|
+
for (let i = 0; i < args.length; i++) {
|
|
540
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
541
|
+
options.port = parseInt(args[i + 1], 10);
|
|
542
|
+
i++;
|
|
543
|
+
} else if (args[i] === '--host' && args[i + 1]) {
|
|
544
|
+
options.host = args[i + 1];
|
|
545
|
+
i++;
|
|
546
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
547
|
+
console.log('Vibe Forge Dashboard Server');
|
|
548
|
+
console.log('');
|
|
549
|
+
console.log('Usage: node server.js [options]');
|
|
550
|
+
console.log('');
|
|
551
|
+
console.log('Options:');
|
|
552
|
+
console.log(' --port PORT Port to listen on (default: 2800)');
|
|
553
|
+
console.log(' --host HOST Host to bind to (default: localhost)');
|
|
554
|
+
console.log(' --help, -h Show this help message');
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log('Environment:');
|
|
557
|
+
console.log(' DASHBOARD_PORT Port to listen on');
|
|
558
|
+
console.log(' DASHBOARD_HOST Host to bind to');
|
|
559
|
+
process.exit(0);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
startServer(options);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
module.exports = { startServer, broadcast };
|