@nordsym/apiclaw 2.2.0 → 2.3.0

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 (176) hide show
  1. package/README.md +15 -2
  2. package/dist/bin-http.js +0 -0
  3. package/dist/bin.bundled.js +79288 -0
  4. package/dist/gateway-client.d.ts.map +1 -1
  5. package/dist/gateway-client.js +24 -2
  6. package/dist/gateway-client.js.map +1 -1
  7. package/dist/index.bundled.js +61263 -0
  8. package/dist/index.js +2 -2
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -2
  11. package/.claude/settings.local.json +0 -13
  12. package/.env.prod +0 -1
  13. package/apiclaw-README.md +0 -494
  14. package/convex/_generated/api.d.ts +0 -145
  15. package/convex/_generated/api.js +0 -23
  16. package/convex/_generated/dataModel.d.ts +0 -60
  17. package/convex/_generated/server.d.ts +0 -143
  18. package/convex/_generated/server.js +0 -93
  19. package/convex/_listWorkspaces.ts +0 -13
  20. package/convex/adminActivate.ts +0 -53
  21. package/convex/adminStats.ts +0 -306
  22. package/convex/agents.ts +0 -939
  23. package/convex/analytics.ts +0 -187
  24. package/convex/apiKeys.ts +0 -220
  25. package/convex/backfillAnalytics.ts +0 -272
  26. package/convex/backfillSearchLogs.ts +0 -35
  27. package/convex/billing.ts +0 -834
  28. package/convex/capabilities.ts +0 -157
  29. package/convex/chains.ts +0 -1318
  30. package/convex/credits.ts +0 -211
  31. package/convex/crons.ts +0 -65
  32. package/convex/debugFilestackLogs.ts +0 -16
  33. package/convex/debugGetToken.ts +0 -18
  34. package/convex/directCall.ts +0 -713
  35. package/convex/earnProgress.ts +0 -753
  36. package/convex/email.ts +0 -329
  37. package/convex/feedback.ts +0 -265
  38. package/convex/funnel.ts +0 -431
  39. package/convex/guards.ts +0 -174
  40. package/convex/http.ts +0 -3756
  41. package/convex/inbound.ts +0 -32
  42. package/convex/logs.ts +0 -701
  43. package/convex/migrateFilestack.ts +0 -81
  44. package/convex/migratePartnersProd.ts +0 -174
  45. package/convex/migratePratham.ts +0 -126
  46. package/convex/migrateProviderWorkspaces.ts +0 -175
  47. package/convex/mou.ts +0 -91
  48. package/convex/nurture.ts +0 -355
  49. package/convex/providerKeys.ts +0 -289
  50. package/convex/providers.ts +0 -1135
  51. package/convex/purchases.ts +0 -183
  52. package/convex/ratelimit.ts +0 -104
  53. package/convex/schema.ts +0 -926
  54. package/convex/searchLogs.ts +0 -265
  55. package/convex/seedAPILayerAPIs.ts +0 -191
  56. package/convex/seedDirectCallConfigs.ts +0 -336
  57. package/convex/seedPratham.ts +0 -149
  58. package/convex/spendAlerts.ts +0 -442
  59. package/convex/stripeActions.ts +0 -607
  60. package/convex/teams.ts +0 -243
  61. package/convex/telemetry.ts +0 -81
  62. package/convex/tsconfig.json +0 -25
  63. package/convex/updateAPIStatus.ts +0 -44
  64. package/convex/usage.ts +0 -260
  65. package/convex/usageReports.ts +0 -357
  66. package/convex/waitlist.ts +0 -55
  67. package/convex/webhooks.ts +0 -494
  68. package/convex/workspaceSettings.ts +0 -143
  69. package/convex/workspaces.ts +0 -1331
  70. package/convex.json +0 -3
  71. package/direct-test.mjs +0 -51
  72. package/email-templates/filestack-provider-outreach.html +0 -162
  73. package/email-templates/partnership-template.html +0 -116
  74. package/email-templates/pratham-draft-preview.txt +0 -57
  75. package/email-templates/pratham-partnership-draft.html +0 -141
  76. package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
  77. package/reports/pipeline/PIPELINE-REPORT.json +0 -153
  78. package/reports/pipeline/acquire_apisguru.json +0 -17
  79. package/reports/pipeline/capabilities.json +0 -38
  80. package/reports/pipeline/discover_azure_recursive.json +0 -1551
  81. package/reports/pipeline/discover_github.json +0 -25
  82. package/reports/pipeline/discover_github_repos.json +0 -49
  83. package/reports/pipeline/discover_swaggerhub.json +0 -24
  84. package/reports/pipeline/discover_well_known.json +0 -23
  85. package/reports/pipeline/fetch_specs.json +0 -19
  86. package/reports/pipeline/generate_providers.json +0 -14
  87. package/reports/pipeline/match_registry.json +0 -11
  88. package/reports/pipeline/parse_specs.json +0 -17
  89. package/reports/pipeline/promote_candidates.json +0 -34
  90. package/reports/pipeline/validate.json +0 -30
  91. package/reports/pipeline/validate_smoke_details.json +0 -3835
  92. package/reports/session-report-2026-04-05.html +0 -433
  93. package/seed-apis-direct.mjs +0 -106
  94. package/src/access-control.ts +0 -174
  95. package/src/adapters/base.ts +0 -364
  96. package/src/adapters/claude-desktop.ts +0 -41
  97. package/src/adapters/cline.ts +0 -88
  98. package/src/adapters/continue.ts +0 -91
  99. package/src/adapters/cursor.ts +0 -43
  100. package/src/adapters/custom.ts +0 -188
  101. package/src/adapters/detect.ts +0 -202
  102. package/src/adapters/index.ts +0 -47
  103. package/src/adapters/windsurf.ts +0 -44
  104. package/src/bin-http.ts +0 -45
  105. package/src/bin.ts +0 -34
  106. package/src/capability-router.ts +0 -331
  107. package/src/chainExecutor.ts +0 -730
  108. package/src/chainResolver.test.ts +0 -246
  109. package/src/chainResolver.ts +0 -658
  110. package/src/cli/commands/demo.ts +0 -109
  111. package/src/cli/commands/doctor.ts +0 -435
  112. package/src/cli/commands/index.ts +0 -9
  113. package/src/cli/commands/login.ts +0 -203
  114. package/src/cli/commands/mcp-install.ts +0 -373
  115. package/src/cli/commands/restore.ts +0 -333
  116. package/src/cli/commands/setup.ts +0 -297
  117. package/src/cli/commands/uninstall.ts +0 -240
  118. package/src/cli/index.ts +0 -148
  119. package/src/cli.ts +0 -370
  120. package/src/confirmation.ts +0 -296
  121. package/src/credentials.ts +0 -455
  122. package/src/credits.ts +0 -329
  123. package/src/crypto.ts +0 -75
  124. package/src/discovery.ts +0 -568
  125. package/src/enterprise/env.ts +0 -156
  126. package/src/enterprise/index.ts +0 -7
  127. package/src/enterprise/script-generator.ts +0 -481
  128. package/src/execute-dynamic.ts +0 -617
  129. package/src/execute.ts +0 -2386
  130. package/src/funnel-client.ts +0 -168
  131. package/src/funnel.test.ts +0 -187
  132. package/src/gateway-client.ts +0 -192
  133. package/src/hivr-whitelist.ts +0 -110
  134. package/src/http-api.ts +0 -286
  135. package/src/http-server-minimal.ts +0 -154
  136. package/src/index.ts +0 -2702
  137. package/src/intelligent-gateway.ts +0 -339
  138. package/src/mcp-analytics.ts +0 -156
  139. package/src/metered.ts +0 -149
  140. package/src/open-apis-generated.ts +0 -157
  141. package/src/open-apis.ts +0 -558
  142. package/src/postinstall.ts +0 -40
  143. package/src/product-whitelist.ts +0 -246
  144. package/src/proxy.ts +0 -36
  145. package/src/registration-guard.ts +0 -117
  146. package/src/session.ts +0 -129
  147. package/src/stripe.ts +0 -497
  148. package/src/telemetry.ts +0 -71
  149. package/src/test.ts +0 -135
  150. package/src/types/convex-api.d.ts +0 -20
  151. package/src/types/convex-api.ts +0 -21
  152. package/src/types.ts +0 -109
  153. package/src/ui/colors.ts +0 -219
  154. package/src/ui/errors.ts +0 -394
  155. package/src/ui/index.ts +0 -17
  156. package/src/ui/prompts.ts +0 -390
  157. package/src/ui/spinner.ts +0 -325
  158. package/src/utils/backup.ts +0 -224
  159. package/src/utils/config.ts +0 -318
  160. package/src/utils/os.ts +0 -124
  161. package/src/utils/paths.ts +0 -203
  162. package/src/webhook.ts +0 -107
  163. package/test-10-working.cjs +0 -97
  164. package/test-14-final.cjs +0 -96
  165. package/test-actual-handlers.ts +0 -92
  166. package/test-apilayer-all-14.ts +0 -249
  167. package/test-apilayer-fixed.ts +0 -248
  168. package/test-direct-endpoints.ts +0 -174
  169. package/test-exact-endpoints.ts +0 -144
  170. package/test-final.ts +0 -83
  171. package/test-full-routing.ts +0 -100
  172. package/test-handlers-correct.ts +0 -217
  173. package/test-numverify-key.ts +0 -41
  174. package/test-via-handlers.ts +0 -92
  175. package/test-worldnews.mjs +0 -26
  176. package/tsconfig.json +0 -20
