heyio 0.42.1 → 1.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 (100) hide show
  1. package/README.md +40 -52
  2. package/dist/api/auth.js +35 -38
  3. package/dist/api/server.js +157 -1134
  4. package/dist/config.js +49 -32
  5. package/dist/copilot/agents.js +72 -1055
  6. package/dist/copilot/client.js +6 -17
  7. package/dist/copilot/io-scheduler.js +55 -139
  8. package/dist/copilot/model-router.js +100 -72
  9. package/dist/copilot/orchestrator.js +91 -515
  10. package/dist/copilot/scheduler.js +67 -189
  11. package/dist/copilot/skills.js +41 -366
  12. package/dist/copilot/system-message.js +40 -200
  13. package/dist/copilot/tools.js +191 -2042
  14. package/dist/daemon.js +54 -201
  15. package/dist/index.js +15 -133
  16. package/dist/mcp/config.js +23 -31
  17. package/dist/mcp/index.js +2 -3
  18. package/dist/mcp/registry.js +33 -88
  19. package/dist/notify.js +18 -100
  20. package/dist/paths.js +13 -24
  21. package/dist/setup.js +35 -0
  22. package/dist/store/db.js +111 -297
  23. package/dist/store/feed.js +29 -97
  24. package/dist/store/instances.js +56 -121
  25. package/dist/store/schedules.js +21 -73
  26. package/dist/store/squads.js +35 -186
  27. package/dist/store/tasks.js +25 -168
  28. package/dist/telegram/bot.js +20 -312
  29. package/dist/telegram/handlers.js +39 -3
  30. package/dist/watchdog.js +31 -45
  31. package/dist/wiki/fs.js +38 -155
  32. package/dist/wiki/search.js +31 -44
  33. package/package.json +5 -8
  34. package/web-dist/assets/ChatView-EFFiln1H.js +11 -0
  35. package/web-dist/assets/FeedView-bN4NMOL7.js +6 -0
  36. package/web-dist/assets/LoginView-CNtasq3n.js +1 -0
  37. package/web-dist/assets/McpView-C2CHiwsi.js +1 -0
  38. package/web-dist/assets/SchedulesView-CyilLban.js +1 -0
  39. package/web-dist/assets/SettingsView-1wLXKEF4.js +1 -0
  40. package/web-dist/assets/SkillsView-BLsD-0u0.js +1 -0
  41. package/web-dist/assets/SquadDetailView-CsCw2ZLp.js +21 -0
  42. package/web-dist/assets/SquadsView-DQ3vFlyO.js +6 -0
  43. package/web-dist/assets/WikiView-19M3oqnq.js +21 -0
  44. package/web-dist/assets/api-WGvTsXaE.js +1 -0
  45. package/web-dist/assets/index-D7M5O-_l.css +1 -0
  46. package/web-dist/assets/index-DZOS9syn.js +95 -0
  47. package/web-dist/assets/plus-BOvyX1BC.js +6 -0
  48. package/web-dist/assets/trash-2-DHoetkC4.js +6 -0
  49. package/web-dist/favicon.svg +4 -1
  50. package/web-dist/index.html +7 -10
  51. package/dist/api/logout.test.js +0 -128
  52. package/dist/api/mcp.test.js +0 -285
  53. package/dist/api/wiki.test.js +0 -283
  54. package/dist/auth/session-logic.js +0 -79
  55. package/dist/auth/session-logic.test.js +0 -201
  56. package/dist/copilot/auto-complete-instance.test.js +0 -104
  57. package/dist/copilot/cron.js +0 -136
  58. package/dist/copilot/event-summary.js +0 -286
  59. package/dist/copilot/instance-deactivate.test.js +0 -119
  60. package/dist/copilot/model-router.test.js +0 -71
  61. package/dist/copilot/review-backfill.js +0 -57
  62. package/dist/copilot/session-timeout.js +0 -112
  63. package/dist/copilot/session-timeout.test.js +0 -372
  64. package/dist/copilot/skills.test.js +0 -55
  65. package/dist/copilot/universes.js +0 -469
  66. package/dist/instance-watchdog.js +0 -104
  67. package/dist/instance-watchdog.test.js +0 -183
  68. package/dist/mcp/client.js +0 -109
  69. package/dist/mcp/client.test.js +0 -99
  70. package/dist/mcp/config.test.js +0 -49
  71. package/dist/mcp/registry.test.js +0 -79
  72. package/dist/notify.test.js +0 -232
  73. package/dist/store/feed.test.js +0 -279
  74. package/dist/store/instances.test.js +0 -310
  75. package/dist/store/io-schedules.js +0 -63
  76. package/dist/store/notifications.js +0 -79
  77. package/dist/store/notifications.test.js +0 -197
  78. package/dist/store/schedule-runs.js +0 -46
  79. package/dist/store/squads.test.js +0 -405
  80. package/dist/store/tasks.test.js +0 -150
  81. package/dist/store/worktrees.js +0 -83
  82. package/dist/tui/index.js +0 -286
  83. package/dist/update.js +0 -81
  84. package/dist/watchdog.test.js +0 -83
  85. package/dist/wiki/wiki-squad.test.js +0 -54
  86. package/web-dist/assets/AgentActivityView-B1PaNYy8.js +0 -1
  87. package/web-dist/assets/ChatView-BbpWnrtC.js +0 -4
  88. package/web-dist/assets/FeedView-B5LaMV0I.js +0 -1
  89. package/web-dist/assets/InboxView-Cwqt8rH7.js +0 -1
  90. package/web-dist/assets/LoginView-refmPLKT.js +0 -1
  91. package/web-dist/assets/McpView-B1w0dRFY.js +0 -1
  92. package/web-dist/assets/SchedulesView-D9l2DI7X.js +0 -1
  93. package/web-dist/assets/SettingsTabs.vue_vue_type_script_setup_true_lang-DncOVVEB.js +0 -1
  94. package/web-dist/assets/SkillsView-uFX0q1mV.js +0 -1
  95. package/web-dist/assets/SquadsView-B1nZW4ml.js +0 -1
  96. package/web-dist/assets/StatusIndicator.vue_vue_type_script_setup_true_lang-pTrJJwX1.js +0 -1
  97. package/web-dist/assets/WikiView-B54cCKIK.js +0 -1
  98. package/web-dist/assets/index-C0VEUWQ1.js +0 -81
  99. package/web-dist/assets/index-eluTyieM.css +0 -10
  100. package/web-dist/icons.svg +0 -24
@@ -1,1197 +1,220 @@
1
- import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { existsSync, readFileSync } from "node:fs";
4
1
  import express from "express";
