@nordsym/apiclaw 1.3.5 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +33 -0
  2. package/convex/_generated/api.d.ts +12 -0
  3. package/convex/billing.ts +651 -216
  4. package/convex/crons.ts +17 -0
  5. package/convex/email.ts +135 -82
  6. package/convex/feedback.ts +265 -0
  7. package/convex/http.ts +80 -4
  8. package/convex/logs.ts +287 -0
  9. package/convex/providerKeys.ts +209 -0
  10. package/convex/providers.ts +18 -0
  11. package/convex/schema.ts +115 -0
  12. package/convex/stripeActions.ts +512 -0
  13. package/convex/webhooks.ts +494 -0
  14. package/convex/workspaces.ts +74 -1
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +38 -7
  17. package/dist/cli.js.map +1 -1
  18. package/dist/index.js +178 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/metered.d.ts +62 -0
  21. package/dist/metered.d.ts.map +1 -0
  22. package/dist/metered.js +81 -0
  23. package/dist/metered.js.map +1 -0
  24. package/dist/stripe.d.ts +62 -0
  25. package/dist/stripe.d.ts.map +1 -1
  26. package/dist/stripe.js +212 -0
  27. package/dist/stripe.js.map +1 -1
  28. package/docs/PRD-final-polish.md +117 -0
  29. package/docs/PRD-mobile-responsive.md +56 -0
  30. package/docs/PRD-navigation-expansion.md +295 -0
  31. package/docs/PRD-stripe-billing.md +312 -0
  32. package/docs/PRD-workspace-cleanup.md +200 -0
  33. package/landing/src/app/api/billing/checkout/route.ts +109 -0
  34. package/landing/src/app/api/billing/payment-method/route.ts +118 -0
  35. package/landing/src/app/api/billing/portal/route.ts +64 -0
  36. package/landing/src/app/auth/verify/page.tsx +20 -5
  37. package/landing/src/app/earn/page.tsx +6 -6
  38. package/landing/src/app/login/page.tsx +1 -1
  39. package/landing/src/app/page.tsx +70 -70
  40. package/landing/src/app/providers/dashboard/page.tsx +1 -1
  41. package/landing/src/app/workspace/page.tsx +3497 -535
  42. package/landing/src/components/CheckoutButton.tsx +188 -0
  43. package/landing/src/components/Toast.tsx +84 -0
  44. package/landing/src/lib/stats.json +1 -1
  45. package/landing/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +1 -1
  47. package/src/cli.ts +57 -7
  48. package/src/index.ts +205 -0
  49. package/src/metered.ts +149 -0
  50. package/src/stripe.ts +253 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordsym/apiclaw",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "Agent-native API discovery and Direct Call execution via MCP",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/cli.ts CHANGED
