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