@kevin5251984/guild 0.2.12

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/bin/guildd.mjs +20 -0
  3. package/cordis.yml +24 -0
  4. package/package.json +52 -0
  5. package/src/agent-file.ts +125 -0
  6. package/src/browser.ts +668 -0
  7. package/src/catalog/default-bots.ts +263 -0
  8. package/src/catalog/skills.ts +128 -0
  9. package/src/catalog/subagents.ts +70 -0
  10. package/src/chat-parts.ts +71 -0
  11. package/src/cli-args.ts +75 -0
  12. package/src/cli.ts +60 -0
  13. package/src/compact.ts +355 -0
  14. package/src/cordis.d.ts +40 -0
  15. package/src/db.ts +653 -0
  16. package/src/generate.ts +673 -0
  17. package/src/handlers.ts +1623 -0
  18. package/src/harness.ts +326 -0
  19. package/src/host-agents.ts +137 -0
  20. package/src/host-browse.ts +199 -0
  21. package/src/host-skills.ts +150 -0
  22. package/src/image-gen.ts +270 -0
  23. package/src/index.ts +12 -0
  24. package/src/llm.ts +993 -0
  25. package/src/mcp.ts +563 -0
  26. package/src/memory.ts +159 -0
  27. package/src/mention.ts +176 -0
  28. package/src/oauth.ts +1474 -0
  29. package/src/plugins/api.ts +8 -0
  30. package/src/plugins/chat.ts +31 -0
  31. package/src/plugins/harness.ts +77 -0
  32. package/src/plugins/llm.ts +50 -0
  33. package/src/plugins/mcp.ts +58 -0
  34. package/src/plugins/memory.ts +42 -0
  35. package/src/plugins/oauth.ts +47 -0
  36. package/src/plugins/server.ts +126 -0
  37. package/src/plugins/store.ts +29 -0
  38. package/src/plugins/tools.ts +79 -0
  39. package/src/public/buddy.js +432 -0
  40. package/src/public/chat.css +3045 -0
  41. package/src/public/chat.html +5834 -0
  42. package/src/public/favicon-16.png +0 -0
  43. package/src/public/favicon-16.svg +10 -0
  44. package/src/public/favicon-32.png +0 -0
  45. package/src/public/favicon.ico +0 -0
  46. package/src/public/favicon.svg +13 -0
  47. package/src/public/i18n.js +663 -0
  48. package/src/public/index.html +143 -0
  49. package/src/public/library.html +678 -0
  50. package/src/public/mcp-add.html +126 -0
  51. package/src/public/md.js +332 -0
  52. package/src/public/rpg/inn-street.jpg +0 -0
  53. package/src/public/settings.html +795 -0
  54. package/src/public/skills-add.html +212 -0
  55. package/src/public/studio.html +1181 -0
  56. package/src/public/style.css +1678 -0
  57. package/src/public/subagents-add.html +152 -0
  58. package/src/router.ts +978 -0
  59. package/src/send-budget.ts +52 -0
  60. package/src/server.ts +1 -0
  61. package/src/skill-import.ts +250 -0
  62. package/src/slash.ts +15 -0
  63. package/src/start.ts +103 -0
  64. package/src/store.ts +1208 -0
  65. package/src/subagent.ts +355 -0
  66. package/src/tools.ts +818 -0
  67. package/src/trajectory.ts +339 -0
  68. package/src/usage.ts +111 -0
  69. package/vendor/protocol/package.json +19 -0
  70. package/vendor/protocol/src/index.ts +159 -0
