@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.
Files changed (38) hide show
  1. package/openapi.json +767 -4
  2. package/package.json +1 -1
  3. package/src/be/db.ts +642 -0
  4. package/src/be/migrations/019_skills.sql +65 -0
  5. package/src/be/migrations/020_approval_requests.sql +41 -0
  6. package/src/be/skill-parser.ts +70 -0
  7. package/src/be/skill-sync.ts +106 -0
  8. package/src/commands/runner.ts +136 -41
  9. package/src/http/approval-requests.ts +247 -0
  10. package/src/http/config.ts +3 -3
  11. package/src/http/index.ts +4 -0
  12. package/src/http/skills.ts +479 -0
  13. package/src/prompts/base-prompt.ts +8 -0
  14. package/src/server.ts +29 -0
  15. package/src/tests/approval-requests.test.ts +735 -0
  16. package/src/tests/skill-parser.test.ts +178 -0
  17. package/src/tests/skill-sync.test.ts +171 -0
  18. package/src/tests/tool-annotations.test.ts +2 -1
  19. package/src/tests/workflow-executors.test.ts +4 -2
  20. package/src/tools/request-human-input.ts +106 -0
  21. package/src/tools/skills/index.ts +11 -0
  22. package/src/tools/skills/skill-create.ts +105 -0
  23. package/src/tools/skills/skill-delete.ts +67 -0
  24. package/src/tools/skills/skill-get.ts +75 -0
  25. package/src/tools/skills/skill-install-remote.ts +152 -0
  26. package/src/tools/skills/skill-install.ts +101 -0
  27. package/src/tools/skills/skill-list.ts +77 -0
  28. package/src/tools/skills/skill-publish.ts +123 -0
  29. package/src/tools/skills/skill-search.ts +43 -0
  30. package/src/tools/skills/skill-sync-remote.ts +128 -0
  31. package/src/tools/skills/skill-uninstall.ts +60 -0
  32. package/src/tools/skills/skill-update.ts +128 -0
  33. package/src/tools/tool-config.ts +16 -0
  34. package/src/types.ts +54 -0
  35. package/src/workflows/executors/human-in-the-loop.ts +160 -0
  36. package/src/workflows/executors/registry.ts +2 -0
  37. package/src/workflows/recovery.ts +72 -0
  38. 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.0",
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>",
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
+ }