heyio 1.13.0 → 3.0.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 (327) hide show
  1. package/dist/api/middleware/auth.d.ts +14 -0
  2. package/dist/api/middleware/auth.d.ts.map +1 -0
  3. package/dist/api/middleware/auth.js +66 -0
  4. package/dist/api/middleware/auth.js.map +1 -0
  5. package/dist/api/notifications.d.ts +14 -0
  6. package/dist/api/notifications.d.ts.map +1 -0
  7. package/dist/api/notifications.js +112 -0
  8. package/dist/api/notifications.js.map +1 -0
  9. package/dist/api/routes/activity.d.ts +3 -0
  10. package/dist/api/routes/activity.d.ts.map +1 -0
  11. package/dist/api/routes/activity.js +28 -0
  12. package/dist/api/routes/activity.js.map +1 -0
  13. package/dist/api/routes/attachments.d.ts +3 -0
  14. package/dist/api/routes/attachments.d.ts.map +1 -0
  15. package/dist/api/routes/attachments.js +83 -0
  16. package/dist/api/routes/attachments.js.map +1 -0
  17. package/dist/api/routes/config.d.ts +3 -0
  18. package/dist/api/routes/config.d.ts.map +1 -0
  19. package/dist/api/routes/config.js +106 -0
  20. package/dist/api/routes/config.js.map +1 -0
  21. package/dist/api/routes/conversations.d.ts +3 -0
  22. package/dist/api/routes/conversations.d.ts.map +1 -0
  23. package/dist/api/routes/conversations.js +69 -0
  24. package/dist/api/routes/conversations.js.map +1 -0
  25. package/dist/api/routes/health.d.ts +3 -0
  26. package/dist/api/routes/health.d.ts.map +1 -0
  27. package/dist/api/routes/health.js +16 -0
  28. package/dist/api/routes/health.js.map +1 -0
  29. package/dist/api/routes/inbox.d.ts +3 -0
  30. package/dist/api/routes/inbox.d.ts.map +1 -0
  31. package/dist/api/routes/inbox.js +88 -0
  32. package/dist/api/routes/inbox.js.map +1 -0
  33. package/dist/api/routes/schedules.d.ts +3 -0
  34. package/dist/api/routes/schedules.d.ts.map +1 -0
  35. package/dist/api/routes/schedules.js +96 -0
  36. package/dist/api/routes/schedules.js.map +1 -0
  37. package/dist/api/routes/skills.d.ts +2 -0
  38. package/dist/api/routes/skills.d.ts.map +1 -0
  39. package/dist/api/routes/skills.js +85 -0
  40. package/dist/api/routes/skills.js.map +1 -0
  41. package/dist/api/routes/squads.d.ts +3 -0
  42. package/dist/api/routes/squads.d.ts.map +1 -0
  43. package/dist/api/routes/squads.js +129 -0
  44. package/dist/api/routes/squads.js.map +1 -0
  45. package/dist/api/routes/usage.d.ts +3 -0
  46. package/dist/api/routes/usage.d.ts.map +1 -0
  47. package/dist/api/routes/usage.js +55 -0
  48. package/dist/api/routes/usage.js.map +1 -0
  49. package/dist/api/routes/wiki.d.ts +2 -0
  50. package/dist/api/routes/wiki.d.ts.map +1 -0
  51. package/dist/api/routes/wiki.js +43 -0
  52. package/dist/api/routes/wiki.js.map +1 -0
  53. package/dist/api/server.d.ts +7 -0
  54. package/dist/api/server.d.ts.map +1 -0
  55. package/dist/api/server.js +136 -634
  56. package/dist/api/server.js.map +1 -0
  57. package/dist/config.d.ts +3 -0
  58. package/dist/config.d.ts.map +1 -0
  59. package/dist/config.js +2 -91
  60. package/dist/config.js.map +1 -0
  61. package/dist/copilot/client.d.ts +5 -0
  62. package/dist/copilot/client.d.ts.map +1 -0
  63. package/dist/copilot/client.js +19 -11
  64. package/dist/copilot/client.js.map +1 -0
  65. package/dist/copilot/health-monitor.d.ts +14 -0
  66. package/dist/copilot/health-monitor.d.ts.map +1 -0
  67. package/dist/copilot/health-monitor.js +70 -0
  68. package/dist/copilot/health-monitor.js.map +1 -0
  69. package/dist/copilot/orchestrator.d.ts +5 -0
  70. package/dist/copilot/orchestrator.d.ts.map +1 -0
  71. package/dist/copilot/orchestrator.js +127 -123
  72. package/dist/copilot/orchestrator.js.map +1 -0
  73. package/dist/copilot/tools.d.ts +49 -0
  74. package/dist/copilot/tools.d.ts.map +1 -0
  75. package/dist/copilot/tools.js +545 -321
  76. package/dist/copilot/tools.js.map +1 -0
  77. package/dist/index.d.ts +3 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +82 -26
  80. package/dist/index.js.map +1 -0
  81. package/dist/logging/logger.d.ts +6 -0
  82. package/dist/logging/logger.d.ts.map +1 -0
  83. package/dist/logging/logger.js +21 -0
  84. package/dist/logging/logger.js.map +1 -0
  85. package/dist/models/index.d.ts +6 -0
  86. package/dist/models/index.d.ts.map +1 -0
  87. package/dist/models/index.js +4 -0
  88. package/dist/models/index.js.map +1 -0
  89. package/dist/models/pricing.d.ts +25 -0
  90. package/dist/models/pricing.d.ts.map +1 -0
  91. package/dist/models/pricing.js +96 -0
  92. package/dist/models/pricing.js.map +1 -0
  93. package/dist/models/registry.d.ts +34 -0
  94. package/dist/models/registry.d.ts.map +1 -0
  95. package/dist/models/registry.js +109 -0
  96. package/dist/models/registry.js.map +1 -0
  97. package/dist/models/token-tracker.d.ts +40 -0
  98. package/dist/models/token-tracker.d.ts.map +1 -0
  99. package/dist/models/token-tracker.js +102 -0
  100. package/dist/models/token-tracker.js.map +1 -0
  101. package/dist/scheduler/engine.d.ts +9 -0
  102. package/dist/scheduler/engine.d.ts.map +1 -0
  103. package/dist/scheduler/engine.js +127 -0
  104. package/dist/scheduler/engine.js.map +1 -0
  105. package/dist/skills/index.d.ts +3 -0
  106. package/dist/skills/index.d.ts.map +1 -0
  107. package/dist/skills/index.js +2 -0
  108. package/dist/skills/index.js.map +1 -0
  109. package/dist/skills/store.d.ts +52 -0
  110. package/dist/skills/store.d.ts.map +1 -0
  111. package/dist/skills/store.js +148 -0
  112. package/dist/skills/store.js.map +1 -0
  113. package/dist/squad/agent.d.ts +46 -0
  114. package/dist/squad/agent.d.ts.map +1 -0
  115. package/dist/squad/agent.js +261 -0
  116. package/dist/squad/agent.js.map +1 -0
  117. package/dist/squad/autonomy.d.ts +16 -0
  118. package/dist/squad/autonomy.d.ts.map +1 -0
  119. package/dist/squad/autonomy.js +63 -0
  120. package/dist/squad/autonomy.js.map +1 -0
  121. package/dist/squad/event-bus.d.ts +22 -0
  122. package/dist/squad/event-bus.d.ts.map +1 -0
  123. package/dist/squad/event-bus.js +56 -0
  124. package/dist/squad/event-bus.js.map +1 -0
  125. package/dist/squad/execution/index.d.ts +12 -0
  126. package/dist/squad/execution/index.d.ts.map +1 -0
  127. package/dist/squad/execution/index.js +7 -0
  128. package/dist/squad/execution/index.js.map +1 -0
  129. package/dist/squad/execution/instance.d.ts +40 -0
  130. package/dist/squad/execution/instance.d.ts.map +1 -0
  131. package/dist/squad/execution/instance.js +138 -0
  132. package/dist/squad/execution/instance.js.map +1 -0
  133. package/dist/squad/execution/meeting.d.ts +25 -0
  134. package/dist/squad/execution/meeting.d.ts.map +1 -0
  135. package/dist/squad/execution/meeting.js +140 -0
  136. package/dist/squad/execution/meeting.js.map +1 -0
  137. package/dist/squad/execution/pr.d.ts +15 -0
  138. package/dist/squad/execution/pr.d.ts.map +1 -0
  139. package/dist/squad/execution/pr.js +93 -0
  140. package/dist/squad/execution/pr.js.map +1 -0
  141. package/dist/squad/execution/runner.d.ts +22 -0
  142. package/dist/squad/execution/runner.d.ts.map +1 -0
  143. package/dist/squad/execution/runner.js +68 -0
  144. package/dist/squad/execution/runner.js.map +1 -0
  145. package/dist/squad/execution/tasks.d.ts +11 -0
  146. package/dist/squad/execution/tasks.d.ts.map +1 -0
  147. package/dist/squad/execution/tasks.js +85 -0
  148. package/dist/squad/execution/tasks.js.map +1 -0
  149. package/dist/squad/execution/worktree.d.ts +26 -0
  150. package/dist/squad/execution/worktree.d.ts.map +1 -0
  151. package/dist/squad/execution/worktree.js +111 -0
  152. package/dist/squad/execution/worktree.js.map +1 -0
  153. package/dist/squad/hiring.d.ts +32 -0
  154. package/dist/squad/hiring.d.ts.map +1 -0
  155. package/dist/squad/hiring.js +200 -0
  156. package/dist/squad/hiring.js.map +1 -0
  157. package/dist/squad/index.d.ts +8 -0
  158. package/dist/squad/index.d.ts.map +1 -0
  159. package/dist/squad/index.js +6 -0
  160. package/dist/squad/index.js.map +1 -0
  161. package/dist/squad/manager.d.ts +48 -0
  162. package/dist/squad/manager.d.ts.map +1 -0
  163. package/dist/squad/manager.js +274 -0
  164. package/dist/squad/manager.js.map +1 -0
  165. package/dist/squad/name-generator.d.ts +16 -0
  166. package/dist/squad/name-generator.d.ts.map +1 -0
  167. package/dist/squad/name-generator.js +113 -0
  168. package/dist/squad/name-generator.js.map +1 -0
  169. package/dist/squad/roles/templates.d.ts +5 -0
  170. package/dist/squad/roles/templates.d.ts.map +1 -0
  171. package/dist/squad/roles/templates.js +102 -0
  172. package/dist/squad/roles/templates.js.map +1 -0
  173. package/dist/squad/skill-parser.d.ts +36 -0
  174. package/dist/squad/skill-parser.d.ts.map +1 -0
  175. package/dist/squad/skill-parser.js +83 -0
  176. package/dist/squad/skill-parser.js.map +1 -0
  177. package/dist/squad/source-resolver.d.ts +20 -0
  178. package/dist/squad/source-resolver.d.ts.map +1 -0
  179. package/dist/squad/source-resolver.js +52 -0
  180. package/dist/squad/source-resolver.js.map +1 -0
  181. package/dist/store/activity.d.ts +43 -0
  182. package/dist/store/activity.d.ts.map +1 -0
  183. package/dist/store/activity.js +131 -0
  184. package/dist/store/activity.js.map +1 -0
  185. package/dist/store/db.d.ts +5 -0
  186. package/dist/store/db.d.ts.map +1 -0
  187. package/dist/store/db.js +209 -248
  188. package/dist/store/db.js.map +1 -0
  189. package/dist/store/inbox.d.ts +53 -0
  190. package/dist/store/inbox.d.ts.map +1 -0
  191. package/dist/store/inbox.js +151 -0
  192. package/dist/store/inbox.js.map +1 -0
  193. package/dist/store/schedules.d.ts +53 -0
  194. package/dist/store/schedules.d.ts.map +1 -0
  195. package/dist/store/schedules.js +149 -54
  196. package/dist/store/schedules.js.map +1 -0
  197. package/dist/wiki/index.d.ts +3 -0
  198. package/dist/wiki/index.d.ts.map +1 -0
  199. package/dist/wiki/index.js +2 -0
  200. package/dist/wiki/index.js.map +1 -0
  201. package/dist/wiki/store.d.ts +49 -0
  202. package/dist/wiki/store.d.ts.map +1 -0
  203. package/dist/wiki/store.js +115 -0
  204. package/dist/wiki/store.js.map +1 -0
  205. package/package.json +52 -59
  206. package/src/api/middleware/auth.ts +76 -0
  207. package/src/api/notifications.ts +122 -0
  208. package/src/api/routes/activity.ts +29 -0
  209. package/src/api/routes/attachments.ts +93 -0
  210. package/src/api/routes/config.ts +115 -0
  211. package/src/api/routes/conversations.ts +87 -0
  212. package/src/api/routes/health.ts +18 -0
  213. package/src/api/routes/inbox.ts +98 -0
  214. package/src/api/routes/schedules.ts +121 -0
  215. package/src/api/routes/skills.ts +105 -0
  216. package/src/api/routes/squads.ts +145 -0
  217. package/src/api/routes/usage.ts +57 -0
  218. package/src/api/routes/wiki.ts +49 -0
  219. package/src/api/server.ts +186 -0
  220. package/src/config.ts +3 -0
  221. package/src/copilot/client.ts +42 -0
  222. package/src/copilot/health-monitor.ts +85 -0
  223. package/src/copilot/orchestrator.ts +222 -0
  224. package/src/copilot/tools.ts +707 -0
  225. package/src/index.ts +112 -0
  226. package/src/logging/logger.ts +26 -0
  227. package/src/models/index.ts +11 -0
  228. package/src/models/pricing.ts +121 -0
  229. package/src/models/registry.ts +131 -0
  230. package/src/models/token-tracker.ts +151 -0
  231. package/src/scheduler/engine.ts +146 -0
  232. package/src/skills/index.ts +13 -0
  233. package/src/skills/store.ts +188 -0
  234. package/src/squad/agent.ts +326 -0
  235. package/src/squad/autonomy.ts +78 -0
  236. package/src/squad/event-bus.ts +71 -0
  237. package/src/squad/execution/index.ts +17 -0
  238. package/src/squad/execution/instance.ts +186 -0
  239. package/src/squad/execution/meeting.ts +191 -0
  240. package/src/squad/execution/pr.ts +127 -0
  241. package/src/squad/execution/runner.ts +97 -0
  242. package/src/squad/execution/tasks.ts +111 -0
  243. package/src/squad/execution/worktree.ts +138 -0
  244. package/src/squad/hiring.ts +222 -0
  245. package/src/squad/index.ts +17 -0
  246. package/src/squad/manager.ts +337 -0
  247. package/src/squad/name-generator.ts +135 -0
  248. package/src/squad/roles/templates.ts +104 -0
  249. package/src/squad/skill-parser.ts +120 -0
  250. package/src/squad/source-resolver.ts +57 -0
  251. package/src/store/activity.ts +176 -0
  252. package/src/store/db.ts +237 -0
  253. package/src/store/inbox.ts +199 -0
  254. package/src/store/schedules.ts +199 -0
  255. package/src/wiki/index.ts +12 -0
  256. package/src/wiki/store.ts +139 -0
  257. package/tsconfig.json +9 -0
  258. package/LICENSE +0 -21
  259. package/README.md +0 -333
  260. package/dist/api/auth.js +0 -46
  261. package/dist/chat/attachments.js +0 -112
  262. package/dist/copilot/agents.js +0 -309
  263. package/dist/copilot/ceremonies.js +0 -174
  264. package/dist/copilot/gh-token.js +0 -64
  265. package/dist/copilot/io-scheduler.js +0 -79
  266. package/dist/copilot/model-router.js +0 -114
  267. package/dist/copilot/scheduler.js +0 -88
  268. package/dist/copilot/skills.js +0 -252
  269. package/dist/copilot/specialist-runner.js +0 -191
  270. package/dist/copilot/squad-tools.js +0 -258
  271. package/dist/copilot/system-message.js +0 -86
  272. package/dist/copilot/token-tracker.js +0 -98
  273. package/dist/copilot/trigger-schedule.js +0 -33
  274. package/dist/daemon.js +0 -67
  275. package/dist/logging.js +0 -27
  276. package/dist/mcp/config.js +0 -29
  277. package/dist/mcp/index.js +0 -3
  278. package/dist/mcp/registry.js +0 -42
  279. package/dist/notify.js +0 -25
  280. package/dist/paths.js +0 -17
  281. package/dist/setup.js +0 -35
  282. package/dist/store/agent-events.js +0 -19
  283. package/dist/store/audit-log.js +0 -71
  284. package/dist/store/conversations.js +0 -164
  285. package/dist/store/feed.js +0 -44
  286. package/dist/store/instances.js +0 -75
  287. package/dist/store/squad-colors.js +0 -23
  288. package/dist/store/squads.js +0 -60
  289. package/dist/store/tasks.js +0 -78
  290. package/dist/store/token-usage.js +0 -94
  291. package/dist/telegram/bot.js +0 -41
  292. package/dist/telegram/handlers.js +0 -42
  293. package/dist/watchdog.js +0 -37
  294. package/dist/wiki/backlinks.js +0 -51
  295. package/dist/wiki/fs.js +0 -108
  296. package/dist/wiki/search.js +0 -47
  297. package/web-dist/assets/AuditLogView-BzfjNXBT.js +0 -6
  298. package/web-dist/assets/ChatView-BdMukPKG.js +0 -1
  299. package/web-dist/assets/FeedView-BfPIabGr.js +0 -6
  300. package/web-dist/assets/HistoryView-BmEEk3Rs.js +0 -1
  301. package/web-dist/assets/LoginView-D7LrkeX7.js +0 -1
  302. package/web-dist/assets/McpView-BAP_ah3T.js +0 -1
  303. package/web-dist/assets/SchedulesView-CAtsUPCZ.js +0 -6
  304. package/web-dist/assets/SettingsView-BovjWZDa.js +0 -1
  305. package/web-dist/assets/SkillsView-DQSMM5LN.js +0 -16
  306. package/web-dist/assets/SquadDetailView-DBscu0m2.js +0 -26
  307. package/web-dist/assets/SquadHealthView-D686BuQo.js +0 -11
  308. package/web-dist/assets/SquadsView-AzMht2NJ.js +0 -6
  309. package/web-dist/assets/ToggleSwitch.vue_vue_type_script_setup_true_lang-DtShZAjW.js +0 -1
  310. package/web-dist/assets/UsageView-DiVn97aI.js +0 -16
  311. package/web-dist/assets/WikiView-Cn7KipkZ.js +0 -26
  312. package/web-dist/assets/api-D4mHJ3u0.js +0 -1
  313. package/web-dist/assets/arrow-left-D_qUNUWW.js +0 -6
  314. package/web-dist/assets/git-branch-DOM-orcl.js +0 -6
  315. package/web-dist/assets/index-Bo83B1LR.css +0 -1
  316. package/web-dist/assets/index-ELvnkQjd.js +0 -273
  317. package/web-dist/assets/pencil-CFsi7ufI.js +0 -6
  318. package/web-dist/assets/plus-BAzlGFd_.js +0 -6
  319. package/web-dist/assets/save-BmgCYJ1g.js +0 -6
  320. package/web-dist/assets/search-CS9zSIeW.js +0 -6
  321. package/web-dist/assets/squad-colors-B8B_Y-lz.js +0 -1
  322. package/web-dist/assets/trash-2-DLveUEsd.js +0 -6
  323. package/web-dist/assets/triangle-alert-lj4I30rL.js +0 -6
  324. package/web-dist/assets/x-CjXR97Fa.js +0 -6
  325. package/web-dist/favicon.svg +0 -10
  326. package/web-dist/index.html +0 -14
  327. package/web-dist/logo.svg +0 -10
