@nordsym/apiclaw 1.0.0 → 1.1.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 (161) hide show
  1. package/AGENTS.md +74 -0
  2. package/HEARTBEAT.md +4 -0
  3. package/IDENTITY.md +22 -0
  4. package/README.md +193 -202
  5. package/SOUL.md +36 -0
  6. package/STATUS.md +237 -0
  7. package/TOOLS.md +36 -0
  8. package/USER.md +17 -0
  9. package/{backend/convex → convex}/_generated/api.d.ts +12 -6
  10. package/convex/analytics.ts +90 -0
  11. package/convex/credits.ts +211 -0
  12. package/convex/http.ts +578 -0
  13. package/convex/providers.ts +516 -0
  14. package/convex/purchases.ts +183 -0
  15. package/convex/ratelimit.ts +104 -0
  16. package/convex/schema.ts +220 -0
  17. package/convex/telemetry.ts +81 -0
  18. package/convex.json +3 -0
  19. package/dist/credentials.d.ts +19 -0
  20. package/dist/credentials.d.ts.map +1 -0
  21. package/dist/credentials.js +158 -0
  22. package/dist/credentials.js.map +1 -0
  23. package/dist/credits.d.ts +14 -11
  24. package/dist/credits.d.ts.map +1 -1
  25. package/dist/credits.js +151 -99
  26. package/dist/credits.js.map +1 -1
  27. package/dist/discovery.d.ts +7 -16
  28. package/dist/discovery.d.ts.map +1 -1
  29. package/dist/discovery.js +33 -40
  30. package/dist/discovery.js.map +1 -1
  31. package/dist/execute.d.ts +19 -0
  32. package/dist/execute.d.ts.map +1 -0
  33. package/dist/execute.js +285 -0
  34. package/dist/execute.js.map +1 -0
  35. package/dist/index.js +175 -31
  36. package/dist/index.js.map +1 -1
  37. package/dist/proxy.d.ts +6 -0
  38. package/dist/proxy.d.ts.map +1 -0
  39. package/dist/proxy.js +19 -0
  40. package/dist/proxy.js.map +1 -0
  41. package/dist/registry/apis.json +95362 -202
  42. package/dist/registry/apis_expanded.json +100853 -0
  43. package/dist/stripe.d.ts +68 -0
  44. package/dist/stripe.d.ts.map +1 -0
  45. package/dist/stripe.js +196 -0
  46. package/dist/stripe.js.map +1 -0
  47. package/dist/telemetry.d.ts +28 -0
  48. package/dist/telemetry.d.ts.map +1 -0
  49. package/dist/telemetry.js +50 -0
  50. package/dist/telemetry.js.map +1 -0
  51. package/dist/test.d.ts +3 -2
  52. package/dist/test.d.ts.map +1 -1
  53. package/dist/test.js +105 -75
  54. package/dist/test.js.map +1 -1
  55. package/dist/types.d.ts +0 -28
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/webhook.d.ts +2 -0
  58. package/dist/webhook.d.ts.map +1 -0
  59. package/dist/webhook.js +90 -0
  60. package/dist/webhook.js.map +1 -0
  61. package/landing/DESIGN.md +343 -0
  62. package/landing/package-lock.json +1196 -7
  63. package/landing/package.json +5 -1
  64. package/landing/public/android-chrome-192x192.png +0 -0
  65. package/landing/public/android-chrome-512x512.png +0 -0
  66. package/landing/public/apple-touch-icon.png +0 -0
  67. package/landing/public/demo.gif +0 -0
  68. package/landing/public/demo.mp4 +0 -0
  69. package/landing/public/favicon-16x16.png +0 -0
  70. package/landing/public/favicon-32x32.png +0 -0
  71. package/landing/public/favicon.ico +0 -0
  72. package/landing/public/favicon.svg +3 -0
  73. package/landing/public/icon.svg +47 -0
  74. package/landing/public/logo-mono.svg +37 -0
  75. package/landing/public/logo-simple.svg +45 -0
  76. package/landing/public/logo.svg +84 -0
  77. package/landing/public/og-template.html +184 -0
  78. package/landing/public/site.webmanifest +31 -0
  79. package/landing/scripts/generate-assets.js +284 -0
  80. package/landing/scripts/generate-pngs.js +48 -0
  81. package/landing/scripts/generate-stats.js +42 -0
  82. package/landing/src/app/admin/page.tsx +348 -0
  83. package/landing/src/app/api/auth/magic-link/route.ts +73 -0
  84. package/landing/src/app/api/auth/session/route.ts +38 -0
  85. package/landing/src/app/api/auth/verify/route.ts +43 -0
  86. package/landing/src/app/api/og/route.tsx +84 -0
  87. package/landing/src/app/globals.css +439 -100
  88. package/landing/src/app/layout.tsx +37 -7
  89. package/landing/src/app/page.tsx +627 -552
  90. package/landing/src/app/providers/dashboard/login/page.tsx +176 -0
  91. package/landing/src/app/providers/dashboard/page.tsx +589 -0
  92. package/landing/src/app/providers/dashboard/verify/page.tsx +106 -0
  93. package/landing/src/app/providers/layout.tsx +14 -0
  94. package/landing/src/app/providers/page.tsx +402 -0
  95. package/landing/src/app/providers/register/page.tsx +670 -0
  96. package/landing/src/components/ProviderDashboard.tsx +794 -0
  97. package/landing/src/hooks/useDashboardData.ts +99 -0
  98. package/landing/src/lib/apis.json +116054 -0
  99. package/landing/src/lib/convex-client.ts +106 -0
  100. package/landing/src/lib/mock-data.ts +285 -0
  101. package/landing/src/lib/stats.json +6 -0
  102. package/landing/tailwind.config.ts +12 -11
  103. package/landing/tsconfig.tsbuildinfo +1 -0
  104. package/package.json +21 -20
  105. package/scripts/SYMBOT-FIX.md +238 -0
  106. package/scripts/demo-simulation.py +177 -0
  107. package/scripts/expand-more.py +502 -0
  108. package/scripts/expand-registry.py +434 -0
  109. package/scripts/history-sanitizer.ts +272 -0
  110. package/scripts/mass-scrape.py +1308 -0
  111. package/scripts/sync-and-deploy.sh +36 -0
  112. package/src/credentials.ts +177 -0
  113. package/src/credits.ts +190 -122
  114. package/src/discovery.ts +45 -58
  115. package/src/execute.ts +350 -0
  116. package/src/index.ts +184 -32
  117. package/src/proxy.ts +24 -0
  118. package/src/registry/apis.json +95362 -202
  119. package/src/registry/apis_expanded.json +100853 -0
  120. package/src/stripe.ts +243 -0
  121. package/src/telemetry.ts +71 -0
  122. package/src/test.ts +127 -89
  123. package/src/types.ts +0 -34
  124. package/src/webhook.ts +107 -0
  125. package/.github/ISSUE_TEMPLATE/add-api.yml +0 -123
  126. package/BRIEFING.md +0 -30
  127. package/backend/convex/apiKeys.ts +0 -75
  128. package/backend/convex/purchases.ts +0 -74
  129. package/backend/convex/schema.ts +0 -45
  130. package/backend/convex/transactions.ts +0 -57
  131. package/backend/convex/users.ts +0 -94
  132. package/backend/package-lock.json +0 -521
  133. package/backend/package.json +0 -15
  134. package/dist/registry/parse_apis.py +0 -146
  135. package/dist/revenuecat.d.ts +0 -61
  136. package/dist/revenuecat.d.ts.map +0 -1
  137. package/dist/revenuecat.js +0 -166
  138. package/dist/revenuecat.js.map +0 -1
  139. package/dist/webhooks/revenuecat.d.ts +0 -48
  140. package/dist/webhooks/revenuecat.d.ts.map +0 -1
  141. package/dist/webhooks/revenuecat.js +0 -119
  142. package/dist/webhooks/revenuecat.js.map +0 -1
  143. package/docs/revenuecat-setup.md +0 -89
  144. package/landing/src/app/api/keys/route.ts +0 -71
  145. package/landing/src/app/api/log/route.ts +0 -37
  146. package/landing/src/app/api/stats/route.ts +0 -37
  147. package/landing/src/app/page.tsx.bak +0 -567
  148. package/landing/src/components/AddKeyModal.tsx +0 -159
  149. package/newsletter-template.html +0 -71
  150. package/outreach/OUTREACH-SYSTEM.md +0 -211
  151. package/outreach/email-template.html +0 -179
  152. package/outreach/targets.md +0 -133
  153. package/src/registry/parse_apis.py +0 -146
  154. package/src/revenuecat.ts +0 -239
  155. package/src/webhooks/revenuecat.ts +0 -187
  156. /package/{backend/convex → convex}/README.md +0 -0
  157. /package/{backend/convex → convex}/_generated/api.js +0 -0
  158. /package/{backend/convex → convex}/_generated/dataModel.d.ts +0 -0
  159. /package/{backend/convex → convex}/_generated/server.d.ts +0 -0
  160. /package/{backend/convex → convex}/_generated/server.js +0 -0
  161. /package/{backend/convex → convex}/tsconfig.json +0 -0
