@nordsym/apiclaw 1.5.13 → 1.5.14

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 (68) hide show
  1. package/dist/bin.js +1 -1
  2. package/dist/cli/index.js +7 -0
  3. package/dist/convex/adminActivate.js +46 -0
  4. package/dist/convex/adminStats.js +41 -0
  5. package/dist/convex/agents.js +498 -0
  6. package/dist/convex/analytics.js +165 -0
  7. package/dist/convex/billing.js +654 -0
  8. package/dist/convex/capabilities.js +144 -0
  9. package/dist/convex/chains.js +1041 -0
  10. package/dist/convex/credits.js +185 -0
  11. package/dist/convex/crons.js +16 -0
  12. package/dist/convex/directCall.js +626 -0
  13. package/dist/convex/earnProgress.js +648 -0
  14. package/dist/convex/email.js +299 -0
  15. package/dist/convex/feedback.js +226 -0
  16. package/dist/convex/http.js +909 -0
  17. package/dist/convex/logs.js +486 -0
  18. package/dist/convex/mou.js +81 -0
  19. package/dist/convex/providerKeys.js +256 -0
  20. package/dist/convex/providers.js +755 -0
  21. package/dist/convex/purchases.js +156 -0
  22. package/dist/convex/ratelimit.js +90 -0
  23. package/dist/convex/schema.js +709 -0
  24. package/dist/convex/searchLogs.js +128 -0
  25. package/dist/convex/spendAlerts.js +379 -0
  26. package/dist/convex/stripeActions.js +410 -0
  27. package/dist/convex/teams.js +214 -0
  28. package/dist/convex/telemetry.js +73 -0
  29. package/dist/convex/usage.js +228 -0
  30. package/dist/convex/waitlist.js +48 -0
  31. package/dist/convex/webhooks.js +409 -0
  32. package/dist/convex/workspaces.js +879 -0
  33. package/dist/src/analytics.js +129 -0
  34. package/dist/src/bin.js +17 -0
  35. package/dist/src/capability-router.js +240 -0
  36. package/dist/src/chainExecutor.js +451 -0
  37. package/dist/src/chainResolver.js +518 -0
  38. package/dist/src/cli/commands/doctor.js +324 -0
  39. package/dist/src/cli/commands/mcp-install.js +255 -0
  40. package/dist/src/cli/commands/restore.js +259 -0
  41. package/dist/src/cli/commands/setup.js +205 -0
  42. package/dist/src/cli/commands/uninstall.js +188 -0
  43. package/dist/src/cli/index.js +111 -0
  44. package/dist/src/cli.js +302 -0
  45. package/dist/src/confirmation.js +240 -0
  46. package/dist/src/credentials.js +357 -0
  47. package/dist/src/credits.js +260 -0
  48. package/dist/src/crypto.js +66 -0
  49. package/dist/src/discovery.js +504 -0
  50. package/dist/src/enterprise/env.js +123 -0
  51. package/dist/src/enterprise/script-generator.js +460 -0
  52. package/dist/src/execute-dynamic.js +473 -0
  53. package/dist/src/execute.js +1727 -0
  54. package/dist/src/index.js +2062 -0
  55. package/dist/src/metered.js +80 -0
  56. package/dist/src/open-apis.js +276 -0
  57. package/dist/src/proxy.js +28 -0
  58. package/dist/src/session.js +86 -0
  59. package/dist/src/stripe.js +407 -0
  60. package/dist/src/telemetry.js +49 -0
  61. package/dist/src/types.js +2 -0
  62. package/dist/src/utils/backup.js +181 -0
  63. package/dist/src/utils/config.js +220 -0
  64. package/dist/src/utils/os.js +105 -0
  65. package/dist/src/utils/paths.js +159 -0
  66. package/package.json +1 -1
  67. package/src/bin.ts +1 -1
  68. package/src/cli/index.ts +8 -0
