apteva 0.4.17 → 0.4.19

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 (78) hide show
  1. package/dist/ActivityPage.9a1qg4bp.js +3 -0
  2. package/dist/ApiDocsPage.rfpf7ws1.js +4 -0
  3. package/dist/App.1nmg2h01.js +4 -0
  4. package/dist/App.5qw2dtxs.js +4 -0
  5. package/dist/App.6nc5acvk.js +4 -0
  6. package/dist/App.7vzbaz56.js +4 -0
  7. package/dist/App.8rfz30p1.js +4 -0
  8. package/dist/App.amwp54wf.js +4 -0
  9. package/dist/App.e4202qb4.js +267 -0
  10. package/dist/App.errxz2q4.js +4 -0
  11. package/dist/App.f8qsyhpr.js +4 -0
  12. package/dist/App.g8vq68n0.js +20 -0
  13. package/dist/App.kfyrnznw.js +13 -0
  14. package/dist/{App.mq6jqare.js → App.p02f4ret.js} +1 -1
  15. package/dist/App.p93mmyqw.js +4 -0
  16. package/dist/App.qmg33p02.js +4 -0
  17. package/dist/App.sdsc0258.js +4 -0
  18. package/dist/ConnectionsPage.7zqba1r0.js +3 -0
  19. package/dist/McpPage.kf2g327t.js +3 -0
  20. package/dist/SettingsPage.472c15ep.js +3 -0
  21. package/dist/SkillsPage.xdxnh68a.js +3 -0
  22. package/dist/TasksPage.7g0b8xwc.js +3 -0
  23. package/dist/TelemetryPage.pr7rbz4r.js +3 -0
  24. package/dist/TestsPage.zhc6rqjm.js +3 -0
  25. package/dist/apteva-kit.css +1 -1
  26. package/dist/index.html +1 -1
  27. package/dist/styles.css +1 -1
  28. package/package.json +9 -4
  29. package/src/auth/middleware.ts +2 -0
  30. package/src/channels/index.ts +40 -0
  31. package/src/channels/telegram.ts +306 -0
  32. package/src/db.ts +342 -11
  33. package/src/integrations/agentdojo.ts +1 -1
  34. package/src/mcp-handler.ts +31 -24
  35. package/src/mcp-platform.ts +41 -1
  36. package/src/providers.ts +22 -9
  37. package/src/routes/api/agent-utils.ts +38 -2
  38. package/src/routes/api/agents.ts +65 -2
  39. package/src/routes/api/channels.ts +182 -0
  40. package/src/routes/api/integrations.ts +13 -5
  41. package/src/routes/api/mcp.ts +27 -9
  42. package/src/routes/api/projects.ts +19 -2
  43. package/src/routes/api/system.ts +26 -12
  44. package/src/routes/api/telemetry.ts +30 -0
  45. package/src/routes/api/triggers.ts +478 -0
  46. package/src/routes/api/webhooks.ts +171 -0
  47. package/src/routes/api.ts +7 -1
  48. package/src/routes/static.ts +12 -3
  49. package/src/server.ts +43 -6
  50. package/src/triggers/agentdojo.ts +253 -0
  51. package/src/triggers/composio.ts +264 -0
  52. package/src/triggers/index.ts +71 -0
  53. package/src/tui/AgentList.tsx +145 -0
  54. package/src/tui/App.tsx +102 -0
  55. package/src/tui/Login.tsx +104 -0
  56. package/src/tui/api.ts +72 -0
  57. package/src/tui/index.tsx +7 -0
  58. package/src/web/App.tsx +18 -11
  59. package/src/web/components/agents/AgentCard.tsx +14 -7
  60. package/src/web/components/agents/AgentPanel.tsx +94 -137
  61. package/src/web/components/common/Icons.tsx +16 -0
  62. package/src/web/components/common/index.ts +1 -0
  63. package/src/web/components/connections/ConnectionsPage.tsx +54 -0
  64. package/src/web/components/connections/IntegrationsTab.tsx +144 -0
  65. package/src/web/components/connections/OverviewTab.tsx +137 -0
  66. package/src/web/components/connections/TriggersTab.tsx +1169 -0
  67. package/src/web/components/index.ts +1 -0
  68. package/src/web/components/layout/Header.tsx +196 -4
  69. package/src/web/components/layout/Sidebar.tsx +7 -1
  70. package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
  71. package/src/web/components/settings/SettingsPage.tsx +364 -2
  72. package/src/web/components/tasks/TasksPage.tsx +2 -2
  73. package/src/web/components/tests/TestsPage.tsx +1 -2
  74. package/src/web/context/TelemetryContext.tsx +14 -1
  75. package/src/web/context/index.ts +1 -1
  76. package/src/web/hooks/useAgents.ts +15 -11
  77. package/src/web/types.ts +1 -1
  78. package/dist/App.fq4xbpcz.js +0 -228