package/src/discovery.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Discovery engine for APIvault
2
2
  // MVP: Keyword matching. Future: Embeddings + semantic search
3
3
 
4
- import { SimpleAPI } from './types.js';
4
+ import { APIProvider, SearchResult } from './types.js';
5
5
  import { readFileSync } from 'fs';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { dirname, join } from 'path';
@@ -9,19 +9,10 @@ import { dirname, join } from 'path';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = dirname(__filename);
11
11
 
12
- // Load APIs from registry
13
12
  const apisData = JSON.parse(
14
13
  readFileSync(join(__dirname, 'registry', 'apis.json'), 'utf-8')
15
14
  );
16
- const apis: SimpleAPI[] = apisData.apis;
17
-
18
- console.error(`[APIvault] Loaded ${apis.length} APIs from registry`);
19
-
20
- export interface SimpleSearchResult {
21
- api: SimpleAPI;
22
- relevance_score: number;
23
- match_reasons: string[];
24
- }
15
+ const apis: APIProvider[] = apisData.apis;
25
16
 
26
17
  /**
27
18
  * Discover APIs based on a natural language query
@@ -32,73 +23,76 @@ export function discoverAPIs(
32
23
  options: {
33
24
  category?: string;
34
25
  maxResults?: number;
35
- auth?: string;
36
- httpsOnly?: boolean;
26
+ maxPrice?: number;
27
+ region?: string;
37
28
  } = {}
38
- ): SimpleSearchResult[] {
39
- const { category, maxResults = 10, auth, httpsOnly } = options;
29
+ ): SearchResult[] {
30
+ const { category, maxResults = 5, maxPrice, region } = options;
40
31
 
41
32
  const queryLower = query.toLowerCase();
42
33
  const queryWords = queryLower.split(/\s+/).filter(w => w.length > 2);
43
34
 
44
- const results: SimpleSearchResult[] = [];
35
+ const results: SearchResult[] = [];
45
36
 
46
37
  for (const api of apis) {
47
- // Category filter (case-insensitive)
48
- if (category && api.category.toLowerCase() !== category.toLowerCase()) continue;
38
+ // Category filter
39
+ if (category && api.category !== category) continue;
49
40
 
50
- // Auth filter
51
- if (auth && api.auth.toLowerCase() !== auth.toLowerCase()) continue;
52
-
53
- // HTTPS filter
54
- if (httpsOnly && !api.https) continue;
41
+ // Region filter
42
+ if (region && api.regions && !api.regions.includes(region) && !api.regions.includes('global')) continue;
55
43
 
56
44
  // Calculate relevance score
57
45
  let score = 0;
58
46
  const matchReasons: string[] = [];
59
47
 
48
+ // Check keywords
60
49
  for (const word of queryWords) {
61
- // Name match (highest weight)
62
- if (api.name.toLowerCase().includes(word)) {
63
- score += 25;
64
- matchReasons.push(`name: ${word}`);
50
+ // Direct keyword match
51
+ if (api.keywords?.some(k => k.includes(word))) {
52
+ score += 10;
53
+ matchReasons.push(`keyword: ${word}`);
65
54
  }
66
55
 
67
- // Keyword match
68
- if (api.keywords && api.keywords.some(k => k.toLowerCase().includes(word))) {
56
+ // Capability match
57
+ if (api.capabilities?.some(c => c.includes(word))) {
69
58
  score += 15;
70
- matchReasons.push(`keyword: ${word}`);
59
+ matchReasons.push(`capability: ${word}`);
60
+ }
61
+
62
+ // Name match
63
+ if (api.name.toLowerCase().includes(word)) {
64
+ score += 20;
65
+ matchReasons.push(`name: ${word}`);
71
66
  }
72
67
 
73
68
  // Description match
74
69
  if (api.description.toLowerCase().includes(word)) {
75
- score += 10;
70
+ score += 5;
76
71
  matchReasons.push(`description: ${word}`);
77
72
  }
78
73
 
79
- // Category match
80
- if (api.category.toLowerCase().includes(word)) {
74
+ // Feature match
75
+ if (api.features?.some(f => f.toLowerCase().includes(word))) {
81
76
  score += 8;
82
- matchReasons.push(`category: ${word}`);
77
+ matchReasons.push(`feature: ${word}`);
83
78
  }
84
79
  }
85
80
 
86
- // Only add secondary boosts if there's a primary match
87
- if (score > 0) {
88
- // Boost for HTTPS
89
- if (api.https) {
90
- score += 2;
91
- }
92
-
93
- // Boost for known CORS support
94
- if (api.cors === 'yes') {
95
- score += 1;
96
- }
81
+ // Boost for high success rate (default to 0.8 if not set)
82
+ score += (api.agent_success_rate ?? 0.8) * 10;
83
+
84
+ // Boost for low latency (default to 500ms if not set)
85
+ score += Math.max(0, (1000 - (api.avg_latency_ms ?? 500)) / 100);
86
+
87
+ // Boost for free tier
88
+ if (api.pricing?.free_tier) {
89
+ score += 5;
90
+ matchReasons.push('has free tier');
97
91
  }
98
92
 
99
93
  if (score > 0) {
100
94
  results.push({
101
- api,
95
+ provider: api,
102
96
  relevance_score: Math.round(score * 100) / 100,
103
97
  match_reasons: [...new Set(matchReasons)]
104
98
  });
@@ -114,34 +108,27 @@ export function discoverAPIs(
114
108
  /**
115
109
  * Get detailed information about a specific API
116
110
  */