@@ -1,650 +1,152 @@
1
- import express from "express";
2
- import { join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { dirname, resolve } from "node:path";
5
- import { existsSync } from "node:fs";
6
- import { loadConfig, saveConfig } from "../config.js";
7
- import { createAuthMiddleware } from "./auth.js";
8
- import { sendToOrchestrator } from "../copilot/orchestrator.js";
9
- import { listSquads, getSquad, getAgentsForSquad } from "../store/squads.js";
10
- import { getTasksForSquad, getSquadTaskMetrics } from "../store/tasks.js";
11
- import { getInstancesForSquad, destroyInstance } from "../store/instances.js";
12
- import { getAgentEvents } from "../store/agent-events.js";
13
- import { getAuditLog, countAuditLog } from "../store/audit-log.js";
14
- import { getFeedItems, markFeedItemRead, deleteFeedItem, getUnreadCount, } from "../store/feed.js";
15
- import { listSchedules, createSchedule, updateSchedule, deleteSchedule, getSchedule } from "../store/schedules.js";
16
- import { triggerSchedule } from "../copilot/trigger-schedule.js";
17
- import { listServers, toggleMcpServer, addMcpServer, removeMcpServer } from "../mcp/index.js";
18
- import { listSkills, addSkill, createSkill, removeSkill, getSkillContent, updateSkillContent, discoverSkills, installFromSource, fetchRemoteSkillPreview } from "../copilot/skills.js";
19
- import { readPage, writePage, deletePage, listPages, listTemplates, readTemplate, writeTemplate, deleteTemplate } from "../wiki/fs.js";
20
- import { searchPages } from "../wiki/search.js";
21
- import { getBacklinks } from "../wiki/backlinks.js";
22
- import { saveMessage, getConversation, listConversations, searchConversations, deleteConversation, } from "../store/conversations.js";
23
- import { getTokenUsageSummary, getTokenUsageBySquad, getTokenUsageByAgent, getDailyTokenUsage, } from "../store/token-usage.js";
24
- import { DEFAULT_MODEL_PRICING } from "../copilot/token-tracker.js";
25
- import { randomUUID } from "node:crypto";
26
- import { validateMessageAttachments } from "../chat/attachments.js";
27
- const __filename = fileURLToPath(import.meta.url);
28
- const __dirname = dirname(__filename);
29
- const sseClients = [];
30
- function broadcast(event, data) {
31
- const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
32
- for (const client of sseClients) {
33
- if (!client.res.writableEnded) {
34
- client.res.write(payload);
35
- }
36
- }
37
- }
38
- export async function startApiServer(config) {
1
+ import { existsSync } from 'node:fs';
2
+ import { createServer } from 'node:http';
3
+ import { join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import express from 'express';
6
+ import { WebSocketServer } from 'ws';
7
+ import { sendMessage } from '../copilot/orchestrator.js';
8
+ import { createChildLogger } from '../logging/logger.js';
9
+ import { authMiddleware, verifyWsToken } from './middleware/auth.js';
10
+ import { initNotifications, subscribeClient, unsubscribeClient } from './notifications.js';
11
+ import { activityRouter } from './routes/activity.js';
12
+ import { attachmentsRouter } from './routes/attachments.js';
13
+ import { configRouter } from './routes/config.js';
14
+ import { conversationsRouter } from './routes/conversations.js';
15
+ import { healthRouter } from './routes/health.js';
16
+ import { inboxRouter } from './routes/inbox.js';
17
+ import { schedulesRouter } from './routes/schedules.js';
18
+ import { skillsRouter } from './routes/skills.js';
19
+ import { squadsRouter } from './routes/squads.js';
20
+ import { usageRouter } from './routes/usage.js';
21
+ import { wikiRouter } from './routes/wiki.js';
22
+ // Connected WebSocket clients keyed by connection ID
23
+ const wsClients = new Map();
24
+ export function createApiServer(config) {
25
+ const logger = createChildLogger('api');
39
26
  const app = express();
40
- app.use(express.json({ limit: "30mb" }));
41
- // Serve static web frontend
42
- const webDistPath = resolve(__dirname, "..", "..", "web-dist");
43
- const webIndexPath = join(webDistPath, "index.html");
44
- if (existsSync(webDistPath)) {
45
- app.use(express.static(webDistPath));
46
- }
47
- else {
48
- console.warn(`[io] Warning: web-dist not found at ${webDistPath}. Web UI will not be available.`);
49
- }
50
- // Auth middleware for all API routes
51
- const auth = createAuthMiddleware(config);
52
- // Public endpoint — serves Supabase config to the frontend (no auth required)
53
- app.get("/api/auth/config", (_req, res) => {
54
- res.json({
55
- supabaseUrl: config.supabaseUrl ?? null,
56
- supabaseAnonKey: config.supabaseAnonKey ?? null,
57
- });
58
- });
59
- app.use("/api", auth);
60
- // --- SSE Stream ---
61
- app.get("/api/stream", (req, res) => {
62
- res.setHeader("Content-Type", "text/event-stream");
63
- res.setHeader("Cache-Control", "no-cache");
64
- res.setHeader("Connection", "keep-alive");
65
- res.flushHeaders();
66
- const client = { id: randomUUID(), res };
67
- sseClients.push(client);
68
- res.write(`event: connected\ndata: ${JSON.stringify({ id: client.id })}\n\n`);
69
- req.on("close", () => {
70
- const idx = sseClients.indexOf(client);
71
- if (idx !== -1)
72
- sseClients.splice(idx, 1);
73
- });
74
- });
75
- // --- Chat (SSE-streamed response) ---
76
- app.post("/api/message", async (req, res) => {
77
- const { prompt, conversationId: clientConvId, attachments: rawAttachments } = req.body;
78
- if (!prompt || typeof prompt !== "string") {
79
- res.status(400).json({ error: "prompt is required" });
80
- return;
81
- }
82
- const validation = validateMessageAttachments(rawAttachments);
83
- if (!validation.ok) {
84
- res.status(400).json({ error: validation.error });
85
- return;
86
- }
87
- const attachments = validation.attachments;
88
- const conversationId = (typeof clientConvId === "string" && clientConvId) ? clientConvId : randomUUID();
89
- // Persist the user message
90
- saveMessage(conversationId, "user", prompt, "web", attachments);
91
- // Switch to SSE streaming to avoid Cloudflare 524 timeouts
92
- res.setHeader("Content-Type", "text/event-stream");
93
- res.setHeader("Cache-Control", "no-cache");
94
- res.setHeader("Connection", "keep-alive");
95
- res.setHeader("X-Accel-Buffering", "no");
96
- res.flushHeaders();
97
- // Keepalive: send a comment every 30s to keep Cloudflare happy
98
- const keepalive = setInterval(() => {
99
- if (!finished)
100
- res.write(": keepalive\n\n");
101
- }, 30_000);
102
- let finished = false;
103
- req.on("close", () => { finished = true; clearInterval(keepalive); });
104
- try {
105
- await sendToOrchestrator(prompt, "web", (content, done) => {
106
- if (finished)
107
- return;
108
- if (done) {
109
- finished = true;
110
- saveMessage(conversationId, "assistant", content, "web");
111
- res.write(`event: done\ndata: ${JSON.stringify({ content, conversationId })}\n\n`);
112
- clearInterval(keepalive);
113
- res.end();
114
- }
115
- else {
116
- res.write(`event: delta\ndata: ${JSON.stringify({ content })}\n\n`);
117
- // Also broadcast to other SSE listeners (e.g. ChatOverlay)
118
- broadcast("message_delta", { content, done: false });
119
- }
120
- }, attachments);
121
- }
122
- catch (err) {
123
- if (!finished) {
124
- finished = true;
125
- const message = err instanceof Error ? err.message : "Unknown error";
126
- res.write(`event: error\ndata: ${JSON.stringify({ error: message })}\n\n`);
127
- clearInterval(keepalive);
128
- res.end();
27
+ app.use(express.json());
28
+ // Auth middleware verifies Supabase JWT if configured
29
+ app.use('/api', authMiddleware(config));
30
+ // Routes
31
+ app.use('/api', healthRouter());
32
+ app.use('/api', usageRouter());
33
+ app.use('/api', squadsRouter());
34
+ app.use('/api', activityRouter());
35
+ app.use('/api', attachmentsRouter(config.dataDir));
36
+ app.use('/api', inboxRouter());
37
+ app.use('/api', schedulesRouter());
38
+ app.use('/api', conversationsRouter());
39
+ app.use('/api', configRouter());
40
+ app.use('/api/wiki', wikiRouter);
41
+ app.use('/api', skillsRouter);
42
+ // POST /api/messages — send a message to the orchestrator
43
+ app.post('/api/messages', async (req, res) => {
44
+ const { content, source, connectionId } = req.body;
45
+ if (!content) {
46
+ res.status(400).json({ error: 'content is required' });
47
+ return;
48
+ }
49
+ const ws = connectionId ? wsClients.get(connectionId) : undefined;
50
+ const onDelta = (accumulated, done) => {
51
+ if (ws && ws.readyState === ws.OPEN) {
52
+ ws.send(JSON.stringify({
53
+ type: done ? 'message' : 'delta',
54
+ content: accumulated,
55
+ }));
129
56
  }
130
- }
131
- });
132
- // --- History ---
133
- app.get("/api/history", (req, res) => {
134
- const q = req.query.q;
135
- const from = req.query.from;
136
- const to = req.query.to;
137
- const limit = parseInt(req.query.limit) || 50;
138
- const offset = parseInt(req.query.offset) || 0;
139
- if (q) {
140
- res.json(searchConversations(q, { limit, offset, from, to }));
141
- }
142
- else {
143
- res.json(listConversations({ limit, offset, from, to }));
144
- }
145
- });
146
- app.get("/api/history/:id", (req, res) => {
147
- const messages = getConversation(req.params.id);
148
- if (messages.length === 0) {
149
- res.status(404).json({ error: "Conversation not found" });
150
- return;
151
- }
152
- res.json(messages);
153
- });
154
- app.delete("/api/history/:id", (req, res) => {
155
- deleteConversation(req.params.id);
156
- res.json({ ok: true });
157
- });
158
- // --- Squads ---
159
- app.get("/api/squads", (_req, res) => {
160
- const data = listSquads();
161
- const instanceCounts = {};
162
- for (const squad of data.squads) {
163
- instanceCounts[squad.id] = getInstancesForSquad(squad.id).length;
164
- }
165
- res.json({ ...data, instanceCounts });
166
- });
167
- // --- Squad Health Dashboard ---
168
- app.get("/api/squads/health", (_req, res) => {
169
- const { squads, agents } = listSquads();
170
- const health = squads.map((squad) => {
171
- const squadAgents = agents.filter((a) => a.squad_id === squad.id);
172
- const instances = getInstancesForSquad(squad.id);
173
- const metrics = getSquadTaskMetrics(squad.id);
174
- return {
175
- id: squad.id,
176
- name: squad.name,
177
- universe: squad.universe,
178
- agentCount: squadAgents.length,
179
- activeInstanceCount: instances.length,
180
- activeInstances: instances.map((inst) => ({
181
- id: inst.id,
182
- branch: inst.branch,
183
- lastActivity: inst.last_activity,
184
- })),
185
- tasksTotal: metrics.tasksTotal,
186
- tasksCompleted: metrics.tasksCompleted,
187
- tasksCompletedRecent: metrics.tasksCompletedRecent,
188
- tasksPending: metrics.tasksPending,
189
- tasksInProgress: metrics.tasksInProgress,
190
- tasksFailed: metrics.tasksFailed,
191
- avgCycleTimeMinutes: metrics.avgCycleTimeMinutes,
192
- isStalled: metrics.isStalled,
193
- recentTasks: metrics.recentTasks.map((t) => ({
194
- id: t.id,
195
- description: t.description,
196
- status: t.status,
197
- updatedAt: t.updated_at,
198
- })),
199
- };
200
- });
201
- res.json({ health });
202
- });
203
- app.get("/api/squads/:id", (req, res) => {
204
- const squad = getSquad(req.params.id);
205
- if (!squad) {
206
- res.status(404).json({ error: "Squad not found" });
207
- return;
208
- }
209
- const agents = getAgentsForSquad(req.params.id);
210
- const tasks = getTasksForSquad(req.params.id);
211
- const instances = getInstancesForSquad(req.params.id);
212
- res.json({ squad, agents, tasks, instances });
213
- });
214
- app.delete("/api/instances/:id", async (req, res) => {
215
- try {
216
- await destroyInstance(req.params.id);
217
- res.json({ ok: true });
218
- }
219
- catch (err) {
220
- const msg = err?.message ?? "Unknown error";
221
- const status = msg.toLowerCase().includes("not found") ? 404 : 500;
222
- res.status(status).json({ error: msg });
223
- }
224
- });
225
- // --- Task Events ---
226
- app.get("/api/tasks/:taskId/events", (req, res) => {
227
- const events = getAgentEvents(req.params.taskId);
228
- res.json(events);
229
- });
230
- // --- Stop Task ---
231
- app.post("/api/tasks/:taskId/stop", async (req, res) => {
57
+ };
232
58
  try {
233
- const { stopTask } = await import("../copilot/agents.js");
234
- await stopTask(req.params.taskId);
235
- res.json({ ok: true });
59
+ const response = await sendMessage(content, source ?? 'web', onDelta);
60
+ res.json({ status: 'ok', content: response });
236
61
  }
237
62
  catch (err) {
238
- const msg = err?.message ?? "Unknown error";
239
- const isNotRunning = msg.toLowerCase().includes("not currently running") || msg.toLowerCase().includes("already completed");
240
- res.status(isNotRunning ? 404 : 500).json({ error: msg });
63
+ logger.error({ err }, 'Error processing message');
64
+ res.status(500).json({ error: 'Failed to process message' });
241
65
  }
242
66
  });
243
- // --- Audit Log ---
244
- app.get("/api/audit-log", (req, res) => {
245
- const squad_id = req.query.squad_id;
246
- const agent_id = req.query.agent_id;
247
- const action_type = req.query.action_type;
248
- const from = req.query.from;
249
- const to = req.query.to;
250
- const limit = parseInt(req.query.limit) || 50;
251
- const offset = parseInt(req.query.offset) || 0;
252
- const filters = { squad_id, agent_id, action_type, from, to, limit, offset };
253
- res.json({
254
- entries: getAuditLog(filters),
255
- total: countAuditLog(filters),
256
- });
257
- });
258
- // --- Feed ---
259
- app.get("/api/feed", (req, res) => {
260
- const unreadOnly = req.query.unread === "true";
261
- const source = req.query.source;
262
- const limit = parseInt(req.query.limit) || 50;
263
- const offset = parseInt(req.query.offset) || 0;
264
- res.json({
265
- items: getFeedItems({ unreadOnly, source, limit, offset }),
266
- unreadCount: getUnreadCount(),
67
+ // Serve web frontend static files (production build)
68
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
69
+ const webDistPath = resolve(__dirname, '../../../web/dist');
70
+ if (existsSync(webDistPath)) {
71
+ app.use(express.static(webDistPath));
72
+ // SPA fallback: serve index.html for any non-API route
73
+ app.get('*', (_req, res) => {
74
+ res.sendFile(join(webDistPath, 'index.html'));
267
75
  });
268
- });
269
- app.post("/api/feed/:id/read", (req, res) => {
270
- markFeedItemRead(req.params.id);
271
- res.json({ ok: true });
272
- });
273
- app.delete("/api/feed/:id", (req, res) => {
274
- deleteFeedItem(req.params.id);
275
- res.json({ ok: true });
276
- });
277
- // --- MCP Servers ---
278
- app.get("/api/mcp", (_req, res) => {
279
- res.json(listServers());
280
- });
281
- app.post("/api/mcp", (req, res) => {
282
- const server = { id: randomUUID(), ...req.body, enabled: true };
283
- addMcpServer(server);
284
- res.json(server);
285
- });
286
- app.put("/api/mcp/:id", (req, res) => {
287
- const { enabled } = req.body;
288
- if (typeof enabled === "boolean") {
289
- toggleMcpServer(req.params.id, enabled);
290
- }
291
- res.json({ ok: true });
292
- });
293
- app.delete("/api/mcp/:id", (req, res) => {
294
- removeMcpServer(req.params.id);
295
- res.json({ ok: true });
296
- });
297
- // --- Skills ---
298
- app.get("/api/skills", async (_req, res) => {
299
- const skills = await listSkills();
300
- res.json(skills);
301
- });
302
- app.get("/api/skills/discover", async (req, res) => {
303
- const source = req.query.source;
304
- if (source !== "awesome-copilot" && source !== "skillssh") {
305
- res.status(400).json({ error: "source must be 'awesome-copilot' or 'skillssh'" });
306
- return;
307
- }
308
- const q = req.query.q;
309
- try {
310
- const skills = await discoverSkills(source, q);
311
- res.json(skills);
312
- }
313
- catch (err) {
314
- res.status(502).json({ error: err.message });
315
- }
316
- });
317
- app.get("/api/skills/preview", async (req, res) => {
318
- const source = req.query.source;
319
- const slug = req.query.slug;
320
- if (source !== "awesome-copilot" && source !== "skillssh") {
321
- res.status(400).json({ error: "source must be 'awesome-copilot' or 'skillssh'" });
322
- return;
323
- }
324
- if (!slug) {
325
- res.status(400).json({ error: "slug is required" });
326
- return;
327
- }
328
- try {
329
- const sourceRepo = req.query.sourceRepo;
330
- const content = await fetchRemoteSkillPreview(source, slug, sourceRepo);
331
- res.json({ content });
332
- }
333
- catch (err) {
334
- res.status(502).json({ error: err.message });
335
- }
336
- });
337
- app.post("/api/skills", async (req, res) => {
338
- try {
339
- const { url, source, slug, content, sourceRepo } = req.body;
340
- if (source && slug) {
341
- if (source !== "awesome-copilot" && source !== "skillssh") {
342
- res.status(400).json({ error: "source must be 'awesome-copilot' or 'skillssh'" });
343
- return;
76
+ logger.info({ path: webDistPath }, 'Serving web frontend');
77
+ }
78
+ const server = createServer(app);
79
+ // WebSocket server for streaming
80
+ const wss = new WebSocketServer({ server, path: '/ws' });
81
+ wss.on('connection', (ws, req) => {
82
+ // Verify token from query string if auth is configured
83
+ const url = new URL(req.url ?? '', `http://${req.headers.host}`);
84
+ const token = url.searchParams.get('token');
85
+ if (!verifyWsToken(config, token)) {
86
+ ws.close(4001, 'Unauthorized');
87
+ return;
88
+ }
89
+ const connectionId = crypto.randomUUID();
90
+ wsClients.set(connectionId, ws);
91
+ subscribeClient(connectionId, ws);
92
+ logger.info({ connectionId }, 'WebSocket client connected');
93
+ // Send the connection ID to the client
94
+ ws.send(JSON.stringify({ type: 'connected', connectionId }));
95
+ ws.on('message', (data) => {
96
+ try {
97
+ const parsed = JSON.parse(data.toString());
98
+ if (parsed.type === 'message' && parsed.content) {
99
+ const source = parsed.source ?? 'tui';
100
+ const onDelta = (accumulated, done) => {
101
+ if (ws.readyState === ws.OPEN) {
102
+ ws.send(JSON.stringify({
103
+ type: done ? 'message' : 'delta',
104
+ content: accumulated,
105
+ }));
106
+ }
107
+ };
108
+ sendMessage(parsed.content, source, onDelta).catch((err) => {
109
+ logger.error({ err }, 'Error processing WebSocket message');
110
+ if (ws.readyState === ws.OPEN) {
111
+ ws.send(JSON.stringify({ type: 'error', content: 'Failed to process message' }));
112
+ }
113
+ });
344
114
  }
345
- await installFromSource(source, slug, sourceRepo);
346
- }
347
- else if (url && typeof url === "string") {
348
- // Git-clone method
349
- await addSkill(url);
350
- }
351
- else if (slug && typeof slug === "string" && content && typeof content === "string") {
352
- // Direct-creation method
353
- await createSkill(slug, content);
354
115
  }
355
- else {
356
- res.status(400).json({ error: "Provide 'url' (git clone), 'source' + 'slug' (community install), or 'slug' + 'content' (direct create)" });
357
- return;
116
+ catch (err) {
117
+ logger.error({ err }, 'Failed to parse WebSocket message');
358
118
  }
359
- res.status(201).json({ ok: true });
360
- }
361
- catch (err) {
362
- res.status(400).json({ error: err.message });
363
- }
364
- });
365
- app.delete("/api/skills/:slug", async (req, res) => {
366
- try {
367
- await removeSkill(req.params.slug);
368
- res.json({ ok: true });
369
- }
370
- catch (err) {
371
- res.status(404).json({ error: err.message });
372
- }
373
- });
374
- app.get("/api/skills/:slug/content", async (req, res) => {
375
- try {
376
- const content = await getSkillContent(req.params.slug);
377
- res.json({ content });
378
- }
379
- catch (err) {
380
- res.status(404).json({ error: err.message });
381
- }
382
- });
383
- app.put("/api/skills/:slug/content", async (req, res) => {
384
- try {
385
- const { content } = req.body;
386
- await updateSkillContent(req.params.slug, content);
387
- res.json({ ok: true });
388
- }
389
- catch (err) {
390
- res.status(404).json({ error: err.message });
391
- }
392
- });
393
- // --- Wiki ---
394
- app.get("/api/wiki/pages", async (_req, res) => {
395
- const pages = await listPages();
396
- res.json(pages);
397
- });
398
- app.get("/api/wiki/page/*path", async (req, res) => {
399
- try {
400
- const raw = req.params.path;
401
- const pagePath = Array.isArray(raw) ? raw.join("/") : raw;
402
- const content = await readPage(pagePath);
403
- res.json({ path: pagePath, content });
404
- }
405
- catch (err) {
406
- res.status(404).json({ error: err.message });
407
- }
408
- });
409
- app.put("/api/wiki/page/*path", async (req, res) => {
410
- const raw = req.params.path;
411
- const pagePath = Array.isArray(raw) ? raw.join("/") : raw;
412
- const { content } = req.body;
413
- await writePage(pagePath, content);
414
- res.json({ ok: true });
415
- });
416
- app.delete("/api/wiki/page/*path", async (req, res) => {
417
- try {
418
- const raw = req.params.path;
419
- const pagePath = Array.isArray(raw) ? raw.join("/") : raw;
420
- await deletePage(pagePath);
421
- res.json({ ok: true });
422
- }
423
- catch (err) {
424
- res.status(404).json({ error: err.message });
425
- }
426
- });
427
- app.get("/api/wiki/search", async (req, res) => {
428
- const query = req.query.q;
429
- if (!query) {
430
- res.status(400).json({ error: "q is required" });
431
- return;
432
- }
433
- const results = await searchPages(query);
434
- res.json(results);
435
- });
436
- app.get("/api/wiki/backlinks/*path", async (req, res) => {
437
- const raw = req.params.path;
438
- const pagePath = Array.isArray(raw) ? raw.join("/") : raw;
439
- const backlinks = await getBacklinks(pagePath);
440
- res.json(backlinks);
441
- });
442
- // --- Wiki Templates ---
443
- app.get("/api/wiki/templates/squad", async (_req, res) => {
444
- const files = await listTemplates();
445
- res.json(files);
446
- });
447
- app.get("/api/wiki/template/squad/*path", async (req, res) => {
448
- try {
449
- const raw = req.params.path;
450
- const templatePath = Array.isArray(raw) ? raw.join("/") : raw;
451
- const content = await readTemplate(templatePath);
452
- res.json({ path: templatePath, content });
453
- }
454
- catch (err) {
455
- res.status(404).json({ error: err.message });
456
- }
457
- });
458
- app.put("/api/wiki/template/squad/*path", async (req, res) => {
459
- const raw = req.params.path;
460
- const templatePath = Array.isArray(raw) ? raw.join("/") : raw;
461
- const { content } = req.body;
462
- await writeTemplate(templatePath, content);
463
- res.json({ ok: true });
464
- });
465
- app.delete("/api/wiki/template/squad/*path", async (req, res) => {
466
- try {
467
- const raw = req.params.path;
468
- const templatePath = Array.isArray(raw) ? raw.join("/") : raw;
469
- await deleteTemplate(templatePath);
470
- res.json({ ok: true });
471
- }
472
- catch (err) {
473
- res.status(404).json({ error: err.message });
474
- }
475
- });
476
- // --- Schedules ---
477
- app.get("/api/schedules", (_req, res) => {
478
- const type = undefined; // return all
479
- res.json(listSchedules(type));
480
- });
481
- app.post("/api/schedules", (req, res) => {
482
- const { type, cron, squad_id, agenda, prompt } = req.body ?? {};
483
- if (type !== "squad" && type !== "io") {
484
- res.status(400).json({ error: "type must be 'squad' or 'io'" });
485
- return;
486
- }
487
- if (!cron || typeof cron !== "string") {
488
- res.status(400).json({ error: "cron is required" });
489
- return;
490
- }
491
- if (!squad_id || typeof squad_id !== "string" || !squad_id.trim()) {
492
- res.status(400).json({ error: "squad_id is required" });
493
- return;
494
- }
495
- const schedule = createSchedule({ type, cron, squad_id, agenda, prompt });
496
- res.json(schedule);
497
- });
498
- app.put("/api/schedules/:id", (req, res) => {
499
- const schedule = getSchedule(req.params.id);
500
- if (!schedule) {
501
- res.status(404).json({ error: "Schedule not found" });
502
- return;
503
- }
504
- const { enabled, cron, agenda, prompt } = req.body ?? {};
505
- const updated = updateSchedule(req.params.id, {
506
- cron: typeof cron === "string" ? cron : undefined,
507
- agenda: typeof agenda === "string" ? agenda : undefined,
508
- prompt: typeof prompt === "string" ? prompt : undefined,
509
- enabled: typeof enabled === "boolean" ? enabled : undefined,
510
- });
511
- res.json(updated);
512
- });
513
- app.post("/api/schedules/:id/trigger", (req, res) => {
514
- const schedule = triggerSchedule(req.params.id);
515
- if (!schedule) {
516
- res.status(404).json({ error: "Schedule not found" });
517
- return;
518
- }
519
- res.json({ ok: true, schedule });
520
- });
521
- app.delete("/api/schedules/:id", (req, res) => {
522
- deleteSchedule(req.params.id);
523
- res.json({ ok: true });
524
- });
525
- // --- Settings ---
526
- app.get("/api/settings", (_req, res) => {
527
- const current = loadConfig();
528
- // Don't expose full Supabase key — mask it
529
- res.json({
530
- defaultModel: current.defaultModel,
531
- port: current.port,
532
- telegramEnabled: current.telegramEnabled,
533
- telegramBotToken: current.telegramBotToken ? "••••••••" : "",
534
- authorizedUserId: current.authorizedUserId ?? null,
535
- supabaseUrl: current.supabaseUrl ?? "",
536
- supabaseAnonKey: current.supabaseAnonKey ? "••••••••" : "",
537
- authorizedEmail: current.authorizedEmail ?? "",
538
- backgroundNotifyMode: current.backgroundNotifyMode,
539
- backgroundNotifyTelegram: current.backgroundNotifyTelegram,
540
- selfEditEnabled: current.selfEditEnabled,
541
- watchdogEnabled: current.watchdogEnabled,
542
- });
543
- });
544
- app.put("/api/settings", (req, res) => {
545
- const updates = {};
546
- const body = req.body;
547
- // Only apply fields that are explicitly provided and not masked
548
- if (body.defaultModel !== undefined)
549
- updates.defaultModel = body.defaultModel;
550
- if (body.port !== undefined)
551
- updates.port = body.port;
552
- if (body.telegramEnabled !== undefined)
553
- updates.telegramEnabled = body.telegramEnabled;
554
- if (body.telegramBotToken && body.telegramBotToken !== "••••••••")
555
- updates.telegramBotToken = body.telegramBotToken;
556
- if (body.authorizedUserId !== undefined)
557
- updates.authorizedUserId = body.authorizedUserId;
558
- if (body.supabaseUrl !== undefined)
559
- updates.supabaseUrl = body.supabaseUrl;
560
- if (body.supabaseAnonKey && body.supabaseAnonKey !== "••••••••")
561
- updates.supabaseAnonKey = body.supabaseAnonKey;
562
- if (body.authorizedEmail !== undefined)
563
- updates.authorizedEmail = body.authorizedEmail;
564
- if (body.backgroundNotifyMode !== undefined)
565
- updates.backgroundNotifyMode = body.backgroundNotifyMode;
566
- if (body.backgroundNotifyTelegram !== undefined)
567
- updates.backgroundNotifyTelegram = body.backgroundNotifyTelegram;
568
- if (body.selfEditEnabled !== undefined)
569
- updates.selfEditEnabled = body.selfEditEnabled;
570
- if (body.watchdogEnabled !== undefined)
571
- updates.watchdogEnabled = body.watchdogEnabled;
572
- saveConfig(updates);
573
- res.json({ ok: true });
574
- });
575
- // --- Token Usage ---
576
- app.get("/api/token-usage/summary", (req, res) => {
577
- const since = req.query.since;
578
- res.json(getTokenUsageSummary({ since }));
579
- });
580
- app.get("/api/token-usage/by-squad", (req, res) => {
581
- const since = req.query.since;
582
- res.json(getTokenUsageBySquad({ since }));
583
- });
584
- app.get("/api/token-usage/by-agent", (req, res) => {
585
- const since = req.query.since;
586
- const squadId = req.query.squad_id;
587
- res.json(getTokenUsageByAgent({ since, squadId }));
588
- });
589
- app.get("/api/token-usage/daily", (req, res) => {
590
- const days = parseInt(req.query.days) || 30;
591
- res.json(getDailyTokenUsage(days));
592
- });
593
- app.get("/api/token-usage/pricing", (_req, res) => {
594
- const config = loadConfig();
595
- const merged = { ...DEFAULT_MODEL_PRICING, ...(config.modelPricing ?? {}) };
596
- res.json(merged);
597
- });
598
- app.put("/api/token-usage/pricing", (req, res) => {
599
- const pricing = req.body;
600
- if (typeof pricing !== "object" || pricing === null) {
601
- res.status(400).json({ error: "Expected object body" });
602
- return;
603
- }
604
- saveConfig({ modelPricing: pricing });
605
- res.json({ ok: true });
606
- });
607
- app.get("/api/token-usage/alert-threshold", (_req, res) => {
608
- const config = loadConfig();
609
- res.json({ tokenAlertThreshold: config.tokenAlertThreshold ?? null });
610
- });
611
- app.put("/api/token-usage/alert-threshold", (req, res) => {
612
- const { tokenAlertThreshold } = req.body;
613
- if (tokenAlertThreshold !== null && typeof tokenAlertThreshold !== "number") {
614
- res.status(400).json({ error: "tokenAlertThreshold must be a number or null" });
615
- return;
616
- }
617
- saveConfig({ tokenAlertThreshold: tokenAlertThreshold ?? undefined });
618
- res.json({ ok: true });
619
- });
620
- // --- Health (unauthenticated) ---
621
- app.get("/health", (_req, res) => {
622
- res.json({
623
- status: "ok",
624
- version: process.env.npm_package_version ?? "unknown",
625
- webUi: existsSync(webIndexPath),
626
- webDistPath,
627
119
  });
628
- });
629
- // SPA fallback — serve index.html for non-API routes
630
- app.get("*splat", (req, res) => {
631
- if (req.path.startsWith("/api/")) {
632
- res.status(404).json({ error: "Not found" });
633
- return;
634
- }
635
- if (existsSync(webIndexPath)) {
636
- res.sendFile(webIndexPath);
637
- }
638
- else {
639
- res.status(503).send("Web UI not available — web-dist not found.");
640
- }
641
- });
642
- return new Promise((resolvePromise, reject) => {
643
- const server = app.listen(config.port, () => {
644
- resolvePromise();
120
+ ws.on('close', () => {
121
+ wsClients.delete(connectionId);
122
+ unsubscribeClient(connectionId);
123
+ logger.info({ connectionId }, 'WebSocket client disconnected');
645
124
  });
646
- server.on("error", reject);
647
125
  });
126
+ return {
127
+ async start() {
128
+ return new Promise((resolve) => {
129
+ server.listen(config.apiPort, () => {
130
+ logger.info({ port: config.apiPort }, 'API server listening');
131
+ resolve();
132
+ });
133
+ });
134
+ },
135
+ async stop() {
136
+ return new Promise((resolve, reject) => {
137
+ for (const ws of wsClients.values()) {
138
+ ws.close();
139
+ }
140
+ wsClients.clear();
141
+ wss.close();
142
+ server.close((err) => {
143
+ if (err)
144
+ reject(err);
145
+ else
146
+ resolve();
147
+ });
148
+ });
149
+ },
150
+ };
648
151
  }
649
- export { broadcast };
650
152
  //# sourceMappingURL=server.js.map