@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.
Files changed (201) hide show
  1. package/.claude/commands/clear-attention.md +63 -0
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -0
  4. package/.claude/commands/forge.md +171 -0
  5. package/.claude/commands/need-help.md +77 -0
  6. package/.claude/commands/update-status.md +64 -0
  7. package/.claude/commands/worker-loop.md +106 -0
  8. package/.claude/hooks/worker-loop.js +198 -0
  9. package/.claude/scripts/setup-worker-loop.sh +45 -0
  10. package/.claude/settings.local.json +46 -0
  11. package/LICENSE +21 -0
  12. package/README.md +238 -0
  13. package/agents/aegis/personality.md +294 -0
  14. package/agents/anvil/personality.md +276 -0
  15. package/agents/architect/personality.md +258 -0
  16. package/agents/crucible/personality.md +360 -0
  17. package/agents/ember/personality.md +291 -0
  18. package/agents/forge-master/capabilities.md +144 -0
  19. package/agents/forge-master/context-template.md +128 -0
  20. package/agents/forge-master/personality.md +138 -0
  21. package/agents/furnace/personality.md +340 -0
  22. package/agents/herald/personality.md +247 -0
  23. package/agents/loki/personality.md +108 -0
  24. package/agents/oracle/personality.md +283 -0
  25. package/agents/pixel/personality.md +113 -0
  26. package/agents/planning-hub/personality.md +320 -0
  27. package/agents/scribe/personality.md +251 -0
  28. package/agents/temper/personality.md +218 -0
  29. package/bin/cli.js +375 -0
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +483 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/frontend/index.html +13 -0
  34. package/bin/dashboard/frontend/package.json +16 -0
  35. package/bin/dashboard/frontend/src/App.svelte +222 -0
  36. package/bin/dashboard/frontend/src/app.css +1777 -0
  37. package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
  38. package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
  39. package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
  40. package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
  41. package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
  42. package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
  43. package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
  44. package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
  45. package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
  46. package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
  47. package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
  48. package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
  49. package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
  50. package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
  51. package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
  52. package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
  53. package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
  54. package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
  55. package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
  56. package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
  57. package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
  58. package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
  59. package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
  60. package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
  61. package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
  62. package/bin/dashboard/frontend/src/main.js +9 -0
  63. package/bin/dashboard/frontend/svelte.config.js +5 -0
  64. package/bin/dashboard/frontend/vite.config.js +20 -0
  65. package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
  66. package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
  67. package/bin/dashboard/public/index.html +14 -0
  68. package/bin/dashboard/server.js +566 -0
  69. package/bin/forge-daemon.sh +463 -0
  70. package/bin/forge-setup.sh +645 -0
  71. package/bin/forge-spawn.sh +164 -0
  72. package/bin/forge.cmd +83 -0
  73. package/bin/forge.sh +533 -0
  74. package/bin/lib/agents.sh +177 -0
  75. package/bin/lib/colors.sh +44 -0
  76. package/bin/lib/config.sh +347 -0
  77. package/bin/lib/constants.sh +241 -0
  78. package/bin/lib/daemon/display.sh +128 -0
  79. package/bin/lib/daemon/notifications.sh +263 -0
  80. package/bin/lib/daemon/routing.sh +77 -0
  81. package/bin/lib/daemon/state.sh +115 -0
  82. package/bin/lib/daemon/sync.sh +95 -0
  83. package/bin/lib/database.sh +310 -0
  84. package/bin/lib/heimdall-setup.js +113 -0
  85. package/bin/lib/heimdall.js +265 -0
  86. package/bin/lib/json.sh +264 -0
  87. package/bin/lib/terminal.js +451 -0
  88. package/bin/lib/util.sh +126 -0
  89. package/bin/lib/vcs.js +349 -0
  90. package/config/agent-manifest.yaml +203 -0
  91. package/config/agents.json +168 -0
  92. package/config/task-template.md +159 -0
  93. package/config/task-types.yaml +106 -0
  94. package/context/agent-status/aegis.json +7 -0
  95. package/context/agent-status/anvil.json +7 -0
  96. package/context/agent-status/architect.json +7 -0
  97. package/context/agent-status/crucible.json +7 -0
  98. package/context/agent-status/ember.json +7 -0
  99. package/context/agent-status/furnace.json +7 -0
  100. package/context/agent-status/loki.json +7 -0
  101. package/context/agent-status/oracle.json +7 -0
  102. package/context/agent-status/pixel.json +7 -0
  103. package/context/agent-status/planning-hub.json +7 -0
  104. package/context/agent-status/scribe.json +7 -0
  105. package/context/agent-status/temper.json +7 -0
  106. package/context/feature-brainstorm.md +426 -0
  107. package/context/forge-state.yaml +19 -0
  108. package/context/modern-conventions.md +129 -0
  109. package/context/project-context-template.md +122 -0
  110. package/context/project-context.md +122 -0
  111. package/docs/TODO.md +150 -0
  112. package/docs/agents.md +409 -0
  113. package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
  114. package/docs/architecture/vibe-lab-integration.md +684 -0
  115. package/docs/architecture.md +194 -0
  116. package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
  117. package/docs/cleanup-workflow.md +329 -0
  118. package/docs/commands.md +451 -0
  119. package/docs/dashboard-mockup.html +989 -0
  120. package/docs/getting-started.md +261 -0
  121. package/docs/integration/forge-ownership-policy.md +112 -0
  122. package/docs/npm-publishing.md +132 -0
  123. package/docs/roadmap-2026.md +519 -0
  124. package/docs/security.md +144 -0
  125. package/docs/wireframes/dashboard-mvp.md +1164 -0
  126. package/docs/workflows/README.md +32 -0
  127. package/docs/workflows/azure-devops.md +108 -0
  128. package/docs/workflows/bitbucket.md +104 -0
  129. package/docs/workflows/git-only.md +130 -0
  130. package/docs/workflows/gitea.md +168 -0
  131. package/docs/workflows/github.md +103 -0
  132. package/docs/workflows/gitlab.md +105 -0
  133. package/docs/workflows.md +454 -0
  134. package/package.json +73 -0
  135. package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
  136. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
  137. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
  138. package/tasks/completed/ARCH-009-test-organization.md +78 -0
  139. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
  140. package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
  141. package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
  142. package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
  143. package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
  144. package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
  145. package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
  146. package/tasks/completed/CLEAN-001.md +38 -0
  147. package/tasks/completed/CLEAN-002.md +43 -0
  148. package/tasks/completed/CLEAN-003.md +47 -0
  149. package/tasks/completed/CLEAN-004.md +56 -0
  150. package/tasks/completed/CLEAN-005.md +75 -0
  151. package/tasks/completed/CLEAN-006.md +47 -0
  152. package/tasks/completed/CLEAN-007.md +34 -0
  153. package/tasks/completed/CLEAN-008.md +49 -0
  154. package/tasks/completed/CLEAN-012.md +58 -0
  155. package/tasks/completed/CLEAN-013.md +45 -0
  156. package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
  157. package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
  158. package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
  159. package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
  160. package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
  161. package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
  162. package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
  163. package/tasks/completed/PLAT-1-heimdall.md +420 -0
  164. package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
  165. package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
  166. package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
  167. package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
  168. package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
  169. package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
  170. package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
  171. package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
  172. package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
  173. package/tasks/completed/review-bmad-aegis.md +89 -0
  174. package/tasks/completed/review-bmad-anvil.md +80 -0
  175. package/tasks/completed/review-bmad-crucible.md +81 -0
  176. package/tasks/completed/review-bmad-ember.md +90 -0
  177. package/tasks/completed/review-bmad-furnace.md +79 -0
  178. package/tasks/completed/review-bmad-pixel.md +82 -0
  179. package/tasks/completed/review-bmad-scribe.md +92 -0
  180. package/tasks/completed/review-bmad-sentinel.md +83 -0
  181. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
  182. package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
  183. package/tasks/pending/ARCH-006-task-template-location.md +64 -0
  184. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
  185. package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
  186. package/tasks/pending/CLEAN-009.md +31 -0
  187. package/tasks/pending/CLEAN-010.md +30 -0
  188. package/tasks/pending/CLEAN-011.md +30 -0
  189. package/tasks/pending/CLEAN-014.md +32 -0
  190. package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
  191. package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
  192. package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
  193. package/tasks/review/bmad-review-aegis.md +349 -0
  194. package/tasks/review/bmad-review-anvil.md +259 -0
  195. package/tasks/review/bmad-review-crucible.md +277 -0
  196. package/tasks/review/bmad-review-ember.md +307 -0
  197. package/tasks/review/bmad-review-furnace.md +285 -0
  198. package/tasks/review/bmad-review-pixel.md +329 -0
  199. package/tasks/review/bmad-review-scribe.md +361 -0
  200. package/tasks/review/bmad-review-sentinel.md +242 -0
  201. 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 };