@nordsym/apiclaw 2.1.0 → 2.2.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 (185) hide show
  1. package/README.md +15 -2
  2. package/dist/bin-http.js +0 -0
  3. package/dist/bin.bundled.js +79288 -0
  4. package/dist/funnel-client.d.ts +24 -0
  5. package/dist/funnel-client.d.ts.map +1 -0
  6. package/dist/funnel-client.js +131 -0
  7. package/dist/funnel-client.js.map +1 -0
  8. package/dist/funnel.test.d.ts +2 -0
  9. package/dist/funnel.test.d.ts.map +1 -0
  10. package/dist/funnel.test.js +145 -0
  11. package/dist/funnel.test.js.map +1 -0
  12. package/dist/gateway-client.d.ts.map +1 -1
  13. package/dist/gateway-client.js +24 -2
  14. package/dist/gateway-client.js.map +1 -1
  15. package/dist/index.bundled.js +61263 -0
  16. package/dist/index.js +161 -74
  17. package/dist/index.js.map +1 -1
  18. package/dist/postinstall.d.ts +0 -5
  19. package/dist/postinstall.d.ts.map +1 -1
  20. package/dist/postinstall.js +24 -3
  21. package/dist/postinstall.js.map +1 -1
  22. package/dist/registration-guard.d.ts +29 -0
  23. package/dist/registration-guard.d.ts.map +1 -0
  24. package/dist/registration-guard.js +87 -0
  25. package/dist/registration-guard.js.map +1 -0
  26. package/package.json +7 -2
  27. package/.claude/settings.local.json +0 -9
  28. package/.env.prod +0 -1
  29. package/apiclaw-README.md +0 -494
  30. package/convex/_generated/api.d.ts +0 -137
  31. package/convex/_generated/api.js +0 -23
  32. package/convex/_generated/dataModel.d.ts +0 -60
  33. package/convex/_generated/server.d.ts +0 -143
  34. package/convex/_generated/server.js +0 -93
  35. package/convex/adminActivate.ts +0 -53
  36. package/convex/adminStats.ts +0 -306
  37. package/convex/agents.ts +0 -939
  38. package/convex/analytics.ts +0 -187
  39. package/convex/apiKeys.ts +0 -220
  40. package/convex/backfillAnalytics.ts +0 -272
  41. package/convex/backfillSearchLogs.ts +0 -35
  42. package/convex/billing.ts +0 -834
  43. package/convex/capabilities.ts +0 -157
  44. package/convex/chains.ts +0 -1318
  45. package/convex/credits.ts +0 -211
  46. package/convex/crons.ts +0 -50
  47. package/convex/debugFilestackLogs.ts +0 -16
  48. package/convex/debugGetToken.ts +0 -18
  49. package/convex/directCall.ts +0 -713
  50. package/convex/earnProgress.ts +0 -753
  51. package/convex/email.ts +0 -329
  52. package/convex/feedback.ts +0 -265
  53. package/convex/http.ts +0 -3430
  54. package/convex/inbound.ts +0 -32
  55. package/convex/logs.ts +0 -701
  56. package/convex/migrateFilestack.ts +0 -81
  57. package/convex/migratePartnersProd.ts +0 -174
  58. package/convex/migratePratham.ts +0 -126
  59. package/convex/migrateProviderWorkspaces.ts +0 -175
  60. package/convex/mou.ts +0 -91
  61. package/convex/providerKeys.ts +0 -289
  62. package/convex/providers.ts +0 -1135
  63. package/convex/purchases.ts +0 -183
  64. package/convex/ratelimit.ts +0 -104
  65. package/convex/schema.ts +0 -869
  66. package/convex/searchLogs.ts +0 -265
  67. package/convex/seedAPILayerAPIs.ts +0 -191
  68. package/convex/seedDirectCallConfigs.ts +0 -336
  69. package/convex/seedPratham.ts +0 -149
  70. package/convex/spendAlerts.ts +0 -442
  71. package/convex/stripeActions.ts +0 -607
  72. package/convex/teams.ts +0 -243
  73. package/convex/telemetry.ts +0 -81
  74. package/convex/tsconfig.json +0 -25
  75. package/convex/updateAPIStatus.ts +0 -44
  76. package/convex/usage.ts +0 -260
  77. package/convex/usageReports.ts +0 -357
  78. package/convex/waitlist.ts +0 -55
  79. package/convex/webhooks.ts +0 -494
  80. package/convex/workspaceSettings.ts +0 -143
  81. package/convex/workspaces.ts +0 -1331
  82. package/convex.json +0 -3
  83. package/direct-test.mjs +0 -51
  84. package/email-templates/filestack-provider-outreach.html +0 -162
  85. package/email-templates/partnership-template.html +0 -116
  86. package/email-templates/pratham-draft-preview.txt +0 -57
  87. package/email-templates/pratham-partnership-draft.html +0 -141
  88. package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
  89. package/reports/pipeline/PIPELINE-REPORT.json +0 -153
  90. package/reports/pipeline/acquire_apisguru.json +0 -17
  91. package/reports/pipeline/capabilities.json +0 -38
  92. package/reports/pipeline/discover_azure_recursive.json +0 -1551
  93. package/reports/pipeline/discover_github.json +0 -25
  94. package/reports/pipeline/discover_github_repos.json +0 -49
  95. package/reports/pipeline/discover_swaggerhub.json +0 -24
  96. package/reports/pipeline/discover_well_known.json +0 -23
  97. package/reports/pipeline/fetch_specs.json +0 -19
  98. package/reports/pipeline/generate_providers.json +0 -14
  99. package/reports/pipeline/match_registry.json +0 -11
  100. package/reports/pipeline/parse_specs.json +0 -17
  101. package/reports/pipeline/promote_candidates.json +0 -34
  102. package/reports/pipeline/validate.json +0 -30
  103. package/reports/pipeline/validate_smoke_details.json +0 -3835
  104. package/reports/session-report-2026-04-05.html +0 -433
  105. package/seed-apis-direct.mjs +0 -106
  106. package/src/access-control.ts +0 -174
  107. package/src/adapters/base.ts +0 -364
  108. package/src/adapters/claude-desktop.ts +0 -41
  109. package/src/adapters/cline.ts +0 -88
  110. package/src/adapters/continue.ts +0 -91
  111. package/src/adapters/cursor.ts +0 -43
  112. package/src/adapters/custom.ts +0 -188
  113. package/src/adapters/detect.ts +0 -202
  114. package/src/adapters/index.ts +0 -47
  115. package/src/adapters/windsurf.ts +0 -44
  116. package/src/bin-http.ts +0 -45
  117. package/src/bin.ts +0 -34
  118. package/src/capability-router.ts +0 -331
  119. package/src/chainExecutor.ts +0 -730
  120. package/src/chainResolver.test.ts +0 -246
  121. package/src/chainResolver.ts +0 -658
  122. package/src/cli/commands/demo.ts +0 -109
  123. package/src/cli/commands/doctor.ts +0 -435
  124. package/src/cli/commands/index.ts +0 -9
  125. package/src/cli/commands/login.ts +0 -203
  126. package/src/cli/commands/mcp-install.ts +0 -373
  127. package/src/cli/commands/restore.ts +0 -333
  128. package/src/cli/commands/setup.ts +0 -297
  129. package/src/cli/commands/uninstall.ts +0 -240
  130. package/src/cli/index.ts +0 -148
  131. package/src/cli.ts +0 -370
  132. package/src/confirmation.ts +0 -296
  133. package/src/credentials.ts +0 -455
  134. package/src/credits.ts +0 -329
  135. package/src/crypto.ts +0 -75
  136. package/src/discovery.ts +0 -568
  137. package/src/enterprise/env.ts +0 -156
  138. package/src/enterprise/index.ts +0 -7
  139. package/src/enterprise/script-generator.ts +0 -481
  140. package/src/execute-dynamic.ts +0 -617
  141. package/src/execute.ts +0 -2386
  142. package/src/gateway-client.ts +0 -192
  143. package/src/hivr-whitelist.ts +0 -110
  144. package/src/http-api.ts +0 -286
  145. package/src/http-server-minimal.ts +0 -154
  146. package/src/index.ts +0 -2611
  147. package/src/intelligent-gateway.ts +0 -339
  148. package/src/mcp-analytics.ts +0 -156
  149. package/src/metered.ts +0 -149
  150. package/src/open-apis-generated.ts +0 -157
  151. package/src/open-apis.ts +0 -558
  152. package/src/postinstall.ts +0 -18
  153. package/src/product-whitelist.ts +0 -246
  154. package/src/proxy.ts +0 -36
  155. package/src/session.ts +0 -129
  156. package/src/stripe.ts +0 -497
  157. package/src/telemetry.ts +0 -71
  158. package/src/test.ts +0 -135
  159. package/src/types/convex-api.d.ts +0 -20
  160. package/src/types/convex-api.ts +0 -21
  161. package/src/types.ts +0 -109
  162. package/src/ui/colors.ts +0 -219
  163. package/src/ui/errors.ts +0 -394
  164. package/src/ui/index.ts +0 -17
  165. package/src/ui/prompts.ts +0 -390
  166. package/src/ui/spinner.ts +0 -325
  167. package/src/utils/backup.ts +0 -224
  168. package/src/utils/config.ts +0 -318
  169. package/src/utils/os.ts +0 -124
  170. package/src/utils/paths.ts +0 -203
  171. package/src/webhook.ts +0 -107
  172. package/test-10-working.cjs +0 -97
  173. package/test-14-final.cjs +0 -96
  174. package/test-actual-handlers.ts +0 -92
  175. package/test-apilayer-all-14.ts +0 -249
  176. package/test-apilayer-fixed.ts +0 -248
  177. package/test-direct-endpoints.ts +0 -174
  178. package/test-exact-endpoints.ts +0 -144
  179. package/test-final.ts +0 -83
  180. package/test-full-routing.ts +0 -100
  181. package/test-handlers-correct.ts +0 -217
  182. package/test-numverify-key.ts +0 -41
  183. package/test-via-handlers.ts +0 -92
  184. package/test-worldnews.mjs +0 -26
  185. package/tsconfig.json +0 -20