117
- export function getAPIDetails(apiId: string): SimpleAPI | null {
111
+ export function getAPIDetails(apiId: string): APIProvider | null {
118
112
  return apis.find(api => api.id === apiId) || null;
119
113
  }
120
114
 
121
115
  /**
122
116
  * List all APIs in a category
123
117
  */
124
- export function listByCategory(category: string): SimpleAPI[] {
125
- return apis.filter(api => api.category.toLowerCase() === category.toLowerCase());
118
+ export function listByCategory(category: string): APIProvider[] {
119
+ return apis.filter(api => api.category === category);
126
120
  }
127
121
 
128
122
  /**
129
123
  * Get all available categories
130
124
  */
131
125
  export function getCategories(): string[] {
132
- return [...new Set(apis.map(api => api.category))].sort();
126
+ return [...new Set(apis.map(api => api.category))];
133
127
  }
134
128
 
135
129
  /**
136
130
  * Get all APIs
137
131
  */
138
- export function getAllAPIs(): SimpleAPI[] {
132
+ export function getAllAPIs(): APIProvider[] {
139
133
  return apis;
140
134
  }
141
-
142
- /**
143
- * Get API count
144
- */
145
- export function getAPICount(): number {
146
- return apis.length;
147
- }
package/src/execute.ts ADDED
@@ -0,0 +1,350 @@
1
+ /**
2
+ * APIClaw Instant Connect - Execute API calls through connected providers
3
+ */
4
+
5
+ import { getCredentials } from './credentials.js';
6
+ import { callProxy, PROXY_PROVIDERS } from './proxy.js';
7
+
8
+ interface ExecuteResult {
9
+ success: boolean;
10
+ provider: string;
11
+ action: string;
12
+ data?: unknown;
13
+ error?: string;
14
+ cost?: number;
15
+ }
16
+
17
+ // Helper to safely access properties
18
+ function safeGet(obj: unknown, ...keys: string[]): unknown {
19
+ let current: unknown = obj;
20
+ for (const key of keys) {
21
+ if (current && typeof current === 'object' && key in current) {
22
+ current = (current as Record<string, unknown>)[key];
23
+ } else {
24
+ return undefined;
25
+ }
26
+ }
27
+ return current;
28
+ }
29
+
30
+ // Provider action handlers
31
+ const handlers: Record<string, Record<string, (params: any, creds: any) => Promise<ExecuteResult>>> = {
32
+
33
+ // 46elks - Swedish SMS/Voice
34
+ '46elks': {
35
+ send_sms: async (params, creds) => {
36
+ const { to, message, from = 'APIClaw' } = params;
37
+
38
+ if (!to || !message) {
39
+ return { success: false, provider: '46elks', action: 'send_sms', error: 'Missing required params: to, message' };
40
+ }
41
+
42
+ const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
43
+
44
+ const response = await fetch('https://api.46elks.com/a1/sms', {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Authorization': `Basic ${auth}`,
48
+ 'Content-Type': 'application/x-www-form-urlencoded',
49
+ },
50
+ body: new URLSearchParams({ from, to, message }),
51
+ });
52
+
53
+ const data = await response.json() as Record<string, unknown>;
54
+
55
+ if (!response.ok) {
56
+ return { success: false, provider: '46elks', action: 'send_sms', error: (data.message as string) || 'SMS failed' };
57
+ }
58
+
59
+ return {
60
+ success: true,
61
+ provider: '46elks',
62
+ action: 'send_sms',
63
+ data: { id: data.id, to: data.to, cost: data.cost },
64
+ cost: parseInt(String(data.cost)) / 10000000 // Convert microöre to SEK
65
+ };
66
+ },
67
+ },
68
+
69
+ // Twilio - Global SMS/Voice
70
+ twilio: {
71
+ send_sms: async (params, creds) => {
72
+ const { to, message, from } = params;
73
+
74
+ if (!to || !message) {
75
+ return { success: false, provider: 'twilio', action: 'send_sms', error: 'Missing required params: to, message' };
76
+ }
77
+
78
+ const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
79
+ const fromNumber = from || creds.from_number || '+15017122661';
80
+
81
+ const response = await fetch(
82
+ `https://api.twilio.com/2010-04-01/Accounts/${creds.username}/Messages.json`,
83
+ {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Authorization': `Basic ${auth}`,
87
+ 'Content-Type': 'application/x-www-form-urlencoded',
88
+ },
89
+ body: new URLSearchParams({ From: fromNumber, To: to, Body: message }),
90
+ }
91
+ );
92
+
93
+ const data = await response.json() as Record<string, unknown>;
94
+
95
+ if (!response.ok) {
96
+ return { success: false, provider: 'twilio', action: 'send_sms', error: (data.message as string) || 'SMS failed' };
97
+ }
98
+
99
+ return {
100
+ success: true,
101
+ provider: 'twilio',
102
+ action: 'send_sms',
103
+ data: { sid: data.sid, to: data.to, status: data.status }
104
+ };
105
+ },
106
+ },
107
+
108
+ // Brave Search
109
+ brave_search: {
110
+ search: async (params, creds) => {
111
+ const { query, count = 5 } = params;
112
+
113
+ if (!query) {
114
+ return { success: false, provider: 'brave_search', action: 'search', error: 'Missing required param: query' };
115
+ }
116
+
117
+ const url = new URL('https://api.search.brave.com/res/v1/web/search');
118
+ url.searchParams.set('q', query);
119
+ url.searchParams.set('count', count.toString());
120
+
121
+ const response = await fetch(url.toString(), {
122
+ headers: { 'X-Subscription-Token': creds.api_key },
123
+ });
124
+
125
+ const data = await response.json() as Record<string, unknown>;
126
+
127
+ if (!response.ok) {
128
+ return { success: false, provider: 'brave_search', action: 'search', error: (data.message as string) || 'Search failed' };
129
+ }
130
+
131
+ const webData = data.web as Record<string, unknown> | undefined;
132
+ const rawResults = (webData?.results as Array<Record<string, unknown>>) || [];
133
+ const results = rawResults.map((r) => ({
134
+ title: r.title,
135
+ url: r.url,
136
+ description: r.description,
137
+ }));
138
+
139
+ return {
140
+ success: true,
141
+ provider: 'brave_search',
142
+ action: 'search',
143
+ data: { query, results, total: results.length }
144
+ };
145
+ },
146
+ },
147
+
148
+ // Resend - Email
149
+ resend: {
150
+ send_email: async (params, creds) => {
151
+ const { to, subject, html, text, from = 'APIClaw <noreply@apiclaw.com>' } = params;
152
+
153
+ if (!to || !subject || (!html && !text)) {
154
+ return { success: false, provider: 'resend', action: 'send_email', error: 'Missing required params: to, subject, html or text' };
155
+ }
156
+
157
+ const response = await fetch('https://api.resend.com/emails', {
158
+ method: 'POST',
159
+ headers: {
160
+ 'Authorization': `Bearer ${creds.api_key}`,
161
+ 'Content-Type': 'application/json',
162
+ },
163
+ body: JSON.stringify({ from, to, subject, html, text }),
164
+ });
165
+
166
+ const data = await response.json() as Record<string, unknown>;
167
+
168
+ if (!response.ok) {
169
+ return { success: false, provider: 'resend', action: 'send_email', error: (data.message as string) || 'Email failed' };
170
+ }
171
+
172
+ return {
173
+ success: true,
174
+ provider: 'resend',
175
+ action: 'send_email',
176
+ data: { id: data.id }
177
+ };
178
+ },
179
+ },
180
+
181
+ // OpenRouter - AI Models
182
+ openrouter: {
183
+ chat: async (params, creds) => {
184
+ const { messages, model = 'anthropic/claude-3-haiku', max_tokens = 1000 } = params;
185
+
186
+ if (!messages || !Array.isArray(messages)) {
187
+ return { success: false, provider: 'openrouter', action: 'chat', error: 'Missing required param: messages (array)' };
188
+ }
189
+
190
+ const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
191
+ method: 'POST',
192
+ headers: {
193
+ 'Authorization': `Bearer ${creds.api_key}`,
194
+ 'Content-Type': 'application/json',
195
+ 'HTTP-Referer': 'https://apiclaw.nordsym.com',
196
+ },
197
+ body: JSON.stringify({ model, messages, max_tokens }),
198
+ });
199
+
200
+ const data = await response.json() as Record<string, unknown>;
201
+
202
+ if (!response.ok) {
203
+ const errorData = data.error as Record<string, unknown> | undefined;
204
+ return { success: false, provider: 'openrouter', action: 'chat', error: (errorData?.message as string) || 'Chat failed' };
205
+ }
206
+
207
+ const choices = data.choices as Array<Record<string, unknown>> | undefined;
208
+ const firstChoice = choices?.[0];
209
+ const message = firstChoice?.message as Record<string, unknown> | undefined;
210
+
211
+ return {
212
+ success: true,
213
+ provider: 'openrouter',
214
+ action: 'chat',
215
+ data: {
216
+ content: message?.content,
217
+ model: data.model,
218
+ usage: data.usage
219
+ }
220
+ };
221
+ },
222
+ },
223
+
224
+ // ElevenLabs - Text-to-Speech
225
+ elevenlabs: {
226
+ text_to_speech: async (params, creds) => {
227
+ const { text, voice_id = '21m00Tcm4TlvDq8ikWAM', model_id = 'eleven_monolingual_v1' } = params;
228
+
229
+ if (!text) {
230
+ return { success: false, provider: 'elevenlabs', action: 'text_to_speech', error: 'Missing required param: text' };
231
+ }
232
+
233
+ const response = await fetch(
234
+ `https://api.elevenlabs.io/v1/text-to-speech/${voice_id}`,
235
+ {
236
+ method: 'POST',
237
+ headers: {
238
+ 'xi-api-key': creds.api_key,
239
+ 'Content-Type': 'application/json',
240
+ },
241
+ body: JSON.stringify({ text, model_id }),
242
+ }
243
+ );
244
+
245
+ if (!response.ok) {
246
+ const error = await response.json().catch(() => ({})) as Record<string, unknown>;
247
+ return { success: false, provider: 'elevenlabs', action: 'text_to_speech', error: (error.detail as string) || 'TTS failed' };
248
+ }
249
+
250
+ // Return audio as base64
251
+ const buffer = await response.arrayBuffer();
252
+ const base64 = Buffer.from(buffer).toString('base64');
253
+
254
+ return {
255
+ success: true,
256
+ provider: 'elevenlabs',
257
+ action: 'text_to_speech',
258
+ data: {
259
+ audio_base64: base64,
260
+ format: 'mp3',
261
+ text_length: text.length
262
+ }
263
+ };
264
+ },
265
+ },
266
+ };
267
+
268
+ // Get available actions for a provider
269
+ export function getProviderActions(providerId: string): string[] {
270
+ return Object.keys(handlers[providerId] || {});
271
+ }
272
+
273
+ // Get all connected providers with their actions
274
+ export function getConnectedProviders(): { provider: string; actions: string[] }[] {
275
+ return Object.entries(handlers).map(([provider, actions]) => ({
276
+ provider,
277
+ actions: Object.keys(actions),
278
+ }));
279
+ }
280
+
281
+ // Execute an API call
282
+ export async function executeAPICall(
283
+ providerId: string,
284
+ action: string,
285
+ params: Record<string, any>
286
+ ): Promise<ExecuteResult> {
287
+ // Check if provider exists
288
+ const providerHandlers = handlers[providerId];
289
+ if (!providerHandlers) {
290
+ return {
291
+ success: false,
292
+ provider: providerId,
293
+ action,
294
+ error: `Provider '${providerId}' not connected. Available: ${Object.keys(handlers).join(', ')}`,
295
+ };
296
+ }
297
+
298
+ // Check if action exists
299
+ const handler = providerHandlers[action];
300
+ if (!handler) {
301
+ return {
302
+ success: false,
303
+ provider: providerId,
304
+ action,
305
+ error: `Action '${action}' not available for ${providerId}. Available: ${Object.keys(providerHandlers).join(', ')}`,
306
+ };
307
+ }
308
+
309
+ // Get credentials - fallback to proxy if not available locally
310
+ const creds = getCredentials(providerId);
311
+ if (!creds) {
312
+ // Try proxy for supported providers
313
+ if (PROXY_PROVIDERS.includes(providerId)) {
314
+ try {
315
+ const proxyResult = await callProxy(providerId, { action, ...params });
316
+ return {
317
+ success: true,
318
+ provider: providerId,
319
+ action,
320
+ data: proxyResult,
321
+ };
322
+ } catch (e: any) {
323
+ return {
324
+ success: false,
325
+ provider: providerId,
326
+ action,
327
+ error: e.message || 'Proxy call failed',
328
+ };
329
+ }
330
+ }
331
+ return {
332
+ success: false,
333
+ provider: providerId,
334
+ action,
335
+ error: `No credentials configured for ${providerId}. Set up ~/.secrets/${providerId}.env`,
336
+ };
337
+ }
338
+
339
+ // Execute
340
+ try {
341
+ return await handler(params, creds);
342
+ } catch (error: any) {
343
+ return {
344
+ success: false,
345
+ provider: providerId,
346
+ action,
347
+ error: error.message || 'Unknown error',
348
+ };
349
+ }
350
+ }