heyio 0.39.1 → 0.41.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/dist/api/server.js +96 -2
- package/dist/api/wiki.test.js +283 -0
- package/dist/copilot/agents.js +4 -5
- package/dist/copilot/model-router.test.js +71 -0
- package/dist/copilot/skills.js +72 -0
- package/dist/copilot/skills.test.js +55 -0
- package/dist/copilot/tools.js +2 -2
- package/dist/store/db.js +28 -0
- package/dist/store/squads.js +33 -1
- package/dist/store/squads.test.js +52 -0
- package/package.json +1 -1
- package/web-dist/assets/{AgentActivityView-BR91djhV.js → AgentActivityView-Dxppa-7j.js} +1 -1
- package/web-dist/assets/{ChatView-BEHq7ipI.js → ChatView-CFkHd2XI.js} +1 -1
- package/web-dist/assets/FeedView-B6ehFAhf.js +1 -0
- package/web-dist/assets/{InboxView-DYqFsN1q.js → InboxView-Btmn836W.js} +1 -1
- package/web-dist/assets/{LoginView-rHOPTQtH.js → LoginView-CiBwBd04.js} +1 -1
- package/web-dist/assets/{McpView-CbbqH6O6.js → McpView-BhOHMFKD.js} +1 -1
- package/web-dist/assets/{SchedulesView-B5U9R5xb.js → SchedulesView-COk9mHap.js} +1 -1
- package/web-dist/assets/{SettingsTabs.vue_vue_type_script_setup_true_lang-B7GKSgO5.js → SettingsTabs.vue_vue_type_script_setup_true_lang-CVs3VGNq.js} +1 -1
- package/web-dist/assets/SkillsView-32U-60Dv.js +1 -0
- package/web-dist/assets/SquadsView-ByOGRKXx.js +1 -0
- package/web-dist/assets/StatusIndicator.vue_vue_type_script_setup_true_lang-B7XkblIY.js +1 -0
- package/web-dist/assets/WikiView-CqiPSoBp.js +1 -0
- package/web-dist/assets/{index-B3ZOJqJ1.js → index-CQJxzKQb.js} +24 -24
- package/web-dist/assets/index-DC4EVTvc.css +10 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/FeedView-Cr-wMZ0K.js +0 -1
- package/web-dist/assets/SkillsView-DjxwYTH3.js +0 -1
- package/web-dist/assets/SquadsView-D2pIRWTM.js +0 -1
- package/web-dist/assets/StatusIndicator.vue_vue_type_script_setup_true_lang-DaZufA4t.js +0 -1
- package/web-dist/assets/WikiView-DK_0MFE6.js +0 -1
- package/web-dist/assets/index-DMGoXFX1.css +0 -10
package/dist/api/server.js
CHANGED
|
@@ -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));
|
|
@@ -958,7 +958,7 @@ export async function startApiServer() {
|
|
|
958
958
|
});
|
|
959
959
|
api.get("/wiki/*path", (req, res) => {
|
|
960
960
|
try {
|
|
961
|
-
const pagePath = Array.isArray(req.params.path) ? req.params.path
|
|
961
|
+
const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
|
|
962
962
|
if (!pagePath) {
|
|
963
963
|
res.status(400).json({ error: "Missing page path" });
|
|
964
964
|
return;
|
|
@@ -975,6 +975,100 @@ export async function startApiServer() {
|
|
|
975
975
|
res.status(500).json({ error: "Failed to read wiki page" });
|
|
976
976
|
}
|
|
977
977
|
});
|
|
978
|
+
// Create a new wiki page
|
|
979
|
+
api.post("/wiki", (req, res) => {
|
|
980
|
+
try {
|
|
981
|
+
const { path: pagePath, content } = req.body;
|
|
982
|
+
if (!pagePath || typeof pagePath !== "string") {
|
|
983
|
+
res.status(400).json({ error: "Missing page path" });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (content === undefined || typeof content !== "string") {
|
|
987
|
+
res.status(400).json({ error: "Missing page content" });
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
try {
|
|
991
|
+
assertPagePath(pagePath);
|
|
992
|
+
}
|
|
993
|
+
catch (e) {
|
|
994
|
+
res.status(400).json({ error: e.message });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (readPage(pagePath) !== undefined) {
|
|
998
|
+
res.status(409).json({ error: "Page already exists" });
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
writePage(pagePath, content);
|
|
1002
|
+
res.status(201).json({ path: pagePath, content });
|
|
1003
|
+
}
|
|
1004
|
+
catch (e) {
|
|
1005
|
+
console.error("Error creating wiki page:", e);
|
|
1006
|
+
res.status(500).json({ error: "Failed to create wiki page" });
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
// Update an existing wiki page
|
|
1010
|
+
api.put("/wiki/*path", (req, res) => {
|
|
1011
|
+
try {
|
|
1012
|
+
const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
|
|
1013
|
+
if (!pagePath) {
|
|
1014
|
+
res.status(400).json({ error: "Missing page path" });
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const { content } = req.body;
|
|
1018
|
+
if (content === undefined || typeof content !== "string") {
|
|
1019
|
+
res.status(400).json({ error: "Missing page content" });
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
try {
|
|
1023
|
+
assertPagePath(pagePath);
|
|
1024
|
+
}
|
|
1025
|
+
catch (e) {
|
|
1026
|
+
res.status(400).json({ error: e.message });
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (readPage(pagePath) === undefined) {
|
|
1030
|
+
res.status(404).json({ error: "Page not found" });
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
writePage(pagePath, content);
|
|
1034
|
+
res.json({ path: pagePath, content });
|
|
1035
|
+
}
|
|
1036
|
+
catch (e) {
|
|
1037
|
+
console.error("Error updating wiki page:", e);
|
|
1038
|
+
res.status(500).json({ error: "Failed to update wiki page" });
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
// Delete a wiki page
|
|
1042
|
+
api.delete("/wiki/*path", (req, res) => {
|
|
1043
|
+
try {
|
|
1044
|
+
const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
|
|
1045
|
+
if (!pagePath) {
|
|
1046
|
+
res.status(400).json({ error: "Missing page path" });
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
assertPagePath(pagePath);
|
|
1051
|
+
}
|
|
1052
|
+
catch (e) {
|
|
1053
|
+
res.status(400).json({ error: e.message });
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const deleted = deletePage(pagePath);
|
|
1057
|
+
if (!deleted) {
|
|
1058
|
+
res.status(404).json({ error: "Page not found" });
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
res.status(204).send();
|
|
1062
|
+
}
|
|
1063
|
+
catch (e) {
|
|
1064
|
+
console.error("Error deleting wiki page:", e);
|
|
1065
|
+
res.status(500).json({ error: "Failed to delete wiki page" });
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
// Get available wiki categories
|
|
1069
|
+
api.get("/wiki-categories", (_req, res) => {
|
|
1070
|
+
res.json({ categories: ["preferences", "projects", "people", "general", "squads"] });
|
|
1071
|
+
});
|
|
978
1072
|
// Mount API at /api (for frontend)
|
|
979
1073
|
app.use("/api", api);
|
|
980
1074
|
// 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
|
package/dist/copilot/agents.js
CHANGED
|
@@ -376,11 +376,10 @@ export function getActiveAgentTasks() {
|
|
|
376
376
|
*/
|
|
377
377
|
async function getOrCreateAgentSession(squadSlug, agent, taskDescription) {
|
|
378
378
|
const key = agentSessionKey(squadSlug, agent.character_name);
|
|
379
|
-
// Determine model
|
|
379
|
+
// Determine model: task complexity is sole determinant when task context exists;
|
|
380
|
+
// stored model_tier is only a fallback for ad-hoc sessions without task context.
|
|
380
381
|
const agentTier = agent.model_tier;
|
|
381
|
-
const
|
|
382
|
-
const tierRank = { high: 3, medium: 2, low: 1 };
|
|
383
|
-
const effectiveTier = tierRank[taskTier] >= tierRank[agentTier] ? taskTier : agentTier;
|
|
382
|
+
const effectiveTier = taskDescription ? classifyComplexity(taskDescription) : agentTier;
|
|
384
383
|
const model = getModelForTier(effectiveTier);
|
|
385
384
|
// If we have a cached session, check if the model matches AND the agent
|
|
386
385
|
// hasn't been left in an error state by a previous task. If either is off,
|
|
@@ -422,7 +421,7 @@ async function getOrCreateAgentSession(squadSlug, agent, taskDescription) {
|
|
|
422
421
|
const wikiSection = wikiPages.length > 0
|
|
423
422
|
? `\n\n## Squad Wiki\n${wikiPages.map(p => `### ${p.path}\n${p.content}`).join("\n\n")}`
|
|
424
423
|
: "";
|
|
425
|
-
console.error(`[io] Agent ${agent.character_name}: using model "${model}" (
|
|
424
|
+
console.error(`[io] Agent ${agent.character_name}: using model "${model}" (stored tier: ${agentTier}, effective: ${effectiveTier})`);
|
|
426
425
|
const universeName = squad.universe
|
|
427
426
|
? getUniverse(squad.universe)?.name ?? squad.universe
|
|
428
427
|
: "Unknown";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { classifyComplexity } from "./model-router.js";
|
|
4
|
+
describe("classifyComplexity", () => {
|
|
5
|
+
describe("high complexity", () => {
|
|
6
|
+
it("returns high when multiple high keywords are present", () => {
|
|
7
|
+
assert.equal(classifyComplexity("Refactor and redesign the auth module"), "high");
|
|
8
|
+
});
|
|
9
|
+
it("returns high with architect + security keywords", () => {
|
|
10
|
+
assert.equal(classifyComplexity("Architect a security overhaul"), "high");
|
|
11
|
+
});
|
|
12
|
+
it("returns high with debug + investigate", () => {
|
|
13
|
+
assert.equal(classifyComplexity("Investigate and debug the memory leak"), "high");
|
|
14
|
+
});
|
|
15
|
+
it("returns high for performance + optimize", () => {
|
|
16
|
+
assert.equal(classifyComplexity("Optimize performance of the query engine"), "high");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe("low complexity", () => {
|
|
20
|
+
it("returns low when multiple low keywords are present", () => {
|
|
21
|
+
assert.equal(classifyComplexity("Read the file and check status"), "low");
|
|
22
|
+
});
|
|
23
|
+
it("returns low for simple rename task", () => {
|
|
24
|
+
assert.equal(classifyComplexity("Simple rename of the variable"), "low");
|
|
25
|
+
});
|
|
26
|
+
it("returns low for list + format", () => {
|
|
27
|
+
assert.equal(classifyComplexity("List all entries and format them"), "low");
|
|
28
|
+
});
|
|
29
|
+
it("returns low for delete file + remove file", () => {
|
|
30
|
+
assert.equal(classifyComplexity("Delete file foo.txt and remove file bar.txt"), "low");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe("medium complexity", () => {
|
|
34
|
+
it("returns medium for ambiguous description with no keywords", () => {
|
|
35
|
+
assert.equal(classifyComplexity("Update the user profile page"), "medium");
|
|
36
|
+
});
|
|
37
|
+
it("returns medium for empty string", () => {
|
|
38
|
+
assert.equal(classifyComplexity(""), "medium");
|
|
39
|
+
});
|
|
40
|
+
it("returns medium when one high and one low keyword tie", () => {
|
|
41
|
+
// highScore=1, lowScore=1 — both weak, neither wins, falls to default
|
|
42
|
+
assert.equal(classifyComplexity("Debug and check the output"), "medium");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("weak signal tiebreaker", () => {
|
|
46
|
+
it("returns high when single high keyword beats zero low keywords", () => {
|
|
47
|
+
assert.equal(classifyComplexity("Refactor the utils module"), "high");
|
|
48
|
+
});
|
|
49
|
+
it("returns low when single low keyword beats zero high keywords", () => {
|
|
50
|
+
assert.equal(classifyComplexity("Check the build output"), "low");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("case insensitivity", () => {
|
|
54
|
+
it("matches keywords regardless of case", () => {
|
|
55
|
+
assert.equal(classifyComplexity("REFACTOR and REDESIGN everything"), "high");
|
|
56
|
+
});
|
|
57
|
+
it("matches low keywords in mixed case", () => {
|
|
58
|
+
assert.equal(classifyComplexity("FORMAT the List of entries"), "low");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe("multi-word keywords", () => {
|
|
62
|
+
it("matches 'delete file' as a single keyword", () => {
|
|
63
|
+
assert.equal(classifyComplexity("Please delete file and copy file"), "low");
|
|
64
|
+
});
|
|
65
|
+
it("does not match partial multi-word keywords", () => {
|
|
66
|
+
// "delete" alone is not a keyword, only "delete file" is
|
|
67
|
+
assert.equal(classifyComplexity("Delete the branch"), "medium");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=model-router.test.js.map
|
package/dist/copilot/skills.js
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for src/copilot/skills.ts — skill installation, listing, and management.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { parseSkillUrl, removeSkill, updateSkill, deleteSkill, } from "./skills.js";
|
|
7
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
8
|
+
describe("parseSkillUrl", () => {
|
|
9
|
+
it("parses GitHub blob URLs for SKILL.md", () => {
|
|
10
|
+
const url = "https://github.com/owner/repo/blob/main/SKILL.md";
|
|
11
|
+
const result = parseSkillUrl(url);
|
|
12
|
+
assert.equal(result.type, "file");
|
|
13
|
+
assert.ok("rawUrl" in result);
|
|
14
|
+
assert.ok(result.rawUrl.includes("raw.githubusercontent.com"));
|
|
15
|
+
});
|
|
16
|
+
it("parses GitHub raw content URLs", () => {
|
|
17
|
+
const url = "https://raw.githubusercontent.com/owner/repo/main/SKILL.md";
|
|
18
|
+
const result = parseSkillUrl(url);
|
|
19
|
+
assert.equal(result.type, "file");
|
|
20
|
+
assert.ok("rawUrl" in result);
|
|
21
|
+
});
|
|
22
|
+
it("parses GitHub repo URLs as repo type", () => {
|
|
23
|
+
const url = "https://github.com/owner/repo";
|
|
24
|
+
const result = parseSkillUrl(url);
|
|
25
|
+
assert.equal(result.type, "repo");
|
|
26
|
+
assert.equal(result.url, url);
|
|
27
|
+
});
|
|
28
|
+
it("rejects non-https SKILL.md URLs", () => {
|
|
29
|
+
const url = "http://example.com/SKILL.md";
|
|
30
|
+
assert.throws(() => parseSkillUrl(url));
|
|
31
|
+
});
|
|
32
|
+
it("derives slug from repo name with path prefix", () => {
|
|
33
|
+
const url = "https://github.com/owner/repo/blob/main/skills/ai-chat/SKILL.md";
|
|
34
|
+
const result = parseSkillUrl(url);
|
|
35
|
+
assert.equal(result.type, "file");
|
|
36
|
+
assert.ok(result.slug.includes("repo"));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("skill store functions", () => {
|
|
40
|
+
it("exports updateSkill function", () => {
|
|
41
|
+
assert.ok(typeof updateSkill === "function");
|
|
42
|
+
});
|
|
43
|
+
it("exports deleteSkill function", () => {
|
|
44
|
+
assert.ok(typeof deleteSkill === "function");
|
|
45
|
+
});
|
|
46
|
+
it("exports removeSkill function", () => {
|
|
47
|
+
assert.ok(typeof removeSkill === "function");
|
|
48
|
+
});
|
|
49
|
+
it("deleteSkill is callable (alias)", () => {
|
|
50
|
+
// Just verify these are functions and importable
|
|
51
|
+
assert.ok(deleteSkill !== undefined);
|
|
52
|
+
assert.ok(removeSkill !== undefined);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=skills.test.js.map
|
package/dist/copilot/tools.js
CHANGED
|
@@ -651,7 +651,7 @@ export function createTools(deps) {
|
|
|
651
651
|
console.error(`[io] squad_add_agent called: ${slug} — ${role_title}`);
|
|
652
652
|
try {
|
|
653
653
|
const agent = deps.addSquadAgent(slug, role_title, charter, model_tier);
|
|
654
|
-
return `Agent added to squad "${slug}":\n- **${agent.character_name}** — ${agent.role_title}\n- Personality: ${agent.personality}\n- Model
|
|
654
|
+
return `Agent added to squad "${slug}":\n- **${agent.character_name}** — ${agent.role_title}\n- Personality: ${agent.personality}\n- Model: dynamic (task-based)`;
|
|
655
655
|
}
|
|
656
656
|
catch (err) {
|
|
657
657
|
return `Error adding agent: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -699,7 +699,7 @@ export function createTools(deps) {
|
|
|
699
699
|
const statsStr = st.task_count === 0
|
|
700
700
|
? " — 📊 never delegated"
|
|
701
701
|
: ` — 📊 ${st.task_count} ${st.task_count === 1 ? "task" : "tasks"} · last ${formatRelativeTime(st.last_delegated_at)}`;
|
|
702
|
-
return `- **${a.character_name}**${leadBadge}${qaBadge} — ${a.role_title} (
|
|
702
|
+
return `- **${a.character_name}**${leadBadge}${qaBadge} — ${a.role_title} (dynamic) — ${a.status}${statsStr}${a.personality ? `\n _${a.personality}_` : ""}`;
|
|
703
703
|
});
|
|
704
704
|
const coverage = assessSquadCoverage(agents);
|
|
705
705
|
const coverageBlock = coverage.warning ? `\n\n${coverage.warning}` : "";
|