@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.
- package/README.md +15 -2
- package/dist/bin-http.js +0 -0
- package/dist/bin.bundled.js +79288 -0
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +24 -2
- package/dist/gateway-client.js.map +1 -1
- package/dist/index.bundled.js +61263 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
- package/.claude/settings.local.json +0 -13
- package/.env.prod +0 -1
- package/apiclaw-README.md +0 -494
- package/convex/_generated/api.d.ts +0 -145
- package/convex/_generated/api.js +0 -23
- package/convex/_generated/dataModel.d.ts +0 -60
- package/convex/_generated/server.d.ts +0 -143
- package/convex/_generated/server.js +0 -93
- package/convex/_listWorkspaces.ts +0 -13
- package/convex/adminActivate.ts +0 -53
- package/convex/adminStats.ts +0 -306
- package/convex/agents.ts +0 -939
- package/convex/analytics.ts +0 -187
- package/convex/apiKeys.ts +0 -220
- package/convex/backfillAnalytics.ts +0 -272
- package/convex/backfillSearchLogs.ts +0 -35
- package/convex/billing.ts +0 -834
- package/convex/capabilities.ts +0 -157
- package/convex/chains.ts +0 -1318
- package/convex/credits.ts +0 -211
- package/convex/crons.ts +0 -65
- package/convex/debugFilestackLogs.ts +0 -16
- package/convex/debugGetToken.ts +0 -18
- package/convex/directCall.ts +0 -713
- package/convex/earnProgress.ts +0 -753
- package/convex/email.ts +0 -329
- package/convex/feedback.ts +0 -265
- package/convex/funnel.ts +0 -431
- package/convex/guards.ts +0 -174
- package/convex/http.ts +0 -3756
- package/convex/inbound.ts +0 -32
- package/convex/logs.ts +0 -701
- package/convex/migrateFilestack.ts +0 -81
- package/convex/migratePartnersProd.ts +0 -174
- package/convex/migratePratham.ts +0 -126
- package/convex/migrateProviderWorkspaces.ts +0 -175
- package/convex/mou.ts +0 -91
- package/convex/nurture.ts +0 -355
- package/convex/providerKeys.ts +0 -289
- package/convex/providers.ts +0 -1135
- package/convex/purchases.ts +0 -183
- package/convex/ratelimit.ts +0 -104
- package/convex/schema.ts +0 -926
- package/convex/searchLogs.ts +0 -265
- package/convex/seedAPILayerAPIs.ts +0 -191
- package/convex/seedDirectCallConfigs.ts +0 -336
- package/convex/seedPratham.ts +0 -149
- package/convex/spendAlerts.ts +0 -442
- package/convex/stripeActions.ts +0 -607
- package/convex/teams.ts +0 -243
- package/convex/telemetry.ts +0 -81
- package/convex/tsconfig.json +0 -25
- package/convex/updateAPIStatus.ts +0 -44
- package/convex/usage.ts +0 -260
- package/convex/usageReports.ts +0 -357
- package/convex/waitlist.ts +0 -55
- package/convex/webhooks.ts +0 -494
- package/convex/workspaceSettings.ts +0 -143
- package/convex/workspaces.ts +0 -1331
- package/convex.json +0 -3
- package/direct-test.mjs +0 -51
- package/email-templates/filestack-provider-outreach.html +0 -162
- package/email-templates/partnership-template.html +0 -116
- package/email-templates/pratham-draft-preview.txt +0 -57
- package/email-templates/pratham-partnership-draft.html +0 -141
- package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
- package/reports/pipeline/PIPELINE-REPORT.json +0 -153
- package/reports/pipeline/acquire_apisguru.json +0 -17
- package/reports/pipeline/capabilities.json +0 -38
- package/reports/pipeline/discover_azure_recursive.json +0 -1551
- package/reports/pipeline/discover_github.json +0 -25
- package/reports/pipeline/discover_github_repos.json +0 -49
- package/reports/pipeline/discover_swaggerhub.json +0 -24
- package/reports/pipeline/discover_well_known.json +0 -23
- package/reports/pipeline/fetch_specs.json +0 -19
- package/reports/pipeline/generate_providers.json +0 -14
- package/reports/pipeline/match_registry.json +0 -11
- package/reports/pipeline/parse_specs.json +0 -17
- package/reports/pipeline/promote_candidates.json +0 -34
- package/reports/pipeline/validate.json +0 -30
- package/reports/pipeline/validate_smoke_details.json +0 -3835
- package/reports/session-report-2026-04-05.html +0 -433
- package/seed-apis-direct.mjs +0 -106
- package/src/access-control.ts +0 -174
- package/src/adapters/base.ts +0 -364
- package/src/adapters/claude-desktop.ts +0 -41
- package/src/adapters/cline.ts +0 -88
- package/src/adapters/continue.ts +0 -91
- package/src/adapters/cursor.ts +0 -43
- package/src/adapters/custom.ts +0 -188
- package/src/adapters/detect.ts +0 -202
- package/src/adapters/index.ts +0 -47
- package/src/adapters/windsurf.ts +0 -44
- package/src/bin-http.ts +0 -45
- package/src/bin.ts +0 -34
- package/src/capability-router.ts +0 -331
- package/src/chainExecutor.ts +0 -730
- package/src/chainResolver.test.ts +0 -246
- package/src/chainResolver.ts +0 -658
- package/src/cli/commands/demo.ts +0 -109
- package/src/cli/commands/doctor.ts +0 -435
- package/src/cli/commands/index.ts +0 -9
- package/src/cli/commands/login.ts +0 -203
- package/src/cli/commands/mcp-install.ts +0 -373
- package/src/cli/commands/restore.ts +0 -333
- package/src/cli/commands/setup.ts +0 -297
- package/src/cli/commands/uninstall.ts +0 -240
- package/src/cli/index.ts +0 -148
- package/src/cli.ts +0 -370
- package/src/confirmation.ts +0 -296
- package/src/credentials.ts +0 -455
- package/src/credits.ts +0 -329
- package/src/crypto.ts +0 -75
- package/src/discovery.ts +0 -568
- package/src/enterprise/env.ts +0 -156
- package/src/enterprise/index.ts +0 -7
- package/src/enterprise/script-generator.ts +0 -481
- package/src/execute-dynamic.ts +0 -617
- package/src/execute.ts +0 -2386
- package/src/funnel-client.ts +0 -168
- package/src/funnel.test.ts +0 -187
- package/src/gateway-client.ts +0 -192
- package/src/hivr-whitelist.ts +0 -110
- package/src/http-api.ts +0 -286
- package/src/http-server-minimal.ts +0 -154
- package/src/index.ts +0 -2702
- package/src/intelligent-gateway.ts +0 -339
- package/src/mcp-analytics.ts +0 -156
- package/src/metered.ts +0 -149
- package/src/open-apis-generated.ts +0 -157
- package/src/open-apis.ts +0 -558
- package/src/postinstall.ts +0 -40
- package/src/product-whitelist.ts +0 -246
- package/src/proxy.ts +0 -36
- package/src/registration-guard.ts +0 -117
- package/src/session.ts +0 -129
- package/src/stripe.ts +0 -497
- package/src/telemetry.ts +0 -71
- package/src/test.ts +0 -135
- package/src/types/convex-api.d.ts +0 -20
- package/src/types/convex-api.ts +0 -21
- package/src/types.ts +0 -109
- package/src/ui/colors.ts +0 -219
- package/src/ui/errors.ts +0 -394
- package/src/ui/index.ts +0 -17
- package/src/ui/prompts.ts +0 -390
- package/src/ui/spinner.ts +0 -325
- package/src/utils/backup.ts +0 -224
- package/src/utils/config.ts +0 -318
- package/src/utils/os.ts +0 -124
- package/src/utils/paths.ts +0 -203
- package/src/webhook.ts +0 -107
- package/test-10-working.cjs +0 -97
- package/test-14-final.cjs +0 -96
- package/test-actual-handlers.ts +0 -92
- package/test-apilayer-all-14.ts +0 -249
- package/test-apilayer-fixed.ts +0 -248
- package/test-direct-endpoints.ts +0 -174
- package/test-exact-endpoints.ts +0 -144
- package/test-final.ts +0 -83
- package/test-full-routing.ts +0 -100
- package/test-handlers-correct.ts +0 -217
- package/test-numverify-key.ts +0 -41
- package/test-via-handlers.ts +0 -92
- package/test-worldnews.mjs +0 -26
- 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);
|