@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/dist/index.js CHANGED
@@ -1,379 +1,413 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import {
5
- CallToolRequestSchema,
6
- ListToolsRequestSchema
7
- } from "@modelcontextprotocol/sdk/types.js";
8
- import { discoverAPIs, getAPIDetails, getCategories, getAllAPIs } from "./discovery.js";
9
- import { trackStartup, trackSearch } from "./telemetry.js";
10
- import {
11
- addCredits,
12
- purchaseAPIAccess,
13
- getBalanceSummary
14
- } from "./credits.js";
15
- import { hasRealCredentials } from "./credentials.js";
16
- import { getConnectedProviders } from "./execute.js";
17
- import { executeMetered } from "./metered.js";
18
- import { logAPICall } from "./mcp-analytics.js";
19
- import { isOpenAPI, executeOpenAPI, listOpenAPIs } from "./open-apis.js";
20
- import { PROXY_PROVIDERS } from "./proxy.js";
21
- import {
22
- requiresConfirmationAsync,
23
- createPendingAction,
24
- consumePendingAction,
25
- generatePreview,
26
- validateParams
27
- } from "./confirmation.js";
28
- import { executeCapability, listCapabilities, hasCapability } from "./capability-router.js";
29
- import { readSession, writeSession, clearSession, getMachineFingerprint, detectMCPClient } from "./session.js";
30
- import { ConvexHttpClient } from "convex/browser";
31
- import {
32
- getOrCreateCustomer,
33
- createMeteredCheckoutSession,
34
- getUsageSummary,
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 = /* @__PURE__ */ new Map();
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
- const now = /* @__PURE__ */ new Date();
54
- const nextHour = new Date(now);
55
- nextHour.setHours(now.getHours() + 1, 0, 0, 0);
56
- return Math.ceil((nextHour.getTime() - now.getTime()) / 6e4);
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
- const now = /* @__PURE__ */ new Date();
60
- const nextMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));
61
- return nextMonth.toISOString().replace("T", " ").slice(0, 16) + " UTC";
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
- const now = Date.now();
65
- const hourInMs = 60 * 60 * 1e3;
66
- const weekInMs = 7 * 24 * hourInMs;
67
- let state = anonymousRateLimits.get(fingerprint);
68
- if (!state) {
69
- state = {
70
- hourlyCount: 0,
71
- hourlyResetTime: now + hourInMs,
72
- weeklyCount: 0,
73
- weeklyResetTime: now + weekInMs
74
- };
75
- anonymousRateLimits.set(fingerprint, state);
76
- }
77
- if (now >= state.hourlyResetTime) {
78
- state.hourlyCount = 0;
79
- state.hourlyResetTime = now + hourInMs;
80
- }
81
- if (now >= state.weeklyResetTime) {
82
- state.weeklyCount = 0;
83
- state.weeklyResetTime = now + weekInMs;
84
- }
85
- if (state.hourlyCount >= ANONYMOUS_HOURLY_LIMIT) {
86
- return {
87
- allowed: false,
88
- error: JSON.stringify({
89
- success: false,
90
- error: `Hourly rate limit (${ANONYMOUS_HOURLY_LIMIT} calls/hour)`,
91
- retry_after_minutes: calculateMinutesUntilNextHour(),
92
- hint: "Rate limit resets at top of hour",
93
- action: "Register to get higher limits: register_owner({ email: 'you@example.com' })"
94
- }, null, 2)
95
- };
96
- }
97
- if (state.weeklyCount >= ANONYMOUS_WEEKLY_LIMIT) {
98
- return {
99
- allowed: false,
100
- error: JSON.stringify({
101
- success: false,
102
- error: `\u26A1 You've hit your free tier limit (${ANONYMOUS_WEEKLY_LIMIT} calls/week).
103
- Upgrade: https://apiclaw.nordsym.com/upgrade`,
104
- hint: "Register for 50 calls/week, or upgrade for unlimited",
105
- action: "Run: register_owner({ email: 'you@example.com' })",
106
- upgrade_url: "https://apiclaw.nordsym.com/upgrade",
107
- retry_after: getNextMonthUTC()
108
- }, null, 2)
109
- };
110
- }
111
- state.hourlyCount++;
112
- state.weeklyCount++;
113
- return { allowed: true };
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
- const session = readSession();
117
- if (!session) {
118
- console.error("[APIClaw] No session found. Use register_owner to authenticate.");
119
- return false;
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
- if (result.status !== "active") {
131
- console.error(`[APIClaw] Workspace status: ${result.status}. Please verify your email.`);
132
- return false;
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
- try {
156
- await convex.mutation("earnProgress:markFirstDirectCall", {
157
- workspaceId
158
- });
159
- const apiId = `${provider}:${action}`;
160
- await convex.mutation("earnProgress:trackApiUsed", {
161
- workspaceId,
162
- apiId
163
- });
164
- } catch (e) {
165
- console.error("[APIClaw] Failed to track earn progress:", e);
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 = /* @__PURE__ */ new Map();
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
- if (providerId && PROXY_PROVIDERS.includes(providerId)) {
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
- allowed: false,
178
- error: rateLimitCheck.error,
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
- return { allowed: true, isAnonymous: false };
185
- }
186
- if (!workspaceContext) {
187
- return {
188
- allowed: false,
189
- error: "Not authenticated. Use register_owner to authenticate your workspace."
190
- };
191
- }
192
- if (workspaceContext.status !== "active") {
193
- return {
194
- allowed: false,
195
- error: `Workspace status: ${workspaceContext.status}. Please verify your email.`
196
- };
197
- }
198
- if (!workspaceContext.email && workspaceContext.usageCount >= UNREGISTERED_CALL_LIMIT) {
199
- return {
200
- allowed: false,
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
- return {
224
- allowed: false,
225
- error: `\u26A1 You've hit your free tier limit (${FREE_MONTHLY_LIMIT} calls/week).
226
- Upgrade: https://apiclaw.nordsym.com/upgrade`
227
- };
228
- }
229
- return { allowed: true, isAnonymous: false };
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
- const exactKey = `${providerId.toUpperCase().replace(/-/g, "_")}_API_KEY`;
233
- if (process.env[exactKey]) {
234
- return process.env[exactKey];
235
- }
236
- const variations = [
237
- `${providerId.toUpperCase()}_API_KEY`,
238
- `${providerId.toUpperCase()}_KEY`,
239
- `${providerId.toUpperCase().replace(/_/g, "")}_API_KEY`
240
- ];
241
- for (const key of variations) {
242
- if (process.env[key]) {
243
- return process.env[key];
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
- name: "apiclaw_help",
251
- description: "Get help and see available commands. Start here if you are new to APIClaw.",
252
- inputSchema: {
253
- type: "object",
254
- properties: {},
255
- required: []
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
- required: ["query"]
292
- }
293
- },
294
- {
295
- name: "get_api_details",
296
- description: "Get detailed information about a specific API provider, including endpoints, pricing, and features. Use compact=true to save ~60% tokens.",
297
- inputSchema: {
298
- type: "object",
299
- properties: {
300
- api_id: {
301
- type: "string",
302
- description: 'The API provider ID (e.g., "46elks", "resend", "openrouter")'
303
- },
304
- compact: {
305
- type: "boolean",
306
- description: "If true, returns minified spec (strips examples, keeps essential params). Saves ~60% context window.",
307
- default: false
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
- required: ["api_id"]
311
- }
312
- },
313
- {
314
- name: "purchase_access",
315
- description: "Purchase access to an API using your credit balance. Returns API credentials on success.",
316
- inputSchema: {
317
- type: "object",
318
- properties: {
319
- api_id: {
320
- type: "string",
321
- description: "The API provider ID to purchase access to"
322
- },
323
- amount_usd: {
324
- type: "number",
325
- description: "Amount in USD to spend on this API"
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
- required: ["api_id", "amount_usd"]
333
- }
334
- },
335
- {
336
- name: "check_balance",
337
- description: "Check your credit balance and list active API purchases.",
338
- inputSchema: {
339
- type: "object",
340
- properties: {
341
- agent_id: {
342
- type: "string",
343
- description: "Your agent identifier (optional, uses default if not provided)"
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
- name: "add_credits",
350
- description: "Add credits to your account. (For testing/demo purposes)",
351
- inputSchema: {
352
- type: "object",
353
- properties: {
354
- amount_usd: {
355
- type: "number",
356
- description: "Amount in USD to add to your balance"
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
- required: ["amount_usd"]
364
- }
365
- },
366
- {
367
- name: "list_categories",
368
- description: "List all available API categories.",
369
- inputSchema: {
370
- type: "object",
371
- properties: {}
372
- }
373
- },
374
- {
375
- name: "call_api",
376
- description: `Execute an API call through APIClaw. Supports single calls AND multi-step chains.
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
- inputSchema: {
395
- type: "object",
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
- id: { type: "string", description: "Step identifier for cross-step references" },
430
- provider: { type: "string", description: "API provider" },
431
- action: { type: "string", description: "Action to execute" },
432
- params: { type: "object", description: "Action parameters. Use $stepId.path for references." },
433
- parallel: { type: "array", description: "Steps to run in parallel" },
434
- if: { type: "string", description: 'Condition for conditional execution (e.g., "$step1.success")' },
435
- then: { type: "object", description: "Step to execute if condition is true" },
436
- else: { type: "object", description: "Step to execute if condition is false" },
437
- forEach: { type: "string", description: 'Array reference to iterate (e.g., "$search.results")' },
438
- as: { type: "string", description: "Variable name for current item in loop" },
439
- do: { type: "object", description: "Step to execute for each item" },
440
- onError: {
441
- type: "object",
442
- description: "Error handling configuration",
443
- properties: {
444
- retry: {
445
- type: "object",
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
- attempts: { type: "number", description: "Max retry attempts" },
448
- backoff: { type: "string", description: '"exponential" or "linear" or array of ms delays' }
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
- fallback: { type: "object", description: "Fallback step if this fails" },
452
- abort: { type: "boolean", description: "Abort entire chain on failure" }
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
- required: []
485
- }
486
- },
487
- {
488
- name: "list_connected",
489
- description: "List all APIs available for Direct Call (no API key needed).",
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
- required: ["capability", "action", "params"]
533
- }
534
- },
535
- {
536
- name: "list_capabilities",
537
- description: "List all available capabilities and their providers.",
538
- inputSchema: {
539
- type: "object",
540
- properties: {}
541
- }
542
- },
543
- // ============================================
544
- // WORKSPACE TOOLS
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
- required: ["email"]
558
- }
559
- },
560
- {
561
- name: "check_workspace_status",
562
- description: "Check your workspace status, tier, and usage remaining.",
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
- required: ["email"]
599
- }
600
- },
601
- {
602
- name: "get_usage_summary",
603
- description: "Get current billing period usage and estimated cost for metered billing.",
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
- required: ["subscription_id"]
613
- }
614
- },
615
- {
616
- name: "estimate_cost",
617
- description: "Estimate the cost for a given number of API calls.",
618
- inputSchema: {
619
- type: "object",
620
- properties: {
621
- call_count: {
622
- type: "number",
623
- description: "Number of API calls to estimate cost for"
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
- required: ["call_count"]
627
- }
628
- },
629
- // ============================================
630
- // CHAIN MANAGEMENT TOOLS
631
- // ============================================
632
- {
633
- name: "get_chain_status",
634
- description: "Check the status of an async chain execution. Use the chainId returned from call_api with async: true.",
635
- inputSchema: {
636
- type: "object",
637
- properties: {
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
- required: ["chain_id"]
644
- }
645
- },
646
- {
647
- name: "resume_chain",
648
- description: "Resume a failed chain from the point of failure. Use the resumeToken from the error response. Requires the original chain definition.",
649
- inputSchema: {
650
- type: "object",
651
- properties: {
652
- resume_token: {
653
- type: "string",
654
- description: 'Resume token from a failed chain (e.g., "chain_xyz_step_2")'
655
- },
656
- original_chain: {
657
- type: "array",
658
- description: "The original chain definition that failed. Required to resume execution.",
659
- items: { type: "object" }
660
- },
661
- overrides: {
662
- type: "object",
663
- description: 'Optional parameter overrides for specific steps. Format: { "stepId": { ...newParams } }'
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
- const server = new Server(
671
- {
672
- name: "apivault",
673
- version: "0.1.0"
674
- },
675
- {
704
+ // Create server
705
+ const server = new Server({
706
+ name: 'apivault',
707
+ version: '0.1.0',
708
+ }, {
676
709
  capabilities: {
677
- tools: {}
678
- }
679
- }
680
- );
710
+ tools: {},
711
+ },
712
+ });
713
+ // Handle list tools
681
714
  server.setRequestHandler(ListToolsRequestSchema, async () => {
682
- return { tools };
715
+ return { tools };
683
716
  });
717
+ // Handle tool calls
684
718
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
685
- const { name, arguments: args } = request.params;
686
- try {
687
- switch (name) {
688
- case "apiclaw_help": {
689
- const helpText = `
690
- \u{1F99E} APIClaw \u2014 The API Layer for AI Agents
691
- \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
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
- \u2022 brave_search \u2014 Web search
707
- \u2022 46elks \u2014 SMS (Sweden)
708
- \u2022 twilio \u2014 SMS (Global)
709
- \u2022 resend \u2014 Email
710
- \u2022 openrouter \u2014 LLM routing (100+ models)
711
- \u2022 elevenlabs \u2014 Text-to-speech
712
- \u2022 replicate \u2014 AI models (images, video, audio)
713
- \u2022 firecrawl \u2014 Web scraping & crawling
714
- \u2022 github \u2014 Code repos & developer data
715
- \u2022 e2b \u2014 Code sandbox for AI agents
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
- return {
724
- content: [{ type: "text", text: helpText }]
725
- };
726
- }
727
- case "discover_apis": {
728
- const query = args?.query;
729
- const category = args?.category;
730
- const maxResults = args?.max_results || 5;
731
- const region = args?.region;
732
- const subagentId = args?.subagent_id;
733
- const aiBackend = args?.ai_backend;
734
- const startTime = Date.now();
735
- const results = discoverAPIs(query, { category, maxResults, region });
736
- const responseTimeMs = Date.now() - startTime;
737
- trackSearch(query, results.length, responseTimeMs);
738
- const analyticsUserId = workspaceContext?.workspaceId || `anon:${getMachineFingerprint()}`;
739
- const convexUrl = CONVEX_URL;
740
- if (convexUrl) {
741
- fetch(`${convexUrl}/api/mutation`, {
742
- method: "POST",
743
- headers: { "Content-Type": "application/json" },
744
- body: JSON.stringify({
745
- path: "analytics:log",
746
- args: {
747
- event: "search_query",
748
- provider: void 0,
749
- query,
750
- identifier: analyticsUserId,
751
- metadata: {
752
- resultCount: results.length,
753
- matchedProviders: results.slice(0, 10).map((r) => r.provider.id),
754
- responseTimeMs,
755
- category,
756
- authenticated: !!workspaceContext
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
- }).catch(() => {
761
- });
762
- }
763
- if (workspaceContext?.sessionToken) {
764
- const searchLogPayload = {
765
- path: "searchLogs:log",
766
- args: {
767
- sessionToken: workspaceContext.sessionToken,
768
- subagentId: subagentId || void 0,
769
- query,
770
- resultCount: results.length,
771
- matchedProviders: results.slice(0, 10).map((r) => r.provider.id),
772
- responseTimeMs
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
- fetch(`${convexUrl}/api/mutation`, {
776
- method: "POST",
777
- headers: { "Content-Type": "application/json" },
778
- body: JSON.stringify(searchLogPayload)
779
- }).catch(() => {
780
- });
781
- const PROVIDER_KEYWORDS = {
782
- 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"],
783
- 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"]
784
- };
785
- const queryLower = query.toLowerCase();
786
- for (const [provider, keywords] of Object.entries(PROVIDER_KEYWORDS)) {
787
- if (keywords.some((kw) => queryLower.includes(kw))) {
788
- fetch(`${convexUrl}/api/mutation`, {
789
- method: "POST",
790
- headers: { "Content-Type": "application/json" },
791
- body: JSON.stringify({
792
- path: "providers:logDiscovery",
793
- args: {
794
- provider,
795
- query: query.substring(0, 100),
796
- latencyMs: responseTimeMs,
797
- callerWorkspaceId: workspaceContext?.workspaceId || "anonymous"
798
- }
799
- })
800
- }).catch(() => {
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
- if (aiBackend && workspaceContext?.sessionToken) {
806
- fetch("https://adventurous-avocet-799.convex.cloud/api/mutation", {
807
- method: "POST",
808
- headers: { "Content-Type": "application/json" },
809
- body: JSON.stringify({
810
- path: "agents:updateAIBackend",
811
- args: {
812
- token: workspaceContext.sessionToken,
813
- subagentId: subagentId || void 0,
814
- aiBackend
815
- }
816
- })
817
- }).catch(() => {
818
- });
819
- }
820
- if (results.length === 0) {
821
- return {
822
- content: [
823
- {
824
- type: "text",
825
- text: JSON.stringify({
826
- status: "no_results",
827
- message: `No APIs found matching "${query}". Try broader terms or check available categories with list_categories.`,
828
- available_categories: getCategories()
829
- }, null, 2)
830
- }
831
- ]
832
- };
833
- }
834
- return {
835
- content: [
836
- {
837
- type: "text",
838
- text: JSON.stringify({
839
- status: "success",
840
- query,
841
- results_count: results.length,
842
- results: results.map((r) => ({
843
- id: r.provider.id,
844
- name: r.provider.name,
845
- description: r.provider.description,
846
- category: r.provider.category,
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
- case "get_api_details": {
860
- const apiId = args?.api_id;
861
- const compact = args?.compact || false;
862
- const api = getAPIDetails(apiId, { compact });
863
- if (!api) {
864
- return {
865
- content: [
866
- {
867
- type: "text",
868
- text: JSON.stringify({
869
- status: "error",
870
- message: `API not found: ${apiId}`,
871
- hint: "Try discover_apis to search, or list_connected for direct-call APIs"
872
- }, null, 2)
873
- }
874
- ]
875
- };
876
- }
877
- if (compact) {
878
- return {
879
- content: [
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
- case "purchase_access": {
900
- const apiId = args?.api_id;
901
- const amountUsd = args?.amount_usd;
902
- const agentId = args?.agent_id || DEFAULT_AGENT_ID;
903
- const result = purchaseAPIAccess(agentId, apiId, amountUsd);
904
- if (!result.success) {
905
- return {
906
- content: [
907
- {
908
- type: "text",
909
- text: JSON.stringify({
910
- status: "error",
911
- message: result.error
912
- }, null, 2)
913
- }
914
- ]
915
- };
916
- }
917
- const api = getAPIDetails(apiId);
918
- return {
919
- content: [
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
- }, null, 2)
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
- case "check_balance": {
945
- const agentId = args?.agent_id || DEFAULT_AGENT_ID;
946
- const summary = getBalanceSummary(agentId);
947
- return {
948
- content: [
949
- {
950
- type: "text",
951
- text: JSON.stringify({
952
- status: "success",
953
- agent_id: agentId,
954
- balance_usd: summary.credits.balance_usd,
955
- currency: summary.credits.currency,
956
- total_spent_usd: summary.total_spent_usd,
957
- real_credential_providers: summary.real_credentials_available,
958
- active_purchases: summary.active_purchases.map((p) => ({
959
- id: p.id,
960
- provider: p.provider_id,
961
- credits_remaining: p.credits_purchased,
962
- status: p.status,
963
- real_credentials: hasRealCredentials(p.provider_id)
964
- }))
965
- }, null, 2)
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
- case "add_credits": {
971
- const amountUsd = args?.amount_usd;
972
- const agentId = args?.agent_id || DEFAULT_AGENT_ID;
973
- const credits = addCredits(agentId, amountUsd);
974
- return {
975
- content: [
976
- {
977
- type: "text",
978
- text: JSON.stringify({
979
- status: "success",
980
- message: `Added $${amountUsd.toFixed(2)} to your account`,
981
- new_balance_usd: credits.balance_usd
982
- }, null, 2)
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
- case "list_categories": {
988
- const categories = getCategories();
989
- const apisByCategory = {};
990
- for (const cat of categories) {
991
- apisByCategory[cat] = getAllAPIs().filter((a) => a.category === cat).map((a) => a.id);
992
- }
993
- return {
994
- content: [
995
- {
996
- type: "text",
997
- text: JSON.stringify({
998
- status: "success",
999
- categories: apisByCategory
1000
- }, null, 2)
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
- case "call_api": {
1006
- const provider = args?.provider;
1007
- const action = args?.action;
1008
- const params = args?.params || {};
1009
- const confirmToken = args?.confirm_token;
1010
- const dryRun = args?.dry_run;
1011
- const chain = args?.chain;
1012
- const subagentId = args?.subagent_id;
1013
- const aiBackend = args?.ai_backend;
1014
- if (aiBackend && workspaceContext?.sessionToken) {
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
- return {
1042
- content: [{
1043
- type: "text",
1044
- text: JSON.stringify({
1045
- status: "error",
1046
- error: access.error,
1047
- hint: "Use register_owner to authenticate your workspace."
1048
- }, null, 2)
1049
- }],
1050
- isError: true
1051
- };
1052
- }
1053
- try {
1054
- const chainDefinition = {
1055
- steps: chain,
1056
- timeout: args?.timeout,
1057
- errorPolicy: args?.continueOnError ? { mode: "best-effort" } : { mode: "fail-fast" }
1058
- };
1059
- const chainCredentials = {
1060
- userId: DEFAULT_AGENT_ID,
1061
- customerKeys: {}
1062
- };
1063
- const customerKey = args?.customer_key;
1064
- if (customerKey) {
1065
- chainCredentials.customerKeys = { default: customerKey };
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
- const chainOptions = {
1068
- verbose: false
1069
- };
1070
- const chainResult = await executeChain(
1071
- chainDefinition,
1072
- chainCredentials,
1073
- {},
1074
- // inputs
1075
- chainOptions
1076
- );
1077
- if (chainResult.success && workspaceContext) {
1078
- const completedCount = chainResult.completedSteps.length;
1079
- for (let i = 0; i < completedCount; i++) {
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
- await convex.mutation("workspaces:incrementUsage", {
1082
- workspaceId: workspaceContext.workspaceId
1083
- });
1084
- } catch (e) {
1085
- console.error("[APIClaw] Failed to track chain usage:", e);
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
- return {
1090
- content: [{
1091
- type: "text",
1092
- text: JSON.stringify({
1093
- status: chainResult.success ? "success" : "error",
1094
- mode: "chain",
1095
- chainId: chainResult.chainId,
1096
- steps: chainResult.trace.map((t) => ({
1097
- id: t.stepId,
1098
- status: t.success ? "completed" : "failed",
1099
- result: t.output,
1100
- error: t.error,
1101
- latencyMs: t.latencyMs,
1102
- cost: t.cost
1103
- })),
1104
- finalResult: chainResult.finalResult,
1105
- totalLatencyMs: chainResult.totalLatencyMs,
1106
- totalCost: chainResult.totalCost,
1107
- tokensSaved: (chain.length - 1) * 500,
1108
- // Estimate tokens saved
1109
- ...chainResult.error ? {
1110
- completedSteps: chainResult.completedSteps,
1111
- failedStep: chainResult.failedStep ? {
1112
- id: chainResult.failedStep.stepId,
1113
- error: chainResult.failedStep.error,
1114
- code: chainResult.failedStep.errorCode
1115
- } : void 0,
1116
- partialResults: chainResult.results,
1117
- canResume: chainResult.canResume,
1118
- resumeToken: chainResult.resumeToken
1119
- } : {}
1120
- }, null, 2)
1121
- }],
1122
- isError: !chainResult.success
1123
- };
1124
- } catch (error) {
1125
- return {
1126
- content: [{
1127
- type: "text",
1128
- text: JSON.stringify({
1129
- status: "error",
1130
- mode: "chain",
1131
- error: error instanceof Error ? error.message : String(error)
1132
- }, null, 2)
1133
- }],
1134
- isError: true
1135
- };
1136
- }
1137
- }
1138
- if (dryRun) {
1139
- const { generateDryRun } = await import("./execute.js");
1140
- const dryRunResult = generateDryRun(provider, action, params);
1141
- return {
1142
- content: [{
1143
- type: "text",
1144
- text: JSON.stringify(dryRunResult, null, 2)
1145
- }]
1146
- };
1147
- }
1148
- const isFreeAPI = isOpenAPI(provider);
1149
- if (!isFreeAPI) {
1150
- const access = checkWorkspaceAccess(provider);
1151
- if (!access.allowed) {
1152
- return {
1153
- content: [{
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
- const preview = generatePreview(provider, action, params);
1238
- if (confirmCheck.estimatedCost) {
1239
- preview.estimated_cost = confirmCheck.estimatedCost;
1240
- }
1241
- const pending = createPendingAction(provider, action, params, preview, DEFAULT_AGENT_ID);
1242
- return {
1243
- content: [{
1244
- type: "text",
1245
- text: JSON.stringify({
1246
- status: "requires_confirmation",
1247
- message: "\u26A0\uFE0F This action costs money. Please review and confirm.",
1248
- preview,
1249
- confirm_token: pending.token,
1250
- expires_in_seconds: 300,
1251
- how_to_confirm: `Call again with confirm_token: "${pending.token}"`
1252
- }, null, 2)
1253
- }]
1254
- };
1255
- }
1256
- if (isOpenAPI(provider)) {
1257
- apiType = "open";
1258
- result = await executeOpenAPI(provider, action, params);
1259
- } else {
1260
- apiType = "direct";
1261
- const customerKey = args?.customer_key || getCustomerKey(provider);
1262
- const stripeCustomerId = args?.stripe_customer_id || process.env.APICLAW_STRIPE_CUSTOMER_ID;
1263
- result = await executeMetered(provider, action, params, {
1264
- customerId: stripeCustomerId,
1265
- customerKey,
1266
- userId: DEFAULT_AGENT_ID
1267
- });
1268
- }
1269
- const analyticsUserId = workspaceContext ? workspaceContext.workspaceId : `anon:${getMachineFingerprint()}`;
1270
- logAPICall({
1271
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1272
- provider,
1273
- action,
1274
- type: apiType,
1275
- userId: analyticsUserId,
1276
- success: result.success,
1277
- latencyMs: Date.now() - startTime,
1278
- error: result.error
1279
- });
1280
- if (workspaceContext) {
1281
- convex.mutation("logs:createLogInternal", {
1282
- workspaceId: workspaceContext.workspaceId,
1283
- sessionToken: workspaceContext.sessionToken || "",
1284
- provider,
1285
- action,
1286
- status: result.success ? "success" : "error",
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
- if (currentAgentId) {
1311
- convex.mutation("agents:incrementAgentCalls", { agentId: currentAgentId }).catch(() => {
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
- await trackEarnProgress(workspaceContext.workspaceId, provider, action);
1315
- } catch (e) {
1316
- console.error("[APIClaw] Failed to track usage:", e);
1317
- }
1318
- }
1319
- const responseData = {
1320
- status: result.success ? "success" : "error",
1321
- provider: result.provider,
1322
- action: result.action,
1323
- type: apiType,
1324
- ...result.success ? { data: result.data } : { error: result.error },
1325
- ...result.cost !== void 0 ? { cost_sek: result.cost } : {}
1326
- };
1327
- if (result.success && workspaceContext && !workspaceContext.email) {
1328
- const remaining = UNREGISTERED_CALL_LIMIT - (workspaceContext.usageCount || 0);
1329
- if (remaining > 0 && remaining <= 3) {
1330
- responseData._notice = `${remaining} free calls remaining. Run register_owner to unlock 50/month.`;
1331
- }
1332
- }
1333
- return {
1334
- content: [
1335
- {
1336
- type: "text",
1337
- text: JSON.stringify(responseData, null, 2)
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
- isError: !result.success
1341
- };
1342
- }
1343
- case "list_connected": {
1344
- const directProviders = getConnectedProviders();
1345
- const openProviders = listOpenAPIs();
1346
- return {
1347
- content: [
1348
- {
1349
- type: "text",
1350
- text: JSON.stringify({
1351
- status: "success",
1352
- message: "These APIs are available via call_api - no API key needed!",
1353
- direct_call: {
1354
- description: "APIs where we handle authentication",
1355
- providers: directProviders
1356
- },
1357
- open_apis: {
1358
- description: "Free, open APIs (no auth required)",
1359
- providers: openProviders
1360
- },
1361
- usage: "Use call_api with provider, action, and params to execute calls."
1362
- }, null, 2)
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
- case "capability": {
1368
- const capabilityId = args?.capability;
1369
- const action = args?.action;
1370
- const params = args?.params || {};
1371
- const preferences = args?.preferences || {};
1372
- const subagentId = args?.subagent_id;
1373
- const aiBackend = args?.ai_backend;
1374
- if (aiBackend && workspaceContext?.sessionToken) {
1375
- fetch("https://adventurous-avocet-799.convex.cloud/api/mutation", {
1376
- method: "POST",
1377
- headers: { "Content-Type": "application/json" },
1378
- body: JSON.stringify({
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
- } catch (err) {
1480
- console.error("[APIClaw] Warning: Failed to claim anonymous usage:", err);
1481
- }
1482
- workspaceContext = {
1483
- sessionToken: sessionResult.sessionToken,
1484
- workspaceId: existing.id,
1485
- email,
1486
- tier: existing.tier,
1487
- usageRemaining: existing.usageLimit - existing.usageCount,
1488
- usageCount: existing.usageCount,
1489
- status: existing.status
1490
- };
1491
- return {
1492
- content: [{
1493
- type: "text",
1494
- text: JSON.stringify({
1495
- status: "success",
1496
- message: `Welcome back! Authenticated as ${email}`,
1497
- workspace: {
1498
- email,
1499
- tier: existing.tier,
1500
- usageCount: existing.usageCount,
1501
- usageLimit: existing.usageLimit
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
- }, null, 2)
1504
- }]
1505
- };
1506
- }
1507
- }
1508
- const createResult = await convex.mutation("workspaces:createWorkspace", { email });
1509
- let workspaceId;
1510
- if (createResult.success) {
1511
- workspaceId = createResult.workspaceId;
1512
- } else if (createResult.error === "workspace_exists") {
1513
- workspaceId = createResult.workspaceId;
1514
- } else {
1515
- throw new Error(createResult.error);
1516
- }
1517
- const fingerprint = getMachineFingerprint();
1518
- const magicLinkResult = await convex.mutation("workspaces:createMagicLink", {
1519
- email,
1520
- fingerprint
1521
- });
1522
- const verifyUrl = `https://apiclaw.nordsym.com/auth/verify?token=${magicLinkResult.token}`;
1523
- const emailResponse = await fetch("https://api.resend.com/emails", {
1524
- method: "POST",
1525
- headers: {
1526
- "Authorization": `Bearer ${process.env.RESEND_API_KEY}`,
1527
- "Content-Type": "application/json"
1528
- },
1529
- body: JSON.stringify({
1530
- from: "APIClaw <noreply@apiclaw.nordsym.com>",
1531
- to: email,
1532
- subject: "Verify your APIClaw workspace",
1533
- html: `<p>Click to verify: <a href="${verifyUrl}">${verifyUrl}</a></p><p>Expires in 15 minutes.</p>`
1534
- })
1535
- });
1536
- if (!emailResponse.ok) {
1537
- const errorData = await emailResponse.text();
1538
- throw new Error(`Failed to send verification email: ${errorData}`);
1539
- }
1540
- return {
1541
- content: [{
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
- }, null, 2)
1622
- }]
1623
- };
1624
- } catch (error) {
1625
- return {
1626
- content: [{
1627
- type: "text",
1628
- text: JSON.stringify({
1629
- status: "error",
1630
- error: error instanceof Error ? error.message : "Failed to check status"
1631
- }, null, 2)
1632
- }],
1633
- isError: true
1634
- };
1635
- }
1636
- }
1637
- case "remind_owner": {
1638
- const session = readSession();
1639
- if (!session) {
1640
- return {
1641
- content: [{
1642
- type: "text",
1643
- text: JSON.stringify({
1644
- status: "error",
1645
- error: "No workspace found. Use register_owner first."
1646
- }, null, 2)
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
- content: [{
1857
- type: "text",
1858
- text: JSON.stringify({
1859
- status: "success",
1860
- chain: {
1861
- chainId: chainStatus.chainId,
1862
- executionStatus: chainStatus.status,
1863
- ...chainStatus.result ? {
1864
- result: {
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
- if (process.argv.includes("--cli") || process.argv.includes("-c")) {
2014
- const { startCLI } = await import("./cli.js");
2015
- await startCLI();
2016
- return;
2017
- }
2018
- const transport = new StdioServerTransport();
2019
- await server.connect(transport);
2020
- trackStartup();
2021
- const hasValidSession = await validateSession();
2022
- try {
2023
- const fingerprint = getMachineFingerprint();
2024
- const mcpClient = detectMCPClient();
2025
- const existingSession = readSession();
2026
- const result = await convex.mutation("agents:ensureAgent", {
2027
- fingerprint,
2028
- mcpClient,
2029
- platform: process.platform,
2030
- ...existingSession?.sessionToken ? { sessionToken: existingSession.sessionToken } : {}
2031
- });
2032
- if (result?.agentId) {
2033
- currentAgentId = result.agentId;
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
- if (result?.isNew && result?.sessionToken && !hasValidSession) {
2036
- writeSession(result.sessionToken, result.workspaceId, "");
2131
+ catch (e) {
2132
+ console.error('[APIClaw] Agent registration failed (non-blocking):', e);
2037
2133
  }
2038
- } catch (e) {
2039
- console.error("[APIClaw] Agent registration failed (non-blocking):", e);
2040
- }
2041
- console.error(`
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
- \u2713 19,000+ APIs indexed
2046
- \u2713 23 categories
2047
- \u2713 9 direct-call providers ready
2048
- ${hasValidSession ? `\u2713 Authenticated as ${workspaceContext?.email}` : "\u26A0 Not authenticated - use register_owner"}
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 ' : ""}discover_apis("send SMS to Sweden")
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
- \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
2156
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2063
2157
  `);
2064
2158
  }
2065
2159
  main().catch(console.error);
2160
+ //# sourceMappingURL=index.js.map