package/src/db.ts CHANGED
@@ -194,6 +194,56 @@ export interface SkillRow {
194
194
  updated_at: string;
195
195
  }
196
196
 
197
+ // Subscription: maps trigger events to agents for routing
198
+ export interface Subscription {
199
+ id: string;
200
+ trigger_slug: string;
201
+ trigger_instance_id: string | null;
202
+ agent_id: string;
203
+ enabled: boolean;
204
+ project_id: string | null;
205
+ created_at: string;
206
+ updated_at: string;
207
+ }
208
+
209
+ export interface SubscriptionRow {
210
+ id: string;
211
+ trigger_slug: string;
212
+ trigger_instance_id: string | null;
213
+ agent_id: string;
214
+ enabled: number;
215
+ project_id: string | null;
216
+ created_at: string;
217
+ updated_at: string;
218
+ }
219
+
220
+ // Channel: external messaging platform bound to an agent
221
+ export interface Channel {
222
+ id: string;
223
+ type: "telegram"; // future: "slack", "discord"
224
+ name: string;
225
+ agent_id: string;
226
+ config: string; // encrypted JSON
227
+ status: "stopped" | "running" | "error";
228
+ error: string | null;
229
+ project_id: string | null;
230
+ created_at: string;
231
+ updated_at: string;
232
+ }
233
+
234
+ export interface ChannelRow {
235
+ id: string;
236
+ type: string;
237
+ name: string;
238
+ agent_id: string;
239
+ config: string;
240
+ status: string;
241
+ error: string | null;
242
+ project_id: string | null;
243
+ created_at: string;
244
+ updated_at: string;
245
+ }
246
+
197
247
  export interface McpServerRow {
198
248
  id: string;
199
249
  name: string;
@@ -256,11 +306,21 @@ export function initDatabase(dataDir: string): Database {
256
306
 
257
307
  // Enable WAL mode for better concurrent access
258
308
  db.run("PRAGMA journal_mode = WAL");
309
+ db.run("PRAGMA busy_timeout = 5000");
259
310
  db.run("PRAGMA foreign_keys = ON");
260
311
 
261
312
  // Run migrations
262
313
  runMigrations();
263
314
 
315
+ // Auto-set instance_url from env if not already configured
316
+ const envInstanceUrl = process.env.INSTANCE_URL || process.env.PUBLIC_URL;
317
+ if (envInstanceUrl) {
318
+ const current = SettingsDB.get("instance_url");
319
+ if (!current) {
320
+ SettingsDB.set("instance_url", envInstanceUrl.replace(/\/+$/, ""));
321
+ }
322
+ }
323
+
264
324
  // Database initialized silently
265
325
  return db;
266
326
  }
@@ -685,6 +745,52 @@ function runMigrations() {
685
745
  CREATE INDEX IF NOT EXISTS idx_mcp_server_tools_server ON mcp_server_tools(server_id);
686
746
  `,
687
747
  },
748
+ {
749
+ name: "031_create_subscriptions",
750
+ sql: `
751
+ CREATE TABLE IF NOT EXISTS subscriptions (
752
+ id TEXT PRIMARY KEY,
753
+ trigger_slug TEXT NOT NULL,
754
+ trigger_instance_id TEXT,
755
+ agent_id TEXT NOT NULL,
756
+ enabled INTEGER NOT NULL DEFAULT 1,
757
+ project_id TEXT,
758
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
759
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
760
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
761
+ );
762
+ CREATE INDEX IF NOT EXISTS idx_subscriptions_agent ON subscriptions(agent_id);
763
+ CREATE INDEX IF NOT EXISTS idx_subscriptions_trigger_slug ON subscriptions(trigger_slug);
764
+ CREATE INDEX IF NOT EXISTS idx_subscriptions_trigger_instance ON subscriptions(trigger_instance_id);
765
+ `,
766
+ },
767
+ {
768
+ name: "032_create_channels",
769
+ sql: `
770
+ CREATE TABLE IF NOT EXISTS channels (
771
+ id TEXT PRIMARY KEY,
772
+ type TEXT NOT NULL,
773
+ name TEXT NOT NULL,
774
+ agent_id TEXT NOT NULL,
775
+ config TEXT NOT NULL,
776
+ status TEXT DEFAULT 'stopped',
777
+ error TEXT,
778
+ project_id TEXT,
779
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
780
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
781
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
782
+ );
783
+ CREATE INDEX IF NOT EXISTS idx_channels_agent ON channels(agent_id);
784
+ CREATE INDEX IF NOT EXISTS idx_channels_status ON channels(status);
785
+ `,
786
+ },
787
+ {
788
+ name: "033_add_telemetry_seen",
789
+ sql: `
790
+ ALTER TABLE telemetry_events ADD COLUMN seen INTEGER DEFAULT 0;
791
+ CREATE INDEX IF NOT EXISTS idx_telemetry_seen ON telemetry_events(seen);
792
+ `,
793
+ },
688
794
  {
689
795
  name: "029_fix_provider_keys_unique_constraint",
690
796
  sql: `
@@ -723,8 +829,8 @@ function runMigrations() {
723
829
  for (const migration of migrations) {
724
830
  if (!applied.has(migration.name)) {
725
831
  try {
726
- // Migration runs silently
727
- db.run(migration.sql);
832
+ // Migration runs silently (exec supports multi-statement SQL)
833
+ db.exec(migration.sql);
728
834
  db.run("INSERT INTO migrations (name) VALUES (?)", [migration.name]);
729
835
  } catch (err) {
730
836
  // Log error but continue - some migrations may fail if partially applied
@@ -853,12 +959,13 @@ export const AgentDB = {
853
959
  return rows.map(rowToAgent);
854
960
  },
855
961
 
856
- // Get running agents (for auto-restart)
962
+ // Get running agents
857
963
  findRunning(): Agent[] {
858
964
  const rows = db.query("SELECT * FROM agents WHERE status = 'running'").all() as AgentRow[];
859
965
  return rows.map(rowToAgent);
860
966
  },
861
967
 
968
+
862
969
  // Update agent
863
970
  update(id: string, updates: Partial<Omit<Agent, "id" | "created_at">>): Agent | null {
864
971
  const agent = this.findById(id);
@@ -907,7 +1014,6 @@ export const AgentDB = {
907
1014
  fields.push("project_id = ?");
908
1015
  values.push(updates.project_id);
909
1016
  }
910
-
911
1017
  if (fields.length > 0) {
912
1018
  fields.push("updated_at = ?");
913
1019
  values.push(new Date().toISOString());
@@ -1076,8 +1182,12 @@ export const ProjectDB = {
1076
1182
  return this.findById(id);
1077
1183
  },
1078
1184
 
1079
- // Delete project (agents will have project_id set to NULL)
1185
+ // Delete project with full cleanup
1186
+ // FK constraints handle: agents, mcp_servers, skills (SET NULL), provider_keys (CASCADE)
1187
+ // Manual cleanup: subscriptions, test_cases (no FK on project_id)
1080
1188
  delete(id: string): boolean {
1189
+ db.run("UPDATE subscriptions SET project_id = NULL WHERE project_id = ?", [id]);
1190
+ db.run("UPDATE test_cases SET project_id = NULL WHERE project_id = ?", [id]);
1081
1191
  const result = db.run("DELETE FROM projects WHERE id = ?", [id]);
1082
1192
  return result.changes > 0;
1083
1193
  },
@@ -1386,6 +1496,15 @@ export const McpServerDB = {
1386
1496
  return row ? rowToMcpServer(row) : null;
1387
1497
  },
1388
1498
 
1499
+ findByIds(ids: string[]): Map<string, McpServer> {
1500
+ if (ids.length === 0) return new Map();
1501
+ const placeholders = ids.map(() => "?").join(",");
1502
+ const rows = db.query(`SELECT * FROM mcp_servers WHERE id IN (${placeholders})`).all(...ids) as McpServerRow[];
1503
+ const map = new Map<string, McpServer>();
1504
+ for (const row of rows) map.set(row.id, rowToMcpServer(row));
1505
+ return map;
1506
+ },
1507
+
1389
1508
  findAll(): McpServer[] {
1390
1509
  const rows = db.query("SELECT * FROM mcp_servers ORDER BY created_at DESC").all() as McpServerRow[];
1391
1510
  return rows.map(rowToMcpServer);
@@ -1703,6 +1822,7 @@ export interface TelemetryEvent {
1703
1822
  duration_ms: number | null;
1704
1823
  error: string | null;
1705
1824
  received_at: string;
1825
+ seen?: boolean;
1706
1826
  }
1707
1827
 
1708
1828
  interface TelemetryEventRow {
@@ -1720,6 +1840,7 @@ interface TelemetryEventRow {
1720
1840
  duration_ms: number | null;
1721
1841
  error: string | null;
1722
1842
  received_at: string;
1843
+ seen?: number;
1723
1844
  }
1724
1845
 
1725
1846
  // Telemetry operations
@@ -2001,6 +2122,45 @@ export const TelemetryDB = {
2001
2122
  const row = db.query("SELECT COUNT(*) as count FROM telemetry_events").get() as { count: number };
2002
2123
  return row.count;
2003
2124
  },
2125
+
2126
+ // --- Notification helpers (piggyback on telemetry with `seen` flag) ---
2127
+
2128
+ // Notification-worthy filter: errors + agent crashes
2129
+ getNotifications(limit = 50): TelemetryEvent[] {
2130
+ const rows = db.query(`
2131
+ SELECT * FROM telemetry_events
2132
+ WHERE (level = 'error' OR (category = 'system' AND type = 'agent_stopped') OR category = 'ERROR')
2133
+ ORDER BY timestamp DESC
2134
+ LIMIT ?
2135
+ `).all(limit) as TelemetryEventRow[];
2136
+ return rows.map(rowToTelemetryEvent);
2137
+ },
2138
+
2139
+ getUnseenCount(): number {
2140
+ const row = db.query(`
2141
+ SELECT COUNT(*) as count FROM telemetry_events
2142
+ WHERE seen = 0
2143
+ AND (level = 'error' OR (category = 'system' AND type = 'agent_stopped') OR category = 'ERROR')
2144
+ `).get() as { count: number };
2145
+ return row.count;
2146
+ },
2147
+
2148
+ markSeen(ids: string[]): number {
2149
+ if (ids.length === 0) return 0;
2150
+ const placeholders = ids.map(() => "?").join(",");
2151
+ const result = db.run(
2152
+ `UPDATE telemetry_events SET seen = 1 WHERE id IN (${placeholders})`,
2153
+ ids
2154
+ );
2155
+ return result.changes;
2156
+ },
2157
+
2158
+ markAllSeen(): number {
2159
+ const result = db.run(
2160
+ `UPDATE telemetry_events SET seen = 1 WHERE seen = 0 AND (level = 'error' OR (category = 'system' AND type = 'agent_stopped') OR category = 'ERROR')`
2161
+ );
2162
+ return result.changes;
2163
+ },
2004
2164
  };
2005
2165
 
2006
2166
  function rowToTelemetryEvent(row: TelemetryEventRow): TelemetryEvent {
@@ -2019,6 +2179,7 @@ function rowToTelemetryEvent(row: TelemetryEventRow): TelemetryEvent {
2019
2179
  duration_ms: row.duration_ms,
2020
2180
  error: row.error,
2021
2181
  received_at: row.received_at,
2182
+ seen: row.seen === 1,
2022
2183
  };
2023
2184
  }
2024
2185
 
@@ -2388,6 +2549,15 @@ export const SkillDB = {
2388
2549
  return row ? rowToSkill(row) : null;
2389
2550
  },
2390
2551
 
2552
+ findByIds(ids: string[]): Map<string, Skill> {
2553
+ if (ids.length === 0) return new Map();
2554
+ const placeholders = ids.map(() => "?").join(",");
2555
+ const rows = db.query(`SELECT * FROM skills WHERE id IN (${placeholders})`).all(...ids) as SkillRow[];
2556
+ const map = new Map<string, Skill>();
2557
+ for (const row of rows) map.set(row.id, rowToSkill(row));
2558
+ return map;
2559
+ },
2560
+
2391
2561
  // Find skill by name
2392
2562
  findByName(name: string): Skill | null {
2393
2563
  const row = db.query("SELECT * FROM skills WHERE name = ?").get(name) as SkillRow | null;
@@ -2503,12 +2673,6 @@ export const SkillDB = {
2503
2673
  return row.count;
2504
2674
  },
2505
2675
 
2506
- // Check if skill with name exists
2507
- exists(name: string): boolean {
2508
- const row = db.query("SELECT COUNT(*) as count FROM skills WHERE name = ?").get(name) as { count: number };
2509
- return row.count > 0;
2510
- },
2511
-
2512
2676
  // Find skills by project (null = global only)
2513
2677
  findByProject(projectId: string | null): Skill[] {
2514
2678
  if (projectId === null) {
@@ -2575,6 +2739,173 @@ function rowToSkill(row: SkillRow): Skill {
2575
2739
  };
2576
2740
  }
2577
2741
 
2742
+ // Subscription row → Subscription
2743
+ function rowToSubscription(row: SubscriptionRow): Subscription {
2744
+ return {
2745
+ id: row.id,
2746
+ trigger_slug: row.trigger_slug,
2747
+ trigger_instance_id: row.trigger_instance_id,
2748
+ agent_id: row.agent_id,
2749
+ enabled: row.enabled === 1,
2750
+ project_id: row.project_id,
2751
+ created_at: row.created_at,
2752
+ updated_at: row.updated_at,
2753
+ };
2754
+ }
2755
+
2756
+ export const SubscriptionDB = {
2757
+ create(sub: Omit<Subscription, "id" | "created_at" | "updated_at">): Subscription {
2758
+ const id = generateId();
2759
+ const now = new Date().toISOString();
2760
+ db.run(
2761
+ `INSERT INTO subscriptions (id, trigger_slug, trigger_instance_id, agent_id, enabled, project_id, created_at, updated_at)
2762
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
2763
+ [id, sub.trigger_slug, sub.trigger_instance_id || null, sub.agent_id, sub.enabled ? 1 : 0, sub.project_id || null, now, now]
2764
+ );
2765
+ return this.findById(id)!;
2766
+ },
2767
+
2768
+ findById(id: string): Subscription | null {
2769
+ const row = db.query("SELECT * FROM subscriptions WHERE id = ?").get(id) as SubscriptionRow | null;
2770
+ return row ? rowToSubscription(row) : null;
2771
+ },
2772
+
2773
+ findByTriggerInstanceId(instanceId: string): Subscription[] {
2774
+ const rows = db.query("SELECT * FROM subscriptions WHERE trigger_instance_id = ?").all(instanceId) as SubscriptionRow[];
2775
+ return rows.map(rowToSubscription);
2776
+ },
2777
+
2778
+ findByTriggerSlug(slug: string): Subscription[] {
2779
+ const rows = db.query("SELECT * FROM subscriptions WHERE trigger_slug = ?").all(slug) as SubscriptionRow[];
2780
+ return rows.map(rowToSubscription);
2781
+ },
2782
+
2783
+ findByAgentId(agentId: string): Subscription[] {
2784
+ const rows = db.query("SELECT * FROM subscriptions WHERE agent_id = ?").all(agentId) as SubscriptionRow[];
2785
+ return rows.map(rowToSubscription);
2786
+ },
2787
+
2788
+ findAll(projectId?: string | null): Subscription[] {
2789
+ if (projectId) {
2790
+ const rows = db.query("SELECT * FROM subscriptions WHERE project_id = ? ORDER BY created_at DESC").all(projectId) as SubscriptionRow[];
2791
+ return rows.map(rowToSubscription);
2792
+ }
2793
+ const rows = db.query("SELECT * FROM subscriptions ORDER BY created_at DESC").all() as SubscriptionRow[];
2794
+ return rows.map(rowToSubscription);
2795
+ },
2796
+
2797
+ update(id: string, updates: Partial<Pick<Subscription, "trigger_slug" | "trigger_instance_id" | "agent_id" | "enabled">>): Subscription | null {
2798
+ const sub = this.findById(id);
2799
+ if (!sub) return null;
2800
+
2801
+ const fields: string[] = [];
2802
+ const values: (string | number | null)[] = [];
2803
+
2804
+ if (updates.trigger_slug !== undefined) { fields.push("trigger_slug = ?"); values.push(updates.trigger_slug); }
2805
+ if (updates.trigger_instance_id !== undefined) { fields.push("trigger_instance_id = ?"); values.push(updates.trigger_instance_id || null); }
2806
+ if (updates.agent_id !== undefined) { fields.push("agent_id = ?"); values.push(updates.agent_id); }
2807
+ if (updates.enabled !== undefined) { fields.push("enabled = ?"); values.push(updates.enabled ? 1 : 0); }
2808
+
2809
+ if (fields.length === 0) return sub;
2810
+
2811
+ fields.push("updated_at = ?");
2812
+ values.push(new Date().toISOString());
2813
+ values.push(id);
2814
+
2815
+ db.run(`UPDATE subscriptions SET ${fields.join(", ")} WHERE id = ?`, values);
2816
+ return this.findById(id);
2817
+ },
2818
+
2819
+ delete(id: string): boolean {
2820
+ const result = db.run("DELETE FROM subscriptions WHERE id = ?", [id]);
2821
+ return result.changes > 0;
2822
+ },
2823
+ };
2824
+
2825
+ // --- Channel DB ---
2826
+
2827
+ function rowToChannel(row: ChannelRow): Channel {
2828
+ return {
2829
+ id: row.id,
2830
+ type: row.type as Channel["type"],
2831
+ name: row.name,
2832
+ agent_id: row.agent_id,
2833
+ config: row.config,
2834
+ status: row.status as Channel["status"],
2835
+ error: row.error,
2836
+ project_id: row.project_id,
2837
+ created_at: row.created_at,
2838
+ updated_at: row.updated_at,
2839
+ };
2840
+ }
2841
+
2842
+ export const ChannelDB = {
2843
+ create(channel: { type: string; name: string; agent_id: string; config: string; project_id?: string | null }): Channel {
2844
+ const id = generateId();
2845
+ const now = new Date().toISOString();
2846
+ db.run(
2847
+ `INSERT INTO channels (id, type, name, agent_id, config, status, project_id, created_at, updated_at)
2848
+ VALUES (?, ?, ?, ?, ?, 'stopped', ?, ?, ?)`,
2849
+ [id, channel.type, channel.name, channel.agent_id, channel.config, channel.project_id || null, now, now]
2850
+ );
2851
+ return this.findById(id)!;
2852
+ },
2853
+
2854
+ findById(id: string): Channel | null {
2855
+ const row = db.query("SELECT * FROM channels WHERE id = ?").get(id) as ChannelRow | null;
2856
+ return row ? rowToChannel(row) : null;
2857
+ },
2858
+
2859
+ findAll(): Channel[] {
2860
+ const rows = db.query("SELECT * FROM channels ORDER BY created_at DESC").all() as ChannelRow[];
2861
+ return rows.map(rowToChannel);
2862
+ },
2863
+
2864
+ findByAgentId(agentId: string): Channel[] {
2865
+ const rows = db.query("SELECT * FROM channels WHERE agent_id = ?").all(agentId) as ChannelRow[];
2866
+ return rows.map(rowToChannel);
2867
+ },
2868
+
2869
+ findRunning(): Channel[] {
2870
+ const rows = db.query("SELECT * FROM channels WHERE status = 'running'").all() as ChannelRow[];
2871
+ return rows.map(rowToChannel);
2872
+ },
2873
+
2874
+ update(id: string, updates: { name?: string; agent_id?: string; config?: string; project_id?: string | null }): Channel | null {
2875
+ const channel = this.findById(id);
2876
+ if (!channel) return null;
2877
+
2878
+ const fields: string[] = [];
2879
+ const values: (string | null)[] = [];
2880
+
2881
+ if (updates.name !== undefined) { fields.push("name = ?"); values.push(updates.name); }
2882
+ if (updates.agent_id !== undefined) { fields.push("agent_id = ?"); values.push(updates.agent_id); }
2883
+ if (updates.config !== undefined) { fields.push("config = ?"); values.push(updates.config); }
2884
+ if (updates.project_id !== undefined) { fields.push("project_id = ?"); values.push(updates.project_id || null); }
2885
+
2886
+ if (fields.length === 0) return channel;
2887
+
2888
+ fields.push("updated_at = ?");
2889
+ values.push(new Date().toISOString());
2890
+ values.push(id);
2891
+
2892
+ db.run(`UPDATE channels SET ${fields.join(", ")} WHERE id = ?`, values);
2893
+ return this.findById(id);
2894
+ },
2895
+
2896
+ setStatus(id: string, status: Channel["status"], error?: string | null): void {
2897
+ db.run(
2898
+ "UPDATE channels SET status = ?, error = ?, updated_at = ? WHERE id = ?",
2899
+ [status, error || null, new Date().toISOString(), id]
2900
+ );
2901
+ },
2902
+
2903
+ delete(id: string): boolean {
2904
+ const result = db.run("DELETE FROM channels WHERE id = ?", [id]);
2905
+ return result.changes > 0;
2906
+ },
2907
+ };
2908
+
2578
2909
  // Generate unique ID
2579
2910
  export function generateId(): string {
2580
2911
  return Math.random().toString(36).substring(2, 15);
@@ -127,7 +127,7 @@ export const AgentDojoProvider: IntegrationProvider = {
127
127
 
128
128
  return credentials.map((cred: any) => ({
129
129
  id: String(cred.id),
130
- appId: String(cred.provider_id || cred.toolkit_id || cred.provider_name),
130
+ appId: cred.provider_name || cred.toolkit_name || String(cred.provider_id || cred.toolkit_id),
131
131
  appName: cred.provider_name || cred.name || cred.toolkit_name || String(cred.provider_id),
132
132
  status: (cred.status === "active" || cred.is_valid !== false) ? "active" as const : "failed" as const,
133
133
  createdAt: cred.created_at || new Date().toISOString(),
@@ -73,34 +73,28 @@ function evaluateExpression(
73
73
  args: Record<string, any>,
74
74
  helpers: ReturnType<typeof templateHelpers>,
75
75
  ): any {
76
- // Handle args.* references
76
+ // Handle args.* references (e.g. args.name, args.query)
77
77
  if (expr.startsWith("args.")) {
78
78
  const key = expr.slice(5);
79
79
  return args[key] ?? null;
80
80
  }
81
81
 
82
- // Handle helper functions and values
83
- try {
84
- const fn = new Function(
85
- "args",
86
- "uuid",
87
- "now",
88
- "timestamp",
89
- "random_int",
90
- "random_float",
91
- `return ${expr}`,
92
- );
93
- return fn(
94
- args,
95
- helpers.uuid,
96
- helpers.now,
97
- helpers.timestamp,
98
- helpers.random_int,
99
- helpers.random_float,
100
- );
101
- } catch {
102
- return expr;
103
- }
82
+ // Handle known helper values
83
+ if (expr === "now") return helpers.now;
84
+ if (expr === "timestamp") return helpers.timestamp;
85
+
86
+ // Handle known helper function calls
87
+ const uuidMatch = expr.match(/^uuid\(\)$/);
88
+ if (uuidMatch) return helpers.uuid();
89
+
90
+ const randIntMatch = expr.match(/^random_int\(\s*(\d+)\s*,\s*(\d+)\s*\)$/);
91
+ if (randIntMatch) return helpers.random_int(Number(randIntMatch[1]), Number(randIntMatch[2]));
92
+
93
+ const randFloatMatch = expr.match(/^random_float\(\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/);
94
+ if (randFloatMatch) return helpers.random_float(Number(randFloatMatch[1]), Number(randFloatMatch[2]));
95
+
96
+ // Return expression as-is if not recognized — never execute arbitrary code
97
+ return expr;
104
98
  }
105
99
 
106
100
  // Execute a mock handler — returns the rendered mock_response
@@ -197,7 +191,11 @@ async function executeHttp(
197
191
  }
198
192
  }
199
193
 
200
- // Execute a JavaScript handler — runs code string with args + credentials
194
+ // Execute a JavaScript handler — runs user-defined code in a restricted scope.
195
+ // SECURITY NOTE: This intentionally allows authenticated admins to define custom tool logic.
196
+ // The code runs in a restricted Function scope with only args, credentials, and helpers exposed.
197
+ // process, require, import, Bun, fetch etc. are NOT passed in — but note that new Function()
198
+ // still has access to globalThis. For full sandboxing, consider using a Worker or subprocess.
201
199
  function executeJavascript(
202
200
  tool: McpServerTool,
203
201
  args: Record<string, any>,
@@ -210,6 +208,15 @@ function executeJavascript(
210
208
  };
211
209
  }
212
210
 
211
+ // Basic static checks — block obvious dangerous patterns
212
+ const dangerous = /\b(process|require|import|Bun|Deno|eval|Function|child_process|exec|spawn)\b/;
213
+ if (dangerous.test(tool.code)) {
214
+ return {
215
+ content: [{ type: "text", text: "Error: Tool code contains disallowed keywords (process, require, import, eval, exec, spawn)" }],
216
+ isError: true,
217
+ };
218
+ }
219
+
213
220
  try {
214
221
  const helpers = templateHelpers();
215
222
  const fn = new Function(
@@ -346,6 +346,25 @@ After creating, assign to agents with assign_mcp_server_to_agent. HTTP servers w
346
346
  required: ["agent_id", "skill_id"],
347
347
  },
348
348
  },
349
+ {
350
+ name: "create_skill",
351
+ description: "Create a new skill. Skills are reusable instruction sets (markdown content) that give agents specialized capabilities. Provide a name, description, and the full instructions content (markdown). Optionally specify allowed MCP tools. If the user is working within a project, set project_id to scope the skill to that project.",
352
+ inputSchema: {
353
+ type: "object",
354
+ properties: {
355
+ name: { type: "string", description: "The skill name" },
356
+ description: { type: "string", description: "Short description of what the skill does" },
357
+ content: { type: "string", description: "Full skill instructions in markdown format" },
358
+ allowed_tools: {
359
+ type: "array",
360
+ items: { type: "string" },
361
+ description: "Optional list of MCP tool names this skill is allowed to use",
362
+ },
363
+ project_id: { type: "string", description: "Project ID to scope the skill to. Use the current project ID from context when the user is working within a project. Omit for a global skill." },
364
+ },
365
+ required: ["name", "description", "content"],
366
+ },
367
+ },
349
368
  {
350
369
  name: "delete_skill",
351
370
  description: "Delete a skill. It will be unassigned from all agents.",
@@ -824,6 +843,27 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
824
843
  return { content: [{ type: "text", text: `Removed skill from agent "${agent.name}". Restart the agent for changes to take effect.` }] };
825
844
  }
826
845
 
846
+ case "create_skill": {
847
+ if (!args.name || !args.description || !args.content) {
848
+ return { content: [{ type: "text", text: "name, description, and content are required" }], isError: true };
849
+ }
850
+ const newSkill = SkillDB.create({
851
+ name: args.name,
852
+ description: args.description,
853
+ content: args.content,
854
+ version: "1.0.0",
855
+ license: null,
856
+ compatibility: null,
857
+ metadata: {},
858
+ allowed_tools: args.allowed_tools || [],
859
+ source: "local",
860
+ source_url: null,
861
+ enabled: true,
862
+ project_id: args.project_id || null,
863
+ });
864
+ return { content: [{ type: "text", text: `Skill "${newSkill.name}" created (ID: ${newSkill.id}). You can now assign it to an agent with assign_skill_to_agent.` }] };
865
+ }
866
+
827
867
  case "delete_skill": {
828
868
  const skill = SkillDB.findById(args.skill_id);
829
869
  if (!skill) {
@@ -977,7 +1017,7 @@ You can manage:
977
1017
  - AGENTS: Create, configure, start, stop, and delete AI agents. Each agent has a provider (LLM), model, system prompt, and optional features (memory, tasks, vision, MCP tools, files).
978
1018
  - PROJECTS: Organize agents into projects for grouping.
979
1019
  - MCP SERVERS: Tool integrations that give agents capabilities (web search, file access, APIs). Assign servers to agents.
980
- - SKILLS: Reusable instruction sets that specialize agent behavior. Assign skills to agents.
1020
+ - SKILLS: Reusable instruction sets that specialize agent behavior. Use create_skill to create new skills (pass project_id from context to scope to the current project), then assign them to agents. Use list_skills, get_skill, create_skill, toggle_skill, assign_skill_to_agent, unassign_skill_from_agent, delete_skill.
981
1021
  - PROVIDERS: View which LLM providers have API keys configured.
982
1022
  - TESTS: Create and run automated tests for agent workflows. Tests send a message to an agent, then an LLM judge evaluates the response against success criteria. Use list_tests, create_test, run_test, run_all_tests, get_test_results, delete_test.
983
1023
 
package/src/providers.ts CHANGED
@@ -198,35 +198,48 @@ export const ProviderKeys = {
198
198
  }
199
199
  },
200
200
 
201
- // Get decrypted API key for a provider (global key)
201
+ // Get decrypted API key for a provider (global key, falls back to env var)
202
202
  getDecrypted(providerId: string): string | null {
203
203
  const record = ProviderKeysDB.findByProvider(providerId);
204
- if (!record) return null;
204
+ if (record) {
205
+ try {
206
+ return decrypt(record.encrypted_key);
207
+ } catch (err) {
208
+ console.error(`Failed to decrypt key for ${providerId}:`, err);
209
+ }
210
+ }
205
211
 
206
- try {
207
- return decrypt(record.encrypted_key);
208
- } catch (err) {
209
- console.error(`Failed to decrypt key for ${providerId}:`, err);
210
- return null;
212
+ // Fall back to environment variable
213
+ const provider = PROVIDERS[providerId as ProviderId];
214
+ if (provider?.envVar) {
215
+ const envVal = process.env[provider.envVar];
216
+ if (envVal) return envVal;
211
217
  }
218
+ return null;
212
219
  },
213
220
 
214
221
  // Get decrypted API key for a provider and project
215
222
  // Falls back to global key if no project-specific key exists
216
223
  getDecryptedForProject(providerId: string, projectId: string | null): string | null {
224
+ console.log(`[ProviderKeys.getDecryptedForProject] providerId=${providerId}, projectId=${projectId}`);
217
225
  // Try project-specific key first
218
226
  if (projectId) {
219
227
  const projectRecord = ProviderKeysDB.findByProviderAndProject(providerId, projectId);
228
+ console.log(`[ProviderKeys.getDecryptedForProject] project record found: ${!!projectRecord}`);
220
229
  if (projectRecord) {
221
230
  try {
222
- return decrypt(projectRecord.encrypted_key);
231
+ const key = decrypt(projectRecord.encrypted_key);
232
+ console.log(`[ProviderKeys.getDecryptedForProject] decrypted project key OK, length=${key?.length}`);
233
+ return key;
223
234
  } catch (err) {
224
235
  console.error(`Failed to decrypt project key for ${providerId}/${projectId}:`, err);
225
236
  }
226
237
  }
227
238
  }
228
239
  // Fall back to global key
229
- return this.getDecrypted(providerId);
240
+ const globalKey = this.getDecrypted(providerId);
241
+ console.log(`[ProviderKeys.getDecryptedForProject] global fallback: found=${!!globalKey}, length=${globalKey?.length || 0}`);
242
+ return globalKey;
230
243
  },
231
244
 
232
245
  // Check if a provider has a key configured (global)