@desplega.ai/agent-swarm 1.51.2 → 1.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.json +767 -4
- package/package.json +1 -1
- package/src/be/db.ts +642 -0
- package/src/be/migrations/019_skills.sql +65 -0
- package/src/be/migrations/020_approval_requests.sql +41 -0
- package/src/be/skill-parser.ts +70 -0
- package/src/be/skill-sync.ts +106 -0
- package/src/commands/runner.ts +136 -41
- package/src/http/approval-requests.ts +247 -0
- package/src/http/config.ts +3 -3
- package/src/http/index.ts +4 -0
- package/src/http/skills.ts +479 -0
- package/src/prompts/base-prompt.ts +8 -0
- package/src/server.ts +29 -0
- package/src/tests/approval-requests.test.ts +735 -0
- package/src/tests/skill-parser.test.ts +178 -0
- package/src/tests/skill-sync.test.ts +171 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tools/request-human-input.ts +106 -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/tool-config.ts +16 -0
- package/src/types.ts +54 -0
- package/src/workflows/executors/human-in-the-loop.ts +160 -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
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,
|
|
@@ -6722,3 +6727,640 @@ export function upsertChannelActivityCursor(channelId: string, lastSeenTs: strin
|
|
|
6722
6727
|
)
|
|
6723
6728
|
.run(channelId, lastSeenTs);
|
|
6724
6729
|
}
|
|
6730
|
+
|
|
6731
|
+
// ============================================================================
|
|
6732
|
+
// Approval Requests
|
|
6733
|
+
// ============================================================================
|
|
6734
|
+
|
|
6735
|
+
export interface ApprovalRequest {
|
|
6736
|
+
id: string;
|
|
6737
|
+
title: string;
|
|
6738
|
+
questions: unknown[];
|
|
6739
|
+
workflowRunId: string | null;
|
|
6740
|
+
workflowRunStepId: string | null;
|
|
6741
|
+
sourceTaskId: string | null;
|
|
6742
|
+
approvers: unknown;
|
|
6743
|
+
status: "pending" | "approved" | "rejected" | "timeout";
|
|
6744
|
+
responses: unknown | null;
|
|
6745
|
+
resolvedBy: string | null;
|
|
6746
|
+
resolvedAt: string | null;
|
|
6747
|
+
timeoutSeconds: number | null;
|
|
6748
|
+
expiresAt: string | null;
|
|
6749
|
+
notificationChannels: unknown[] | null;
|
|
6750
|
+
createdAt: string;
|
|
6751
|
+
updatedAt: string;
|
|
6752
|
+
}
|
|
6753
|
+
|
|
6754
|
+
interface ApprovalRequestRow {
|
|
6755
|
+
id: string;
|
|
6756
|
+
title: string;
|
|
6757
|
+
questions: string;
|
|
6758
|
+
workflowRunId: string | null;
|
|
6759
|
+
workflowRunStepId: string | null;
|
|
6760
|
+
sourceTaskId: string | null;
|
|
6761
|
+
approvers: string;
|
|
6762
|
+
status: string;
|
|
6763
|
+
responses: string | null;
|
|
6764
|
+
resolvedBy: string | null;
|
|
6765
|
+
resolvedAt: string | null;
|
|
6766
|
+
timeoutSeconds: number | null;
|
|
6767
|
+
expiresAt: string | null;
|
|
6768
|
+
notificationChannels: string | null;
|
|
6769
|
+
createdAt: string;
|
|
6770
|
+
updatedAt: string;
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
function rowToApprovalRequest(row: ApprovalRequestRow): ApprovalRequest {
|
|
6774
|
+
return {
|
|
6775
|
+
id: row.id,
|
|
6776
|
+
title: row.title,
|
|
6777
|
+
questions: JSON.parse(row.questions),
|
|
6778
|
+
workflowRunId: row.workflowRunId,
|
|
6779
|
+
workflowRunStepId: row.workflowRunStepId,
|
|
6780
|
+
sourceTaskId: row.sourceTaskId,
|
|
6781
|
+
approvers: JSON.parse(row.approvers),
|
|
6782
|
+
status: row.status as ApprovalRequest["status"],
|
|
6783
|
+
responses: row.responses ? JSON.parse(row.responses) : null,
|
|
6784
|
+
resolvedBy: row.resolvedBy,
|
|
6785
|
+
resolvedAt: row.resolvedAt,
|
|
6786
|
+
timeoutSeconds: row.timeoutSeconds,
|
|
6787
|
+
expiresAt: row.expiresAt,
|
|
6788
|
+
notificationChannels: row.notificationChannels ? JSON.parse(row.notificationChannels) : null,
|
|
6789
|
+
createdAt: row.createdAt,
|
|
6790
|
+
updatedAt: row.updatedAt,
|
|
6791
|
+
};
|
|
6792
|
+
}
|
|
6793
|
+
|
|
6794
|
+
export function createApprovalRequest(data: {
|
|
6795
|
+
id: string;
|
|
6796
|
+
title: string;
|
|
6797
|
+
questions: unknown[];
|
|
6798
|
+
approvers: unknown;
|
|
6799
|
+
workflowRunId?: string;
|
|
6800
|
+
workflowRunStepId?: string;
|
|
6801
|
+
sourceTaskId?: string;
|
|
6802
|
+
timeoutSeconds?: number;
|
|
6803
|
+
notificationChannels?: unknown[];
|
|
6804
|
+
}): ApprovalRequest {
|
|
6805
|
+
const now = new Date().toISOString();
|
|
6806
|
+
const expiresAt = data.timeoutSeconds
|
|
6807
|
+
? new Date(Date.now() + data.timeoutSeconds * 1000).toISOString()
|
|
6808
|
+
: null;
|
|
6809
|
+
|
|
6810
|
+
const row = getDb()
|
|
6811
|
+
.prepare<
|
|
6812
|
+
ApprovalRequestRow,
|
|
6813
|
+
[
|
|
6814
|
+
string,
|
|
6815
|
+
string,
|
|
6816
|
+
string,
|
|
6817
|
+
string | null,
|
|
6818
|
+
string | null,
|
|
6819
|
+
string | null,
|
|
6820
|
+
string,
|
|
6821
|
+
number | null,
|
|
6822
|
+
string | null,
|
|
6823
|
+
string | null,
|
|
6824
|
+
string,
|
|
6825
|
+
string,
|
|
6826
|
+
]
|
|
6827
|
+
>(
|
|
6828
|
+
`INSERT INTO approval_requests (id, title, questions, workflowRunId, workflowRunStepId, sourceTaskId, approvers, timeoutSeconds, expiresAt, notificationChannels, createdAt, updatedAt)
|
|
6829
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6830
|
+
RETURNING *`,
|
|
6831
|
+
)
|
|
6832
|
+
.get(
|
|
6833
|
+
data.id,
|
|
6834
|
+
data.title,
|
|
6835
|
+
JSON.stringify(data.questions),
|
|
6836
|
+
data.workflowRunId ?? null,
|
|
6837
|
+
data.workflowRunStepId ?? null,
|
|
6838
|
+
data.sourceTaskId ?? null,
|
|
6839
|
+
JSON.stringify(data.approvers),
|
|
6840
|
+
data.timeoutSeconds ?? null,
|
|
6841
|
+
expiresAt,
|
|
6842
|
+
data.notificationChannels ? JSON.stringify(data.notificationChannels) : null,
|
|
6843
|
+
now,
|
|
6844
|
+
now,
|
|
6845
|
+
);
|
|
6846
|
+
|
|
6847
|
+
return rowToApprovalRequest(row!);
|
|
6848
|
+
}
|
|
6849
|
+
|
|
6850
|
+
export function getApprovalRequestById(id: string): ApprovalRequest | null {
|
|
6851
|
+
const row = getDb()
|
|
6852
|
+
.prepare<ApprovalRequestRow, [string]>("SELECT * FROM approval_requests WHERE id = ?")
|
|
6853
|
+
.get(id);
|
|
6854
|
+
return row ? rowToApprovalRequest(row) : null;
|
|
6855
|
+
}
|
|
6856
|
+
|
|
6857
|
+
export function resolveApprovalRequest(
|
|
6858
|
+
id: string,
|
|
6859
|
+
data: {
|
|
6860
|
+
status: "approved" | "rejected" | "timeout";
|
|
6861
|
+
responses?: unknown;
|
|
6862
|
+
resolvedBy?: string;
|
|
6863
|
+
},
|
|
6864
|
+
): ApprovalRequest | null {
|
|
6865
|
+
const now = new Date().toISOString();
|
|
6866
|
+
const row = getDb()
|
|
6867
|
+
.prepare<ApprovalRequestRow, [string, string | null, string | null, string, string, string]>(
|
|
6868
|
+
`UPDATE approval_requests
|
|
6869
|
+
SET status = ?, responses = ?, resolvedBy = ?, resolvedAt = ?, updatedAt = ?
|
|
6870
|
+
WHERE id = ? AND status = 'pending'
|
|
6871
|
+
RETURNING *`,
|
|
6872
|
+
)
|
|
6873
|
+
.get(
|
|
6874
|
+
data.status,
|
|
6875
|
+
data.responses ? JSON.stringify(data.responses) : null,
|
|
6876
|
+
data.resolvedBy ?? null,
|
|
6877
|
+
now,
|
|
6878
|
+
now,
|
|
6879
|
+
id,
|
|
6880
|
+
);
|
|
6881
|
+
return row ? rowToApprovalRequest(row) : null;
|
|
6882
|
+
}
|
|
6883
|
+
|
|
6884
|
+
export function listApprovalRequests(filters?: {
|
|
6885
|
+
status?: string;
|
|
6886
|
+
workflowRunId?: string;
|
|
6887
|
+
limit?: number;
|
|
6888
|
+
}): ApprovalRequest[] {
|
|
6889
|
+
const conditions: string[] = [];
|
|
6890
|
+
const params: (string | number)[] = [];
|
|
6891
|
+
|
|
6892
|
+
if (filters?.status) {
|
|
6893
|
+
conditions.push("status = ?");
|
|
6894
|
+
params.push(filters.status);
|
|
6895
|
+
}
|
|
6896
|
+
if (filters?.workflowRunId) {
|
|
6897
|
+
conditions.push("workflowRunId = ?");
|
|
6898
|
+
params.push(filters.workflowRunId);
|
|
6899
|
+
}
|
|
6900
|
+
|
|
6901
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
6902
|
+
const limit = filters?.limit ?? 100;
|
|
6903
|
+
params.push(limit);
|
|
6904
|
+
|
|
6905
|
+
const stmt = getDb().prepare(
|
|
6906
|
+
`SELECT * FROM approval_requests ${where} ORDER BY createdAt DESC LIMIT ?`,
|
|
6907
|
+
);
|
|
6908
|
+
const rows = stmt.all(...params) as ApprovalRequestRow[];
|
|
6909
|
+
|
|
6910
|
+
return rows.map(rowToApprovalRequest);
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6913
|
+
export interface StuckApprovalRun {
|
|
6914
|
+
runId: string;
|
|
6915
|
+
stepId: string;
|
|
6916
|
+
nodeId: string;
|
|
6917
|
+
workflowId: string;
|
|
6918
|
+
approvalId: string;
|
|
6919
|
+
approvalStatus: string;
|
|
6920
|
+
approvalResponses: string | null;
|
|
6921
|
+
expiresAt: string | null;
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
export function getStuckApprovalRuns(): StuckApprovalRun[] {
|
|
6925
|
+
return getDb()
|
|
6926
|
+
.prepare<StuckApprovalRun, []>(
|
|
6927
|
+
`SELECT
|
|
6928
|
+
wr.id as runId,
|
|
6929
|
+
wrs.id as stepId,
|
|
6930
|
+
wrs.nodeId,
|
|
6931
|
+
wr.workflowId,
|
|
6932
|
+
ar.id as approvalId,
|
|
6933
|
+
ar.status as approvalStatus,
|
|
6934
|
+
ar.responses as approvalResponses,
|
|
6935
|
+
ar.expiresAt
|
|
6936
|
+
FROM workflow_runs wr
|
|
6937
|
+
JOIN workflow_run_steps wrs ON wrs.runId = wr.id AND wrs.status = 'waiting'
|
|
6938
|
+
JOIN approval_requests ar ON ar.workflowRunStepId = wrs.id
|
|
6939
|
+
WHERE wr.status = 'waiting'
|
|
6940
|
+
AND (ar.status IN ('approved', 'rejected', 'timeout')
|
|
6941
|
+
OR (ar.status = 'pending' AND ar.expiresAt IS NOT NULL AND ar.expiresAt < datetime('now')))`,
|
|
6942
|
+
)
|
|
6943
|
+
.all();
|
|
6944
|
+
}
|
|
6945
|
+
|
|
6946
|
+
export function getApprovalRequestByStepId(stepId: string): ApprovalRequest | null {
|
|
6947
|
+
const row = getDb()
|
|
6948
|
+
.prepare<ApprovalRequestRow, [string]>(
|
|
6949
|
+
"SELECT * FROM approval_requests WHERE workflowRunStepId = ?",
|
|
6950
|
+
)
|
|
6951
|
+
.get(stepId);
|
|
6952
|
+
return row ? rowToApprovalRequest(row) : null;
|
|
6953
|
+
}
|
|
6954
|
+
|
|
6955
|
+
// TODO: Wire into a periodic cron/sweep to auto-timeout expired approval requests (Phase 2)
|
|
6956
|
+
export function getExpiredPendingApprovals(): ApprovalRequest[] {
|
|
6957
|
+
const rows = getDb()
|
|
6958
|
+
.prepare<ApprovalRequestRow, []>(
|
|
6959
|
+
`SELECT * FROM approval_requests
|
|
6960
|
+
WHERE status = 'pending'
|
|
6961
|
+
AND expiresAt IS NOT NULL
|
|
6962
|
+
AND expiresAt < datetime('now')`,
|
|
6963
|
+
)
|
|
6964
|
+
.all();
|
|
6965
|
+
return rows.map(rowToApprovalRequest);
|
|
6966
|
+
}
|
|
6967
|
+
|
|
6968
|
+
// ============================================================================
|
|
6969
|
+
// Skills
|
|
6970
|
+
// ============================================================================
|
|
6971
|
+
|
|
6972
|
+
type SkillRow = {
|
|
6973
|
+
id: string;
|
|
6974
|
+
name: string;
|
|
6975
|
+
description: string;
|
|
6976
|
+
content: string;
|
|
6977
|
+
type: string;
|
|
6978
|
+
scope: string;
|
|
6979
|
+
ownerAgentId: string | null;
|
|
6980
|
+
sourceUrl: string | null;
|
|
6981
|
+
sourceRepo: string | null;
|
|
6982
|
+
sourcePath: string | null;
|
|
6983
|
+
sourceBranch: string;
|
|
6984
|
+
sourceHash: string | null;
|
|
6985
|
+
isComplex: number;
|
|
6986
|
+
allowedTools: string | null;
|
|
6987
|
+
model: string | null;
|
|
6988
|
+
effort: string | null;
|
|
6989
|
+
context: string | null;
|
|
6990
|
+
agent: string | null;
|
|
6991
|
+
disableModelInvocation: number;
|
|
6992
|
+
userInvocable: number;
|
|
6993
|
+
version: number;
|
|
6994
|
+
isEnabled: number;
|
|
6995
|
+
createdAt: string;
|
|
6996
|
+
lastUpdatedAt: string;
|
|
6997
|
+
lastFetchedAt: string | null;
|
|
6998
|
+
};
|
|
6999
|
+
|
|
7000
|
+
function rowToSkill(row: SkillRow): Skill {
|
|
7001
|
+
return {
|
|
7002
|
+
id: row.id,
|
|
7003
|
+
name: row.name,
|
|
7004
|
+
description: row.description,
|
|
7005
|
+
content: row.content,
|
|
7006
|
+
type: row.type as SkillType,
|
|
7007
|
+
scope: row.scope as SkillScope,
|
|
7008
|
+
ownerAgentId: row.ownerAgentId,
|
|
7009
|
+
sourceUrl: row.sourceUrl,
|
|
7010
|
+
sourceRepo: row.sourceRepo,
|
|
7011
|
+
sourcePath: row.sourcePath,
|
|
7012
|
+
sourceBranch: row.sourceBranch,
|
|
7013
|
+
sourceHash: row.sourceHash,
|
|
7014
|
+
isComplex: row.isComplex === 1,
|
|
7015
|
+
allowedTools: row.allowedTools,
|
|
7016
|
+
model: row.model,
|
|
7017
|
+
effort: row.effort,
|
|
7018
|
+
context: row.context,
|
|
7019
|
+
agent: row.agent,
|
|
7020
|
+
disableModelInvocation: row.disableModelInvocation === 1,
|
|
7021
|
+
userInvocable: row.userInvocable === 1,
|
|
7022
|
+
version: row.version,
|
|
7023
|
+
isEnabled: row.isEnabled === 1,
|
|
7024
|
+
createdAt: row.createdAt,
|
|
7025
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
7026
|
+
lastFetchedAt: row.lastFetchedAt,
|
|
7027
|
+
};
|
|
7028
|
+
}
|
|
7029
|
+
|
|
7030
|
+
type AgentSkillRow = {
|
|
7031
|
+
id: string;
|
|
7032
|
+
agentId: string;
|
|
7033
|
+
skillId: string;
|
|
7034
|
+
isActive: number;
|
|
7035
|
+
installedAt: string;
|
|
7036
|
+
};
|
|
7037
|
+
|
|
7038
|
+
function rowToAgentSkill(row: AgentSkillRow): AgentSkill {
|
|
7039
|
+
return {
|
|
7040
|
+
id: row.id,
|
|
7041
|
+
agentId: row.agentId,
|
|
7042
|
+
skillId: row.skillId,
|
|
7043
|
+
isActive: row.isActive === 1,
|
|
7044
|
+
installedAt: row.installedAt,
|
|
7045
|
+
};
|
|
7046
|
+
}
|
|
7047
|
+
|
|
7048
|
+
type SkillWithInstallRow = SkillRow & { isActive: number; installedAt: string };
|
|
7049
|
+
|
|
7050
|
+
function rowToSkillWithInstall(row: SkillWithInstallRow): SkillWithInstallInfo {
|
|
7051
|
+
return {
|
|
7052
|
+
...rowToSkill(row),
|
|
7053
|
+
isActive: row.isActive === 1,
|
|
7054
|
+
installedAt: row.installedAt,
|
|
7055
|
+
};
|
|
7056
|
+
}
|
|
7057
|
+
|
|
7058
|
+
export interface SkillInsert {
|
|
7059
|
+
name: string;
|
|
7060
|
+
description: string;
|
|
7061
|
+
content: string;
|
|
7062
|
+
type?: SkillType;
|
|
7063
|
+
scope?: SkillScope;
|
|
7064
|
+
ownerAgentId?: string;
|
|
7065
|
+
sourceUrl?: string;
|
|
7066
|
+
sourceRepo?: string;
|
|
7067
|
+
sourcePath?: string;
|
|
7068
|
+
sourceBranch?: string;
|
|
7069
|
+
sourceHash?: string;
|
|
7070
|
+
isComplex?: boolean;
|
|
7071
|
+
allowedTools?: string;
|
|
7072
|
+
model?: string;
|
|
7073
|
+
effort?: string;
|
|
7074
|
+
context?: string;
|
|
7075
|
+
agent?: string;
|
|
7076
|
+
disableModelInvocation?: boolean;
|
|
7077
|
+
userInvocable?: boolean;
|
|
7078
|
+
}
|
|
7079
|
+
|
|
7080
|
+
export function createSkill(data: SkillInsert): Skill {
|
|
7081
|
+
const id = crypto.randomUUID();
|
|
7082
|
+
const now = new Date().toISOString();
|
|
7083
|
+
|
|
7084
|
+
const row = getDb()
|
|
7085
|
+
.prepare<SkillRow, (string | number | null)[]>(
|
|
7086
|
+
`INSERT INTO skills (
|
|
7087
|
+
id, name, description, content, type, scope, ownerAgentId,
|
|
7088
|
+
sourceUrl, sourceRepo, sourcePath, sourceBranch, sourceHash, isComplex,
|
|
7089
|
+
allowedTools, model, effort, context, agent, disableModelInvocation, userInvocable,
|
|
7090
|
+
version, isEnabled, createdAt, lastUpdatedAt
|
|
7091
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?) RETURNING *`,
|
|
7092
|
+
)
|
|
7093
|
+
.get(
|
|
7094
|
+
id,
|
|
7095
|
+
data.name,
|
|
7096
|
+
data.description,
|
|
7097
|
+
data.content,
|
|
7098
|
+
data.type ?? "personal",
|
|
7099
|
+
data.scope ?? "agent",
|
|
7100
|
+
data.ownerAgentId ?? null,
|
|
7101
|
+
data.sourceUrl ?? null,
|
|
7102
|
+
data.sourceRepo ?? null,
|
|
7103
|
+
data.sourcePath ?? null,
|
|
7104
|
+
data.sourceBranch ?? "main",
|
|
7105
|
+
data.sourceHash ?? null,
|
|
7106
|
+
data.isComplex ? 1 : 0,
|
|
7107
|
+
data.allowedTools ?? null,
|
|
7108
|
+
data.model ?? null,
|
|
7109
|
+
data.effort ?? null,
|
|
7110
|
+
data.context ?? null,
|
|
7111
|
+
data.agent ?? null,
|
|
7112
|
+
data.disableModelInvocation ? 1 : 0,
|
|
7113
|
+
data.userInvocable === false ? 0 : 1,
|
|
7114
|
+
now,
|
|
7115
|
+
now,
|
|
7116
|
+
);
|
|
7117
|
+
|
|
7118
|
+
if (!row) throw new Error("Failed to create skill");
|
|
7119
|
+
return rowToSkill(row);
|
|
7120
|
+
}
|
|
7121
|
+
|
|
7122
|
+
export function updateSkill(
|
|
7123
|
+
id: string,
|
|
7124
|
+
updates: Partial<SkillInsert> & { isEnabled?: boolean; lastFetchedAt?: string },
|
|
7125
|
+
): Skill | null {
|
|
7126
|
+
const existing = getSkillById(id);
|
|
7127
|
+
if (!existing) return null;
|
|
7128
|
+
|
|
7129
|
+
const now = new Date().toISOString();
|
|
7130
|
+
const sets: string[] = ["lastUpdatedAt = ?"];
|
|
7131
|
+
const params: (string | number | null)[] = [now];
|
|
7132
|
+
|
|
7133
|
+
if (updates.name !== undefined) {
|
|
7134
|
+
sets.push("name = ?");
|
|
7135
|
+
params.push(updates.name);
|
|
7136
|
+
}
|
|
7137
|
+
if (updates.description !== undefined) {
|
|
7138
|
+
sets.push("description = ?");
|
|
7139
|
+
params.push(updates.description);
|
|
7140
|
+
}
|
|
7141
|
+
if (updates.content !== undefined) {
|
|
7142
|
+
sets.push("content = ?");
|
|
7143
|
+
params.push(updates.content);
|
|
7144
|
+
}
|
|
7145
|
+
if (updates.scope !== undefined) {
|
|
7146
|
+
sets.push("scope = ?");
|
|
7147
|
+
params.push(updates.scope);
|
|
7148
|
+
}
|
|
7149
|
+
if (updates.isEnabled !== undefined) {
|
|
7150
|
+
sets.push("isEnabled = ?");
|
|
7151
|
+
params.push(updates.isEnabled ? 1 : 0);
|
|
7152
|
+
}
|
|
7153
|
+
if (updates.allowedTools !== undefined) {
|
|
7154
|
+
sets.push("allowedTools = ?");
|
|
7155
|
+
params.push(updates.allowedTools ?? null);
|
|
7156
|
+
}
|
|
7157
|
+
if (updates.model !== undefined) {
|
|
7158
|
+
sets.push("model = ?");
|
|
7159
|
+
params.push(updates.model ?? null);
|
|
7160
|
+
}
|
|
7161
|
+
if (updates.effort !== undefined) {
|
|
7162
|
+
sets.push("effort = ?");
|
|
7163
|
+
params.push(updates.effort ?? null);
|
|
7164
|
+
}
|
|
7165
|
+
if (updates.context !== undefined) {
|
|
7166
|
+
sets.push("context = ?");
|
|
7167
|
+
params.push(updates.context ?? null);
|
|
7168
|
+
}
|
|
7169
|
+
if (updates.agent !== undefined) {
|
|
7170
|
+
sets.push("agent = ?");
|
|
7171
|
+
params.push(updates.agent ?? null);
|
|
7172
|
+
}
|
|
7173
|
+
if (updates.disableModelInvocation !== undefined) {
|
|
7174
|
+
sets.push("disableModelInvocation = ?");
|
|
7175
|
+
params.push(updates.disableModelInvocation ? 1 : 0);
|
|
7176
|
+
}
|
|
7177
|
+
if (updates.userInvocable !== undefined) {
|
|
7178
|
+
sets.push("userInvocable = ?");
|
|
7179
|
+
params.push(updates.userInvocable ? 1 : 0);
|
|
7180
|
+
}
|
|
7181
|
+
if (updates.sourceUrl !== undefined) {
|
|
7182
|
+
sets.push("sourceUrl = ?");
|
|
7183
|
+
params.push(updates.sourceUrl ?? null);
|
|
7184
|
+
}
|
|
7185
|
+
if (updates.sourceRepo !== undefined) {
|
|
7186
|
+
sets.push("sourceRepo = ?");
|
|
7187
|
+
params.push(updates.sourceRepo ?? null);
|
|
7188
|
+
}
|
|
7189
|
+
if (updates.sourcePath !== undefined) {
|
|
7190
|
+
sets.push("sourcePath = ?");
|
|
7191
|
+
params.push(updates.sourcePath ?? null);
|
|
7192
|
+
}
|
|
7193
|
+
if (updates.sourceBranch !== undefined) {
|
|
7194
|
+
sets.push("sourceBranch = ?");
|
|
7195
|
+
params.push(updates.sourceBranch ?? "main");
|
|
7196
|
+
}
|
|
7197
|
+
if (updates.sourceHash !== undefined) {
|
|
7198
|
+
sets.push("sourceHash = ?");
|
|
7199
|
+
params.push(updates.sourceHash ?? null);
|
|
7200
|
+
}
|
|
7201
|
+
if (updates.isComplex !== undefined) {
|
|
7202
|
+
sets.push("isComplex = ?");
|
|
7203
|
+
params.push(updates.isComplex ? 1 : 0);
|
|
7204
|
+
}
|
|
7205
|
+
if (updates.lastFetchedAt !== undefined) {
|
|
7206
|
+
sets.push("lastFetchedAt = ?");
|
|
7207
|
+
params.push(updates.lastFetchedAt);
|
|
7208
|
+
}
|
|
7209
|
+
|
|
7210
|
+
// Bump version when content changes
|
|
7211
|
+
if (updates.content !== undefined) {
|
|
7212
|
+
sets.push("version = version + 1");
|
|
7213
|
+
}
|
|
7214
|
+
|
|
7215
|
+
params.push(id);
|
|
7216
|
+
const row = getDb()
|
|
7217
|
+
.prepare<SkillRow, (string | number | null)[]>(
|
|
7218
|
+
`UPDATE skills SET ${sets.join(", ")} WHERE id = ? RETURNING *`,
|
|
7219
|
+
)
|
|
7220
|
+
.get(...params);
|
|
7221
|
+
|
|
7222
|
+
return row ? rowToSkill(row) : null;
|
|
7223
|
+
}
|
|
7224
|
+
|
|
7225
|
+
export function deleteSkill(id: string): boolean {
|
|
7226
|
+
const result = getDb().prepare("DELETE FROM skills WHERE id = ?").run(id);
|
|
7227
|
+
return result.changes > 0;
|
|
7228
|
+
}
|
|
7229
|
+
|
|
7230
|
+
export function getSkillById(id: string): Skill | null {
|
|
7231
|
+
const row = getDb().prepare<SkillRow, [string]>("SELECT * FROM skills WHERE id = ?").get(id);
|
|
7232
|
+
return row ? rowToSkill(row) : null;
|
|
7233
|
+
}
|
|
7234
|
+
|
|
7235
|
+
export function getSkillByName(
|
|
7236
|
+
name: string,
|
|
7237
|
+
scope: SkillScope,
|
|
7238
|
+
ownerAgentId?: string,
|
|
7239
|
+
): Skill | null {
|
|
7240
|
+
const row = getDb()
|
|
7241
|
+
.prepare<SkillRow, [string, string, string]>(
|
|
7242
|
+
"SELECT * FROM skills WHERE name = ? AND scope = ? AND COALESCE(ownerAgentId, '') = ?",
|
|
7243
|
+
)
|
|
7244
|
+
.get(name, scope, ownerAgentId ?? "");
|
|
7245
|
+
return row ? rowToSkill(row) : null;
|
|
7246
|
+
}
|
|
7247
|
+
|
|
7248
|
+
export interface SkillFilters {
|
|
7249
|
+
type?: SkillType;
|
|
7250
|
+
scope?: SkillScope;
|
|
7251
|
+
ownerAgentId?: string;
|
|
7252
|
+
isEnabled?: boolean;
|
|
7253
|
+
search?: string;
|
|
7254
|
+
limit?: number;
|
|
7255
|
+
includeContent?: boolean;
|
|
7256
|
+
}
|
|
7257
|
+
|
|
7258
|
+
export function listSkills(filters?: SkillFilters): Skill[] {
|
|
7259
|
+
const columns =
|
|
7260
|
+
filters?.includeContent === false
|
|
7261
|
+
? "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"
|
|
7262
|
+
: "*";
|
|
7263
|
+
let query = `SELECT ${columns} FROM skills WHERE 1=1`;
|
|
7264
|
+
const params: (string | number)[] = [];
|
|
7265
|
+
|
|
7266
|
+
if (filters?.type) {
|
|
7267
|
+
query += " AND type = ?";
|
|
7268
|
+
params.push(filters.type);
|
|
7269
|
+
}
|
|
7270
|
+
if (filters?.scope) {
|
|
7271
|
+
query += " AND scope = ?";
|
|
7272
|
+
params.push(filters.scope);
|
|
7273
|
+
}
|
|
7274
|
+
if (filters?.ownerAgentId) {
|
|
7275
|
+
query += " AND ownerAgentId = ?";
|
|
7276
|
+
params.push(filters.ownerAgentId);
|
|
7277
|
+
}
|
|
7278
|
+
if (filters?.isEnabled !== undefined) {
|
|
7279
|
+
query += " AND isEnabled = ?";
|
|
7280
|
+
params.push(filters.isEnabled ? 1 : 0);
|
|
7281
|
+
}
|
|
7282
|
+
if (filters?.search) {
|
|
7283
|
+
query += " AND (name LIKE ? OR description LIKE ?)";
|
|
7284
|
+
const term = `%${filters.search}%`;
|
|
7285
|
+
params.push(term, term);
|
|
7286
|
+
}
|
|
7287
|
+
|
|
7288
|
+
query += " ORDER BY name ASC";
|
|
7289
|
+
|
|
7290
|
+
if (filters?.limit) {
|
|
7291
|
+
query += " LIMIT ?";
|
|
7292
|
+
params.push(filters.limit);
|
|
7293
|
+
}
|
|
7294
|
+
|
|
7295
|
+
return getDb()
|
|
7296
|
+
.prepare<SkillRow, (string | number)[]>(query)
|
|
7297
|
+
.all(...params)
|
|
7298
|
+
.map(rowToSkill);
|
|
7299
|
+
}
|
|
7300
|
+
|
|
7301
|
+
export function searchSkills(query: string, limit = 20): Skill[] {
|
|
7302
|
+
const term = `%${query}%`;
|
|
7303
|
+
return getDb()
|
|
7304
|
+
.prepare<SkillRow, [string, string, number]>(
|
|
7305
|
+
"SELECT * FROM skills WHERE (name LIKE ? OR description LIKE ?) AND isEnabled = 1 ORDER BY name ASC LIMIT ?",
|
|
7306
|
+
)
|
|
7307
|
+
.all(term, term, limit)
|
|
7308
|
+
.map(rowToSkill);
|
|
7309
|
+
}
|
|
7310
|
+
|
|
7311
|
+
export function installSkill(agentId: string, skillId: string): AgentSkill {
|
|
7312
|
+
const id = crypto.randomUUID();
|
|
7313
|
+
const now = new Date().toISOString();
|
|
7314
|
+
|
|
7315
|
+
const row = getDb()
|
|
7316
|
+
.prepare<AgentSkillRow, [string, string, string, string]>(
|
|
7317
|
+
`INSERT INTO agent_skills (id, agentId, skillId, isActive, installedAt)
|
|
7318
|
+
VALUES (?, ?, ?, 1, ?)
|
|
7319
|
+
ON CONFLICT(agentId, skillId) DO UPDATE SET isActive = 1
|
|
7320
|
+
RETURNING *`,
|
|
7321
|
+
)
|
|
7322
|
+
.get(id, agentId, skillId, now);
|
|
7323
|
+
|
|
7324
|
+
if (!row) throw new Error("Failed to install skill");
|
|
7325
|
+
return rowToAgentSkill(row);
|
|
7326
|
+
}
|
|
7327
|
+
|
|
7328
|
+
export function uninstallSkill(agentId: string, skillId: string): boolean {
|
|
7329
|
+
const result = getDb()
|
|
7330
|
+
.prepare("DELETE FROM agent_skills WHERE agentId = ? AND skillId = ?")
|
|
7331
|
+
.run(agentId, skillId);
|
|
7332
|
+
return result.changes > 0;
|
|
7333
|
+
}
|
|
7334
|
+
|
|
7335
|
+
export function getAgentSkills(agentId: string, activeOnly = true): SkillWithInstallInfo[] {
|
|
7336
|
+
const query = `
|
|
7337
|
+
SELECT s.*, as2.isActive, as2.installedAt
|
|
7338
|
+
FROM skills s
|
|
7339
|
+
JOIN agent_skills as2 ON s.id = as2.skillId
|
|
7340
|
+
WHERE as2.agentId = ?
|
|
7341
|
+
${activeOnly ? "AND as2.isActive = 1" : ""}
|
|
7342
|
+
AND s.isEnabled = 1
|
|
7343
|
+
ORDER BY
|
|
7344
|
+
CASE WHEN s.type = 'personal' THEN 0 ELSE 1 END,
|
|
7345
|
+
s.name
|
|
7346
|
+
`;
|
|
7347
|
+
|
|
7348
|
+
const rows = getDb().prepare<SkillWithInstallRow, [string]>(query).all(agentId);
|
|
7349
|
+
|
|
7350
|
+
// Deduplicate by name — personal skills take precedence (already sorted first)
|
|
7351
|
+
const seen = new Set<string>();
|
|
7352
|
+
return rows
|
|
7353
|
+
.filter((r) => {
|
|
7354
|
+
if (seen.has(r.name)) return false;
|
|
7355
|
+
seen.add(r.name);
|
|
7356
|
+
return true;
|
|
7357
|
+
})
|
|
7358
|
+
.map(rowToSkillWithInstall);
|
|
7359
|
+
}
|
|
7360
|
+
|
|
7361
|
+
export function toggleAgentSkill(agentId: string, skillId: string, isActive: boolean): boolean {
|
|
7362
|
+
const result = getDb()
|
|
7363
|
+
.prepare("UPDATE agent_skills SET isActive = ? WHERE agentId = ? AND skillId = ?")
|
|
7364
|
+
.run(isActive ? 1 : 0, agentId, skillId);
|
|
7365
|
+
return result.changes > 0;
|
|
7366
|
+
}
|