@nordsym/apiclaw 1.7.2 → 1.7.4
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/convex/_generated/api.d.ts +115 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/adminActivate.d.ts +3 -0
- package/convex/adminActivate.d.ts.map +1 -0
- package/convex/adminActivate.js +47 -0
- package/convex/adminActivate.js.map +1 -0
- package/convex/adminActivate.ts +54 -0
- package/convex/adminStats.d.ts +3 -0
- package/convex/adminStats.d.ts.map +1 -0
- package/convex/adminStats.js +42 -0
- package/convex/adminStats.js.map +1 -0
- package/convex/adminStats.ts +44 -0
- package/convex/agents.d.ts +76 -0
- package/convex/agents.d.ts.map +1 -0
- package/convex/agents.js +699 -0
- package/convex/agents.js.map +1 -0
- package/convex/agents.ts +814 -0
- package/convex/analytics.d.ts +5 -0
- package/convex/analytics.d.ts.map +1 -0
- package/convex/analytics.js +166 -0
- package/convex/analytics.js.map +1 -0
- package/convex/analytics.ts +186 -0
- package/convex/billing.d.ts +88 -0
- package/convex/billing.d.ts.map +1 -0
- package/convex/billing.js +655 -0
- package/convex/billing.js.map +1 -0
- package/convex/billing.ts +791 -0
- package/convex/capabilities.d.ts +9 -0
- package/convex/capabilities.d.ts.map +1 -0
- package/convex/capabilities.js +145 -0
- package/convex/capabilities.js.map +1 -0
- package/convex/capabilities.ts +157 -0
- package/convex/chains.d.ts +68 -0
- package/convex/chains.d.ts.map +1 -0
- package/convex/chains.js +1105 -0
- package/convex/chains.js.map +1 -0
- package/convex/chains.ts +1318 -0
- package/convex/credits.d.ts +25 -0
- package/convex/credits.d.ts.map +1 -0
- package/convex/credits.js +186 -0
- package/convex/credits.js.map +1 -0
- package/convex/credits.ts +211 -0
- package/convex/crons.d.ts +3 -0
- package/convex/crons.d.ts.map +1 -0
- package/convex/crons.js +17 -0
- package/convex/crons.js.map +1 -0
- package/convex/crons.ts +28 -0
- package/convex/directCall.d.ts +72 -0
- package/convex/directCall.d.ts.map +1 -0
- package/convex/directCall.js +627 -0
- package/convex/directCall.js.map +1 -0
- package/convex/directCall.ts +678 -0
- package/convex/earnProgress.d.ts +58 -0
- package/convex/earnProgress.d.ts.map +1 -0
- package/convex/earnProgress.js +649 -0
- package/convex/earnProgress.js.map +1 -0
- package/convex/earnProgress.ts +753 -0
- package/convex/email.d.ts +14 -0
- package/convex/email.d.ts.map +1 -0
- package/convex/email.js +300 -0
- package/convex/email.js.map +1 -0
- package/convex/email.ts +329 -0
- package/convex/feedback.d.ts +7 -0
- package/convex/feedback.d.ts.map +1 -0
- package/convex/feedback.js +227 -0
- package/convex/feedback.js.map +1 -0
- package/convex/feedback.ts +265 -0
- package/convex/http.d.ts +3 -0
- package/convex/http.d.ts.map +1 -0
- package/convex/http.js +1405 -0
- package/convex/http.js.map +1 -0
- package/convex/http.ts +1577 -0
- package/convex/inbound.d.ts +2 -0
- package/convex/inbound.d.ts.map +1 -0
- package/convex/inbound.js +32 -0
- package/convex/inbound.js.map +1 -0
- package/convex/inbound.ts +32 -0
- package/convex/logs.d.ts +38 -0
- package/convex/logs.d.ts.map +1 -0
- package/convex/logs.js +487 -0
- package/convex/logs.js.map +1 -0
- package/convex/logs.ts +550 -0
- package/convex/mou.d.ts +6 -0
- package/convex/mou.d.ts.map +1 -0
- package/convex/mou.js +82 -0
- package/convex/mou.js.map +1 -0
- package/convex/mou.ts +91 -0
- package/convex/providerKeys.d.ts +31 -0
- package/convex/providerKeys.d.ts.map +1 -0
- package/convex/providerKeys.js +257 -0
- package/convex/providerKeys.js.map +1 -0
- package/convex/providerKeys.ts +289 -0
- package/convex/providers.d.ts +32 -0
- package/convex/providers.d.ts.map +1 -0
- package/convex/providers.js +814 -0
- package/convex/providers.js.map +1 -0
- package/convex/providers.ts +909 -0
- package/convex/purchases.d.ts +7 -0
- package/convex/purchases.d.ts.map +1 -0
- package/convex/purchases.js +157 -0
- package/convex/purchases.js.map +1 -0
- package/convex/purchases.ts +183 -0
- package/convex/ratelimit.d.ts +4 -0
- package/convex/ratelimit.d.ts.map +1 -0
- package/convex/ratelimit.js +91 -0
- package/convex/ratelimit.js.map +1 -0
- package/convex/ratelimit.ts +104 -0
- package/convex/schema.ts +802 -0
- package/convex/searchLogs.d.ts +4 -0
- package/convex/searchLogs.d.ts.map +1 -0
- package/convex/searchLogs.js +129 -0
- package/convex/searchLogs.js.map +1 -0
- package/convex/searchLogs.ts +146 -0
- package/convex/seedAPILayerAPIs.d.ts +7 -0
- package/convex/seedAPILayerAPIs.d.ts.map +1 -0
- package/convex/seedAPILayerAPIs.js +177 -0
- package/convex/seedAPILayerAPIs.js.map +1 -0
- package/convex/seedAPILayerAPIs.ts +191 -0
- package/convex/seedDirectCallConfigs.d.ts +2 -0
- package/convex/seedDirectCallConfigs.d.ts.map +1 -0
- package/convex/seedDirectCallConfigs.js +324 -0
- package/convex/seedDirectCallConfigs.js.map +1 -0
- package/convex/seedDirectCallConfigs.ts +336 -0
- package/convex/seedPratham.d.ts +6 -0
- package/convex/seedPratham.d.ts.map +1 -0
- package/convex/seedPratham.js +150 -0
- package/convex/seedPratham.js.map +1 -0
- package/convex/seedPratham.ts +161 -0
- package/convex/spendAlerts.d.ts +36 -0
- package/convex/spendAlerts.d.ts.map +1 -0
- package/convex/spendAlerts.js +380 -0
- package/convex/spendAlerts.js.map +1 -0
- package/convex/spendAlerts.ts +442 -0
- package/convex/stripeActions.d.ts +19 -0
- package/convex/stripeActions.d.ts.map +1 -0
- package/convex/stripeActions.js +411 -0
- package/convex/stripeActions.js.map +1 -0
- package/convex/stripeActions.ts +512 -0
- package/convex/teams.d.ts +21 -0
- package/convex/teams.d.ts.map +1 -0
- package/convex/teams.js +215 -0
- package/convex/teams.js.map +1 -0
- package/convex/teams.ts +243 -0
- package/convex/telemetry.d.ts +4 -0
- package/convex/telemetry.d.ts.map +1 -0
- package/convex/telemetry.js +74 -0
- package/convex/telemetry.js.map +1 -0
- package/convex/telemetry.ts +81 -0
- package/convex/tsconfig.json +25 -0
- package/convex/updateAPIStatus.d.ts +6 -0
- package/convex/updateAPIStatus.d.ts.map +1 -0
- package/convex/updateAPIStatus.js +40 -0
- package/convex/updateAPIStatus.js.map +1 -0
- package/convex/updateAPIStatus.ts +45 -0
- package/convex/usage.d.ts +27 -0
- package/convex/usage.d.ts.map +1 -0
- package/convex/usage.js +229 -0
- package/convex/usage.js.map +1 -0
- package/convex/usage.ts +260 -0
- package/convex/waitlist.d.ts +4 -0
- package/convex/waitlist.d.ts.map +1 -0
- package/convex/waitlist.js +49 -0
- package/convex/waitlist.js.map +1 -0
- package/convex/waitlist.ts +55 -0
- package/convex/webhooks.d.ts +12 -0
- package/convex/webhooks.d.ts.map +1 -0
- package/convex/webhooks.js +410 -0
- package/convex/webhooks.js.map +1 -0
- package/convex/webhooks.ts +494 -0
- package/convex/workspaces.d.ts +31 -0
- package/convex/workspaces.d.ts.map +1 -0
- package/convex/workspaces.js +975 -0
- package/convex/workspaces.js.map +1 -0
- package/convex/workspaces.ts +1130 -0
- package/dist/bin.js +0 -0
- package/dist/capability-router.js +1 -1
- package/dist/capability-router.js.map +1 -1
- package/dist/execute.d.ts +2 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +18 -4
- package/dist/execute.js.map +1 -1
- package/dist/http-api.js +1 -1
- package/dist/http-api.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-analytics.d.ts +32 -0
- package/dist/mcp-analytics.d.ts.map +1 -0
- package/dist/mcp-analytics.js +130 -0
- package/dist/mcp-analytics.js.map +1 -0
- package/package.json +1 -1
- package/dist/chain-types.d.ts +0 -187
- package/dist/chain-types.d.ts.map +0 -1
- package/dist/chain-types.js +0 -33
- package/dist/chain-types.js.map +0 -1
- package/dist/registry/apis.json.bak +0 -248811
- package/dist/src/bin.js +0 -17
- package/dist/src/capability-router.js +0 -240
- package/dist/src/chainExecutor.js +0 -451
- package/dist/src/chainResolver.js +0 -518
- package/dist/src/cli/commands/doctor.js +0 -324
- package/dist/src/cli/commands/mcp-install.js +0 -255
- package/dist/src/cli/commands/restore.js +0 -259
- package/dist/src/cli/commands/setup.js +0 -205
- package/dist/src/cli/commands/uninstall.js +0 -188
- package/dist/src/cli/index.js +0 -111
- package/dist/src/cli.js +0 -302
- package/dist/src/confirmation.js +0 -240
- package/dist/src/credentials.js +0 -357
- package/dist/src/credits.js +0 -260
- package/dist/src/crypto.js +0 -66
- package/dist/src/discovery.js +0 -504
- package/dist/src/enterprise/env.js +0 -123
- package/dist/src/enterprise/script-generator.js +0 -460
- package/dist/src/execute-dynamic.js +0 -473
- package/dist/src/execute.js +0 -1727
- package/dist/src/index.js +0 -2062
- package/dist/src/metered.js +0 -80
- package/dist/src/open-apis.js +0 -276
- package/dist/src/proxy.js +0 -28
- package/dist/src/session.js +0 -86
- package/dist/src/stripe.js +0 -407
- package/dist/src/telemetry.js +0 -49
- package/dist/src/types.js +0 -2
- package/dist/src/utils/backup.js +0 -181
- package/dist/src/utils/config.js +0 -220
- package/dist/src/utils/os.js +0 -105
- package/dist/src/utils/paths.js +0 -159
package/dist/src/index.js
DELETED
|
@@ -1,2062 +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
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
13
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
-
import { discoverAPIs, getAPIDetails, getCategories, getAllAPIs } from './discovery.js';
|
|
16
|
-
import { trackStartup, trackSearch } from './telemetry.js';
|
|
17
|
-
import { addCredits, purchaseAPIAccess, getBalanceSummary } from './credits.js';
|
|
18
|
-
import { hasRealCredentials } from './credentials.js';
|
|
19
|
-
import { getConnectedProviders } from './execute.js';
|
|
20
|
-
import { executeMetered } from './metered.js';
|
|
21
|
-
import { logAPICall } from './analytics.js';
|
|
22
|
-
import { isOpenAPI, executeOpenAPI, listOpenAPIs } from './open-apis.js';
|
|
23
|
-
import { PROXY_PROVIDERS } from './proxy.js';
|
|
24
|
-
import { requiresConfirmationAsync, createPendingAction, consumePendingAction, generatePreview, validateParams } from './confirmation.js';
|
|
25
|
-
import { executeCapability, listCapabilities, hasCapability } from './capability-router.js';
|
|
26
|
-
import { readSession, writeSession, clearSession, getMachineFingerprint } from './session.js';
|
|
27
|
-
import { ConvexHttpClient } from 'convex/browser';
|
|
28
|
-
import { getOrCreateCustomer, createMeteredCheckoutSession, getUsageSummary, METERED_BILLING } from './stripe.js';
|
|
29
|
-
import { estimateCost } from './metered.js';
|
|
30
|
-
import { executeChain, getChainStatus, resumeChain } from './chainExecutor.js';
|
|
31
|
-
// Default agent ID for MVP (in production, this would come from auth)
|
|
32
|
-
const DEFAULT_AGENT_ID = 'agent_default';
|
|
33
|
-
// Convex client for workspace management
|
|
34
|
-
const CONVEX_URL = process.env.CONVEX_URL || 'https://brilliant-puffin-712.eu-west-1.convex.cloud';
|
|
35
|
-
const convex = new ConvexHttpClient(CONVEX_URL);
|
|
36
|
-
let workspaceContext = null;
|
|
37
|
-
const anonymousRateLimits = new Map();
|
|
38
|
-
// Rate limit constants
|
|
39
|
-
const ANONYMOUS_HOURLY_LIMIT = 5;
|
|
40
|
-
const ANONYMOUS_WEEKLY_LIMIT = 10;
|
|
41
|
-
const FREE_WEEKLY_LIMIT = 50;
|
|
42
|
-
/**
|
|
43
|
-
* Calculate minutes until next hour
|
|
44
|
-
*/
|
|
45
|
-
function calculateMinutesUntilNextHour() {
|
|
46
|
-
const now = new Date();
|
|
47
|
-
const nextHour = new Date(now);
|
|
48
|
-
nextHour.setHours(now.getHours() + 1, 0, 0, 0);
|
|
49
|
-
return Math.ceil((nextHour.getTime() - now.getTime()) / 60000);
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Get next Monday 00:00 UTC as ISO string
|
|
53
|
-
*/
|
|
54
|
-
function getNextMondayUTC() {
|
|
55
|
-
const now = new Date();
|
|
56
|
-
const dayOfWeek = now.getUTCDay(); // 0 = Sunday, 1 = Monday, etc.
|
|
57
|
-
const daysUntilMonday = dayOfWeek === 0 ? 1 : 8 - dayOfWeek;
|
|
58
|
-
const nextMonday = new Date(now);
|
|
59
|
-
nextMonday.setUTCDate(now.getUTCDate() + daysUntilMonday);
|
|
60
|
-
nextMonday.setUTCHours(0, 0, 0, 0);
|
|
61
|
-
return nextMonday.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Check anonymous rate limits for proxy provider usage
|
|
65
|
-
*/
|
|
66
|
-
function checkAnonymousRateLimit(fingerprint) {
|
|
67
|
-
const now = Date.now();
|
|
68
|
-
const hourInMs = 60 * 60 * 1000;
|
|
69
|
-
const weekInMs = 7 * 24 * hourInMs;
|
|
70
|
-
// Get or initialize rate limit state
|
|
71
|
-
let state = anonymousRateLimits.get(fingerprint);
|
|
72
|
-
if (!state) {
|
|
73
|
-
state = {
|
|
74
|
-
hourlyCount: 0,
|
|
75
|
-
hourlyResetTime: now + hourInMs,
|
|
76
|
-
weeklyCount: 0,
|
|
77
|
-
weeklyResetTime: now + weekInMs,
|
|
78
|
-
};
|
|
79
|
-
anonymousRateLimits.set(fingerprint, state);
|
|
80
|
-
}
|
|
81
|
-
// Reset hourly counter if time elapsed
|
|
82
|
-
if (now >= state.hourlyResetTime) {
|
|
83
|
-
state.hourlyCount = 0;
|
|
84
|
-
state.hourlyResetTime = now + hourInMs;
|
|
85
|
-
}
|
|
86
|
-
// Reset weekly counter if time elapsed
|
|
87
|
-
if (now >= state.weeklyResetTime) {
|
|
88
|
-
state.weeklyCount = 0;
|
|
89
|
-
state.weeklyResetTime = now + weekInMs;
|
|
90
|
-
}
|
|
91
|
-
// Check hourly limit
|
|
92
|
-
if (state.hourlyCount >= ANONYMOUS_HOURLY_LIMIT) {
|
|
93
|
-
return {
|
|
94
|
-
allowed: false,
|
|
95
|
-
error: JSON.stringify({
|
|
96
|
-
success: false,
|
|
97
|
-
error: `Hourly rate limit (${ANONYMOUS_HOURLY_LIMIT} calls/hour)`,
|
|
98
|
-
retry_after_minutes: calculateMinutesUntilNextHour(),
|
|
99
|
-
hint: "Rate limit resets at top of hour",
|
|
100
|
-
action: "Register to get higher limits: register_owner({ email: 'you@example.com' })"
|
|
101
|
-
}, null, 2)
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
// Check weekly limit
|
|
105
|
-
if (state.weeklyCount >= ANONYMOUS_WEEKLY_LIMIT) {
|
|
106
|
-
return {
|
|
107
|
-
allowed: false,
|
|
108
|
-
error: JSON.stringify({
|
|
109
|
-
success: false,
|
|
110
|
-
error: `Weekly limit reached (${ANONYMOUS_WEEKLY_LIMIT} calls)`,
|
|
111
|
-
hint: "Register to get 50 calls/week",
|
|
112
|
-
action: "Run: register_owner({ email: 'you@example.com' })",
|
|
113
|
-
retry_after: getNextMondayUTC()
|
|
114
|
-
}, null, 2)
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
// Increment counters
|
|
118
|
-
state.hourlyCount++;
|
|
119
|
-
state.weeklyCount++;
|
|
120
|
-
return { allowed: true };
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Validate session on startup
|
|
124
|
-
*/
|
|
125
|
-
async function validateSession() {
|
|
126
|
-
const session = readSession();
|
|
127
|
-
if (!session) {
|
|
128
|
-
console.error('[APIClaw] No session found. Use register_owner to authenticate.');
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
const result = await convex.query("workspaces:getWorkspaceStatus", {
|
|
133
|
-
sessionToken: session.sessionToken,
|
|
134
|
-
});
|
|
135
|
-
if (!result.authenticated) {
|
|
136
|
-
console.error('[APIClaw] Session invalid or expired. Clearing...');
|
|
137
|
-
clearSession();
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
if (result.status !== 'active') {
|
|
141
|
-
console.error(`[APIClaw] Workspace status: ${result.status}. Please verify your email.`);
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
workspaceContext = {
|
|
145
|
-
sessionToken: session.sessionToken,
|
|
146
|
-
workspaceId: session.workspaceId,
|
|
147
|
-
email: result.email ?? '',
|
|
148
|
-
tier: result.tier ?? 'free',
|
|
149
|
-
usageRemaining: result.usageRemaining ?? 0,
|
|
150
|
-
status: result.status ?? 'unknown',
|
|
151
|
-
};
|
|
152
|
-
console.error(`[APIClaw] ✓ Authenticated as ${result.email} (${result.tier} tier)`);
|
|
153
|
-
console.error(`[APIClaw] ✓ Usage: ${result.usageCount}/${result.usageLimit === -1 ? '∞' : result.usageLimit} calls`);
|
|
154
|
-
// Touch session to update last used
|
|
155
|
-
await convex.mutation("workspaces:touchSession", {
|
|
156
|
-
sessionToken: session.sessionToken,
|
|
157
|
-
});
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
console.error('[APIClaw] Error validating session:', error);
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Track earn progress after successful API call
|
|
167
|
-
* Handles firstDirectCall and apisUsed tracking
|
|
168
|
-
*/
|
|
169
|
-
async function trackEarnProgress(workspaceId, provider, action) {
|
|
170
|
-
try {
|
|
171
|
-
// Track first direct call
|
|
172
|
-
await convex.mutation("earnProgress:markFirstDirectCall", {
|
|
173
|
-
workspaceId: workspaceId,
|
|
174
|
-
});
|
|
175
|
-
// Track unique API usage
|
|
176
|
-
const apiId = `${provider}:${action}`;
|
|
177
|
-
await convex.mutation("earnProgress:trackApiUsed", {
|
|
178
|
-
workspaceId: workspaceId,
|
|
179
|
-
apiId,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
catch (e) {
|
|
183
|
-
// Non-critical - don't fail the API call if earn tracking fails
|
|
184
|
-
console.error('[APIClaw] Failed to track earn progress:', e);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const rateLimitStore = new Map();
|
|
188
|
-
/**
|
|
189
|
-
* For proxy providers, allow anonymous usage with rate limiting
|
|
190
|
-
*/
|
|
191
|
-
function checkWorkspaceAccess(providerId) {
|
|
192
|
-
// Allow anonymous access for proxy providers
|
|
193
|
-
if (providerId && PROXY_PROVIDERS.includes(providerId)) {
|
|
194
|
-
if (!workspaceContext) {
|
|
195
|
-
// Anonymous user - check rate limits
|
|
196
|
-
const fingerprint = getMachineFingerprint();
|
|
197
|
-
const rateLimitCheck = checkAnonymousRateLimit(fingerprint);
|
|
198
|
-
if (!rateLimitCheck.allowed) {
|
|
199
|
-
return {
|
|
200
|
-
allowed: false,
|
|
201
|
-
error: rateLimitCheck.error,
|
|
202
|
-
isAnonymous: true
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
return { allowed: true, isAnonymous: true };
|
|
206
|
-
}
|
|
207
|
-
// Authenticated user using proxy provider - allow with higher limits
|
|
208
|
-
return { allowed: true, isAnonymous: false };
|
|
209
|
-
}
|
|
210
|
-
// Non-proxy providers require authentication
|
|
211
|
-
if (!workspaceContext) {
|
|
212
|
-
return {
|
|
213
|
-
allowed: false,
|
|
214
|
-
error: 'Not authenticated. Use register_owner to authenticate your workspace.'
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
if (workspaceContext.status !== 'active') {
|
|
218
|
-
return {
|
|
219
|
-
allowed: false,
|
|
220
|
-
error: `Workspace status: ${workspaceContext.status}. Please verify your email.`
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
if (workspaceContext.usageRemaining === 0) {
|
|
224
|
-
// Free tier hit weekly limit
|
|
225
|
-
if (workspaceContext.tier === 'free') {
|
|
226
|
-
return {
|
|
227
|
-
allowed: false,
|
|
228
|
-
error: JSON.stringify({
|
|
229
|
-
success: false,
|
|
230
|
-
error: `Weekly limit reached (${FREE_WEEKLY_LIMIT} calls)`,
|
|
231
|
-
hint: "Upgrade to Backer for unlimited calls",
|
|
232
|
-
upgrade_url: "https://apiclaw.nordsym.com/upgrade",
|
|
233
|
-
retry_after: getNextMondayUTC()
|
|
234
|
-
}, null, 2)
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
// Other tiers (shouldn't happen, but handle gracefully)
|
|
238
|
-
return {
|
|
239
|
-
allowed: false,
|
|
240
|
-
error: `Usage limit reached. Contact support or check your plan at https://apiclaw.nordsym.com/account`
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
return { allowed: true, isAnonymous: false };
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Get customer API key from environment variable
|
|
247
|
-
* Convention: {PROVIDER}_API_KEY (e.g., COACCEPT_API_KEY, ELKS_API_KEY)
|
|
248
|
-
*/
|
|
249
|
-
function getCustomerKey(providerId) {
|
|
250
|
-
// Try exact match first (e.g., 46elks -> 46ELKS_API_KEY)
|
|
251
|
-
const exactKey = `${providerId.toUpperCase().replace(/-/g, '_')}_API_KEY`;
|
|
252
|
-
if (process.env[exactKey]) {
|
|
253
|
-
return process.env[exactKey];
|
|
254
|
-
}
|
|
255
|
-
// Try common variations
|
|
256
|
-
const variations = [
|
|
257
|
-
`${providerId.toUpperCase()}_API_KEY`,
|
|
258
|
-
`${providerId.toUpperCase()}_KEY`,
|
|
259
|
-
`${providerId.toUpperCase().replace(/_/g, '')}_API_KEY`,
|
|
260
|
-
];
|
|
261
|
-
for (const key of variations) {
|
|
262
|
-
if (process.env[key]) {
|
|
263
|
-
return process.env[key];
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return undefined;
|
|
267
|
-
}
|
|
268
|
-
// Tool definitions
|
|
269
|
-
const tools = [
|
|
270
|
-
{
|
|
271
|
-
name: 'apiclaw_help',
|
|
272
|
-
description: 'Get help and see available commands. Start here if you are new to APIClaw.',
|
|
273
|
-
inputSchema: {
|
|
274
|
-
type: 'object',
|
|
275
|
-
properties: {},
|
|
276
|
-
required: []
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
name: 'discover_apis',
|
|
281
|
-
description: 'Search for APIs based on what you need to do. Describe your use case naturally.',
|
|
282
|
-
inputSchema: {
|
|
283
|
-
type: 'object',
|
|
284
|
-
properties: {
|
|
285
|
-
query: {
|
|
286
|
-
type: 'string',
|
|
287
|
-
description: 'Natural language query describing what you need (e.g., "send SMS to Sweden", "search the web", "generate speech from text")'
|
|
288
|
-
},
|
|
289
|
-
category: {
|
|
290
|
-
type: 'string',
|
|
291
|
-
description: 'Filter by category: communication, search, ai',
|
|
292
|
-
enum: ['communication', 'search', 'ai']
|
|
293
|
-
},
|
|
294
|
-
max_results: {
|
|
295
|
-
type: 'number',
|
|
296
|
-
description: 'Maximum number of results to return (default: 5)',
|
|
297
|
-
default: 5
|
|
298
|
-
},
|
|
299
|
-
region: {
|
|
300
|
-
type: 'string',
|
|
301
|
-
description: 'Filter by region (e.g., "SE", "EU", "global")'
|
|
302
|
-
},
|
|
303
|
-
subagent_id: {
|
|
304
|
-
type: 'string',
|
|
305
|
-
description: 'Optional subagent identifier for multi-agent tracking'
|
|
306
|
-
},
|
|
307
|
-
ai_backend: {
|
|
308
|
-
type: 'string',
|
|
309
|
-
description: 'AI backend making this request (e.g., "claude-3-sonnet", "gpt-4"). Used for analytics.'
|
|
310
|
-
}
|
|
311
|
-
},
|
|
312
|
-
required: ['query']
|
|
313
|
-
}
|
|
314
|
-
},
|
|
315
|
-
{
|
|
316
|
-
name: 'get_api_details',
|
|
317
|
-
description: 'Get detailed information about a specific API provider, including endpoints, pricing, and features. Use compact=true to save ~60% tokens.',
|
|
318
|
-
inputSchema: {
|
|
319
|
-
type: 'object',
|
|
320
|
-
properties: {
|
|
321
|
-
api_id: {
|
|
322
|
-
type: 'string',
|
|
323
|
-
description: 'The API provider ID (e.g., "46elks", "resend", "openrouter")'
|
|
324
|
-
},
|
|
325
|
-
compact: {
|
|
326
|
-
type: 'boolean',
|
|
327
|
-
description: 'If true, returns minified spec (strips examples, keeps essential params). Saves ~60% context window.',
|
|
328
|
-
default: false
|
|
329
|
-
}
|
|
330
|
-
},
|
|
331
|
-
required: ['api_id']
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
name: 'purchase_access',
|
|
336
|
-
description: 'Purchase access to an API using your credit balance. Returns API credentials on success.',
|
|
337
|
-
inputSchema: {
|
|
338
|
-
type: 'object',
|
|
339
|
-
properties: {
|
|
340
|
-
api_id: {
|
|
341
|
-
type: 'string',
|
|
342
|
-
description: 'The API provider ID to purchase access to'
|
|
343
|
-
},
|
|
344
|
-
amount_usd: {
|
|
345
|
-
type: 'number',
|
|
346
|
-
description: 'Amount in USD to spend on this API'
|
|
347
|
-
},
|
|
348
|
-
agent_id: {
|
|
349
|
-
type: 'string',
|
|
350
|
-
description: 'Your agent identifier (optional, uses default if not provided)'
|
|
351
|
-
}
|
|
352
|
-
},
|
|
353
|
-
required: ['api_id', 'amount_usd']
|
|
354
|
-
}
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
name: 'check_balance',
|
|
358
|
-
description: 'Check your credit balance and list active API purchases.',
|
|
359
|
-
inputSchema: {
|
|
360
|
-
type: 'object',
|
|
361
|
-
properties: {
|
|
362
|
-
agent_id: {
|
|
363
|
-
type: 'string',
|
|
364
|
-
description: 'Your agent identifier (optional, uses default if not provided)'
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
},
|
|
369
|
-
{
|
|
370
|
-
name: 'add_credits',
|
|
371
|
-
description: 'Add credits to your account. (For testing/demo purposes)',
|
|
372
|
-
inputSchema: {
|
|
373
|
-
type: 'object',
|
|
374
|
-
properties: {
|
|
375
|
-
amount_usd: {
|
|
376
|
-
type: 'number',
|
|
377
|
-
description: 'Amount in USD to add to your balance'
|
|
378
|
-
},
|
|
379
|
-
agent_id: {
|
|
380
|
-
type: 'string',
|
|
381
|
-
description: 'Your agent identifier (optional, uses default if not provided)'
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
required: ['amount_usd']
|
|
385
|
-
}
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
name: 'list_categories',
|
|
389
|
-
description: 'List all available API categories.',
|
|
390
|
-
inputSchema: {
|
|
391
|
-
type: 'object',
|
|
392
|
-
properties: {}
|
|
393
|
-
}
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
name: 'call_api',
|
|
397
|
-
description: `Execute an API call through APIClaw. Supports single calls AND multi-step chains.
|
|
398
|
-
|
|
399
|
-
SINGLE CALL: Provide provider + action + params
|
|
400
|
-
CHAIN: Provide chain array to execute multiple APIs in sequence/parallel with cross-step references.
|
|
401
|
-
|
|
402
|
-
Chain features:
|
|
403
|
-
- Sequential: Steps execute in order, each can reference previous results via $stepId.property
|
|
404
|
-
- Parallel: Use { parallel: [...steps] } to run concurrently
|
|
405
|
-
- Conditional: Use { if: "$step.success", then: {...}, else: {...} }
|
|
406
|
-
- Loops: Use { forEach: "$step.results", as: "item", do: {...} }
|
|
407
|
-
- Error handling: Per-step retry/fallback via onError
|
|
408
|
-
- Async: Set async: true to get chainId immediately, poll or use webhook
|
|
409
|
-
|
|
410
|
-
Example chain:
|
|
411
|
-
chain: [
|
|
412
|
-
{ id: "search", provider: "brave_search", action: "search", params: { query: "AI agents" } },
|
|
413
|
-
{ id: "summarize", provider: "openrouter", action: "chat", params: { message: "Summarize: $search.results" } }
|
|
414
|
-
]`,
|
|
415
|
-
inputSchema: {
|
|
416
|
-
type: 'object',
|
|
417
|
-
properties: {
|
|
418
|
-
// Single call params
|
|
419
|
-
provider: {
|
|
420
|
-
type: 'string',
|
|
421
|
-
description: 'Provider ID (e.g., "46elks", "brave_search", "resend", "openrouter", "elevenlabs", "twilio", "coaccept", "frankfurter")'
|
|
422
|
-
},
|
|
423
|
-
action: {
|
|
424
|
-
type: 'string',
|
|
425
|
-
description: 'Action to perform (e.g., "send_sms", "search", "send_email", "chat", "send_invoice", "convert")'
|
|
426
|
-
},
|
|
427
|
-
params: {
|
|
428
|
-
type: 'object',
|
|
429
|
-
description: 'Parameters for the action. Varies by provider/action.'
|
|
430
|
-
},
|
|
431
|
-
customer_key: {
|
|
432
|
-
type: 'string',
|
|
433
|
-
description: 'Optional: Your own API key for providers that require customer authentication (e.g., CoAccept).'
|
|
434
|
-
},
|
|
435
|
-
confirm_token: {
|
|
436
|
-
type: 'string',
|
|
437
|
-
description: 'Confirmation token from a previous call. Required to execute actions that cost money after reviewing the preview.'
|
|
438
|
-
},
|
|
439
|
-
dry_run: {
|
|
440
|
-
type: 'boolean',
|
|
441
|
-
description: 'If true, shows what WOULD be sent without making actual API calls. Returns mock response and request details. Great for testing and debugging.'
|
|
442
|
-
},
|
|
443
|
-
// Chain execution params
|
|
444
|
-
chain: {
|
|
445
|
-
type: 'array',
|
|
446
|
-
description: 'Execute multiple API calls as a single chain. Each step can reference previous results via $stepId.property',
|
|
447
|
-
items: {
|
|
448
|
-
type: 'object',
|
|
449
|
-
properties: {
|
|
450
|
-
id: { type: 'string', description: 'Step identifier for cross-step references' },
|
|
451
|
-
provider: { type: 'string', description: 'API provider' },
|
|
452
|
-
action: { type: 'string', description: 'Action to execute' },
|
|
453
|
-
params: { type: 'object', description: 'Action parameters. Use $stepId.path for references.' },
|
|
454
|
-
parallel: { type: 'array', description: 'Steps to run in parallel' },
|
|
455
|
-
if: { type: 'string', description: 'Condition for conditional execution (e.g., "$step1.success")' },
|
|
456
|
-
then: { type: 'object', description: 'Step to execute if condition is true' },
|
|
457
|
-
else: { type: 'object', description: 'Step to execute if condition is false' },
|
|
458
|
-
forEach: { type: 'string', description: 'Array reference to iterate (e.g., "$search.results")' },
|
|
459
|
-
as: { type: 'string', description: 'Variable name for current item in loop' },
|
|
460
|
-
do: { type: 'object', description: 'Step to execute for each item' },
|
|
461
|
-
onError: {
|
|
462
|
-
type: 'object',
|
|
463
|
-
description: 'Error handling configuration',
|
|
464
|
-
properties: {
|
|
465
|
-
retry: {
|
|
466
|
-
type: 'object',
|
|
467
|
-
properties: {
|
|
468
|
-
attempts: { type: 'number', description: 'Max retry attempts' },
|
|
469
|
-
backoff: { type: 'string', description: '"exponential" or "linear" or array of ms delays' }
|
|
470
|
-
}
|
|
471
|
-
},
|
|
472
|
-
fallback: { type: 'object', description: 'Fallback step if this fails' },
|
|
473
|
-
abort: { type: 'boolean', description: 'Abort entire chain on failure' }
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
},
|
|
479
|
-
// Chain options
|
|
480
|
-
continueOnError: {
|
|
481
|
-
type: 'boolean',
|
|
482
|
-
description: 'Continue chain execution even if a step fails (default: false)'
|
|
483
|
-
},
|
|
484
|
-
timeout: {
|
|
485
|
-
type: 'number',
|
|
486
|
-
description: 'Maximum execution time for the entire chain in milliseconds'
|
|
487
|
-
},
|
|
488
|
-
async: {
|
|
489
|
-
type: 'boolean',
|
|
490
|
-
description: 'Return immediately with chainId. Use get_chain_status to poll or provide webhook.'
|
|
491
|
-
},
|
|
492
|
-
webhook: {
|
|
493
|
-
type: 'string',
|
|
494
|
-
description: 'URL to POST results when async chain completes'
|
|
495
|
-
},
|
|
496
|
-
subagent_id: {
|
|
497
|
-
type: 'string',
|
|
498
|
-
description: 'Optional subagent identifier for multi-agent tracking'
|
|
499
|
-
},
|
|
500
|
-
ai_backend: {
|
|
501
|
-
type: 'string',
|
|
502
|
-
description: 'AI backend making this request (e.g., "claude-3-sonnet", "gpt-4"). Used for analytics.'
|
|
503
|
-
}
|
|
504
|
-
},
|
|
505
|
-
required: []
|
|
506
|
-
}
|
|
507
|
-
},
|
|
508
|
-
{
|
|
509
|
-
name: 'list_connected',
|
|
510
|
-
description: 'List all APIs available for Direct Call (no API key needed).',
|
|
511
|
-
inputSchema: {
|
|
512
|
-
type: 'object',
|
|
513
|
-
properties: {}
|
|
514
|
-
}
|
|
515
|
-
},
|
|
516
|
-
{
|
|
517
|
-
name: 'capability',
|
|
518
|
-
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"})',
|
|
519
|
-
inputSchema: {
|
|
520
|
-
type: 'object',
|
|
521
|
-
properties: {
|
|
522
|
-
capability: {
|
|
523
|
-
type: 'string',
|
|
524
|
-
description: 'Capability ID: "sms", "email", "search", "tts", "invoice", "llm"'
|
|
525
|
-
},
|
|
526
|
-
action: {
|
|
527
|
-
type: 'string',
|
|
528
|
-
description: 'Action to perform: "send", "search", "generate", etc.'
|
|
529
|
-
},
|
|
530
|
-
params: {
|
|
531
|
-
type: 'object',
|
|
532
|
-
description: 'Parameters for the action (capability-standard params, not provider-specific)'
|
|
533
|
-
},
|
|
534
|
-
preferences: {
|
|
535
|
-
type: 'object',
|
|
536
|
-
description: 'Optional routing preferences',
|
|
537
|
-
properties: {
|
|
538
|
-
region: { type: 'string', description: 'Preferred region: "SE", "EU", "US"' },
|
|
539
|
-
maxPrice: { type: 'number', description: 'Max price per unit in cents/öre' },
|
|
540
|
-
preferredProvider: { type: 'string', description: 'Hint to prefer a specific provider' },
|
|
541
|
-
fallback: { type: 'boolean', description: 'Enable fallback to other providers (default: true)' }
|
|
542
|
-
}
|
|
543
|
-
},
|
|
544
|
-
subagent_id: {
|
|
545
|
-
type: 'string',
|
|
546
|
-
description: 'Optional subagent identifier for multi-agent tracking'
|
|
547
|
-
},
|
|
548
|
-
ai_backend: {
|
|
549
|
-
type: 'string',
|
|
550
|
-
description: 'AI backend making this request (e.g., "claude-3-sonnet", "gpt-4"). Used for analytics.'
|
|
551
|
-
}
|
|
552
|
-
},
|
|
553
|
-
required: ['capability', 'action', 'params']
|
|
554
|
-
}
|
|
555
|
-
},
|
|
556
|
-
{
|
|
557
|
-
name: 'list_capabilities',
|
|
558
|
-
description: 'List all available capabilities and their providers.',
|
|
559
|
-
inputSchema: {
|
|
560
|
-
type: 'object',
|
|
561
|
-
properties: {}
|
|
562
|
-
}
|
|
563
|
-
},
|
|
564
|
-
// ============================================
|
|
565
|
-
// WORKSPACE TOOLS
|
|
566
|
-
// ============================================
|
|
567
|
-
{
|
|
568
|
-
name: 'register_owner',
|
|
569
|
-
description: 'Register your email to create a workspace. This authenticates your agent with APIClaw. You will receive a magic link to verify ownership.',
|
|
570
|
-
inputSchema: {
|
|
571
|
-
type: 'object',
|
|
572
|
-
properties: {
|
|
573
|
-
email: {
|
|
574
|
-
type: 'string',
|
|
575
|
-
description: 'Your email address (used for verification and account recovery)'
|
|
576
|
-
}
|
|
577
|
-
},
|
|
578
|
-
required: ['email']
|
|
579
|
-
}
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
name: 'check_workspace_status',
|
|
583
|
-
description: 'Check your workspace status, tier, and usage remaining.',
|
|
584
|
-
inputSchema: {
|
|
585
|
-
type: 'object',
|
|
586
|
-
properties: {}
|
|
587
|
-
}
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
name: 'remind_owner',
|
|
591
|
-
description: 'Send a reminder email to verify workspace ownership (if verification is pending).',
|
|
592
|
-
inputSchema: {
|
|
593
|
-
type: 'object',
|
|
594
|
-
properties: {}
|
|
595
|
-
}
|
|
596
|
-
},
|
|
597
|
-
// Metered Billing Tools
|
|
598
|
-
{
|
|
599
|
-
name: 'setup_metered_billing',
|
|
600
|
-
description: 'Set up pay-per-call billing. Creates a subscription that charges $0.002 per API call at end of month.',
|
|
601
|
-
inputSchema: {
|
|
602
|
-
type: 'object',
|
|
603
|
-
properties: {
|
|
604
|
-
email: {
|
|
605
|
-
type: 'string',
|
|
606
|
-
description: 'Email for the billing account'
|
|
607
|
-
},
|
|
608
|
-
success_url: {
|
|
609
|
-
type: 'string',
|
|
610
|
-
description: 'URL to redirect after successful setup',
|
|
611
|
-
default: 'https://apiclaw.nordsym.com/billing/success'
|
|
612
|
-
},
|
|
613
|
-
cancel_url: {
|
|
614
|
-
type: 'string',
|
|
615
|
-
description: 'URL to redirect if setup is cancelled',
|
|
616
|
-
default: 'https://apiclaw.nordsym.com/billing/cancel'
|
|
617
|
-
}
|
|
618
|
-
},
|
|
619
|
-
required: ['email']
|
|
620
|
-
}
|
|
621
|
-
},
|
|
622
|
-
{
|
|
623
|
-
name: 'get_usage_summary',
|
|
624
|
-
description: 'Get current billing period usage and estimated cost for metered billing.',
|
|
625
|
-
inputSchema: {
|
|
626
|
-
type: 'object',
|
|
627
|
-
properties: {
|
|
628
|
-
subscription_id: {
|
|
629
|
-
type: 'string',
|
|
630
|
-
description: 'Stripe subscription ID (stored after setup_metered_billing)'
|
|
631
|
-
}
|
|
632
|
-
},
|
|
633
|
-
required: ['subscription_id']
|
|
634
|
-
}
|
|
635
|
-
},
|
|
636
|
-
{
|
|
637
|
-
name: 'estimate_cost',
|
|
638
|
-
description: 'Estimate the cost for a given number of API calls.',
|
|
639
|
-
inputSchema: {
|
|
640
|
-
type: 'object',
|
|
641
|
-
properties: {
|
|
642
|
-
call_count: {
|
|
643
|
-
type: 'number',
|
|
644
|
-
description: 'Number of API calls to estimate cost for'
|
|
645
|
-
}
|
|
646
|
-
},
|
|
647
|
-
required: ['call_count']
|
|
648
|
-
}
|
|
649
|
-
},
|
|
650
|
-
// ============================================
|
|
651
|
-
// CHAIN MANAGEMENT TOOLS
|
|
652
|
-
// ============================================
|
|
653
|
-
{
|
|
654
|
-
name: 'get_chain_status',
|
|
655
|
-
description: 'Check the status of an async chain execution. Use the chainId returned from call_api with async: true.',
|
|
656
|
-
inputSchema: {
|
|
657
|
-
type: 'object',
|
|
658
|
-
properties: {
|
|
659
|
-
chain_id: {
|
|
660
|
-
type: 'string',
|
|
661
|
-
description: 'Chain ID returned from async chain execution'
|
|
662
|
-
}
|
|
663
|
-
},
|
|
664
|
-
required: ['chain_id']
|
|
665
|
-
}
|
|
666
|
-
},
|
|
667
|
-
{
|
|
668
|
-
name: 'resume_chain',
|
|
669
|
-
description: 'Resume a failed chain from the point of failure. Use the resumeToken from the error response. Requires the original chain definition.',
|
|
670
|
-
inputSchema: {
|
|
671
|
-
type: 'object',
|
|
672
|
-
properties: {
|
|
673
|
-
resume_token: {
|
|
674
|
-
type: 'string',
|
|
675
|
-
description: 'Resume token from a failed chain (e.g., "chain_xyz_step_2")'
|
|
676
|
-
},
|
|
677
|
-
original_chain: {
|
|
678
|
-
type: 'array',
|
|
679
|
-
description: 'The original chain definition that failed. Required to resume execution.',
|
|
680
|
-
items: { type: 'object' }
|
|
681
|
-
},
|
|
682
|
-
overrides: {
|
|
683
|
-
type: 'object',
|
|
684
|
-
description: 'Optional parameter overrides for specific steps. Format: { "stepId": { ...newParams } }'
|
|
685
|
-
}
|
|
686
|
-
},
|
|
687
|
-
required: ['resume_token', 'original_chain']
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
];
|
|
691
|
-
// Create server
|
|
692
|
-
const server = new Server({
|
|
693
|
-
name: 'apivault',
|
|
694
|
-
version: '0.1.0',
|
|
695
|
-
}, {
|
|
696
|
-
capabilities: {
|
|
697
|
-
tools: {},
|
|
698
|
-
},
|
|
699
|
-
});
|
|
700
|
-
// Handle list tools
|
|
701
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
702
|
-
return { tools };
|
|
703
|
-
});
|
|
704
|
-
// Handle tool calls
|
|
705
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
706
|
-
const { name, arguments: args } = request.params;
|
|
707
|
-
try {
|
|
708
|
-
switch (name) {
|
|
709
|
-
case 'apiclaw_help': {
|
|
710
|
-
const helpText = `
|
|
711
|
-
🦞 APIClaw — The API Layer for AI Agents
|
|
712
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
713
|
-
|
|
714
|
-
DISCOVER APIs:
|
|
715
|
-
discover_apis({ query: "send SMS to Sweden" })
|
|
716
|
-
discover_apis({ query: "search the web", max_results: 10 })
|
|
717
|
-
discover_apis({ query: "text to speech", category: "ai" })
|
|
718
|
-
|
|
719
|
-
GET DETAILS:
|
|
720
|
-
get_api_details({ api_id: "46elks" })
|
|
721
|
-
|
|
722
|
-
DIRECT CALL (8 APIs, no key needed):
|
|
723
|
-
get_connected_providers()
|
|
724
|
-
call_api({ provider: "brave_search", endpoint: "search", params: { query: "AI agents" } })
|
|
725
|
-
|
|
726
|
-
Available direct-call providers:
|
|
727
|
-
• brave_search — Web search
|
|
728
|
-
• 46elks — SMS (Sweden)
|
|
729
|
-
• twilio — SMS (Global)
|
|
730
|
-
• resend — Email
|
|
731
|
-
• openrouter — LLM routing (100+ models)
|
|
732
|
-
• elevenlabs — Text-to-speech
|
|
733
|
-
• replicate — AI models (images, video, audio)
|
|
734
|
-
• firecrawl — Web scraping & crawling
|
|
735
|
-
• github — Code repos & developer data
|
|
736
|
-
• e2b — Code sandbox for AI agents
|
|
737
|
-
|
|
738
|
-
BROWSE:
|
|
739
|
-
list_categories()
|
|
740
|
-
list_all_apis({ category: "communication", limit: 20 })
|
|
741
|
-
|
|
742
|
-
Docs: https://apiclaw.nordsym.com
|
|
743
|
-
`;
|
|
744
|
-
return {
|
|
745
|
-
content: [{ type: 'text', text: helpText }]
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
case 'discover_apis': {
|
|
749
|
-
const query = args?.query;
|
|
750
|
-
const category = args?.category;
|
|
751
|
-
const maxResults = args?.max_results || 5;
|
|
752
|
-
const region = args?.region;
|
|
753
|
-
const subagentId = args?.subagent_id;
|
|
754
|
-
const aiBackend = args?.ai_backend;
|
|
755
|
-
const startTime = Date.now();
|
|
756
|
-
const results = discoverAPIs(query, { category, maxResults, region });
|
|
757
|
-
const responseTimeMs = Date.now() - startTime;
|
|
758
|
-
trackSearch(query, results.length, responseTimeMs);
|
|
759
|
-
// Log search to Convex analytics (authenticated + anonymous)
|
|
760
|
-
const analyticsUserId = workspaceContext?.workspaceId || `anon:${getMachineFingerprint()}`;
|
|
761
|
-
const convexUrl = process.env.APICLAW_CONVEX_URL || process.env.NEXT_PUBLIC_CONVEX_URL;
|
|
762
|
-
if (convexUrl) {
|
|
763
|
-
fetch(`${convexUrl}/api/mutation`, {
|
|
764
|
-
method: 'POST',
|
|
765
|
-
headers: { 'Content-Type': 'application/json' },
|
|
766
|
-
body: JSON.stringify({
|
|
767
|
-
path: 'analytics:log',
|
|
768
|
-
args: {
|
|
769
|
-
event: 'search_query',
|
|
770
|
-
provider: undefined,
|
|
771
|
-
query,
|
|
772
|
-
identifier: analyticsUserId,
|
|
773
|
-
metadata: {
|
|
774
|
-
resultCount: results.length,
|
|
775
|
-
matchedProviders: results.slice(0, 10).map(r => r.provider.id),
|
|
776
|
-
responseTimeMs,
|
|
777
|
-
category,
|
|
778
|
-
authenticated: !!workspaceContext,
|
|
779
|
-
},
|
|
780
|
-
},
|
|
781
|
-
}),
|
|
782
|
-
}).catch(() => { }); // Fire and forget
|
|
783
|
-
}
|
|
784
|
-
// Log search to searchLogs table (authenticated only - requires workspace)
|
|
785
|
-
if (workspaceContext?.sessionToken) {
|
|
786
|
-
const searchLogPayload = {
|
|
787
|
-
path: 'searchLogs:log',
|
|
788
|
-
args: {
|
|
789
|
-
sessionToken: workspaceContext.sessionToken,
|
|
790
|
-
subagentId: subagentId || undefined,
|
|
791
|
-
query,
|
|
792
|
-
resultCount: results.length,
|
|
793
|
-
matchedProviders: results.slice(0, 10).map(r => r.provider.id),
|
|
794
|
-
responseTimeMs,
|
|
795
|
-
},
|
|
796
|
-
};
|
|
797
|
-
fetch(`${convexUrl}/api/mutation`, {
|
|
798
|
-
method: 'POST',
|
|
799
|
-
headers: { 'Content-Type': 'application/json' },
|
|
800
|
-
body: JSON.stringify(searchLogPayload),
|
|
801
|
-
}).catch(() => { }); // Fire and forget
|
|
802
|
-
}
|
|
803
|
-
// Update AI backend tracking if provided
|
|
804
|
-
if (aiBackend && workspaceContext?.sessionToken) {
|
|
805
|
-
fetch('https://brilliant-puffin-712.eu-west-1.convex.cloud/api/mutation', {
|
|
806
|
-
method: 'POST',
|
|
807
|
-
headers: { 'Content-Type': 'application/json' },
|
|
808
|
-
body: JSON.stringify({
|
|
809
|
-
path: 'agents:updateAIBackend',
|
|
810
|
-
args: {
|
|
811
|
-
token: workspaceContext.sessionToken,
|
|
812
|
-
subagentId: subagentId || undefined,
|
|
813
|
-
aiBackend,
|
|
814
|
-
},
|
|
815
|
-
}),
|
|
816
|
-
}).catch(() => { }); // Fire and forget
|
|
817
|
-
}
|
|
818
|
-
if (results.length === 0) {
|
|
819
|
-
return {
|
|
820
|
-
content: [
|
|
821
|
-
{
|
|
822
|
-
type: 'text',
|
|
823
|
-
text: JSON.stringify({
|
|
824
|
-
status: 'no_results',
|
|
825
|
-
message: `No APIs found matching "${query}". Try broader terms or check available categories with list_categories.`,
|
|
826
|
-
available_categories: getCategories()
|
|
827
|
-
}, null, 2)
|
|
828
|
-
}
|
|
829
|
-
]
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
return {
|
|
833
|
-
content: [
|
|
834
|
-
{
|
|
835
|
-
type: 'text',
|
|
836
|
-
text: JSON.stringify({
|
|
837
|
-
status: 'success',
|
|
838
|
-
query,
|
|
839
|
-
results_count: results.length,
|
|
840
|
-
results: results.map(r => ({
|
|
841
|
-
id: r.provider.id,
|
|
842
|
-
name: r.provider.name,
|
|
843
|
-
description: r.provider.description,
|
|
844
|
-
category: r.provider.category,
|
|
845
|
-
capabilities: r.provider.capabilities,
|
|
846
|
-
pricing_model: r.provider.pricing.model,
|
|
847
|
-
has_free_tier: r.provider.pricing.free_tier,
|
|
848
|
-
agent_success_rate: r.provider.agent_success_rate,
|
|
849
|
-
relevance_score: r.relevance_score,
|
|
850
|
-
match_reasons: r.match_reasons
|
|
851
|
-
}))
|
|
852
|
-
}, null, 2)
|
|
853
|
-
}
|
|
854
|
-
]
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
case 'get_api_details': {
|
|
858
|
-
const apiId = args?.api_id;
|
|
859
|
-
const compact = args?.compact || false;
|
|
860
|
-
const api = getAPIDetails(apiId, { compact });
|
|
861
|
-
if (!api) {
|
|
862
|
-
return {
|
|
863
|
-
content: [
|
|
864
|
-
{
|
|
865
|
-
type: 'text',
|
|
866
|
-
text: JSON.stringify({
|
|
867
|
-
status: 'error',
|
|
868
|
-
message: `API not found: ${apiId}`,
|
|
869
|
-
hint: 'Try discover_apis to search, or list_connected for direct-call APIs',
|
|
870
|
-
}, null, 2)
|
|
871
|
-
}
|
|
872
|
-
]
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
// Compact mode: minimal JSON, no pretty-print
|
|
876
|
-
if (compact) {
|
|
877
|
-
return {
|
|
878
|
-
content: [
|
|
879
|
-
{
|
|
880
|
-
type: 'text',
|
|
881
|
-
text: JSON.stringify({ status: 'ok', ...api })
|
|
882
|
-
}
|
|
883
|
-
]
|
|
884
|
-
};
|
|
885
|
-
}
|
|
886
|
-
return {
|
|
887
|
-
content: [
|
|
888
|
-
{
|
|
889
|
-
type: 'text',
|
|
890
|
-
text: JSON.stringify({
|
|
891
|
-
status: 'success',
|
|
892
|
-
api
|
|
893
|
-
}, null, 2)
|
|
894
|
-
}
|
|
895
|
-
]
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
case 'purchase_access': {
|
|
899
|
-
const apiId = args?.api_id;
|
|
900
|
-
const amountUsd = args?.amount_usd;
|
|
901
|
-
const agentId = args?.agent_id || DEFAULT_AGENT_ID;
|
|
902
|
-
const result = purchaseAPIAccess(agentId, apiId, amountUsd);
|
|
903
|
-
if (!result.success) {
|
|
904
|
-
return {
|
|
905
|
-
content: [
|
|
906
|
-
{
|
|
907
|
-
type: 'text',
|
|
908
|
-
text: JSON.stringify({
|
|
909
|
-
status: 'error',
|
|
910
|
-
message: result.error
|
|
911
|
-
}, null, 2)
|
|
912
|
-
}
|
|
913
|
-
]
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
const api = getAPIDetails(apiId);
|
|
917
|
-
return {
|
|
918
|
-
content: [
|
|
919
|
-
{
|
|
920
|
-
type: 'text',
|
|
921
|
-
text: JSON.stringify({
|
|
922
|
-
status: 'success',
|
|
923
|
-
message: `Successfully purchased access to ${apiId}`,
|
|
924
|
-
purchase: {
|
|
925
|
-
id: result.purchase.id,
|
|
926
|
-
provider: apiId,
|
|
927
|
-
amount_paid_usd: amountUsd,
|
|
928
|
-
credits_received: result.purchase.credits_purchased,
|
|
929
|
-
status: result.purchase.status,
|
|
930
|
-
real_credentials: hasRealCredentials(apiId)
|
|
931
|
-
},
|
|
932
|
-
credentials: result.purchase.credentials,
|
|
933
|
-
access: {
|
|
934
|
-
base_url: api?.base_url,
|
|
935
|
-
docs_url: api?.docs_url,
|
|
936
|
-
auth_type: api?.auth_type
|
|
937
|
-
}
|
|
938
|
-
}, null, 2)
|
|
939
|
-
}
|
|
940
|
-
]
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
case 'check_balance': {
|
|
944
|
-
const agentId = args?.agent_id || DEFAULT_AGENT_ID;
|
|
945
|
-
const summary = getBalanceSummary(agentId);
|
|
946
|
-
return {
|
|
947
|
-
content: [
|
|
948
|
-
{
|
|
949
|
-
type: 'text',
|
|
950
|
-
text: JSON.stringify({
|
|
951
|
-
status: 'success',
|
|
952
|
-
agent_id: agentId,
|
|
953
|
-
balance_usd: summary.credits.balance_usd,
|
|
954
|
-
currency: summary.credits.currency,
|
|
955
|
-
total_spent_usd: summary.total_spent_usd,
|
|
956
|
-
real_credential_providers: summary.real_credentials_available,
|
|
957
|
-
active_purchases: summary.active_purchases.map(p => ({
|
|
958
|
-
id: p.id,
|
|
959
|
-
provider: p.provider_id,
|
|
960
|
-
credits_remaining: p.credits_purchased,
|
|
961
|
-
status: p.status,
|
|
962
|
-
real_credentials: hasRealCredentials(p.provider_id)
|
|
963
|
-
}))
|
|
964
|
-
}, null, 2)
|
|
965
|
-
}
|
|
966
|
-
]
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
case 'add_credits': {
|
|
970
|
-
const amountUsd = args?.amount_usd;
|
|
971
|
-
const agentId = args?.agent_id || DEFAULT_AGENT_ID;
|
|
972
|
-
const credits = addCredits(agentId, amountUsd);
|
|
973
|
-
return {
|
|
974
|
-
content: [
|
|
975
|
-
{
|
|
976
|
-
type: 'text',
|
|
977
|
-
text: JSON.stringify({
|
|
978
|
-
status: 'success',
|
|
979
|
-
message: `Added $${amountUsd.toFixed(2)} to your account`,
|
|
980
|
-
new_balance_usd: credits.balance_usd
|
|
981
|
-
}, null, 2)
|
|
982
|
-
}
|
|
983
|
-
]
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
case 'list_categories': {
|
|
987
|
-
const categories = getCategories();
|
|
988
|
-
const apisByCategory = {};
|
|
989
|
-
for (const cat of categories) {
|
|
990
|
-
apisByCategory[cat] = getAllAPIs()
|
|
991
|
-
.filter(a => a.category === cat)
|
|
992
|
-
.map(a => a.id);
|
|
993
|
-
}
|
|
994
|
-
return {
|
|
995
|
-
content: [
|
|
996
|
-
{
|
|
997
|
-
type: 'text',
|
|
998
|
-
text: JSON.stringify({
|
|
999
|
-
status: 'success',
|
|
1000
|
-
categories: apisByCategory
|
|
1001
|
-
}, null, 2)
|
|
1002
|
-
}
|
|
1003
|
-
]
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
case 'call_api': {
|
|
1007
|
-
const provider = args?.provider;
|
|
1008
|
-
const action = args?.action;
|
|
1009
|
-
const params = args?.params || {};
|
|
1010
|
-
const confirmToken = args?.confirm_token;
|
|
1011
|
-
const dryRun = args?.dry_run;
|
|
1012
|
-
const chain = args?.chain;
|
|
1013
|
-
const subagentId = args?.subagent_id;
|
|
1014
|
-
const aiBackend = args?.ai_backend;
|
|
1015
|
-
// Track AI backend if provided
|
|
1016
|
-
if (aiBackend && workspaceContext?.sessionToken) {
|
|
1017
|
-
fetch('https://brilliant-puffin-712.eu-west-1.convex.cloud/api/mutation', {
|
|
1018
|
-
method: 'POST',
|
|
1019
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1020
|
-
body: JSON.stringify({
|
|
1021
|
-
path: 'agents:updateAIBackend',
|
|
1022
|
-
args: {
|
|
1023
|
-
token: workspaceContext.sessionToken,
|
|
1024
|
-
subagentId: subagentId || undefined,
|
|
1025
|
-
aiBackend,
|
|
1026
|
-
},
|
|
1027
|
-
}),
|
|
1028
|
-
}).catch(() => { }); // Fire and forget
|
|
1029
|
-
}
|
|
1030
|
-
// ============================================
|
|
1031
|
-
// CHAIN EXECUTION MODE
|
|
1032
|
-
// ============================================
|
|
1033
|
-
if (chain && Array.isArray(chain) && chain.length > 0) {
|
|
1034
|
-
// Check workspace access for chains
|
|
1035
|
-
const access = checkWorkspaceAccess();
|
|
1036
|
-
if (!access.allowed) {
|
|
1037
|
-
// If error is already formatted JSON (from rate limit checks), return as-is
|
|
1038
|
-
if (access.error?.startsWith('{')) {
|
|
1039
|
-
return {
|
|
1040
|
-
content: [{
|
|
1041
|
-
type: 'text',
|
|
1042
|
-
text: access.error
|
|
1043
|
-
}],
|
|
1044
|
-
isError: true
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
// Otherwise, wrap in standard error format
|
|
1048
|
-
return {
|
|
1049
|
-
content: [{
|
|
1050
|
-
type: 'text',
|
|
1051
|
-
text: JSON.stringify({
|
|
1052
|
-
status: 'error',
|
|
1053
|
-
error: access.error,
|
|
1054
|
-
hint: 'Use register_owner to authenticate your workspace.',
|
|
1055
|
-
}, null, 2)
|
|
1056
|
-
}],
|
|
1057
|
-
isError: true
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
try {
|
|
1061
|
-
// Construct ChainDefinition from the input
|
|
1062
|
-
const chainDefinition = {
|
|
1063
|
-
steps: chain,
|
|
1064
|
-
timeout: args?.timeout,
|
|
1065
|
-
errorPolicy: args?.continueOnError
|
|
1066
|
-
? { mode: 'best-effort' }
|
|
1067
|
-
: { mode: 'fail-fast' },
|
|
1068
|
-
};
|
|
1069
|
-
const chainCredentials = {
|
|
1070
|
-
userId: DEFAULT_AGENT_ID,
|
|
1071
|
-
customerKeys: {},
|
|
1072
|
-
};
|
|
1073
|
-
// Add customer key if provided
|
|
1074
|
-
const customerKey = args?.customer_key;
|
|
1075
|
-
if (customerKey) {
|
|
1076
|
-
// Apply to all providers (or could be provider-specific)
|
|
1077
|
-
chainCredentials.customerKeys = { default: customerKey };
|
|
1078
|
-
}
|
|
1079
|
-
const chainOptions = {
|
|
1080
|
-
verbose: false,
|
|
1081
|
-
};
|
|
1082
|
-
// Execute the chain
|
|
1083
|
-
const chainResult = await executeChain(chainDefinition, chainCredentials, {}, // inputs
|
|
1084
|
-
chainOptions);
|
|
1085
|
-
// Track usage for chain (count completed steps)
|
|
1086
|
-
if (chainResult.success && workspaceContext) {
|
|
1087
|
-
const completedCount = chainResult.completedSteps.length;
|
|
1088
|
-
for (let i = 0; i < completedCount; i++) {
|
|
1089
|
-
try {
|
|
1090
|
-
await convex.mutation("workspaces:incrementUsage", {
|
|
1091
|
-
workspaceId: workspaceContext.workspaceId,
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
catch (e) {
|
|
1095
|
-
console.error('[APIClaw] Failed to track chain usage:', e);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
// Format response to match expected chain response format
|
|
1100
|
-
return {
|
|
1101
|
-
content: [{
|
|
1102
|
-
type: 'text',
|
|
1103
|
-
text: JSON.stringify({
|
|
1104
|
-
status: chainResult.success ? 'success' : 'error',
|
|
1105
|
-
mode: 'chain',
|
|
1106
|
-
chainId: chainResult.chainId,
|
|
1107
|
-
steps: chainResult.trace.map(t => ({
|
|
1108
|
-
id: t.stepId,
|
|
1109
|
-
status: t.success ? 'completed' : 'failed',
|
|
1110
|
-
result: t.output,
|
|
1111
|
-
error: t.error,
|
|
1112
|
-
latencyMs: t.latencyMs,
|
|
1113
|
-
cost: t.cost,
|
|
1114
|
-
})),
|
|
1115
|
-
finalResult: chainResult.finalResult,
|
|
1116
|
-
totalLatencyMs: chainResult.totalLatencyMs,
|
|
1117
|
-
totalCost: chainResult.totalCost,
|
|
1118
|
-
tokensSaved: (chain.length - 1) * 500, // Estimate tokens saved
|
|
1119
|
-
...(chainResult.error ? {
|
|
1120
|
-
completedSteps: chainResult.completedSteps,
|
|
1121
|
-
failedStep: chainResult.failedStep ? {
|
|
1122
|
-
id: chainResult.failedStep.stepId,
|
|
1123
|
-
error: chainResult.failedStep.error,
|
|
1124
|
-
code: chainResult.failedStep.errorCode,
|
|
1125
|
-
} : undefined,
|
|
1126
|
-
partialResults: chainResult.results,
|
|
1127
|
-
canResume: chainResult.canResume,
|
|
1128
|
-
resumeToken: chainResult.resumeToken,
|
|
1129
|
-
} : {}),
|
|
1130
|
-
}, null, 2)
|
|
1131
|
-
}],
|
|
1132
|
-
isError: !chainResult.success
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
catch (error) {
|
|
1136
|
-
return {
|
|
1137
|
-
content: [{
|
|
1138
|
-
type: 'text',
|
|
1139
|
-
text: JSON.stringify({
|
|
1140
|
-
status: 'error',
|
|
1141
|
-
mode: 'chain',
|
|
1142
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1143
|
-
}, null, 2)
|
|
1144
|
-
}],
|
|
1145
|
-
isError: true
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
// ============================================
|
|
1150
|
-
// SINGLE CALL MODE (existing logic)
|
|
1151
|
-
// ============================================
|
|
1152
|
-
// Handle dry-run mode - no actual API calls, just show what would happen
|
|
1153
|
-
if (dryRun) {
|
|
1154
|
-
const { generateDryRun } = await import('./execute.js');
|
|
1155
|
-
const dryRunResult = generateDryRun(provider, action, params);
|
|
1156
|
-
return {
|
|
1157
|
-
content: [{
|
|
1158
|
-
type: 'text',
|
|
1159
|
-
text: JSON.stringify(dryRunResult, null, 2)
|
|
1160
|
-
}]
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
// Check workspace access (skip for free/open APIs)
|
|
1164
|
-
const isFreeAPI = isOpenAPI(provider);
|
|
1165
|
-
if (!isFreeAPI) {
|
|
1166
|
-
const access = checkWorkspaceAccess(provider);
|
|
1167
|
-
if (!access.allowed) {
|
|
1168
|
-
return {
|
|
1169
|
-
content: [{
|
|
1170
|
-
type: 'text',
|
|
1171
|
-
text: JSON.stringify({
|
|
1172
|
-
status: 'error',
|
|
1173
|
-
error: access.error,
|
|
1174
|
-
hint: access.isAnonymous
|
|
1175
|
-
? 'Rate limit reached. Use register_owner to authenticate for higher limits.'
|
|
1176
|
-
: 'Use register_owner to authenticate your workspace.',
|
|
1177
|
-
}, null, 2)
|
|
1178
|
-
}],
|
|
1179
|
-
isError: true
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
const startTime = Date.now();
|
|
1184
|
-
let result;
|
|
1185
|
-
let apiType;
|
|
1186
|
-
// Check if this is a confirmation of a pending action
|
|
1187
|
-
if (confirmToken) {
|
|
1188
|
-
const pending = consumePendingAction(confirmToken);
|
|
1189
|
-
if (!pending) {
|
|
1190
|
-
return {
|
|
1191
|
-
content: [{
|
|
1192
|
-
type: 'text',
|
|
1193
|
-
text: JSON.stringify({
|
|
1194
|
-
status: 'error',
|
|
1195
|
-
error: 'Invalid or expired confirmation token. Please start over.',
|
|
1196
|
-
}, null, 2)
|
|
1197
|
-
}],
|
|
1198
|
-
isError: true
|
|
1199
|
-
};
|
|
1200
|
-
}
|
|
1201
|
-
// Execute the confirmed action with metered billing
|
|
1202
|
-
apiType = 'direct';
|
|
1203
|
-
const customerKey = args?.customer_key || getCustomerKey(pending.provider);
|
|
1204
|
-
const stripeCustomerId = args?.stripe_customer_id || process.env.APICLAW_STRIPE_CUSTOMER_ID;
|
|
1205
|
-
result = await executeMetered(pending.provider, pending.action, pending.params, {
|
|
1206
|
-
customerId: stripeCustomerId,
|
|
1207
|
-
customerKey,
|
|
1208
|
-
userId: DEFAULT_AGENT_ID,
|
|
1209
|
-
});
|
|
1210
|
-
// Log the confirmed API call (with fingerprint for anonymous users)
|
|
1211
|
-
const analyticsUserId = workspaceContext
|
|
1212
|
-
? workspaceContext.workspaceId
|
|
1213
|
-
: `anon:${getMachineFingerprint()}`;
|
|
1214
|
-
logAPICall({
|
|
1215
|
-
timestamp: new Date().toISOString(),
|
|
1216
|
-
provider: pending.provider,
|
|
1217
|
-
action: pending.action,
|
|
1218
|
-
type: apiType,
|
|
1219
|
-
userId: analyticsUserId,
|
|
1220
|
-
success: result.success,
|
|
1221
|
-
latencyMs: Date.now() - startTime,
|
|
1222
|
-
error: result.error,
|
|
1223
|
-
});
|
|
1224
|
-
// Track earn progress for confirmed actions
|
|
1225
|
-
if (result.success && workspaceContext) {
|
|
1226
|
-
await trackEarnProgress(workspaceContext.workspaceId, pending.provider, pending.action);
|
|
1227
|
-
}
|
|
1228
|
-
return {
|
|
1229
|
-
content: [{
|
|
1230
|
-
type: 'text',
|
|
1231
|
-
text: JSON.stringify({
|
|
1232
|
-
status: result.success ? 'success' : 'error',
|
|
1233
|
-
provider: result.provider,
|
|
1234
|
-
action: result.action,
|
|
1235
|
-
confirmed: true,
|
|
1236
|
-
...(result.success ? { data: result.data } : { error: result.error }),
|
|
1237
|
-
}, null, 2)
|
|
1238
|
-
}],
|
|
1239
|
-
isError: !result.success
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
// Check if this action requires confirmation (both hardcoded and dynamic providers)
|
|
1243
|
-
const confirmCheck = await requiresConfirmationAsync(provider, action);
|
|
1244
|
-
if (confirmCheck.required) {
|
|
1245
|
-
// Validate params first (for hardcoded providers)
|
|
1246
|
-
if (!confirmCheck.isDynamic) {
|
|
1247
|
-
const validation = validateParams(provider, action, params);
|
|
1248
|
-
if (!validation.valid) {
|
|
1249
|
-
return {
|
|
1250
|
-
content: [{
|
|
1251
|
-
type: 'text',
|
|
1252
|
-
text: JSON.stringify({
|
|
1253
|
-
status: 'error',
|
|
1254
|
-
error: 'Validation failed',
|
|
1255
|
-
missing_or_invalid: validation.errors,
|
|
1256
|
-
hint: 'Please provide all required fields before sending.',
|
|
1257
|
-
}, null, 2)
|
|
1258
|
-
}],
|
|
1259
|
-
isError: true
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
// Generate preview and create pending action
|
|
1264
|
-
const preview = generatePreview(provider, action, params);
|
|
1265
|
-
if (confirmCheck.estimatedCost) {
|
|
1266
|
-
preview.estimated_cost = confirmCheck.estimatedCost;
|
|
1267
|
-
}
|
|
1268
|
-
const pending = createPendingAction(provider, action, params, preview, DEFAULT_AGENT_ID);
|
|
1269
|
-
return {
|
|
1270
|
-
content: [{
|
|
1271
|
-
type: 'text',
|
|
1272
|
-
text: JSON.stringify({
|
|
1273
|
-
status: 'requires_confirmation',
|
|
1274
|
-
message: '⚠️ This action costs money. Please review and confirm.',
|
|
1275
|
-
preview,
|
|
1276
|
-
confirm_token: pending.token,
|
|
1277
|
-
expires_in_seconds: 300,
|
|
1278
|
-
how_to_confirm: `Call again with confirm_token: "${pending.token}"`,
|
|
1279
|
-
}, null, 2)
|
|
1280
|
-
}]
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
// Regular execution (no confirmation needed)
|
|
1284
|
-
if (isOpenAPI(provider)) {
|
|
1285
|
-
apiType = 'open';
|
|
1286
|
-
result = await executeOpenAPI(provider, action, params);
|
|
1287
|
-
}
|
|
1288
|
-
else {
|
|
1289
|
-
apiType = 'direct';
|
|
1290
|
-
const customerKey = args?.customer_key || getCustomerKey(provider);
|
|
1291
|
-
const stripeCustomerId = args?.stripe_customer_id || process.env.APICLAW_STRIPE_CUSTOMER_ID;
|
|
1292
|
-
result = await executeMetered(provider, action, params, {
|
|
1293
|
-
customerId: stripeCustomerId,
|
|
1294
|
-
customerKey,
|
|
1295
|
-
userId: DEFAULT_AGENT_ID,
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
|
-
// Log the API call for analytics (with fingerprint for anonymous users)
|
|
1299
|
-
const analyticsUserId = workspaceContext
|
|
1300
|
-
? workspaceContext.workspaceId
|
|
1301
|
-
: `anon:${getMachineFingerprint()}`;
|
|
1302
|
-
logAPICall({
|
|
1303
|
-
timestamp: new Date().toISOString(),
|
|
1304
|
-
provider,
|
|
1305
|
-
action,
|
|
1306
|
-
type: apiType,
|
|
1307
|
-
userId: analyticsUserId,
|
|
1308
|
-
success: result.success,
|
|
1309
|
-
latencyMs: Date.now() - startTime,
|
|
1310
|
-
error: result.error,
|
|
1311
|
-
});
|
|
1312
|
-
// Increment usage for workspace (non-free APIs only)
|
|
1313
|
-
if (result.success && workspaceContext && !isFreeAPI) {
|
|
1314
|
-
try {
|
|
1315
|
-
const usageResult = await convex.mutation("workspaces:incrementUsage", {
|
|
1316
|
-
workspaceId: workspaceContext.workspaceId,
|
|
1317
|
-
});
|
|
1318
|
-
if (usageResult.success) {
|
|
1319
|
-
workspaceContext.usageRemaining = usageResult.remaining ?? -1;
|
|
1320
|
-
}
|
|
1321
|
-
// Track earn progress (first direct call + unique APIs)
|
|
1322
|
-
await trackEarnProgress(workspaceContext.workspaceId, provider, action);
|
|
1323
|
-
}
|
|
1324
|
-
catch (e) {
|
|
1325
|
-
console.error('[APIClaw] Failed to track usage:', e);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
return {
|
|
1329
|
-
content: [
|
|
1330
|
-
{
|
|
1331
|
-
type: 'text',
|
|
1332
|
-
text: JSON.stringify({
|
|
1333
|
-
status: result.success ? 'success' : 'error',
|
|
1334
|
-
provider: result.provider,
|
|
1335
|
-
action: result.action,
|
|
1336
|
-
type: apiType,
|
|
1337
|
-
...(result.success ? { data: result.data } : { error: result.error }),
|
|
1338
|
-
...(result.cost !== undefined ? { cost_sek: result.cost } : {})
|
|
1339
|
-
}, null, 2)
|
|
1340
|
-
}
|
|
1341
|
-
],
|
|
1342
|
-
isError: !result.success
|
|
1343
|
-
};
|
|
1344
|
-
}
|
|
1345
|
-
case 'list_connected': {
|
|
1346
|
-
const directProviders = getConnectedProviders();
|
|
1347
|
-
const openProviders = listOpenAPIs();
|
|
1348
|
-
return {
|
|
1349
|
-
content: [
|
|
1350
|
-
{
|
|
1351
|
-
type: 'text',
|
|
1352
|
-
text: JSON.stringify({
|
|
1353
|
-
status: 'success',
|
|
1354
|
-
message: 'These APIs are available via call_api - no API key needed!',
|
|
1355
|
-
direct_call: {
|
|
1356
|
-
description: 'APIs where we handle authentication',
|
|
1357
|
-
providers: directProviders,
|
|
1358
|
-
},
|
|
1359
|
-
open_apis: {
|
|
1360
|
-
description: 'Free, open APIs (no auth required)',
|
|
1361
|
-
providers: openProviders,
|
|
1362
|
-
},
|
|
1363
|
-
usage: 'Use call_api with provider, action, and params to execute calls.'
|
|
1364
|
-
}, null, 2)
|
|
1365
|
-
}
|
|
1366
|
-
]
|
|
1367
|
-
};
|
|
1368
|
-
}
|
|
1369
|
-
case 'capability': {
|
|
1370
|
-
const capabilityId = args?.capability;
|
|
1371
|
-
const action = args?.action;
|
|
1372
|
-
const params = args?.params || {};
|
|
1373
|
-
const preferences = args?.preferences || {};
|
|
1374
|
-
const subagentId = args?.subagent_id;
|
|
1375
|
-
const aiBackend = args?.ai_backend;
|
|
1376
|
-
// Track AI backend if provided
|
|
1377
|
-
if (aiBackend && workspaceContext?.sessionToken) {
|
|
1378
|
-
fetch('https://brilliant-puffin-712.eu-west-1.convex.cloud/api/mutation', {
|
|
1379
|
-
method: 'POST',
|
|
1380
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1381
|
-
body: JSON.stringify({
|
|
1382
|
-
path: 'agents:updateAIBackend',
|
|
1383
|
-
args: {
|
|
1384
|
-
token: workspaceContext.sessionToken,
|
|
1385
|
-
subagentId: subagentId || undefined,
|
|
1386
|
-
aiBackend,
|
|
1387
|
-
},
|
|
1388
|
-
}),
|
|
1389
|
-
}).catch(() => { }); // Fire and forget
|
|
1390
|
-
}
|
|
1391
|
-
// Check if capability exists
|
|
1392
|
-
const exists = await hasCapability(capabilityId);
|
|
1393
|
-
if (!exists) {
|
|
1394
|
-
// Try to help with available capabilities
|
|
1395
|
-
const available = await listCapabilities();
|
|
1396
|
-
return {
|
|
1397
|
-
content: [{
|
|
1398
|
-
type: 'text',
|
|
1399
|
-
text: JSON.stringify({
|
|
1400
|
-
status: 'error',
|
|
1401
|
-
error: `Unknown capability: ${capabilityId}`,
|
|
1402
|
-
available_capabilities: available.map(c => c.id),
|
|
1403
|
-
hint: 'Use list_capabilities to see all available capabilities.'
|
|
1404
|
-
}, null, 2)
|
|
1405
|
-
}],
|
|
1406
|
-
isError: true
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
// Execute capability
|
|
1410
|
-
const result = await executeCapability(capabilityId, action, params, DEFAULT_AGENT_ID, preferences);
|
|
1411
|
-
return {
|
|
1412
|
-
content: [{
|
|
1413
|
-
type: 'text',
|
|
1414
|
-
text: JSON.stringify({
|
|
1415
|
-
status: result.success ? 'success' : 'error',
|
|
1416
|
-
capability: result.capability,
|
|
1417
|
-
action: result.action,
|
|
1418
|
-
provider_used: result.providerUsed,
|
|
1419
|
-
fallback_attempted: result.fallbackAttempted,
|
|
1420
|
-
...(result.fallbackReason ? { fallback_reason: result.fallbackReason } : {}),
|
|
1421
|
-
...(result.success ? { data: result.data } : { error: result.error }),
|
|
1422
|
-
...(result.cost !== undefined ? { cost: result.cost, currency: result.currency } : {}),
|
|
1423
|
-
latency_ms: result.latencyMs,
|
|
1424
|
-
}, null, 2)
|
|
1425
|
-
}],
|
|
1426
|
-
isError: !result.success
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
case 'list_capabilities': {
|
|
1430
|
-
const capabilities = await listCapabilities();
|
|
1431
|
-
return {
|
|
1432
|
-
content: [{
|
|
1433
|
-
type: 'text',
|
|
1434
|
-
text: JSON.stringify({
|
|
1435
|
-
status: 'success',
|
|
1436
|
-
message: 'Available capabilities - use capability() to execute',
|
|
1437
|
-
capabilities,
|
|
1438
|
-
usage: 'capability("sms", "send", {to: "+46...", message: "Hello"})'
|
|
1439
|
-
}, null, 2)
|
|
1440
|
-
}]
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1443
|
-
// ============================================
|
|
1444
|
-
// WORKSPACE TOOLS
|
|
1445
|
-
// ============================================
|
|
1446
|
-
case 'register_owner': {
|
|
1447
|
-
const email = args?.email;
|
|
1448
|
-
if (!email || !email.includes('@')) {
|
|
1449
|
-
return {
|
|
1450
|
-
content: [{
|
|
1451
|
-
type: 'text',
|
|
1452
|
-
text: JSON.stringify({
|
|
1453
|
-
status: 'error',
|
|
1454
|
-
error: 'Invalid email address',
|
|
1455
|
-
}, null, 2)
|
|
1456
|
-
}],
|
|
1457
|
-
isError: true
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
try {
|
|
1461
|
-
// Check if workspace already exists
|
|
1462
|
-
const existing = await convex.query("workspaces:getByEmail", { email });
|
|
1463
|
-
if (existing && existing.status === 'active') {
|
|
1464
|
-
// Workspace exists and is active - create session directly
|
|
1465
|
-
const fingerprint = getMachineFingerprint();
|
|
1466
|
-
const sessionResult = await convex.mutation("workspaces:createAgentSession", {
|
|
1467
|
-
workspaceId: existing.id,
|
|
1468
|
-
fingerprint,
|
|
1469
|
-
});
|
|
1470
|
-
if (sessionResult.success) {
|
|
1471
|
-
writeSession(sessionResult.sessionToken, existing.id, email);
|
|
1472
|
-
// Claim anonymous usage history
|
|
1473
|
-
try {
|
|
1474
|
-
const claimResult = await convex.mutation("workspaces:claimAnonymousUsage", {
|
|
1475
|
-
workspaceId: existing.id,
|
|
1476
|
-
machineFingerprint: fingerprint,
|
|
1477
|
-
});
|
|
1478
|
-
if (claimResult.success && claimResult.claimedCount) {
|
|
1479
|
-
console.error(`[APIClaw] ✓ Claimed ${claimResult.claimedCount} anonymous usage records`);
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
catch (err) {
|
|
1483
|
-
// Non-critical error, just log it
|
|
1484
|
-
console.error('[APIClaw] Warning: Failed to claim anonymous usage:', err);
|
|
1485
|
-
}
|
|
1486
|
-
// Update global context
|
|
1487
|
-
workspaceContext = {
|
|
1488
|
-
sessionToken: sessionResult.sessionToken,
|
|
1489
|
-
workspaceId: existing.id,
|
|
1490
|
-
email,
|
|
1491
|
-
tier: existing.tier,
|
|
1492
|
-
usageRemaining: existing.usageLimit - existing.usageCount,
|
|
1493
|
-
status: existing.status,
|
|
1494
|
-
};
|
|
1495
|
-
return {
|
|
1496
|
-
content: [{
|
|
1497
|
-
type: 'text',
|
|
1498
|
-
text: JSON.stringify({
|
|
1499
|
-
status: 'success',
|
|
1500
|
-
message: `Welcome back! Authenticated as ${email}`,
|
|
1501
|
-
workspace: {
|
|
1502
|
-
email,
|
|
1503
|
-
tier: existing.tier,
|
|
1504
|
-
usageCount: existing.usageCount,
|
|
1505
|
-
usageLimit: existing.usageLimit,
|
|
1506
|
-
},
|
|
1507
|
-
}, null, 2)
|
|
1508
|
-
}]
|
|
1509
|
-
};
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
// Create workspace and magic link
|
|
1513
|
-
const createResult = await convex.mutation("workspaces:createWorkspace", { email });
|
|
1514
|
-
let workspaceId;
|
|
1515
|
-
if (createResult.success) {
|
|
1516
|
-
workspaceId = createResult.workspaceId;
|
|
1517
|
-
}
|
|
1518
|
-
else if (createResult.error === 'workspace_exists') {
|
|
1519
|
-
workspaceId = createResult.workspaceId;
|
|
1520
|
-
}
|
|
1521
|
-
else {
|
|
1522
|
-
throw new Error(createResult.error);
|
|
1523
|
-
}
|
|
1524
|
-
// Create magic link
|
|
1525
|
-
const fingerprint = getMachineFingerprint();
|
|
1526
|
-
const magicLinkResult = await convex.mutation("workspaces:createMagicLink", {
|
|
1527
|
-
email,
|
|
1528
|
-
fingerprint,
|
|
1529
|
-
});
|
|
1530
|
-
// Send magic link via email
|
|
1531
|
-
const verifyUrl = `https://apiclaw.nordsym.com/verify?token=${magicLinkResult.token}`;
|
|
1532
|
-
const emailResponse = await fetch('https://api.resend.com/emails', {
|
|
1533
|
-
method: 'POST',
|
|
1534
|
-
headers: {
|
|
1535
|
-
'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
|
|
1536
|
-
'Content-Type': 'application/json',
|
|
1537
|
-
},
|
|
1538
|
-
body: JSON.stringify({
|
|
1539
|
-
from: 'APIClaw <noreply@apiclaw.nordsym.com>',
|
|
1540
|
-
to: email,
|
|
1541
|
-
subject: 'Verify your APIClaw workspace',
|
|
1542
|
-
html: `<p>Click to verify: <a href="${verifyUrl}">${verifyUrl}</a></p><p>Expires in 15 minutes.</p>`
|
|
1543
|
-
})
|
|
1544
|
-
});
|
|
1545
|
-
if (!emailResponse.ok) {
|
|
1546
|
-
const errorData = await emailResponse.text();
|
|
1547
|
-
throw new Error(`Failed to send verification email: ${errorData}`);
|
|
1548
|
-
}
|
|
1549
|
-
return {
|
|
1550
|
-
content: [{
|
|
1551
|
-
type: 'text',
|
|
1552
|
-
text: JSON.stringify({
|
|
1553
|
-
status: 'pending_verification',
|
|
1554
|
-
message: 'Workspace created! Check your email for verification link.',
|
|
1555
|
-
email,
|
|
1556
|
-
expires_in_minutes: 15,
|
|
1557
|
-
next_step: 'Check your email, click the verification link, then run check_workspace_status',
|
|
1558
|
-
}, null, 2)
|
|
1559
|
-
}]
|
|
1560
|
-
};
|
|
1561
|
-
}
|
|
1562
|
-
catch (error) {
|
|
1563
|
-
return {
|
|
1564
|
-
content: [{
|
|
1565
|
-
type: 'text',
|
|
1566
|
-
text: JSON.stringify({
|
|
1567
|
-
status: 'error',
|
|
1568
|
-
error: error instanceof Error ? error.message : 'Registration failed',
|
|
1569
|
-
}, null, 2)
|
|
1570
|
-
}],
|
|
1571
|
-
isError: true
|
|
1572
|
-
};
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
case 'check_workspace_status': {
|
|
1576
|
-
// Check if we have a local session
|
|
1577
|
-
const session = readSession();
|
|
1578
|
-
if (!session) {
|
|
1579
|
-
return {
|
|
1580
|
-
content: [{
|
|
1581
|
-
type: 'text',
|
|
1582
|
-
text: JSON.stringify({
|
|
1583
|
-
status: 'not_authenticated',
|
|
1584
|
-
message: 'No active session. Use register_owner to authenticate.',
|
|
1585
|
-
}, null, 2)
|
|
1586
|
-
}]
|
|
1587
|
-
};
|
|
1588
|
-
}
|
|
1589
|
-
try {
|
|
1590
|
-
const result = await convex.query("workspaces:getWorkspaceStatus", {
|
|
1591
|
-
sessionToken: session.sessionToken,
|
|
1592
|
-
});
|
|
1593
|
-
if (!result.authenticated) {
|
|
1594
|
-
clearSession();
|
|
1595
|
-
workspaceContext = null;
|
|
1596
|
-
return {
|
|
1597
|
-
content: [{
|
|
1598
|
-
type: 'text',
|
|
1599
|
-
text: JSON.stringify({
|
|
1600
|
-
status: 'session_expired',
|
|
1601
|
-
message: 'Session expired. Use register_owner to re-authenticate.',
|
|
1602
|
-
}, null, 2)
|
|
1603
|
-
}]
|
|
1604
|
-
};
|
|
1605
|
-
}
|
|
1606
|
-
// Update global context
|
|
1607
|
-
workspaceContext = {
|
|
1608
|
-
sessionToken: session.sessionToken,
|
|
1609
|
-
workspaceId: session.workspaceId,
|
|
1610
|
-
email: result.email ?? '',
|
|
1611
|
-
tier: result.tier ?? 'free',
|
|
1612
|
-
usageRemaining: result.usageRemaining ?? 0,
|
|
1613
|
-
status: result.status ?? 'unknown',
|
|
1614
|
-
};
|
|
1615
|
-
return {
|
|
1616
|
-
content: [{
|
|
1617
|
-
type: 'text',
|
|
1618
|
-
text: JSON.stringify({
|
|
1619
|
-
status: 'success',
|
|
1620
|
-
workspace: {
|
|
1621
|
-
email: result.email,
|
|
1622
|
-
status: result.status,
|
|
1623
|
-
tier: result.tier,
|
|
1624
|
-
usage: {
|
|
1625
|
-
count: result.usageCount,
|
|
1626
|
-
limit: result.usageLimit === -1 ? 'unlimited' : result.usageLimit,
|
|
1627
|
-
remaining: result.usageRemaining === -1 ? 'unlimited' : result.usageRemaining,
|
|
1628
|
-
},
|
|
1629
|
-
hasStripe: result.hasStripe,
|
|
1630
|
-
createdAt: result.createdAt ? new Date(result.createdAt).toISOString() : undefined,
|
|
1631
|
-
},
|
|
1632
|
-
}, null, 2)
|
|
1633
|
-
}]
|
|
1634
|
-
};
|
|
1635
|
-
}
|
|
1636
|
-
catch (error) {
|
|
1637
|
-
return {
|
|
1638
|
-
content: [{
|
|
1639
|
-
type: 'text',
|
|
1640
|
-
text: JSON.stringify({
|
|
1641
|
-
status: 'error',
|
|
1642
|
-
error: error instanceof Error ? error.message : 'Failed to check status',
|
|
1643
|
-
}, null, 2)
|
|
1644
|
-
}],
|
|
1645
|
-
isError: true
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
case 'remind_owner': {
|
|
1650
|
-
const session = readSession();
|
|
1651
|
-
if (!session) {
|
|
1652
|
-
return {
|
|
1653
|
-
content: [{
|
|
1654
|
-
type: 'text',
|
|
1655
|
-
text: JSON.stringify({
|
|
1656
|
-
status: 'error',
|
|
1657
|
-
error: 'No workspace found. Use register_owner first.',
|
|
1658
|
-
}, null, 2)
|
|
1659
|
-
}],
|
|
1660
|
-
isError: true
|
|
1661
|
-
};
|
|
1662
|
-
}
|
|
1663
|
-
try {
|
|
1664
|
-
// Check current status
|
|
1665
|
-
const result = await convex.query("workspaces:getWorkspaceStatus", {
|
|
1666
|
-
sessionToken: session.sessionToken,
|
|
1667
|
-
});
|
|
1668
|
-
if (result.authenticated && result.status === 'active') {
|
|
1669
|
-
return {
|
|
1670
|
-
content: [{
|
|
1671
|
-
type: 'text',
|
|
1672
|
-
text: JSON.stringify({
|
|
1673
|
-
status: 'already_verified',
|
|
1674
|
-
message: 'Workspace is already verified and active!',
|
|
1675
|
-
email: result.email,
|
|
1676
|
-
}, null, 2)
|
|
1677
|
-
}]
|
|
1678
|
-
};
|
|
1679
|
-
}
|
|
1680
|
-
// Create new magic link
|
|
1681
|
-
const fingerprint = getMachineFingerprint();
|
|
1682
|
-
const magicLinkResult = await convex.mutation("workspaces:createMagicLink", {
|
|
1683
|
-
email: session.email,
|
|
1684
|
-
fingerprint,
|
|
1685
|
-
});
|
|
1686
|
-
// TODO: Agent 2 will implement actual email sending
|
|
1687
|
-
const verifyUrl = `https://apiclaw.nordsym.com/verify?token=${magicLinkResult.token}`;
|
|
1688
|
-
return {
|
|
1689
|
-
content: [{
|
|
1690
|
-
type: 'text',
|
|
1691
|
-
text: JSON.stringify({
|
|
1692
|
-
status: 'reminder_sent',
|
|
1693
|
-
message: 'New verification link created.',
|
|
1694
|
-
email: session.email,
|
|
1695
|
-
verification_url: verifyUrl,
|
|
1696
|
-
expires_in_minutes: 15,
|
|
1697
|
-
note: 'Email sending will be implemented by Agent 2',
|
|
1698
|
-
}, null, 2)
|
|
1699
|
-
}]
|
|
1700
|
-
};
|
|
1701
|
-
}
|
|
1702
|
-
catch (error) {
|
|
1703
|
-
return {
|
|
1704
|
-
content: [{
|
|
1705
|
-
type: 'text',
|
|
1706
|
-
text: JSON.stringify({
|
|
1707
|
-
status: 'error',
|
|
1708
|
-
error: error instanceof Error ? error.message : 'Failed to send reminder',
|
|
1709
|
-
}, null, 2)
|
|
1710
|
-
}],
|
|
1711
|
-
isError: true
|
|
1712
|
-
};
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
// Metered Billing Tools
|
|
1716
|
-
case 'setup_metered_billing': {
|
|
1717
|
-
const { email, success_url, cancel_url } = args;
|
|
1718
|
-
if (!email) {
|
|
1719
|
-
return {
|
|
1720
|
-
content: [{
|
|
1721
|
-
type: 'text',
|
|
1722
|
-
text: JSON.stringify({ status: 'error', error: 'Email is required' }, null, 2)
|
|
1723
|
-
}],
|
|
1724
|
-
isError: true
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
|
-
// Create or get customer
|
|
1728
|
-
const customerResult = await getOrCreateCustomer(email, email);
|
|
1729
|
-
if ('error' in customerResult) {
|
|
1730
|
-
return {
|
|
1731
|
-
content: [{
|
|
1732
|
-
type: 'text',
|
|
1733
|
-
text: JSON.stringify({ status: 'error', error: customerResult.error }, null, 2)
|
|
1734
|
-
}],
|
|
1735
|
-
isError: true
|
|
1736
|
-
};
|
|
1737
|
-
}
|
|
1738
|
-
// Create checkout session for metered subscription
|
|
1739
|
-
const checkoutResult = await createMeteredCheckoutSession(email, success_url || 'https://apiclaw.nordsym.com/billing/success', cancel_url || 'https://apiclaw.nordsym.com/billing/cancel');
|
|
1740
|
-
if ('error' in checkoutResult) {
|
|
1741
|
-
return {
|
|
1742
|
-
content: [{
|
|
1743
|
-
type: 'text',
|
|
1744
|
-
text: JSON.stringify({ status: 'error', error: checkoutResult.error }, null, 2)
|
|
1745
|
-
}],
|
|
1746
|
-
isError: true
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
return {
|
|
1750
|
-
content: [{
|
|
1751
|
-
type: 'text',
|
|
1752
|
-
text: JSON.stringify({
|
|
1753
|
-
status: 'checkout_ready',
|
|
1754
|
-
message: 'Complete checkout to activate pay-per-call billing',
|
|
1755
|
-
checkout_url: checkoutResult.url,
|
|
1756
|
-
session_id: checkoutResult.sessionId,
|
|
1757
|
-
customer_id: customerResult.customerId,
|
|
1758
|
-
pricing: {
|
|
1759
|
-
per_call: '$0.002',
|
|
1760
|
-
billing_period: 'monthly',
|
|
1761
|
-
billed_at: 'end of period based on usage'
|
|
1762
|
-
}
|
|
1763
|
-
}, null, 2)
|
|
1764
|
-
}]
|
|
1765
|
-
};
|
|
1766
|
-
}
|
|
1767
|
-
case 'get_usage_summary': {
|
|
1768
|
-
const { subscription_id } = args;
|
|
1769
|
-
if (!subscription_id) {
|
|
1770
|
-
return {
|
|
1771
|
-
content: [{
|
|
1772
|
-
type: 'text',
|
|
1773
|
-
text: JSON.stringify({ status: 'error', error: 'subscription_id is required' }, null, 2)
|
|
1774
|
-
}],
|
|
1775
|
-
isError: true
|
|
1776
|
-
};
|
|
1777
|
-
}
|
|
1778
|
-
const usage = await getUsageSummary(subscription_id);
|
|
1779
|
-
if ('error' in usage) {
|
|
1780
|
-
return {
|
|
1781
|
-
content: [{
|
|
1782
|
-
type: 'text',
|
|
1783
|
-
text: JSON.stringify({ status: 'error', error: usage.error }, null, 2)
|
|
1784
|
-
}],
|
|
1785
|
-
isError: true
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
return {
|
|
1789
|
-
content: [{
|
|
1790
|
-
type: 'text',
|
|
1791
|
-
text: JSON.stringify({
|
|
1792
|
-
status: 'success',
|
|
1793
|
-
billing_period: {
|
|
1794
|
-
start: new Date(usage.period.start * 1000).toISOString(),
|
|
1795
|
-
end: new Date(usage.period.end * 1000).toISOString()
|
|
1796
|
-
},
|
|
1797
|
-
usage: {
|
|
1798
|
-
total_calls: usage.totalCalls,
|
|
1799
|
-
price_per_call: METERED_BILLING.pricePerCall,
|
|
1800
|
-
estimated_cost: `$${usage.totalCost.toFixed(4)}`
|
|
1801
|
-
}
|
|
1802
|
-
}, null, 2)
|
|
1803
|
-
}]
|
|
1804
|
-
};
|
|
1805
|
-
}
|
|
1806
|
-
case 'estimate_cost': {
|
|
1807
|
-
const { call_count } = args;
|
|
1808
|
-
if (!call_count || call_count < 0) {
|
|
1809
|
-
return {
|
|
1810
|
-
content: [{
|
|
1811
|
-
type: 'text',
|
|
1812
|
-
text: JSON.stringify({ status: 'error', error: 'Valid call_count is required' }, null, 2)
|
|
1813
|
-
}],
|
|
1814
|
-
isError: true
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
const estimate = estimateCost(call_count);
|
|
1818
|
-
return {
|
|
1819
|
-
content: [{
|
|
1820
|
-
type: 'text',
|
|
1821
|
-
text: JSON.stringify({
|
|
1822
|
-
status: 'success',
|
|
1823
|
-
estimate: {
|
|
1824
|
-
calls: estimate.calls,
|
|
1825
|
-
price_per_call: `$${estimate.pricePerCall}`,
|
|
1826
|
-
total_cost: `$${estimate.totalCost.toFixed(4)}`,
|
|
1827
|
-
currency: estimate.currency
|
|
1828
|
-
},
|
|
1829
|
-
examples: {
|
|
1830
|
-
'100 calls': `$${(100 * METERED_BILLING.pricePerCall).toFixed(2)}`,
|
|
1831
|
-
'1,000 calls': `$${(1000 * METERED_BILLING.pricePerCall).toFixed(2)}`,
|
|
1832
|
-
'10,000 calls': `$${(10000 * METERED_BILLING.pricePerCall).toFixed(2)}`
|
|
1833
|
-
}
|
|
1834
|
-
}, null, 2)
|
|
1835
|
-
}]
|
|
1836
|
-
};
|
|
1837
|
-
}
|
|
1838
|
-
// ============================================
|
|
1839
|
-
// CHAIN MANAGEMENT TOOLS
|
|
1840
|
-
// ============================================
|
|
1841
|
-
case 'get_chain_status': {
|
|
1842
|
-
const chainId = args?.chain_id;
|
|
1843
|
-
if (!chainId) {
|
|
1844
|
-
return {
|
|
1845
|
-
content: [{
|
|
1846
|
-
type: 'text',
|
|
1847
|
-
text: JSON.stringify({
|
|
1848
|
-
status: 'error',
|
|
1849
|
-
error: 'chain_id is required'
|
|
1850
|
-
}, null, 2)
|
|
1851
|
-
}],
|
|
1852
|
-
isError: true
|
|
1853
|
-
};
|
|
1854
|
-
}
|
|
1855
|
-
const chainStatus = await getChainStatus(chainId);
|
|
1856
|
-
if (chainStatus.status === 'not_found') {
|
|
1857
|
-
return {
|
|
1858
|
-
content: [{
|
|
1859
|
-
type: 'text',
|
|
1860
|
-
text: JSON.stringify({
|
|
1861
|
-
status: 'error',
|
|
1862
|
-
error: `Chain not found: ${chainId}`,
|
|
1863
|
-
hint: 'Chain states expire after 1 hour. The chain may have completed or expired.'
|
|
1864
|
-
}, null, 2)
|
|
1865
|
-
}],
|
|
1866
|
-
isError: true
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
return {
|
|
1870
|
-
content: [{
|
|
1871
|
-
type: 'text',
|
|
1872
|
-
text: JSON.stringify({
|
|
1873
|
-
status: 'success',
|
|
1874
|
-
chain: {
|
|
1875
|
-
chainId: chainStatus.chainId,
|
|
1876
|
-
executionStatus: chainStatus.status,
|
|
1877
|
-
...(chainStatus.result ? {
|
|
1878
|
-
result: {
|
|
1879
|
-
success: chainStatus.result.success,
|
|
1880
|
-
completedSteps: chainStatus.result.completedSteps,
|
|
1881
|
-
totalLatencyMs: chainStatus.result.totalLatencyMs,
|
|
1882
|
-
totalCost: chainStatus.result.totalCost,
|
|
1883
|
-
finalResult: chainStatus.result.finalResult,
|
|
1884
|
-
error: chainStatus.result.error,
|
|
1885
|
-
canResume: chainStatus.result.canResume,
|
|
1886
|
-
resumeToken: chainStatus.result.resumeToken,
|
|
1887
|
-
}
|
|
1888
|
-
} : {})
|
|
1889
|
-
}
|
|
1890
|
-
}, null, 2)
|
|
1891
|
-
}]
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
case 'resume_chain': {
|
|
1895
|
-
const resumeToken = args?.resume_token;
|
|
1896
|
-
const overrides = args?.overrides;
|
|
1897
|
-
const originalChain = args?.original_chain;
|
|
1898
|
-
if (!resumeToken) {
|
|
1899
|
-
return {
|
|
1900
|
-
content: [{
|
|
1901
|
-
type: 'text',
|
|
1902
|
-
text: JSON.stringify({
|
|
1903
|
-
status: 'error',
|
|
1904
|
-
error: 'resume_token is required'
|
|
1905
|
-
}, null, 2)
|
|
1906
|
-
}],
|
|
1907
|
-
isError: true
|
|
1908
|
-
};
|
|
1909
|
-
}
|
|
1910
|
-
// Check workspace access
|
|
1911
|
-
const access = checkWorkspaceAccess();
|
|
1912
|
-
if (!access.allowed) {
|
|
1913
|
-
return {
|
|
1914
|
-
content: [{
|
|
1915
|
-
type: 'text',
|
|
1916
|
-
text: JSON.stringify({
|
|
1917
|
-
status: 'error',
|
|
1918
|
-
error: access.error,
|
|
1919
|
-
hint: 'Use register_owner to authenticate your workspace.',
|
|
1920
|
-
}, null, 2)
|
|
1921
|
-
}],
|
|
1922
|
-
isError: true
|
|
1923
|
-
};
|
|
1924
|
-
}
|
|
1925
|
-
try {
|
|
1926
|
-
// Note: The resume_chain function requires the original chain definition
|
|
1927
|
-
// In practice, you'd store this or require the caller to provide it
|
|
1928
|
-
if (!originalChain) {
|
|
1929
|
-
return {
|
|
1930
|
-
content: [{
|
|
1931
|
-
type: 'text',
|
|
1932
|
-
text: JSON.stringify({
|
|
1933
|
-
status: 'error',
|
|
1934
|
-
error: 'original_chain is required to resume. Please provide the original chain definition.',
|
|
1935
|
-
hint: 'Pass original_chain: [...] with the same chain array used in the failed execution.'
|
|
1936
|
-
}, null, 2)
|
|
1937
|
-
}],
|
|
1938
|
-
isError: true
|
|
1939
|
-
};
|
|
1940
|
-
}
|
|
1941
|
-
const chainDefinition = {
|
|
1942
|
-
steps: originalChain,
|
|
1943
|
-
};
|
|
1944
|
-
const chainCredentials = {
|
|
1945
|
-
userId: DEFAULT_AGENT_ID,
|
|
1946
|
-
customerKeys: {},
|
|
1947
|
-
};
|
|
1948
|
-
const customerKey = args?.customer_key;
|
|
1949
|
-
if (customerKey) {
|
|
1950
|
-
chainCredentials.customerKeys = { default: customerKey };
|
|
1951
|
-
}
|
|
1952
|
-
const result = await resumeChain(resumeToken, chainDefinition, chainCredentials, {}, // inputs
|
|
1953
|
-
overrides, { verbose: false });
|
|
1954
|
-
return {
|
|
1955
|
-
content: [{
|
|
1956
|
-
type: 'text',
|
|
1957
|
-
text: JSON.stringify({
|
|
1958
|
-
status: result.success ? 'success' : 'error',
|
|
1959
|
-
mode: 'chain_resumed',
|
|
1960
|
-
chainId: result.chainId,
|
|
1961
|
-
steps: result.trace.map(t => ({
|
|
1962
|
-
id: t.stepId,
|
|
1963
|
-
status: t.success ? 'completed' : 'failed',
|
|
1964
|
-
result: t.output,
|
|
1965
|
-
error: t.error,
|
|
1966
|
-
latencyMs: t.latencyMs,
|
|
1967
|
-
})),
|
|
1968
|
-
finalResult: result.finalResult,
|
|
1969
|
-
totalLatencyMs: result.totalLatencyMs,
|
|
1970
|
-
totalCost: result.totalCost,
|
|
1971
|
-
...(result.error ? {
|
|
1972
|
-
error: result.error,
|
|
1973
|
-
canResume: result.canResume,
|
|
1974
|
-
resumeToken: result.resumeToken,
|
|
1975
|
-
} : {}),
|
|
1976
|
-
}, null, 2)
|
|
1977
|
-
}],
|
|
1978
|
-
isError: !result.success
|
|
1979
|
-
};
|
|
1980
|
-
}
|
|
1981
|
-
catch (error) {
|
|
1982
|
-
return {
|
|
1983
|
-
content: [{
|
|
1984
|
-
type: 'text',
|
|
1985
|
-
text: JSON.stringify({
|
|
1986
|
-
status: 'error',
|
|
1987
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1988
|
-
}, null, 2)
|
|
1989
|
-
}],
|
|
1990
|
-
isError: true
|
|
1991
|
-
};
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
default:
|
|
1995
|
-
return {
|
|
1996
|
-
content: [
|
|
1997
|
-
{
|
|
1998
|
-
type: 'text',
|
|
1999
|
-
text: JSON.stringify({
|
|
2000
|
-
status: 'error',
|
|
2001
|
-
message: `Unknown tool: ${name}`
|
|
2002
|
-
}, null, 2)
|
|
2003
|
-
}
|
|
2004
|
-
],
|
|
2005
|
-
isError: true
|
|
2006
|
-
};
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
catch (error) {
|
|
2010
|
-
return {
|
|
2011
|
-
content: [
|
|
2012
|
-
{
|
|
2013
|
-
type: 'text',
|
|
2014
|
-
text: JSON.stringify({
|
|
2015
|
-
status: 'error',
|
|
2016
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
2017
|
-
}, null, 2)
|
|
2018
|
-
}
|
|
2019
|
-
],
|
|
2020
|
-
isError: true
|
|
2021
|
-
};
|
|
2022
|
-
}
|
|
2023
|
-
});
|
|
2024
|
-
// Start server
|
|
2025
|
-
async function main() {
|
|
2026
|
-
// Check for CLI mode
|
|
2027
|
-
if (process.argv.includes('--cli') || process.argv.includes('-c')) {
|
|
2028
|
-
const { startCLI } = await import('./cli.js');
|
|
2029
|
-
await startCLI();
|
|
2030
|
-
return;
|
|
2031
|
-
}
|
|
2032
|
-
const transport = new StdioServerTransport();
|
|
2033
|
-
await server.connect(transport);
|
|
2034
|
-
trackStartup();
|
|
2035
|
-
// Validate session on startup
|
|
2036
|
-
const hasValidSession = await validateSession();
|
|
2037
|
-
// Welcome message with onboarding
|
|
2038
|
-
console.error(`
|
|
2039
|
-
🦞 APIClaw v1.1.5 — The API Layer for AI Agents
|
|
2040
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
2041
|
-
|
|
2042
|
-
✓ 19,000+ APIs indexed
|
|
2043
|
-
✓ 23 categories
|
|
2044
|
-
✓ 9 direct-call providers ready
|
|
2045
|
-
${hasValidSession ? `✓ Authenticated as ${workspaceContext?.email}` : '⚠ Not authenticated - use register_owner'}
|
|
2046
|
-
|
|
2047
|
-
Quick Start:
|
|
2048
|
-
${!hasValidSession ? 'register_owner({ email: "you@example.com" }) # First, authenticate\n ' : ''}discover_apis("send SMS to Sweden")
|
|
2049
|
-
discover_apis("search the web")
|
|
2050
|
-
call_api({ provider: "brave_search", ... })
|
|
2051
|
-
|
|
2052
|
-
Direct Call (no API key needed):
|
|
2053
|
-
list_connected()
|
|
2054
|
-
|
|
2055
|
-
Interactive CLI mode:
|
|
2056
|
-
npx @nordsym/apiclaw --cli
|
|
2057
|
-
|
|
2058
|
-
Docs: https://apiclaw.nordsym.com
|
|
2059
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
2060
|
-
`);
|
|
2061
|
-
}
|
|
2062
|
-
main().catch(console.error);
|