@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.1",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "desplega.sh <contact@desplega.sh>",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"e2e:workflows:docker": "bun scripts/e2e-workflow-test.ts --with-docker",
|
|
75
75
|
"docs:mcp": "bun scripts/generate-mcp-docs.ts",
|
|
76
76
|
"docs:openapi": "bun scripts/generate-openapi.ts",
|
|
77
|
+
"docs:business-use": "bun scripts/generate-business-use-docs.ts",
|
|
77
78
|
"pm2-start": "pm2 start ecosystem.config.cjs",
|
|
78
79
|
"pm2-stop": "pm2 stop ecosystem.config.cjs",
|
|
79
80
|
"pm2-restart": "pm2 restart ecosystem.config.cjs",
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
"dependencies": {
|
|
94
95
|
"@ai-sdk/openai": "^3.0.41",
|
|
95
96
|
"@asteasolutions/zod-to-openapi": "^8.0.0",
|
|
97
|
+
"@desplega.ai/business-use": "^0.4.2",
|
|
96
98
|
"@desplega.ai/localtunnel": "^2.2.0",
|
|
97
99
|
"@inkjs/ui": "^2.0.0",
|
|
98
100
|
"@linear/sdk": "^77.0.0",
|
package/src/be/db.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
AgentMemory,
|
|
9
9
|
AgentMemoryScope,
|
|
10
10
|
AgentMemorySource,
|
|
11
|
+
AgentSkill,
|
|
11
12
|
AgentStatus,
|
|
12
13
|
AgentTask,
|
|
13
14
|
AgentTaskSource,
|
|
@@ -32,6 +33,10 @@ import type {
|
|
|
32
33
|
ServiceStatus,
|
|
33
34
|
SessionCost,
|
|
34
35
|
SessionLog,
|
|
36
|
+
Skill,
|
|
37
|
+
SkillScope,
|
|
38
|
+
SkillType,
|
|
39
|
+
SkillWithInstallInfo,
|
|
35
40
|
SwarmConfig,
|
|
36
41
|
SwarmRepo,
|
|
37
42
|
TriggerConfig,
|
|
@@ -935,6 +940,23 @@ export function getTasksByAgentId(agentId: string): AgentTask[] {
|
|
|
935
940
|
return taskQueries.getByAgentId().all(agentId).map(rowToAgentTask);
|
|
936
941
|
}
|
|
937
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Get the most recently updated in-progress task for an agent.
|
|
945
|
+
* Used as a fallback when X-Source-Task-Id header is missing (e.g. lead agent HITL requests).
|
|
946
|
+
*
|
|
947
|
+
* Note: if agent has multiple in-progress tasks, returns the most recently
|
|
948
|
+
* updated one. This is a best-effort fallback — the X-Source-Task-Id header
|
|
949
|
+
* is the authoritative source when available.
|
|
950
|
+
*/
|
|
951
|
+
export function getAgentCurrentTask(agentId: string): AgentTask | null {
|
|
952
|
+
const row = getDb()
|
|
953
|
+
.prepare<AgentTaskRow, [string]>(
|
|
954
|
+
"SELECT * FROM agent_tasks WHERE agentId = ? AND status = 'in_progress' ORDER BY lastUpdatedAt DESC LIMIT 1",
|
|
955
|
+
)
|
|
956
|
+
.get(agentId);
|
|
957
|
+
return row ? rowToAgentTask(row) : null;
|
|
958
|
+
}
|
|
959
|
+
|
|
938
960
|
export function getTasksByStatus(status: AgentTaskStatus): AgentTask[] {
|
|
939
961
|
return taskQueries.getByStatus().all(status).map(rowToAgentTask);
|
|
940
962
|
}
|
|
@@ -6722,3 +6744,650 @@ export function upsertChannelActivityCursor(channelId: string, lastSeenTs: strin
|
|
|
6722
6744
|
)
|
|
6723
6745
|
.run(channelId, lastSeenTs);
|
|
6724
6746
|
}
|
|
6747
|
+
|
|
6748
|
+
// ============================================================================
|
|
6749
|
+
// Approval Requests
|
|
6750
|
+
// ============================================================================
|
|
6751
|
+
|
|
6752
|
+
export interface ApprovalRequest {
|
|
6753
|
+
id: string;
|
|
6754
|
+
title: string;
|
|
6755
|
+
questions: unknown[];
|
|
6756
|
+
workflowRunId: string | null;
|
|
6757
|
+
workflowRunStepId: string | null;
|
|
6758
|
+
sourceTaskId: string | null;
|
|
6759
|
+
approvers: unknown;
|
|
6760
|
+
status: "pending" | "approved" | "rejected" | "timeout";
|
|
6761
|
+
responses: unknown | null;
|
|
6762
|
+
resolvedBy: string | null;
|
|
6763
|
+
resolvedAt: string | null;
|
|
6764
|
+
timeoutSeconds: number | null;
|
|
6765
|
+
expiresAt: string | null;
|
|
6766
|
+
notificationChannels: unknown[] | null;
|
|
6767
|
+
createdAt: string;
|
|
6768
|
+
updatedAt: string;
|
|
6769
|
+
}
|
|
6770
|
+
|
|
6771
|
+
interface ApprovalRequestRow {
|
|
6772
|
+
id: string;
|
|
6773
|
+
title: string;
|
|
6774
|
+
questions: string;
|
|
6775
|
+
workflowRunId: string | null;
|
|
6776
|
+
workflowRunStepId: string | null;
|
|
6777
|
+
sourceTaskId: string | null;
|
|
6778
|
+
approvers: string;
|
|
6779
|
+
status: string;
|
|
6780
|
+
responses: string | null;
|
|
6781
|
+
resolvedBy: string | null;
|
|
6782
|
+
resolvedAt: string | null;
|
|
6783
|
+
timeoutSeconds: number | null;
|
|
6784
|
+
expiresAt: string | null;
|
|
6785
|
+
notificationChannels: string | null;
|
|
6786
|
+
createdAt: string;
|
|
6787
|
+
updatedAt: string;
|
|
6788
|
+
}
|
|
6789
|
+
|
|
6790
|
+
function rowToApprovalRequest(row: ApprovalRequestRow): ApprovalRequest {
|
|
6791
|
+
return {
|
|
6792
|
+
id: row.id,
|
|
6793
|
+
title: row.title,
|
|
6794
|
+
questions: JSON.parse(row.questions),
|
|
6795
|
+
workflowRunId: row.workflowRunId,
|
|
6796
|
+
workflowRunStepId: row.workflowRunStepId,
|
|
6797
|
+
sourceTaskId: row.sourceTaskId,
|
|
6798
|
+
approvers: JSON.parse(row.approvers),
|
|
6799
|
+
status: row.status as ApprovalRequest["status"],
|
|
6800
|
+
responses: row.responses ? JSON.parse(row.responses) : null,
|
|
6801
|
+
resolvedBy: row.resolvedBy,
|
|
6802
|
+
resolvedAt: row.resolvedAt,
|
|
6803
|
+
timeoutSeconds: row.timeoutSeconds,
|
|
6804
|
+
expiresAt: row.expiresAt,
|
|
6805
|
+
notificationChannels: row.notificationChannels ? JSON.parse(row.notificationChannels) : null,
|
|
6806
|
+
createdAt: row.createdAt,
|
|
6807
|
+
updatedAt: row.updatedAt,
|
|
6808
|
+
};
|
|
6809
|
+
}
|
|
6810
|
+
|
|
6811
|
+
export function createApprovalRequest(data: {
|
|
6812
|
+
id: string;
|
|
6813
|
+
title: string;
|
|
6814
|
+
questions: unknown[];
|
|
6815
|
+
approvers: unknown;
|
|
6816
|
+
workflowRunId?: string;
|
|
6817
|
+
workflowRunStepId?: string;
|
|
6818
|
+
sourceTaskId?: string;
|
|
6819
|
+
timeoutSeconds?: number;
|
|
6820
|
+
notificationChannels?: unknown[];
|
|
6821
|
+
}): ApprovalRequest {
|
|
6822
|
+
const now = new Date().toISOString();
|
|
6823
|
+
const expiresAt = data.timeoutSeconds
|
|
6824
|
+
? new Date(Date.now() + data.timeoutSeconds * 1000).toISOString()
|
|
6825
|
+
: null;
|
|
6826
|
+
|
|
6827
|
+
const row = getDb()
|
|
6828
|
+
.prepare<
|
|
6829
|
+
ApprovalRequestRow,
|
|
6830
|
+
[
|
|
6831
|
+
string,
|
|
6832
|
+
string,
|
|
6833
|
+
string,
|
|
6834
|
+
string | null,
|
|
6835
|
+
string | null,
|
|
6836
|
+
string | null,
|
|
6837
|
+
string,
|
|
6838
|
+
number | null,
|
|
6839
|
+
string | null,
|
|
6840
|
+
string | null,
|
|
6841
|
+
string,
|
|
6842
|
+
string,
|
|
6843
|
+
]
|
|
6844
|
+
>(
|
|
6845
|
+
`INSERT INTO approval_requests (id, title, questions, workflowRunId, workflowRunStepId, sourceTaskId, approvers, timeoutSeconds, expiresAt, notificationChannels, createdAt, updatedAt)
|
|
6846
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6847
|
+
RETURNING *`,
|
|
6848
|
+
)
|
|
6849
|
+
.get(
|
|
6850
|
+
data.id,
|
|
6851
|
+
data.title,
|
|
6852
|
+
JSON.stringify(data.questions),
|
|
6853
|
+
data.workflowRunId ?? null,
|
|
6854
|
+
data.workflowRunStepId ?? null,
|
|
6855
|
+
data.sourceTaskId ?? null,
|
|
6856
|
+
JSON.stringify(data.approvers),
|
|
6857
|
+
data.timeoutSeconds ?? null,
|
|
6858
|
+
expiresAt,
|
|
6859
|
+
data.notificationChannels ? JSON.stringify(data.notificationChannels) : null,
|
|
6860
|
+
now,
|
|
6861
|
+
now,
|
|
6862
|
+
);
|
|
6863
|
+
|
|
6864
|
+
return rowToApprovalRequest(row!);
|
|
6865
|
+
}
|
|
6866
|
+
|
|
6867
|
+
export function getApprovalRequestById(id: string): ApprovalRequest | null {
|
|
6868
|
+
const row = getDb()
|
|
6869
|
+
.prepare<ApprovalRequestRow, [string]>("SELECT * FROM approval_requests WHERE id = ?")
|
|
6870
|
+
.get(id);
|
|
6871
|
+
return row ? rowToApprovalRequest(row) : null;
|
|
6872
|
+
}
|
|
6873
|
+
|
|
6874
|
+
export function resolveApprovalRequest(
|
|
6875
|
+
id: string,
|
|
6876
|
+
data: {
|
|
6877
|
+
status: "approved" | "rejected" | "timeout";
|
|
6878
|
+
responses?: unknown;
|
|
6879
|
+
resolvedBy?: string;
|
|
6880
|
+
},
|
|
6881
|
+
): ApprovalRequest | null {
|
|
6882
|
+
const now = new Date().toISOString();
|
|
6883
|
+
const row = getDb()
|
|
6884
|
+
.prepare<ApprovalRequestRow, [string, string | null, string | null, string, string, string]>(
|
|
6885
|
+
`UPDATE approval_requests
|
|
6886
|
+
SET status = ?, responses = ?, resolvedBy = ?, resolvedAt = ?, updatedAt = ?
|
|
6887
|
+
WHERE id = ? AND status = 'pending'
|
|
6888
|
+
RETURNING *`,
|
|
6889
|
+
)
|
|
6890
|
+
.get(
|
|
6891
|
+
data.status,
|
|
6892
|
+
data.responses ? JSON.stringify(data.responses) : null,
|
|
6893
|
+
data.resolvedBy ?? null,
|
|
6894
|
+
now,
|
|
6895
|
+
now,
|
|
6896
|
+
id,
|
|
6897
|
+
);
|
|
6898
|
+
return row ? rowToApprovalRequest(row) : null;
|
|
6899
|
+
}
|
|
6900
|
+
|
|
6901
|
+
export function updateApprovalRequestNotifications(
|
|
6902
|
+
id: string,
|
|
6903
|
+
notificationChannels: Array<{ channel: string; target: string; messageTs?: string }>,
|
|
6904
|
+
): void {
|
|
6905
|
+
const now = new Date().toISOString();
|
|
6906
|
+
getDb()
|
|
6907
|
+
.prepare("UPDATE approval_requests SET notificationChannels = ?, updatedAt = ? WHERE id = ?")
|
|
6908
|
+
.run(JSON.stringify(notificationChannels), now, id);
|
|
6909
|
+
}
|
|
6910
|
+
|
|
6911
|
+
export function listApprovalRequests(filters?: {
|
|
6912
|
+
status?: string;
|
|
6913
|
+
workflowRunId?: string;
|
|
6914
|
+
limit?: number;
|
|
6915
|
+
}): ApprovalRequest[] {
|
|
6916
|
+
const conditions: string[] = [];
|
|
6917
|
+
const params: (string | number)[] = [];
|
|
6918
|
+
|
|
6919
|
+
if (filters?.status) {
|
|
6920
|
+
conditions.push("status = ?");
|
|
6921
|
+
params.push(filters.status);
|
|
6922
|
+
}
|
|
6923
|
+
if (filters?.workflowRunId) {
|
|
6924
|
+
conditions.push("workflowRunId = ?");
|
|
6925
|
+
params.push(filters.workflowRunId);
|
|
6926
|
+
}
|
|
6927
|
+
|
|
6928
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
6929
|
+
const limit = filters?.limit ?? 100;
|
|
6930
|
+
params.push(limit);
|
|
6931
|
+
|
|
6932
|
+
const stmt = getDb().prepare(
|
|
6933
|
+
`SELECT * FROM approval_requests ${where} ORDER BY createdAt DESC LIMIT ?`,
|
|
6934
|
+
);
|
|
6935
|
+
const rows = stmt.all(...params) as ApprovalRequestRow[];
|
|
6936
|
+
|
|
6937
|
+
return rows.map(rowToApprovalRequest);
|
|
6938
|
+
}
|
|
6939
|
+
|
|
6940
|
+
export interface StuckApprovalRun {
|
|
6941
|
+
runId: string;
|
|
6942
|
+
stepId: string;
|
|
6943
|
+
nodeId: string;
|
|
6944
|
+
workflowId: string;
|
|
6945
|
+
approvalId: string;
|
|
6946
|
+
approvalStatus: string;
|
|
6947
|
+
approvalResponses: string | null;
|
|
6948
|
+
expiresAt: string | null;
|
|
6949
|
+
}
|
|
6950
|
+
|
|
6951
|
+
export function getStuckApprovalRuns(): StuckApprovalRun[] {
|
|
6952
|
+
return getDb()
|
|
6953
|
+
.prepare<StuckApprovalRun, []>(
|
|
6954
|
+
`SELECT
|
|
6955
|
+
wr.id as runId,
|
|
6956
|
+
wrs.id as stepId,
|
|
6957
|
+
wrs.nodeId,
|
|
6958
|
+
wr.workflowId,
|
|
6959
|
+
ar.id as approvalId,
|
|
6960
|
+
ar.status as approvalStatus,
|
|
6961
|
+
ar.responses as approvalResponses,
|
|
6962
|
+
ar.expiresAt
|
|
6963
|
+
FROM workflow_runs wr
|
|
6964
|
+
JOIN workflow_run_steps wrs ON wrs.runId = wr.id AND wrs.status = 'waiting'
|
|
6965
|
+
JOIN approval_requests ar ON ar.workflowRunStepId = wrs.id
|
|
6966
|
+
WHERE wr.status = 'waiting'
|
|
6967
|
+
AND (ar.status IN ('approved', 'rejected', 'timeout')
|
|
6968
|
+
OR (ar.status = 'pending' AND ar.expiresAt IS NOT NULL AND ar.expiresAt < datetime('now')))`,
|
|
6969
|
+
)
|
|
6970
|
+
.all();
|
|
6971
|
+
}
|
|
6972
|
+
|
|
6973
|
+
export function getApprovalRequestByStepId(stepId: string): ApprovalRequest | null {
|
|
6974
|
+
const row = getDb()
|
|
6975
|
+
.prepare<ApprovalRequestRow, [string]>(
|
|
6976
|
+
"SELECT * FROM approval_requests WHERE workflowRunStepId = ?",
|
|
6977
|
+
)
|
|
6978
|
+
.get(stepId);
|
|
6979
|
+
return row ? rowToApprovalRequest(row) : null;
|
|
6980
|
+
}
|
|
6981
|
+
|
|
6982
|
+
// TODO: Wire into a periodic cron/sweep to auto-timeout expired approval requests (Phase 2)
|
|
6983
|
+
export function getExpiredPendingApprovals(): ApprovalRequest[] {
|
|
6984
|
+
const rows = getDb()
|
|
6985
|
+
.prepare<ApprovalRequestRow, []>(
|
|
6986
|
+
`SELECT * FROM approval_requests
|
|
6987
|
+
WHERE status = 'pending'
|
|
6988
|
+
AND expiresAt IS NOT NULL
|
|
6989
|
+
AND expiresAt < datetime('now')`,
|
|
6990
|
+
)
|
|
6991
|
+
.all();
|
|
6992
|
+
return rows.map(rowToApprovalRequest);
|
|
6993
|
+
}
|
|
6994
|
+
|
|
6995
|
+
// ============================================================================
|
|
6996
|
+
// Skills
|
|
6997
|
+
// ============================================================================
|
|
6998
|
+
|
|
6999
|
+
type SkillRow = {
|
|
7000
|
+
id: string;
|
|
7001
|
+
name: string;
|
|
7002
|
+
description: string;
|
|
7003
|
+
content: string;
|
|
7004
|
+
type: string;
|
|
7005
|
+
scope: string;
|
|
7006
|
+
ownerAgentId: string | null;
|
|
7007
|
+
sourceUrl: string | null;
|
|
7008
|
+
sourceRepo: string | null;
|
|
7009
|
+
sourcePath: string | null;
|
|
7010
|
+
sourceBranch: string;
|
|
7011
|
+
sourceHash: string | null;
|
|
7012
|
+
isComplex: number;
|
|
7013
|
+
allowedTools: string | null;
|
|
7014
|
+
model: string | null;
|
|
7015
|
+
effort: string | null;
|
|
7016
|
+
context: string | null;
|
|
7017
|
+
agent: string | null;
|
|
7018
|
+
disableModelInvocation: number;
|
|
7019
|
+
userInvocable: number;
|
|
7020
|
+
version: number;
|
|
7021
|
+
isEnabled: number;
|
|
7022
|
+
createdAt: string;
|
|
7023
|
+
lastUpdatedAt: string;
|
|
7024
|
+
lastFetchedAt: string | null;
|
|
7025
|
+
};
|
|
7026
|
+
|
|
7027
|
+
function rowToSkill(row: SkillRow): Skill {
|
|
7028
|
+
return {
|
|
7029
|
+
id: row.id,
|
|
7030
|
+
name: row.name,
|
|
7031
|
+
description: row.description,
|
|
7032
|
+
content: row.content,
|
|
7033
|
+
type: row.type as SkillType,
|
|
7034
|
+
scope: row.scope as SkillScope,
|
|
7035
|
+
ownerAgentId: row.ownerAgentId,
|
|
7036
|
+
sourceUrl: row.sourceUrl,
|
|
7037
|
+
sourceRepo: row.sourceRepo,
|
|
7038
|
+
sourcePath: row.sourcePath,
|
|
7039
|
+
sourceBranch: row.sourceBranch,
|
|
7040
|
+
sourceHash: row.sourceHash,
|
|
7041
|
+
isComplex: row.isComplex === 1,
|
|
7042
|
+
allowedTools: row.allowedTools,
|
|
7043
|
+
model: row.model,
|
|
7044
|
+
effort: row.effort,
|
|
7045
|
+
context: row.context,
|
|
7046
|
+
agent: row.agent,
|
|
7047
|
+
disableModelInvocation: row.disableModelInvocation === 1,
|
|
7048
|
+
userInvocable: row.userInvocable === 1,
|
|
7049
|
+
version: row.version,
|
|
7050
|
+
isEnabled: row.isEnabled === 1,
|
|
7051
|
+
createdAt: row.createdAt,
|
|
7052
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
7053
|
+
lastFetchedAt: row.lastFetchedAt,
|
|
7054
|
+
};
|
|
7055
|
+
}
|
|
7056
|
+
|
|
7057
|
+
type AgentSkillRow = {
|
|
7058
|
+
id: string;
|
|
7059
|
+
agentId: string;
|
|
7060
|
+
skillId: string;
|
|
7061
|
+
isActive: number;
|
|
7062
|
+
installedAt: string;
|
|
7063
|
+
};
|
|
7064
|
+
|
|
7065
|
+
function rowToAgentSkill(row: AgentSkillRow): AgentSkill {
|
|
7066
|
+
return {
|
|
7067
|
+
id: row.id,
|
|
7068
|
+
agentId: row.agentId,
|
|
7069
|
+
skillId: row.skillId,
|
|
7070
|
+
isActive: row.isActive === 1,
|
|
7071
|
+
installedAt: row.installedAt,
|
|
7072
|
+
};
|
|
7073
|
+
}
|
|
7074
|
+
|
|
7075
|
+
type SkillWithInstallRow = SkillRow & { isActive: number; installedAt: string };
|
|
7076
|
+
|
|
7077
|
+
function rowToSkillWithInstall(row: SkillWithInstallRow): SkillWithInstallInfo {
|
|
7078
|
+
return {
|
|
7079
|
+
...rowToSkill(row),
|
|
7080
|
+
isActive: row.isActive === 1,
|
|
7081
|
+
installedAt: row.installedAt,
|
|
7082
|
+
};
|
|
7083
|
+
}
|
|
7084
|
+
|
|
7085
|
+
export interface SkillInsert {
|
|
7086
|
+
name: string;
|
|
7087
|
+
description: string;
|
|
7088
|
+
content: string;
|
|
7089
|
+
type?: SkillType;
|
|
7090
|
+
scope?: SkillScope;
|
|
7091
|
+
ownerAgentId?: string;
|
|
7092
|
+
sourceUrl?: string;
|
|
7093
|
+
sourceRepo?: string;
|
|
7094
|
+
sourcePath?: string;
|
|
7095
|
+
sourceBranch?: string;
|
|
7096
|
+
sourceHash?: string;
|
|
7097
|
+
isComplex?: boolean;
|
|
7098
|
+
allowedTools?: string;
|
|
7099
|
+
model?: string;
|
|
7100
|
+
effort?: string;
|
|
7101
|
+
context?: string;
|
|
7102
|
+
agent?: string;
|
|
7103
|
+
disableModelInvocation?: boolean;
|
|
7104
|
+
userInvocable?: boolean;
|
|
7105
|
+
}
|
|
7106
|
+
|
|
7107
|
+
export function createSkill(data: SkillInsert): Skill {
|
|
7108
|
+
const id = crypto.randomUUID();
|
|
7109
|
+
const now = new Date().toISOString();
|
|
7110
|
+
|
|
7111
|
+
const row = getDb()
|
|
7112
|
+
.prepare<SkillRow, (string | number | null)[]>(
|
|
7113
|
+
`INSERT INTO skills (
|
|
7114
|
+
id, name, description, content, type, scope, ownerAgentId,
|
|
7115
|
+
sourceUrl, sourceRepo, sourcePath, sourceBranch, sourceHash, isComplex,
|
|
7116
|
+
allowedTools, model, effort, context, agent, disableModelInvocation, userInvocable,
|
|
7117
|
+
version, isEnabled, createdAt, lastUpdatedAt
|
|
7118
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?) RETURNING *`,
|
|
7119
|
+
)
|
|
7120
|
+
.get(
|
|
7121
|
+
id,
|
|
7122
|
+
data.name,
|
|
7123
|
+
data.description,
|
|
7124
|
+
data.content,
|
|
7125
|
+
data.type ?? "personal",
|
|
7126
|
+
data.scope ?? "agent",
|
|
7127
|
+
data.ownerAgentId ?? null,
|
|
7128
|
+
data.sourceUrl ?? null,
|
|
7129
|
+
data.sourceRepo ?? null,
|
|
7130
|
+
data.sourcePath ?? null,
|
|
7131
|
+
data.sourceBranch ?? "main",
|
|
7132
|
+
data.sourceHash ?? null,
|
|
7133
|
+
data.isComplex ? 1 : 0,
|
|
7134
|
+
data.allowedTools ?? null,
|
|
7135
|
+
data.model ?? null,
|
|
7136
|
+
data.effort ?? null,
|
|
7137
|
+
data.context ?? null,
|
|
7138
|
+
data.agent ?? null,
|
|
7139
|
+
data.disableModelInvocation ? 1 : 0,
|
|
7140
|
+
data.userInvocable === false ? 0 : 1,
|
|
7141
|
+
now,
|
|
7142
|
+
now,
|
|
7143
|
+
);
|
|
7144
|
+
|
|
7145
|
+
if (!row) throw new Error("Failed to create skill");
|
|
7146
|
+
return rowToSkill(row);
|
|
7147
|
+
}
|
|
7148
|
+
|
|
7149
|
+
export function updateSkill(
|
|
7150
|
+
id: string,
|
|
7151
|
+
updates: Partial<SkillInsert> & { isEnabled?: boolean; lastFetchedAt?: string },
|
|
7152
|
+
): Skill | null {
|
|
7153
|
+
const existing = getSkillById(id);
|
|
7154
|
+
if (!existing) return null;
|
|
7155
|
+
|
|
7156
|
+
const now = new Date().toISOString();
|
|
7157
|
+
const sets: string[] = ["lastUpdatedAt = ?"];
|
|
7158
|
+
const params: (string | number | null)[] = [now];
|
|
7159
|
+
|
|
7160
|
+
if (updates.name !== undefined) {
|
|
7161
|
+
sets.push("name = ?");
|
|
7162
|
+
params.push(updates.name);
|
|
7163
|
+
}
|
|
7164
|
+
if (updates.description !== undefined) {
|
|
7165
|
+
sets.push("description = ?");
|
|
7166
|
+
params.push(updates.description);
|
|
7167
|
+
}
|
|
7168
|
+
if (updates.content !== undefined) {
|
|
7169
|
+
sets.push("content = ?");
|
|
7170
|
+
params.push(updates.content);
|
|
7171
|
+
}
|
|
7172
|
+
if (updates.scope !== undefined) {
|
|
7173
|
+
sets.push("scope = ?");
|
|
7174
|
+
params.push(updates.scope);
|
|
7175
|
+
}
|
|
7176
|
+
if (updates.isEnabled !== undefined) {
|
|
7177
|
+
sets.push("isEnabled = ?");
|
|
7178
|
+
params.push(updates.isEnabled ? 1 : 0);
|
|
7179
|
+
}
|
|
7180
|
+
if (updates.allowedTools !== undefined) {
|
|
7181
|
+
sets.push("allowedTools = ?");
|
|
7182
|
+
params.push(updates.allowedTools ?? null);
|
|
7183
|
+
}
|
|
7184
|
+
if (updates.model !== undefined) {
|
|
7185
|
+
sets.push("model = ?");
|
|
7186
|
+
params.push(updates.model ?? null);
|
|
7187
|
+
}
|
|
7188
|
+
if (updates.effort !== undefined) {
|
|
7189
|
+
sets.push("effort = ?");
|
|
7190
|
+
params.push(updates.effort ?? null);
|
|
7191
|
+
}
|
|
7192
|
+
if (updates.context !== undefined) {
|
|
7193
|
+
sets.push("context = ?");
|
|
7194
|
+
params.push(updates.context ?? null);
|
|
7195
|
+
}
|
|
7196
|
+
if (updates.agent !== undefined) {
|
|
7197
|
+
sets.push("agent = ?");
|
|
7198
|
+
params.push(updates.agent ?? null);
|
|
7199
|
+
}
|
|
7200
|
+
if (updates.disableModelInvocation !== undefined) {
|
|
7201
|
+
sets.push("disableModelInvocation = ?");
|
|
7202
|
+
params.push(updates.disableModelInvocation ? 1 : 0);
|
|
7203
|
+
}
|
|
7204
|
+
if (updates.userInvocable !== undefined) {
|
|
7205
|
+
sets.push("userInvocable = ?");
|
|
7206
|
+
params.push(updates.userInvocable ? 1 : 0);
|
|
7207
|
+
}
|
|
7208
|
+
if (updates.sourceUrl !== undefined) {
|
|
7209
|
+
sets.push("sourceUrl = ?");
|
|
7210
|
+
params.push(updates.sourceUrl ?? null);
|
|
7211
|
+
}
|
|
7212
|
+
if (updates.sourceRepo !== undefined) {
|
|
7213
|
+
sets.push("sourceRepo = ?");
|
|
7214
|
+
params.push(updates.sourceRepo ?? null);
|
|
7215
|
+
}
|
|
7216
|
+
if (updates.sourcePath !== undefined) {
|
|
7217
|
+
sets.push("sourcePath = ?");
|
|
7218
|
+
params.push(updates.sourcePath ?? null);
|
|
7219
|
+
}
|
|
7220
|
+
if (updates.sourceBranch !== undefined) {
|
|
7221
|
+
sets.push("sourceBranch = ?");
|
|
7222
|
+
params.push(updates.sourceBranch ?? "main");
|
|
7223
|
+
}
|
|
7224
|
+
if (updates.sourceHash !== undefined) {
|
|
7225
|
+
sets.push("sourceHash = ?");
|
|
7226
|
+
params.push(updates.sourceHash ?? null);
|
|
7227
|
+
}
|
|
7228
|
+
if (updates.isComplex !== undefined) {
|
|
7229
|
+
sets.push("isComplex = ?");
|
|
7230
|
+
params.push(updates.isComplex ? 1 : 0);
|
|
7231
|
+
}
|
|
7232
|
+
if (updates.lastFetchedAt !== undefined) {
|
|
7233
|
+
sets.push("lastFetchedAt = ?");
|
|
7234
|
+
params.push(updates.lastFetchedAt);
|
|
7235
|
+
}
|
|
7236
|
+
|
|
7237
|
+
// Bump version when content changes
|
|
7238
|
+
if (updates.content !== undefined) {
|
|
7239
|
+
sets.push("version = version + 1");
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7242
|
+
params.push(id);
|
|
7243
|
+
const row = getDb()
|
|
7244
|
+
.prepare<SkillRow, (string | number | null)[]>(
|
|
7245
|
+
`UPDATE skills SET ${sets.join(", ")} WHERE id = ? RETURNING *`,
|
|
7246
|
+
)
|
|
7247
|
+
.get(...params);
|
|
7248
|
+
|
|
7249
|
+
return row ? rowToSkill(row) : null;
|
|
7250
|
+
}
|
|
7251
|
+
|
|
7252
|
+
export function deleteSkill(id: string): boolean {
|
|
7253
|
+
const result = getDb().prepare("DELETE FROM skills WHERE id = ?").run(id);
|
|
7254
|
+
return result.changes > 0;
|
|
7255
|
+
}
|
|
7256
|
+
|
|
7257
|
+
export function getSkillById(id: string): Skill | null {
|
|
7258
|
+
const row = getDb().prepare<SkillRow, [string]>("SELECT * FROM skills WHERE id = ?").get(id);
|
|
7259
|
+
return row ? rowToSkill(row) : null;
|
|
7260
|
+
}
|
|
7261
|
+
|
|
7262
|
+
export function getSkillByName(
|
|
7263
|
+
name: string,
|
|
7264
|
+
scope: SkillScope,
|
|
7265
|
+
ownerAgentId?: string,
|
|
7266
|
+
): Skill | null {
|
|
7267
|
+
const row = getDb()
|
|
7268
|
+
.prepare<SkillRow, [string, string, string]>(
|
|
7269
|
+
"SELECT * FROM skills WHERE name = ? AND scope = ? AND COALESCE(ownerAgentId, '') = ?",
|
|
7270
|
+
)
|
|
7271
|
+
.get(name, scope, ownerAgentId ?? "");
|
|
7272
|
+
return row ? rowToSkill(row) : null;
|
|
7273
|
+
}
|
|
7274
|
+
|
|
7275
|
+
export interface SkillFilters {
|
|
7276
|
+
type?: SkillType;
|
|
7277
|
+
scope?: SkillScope;
|
|
7278
|
+
ownerAgentId?: string;
|
|
7279
|
+
isEnabled?: boolean;
|
|
7280
|
+
search?: string;
|
|
7281
|
+
limit?: number;
|
|
7282
|
+
includeContent?: boolean;
|
|
7283
|
+
}
|
|
7284
|
+
|
|
7285
|
+
export function listSkills(filters?: SkillFilters): Skill[] {
|
|
7286
|
+
const columns =
|
|
7287
|
+
filters?.includeContent === false
|
|
7288
|
+
? "id, name, description, type, scope, ownerAgentId, sourceUrl, sourceRepo, sourcePath, sourceBranch, sourceHash, isComplex, allowedTools, model, effort, context, agent, disableModelInvocation, userInvocable, version, isEnabled, createdAt, lastUpdatedAt, lastFetchedAt, '' as content"
|
|
7289
|
+
: "*";
|
|
7290
|
+
let query = `SELECT ${columns} FROM skills WHERE 1=1`;
|
|
7291
|
+
const params: (string | number)[] = [];
|
|
7292
|
+
|
|
7293
|
+
if (filters?.type) {
|
|
7294
|
+
query += " AND type = ?";
|
|
7295
|
+
params.push(filters.type);
|
|
7296
|
+
}
|
|
7297
|
+
if (filters?.scope) {
|
|
7298
|
+
query += " AND scope = ?";
|
|
7299
|
+
params.push(filters.scope);
|
|
7300
|
+
}
|
|
7301
|
+
if (filters?.ownerAgentId) {
|
|
7302
|
+
query += " AND ownerAgentId = ?";
|
|
7303
|
+
params.push(filters.ownerAgentId);
|
|
7304
|
+
}
|
|
7305
|
+
if (filters?.isEnabled !== undefined) {
|
|
7306
|
+
query += " AND isEnabled = ?";
|
|
7307
|
+
params.push(filters.isEnabled ? 1 : 0);
|
|
7308
|
+
}
|
|
7309
|
+
if (filters?.search) {
|
|
7310
|
+
query += " AND (name LIKE ? OR description LIKE ?)";
|
|
7311
|
+
const term = `%${filters.search}%`;
|
|
7312
|
+
params.push(term, term);
|
|
7313
|
+
}
|
|
7314
|
+
|
|
7315
|
+
query += " ORDER BY name ASC";
|
|
7316
|
+
|
|
7317
|
+
if (filters?.limit) {
|
|
7318
|
+
query += " LIMIT ?";
|
|
7319
|
+
params.push(filters.limit);
|
|
7320
|
+
}
|
|
7321
|
+
|
|
7322
|
+
return getDb()
|
|
7323
|
+
.prepare<SkillRow, (string | number)[]>(query)
|
|
7324
|
+
.all(...params)
|
|
7325
|
+
.map(rowToSkill);
|
|
7326
|
+
}
|
|
7327
|
+
|
|
7328
|
+
export function searchSkills(query: string, limit = 20): Skill[] {
|
|
7329
|
+
const term = `%${query}%`;
|
|
7330
|
+
return getDb()
|
|
7331
|
+
.prepare<SkillRow, [string, string, number]>(
|
|
7332
|
+
"SELECT * FROM skills WHERE (name LIKE ? OR description LIKE ?) AND isEnabled = 1 ORDER BY name ASC LIMIT ?",
|
|
7333
|
+
)
|
|
7334
|
+
.all(term, term, limit)
|
|
7335
|
+
.map(rowToSkill);
|
|
7336
|
+
}
|
|
7337
|
+
|
|
7338
|
+
export function installSkill(agentId: string, skillId: string): AgentSkill {
|
|
7339
|
+
const id = crypto.randomUUID();
|
|
7340
|
+
const now = new Date().toISOString();
|
|
7341
|
+
|
|
7342
|
+
const row = getDb()
|
|
7343
|
+
.prepare<AgentSkillRow, [string, string, string, string]>(
|
|
7344
|
+
`INSERT INTO agent_skills (id, agentId, skillId, isActive, installedAt)
|
|
7345
|
+
VALUES (?, ?, ?, 1, ?)
|
|
7346
|
+
ON CONFLICT(agentId, skillId) DO UPDATE SET isActive = 1
|
|
7347
|
+
RETURNING *`,
|
|
7348
|
+
)
|
|
7349
|
+
.get(id, agentId, skillId, now);
|
|
7350
|
+
|
|
7351
|
+
if (!row) throw new Error("Failed to install skill");
|
|
7352
|
+
return rowToAgentSkill(row);
|
|
7353
|
+
}
|
|
7354
|
+
|
|
7355
|
+
export function uninstallSkill(agentId: string, skillId: string): boolean {
|
|
7356
|
+
const result = getDb()
|
|
7357
|
+
.prepare("DELETE FROM agent_skills WHERE agentId = ? AND skillId = ?")
|
|
7358
|
+
.run(agentId, skillId);
|
|
7359
|
+
return result.changes > 0;
|
|
7360
|
+
}
|
|
7361
|
+
|
|
7362
|
+
export function getAgentSkills(agentId: string, activeOnly = true): SkillWithInstallInfo[] {
|
|
7363
|
+
const query = `
|
|
7364
|
+
SELECT s.*, as2.isActive, as2.installedAt
|
|
7365
|
+
FROM skills s
|
|
7366
|
+
JOIN agent_skills as2 ON s.id = as2.skillId
|
|
7367
|
+
WHERE as2.agentId = ?
|
|
7368
|
+
${activeOnly ? "AND as2.isActive = 1" : ""}
|
|
7369
|
+
AND s.isEnabled = 1
|
|
7370
|
+
ORDER BY
|
|
7371
|
+
CASE WHEN s.type = 'personal' THEN 0 ELSE 1 END,
|
|
7372
|
+
s.name
|
|
7373
|
+
`;
|
|
7374
|
+
|
|
7375
|
+
const rows = getDb().prepare<SkillWithInstallRow, [string]>(query).all(agentId);
|
|
7376
|
+
|
|
7377
|
+
// Deduplicate by name — personal skills take precedence (already sorted first)
|
|
7378
|
+
const seen = new Set<string>();
|
|
7379
|
+
return rows
|
|
7380
|
+
.filter((r) => {
|
|
7381
|
+
if (seen.has(r.name)) return false;
|
|
7382
|
+
seen.add(r.name);
|
|
7383
|
+
return true;
|
|
7384
|
+
})
|
|
7385
|
+
.map(rowToSkillWithInstall);
|
|
7386
|
+
}
|
|
7387
|
+
|
|
7388
|
+
export function toggleAgentSkill(agentId: string, skillId: string, isActive: boolean): boolean {
|
|
7389
|
+
const result = getDb()
|
|
7390
|
+
.prepare("UPDATE agent_skills SET isActive = ? WHERE agentId = ? AND skillId = ?")
|
|
7391
|
+
.run(isActive ? 1 : 0, agentId, skillId);
|
|
7392
|
+
return result.changes > 0;
|
|
7393
|
+
}
|