@desplega.ai/agent-swarm 1.92.0 → 1.92.2
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 +1 -1
- package/openapi.json +276 -3
- package/package.json +6 -6
- package/plugin/skills/pages/SKILL.md +5 -2
- package/src/be/db.ts +416 -20
- package/src/be/memory/boot-reembed.ts +85 -0
- package/src/be/memory/constants.ts +44 -2
- package/src/be/memory/providers/openai-embedding.ts +15 -5
- package/src/be/memory/providers/sqlite-store.ts +325 -76
- package/src/be/memory/reranker.ts +35 -17
- package/src/be/memory/types.ts +43 -0
- package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
- package/src/be/migrations/085_script_runs_kind.sql +9 -0
- package/src/be/migrations/086_pages_default_authed.sql +64 -0
- package/src/be/migrations/087_skill_files.sql +19 -0
- package/src/be/modelsdev-cache.json +5622 -2543
- package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +465 -0
- package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
- package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +34 -439
- package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
- package/src/be/seed-scripts/index.ts +32 -4
- package/src/be/seed-skills/index.ts +0 -7
- package/src/be/skill-sync.ts +91 -7
- package/src/commands/runner.ts +6 -2
- package/src/heartbeat/templates.ts +20 -16
- package/src/http/index.ts +50 -7
- package/src/http/mcp-user.ts +23 -0
- package/src/http/mcp.ts +58 -0
- package/src/http/memory.ts +62 -0
- package/src/http/pages.ts +1 -1
- package/src/http/script-runs.ts +2 -0
- package/src/http/scripts.ts +39 -2
- package/src/http/skills.ts +225 -0
- package/src/providers/claude-adapter.ts +56 -24
- package/src/script-workflows/workflow-ctx.ts +7 -3
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +13 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +2 -0
- package/src/tasks/worker-follow-up.ts +12 -0
- package/src/tests/claude-adapter-binary.test.ts +135 -81
- package/src/tests/create-page-tool.test.ts +19 -2
- package/src/tests/heartbeat-checklist.test.ts +36 -0
- package/src/tests/mcp-transport-gc.test.ts +58 -0
- package/src/tests/memory-e2e.test.ts +6 -6
- package/src/tests/memory-health-endpoint.test.ts +78 -0
- package/src/tests/memory-rater-e2e.test.ts +4 -5
- package/src/tests/memory-reranker.test.ts +135 -124
- package/src/tests/memory-store.test.ts +221 -1
- package/src/tests/memory.test.ts +13 -12
- package/src/tests/pages-http.test.ts +20 -2
- package/src/tests/pages-storage.test.ts +26 -0
- package/src/tests/scripts-mcp-e2e.test.ts +53 -0
- package/src/tests/seed-scripts.test.ts +328 -3
- package/src/tests/skill-files-http.test.ts +171 -0
- package/src/tests/skill-files.test.ts +162 -0
- package/src/tests/skill-get-file-tool.test.ts +110 -0
- package/src/tests/skill-sync.test.ts +125 -6
- package/src/tests/task-cascade-fail.test.ts +304 -0
- package/src/tools/create-page.ts +2 -2
- package/src/tools/skills/index.ts +1 -0
- package/src/tools/skills/skill-get-file.ts +80 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +20 -0
- package/src/utils/internal-ai/complete-structured.ts +2 -2
- package/templates/schedules/daily-blocker-digest/content.md +68 -54
- package/templates/schedules/daily-compounding-reflection/content.md +4 -4
- package/templates/schedules/daily-hn-briefing/content.md +5 -5
- package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
- package/templates/schedules/gtm-weekly-review/content.md +9 -9
- package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
- package/templates/skills/agentmail-sending/content.md +6 -7
- package/templates/skills/desloppify/content.md +8 -9
- package/templates/skills/jira-interaction/content.md +25 -33
- package/templates/skills/kapso-whatsapp/content.md +29 -30
- package/templates/skills/linear-interaction/content.md +8 -9
- package/templates/skills/profile-corruption-escalation/content.md +44 -85
- package/templates/skills/sprite-cli/content.md +4 -5
- package/templates/skills/turso-interaction/content.md +14 -17
- package/templates/skills/workflow-iterate/content.md +38 -391
- package/templates/skills/x-api-interactions/content.md +4 -6
- package/templates/workflows/llm-safe-release-context/config.json +13 -0
- package/templates/workflows/llm-safe-release-context/content.md +69 -0
- package/templates/skills/scheduled-task-resilience/config.json +0 -14
- package/templates/skills/scheduled-task-resilience/content.md +0 -95
package/src/http/mcp.ts
CHANGED
|
@@ -4,10 +4,62 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
4
4
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { createServer } from "@/server";
|
|
6
6
|
|
|
7
|
+
export type McpTransportActivity = Record<string, number>;
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_MCP_TRANSPORT_IDLE_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
export function markMcpTransportActivity(
|
|
12
|
+
sessionActivity: McpTransportActivity,
|
|
13
|
+
sessionId: string | undefined,
|
|
14
|
+
now = Date.now(),
|
|
15
|
+
): void {
|
|
16
|
+
if (sessionId) {
|
|
17
|
+
sessionActivity[sessionId] = now;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function closeIdleMcpTransports(
|
|
22
|
+
transports: Record<string, StreamableHTTPServerTransport>,
|
|
23
|
+
sessionActivity: McpTransportActivity,
|
|
24
|
+
options: {
|
|
25
|
+
now?: number;
|
|
26
|
+
idleTimeoutMs?: number;
|
|
27
|
+
label?: string;
|
|
28
|
+
onClose?: (id: string) => void;
|
|
29
|
+
} = {},
|
|
30
|
+
): number {
|
|
31
|
+
const now = options.now ?? Date.now();
|
|
32
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_MCP_TRANSPORT_IDLE_TIMEOUT_MS;
|
|
33
|
+
let closed = 0;
|
|
34
|
+
|
|
35
|
+
for (const [id, transport] of Object.entries(transports)) {
|
|
36
|
+
const lastActivity = sessionActivity[id];
|
|
37
|
+
if (lastActivity === undefined) {
|
|
38
|
+
sessionActivity[id] = now;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (now - lastActivity < idleTimeoutMs) continue;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
transport.close();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.warn(`[HTTP] Failed to close idle ${options.label ?? "MCP"} transport ${id}: ${err}`);
|
|
47
|
+
} finally {
|
|
48
|
+
delete transports[id];
|
|
49
|
+
delete sessionActivity[id];
|
|
50
|
+
options.onClose?.(id);
|
|
51
|
+
closed++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return closed;
|
|
56
|
+
}
|
|
57
|
+
|
|
7
58
|
export async function handleMcp(
|
|
8
59
|
req: IncomingMessage,
|
|
9
60
|
res: ServerResponse,
|
|
10
61
|
transports: Record<string, StreamableHTTPServerTransport>,
|
|
62
|
+
sessionActivity: McpTransportActivity = {},
|
|
11
63
|
): Promise<boolean> {
|
|
12
64
|
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
13
65
|
|
|
@@ -26,20 +78,24 @@ export async function handleMcp(
|
|
|
26
78
|
|
|
27
79
|
if (sessionId && transports[sessionId]) {
|
|
28
80
|
transport = transports[sessionId];
|
|
81
|
+
markMcpTransportActivity(sessionActivity, sessionId);
|
|
29
82
|
} else if (!sessionId && isInitializeRequest(body)) {
|
|
30
83
|
transport = new StreamableHTTPServerTransport({
|
|
31
84
|
sessionIdGenerator: () => randomUUID(),
|
|
32
85
|
onsessioninitialized: (id) => {
|
|
33
86
|
transports[id] = transport;
|
|
87
|
+
markMcpTransportActivity(sessionActivity, id);
|
|
34
88
|
},
|
|
35
89
|
onsessionclosed: (id) => {
|
|
36
90
|
delete transports[id];
|
|
91
|
+
delete sessionActivity[id];
|
|
37
92
|
},
|
|
38
93
|
});
|
|
39
94
|
|
|
40
95
|
transport.onclose = () => {
|
|
41
96
|
if (transport.sessionId) {
|
|
42
97
|
delete transports[transport.sessionId];
|
|
98
|
+
delete sessionActivity[transport.sessionId];
|
|
43
99
|
}
|
|
44
100
|
};
|
|
45
101
|
|
|
@@ -58,11 +114,13 @@ export async function handleMcp(
|
|
|
58
114
|
}
|
|
59
115
|
|
|
60
116
|
await transport.handleRequest(req, res, body);
|
|
117
|
+
markMcpTransportActivity(sessionActivity, transport.sessionId);
|
|
61
118
|
return true;
|
|
62
119
|
}
|
|
63
120
|
|
|
64
121
|
if (req.method === "GET" || req.method === "DELETE") {
|
|
65
122
|
if (sessionId && transports[sessionId]) {
|
|
123
|
+
markMcpTransportActivity(sessionActivity, sessionId);
|
|
66
124
|
await transports[sessionId].handleRequest(req, res);
|
|
67
125
|
return true;
|
|
68
126
|
}
|
package/src/http/memory.ts
CHANGED
|
@@ -115,6 +115,18 @@ const listMemory = route({
|
|
|
115
115
|
},
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
const memoryHealth = route({
|
|
119
|
+
method: "get",
|
|
120
|
+
path: "/api/memory/health",
|
|
121
|
+
pattern: ["api", "memory", "health"],
|
|
122
|
+
summary: "Report memory vector index health and retrieval mode",
|
|
123
|
+
tags: ["Memory"],
|
|
124
|
+
auth: { apiKey: true },
|
|
125
|
+
responses: {
|
|
126
|
+
200: { description: "Memory vector index health" },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
118
130
|
const deleteMemoryById = route({
|
|
119
131
|
method: "delete",
|
|
120
132
|
path: "/api/memory/{id}",
|
|
@@ -375,6 +387,8 @@ export async function handleMemory(
|
|
|
375
387
|
name: r.name,
|
|
376
388
|
content: r.content,
|
|
377
389
|
similarity: r.similarity,
|
|
390
|
+
rawSimilarity: r.rawSimilarity,
|
|
391
|
+
compositeScore: r.compositeScore,
|
|
378
392
|
source: r.source,
|
|
379
393
|
scope: r.scope,
|
|
380
394
|
})),
|
|
@@ -430,6 +444,8 @@ export async function handleMemory(
|
|
|
430
444
|
scope: r.scope,
|
|
431
445
|
source: r.source,
|
|
432
446
|
similarity: r.similarity,
|
|
447
|
+
rawSimilarity: r.rawSimilarity,
|
|
448
|
+
compositeScore: r.compositeScore,
|
|
433
449
|
createdAt: r.createdAt,
|
|
434
450
|
accessedAt: r.accessedAt,
|
|
435
451
|
accessCount: r.accessCount ?? 0,
|
|
@@ -498,6 +514,14 @@ export async function handleMemory(
|
|
|
498
514
|
return true;
|
|
499
515
|
}
|
|
500
516
|
|
|
517
|
+
if (memoryHealth.match(req.method, pathSegments)) {
|
|
518
|
+
const parsed = await memoryHealth.parse(req, res, pathSegments, new URLSearchParams());
|
|
519
|
+
if (!parsed) return true;
|
|
520
|
+
|
|
521
|
+
json(res, getMemoryStore().getHealth());
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
|
|
501
525
|
if (deleteMemoryById.match(req.method, pathSegments)) {
|
|
502
526
|
const parsed = await deleteMemoryById.parse(req, res, pathSegments, new URLSearchParams());
|
|
503
527
|
if (!parsed) return true;
|
|
@@ -663,3 +687,41 @@ export async function handleMemory(
|
|
|
663
687
|
|
|
664
688
|
return false;
|
|
665
689
|
}
|
|
690
|
+
|
|
691
|
+
// ─── Expired Memory GC ──────────────────────────────────────────────────────
|
|
692
|
+
|
|
693
|
+
const MEMORY_GC_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
694
|
+
let memoryGcTimer: ReturnType<typeof setInterval> | null = null;
|
|
695
|
+
|
|
696
|
+
export function startMemoryGc(intervalMs = MEMORY_GC_INTERVAL_MS): void {
|
|
697
|
+
if (memoryGcTimer) return;
|
|
698
|
+
|
|
699
|
+
// Run immediately on startup to clear any backlog
|
|
700
|
+
try {
|
|
701
|
+
const purged = getMemoryStore().purgeExpired();
|
|
702
|
+
if (purged > 0) {
|
|
703
|
+
console.log(`[memory-gc] Initial purge removed ${purged} expired memory row(s)`);
|
|
704
|
+
}
|
|
705
|
+
} catch (err) {
|
|
706
|
+
console.error("[memory-gc] Initial purge failed:", err);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
memoryGcTimer = setInterval(() => {
|
|
710
|
+
try {
|
|
711
|
+
const purged = getMemoryStore().purgeExpired();
|
|
712
|
+
if (purged > 0) {
|
|
713
|
+
console.log(`[memory-gc] Periodic purge removed ${purged} expired memory row(s)`);
|
|
714
|
+
}
|
|
715
|
+
} catch (err) {
|
|
716
|
+
console.error("[memory-gc] Periodic purge failed:", err);
|
|
717
|
+
}
|
|
718
|
+
}, intervalMs);
|
|
719
|
+
if (typeof memoryGcTimer?.unref === "function") memoryGcTimer.unref();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function stopMemoryGc(): void {
|
|
723
|
+
if (memoryGcTimer) {
|
|
724
|
+
clearInterval(memoryGcTimer);
|
|
725
|
+
memoryGcTimer = null;
|
|
726
|
+
}
|
|
727
|
+
}
|
package/src/http/pages.ts
CHANGED
|
@@ -58,7 +58,7 @@ const createPageRoute = route({
|
|
|
58
58
|
title: z.string().min(1),
|
|
59
59
|
description: z.string().optional(),
|
|
60
60
|
contentType: PageContentTypeSchema,
|
|
61
|
-
authMode: PageAuthModeSchema,
|
|
61
|
+
authMode: PageAuthModeSchema.default("authed"),
|
|
62
62
|
password: z.string().min(1).optional(),
|
|
63
63
|
body: z.string(),
|
|
64
64
|
needsCredentials: z.array(z.string()).optional(),
|
package/src/http/script-runs.ts
CHANGED
|
@@ -62,6 +62,7 @@ const journalStepBodySchema = z.object({
|
|
|
62
62
|
status: z.enum(["completed", "failed"]),
|
|
63
63
|
result: z.unknown().optional(),
|
|
64
64
|
error: z.string().optional(),
|
|
65
|
+
durationMs: z.number().int().nonnegative().optional(),
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
const statusBodySchema = z.discriminatedUnion("status", [
|
|
@@ -440,6 +441,7 @@ export async function handleScriptRuns(
|
|
|
440
441
|
status: parsed.body.status,
|
|
441
442
|
result: parsed.body.result,
|
|
442
443
|
error: parsed.body.error,
|
|
444
|
+
durationMs: parsed.body.durationMs,
|
|
443
445
|
});
|
|
444
446
|
const limit = assertRunWithinLimits(run.id);
|
|
445
447
|
if (!limit.ok) {
|
package/src/http/scripts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { getAgentById, upsertKv } from "../be/db";
|
|
3
|
+
import { getAgentById, recordInlineScriptRun, upsertKv } from "../be/db";
|
|
4
4
|
import { createEvent } from "../be/events";
|
|
5
5
|
import { deleteScript, getScript, upsertScriptByName } from "../be/scripts/db";
|
|
6
6
|
import { searchScripts } from "../be/scripts/embeddings";
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type ScriptScope,
|
|
15
15
|
ScriptScopeSchema,
|
|
16
16
|
} from "../types";
|
|
17
|
-
import { scrubObject } from "../utils/secret-scrubber";
|
|
17
|
+
import { scrubObject, scrubSecrets } from "../utils/secret-scrubber";
|
|
18
18
|
import { route } from "./route-def";
|
|
19
19
|
import { json, jsonError } from "./utils";
|
|
20
20
|
|
|
@@ -283,6 +283,7 @@ export async function handleScripts(
|
|
|
283
283
|
return true;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
const startedAt = new Date().toISOString();
|
|
286
287
|
const output = await runScript({
|
|
287
288
|
source: source as string,
|
|
288
289
|
args: parsed.body.args,
|
|
@@ -331,6 +332,42 @@ export async function handleScripts(
|
|
|
331
332
|
autoSaved = { slug, reason: "successful_inline_run" };
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
// Persist the inline run (no journal) so one-off executions show up alongside
|
|
336
|
+
// durable workflow runs in the Script Runs dashboard. Best-effort: recording
|
|
337
|
+
// must never fail the actual execution.
|
|
338
|
+
const ok = output.exitCode === 0 && !output.error && !output.runtimeError;
|
|
339
|
+
const runError = ok
|
|
340
|
+
? undefined
|
|
341
|
+
: scrubSecrets(
|
|
342
|
+
[
|
|
343
|
+
output.error,
|
|
344
|
+
output.runtimeError
|
|
345
|
+
? `${output.runtimeError.name}: ${output.runtimeError.message}`
|
|
346
|
+
: undefined,
|
|
347
|
+
]
|
|
348
|
+
.filter(Boolean)
|
|
349
|
+
.join(" — ") || `Script exited with code ${output.exitCode}`,
|
|
350
|
+
);
|
|
351
|
+
try {
|
|
352
|
+
recordInlineScriptRun({
|
|
353
|
+
id: crypto.randomUUID(),
|
|
354
|
+
agentId: agent.id,
|
|
355
|
+
source: source as string,
|
|
356
|
+
// Scrub args + result before persisting: the stored row is later served
|
|
357
|
+
// raw by GET /api/script-runs/{id} to the dashboard, so it needs the same
|
|
358
|
+
// redaction guarantees as the scrubbed run response below.
|
|
359
|
+
args: scrubObject(parsed.body.args ?? null),
|
|
360
|
+
scriptName: parsed.body.name,
|
|
361
|
+
status: ok ? "completed" : "failed",
|
|
362
|
+
output: scrubObject(output.result),
|
|
363
|
+
error: runError,
|
|
364
|
+
startedAt,
|
|
365
|
+
finishedAt: new Date().toISOString(),
|
|
366
|
+
});
|
|
367
|
+
} catch {
|
|
368
|
+
// swallow — the run already executed; persistence is observability only.
|
|
369
|
+
}
|
|
370
|
+
|
|
334
371
|
json(
|
|
335
372
|
res,
|
|
336
373
|
scrubObject({
|
package/src/http/skills.ts
CHANGED
|
@@ -3,13 +3,18 @@ import { z } from "zod";
|
|
|
3
3
|
import {
|
|
4
4
|
createSkill,
|
|
5
5
|
deleteSkill,
|
|
6
|
+
deleteSkillFile,
|
|
6
7
|
getAgentSkills,
|
|
7
8
|
getSkillById,
|
|
9
|
+
getSkillFile,
|
|
8
10
|
installSkill,
|
|
11
|
+
listSkillFileManifest,
|
|
9
12
|
listSkills,
|
|
10
13
|
searchSkills,
|
|
11
14
|
uninstallSkill,
|
|
12
15
|
updateSkill,
|
|
16
|
+
upsertSkillFile,
|
|
17
|
+
upsertSkillFiles,
|
|
13
18
|
} from "../be/db";
|
|
14
19
|
import { parseSkillContent } from "../be/skill-parser";
|
|
15
20
|
import { computeAgentSkillsSignature, syncSkillsToFilesystem } from "../be/skill-sync";
|
|
@@ -19,6 +24,24 @@ import { json, jsonError } from "./utils";
|
|
|
19
24
|
const SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE =
|
|
20
25
|
"This skill is system-managed and cannot be edited from the UI; it is re-seeded on each start. Fork it under a new name to customize.";
|
|
21
26
|
|
|
27
|
+
const skillFileBodySchema = z.object({
|
|
28
|
+
content: z.string(),
|
|
29
|
+
mimeType: z.string().optional(),
|
|
30
|
+
isBinary: z.boolean().optional(),
|
|
31
|
+
size: z.number().int().nonnegative().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const skillFileWithPathSchema = skillFileBodySchema.extend({
|
|
35
|
+
path: z.string().min(1),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function decodeSkillFilePath(pathSegments: string[]): string {
|
|
39
|
+
return pathSegments
|
|
40
|
+
.slice(4)
|
|
41
|
+
.map((segment) => decodeURIComponent(segment))
|
|
42
|
+
.join("/");
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
23
46
|
|
|
24
47
|
const listSkillsRoute = route({
|
|
@@ -58,6 +81,86 @@ const getSkillRoute = route({
|
|
|
58
81
|
},
|
|
59
82
|
});
|
|
60
83
|
|
|
84
|
+
const listSkillFilesRoute = route({
|
|
85
|
+
method: "get",
|
|
86
|
+
path: "/api/skills/{id}/files",
|
|
87
|
+
pattern: ["api", "skills", null, "files"],
|
|
88
|
+
summary: "List bundled files for a skill",
|
|
89
|
+
description: "Returns a manifest of bundled skill files without file content.",
|
|
90
|
+
tags: ["Skills"],
|
|
91
|
+
auth: { apiKey: true },
|
|
92
|
+
params: z.object({ id: z.string() }),
|
|
93
|
+
responses: {
|
|
94
|
+
200: { description: "Skill file manifest" },
|
|
95
|
+
404: { description: "Skill not found" },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const bulkUpsertSkillFilesRoute = route({
|
|
100
|
+
method: "post",
|
|
101
|
+
path: "/api/skills/{id}/files",
|
|
102
|
+
pattern: ["api", "skills", null, "files"],
|
|
103
|
+
summary: "Bulk upsert bundled files for a skill",
|
|
104
|
+
tags: ["Skills"],
|
|
105
|
+
auth: { apiKey: true },
|
|
106
|
+
params: z.object({ id: z.string() }),
|
|
107
|
+
body: z.object({
|
|
108
|
+
files: z.array(skillFileWithPathSchema).max(100),
|
|
109
|
+
}),
|
|
110
|
+
responses: {
|
|
111
|
+
200: { description: "Skill files upserted" },
|
|
112
|
+
400: { description: "Validation error" },
|
|
113
|
+
404: { description: "Skill not found" },
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const getSkillFileRoute = route({
|
|
118
|
+
method: "get",
|
|
119
|
+
path: "/api/skills/{id}/files/{path}",
|
|
120
|
+
pattern: ["api", "skills", null, "files", null],
|
|
121
|
+
exact: false,
|
|
122
|
+
summary: "Get a bundled skill file",
|
|
123
|
+
tags: ["Skills"],
|
|
124
|
+
auth: { apiKey: true },
|
|
125
|
+
params: z.object({ id: z.string(), path: z.string() }),
|
|
126
|
+
responses: {
|
|
127
|
+
200: { description: "Skill file" },
|
|
128
|
+
404: { description: "Skill or file not found" },
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const upsertSkillFileRoute = route({
|
|
133
|
+
method: "put",
|
|
134
|
+
path: "/api/skills/{id}/files/{path}",
|
|
135
|
+
pattern: ["api", "skills", null, "files", null],
|
|
136
|
+
exact: false,
|
|
137
|
+
summary: "Upsert a bundled skill file",
|
|
138
|
+
tags: ["Skills"],
|
|
139
|
+
auth: { apiKey: true },
|
|
140
|
+
params: z.object({ id: z.string(), path: z.string() }),
|
|
141
|
+
body: skillFileBodySchema,
|
|
142
|
+
responses: {
|
|
143
|
+
200: { description: "Skill file upserted" },
|
|
144
|
+
400: { description: "Validation error" },
|
|
145
|
+
404: { description: "Skill not found" },
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const deleteSkillFileRoute = route({
|
|
150
|
+
method: "delete",
|
|
151
|
+
path: "/api/skills/{id}/files/{path}",
|
|
152
|
+
pattern: ["api", "skills", null, "files", null],
|
|
153
|
+
exact: false,
|
|
154
|
+
summary: "Delete a bundled skill file",
|
|
155
|
+
tags: ["Skills"],
|
|
156
|
+
auth: { apiKey: true },
|
|
157
|
+
params: z.object({ id: z.string(), path: z.string() }),
|
|
158
|
+
responses: {
|
|
159
|
+
200: { description: "Skill file deleted" },
|
|
160
|
+
404: { description: "Skill or file not found" },
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
61
164
|
const createSkillRoute = route({
|
|
62
165
|
method: "post",
|
|
63
166
|
path: "/api/skills",
|
|
@@ -423,6 +526,128 @@ export async function handleSkills(
|
|
|
423
526
|
return true;
|
|
424
527
|
}
|
|
425
528
|
|
|
529
|
+
// GET /api/skills/:id/files
|
|
530
|
+
if (listSkillFilesRoute.match(req.method, pathSegments)) {
|
|
531
|
+
const parsed = await listSkillFilesRoute.parse(req, res, pathSegments, queryParams);
|
|
532
|
+
if (!parsed) return true;
|
|
533
|
+
|
|
534
|
+
const skill = getSkillById(parsed.params.id);
|
|
535
|
+
if (!skill) {
|
|
536
|
+
jsonError(res, "Skill not found", 404);
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const files = listSkillFileManifest(parsed.params.id);
|
|
541
|
+
json(res, { files, total: files.length });
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// POST /api/skills/:id/files
|
|
546
|
+
if (bulkUpsertSkillFilesRoute.match(req.method, pathSegments)) {
|
|
547
|
+
const parsed = await bulkUpsertSkillFilesRoute.parse(req, res, pathSegments, queryParams);
|
|
548
|
+
if (!parsed) return true;
|
|
549
|
+
|
|
550
|
+
const skill = getSkillById(parsed.params.id);
|
|
551
|
+
if (!skill) {
|
|
552
|
+
jsonError(res, "Skill not found", 404);
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
if (skill.systemDefault) {
|
|
556
|
+
jsonError(res, SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE, 403);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
const files = upsertSkillFiles(parsed.params.id, parsed.body.files);
|
|
562
|
+
const updatedSkill = getSkillById(parsed.params.id);
|
|
563
|
+
json(res, { files, total: files.length, skill: updatedSkill });
|
|
564
|
+
} catch (err) {
|
|
565
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to upsert files", 400);
|
|
566
|
+
}
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// GET /api/skills/:id/files/:path
|
|
571
|
+
if (getSkillFileRoute.match(req.method, pathSegments)) {
|
|
572
|
+
const parsed = await getSkillFileRoute.parse(req, res, pathSegments, queryParams);
|
|
573
|
+
if (!parsed) return true;
|
|
574
|
+
|
|
575
|
+
const skill = getSkillById(parsed.params.id);
|
|
576
|
+
if (!skill) {
|
|
577
|
+
jsonError(res, "Skill not found", 404);
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
const file = getSkillFile(parsed.params.id, decodeSkillFilePath(pathSegments));
|
|
583
|
+
if (!file) {
|
|
584
|
+
jsonError(res, "Skill file not found", 404);
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
json(res, { file });
|
|
588
|
+
} catch (err) {
|
|
589
|
+
jsonError(res, err instanceof Error ? err.message : "Invalid file path", 400);
|
|
590
|
+
}
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// PUT /api/skills/:id/files/:path
|
|
595
|
+
if (upsertSkillFileRoute.match(req.method, pathSegments)) {
|
|
596
|
+
const parsed = await upsertSkillFileRoute.parse(req, res, pathSegments, queryParams);
|
|
597
|
+
if (!parsed) return true;
|
|
598
|
+
|
|
599
|
+
const skill = getSkillById(parsed.params.id);
|
|
600
|
+
if (!skill) {
|
|
601
|
+
jsonError(res, "Skill not found", 404);
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
if (skill.systemDefault) {
|
|
605
|
+
jsonError(res, SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE, 403);
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
const file = upsertSkillFile(parsed.params.id, {
|
|
611
|
+
path: decodeSkillFilePath(pathSegments),
|
|
612
|
+
...parsed.body,
|
|
613
|
+
});
|
|
614
|
+
const updatedSkill = getSkillById(parsed.params.id);
|
|
615
|
+
json(res, { file, skill: updatedSkill });
|
|
616
|
+
} catch (err) {
|
|
617
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to upsert file", 400);
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// DELETE /api/skills/:id/files/:path
|
|
623
|
+
if (deleteSkillFileRoute.match(req.method, pathSegments)) {
|
|
624
|
+
const parsed = await deleteSkillFileRoute.parse(req, res, pathSegments, queryParams);
|
|
625
|
+
if (!parsed) return true;
|
|
626
|
+
|
|
627
|
+
const skill = getSkillById(parsed.params.id);
|
|
628
|
+
if (!skill) {
|
|
629
|
+
jsonError(res, "Skill not found", 404);
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
if (skill.systemDefault) {
|
|
633
|
+
jsonError(res, SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE, 403);
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
const deleted = deleteSkillFile(parsed.params.id, decodeSkillFilePath(pathSegments));
|
|
639
|
+
if (!deleted) {
|
|
640
|
+
jsonError(res, "Skill file not found", 404);
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
const updatedSkill = getSkillById(parsed.params.id);
|
|
644
|
+
json(res, { success: true, skill: updatedSkill });
|
|
645
|
+
} catch (err) {
|
|
646
|
+
jsonError(res, err instanceof Error ? err.message : "Invalid file path", 400);
|
|
647
|
+
}
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
|
|
426
651
|
// GET /api/skills/:id
|
|
427
652
|
if (getSkillRoute.match(req.method, pathSegments)) {
|
|
428
653
|
const parsed = await getSkillRoute.parse(req, res, pathSegments, queryParams);
|