@desplega.ai/agent-swarm 1.51.2 → 1.52.1
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 +131 -0
- package/openapi.json +767 -4
- package/package.json +3 -1
- package/src/be/db.ts +669 -0
- package/src/be/migrations/019_skills.sql +65 -0
- package/src/be/migrations/020_approval_requests.sql +41 -0
- package/src/be/migrations/runner.ts +4 -4
- package/src/be/skill-parser.ts +70 -0
- package/src/be/skill-sync.ts +106 -0
- package/src/commands/runner.ts +299 -52
- package/src/http/agents.ts +29 -0
- package/src/http/approval-requests.ts +310 -0
- package/src/http/config.ts +3 -3
- package/src/http/index.ts +26 -2
- package/src/http/poll.ts +15 -0
- package/src/http/skills.ts +479 -0
- package/src/http/tasks.ts +94 -0
- package/src/linear/outbound.ts +12 -12
- package/src/prompts/base-prompt.ts +8 -0
- package/src/providers/claude-adapter.ts +19 -3
- package/src/scheduler/scheduler.ts +24 -1
- package/src/server.ts +29 -0
- package/src/slack/blocks.ts +1 -1
- package/src/tests/approval-requests.test.ts +948 -0
- package/src/tests/skill-parser.test.ts +178 -0
- package/src/tests/skill-sync.test.ts +171 -0
- package/src/tests/slack-blocks.test.ts +3 -2
- package/src/tests/structured-output.test.ts +1 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/tool-call-progress.test.ts +207 -0
- package/src/tests/tool-registrar-no-input.test.ts +114 -0
- package/src/tests/update-profile-auth.test.ts +1 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tools/request-human-input.ts +117 -0
- package/src/tools/skills/index.ts +11 -0
- package/src/tools/skills/skill-create.ts +105 -0
- package/src/tools/skills/skill-delete.ts +67 -0
- package/src/tools/skills/skill-get.ts +75 -0
- package/src/tools/skills/skill-install-remote.ts +152 -0
- package/src/tools/skills/skill-install.ts +101 -0
- package/src/tools/skills/skill-list.ts +77 -0
- package/src/tools/skills/skill-publish.ts +123 -0
- package/src/tools/skills/skill-search.ts +43 -0
- package/src/tools/skills/skill-sync-remote.ts +128 -0
- package/src/tools/skills/skill-uninstall.ts +60 -0
- package/src/tools/skills/skill-update.ts +128 -0
- package/src/tools/store-progress.ts +31 -0
- package/src/tools/templates.ts +28 -0
- package/src/tools/tool-config.ts +16 -0
- package/src/tools/utils.ts +9 -7
- package/src/types.ts +54 -0
- package/src/workflows/executors/human-in-the-loop.ts +273 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/recovery.ts +72 -0
- package/src/workflows/resume.ts +65 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- Skills table: stores skill definitions
|
|
2
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
3
|
+
id TEXT PRIMARY KEY,
|
|
4
|
+
name TEXT NOT NULL,
|
|
5
|
+
description TEXT NOT NULL,
|
|
6
|
+
content TEXT NOT NULL DEFAULT '',
|
|
7
|
+
|
|
8
|
+
-- Type & ownership
|
|
9
|
+
type TEXT NOT NULL DEFAULT 'personal'
|
|
10
|
+
CHECK (type IN ('remote', 'personal')),
|
|
11
|
+
scope TEXT NOT NULL DEFAULT 'agent'
|
|
12
|
+
CHECK (scope IN ('global', 'swarm', 'agent')),
|
|
13
|
+
ownerAgentId TEXT,
|
|
14
|
+
|
|
15
|
+
-- Remote skill metadata
|
|
16
|
+
sourceUrl TEXT,
|
|
17
|
+
sourceRepo TEXT,
|
|
18
|
+
sourcePath TEXT,
|
|
19
|
+
sourceBranch TEXT DEFAULT 'main',
|
|
20
|
+
sourceHash TEXT,
|
|
21
|
+
isComplex INTEGER DEFAULT 0,
|
|
22
|
+
|
|
23
|
+
-- Parsed frontmatter cache (denormalized)
|
|
24
|
+
allowedTools TEXT,
|
|
25
|
+
model TEXT,
|
|
26
|
+
effort TEXT,
|
|
27
|
+
context TEXT,
|
|
28
|
+
agent TEXT,
|
|
29
|
+
disableModelInvocation INTEGER DEFAULT 0,
|
|
30
|
+
userInvocable INTEGER DEFAULT 1,
|
|
31
|
+
|
|
32
|
+
-- Metadata
|
|
33
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
34
|
+
isEnabled INTEGER NOT NULL DEFAULT 1,
|
|
35
|
+
|
|
36
|
+
createdAt TEXT NOT NULL,
|
|
37
|
+
lastUpdatedAt TEXT NOT NULL,
|
|
38
|
+
lastFetchedAt TEXT,
|
|
39
|
+
|
|
40
|
+
FOREIGN KEY (ownerAgentId) REFERENCES agents(id)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
-- Unique constraint: name must be unique within scope+owner combination
|
|
44
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_skills_name_scope
|
|
45
|
+
ON skills(name, scope, COALESCE(ownerAgentId, ''));
|
|
46
|
+
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_skills_type ON skills(type);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_skills_scope ON skills(scope);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_skills_owner ON skills(ownerAgentId);
|
|
50
|
+
|
|
51
|
+
-- Agent-skill junction table
|
|
52
|
+
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
agentId TEXT NOT NULL,
|
|
55
|
+
skillId TEXT NOT NULL,
|
|
56
|
+
isActive INTEGER NOT NULL DEFAULT 1,
|
|
57
|
+
installedAt TEXT NOT NULL,
|
|
58
|
+
|
|
59
|
+
FOREIGN KEY (agentId) REFERENCES agents(id),
|
|
60
|
+
FOREIGN KEY (skillId) REFERENCES skills(id) ON DELETE CASCADE,
|
|
61
|
+
UNIQUE(agentId, skillId)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_agent_skills_agent ON agent_skills(agentId);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_agent_skills_skill ON agent_skills(skillId);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- 019_approval_requests.sql
|
|
2
|
+
-- Human-in-the-Loop approval request system for workflow nodes and agent tools
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS approval_requests (
|
|
5
|
+
id TEXT PRIMARY KEY, -- UUID
|
|
6
|
+
title TEXT NOT NULL, -- Display title for the request
|
|
7
|
+
questions JSONB NOT NULL, -- Array of Question objects
|
|
8
|
+
|
|
9
|
+
-- Source context (one of these pairs will be set)
|
|
10
|
+
workflowRunId TEXT, -- FK → workflow_runs.id (if from workflow)
|
|
11
|
+
workflowRunStepId TEXT, -- FK → workflow_run_steps.id (if from workflow)
|
|
12
|
+
sourceTaskId TEXT, -- FK → agent_tasks.id (if from agent tool)
|
|
13
|
+
|
|
14
|
+
-- Approver policy
|
|
15
|
+
approvers JSONB NOT NULL, -- { users: [...], roles: [...], policy: "any"|"all"|{min:N} }
|
|
16
|
+
|
|
17
|
+
-- State
|
|
18
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'timeout')),
|
|
19
|
+
responses JSONB, -- Keyed by question ID
|
|
20
|
+
resolvedBy TEXT, -- user ID of who responded
|
|
21
|
+
resolvedAt DATETIME, -- when resolved
|
|
22
|
+
|
|
23
|
+
-- Timeout
|
|
24
|
+
timeoutSeconds INTEGER, -- NULL = no timeout
|
|
25
|
+
expiresAt DATETIME, -- computed: createdAt + timeoutSeconds
|
|
26
|
+
|
|
27
|
+
-- Notifications sent
|
|
28
|
+
notificationChannels JSONB, -- [{ channel: "slack", target: "...", messageTs: "..." }]
|
|
29
|
+
|
|
30
|
+
-- Metadata
|
|
31
|
+
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
32
|
+
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- For the history UI
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests(status);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_approval_requests_created ON approval_requests(createdAt DESC);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_approval_requests_workflow ON approval_requests(workflowRunId);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_approval_requests_task ON approval_requests(sourceTaskId);
|
|
40
|
+
-- For recovery: find pending requests that may have expired during downtime
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_approval_requests_expires ON approval_requests(expiresAt) WHERE status = 'pending';
|
|
@@ -129,7 +129,7 @@ export function runMigrations(db: Database): void {
|
|
|
129
129
|
const initialMigration = migrations.find((m) => m.version === 1);
|
|
130
130
|
if (initialMigration) {
|
|
131
131
|
if (shouldBootstrapInitialMigration(db)) {
|
|
132
|
-
console.
|
|
132
|
+
console.debug("[migrations] Existing database detected — bootstrapping migration tracking");
|
|
133
133
|
db.run(
|
|
134
134
|
"INSERT INTO _migrations (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
|
|
135
135
|
[
|
|
@@ -145,7 +145,7 @@ export function runMigrations(db: Database): void {
|
|
|
145
145
|
checksum: initialMigration.checksum,
|
|
146
146
|
});
|
|
147
147
|
} else {
|
|
148
|
-
console.
|
|
148
|
+
console.warn(
|
|
149
149
|
"[migrations] Existing database appears incomplete — applying 001_initial migration",
|
|
150
150
|
);
|
|
151
151
|
}
|
|
@@ -169,7 +169,7 @@ export function runMigrations(db: Database): void {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// Apply migration in a transaction
|
|
172
|
-
console.
|
|
172
|
+
console.debug(`[migrations] Applying: ${migration.name}`);
|
|
173
173
|
const start = performance.now();
|
|
174
174
|
|
|
175
175
|
db.transaction(() => {
|
|
@@ -183,6 +183,6 @@ export function runMigrations(db: Database): void {
|
|
|
183
183
|
})();
|
|
184
184
|
|
|
185
185
|
const elapsed = (performance.now() - start).toFixed(1);
|
|
186
|
-
console.
|
|
186
|
+
console.debug(`[migrations] Applied: ${migration.name} (${elapsed}ms)`);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses SKILL.md content (YAML frontmatter + markdown body).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ParsedSkill {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
allowedTools?: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
effort?: string;
|
|
11
|
+
context?: string;
|
|
12
|
+
agent?: string;
|
|
13
|
+
disableModelInvocation?: boolean;
|
|
14
|
+
userInvocable?: boolean;
|
|
15
|
+
body: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse SKILL.md content into structured metadata + body.
|
|
20
|
+
* Frontmatter is delimited by `---` lines at the start.
|
|
21
|
+
*/
|
|
22
|
+
export function parseSkillContent(content: string): ParsedSkill {
|
|
23
|
+
const trimmed = content.trim();
|
|
24
|
+
|
|
25
|
+
if (!trimmed.startsWith("---")) {
|
|
26
|
+
throw new Error("Skill content must start with YAML frontmatter (---). No frontmatter found.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const secondDelimiter = trimmed.indexOf("---", 3);
|
|
30
|
+
if (secondDelimiter === -1) {
|
|
31
|
+
throw new Error("Skill content has unterminated frontmatter. Missing closing ---.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const frontmatterRaw = trimmed.slice(3, secondDelimiter).trim();
|
|
35
|
+
const body = trimmed.slice(secondDelimiter + 3).trim();
|
|
36
|
+
|
|
37
|
+
// Parse simple YAML key-value pairs
|
|
38
|
+
const metadata: Record<string, string> = {};
|
|
39
|
+
for (const line of frontmatterRaw.split("\n")) {
|
|
40
|
+
const colonIdx = line.indexOf(":");
|
|
41
|
+
if (colonIdx === -1) continue;
|
|
42
|
+
const key = line.slice(0, colonIdx).trim();
|
|
43
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
44
|
+
if (key && value) {
|
|
45
|
+
metadata[key] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate required fields
|
|
50
|
+
if (!metadata.name) {
|
|
51
|
+
throw new Error('Skill frontmatter is missing required field: "name"');
|
|
52
|
+
}
|
|
53
|
+
if (!metadata.description) {
|
|
54
|
+
throw new Error('Skill frontmatter is missing required field: "description"');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
name: metadata.name,
|
|
59
|
+
description: metadata.description,
|
|
60
|
+
allowedTools: metadata["allowed-tools"] || undefined,
|
|
61
|
+
model: metadata.model || undefined,
|
|
62
|
+
effort: metadata.effort || undefined,
|
|
63
|
+
context: metadata.context || undefined,
|
|
64
|
+
agent: metadata.agent || undefined,
|
|
65
|
+
disableModelInvocation: metadata["disable-model-invocation"] === "true" ? true : undefined,
|
|
66
|
+
userInvocable:
|
|
67
|
+
metadata["user-invocable"] !== undefined ? metadata["user-invocable"] !== "false" : undefined,
|
|
68
|
+
body,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem sync for skills.
|
|
3
|
+
*
|
|
4
|
+
* Writes installed skills to ~/.claude/skills/<name>/SKILL.md (and optionally
|
|
5
|
+
* ~/.pi/agent/skills/<name>/SKILL.md) so Claude Code and Pi discover them
|
|
6
|
+
* natively.
|
|
7
|
+
*
|
|
8
|
+
* This runs on the API side — workers call it via POST /api/skills/sync-filesystem.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { getAgentSkills } from "./db";
|
|
15
|
+
|
|
16
|
+
export interface SkillSyncResult {
|
|
17
|
+
synced: number;
|
|
18
|
+
removed: number;
|
|
19
|
+
errors: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sync agent's installed skills to the filesystem.
|
|
24
|
+
*
|
|
25
|
+
* For simple skills (content in DB): writes SKILL.md to ~/.claude/skills/<name>/
|
|
26
|
+
* For complex skills (isComplex=true): skipped here (handled by npx in entrypoint)
|
|
27
|
+
*/
|
|
28
|
+
export function syncSkillsToFilesystem(
|
|
29
|
+
agentId: string,
|
|
30
|
+
harnessType: "claude" | "pi" | "both" = "both",
|
|
31
|
+
homeOverride?: string,
|
|
32
|
+
): SkillSyncResult {
|
|
33
|
+
const skills = getAgentSkills(agentId);
|
|
34
|
+
const home = homeOverride ?? homedir();
|
|
35
|
+
const errors: string[] = [];
|
|
36
|
+
let synced = 0;
|
|
37
|
+
|
|
38
|
+
// Directories to write to
|
|
39
|
+
const skillDirs: string[] = [];
|
|
40
|
+
if (harnessType === "claude" || harnessType === "both") {
|
|
41
|
+
skillDirs.push(join(home, ".claude", "skills"));
|
|
42
|
+
}
|
|
43
|
+
if (harnessType === "pi" || harnessType === "both") {
|
|
44
|
+
skillDirs.push(join(home, ".pi", "agent", "skills"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Ensure base dirs exist
|
|
48
|
+
for (const dir of skillDirs) {
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Track which skill names we write (for cleanup)
|
|
53
|
+
const writtenNames = new Set<string>();
|
|
54
|
+
|
|
55
|
+
for (const skill of skills) {
|
|
56
|
+
if (!skill.isActive || !skill.isEnabled) continue;
|
|
57
|
+
if (skill.isComplex) continue; // Complex skills handled by npx
|
|
58
|
+
if (!skill.content) continue;
|
|
59
|
+
|
|
60
|
+
// Sanitize skill name to prevent path traversal (strip /, .., and non-safe chars)
|
|
61
|
+
const safeName = skill.name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
62
|
+
if (!safeName) continue;
|
|
63
|
+
|
|
64
|
+
writtenNames.add(safeName);
|
|
65
|
+
|
|
66
|
+
for (const baseDir of skillDirs) {
|
|
67
|
+
const skillDir = join(baseDir, safeName);
|
|
68
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
mkdirSync(skillDir, { recursive: true });
|
|
72
|
+
writeFileSync(skillFile, skill.content, "utf-8");
|
|
73
|
+
synced++;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
errors.push(
|
|
76
|
+
`${skill.name} -> ${skillDir}: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Cleanup: remove skill directories that are no longer installed
|
|
83
|
+
let removed = 0;
|
|
84
|
+
for (const baseDir of skillDirs) {
|
|
85
|
+
if (!existsSync(baseDir)) continue;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const existing = readdirSync(baseDir, { withFileTypes: true });
|
|
89
|
+
for (const entry of existing) {
|
|
90
|
+
if (entry.isDirectory() && !writtenNames.has(entry.name)) {
|
|
91
|
+
const skillDir = join(baseDir, entry.name);
|
|
92
|
+
try {
|
|
93
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
94
|
+
removed++;
|
|
95
|
+
} catch {
|
|
96
|
+
// Non-fatal — skip cleanup errors
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Non-fatal — skip if we can't read the directory
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { synced, removed, errors };
|
|
106
|
+
}
|