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