@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.
Files changed (55) hide show
  1. package/README.md +131 -0
  2. package/openapi.json +767 -4
  3. package/package.json +3 -1
  4. package/src/be/db.ts +669 -0
  5. package/src/be/migrations/019_skills.sql +65 -0
  6. package/src/be/migrations/020_approval_requests.sql +41 -0
  7. package/src/be/migrations/runner.ts +4 -4
  8. package/src/be/skill-parser.ts +70 -0
  9. package/src/be/skill-sync.ts +106 -0
  10. package/src/commands/runner.ts +299 -52
  11. package/src/http/agents.ts +29 -0
  12. package/src/http/approval-requests.ts +310 -0
  13. package/src/http/config.ts +3 -3
  14. package/src/http/index.ts +26 -2
  15. package/src/http/poll.ts +15 -0
  16. package/src/http/skills.ts +479 -0
  17. package/src/http/tasks.ts +94 -0
  18. package/src/linear/outbound.ts +12 -12
  19. package/src/prompts/base-prompt.ts +8 -0
  20. package/src/providers/claude-adapter.ts +19 -3
  21. package/src/scheduler/scheduler.ts +24 -1
  22. package/src/server.ts +29 -0
  23. package/src/slack/blocks.ts +1 -1
  24. package/src/tests/approval-requests.test.ts +948 -0
  25. package/src/tests/skill-parser.test.ts +178 -0
  26. package/src/tests/skill-sync.test.ts +171 -0
  27. package/src/tests/slack-blocks.test.ts +3 -2
  28. package/src/tests/structured-output.test.ts +1 -0
  29. package/src/tests/tool-annotations.test.ts +2 -1
  30. package/src/tests/tool-call-progress.test.ts +207 -0
  31. package/src/tests/tool-registrar-no-input.test.ts +114 -0
  32. package/src/tests/update-profile-auth.test.ts +1 -0
  33. package/src/tests/workflow-executors.test.ts +4 -2
  34. package/src/tools/request-human-input.ts +117 -0
  35. package/src/tools/skills/index.ts +11 -0
  36. package/src/tools/skills/skill-create.ts +105 -0
  37. package/src/tools/skills/skill-delete.ts +67 -0
  38. package/src/tools/skills/skill-get.ts +75 -0
  39. package/src/tools/skills/skill-install-remote.ts +152 -0
  40. package/src/tools/skills/skill-install.ts +101 -0
  41. package/src/tools/skills/skill-list.ts +77 -0
  42. package/src/tools/skills/skill-publish.ts +123 -0
  43. package/src/tools/skills/skill-search.ts +43 -0
  44. package/src/tools/skills/skill-sync-remote.ts +128 -0
  45. package/src/tools/skills/skill-uninstall.ts +60 -0
  46. package/src/tools/skills/skill-update.ts +128 -0
  47. package/src/tools/store-progress.ts +31 -0
  48. package/src/tools/templates.ts +28 -0
  49. package/src/tools/tool-config.ts +16 -0
  50. package/src/tools/utils.ts +9 -7
  51. package/src/types.ts +54 -0
  52. package/src/workflows/executors/human-in-the-loop.ts +273 -0
  53. package/src/workflows/executors/registry.ts +2 -0
  54. package/src/workflows/recovery.ts +72 -0
  55. 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.51.2",
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
+ }