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