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.
- package/README.md +40 -52
- package/dist/api/auth.js +35 -38
- package/dist/api/server.js +157 -1139
- 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 -129
- 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-CedxxE6K.js +0 -1
- package/web-dist/assets/ChatView-DMkYQo_V.js +0 -4
- package/web-dist/assets/FeedView-BH4q-31V.js +0 -1
- package/web-dist/assets/InboxView-BVwVP4EW.js +0 -1
- package/web-dist/assets/LoginView-DRPDhnwu.js +0 -1
- package/web-dist/assets/McpView-D8yWz-lq.js +0 -1
- package/web-dist/assets/SchedulesView-BzzyncGF.js +0 -1
- package/web-dist/assets/SettingsTabs.vue_vue_type_script_setup_true_lang-oW3ySu7Y.js +0 -1
- package/web-dist/assets/SkillsView-oxpYuhx7.js +0 -1
- package/web-dist/assets/SquadsView-CaKUIKlq.js +0 -1
- package/web-dist/assets/StatusIndicator.vue_vue_type_script_setup_true_lang-8U15Qp_Q.js +0 -1
- package/web-dist/assets/WikiView-C5jXUlfW.js +0 -1
- package/web-dist/assets/index-BrWzNw-N.css +0 -10
- package/web-dist/assets/index-f67odrrt.js +0 -81
- package/web-dist/icons.svg +0 -24
package/dist/api/server.js
CHANGED
|
@@ -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 {
|
|
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
|
-
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
|
-
|
|
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
|
-
|
|
45
|
+
const idx = sseClients.indexOf(client);
|
|
46
|
+
if (idx !== -1)
|
|
47
|
+
sseClients.splice(idx, 1);
|
|
202
48
|
});
|
|
203
49
|
});
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
const {
|
|
207
|
-
if (!
|
|
208
|
-
res.status(400).json({ error: "
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
res.
|
|
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
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
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
|
|
787
|
-
if (!
|
|
788
|
-
res.status(
|
|
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
|
-
|
|
131
|
+
await addSkill(url);
|
|
132
|
+
res.status(201).json({ ok: true });
|
|
792
133
|
}
|
|
793
|
-
catch (
|
|
794
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
813
|
-
|
|
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
|
-
//
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
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
|
-
|
|
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
|
|
845
|
-
|
|
846
|
-
|
|
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 (
|
|
852
|
-
|
|
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
|
-
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
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
|
|
883
|
-
|
|
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 (
|
|
890
|
-
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
|
948
|
-
|
|
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
|
-
//
|
|
962
|
-
|
|
963
|
-
const
|
|
964
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
//
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
1177
|
-
|
|
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
|