package/src/router.ts ADDED
@@ -0,0 +1,978 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import type { IncomingMessage, ServerResponse } from "node:http";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { LibraryKind, ModelRef, ModelsFile } from "@guild/protocol";
5
+ import {
6
+ addChannelMember,
7
+ createBot,
8
+ createChannel,
9
+ deleteBot,
10
+ deleteChannel,
11
+ renameChannel,
12
+ createLibraryItem,
13
+ getChannelMd,
14
+ setChannelMd,
15
+ getBotMemory,
16
+ setBotMemory,
17
+ getChannelMemory,
18
+ setChannelMemory,
19
+ generateKind,
20
+ pickBotSkills,
21
+ generateBotLook,
22
+ getBotDetail,
23
+ getLiveTurn,
24
+ abortLiveTurn,
25
+ healthPayload,
26
+ importSkills,
27
+ mergeModelsFile,
28
+ publicModels,
29
+ listBench,
30
+ listLibrary,
31
+ listMcpServers,
32
+ listHostMcpServers,
33
+ createMcpServer,
34
+ importMcpServer,
35
+ deleteMcpServer,
36
+ deleteRoomMessage,
37
+ listRoomMessages,
38
+ listRoomTrajectory,
39
+ openDm,
40
+ parseAttachments,
41
+ postUserMessage,
42
+ removeChannelMember,
43
+ retryMessage,
44
+ steerUserMessage,
45
+ StoreError,
46
+ updateBot,
47
+ workspace,
48
+ type HandlerExtras,
49
+ } from "./handlers.ts";
50
+ import type { GuildStore } from "./store.ts";
51
+ import {
52
+ completeLogin,
53
+ listSubscriptions,
54
+ refreshCopilotCatalog,
55
+ logoutOAuth,
56
+ pollLogin,
57
+ startLogin,
58
+ } from "./oauth.ts";
59
+ import { hostGit, hostList, hostRead, hostTree } from "./host-browse.ts";
60
+ import { listHostSkills } from "./host-skills.ts";
61
+ import { listHostAgents } from "./host-agents.ts";
62
+ import { generatedDir, isSafeGeneratedName } from "./image-gen.ts";
63
+
64
+ const PUBLIC = fileURLToPath(new URL("./public/", import.meta.url));
65
+
66
+ const PAGES: Record<string, { file: string; type: string }> = {
67
+ "/": { file: "chat.html", type: "text/html; charset=utf-8" },
68
+ "/index.html": { file: "chat.html", type: "text/html; charset=utf-8" },
69
+ "/library": { file: "library.html", type: "text/html; charset=utf-8" },
70
+ "/subagents": { file: "library.html", type: "text/html; charset=utf-8" },
71
+ "/subagents/add": { file: "subagents-add.html", type: "text/html; charset=utf-8" },
72
+ "/mcp": { file: "library.html", type: "text/html; charset=utf-8" },
73
+ "/mcp/add": { file: "mcp-add.html", type: "text/html; charset=utf-8" },
74
+ "/studio": { file: "studio.html", type: "text/html; charset=utf-8" },
75
+ "/skills/add": { file: "skills-add.html", type: "text/html; charset=utf-8" },
76
+ "/chat": { file: "chat.html", type: "text/html; charset=utf-8" },
77
+ "/style.css": { file: "style.css", type: "text/css; charset=utf-8" },
78
+ "/chat.css": { file: "chat.css", type: "text/css; charset=utf-8" },
79
+ "/md.js": { file: "md.js", type: "text/javascript; charset=utf-8" },
80
+ "/i18n.js": { file: "i18n.js", type: "text/javascript; charset=utf-8" },
81
+ "/buddy.js": { file: "buddy.js", type: "text/javascript; charset=utf-8" },
82
+ "/favicon.ico": { file: "favicon.ico", type: "image/x-icon" },
83
+ "/favicon.svg": { file: "favicon.svg", type: "image/svg+xml" },
84
+ "/favicon-16.svg": { file: "favicon-16.svg", type: "image/svg+xml" },
85
+ "/favicon-32.png": { file: "favicon-32.png", type: "image/png" },
86
+ "/favicon-16.png": { file: "favicon-16.png", type: "image/png" },
87
+ "/settings": { file: "settings.html", type: "text/html; charset=utf-8" },
88
+ "/settings/subs": { file: "settings.html", type: "text/html; charset=utf-8" },
89
+ "/settings/keys": { file: "settings.html", type: "text/html; charset=utf-8" },
90
+ };
91
+
92
+ const LIBRARY_KINDS = new Set<LibraryKind>([
93
+ "souls",
94
+ "agents",
95
+ "skills",
96
+ "positions",
97
+ "subagents",
98
+ ]);
99
+
100
+ const CORS_HEADERS = {
101
+ "access-control-allow-origin": "*",
102
+ "access-control-allow-methods": "GET, POST, PATCH, PUT, DELETE, OPTIONS",
103
+ "access-control-allow-headers": "content-type, authorization",
104
+ } as const;
105
+
106
+ function send(
107
+ res: ServerResponse,
108
+ status: number,
109
+ type: string,
110
+ body: string | Buffer,
111
+ extra: Record<string, string> = {},
112
+ ): void {
113
+ res.writeHead(status, {
114
+ "content-type": type,
115
+ "content-length": Buffer.byteLength(body),
116
+ "cache-control": "no-store",
117
+ ...CORS_HEADERS,
118
+ ...extra,
119
+ });
120
+ res.end(body);
121
+ }
122
+
123
+ function json(res: ServerResponse, status: number, body: unknown): void {
124
+ send(res, status, "application/json; charset=utf-8", JSON.stringify(body));
125
+ }
126
+
127
+ function requestUrl(req: IncomingMessage): URL {
128
+ return new URL(req.url ?? "/", "http://127.0.0.1");
129
+ }
130
+
131
+ function pathname(req: IncomingMessage): string {
132
+ return requestUrl(req).pathname.replace(/\/+$/, "") || "/";
133
+ }
134
+
135
+ async function readJson(req: IncomingMessage): Promise<unknown> {
136
+ const chunks: Buffer[] = [];
137
+ let size = 0;
138
+ for await (const chunk of req) {
139
+ size += chunk.length;
140
+ if (size > 1_000_000) {
141
+ throw new StoreError(413, "body too large");
142
+ }
143
+ chunks.push(chunk as Buffer);
144
+ }
145
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
146
+ if (!raw) return {};
147
+ try {
148
+ return JSON.parse(raw) as unknown;
149
+ } catch {
150
+ throw new StoreError(400, "invalid JSON");
151
+ }
152
+ }
153
+
154
+ function asRecord(value: unknown): Record<string, unknown> {
155
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
156
+ return value as Record<string, unknown>;
157
+ }
158
+ throw new StoreError(400, "JSON object required");
159
+ }
160
+
161
+ function str(record: Record<string, unknown>, key: string): string {
162
+ const value = record[key];
163
+ if (typeof value !== "string") return "";
164
+ return value;
165
+ }
166
+
167
+ function modelRefFrom(value: unknown): ModelRef | null {
168
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
169
+ const rec = value as Record<string, unknown>;
170
+ const provider = str(rec, "provider").trim();
171
+ const model = str(rec, "model").trim();
172
+ if (!provider || !model) return null;
173
+ return { provider, model };
174
+ }
175
+
176
+ function strList(record: Record<string, unknown>, key: string): string[] {
177
+ const value = record[key];
178
+ if (Array.isArray(value)) {
179
+ return value.filter((item): item is string => typeof item === "string");
180
+ }
181
+ if (typeof value === "string" && value) return [value];
182
+ return [];
183
+ }
184
+
185
+ function skillPickCatalog(
186
+ record: Record<string, unknown>,
187
+ ): { id: string; name: string; description?: string; tags?: string[]; slug?: string }[] {
188
+ const raw = record.skills;
189
+ if (!Array.isArray(raw)) return [];
190
+ const out: {
191
+ id: string;
192
+ name: string;
193
+ description?: string;
194
+ tags?: string[];
195
+ slug?: string;
196
+ }[] = [];
197
+ for (const item of raw) {
198
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
199
+ const rec = item as Record<string, unknown>;
200
+ const id = str(rec, "id").trim();
201
+ const name = str(rec, "name").trim();
202
+ if (!id || !name) continue;
203
+ out.push({
204
+ id,
205
+ name,
206
+ description: str(rec, "description"),
207
+ slug: str(rec, "slug"),
208
+ tags: strList(rec, "tags"),
209
+ });
210
+ }
211
+ return out;
212
+ }
213
+
214
+ function draft(
215
+ record: Record<string, unknown>,
216
+ key: string,
217
+ ): { name: string; body: string } | undefined {
218
+ const value = record[key];
219
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
220
+ return undefined;
221
+ }
222
+ const nested = value as Record<string, unknown>;
223
+ const name = str(nested, "name");
224
+ const body = str(nested, "body");
225
+ if (!body.trim()) return undefined;
226
+ return { name, body };
227
+ }
228
+
229
+ function libraryKindFromPath(path: string): LibraryKind | null {
230
+ const match = /^\/library\/(souls|agents|skills|positions|subagents)$/.exec(path);
231
+ if (!match) return null;
232
+ const kind = match[1];
233
+ if (!LIBRARY_KINDS.has(kind as LibraryKind)) return null;
234
+ return kind as LibraryKind;
235
+ }
236
+
237
+ /** Shipped request handler used by the daemon process and tests. */
238
+ export async function handleRequest(
239
+ req: IncomingMessage,
240
+ res: ServerResponse,
241
+ store: GuildStore,
242
+ env: NodeJS.ProcessEnv = process.env,
243
+ extras: HandlerExtras = {},
244
+ ): Promise<void> {
245
+ const method = req.method ?? "GET";
246
+ const path = pathname(req);
247
+
248
+ try {
249
+ if (method === "OPTIONS") {
250
+ res.writeHead(204, CORS_HEADERS);
251
+ res.end();
252
+ return;
253
+ }
254
+
255
+ const page = PAGES[path];
256
+ if (method === "GET" && /^\/edit\/[^/]+$/.test(path)) {
257
+ const html = readFileSync(`${PUBLIC}studio.html`);
258
+ send(res, 200, "text/html; charset=utf-8", html);
259
+ return;
260
+ }
261
+
262
+ if (method === "GET" && page) {
263
+ const body = readFileSync(`${PUBLIC}${page.file}`);
264
+ const extra = page.type.startsWith("image/")
265
+ ? { "cache-control": "public, max-age=86400" }
266
+ : {};
267
+ send(res, 200, page.type, body, extra);
268
+ return;
269
+ }
270
+
271
+ if (method === "GET" && path.startsWith("/generated/")) {
272
+ const name = decodeURIComponent(path.slice("/generated/".length));
273
+ if (!isSafeGeneratedName(name)) {
274
+ json(res, 404, { error: "not_found", path });
275
+ return;
276
+ }
277
+ const file = `${generatedDir(store.dataDir)}/${name}`;
278
+ if (!existsSync(file)) {
279
+ json(res, 404, { error: "not_found", path });
280
+ return;
281
+ }
282
+ const type = name.endsWith(".png")
283
+ ? "image/png"
284
+ : name.endsWith(".webp")
285
+ ? "image/webp"
286
+ : name.endsWith(".gif")
287
+ ? "image/gif"
288
+ : "image/jpeg";
289
+ const bytes = readFileSync(file);
290
+ res.writeHead(200, {
291
+ "content-type": type,
292
+ "content-length": bytes.length,
293
+ "cache-control": "private, max-age=86400",
294
+ ...CORS_HEADERS,
295
+ });
296
+ res.end(bytes);
297
+ return;
298
+ }
299
+
300
+ if (method === "GET" && path.startsWith("/rpg/")) {
301
+ const name = path.slice(5);
302
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) {
303
+ json(res, 404, { error: "not_found", path });
304
+ return;
305
+ }
306
+ const file = `${PUBLIC}rpg/${name}`;
307
+ if (!existsSync(file)) {
308
+ json(res, 404, { error: "not_found", path });
309
+ return;
310
+ }
311
+ const type = name.endsWith(".png")
312
+ ? "image/png"
313
+ : name.endsWith(".jpg") || name.endsWith(".jpeg")
314
+ ? "image/jpeg"
315
+ : "application/octet-stream";
316
+ send(res, 200, type, readFileSync(file));
317
+ return;
318
+ }
319
+
320
+ if (method === "GET" && path === "/health") {
321
+ json(res, 200, healthPayload());
322
+ return;
323
+ }
324
+
325
+ if (method === "GET" && (path === "/bots" || path === "/bench")) {
326
+ json(res, 200, listBench(store));
327
+ return;
328
+ }
329
+
330
+ if (method === "GET" && path === "/workspace") {
331
+ json(res, 200, workspace(store));
332
+ return;
333
+ }
334
+
335
+ if (method === "GET" && path === "/host/ls") {
336
+ json(res, 200, hostList(requestUrl(req).searchParams.get("path") || "~"));
337
+ return;
338
+ }
339
+ if (method === "GET" && path === "/host/read") {
340
+ json(res, 200, hostRead(requestUrl(req).searchParams.get("path") || ""));
341
+ return;
342
+ }
343
+ if (method === "GET" && path === "/host/tree") {
344
+ json(res, 200, hostTree(requestUrl(req).searchParams.get("path") || "~"));
345
+ return;
346
+ }
347
+ if (method === "GET" && path === "/host/git") {
348
+ json(res, 200, await hostGit(requestUrl(req).searchParams.get("path") || "~"));
349
+ return;
350
+ }
351
+
352
+ if (method === "POST" && path === "/channels") {
353
+ const body = asRecord(await readJson(req));
354
+ json(res, 201, createChannel(store, str(body, "name")));
355
+ return;
356
+ }
357
+ const channelOne = path.match(/^\/channels\/([^/]+)$/);
358
+ if (channelOne && method === "DELETE") {
359
+ json(res, 200, deleteChannel(store, decodeURIComponent(channelOne[1])));
360
+ return;
361
+ }
362
+ if (channelOne && method === "PATCH") {
363
+ const body = asRecord(await readJson(req));
364
+ json(
365
+ res,
366
+ 200,
367
+ renameChannel(
368
+ store,
369
+ decodeURIComponent(channelOne[1]),
370
+ str(body, "name"),
371
+ ),
372
+ );
373
+ return;
374
+ }
375
+
376
+ const channelMd = path.match(/^\/channels\/([^/]+)\/channel\.md$/);
377
+ if (channelMd && method === "GET") {
378
+ json(res, 200, getChannelMd(store, decodeURIComponent(channelMd[1])));
379
+ return;
380
+ }
381
+ if (channelMd && method === "PUT") {
382
+ const body = asRecord(await readJson(req));
383
+ json(
384
+ res,
385
+ 200,
386
+ setChannelMd(
387
+ store,
388
+ decodeURIComponent(channelMd[1]),
389
+ typeof body.body === "string" ? body.body : "",
390
+ ),
391
+ );
392
+ return;
393
+ }
394
+
395
+ const dmChannelMd = path.match(/^\/dms\/([^/]+)\/channel\.md$/);
396
+ if (dmChannelMd && (method === "GET" || method === "PUT")) {
397
+ throw new StoreError(400, "Channel.md is only for channels");
398
+ }
399
+
400
+ const botMemory = path.match(/^\/bots\/([^/]+)\/memory\.md$/);
401
+ if (botMemory && method === "GET") {
402
+ json(res, 200, getBotMemory(store, decodeURIComponent(botMemory[1])));
403
+ return;
404
+ }
405
+ if (botMemory && method === "PUT") {
406
+ const body = asRecord(await readJson(req));
407
+ json(
408
+ res,
409
+ 200,
410
+ setBotMemory(
411
+ store,
412
+ decodeURIComponent(botMemory[1]),
413
+ typeof body.body === "string" ? body.body : "",
414
+ ),
415
+ );
416
+ return;
417
+ }
418
+
419
+ const channelMemory = path.match(/^\/channels\/([^/]+)\/memory\.md$/);
420
+ if (channelMemory && method === "GET") {
421
+ json(
422
+ res,
423
+ 200,
424
+ getChannelMemory(store, decodeURIComponent(channelMemory[1])),
425
+ );
426
+ return;
427
+ }
428
+ if (channelMemory && method === "PUT") {
429
+ const body = asRecord(await readJson(req));
430
+ json(
431
+ res,
432
+ 200,
433
+ setChannelMemory(
434
+ store,
435
+ decodeURIComponent(channelMemory[1]),
436
+ typeof body.body === "string" ? body.body : "",
437
+ ),
438
+ );
439
+ return;
440
+ }
441
+
442
+ const dmMemory = path.match(/^\/dms\/([^/]+)\/memory\.md$/);
443
+ if (dmMemory && (method === "GET" || method === "PUT")) {
444
+ throw new StoreError(400, "Channel MEMORY.md is only for channels");
445
+ }
446
+
447
+ const channelMember = path.match(
448
+ /^\/channels\/([^/]+)\/members\/([^/]+)$/,
449
+ );
450
+ if (channelMember && method === "DELETE") {
451
+ json(
452
+ res,
453
+ 200,
454
+ removeChannelMember(store, channelMember[1], channelMember[2]),
455
+ );
456
+ return;
457
+ }
458
+
459
+ const channelMembers = path.match(/^\/channels\/([^/]+)\/members$/);
460
+ if (channelMembers && method === "POST") {
461
+ const body = asRecord(await readJson(req));
462
+ json(
463
+ res,
464
+ 200,
465
+ addChannelMember(store, channelMembers[1], str(body, "botId")),
466
+ );
467
+ return;
468
+ }
469
+
470
+ const retryChannel = path.match(
471
+ /^\/channels\/([^/]+)\/messages\/([^/]+)\/retry$/,
472
+ );
473
+ if (retryChannel && method === "POST") {
474
+ const body = asRecord(await readJson(req));
475
+ json(
476
+ res,
477
+ 200,
478
+ await retryMessage(
479
+ store,
480
+ retryChannel[1],
481
+ retryChannel[2],
482
+ str(body, "body") || undefined,
483
+ env,
484
+ str(body, "assigneeId") || undefined,
485
+ extras,
486
+ ),
487
+ );
488
+ return;
489
+ }
490
+
491
+ const retryDm = path.match(
492
+ /^\/dms\/([^/]+)\/messages\/([^/]+)\/retry$/,
493
+ );
494
+ if (retryDm && method === "POST") {
495
+ const room = openDm(store, retryDm[1]);
496
+ const body = asRecord(await readJson(req));
497
+ json(
498
+ res,
499
+ 200,
500
+ await retryMessage(
501
+ store,
502
+ room.id,
503
+ retryDm[2],
504
+ str(body, "body") || undefined,
505
+ env,
506
+ str(body, "assigneeId") || undefined,
507
+ extras,
508
+ ),
509
+ );
510
+ return;
511
+ }
512
+
513
+ const channelTraj = path.match(/^\/channels\/([^/]+)\/trajectory$/);
514
+ if (channelTraj && method === "GET") {
515
+ json(res, 200, listRoomTrajectory(store, channelTraj[1]));
516
+ return;
517
+ }
518
+ const dmTraj = path.match(/^\/dms\/([^/]+)\/trajectory$/);
519
+ if (dmTraj && method === "GET") {
520
+ const room = openDm(store, dmTraj[1]);
521
+ json(res, 200, listRoomTrajectory(store, room.id));
522
+ return;
523
+ }
524
+
525
+ const channelLive = path.match(/^\/channels\/([^/]+)\/live$/);
526
+ if (channelLive && method === "GET") {
527
+ json(res, 200, getLiveTurn(store, decodeURIComponent(channelLive[1])));
528
+ return;
529
+ }
530
+ const dmLive = path.match(/^\/dms\/([^/]+)\/live$/);
531
+ if (dmLive && method === "GET") {
532
+ const room = openDm(store, decodeURIComponent(dmLive[1]));
533
+ json(res, 200, getLiveTurn(store, room.id));
534
+ return;
535
+ }
536
+
537
+ const channelAbort = path.match(/^\/channels\/([^/]+)\/abort$/);
538
+ if (channelAbort && method === "POST") {
539
+ const body = asRecord(await readJson(req));
540
+ json(
541
+ res,
542
+ 200,
543
+ abortLiveTurn(
544
+ store,
545
+ decodeURIComponent(channelAbort[1]),
546
+ str(body, "botId") || undefined,
547
+ ),
548
+ );
549
+ return;
550
+ }
551
+ const dmAbort = path.match(/^\/dms\/([^/]+)\/abort$/);
552
+ if (dmAbort && method === "POST") {
553
+ const room = openDm(store, decodeURIComponent(dmAbort[1]));
554
+ const body = asRecord(await readJson(req));
555
+ json(
556
+ res,
557
+ 200,
558
+ abortLiveTurn(store, room.id, str(body, "botId") || undefined),
559
+ );
560
+ return;
561
+ }
562
+
563
+ const channelSteer = path.match(/^\/channels\/([^/]+)\/steer$/);
564
+ if (channelSteer && method === "POST") {
565
+ const body = asRecord(await readJson(req));
566
+ json(
567
+ res,
568
+ 201,
569
+ steerUserMessage(
570
+ store,
571
+ decodeURIComponent(channelSteer[1]),
572
+ str(body, "body"),
573
+ parseAttachments(body.attachments),
574
+ str(body, "replyTo") || undefined,
575
+ str(body, "botId") || undefined,
576
+ ),
577
+ );
578
+ return;
579
+ }
580
+ const dmSteer = path.match(/^\/dms\/([^/]+)\/steer$/);
581
+ if (dmSteer && method === "POST") {
582
+ const room = openDm(store, decodeURIComponent(dmSteer[1]));
583
+ const body = asRecord(await readJson(req));
584
+ json(
585
+ res,
586
+ 201,
587
+ steerUserMessage(
588
+ store,
589
+ room.id,
590
+ str(body, "body"),
591
+ parseAttachments(body.attachments),
592
+ str(body, "replyTo") || undefined,
593
+ str(body, "botId") || undefined,
594
+ ),
595
+ );
596
+ return;
597
+ }
598
+
599
+ const channelMessageOne = path.match(
600
+ /^\/channels\/([^/]+)\/messages\/([^/]+)$/,
601
+ );
602
+ if (channelMessageOne && method === "DELETE") {
603
+ json(
604
+ res,
605
+ 200,
606
+ deleteRoomMessage(
607
+ store,
608
+ decodeURIComponent(channelMessageOne[1]),
609
+ decodeURIComponent(channelMessageOne[2]),
610
+ ),
611
+ );
612
+ return;
613
+ }
614
+
615
+ const dmMessageOne = path.match(/^\/dms\/([^/]+)\/messages\/([^/]+)$/);
616
+ if (dmMessageOne && method === "DELETE") {
617
+ const room = openDm(store, decodeURIComponent(dmMessageOne[1]));
618
+ json(
619
+ res,
620
+ 200,
621
+ deleteRoomMessage(store, room.id, decodeURIComponent(dmMessageOne[2])),
622
+ );
623
+ return;
624
+ }
625
+
626
+ const channelMessages = path.match(/^\/channels\/([^/]+)\/messages$/);
627
+ if (channelMessages && method === "GET") {
628
+ json(res, 200, listRoomMessages(store, channelMessages[1]));
629
+ return;
630
+ }
631
+ if (channelMessages && method === "POST") {
632
+ const body = asRecord(await readJson(req));
633
+ json(
634
+ res,
635
+ 201,
636
+ await postUserMessage(
637
+ store,
638
+ channelMessages[1],
639
+ str(body, "body"),
640
+ env,
641
+ str(body, "replyTo") || undefined,
642
+ parseAttachments(body.attachments),
643
+ str(body, "assigneeId") || undefined,
644
+ extras,
645
+ ),
646
+ );
647
+ return;
648
+ }
649
+
650
+ const dmMessages = path.match(/^\/dms\/([^/]+)\/messages$/);
651
+ if (dmMessages && method === "GET") {
652
+ const room = openDm(store, dmMessages[1]);
653
+ json(res, 200, listRoomMessages(store, room.id));
654
+ return;
655
+ }
656
+ if (dmMessages && method === "POST") {
657
+ const room = openDm(store, dmMessages[1]);
658
+ const body = asRecord(await readJson(req));
659
+ json(
660
+ res,
661
+ 201,
662
+ await postUserMessage(
663
+ store,
664
+ room.id,
665
+ str(body, "body"),
666
+ env,
667
+ str(body, "replyTo") || undefined,
668
+ parseAttachments(body.attachments),
669
+ str(body, "assigneeId") || undefined,
670
+ extras,
671
+ ),
672
+ );
673
+ return;
674
+ }
675
+
676
+ if (method === "GET" && path === "/mcp/servers") {
677
+ if (extras.mcp === false) {
678
+ json(res, 200, []);
679
+ return;
680
+ }
681
+ json(res, 200, listMcpServers(store));
682
+ return;
683
+ }
684
+ if (method === "GET" && path === "/mcp/host") {
685
+ json(res, 200, listHostMcpServers());
686
+ return;
687
+ }
688
+ if (method === "POST" && path === "/mcp/servers") {
689
+ if (extras.mcp === false) {
690
+ json(res, 503, { error: "mcp_disabled" });
691
+ return;
692
+ }
693
+ const body = asRecord(await readJson(req));
694
+ const envValue = body.env;
695
+ const env: Record<string, string> = {};
696
+ if (envValue && typeof envValue === "object" && !Array.isArray(envValue)) {
697
+ for (const [key, value] of Object.entries(
698
+ envValue as Record<string, unknown>,
699
+ )) {
700
+ if (typeof value === "string") env[key] = value;
701
+ }
702
+ }
703
+ json(
704
+ res,
705
+ 201,
706
+ createMcpServer(store, {
707
+ name: str(body, "name"),
708
+ command: str(body, "command"),
709
+ args: strList(body, "args"),
710
+ env: Object.keys(env).length ? env : undefined,
711
+ cwd: str(body, "cwd") || undefined,
712
+ url: str(body, "url") || undefined,
713
+ }),
714
+ );
715
+ return;
716
+ }
717
+ if (method === "POST" && path === "/mcp/import") {
718
+ if (extras.mcp === false) {
719
+ json(res, 503, { error: "mcp_disabled" });
720
+ return;
721
+ }
722
+ const body = asRecord(await readJson(req));
723
+ json(res, 201, importMcpServer(store, str(body, "id")));
724
+ return;
725
+ }
726
+ const mcpOne = path.match(/^\/mcp\/servers\/([^/]+)$/);
727
+ if (mcpOne && method === "DELETE") {
728
+ if (extras.mcp === false) {
729
+ json(res, 503, { error: "mcp_disabled" });
730
+ return;
731
+ }
732
+ json(res, 200, deleteMcpServer(store, decodeURIComponent(mcpOne[1])));
733
+ return;
734
+ }
735
+
736
+ if (method === "GET" && path === "/library/skills/host") {
737
+ const url = requestUrl(req);
738
+ const id = url.searchParams.get("id");
739
+ const includeBody = id ? true : url.searchParams.get("body") !== "0";
740
+ const listed = listHostSkills({ includeBody });
741
+ if (id) {
742
+ const hit = listed.find((item) => item.id === id);
743
+ if (!hit) {
744
+ json(res, 404, { error: "not_found" });
745
+ return;
746
+ }
747
+ json(res, 200, hit);
748
+ return;
749
+ }
750
+ json(res, 200, listed);
751
+ return;
752
+ }
753
+
754
+ if (method === "GET" && path === "/library/subagents/host") {
755
+ const url = requestUrl(req);
756
+ const id = url.searchParams.get("id");
757
+ const includeBody = id ? true : url.searchParams.get("body") !== "0";
758
+ const listed = listHostAgents({ includeBody });
759
+ if (id) {
760
+ const hit = listed.find((item) => item.id === id);
761
+ if (!hit) {
762
+ json(res, 404, { error: "not_found" });
763
+ return;
764
+ }
765
+ json(res, 200, hit);
766
+ return;
767
+ }
768
+ json(res, 200, listed);
769
+ return;
770
+ }
771
+
772
+ if (method === "POST" && path === "/library/skills/import") {
773
+ const body = asRecord(await readJson(req));
774
+ const imported = await importSkills(store, {
775
+ source: str(body, "source"),
776
+ url: str(body, "url") || undefined,
777
+ repo: str(body, "repo") || undefined,
778
+ });
779
+ json(res, 201, { imported });
780
+ return;
781
+ }
782
+
783
+ if (method === "POST" && path === "/generate") {
784
+ const body = asRecord(await readJson(req));
785
+ const generated = await generateKind(
786
+ store,
787
+ str(body, "kind"),
788
+ str(body, "prompt"),
789
+ );
790
+ json(res, 200, generated);
791
+ return;
792
+ }
793
+
794
+ if (method === "POST" && path === "/generate/skills") {
795
+ const body = asRecord(await readJson(req));
796
+ const picked = await pickBotSkills(store, {
797
+ name: str(body, "name"),
798
+ handle: str(body, "handle"),
799
+ oneLiner: str(body, "oneLiner"),
800
+ soul: str(body, "soul"),
801
+ agent: str(body, "agent"),
802
+ position: str(body, "position"),
803
+ skills: skillPickCatalog(body),
804
+ });
805
+ json(res, 200, picked);
806
+ return;
807
+ }
808
+
809
+ if (method === "GET" && path === "/settings/models") {
810
+ await refreshCopilotCatalog(store.dataDir);
811
+ json(res, 200, publicModels(store.dataDir));
812
+ return;
813
+ }
814
+ if (method === "PUT" && path === "/settings/models") {
815
+ const body = asRecord(await readJson(req));
816
+ const providers = body.providers;
817
+ const patch: Partial<ModelsFile> = {};
818
+ if (providers && typeof providers === "object" && !Array.isArray(providers)) {
819
+ patch.providers = providers as ModelsFile["providers"];
820
+ }
821
+ if (Object.hasOwn(body, "default")) {
822
+ patch.default =
823
+ body.default && typeof body.default === "object"
824
+ ? {
825
+ provider: str(body.default as Record<string, unknown>, "provider"),
826
+ model: str(body.default as Record<string, unknown>, "model"),
827
+ }
828
+ : null;
829
+ }
830
+ if (typeof body.reasoning === "string") {
831
+ patch.reasoning = body.reasoning as ModelsFile["reasoning"];
832
+ }
833
+ if (typeof body.fast === "boolean") patch.fast = body.fast;
834
+ if (Object.hasOwn(body, "aux") && body.aux && typeof body.aux === "object") {
835
+ patch.aux = body.aux as ModelsFile["aux"];
836
+ }
837
+ mergeModelsFile(store.dataDir, patch);
838
+ json(res, 200, publicModels(store.dataDir));
839
+ return;
840
+ }
841
+
842
+ if (method === "GET" && path === "/settings/oauth") {
843
+ if (extras.oauth === false) {
844
+ json(res, 200, { subscriptions: [] });
845
+ return;
846
+ }
847
+ await refreshCopilotCatalog(store.dataDir);
848
+ json(res, 200, { subscriptions: listSubscriptions(store.dataDir) });
849
+ return;
850
+ }
851
+ const oauthLogin = path.match(/^\/settings\/oauth\/([^/]+)\/login$/);
852
+ if (oauthLogin && method === "POST") {
853
+ if (extras.oauth === false) {
854
+ json(res, 503, { error: "oauth_disabled" });
855
+ return;
856
+ }
857
+ json(res, 200, await startLogin(store.dataDir, decodeURIComponent(oauthLogin[1])));
858
+ return;
859
+ }
860
+ const oauthPoll = path.match(/^\/settings\/oauth\/([^/]+)\/poll$/);
861
+ if (oauthPoll && method === "POST") {
862
+ if (extras.oauth === false) {
863
+ json(res, 503, { error: "oauth_disabled" });
864
+ return;
865
+ }
866
+ json(res, 200, await pollLogin(store.dataDir, decodeURIComponent(oauthPoll[1])));
867
+ return;
868
+ }
869
+ const oauthComplete = path.match(/^\/settings\/oauth\/([^/]+)\/complete$/);
870
+ if (oauthComplete && method === "POST") {
871
+ if (extras.oauth === false) {
872
+ json(res, 503, { error: "oauth_disabled" });
873
+ return;
874
+ }
875
+ const body = asRecord(await readJson(req));
876
+ json(
877
+ res,
878
+ 200,
879
+ await completeLogin(store.dataDir, decodeURIComponent(oauthComplete[1]), {
880
+ code: str(body, "code") || undefined,
881
+ url: str(body, "url") || undefined,
882
+ }),
883
+ );
884
+ return;
885
+ }
886
+ const oauthLogout = path.match(/^\/settings\/oauth\/([^/]+)\/logout$/);
887
+ if (oauthLogout && method === "POST") {
888
+ if (extras.oauth === false) {
889
+ json(res, 503, { error: "oauth_disabled" });
890
+ return;
891
+ }
892
+ json(res, 200, await logoutOAuth(store.dataDir, decodeURIComponent(oauthLogout[1])));
893
+ return;
894
+ }
895
+
896
+ const lookId = path.match(/^\/bots\/([^/]+)\/look$/)?.[1];
897
+ if (lookId && method === "POST") {
898
+ json(res, 200, await generateBotLook(store, decodeURIComponent(lookId), env));
899
+ return;
900
+ }
901
+
902
+ const botId = path.match(/^\/bots\/([^/]+)$/)?.[1];
903
+ if (botId && method === "GET") {
904
+ json(res, 200, getBotDetail(store, botId));
905
+ return;
906
+ }
907
+ if (botId && method === "DELETE") {
908
+ json(res, 200, deleteBot(store, decodeURIComponent(botId)));
909
+ return;
910
+ }
911
+ if (botId && method === "PATCH") {
912
+ const body = asRecord(await readJson(req));
913
+ const bot = updateBot(store, botId, {
914
+ name: str(body, "name") || undefined,
915
+ handle: str(body, "handle") || undefined,
916
+ oneLiner: str(body, "oneLiner") || undefined,
917
+ ...(Object.hasOwn(body, "portrait")
918
+ ? { portrait: body.portrait == null ? null : str(body, "portrait") }
919
+ : {}),
920
+ skillIds: Object.hasOwn(body, "skillIds")
921
+ ? strList(body, "skillIds")
922
+ : undefined,
923
+ soul: draft(body, "soul"),
924
+ agent: draft(body, "agent"),
925
+ position: draft(body, "position"),
926
+ ...(Object.hasOwn(body, "model")
927
+ ? { model: modelRefFrom(body.model) }
928
+ : {}),
929
+ });
930
+ json(res, 200, bot);
931
+ return;
932
+ }
933
+
934
+ if (method === "POST" && path === "/bots") {
935
+ const body = asRecord(await readJson(req));
936
+ const bot = createBot(store, {
937
+ name: str(body, "name"),
938
+ handle: str(body, "handle"),
939
+ oneLiner: str(body, "oneLiner") || undefined,
940
+ soulId: str(body, "soulId") || undefined,
941
+ agentTemplateId: str(body, "agentTemplateId") || undefined,
942
+ defaultPositionId: str(body, "defaultPositionId") || undefined,
943
+ soul: draft(body, "soul"),
944
+ agent: draft(body, "agent"),
945
+ position: draft(body, "position"),
946
+ skillIds: strList(body, "skillIds"),
947
+ skillId: str(body, "skillId") || undefined,
948
+ });
949
+ json(res, 201, bot);
950
+ return;
951
+ }
952
+
953
+ const kind = libraryKindFromPath(path);
954
+ if (kind && method === "GET") {
955
+ json(res, 200, listLibrary(store, kind));
956
+ return;
957
+ }
958
+ if (kind && method === "POST") {
959
+ const body = asRecord(await readJson(req));
960
+ const item = createLibraryItem(store, kind, {
961
+ name: str(body, "name"),
962
+ body: str(body, "body"),
963
+ slug: str(body, "slug") || undefined,
964
+ description: str(body, "description") || undefined,
965
+ });
966
+ json(res, 201, item);
967
+ return;
968
+ }
969
+
970
+ json(res, 404, { error: "not_found", path });
971
+ } catch (error) {
972
+ if (error instanceof StoreError) {
973
+ json(res, error.status, { error: error.message });
974
+ return;
975
+ }
976
+ json(res, 500, { error: "internal_error" });
977
+ }
978
+ }