@@ -83,23 +83,73 @@ async function registerOwner(email: string): Promise<void> {
83
83
 
84
84
  try {
85
85
  const fingerprint = getMachineFingerprint();
86
- const result = await convex.mutation("workspaces:requestMagicLink" as any, {
87
- email,
88
- fingerprint,
89
- }) as any;
90
86
 
91
- if (result?.sent) {
87
+ // Use HTTP endpoint for magic link
88
+ const response = await fetch(`${CONVEX_URL.replace('.cloud', '.site')}/workspace/magic-link`, {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({ email, fingerprint }),
92
+ });
93
+
94
+ const result = await response.json() as { success?: boolean; token?: string; error?: string };
95
+
96
+ if (result?.success && result?.token) {
92
97
  success(`Magic link sent to ${email}`);
93
98
  log(`\nšŸ“§ Check your email and click the link to authenticate.`);
94
- log(` Then run ${colors.cyan}status${colors.reset} to verify.\n`);
99
+
100
+ // Start polling for verification
101
+ log(`\nā³ Waiting for you to click the link...`);
102
+ log(` (Press Ctrl+C to cancel)\n`);
103
+
104
+ await pollForVerification(result.token, fingerprint);
95
105
  } else {
96
- error(`Failed to send magic link: ${result?.error || 'Unknown error'}`);
106
+ error(`Failed: ${result?.error || 'Unknown error'}`);
97
107
  }
98
108
  } catch (err) {
99
109
  error(`Failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
100
110
  }
101
111
  }
102
112
 
113
+ async function pollForVerification(token: string, fingerprint: string): Promise<void> {
114
+ const maxAttempts = 60; // 5 minutes
115
+ for (let i = 0; i < maxAttempts; i++) {
116
+ await new Promise(r => setTimeout(r, 5000)); // Poll every 5 seconds
117
+
118
+ try {
119
+ const response = await fetch(`${CONVEX_URL.replace('.cloud', '.site')}/workspace/poll?token=${token}`);
120
+ const result = await response.json() as {
121
+ verified?: boolean;
122
+ sessionToken?: string;
123
+ workspaceId?: string;
124
+ email?: string;
125
+ };
126
+
127
+ if (result?.verified && result?.sessionToken) {
128
+ // Save the real session
129
+ writeSession(
130
+ result.sessionToken,
131
+ result.workspaceId || '',
132
+ result.email || ''
133
+ );
134
+
135
+ success(`Authenticated as ${result.email}!`);
136
+
137
+ // Reload workspace context
138
+ await validateSession();
139
+ return;
140
+ }
141
+ } catch {
142
+ // Continue polling
143
+ }
144
+
145
+ // Show progress dot
146
+ process.stdout.write('.');
147
+ }
148
+
149
+ log('\n');
150
+ error('Verification timed out. Please try again.');
151
+ }
152
+
103
153
  async function showStatus(): Promise<void> {
104
154
  const valid = await validateSession();
105
155
 
package/src/index.ts CHANGED
@@ -43,6 +43,13 @@ import {
43
43
  import { executeCapability, listCapabilities, hasCapability } from './capability-router.js';
44
44
  import { readSession, writeSession, clearSession, getMachineFingerprint, SessionData } from './session.js';
45
45
  import { ConvexHttpClient } from 'convex/browser';
46
+ import {
47
+ getOrCreateCustomer,
48
+ createMeteredCheckoutSession,
49
+ getUsageSummary,
50
+ METERED_BILLING
51
+ } from './stripe.js';
52
+ import { estimateCost } from './metered.js';
46
53
 
47
54
  // Default agent ID for MVP (in production, this would come from auth)
48
55
  const DEFAULT_AGENT_ID = 'agent_default';
@@ -392,6 +399,59 @@ const tools: Tool[] = [
392
399
  type: 'object',
393
400
  properties: {}
394
401
  }
402
+ },
403
+ // Metered Billing Tools
404
+ {
405
+ name: 'setup_metered_billing',
406
+ description: 'Set up pay-per-call billing. Creates a subscription that charges $0.002 per API call at end of month.',
407
+ inputSchema: {
408
+ type: 'object',
409
+ properties: {
410
+ email: {
411
+ type: 'string',
412
+ description: 'Email for the billing account'
413
+ },
414
+ success_url: {
415
+ type: 'string',
416
+ description: 'URL to redirect after successful setup',
417
+ default: 'https://apiclaw.nordsym.com/billing/success'
418
+ },
419
+ cancel_url: {
420
+ type: 'string',
421
+ description: 'URL to redirect if setup is cancelled',
422
+ default: 'https://apiclaw.nordsym.com/billing/cancel'
423
+ }
424
+ },
425
+ required: ['email']
426
+ }
427
+ },
428
+ {
429
+ name: 'get_usage_summary',
430
+ description: 'Get current billing period usage and estimated cost for metered billing.',
431
+ inputSchema: {
432
+ type: 'object',
433
+ properties: {
434
+ subscription_id: {
435
+ type: 'string',
436
+ description: 'Stripe subscription ID (stored after setup_metered_billing)'
437
+ }
438
+ },
439
+ required: ['subscription_id']
440
+ }
441
+ },
442
+ {
443
+ name: 'estimate_cost',
444
+ description: 'Estimate the cost for a given number of API calls.',
445
+ inputSchema: {
446
+ type: 'object',
447
+ properties: {
448
+ call_count: {
449
+ type: 'number',
450
+ description: 'Number of API calls to estimate cost for'
451
+ }
452
+ },
453
+ required: ['call_count']
454
+ }
395
455
  }
396
456
  ];
397
457
 
@@ -1201,6 +1261,151 @@ Docs: https://apiclaw.nordsym.com
1201
1261
  }
1202
1262
  }
1203
1263
 
1264
+ // Metered Billing Tools
1265
+ case 'setup_metered_billing': {
1266
+ const { email, success_url, cancel_url } = args as {
1267
+ email: string;
1268
+ success_url?: string;
1269
+ cancel_url?: string;
1270
+ };
1271
+
1272
+ if (!email) {
1273
+ return {
1274
+ content: [{
1275
+ type: 'text',
1276
+ text: JSON.stringify({ status: 'error', error: 'Email is required' }, null, 2)
1277
+ }],
1278
+ isError: true
1279
+ };
1280
+ }
1281
+
1282
+ // Create or get customer
1283
+ const customerResult = await getOrCreateCustomer(email, email);
1284
+ if ('error' in customerResult) {
1285
+ return {
1286
+ content: [{
1287
+ type: 'text',
1288
+ text: JSON.stringify({ status: 'error', error: customerResult.error }, null, 2)
1289
+ }],
1290
+ isError: true
1291
+ };
1292
+ }
1293
+
1294
+ // Create checkout session for metered subscription
1295
+ const checkoutResult = await createMeteredCheckoutSession(
1296
+ email,
1297
+ success_url || 'https://apiclaw.nordsym.com/billing/success',
1298
+ cancel_url || 'https://apiclaw.nordsym.com/billing/cancel'
1299
+ );
1300
+
1301
+ if ('error' in checkoutResult) {
1302
+ return {
1303
+ content: [{
1304
+ type: 'text',
1305
+ text: JSON.stringify({ status: 'error', error: checkoutResult.error }, null, 2)
1306
+ }],
1307
+ isError: true
1308
+ };
1309
+ }
1310
+
1311
+ return {
1312
+ content: [{
1313
+ type: 'text',
1314
+ text: JSON.stringify({
1315
+ status: 'checkout_ready',
1316
+ message: 'Complete checkout to activate pay-per-call billing',
1317
+ checkout_url: checkoutResult.url,
1318
+ session_id: checkoutResult.sessionId,
1319
+ customer_id: customerResult.customerId,
1320
+ pricing: {
1321
+ per_call: '$0.002',
1322
+ billing_period: 'monthly',
1323
+ billed_at: 'end of period based on usage'
1324
+ }
1325
+ }, null, 2)
1326
+ }]
1327
+ };
1328
+ }
1329
+
1330
+ case 'get_usage_summary': {
1331
+ const { subscription_id } = args as { subscription_id: string };
1332
+
1333
+ if (!subscription_id) {
1334
+ return {
1335
+ content: [{
1336
+ type: 'text',
1337
+ text: JSON.stringify({ status: 'error', error: 'subscription_id is required' }, null, 2)
1338
+ }],
1339
+ isError: true
1340
+ };
1341
+ }
1342
+
1343
+ const usage = await getUsageSummary(subscription_id);
1344
+ if ('error' in usage) {
1345
+ return {
1346
+ content: [{
1347
+ type: 'text',
1348
+ text: JSON.stringify({ status: 'error', error: usage.error }, null, 2)
1349
+ }],
1350
+ isError: true
1351
+ };
1352
+ }
1353
+
1354
+ return {
1355
+ content: [{
1356
+ type: 'text',
1357
+ text: JSON.stringify({
1358
+ status: 'success',
1359
+ billing_period: {
1360
+ start: new Date(usage.period.start * 1000).toISOString(),
1361
+ end: new Date(usage.period.end * 1000).toISOString()
1362
+ },
1363
+ usage: {
1364
+ total_calls: usage.totalCalls,
1365
+ price_per_call: METERED_BILLING.pricePerCall,
1366
+ estimated_cost: `$${usage.totalCost.toFixed(4)}`
1367
+ }
1368
+ }, null, 2)
1369
+ }]
1370
+ };
1371
+ }
1372
+
1373
+ case 'estimate_cost': {
1374
+ const { call_count } = args as { call_count: number };
1375
+
1376
+ if (!call_count || call_count < 0) {
1377
+ return {
1378
+ content: [{
1379
+ type: 'text',
1380
+ text: JSON.stringify({ status: 'error', error: 'Valid call_count is required' }, null, 2)
1381
+ }],
1382
+ isError: true
1383
+ };
1384
+ }
1385
+
1386
+ const estimate = estimateCost(call_count);
1387
+
1388
+ return {
1389
+ content: [{
1390
+ type: 'text',
1391
+ text: JSON.stringify({
1392
+ status: 'success',
1393
+ estimate: {
1394
+ calls: estimate.calls,
1395
+ price_per_call: `$${estimate.pricePerCall}`,
1396
+ total_cost: `$${estimate.totalCost.toFixed(4)}`,
1397
+ currency: estimate.currency
1398
+ },
1399
+ examples: {
1400
+ '100 calls': `$${(100 * METERED_BILLING.pricePerCall).toFixed(2)}`,
1401
+ '1,000 calls': `$${(1000 * METERED_BILLING.pricePerCall).toFixed(2)}`,
1402
+ '10,000 calls': `$${(10000 * METERED_BILLING.pricePerCall).toFixed(2)}`
1403
+ }
1404
+ }, null, 2)
1405
+ }]
1406
+ };
1407
+ }
1408
+
1204
1409
  default:
1205
1410
  return {
1206
1411
  content: [
package/src/metered.ts ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * APIClaw Metered Billing - Pay-per-call execution wrapper
3
+ *
4
+ * Usage:
5
+ * 1. Customer signs up with metered subscription
6
+ * 2. Each API call via executeMetered() reports usage to Stripe
7
+ * 3. Customer is billed at end of billing period based on usage
8
+ */
9
+
10
+ import { executeAPICall } from './execute.js';
11
+ import { reportUsage, hasActiveMeteredSubscription, METERED_BILLING } from './stripe.js';
12
+
13
+ interface MeteredResult {
14
+ success: boolean;
15
+ provider: string;
16
+ action: string;
17
+ data?: unknown;
18
+ error?: string;
19
+ cost?: number;
20
+ billing?: {
21
+ tracked: boolean;
22
+ customerId?: string;
23
+ pricePerCall: number;
24
+ };
25
+ }
26
+
27
+ interface MeteredOptions {
28
+ /** Stripe customer ID for usage tracking */
29
+ customerId?: string;
30
+ /** Skip usage reporting (for testing/free tier) */
31
+ skipBilling?: boolean;
32
+ /** Provider-specific API key (Direct Call mode) */
33
+ customerKey?: string;
34
+ /** User ID for dynamic providers */
35
+ userId?: string;
36
+ }
37
+
38
+ /**
39
+ * Execute an API call with metered billing tracking
40
+ *
41
+ * If customerId is provided and has active metered subscription,
42
+ * usage is reported to Stripe after successful execution.
43
+ */
44
+ export async function executeMetered(
45
+ providerId: string,
46
+ action: string,
47
+ params: Record<string, unknown>,
48
+ options: MeteredOptions = {}
49
+ ): Promise<MeteredResult> {
50
+ const { customerId, skipBilling, customerKey, userId } = options;
51
+
52
+ // Execute the API call
53
+ const result = await executeAPICall(
54
+ providerId,
55
+ action,
56
+ params,
57
+ userId,
58
+ customerKey
59
+ );
60
+
61
+ // Build response
62
+ const response: MeteredResult = {
63
+ ...result,
64
+ billing: {
65
+ tracked: false,
66
+ pricePerCall: METERED_BILLING.pricePerCall,
67
+ },
68
+ };
69
+
70
+ // Skip billing if requested or no customer
71
+ if (skipBilling || !customerId) {
72
+ return response;
73
+ }
74
+
75
+ // Skip billing for failed calls
76
+ if (!result.success) {
77
+ return response;
78
+ }
79
+
80
+ // Check for active metered subscription
81
+ const subscription = await hasActiveMeteredSubscription(customerId);
82
+ if (!subscription.active) {
83
+ console.log(`Customer ${customerId} has no active metered subscription`);
84
+ return response;
85
+ }
86
+
87
+ // Report usage to Stripe meter
88
+ const usageReport = await reportUsage(
89
+ customerId,
90
+ 1,
91
+ `${customerId}_${providerId}_${action}_${Date.now()}`
92
+ );
93
+
94
+ if (usageReport.success) {
95
+ response.billing = {
96
+ tracked: true,
97
+ customerId,
98
+ pricePerCall: METERED_BILLING.pricePerCall,
99
+ };
100
+ } else {
101
+ console.error(`Failed to report usage for ${customerId}:`, usageReport.error);
102
+ }
103
+
104
+ return response;
105
+ }
106
+
107
+ /**
108
+ * Execute multiple API calls in batch with usage tracking
109
+ */
110
+ export async function executeMeteredBatch(
111
+ calls: Array<{
112
+ provider: string;
113
+ action: string;
114
+ params: Record<string, unknown>;
115
+ }>,
116
+ options: MeteredOptions = {}
117
+ ): Promise<MeteredResult[]> {
118
+ const results = await Promise.all(
119
+ calls.map((call) =>
120
+ executeMetered(call.provider, call.action, call.params, options)
121
+ )
122
+ );
123
+
124
+ return results;
125
+ }
126
+
127
+ /**
128
+ * Calculate estimated cost for a number of API calls
129
+ */
130
+ export function estimateCost(callCount: number): {
131
+ calls: number;
132
+ pricePerCall: number;
133
+ totalCost: number;
134
+ currency: string;
135
+ } {
136
+ return {
137
+ calls: callCount,
138
+ pricePerCall: METERED_BILLING.pricePerCall,
139
+ totalCost: callCount * METERED_BILLING.pricePerCall,
140
+ currency: 'USD',
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Get metered billing configuration
146
+ */
147
+ export function getMeteredConfig(): typeof METERED_BILLING {
148
+ return METERED_BILLING;
149
+ }