package/convex/logs.ts DELETED
@@ -1,701 +0,0 @@
1
- import { v } from "convex/values";
2
- import { mutation, query } from "./_generated/server";
3
- import { api } from "./_generated/api";
4
-
5
- // ============================================
6
- // MUTATIONS
7
- // ============================================
8
-
9
- /**
10
- * Create a log entry for an API call
11
- * Called after each Direct Call execution
12
- */
13
- export const createLog = mutation({
14
- args: {
15
- token: v.string(),
16
- provider: v.string(),
17
- action: v.string(),
18
- status: v.union(v.literal("success"), v.literal("error")),
19
- latencyMs: v.number(),
20
- errorMessage: v.optional(v.string()),
21
- subagentId: v.optional(v.string()), // from X-APIClaw-Subagent header
22
- },
23
- handler: async (ctx, args) => {
24
- // Verify session and get workspace
25
- const session = await ctx.db
26
- .query("agentSessions")
27
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
28
- .first();
29
-
30
- if (!session) {
31
- throw new Error("Invalid session token");
32
- }
33
-
34
- // Create log entry
35
- return await ctx.db.insert("apiLogs", {
36
- workspaceId: session.workspaceId,
37
- sessionToken: args.token,
38
- subagentId: args.subagentId,
39
- provider: args.provider,
40
- action: args.action,
41
- status: args.status,
42
- latencyMs: args.latencyMs,
43
- errorMessage: args.errorMessage,
44
- createdAt: Date.now(),
45
- });
46
- },
47
- });
48
-
49
- /**
50
- * Internal log creation (when workspaceId is already known)
51
- * Used by execute functions that have already verified the session
52
- */
53
- export const createLogInternal = mutation({
54
- args: {
55
- workspaceId: v.id("workspaces"),
56
- sessionToken: v.string(),
57
- provider: v.string(),
58
- action: v.string(),
59
- status: v.union(v.literal("success"), v.literal("error")),
60
- latencyMs: v.number(),
61
- errorMessage: v.optional(v.string()),
62
- subagentId: v.optional(v.string()), // from X-APIClaw-Subagent header
63
- },
64
- handler: async (ctx, args) => {
65
- return await ctx.db.insert("apiLogs", {
66
- workspaceId: args.workspaceId,
67
- sessionToken: args.sessionToken,
68
- subagentId: args.subagentId,
69
- provider: args.provider,
70
- action: args.action,
71
- status: args.status,
72
- latencyMs: args.latencyMs,
73
- errorMessage: args.errorMessage,
74
- createdAt: Date.now(),
75
- });
76
- },
77
- });
78
-
79
- /**
80
- * Log an inbound call to a provider's workspace
81
- * Called when someone uses an API that belongs to another workspace
82
- */
83
- export const logProviderCall = mutation({
84
- args: {
85
- provider: v.string(),
86
- action: v.string(),
87
- status: v.union(v.literal("success"), v.literal("error")),
88
- latencyMs: v.number(),
89
- callerWorkspaceId: v.string(),
90
- subagentId: v.optional(v.string()),
91
- errorMessage: v.optional(v.string()),
92
- },
93
- handler: async (ctx, args) => {
94
- // Resolve provider → workspace dynamically (no hardcoded email maps)
95
- const providerNameLower = args.provider.toLowerCase();
96
- const allProviders = await ctx.db.query("providers").collect();
97
- const providerRecord = allProviders.find(
98
- (p) =>
99
- p.name.toLowerCase() === providerNameLower ||
100
- (p.company && p.company.toLowerCase() === providerNameLower) ||
101
- p.name.toLowerCase().includes(providerNameLower) ||
102
- (p.company && p.company.toLowerCase().includes(providerNameLower))
103
- );
104
-
105
- let workspace: any = null;
106
- if (providerRecord && (providerRecord as any).workspaceId) {
107
- workspace = await ctx.db.get((providerRecord as any).workspaceId);
108
- }
109
-
110
- // Always log to global provider analytics (even without workspace)
111
- await ctx.db.insert("apiLogs", {
112
- workspaceId: workspace?._id || ("global" as any),
113
- sessionToken: "",
114
- provider: args.provider,
115
- action: args.action,
116
- status: args.status,
117
- latencyMs: args.latencyMs,
118
- direction: "provider-call",
119
- callerWorkspaceId: args.callerWorkspaceId,
120
- subagentId: args.subagentId,
121
- errorMessage: args.errorMessage,
122
- createdAt: Date.now(),
123
- });
124
-
125
- // If provider has workspace, also log as inbound to their workspace
126
- if (!workspace) return { logged: "global" };
127
-
128
- return await ctx.db.insert("apiLogs", {
129
- workspaceId: workspace._id,
130
- sessionToken: "",
131
- provider: args.provider,
132
- action: args.action,
133
- status: args.status,
134
- latencyMs: args.latencyMs,
135
- direction: "inbound",
136
- callerWorkspaceId: args.callerWorkspaceId,
137
- subagentId: args.subagentId,
138
- errorMessage: args.errorMessage,
139
- createdAt: Date.now(),
140
- });
141
- },
142
- });
143
-
144
- /**
145
- * Get analytics for a provider workspace (inbound calls to their APIs)
146
- * Used by API Analytics tab in workspace dashboard
147
- */
148
- export const getProviderAnalytics = query({
149
- args: {
150
- token: v.string(),
151
- hoursBack: v.optional(v.number()),
152
- direction: v.optional(v.string()), // "outbound" = my usage, "inbound" = traffic to my APIs, omit = all
153
- },
154
- handler: async (ctx, { token, hoursBack = 168, direction }) => {
155
- const session = await ctx.db
156
- .query("agentSessions")
157
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
158
- .first();
159
- if (!session) return null;
160
-
161
- const since = Date.now() - hoursBack * 60 * 60 * 1000;
162
-
163
- // Get all logs for this workspace
164
- const allLogs = await ctx.db
165
- .query("apiLogs")
166
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
167
- .collect();
168
-
169
- let periodLogs = allLogs.filter((l) => l.createdAt >= since);
170
-
171
- // Filter by direction if specified
172
- if (direction === "outbound") {
173
- periodLogs = periodLogs.filter((l) => l.direction !== "inbound");
174
- } else if (direction === "inbound") {
175
- periodLogs = periodLogs.filter((l) => l.direction === "inbound");
176
- }
177
-
178
- const inboundLogs = periodLogs.filter((l) => l.direction === "inbound");
179
-
180
- // Daily buckets
181
- const byDay: Record<string, { calls: number; searches: number }> = {};
182
- periodLogs.forEach((l) => {
183
- const day = new Date(l.createdAt).toISOString().split("T")[0];
184
- if (!byDay[day]) byDay[day] = { calls: 0, searches: 0 };
185
- if (l.action.startsWith("discovery:")) {
186
- byDay[day].searches++;
187
- } else {
188
- byDay[day].calls++;
189
- }
190
- });
191
-
192
- // By action
193
- const byAction: Record<string, { calls: number; success: number; type: string }> = {};
194
- periodLogs.forEach((l) => {
195
- const isDiscovery = l.action.startsWith("discovery:");
196
- const displayName = isDiscovery ? l.action.replace("discovery:", "Search: ") : l.action;
197
- if (!byAction[displayName]) byAction[displayName] = { calls: 0, success: 0, type: isDiscovery ? "discovery" : "call" };
198
- byAction[displayName].calls++;
199
- if (l.status === "success") byAction[displayName].success++;
200
- });
201
-
202
- // Unique callers (for inbound)
203
- const uniqueCallers = new Set(inboundLogs.map((l) => l.callerWorkspaceId).filter(Boolean)).size;
204
-
205
- const callLogs = periodLogs.filter((l) => !l.action.startsWith("discovery:"));
206
- const discoveryLogs = periodLogs.filter((l) => l.action.startsWith("discovery:"));
207
-
208
- return {
209
- totalCalls: callLogs.length,
210
- totalDiscoveries: discoveryLogs.length,
211
- inboundCalls: inboundLogs.filter((l) => !l.action.startsWith("discovery:")).length,
212
- uniqueCallers,
213
- byDay: Object.entries(byDay)
214
- .map(([date, data]) => ({ date, calls: data.calls, searches: data.searches }))
215
- .sort((a, b) => a.date.localeCompare(b.date)),
216
- byAction: Object.entries(byAction)
217
- .map(([action, stats]) => ({ action, ...stats }))
218
- .sort((a, b) => b.calls - a.calls),
219
- successRate: periodLogs.length > 0
220
- ? (periodLogs.filter((l) => l.status === "success").length / periodLogs.length) * 100
221
- : 100,
222
- avgLatency: periodLogs.length > 0
223
- ? periodLogs.reduce((sum, l) => sum + l.latencyMs, 0) / periodLogs.length
224
- : 0,
225
- };
226
- },
227
- });
228
-
229
- // ============================================
230
- // HELPER: Get month start
231
- // ============================================
232
-
233
- function getMonthStart(): number {
234
- const now = new Date();
235
- return new Date(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0).getTime();
236
- }
237
-
238
- /**
239
- * Combined log creation + spend tracking (PRD 2.6)
240
- * Creates log entry, tracks spend, returns budget status
241
- * Returns shouldSendAlert: true if 80% threshold crossed (caller should send email)
242
- */
243
- export const createLogWithSpend = mutation({
244
- args: {
245
- workspaceId: v.id("workspaces"),
246
- sessionToken: v.string(),
247
- provider: v.string(),
248
- action: v.string(),
249
- status: v.union(v.literal("success"), v.literal("error")),
250
- latencyMs: v.number(),
251
- costCents: v.number(), // Cost in USD cents
252
- errorMessage: v.optional(v.string()),
253
- subagentId: v.optional(v.string()),
254
- },
255
- handler: async (ctx, args) => {
256
- const now = Date.now();
257
- const monthStart = getMonthStart();
258
-
259
- // 1. Create log entry
260
- const logId = await ctx.db.insert("apiLogs", {
261
- workspaceId: args.workspaceId,
262
- sessionToken: args.sessionToken,
263
- subagentId: args.subagentId,
264
- provider: args.provider,
265
- action: args.action,
266
- status: args.status,
267
- latencyMs: args.latencyMs,
268
- errorMessage: args.errorMessage,
269
- createdAt: now,
270
- });
271
-
272
- // 2. Track spend if successful call with cost
273
- if (args.status === "success" && args.costCents > 0) {
274
- const workspace = await ctx.db.get(args.workspaceId);
275
- if (!workspace) {
276
- return { logId, spendTracked: false };
277
- }
278
-
279
- // Reset monthly spend if new month
280
- let currentSpend = workspace.monthlySpendCents || 0;
281
- if (!workspace.lastSpendResetAt || workspace.lastSpendResetAt < monthStart) {
282
- currentSpend = 0;
283
- }
284
-
285
- // Add new spend
286
- const newSpend = currentSpend + args.costCents;
287
- const budgetCap = workspace.budgetCap;
288
-
289
- // Update workspace
290
- await ctx.db.patch(args.workspaceId, {
291
- monthlySpendCents: newSpend,
292
- lastSpendResetAt: monthStart,
293
- updatedAt: now,
294
- });
295
-
296
- // Check if we need to send alert (80% threshold)
297
- let shouldSendAlert = false;
298
- let budgetExceeded = false;
299
-
300
- if (budgetCap && budgetCap > 0) {
301
- const threshold = budgetCap * 0.8;
302
- const alertAlreadySentThisMonth = workspace.budgetAlertSentAt &&
303
- workspace.budgetAlertSentAt >= monthStart;
304
-
305
- // Check if at 80% and alert not yet sent
306
- if (newSpend >= threshold && !alertAlreadySentThisMonth) {
307
- shouldSendAlert = true;
308
- await ctx.db.patch(args.workspaceId, {
309
- budgetAlertSentAt: now,
310
- });
311
- }
312
-
313
- // Check if budget exceeded
314
- if (newSpend >= budgetCap) {
315
- budgetExceeded = true;
316
- }
317
- }
318
-
319
- return {
320
- logId,
321
- spendTracked: true,
322
- currentSpendCents: newSpend,
323
- budgetCapCents: budgetCap || null,
324
- budgetPercentage: budgetCap ? Math.round((newSpend / budgetCap) * 100) : null,
325
- shouldSendAlert,
326
- budgetExceeded,
327
- email: workspace.email,
328
- };
329
- }
330
-
331
- return { logId, spendTracked: false };
332
- },
333
- });
334
-
335
- // ============================================
336
- // QUERIES
337
- // ============================================
338
-
339
- /**
340
- * Get logs for a workspace with pagination and filters
341
- */
342
- export const getLogs = query({
343
- args: {
344
- token: v.string(),
345
- limit: v.optional(v.number()),
346
- cursor: v.optional(v.number()), // createdAt timestamp for pagination
347
- status: v.optional(v.union(v.literal("success"), v.literal("error"), v.literal("all"))),
348
- provider: v.optional(v.string()),
349
- subagentId: v.optional(v.string()), // filter by subagent
350
- },
351
- handler: async (ctx, args) => {
352
- const limit = args.limit ?? 50;
353
- const status = args.status ?? "all";
354
- const provider = args.provider;
355
- const subagentId = args.subagentId;
356
- const cursor = args.cursor;
357
-
358
- // Verify session
359
- const session = await ctx.db
360
- .query("agentSessions")
361
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
362
- .first();
363
-
364
- if (!session) {
365
- return { logs: [], hasMore: false };
366
- }
367
-
368
- // Get logs for workspace
369
- let query = ctx.db
370
- .query("apiLogs")
371
- .withIndex("by_workspaceId_createdAt", (q) => q.eq("workspaceId", session.workspaceId))
372
- .order("desc");
373
-
374
- // Apply cursor (pagination)
375
- if (cursor) {
376
- query = query.filter((q) => q.lt(q.field("createdAt"), cursor));
377
- }
378
-
379
- // Collect more than limit to check hasMore
380
- const allLogs = await query.take(limit + 1);
381
-
382
- // Apply filters in-memory (Convex doesn't support complex compound filters)
383
- let filteredLogs = allLogs;
384
-
385
- if (status !== "all") {
386
- filteredLogs = filteredLogs.filter((log) => log.status === status);
387
- }
388
-
389
- if (provider && provider !== "all") {
390
- filteredLogs = filteredLogs.filter((log) => log.provider === provider);
391
- }
392
-
393
- // Filter by subagent
394
- if (subagentId) {
395
- if (subagentId === "main") {
396
- // Main agent calls (no subagentId)
397
- filteredLogs = filteredLogs.filter((log) => !log.subagentId);
398
- } else {
399
- filteredLogs = filteredLogs.filter((log) => log.subagentId === subagentId);
400
- }
401
- }
402
-
403
- const hasMore = filteredLogs.length > limit;
404
- const logs = filteredLogs.slice(0, limit);
405
-
406
- return {
407
- logs: logs.map((log) => ({
408
- id: log._id,
409
- provider: log.provider,
410
- action: log.action,
411
- status: log.status,
412
- latencyMs: log.latencyMs,
413
- errorMessage: log.errorMessage,
414
- subagentId: log.subagentId || null,
415
- createdAt: log.createdAt,
416
- })),
417
- hasMore,
418
- nextCursor: logs.length > 0 ? logs[logs.length - 1].createdAt : undefined,
419
- };
420
- },
421
- });
422
-
423
- /**
424
- * Get aggregated log stats for workspace
425
- */
426
- export const getLogStats = query({
427
- args: {
428
- token: v.string(),
429
- periodDays: v.optional(v.number()),
430
- },
431
- handler: async (ctx, args) => {
432
- const periodDays = args.periodDays ?? 7;
433
-
434
- // Verify session
435
- const session = await ctx.db
436
- .query("agentSessions")
437
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
438
- .first();
439
-
440
- if (!session) {
441
- return {
442
- totalCalls: 0,
443
- successCount: 0,
444
- errorCount: 0,
445
- successRate: 0,
446
- avgLatency: 0,
447
- byProvider: [],
448
- byDay: [],
449
- };
450
- }
451
-
452
- const now = Date.now();
453
- const periodStart = now - periodDays * 24 * 60 * 60 * 1000;
454
-
455
- // Get all logs for this workspace in the period
456
- const logs = await ctx.db
457
- .query("apiLogs")
458
- .withIndex("by_workspaceId_createdAt", (q) => q.eq("workspaceId", session.workspaceId))
459
- .filter((q) => q.gte(q.field("createdAt"), periodStart))
460
- .collect();
461
-
462
- const totalCalls = logs.length;
463
- const successCount = logs.filter((l) => l.status === "success").length;
464
- const errorCount = logs.filter((l) => l.status === "error").length;
465
- const successRate = totalCalls > 0 ? (successCount / totalCalls) * 100 : 0;
466
- const totalLatency = logs.reduce((sum, l) => sum + l.latencyMs, 0);
467
- const avgLatency = totalCalls > 0 ? Math.round(totalLatency / totalCalls) : 0;
468
-
469
- // Group by provider
470
- const byProviderMap: Record<string, { calls: number; success: number; error: number; latency: number }> = {};
471
- for (const log of logs) {
472
- if (!byProviderMap[log.provider]) {
473
- byProviderMap[log.provider] = { calls: 0, success: 0, error: 0, latency: 0 };
474
- }
475
- byProviderMap[log.provider].calls++;
476
- byProviderMap[log.provider].latency += log.latencyMs;
477
- if (log.status === "success") {
478
- byProviderMap[log.provider].success++;
479
- } else {
480
- byProviderMap[log.provider].error++;
481
- }
482
- }
483
-
484
- const byProvider = Object.entries(byProviderMap)
485
- .map(([provider, data]) => ({
486
- provider,
487
- calls: data.calls,
488
- successRate: data.calls > 0 ? (data.success / data.calls) * 100 : 0,
489
- avgLatency: data.calls > 0 ? Math.round(data.latency / data.calls) : 0,
490
- }))
491
- .sort((a, b) => b.calls - a.calls);
492
-
493
- // Group by day
494
- const byDayMap: Record<string, { calls: number; success: number; error: number }> = {};
495
- for (const log of logs) {
496
- const day = new Date(log.createdAt).toISOString().split("T")[0];
497
- if (!byDayMap[day]) {
498
- byDayMap[day] = { calls: 0, success: 0, error: 0 };
499
- }
500
- byDayMap[day].calls++;
501
- if (log.status === "success") {
502
- byDayMap[day].success++;
503
- } else {
504
- byDayMap[day].error++;
505
- }
506
- }
507
-
508
- const byDay = Object.entries(byDayMap)
509
- .map(([date, data]) => ({
510
- date,
511
- calls: data.calls,
512
- success: data.success,
513
- error: data.error,
514
- }))
515
- .sort((a, b) => a.date.localeCompare(b.date));
516
-
517
- // Get unique providers and agents for filter dropdowns
518
- const providers = [...new Set(logs.map((l) => l.provider))].sort();
519
- const agents = [...new Set(logs.map((l) => l.subagentId || "main"))].sort();
520
-
521
- return {
522
- totalCalls,
523
- successCount,
524
- errorCount,
525
- successRate: Math.round(successRate * 10) / 10,
526
- avgLatency,
527
- byProvider,
528
- byDay,
529
- providers,
530
- agents,
531
- };
532
- },
533
- });
534
-
535
- /**
536
- * Get unique providers for filter dropdown
537
- */
538
- export const getProviders = query({
539
- args: {
540
- token: v.string(),
541
- },
542
- handler: async (ctx, args) => {
543
- // Verify session
544
- const session = await ctx.db
545
- .query("agentSessions")
546
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
547
- .first();
548
-
549
- if (!session) {
550
- return [];
551
- }
552
-
553
- // Get all logs for this workspace
554
- const logs = await ctx.db
555
- .query("apiLogs")
556
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
557
- .collect();
558
-
559
- // Get unique providers
560
- const providers = [...new Set(logs.map((l) => l.provider))].sort();
561
- return providers;
562
- },
563
- });
564
-
565
- /**
566
- * Get logs for a specific subagent
567
- */
568
- export const getBySubagent = query({
569
- args: {
570
- token: v.string(),
571
- subagentId: v.string(),
572
- limit: v.optional(v.number()),
573
- },
574
- handler: async (ctx, { token, subagentId, limit = 20 }) => {
575
- // Verify session
576
- const session = await ctx.db
577
- .query("agentSessions")
578
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
579
- .first();
580
-
581
- if (!session) return null;
582
-
583
- // Get API logs for this subagent
584
- const apiLogs = await ctx.db
585
- .query("apiLogs")
586
- .withIndex("by_subagentId", (q) => q.eq("subagentId", subagentId))
587
- .order("desc")
588
- .take(limit);
589
-
590
- // Get search logs for this subagent
591
- const searchLogs = await ctx.db
592
- .query("searchLogs")
593
- .filter((q) => q.eq(q.field("subagentId"), subagentId))
594
- .order("desc")
595
- .take(limit);
596
-
597
- // Merge and sort by timestamp
598
- const combined = [
599
- ...apiLogs.map(l => ({
600
- ...l,
601
- type: "direct_call" as const,
602
- timestamp: l.createdAt
603
- })),
604
- ...searchLogs.map(l => ({
605
- ...l,
606
- type: "search" as const,
607
- timestamp: l.timestamp
608
- })),
609
- ].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
610
-
611
- return combined.slice(0, limit);
612
- },
613
- });
614
-
615
- /**
616
- * Clear all logs for a workspace (admin cleanup)
617
- */
618
- export const clearWorkspaceLogs = mutation({
619
- args: {
620
- token: v.string(),
621
- },
622
- handler: async (ctx, { token }) => {
623
- const session = await ctx.db
624
- .query("agentSessions")
625
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
626
- .first();
627
-
628
- if (!session) throw new Error("Invalid session");
629
-
630
- // Delete all apiLogs
631
- const apiLogs = await ctx.db
632
- .query("apiLogs")
633
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
634
- .collect();
635
-
636
- for (const log of apiLogs) {
637
- await ctx.db.delete(log._id);
638
- }
639
-
640
- // Delete all searchLogs
641
- const searchLogs = await ctx.db
642
- .query("searchLogs")
643
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
644
- .collect();
645
-
646
- for (const log of searchLogs) {
647
- await ctx.db.delete(log._id);
648
- }
649
-
650
- return {
651
- deleted: {
652
- apiLogs: apiLogs.length,
653
- searchLogs: searchLogs.length
654
- }
655
- };
656
- },
657
- });
658
-
659
- // Log proxy API calls from external agents (Hivr bees)
660
- export const createProxyLog = mutation({
661
- args: {
662
- workspaceId: v.id("workspaces"),
663
- provider: v.string(),
664
- action: v.string(),
665
- subagentId: v.optional(v.string()),
666
- sessionToken: v.optional(v.string()),
667
- },
668
- handler: async (ctx, { workspaceId, provider, action, subagentId, sessionToken }) => {
669
- const now = Date.now();
670
-
671
- await ctx.db.insert("apiLogs", {
672
- workspaceId,
673
- provider,
674
- action,
675
- subagentId: subagentId || "unknown",
676
- sessionToken: sessionToken || "proxy",
677
- status: "success",
678
- latencyMs: 0,
679
- direction: "outbound",
680
- createdAt: now,
681
- });
682
-
683
- // Update workspace lastActiveAt (main agent activity)
684
- await ctx.db.patch(workspaceId, { lastActiveAt: now });
685
-
686
- // If this is a subagent call, update that subagent's timestamp
687
- if (subagentId && subagentId !== "unknown" && subagentId !== "main") {
688
- const subagent = await ctx.db
689
- .query("subagents")
690
- .withIndex("by_workspaceId_subagentId", (q) =>
691
- q.eq("workspaceId", workspaceId).eq("subagentId", subagentId))
692
- .first();
693
-
694
- if (subagent) {
695
- await ctx.db.patch(subagent._id, { lastActiveAt: now });
696
- }
697
- }
698
-
699
- return { success: true };
700
- },
701
- });