heyio 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/api/logout.test.js +129 -0
  2. package/dist/api/server.js +118 -2
  3. package/dist/api/wiki.test.js +283 -0
  4. package/dist/copilot/skills.js +72 -0
  5. package/dist/copilot/skills.test.js +55 -0
  6. package/dist/store/db.js +28 -0
  7. package/dist/store/squads.js +33 -1
  8. package/dist/store/squads.test.js +52 -0
  9. package/package.json +1 -1
  10. package/web-dist/assets/{AgentActivityView-D-xGvKsm.js → AgentActivityView-CedxxE6K.js} +1 -1
  11. package/web-dist/assets/{ChatView-DV4zdwDI.js → ChatView-DMkYQo_V.js} +1 -1
  12. package/web-dist/assets/FeedView-BH4q-31V.js +1 -0
  13. package/web-dist/assets/InboxView-BVwVP4EW.js +1 -0
  14. package/web-dist/assets/{LoginView-DNcKxnq-.js → LoginView-DRPDhnwu.js} +1 -1
  15. package/web-dist/assets/{McpView-CbiIag57.js → McpView-D8yWz-lq.js} +1 -1
  16. package/web-dist/assets/{SchedulesView-uxtLtxEL.js → SchedulesView-BzzyncGF.js} +1 -1
  17. package/web-dist/assets/{SettingsTabs.vue_vue_type_script_setup_true_lang-BE0YBCo4.js → SettingsTabs.vue_vue_type_script_setup_true_lang-oW3ySu7Y.js} +1 -1
  18. package/web-dist/assets/SkillsView-oxpYuhx7.js +1 -0
  19. package/web-dist/assets/SquadsView-CaKUIKlq.js +1 -0
  20. package/web-dist/assets/StatusIndicator.vue_vue_type_script_setup_true_lang-8U15Qp_Q.js +1 -0
  21. package/web-dist/assets/WikiView-C5jXUlfW.js +1 -0
  22. package/web-dist/assets/index-BrWzNw-N.css +10 -0
  23. package/web-dist/assets/{index-D1C7prBJ.js → index-f67odrrt.js} +32 -32
  24. package/web-dist/index.html +2 -2
  25. package/web-dist/assets/FeedView-BBbnBU_A.js +0 -1
  26. package/web-dist/assets/InboxView-C_6LL8bG.js +0 -1
  27. package/web-dist/assets/SkillsView-CobJkgd1.js +0 -1
  28. package/web-dist/assets/SquadsView-DpI4RInq.js +0 -1
  29. package/web-dist/assets/StatusIndicator.vue_vue_type_script_setup_true_lang-BwmRY2qH.js +0 -1
  30. package/web-dist/assets/WikiView-DeY1mFGq.js +0 -1
  31. package/web-dist/assets/index-DMGoXFX1.css +0 -10
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Integration tests for Logout API endpoint (#325).
3
+ * Tests POST /api/logout endpoint for sign-out functionality.
4
+ */
5
+ import { describe, it, before, after } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import http from "node:http";
8
+ import express from "express";
9
+ // ── Helpers ───────────────────────────────────────────────────────────────────
10
+ function req(method, port, path, headers, body) {
11
+ return new Promise((resolve, reject) => {
12
+ const payload = body !== undefined ? JSON.stringify(body) : undefined;
13
+ const options = {
14
+ hostname: "127.0.0.1",
15
+ port,
16
+ path,
17
+ method,
18
+ headers: {
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
+ // Mock auth middleware (simulates requireAuth)
46
+ app.use((req, res, next) => {
47
+ const token = req.headers.authorization?.startsWith("Bearer ")
48
+ ? req.headers.authorization.slice(7)
49
+ : undefined;
50
+ // If token is explicitly "invalid", fail auth
51
+ if (token === "invalid") {
52
+ res.status(401).json({ error: "Invalid or expired token" });
53
+ return;
54
+ }
55
+ // Otherwise, pass through (mock valid auth)
56
+ next();
57
+ });
58
+ // Logout endpoint
59
+ app.post("/api/logout", (req, res) => {
60
+ try {
61
+ // Extract token from Authorization header for potential future token revocation
62
+ const authHeader = req.headers.authorization;
63
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
64
+ if (!token) {
65
+ res.status(401).json({ error: "Missing authorization token" });
66
+ return;
67
+ }
68
+ // Token invalidation approach:
69
+ // Supabase JWT tokens are short-lived (1 hour by default). Since we don't maintain
70
+ // a token blacklist, logout on the client side (clearing localStorage) is sufficient.
71
+ // In a production system with token revocation, the token would be added to a blacklist here.
72
+ // For now, we simply confirm the logout and rely on client-side token removal.
73
+ res.json({ status: "logged_out" });
74
+ }
75
+ catch (e) {
76
+ console.error("Error during logout:", e);
77
+ res.status(500).json({ error: "Logout failed" });
78
+ }
79
+ });
80
+ return app;
81
+ }
82
+ // ── Tests ─────────────────────────────────────────────────────────────────────
83
+ describe("Logout API Endpoint", () => {
84
+ let server;
85
+ let port;
86
+ before(() => {
87
+ const app = createTestServer();
88
+ server = app.listen(0);
89
+ port = server.address().port;
90
+ });
91
+ after(() => {
92
+ server.close();
93
+ });
94
+ describe("POST /api/logout", () => {
95
+ it("returns 200 with logged_out status on successful logout", async () => {
96
+ const { status, body } = await req("POST", port, "/api/logout", {
97
+ Authorization: "Bearer valid-token-12345",
98
+ });
99
+ assert.equal(status, 200);
100
+ assert.equal(body.status, "logged_out");
101
+ });
102
+ it("returns 401 when no Authorization header is provided", async () => {
103
+ const { status, body } = await req("POST", port, "/api/logout");
104
+ assert.equal(status, 401);
105
+ assert.ok(body.error.includes("Missing"));
106
+ });
107
+ it("returns 401 when Authorization header is invalid", async () => {
108
+ const { status, body } = await req("POST", port, "/api/logout", {
109
+ Authorization: "Bearer invalid",
110
+ });
111
+ assert.equal(status, 401);
112
+ assert.ok(body.error);
113
+ });
114
+ it("accepts Bearer token format", async () => {
115
+ const { status } = await req("POST", port, "/api/logout", {
116
+ Authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
117
+ });
118
+ assert.equal(status, 200);
119
+ });
120
+ it("returns error for malformed Authorization header", async () => {
121
+ const { status } = await req("POST", port, "/api/logout", {
122
+ Authorization: "NotBearer token",
123
+ });
124
+ // NotBearer does not start with "Bearer " prefix, so validation fails
125
+ assert.equal(status, 401);
126
+ });
127
+ });
128
+ });
129
+ //# sourceMappingURL=logout.test.js.map
@@ -19,7 +19,7 @@ import { listSchedules, getSchedule, deleteSchedule, setScheduleEnabled } from "
19
19
  import { listIoSchedules, getIoSchedule, deleteIoSchedule, setIoScheduleEnabled } from "../store/io-schedules.js";
20
20
  import { getScheduleRuns } from "../store/schedule-runs.js";
21
21
  import { createFeedEntry, listFeedEntries, listFeedSquads, countUnreadFeedEntries, markFeedEntryRead, markAllFeedEntriesRead, deleteFeedEntry, markFeedEntriesRead, deleteFeedEntries } from "../store/feed.js";
22
- import { listPages, readPage } from "../wiki/fs.js";
22
+ import { listPages, readPage, writePage, deletePage, assertPagePath } from "../wiki/fs.js";
23
23
  import { runScheduleNow } from "../copilot/scheduler.js";
24
24
  import { runIoScheduleNow } from "../copilot/io-scheduler.js";
25
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -70,6 +70,28 @@ export async function startApiServer() {
70
70
  });
71
71
  // Apply auth middleware — all routes below require a valid JWT
72
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
+ });
73
95
  // Skills read endpoints
74
96
  api.get("/skills", (_req, res) => {
75
97
  try {
@@ -958,7 +980,7 @@ export async function startApiServer() {
958
980
  });
959
981
  api.get("/wiki/*path", (req, res) => {
960
982
  try {
961
- const pagePath = Array.isArray(req.params.path) ? req.params.path[0] : req.params.path;
983
+ const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
962
984
  if (!pagePath) {
963
985
  res.status(400).json({ error: "Missing page path" });
964
986
  return;
@@ -975,6 +997,100 @@ export async function startApiServer() {
975
997
  res.status(500).json({ error: "Failed to read wiki page" });
976
998
  }
977
999
  });
1000
+ // Create a new wiki page
1001
+ api.post("/wiki", (req, res) => {
1002
+ try {
1003
+ const { path: pagePath, content } = req.body;
1004
+ if (!pagePath || typeof pagePath !== "string") {
1005
+ res.status(400).json({ error: "Missing page path" });
1006
+ return;
1007
+ }
1008
+ if (content === undefined || typeof content !== "string") {
1009
+ res.status(400).json({ error: "Missing page content" });
1010
+ return;
1011
+ }
1012
+ try {
1013
+ assertPagePath(pagePath);
1014
+ }
1015
+ catch (e) {
1016
+ res.status(400).json({ error: e.message });
1017
+ return;
1018
+ }
1019
+ if (readPage(pagePath) !== undefined) {
1020
+ res.status(409).json({ error: "Page already exists" });
1021
+ return;
1022
+ }
1023
+ writePage(pagePath, content);
1024
+ res.status(201).json({ path: pagePath, content });
1025
+ }
1026
+ catch (e) {
1027
+ console.error("Error creating wiki page:", e);
1028
+ res.status(500).json({ error: "Failed to create wiki page" });
1029
+ }
1030
+ });
1031
+ // Update an existing wiki page
1032
+ api.put("/wiki/*path", (req, res) => {
1033
+ try {
1034
+ const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
1035
+ if (!pagePath) {
1036
+ res.status(400).json({ error: "Missing page path" });
1037
+ return;
1038
+ }
1039
+ const { content } = req.body;
1040
+ if (content === undefined || typeof content !== "string") {
1041
+ res.status(400).json({ error: "Missing page content" });
1042
+ return;
1043
+ }
1044
+ try {
1045
+ assertPagePath(pagePath);
1046
+ }
1047
+ catch (e) {
1048
+ res.status(400).json({ error: e.message });
1049
+ return;
1050
+ }
1051
+ if (readPage(pagePath) === undefined) {
1052
+ res.status(404).json({ error: "Page not found" });
1053
+ return;
1054
+ }
1055
+ writePage(pagePath, content);
1056
+ res.json({ path: pagePath, content });
1057
+ }
1058
+ catch (e) {
1059
+ console.error("Error updating wiki page:", e);
1060
+ res.status(500).json({ error: "Failed to update wiki page" });
1061
+ }
1062
+ });
1063
+ // Delete a wiki page
1064
+ api.delete("/wiki/*path", (req, res) => {
1065
+ try {
1066
+ const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
1067
+ if (!pagePath) {
1068
+ res.status(400).json({ error: "Missing page path" });
1069
+ return;
1070
+ }
1071
+ try {
1072
+ assertPagePath(pagePath);
1073
+ }
1074
+ catch (e) {
1075
+ res.status(400).json({ error: e.message });
1076
+ return;
1077
+ }
1078
+ const deleted = deletePage(pagePath);
1079
+ if (!deleted) {
1080
+ res.status(404).json({ error: "Page not found" });
1081
+ return;
1082
+ }
1083
+ res.status(204).send();
1084
+ }
1085
+ catch (e) {
1086
+ console.error("Error deleting wiki page:", e);
1087
+ res.status(500).json({ error: "Failed to delete wiki page" });
1088
+ }
1089
+ });
1090
+ // Get available wiki categories
1091
+ api.get("/wiki-categories", (_req, res) => {
1092
+ res.json({ categories: ["preferences", "projects", "people", "general", "squads"] });
1093
+ });
978
1094
  // Mount API at /api (for frontend)
979
1095
  app.use("/api", api);
980
1096
  // Serve Vue frontend if built assets exist (before backward-compat API mount)
@@ -0,0 +1,283 @@
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
@@ -310,4 +310,76 @@ export async function searchSkillsRegistry(query) {
310
310
  return [];
311
311
  }
312
312
  }
313
+ /**
314
+ * Update a skill's metadata (name and/or description).
315
+ * Rewrites the SKILL.md file with the updated frontmatter while preserving content.
316
+ * Throws if the skill doesn't exist.
317
+ */
318
+ export function updateSkill(slug, updates) {
319
+ const skillDir = join(SKILLS_DIR, slug);
320
+ if (!existsSync(skillDir)) {
321
+ throw new Error(`Skill not found: ${slug}`);
322
+ }
323
+ const skillMdPath = join(skillDir, "SKILL.md");
324
+ if (!existsSync(skillMdPath)) {
325
+ throw new Error(`SKILL.md not found for skill: ${slug}`);
326
+ }
327
+ const currentContent = readFileSync(skillMdPath, "utf-8");
328
+ const { name: currentName, description: currentDescription } = parseSkillMd(currentContent);
329
+ // Use provided updates or fall back to current values
330
+ const newName = updates.name ?? currentName;
331
+ const newDescription = updates.description ?? currentDescription;
332
+ // Rebuild SKILL.md with new metadata
333
+ // Keep everything after the description intact (preserve any extra content)
334
+ const lines = currentContent.split(/\r?\n/);
335
+ const newLines = [];
336
+ // Add the new heading
337
+ newLines.push(`# ${newName}`);
338
+ newLines.push("");
339
+ // Add the new description
340
+ if (newDescription) {
341
+ newLines.push(newDescription);
342
+ newLines.push("");
343
+ }
344
+ // Find where the original description ends and append the rest
345
+ let foundHeading = false;
346
+ let skippedDescription = false;
347
+ let blankLinesSeen = 0;
348
+ for (const line of lines) {
349
+ if (!foundHeading) {
350
+ if (line.match(/^#\s+(.+)/)) {
351
+ foundHeading = true;
352
+ }
353
+ continue;
354
+ }
355
+ if (!skippedDescription) {
356
+ if (line.trim() === "") {
357
+ blankLinesSeen++;
358
+ if (blankLinesSeen >= 2) {
359
+ skippedDescription = true;
360
+ }
361
+ }
362
+ else {
363
+ blankLinesSeen = 0;
364
+ }
365
+ continue;
366
+ }
367
+ // We're past the description now
368
+ newLines.push(line);
369
+ }
370
+ const newContent = newLines.join("\n");
371
+ writeFileSync(skillMdPath, newContent, "utf-8");
372
+ return {
373
+ name: newName,
374
+ slug,
375
+ description: newDescription,
376
+ path: skillDir,
377
+ };
378
+ }
379
+ /**
380
+ * Delete a skill (alias for removeSkill for consistency with other store modules).
381
+ */
382
+ export function deleteSkill(slug) {
383
+ return removeSkill(slug);
384
+ }
313
385
  //# sourceMappingURL=skills.js.map