@nordsym/apiclaw 1.5.13 → 1.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +1 -1
- package/dist/cli/commands/mcp-install.js +44 -49
- package/dist/cli/commands/mcp-install.js.map +1 -1
- package/dist/cli/index.js +7 -0
- package/dist/convex/adminActivate.js +46 -0
- package/dist/convex/adminStats.js +41 -0
- package/dist/convex/agents.js +498 -0
- package/dist/convex/analytics.js +165 -0
- package/dist/convex/billing.js +654 -0
- package/dist/convex/capabilities.js +144 -0
- package/dist/convex/chains.js +1041 -0
- package/dist/convex/credits.js +185 -0
- package/dist/convex/crons.js +16 -0
- package/dist/convex/directCall.js +626 -0
- package/dist/convex/earnProgress.js +648 -0
- package/dist/convex/email.js +299 -0
- package/dist/convex/feedback.js +226 -0
- package/dist/convex/http.js +909 -0
- package/dist/convex/logs.js +486 -0
- package/dist/convex/mou.js +81 -0
- package/dist/convex/providerKeys.js +256 -0
- package/dist/convex/providers.js +755 -0
- package/dist/convex/purchases.js +156 -0
- package/dist/convex/ratelimit.js +90 -0
- package/dist/convex/schema.js +709 -0
- package/dist/convex/searchLogs.js +128 -0
- package/dist/convex/spendAlerts.js +379 -0
- package/dist/convex/stripeActions.js +410 -0
- package/dist/convex/teams.js +214 -0
- package/dist/convex/telemetry.js +73 -0
- package/dist/convex/usage.js +228 -0
- package/dist/convex/waitlist.js +48 -0
- package/dist/convex/webhooks.js +409 -0
- package/dist/convex/workspaces.js +879 -0
- package/dist/src/analytics.js +129 -0
- package/dist/src/bin.js +17 -0
- package/dist/src/capability-router.js +240 -0
- package/dist/src/chainExecutor.js +451 -0
- package/dist/src/chainResolver.js +518 -0
- package/dist/src/cli/commands/doctor.js +324 -0
- package/dist/src/cli/commands/mcp-install.js +255 -0
- package/dist/src/cli/commands/restore.js +259 -0
- package/dist/src/cli/commands/setup.js +205 -0
- package/dist/src/cli/commands/uninstall.js +188 -0
- package/dist/src/cli/index.js +111 -0
- package/dist/src/cli.js +302 -0
- package/dist/src/confirmation.js +240 -0
- package/dist/src/credentials.js +357 -0
- package/dist/src/credits.js +260 -0
- package/dist/src/crypto.js +66 -0
- package/dist/src/discovery.js +504 -0
- package/dist/src/enterprise/env.js +123 -0
- package/dist/src/enterprise/script-generator.js +460 -0
- package/dist/src/execute-dynamic.js +473 -0
- package/dist/src/execute.js +1727 -0
- package/dist/src/index.js +2062 -0
- package/dist/src/metered.js +80 -0
- package/dist/src/open-apis.js +276 -0
- package/dist/src/proxy.js +28 -0
- package/dist/src/session.js +86 -0
- package/dist/src/stripe.js +407 -0
- package/dist/src/telemetry.js +49 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/backup.js +181 -0
- package/dist/src/utils/config.js +220 -0
- package/dist/src/utils/os.js +105 -0
- package/dist/src/utils/paths.js +159 -0
- package/package.json +1 -1
- package/src/bin.ts +1 -1
- package/src/cli/index.ts +8 -0
|
@@ -0,0 +1,486 @@
|
|
|
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
|
+
// HELPER: Get month start
|
|
74
|
+
// ============================================
|
|
75
|
+
function getMonthStart() {
|
|
76
|
+
const now = new Date();
|
|
77
|
+
return new Date(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0).getTime();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Combined log creation + spend tracking (PRD 2.6)
|
|
81
|
+
* Creates log entry, tracks spend, returns budget status
|
|
82
|
+
* Returns shouldSendAlert: true if 80% threshold crossed (caller should send email)
|
|
83
|
+
*/
|
|
84
|
+
export const createLogWithSpend = mutation({
|
|
85
|
+
args: {
|
|
86
|
+
workspaceId: v.id("workspaces"),
|
|
87
|
+
sessionToken: v.string(),
|
|
88
|
+
provider: v.string(),
|
|
89
|
+
action: v.string(),
|
|
90
|
+
status: v.union(v.literal("success"), v.literal("error")),
|
|
91
|
+
latencyMs: v.number(),
|
|
92
|
+
costCents: v.number(), // Cost in USD cents
|
|
93
|
+
errorMessage: v.optional(v.string()),
|
|
94
|
+
subagentId: v.optional(v.string()),
|
|
95
|
+
},
|
|
96
|
+
handler: async (ctx, args) => {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
const monthStart = getMonthStart();
|
|
99
|
+
// 1. Create log entry
|
|
100
|
+
const logId = await ctx.db.insert("apiLogs", {
|
|
101
|
+
workspaceId: args.workspaceId,
|
|
102
|
+
sessionToken: args.sessionToken,
|
|
103
|
+
subagentId: args.subagentId,
|
|
104
|
+
provider: args.provider,
|
|
105
|
+
action: args.action,
|
|
106
|
+
status: args.status,
|
|
107
|
+
latencyMs: args.latencyMs,
|
|
108
|
+
errorMessage: args.errorMessage,
|
|
109
|
+
createdAt: now,
|
|
110
|
+
});
|
|
111
|
+
// 2. Track spend if successful call with cost
|
|
112
|
+
if (args.status === "success" && args.costCents > 0) {
|
|
113
|
+
const workspace = await ctx.db.get(args.workspaceId);
|
|
114
|
+
if (!workspace) {
|
|
115
|
+
return { logId, spendTracked: false };
|
|
116
|
+
}
|
|
117
|
+
// Reset monthly spend if new month
|
|
118
|
+
let currentSpend = workspace.monthlySpendCents || 0;
|
|
119
|
+
if (!workspace.lastSpendResetAt || workspace.lastSpendResetAt < monthStart) {
|
|
120
|
+
currentSpend = 0;
|
|
121
|
+
}
|
|
122
|
+
// Add new spend
|
|
123
|
+
const newSpend = currentSpend + args.costCents;
|
|
124
|
+
const budgetCap = workspace.budgetCap;
|
|
125
|
+
// Update workspace
|
|
126
|
+
await ctx.db.patch(args.workspaceId, {
|
|
127
|
+
monthlySpendCents: newSpend,
|
|
128
|
+
lastSpendResetAt: monthStart,
|
|
129
|
+
updatedAt: now,
|
|
130
|
+
});
|
|
131
|
+
// Check if we need to send alert (80% threshold)
|
|
132
|
+
let shouldSendAlert = false;
|
|
133
|
+
let budgetExceeded = false;
|
|
134
|
+
if (budgetCap && budgetCap > 0) {
|
|
135
|
+
const threshold = budgetCap * 0.8;
|
|
136
|
+
const alertAlreadySentThisMonth = workspace.budgetAlertSentAt &&
|
|
137
|
+
workspace.budgetAlertSentAt >= monthStart;
|
|
138
|
+
// Check if at 80% and alert not yet sent
|
|
139
|
+
if (newSpend >= threshold && !alertAlreadySentThisMonth) {
|
|
140
|
+
shouldSendAlert = true;
|
|
141
|
+
await ctx.db.patch(args.workspaceId, {
|
|
142
|
+
budgetAlertSentAt: now,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// Check if budget exceeded
|
|
146
|
+
if (newSpend >= budgetCap) {
|
|
147
|
+
budgetExceeded = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
logId,
|
|
152
|
+
spendTracked: true,
|
|
153
|
+
currentSpendCents: newSpend,
|
|
154
|
+
budgetCapCents: budgetCap || null,
|
|
155
|
+
budgetPercentage: budgetCap ? Math.round((newSpend / budgetCap) * 100) : null,
|
|
156
|
+
shouldSendAlert,
|
|
157
|
+
budgetExceeded,
|
|
158
|
+
email: workspace.email,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { logId, spendTracked: false };
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
// ============================================
|
|
165
|
+
// QUERIES
|
|
166
|
+
// ============================================
|
|
167
|
+
/**
|
|
168
|
+
* Get logs for a workspace with pagination and filters
|
|
169
|
+
*/
|
|
170
|
+
export const getLogs = query({
|
|
171
|
+
args: {
|
|
172
|
+
token: v.string(),
|
|
173
|
+
limit: v.optional(v.number()),
|
|
174
|
+
cursor: v.optional(v.number()), // createdAt timestamp for pagination
|
|
175
|
+
status: v.optional(v.union(v.literal("success"), v.literal("error"), v.literal("all"))),
|
|
176
|
+
provider: v.optional(v.string()),
|
|
177
|
+
subagentId: v.optional(v.string()), // filter by subagent
|
|
178
|
+
},
|
|
179
|
+
handler: async (ctx, args) => {
|
|
180
|
+
const limit = args.limit ?? 50;
|
|
181
|
+
const status = args.status ?? "all";
|
|
182
|
+
const provider = args.provider;
|
|
183
|
+
const subagentId = args.subagentId;
|
|
184
|
+
const cursor = args.cursor;
|
|
185
|
+
// Verify session
|
|
186
|
+
const session = await ctx.db
|
|
187
|
+
.query("agentSessions")
|
|
188
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
189
|
+
.first();
|
|
190
|
+
if (!session) {
|
|
191
|
+
return { logs: [], hasMore: false };
|
|
192
|
+
}
|
|
193
|
+
// Get logs for workspace
|
|
194
|
+
let query = ctx.db
|
|
195
|
+
.query("apiLogs")
|
|
196
|
+
.withIndex("by_workspaceId_createdAt", (q) => q.eq("workspaceId", session.workspaceId))
|
|
197
|
+
.order("desc");
|
|
198
|
+
// Apply cursor (pagination)
|
|
199
|
+
if (cursor) {
|
|
200
|
+
query = query.filter((q) => q.lt(q.field("createdAt"), cursor));
|
|
201
|
+
}
|
|
202
|
+
// Collect more than limit to check hasMore
|
|
203
|
+
const allLogs = await query.take(limit + 1);
|
|
204
|
+
// Apply filters in-memory (Convex doesn't support complex compound filters)
|
|
205
|
+
let filteredLogs = allLogs;
|
|
206
|
+
if (status !== "all") {
|
|
207
|
+
filteredLogs = filteredLogs.filter((log) => log.status === status);
|
|
208
|
+
}
|
|
209
|
+
if (provider && provider !== "all") {
|
|
210
|
+
filteredLogs = filteredLogs.filter((log) => log.provider === provider);
|
|
211
|
+
}
|
|
212
|
+
// Filter by subagent
|
|
213
|
+
if (subagentId) {
|
|
214
|
+
if (subagentId === "main") {
|
|
215
|
+
// Main agent calls (no subagentId)
|
|
216
|
+
filteredLogs = filteredLogs.filter((log) => !log.subagentId);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
filteredLogs = filteredLogs.filter((log) => log.subagentId === subagentId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const hasMore = filteredLogs.length > limit;
|
|
223
|
+
const logs = filteredLogs.slice(0, limit);
|
|
224
|
+
return {
|
|
225
|
+
logs: logs.map((log) => ({
|
|
226
|
+
id: log._id,
|
|
227
|
+
provider: log.provider,
|
|
228
|
+
action: log.action,
|
|
229
|
+
status: log.status,
|
|
230
|
+
latencyMs: log.latencyMs,
|
|
231
|
+
errorMessage: log.errorMessage,
|
|
232
|
+
subagentId: log.subagentId || null,
|
|
233
|
+
createdAt: log.createdAt,
|
|
234
|
+
})),
|
|
235
|
+
hasMore,
|
|
236
|
+
nextCursor: logs.length > 0 ? logs[logs.length - 1].createdAt : undefined,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
/**
|
|
241
|
+
* Get aggregated log stats for workspace
|
|
242
|
+
*/
|
|
243
|
+
export const getLogStats = query({
|
|
244
|
+
args: {
|
|
245
|
+
token: v.string(),
|
|
246
|
+
periodDays: v.optional(v.number()),
|
|
247
|
+
},
|
|
248
|
+
handler: async (ctx, args) => {
|
|
249
|
+
const periodDays = args.periodDays ?? 7;
|
|
250
|
+
// Verify session
|
|
251
|
+
const session = await ctx.db
|
|
252
|
+
.query("agentSessions")
|
|
253
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
254
|
+
.first();
|
|
255
|
+
if (!session) {
|
|
256
|
+
return {
|
|
257
|
+
totalCalls: 0,
|
|
258
|
+
successCount: 0,
|
|
259
|
+
errorCount: 0,
|
|
260
|
+
successRate: 0,
|
|
261
|
+
avgLatency: 0,
|
|
262
|
+
byProvider: [],
|
|
263
|
+
byDay: [],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const now = Date.now();
|
|
267
|
+
const periodStart = now - periodDays * 24 * 60 * 60 * 1000;
|
|
268
|
+
// Get all logs for this workspace in the period
|
|
269
|
+
const logs = await ctx.db
|
|
270
|
+
.query("apiLogs")
|
|
271
|
+
.withIndex("by_workspaceId_createdAt", (q) => q.eq("workspaceId", session.workspaceId))
|
|
272
|
+
.filter((q) => q.gte(q.field("createdAt"), periodStart))
|
|
273
|
+
.collect();
|
|
274
|
+
const totalCalls = logs.length;
|
|
275
|
+
const successCount = logs.filter((l) => l.status === "success").length;
|
|
276
|
+
const errorCount = logs.filter((l) => l.status === "error").length;
|
|
277
|
+
const successRate = totalCalls > 0 ? (successCount / totalCalls) * 100 : 0;
|
|
278
|
+
const totalLatency = logs.reduce((sum, l) => sum + l.latencyMs, 0);
|
|
279
|
+
const avgLatency = totalCalls > 0 ? Math.round(totalLatency / totalCalls) : 0;
|
|
280
|
+
// Group by provider
|
|
281
|
+
const byProviderMap = {};
|
|
282
|
+
for (const log of logs) {
|
|
283
|
+
if (!byProviderMap[log.provider]) {
|
|
284
|
+
byProviderMap[log.provider] = { calls: 0, success: 0, error: 0, latency: 0 };
|
|
285
|
+
}
|
|
286
|
+
byProviderMap[log.provider].calls++;
|
|
287
|
+
byProviderMap[log.provider].latency += log.latencyMs;
|
|
288
|
+
if (log.status === "success") {
|
|
289
|
+
byProviderMap[log.provider].success++;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
byProviderMap[log.provider].error++;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const byProvider = Object.entries(byProviderMap)
|
|
296
|
+
.map(([provider, data]) => ({
|
|
297
|
+
provider,
|
|
298
|
+
calls: data.calls,
|
|
299
|
+
successRate: data.calls > 0 ? (data.success / data.calls) * 100 : 0,
|
|
300
|
+
avgLatency: data.calls > 0 ? Math.round(data.latency / data.calls) : 0,
|
|
301
|
+
}))
|
|
302
|
+
.sort((a, b) => b.calls - a.calls);
|
|
303
|
+
// Group by day
|
|
304
|
+
const byDayMap = {};
|
|
305
|
+
for (const log of logs) {
|
|
306
|
+
const day = new Date(log.createdAt).toISOString().split("T")[0];
|
|
307
|
+
if (!byDayMap[day]) {
|
|
308
|
+
byDayMap[day] = { calls: 0, success: 0, error: 0 };
|
|
309
|
+
}
|
|
310
|
+
byDayMap[day].calls++;
|
|
311
|
+
if (log.status === "success") {
|
|
312
|
+
byDayMap[day].success++;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
byDayMap[day].error++;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const byDay = Object.entries(byDayMap)
|
|
319
|
+
.map(([date, data]) => ({
|
|
320
|
+
date,
|
|
321
|
+
calls: data.calls,
|
|
322
|
+
success: data.success,
|
|
323
|
+
error: data.error,
|
|
324
|
+
}))
|
|
325
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
326
|
+
// Get unique providers and agents for filter dropdowns
|
|
327
|
+
const providers = [...new Set(logs.map((l) => l.provider))].sort();
|
|
328
|
+
const agents = [...new Set(logs.map((l) => l.subagentId || "main"))].sort();
|
|
329
|
+
return {
|
|
330
|
+
totalCalls,
|
|
331
|
+
successCount,
|
|
332
|
+
errorCount,
|
|
333
|
+
successRate: Math.round(successRate * 10) / 10,
|
|
334
|
+
avgLatency,
|
|
335
|
+
byProvider,
|
|
336
|
+
byDay,
|
|
337
|
+
providers,
|
|
338
|
+
agents,
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
/**
|
|
343
|
+
* Get unique providers for filter dropdown
|
|
344
|
+
*/
|
|
345
|
+
export const getProviders = query({
|
|
346
|
+
args: {
|
|
347
|
+
token: v.string(),
|
|
348
|
+
},
|
|
349
|
+
handler: async (ctx, args) => {
|
|
350
|
+
// Verify session
|
|
351
|
+
const session = await ctx.db
|
|
352
|
+
.query("agentSessions")
|
|
353
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
354
|
+
.first();
|
|
355
|
+
if (!session) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
// Get all logs for this workspace
|
|
359
|
+
const logs = await ctx.db
|
|
360
|
+
.query("apiLogs")
|
|
361
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
362
|
+
.collect();
|
|
363
|
+
// Get unique providers
|
|
364
|
+
const providers = [...new Set(logs.map((l) => l.provider))].sort();
|
|
365
|
+
return providers;
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
/**
|
|
369
|
+
* Get logs for a specific subagent
|
|
370
|
+
*/
|
|
371
|
+
export const getBySubagent = query({
|
|
372
|
+
args: {
|
|
373
|
+
token: v.string(),
|
|
374
|
+
subagentId: v.string(),
|
|
375
|
+
limit: v.optional(v.number()),
|
|
376
|
+
},
|
|
377
|
+
handler: async (ctx, { token, subagentId, limit = 20 }) => {
|
|
378
|
+
// Verify session
|
|
379
|
+
const session = await ctx.db
|
|
380
|
+
.query("agentSessions")
|
|
381
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
382
|
+
.first();
|
|
383
|
+
if (!session)
|
|
384
|
+
return null;
|
|
385
|
+
// Get API logs for this subagent
|
|
386
|
+
const apiLogs = await ctx.db
|
|
387
|
+
.query("apiLogs")
|
|
388
|
+
.withIndex("by_subagentId", (q) => q.eq("subagentId", subagentId))
|
|
389
|
+
.order("desc")
|
|
390
|
+
.take(limit);
|
|
391
|
+
// Get search logs for this subagent
|
|
392
|
+
const searchLogs = await ctx.db
|
|
393
|
+
.query("searchLogs")
|
|
394
|
+
.filter((q) => q.eq(q.field("subagentId"), subagentId))
|
|
395
|
+
.order("desc")
|
|
396
|
+
.take(limit);
|
|
397
|
+
// Merge and sort by timestamp
|
|
398
|
+
const combined = [
|
|
399
|
+
...apiLogs.map(l => ({
|
|
400
|
+
...l,
|
|
401
|
+
type: "direct_call",
|
|
402
|
+
timestamp: l.createdAt
|
|
403
|
+
})),
|
|
404
|
+
...searchLogs.map(l => ({
|
|
405
|
+
...l,
|
|
406
|
+
type: "search",
|
|
407
|
+
timestamp: l.timestamp
|
|
408
|
+
})),
|
|
409
|
+
].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
410
|
+
return combined.slice(0, limit);
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
/**
|
|
414
|
+
* Clear all logs for a workspace (admin cleanup)
|
|
415
|
+
*/
|
|
416
|
+
export const clearWorkspaceLogs = mutation({
|
|
417
|
+
args: {
|
|
418
|
+
token: v.string(),
|
|
419
|
+
},
|
|
420
|
+
handler: async (ctx, { token }) => {
|
|
421
|
+
const session = await ctx.db
|
|
422
|
+
.query("agentSessions")
|
|
423
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
424
|
+
.first();
|
|
425
|
+
if (!session)
|
|
426
|
+
throw new Error("Invalid session");
|
|
427
|
+
// Delete all apiLogs
|
|
428
|
+
const apiLogs = await ctx.db
|
|
429
|
+
.query("apiLogs")
|
|
430
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
431
|
+
.collect();
|
|
432
|
+
for (const log of apiLogs) {
|
|
433
|
+
await ctx.db.delete(log._id);
|
|
434
|
+
}
|
|
435
|
+
// Delete all searchLogs
|
|
436
|
+
const searchLogs = await ctx.db
|
|
437
|
+
.query("searchLogs")
|
|
438
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
439
|
+
.collect();
|
|
440
|
+
for (const log of searchLogs) {
|
|
441
|
+
await ctx.db.delete(log._id);
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
deleted: {
|
|
445
|
+
apiLogs: apiLogs.length,
|
|
446
|
+
searchLogs: searchLogs.length
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
// Log proxy API calls from external agents (Hivr bees)
|
|
452
|
+
export const createProxyLog = mutation({
|
|
453
|
+
args: {
|
|
454
|
+
workspaceId: v.id("workspaces"),
|
|
455
|
+
provider: v.string(),
|
|
456
|
+
action: v.string(),
|
|
457
|
+
subagentId: v.optional(v.string()),
|
|
458
|
+
sessionToken: v.optional(v.string()),
|
|
459
|
+
},
|
|
460
|
+
handler: async (ctx, { workspaceId, provider, action, subagentId, sessionToken }) => {
|
|
461
|
+
const now = Date.now();
|
|
462
|
+
await ctx.db.insert("apiLogs", {
|
|
463
|
+
workspaceId,
|
|
464
|
+
provider,
|
|
465
|
+
action,
|
|
466
|
+
subagentId: subagentId || "unknown",
|
|
467
|
+
sessionToken: sessionToken || "proxy",
|
|
468
|
+
status: "success",
|
|
469
|
+
latencyMs: 0, // Proxy calls don't track latency
|
|
470
|
+
createdAt: now,
|
|
471
|
+
});
|
|
472
|
+
// Update workspace lastActiveAt (main agent activity)
|
|
473
|
+
await ctx.db.patch(workspaceId, { lastActiveAt: now });
|
|
474
|
+
// If this is a subagent call, update that subagent's timestamp
|
|
475
|
+
if (subagentId && subagentId !== "unknown" && subagentId !== "main") {
|
|
476
|
+
const subagent = await ctx.db
|
|
477
|
+
.query("subagents")
|
|
478
|
+
.withIndex("by_workspaceId_subagentId", (q) => q.eq("workspaceId", workspaceId).eq("subagentId", subagentId))
|
|
479
|
+
.first();
|
|
480
|
+
if (subagent) {
|
|
481
|
+
await ctx.db.patch(subagent._id, { lastActiveAt: now });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { success: true };
|
|
485
|
+
},
|
|
486
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { query, mutation } from "./_generated/server";
|
|
3
|
+
// Get MOU by partnerId
|
|
4
|
+
export const getByPartnerId = query({
|
|
5
|
+
args: { partnerId: v.string() },
|
|
6
|
+
handler: async (ctx, args) => {
|
|
7
|
+
return await ctx.db
|
|
8
|
+
.query("mouDocuments")
|
|
9
|
+
.withIndex("by_partnerId", (q) => q.eq("partnerId", args.partnerId))
|
|
10
|
+
.first();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
// Create new MOU document
|
|
14
|
+
export const create = mutation({
|
|
15
|
+
args: {
|
|
16
|
+
partnerId: v.string(),
|
|
17
|
+
partnerName: v.string(),
|
|
18
|
+
partnerEmail: v.string(),
|
|
19
|
+
documentHtml: v.string(),
|
|
20
|
+
},
|
|
21
|
+
handler: async (ctx, args) => {
|
|
22
|
+
return await ctx.db.insert("mouDocuments", {
|
|
23
|
+
...args,
|
|
24
|
+
status: "pending",
|
|
25
|
+
createdAt: Date.now(),
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
// Sign MOU
|
|
30
|
+
export const sign = mutation({
|
|
31
|
+
args: {
|
|
32
|
+
partnerId: v.string(),
|
|
33
|
+
signatureDataUrl: v.string(),
|
|
34
|
+
signerName: v.string(),
|
|
35
|
+
signerTitle: v.string(),
|
|
36
|
+
signerIp: v.optional(v.string()),
|
|
37
|
+
},
|
|
38
|
+
handler: async (ctx, args) => {
|
|
39
|
+
const mou = await ctx.db
|
|
40
|
+
.query("mouDocuments")
|
|
41
|
+
.withIndex("by_partnerId", (q) => q.eq("partnerId", args.partnerId))
|
|
42
|
+
.first();
|
|
43
|
+
if (!mou) {
|
|
44
|
+
throw new Error("MOU not found");
|
|
45
|
+
}
|
|
46
|
+
if (mou.status === "signed") {
|
|
47
|
+
throw new Error("MOU already signed");
|
|
48
|
+
}
|
|
49
|
+
await ctx.db.patch(mou._id, {
|
|
50
|
+
status: "signed",
|
|
51
|
+
signedAt: Date.now(),
|
|
52
|
+
signatureDataUrl: args.signatureDataUrl,
|
|
53
|
+
signerName: args.signerName,
|
|
54
|
+
signerTitle: args.signerTitle,
|
|
55
|
+
signerIp: args.signerIp,
|
|
56
|
+
});
|
|
57
|
+
return { success: true };
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
// List all MOUs (admin)
|
|
61
|
+
export const list = query({
|
|
62
|
+
args: {},
|
|
63
|
+
handler: async (ctx) => {
|
|
64
|
+
return await ctx.db.query("mouDocuments").collect();
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// Delete MOU (admin)
|
|
68
|
+
export const remove = mutation({
|
|
69
|
+
args: { partnerId: v.string() },
|
|
70
|
+
handler: async (ctx, args) => {
|
|
71
|
+
const mou = await ctx.db
|
|
72
|
+
.query("mouDocuments")
|
|
73
|
+
.withIndex("by_partnerId", (q) => q.eq("partnerId", args.partnerId))
|
|
74
|
+
.first();
|
|
75
|
+
if (mou) {
|
|
76
|
+
await ctx.db.delete(mou._id);
|
|
77
|
+
return { success: true, deleted: args.partnerId };
|
|
78
|
+
}
|
|
79
|
+
return { success: false, message: "MOU not found" };
|
|
80
|
+
},
|
|
81
|
+
});
|