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/mcp.test.js
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for MCP API endpoints (#279).
|
|
3
|
-
*
|
|
4
|
-
* Strategy: spin up a minimal Express server that mounts the same MCP route
|
|
5
|
-
* handlers as src/api/server.ts, but pointed at a temp config file via
|
|
6
|
-
* setMcpConfigPathForTests(). Tests make real HTTP requests using node:http.
|
|
7
|
-
* The /mcp/reload endpoint uses an injectable reload fn to avoid pulling in
|
|
8
|
-
* the full orchestrator (which needs DB + Copilot client).
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, before, after, beforeEach } from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import http from "node:http";
|
|
16
|
-
import express from "express";
|
|
17
|
-
import { setMcpConfigPathForTests, resetMcpConfigPath, loadMcpConfig, saveMcpConfig } from "../mcp/config.js";
|
|
18
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
19
|
-
function req(method, port, path, body) {
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
|
22
|
-
const options = {
|
|
23
|
-
hostname: "127.0.0.1",
|
|
24
|
-
port,
|
|
25
|
-
path,
|
|
26
|
-
method,
|
|
27
|
-
headers: {
|
|
28
|
-
...(payload ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } : {}),
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
const r = http.request(options, (res) => {
|
|
32
|
-
let data = "";
|
|
33
|
-
res.on("data", (chunk) => { data += chunk; });
|
|
34
|
-
res.on("end", () => {
|
|
35
|
-
try {
|
|
36
|
-
resolve({ status: res.statusCode ?? 0, body: JSON.parse(data) });
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
resolve({ status: res.statusCode ?? 0, body: data });
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
r.on("error", reject);
|
|
44
|
-
if (payload)
|
|
45
|
-
r.write(payload);
|
|
46
|
-
r.end();
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
// ── Test server setup ─────────────────────────────────────────────────────────
|
|
50
|
-
let server;
|
|
51
|
-
let port;
|
|
52
|
-
let tmpDir;
|
|
53
|
-
let configPath;
|
|
54
|
-
function buildMcpApp(reloadFn = async () => { }) {
|
|
55
|
-
const app = express();
|
|
56
|
-
app.use(express.json());
|
|
57
|
-
app.get("/api/mcp/servers", (_req, res) => {
|
|
58
|
-
try {
|
|
59
|
-
const config = loadMcpConfig();
|
|
60
|
-
res.json({ servers: config.servers });
|
|
61
|
-
}
|
|
62
|
-
catch (e) {
|
|
63
|
-
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
app.post("/api/mcp/servers", (req, res) => {
|
|
67
|
-
const { name, command, args, url, env } = req.body;
|
|
68
|
-
if (!name) {
|
|
69
|
-
res.status(400).json({ error: "name is required" });
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (!command && !url) {
|
|
73
|
-
res.status(400).json({ error: "command or url is required" });
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
const config = loadMcpConfig();
|
|
78
|
-
if (config.servers.find(s => s.name === name)) {
|
|
79
|
-
res.status(409).json({ error: "server already exists" });
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
config.servers.push({ name, command, args, url, env, enabled: true });
|
|
83
|
-
saveMcpConfig(config);
|
|
84
|
-
res.status(201).json({ ok: true });
|
|
85
|
-
}
|
|
86
|
-
catch (e) {
|
|
87
|
-
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
app.delete("/api/mcp/servers/:name", (req, res) => {
|
|
91
|
-
try {
|
|
92
|
-
const config = loadMcpConfig();
|
|
93
|
-
const idx = config.servers.findIndex(s => s.name === req.params.name);
|
|
94
|
-
if (idx === -1) {
|
|
95
|
-
res.status(404).json({ error: "server not found" });
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
config.servers.splice(idx, 1);
|
|
99
|
-
saveMcpConfig(config);
|
|
100
|
-
res.json({ ok: true });
|
|
101
|
-
}
|
|
102
|
-
catch (e) {
|
|
103
|
-
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
app.patch("/api/mcp/servers/:name/toggle", (req, res) => {
|
|
107
|
-
try {
|
|
108
|
-
const config = loadMcpConfig();
|
|
109
|
-
const srv = config.servers.find(s => s.name === req.params.name);
|
|
110
|
-
if (!srv) {
|
|
111
|
-
res.status(404).json({ error: "server not found" });
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
srv.enabled = srv.enabled === false ? true : false;
|
|
115
|
-
saveMcpConfig(config);
|
|
116
|
-
res.json({ ok: true, enabled: srv.enabled });
|
|
117
|
-
}
|
|
118
|
-
catch (e) {
|
|
119
|
-
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
app.post("/api/mcp/reload", async (_req, res) => {
|
|
123
|
-
try {
|
|
124
|
-
await reloadFn();
|
|
125
|
-
res.json({ ok: true });
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
res.status(500).json({ error: err instanceof Error ? err.message : "reload failed" });
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
return app;
|
|
132
|
-
}
|
|
133
|
-
before(async () => {
|
|
134
|
-
tmpDir = mkdtempSync(join(tmpdir(), "io-mcp-api-test-"));
|
|
135
|
-
configPath = join(tmpDir, "mcp.json");
|
|
136
|
-
setMcpConfigPathForTests(configPath);
|
|
137
|
-
await new Promise((resolve) => {
|
|
138
|
-
server = buildMcpApp().listen(0, "127.0.0.1", () => {
|
|
139
|
-
port = server.address().port;
|
|
140
|
-
resolve();
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
after(async () => {
|
|
145
|
-
await new Promise((resolve, reject) => server.close((e) => e ? reject(e) : resolve()));
|
|
146
|
-
resetMcpConfigPath();
|
|
147
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
148
|
-
});
|
|
149
|
-
beforeEach(() => {
|
|
150
|
-
// Reset config file between tests
|
|
151
|
-
saveMcpConfig({ servers: [] });
|
|
152
|
-
});
|
|
153
|
-
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
154
|
-
describe("GET /api/mcp/servers", () => {
|
|
155
|
-
it("returns empty array when no servers configured", async () => {
|
|
156
|
-
const res = await req("GET", port, "/api/mcp/servers");
|
|
157
|
-
assert.equal(res.status, 200);
|
|
158
|
-
assert.deepEqual(res.body.servers, []);
|
|
159
|
-
});
|
|
160
|
-
it("returns configured servers", async () => {
|
|
161
|
-
saveMcpConfig({ servers: [{ name: "figma", command: "npx", enabled: true }] });
|
|
162
|
-
const res = await req("GET", port, "/api/mcp/servers");
|
|
163
|
-
assert.equal(res.status, 200);
|
|
164
|
-
const { servers } = res.body;
|
|
165
|
-
assert.equal(servers.length, 1);
|
|
166
|
-
assert.equal(servers[0].name, "figma");
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
describe("POST /api/mcp/servers", () => {
|
|
170
|
-
it("creates a stdio server successfully", async () => {
|
|
171
|
-
const res = await req("POST", port, "/api/mcp/servers", {
|
|
172
|
-
name: "github",
|
|
173
|
-
command: "npx",
|
|
174
|
-
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
175
|
-
});
|
|
176
|
-
assert.equal(res.status, 201);
|
|
177
|
-
assert.equal(res.body.ok, true);
|
|
178
|
-
const config = loadMcpConfig();
|
|
179
|
-
assert.equal(config.servers.length, 1);
|
|
180
|
-
assert.equal(config.servers[0].name, "github");
|
|
181
|
-
assert.equal(config.servers[0].command, "npx");
|
|
182
|
-
assert.deepEqual(config.servers[0].args, ["-y", "@modelcontextprotocol/server-github"]);
|
|
183
|
-
assert.equal(config.servers[0].enabled, true);
|
|
184
|
-
});
|
|
185
|
-
it("creates an SSE server successfully", async () => {
|
|
186
|
-
const res = await req("POST", port, "/api/mcp/servers", {
|
|
187
|
-
name: "postgres",
|
|
188
|
-
url: "http://localhost:3001/sse",
|
|
189
|
-
});
|
|
190
|
-
assert.equal(res.status, 201);
|
|
191
|
-
const config = loadMcpConfig();
|
|
192
|
-
assert.equal(config.servers[0].url, "http://localhost:3001/sse");
|
|
193
|
-
});
|
|
194
|
-
it("returns 400 when name is missing", async () => {
|
|
195
|
-
const res = await req("POST", port, "/api/mcp/servers", { command: "npx" });
|
|
196
|
-
assert.equal(res.status, 400);
|
|
197
|
-
assert.ok(res.body.error.includes("name"));
|
|
198
|
-
});
|
|
199
|
-
it("returns 400 when both command and url are missing", async () => {
|
|
200
|
-
const res = await req("POST", port, "/api/mcp/servers", { name: "bad-server" });
|
|
201
|
-
assert.equal(res.status, 400);
|
|
202
|
-
assert.ok(res.body.error.includes("command"));
|
|
203
|
-
});
|
|
204
|
-
it("returns 409 for duplicate server name", async () => {
|
|
205
|
-
saveMcpConfig({ servers: [{ name: "figma", command: "npx" }] });
|
|
206
|
-
const res = await req("POST", port, "/api/mcp/servers", { name: "figma", command: "npx" });
|
|
207
|
-
assert.equal(res.status, 409);
|
|
208
|
-
assert.ok(res.body.error.includes("already exists"));
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
describe("DELETE /api/mcp/servers/:name", () => {
|
|
212
|
-
it("removes an existing server", async () => {
|
|
213
|
-
saveMcpConfig({ servers: [{ name: "figma", command: "npx" }, { name: "postgres", url: "http://localhost/sse" }] });
|
|
214
|
-
const res = await req("DELETE", port, "/api/mcp/servers/figma");
|
|
215
|
-
assert.equal(res.status, 200);
|
|
216
|
-
assert.equal(res.body.ok, true);
|
|
217
|
-
const config = loadMcpConfig();
|
|
218
|
-
assert.equal(config.servers.length, 1);
|
|
219
|
-
assert.equal(config.servers[0].name, "postgres");
|
|
220
|
-
});
|
|
221
|
-
it("returns 404 for unknown server", async () => {
|
|
222
|
-
const res = await req("DELETE", port, "/api/mcp/servers/nonexistent");
|
|
223
|
-
assert.equal(res.status, 404);
|
|
224
|
-
assert.ok(res.body.error.includes("not found"));
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
describe("PATCH /api/mcp/servers/:name/toggle", () => {
|
|
228
|
-
it("disables an enabled server", async () => {
|
|
229
|
-
saveMcpConfig({ servers: [{ name: "figma", command: "npx", enabled: true }] });
|
|
230
|
-
const res = await req("PATCH", port, "/api/mcp/servers/figma/toggle");
|
|
231
|
-
assert.equal(res.status, 200);
|
|
232
|
-
assert.equal(res.body.enabled, false);
|
|
233
|
-
assert.equal(loadMcpConfig().servers[0].enabled, false);
|
|
234
|
-
});
|
|
235
|
-
it("enables a disabled server", async () => {
|
|
236
|
-
saveMcpConfig({ servers: [{ name: "figma", command: "npx", enabled: false }] });
|
|
237
|
-
const res = await req("PATCH", port, "/api/mcp/servers/figma/toggle");
|
|
238
|
-
assert.equal(res.status, 200);
|
|
239
|
-
assert.equal(res.body.enabled, true);
|
|
240
|
-
});
|
|
241
|
-
it("returns 404 for unknown server", async () => {
|
|
242
|
-
const res = await req("PATCH", port, "/api/mcp/servers/nonexistent/toggle");
|
|
243
|
-
assert.equal(res.status, 404);
|
|
244
|
-
assert.ok(res.body.error.includes("not found"));
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
describe("POST /api/mcp/reload", () => {
|
|
248
|
-
it("calls reload function and returns ok", async () => {
|
|
249
|
-
let reloadCalled = false;
|
|
250
|
-
const app = buildMcpApp(async () => { reloadCalled = true; });
|
|
251
|
-
await new Promise((resolve, reject) => {
|
|
252
|
-
const s = app.listen(0, "127.0.0.1", async () => {
|
|
253
|
-
const p = s.address().port;
|
|
254
|
-
try {
|
|
255
|
-
const res = await req("POST", p, "/api/mcp/reload");
|
|
256
|
-
assert.equal(res.status, 200);
|
|
257
|
-
assert.equal(res.body.ok, true);
|
|
258
|
-
assert.ok(reloadCalled, "reload function should have been called");
|
|
259
|
-
}
|
|
260
|
-
finally {
|
|
261
|
-
s.close(() => resolve());
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
s.on("error", reject);
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
it("returns 500 when reload throws", async () => {
|
|
268
|
-
const app = buildMcpApp(async () => { throw new Error("init failed"); });
|
|
269
|
-
await new Promise((resolve, reject) => {
|
|
270
|
-
const s = app.listen(0, "127.0.0.1", async () => {
|
|
271
|
-
const p = s.address().port;
|
|
272
|
-
try {
|
|
273
|
-
const res = await req("POST", p, "/api/mcp/reload");
|
|
274
|
-
assert.equal(res.status, 500);
|
|
275
|
-
assert.ok(res.body.error.includes("init failed"));
|
|
276
|
-
}
|
|
277
|
-
finally {
|
|
278
|
-
s.close(() => resolve());
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
s.on("error", reject);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
//# sourceMappingURL=mcp.test.js.map
|
package/dist/api/wiki.test.js
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for Wiki API endpoints (#312).
|
|
3
|
-
* Tests POST, PUT, DELETE endpoints for wiki pages.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, before, after, afterEach } from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import http from "node:http";
|
|
8
|
-
import express from "express";
|
|
9
|
-
import { listPages, readPage, writePage, deletePage, assertPagePath, ensureWikiStructure, } from "../wiki/fs.js";
|
|
10
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
11
|
-
function req(method, port, path, body) {
|
|
12
|
-
return new Promise((resolve, reject) => {
|
|
13
|
-
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
|
14
|
-
const options = {
|
|
15
|
-
hostname: "127.0.0.1",
|
|
16
|
-
port,
|
|
17
|
-
path,
|
|
18
|
-
method,
|
|
19
|
-
headers: {
|
|
20
|
-
...(payload ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } : {}),
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
const r = http.request(options, (res) => {
|
|
24
|
-
let data = "";
|
|
25
|
-
res.on("data", (chunk) => { data += chunk; });
|
|
26
|
-
res.on("end", () => {
|
|
27
|
-
try {
|
|
28
|
-
resolve({ status: res.statusCode ?? 0, body: data ? JSON.parse(data) : {} });
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
resolve({ status: res.statusCode ?? 0, body: data });
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
r.on("error", reject);
|
|
36
|
-
if (payload)
|
|
37
|
-
r.write(payload);
|
|
38
|
-
r.end();
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
// ── Server Setup ──────────────────────────────────────────────────────────────
|
|
42
|
-
function createTestServer() {
|
|
43
|
-
const app = express();
|
|
44
|
-
app.use(express.json());
|
|
45
|
-
// Wiki list
|
|
46
|
-
app.get("/api/wiki", (_req, res) => {
|
|
47
|
-
try {
|
|
48
|
-
const pages = listPages();
|
|
49
|
-
const result = pages.map((pagePath) => {
|
|
50
|
-
const pageContent = readPage(pagePath);
|
|
51
|
-
const title = pageContent?.match(/^#\s+(.+)/m)?.[1]?.trim() ?? pagePath;
|
|
52
|
-
return { path: pagePath, title };
|
|
53
|
-
});
|
|
54
|
-
res.json({ pages: result });
|
|
55
|
-
}
|
|
56
|
-
catch (e) {
|
|
57
|
-
res.status(500).json({ error: "Failed to list wiki pages" });
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
// Wiki read
|
|
61
|
-
app.get("/api/wiki/*path", (req, res) => {
|
|
62
|
-
const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
|
|
63
|
-
if (!pagePath) {
|
|
64
|
-
res.status(400).json({ error: "Missing page path" });
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const pageContent = readPage(pagePath);
|
|
68
|
-
if (pageContent === undefined) {
|
|
69
|
-
res.status(404).json({ error: "Page not found" });
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
res.json({ path: pagePath, content: pageContent });
|
|
73
|
-
});
|
|
74
|
-
// Wiki create
|
|
75
|
-
app.post("/api/wiki", (req, res) => {
|
|
76
|
-
const { path: pagePath, content } = req.body;
|
|
77
|
-
if (!pagePath || typeof pagePath !== "string") {
|
|
78
|
-
res.status(400).json({ error: "Missing page path" });
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (content === undefined || typeof content !== "string") {
|
|
82
|
-
res.status(400).json({ error: "Missing page content" });
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
try {
|
|
86
|
-
assertPagePath(pagePath);
|
|
87
|
-
}
|
|
88
|
-
catch (e) {
|
|
89
|
-
res.status(400).json({ error: e.message });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (readPage(pagePath) !== undefined) {
|
|
93
|
-
res.status(409).json({ error: "Page already exists" });
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
writePage(pagePath, content);
|
|
97
|
-
res.status(201).json({ path: pagePath, content });
|
|
98
|
-
});
|
|
99
|
-
// Wiki update
|
|
100
|
-
app.put("/api/wiki/*path", (req, res) => {
|
|
101
|
-
const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
|
|
102
|
-
if (!pagePath) {
|
|
103
|
-
res.status(400).json({ error: "Missing page path" });
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const { content } = req.body;
|
|
107
|
-
if (content === undefined || typeof content !== "string") {
|
|
108
|
-
res.status(400).json({ error: "Missing page content" });
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
assertPagePath(pagePath);
|
|
113
|
-
}
|
|
114
|
-
catch (e) {
|
|
115
|
-
res.status(400).json({ error: e.message });
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (readPage(pagePath) === undefined) {
|
|
119
|
-
res.status(404).json({ error: "Page not found" });
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
writePage(pagePath, content);
|
|
123
|
-
res.json({ path: pagePath, content });
|
|
124
|
-
});
|
|
125
|
-
// Wiki delete
|
|
126
|
-
app.delete("/api/wiki/*path", (req, res) => {
|
|
127
|
-
const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
|
|
128
|
-
if (!pagePath) {
|
|
129
|
-
res.status(400).json({ error: "Missing page path" });
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
assertPagePath(pagePath);
|
|
134
|
-
}
|
|
135
|
-
catch (e) {
|
|
136
|
-
res.status(400).json({ error: e.message });
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const deleted = deletePage(pagePath);
|
|
140
|
-
if (!deleted) {
|
|
141
|
-
res.status(404).json({ error: "Page not found" });
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
res.status(204).send();
|
|
145
|
-
});
|
|
146
|
-
// Wiki categories
|
|
147
|
-
app.get("/api/wiki-categories", (_req, res) => {
|
|
148
|
-
res.json({ categories: ["preferences", "projects", "people", "general", "squads"] });
|
|
149
|
-
});
|
|
150
|
-
return app;
|
|
151
|
-
}
|
|
152
|
-
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
153
|
-
describe("Wiki API Endpoints", () => {
|
|
154
|
-
let server;
|
|
155
|
-
let port;
|
|
156
|
-
const testPagePath = `pages/general/test-wiki-api-${Date.now()}.md`;
|
|
157
|
-
before(() => {
|
|
158
|
-
ensureWikiStructure();
|
|
159
|
-
const app = createTestServer();
|
|
160
|
-
server = app.listen(0);
|
|
161
|
-
port = server.address().port;
|
|
162
|
-
});
|
|
163
|
-
after(() => {
|
|
164
|
-
server.close();
|
|
165
|
-
// Clean up test page if exists
|
|
166
|
-
try {
|
|
167
|
-
deletePage(testPagePath);
|
|
168
|
-
}
|
|
169
|
-
catch { /* ignore */ }
|
|
170
|
-
});
|
|
171
|
-
afterEach(() => {
|
|
172
|
-
// Clean up test page after each test
|
|
173
|
-
try {
|
|
174
|
-
deletePage(testPagePath);
|
|
175
|
-
}
|
|
176
|
-
catch { /* ignore */ }
|
|
177
|
-
});
|
|
178
|
-
describe("GET /api/wiki", () => {
|
|
179
|
-
it("returns list of wiki pages", async () => {
|
|
180
|
-
const { status, body } = await req("GET", port, "/api/wiki");
|
|
181
|
-
assert.equal(status, 200);
|
|
182
|
-
assert.ok(Array.isArray(body.pages));
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
describe("GET /api/wiki-categories", () => {
|
|
186
|
-
it("returns available categories", async () => {
|
|
187
|
-
const { status, body } = await req("GET", port, "/api/wiki-categories");
|
|
188
|
-
assert.equal(status, 200);
|
|
189
|
-
const categories = body.categories;
|
|
190
|
-
assert.ok(categories.includes("general"));
|
|
191
|
-
assert.ok(categories.includes("projects"));
|
|
192
|
-
assert.ok(categories.includes("preferences"));
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
describe("POST /api/wiki", () => {
|
|
196
|
-
it("creates a new wiki page", async () => {
|
|
197
|
-
const { status, body } = await req("POST", port, "/api/wiki", {
|
|
198
|
-
path: testPagePath,
|
|
199
|
-
content: "# Test Page\n\nThis is a test.",
|
|
200
|
-
});
|
|
201
|
-
assert.equal(status, 201);
|
|
202
|
-
assert.equal(body.path, testPagePath);
|
|
203
|
-
// Verify it was created
|
|
204
|
-
const page = readPage(testPagePath);
|
|
205
|
-
assert.ok(page?.includes("# Test Page"));
|
|
206
|
-
});
|
|
207
|
-
it("returns 409 if page already exists", async () => {
|
|
208
|
-
writePage(testPagePath, "# Existing");
|
|
209
|
-
const { status, body } = await req("POST", port, "/api/wiki", {
|
|
210
|
-
path: testPagePath,
|
|
211
|
-
content: "# New Content",
|
|
212
|
-
});
|
|
213
|
-
assert.equal(status, 409);
|
|
214
|
-
assert.equal(body.error, "Page already exists");
|
|
215
|
-
});
|
|
216
|
-
it("returns 400 for missing path", async () => {
|
|
217
|
-
const { status } = await req("POST", port, "/api/wiki", {
|
|
218
|
-
content: "# Test",
|
|
219
|
-
});
|
|
220
|
-
assert.equal(status, 400);
|
|
221
|
-
});
|
|
222
|
-
it("returns 400 for missing content", async () => {
|
|
223
|
-
const { status } = await req("POST", port, "/api/wiki", {
|
|
224
|
-
path: testPagePath,
|
|
225
|
-
});
|
|
226
|
-
assert.equal(status, 400);
|
|
227
|
-
});
|
|
228
|
-
it("returns 400 for invalid path (not under pages/)", async () => {
|
|
229
|
-
const { status, body } = await req("POST", port, "/api/wiki", {
|
|
230
|
-
path: "invalid/path.md",
|
|
231
|
-
content: "# Test",
|
|
232
|
-
});
|
|
233
|
-
assert.equal(status, 400);
|
|
234
|
-
assert.ok(body.error.includes("pages/"));
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
describe("PUT /api/wiki/*path", () => {
|
|
238
|
-
it("updates an existing wiki page", async () => {
|
|
239
|
-
writePage(testPagePath, "# Original Content");
|
|
240
|
-
const { status, body } = await req("PUT", port, `/api/wiki/${testPagePath}`, {
|
|
241
|
-
content: "# Updated Content\n\nNew text here.",
|
|
242
|
-
});
|
|
243
|
-
assert.equal(status, 200);
|
|
244
|
-
assert.equal(body.path, testPagePath);
|
|
245
|
-
// Verify it was updated
|
|
246
|
-
const page = readPage(testPagePath);
|
|
247
|
-
assert.ok(page?.includes("Updated Content"));
|
|
248
|
-
});
|
|
249
|
-
it("returns 404 if page does not exist", async () => {
|
|
250
|
-
const { status, body } = await req("PUT", port, `/api/wiki/${testPagePath}`, {
|
|
251
|
-
content: "# New Content",
|
|
252
|
-
});
|
|
253
|
-
assert.equal(status, 404);
|
|
254
|
-
assert.equal(body.error, "Page not found");
|
|
255
|
-
});
|
|
256
|
-
it("returns 400 for missing content", async () => {
|
|
257
|
-
writePage(testPagePath, "# Original");
|
|
258
|
-
const { status } = await req("PUT", port, `/api/wiki/${testPagePath}`, {});
|
|
259
|
-
assert.equal(status, 400);
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
describe("DELETE /api/wiki/*path", () => {
|
|
263
|
-
it("deletes an existing wiki page", async () => {
|
|
264
|
-
writePage(testPagePath, "# To Delete");
|
|
265
|
-
const { status } = await req("DELETE", port, `/api/wiki/${testPagePath}`);
|
|
266
|
-
assert.equal(status, 204);
|
|
267
|
-
// Verify it was deleted
|
|
268
|
-
const page = readPage(testPagePath);
|
|
269
|
-
assert.equal(page, undefined);
|
|
270
|
-
});
|
|
271
|
-
it("returns 404 if page does not exist", async () => {
|
|
272
|
-
const { status, body } = await req("DELETE", port, `/api/wiki/${testPagePath}`);
|
|
273
|
-
assert.equal(status, 404);
|
|
274
|
-
assert.equal(body.error, "Page not found");
|
|
275
|
-
});
|
|
276
|
-
it("returns 400 for invalid path", async () => {
|
|
277
|
-
const { status, body } = await req("DELETE", port, "/api/wiki/invalid-path.md");
|
|
278
|
-
assert.equal(status, 400);
|
|
279
|
-
assert.ok(body.error.includes("pages/"));
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
//# sourceMappingURL=wiki.test.js.map
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure session-handling logic extracted from web/src/stores/auth.ts.
|
|
3
|
-
*
|
|
4
|
-
* These functions contain the core behavioral decisions made across four
|
|
5
|
-
* consecutive auth patches (#189→#192→#194→#196) that fixed undocumented
|
|
6
|
-
* Supabase JS v2 side-effects. They are expressed as framework-agnostic
|
|
7
|
-
* functions so they can be unit-tested with the Node test runner without
|
|
8
|
-
* requiring Vue or Pinia.
|
|
9
|
-
*
|
|
10
|
-
* The authoritative implementation lives in web/src/stores/auth.ts.
|
|
11
|
-
* Any change to the auth store's session logic MUST be reflected here and
|
|
12
|
-
* all tests in session-logic.test.ts must continue to pass.
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* Mirrors the onAuthStateChange handler in web/src/stores/auth.ts.
|
|
16
|
-
*
|
|
17
|
-
* Decision: only SIGNED_OUT clears the cached session. All other events that
|
|
18
|
-
* fire with a null session (TOKEN_REFRESHED failure, lock timeout, internal
|
|
19
|
-
* reconciliation) are intentionally ignored to prevent cache poisoning.
|
|
20
|
-
*
|
|
21
|
-
* See squad decision 2026-05-16 03:17:52 and issue #193.
|
|
22
|
-
*/
|
|
23
|
-
export function handleAuthStateChange(cachedSession, event, newSession) {
|
|
24
|
-
if (newSession) {
|
|
25
|
-
// Accept any valid session regardless of event type
|
|
26
|
-
return newSession;
|
|
27
|
-
}
|
|
28
|
-
if (event === "SIGNED_OUT") {
|
|
29
|
-
// Only explicit sign-out clears the cache
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
// All other null-session events: leave the existing cache untouched
|
|
33
|
-
return cachedSession;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Mirrors getAccessToken() in web/src/stores/auth.ts.
|
|
37
|
-
*
|
|
38
|
-
* Decisions:
|
|
39
|
-
* - Reads from cached session — NEVER calls getSession() (Supabase JS v2
|
|
40
|
-
* side-effect: can fire onAuthStateChange(null) during reconciliation).
|
|
41
|
-
* - Proactively refreshes when within 30 seconds of expiry.
|
|
42
|
-
* - Falls back to refreshSession() if cache is null and auth is enabled.
|
|
43
|
-
* - Returns null when auth is disabled.
|
|
44
|
-
*
|
|
45
|
-
* See squad decisions 2026-05-16 02:23:38, 2026-05-16 03:16:46 and issues
|
|
46
|
-
* #191, #193.
|
|
47
|
-
*/
|
|
48
|
-
export async function getAccessToken(cachedSession, authEnabled, refresh, now = Date.now) {
|
|
49
|
-
if (cachedSession?.access_token) {
|
|
50
|
-
// Proactively refresh when within 30 seconds of expiry
|
|
51
|
-
if (cachedSession.expires_at !== undefined &&
|
|
52
|
-
cachedSession.expires_at * 1000 - now() < 30_000) {
|
|
53
|
-
try {
|
|
54
|
-
const fresh = await refresh();
|
|
55
|
-
if (fresh)
|
|
56
|
-
return fresh.access_token;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// fall through to cached token
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return cachedSession.access_token;
|
|
63
|
-
}
|
|
64
|
-
// No cached session — attempt recovery if auth is enabled.
|
|
65
|
-
// Handles the edge case where the cache was spuriously cleared but a
|
|
66
|
-
// valid refresh token still exists in storage.
|
|
67
|
-
if (authEnabled) {
|
|
68
|
-
try {
|
|
69
|
-
const fresh = await refresh();
|
|
70
|
-
if (fresh)
|
|
71
|
-
return fresh.access_token;
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// fall through to null
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
//# sourceMappingURL=session-logic.js.map
|