package/src/index.ts DELETED
@@ -1,2611 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * APIvault - Agent-Native API Discovery MCP Server
4
- *
5
- * Tools:
6
- * - discover_apis: Search for APIs by capability
7
- * - get_api_details: Get full info about an API
8
- * - purchase_access: Buy API access with credits
9
- * - check_balance: Check credits and active purchases
10
- * - add_credits: Add credits to account (for testing)
11
- */
12
-
13
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
- import {
16
- CallToolRequestSchema,
17
- ListToolsRequestSchema,
18
- Tool,
19
- } from '@modelcontextprotocol/sdk/types.js';
20
-
21
- import { discoverAPIs, getAPIDetails, getCategories, getAllAPIs } from './discovery.js';
22
- import { trackStartup, trackSearch, trackExecute, trackDiscovery } from './telemetry.js';
23
- import {
24
- getAgentCredits,
25
- addCredits,
26
- purchaseAPIAccess,
27
- getBalanceSummary,
28
- getAgentPurchases,
29
- getProvidersWithRealCredentials
30
- } from './credits.js';
31
- import { hasRealCredentials } from './credentials.js';
32
- import { getConnectedProviders } from './execute.js';
33
- import { executeMetered } from './metered.js';
34
- import { logAPICall } from './mcp-analytics.js';
35
- import { isOpenAPI, executeOpenAPI, listOpenAPIs, getOpenAPIActions, getOpenAPIBaseUrl } from './open-apis.js';
36
- import { getGateway, isGatewayEnabled, type GatewayResponse } from './gateway-client.js';
37
- import { PROXY_PROVIDERS } from './proxy.js';
38
- import {
39
- requiresConfirmation,
40
- requiresConfirmationAsync,
41
- createPendingAction,
42
- consumePendingAction,
43
- generatePreview,
44
- validateParams
45
- } from './confirmation.js';
46
- import { executeCapability, listCapabilities, hasCapability } from './capability-router.js';
47
- import { readSession, writeSession, clearSession, getMachineFingerprint, detectMCPClient, SessionData } from './session.js';
48
- import { ConvexHttpClient } from 'convex/browser';
49
- import {
50
- getOrCreateCustomer,
51
- createMeteredCheckoutSession,
52
- getUsageSummary,
53
- METERED_BILLING
54
- } from './stripe.js';
55
- import { estimateCost } from './metered.js';
56
- import {
57
- executeChain,
58
- getChainStatus,
59
- resumeChain,
60
- type ChainDefinition,
61
- type ChainResult,
62
- type Credentials as ChainCredentials,
63
- type ChainOptions,
64
- type ChainStepUnion
65
- } from './chainExecutor.js';
66
-
67
- // Default agent ID for MVP (in production, this would come from auth)
68
- const DEFAULT_AGENT_ID = 'agent_default';
69
-
70
- // Convex client for workspace management
71
- const CONVEX_URL = process.env.CONVEX_URL || 'https://adventurous-avocet-799.convex.cloud';
72
- const convex = new ConvexHttpClient(CONVEX_URL);
73
-
74
- // Global workspace context (set on startup if session is valid)
75
- interface WorkspaceContext {
76
- sessionToken: string;
77
- workspaceId: string;
78
- email: string;
79
- tier: string;
80
- usageRemaining: number;
81
- usageCount: number;
82
- status: string;
83
- }
84
-
85
- let workspaceContext: WorkspaceContext | null = null;
86
- let currentAgentId: string | null = null; // Agent ID from agents table (set on startup)
87
- let pendingRegistrationEmail: string | null = null; // Email waiting for OTP verification
88
-
89
- // Anonymous rate limit tracking (in-memory, per machine fingerprint)
90
- interface AnonymousRateLimitState {
91
- hourlyCount: number;
92
- hourlyResetTime: number;
93
- weeklyCount: number;
94
- weeklyResetTime: number;
95
- }
96
-
97
- const anonymousRateLimits = new Map<string, AnonymousRateLimitState>();
98
-
99
- // Rate limit constants
100
- const ANONYMOUS_HOURLY_LIMIT = 5;
101
- const ANONYMOUS_WEEKLY_LIMIT = 10;
102
- const FREE_MONTHLY_LIMIT = 50;
103
-
104
- /**
105
- * Calculate minutes until next hour
106
- */
107
- function calculateMinutesUntilNextHour(): number {
108
- const now = new Date();
109
- const nextHour = new Date(now);
110
- nextHour.setHours(now.getHours() + 1, 0, 0, 0);
111
- return Math.ceil((nextHour.getTime() - now.getTime()) / 60000);
112
- }
113
-
114
- /**
115
- * Get next Monday 00:00 UTC as ISO string
116
- */
117
- function getNextMonthUTC(): string {
118
- const now = new Date();
119
- const nextMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));
120
- return nextMonth.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
121
- }
122
-
123
- /**
124
- * Check anonymous rate limits for proxy provider usage
125
- */
126
- function checkAnonymousRateLimit(fingerprint: string): { allowed: boolean; error?: string; isAnonymous?: boolean } {
127
- const now = Date.now();
128
- const hourInMs = 60 * 60 * 1000;
129
- const weekInMs = 7 * 24 * hourInMs;
130
-
131
- // Get or initialize rate limit state
132
- let state = anonymousRateLimits.get(fingerprint);
133
- if (!state) {
134
- state = {
135
- hourlyCount: 0,
136
- hourlyResetTime: now + hourInMs,
137
- weeklyCount: 0,
138
- weeklyResetTime: now + weekInMs,
139
- };
140
- anonymousRateLimits.set(fingerprint, state);
141
- }
142
-
143
- // Reset hourly counter if time elapsed
144
- if (now >= state.hourlyResetTime) {
145
- state.hourlyCount = 0;
146
- state.hourlyResetTime = now + hourInMs;
147
- }
148
-
149
- // Reset weekly counter if time elapsed
150
- if (now >= state.weeklyResetTime) {
151
- state.weeklyCount = 0;
152
- state.weeklyResetTime = now + weekInMs;
153
- }
154
-
155
- // Check hourly limit
156
- if (state.hourlyCount >= ANONYMOUS_HOURLY_LIMIT) {
157
- return {
158
- allowed: false,
159
- error: JSON.stringify({
160
- success: false,
161
- error: `Hourly rate limit (${ANONYMOUS_HOURLY_LIMIT} calls/hour)`,
162
- retry_after_minutes: calculateMinutesUntilNextHour(),
163
- hint: "Rate limit resets at top of hour",
164
- action: "Register to get higher limits: register_owner({ email: 'you@example.com' })"
165
- }, null, 2)
166
- };
167
- }
168
-
169
- // Check weekly limit
170
- if (state.weeklyCount >= ANONYMOUS_WEEKLY_LIMIT) {
171
- return {
172
- allowed: false,
173
- error: JSON.stringify({
174
- success: false,
175
- error: `⚡ You've hit your free tier limit (${ANONYMOUS_WEEKLY_LIMIT} calls/week).\n Upgrade: https://apiclaw.cloud/upgrade`,
176
- hint: "Register for 50 calls/week, or upgrade for unlimited",
177
- action: "Run: register_owner({ email: 'you@example.com' })",
178
- upgrade_url: "https://apiclaw.cloud/upgrade",
179
- retry_after: getNextMonthUTC()
180
- }, null, 2)
181
- };
182
- }
183
-
184
- // Increment counters
185
- state.hourlyCount++;
186
- state.weeklyCount++;
187
-
188
-
189
- return { allowed: true };
190
- }
191
-
192
- /**
193
- * Validate session on startup
194
- */
195
- async function validateSession(): Promise<boolean> {
196
- const session = readSession();
197
- if (!session) {
198
- console.error('[APIClaw] No session found. Use register_owner to authenticate.');
199
- return false;
200
- }
201
-
202
- try {
203
- const result = await convex.query("workspaces:getWorkspaceStatus" as any, {
204
- sessionToken: session.sessionToken,
205
- }) as { authenticated: boolean; email?: string; status?: string; tier?: string; usageCount?: number; usageLimit?: number; usageRemaining?: number };
206
-
207
- if (!result.authenticated) {
208
- console.error('[APIClaw] Session invalid or expired. Clearing...');
209
- clearSession();
210
- return false;
211
- }
212
-
213
- if (result.status !== 'active') {
214
- console.error(`[APIClaw] Workspace status: ${result.status}. Please verify your email.`);
215
- return false;
216
- }
217
-
218
- workspaceContext = {
219
- sessionToken: session.sessionToken,
220
- workspaceId: session.workspaceId,
221
- email: result.email ?? '',
222
- tier: result.tier ?? 'free',
223
- usageRemaining: result.usageRemaining ?? 0,
224
- usageCount: result.usageCount ?? 0,
225
- status: result.status ?? 'unknown',
226
- };
227
-
228
- console.error(`[APIClaw] ✓ Authenticated as ${result.email} (${result.tier} tier)`);
229
- console.error(`[APIClaw] ✓ Usage: ${result.usageCount}/${result.usageLimit === -1 ? '∞' : result.usageLimit} calls`);
230
-
231
- // Touch session to update last used
232
- await convex.mutation("workspaces:touchSession" as any, {
233
- sessionToken: session.sessionToken,
234
- });
235
-
236
- return true;
237
- } catch (error) {
238
- console.error('[APIClaw] Error validating session:', error);
239
- return false;
240
- }
241
- }
242
-
243
- /**
244
- * Track earn progress after successful API call
245
- * Handles firstDirectCall and apisUsed tracking
246
- */
247
- async function trackEarnProgress(workspaceId: string, provider: string, action: string): Promise<void> {
248
- try {
249
- // Track first direct call
250
- await convex.mutation("earnProgress:markFirstDirectCall" as any, {
251
- workspaceId: workspaceId as any,
252
- });
253
-
254
- // Track unique API usage
255
- const apiId = `${provider}:${action}`;
256
- await convex.mutation("earnProgress:trackApiUsed" as any, {
257
- workspaceId: workspaceId as any,
258
- apiId,
259
- });
260
- } catch (e) {
261
- // Non-critical - don't fail the API call if earn tracking fails
262
- console.error('[APIClaw] Failed to track earn progress:', e);
263
- }
264
- }
265
-
266
- /**
267
- * Rate limiting for anonymous proxy usage
268
- * Limits: 10 calls/week, 5 calls/hour (anonymous)
269
- * 50 calls/month, 10 calls/hour (authenticated)
270
- */
271
- interface RateLimitState {
272
- hourly: { count: number; resetAt: number };
273
- weekly: { count: number; resetAt: number };
274
- }
275
-
276
- const rateLimitStore = new Map<string, RateLimitState>();
277
-
278
- // Unregistered (auto-provisioned, no email) users get this many calls before signup required
279
- const UNREGISTERED_CALL_LIMIT = 5;
280
-
281
- /**
282
- * Check workspace access -- registration required for all API calls
283
- */
284
- function checkWorkspaceAccess(providerId?: string): { allowed: boolean; error?: string; isAnonymous?: boolean } {
285
- // All API calls require registration now
286
- if (!workspaceContext) {
287
- return {
288
- allowed: false,
289
- error: JSON.stringify({
290
- status: 'registration_required',
291
- error: 'Registration required to call APIs.',
292
- message: 'Ask the user for their email, then call register_owner({ email: "..." }). A 6-digit code will be sent. Then call verify_code with the code.',
293
- action: 'register_owner',
294
- free_tier: '50 API calls/month -- completely free.',
295
- }, null, 2),
296
- isAnonymous: true,
297
- };
298
- }
299
-
300
- if (workspaceContext.status !== 'active') {
301
- return {
302
- allowed: false,
303
- error: `Workspace status: ${workspaceContext.status}. Please verify your email.`
304
- };
305
- }
306
-
307
- // Unregistered workspaces (auto-provisioned, no email) get limited calls then must register
308
- if (!workspaceContext.email && workspaceContext.usageCount >= UNREGISTERED_CALL_LIMIT) {
309
- return {
310
- allowed: false,
311
- error: JSON.stringify({
312
- success: false,
313
- error: `Register to continue. You've used ${UNREGISTERED_CALL_LIMIT} free calls.`,
314
- hint: "Run register_owner with your email to unlock 50 calls/month.",
315
- action: "register_owner"
316
- }, null, 2)
317
- };
318
- }
319
-
320
- if (workspaceContext.usageRemaining === 0) {
321
- // Free tier hit weekly limit
322
- if (workspaceContext.tier === 'free') {
323
- return {
324
- allowed: false,
325
- error: JSON.stringify({
326
- success: false,
327
- error: `⚡ You've hit your free tier limit (${FREE_MONTHLY_LIMIT} calls/week).\n Upgrade: https://apiclaw.cloud/upgrade`,
328
- hint: "Upgrade to Pro for unlimited calls",
329
- upgrade_url: "https://apiclaw.cloud/upgrade",
330
- retry_after: getNextMonthUTC()
331
- }, null, 2)
332
- };
333
- }
334
-
335
- // Other tiers (shouldn't happen, but handle gracefully)
336
- return {
337
- allowed: false,
338
- error: `⚡ You've hit your free tier limit (${FREE_MONTHLY_LIMIT} calls/week).\n Upgrade: https://apiclaw.cloud/upgrade`
339
- };
340
- }
341
-
342
- return { allowed: true, isAnonymous: false };
343
- }
344
-
345
- /**
346
- * Get customer API key from environment variable
347
- * Convention: {PROVIDER}_API_KEY (e.g., COACCEPT_API_KEY, ELKS_API_KEY)
348
- */
349
- function getCustomerKey(providerId: string): string | undefined {
350
- // Try exact match first (e.g., 46elks -> 46ELKS_API_KEY)
351
- const exactKey = `${providerId.toUpperCase().replace(/-/g, '_')}_API_KEY`;
352
- if (process.env[exactKey]) {
353
- return process.env[exactKey];
354
- }
355
-
356
- // Try common variations
357
- const variations = [
358
- `${providerId.toUpperCase()}_API_KEY`,
359
- `${providerId.toUpperCase()}_KEY`,
360
- `${providerId.toUpperCase().replace(/_/g, '')}_API_KEY`,
361
- ];
362
-
363
- for (const key of variations) {
364
- if (process.env[key]) {
365
- return process.env[key];
366
- }
367
- }
368
-
369
- return undefined;
370
- }
371
-
372
- // Tool definitions
373
- const tools: Tool[] = [
374
- {
375
- name: 'apiclaw_help',
376
- description: 'Get help and see available commands. Start here if you are new to APIClaw.',
377
- inputSchema: {
378
- type: 'object',
379
- properties: {},
380
- required: []
381
- }
382
- },
383
- {
384
- name: 'discover_apis',
385
- description: 'Search for APIs based on what you need to do. Describe your use case naturally.',
386
- inputSchema: {
387
- type: 'object',
388
- properties: {
389
- query: {
390
- type: 'string',
391
- description: 'Natural language query describing what you need (e.g., "send SMS to Sweden", "search the web", "generate speech from text")'
392
- },
393
- category: {
394
- type: 'string',
395
- description: 'Filter by category: communication, search, ai',
396
- enum: ['communication', 'search', 'ai']
397
- },
398
- max_results: {
399
- type: 'number',
400
- description: 'Maximum number of results to return (default: 5)',
401
- default: 5
402
- },
403
- region: {
404
- type: 'string',
405
- description: 'Filter by region (e.g., "SE", "EU", "global")'
406
- },
407
- subagent_id: {
408
- type: 'string',
409
- description: 'Optional subagent identifier for multi-agent tracking'
410
- },
411
- ai_backend: {
412
- type: 'string',
413
- description: 'AI backend making this request (e.g., "claude-3-sonnet", "gpt-4"). Used for analytics.'
414
- }
415
- },
416
- required: ['query']
417
- }
418
- },
419
- {
420
- name: 'get_api_details',
421
- description: 'Get detailed information about a specific API provider, including endpoints, pricing, and features. Use compact=true to save ~60% tokens.',
422
- inputSchema: {
423
- type: 'object',
424
- properties: {
425
- api_id: {
426
- type: 'string',
427
- description: 'The API provider ID (e.g., "46elks", "resend", "openrouter")'
428
- },
429
- compact: {
430
- type: 'boolean',
431
- description: 'If true, returns minified spec (strips examples, keeps essential params). Saves ~60% context window.',
432
- default: false
433
- }
434
- },
435
- required: ['api_id']
436
- }
437
- },
438
- {
439
- name: 'purchase_access',
440
- description: 'Purchase access to an API using your credit balance. Returns API credentials on success.',
441
- inputSchema: {
442
- type: 'object',
443
- properties: {
444
- api_id: {
445
- type: 'string',
446
- description: 'The API provider ID to purchase access to'
447
- },
448
- amount_usd: {
449
- type: 'number',
450
- description: 'Amount in USD to spend on this API'
451
- },
452
- agent_id: {
453
- type: 'string',
454
- description: 'Your agent identifier (optional, uses default if not provided)'
455
- }
456
- },
457
- required: ['api_id', 'amount_usd']
458
- }
459
- },
460
- {
461
- name: 'check_balance',
462
- description: 'Check your credit balance and list active API purchases.',
463
- inputSchema: {
464
- type: 'object',
465
- properties: {
466
- agent_id: {
467
- type: 'string',
468
- description: 'Your agent identifier (optional, uses default if not provided)'
469
- }
470
- }
471
- }
472
- },
473
- {
474
- name: 'add_credits',
475
- description: 'Add credits to your account. (For testing/demo purposes)',
476
- inputSchema: {
477
- type: 'object',
478
- properties: {
479
- amount_usd: {
480
- type: 'number',
481
- description: 'Amount in USD to add to your balance'
482
- },
483
- agent_id: {
484
- type: 'string',
485
- description: 'Your agent identifier (optional, uses default if not provided)'
486
- }
487
- },
488
- required: ['amount_usd']
489
- }
490
- },
491
- {
492
- name: 'list_categories',
493
- description: 'List all available API categories.',
494
- inputSchema: {
495
- type: 'object',
496
- properties: {}
497
- }
498
- },
499
- {
500
- name: 'call_api',
501
- description: `Execute an API call through APIClaw. Requires registration (free). If not registered, call register_owner first.
502
-
503
- SINGLE CALL: Provide provider + action + params
504
- CHAIN: Provide chain array to execute multiple APIs in sequence/parallel with cross-step references.
505
-
506
- Chain features:
507
- - Sequential: Steps execute in order, each can reference previous results via $stepId.property
508
- - Parallel: Use { parallel: [...steps] } to run concurrently
509
- - Conditional: Use { if: "$step.success", then: {...}, else: {...} }
510
- - Loops: Use { forEach: "$step.results", as: "item", do: {...} }
511
- - Error handling: Per-step retry/fallback via onError
512
- - Async: Set async: true to get chainId immediately, poll or use webhook
513
-
514
- Example chain:
515
- chain: [
516
- { id: "search", provider: "brave_search", action: "search", params: { query: "AI agents" } },
517
- { id: "summarize", provider: "openrouter", action: "chat", params: { message: "Summarize: $search.results" } }
518
- ]`,
519
- inputSchema: {
520
- type: 'object',
521
- properties: {
522
- // Single call params
523
- provider: {
524
- type: 'string',
525
- description: 'Provider ID (e.g., "46elks", "brave_search", "resend", "openrouter", "elevenlabs", "twilio", "coaccept", "frankfurter")'
526
- },
527
- action: {
528
- type: 'string',
529
- description: 'Action to perform (e.g., "send_sms", "search", "send_email", "chat", "send_invoice", "convert")'
530
- },
531
- params: {
532
- type: 'object',
533
- description: 'Parameters for the action. Varies by provider/action.'
534
- },
535
- customer_key: {
536
- type: 'string',
537
- description: 'Optional: Your own API key for providers that require customer authentication (e.g., CoAccept).'
538
- },
539
- confirm_token: {
540
- type: 'string',
541
- description: 'Confirmation token from a previous call. Required to execute actions that cost money after reviewing the preview.'
542
- },
543
- dry_run: {
544
- type: 'boolean',
545
- description: 'If true, shows what WOULD be sent without making actual API calls. Returns mock response and request details. Great for testing and debugging.'
546
- },
547
- // Chain execution params
548
- chain: {
549
- type: 'array',
550
- description: 'Execute multiple API calls as a single chain. Each step can reference previous results via $stepId.property',
551
- items: {
552
- type: 'object',
553
- properties: {
554
- id: { type: 'string', description: 'Step identifier for cross-step references' },
555
- provider: { type: 'string', description: 'API provider' },
556
- action: { type: 'string', description: 'Action to execute' },
557
- params: { type: 'object', description: 'Action parameters. Use $stepId.path for references.' },
558
- parallel: { type: 'array', description: 'Steps to run in parallel' },
559
- if: { type: 'string', description: 'Condition for conditional execution (e.g., "$step1.success")' },
560
- then: { type: 'object', description: 'Step to execute if condition is true' },
561
- else: { type: 'object', description: 'Step to execute if condition is false' },
562
- forEach: { type: 'string', description: 'Array reference to iterate (e.g., "$search.results")' },
563
- as: { type: 'string', description: 'Variable name for current item in loop' },
564
- do: { type: 'object', description: 'Step to execute for each item' },
565
- onError: {
566
- type: 'object',
567
- description: 'Error handling configuration',
568
- properties: {
569
- retry: {
570
- type: 'object',
571
- properties: {
572
- attempts: { type: 'number', description: 'Max retry attempts' },
573
- backoff: { type: 'string', description: '"exponential" or "linear" or array of ms delays' }
574
- }
575
- },
576
- fallback: { type: 'object', description: 'Fallback step if this fails' },
577
- abort: { type: 'boolean', description: 'Abort entire chain on failure' }
578
- }
579
- }
580
- }
581
- }
582
- },
583
- // Chain options
584
- continueOnError: {
585
- type: 'boolean',
586
- description: 'Continue chain execution even if a step fails (default: false)'
587
- },
588
- timeout: {
589
- type: 'number',
590
- description: 'Maximum execution time for the entire chain in milliseconds'
591
- },
592
- async: {
593
- type: 'boolean',
594
- description: 'Return immediately with chainId. Use get_chain_status to poll or provide webhook.'
595
- },
596
- webhook: {
597
- type: 'string',
598
- description: 'URL to POST results when async chain completes'
599
- },
600
- subagent_id: {
601
- type: 'string',
602
- description: 'Optional subagent identifier for multi-agent tracking'
603
- },
604
- ai_backend: {
605
- type: 'string',
606
- description: 'AI backend making this request (e.g., "claude-3-sonnet", "gpt-4"). Used for analytics.'
607
- }
608
- },
609
- required: []
610
- }
611
- },
612
- {
613
- name: 'list_connected',
614
- description: 'List all APIs available for Direct Call (no API key needed).',
615
- inputSchema: {
616
- type: 'object',
617
- properties: {}
618
- }
619
- },
620
- {
621
- name: 'capability',
622
- description: 'Execute an action by capability, not provider. APIClaw automatically selects the best provider, handles fallback, and optimizes for cost/speed. Example: capability("sms", "send", {to: "+46...", message: "Hello"})',
623
- inputSchema: {
624
- type: 'object',
625
- properties: {
626
- capability: {
627
- type: 'string',
628
- description: 'Capability ID: "sms", "email", "search", "tts", "invoice", "llm"'
629
- },
630
- action: {
631
- type: 'string',
632
- description: 'Action to perform: "send", "search", "generate", etc.'
633
- },
634
- params: {
635
- type: 'object',
636
- description: 'Parameters for the action (capability-standard params, not provider-specific)'
637
- },
638
- preferences: {
639
- type: 'object',
640
- description: 'Optional routing preferences',
641
- properties: {
642
- region: { type: 'string', description: 'Preferred region: "SE", "EU", "US"' },
643
- maxPrice: { type: 'number', description: 'Max price per unit in cents/öre' },
644
- preferredProvider: { type: 'string', description: 'Hint to prefer a specific provider' },
645
- fallback: { type: 'boolean', description: 'Enable fallback to other providers (default: true)' }
646
- }
647
- },
648
- subagent_id: {
649
- type: 'string',
650
- description: 'Optional subagent identifier for multi-agent tracking'
651
- },
652
- ai_backend: {
653
- type: 'string',
654
- description: 'AI backend making this request (e.g., "claude-3-sonnet", "gpt-4"). Used for analytics.'
655
- }
656
- },
657
- required: ['capability', 'action', 'params']
658
- }
659
- },
660
- {
661
- name: 'list_capabilities',
662
- description: 'List all available capabilities and their providers.',
663
- inputSchema: {
664
- type: 'object',
665
- properties: {}
666
- }
667
- },
668
- // ============================================
669
- // WORKSPACE TOOLS
670
- // ============================================
671
- {
672
- name: 'register_owner',
673
- description: 'REQUIRED before using any API. Register your email to create a workspace. A 6-digit verification code will be sent to your email. After calling this, ask the user for the code and call verify_code.',
674
- inputSchema: {
675
- type: 'object',
676
- properties: {
677
- email: {
678
- type: 'string',
679
- description: 'Your email address (used for verification and account recovery)'
680
- }
681
- },
682
- required: ['email']
683
- }
684
- },
685
- {
686
- name: 'verify_code',
687
- description: 'Verify the 6-digit code sent to your email after register_owner. This completes registration and activates your workspace. Ask the user to check their email and paste the code.',
688
- inputSchema: {
689
- type: 'object',
690
- properties: {
691
- email: {
692
- type: 'string',
693
- description: 'The email address used in register_owner'
694
- },
695
- code: {
696
- type: 'string',
697
- description: 'The 6-digit verification code from the email'
698
- }
699
- },
700
- required: ['email', 'code']
701
- }
702
- },
703
- {
704
- name: 'check_workspace_status',
705
- description: 'Check your workspace status, tier, and usage remaining.',
706
- inputSchema: {
707
- type: 'object',
708
- properties: {}
709
- }
710
- },
711
- {
712
- name: 'remind_owner',
713
- description: 'Send a reminder email to verify workspace ownership (if verification is pending).',
714
- inputSchema: {
715
- type: 'object',
716
- properties: {}
717
- }
718
- },
719
- // Metered Billing Tools
720
- {
721
- name: 'setup_metered_billing',
722
- description: 'Set up pay-per-call billing. Creates a subscription that charges $0.002 per API call at end of month.',
723
- inputSchema: {
724
- type: 'object',
725
- properties: {
726
- email: {
727
- type: 'string',
728
- description: 'Email for the billing account'
729
- },
730
- success_url: {
731
- type: 'string',
732
- description: 'URL to redirect after successful setup',
733
- default: 'https://apiclaw.cloud/billing/success'
734
- },
735
- cancel_url: {
736
- type: 'string',
737
- description: 'URL to redirect if setup is cancelled',
738
- default: 'https://apiclaw.cloud/billing/cancel'
739
- }
740
- },
741
- required: ['email']
742
- }
743
- },
744
- {
745
- name: 'get_usage_summary',
746
- description: 'Get current billing period usage and estimated cost for metered billing.',
747
- inputSchema: {
748
- type: 'object',
749
- properties: {
750
- subscription_id: {
751
- type: 'string',
752
- description: 'Stripe subscription ID (stored after setup_metered_billing)'
753
- }
754
- },
755
- required: ['subscription_id']
756
- }
757
- },
758
- {
759
- name: 'estimate_cost',
760
- description: 'Estimate the cost for a given number of API calls.',
761
- inputSchema: {
762
- type: 'object',
763
- properties: {
764
- call_count: {
765
- type: 'number',
766
- description: 'Number of API calls to estimate cost for'
767
- }
768
- },
769
- required: ['call_count']
770
- }
771
- },
772
- // ============================================
773
- // CHAIN MANAGEMENT TOOLS
774
- // ============================================
775
- {
776
- name: 'get_chain_status',
777
- description: 'Check the status of an async chain execution. Use the chainId returned from call_api with async: true.',
778
- inputSchema: {
779
- type: 'object',
780
- properties: {
781
- chain_id: {
782
- type: 'string',
783
- description: 'Chain ID returned from async chain execution'
784
- }
785
- },
786
- required: ['chain_id']
787
- }
788
- },
789
- {
790
- name: 'resume_chain',
791
- description: 'Resume a failed chain from the point of failure. Use the resumeToken from the error response. Requires the original chain definition.',
792
- inputSchema: {
793
- type: 'object',
794
- properties: {
795
- resume_token: {
796
- type: 'string',
797
- description: 'Resume token from a failed chain (e.g., "chain_xyz_step_2")'
798
- },
799
- original_chain: {
800
- type: 'array',
801
- description: 'The original chain definition that failed. Required to resume execution.',
802
- items: { type: 'object' }
803
- },
804
- overrides: {
805
- type: 'object',
806
- description: 'Optional parameter overrides for specific steps. Format: { "stepId": { ...newParams } }'
807
- }
808
- },
809
- required: ['resume_token', 'original_chain']
810
- }
811
- }
812
- ];
813
-
814
- // Create server
815
- const server = new Server(
816
- {
817
- name: 'apivault',
818
- version: '0.1.0',
819
- },
820
- {
821
- capabilities: {
822
- tools: {},
823
- },
824
- }
825
- );
826
-
827
- // Handle list tools
828
- server.setRequestHandler(ListToolsRequestSchema, async () => {
829
- return { tools };
830
- });
831
-
832
- // Handle tool calls
833
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
834
- const { name, arguments: args } = request.params;
835
-
836
- try {
837
- switch (name) {
838
- case 'apiclaw_help': {
839
- const isAuthenticated = !!workspaceContext;
840
- const helpText = `
841
- 🦞 APIClaw -- The API Layer for AI Agents
842
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
843
- ${!isAuthenticated ? `
844
- GET STARTED (free):
845
- 1. register_owner({ email: "you@example.com" }) — sends 6-digit code
846
- 2. verify_code({ email: "you@example.com", code: "123456" }) — activates workspace
847
- ` : `
848
- STATUS: Authenticated as ${workspaceContext!.email} (${workspaceContext!.tier} tier)
849
- `}
850
- DISCOVER APIs (free, no registration needed):
851
- discover_apis({ query: "send SMS to Sweden" })
852
- discover_apis({ query: "text to speech", category: "ai" })
853
-
854
- CALL APIs (requires free registration):
855
- call_api({ provider: "brave_search", action: "search", params: { q: "AI agents" } })
856
- call_api({ provider: "elevenlabs", action: "tts", params: { text: "Hello" } })
857
-
858
- 23 MANAGED PROVIDERS:
859
- OpenAI, Anthropic, xAI/Grok, Groq, Mistral, OpenRouter, Together AI,
860
- Replicate, ElevenLabs, Deepgram, AssemblyAI, Brave Search, Firecrawl,
861
- Serper, Resend, 46elks, Twilio, E2B, Stability AI, Cohere, Voyage AI,
862
- GitHub, APILayer (27 sub-APIs)
863
-
864
- 26,700+ DISCOVERABLE | 1,654 CALLABLE | Free tier: 50 calls/month
865
-
866
- Docs: https://apiclaw.cloud
867
- `;
868
-
869
- return {
870
- content: [{ type: 'text', text: helpText }]
871
- };
872
- }
873
-
874
- case 'discover_apis': {
875
- const query = args?.query as string;
876
- const category = args?.category as string | undefined;
877
- const maxResults = (args?.max_results as number) || 5;
878
- const region = args?.region as string | undefined;
879
- const subagentId = args?.subagent_id as string | undefined;
880
- const aiBackend = args?.ai_backend as string | undefined;
881
-
882
- const startTime = Date.now();
883
- const results = discoverAPIs(query, { category, maxResults, region });
884
- const responseTimeMs = Date.now() - startTime;
885
- trackSearch(query, results.length, responseTimeMs);
886
-
887
- // Log search to Convex analytics (authenticated + anonymous)
888
- const analyticsUserId = workspaceContext?.workspaceId || `anon:${getMachineFingerprint()}`;
889
- const convexUrl = CONVEX_URL;
890
- if (convexUrl) {
891
- fetch(`${convexUrl}/api/mutation`, {
892
- method: 'POST',
893
- headers: { 'Content-Type': 'application/json' },
894
- body: JSON.stringify({
895
- path: 'analytics:log',
896
- args: {
897
- event: 'search_query',
898
- provider: undefined,
899
- query,
900
- identifier: analyticsUserId,
901
- metadata: {
902
- resultCount: results.length,
903
- matchedProviders: results.slice(0, 10).map(r => r.provider.id),
904
- responseTimeMs,
905
- category,
906
- authenticated: !!workspaceContext,
907
- },
908
- },
909
- }),
910
- }).catch(() => {}); // Fire and forget
911
- }
912
-
913
- // Log search to searchLogs table (authenticated only - requires workspace)
914
- if (workspaceContext?.sessionToken) {
915
- const searchLogPayload = {
916
- path: 'searchLogs:log',
917
- args: {
918
- sessionToken: workspaceContext.sessionToken,
919
- subagentId: subagentId || undefined,
920
- query,
921
- resultCount: results.length,
922
- matchedProviders: results.slice(0, 10).map(r => r.provider.id),
923
- responseTimeMs,
924
- },
925
- };
926
-
927
- fetch(`${convexUrl}/api/mutation`, {
928
- method: 'POST',
929
- headers: { 'Content-Type': 'application/json' },
930
- body: JSON.stringify(searchLogPayload),
931
- }).catch(() => {}); // Fire and forget
932
-
933
- // Log discovery to provider workspaces
934
- // Single mutation handles both apiLogs + discoveryCount
935
- const PROVIDER_KEYWORDS: Record<string, string[]> = {
936
- apilayer: ['exchange', 'currency', 'fixer', 'weather', 'ip', 'geo', 'flight', 'aviation', 'vat', 'news', 'scrape', 'screenshot', 'pdf', 'email verif', 'phone verif', 'language', 'user agent', 'coinlayer', 'marketstack', 'positionstack', 'ipstack', 'mediastack', 'serpstack', 'userstack', 'scrapestack', 'weatherstack'],
937
- filestack: ['file upload', 'upload file', 'file storage', 'file picker', 'image upload', 'upload image', 'file transform', 'image transform', 'resize image', 'document upload', 'upload document', 'file delivery', 'cdn upload', 'file processing', 'ocr', 'virus scan', 'file convert', 'convert pdf', 'filestack'],
938
- };
939
- const queryLower = query.toLowerCase();
940
- for (const [provider, keywords] of Object.entries(PROVIDER_KEYWORDS)) {
941
- if (keywords.some(kw => queryLower.includes(kw))) {
942
- // Single call: logs to apiLogs + increments discoveryCount on matching APIs
943
- fetch(`${convexUrl}/api/mutation`, {
944
- method: 'POST',
945
- headers: { 'Content-Type': 'application/json' },
946
- body: JSON.stringify({
947
- path: 'providers:logDiscovery',
948
- args: {
949
- provider,
950
- query: query.substring(0, 100),
951
- latencyMs: responseTimeMs,
952
- callerWorkspaceId: workspaceContext?.workspaceId || 'anonymous',
953
- },
954
- }),
955
- }).catch(() => {});
956
- }
957
- }
958
- }
959
-
960
- // Update AI backend tracking if provided
961
- if (aiBackend && workspaceContext?.sessionToken) {
962
- fetch('https://adventurous-avocet-799.convex.cloud/api/mutation', {
963
- method: 'POST',
964
- headers: { 'Content-Type': 'application/json' },
965
- body: JSON.stringify({
966
- path: 'agents:updateAIBackend',
967
- args: {
968
- token: workspaceContext.sessionToken,
969
- subagentId: subagentId || undefined,
970
- aiBackend,
971
- },
972
- }),
973
- }).catch(() => {}); // Fire and forget
974
- }
975
-
976
- if (results.length === 0) {
977
- return {
978
- content: [
979
- {
980
- type: 'text',
981
- text: JSON.stringify({
982
- status: 'no_results',
983
- message: `No APIs found matching "${query}". Try broader terms or check available categories with list_categories.`,
984
- available_categories: getCategories()
985
- }, null, 2)
986
- }
987
- ]
988
- };
989
- }
990
-
991
- return {
992
- content: [
993
- {
994
- type: 'text',
995
- text: JSON.stringify({
996
- status: 'success',
997
- query,
998
- results_count: results.length,
999
- results: results.map(r => ({
1000
- id: r.provider.id,
1001
- name: r.provider.name,
1002
- description: r.provider.description,
1003
- category: r.provider.category,
1004
- capabilities: r.provider.capabilities,
1005
- pricing_model: r.provider.pricing.model,
1006
- has_free_tier: r.provider.pricing.free_tier,
1007
- agent_success_rate: r.provider.agent_success_rate,
1008
- relevance_score: r.relevance_score,
1009
- match_reasons: r.match_reasons
1010
- }))
1011
- }, null, 2)
1012
- }
1013
- ]
1014
- };
1015
- }
1016
-
1017
- case 'get_api_details': {
1018
- const apiId = args?.api_id as string;
1019
- const compact = args?.compact as boolean || false;
1020
- const api = getAPIDetails(apiId, { compact });
1021
-
1022
- if (!api) {
1023
- return {
1024
- content: [
1025
- {
1026
- type: 'text',
1027
- text: JSON.stringify({
1028
- status: 'error',
1029
- message: `API not found: ${apiId}`,
1030
- hint: 'Try discover_apis to search, or list_connected for direct-call APIs',
1031
- }, null, 2)
1032
- }
1033
- ]
1034
- };
1035
- }
1036
-
1037
- // Compact mode: minimal JSON, no pretty-print
1038
- if (compact) {
1039
- return {
1040
- content: [
1041
- {
1042
- type: 'text',
1043
- text: JSON.stringify({ status: 'ok', ...api })
1044
- }
1045
- ]
1046
- };
1047
- }
1048
-
1049
- return {
1050
- content: [
1051
- {
1052
- type: 'text',
1053
- text: JSON.stringify({
1054
- status: 'success',
1055
- api
1056
- }, null, 2)
1057
- }
1058
- ]
1059
- };
1060
- }
1061
-
1062
- case 'purchase_access': {
1063
- const apiId = args?.api_id as string;
1064
- const amountUsd = args?.amount_usd as number;
1065
- const agentId = (args?.agent_id as string) || DEFAULT_AGENT_ID;
1066
-
1067
- const result = purchaseAPIAccess(agentId, apiId, amountUsd);
1068
-
1069
- if (!result.success) {
1070
- return {
1071
- content: [
1072
- {
1073
- type: 'text',
1074
- text: JSON.stringify({
1075
- status: 'error',
1076
- message: result.error
1077
- }, null, 2)
1078
- }
1079
- ]
1080
- };
1081
- }
1082
-
1083
- const api = getAPIDetails(apiId);
1084
-
1085
- return {
1086
- content: [
1087
- {
1088
- type: 'text',
1089
- text: JSON.stringify({
1090
- status: 'success',
1091
- message: `Successfully purchased access to ${apiId}`,
1092
- purchase: {
1093
- id: result.purchase!.id,
1094
- provider: apiId,
1095
- amount_paid_usd: amountUsd,
1096
- credits_received: result.purchase!.credits_purchased,
1097
- status: result.purchase!.status,
1098
- real_credentials: hasRealCredentials(apiId)
1099
- },
1100
- credentials: result.purchase!.credentials,
1101
- access: {
1102
- base_url: api?.base_url,
1103
- docs_url: api?.docs_url,
1104
- auth_type: api?.auth_type
1105
- }
1106
- }, null, 2)
1107
- }
1108
- ]
1109
- };
1110
- }
1111
-
1112
- case 'check_balance': {
1113
- const agentId = (args?.agent_id as string) || DEFAULT_AGENT_ID;
1114
- const summary = getBalanceSummary(agentId);
1115
-
1116
- return {
1117
- content: [
1118
- {
1119
- type: 'text',
1120
- text: JSON.stringify({
1121
- status: 'success',
1122
- agent_id: agentId,
1123
- balance_usd: summary.credits.balance_usd,
1124
- currency: summary.credits.currency,
1125
- total_spent_usd: summary.total_spent_usd,
1126
- real_credential_providers: summary.real_credentials_available,
1127
- active_purchases: summary.active_purchases.map(p => ({
1128
- id: p.id,
1129
- provider: p.provider_id,
1130
- credits_remaining: p.credits_purchased,
1131
- status: p.status,
1132
- real_credentials: hasRealCredentials(p.provider_id)
1133
- }))
1134
- }, null, 2)
1135
- }
1136
- ]
1137
- };
1138
- }
1139
-
1140
- case 'add_credits': {
1141
- const amountUsd = args?.amount_usd as number;
1142
- const agentId = (args?.agent_id as string) || DEFAULT_AGENT_ID;
1143
-
1144
- const credits = addCredits(agentId, amountUsd);
1145
-
1146
- return {
1147
- content: [
1148
- {
1149
- type: 'text',
1150
- text: JSON.stringify({
1151
- status: 'success',
1152
- message: `Added $${amountUsd.toFixed(2)} to your account`,
1153
- new_balance_usd: credits.balance_usd
1154
- }, null, 2)
1155
- }
1156
- ]
1157
- };
1158
- }
1159
-
1160
- case 'list_categories': {
1161
- const categories = getCategories();
1162
- const apisByCategory: Record<string, string[]> = {};
1163
-
1164
- for (const cat of categories) {
1165
- apisByCategory[cat] = getAllAPIs()
1166
- .filter(a => a.category === cat)
1167
- .map(a => a.id);
1168
- }
1169
-
1170
- return {
1171
- content: [
1172
- {
1173
- type: 'text',
1174
- text: JSON.stringify({
1175
- status: 'success',
1176
- categories: apisByCategory
1177
- }, null, 2)
1178
- }
1179
- ]
1180
- };
1181
- }
1182
-
1183
- case 'call_api': {
1184
- // ============================================
1185
- // REGISTRATION GATE: require email before any API call
1186
- // ============================================
1187
- if (!workspaceContext) {
1188
- return {
1189
- content: [{
1190
- type: 'text',
1191
- text: JSON.stringify({
1192
- status: 'registration_required',
1193
- error: 'You need to register before making API calls.',
1194
- message: 'APIClaw requires a free account to use APIs. Ask the user for their email address, then call register_owner({ email: "user@example.com" }). A 6-digit verification code will be sent to their email. Then call verify_code with the code.',
1195
- action: 'register_owner',
1196
- free_tier: '50 API calls/month, unlimited discovery -- completely free.',
1197
- }, null, 2)
1198
- }],
1199
- isError: true
1200
- };
1201
- }
1202
-
1203
- const provider = args?.provider as string;
1204
- const action = args?.action as string;
1205
- const params = (args?.params as Record<string, any>) || {};
1206
- const confirmToken = args?.confirm_token as string | undefined;
1207
- const dryRun = args?.dry_run as boolean | undefined;
1208
- const chain = args?.chain as ChainStepUnion[] | undefined;
1209
- const subagentId = args?.subagent_id as string | undefined;
1210
- const aiBackend = args?.ai_backend as string | undefined;
1211
-
1212
- // Track AI backend if provided
1213
- if (aiBackend && workspaceContext?.sessionToken) {
1214
- fetch('https://adventurous-avocet-799.convex.cloud/api/mutation', {
1215
- method: 'POST',
1216
- headers: { 'Content-Type': 'application/json' },
1217
- body: JSON.stringify({
1218
- path: 'agents:updateAIBackend',
1219
- args: {
1220
- token: workspaceContext.sessionToken,
1221
- subagentId: subagentId || undefined,
1222
- aiBackend,
1223
- },
1224
- }),
1225
- }).catch(() => {}); // Fire and forget
1226
- }
1227
-
1228
- // ============================================
1229
- // CHAIN EXECUTION MODE
1230
- // ============================================
1231
- if (chain && Array.isArray(chain) && chain.length > 0) {
1232
- // Check workspace access for chains
1233
- const access = checkWorkspaceAccess();
1234
- if (!access.allowed) {
1235
- // If error is already formatted JSON (from rate limit checks), return as-is
1236
- if (access.error?.startsWith('{')) {
1237
- return {
1238
- content: [{
1239
- type: 'text',
1240
- text: access.error
1241
- }],
1242
- isError: true
1243
- };
1244
- }
1245
-
1246
- // Otherwise, wrap in standard error format
1247
- return {
1248
- content: [{
1249
- type: 'text',
1250
- text: JSON.stringify({
1251
- status: 'error',
1252
- error: access.error,
1253
- hint: 'Use register_owner to authenticate your workspace.',
1254
- }, null, 2)
1255
- }],
1256
- isError: true
1257
- };
1258
- }
1259
-
1260
- try {
1261
- // Construct ChainDefinition from the input
1262
- const chainDefinition: ChainDefinition = {
1263
- steps: chain as ChainStepUnion[],
1264
- timeout: args?.timeout as number | undefined,
1265
- errorPolicy: args?.continueOnError
1266
- ? { mode: 'best-effort' as const }
1267
- : { mode: 'fail-fast' as const },
1268
- };
1269
-
1270
- const chainCredentials: ChainCredentials = {
1271
- userId: DEFAULT_AGENT_ID,
1272
- customerKeys: {},
1273
- };
1274
-
1275
- // Add customer key if provided
1276
- const customerKey = args?.customer_key as string | undefined;
1277
- if (customerKey) {
1278
- // Apply to all providers (or could be provider-specific)
1279
- chainCredentials.customerKeys = { default: customerKey };
1280
- }
1281
-
1282
- const chainOptions: ChainOptions = {
1283
- verbose: false,
1284
- };
1285
-
1286
- // Execute the chain
1287
- const chainResult = await executeChain(
1288
- chainDefinition,
1289
- chainCredentials,
1290
- {}, // inputs
1291
- chainOptions
1292
- );
1293
-
1294
- // Track usage for chain (count completed steps)
1295
- if (chainResult.success && workspaceContext) {
1296
- const completedCount = chainResult.completedSteps.length;
1297
-
1298
- for (let i = 0; i < completedCount; i++) {
1299
- try {
1300
- await convex.mutation("workspaces:incrementUsage" as any, {
1301
- workspaceId: workspaceContext.workspaceId as any,
1302
- });
1303
- } catch (e) {
1304
- console.error('[APIClaw] Failed to track chain usage:', e);
1305
- }
1306
- }
1307
- }
1308
-
1309
- // Format response to match expected chain response format
1310
- return {
1311
- content: [{
1312
- type: 'text',
1313
- text: JSON.stringify({
1314
- status: chainResult.success ? 'success' : 'error',
1315
- mode: 'chain',
1316
- chainId: chainResult.chainId,
1317
- steps: chainResult.trace.map(t => ({
1318
- id: t.stepId,
1319
- status: t.success ? 'completed' : 'failed',
1320
- result: t.output,
1321
- error: t.error,
1322
- latencyMs: t.latencyMs,
1323
- cost: t.cost,
1324
- })),
1325
- finalResult: chainResult.finalResult,
1326
- totalLatencyMs: chainResult.totalLatencyMs,
1327
- totalCost: chainResult.totalCost,
1328
- tokensSaved: (chain.length - 1) * 500, // Estimate tokens saved
1329
- ...(chainResult.error ? {
1330
- completedSteps: chainResult.completedSteps,
1331
- failedStep: chainResult.failedStep ? {
1332
- id: chainResult.failedStep.stepId,
1333
- error: chainResult.failedStep.error,
1334
- code: chainResult.failedStep.errorCode,
1335
- } : undefined,
1336
- partialResults: chainResult.results,
1337
- canResume: chainResult.canResume,
1338
- resumeToken: chainResult.resumeToken,
1339
- } : {}),
1340
- }, null, 2)
1341
- }],
1342
- isError: !chainResult.success
1343
- };
1344
- } catch (error) {
1345
- return {
1346
- content: [{
1347
- type: 'text',
1348
- text: JSON.stringify({
1349
- status: 'error',
1350
- mode: 'chain',
1351
- error: error instanceof Error ? error.message : String(error),
1352
- }, null, 2)
1353
- }],
1354
- isError: true
1355
- };
1356
- }
1357
- }
1358
-
1359
- // ============================================
1360
- // SINGLE CALL MODE (existing logic)
1361
- // ============================================
1362
-
1363
- // Handle dry-run mode - no actual API calls, just show what would happen
1364
- if (dryRun) {
1365
- const { generateDryRun } = await import('./execute.js');
1366
- const dryRunResult = generateDryRun(provider, action, params);
1367
-
1368
- return {
1369
- content: [{
1370
- type: 'text',
1371
- text: JSON.stringify(dryRunResult, null, 2)
1372
- }]
1373
- };
1374
- }
1375
-
1376
- // Check workspace access (skip for free/open APIs)
1377
- const isFreeAPI = isOpenAPI(provider);
1378
- if (!isFreeAPI) {
1379
- const access = checkWorkspaceAccess(provider);
1380
- if (!access.allowed) {
1381
- return {
1382
- content: [{
1383
- type: 'text',
1384
- text: JSON.stringify({
1385
- status: 'error',
1386
- error: access.error,
1387
- hint: access.isAnonymous
1388
- ? 'Rate limit reached. Use register_owner to authenticate for higher limits.'
1389
- : 'Use register_owner to authenticate your workspace.',
1390
- }, null, 2)
1391
- }],
1392
- isError: true
1393
- };
1394
- }
1395
- }
1396
-
1397
- const startTime = Date.now();
1398
- let result: { success: boolean; provider: string; action: string; data?: any; error?: string; cost?: number };
1399
- let apiType: 'direct' | 'open';
1400
-
1401
- // Check if this is a confirmation of a pending action
1402
- if (confirmToken) {
1403
- const pending = consumePendingAction(confirmToken);
1404
-
1405
- if (!pending) {
1406
- return {
1407
- content: [{
1408
- type: 'text',
1409
- text: JSON.stringify({
1410
- status: 'error',
1411
- error: 'Invalid or expired confirmation token. Please start over.',
1412
- }, null, 2)
1413
- }],
1414
- isError: true
1415
- };
1416
- }
1417
-
1418
- // Execute the confirmed action
1419
- apiType = 'direct';
1420
-
1421
- if (isGatewayEnabled()) {
1422
- // Route through Intelligent Gateway
1423
- const gatewayResult = await getGateway().execute(
1424
- pending.provider,
1425
- pending.action,
1426
- pending.params,
1427
- { workspaceId: workspaceContext?.workspaceId },
1428
- );
1429
- result = {
1430
- success: gatewayResult.success,
1431
- provider: gatewayResult.provider,
1432
- action: gatewayResult.action,
1433
- data: gatewayResult.data,
1434
- error: gatewayResult.error,
1435
- cost: gatewayResult.cost,
1436
- };
1437
- } else {
1438
- // Legacy: direct execution with metered billing
1439
- const customerKey = (args?.customer_key as string) || getCustomerKey(pending.provider);
1440
- const stripeCustomerId = (args?.stripe_customer_id as string) || process.env.APICLAW_STRIPE_CUSTOMER_ID;
1441
- result = await executeMetered(pending.provider, pending.action, pending.params, {
1442
- customerId: stripeCustomerId,
1443
- customerKey,
1444
- userId: DEFAULT_AGENT_ID,
1445
- });
1446
-
1447
- // Legacy logging (gateway handles this when enabled)
1448
- const analyticsUserId = workspaceContext
1449
- ? workspaceContext.workspaceId
1450
- : `anon:${getMachineFingerprint()}`;
1451
- logAPICall({
1452
- timestamp: new Date().toISOString(),
1453
- provider: pending.provider,
1454
- action: pending.action,
1455
- type: apiType,
1456
- userId: analyticsUserId,
1457
- success: result.success,
1458
- latencyMs: Date.now() - startTime,
1459
- error: result.error,
1460
- });
1461
-
1462
- // Track earn progress (legacy path)
1463
- if (result.success && workspaceContext) {
1464
- await trackEarnProgress(workspaceContext.workspaceId, pending.provider, pending.action);
1465
- }
1466
- }
1467
-
1468
- return {
1469
- content: [{
1470
- type: 'text',
1471
- text: JSON.stringify({
1472
- status: result.success ? 'success' : 'error',
1473
- provider: result.provider,
1474
- action: result.action,
1475
- confirmed: true,
1476
- ...(result.success ? { data: result.data } : { error: result.error }),
1477
- }, null, 2)
1478
- }],
1479
- isError: !result.success
1480
- };
1481
- }
1482
-
1483
- // Check if this action requires confirmation (both hardcoded and dynamic providers)
1484
- const confirmCheck = await requiresConfirmationAsync(provider, action);
1485
-
1486
- if (confirmCheck.required) {
1487
- // Validate params first (for hardcoded providers)
1488
- if (!confirmCheck.isDynamic) {
1489
- const validation = validateParams(provider, action, params);
1490
-
1491
- if (!validation.valid) {
1492
- return {
1493
- content: [{
1494
- type: 'text',
1495
- text: JSON.stringify({
1496
- status: 'error',
1497
- error: 'Validation failed',
1498
- missing_or_invalid: validation.errors,
1499
- hint: 'Please provide all required fields before sending.',
1500
- }, null, 2)
1501
- }],
1502
- isError: true
1503
- };
1504
- }
1505
- }
1506
-
1507
- // Generate preview and create pending action
1508
- const preview = generatePreview(provider, action, params);
1509
- if (confirmCheck.estimatedCost) {
1510
- preview.estimated_cost = confirmCheck.estimatedCost;
1511
- }
1512
- const pending = createPendingAction(provider, action, params, preview, DEFAULT_AGENT_ID);
1513
-
1514
- return {
1515
- content: [{
1516
- type: 'text',
1517
- text: JSON.stringify({
1518
- status: 'requires_confirmation',
1519
- message: '⚠️ This action costs money. Please review and confirm.',
1520
- preview,
1521
- confirm_token: pending.token,
1522
- expires_in_seconds: 300,
1523
- how_to_confirm: `Call again with confirm_token: "${pending.token}"`,
1524
- }, null, 2)
1525
- }]
1526
- };
1527
- }
1528
-
1529
- // Regular execution (no confirmation needed)
1530
- apiType = isOpenAPI(provider) ? 'open' : 'direct';
1531
-
1532
- if (isGatewayEnabled()) {
1533
- // Route through Intelligent Gateway (handles billing, logging, analytics)
1534
- const gatewayParams = {
1535
- ...params,
1536
- ...(apiType === 'open' ? { baseUrl: getOpenAPIBaseUrl(provider, action, params) } : {}),
1537
- };
1538
- const gatewayResult = await getGateway().execute(
1539
- provider,
1540
- action,
1541
- gatewayParams,
1542
- { workspaceId: workspaceContext?.workspaceId },
1543
- );
1544
- result = {
1545
- success: gatewayResult.success,
1546
- provider: gatewayResult.provider,
1547
- action: gatewayResult.action,
1548
- data: gatewayResult.data,
1549
- error: gatewayResult.error,
1550
- cost: gatewayResult.cost,
1551
- };
1552
- } else {
1553
- // Legacy: direct local execution
1554
- if (apiType === 'open') {
1555
- result = await executeOpenAPI(provider, action, params);
1556
- } else {
1557
- const customerKey = (args?.customer_key as string) || getCustomerKey(provider);
1558
- const stripeCustomerId = (args?.stripe_customer_id as string) || process.env.APICLAW_STRIPE_CUSTOMER_ID;
1559
- result = await executeMetered(provider, action, params, {
1560
- customerId: stripeCustomerId,
1561
- customerKey,
1562
- userId: DEFAULT_AGENT_ID,
1563
- });
1564
- }
1565
-
1566
- // Legacy logging (gateway handles all of this when enabled)
1567
- const analyticsUserId = workspaceContext
1568
- ? workspaceContext.workspaceId
1569
- : `anon:${getMachineFingerprint()}`;
1570
-
1571
- logAPICall({
1572
- timestamp: new Date().toISOString(),
1573
- provider,
1574
- action,
1575
- type: apiType,
1576
- userId: analyticsUserId,
1577
- success: result.success,
1578
- latencyMs: Date.now() - startTime,
1579
- error: result.error,
1580
- });
1581
-
1582
- if (workspaceContext) {
1583
- convex.mutation("logs:createLogInternal" as any, {
1584
- workspaceId: workspaceContext.workspaceId as any,
1585
- sessionToken: workspaceContext.sessionToken || "",
1586
- provider,
1587
- action,
1588
- status: result.success ? "success" : "error",
1589
- latencyMs: Date.now() - startTime,
1590
- errorMessage: result.success ? undefined : (result.error || "Unknown error"),
1591
- }).catch(() => {}); // fire-and-forget
1592
-
1593
- convex.mutation("logs:logProviderCall" as any, {
1594
- provider,
1595
- action,
1596
- status: result.success ? "success" : "error",
1597
- latencyMs: Date.now() - startTime,
1598
- callerWorkspaceId: workspaceContext.workspaceId,
1599
- errorMessage: result.success ? undefined : (result.error || "Unknown error"),
1600
- }).catch(() => {}); // fire-and-forget
1601
- }
1602
-
1603
- // Increment usage for workspace (non-free APIs only, legacy path)
1604
- if (result.success && workspaceContext && !isFreeAPI) {
1605
- try {
1606
- const usageResult = await convex.mutation("workspaces:incrementUsage" as any, {
1607
- workspaceId: workspaceContext.workspaceId as any,
1608
- }) as { success: boolean; remaining?: number };
1609
- if (usageResult.success) {
1610
- workspaceContext.usageRemaining = usageResult.remaining ?? -1;
1611
- workspaceContext.usageCount = (workspaceContext.usageCount || 0) + 1;
1612
- }
1613
-
1614
- if (currentAgentId) {
1615
- convex.mutation("agents:incrementAgentCalls" as any, { agentId: currentAgentId as any }).catch(() => {});
1616
- }
1617
-
1618
- await trackEarnProgress(workspaceContext.workspaceId, provider, action);
1619
- } catch (e) {
1620
- console.error('[APIClaw] Failed to track usage:', e);
1621
- }
1622
- }
1623
- }
1624
-
1625
- // When gateway is enabled, still update local workspace context for nudge logic
1626
- if (isGatewayEnabled() && result.success && workspaceContext && !isFreeAPI) {
1627
- workspaceContext.usageCount = (workspaceContext.usageCount || 0) + 1;
1628
- }
1629
-
1630
- // Build response with signup nudge for unregistered users
1631
- const responseData: Record<string, unknown> = {
1632
- status: result.success ? 'success' : 'error',
1633
- provider: result.provider,
1634
- action: result.action,
1635
- type: apiType,
1636
- ...(result.success ? { data: result.data } : { error: result.error }),
1637
- ...(result.cost !== undefined ? { cost_sek: result.cost } : {})
1638
- };
1639
-
1640
- // Nudge unregistered users
1641
- if (result.success && workspaceContext && !workspaceContext.email) {
1642
- const remaining = UNREGISTERED_CALL_LIMIT - (workspaceContext.usageCount || 0);
1643
- if (remaining > 0 && remaining <= 3) {
1644
- responseData._notice = `${remaining} free calls remaining. Run register_owner to unlock 50/month.`;
1645
- }
1646
- }
1647
-
1648
- return {
1649
- content: [
1650
- {
1651
- type: 'text',
1652
- text: JSON.stringify(responseData, null, 2)
1653
- }
1654
- ],
1655
- isError: !result.success
1656
- };
1657
- }
1658
-
1659
- case 'list_connected': {
1660
- const directProviders = getConnectedProviders();
1661
- const openProviders = listOpenAPIs();
1662
-
1663
- return {
1664
- content: [
1665
- {
1666
- type: 'text',
1667
- text: JSON.stringify({
1668
- status: 'success',
1669
- message: 'These APIs are available via call_api - no API key needed!',
1670
- direct_call: {
1671
- description: 'APIs where we handle authentication',
1672
- providers: directProviders,
1673
- },
1674
- open_apis: {
1675
- description: 'Free, open APIs (no auth required)',
1676
- providers: openProviders,
1677
- },
1678
- usage: 'Use call_api with provider, action, and params to execute calls.'
1679
- }, null, 2)
1680
- }
1681
- ]
1682
- };
1683
- }
1684
-
1685
- case 'capability': {
1686
- // Registration gate
1687
- if (!workspaceContext) {
1688
- return {
1689
- content: [{
1690
- type: 'text',
1691
- text: JSON.stringify({
1692
- status: 'registration_required',
1693
- error: 'You need to register before making API calls.',
1694
- message: 'Ask the user for their email, then call register_owner({ email: "..." }). A 6-digit code will be sent. Then call verify_code with the code.',
1695
- action: 'register_owner',
1696
- }, null, 2)
1697
- }],
1698
- isError: true
1699
- };
1700
- }
1701
-
1702
- const capabilityId = args?.capability as string;
1703
- const action = args?.action as string;
1704
- const params = (args?.params as Record<string, any>) || {};
1705
- const preferences = (args?.preferences as Record<string, any>) || {};
1706
- const subagentId = args?.subagent_id as string | undefined;
1707
- const aiBackend = args?.ai_backend as string | undefined;
1708
-
1709
- // Track AI backend if provided
1710
- if (aiBackend && workspaceContext?.sessionToken) {
1711
- fetch('https://adventurous-avocet-799.convex.cloud/api/mutation', {
1712
- method: 'POST',
1713
- headers: { 'Content-Type': 'application/json' },
1714
- body: JSON.stringify({
1715
- path: 'agents:updateAIBackend',
1716
- args: {
1717
- token: workspaceContext.sessionToken,
1718
- subagentId: subagentId || undefined,
1719
- aiBackend,
1720
- },
1721
- }),
1722
- }).catch(() => {}); // Fire and forget
1723
- }
1724
-
1725
- // Check if capability exists
1726
- const exists = await hasCapability(capabilityId);
1727
- if (!exists) {
1728
- // Try to help with available capabilities
1729
- const available = await listCapabilities();
1730
- return {
1731
- content: [{
1732
- type: 'text',
1733
- text: JSON.stringify({
1734
- status: 'error',
1735
- error: `Unknown capability: ${capabilityId}`,
1736
- available_capabilities: available.map(c => c.id),
1737
- hint: 'Use list_capabilities to see all available capabilities.'
1738
- }, null, 2)
1739
- }],
1740
- isError: true
1741
- };
1742
- }
1743
-
1744
- // Execute capability
1745
- const result = await executeCapability(
1746
- capabilityId,
1747
- action,
1748
- params,
1749
- DEFAULT_AGENT_ID,
1750
- preferences
1751
- );
1752
-
1753
- return {
1754
- content: [{
1755
- type: 'text',
1756
- text: JSON.stringify({
1757
- status: result.success ? 'success' : 'error',
1758
- capability: result.capability,
1759
- action: result.action,
1760
- provider_used: result.providerUsed,
1761
- fallback_attempted: result.fallbackAttempted,
1762
- ...(result.fallbackReason ? { fallback_reason: result.fallbackReason } : {}),
1763
- ...(result.success ? { data: result.data } : { error: result.error }),
1764
- ...(result.cost !== undefined ? { cost: result.cost, currency: result.currency } : {}),
1765
- latency_ms: result.latencyMs,
1766
- }, null, 2)
1767
- }],
1768
- isError: !result.success
1769
- };
1770
- }
1771
-
1772
- case 'list_capabilities': {
1773
- const capabilities = await listCapabilities();
1774
-
1775
- return {
1776
- content: [{
1777
- type: 'text',
1778
- text: JSON.stringify({
1779
- status: 'success',
1780
- message: 'Available capabilities - use capability() to execute',
1781
- capabilities,
1782
- usage: 'capability("sms", "send", {to: "+46...", message: "Hello"})'
1783
- }, null, 2)
1784
- }]
1785
- };
1786
- }
1787
-
1788
- // ============================================
1789
- // WORKSPACE TOOLS
1790
- // ============================================
1791
-
1792
- case 'register_owner': {
1793
- const email = args?.email as string;
1794
-
1795
- if (!email || !email.includes('@')) {
1796
- return {
1797
- content: [{
1798
- type: 'text',
1799
- text: JSON.stringify({
1800
- status: 'error',
1801
- error: 'Invalid email address',
1802
- }, null, 2)
1803
- }],
1804
- isError: true
1805
- };
1806
- }
1807
-
1808
- try {
1809
- // Check if workspace already exists and is active -- auto-login
1810
- const existing = await convex.query("workspaces:getByEmail" as any, { email }) as { id: string; status: string; tier: string; usageCount: number; usageLimit: number } | null;
1811
-
1812
- if (existing && existing.status === 'active') {
1813
- const fingerprint = getMachineFingerprint();
1814
- const sessionResult = await convex.mutation("workspaces:createAgentSession" as any, {
1815
- workspaceId: existing.id,
1816
- fingerprint,
1817
- }) as { success: boolean; sessionToken?: string };
1818
-
1819
- if (sessionResult.success) {
1820
- writeSession(sessionResult.sessionToken!, existing.id, email);
1821
-
1822
- try {
1823
- const claimResult = await convex.mutation("workspaces:claimAnonymousUsage" as any, {
1824
- workspaceId: existing.id,
1825
- machineFingerprint: fingerprint,
1826
- }) as { success: boolean; claimedCount?: number };
1827
- if (claimResult.success && claimResult.claimedCount) {
1828
- console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1829
- }
1830
- } catch (_) {}
1831
-
1832
- workspaceContext = {
1833
- sessionToken: sessionResult.sessionToken!,
1834
- workspaceId: existing.id,
1835
- email,
1836
- tier: existing.tier,
1837
- usageRemaining: existing.usageLimit - existing.usageCount,
1838
- usageCount: existing.usageCount,
1839
- status: existing.status,
1840
- };
1841
-
1842
- return {
1843
- content: [{
1844
- type: 'text',
1845
- text: JSON.stringify({
1846
- status: 'success',
1847
- message: `Welcome back! Authenticated as ${email}`,
1848
- workspace: {
1849
- email,
1850
- tier: existing.tier,
1851
- usageCount: existing.usageCount,
1852
- usageLimit: existing.usageLimit,
1853
- },
1854
- }, null, 2)
1855
- }]
1856
- };
1857
- }
1858
- }
1859
-
1860
- // New user or pending workspace -- send OTP
1861
- const fingerprint = getMachineFingerprint();
1862
- const otpResult = await convex.mutation("workspaces:createOTP" as any, {
1863
- email,
1864
- fingerprint,
1865
- }) as { code: string; expiresAt: number };
1866
-
1867
- // Send OTP email
1868
- const emailResponse = await fetch('https://api.resend.com/emails', {
1869
- method: 'POST',
1870
- headers: {
1871
- 'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
1872
- 'Content-Type': 'application/json',
1873
- },
1874
- body: JSON.stringify({
1875
- from: 'APIClaw <noreply@apiclaw.cloud>',
1876
- to: email,
1877
- subject: `Your APIClaw verification code: ${otpResult.code}`,
1878
- html: `
1879
- <div style="font-family: Inter, sans-serif; max-width: 480px; margin: 0 auto; padding: 40px 24px;">
1880
- <div style="text-align: center; margin-bottom: 32px;">
1881
- <span style="font-size: 48px;">🦞</span>
1882
- </div>
1883
- <h1 style="font-size: 24px; font-weight: 700; color: #0A0A0A; text-align: center; margin-bottom: 8px;">Your verification code</h1>
1884
- <p style="font-size: 16px; color: #525252; text-align: center; margin-bottom: 32px;">Paste this code in your terminal to activate APIClaw.</p>
1885
- <div style="background: #F5F5F5; border: 1px solid #E5E5E5; border-radius: 12px; padding: 24px; text-align: center; margin-bottom: 24px;">
1886
- <code style="font-size: 36px; font-weight: 700; letter-spacing: 0.3em; color: #EF4444; font-family: 'JetBrains Mono', monospace;">${otpResult.code}</code>
1887
- </div>
1888
- <p style="font-size: 13px; color: #737373; text-align: center;">This code expires in 10 minutes. If you didn't request this, ignore this email.</p>
1889
- <hr style="border: none; border-top: 1px solid #E5E5E5; margin: 32px 0 16px;" />
1890
- <p style="font-size: 12px; color: #A3A3A3; text-align: center;">APIClaw -- The API Layer For AI Agents</p>
1891
- </div>
1892
- `
1893
- })
1894
- });
1895
-
1896
- if (!emailResponse.ok) {
1897
- const errorData = await emailResponse.text();
1898
- throw new Error(`Failed to send verification email: ${errorData}`);
1899
- }
1900
-
1901
- // Store pending email for verify_code
1902
- pendingRegistrationEmail = email;
1903
-
1904
- return {
1905
- content: [{
1906
- type: 'text',
1907
- text: JSON.stringify({
1908
- status: 'code_sent',
1909
- message: `Verification code sent to ${email}`,
1910
- next_step: 'Ask the user to check their email for a 6-digit code, then call verify_code with the email and code.',
1911
- email,
1912
- expires_in_minutes: 10,
1913
- }, null, 2)
1914
- }]
1915
- };
1916
- } catch (error) {
1917
- return {
1918
- content: [{
1919
- type: 'text',
1920
- text: JSON.stringify({
1921
- status: 'error',
1922
- error: error instanceof Error ? error.message : 'Registration failed',
1923
- }, null, 2)
1924
- }],
1925
- isError: true
1926
- };
1927
- }
1928
- }
1929
-
1930
- case 'verify_code': {
1931
- const email = (args?.email as string) || pendingRegistrationEmail;
1932
- const code = args?.code as string;
1933
-
1934
- if (!email || !code) {
1935
- return {
1936
- content: [{
1937
- type: 'text',
1938
- text: JSON.stringify({
1939
- status: 'error',
1940
- error: 'Both email and code are required.',
1941
- hint: 'Call register_owner first to receive a verification code.',
1942
- }, null, 2)
1943
- }],
1944
- isError: true
1945
- };
1946
- }
1947
-
1948
- try {
1949
- const fingerprint = getMachineFingerprint();
1950
- const result = await convex.mutation("workspaces:verifyOTP" as any, {
1951
- email,
1952
- code: code.trim(),
1953
- fingerprint,
1954
- }) as {
1955
- success: boolean;
1956
- error?: string;
1957
- message?: string;
1958
- isNewUser?: boolean;
1959
- sessionToken?: string;
1960
- workspace?: { id: string; email: string; tier: string; status: string; usageCount: number; usageLimit: number }
1961
- };
1962
-
1963
- if (!result.success) {
1964
- // Increment attempt counter
1965
- try {
1966
- await convex.mutation("workspaces:incrementOTPAttempt" as any, { email, code: code.trim() });
1967
- } catch (_) {}
1968
-
1969
- return {
1970
- content: [{
1971
- type: 'text',
1972
- text: JSON.stringify({
1973
- status: 'error',
1974
- error: result.message || 'Verification failed',
1975
- hint: result.error === 'code_expired'
1976
- ? 'Run register_owner again to get a new code.'
1977
- : 'Check the code and try again.',
1978
- }, null, 2)
1979
- }],
1980
- isError: true
1981
- };
1982
- }
1983
-
1984
- // Success! Save session
1985
- writeSession(result.sessionToken!, result.workspace!.id, result.workspace!.email);
1986
-
1987
- // Claim anonymous usage
1988
- try {
1989
- const claimResult = await convex.mutation("workspaces:claimAnonymousUsage" as any, {
1990
- workspaceId: result.workspace!.id,
1991
- machineFingerprint: fingerprint,
1992
- }) as { success: boolean; claimedCount?: number };
1993
- if (claimResult.success && claimResult.claimedCount) {
1994
- console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1995
- }
1996
- } catch (_) {}
1997
-
1998
- // Update global context
1999
- workspaceContext = {
2000
- sessionToken: result.sessionToken!,
2001
- workspaceId: result.workspace!.id,
2002
- email: result.workspace!.email,
2003
- tier: result.workspace!.tier,
2004
- usageRemaining: result.workspace!.usageLimit - result.workspace!.usageCount,
2005
- usageCount: result.workspace!.usageCount,
2006
- status: result.workspace!.status,
2007
- };
2008
-
2009
- pendingRegistrationEmail = null;
2010
-
2011
- return {
2012
- content: [{
2013
- type: 'text',
2014
- text: JSON.stringify({
2015
- status: 'success',
2016
- message: result.isNewUser
2017
- ? `Welcome to APIClaw! Workspace activated for ${result.workspace!.email}`
2018
- : `Welcome back! Authenticated as ${result.workspace!.email}`,
2019
- workspace: {
2020
- email: result.workspace!.email,
2021
- tier: result.workspace!.tier,
2022
- usageCount: result.workspace!.usageCount,
2023
- usageLimit: result.workspace!.usageLimit,
2024
- },
2025
- ready: 'You can now use discover_apis and call_api.',
2026
- }, null, 2)
2027
- }]
2028
- };
2029
- } catch (error) {
2030
- return {
2031
- content: [{
2032
- type: 'text',
2033
- text: JSON.stringify({
2034
- status: 'error',
2035
- error: error instanceof Error ? error.message : 'Verification failed',
2036
- }, null, 2)
2037
- }],
2038
- isError: true
2039
- };
2040
- }
2041
- }
2042
-
2043
- case 'check_workspace_status': {
2044
- // Check if we have a local session
2045
- const session = readSession();
2046
-
2047
- if (!session) {
2048
- return {
2049
- content: [{
2050
- type: 'text',
2051
- text: JSON.stringify({
2052
- status: 'not_authenticated',
2053
- message: 'No active session. Use register_owner to authenticate.',
2054
- }, null, 2)
2055
- }]
2056
- };
2057
- }
2058
-
2059
- try {
2060
- const result = await convex.query("workspaces:getWorkspaceStatus" as any, {
2061
- sessionToken: session.sessionToken,
2062
- }) as { authenticated: boolean; email?: string; status?: string; tier?: string; usageCount?: number; usageLimit?: number; usageRemaining?: number; hasStripe?: boolean; createdAt?: number };
2063
-
2064
- if (!result.authenticated) {
2065
- clearSession();
2066
- workspaceContext = null;
2067
-
2068
- return {
2069
- content: [{
2070
- type: 'text',
2071
- text: JSON.stringify({
2072
- status: 'session_expired',
2073
- message: 'Session expired. Use register_owner to re-authenticate.',
2074
- }, null, 2)
2075
- }]
2076
- };
2077
- }
2078
-
2079
- // Update global context
2080
- workspaceContext = {
2081
- sessionToken: session.sessionToken,
2082
- workspaceId: session.workspaceId,
2083
- email: result.email ?? '',
2084
- tier: result.tier ?? 'free',
2085
- usageRemaining: result.usageRemaining ?? 0,
2086
- usageCount: result.usageCount ?? 0,
2087
- status: result.status ?? 'unknown',
2088
- };
2089
-
2090
- return {
2091
- content: [{
2092
- type: 'text',
2093
- text: JSON.stringify({
2094
- status: 'success',
2095
- workspace: {
2096
- email: result.email,
2097
- status: result.status,
2098
- tier: result.tier,
2099
- usage: {
2100
- count: result.usageCount,
2101
- limit: result.usageLimit === -1 ? 'unlimited' : result.usageLimit,
2102
- remaining: result.usageRemaining === -1 ? 'unlimited' : result.usageRemaining,
2103
- },
2104
- hasStripe: result.hasStripe,
2105
- createdAt: result.createdAt ? new Date(result.createdAt).toISOString() : undefined,
2106
- },
2107
- }, null, 2)
2108
- }]
2109
- };
2110
- } catch (error) {
2111
- return {
2112
- content: [{
2113
- type: 'text',
2114
- text: JSON.stringify({
2115
- status: 'error',
2116
- error: error instanceof Error ? error.message : 'Failed to check status',
2117
- }, null, 2)
2118
- }],
2119
- isError: true
2120
- };
2121
- }
2122
- }
2123
-
2124
- case 'remind_owner': {
2125
- const session = readSession();
2126
-
2127
- if (!session) {
2128
- return {
2129
- content: [{
2130
- type: 'text',
2131
- text: JSON.stringify({
2132
- status: 'error',
2133
- error: 'No workspace found. Use register_owner first.',
2134
- }, null, 2)
2135
- }],
2136
- isError: true
2137
- };
2138
- }
2139
-
2140
- try {
2141
- // Check current status
2142
- const result = await convex.query("workspaces:getWorkspaceStatus" as any, {
2143
- sessionToken: session.sessionToken,
2144
- }) as { authenticated: boolean; email?: string; status?: string };
2145
-
2146
- if (result.authenticated && result.status === 'active') {
2147
- return {
2148
- content: [{
2149
- type: 'text',
2150
- text: JSON.stringify({
2151
- status: 'already_verified',
2152
- message: 'Workspace is already verified and active!',
2153
- email: result.email,
2154
- }, null, 2)
2155
- }]
2156
- };
2157
- }
2158
-
2159
- // Create new magic link
2160
- const fingerprint = getMachineFingerprint();
2161
- const magicLinkResult = await convex.mutation("workspaces:createMagicLink" as any, {
2162
- email: session.email,
2163
- fingerprint,
2164
- }) as { token: string; expiresAt: number };
2165
-
2166
- // TODO: Agent 2 will implement actual email sending
2167
- const verifyUrl = `https://apiclaw.cloud/auth/verify?token=${magicLinkResult.token}`;
2168
-
2169
- return {
2170
- content: [{
2171
- type: 'text',
2172
- text: JSON.stringify({
2173
- status: 'reminder_sent',
2174
- message: 'New verification link created.',
2175
- email: session.email,
2176
- verification_url: verifyUrl,
2177
- expires_in_minutes: 15,
2178
- note: 'Email sending will be implemented by Agent 2',
2179
- }, null, 2)
2180
- }]
2181
- };
2182
- } catch (error) {
2183
- return {
2184
- content: [{
2185
- type: 'text',
2186
- text: JSON.stringify({
2187
- status: 'error',
2188
- error: error instanceof Error ? error.message : 'Failed to send reminder',
2189
- }, null, 2)
2190
- }],
2191
- isError: true
2192
- };
2193
- }
2194
- }
2195
-
2196
- // Metered Billing Tools
2197
- case 'setup_metered_billing': {
2198
- const { email, success_url, cancel_url } = args as {
2199
- email: string;
2200
- success_url?: string;
2201
- cancel_url?: string;
2202
- };
2203
-
2204
- if (!email) {
2205
- return {
2206
- content: [{
2207
- type: 'text',
2208
- text: JSON.stringify({ status: 'error', error: 'Email is required' }, null, 2)
2209
- }],
2210
- isError: true
2211
- };
2212
- }
2213
-
2214
- // Create or get customer
2215
- const customerResult = await getOrCreateCustomer(email, email);
2216
- if ('error' in customerResult) {
2217
- return {
2218
- content: [{
2219
- type: 'text',
2220
- text: JSON.stringify({ status: 'error', error: customerResult.error }, null, 2)
2221
- }],
2222
- isError: true
2223
- };
2224
- }
2225
-
2226
- // Create checkout session for metered subscription
2227
- const checkoutResult = await createMeteredCheckoutSession(
2228
- email,
2229
- success_url || 'https://apiclaw.cloud/billing/success',
2230
- cancel_url || 'https://apiclaw.cloud/billing/cancel'
2231
- );
2232
-
2233
- if ('error' in checkoutResult) {
2234
- return {
2235
- content: [{
2236
- type: 'text',
2237
- text: JSON.stringify({ status: 'error', error: checkoutResult.error }, null, 2)
2238
- }],
2239
- isError: true
2240
- };
2241
- }
2242
-
2243
- return {
2244
- content: [{
2245
- type: 'text',
2246
- text: JSON.stringify({
2247
- status: 'checkout_ready',
2248
- message: 'Complete checkout to activate pay-per-call billing',
2249
- checkout_url: checkoutResult.url,
2250
- session_id: checkoutResult.sessionId,
2251
- customer_id: customerResult.customerId,
2252
- pricing: {
2253
- per_call: '$0.002',
2254
- billing_period: 'monthly',
2255
- billed_at: 'end of period based on usage'
2256
- }
2257
- }, null, 2)
2258
- }]
2259
- };
2260
- }
2261
-
2262
- case 'get_usage_summary': {
2263
- const { subscription_id } = args as { subscription_id: string };
2264
-
2265
- if (!subscription_id) {
2266
- return {
2267
- content: [{
2268
- type: 'text',
2269
- text: JSON.stringify({ status: 'error', error: 'subscription_id is required' }, null, 2)
2270
- }],
2271
- isError: true
2272
- };
2273
- }
2274
-
2275
- const usage = await getUsageSummary(subscription_id);
2276
- if ('error' in usage) {
2277
- return {
2278
- content: [{
2279
- type: 'text',
2280
- text: JSON.stringify({ status: 'error', error: usage.error }, null, 2)
2281
- }],
2282
- isError: true
2283
- };
2284
- }
2285
-
2286
- return {
2287
- content: [{
2288
- type: 'text',
2289
- text: JSON.stringify({
2290
- status: 'success',
2291
- billing_period: {
2292
- start: new Date(usage.period.start * 1000).toISOString(),
2293
- end: new Date(usage.period.end * 1000).toISOString()
2294
- },
2295
- usage: {
2296
- total_calls: usage.totalCalls,
2297
- price_per_call: METERED_BILLING.pricePerCall,
2298
- estimated_cost: `$${usage.totalCost.toFixed(4)}`
2299
- }
2300
- }, null, 2)
2301
- }]
2302
- };
2303
- }
2304
-
2305
- case 'estimate_cost': {
2306
- const { call_count } = args as { call_count: number };
2307
-
2308
- if (!call_count || call_count < 0) {
2309
- return {
2310
- content: [{
2311
- type: 'text',
2312
- text: JSON.stringify({ status: 'error', error: 'Valid call_count is required' }, null, 2)
2313
- }],
2314
- isError: true
2315
- };
2316
- }
2317
-
2318
- const estimate = estimateCost(call_count);
2319
-
2320
- return {
2321
- content: [{
2322
- type: 'text',
2323
- text: JSON.stringify({
2324
- status: 'success',
2325
- estimate: {
2326
- calls: estimate.calls,
2327
- price_per_call: `$${estimate.pricePerCall}`,
2328
- total_cost: `$${estimate.totalCost.toFixed(4)}`,
2329
- currency: estimate.currency
2330
- },
2331
- examples: {
2332
- '100 calls': `$${(100 * METERED_BILLING.pricePerCall).toFixed(2)}`,
2333
- '1,000 calls': `$${(1000 * METERED_BILLING.pricePerCall).toFixed(2)}`,
2334
- '10,000 calls': `$${(10000 * METERED_BILLING.pricePerCall).toFixed(2)}`
2335
- }
2336
- }, null, 2)
2337
- }]
2338
- };
2339
- }
2340
-
2341
- // ============================================
2342
- // CHAIN MANAGEMENT TOOLS
2343
- // ============================================
2344
-
2345
- case 'get_chain_status': {
2346
- const chainId = args?.chain_id as string;
2347
-
2348
- if (!chainId) {
2349
- return {
2350
- content: [{
2351
- type: 'text',
2352
- text: JSON.stringify({
2353
- status: 'error',
2354
- error: 'chain_id is required'
2355
- }, null, 2)
2356
- }],
2357
- isError: true
2358
- };
2359
- }
2360
-
2361
- const chainStatus = await getChainStatus(chainId);
2362
-
2363
- if (chainStatus.status === 'not_found') {
2364
- return {
2365
- content: [{
2366
- type: 'text',
2367
- text: JSON.stringify({
2368
- status: 'error',
2369
- error: `Chain not found: ${chainId}`,
2370
- hint: 'Chain states expire after 1 hour. The chain may have completed or expired.'
2371
- }, null, 2)
2372
- }],
2373
- isError: true
2374
- };
2375
- }
2376
-
2377
- return {
2378
- content: [{
2379
- type: 'text',
2380
- text: JSON.stringify({
2381
- status: 'success',
2382
- chain: {
2383
- chainId: chainStatus.chainId,
2384
- executionStatus: chainStatus.status,
2385
- ...(chainStatus.result ? {
2386
- result: {
2387
- success: chainStatus.result.success,
2388
- completedSteps: chainStatus.result.completedSteps,
2389
- totalLatencyMs: chainStatus.result.totalLatencyMs,
2390
- totalCost: chainStatus.result.totalCost,
2391
- finalResult: chainStatus.result.finalResult,
2392
- error: chainStatus.result.error,
2393
- canResume: chainStatus.result.canResume,
2394
- resumeToken: chainStatus.result.resumeToken,
2395
- }
2396
- } : {})
2397
- }
2398
- }, null, 2)
2399
- }]
2400
- };
2401
- }
2402
-
2403
- case 'resume_chain': {
2404
- const resumeToken = args?.resume_token as string;
2405
- const overrides = args?.overrides as Record<string, Record<string, any>> | undefined;
2406
- const originalChain = args?.original_chain as ChainStepUnion[] | undefined;
2407
-
2408
- if (!resumeToken) {
2409
- return {
2410
- content: [{
2411
- type: 'text',
2412
- text: JSON.stringify({
2413
- status: 'error',
2414
- error: 'resume_token is required'
2415
- }, null, 2)
2416
- }],
2417
- isError: true
2418
- };
2419
- }
2420
-
2421
- // Check workspace access
2422
- const access = checkWorkspaceAccess();
2423
- if (!access.allowed) {
2424
- return {
2425
- content: [{
2426
- type: 'text',
2427
- text: JSON.stringify({
2428
- status: 'error',
2429
- error: access.error,
2430
- hint: 'Use register_owner to authenticate your workspace.',
2431
- }, null, 2)
2432
- }],
2433
- isError: true
2434
- };
2435
- }
2436
-
2437
- try {
2438
- // Note: The resume_chain function requires the original chain definition
2439
- // In practice, you'd store this or require the caller to provide it
2440
- if (!originalChain) {
2441
- return {
2442
- content: [{
2443
- type: 'text',
2444
- text: JSON.stringify({
2445
- status: 'error',
2446
- error: 'original_chain is required to resume. Please provide the original chain definition.',
2447
- hint: 'Pass original_chain: [...] with the same chain array used in the failed execution.'
2448
- }, null, 2)
2449
- }],
2450
- isError: true
2451
- };
2452
- }
2453
-
2454
- const chainDefinition: ChainDefinition = {
2455
- steps: originalChain,
2456
- };
2457
-
2458
- const chainCredentials: ChainCredentials = {
2459
- userId: DEFAULT_AGENT_ID,
2460
- customerKeys: {},
2461
- };
2462
-
2463
- const customerKey = args?.customer_key as string | undefined;
2464
- if (customerKey) {
2465
- chainCredentials.customerKeys = { default: customerKey };
2466
- }
2467
-
2468
- const result = await resumeChain(
2469
- resumeToken,
2470
- chainDefinition,
2471
- chainCredentials,
2472
- {}, // inputs
2473
- overrides,
2474
- { verbose: false }
2475
- );
2476
-
2477
- return {
2478
- content: [{
2479
- type: 'text',
2480
- text: JSON.stringify({
2481
- status: result.success ? 'success' : 'error',
2482
- mode: 'chain_resumed',
2483
- chainId: result.chainId,
2484
- steps: result.trace.map(t => ({
2485
- id: t.stepId,
2486
- status: t.success ? 'completed' : 'failed',
2487
- result: t.output,
2488
- error: t.error,
2489
- latencyMs: t.latencyMs,
2490
- })),
2491
- finalResult: result.finalResult,
2492
- totalLatencyMs: result.totalLatencyMs,
2493
- totalCost: result.totalCost,
2494
- ...(result.error ? {
2495
- error: result.error,
2496
- canResume: result.canResume,
2497
- resumeToken: result.resumeToken,
2498
- } : {}),
2499
- }, null, 2)
2500
- }],
2501
- isError: !result.success
2502
- };
2503
- } catch (error) {
2504
- return {
2505
- content: [{
2506
- type: 'text',
2507
- text: JSON.stringify({
2508
- status: 'error',
2509
- error: error instanceof Error ? error.message : String(error),
2510
- }, null, 2)
2511
- }],
2512
- isError: true
2513
- };
2514
- }
2515
- }
2516
-
2517
- default:
2518
- return {
2519
- content: [
2520
- {
2521
- type: 'text',
2522
- text: JSON.stringify({
2523
- status: 'error',
2524
- message: `Unknown tool: ${name}`
2525
- }, null, 2)
2526
- }
2527
- ],
2528
- isError: true
2529
- };
2530
- }
2531
- } catch (error) {
2532
- return {
2533
- content: [
2534
- {
2535
- type: 'text',
2536
- text: JSON.stringify({
2537
- status: 'error',
2538
- message: error instanceof Error ? error.message : 'Unknown error'
2539
- }, null, 2)
2540
- }
2541
- ],
2542
- isError: true
2543
- };
2544
- }
2545
- });
2546
-
2547
- // Start server
2548
- async function main() {
2549
- // Check for CLI mode
2550
- if (process.argv.includes('--cli') || process.argv.includes('-c')) {
2551
- const { startCLI } = await import('./cli.js');
2552
- await startCLI();
2553
- return;
2554
- }
2555
-
2556
- const transport = new StdioServerTransport();
2557
- await server.connect(transport);
2558
- trackStartup();
2559
-
2560
- // Validate session on startup
2561
- const hasValidSession = await validateSession();
2562
-
2563
- // Register/update agent identity (fire-and-forget)
2564
- try {
2565
- const fingerprint = getMachineFingerprint();
2566
- const mcpClient = detectMCPClient();
2567
- const existingSession = readSession();
2568
- const result = await convex.mutation("agents:ensureAgent" as any, {
2569
- fingerprint,
2570
- mcpClient,
2571
- platform: process.platform,
2572
- ...(existingSession?.sessionToken ? { sessionToken: existingSession.sessionToken } : {}),
2573
- });
2574
- if (result?.agentId) {
2575
- currentAgentId = result.agentId;
2576
- }
2577
- // If we got a new session token and don't have one, write it
2578
- if (result?.isNew && result?.sessionToken && !hasValidSession) {
2579
- writeSession(result.sessionToken, result.workspaceId, "");
2580
- }
2581
- } catch (e) {
2582
- console.error('[APIClaw] Agent registration failed (non-blocking):', e);
2583
- }
2584
-
2585
- // Welcome message with onboarding
2586
- console.error(`
2587
- 🦞 APIClaw v1.1.5 — The API Layer for AI Agents
2588
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2589
-
2590
- ✓ 19,000+ APIs indexed
2591
- ✓ 23 categories
2592
- ✓ 9 direct-call providers ready
2593
- ${hasValidSession ? `✓ Authenticated as ${workspaceContext?.email}` : '⚠ Not authenticated - use register_owner'}
2594
-
2595
- Quick Start:
2596
- ${!hasValidSession ? 'register_owner({ email: "you@example.com" }) # First, authenticate\n ' : ''}discover_apis("send SMS to Sweden")
2597
- discover_apis("search the web")
2598
- call_api({ provider: "brave_search", ... })
2599
-
2600
- Direct Call (no API key needed):
2601
- list_connected()
2602
-
2603
- Interactive CLI mode:
2604
- npx @nordsym/apiclaw --cli
2605
-
2606
- Docs: https://apiclaw.cloud
2607
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2608
- `);
2609
- }
2610
-
2611
- main().catch(console.error);