@@ -0,0 +1,755 @@
1
+ import { mutation, query } from "./_generated/server";
2
+ import { v } from "convex/values";
3
+ // Register a new provider and their first API
4
+ export const registerProvider = mutation({
5
+ args: {
6
+ provider: v.object({
7
+ name: v.string(),
8
+ email: v.string(),
9
+ website: v.optional(v.string()),
10
+ }),
11
+ api: v.object({
12
+ name: v.string(),
13
+ description: v.string(),
14
+ category: v.string(),
15
+ openApiUrl: v.optional(v.string()),
16
+ docsUrl: v.optional(v.string()),
17
+ pricingModel: v.string(),
18
+ pricingNotes: v.optional(v.string()),
19
+ }),
20
+ },
21
+ handler: async (ctx, args) => {
22
+ const now = Date.now();
23
+ // Check if provider already exists by email
24
+ const existing = await ctx.db
25
+ .query("providers")
26
+ .withIndex("by_email", (q) => q.eq("email", args.provider.email))
27
+ .first();
28
+ let providerId;
29
+ if (existing) {
30
+ // Use existing provider
31
+ providerId = existing._id;
32
+ }
33
+ else {
34
+ // Create new provider - auto-approve for now
35
+ providerId = await ctx.db.insert("providers", {
36
+ name: args.provider.name,
37
+ email: args.provider.email,
38
+ website: args.provider.website,
39
+ status: "approved", // Auto-approve for MVP
40
+ createdAt: now,
41
+ updatedAt: now,
42
+ approvedAt: now,
43
+ });
44
+ }
45
+ // Create the API listing - auto-approve for now
46
+ const apiId = await ctx.db.insert("providerAPIs", {
47
+ providerId,
48
+ name: args.api.name,
49
+ description: args.api.description,
50
+ category: args.api.category,
51
+ openApiUrl: args.api.openApiUrl,
52
+ docsUrl: args.api.docsUrl,
53
+ pricingModel: args.api.pricingModel,
54
+ pricingNotes: args.api.pricingNotes,
55
+ status: "approved", // Auto-approve for MVP
56
+ createdAt: now,
57
+ approvedAt: now,
58
+ discoveryCount: 0,
59
+ });
60
+ // Create session for auto-login after registration
61
+ const sessionToken = generateToken();
62
+ const sessionExpiresAt = now + 30 * 24 * 60 * 60 * 1000; // 30 days
63
+ await ctx.db.insert("sessions", {
64
+ providerId,
65
+ token: sessionToken,
66
+ expiresAt: sessionExpiresAt,
67
+ createdAt: now,
68
+ });
69
+ return { providerId, apiId, sessionToken };
70
+ },
71
+ });
72
+ // Get provider by email
73
+ export const getProviderByEmail = query({
74
+ args: { email: v.string() },
75
+ handler: async (ctx, args) => {
76
+ return await ctx.db
77
+ .query("providers")
78
+ .withIndex("by_email", (q) => q.eq("email", args.email))
79
+ .first();
80
+ },
81
+ });
82
+ // Get all APIs for a provider
83
+ export const getProviderAPIs = query({
84
+ args: { providerId: v.id("providers") },
85
+ handler: async (ctx, args) => {
86
+ return await ctx.db
87
+ .query("providerAPIs")
88
+ .withIndex("by_providerId", (q) => q.eq("providerId", args.providerId))
89
+ .collect();
90
+ },
91
+ });
92
+ // Get all approved APIs (for the registry)
93
+ export const getApprovedAPIs = query({
94
+ args: {
95
+ category: v.optional(v.string()),
96
+ limit: v.optional(v.number()),
97
+ },
98
+ handler: async (ctx, args) => {
99
+ const query = ctx.db
100
+ .query("providerAPIs")
101
+ .withIndex("by_status", (q) => q.eq("status", "approved"));
102
+ const apis = await query.collect();
103
+ // Filter by category if provided
104
+ let filtered = args.category
105
+ ? apis.filter((api) => api.category === args.category)
106
+ : apis;
107
+ // Apply limit
108
+ if (args.limit) {
109
+ filtered = filtered.slice(0, args.limit);
110
+ }
111
+ return filtered;
112
+ },
113
+ });
114
+ // Get API categories with counts
115
+ export const getCategories = query({
116
+ handler: async (ctx) => {
117
+ const apis = await ctx.db
118
+ .query("providerAPIs")
119
+ .withIndex("by_status", (q) => q.eq("status", "approved"))
120
+ .collect();
121
+ const categories = {};
122
+ for (const api of apis) {
123
+ categories[api.category] = (categories[api.category] || 0) + 1;
124
+ }
125
+ return Object.entries(categories)
126
+ .map(([name, count]) => ({ name, count }))
127
+ .sort((a, b) => b.count - a.count);
128
+ },
129
+ });
130
+ // Increment discovery count when an agent finds an API
131
+ export const trackDiscovery = mutation({
132
+ args: { apiId: v.id("providerAPIs") },
133
+ handler: async (ctx, args) => {
134
+ const api = await ctx.db.get(args.apiId);
135
+ if (!api)
136
+ return;
137
+ await ctx.db.patch(args.apiId, {
138
+ discoveryCount: (api.discoveryCount || 0) + 1,
139
+ lastDiscoveredAt: Date.now(),
140
+ });
141
+ },
142
+ });
143
+ // Admin: List pending providers
144
+ export const getPendingProviders = query({
145
+ handler: async (ctx) => {
146
+ return await ctx.db
147
+ .query("providers")
148
+ .withIndex("by_status", (q) => q.eq("status", "pending"))
149
+ .collect();
150
+ },
151
+ });
152
+ // Admin: Approve provider
153
+ export const approveProvider = mutation({
154
+ args: { providerId: v.id("providers") },
155
+ handler: async (ctx, args) => {
156
+ await ctx.db.patch(args.providerId, {
157
+ status: "approved",
158
+ approvedAt: Date.now(),
159
+ updatedAt: Date.now(),
160
+ });
161
+ },
162
+ });
163
+ // Admin: Reject provider
164
+ export const rejectProvider = mutation({
165
+ args: { providerId: v.id("providers") },
166
+ handler: async (ctx, args) => {
167
+ await ctx.db.patch(args.providerId, {
168
+ status: "rejected",
169
+ updatedAt: Date.now(),
170
+ });
171
+ },
172
+ });
173
+ // Get provider stats
174
+ export const getProviderStats = query({
175
+ handler: async (ctx) => {
176
+ const providers = await ctx.db.query("providers").collect();
177
+ const apis = await ctx.db.query("providerAPIs").collect();
178
+ return {
179
+ totalProviders: providers.length,
180
+ approvedProviders: providers.filter((p) => p.status === "approved").length,
181
+ pendingProviders: providers.filter((p) => p.status === "pending").length,
182
+ totalAPIs: apis.length,
183
+ approvedAPIs: apis.filter((a) => a.status === "approved").length,
184
+ pendingAPIs: apis.filter((a) => a.status === "pending").length,
185
+ totalDiscoveries: apis.reduce((sum, a) => sum + (a.discoveryCount || 0), 0),
186
+ };
187
+ },
188
+ });
189
+ // ============================================
190
+ // DASHBOARD AUTH & SESSION FUNCTIONS
191
+ // ============================================
192
+ // Create magic link for email auth
193
+ export const createMagicLink = mutation({
194
+ args: { email: v.string() },
195
+ handler: async (ctx, { email }) => {
196
+ const token = generateToken();
197
+ const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
198
+ await ctx.db.insert("magicLinks", {
199
+ email: email.toLowerCase(),
200
+ token,
201
+ expiresAt,
202
+ createdAt: Date.now(),
203
+ });
204
+ return { token, expiresAt };
205
+ },
206
+ });
207
+ // Verify magic link and create session
208
+ export const verifyMagicLink = mutation({
209
+ args: { token: v.string() },
210
+ handler: async (ctx, { token }) => {
211
+ const magicLink = await ctx.db
212
+ .query("magicLinks")
213
+ .withIndex("by_token", (q) => q.eq("token", token))
214
+ .first();
215
+ if (!magicLink) {
216
+ return { success: false, error: "Invalid token" };
217
+ }
218
+ if (magicLink.expiresAt < Date.now()) {
219
+ return { success: false, error: "Token expired" };
220
+ }
221
+ if (magicLink.usedAt) {
222
+ return { success: false, error: "Token already used" };
223
+ }
224
+ // Mark as used
225
+ await ctx.db.patch(magicLink._id, { usedAt: Date.now() });
226
+ // Find or create provider
227
+ let provider = await ctx.db
228
+ .query("providers")
229
+ .withIndex("by_email", (q) => q.eq("email", magicLink.email))
230
+ .first();
231
+ if (!provider) {
232
+ const providerId = await ctx.db.insert("providers", {
233
+ email: magicLink.email,
234
+ name: magicLink.email.split("@")[0],
235
+ status: "approved",
236
+ createdAt: Date.now(),
237
+ updatedAt: Date.now(),
238
+ });
239
+ provider = await ctx.db.get(providerId);
240
+ }
241
+ // Create session
242
+ const sessionToken = generateToken();
243
+ const sessionExpiresAt = Date.now() + 30 * 24 * 60 * 60 * 1000; // 30 days
244
+ await ctx.db.insert("sessions", {
245
+ providerId: provider._id,
246
+ token: sessionToken,
247
+ expiresAt: sessionExpiresAt,
248
+ createdAt: Date.now(),
249
+ });
250
+ return {
251
+ success: true,
252
+ sessionToken,
253
+ provider: {
254
+ id: provider._id,
255
+ email: provider.email,
256
+ name: provider.name,
257
+ },
258
+ };
259
+ },
260
+ });
261
+ // Get current session
262
+ export const getSession = query({
263
+ args: { token: v.string() },
264
+ handler: async (ctx, { token }) => {
265
+ const session = await ctx.db
266
+ .query("sessions")
267
+ .withIndex("by_token", (q) => q.eq("token", token))
268
+ .first();
269
+ if (!session || session.expiresAt < Date.now()) {
270
+ return null;
271
+ }
272
+ const provider = await ctx.db.get(session.providerId);
273
+ if (!provider)
274
+ return null;
275
+ return {
276
+ providerId: provider._id,
277
+ email: provider.email,
278
+ name: provider.name,
279
+ stripeOnboardingComplete: provider.stripeOnboardingComplete,
280
+ };
281
+ },
282
+ });
283
+ // ============================================
284
+ // DASHBOARD ANALYTICS
285
+ // ============================================
286
+ // Get single API by ID
287
+ export const getApiById = query({
288
+ args: { apiId: v.string() },
289
+ handler: async (ctx, args) => {
290
+ // Try to get by document ID
291
+ try {
292
+ const api = await ctx.db.get(args.apiId);
293
+ if (api) {
294
+ // Check if it has Direct Call configured
295
+ const directCall = await ctx.db
296
+ .query("providerDirectCall")
297
+ .filter((q) => q.eq(q.field("apiId"), args.apiId))
298
+ .first();
299
+ return { ...api, hasDirectCall: !!directCall, directCallStatus: directCall?.status };
300
+ }
301
+ }
302
+ catch {
303
+ // Not a valid ID format
304
+ }
305
+ return null;
306
+ },
307
+ });
308
+ // Get provider APIs with Direct Call status
309
+ export const getProviderAPIsWithStatus = query({
310
+ args: { providerId: v.string() },
311
+ handler: async (ctx, args) => {
312
+ const apis = await ctx.db
313
+ .query("providerAPIs")
314
+ .filter((q) => q.eq(q.field("providerId"), args.providerId))
315
+ .collect();
316
+ // Add Direct Call status to each API
317
+ const apisWithStatus = await Promise.all(apis.map(async (api) => {
318
+ const directCall = await ctx.db
319
+ .query("providerDirectCall")
320
+ .filter((q) => q.eq(q.field("apiId"), api._id))
321
+ .first();
322
+ return {
323
+ ...api,
324
+ hasDirectCall: !!directCall,
325
+ directCallStatus: directCall?.status,
326
+ };
327
+ }));
328
+ return apisWithStatus;
329
+ },
330
+ });
331
+ // DEBUG: Delete API
332
+ export const debugDeleteAPI = mutation({
333
+ args: { apiId: v.string() },
334
+ handler: async (ctx, args) => {
335
+ await ctx.db.delete(args.apiId);
336
+ return { deleted: true };
337
+ },
338
+ });
339
+ // Add API for logged-in provider (used by register page)
340
+ export const addAPI = mutation({
341
+ args: {
342
+ token: v.string(),
343
+ api: v.object({
344
+ name: v.string(),
345
+ description: v.string(),
346
+ category: v.string(),
347
+ openApiUrl: v.optional(v.string()),
348
+ docsUrl: v.optional(v.string()),
349
+ pricingModel: v.string(),
350
+ pricingNotes: v.optional(v.string()),
351
+ }),
352
+ },
353
+ handler: async (ctx, args) => {
354
+ // Verify session
355
+ const session = await ctx.db
356
+ .query("sessions")
357
+ .withIndex("by_token", (q) => q.eq("token", args.token))
358
+ .first();
359
+ if (!session || session.expiresAt < Date.now()) {
360
+ throw new Error("Invalid or expired session");
361
+ }
362
+ const now = Date.now();
363
+ const apiId = await ctx.db.insert("providerAPIs", {
364
+ providerId: session.providerId,
365
+ name: args.api.name,
366
+ description: args.api.description,
367
+ category: args.api.category,
368
+ openApiUrl: args.api.openApiUrl,
369
+ docsUrl: args.api.docsUrl,
370
+ pricingModel: args.api.pricingModel,
371
+ pricingNotes: args.api.pricingNotes,
372
+ status: "approved",
373
+ createdAt: now,
374
+ approvedAt: now,
375
+ discoveryCount: 0,
376
+ });
377
+ return { apiId, success: true };
378
+ },
379
+ });
380
+ // Delete API for logged-in provider
381
+ export const deleteAPI = mutation({
382
+ args: {
383
+ token: v.string(),
384
+ apiId: v.string(),
385
+ },
386
+ handler: async (ctx, args) => {
387
+ // Verify session
388
+ const session = await ctx.db
389
+ .query("sessions")
390
+ .withIndex("by_token", (q) => q.eq("token", args.token))
391
+ .first();
392
+ if (!session || session.expiresAt < Date.now()) {
393
+ throw new Error("Invalid or expired session");
394
+ }
395
+ // Get the API and verify ownership
396
+ const api = await ctx.db.get(args.apiId);
397
+ if (!api || api.providerId !== session.providerId) {
398
+ throw new Error("API not found or unauthorized");
399
+ }
400
+ // Delete the API
401
+ await ctx.db.delete(args.apiId);
402
+ // Also delete any Direct Call config
403
+ const directCallConfig = await ctx.db
404
+ .query("providerDirectCall")
405
+ .filter((q) => q.eq(q.field("apiId"), args.apiId))
406
+ .first();
407
+ if (directCallConfig) {
408
+ await ctx.db.delete(directCallConfig._id);
409
+ }
410
+ return { deleted: true };
411
+ },
412
+ });
413
+ // DEBUG: Update provider name
414
+ export const debugUpdateProvider = mutation({
415
+ args: {
416
+ providerId: v.string(),
417
+ name: v.optional(v.string()),
418
+ },
419
+ handler: async (ctx, args) => {
420
+ const updates = {};
421
+ if (args.name)
422
+ updates.name = args.name;
423
+ await ctx.db.patch(args.providerId, updates);
424
+ return { updated: true };
425
+ },
426
+ });
427
+ // DEBUG: Add API for provider (seeding)
428
+ export const debugAddAPI = mutation({
429
+ args: {
430
+ providerId: v.string(),
431
+ name: v.string(),
432
+ description: v.string(),
433
+ category: v.string(),
434
+ docsUrl: v.optional(v.string()),
435
+ pricingModel: v.string(),
436
+ pricingNotes: v.optional(v.string()),
437
+ },
438
+ handler: async (ctx, args) => {
439
+ const now = Date.now();
440
+ return await ctx.db.insert("providerAPIs", {
441
+ providerId: args.providerId,
442
+ name: args.name,
443
+ description: args.description,
444
+ category: args.category,
445
+ docsUrl: args.docsUrl,
446
+ pricingModel: args.pricingModel,
447
+ pricingNotes: args.pricingNotes,
448
+ status: "approved",
449
+ createdAt: now,
450
+ approvedAt: now,
451
+ discoveryCount: 0,
452
+ });
453
+ },
454
+ });
455
+ // DEBUG: Delete provider and all related data
456
+ export const debugDeleteProvider = mutation({
457
+ args: { providerId: v.string() },
458
+ handler: async (ctx, args) => {
459
+ const providerId = args.providerId;
460
+ // Delete sessions
461
+ const sessions = await ctx.db.query("sessions").filter(q => q.eq(q.field("providerId"), providerId)).collect();
462
+ for (const s of sessions)
463
+ await ctx.db.delete(s._id);
464
+ // Delete APIs
465
+ const apis = await ctx.db.query("providerAPIs").filter(q => q.eq(q.field("providerId"), providerId)).collect();
466
+ for (const a of apis)
467
+ await ctx.db.delete(a._id);
468
+ // Delete direct call configs
469
+ const configs = await ctx.db.query("providerDirectCall").filter(q => q.eq(q.field("providerId"), providerId)).collect();
470
+ for (const c of configs) {
471
+ // Delete actions for this config
472
+ const actions = await ctx.db.query("providerActions").filter(q => q.eq(q.field("directCallId"), c._id)).collect();
473
+ for (const act of actions)
474
+ await ctx.db.delete(act._id);
475
+ await ctx.db.delete(c._id);
476
+ }
477
+ // Delete provider
478
+ await ctx.db.delete(providerId);
479
+ return { deleted: true };
480
+ },
481
+ });
482
+ // DEBUG: List all sessions
483
+ export const debugListSessions = query({
484
+ args: {},
485
+ handler: async (ctx) => {
486
+ return await ctx.db.query("sessions").collect();
487
+ },
488
+ });
489
+ // DEBUG: List all providers
490
+ export const debugListProviders = query({
491
+ args: {},
492
+ handler: async (ctx) => {
493
+ return await ctx.db.query("providers").collect();
494
+ },
495
+ });
496
+ export const getAnalytics = query({
497
+ args: {
498
+ token: v.string(),
499
+ period: v.optional(v.string()), // "week", "month", "all"
500
+ },
501
+ handler: async (ctx, { token, period = "month" }) => {
502
+ const session = await ctx.db
503
+ .query("sessions")
504
+ .withIndex("by_token", (q) => q.eq("token", token))
505
+ .first();
506
+ if (!session || session.expiresAt < Date.now()) {
507
+ return null;
508
+ }
509
+ const now = Date.now();
510
+ const periodMs = {
511
+ week: 7 * 24 * 60 * 60 * 1000,
512
+ month: 30 * 24 * 60 * 60 * 1000,
513
+ all: now,
514
+ }[period] || 30 * 24 * 60 * 60 * 1000;
515
+ const startTime = now - periodMs;
516
+ // Get usage logs for this provider (from Direct Call usageLog)
517
+ const usageLogs = await ctx.db
518
+ .query("usageLog")
519
+ .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
520
+ .collect();
521
+ const periodCalls = usageLogs.filter((c) => c.timestamp >= startTime);
522
+ // Calculate metrics
523
+ const totalCalls = periodCalls.length;
524
+ const uniqueAgents = new Set(periodCalls.map((c) => c.userId)).size;
525
+ const totalRevenue = periodCalls.reduce((sum, c) => sum + (c.creditsUsed / 100), 0); // cents to dollars
526
+ const successCount = periodCalls.filter((c) => c.success).length;
527
+ const successRate = totalCalls > 0 ? (successCount / totalCalls) * 100 : 100;
528
+ const avgLatency = totalCalls > 0
529
+ ? periodCalls.reduce((sum, c) => sum + c.latencyMs, 0) / totalCalls
530
+ : 0;
531
+ // Calls over time (daily buckets)
532
+ const callsByDay = {};
533
+ periodCalls.forEach((call) => {
534
+ const day = new Date(call.timestamp).toISOString().split("T")[0];
535
+ if (!callsByDay[day]) {
536
+ callsByDay[day] = { calls: 0, revenue: 0, success: 0 };
537
+ }
538
+ callsByDay[day].calls += 1;
539
+ callsByDay[day].revenue += call.creditsUsed / 100;
540
+ if (call.success)
541
+ callsByDay[day].success += 1;
542
+ });
543
+ // Top agents (users)
544
+ const agentCallCounts = {};
545
+ periodCalls.forEach((call) => {
546
+ agentCallCounts[call.userId] = (agentCallCounts[call.userId] || 0) + 1;
547
+ });
548
+ const topAgents = Object.entries(agentCallCounts)
549
+ .sort((a, b) => b[1] - a[1])
550
+ .slice(0, 10)
551
+ .map(([agentId, calls]) => ({ agentId, calls }));
552
+ // Top actions
553
+ const actionCallCounts = {};
554
+ periodCalls.forEach((call) => {
555
+ actionCallCounts[call.actionName] = (actionCallCounts[call.actionName] || 0) + 1;
556
+ });
557
+ const topActions = Object.entries(actionCallCounts)
558
+ .sort((a, b) => b[1] - a[1])
559
+ .slice(0, 10)
560
+ .map(([actionName, calls]) => ({ actionName, calls }));
561
+ // Get provider's APIs
562
+ const apis = await ctx.db
563
+ .query("providerAPIs")
564
+ .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
565
+ .collect();
566
+ // Get Direct Call configs to map directCallId to apiId
567
+ const directCallConfigs = await ctx.db
568
+ .query("providerDirectCall")
569
+ .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
570
+ .collect();
571
+ // Calls per API (via directCallId → apiId mapping)
572
+ const callsByDirectCallId = {};
573
+ periodCalls.forEach((call) => {
574
+ const dcId = call.directCallId;
575
+ callsByDirectCallId[dcId] = (callsByDirectCallId[dcId] || 0) + 1;
576
+ });
577
+ // Map to apiId
578
+ const callsByApiId = {};
579
+ directCallConfigs.forEach((dc) => {
580
+ if (dc.apiId) {
581
+ callsByApiId[dc.apiId] = callsByDirectCallId[dc._id] || 0;
582
+ }
583
+ });
584
+ // Preview data for providers with no usage yet
585
+ const isPreview = totalCalls === 0;
586
+ if (isPreview) {
587
+ // Generate preview data so providers can see what the dashboard looks like
588
+ const previewDays = [];
589
+ for (let i = 13; i >= 0; i--) {
590
+ const date = new Date(now - i * 24 * 60 * 60 * 1000).toISOString().split("T")[0];
591
+ previewDays.push({
592
+ date,
593
+ calls: Math.floor(Math.random() * 50) + 10,
594
+ revenue: Math.random() * 5,
595
+ });
596
+ }
597
+ return {
598
+ totalCalls: 847,
599
+ uniqueAgents: 23,
600
+ totalRevenue: 42.35,
601
+ successRate: 98.2,
602
+ avgLatency: 145,
603
+ callsByDay: previewDays,
604
+ topAgents: [
605
+ { agentId: "agent_demo_1", calls: 234 },
606
+ { agentId: "agent_demo_2", calls: 189 },
607
+ { agentId: "agent_demo_3", calls: 156 },
608
+ { agentId: "agent_demo_4", calls: 98 },
609
+ { agentId: "agent_demo_5", calls: 67 },
610
+ ],
611
+ topActions: [
612
+ { actionName: "send_message", calls: 412 },
613
+ { actionName: "get_status", calls: 289 },
614
+ { actionName: "create_invoice", calls: 146 },
615
+ ],
616
+ apis: apis.map((api) => ({
617
+ id: api._id,
618
+ name: api.name,
619
+ calls: Math.floor(Math.random() * 200) + 50,
620
+ status: api.status,
621
+ })),
622
+ isPreview: true,
623
+ };
624
+ }
625
+ return {
626
+ totalCalls,
627
+ uniqueAgents,
628
+ totalRevenue,
629
+ successRate,
630
+ avgLatency,
631
+ callsByDay: Object.entries(callsByDay)
632
+ .map(([date, data]) => ({
633
+ date,
634
+ calls: data.calls,
635
+ revenue: data.revenue,
636
+ }))
637
+ .sort((a, b) => a.date.localeCompare(b.date)),
638
+ topAgents,
639
+ topActions,
640
+ apis: apis.map((api) => ({
641
+ id: api._id,
642
+ name: api.name,
643
+ calls: callsByApiId[api._id] || 0,
644
+ status: api.status,
645
+ })),
646
+ isPreview: false,
647
+ };
648
+ },
649
+ });
650
+ // ============================================
651
+ // DASHBOARD EARNINGS
652
+ // ============================================
653
+ export const getEarnings = query({
654
+ args: { token: v.string() },
655
+ handler: async (ctx, { token }) => {
656
+ const session = await ctx.db
657
+ .query("sessions")
658
+ .withIndex("by_token", (q) => q.eq("token", token))
659
+ .first();
660
+ if (!session || session.expiresAt < Date.now()) {
661
+ return null;
662
+ }
663
+ // Get all payouts
664
+ const payouts = await ctx.db
665
+ .query("payouts")
666
+ .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
667
+ .collect();
668
+ // Get all API calls to calculate pending
669
+ const allCalls = await ctx.db
670
+ .query("apiCalls")
671
+ .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
672
+ .collect();
673
+ // Find last completed payout
674
+ const completedPayouts = payouts
675
+ .filter((p) => p.status === "completed")
676
+ .sort((a, b) => b.periodEnd - a.periodEnd);
677
+ const lastPayoutEnd = completedPayouts[0]?.periodEnd || 0;
678
+ // Pending = all revenue since last payout
679
+ const pendingCalls = allCalls.filter((c) => c.timestamp > lastPayoutEnd);
680
+ const pendingAmount = pendingCalls.reduce((sum, c) => sum + c.costUsd, 0);
681
+ // Total earned all time
682
+ const totalEarned = allCalls.reduce((sum, c) => sum + c.costUsd, 0);
683
+ // Get provider for Stripe status
684
+ const provider = await ctx.db.get(session.providerId);
685
+ return {
686
+ pendingAmount,
687
+ totalEarned,
688
+ totalPaidOut: completedPayouts.reduce((sum, p) => sum + p.amountUsd, 0),
689
+ stripeConnected: !!provider?.stripeConnectId,
690
+ stripeOnboardingComplete: provider?.stripeOnboardingComplete || false,
691
+ payouts: payouts
692
+ .sort((a, b) => b.createdAt - a.createdAt)
693
+ .slice(0, 20)
694
+ .map((p) => ({
695
+ id: p._id,
696
+ amount: p.amountUsd,
697
+ status: p.status,
698
+ periodStart: p.periodStart,
699
+ periodEnd: p.periodEnd,
700
+ createdAt: p.createdAt,
701
+ completedAt: p.completedAt,
702
+ })),
703
+ };
704
+ },
705
+ });
706
+ // ============================================
707
+ // ADMIN QUERIES
708
+ // ============================================
709
+ // Get all providers (admin only)
710
+ export const getAllProviders = query({
711
+ handler: async (ctx) => {
712
+ return await ctx.db
713
+ .query("providers")
714
+ .order("desc")
715
+ .collect();
716
+ },
717
+ });
718
+ // Get all APIs (admin only)
719
+ export const getAllAPIs = query({
720
+ handler: async (ctx) => {
721
+ return await ctx.db
722
+ .query("providerAPIs")
723
+ .order("desc")
724
+ .collect();
725
+ },
726
+ });
727
+ // Helper function
728
+ function generateToken() {
729
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
730
+ let result = "";
731
+ for (let i = 0; i < 48; i++) {
732
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
733
+ }
734
+ return result;
735
+ }
736
+ // Debug: Update API name/description
737
+ export const debugUpdateAPI = mutation({
738
+ args: {
739
+ apiId: v.string(),
740
+ name: v.optional(v.string()),
741
+ description: v.optional(v.string()),
742
+ category: v.optional(v.string()),
743
+ },
744
+ handler: async (ctx, args) => {
745
+ const updates = {};
746
+ if (args.name)
747
+ updates.name = args.name;
748
+ if (args.description)
749
+ updates.description = args.description;
750
+ if (args.category)
751
+ updates.category = args.category;
752
+ await ctx.db.patch(args.apiId, updates);
753
+ return { updated: true };
754
+ },
755
+ });