@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.
- package/AGENTS.md +74 -0
- package/HEARTBEAT.md +4 -0
- package/IDENTITY.md +22 -0
- package/README.md +193 -202
- package/SOUL.md +36 -0
- package/STATUS.md +237 -0
- package/TOOLS.md +36 -0
- package/USER.md +17 -0
- package/{backend/convex → convex}/_generated/api.d.ts +12 -6
- package/convex/analytics.ts +90 -0
- package/convex/credits.ts +211 -0
- package/convex/http.ts +578 -0
- package/convex/providers.ts +516 -0
- package/convex/purchases.ts +183 -0
- package/convex/ratelimit.ts +104 -0
- package/convex/schema.ts +220 -0
- package/convex/telemetry.ts +81 -0
- package/convex.json +3 -0
- package/dist/credentials.d.ts +19 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +158 -0
- package/dist/credentials.js.map +1 -0
- package/dist/credits.d.ts +14 -11
- package/dist/credits.d.ts.map +1 -1
- package/dist/credits.js +151 -99
- package/dist/credits.js.map +1 -1
- package/dist/discovery.d.ts +7 -16
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +33 -40
- package/dist/discovery.js.map +1 -1
- package/dist/execute.d.ts +19 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +285 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.js +175 -31
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +19 -0
- package/dist/proxy.js.map +1 -0
- package/dist/registry/apis.json +95362 -202
- package/dist/registry/apis_expanded.json +100853 -0
- package/dist/stripe.d.ts +68 -0
- package/dist/stripe.d.ts.map +1 -0
- package/dist/stripe.js +196 -0
- package/dist/stripe.js.map +1 -0
- package/dist/telemetry.d.ts +28 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +50 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/test.d.ts +3 -2
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +105 -75
- package/dist/test.js.map +1 -1
- package/dist/types.d.ts +0 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/webhook.d.ts +2 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +90 -0
- package/dist/webhook.js.map +1 -0
- package/landing/DESIGN.md +343 -0
- package/landing/package-lock.json +1196 -7
- package/landing/package.json +5 -1
- package/landing/public/android-chrome-192x192.png +0 -0
- package/landing/public/android-chrome-512x512.png +0 -0
- package/landing/public/apple-touch-icon.png +0 -0
- package/landing/public/demo.gif +0 -0
- package/landing/public/demo.mp4 +0 -0
- package/landing/public/favicon-16x16.png +0 -0
- package/landing/public/favicon-32x32.png +0 -0
- package/landing/public/favicon.ico +0 -0
- package/landing/public/favicon.svg +3 -0
- package/landing/public/icon.svg +47 -0
- package/landing/public/logo-mono.svg +37 -0
- package/landing/public/logo-simple.svg +45 -0
- package/landing/public/logo.svg +84 -0
- package/landing/public/og-template.html +184 -0
- package/landing/public/site.webmanifest +31 -0
- package/landing/scripts/generate-assets.js +284 -0
- package/landing/scripts/generate-pngs.js +48 -0
- package/landing/scripts/generate-stats.js +42 -0
- package/landing/src/app/admin/page.tsx +348 -0
- package/landing/src/app/api/auth/magic-link/route.ts +73 -0
- package/landing/src/app/api/auth/session/route.ts +38 -0
- package/landing/src/app/api/auth/verify/route.ts +43 -0
- package/landing/src/app/api/og/route.tsx +84 -0
- package/landing/src/app/globals.css +439 -100
- package/landing/src/app/layout.tsx +37 -7
- package/landing/src/app/page.tsx +627 -552
- package/landing/src/app/providers/dashboard/login/page.tsx +176 -0
- package/landing/src/app/providers/dashboard/page.tsx +589 -0
- package/landing/src/app/providers/dashboard/verify/page.tsx +106 -0
- package/landing/src/app/providers/layout.tsx +14 -0
- package/landing/src/app/providers/page.tsx +402 -0
- package/landing/src/app/providers/register/page.tsx +670 -0
- package/landing/src/components/ProviderDashboard.tsx +794 -0
- package/landing/src/hooks/useDashboardData.ts +99 -0
- package/landing/src/lib/apis.json +116054 -0
- package/landing/src/lib/convex-client.ts +106 -0
- package/landing/src/lib/mock-data.ts +285 -0
- package/landing/src/lib/stats.json +6 -0
- package/landing/tailwind.config.ts +12 -11
- package/landing/tsconfig.tsbuildinfo +1 -0
- package/package.json +21 -20
- package/scripts/SYMBOT-FIX.md +238 -0
- package/scripts/demo-simulation.py +177 -0
- package/scripts/expand-more.py +502 -0
- package/scripts/expand-registry.py +434 -0
- package/scripts/history-sanitizer.ts +272 -0
- package/scripts/mass-scrape.py +1308 -0
- package/scripts/sync-and-deploy.sh +36 -0
- package/src/credentials.ts +177 -0
- package/src/credits.ts +190 -122
- package/src/discovery.ts +45 -58
- package/src/execute.ts +350 -0
- package/src/index.ts +184 -32
- package/src/proxy.ts +24 -0
- package/src/registry/apis.json +95362 -202
- package/src/registry/apis_expanded.json +100853 -0
- package/src/stripe.ts +243 -0
- package/src/telemetry.ts +71 -0
- package/src/test.ts +127 -89
- package/src/types.ts +0 -34
- package/src/webhook.ts +107 -0
- package/.github/ISSUE_TEMPLATE/add-api.yml +0 -123
- package/BRIEFING.md +0 -30
- package/backend/convex/apiKeys.ts +0 -75
- package/backend/convex/purchases.ts +0 -74
- package/backend/convex/schema.ts +0 -45
- package/backend/convex/transactions.ts +0 -57
- package/backend/convex/users.ts +0 -94
- package/backend/package-lock.json +0 -521
- package/backend/package.json +0 -15
- package/dist/registry/parse_apis.py +0 -146
- package/dist/revenuecat.d.ts +0 -61
- package/dist/revenuecat.d.ts.map +0 -1
- package/dist/revenuecat.js +0 -166
- package/dist/revenuecat.js.map +0 -1
- package/dist/webhooks/revenuecat.d.ts +0 -48
- package/dist/webhooks/revenuecat.d.ts.map +0 -1
- package/dist/webhooks/revenuecat.js +0 -119
- package/dist/webhooks/revenuecat.js.map +0 -1
- package/docs/revenuecat-setup.md +0 -89
- package/landing/src/app/api/keys/route.ts +0 -71
- package/landing/src/app/api/log/route.ts +0 -37
- package/landing/src/app/api/stats/route.ts +0 -37
- package/landing/src/app/page.tsx.bak +0 -567
- package/landing/src/components/AddKeyModal.tsx +0 -159
- package/newsletter-template.html +0 -71
- package/outreach/OUTREACH-SYSTEM.md +0 -211
- package/outreach/email-template.html +0 -179
- package/outreach/targets.md +0 -133
- package/src/registry/parse_apis.py +0 -146
- package/src/revenuecat.ts +0 -239
- package/src/webhooks/revenuecat.ts +0 -187
- /package/{backend/convex → convex}/README.md +0 -0
- /package/{backend/convex → convex}/_generated/api.js +0 -0
- /package/{backend/convex → convex}/_generated/dataModel.d.ts +0 -0
- /package/{backend/convex → convex}/_generated/server.d.ts +0 -0
- /package/{backend/convex → convex}/_generated/server.js +0 -0
- /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 {
|
|
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:
|
|
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
|
-
|
|
36
|
-
|
|
26
|
+
maxPrice?: number;
|
|
27
|
+
region?: string;
|
|
37
28
|
} = {}
|
|
38
|
-
):
|
|
39
|
-
const { category, maxResults =
|
|
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:
|
|
35
|
+
const results: SearchResult[] = [];
|
|
45
36
|
|
|
46
37
|
for (const api of apis) {
|
|
47
|
-
// Category filter
|
|
48
|
-
if (category && api.category
|
|
38
|
+
// Category filter
|
|
39
|
+
if (category && api.category !== category) continue;
|
|
49
40
|
|
|
50
|
-
//
|
|
51
|
-
if (
|
|
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
|
-
//
|
|
62
|
-
if (api.
|
|
63
|
-
score +=
|
|
64
|
-
matchReasons.push(`
|
|
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
|
-
//
|
|
68
|
-
if (api.
|
|
56
|
+
// Capability match
|
|
57
|
+
if (api.capabilities?.some(c => c.includes(word))) {
|
|
69
58
|
score += 15;
|
|
70
|
-
matchReasons.push(`
|
|
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 +=
|
|
70
|
+
score += 5;
|
|
76
71
|
matchReasons.push(`description: ${word}`);
|
|
77
72
|
}
|
|
78
73
|
|
|
79
|
-
//
|
|
80
|
-
if (api.
|
|
74
|
+
// Feature match
|
|
75
|
+
if (api.features?.some(f => f.toLowerCase().includes(word))) {
|
|
81
76
|
score += 8;
|
|
82
|
-
matchReasons.push(`
|
|
77
|
+
matchReasons.push(`feature: ${word}`);
|
|
83
78
|
}
|
|
84
79
|
}
|
|
85
80
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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):
|
|
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):
|
|
125
|
-
return apis.filter(api => api.category
|
|
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))]
|
|
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():
|
|
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
|
+
}
|