@nordsym/apiclaw 1.8.7 → 1.8.9

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