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.
- package/README.md +40 -52
- package/dist/api/auth.js +35 -38
- package/dist/api/server.js +157 -1134
- package/dist/config.js +49 -32
- package/dist/copilot/agents.js +72 -1055
- package/dist/copilot/client.js +6 -17
- package/dist/copilot/io-scheduler.js +55 -139
- package/dist/copilot/model-router.js +100 -72
- package/dist/copilot/orchestrator.js +91 -515
- package/dist/copilot/scheduler.js +67 -189
- package/dist/copilot/skills.js +41 -366
- package/dist/copilot/system-message.js +40 -200
- package/dist/copilot/tools.js +191 -2042
- package/dist/daemon.js +54 -201
- package/dist/index.js +15 -133
- package/dist/mcp/config.js +23 -31
- package/dist/mcp/index.js +2 -3
- package/dist/mcp/registry.js +33 -88
- package/dist/notify.js +18 -100
- package/dist/paths.js +13 -24
- package/dist/setup.js +35 -0
- package/dist/store/db.js +111 -297
- package/dist/store/feed.js +29 -97
- package/dist/store/instances.js +56 -121
- package/dist/store/schedules.js +21 -73
- package/dist/store/squads.js +35 -186
- package/dist/store/tasks.js +25 -168
- package/dist/telegram/bot.js +20 -312
- package/dist/telegram/handlers.js +39 -3
- package/dist/watchdog.js +31 -45
- package/dist/wiki/fs.js +38 -155
- package/dist/wiki/search.js +31 -44
- package/package.json +5 -8
- package/web-dist/assets/ChatView-EFFiln1H.js +11 -0
- package/web-dist/assets/FeedView-bN4NMOL7.js +6 -0
- package/web-dist/assets/LoginView-CNtasq3n.js +1 -0
- package/web-dist/assets/McpView-C2CHiwsi.js +1 -0
- package/web-dist/assets/SchedulesView-CyilLban.js +1 -0
- package/web-dist/assets/SettingsView-1wLXKEF4.js +1 -0
- package/web-dist/assets/SkillsView-BLsD-0u0.js +1 -0
- package/web-dist/assets/SquadDetailView-CsCw2ZLp.js +21 -0
- package/web-dist/assets/SquadsView-DQ3vFlyO.js +6 -0
- package/web-dist/assets/WikiView-19M3oqnq.js +21 -0
- package/web-dist/assets/api-WGvTsXaE.js +1 -0
- package/web-dist/assets/index-D7M5O-_l.css +1 -0
- package/web-dist/assets/index-DZOS9syn.js +95 -0
- package/web-dist/assets/plus-BOvyX1BC.js +6 -0
- package/web-dist/assets/trash-2-DHoetkC4.js +6 -0
- package/web-dist/favicon.svg +4 -1
- package/web-dist/index.html +7 -10
- package/dist/api/logout.test.js +0 -128
- package/dist/api/mcp.test.js +0 -285
- package/dist/api/wiki.test.js +0 -283
- package/dist/auth/session-logic.js +0 -79
- package/dist/auth/session-logic.test.js +0 -201
- package/dist/copilot/auto-complete-instance.test.js +0 -104
- package/dist/copilot/cron.js +0 -136
- package/dist/copilot/event-summary.js +0 -286
- package/dist/copilot/instance-deactivate.test.js +0 -119
- package/dist/copilot/model-router.test.js +0 -71
- package/dist/copilot/review-backfill.js +0 -57
- package/dist/copilot/session-timeout.js +0 -112
- package/dist/copilot/session-timeout.test.js +0 -372
- package/dist/copilot/skills.test.js +0 -55
- package/dist/copilot/universes.js +0 -469
- package/dist/instance-watchdog.js +0 -104
- package/dist/instance-watchdog.test.js +0 -183
- package/dist/mcp/client.js +0 -109
- package/dist/mcp/client.test.js +0 -99
- package/dist/mcp/config.test.js +0 -49
- package/dist/mcp/registry.test.js +0 -79
- package/dist/notify.test.js +0 -232
- package/dist/store/feed.test.js +0 -279
- package/dist/store/instances.test.js +0 -310
- package/dist/store/io-schedules.js +0 -63
- package/dist/store/notifications.js +0 -79
- package/dist/store/notifications.test.js +0 -197
- package/dist/store/schedule-runs.js +0 -46
- package/dist/store/squads.test.js +0 -405
- package/dist/store/tasks.test.js +0 -150
- package/dist/store/worktrees.js +0 -83
- package/dist/tui/index.js +0 -286
- package/dist/update.js +0 -81
- package/dist/watchdog.test.js +0 -83
- package/dist/wiki/wiki-squad.test.js +0 -54
- package/web-dist/assets/AgentActivityView-B1PaNYy8.js +0 -1
- package/web-dist/assets/ChatView-BbpWnrtC.js +0 -4
- package/web-dist/assets/FeedView-B5LaMV0I.js +0 -1
- package/web-dist/assets/InboxView-Cwqt8rH7.js +0 -1
- package/web-dist/assets/LoginView-refmPLKT.js +0 -1
- package/web-dist/assets/McpView-B1w0dRFY.js +0 -1
- package/web-dist/assets/SchedulesView-D9l2DI7X.js +0 -1
- package/web-dist/assets/SettingsTabs.vue_vue_type_script_setup_true_lang-DncOVVEB.js +0 -1
- package/web-dist/assets/SkillsView-uFX0q1mV.js +0 -1
- package/web-dist/assets/SquadsView-B1nZW4ml.js +0 -1
- package/web-dist/assets/StatusIndicator.vue_vue_type_script_setup_true_lang-pTrJJwX1.js +0 -1
- package/web-dist/assets/WikiView-B54cCKIK.js +0 -1
- package/web-dist/assets/index-C0VEUWQ1.js +0 -81
- package/web-dist/assets/index-eluTyieM.css +0 -10
- package/web-dist/icons.svg +0 -24
package/dist/api/server.js
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
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(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
const idx = sseClients.indexOf(client);
|
|
46
|
+
if (idx !== -1)
|
|
47
|
+
sseClients.splice(idx, 1);
|
|
197
48
|
});
|
|
198
49
|
});
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
const {
|
|
202
|
-
if (!
|
|
203
|
-
res.status(400).json({ error: "
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
res.json({
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
res.
|
|
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
|
-
//
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
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
|
|
782
|
-
if (!
|
|
783
|
-
res.status(
|
|
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
|
-
|
|
131
|
+
await addSkill(url);
|
|
132
|
+
res.status(201).json({ ok: true });
|
|
787
133
|
}
|
|
788
|
-
catch (
|
|
789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
808
|
-
|
|
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
|
-
//
|
|
813
|
-
|
|
814
|
-
const
|
|
815
|
-
|
|
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
|
-
|
|
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
|
|
840
|
-
|
|
841
|
-
|
|
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 (
|
|
847
|
-
|
|
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
|
-
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
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
|
|
878
|
-
|
|
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 (
|
|
885
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
|
904
|
-
|
|
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
|
-
//
|
|
915
|
-
|
|
916
|
-
const
|
|
917
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1172
|
-
|
|
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
|