5
- import { config } from "../config.js";
6
- import { listSkills, installSkill, installSkillFromContent, removeSkill } from "../copilot/skills.js";
7
- import { loadMcpConfig, saveMcpConfig } from "../mcp/config.js";
8
- import { initMcpTools } from "../copilot/orchestrator.js";
9
- import { listSquads, createSquad, listSquadAgents, getSquad } from "../store/squads.js";
10
- import { createInstance, getInstance, listInstances, updateInstanceStatus, getInstanceDecisions, mergeInstanceDecisions, buildContextSnapshot } from "../store/instances.js";
11
- import { createWorktree, removeWorktree } from "../store/worktrees.js";
12
- import { getAgentInfo, cancelAgentTask, getTaskEvents, subscribeToTaskEvents } from "../copilot/agents.js";
13
- import { summarize, summarizeEvent } from "../copilot/event-summary.js";
14
- import { abortOrchestrator } from "../copilot/orchestrator.js";
15
- import { getActiveTasks, getTask, listRecentTasks } from "../store/tasks.js";
16
- import { IO_VERSION, SKILLS_DIR } from "../paths.js";
17
- import { requireAuth } from "./auth.js";
18
- import { listSchedules, getSchedule, deleteSchedule, setScheduleEnabled } from "../store/schedules.js";
19
- import { listIoSchedules, getIoSchedule, deleteIoSchedule, setIoScheduleEnabled } from "../store/io-schedules.js";
20
- import { getScheduleRuns } from "../store/schedule-runs.js";
21
- import { createFeedEntry, listFeedEntries, listFeedSquads, countUnreadFeedEntries, markFeedEntryRead, markAllFeedEntriesRead, deleteFeedEntry, markFeedEntriesRead, deleteFeedEntries } from "../store/feed.js";
22
- import { listPages, readPage, writePage, deletePage, assertPagePath } from "../wiki/fs.js";
23
- import { runScheduleNow } from "../copilot/scheduler.js";
24
- import { runIoScheduleNow } from "../copilot/io-scheduler.js";
25
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
- const WEB_DIST = path.resolve(__dirname, "../../web-dist");
27
- let messageHandler;
28
- const sseConnections = new Set();
29
- export function setMessageHandler(handler) {
30
- messageHandler = handler;
31
- }
32
- export function broadcastToSSE(text) {
33
- const payload = JSON.stringify({ type: "delta", text });
34
- for (const res of sseConnections) {
35
- res.write(`data: ${payload}\n\n`);
36
- }
37
- }
38
- export function broadcastNotificationToSSE(payload) {
39
- const data = JSON.stringify({ type: "feed", ...payload });
40
- for (const res of sseConnections) {
41
- res.write(`data: ${data}\n\n`);
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, resolve } from "node:path";
5
+ import { createAuthMiddleware } from "./auth.js";
6
+ import { sendToOrchestrator } from "../copilot/orchestrator.js";
7
+ import { listSquads, getSquad, getAgentsForSquad } from "../store/squads.js";
8
+ import { getTasksForSquad } from "../store/tasks.js";
9
+ import { getInstancesForSquad } from "../store/instances.js";
10
+ import { getFeedItems, markFeedItemRead, deleteFeedItem, getUnreadCount, } from "../store/feed.js";
11
+ import { listSchedules, createSchedule, deleteSchedule, toggleSchedule } from "../store/schedules.js";
12
+ import { listServers, toggleMcpServer, addMcpServer, removeMcpServer } from "../mcp/index.js";
13
+ import { listSkills, addSkill, removeSkill } from "../copilot/skills.js";
14
+ import { readPage, writePage, deletePage, listPages } from "../wiki/fs.js";
15
+ import { searchPages } from "../wiki/search.js";
16
+ import { randomUUID } from "node:crypto";
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ const sseClients = [];
20
+ function broadcast(event, data) {
21
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
22
+ for (const client of sseClients) {
23
+ client.res.write(payload);
42
24
  }
43
25
  }
44
- export async function startApiServer() {
26
+ export async function startApiServer(config) {
45
27
  const app = express();
46
- app.use(express.json({ limit: "10mb" }));
47
- app.use((_req, res, next) => {
48
- res.setHeader("Access-Control-Allow-Origin", "*");
49
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
50
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
51
- if (_req.method === "OPTIONS") {
52
- res.sendStatus(204);
53
- return;
54
- }
55
- next();
56
- });
57
- // Build API router
58
- const api = express.Router();
59
- // Public endpoints (no auth required)
60
- api.get("/health", (_req, res) => {
61
- res.json({ status: "ok" });
62
- });
63
- api.get("/auth/config", (_req, res) => {
64
- const authEnabled = !!(config.supabaseUrl && config.supabaseAnonKey);
65
- res.json({
66
- authEnabled,
67
- supabaseUrl: config.supabaseUrl ?? null,
68
- supabaseAnonKey: config.supabaseAnonKey ?? null,
69
- });
70
- });
71
- // Apply auth middleware — all routes below require a valid JWT
72
- api.use(requireAuth);
73
- // Auth: Logout endpoint
74
- // At this point, the requireAuth middleware has already validated the token.
75
- // We just need to confirm the logout was successful.
76
- api.post("/logout", (_req, res) => {
77
- try {
78
- // Token invalidation approach:
79
- // Supabase JWT tokens are short-lived (1 hour by default). Since we don't maintain
80
- // a token blacklist, logout on the client side (clearing localStorage) is sufficient.
81
- // In a production system with token revocation, the token would be added to a blacklist here.
82
- // For now, we simply confirm the logout was successful.
83
- res.json({ status: "logged_out" });
84
- }
85
- catch (e) {
86
- console.error("Error during logout:", e);
87
- res.status(500).json({ error: "Logout failed" });
88
- }
89
- });
90
- // Skills read endpoints
91
- api.get("/skills", (_req, res) => {
92
- try {
93
- const skills = listSkills();
94
- res.json({ skills });
95
- }
96
- catch (e) {
97
- console.error("Error listing skills:", e);
98
- res.status(500).json({ error: "Failed to list skills" });
99
- }
100
- });
101
- // Get a single skill's SKILL.md content by slug (issue #119)
102
- api.get("/skills/:slug", (req, res) => {
103
- const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
104
- if (!slug || slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
105
- res.status(400).json({ error: "Invalid skill slug" });
106
- return;
107
- }
108
- const skillFile = `${SKILLS_DIR}/${slug}/SKILL.md`;
109
- try {
110
- if (!existsSync(skillFile)) {
111
- res.status(404).json({ error: "Skill not found" });
112
- return;
113
- }
114
- const content = readFileSync(skillFile, "utf-8");
115
- res.json({ slug, content });
116
- }
117
- catch (e) {
118
- console.error("Error reading skill content:", e);
119
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
120
- }
121
- });
122
- // Feed endpoints — unified deliverables + notifications feed
123
- api.get("/feed/count", (req, res) => {
124
- try {
125
- const rawType = req.query.type;
126
- const type = rawType === "inbox" || rawType === "notification"
127
- ? rawType
128
- : undefined;
129
- const count = countUnreadFeedEntries(type);
130
- res.json({ count });
131
- }
132
- catch (e) {
133
- console.error("Error counting feed entries:", e);
134
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
135
- }
136
- });
137
- api.get("/feed", (req, res) => {
138
- try {
139
- const rawType = req.query.type;
140
- const type = rawType === "inbox" || rawType === "notification"
141
- ? rawType
142
- : undefined;
143
- const unreadOnly = req.query.unread === "true";
144
- const rawLimit = req.query.limit;
145
- const parsed = typeof rawLimit === "string" ? Number.parseInt(rawLimit, 10) : NaN;
146
- const limit = Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 200) : 50;
147
- const search = typeof req.query.search === "string" && req.query.search !== "" ? req.query.search : undefined;
148
- const squad = typeof req.query.squad === "string" && req.query.squad !== "" ? req.query.squad : undefined;
149
- const rows = listFeedEntries({ type, unreadOnly, limit, search, squad });
150
- const unreadCount = countUnreadFeedEntries(type);
151
- const entries = rows.map(({ id, type: entryType, title, body, created_at, read_at, source_type, source_ref }) => {
152
- let source = null;
153
- if (source_type) {
154
- source = { type: source_type };
155
- if (source_ref) {
156
- try {
157
- const parsedRef = JSON.parse(source_ref);
158
- source = { type: source_type, ...parsedRef };
159
- }
160
- catch {
161
- // source_ref is not valid JSON — fall back to type-only
162
- }
163
- }
164
- }
165
- return { id, type: entryType, title, body, created_at, read_at, source };
166
- });
167
- res.json({ entries, unreadCount });
168
- }
169
- catch (e) {
170
- console.error("Error listing feed entries:", e);
171
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
172
- }
173
- });
174
- api.get("/feed/squads", (req, res) => {
175
- try {
176
- const squads = listFeedSquads();
177
- res.json({ squads });
178
- }
179
- catch (e) {
180
- console.error("Error listing feed squads:", e);
181
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
182
- }
183
- });
184
- // Status endpoint
185
- api.get("/status", (_req, res) => {
186
- res.json({ version: IO_VERSION, uptime: process.uptime() });
187
- });
188
- // SSE events endpoint
189
- api.get("/events", (req, res) => {
28
+ app.use(express.json());
29
+ // Serve static web frontend
30
+ const webDistPath = resolve(__dirname, "..", "..", "web-dist");
31
+ app.use(express.static(webDistPath));
32
+ // Auth middleware for all API routes
33
+ const auth = createAuthMiddleware(config);
34
+ app.use("/api", auth);
35
+ // --- SSE Stream ---
36
+ app.get("/api/stream", (req, res) => {
190
37
  res.setHeader("Content-Type", "text/event-stream");
191
38
  res.setHeader("Cache-Control", "no-cache");
192
39
  res.setHeader("Connection", "keep-alive");
193
40
  res.flushHeaders();
194
- sseConnections.add(res);
41
+ const client = { id: randomUUID(), res };
42
+ sseClients.push(client);
43
+ res.write(`event: connected\ndata: ${JSON.stringify({ id: client.id })}\n\n`);
195
44
  req.on("close", () => {
196
- sseConnections.delete(res);
45
+ const idx = sseClients.indexOf(client);
46
+ if (idx !== -1)
47
+ sseClients.splice(idx, 1);
197
48
  });
198
49
  });
199
- // Install a skill from pasted SKILL.md content (issue #117)
200
- api.post("/skills/paste", (req, res) => {
201
- const { content: skillContent, slug } = req.body;
202
- if (!skillContent || typeof skillContent !== "string" || skillContent.trim() === "") {
203
- res.status(400).json({ error: "Missing or empty required field: content" });
204
- return;
205
- }
206
- if (!slug || typeof slug !== "string" || slug.trim() === "") {
207
- res.status(400).json({ error: "Missing or empty required field: slug" });
208
- return;
209
- }
210
- try {
211
- const skill = installSkillFromContent(skillContent, slug.trim());
212
- res.status(201).json({ skill });
213
- }
214
- catch (e) {
215
- res.status(400).json({ error: e instanceof Error ? e.message : String(e) });
216
- }
217
- });
218
- // Install a skill from a git repo URL (mirrors the skill_install tool)
219
- api.post("/skills", async (req, res) => {
220
- const { repoUrl } = req.body;
221
- if (repoUrl === undefined || repoUrl === null || typeof repoUrl !== "string") {
222
- res.status(400).json({ error: "Missing required field: repoUrl" });
223
- return;
224
- }
225
- if (repoUrl.trim() === "") {
226
- res.status(400).json({ error: "repoUrl must not be empty" });
227
- return;
228
- }
229
- const trimmed = repoUrl.trim();
230
- const looksLikeGitUrl = trimmed.startsWith("http://") ||
231
- trimmed.startsWith("https://") ||
232
- trimmed.startsWith("git@") ||
233
- trimmed.startsWith("git://") ||
234
- trimmed.endsWith(".git");
235
- if (!looksLikeGitUrl) {
236
- res.status(400).json({ error: "repoUrl does not look like a git repository URL" });
237
- return;
238
- }
239
- try {
240
- const result = await installSkill(trimmed);
241
- const skills = Array.isArray(result) ? result : [result];
242
- res.status(201).json({ skill: skills[0], skills });
243
- }
244
- catch (e) {
245
- console.error("Error installing skill:", e);
246
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
247
- }
248
- });
249
- // Delete an installed skill by slug (issue #140)
250
- api.delete("/skills/:slug", (req, res) => {
251
- const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
252
- if (!slug || slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
253
- res.status(400).json({ error: "Invalid skill slug" });
254
- return;
255
- }
256
- try {
257
- const deleted = removeSkill(slug);
258
- if (!deleted) {
259
- res.status(404).json({ error: "Skill not found" });
260
- return;
261
- }
262
- res.json({ deleted: true });
263
- }
264
- catch (e) {
265
- console.error("Error deleting skill:", e);
266
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
267
- }
268
- });
269
- // Feed write endpoints
270
- // Note: POST /feed/read-all must be before POST /feed/:id/read to avoid route shadowing
271
- api.post("/feed/read-all", (req, res) => {
272
- try {
273
- const rawType = req.query.type;
274
- const type = rawType === "inbox" || rawType === "notification"
275
- ? rawType
276
- : undefined;
277
- const marked = markAllFeedEntriesRead(type);
278
- res.json({ marked });
279
- }
280
- catch (e) {
281
- console.error("Error marking feed entries read:", e);
282
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
283
- }
284
- });
285
- api.post("/feed/batch-read", (req, res) => {
286
- const { ids } = req.body;
287
- if (!Array.isArray(ids) || ids.length === 0 || !ids.every((x) => typeof x === "number")) {
288
- res.status(400).json({ error: "ids must be a non-empty array of numbers" });
289
- return;
290
- }
291
- try {
292
- const marked = markFeedEntriesRead(ids);
293
- res.json({ marked });
294
- }
295
- catch (e) {
296
- console.error("Error batch-marking feed entries read:", e);
297
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
298
- }
299
- });
300
- api.post("/feed/batch-delete", (req, res) => {
301
- const { ids } = req.body;
302
- if (!Array.isArray(ids) || ids.length === 0 || !ids.every((x) => typeof x === "number")) {
303
- res.status(400).json({ error: "ids must be a non-empty array of numbers" });
304
- return;
305
- }
306
- try {
307
- const deleted = deleteFeedEntries(ids);
308
- res.json({ deleted });
309
- }
310
- catch (e) {
311
- console.error("Error batch-deleting feed entries:", e);
312
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
313
- }
314
- });
315
- api.post("/feed/:id/read", (req, res) => {
316
- const raw = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
317
- const id = Number.parseInt(raw, 10);
318
- if (Number.isNaN(id)) {
319
- res.status(400).json({ error: "Invalid id" });
320
- return;
321
- }
322
- try {
323
- const found = markFeedEntryRead(id);
324
- if (!found) {
325
- res.status(404).json({ error: "Feed entry not found" });
326
- return;
327
- }
328
- res.json({ ok: true });
329
- }
330
- catch (e) {
331
- console.error("Error marking feed entry read:", e);
332
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
333
- }
334
- });
335
- api.post("/feed", (req, res) => {
336
- const { type, title, body, source_type, source_ref } = req.body;
337
- if (type !== "inbox" && type !== "notification") {
338
- res.status(400).json({ error: "type must be 'inbox' or 'notification'" });
339
- return;
340
- }
341
- if (!title || typeof title !== "string" || title.trim() === "") {
342
- res.status(400).json({ error: "Missing or empty required field: title" });
343
- return;
344
- }
345
- if (!body || typeof body !== "string" || body.trim() === "") {
346
- res.status(400).json({ error: "Missing or empty required field: body" });
50
+ // --- Chat ---
51
+ app.post("/api/message", async (req, res) => {
52
+ const { prompt } = req.body;
53
+ if (!prompt || typeof prompt !== "string") {
54
+ res.status(400).json({ error: "prompt is required" });
347
55
  return;
348
56
  }
349
- try {
350
- const entry = createFeedEntry({
351
- type: type,
352
- title: title.trim(),
353
- body: body.trim(),
354
- source_type: typeof source_type === "string" ? source_type : undefined,
355
- source_ref: typeof source_ref === "string" ? source_ref : undefined,
356
- });
357
- res.status(201).json({ entry });
358
- }
359
- catch (e) {
360
- console.error("Error creating feed entry:", e);
361
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
362
- }
363
- });
364
- api.delete("/feed/:id", (req, res) => {
365
- const raw = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
366
- const id = Number.parseInt(raw, 10);
367
- if (Number.isNaN(id)) {
368
- res.status(400).json({ error: "Invalid id" });
369
- return;
370
- }
371
- try {
372
- const deleted = deleteFeedEntry(id);
373
- if (!deleted) {
374
- res.status(404).json({ error: "Feed entry not found" });
375
- return;
57
+ // Stream response via SSE, send final to HTTP response
58
+ await sendToOrchestrator(prompt, "web", (content, done) => {
59
+ broadcast("message_delta", { content, done });
60
+ if (done) {
61
+ res.json({ content });
376
62
  }
377
- res.json({ deleted: true });
378
- }
379
- catch (e) {
380
- console.error("Error deleting feed entry:", e);
381
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
382
- }
63
+ });
383
64
  });
384
- // Inbox endpoints
385
- api.get("/inbox/count", (_req, res) => {
386
- try {
387
- const count = countUnreadFeedEntries("inbox");
388
- res.json({ count });
389
- }
390
- catch (e) {
391
- console.error("Error counting inbox entries:", e);
392
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
393
- }
65
+ // --- Squads ---
66
+ app.get("/api/squads", (_req, res) => {
67
+ res.json(listSquads());
394
68
  });
395
- api.get("/inbox", (_req, res) => {
396
- try {
397
- const entries = listFeedEntries({ type: "inbox" });
398
- res.json({ entries });
399
- }
400
- catch (e) {
401
- console.error("Error listing inbox entries:", e);
402
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
403
- }
404
- });
405
- api.delete("/inbox/:id", (req, res) => {
406
- const raw = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
407
- const id = Number.parseInt(raw, 10);
408
- if (Number.isNaN(id)) {
409
- res.status(400).json({ error: "Invalid id" });
69
+ app.get("/api/squads/:id", (req, res) => {
70
+ const squad = getSquad(req.params.id);
71
+ if (!squad) {
72
+ res.status(404).json({ error: "Squad not found" });
410
73
  return;
411
74
  }
412
- try {
413
- const deleted = deleteFeedEntry(id);
414
- if (!deleted) {
415
- res.status(404).json({ error: "Inbox entry not found" });
416
- return;
417
- }
418
- res.status(204).send();
419
- }
420
- catch (e) {
421
- console.error("Error deleting inbox entry:", e);
422
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
423
- }
424
- });
425
- // Squads endpoints
426
- api.get("/squads", (_req, res) => {
427
- try {
428
- const squads = listSquads();
429
- res.json({ squads });
430
- }
431
- catch (e) {
432
- console.error("Error listing squads:", e);
433
- res.status(500).json({ error: "Failed to list squads" });
434
- }
435
- });
436
- api.post("/squads", (req, res) => {
437
- try {
438
- const { slug, name, projectPath } = req.body;
439
- if (!slug || !name || !projectPath) {
440
- res.status(400).json({ error: "Missing required fields: slug, name, projectPath" });
441
- return;
442
- }
443
- const squad = createSquad(slug, name, projectPath);
444
- res.json({ squad });
445
- }
446
- catch (e) {
447
- console.error("Error creating squad:", e);
448
- res.status(500).json({ error: "Failed to create squad" });
449
- }
450
- });
451
- api.get("/squads/:slug/agents", (req, res) => {
452
- try {
453
- const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
454
- const agents = listSquadAgents(slug);
455
- const activeTasks = getActiveTasks();
456
- const taskByKey = new Map();
457
- for (const t of activeTasks) {
458
- taskByKey.set(t.agent_slug, { task_id: t.task_id, description: t.description });
459
- }
460
- const enriched = agents.map((a) => {
461
- const key = `${slug}:${a.character_name}`;
462
- const task = taskByKey.get(key) ?? taskByKey.get(slug);
463
- return {
464
- ...a,
465
- currentTaskId: task?.task_id ?? null,
466
- currentTask: task?.description ?? null,
467
- };
468
- });
469
- res.json({ agents: enriched });
470
- }
471
- catch (e) {
472
- console.error("Error listing squad agents:", e);
473
- res.status(500).json({ error: "Failed to list squad agents" });
474
- }
75
+ const agents = getAgentsForSquad(req.params.id);
76
+ const tasks = getTasksForSquad(req.params.id);
77
+ const instances = getInstancesForSquad(req.params.id);
78
+ res.json({ squad, agents, tasks, instances });
475
79
  });
476
- // Squad Instances
477
- api.get("/squads/:slug/instances", (req, res) => {
478
- try {
479
- const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
480
- const includeCompleted = req.query.include_completed === "true";
481
- const instances = listInstances(slug, { includeCompleted });
482
- res.json({ instances });
483
- }
484
- catch (e) {
485
- console.error("Error listing instances:", e);
486
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
487
- }
488
- });
489
- api.post("/squads/:slug/instances", (req, res) => {
490
- try {
491
- const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
492
- const { issue_ref, base_branch } = req.body;
493
- const squad = getSquad(slug);
494
- if (!squad) {
495
- res.status(404).json({ error: "Squad not found" });
496
- return;
497
- }
498
- const sanitizedRef = (issue_ref ?? "task").replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
499
- const instanceId = `${slug}--${sanitizedRef}`;
500
- const branchName = `${slug}/instance/${sanitizedRef}`;
501
- const contextSnapshot = buildContextSnapshot(slug);
502
- const worktreePath = createWorktree(squad.project_path, instanceId, branchName, base_branch ?? "main");
503
- let instance;
504
- try {
505
- instance = createInstance({
506
- id: instanceId,
507
- masterSquadSlug: slug,
508
- issueRef: issue_ref,
509
- worktreePath,
510
- branchName,
511
- contextSnapshot,
512
- });
513
- }
514
- catch (createErr) {
515
- // Roll back the worktree if DB insert fails (e.g. max instances exceeded)
516
- removeWorktree(squad.project_path, worktreePath);
517
- throw createErr;
518
- }
519
- updateInstanceStatus(instanceId, "active");
520
- res.status(201).json({ instance: { ...instance, status: "active" } });
521
- }
522
- catch (e) {
523
- console.error("Error creating instance:", e);
524
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
525
- }
526
- });
527
- api.get("/squads/:slug/instances/:id", (req, res) => {
528
- try {
529
- const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
530
- const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
531
- const instance = getInstance(id);
532
- if (!instance || instance.master_squad_slug !== slug) {
533
- res.status(404).json({ error: "Instance not found" });
534
- return;
535
- }
536
- const decisions = getInstanceDecisions(id);
537
- res.json({ instance, decisions });
538
- }
539
- catch (e) {
540
- console.error("Error getting instance:", e);
541
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
542
- }
543
- });
544
- api.post("/squads/:slug/instances/:id/complete", (req, res) => {
545
- try {
546
- const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
547
- const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
548
- const instance = getInstance(id);
549
- if (!instance || instance.master_squad_slug !== slug) {
550
- res.status(404).json({ error: "Instance not found" });
551
- return;
552
- }
553
- if (instance.status === "done") {
554
- res.json({ message: "Already completed", merged: 0 });
555
- return;
556
- }
557
- updateInstanceStatus(id, "merging");
558
- const merged = mergeInstanceDecisions(id, instance.master_squad_slug);
559
- const squad = getSquad(instance.master_squad_slug);
560
- if (squad) {
561
- removeWorktree(squad.project_path, instance.worktree_path);
562
- }
563
- updateInstanceStatus(id, "done");
564
- res.json({ message: "Instance completed", merged });
565
- }
566
- catch (e) {
567
- console.error("Error completing instance:", e);
568
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
569
- }
570
- });
571
- api.post("/squads/:slug/instances/:id/abort", (req, res) => {
572
- try {
573
- const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
574
- const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
575
- const instance = getInstance(id);
576
- if (!instance || instance.master_squad_slug !== slug) {
577
- res.status(404).json({ error: "Instance not found" });
578
- return;
579
- }
580
- if (instance.status === "done" || instance.status === "failed") {
581
- res.json({ message: `Already in terminal state: ${instance.status}` });
582
- return;
583
- }
584
- updateInstanceStatus(id, "failed");
585
- res.json({ message: "Instance aborted", worktree_path: instance.worktree_path });
586
- }
587
- catch (e) {
588
- console.error("Error aborting instance:", e);
589
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
590
- }
591
- });
592
- // Agents endpoints
593
- api.get("/agents", (_req, res) => {
594
- try {
595
- const agents = getAgentInfo();
596
- res.json({ agents });
597
- }
598
- catch (e) {
599
- console.error("Error listing agents:", e);
600
- res.status(500).json({ error: "Failed to list agents" });
601
- }
602
- });
603
- // Task history endpoints
604
- api.get("/tasks", (req, res) => {
605
- try {
606
- const limitRaw = req.query.limit;
607
- const parsed = typeof limitRaw === "string" ? parseInt(limitRaw, 10) : NaN;
608
- const limit = Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 200) : 50;
609
- const tasks = listRecentTasks(limit);
610
- res.json({ tasks });
611
- }
612
- catch (e) {
613
- console.error("Error listing tasks:", e);
614
- res.status(500).json({ error: "Failed to list tasks" });
615
- }
616
- });
617
- api.get("/tasks/:taskId", (req, res) => {
618
- try {
619
- const taskId = Array.isArray(req.params.taskId) ? req.params.taskId[0] : req.params.taskId;
620
- const task = getTask(taskId);
621
- if (!task) {
622
- res.status(404).json({ error: "Task not found" });
623
- return;
624
- }
625
- res.json({ task });
626
- }
627
- catch (e) {
628
- console.error("Error fetching task:", e);
629
- res.status(500).json({ error: "Failed to fetch task" });
630
- }
80
+ // --- Feed ---
81
+ app.get("/api/feed", (req, res) => {
82
+ const unreadOnly = req.query.unread === "true";
83
+ const source = req.query.source;
84
+ const limit = parseInt(req.query.limit) || 50;
85
+ const offset = parseInt(req.query.offset) || 0;
86
+ res.json({
87
+ items: getFeedItems({ unreadOnly, source, limit, offset }),
88
+ unreadCount: getUnreadCount(),
89
+ });
631
90
  });
632
- api.get("/tasks/:taskId/activity", (req, res) => {
633
- const taskId = Array.isArray(req.params.taskId) ? req.params.taskId[0] : req.params.taskId;
634
- try {
635
- const events = getTaskEvents(taskId);
636
- let activity = summarize(events);
637
- // Fallback: when in-memory events are gone (e.g. daemon restart),
638
- // build a minimal entry from the persisted task result so the UI
639
- // doesn't show "no activity" for tasks that actually ran. (#66)
640
- if (activity.length === 0) {
641
- const task = getTask(taskId);
642
- if (task?.result) {
643
- activity = [{
644
- ts: task.completed_at ? new Date(task.completed_at).getTime() : Date.now(),
645
- kind: "outcome",
646
- icon: task.status === "failed" ? "\u274c" : task.status === "done" ? "\u2705" : "\ud83d\udccb",
647
- summary: task.status === "failed"
648
- ? "Task failed (activity log unavailable after restart)"
649
- : "Task completed (activity log unavailable after restart)",
650
- rawType: "task.result.fallback",
651
- detail: task.result,
652
- raw: { result: task.result, status: task.status },
653
- }];
654
- }
655
- }
656
- res.json({ taskId, activity });
657
- }
658
- catch (e) {
659
- console.error("Error building task activity:", e);
660
- res.status(500).json({ error: "Failed to build task activity" });
661
- }
91
+ app.post("/api/feed/:id/read", (req, res) => {
92
+ markFeedItemRead(req.params.id);
93
+ res.json({ ok: true });
662
94
  });
663
- api.get("/tasks/:taskId/events", (req, res) => {
664
- const taskId = Array.isArray(req.params.taskId) ? req.params.taskId[0] : req.params.taskId;
665
- res.setHeader("Content-Type", "text/event-stream");
666
- res.setHeader("Cache-Control", "no-cache");
667
- res.setHeader("Connection", "keep-alive");
668
- res.setHeader("X-Accel-Buffering", "no");
669
- res.flushHeaders();
670
- const send = (ev) => {
671
- try {
672
- const summary = summarizeEvent(ev);
673
- const payload = { ...ev, summary };
674
- res.write(`data: ${JSON.stringify(payload)}\n\n`);
675
- }
676
- catch {
677
- // client likely disconnected; cleanup happens on req.close
678
- }
679
- };
680
- // Replay buffered events first so a late subscriber sees the full thread
681
- for (const ev of getTaskEvents(taskId))
682
- send(ev);
683
- // Subscribe to live events
684
- const unsubscribe = subscribeToTaskEvents(taskId, send);
685
- // Heartbeat to keep proxies / browsers from closing the connection
686
- const heartbeat = setInterval(() => {
687
- try {
688
- res.write(": ping\n\n");
689
- }
690
- catch { /* ignore */ }
691
- }, 15000);
692
- req.on("close", () => {
693
- clearInterval(heartbeat);
694
- unsubscribe();
695
- });
95
+ app.delete("/api/feed/:id", (req, res) => {
96
+ deleteFeedItem(req.params.id);
97
+ res.json({ ok: true });
696
98
  });
697
- // Stop / cancel endpoints
698
- api.post("/orchestrator/abort", async (_req, res) => {
699
- try {
700
- const aborted = await abortOrchestrator();
701
- res.json({ aborted });
702
- }
703
- catch (e) {
704
- console.error("Error aborting orchestrator:", e);
705
- res.status(500).json({ error: "Failed to abort orchestrator" });
706
- }
99
+ // --- MCP Servers ---
100
+ app.get("/api/mcp", (_req, res) => {
101
+ res.json(listServers());
707
102
  });
708
- api.post("/tasks/:taskId/cancel", async (req, res) => {
709
- try {
710
- const taskId = Array.isArray(req.params.taskId) ? req.params.taskId[0] : req.params.taskId;
711
- const cancelled = await cancelAgentTask(taskId);
712
- if (!cancelled) {
713
- res.status(404).json({ error: "Task not found or not running" });
714
- return;
715
- }
716
- res.json({ cancelled: true });
717
- }
718
- catch (e) {
719
- console.error("Error cancelling task:", e);
720
- res.status(500).json({ error: "Failed to cancel task" });
721
- }
103
+ app.post("/api/mcp", (req, res) => {
104
+ const server = { id: randomUUID(), ...req.body, enabled: true };
105
+ addMcpServer(server);
106
+ res.json(server);
722
107
  });
723
- // Schedules endpoints
724
- api.get("/schedules", (_req, res) => {
725
- try {
726
- const io = listIoSchedules();
727
- const squads = listSchedules();
728
- res.json({ io, squads });
729
- }
730
- catch (e) {
731
- console.error("Error listing schedules:", e);
732
- res.status(500).json({ error: "Failed to list schedules" });
108
+ app.put("/api/mcp/:id", (req, res) => {
109
+ const { enabled } = req.body;
110
+ if (typeof enabled === "boolean") {
111
+ toggleMcpServer(req.params.id, enabled);
733
112
  }
113
+ res.json({ ok: true });
734
114
  });
735
- // Squad schedule lifecycle
736
- api.post("/schedules/squads/:id/pause", (req, res) => {
737
- const id = Number(req.params.id);
738
- if (Number.isNaN(id)) {
739
- res.status(400).json({ error: "Invalid schedule id" });
740
- return;
741
- }
742
- try {
743
- const ok = setScheduleEnabled(id, false);
744
- if (!ok) {
745
- res.status(404).json({ error: "Squad schedule not found" });
746
- return;
747
- }
748
- res.json({ ok: true, schedule: getSchedule(id) });
749
- }
750
- catch (e) {
751
- console.error("Error pausing squad schedule:", e);
752
- res.status(500).json({ error: (e instanceof Error ? e.message : String(e)) });
753
- }
115
+ app.delete("/api/mcp/:id", (req, res) => {
116
+ removeMcpServer(req.params.id);
117
+ res.json({ ok: true });
754
118
  });
755
- api.post("/schedules/squads/:id/resume", (req, res) => {
756
- const id = Number(req.params.id);
757
- if (Number.isNaN(id)) {
758
- res.status(400).json({ error: "Invalid schedule id" });
759
- return;
760
- }
761
- try {
762
- const ok = setScheduleEnabled(id, true);
763
- if (!ok) {
764
- res.status(404).json({ error: "Squad schedule not found" });
765
- return;
766
- }
767
- res.json({ ok: true, schedule: getSchedule(id) });
768
- }
769
- catch (e) {
770
- console.error("Error resuming squad schedule:", e);
771
- res.status(500).json({ error: (e instanceof Error ? e.message : String(e)) });
772
- }
119
+ // --- Skills ---
120
+ app.get("/api/skills", async (_req, res) => {
121
+ const skills = await listSkills();
122
+ res.json(skills);
773
123
  });
774
- api.post("/schedules/squads/:id/run-now", async (req, res) => {
775
- const id = Number(req.params.id);
776
- if (Number.isNaN(id)) {
777
- res.status(400).json({ error: "Invalid schedule id" });
778
- return;
779
- }
124
+ app.post("/api/skills", async (req, res) => {
780
125
  try {
781
- const result = await runScheduleNow(id);
782
- if (!result.ok) {
783
- res.status(404).json({ error: result.error ?? "Squad schedule not found" });
126
+ const { url } = req.body;
127
+ if (!url || typeof url !== "string") {
128
+ res.status(400).json({ error: "Missing 'url' in request body" });
784
129
  return;
785
130
  }
786
- res.json({ ok: true });
131
+ await addSkill(url);
132
+ res.status(201).json({ ok: true });
787
133
  }
788
- catch (e) {
789
- console.error("Error running squad schedule now:", e);
790
- res.status(500).json({ error: (e instanceof Error ? e.message : String(e)) });
134
+ catch (err) {
135
+ res.status(400).json({ error: err.message });
791
136
  }
792
137
  });
793
- api.delete("/schedules/squads/:id", (req, res) => {
794
- const id = Number(req.params.id);
795
- if (Number.isNaN(id)) {
796
- res.status(400).json({ error: "Invalid schedule id" });
797
- return;
798
- }
138
+ app.delete("/api/skills/:slug", async (req, res) => {
799
139
  try {
800
- const ok = deleteSchedule(id);
801
- if (!ok) {
802
- res.status(404).json({ error: "Squad schedule not found" });
803
- return;
804
- }
140
+ await removeSkill(req.params.slug);
805
141
  res.json({ ok: true });
806
142
  }
807
- catch (e) {
808
- console.error("Error deleting squad schedule:", e);
809
- res.status(500).json({ error: (e instanceof Error ? e.message : String(e)) });
143
+ catch (err) {
144
+ res.status(404).json({ error: err.message });
810
145
  }
811
146
  });
812
- // IO schedule lifecycle
813
- api.post("/schedules/io/:id/pause", (req, res) => {
814
- const id = Number(req.params.id);
815
- if (Number.isNaN(id)) {
816
- res.status(400).json({ error: "Invalid schedule id" });
817
- return;
818
- }
819
- try {
820
- const ok = setIoScheduleEnabled(id, false);
821
- if (!ok) {
822
- res.status(404).json({ error: "IO schedule not found" });
823
- return;
824
- }
825
- res.json({ ok: true, schedule: getIoSchedule(id) });
826
- }
827
- catch (e) {
828
- console.error("Error pausing IO schedule:", e);
829
- res.status(500).json({ error: (e instanceof Error ? e.message : String(e)) });
830
- }
147
+ // --- Wiki ---
148
+ app.get("/api/wiki/pages", async (_req, res) => {
149
+ const pages = await listPages();
150
+ res.json(pages);
831
151
  });
832
- api.post("/schedules/io/:id/resume", (req, res) => {
833
- const id = Number(req.params.id);
834
- if (Number.isNaN(id)) {
835
- res.status(400).json({ error: "Invalid schedule id" });
836
- return;
837
- }
152
+ app.get("/api/wiki/page/*", async (req, res) => {
838
153
  try {
839
- const ok = setIoScheduleEnabled(id, true);
840
- if (!ok) {
841
- res.status(404).json({ error: "IO schedule not found" });
842
- return;
843
- }
844
- res.json({ ok: true, schedule: getIoSchedule(id) });
154
+ const pagePath = req.params[0];
155
+ const content = await readPage(pagePath);
156
+ res.json({ path: pagePath, content });
845
157
  }
846
- catch (e) {
847
- console.error("Error resuming IO schedule:", e);
848
- res.status(500).json({ error: (e instanceof Error ? e.message : String(e)) });
158
+ catch (err) {
159
+ res.status(404).json({ error: err.message });
849
160
  }
850
161
  });
851
- api.post("/schedules/io/:id/run-now", async (req, res) => {
852
- const id = Number(req.params.id);
853
- if (Number.isNaN(id)) {
854
- res.status(400).json({ error: "Invalid schedule id" });
855
- return;
856
- }
857
- try {
858
- const ok = await runIoScheduleNow(id);
859
- if (!ok) {
860
- res.status(404).json({ error: "IO schedule not found" });
861
- return;
862
- }
863
- res.json({ ok: true });
864
- }
865
- catch (e) {
866
- console.error("Error running IO schedule now:", e);
867
- res.status(500).json({ error: (e instanceof Error ? e.message : String(e)) });
868
- }
162
+ app.put("/api/wiki/page/*", async (req, res) => {
163
+ const pagePath = req.params[0];
164
+ const { content } = req.body;
165
+ await writePage(pagePath, content);
166
+ res.json({ ok: true });
869
167
  });
870
- api.delete("/schedules/io/:id", (req, res) => {
871
- const id = Number(req.params.id);
872
- if (Number.isNaN(id)) {
873
- res.status(400).json({ error: "Invalid schedule id" });
874
- return;
875
- }
168
+ app.delete("/api/wiki/page/*", async (req, res) => {
876
169
  try {
877
- const ok = deleteIoSchedule(id);
878
- if (!ok) {
879
- res.status(404).json({ error: "IO schedule not found" });
880
- return;
881
- }
170
+ const pagePath = req.params[0];
171
+ await deletePage(pagePath);
882
172
  res.json({ ok: true });
883
173
  }
884
- catch (e) {
885
- console.error("Error deleting IO schedule:", e);
886
- res.status(500).json({ error: (e instanceof Error ? e.message : String(e)) });
174
+ catch (err) {
175
+ res.status(404).json({ error: err.message });
887
176
  }
888
177
  });
889
- // Schedule run history (issue #65)
890
- api.get("/schedules/:type/:id/runs", (req, res) => {
891
- const rawType = Array.isArray(req.params.type) ? req.params.type[0] : req.params.type;
892
- const id = Number(Array.isArray(req.params.id) ? req.params.id[0] : req.params.id);
893
- if (Number.isNaN(id)) {
894
- res.status(400).json({ error: "Invalid schedule id" });
895
- return;
896
- }
897
- const scheduleTypeMap = { squads: "squad", io: "io" };
898
- const scheduleType = scheduleTypeMap[rawType];
899
- if (!scheduleType) {
900
- res.status(400).json({ error: "type must be 'squads' or 'io'" });
178
+ app.get("/api/wiki/search", async (req, res) => {
179
+ const query = req.query.q;
180
+ if (!query) {
181
+ res.status(400).json({ error: "q is required" });
901
182
  return;
902
183
  }
903
- const rawLimit = Number.parseInt(String(req.query.limit ?? ""), 10);
904
- const limit = Number.isNaN(rawLimit) ? 25 : Math.min(rawLimit, 100);
905
- try {
906
- const runs = getScheduleRuns(scheduleType, id, limit);
907
- res.json({ runs });
908
- }
909
- catch (e) {
910
- console.error("Error fetching schedule runs:", e);
911
- res.status(500).json({ error: (e instanceof Error ? e.message : String(e)) });
912
- }
184
+ const results = await searchPages(query);
185
+ res.json(results);
913
186
  });
914
- // Chat endpoints
915
- api.post("/message", async (req, res) => {
916
- const { text, attachments } = req.body;
917
- if (!text) {
918
- res.status(400).json({ error: "Missing 'text' in request body" });
919
- return;
920
- }
921
- if (attachments !== undefined) {
922
- if (!Array.isArray(attachments)) {
923
- res.status(400).json({ error: "'attachments' must be an array" });
924
- return;
925
- }
926
- for (const att of attachments) {
927
- if (!att.data || !att.mimeType) {
928
- res.status(400).json({ error: "Each attachment must have 'data' and 'mimeType'" });
929
- return;
930
- }
931
- // Reject single attachments whose base64 payload exceeds ~7MB (≈5MB raw)
932
- if (att.data.length > 7 * 1024 * 1024) {
933
- res.status(413).json({ error: "Attachment exceeds maximum allowed size of 5MB" });
934
- return;
935
- }
936
- }
937
- }
938
- if (!messageHandler) {
939
- res.status(503).json({ error: "No message handler registered" });
940
- return;
941
- }
942
- const connectionId = crypto.randomUUID();
943
- let fullResponse = "";
944
- await messageHandler(text, connectionId, (chunk, done) => {
945
- fullResponse += chunk;
946
- const ssePayload = JSON.stringify({
947
- type: done ? "done" : "delta",
948
- text: chunk,
949
- });
950
- for (const conn of sseConnections) {
951
- conn.write(`data: ${ssePayload}\n\n`);
952
- }
953
- }, attachments);
954
- res.json({ response: fullResponse });
955
- });
956
- // Wiki endpoints (issue #105)
957
- function extractWikiTitle(pageContent, fallback) {
958
- const match = pageContent.match(/^#\s+(.+)/m);
959
- return match ? match[1].trim() : fallback;
960
- }
961
- api.get("/wiki", (_req, res) => {
962
- try {
963
- const pages = listPages();
964
- const result = pages.map((pagePath) => {
965
- const pageContent = readPage(pagePath);
966
- const title = pageContent ? extractWikiTitle(pageContent, pagePath) : pagePath;
967
- return { path: pagePath, title };
968
- });
969
- res.json({ pages: result });
970
- }
971
- catch (e) {
972
- console.error("Error listing wiki pages:", e);
973
- res.status(500).json({ error: "Failed to list wiki pages" });
974
- }
975
- });
976
- api.get("/wiki/*path", (req, res) => {
977
- try {
978
- const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
979
- if (!pagePath) {
980
- res.status(400).json({ error: "Missing page path" });
981
- return;
982
- }
983
- const pageContent = readPage(pagePath);
984
- if (pageContent === undefined) {
985
- res.status(404).json({ error: "Page not found" });
986
- return;
987
- }
988
- res.json({ path: pagePath, content: pageContent });
989
- }
990
- catch (e) {
991
- console.error("Error reading wiki page:", e);
992
- res.status(500).json({ error: "Failed to read wiki page" });
993
- }
994
- });
995
- // Create a new wiki page
996
- api.post("/wiki", (req, res) => {
997
- try {
998
- const { path: pagePath, content } = req.body;
999
- if (!pagePath || typeof pagePath !== "string") {
1000
- res.status(400).json({ error: "Missing page path" });
1001
- return;
1002
- }
1003
- if (content === undefined || typeof content !== "string") {
1004
- res.status(400).json({ error: "Missing page content" });
1005
- return;
1006
- }
1007
- try {
1008
- assertPagePath(pagePath);
1009
- }
1010
- catch (e) {
1011
- res.status(400).json({ error: e.message });
1012
- return;
1013
- }
1014
- if (readPage(pagePath) !== undefined) {
1015
- res.status(409).json({ error: "Page already exists" });
1016
- return;
1017
- }
1018
- writePage(pagePath, content);
1019
- res.status(201).json({ path: pagePath, content });
1020
- }
1021
- catch (e) {
1022
- console.error("Error creating wiki page:", e);
1023
- res.status(500).json({ error: "Failed to create wiki page" });
1024
- }
1025
- });
1026
- // Update an existing wiki page
1027
- api.put("/wiki/*path", (req, res) => {
1028
- try {
1029
- const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
1030
- if (!pagePath) {
1031
- res.status(400).json({ error: "Missing page path" });
1032
- return;
1033
- }
1034
- const { content } = req.body;
1035
- if (content === undefined || typeof content !== "string") {
1036
- res.status(400).json({ error: "Missing page content" });
1037
- return;
1038
- }
1039
- try {
1040
- assertPagePath(pagePath);
1041
- }
1042
- catch (e) {
1043
- res.status(400).json({ error: e.message });
1044
- return;
1045
- }
1046
- if (readPage(pagePath) === undefined) {
1047
- res.status(404).json({ error: "Page not found" });
1048
- return;
1049
- }
1050
- writePage(pagePath, content);
1051
- res.json({ path: pagePath, content });
1052
- }
1053
- catch (e) {
1054
- console.error("Error updating wiki page:", e);
1055
- res.status(500).json({ error: "Failed to update wiki page" });
1056
- }
187
+ // --- Schedules ---
188
+ app.get("/api/schedules", (_req, res) => {
189
+ const type = undefined; // return all
190
+ res.json(listSchedules(type));
1057
191
  });
1058
- // Delete a wiki page
1059
- api.delete("/wiki/*path", (req, res) => {
1060
- try {
1061
- const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
1062
- if (!pagePath) {
1063
- res.status(400).json({ error: "Missing page path" });
1064
- return;
1065
- }
1066
- try {
1067
- assertPagePath(pagePath);
1068
- }
1069
- catch (e) {
1070
- res.status(400).json({ error: e.message });
1071
- return;
1072
- }
1073
- const deleted = deletePage(pagePath);
1074
- if (!deleted) {
1075
- res.status(404).json({ error: "Page not found" });
1076
- return;
1077
- }
1078
- res.status(204).send();
1079
- }
1080
- catch (e) {
1081
- console.error("Error deleting wiki page:", e);
1082
- res.status(500).json({ error: "Failed to delete wiki page" });
1083
- }
1084
- });
1085
- // Get available wiki categories
1086
- api.get("/wiki-categories", (_req, res) => {
1087
- res.json({ categories: ["preferences", "projects", "people", "general", "squads"] });
192
+ app.post("/api/schedules", (req, res) => {
193
+ const schedule = createSchedule(req.body);
194
+ res.json(schedule);
1088
195
  });
1089
- // Mount API at /api (for frontend)
1090
- app.use("/api", api);
1091
- // Serve Vue frontend if built assets exist (before backward-compat API mount)
1092
- if (existsSync(WEB_DIST)) {
1093
- app.use(express.static(WEB_DIST));
1094
- console.log("[io] Web frontend enabled");
1095
- }
1096
- // ── MCP server management endpoints ────────────────────────────────────────
1097
- api.get("/mcp/servers", (_req, res) => {
1098
- try {
1099
- const config = loadMcpConfig();
1100
- res.json({ servers: config.servers });
1101
- }
1102
- catch (e) {
1103
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
1104
- }
1105
- });
1106
- api.post("/mcp/servers", (req, res) => {
1107
- const { name, command, args, url, env } = req.body;
1108
- if (!name) {
1109
- res.status(400).json({ error: "name is required" });
1110
- return;
1111
- }
1112
- if (!command && !url) {
1113
- res.status(400).json({ error: "command or url is required" });
1114
- return;
1115
- }
1116
- try {
1117
- const config = loadMcpConfig();
1118
- if (config.servers.find(s => s.name === name)) {
1119
- res.status(409).json({ error: "server already exists" });
1120
- return;
1121
- }
1122
- config.servers.push({ name, command, args, url, env, enabled: true });
1123
- saveMcpConfig(config);
1124
- res.status(201).json({ ok: true });
1125
- }
1126
- catch (e) {
1127
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
196
+ app.put("/api/schedules/:id", (req, res) => {
197
+ const { enabled } = req.body;
198
+ if (typeof enabled === "boolean") {
199
+ toggleSchedule(req.params.id, enabled);
1128
200
  }
201
+ res.json({ ok: true });
1129
202
  });
1130
- api.delete("/mcp/servers/:name", (req, res) => {
1131
- try {
1132
- const config = loadMcpConfig();
1133
- const idx = config.servers.findIndex(s => s.name === req.params.name);
1134
- if (idx === -1) {
1135
- res.status(404).json({ error: "server not found" });
1136
- return;
1137
- }
1138
- config.servers.splice(idx, 1);
1139
- saveMcpConfig(config);
1140
- res.json({ ok: true });
1141
- }
1142
- catch (e) {
1143
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
1144
- }
203
+ app.delete("/api/schedules/:id", (req, res) => {
204
+ deleteSchedule(req.params.id);
205
+ res.json({ ok: true });
1145
206
  });
1146
- api.patch("/mcp/servers/:name/toggle", (req, res) => {
1147
- try {
1148
- const config = loadMcpConfig();
1149
- const server = config.servers.find(s => s.name === req.params.name);
1150
- if (!server) {
1151
- res.status(404).json({ error: "server not found" });
1152
- return;
1153
- }
1154
- server.enabled = server.enabled === false ? true : false;
1155
- saveMcpConfig(config);
1156
- res.json({ ok: true, enabled: server.enabled });
1157
- }
1158
- catch (e) {
1159
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
1160
- }
207
+ // --- Health (unauthenticated) ---
208
+ app.get("/health", (_req, res) => {
209
+ res.json({ status: "ok", version: process.env.npm_package_version ?? "unknown" });
1161
210
  });
1162
- api.post("/mcp/reload", async (_req, res) => {
1163
- try {
1164
- await initMcpTools();
1165
- res.json({ ok: true });
1166
- }
1167
- catch (err) {
1168
- res.status(500).json({ error: err instanceof Error ? err.message : "reload failed" });
1169
- }
211
+ // SPA fallback serve index.html for non-API routes
212
+ app.get("*", (_req, res) => {
213
+ res.sendFile(join(webDistPath, "index.html"));
1170
214
  });
1171
- // SPA fallback for browser navigation: when the web frontend is built,
1172
- // serve index.html for any GET request that accepts HTML and isn't an API
1173
- // call. This lets vue-router handle client-side routes like /chat, /skills,
1174
- // /squads, etc. on direct URL access and page refresh. Programmatic clients
1175
- // (curl, fetch without Accept: text/html) fall through to the backward-compat
1176
- // API mount below.
1177
- if (existsSync(WEB_DIST)) {
1178
- app.get(/.*/, (req, res, next) => {
1179
- if (req.path.startsWith("/api/"))
1180
- return next();
1181
- const accept = req.headers.accept ?? "";
1182
- if (!accept.includes("text/html"))
1183
- return next();
1184
- res.sendFile(path.join(WEB_DIST, "index.html"));
1185
- });
1186
- }
1187
- // Backward-compat: mount API at / for non-browser clients (after static files
1188
- // and SPA fallback so frontend routes are not intercepted).
1189
- app.use("/", api);
1190
- return new Promise((resolve) => {
1191
- app.listen(config.port, () => {
1192
- console.log(`[io] Server listening on port ${config.port}`);
1193
- resolve();
1194
- });
215
+ app.listen(config.port, () => {
216
+ // Server started
1195
217
  });
1196
218
  }
219
+ export { broadcast };
1197
220
  //# sourceMappingURL=server.js.map