@nordsym/apiclaw 1.4.0 → 1.4.1
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/PRD-ANALYTICS-AGENTS-TEAMS.md +710 -0
- package/PRD-LOGS-SUBAGENTS-V2.md +267 -0
- package/convex/_generated/api.d.ts +4 -0
- package/convex/agents.ts +188 -0
- package/convex/chains.ts +257 -104
- package/convex/logs.ts +94 -0
- package/convex/schema.ts +38 -0
- package/convex/searchLogs.ts +141 -0
- package/convex/teams.ts +243 -0
- package/dist/index.js +96 -1
- package/dist/index.js.map +1 -1
- package/docs/SUBAGENT-NAMING.md +94 -0
- package/landing/src/app/workspace/chains/page.tsx +3 -3
- package/landing/src/app/workspace/page.tsx +1903 -224
- package/landing/src/lib/stats.json +1 -1
- package/package.json +14 -2
- package/src/index.ts +101 -1
- package/src/chain-types.ts +0 -270
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
# APIClaw PRD — Analytics, Agents & Teams
|
|
2
|
+
|
|
3
|
+
**Datum:** 2026-03-03
|
|
4
|
+
**Status:** Ready for Implementation
|
|
5
|
+
**Författare:** Gustav + Symbot
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Executive Summary
|
|
10
|
+
|
|
11
|
+
Tre förbättringsområden:
|
|
12
|
+
|
|
13
|
+
1. **Search Analytics** — Sökningar kopplade till workspace + dashboard
|
|
14
|
+
2. **Agent Model** — Tydlig modell för agents + AI backend tracking
|
|
15
|
+
3. **Teams** — Invite members, roles, shared workspace
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Search Analytics
|
|
20
|
+
|
|
21
|
+
### 1.1 Schema
|
|
22
|
+
|
|
23
|
+
**Ny tabell: `searchLogs`**
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
searchLogs: defineTable({
|
|
27
|
+
workspaceId: v.id("workspaces"),
|
|
28
|
+
subagentId: v.optional(v.string()),
|
|
29
|
+
query: v.string(),
|
|
30
|
+
resultCount: v.number(),
|
|
31
|
+
hasResults: v.boolean(),
|
|
32
|
+
matchedProviders: v.optional(v.array(v.string())),
|
|
33
|
+
responseTimeMs: v.number(),
|
|
34
|
+
timestamp: v.number(),
|
|
35
|
+
})
|
|
36
|
+
.index("by_workspaceId", ["workspaceId"])
|
|
37
|
+
.index("by_timestamp", ["timestamp"])
|
|
38
|
+
.index("by_hasResults", ["hasResults"])
|
|
39
|
+
.index("by_workspaceId_timestamp", ["workspaceId", "timestamp"])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 1.2 Backend Functions
|
|
43
|
+
|
|
44
|
+
**convex/searchLogs.ts**
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// Log a search (called from MCP server)
|
|
48
|
+
export const log = mutation({
|
|
49
|
+
args: {
|
|
50
|
+
sessionToken: v.string(),
|
|
51
|
+
subagentId: v.optional(v.string()),
|
|
52
|
+
query: v.string(),
|
|
53
|
+
resultCount: v.number(),
|
|
54
|
+
matchedProviders: v.optional(v.array(v.string())),
|
|
55
|
+
responseTimeMs: v.number(),
|
|
56
|
+
},
|
|
57
|
+
handler: async (ctx, args) => {
|
|
58
|
+
// Get workspace from session
|
|
59
|
+
const session = await ctx.db
|
|
60
|
+
.query("agentSessions")
|
|
61
|
+
.withIndex("by_sessionToken", q => q.eq("sessionToken", args.sessionToken))
|
|
62
|
+
.first();
|
|
63
|
+
|
|
64
|
+
if (!session) return null;
|
|
65
|
+
|
|
66
|
+
return await ctx.db.insert("searchLogs", {
|
|
67
|
+
workspaceId: session.workspaceId,
|
|
68
|
+
subagentId: args.subagentId,
|
|
69
|
+
query: args.query,
|
|
70
|
+
resultCount: args.resultCount,
|
|
71
|
+
hasResults: args.resultCount > 0,
|
|
72
|
+
matchedProviders: args.matchedProviders,
|
|
73
|
+
responseTimeMs: args.responseTimeMs,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Get search stats for workspace
|
|
80
|
+
export const getStats = query({
|
|
81
|
+
args: {
|
|
82
|
+
token: v.string(),
|
|
83
|
+
hoursBack: v.optional(v.number()),
|
|
84
|
+
},
|
|
85
|
+
handler: async (ctx, { token, hoursBack = 24 }) => {
|
|
86
|
+
const session = await ctx.db
|
|
87
|
+
.query("agentSessions")
|
|
88
|
+
.withIndex("by_sessionToken", q => q.eq("sessionToken", token))
|
|
89
|
+
.first();
|
|
90
|
+
|
|
91
|
+
if (!session) return null;
|
|
92
|
+
|
|
93
|
+
const since = Date.now() - hoursBack * 3600000;
|
|
94
|
+
|
|
95
|
+
const logs = await ctx.db
|
|
96
|
+
.query("searchLogs")
|
|
97
|
+
.withIndex("by_workspaceId_timestamp", q =>
|
|
98
|
+
q.eq("workspaceId", session.workspaceId).gte("timestamp", since)
|
|
99
|
+
)
|
|
100
|
+
.collect();
|
|
101
|
+
|
|
102
|
+
// Aggregate
|
|
103
|
+
const totalSearches = logs.length;
|
|
104
|
+
const zeroResults = logs.filter(l => !l.hasResults).length;
|
|
105
|
+
const avgResponseTime = logs.reduce((a, l) => a + l.responseTimeMs, 0) / logs.length || 0;
|
|
106
|
+
|
|
107
|
+
// Top queries
|
|
108
|
+
const queryCounts: Record<string, number> = {};
|
|
109
|
+
for (const log of logs) {
|
|
110
|
+
queryCounts[log.query] = (queryCounts[log.query] || 0) + 1;
|
|
111
|
+
}
|
|
112
|
+
const topQueries = Object.entries(queryCounts)
|
|
113
|
+
.sort(([,a], [,b]) => b - a)
|
|
114
|
+
.slice(0, 20)
|
|
115
|
+
.map(([query, count]) => ({ query, count }));
|
|
116
|
+
|
|
117
|
+
// Zero-result queries (gold data)
|
|
118
|
+
const zeroResultQueries = logs
|
|
119
|
+
.filter(l => !l.hasResults)
|
|
120
|
+
.reduce((acc, l) => {
|
|
121
|
+
acc[l.query] = (acc[l.query] || 0) + 1;
|
|
122
|
+
return acc;
|
|
123
|
+
}, {} as Record<string, number>);
|
|
124
|
+
const topZeroResults = Object.entries(zeroResultQueries)
|
|
125
|
+
.sort(([,a], [,b]) => b - a)
|
|
126
|
+
.slice(0, 20)
|
|
127
|
+
.map(([query, count]) => ({ query, count }));
|
|
128
|
+
|
|
129
|
+
// By subagent
|
|
130
|
+
const bySubagent: Record<string, number> = {};
|
|
131
|
+
for (const log of logs) {
|
|
132
|
+
const key = log.subagentId || "primary";
|
|
133
|
+
bySubagent[key] = (bySubagent[key] || 0) + 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
totalSearches,
|
|
138
|
+
zeroResults,
|
|
139
|
+
zeroResultRate: totalSearches > 0 ? zeroResults / totalSearches : 0,
|
|
140
|
+
avgResponseTime: Math.round(avgResponseTime),
|
|
141
|
+
topQueries,
|
|
142
|
+
topZeroResults,
|
|
143
|
+
bySubagent,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Get recent searches
|
|
149
|
+
export const getRecent = query({
|
|
150
|
+
args: {
|
|
151
|
+
token: v.string(),
|
|
152
|
+
limit: v.optional(v.number()),
|
|
153
|
+
},
|
|
154
|
+
handler: async (ctx, { token, limit = 50 }) => {
|
|
155
|
+
const session = await ctx.db
|
|
156
|
+
.query("agentSessions")
|
|
157
|
+
.withIndex("by_sessionToken", q => q.eq("sessionToken", token))
|
|
158
|
+
.first();
|
|
159
|
+
|
|
160
|
+
if (!session) return [];
|
|
161
|
+
|
|
162
|
+
return await ctx.db
|
|
163
|
+
.query("searchLogs")
|
|
164
|
+
.withIndex("by_workspaceId_timestamp", q =>
|
|
165
|
+
q.eq("workspaceId", session.workspaceId)
|
|
166
|
+
)
|
|
167
|
+
.order("desc")
|
|
168
|
+
.take(limit);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 1.3 MCP Server Update
|
|
174
|
+
|
|
175
|
+
**src/index.ts** — Update search handler to log:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// In search tool handler
|
|
179
|
+
const startTime = Date.now();
|
|
180
|
+
const results = await searchAPIs(query);
|
|
181
|
+
const responseTimeMs = Date.now() - startTime;
|
|
182
|
+
|
|
183
|
+
// Log to Convex (fire and forget)
|
|
184
|
+
if (sessionToken) {
|
|
185
|
+
fetch(CONVEX_URL, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: { 'Content-Type': 'application/json' },
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
path: 'searchLogs:log',
|
|
190
|
+
args: {
|
|
191
|
+
sessionToken,
|
|
192
|
+
subagentId: headers['x-apiclaw-subagent'],
|
|
193
|
+
query,
|
|
194
|
+
resultCount: results.length,
|
|
195
|
+
matchedProviders: results.map(r => r.provider),
|
|
196
|
+
responseTimeMs,
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
}).catch(() => {});
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 1.4 Dashboard UI
|
|
204
|
+
|
|
205
|
+
**Ny subtab under Analytics: "Search"**
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
Analytics
|
|
209
|
+
├── Overview
|
|
210
|
+
├── Usage
|
|
211
|
+
├── Logs
|
|
212
|
+
├── Chains
|
|
213
|
+
└── Search (NY)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Search subtab innehåller:**
|
|
217
|
+
|
|
218
|
+
1. **Stats Cards**
|
|
219
|
+
- Total Searches (24h)
|
|
220
|
+
- Zero-Result Rate (%)
|
|
221
|
+
- Avg Response Time (ms)
|
|
222
|
+
|
|
223
|
+
2. **Top Queries** (tabell)
|
|
224
|
+
- Query | Count | Avg Results
|
|
225
|
+
|
|
226
|
+
3. **Zero-Result Queries** (highlight, röd bakgrund)
|
|
227
|
+
- Query | Count | "Request API" button
|
|
228
|
+
|
|
229
|
+
4. **Search by Agent** (pie chart)
|
|
230
|
+
- Primary Agent: 65%
|
|
231
|
+
- research-agent: 25%
|
|
232
|
+
- content-writer: 10%
|
|
233
|
+
|
|
234
|
+
5. **Recent Searches** (live feed)
|
|
235
|
+
- Timestamp | Agent | Query | Results
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 2. Agent Model
|
|
240
|
+
|
|
241
|
+
### 2.1 Schema Updates
|
|
242
|
+
|
|
243
|
+
**Uppdatera `workspaces` tabell:**
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Lägg till i workspaces schema
|
|
247
|
+
aiBackend: v.optional(v.string()), // "claude-3-opus", "gpt-4", etc.
|
|
248
|
+
aiBackendLastSeen: v.optional(v.number()),
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Uppdatera `subagents` tabell:**
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Lägg till i subagents schema
|
|
255
|
+
aiBackend: v.optional(v.string()),
|
|
256
|
+
description: v.optional(v.string()), // User-provided description
|
|
257
|
+
isRegistered: v.optional(v.boolean()), // true if pre-registered (not implicit)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 2.2 New Header Support
|
|
261
|
+
|
|
262
|
+
**X-APIClaw-AI-Backend**
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// MCP server extracts and stores
|
|
266
|
+
const aiBackend = headers['x-apiclaw-ai-backend']; // "claude-3-opus"
|
|
267
|
+
|
|
268
|
+
// Update workspace or subagent with AI backend info
|
|
269
|
+
if (aiBackend) {
|
|
270
|
+
await updateAIBackend(workspaceId, subagentId, aiBackend);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 2.3 Agent Registration
|
|
275
|
+
|
|
276
|
+
**convex/agents.ts** — Add pre-registration:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
export const registerTaskAgent = mutation({
|
|
280
|
+
args: {
|
|
281
|
+
token: v.string(),
|
|
282
|
+
subagentId: v.string(),
|
|
283
|
+
name: v.optional(v.string()),
|
|
284
|
+
description: v.optional(v.string()),
|
|
285
|
+
},
|
|
286
|
+
handler: async (ctx, args) => {
|
|
287
|
+
const session = await getSession(ctx, args.token);
|
|
288
|
+
if (!session) throw new Error("Invalid session");
|
|
289
|
+
|
|
290
|
+
// Check if already exists
|
|
291
|
+
const existing = await ctx.db
|
|
292
|
+
.query("subagents")
|
|
293
|
+
.withIndex("by_workspaceId_subagentId", q =>
|
|
294
|
+
q.eq("workspaceId", session.workspaceId).eq("subagentId", args.subagentId)
|
|
295
|
+
)
|
|
296
|
+
.first();
|
|
297
|
+
|
|
298
|
+
if (existing) {
|
|
299
|
+
// Update
|
|
300
|
+
return await ctx.db.patch(existing._id, {
|
|
301
|
+
name: args.name || existing.name,
|
|
302
|
+
description: args.description || existing.description,
|
|
303
|
+
isRegistered: true,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Create new
|
|
308
|
+
return await ctx.db.insert("subagents", {
|
|
309
|
+
workspaceId: session.workspaceId,
|
|
310
|
+
subagentId: args.subagentId,
|
|
311
|
+
name: args.name,
|
|
312
|
+
description: args.description,
|
|
313
|
+
callCount: 0,
|
|
314
|
+
isRegistered: true,
|
|
315
|
+
firstSeenAt: Date.now(),
|
|
316
|
+
lastActiveAt: Date.now(),
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### 2.4 My Agents UI Enhancement
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
My Agents
|
|
326
|
+
│
|
|
327
|
+
├── Primary Agent
|
|
328
|
+
│ ┌─────────────────────────────────────┐
|
|
329
|
+
│ │ 🤖 Symbot [Edit] │
|
|
330
|
+
│ │ ID: abc123-def456 │
|
|
331
|
+
│ │ AI: Claude 3 Opus │
|
|
332
|
+
│ │ Calls: 4,521 | Last: 2 min ago │
|
|
333
|
+
│ └─────────────────────────────────────┘
|
|
334
|
+
│
|
|
335
|
+
├── Task Agents (3)
|
|
336
|
+
│ ┌─────────────────────────────────────┐
|
|
337
|
+
│ │ 📋 research-agent [Edit] │
|
|
338
|
+
│ │ "Researches topics and competitors" │
|
|
339
|
+
│ │ AI: Claude 3.5 Sonnet │
|
|
340
|
+
│ │ Calls: 156 | Last: 1 hour ago │
|
|
341
|
+
│ └─────────────────────────────────────┘
|
|
342
|
+
│ ┌─────────────────────────────────────┐
|
|
343
|
+
│ │ ✍️ content-writer [Edit] │
|
|
344
|
+
│ │ AI: GPT-4 │
|
|
345
|
+
│ │ Calls: 89 | Last: 3 hours ago │
|
|
346
|
+
│ └─────────────────────────────────────┘
|
|
347
|
+
│
|
|
348
|
+
└── [+ Register New Agent]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 3. Teams
|
|
354
|
+
|
|
355
|
+
### 3.1 Schema
|
|
356
|
+
|
|
357
|
+
**Ny tabell: `workspaceMembers`**
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
workspaceMembers: defineTable({
|
|
361
|
+
workspaceId: v.id("workspaces"),
|
|
362
|
+
email: v.string(),
|
|
363
|
+
role: v.union(v.literal("owner"), v.literal("admin"), v.literal("member")),
|
|
364
|
+
invitedBy: v.optional(v.string()), // email of inviter
|
|
365
|
+
inviteToken: v.optional(v.string()),
|
|
366
|
+
status: v.union(v.literal("pending"), v.literal("active"), v.literal("revoked")),
|
|
367
|
+
createdAt: v.number(),
|
|
368
|
+
acceptedAt: v.optional(v.number()),
|
|
369
|
+
})
|
|
370
|
+
.index("by_workspaceId", ["workspaceId"])
|
|
371
|
+
.index("by_email", ["email"])
|
|
372
|
+
.index("by_inviteToken", ["inviteToken"])
|
|
373
|
+
.index("by_workspaceId_email", ["workspaceId", "email"])
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### 3.2 Backend Functions
|
|
377
|
+
|
|
378
|
+
**convex/teams.ts**
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// Get team members
|
|
382
|
+
export const getMembers = query({
|
|
383
|
+
args: { token: v.string() },
|
|
384
|
+
handler: async (ctx, { token }) => {
|
|
385
|
+
const session = await getSession(ctx, token);
|
|
386
|
+
if (!session) return [];
|
|
387
|
+
|
|
388
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
389
|
+
const members = await ctx.db
|
|
390
|
+
.query("workspaceMembers")
|
|
391
|
+
.withIndex("by_workspaceId", q => q.eq("workspaceId", session.workspaceId))
|
|
392
|
+
.collect();
|
|
393
|
+
|
|
394
|
+
// Add owner as first member
|
|
395
|
+
return [
|
|
396
|
+
{
|
|
397
|
+
email: workspace.email,
|
|
398
|
+
role: "owner" as const,
|
|
399
|
+
status: "active" as const,
|
|
400
|
+
isOwner: true,
|
|
401
|
+
},
|
|
402
|
+
...members.map(m => ({
|
|
403
|
+
...m,
|
|
404
|
+
isOwner: false,
|
|
405
|
+
})),
|
|
406
|
+
];
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Invite member (creates pending invite)
|
|
411
|
+
export const inviteMember = mutation({
|
|
412
|
+
args: {
|
|
413
|
+
token: v.string(),
|
|
414
|
+
email: v.string(),
|
|
415
|
+
role: v.union(v.literal("admin"), v.literal("member")),
|
|
416
|
+
},
|
|
417
|
+
handler: async (ctx, { token, email, role }) => {
|
|
418
|
+
const session = await getSession(ctx, token);
|
|
419
|
+
if (!session) throw new Error("Invalid session");
|
|
420
|
+
|
|
421
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
422
|
+
|
|
423
|
+
// Check if already member
|
|
424
|
+
const existing = await ctx.db
|
|
425
|
+
.query("workspaceMembers")
|
|
426
|
+
.withIndex("by_workspaceId_email", q =>
|
|
427
|
+
q.eq("workspaceId", session.workspaceId).eq("email", email)
|
|
428
|
+
)
|
|
429
|
+
.first();
|
|
430
|
+
|
|
431
|
+
if (existing) throw new Error("Already a member");
|
|
432
|
+
|
|
433
|
+
// Generate invite token
|
|
434
|
+
const inviteToken = generateToken();
|
|
435
|
+
|
|
436
|
+
return await ctx.db.insert("workspaceMembers", {
|
|
437
|
+
workspaceId: session.workspaceId,
|
|
438
|
+
email,
|
|
439
|
+
role,
|
|
440
|
+
invitedBy: workspace.email,
|
|
441
|
+
inviteToken,
|
|
442
|
+
status: "pending",
|
|
443
|
+
createdAt: Date.now(),
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Accept invite
|
|
449
|
+
export const acceptInvite = mutation({
|
|
450
|
+
args: { inviteToken: v.string() },
|
|
451
|
+
handler: async (ctx, { inviteToken }) => {
|
|
452
|
+
const member = await ctx.db
|
|
453
|
+
.query("workspaceMembers")
|
|
454
|
+
.withIndex("by_inviteToken", q => q.eq("inviteToken", inviteToken))
|
|
455
|
+
.first();
|
|
456
|
+
|
|
457
|
+
if (!member) throw new Error("Invalid invite");
|
|
458
|
+
if (member.status !== "pending") throw new Error("Invite already used");
|
|
459
|
+
|
|
460
|
+
return await ctx.db.patch(member._id, {
|
|
461
|
+
status: "active",
|
|
462
|
+
acceptedAt: Date.now(),
|
|
463
|
+
inviteToken: undefined, // Clear token
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Remove member
|
|
469
|
+
export const removeMember = mutation({
|
|
470
|
+
args: {
|
|
471
|
+
token: v.string(),
|
|
472
|
+
memberEmail: v.string(),
|
|
473
|
+
},
|
|
474
|
+
handler: async (ctx, { token, memberEmail }) => {
|
|
475
|
+
const session = await getSession(ctx, token);
|
|
476
|
+
if (!session) throw new Error("Invalid session");
|
|
477
|
+
|
|
478
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
479
|
+
if (workspace.email === memberEmail) {
|
|
480
|
+
throw new Error("Cannot remove owner");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const member = await ctx.db
|
|
484
|
+
.query("workspaceMembers")
|
|
485
|
+
.withIndex("by_workspaceId_email", q =>
|
|
486
|
+
q.eq("workspaceId", session.workspaceId).eq("email", memberEmail)
|
|
487
|
+
)
|
|
488
|
+
.first();
|
|
489
|
+
|
|
490
|
+
if (!member) throw new Error("Member not found");
|
|
491
|
+
|
|
492
|
+
return await ctx.db.patch(member._id, {
|
|
493
|
+
status: "revoked",
|
|
494
|
+
});
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### 3.3 Teams UI
|
|
500
|
+
|
|
501
|
+
**Settings → Team (ny sektion)**
|
|
502
|
+
|
|
503
|
+
```
|
|
504
|
+
┌─────────────────────────────────────────────────────┐
|
|
505
|
+
│ Team Members │
|
|
506
|
+
├─────────────────────────────────────────────────────┤
|
|
507
|
+
│ │
|
|
508
|
+
│ 👑 gustav@nordsym.com Owner │
|
|
509
|
+
│ ───────────────────────────────────────── │
|
|
510
|
+
│ 👤 molle@nordsym.com Admin [...] │
|
|
511
|
+
│ Invited • Pending │
|
|
512
|
+
│ ───────────────────────────────────────── │
|
|
513
|
+
│ 👤 peter@cleanbuddy.se Member [...] │
|
|
514
|
+
│ Active since Mar 1 │
|
|
515
|
+
│ │
|
|
516
|
+
├─────────────────────────────────────────────────────┤
|
|
517
|
+
│ [+ Invite Team Member] 🔒 Coming Soon │
|
|
518
|
+
│ │
|
|
519
|
+
│ ┌─────────────────────────────────────────────┐ │
|
|
520
|
+
│ │ Team invites launching soon! │ │
|
|
521
|
+
│ │ Get notified when it's ready. │ │
|
|
522
|
+
│ │ │ │
|
|
523
|
+
│ │ [Notify Me] │ │
|
|
524
|
+
│ └─────────────────────────────────────────────┘ │
|
|
525
|
+
└─────────────────────────────────────────────────────┘
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**"Coming Soon" implementation:**
|
|
529
|
+
- UI är byggd och funktionell
|
|
530
|
+
- Invite-knappen visar "Coming Soon" modal istället för invite flow
|
|
531
|
+
- Backend finns redo att aktivera
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## 4. Symbot Godmode Setup
|
|
536
|
+
|
|
537
|
+
### Steg för att sätta Symbot i godmode på APIClaw:
|
|
538
|
+
|
|
539
|
+
#### 4.1 Verifiera workspace
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
# Kolla att Symbot är kopplad till ditt workspace
|
|
543
|
+
curl -s "https://agile-crane-840.convex.cloud/api/query" \
|
|
544
|
+
-H "Content-Type: application/json" \
|
|
545
|
+
-d '{
|
|
546
|
+
"path": "agents:getMainAgent",
|
|
547
|
+
"args": {"token": "<SESSION_TOKEN>"}
|
|
548
|
+
}'
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Förväntat svar:**
|
|
552
|
+
```json
|
|
553
|
+
{
|
|
554
|
+
"workspaceId": "...",
|
|
555
|
+
"email": "gustav@nordsym.com",
|
|
556
|
+
"mainAgentId": "uuid-xxx",
|
|
557
|
+
"mainAgentName": "Symbot"
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
#### 4.2 Sätt namn till "Symbot"
|
|
562
|
+
|
|
563
|
+
```bash
|
|
564
|
+
curl -s "https://agile-crane-840.convex.cloud/api/mutation" \
|
|
565
|
+
-H "Content-Type: application/json" \
|
|
566
|
+
-d '{
|
|
567
|
+
"path": "agents:renameMainAgent",
|
|
568
|
+
"args": {"token": "<SESSION_TOKEN>", "name": "Symbot"}
|
|
569
|
+
}'
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
#### 4.3 Headers som Symbot ska skicka
|
|
573
|
+
|
|
574
|
+
När Symbot gör APIClaw-anrop, inkludera:
|
|
575
|
+
|
|
576
|
+
```
|
|
577
|
+
X-APIClaw-Session: <SESSION_TOKEN>
|
|
578
|
+
X-APIClaw-AI-Backend: claude-3-opus
|
|
579
|
+
X-APIClaw-Subagent: <optional, om subagent>
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
#### 4.4 Verifiera i Dashboard
|
|
583
|
+
|
|
584
|
+
1. Gå till https://apiclaw.com/workspace?tab=my-agents
|
|
585
|
+
2. Se att "Symbot" visas som Primary Agent
|
|
586
|
+
3. Se att AI Backend visar "Claude 3 Opus"
|
|
587
|
+
|
|
588
|
+
#### 4.5 Clawdbot Config
|
|
589
|
+
|
|
590
|
+
I `~/.clawdbot/clawdbot.json`, säkerställ APIClaw headers:
|
|
591
|
+
|
|
592
|
+
```json
|
|
593
|
+
{
|
|
594
|
+
"apiclaw": {
|
|
595
|
+
"sessionToken": "<APICLAW_SESSION_TOKEN>",
|
|
596
|
+
"aiBackend": "claude-3-opus"
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## 5. Agent Breakdown for Implementation
|
|
604
|
+
|
|
605
|
+
### Agent 1: Schema + Backend Core
|
|
606
|
+
|
|
607
|
+
**Filer:**
|
|
608
|
+
- `convex/schema.ts` — Lägg till `searchLogs`, `workspaceMembers`, uppdatera `workspaces` och `subagents`
|
|
609
|
+
- `convex/searchLogs.ts` — CRUD + stats
|
|
610
|
+
- `convex/teams.ts` — Invite flow
|
|
611
|
+
|
|
612
|
+
**Verifiering:**
|
|
613
|
+
- `npx convex dev --once` kompilerar
|
|
614
|
+
- Alla nya queries/mutations fungerar
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
### Agent 2: MCP Server Updates
|
|
619
|
+
|
|
620
|
+
**Filer:**
|
|
621
|
+
- `src/index.ts` — Lägg till search logging
|
|
622
|
+
- `src/headers.ts` — Extrahera X-APIClaw-AI-Backend
|
|
623
|
+
- `src/tracking.ts` — Uppdatera AI backend på workspace/subagent
|
|
624
|
+
|
|
625
|
+
**Verifiering:**
|
|
626
|
+
- Sökningar loggas till `searchLogs`
|
|
627
|
+
- AI backend sparas korrekt
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
### Agent 3: Analytics Search UI
|
|
632
|
+
|
|
633
|
+
**Filer:**
|
|
634
|
+
- `landing/src/app/workspace/page.tsx` — Lägg till Search subtab
|
|
635
|
+
- `landing/src/components/SearchAnalytics.tsx` — Stats, top queries, zero-results
|
|
636
|
+
|
|
637
|
+
**Verifiering:**
|
|
638
|
+
- Search-tab synlig under Analytics
|
|
639
|
+
- Visar korrekt data från `searchLogs`
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
### Agent 4: My Agents Enhancement
|
|
644
|
+
|
|
645
|
+
**Filer:**
|
|
646
|
+
- `landing/src/app/workspace/page.tsx` — Uppdatera My Agents sektion
|
|
647
|
+
- `landing/src/components/AgentCard.tsx` — Ny komponent för agent display
|
|
648
|
+
- `landing/src/components/RegisterAgentModal.tsx` — Modal för pre-registration
|
|
649
|
+
|
|
650
|
+
**Verifiering:**
|
|
651
|
+
- Primary Agent visar namn + AI backend
|
|
652
|
+
- Task Agents listar med description
|
|
653
|
+
- "Register New Agent" fungerar
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
### Agent 5: Teams UI
|
|
658
|
+
|
|
659
|
+
**Filer:**
|
|
660
|
+
- `landing/src/app/workspace/page.tsx` — Lägg till Team sektion i Settings
|
|
661
|
+
- `landing/src/components/TeamMembers.tsx` — Lista members
|
|
662
|
+
- `landing/src/components/InviteModal.tsx` — Invite modal (med Coming Soon state)
|
|
663
|
+
|
|
664
|
+
**Verifiering:**
|
|
665
|
+
- Team-sektion visas i Settings
|
|
666
|
+
- Owner visas
|
|
667
|
+
- "Coming Soon" visas vid invite-klick
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## 6. File Changes Summary
|
|
672
|
+
|
|
673
|
+
### Convex (Backend)
|
|
674
|
+
|
|
675
|
+
| Fil | Action | Beskrivning |
|
|
676
|
+
|-----|--------|-------------|
|
|
677
|
+
| `schema.ts` | EDIT | Lägg till `searchLogs`, `workspaceMembers`, uppdatera existing |
|
|
678
|
+
| `searchLogs.ts` | CREATE | log, getStats, getRecent |
|
|
679
|
+
| `teams.ts` | CREATE | getMembers, inviteMember, acceptInvite, removeMember |
|
|
680
|
+
| `agents.ts` | EDIT | Lägg till registerTaskAgent, updateAIBackend |
|
|
681
|
+
|
|
682
|
+
### MCP Server (src/)
|
|
683
|
+
|
|
684
|
+
| Fil | Action | Beskrivning |
|
|
685
|
+
|-----|--------|-------------|
|
|
686
|
+
| `index.ts` | EDIT | Lägg till search logging, AI backend header |
|
|
687
|
+
| `headers.ts` | CREATE | Header extraction utilities |
|
|
688
|
+
|
|
689
|
+
### Landing (Dashboard)
|
|
690
|
+
|
|
691
|
+
| Fil | Action | Beskrivning |
|
|
692
|
+
|-----|--------|-------------|
|
|
693
|
+
| `workspace/page.tsx` | EDIT | Search subtab, Teams section, My Agents update |
|
|
694
|
+
| `components/SearchAnalytics.tsx` | CREATE | Search analytics component |
|
|
695
|
+
| `components/AgentCard.tsx` | CREATE | Agent display card |
|
|
696
|
+
| `components/RegisterAgentModal.tsx` | CREATE | Agent registration |
|
|
697
|
+
| `components/TeamMembers.tsx` | CREATE | Team list |
|
|
698
|
+
| `components/InviteModal.tsx` | CREATE | Invite with Coming Soon |
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## 7. Deployment
|
|
703
|
+
|
|
704
|
+
1. Deploy Convex schema först (`npx convex deploy`)
|
|
705
|
+
2. Deploy MCP server (`npm run build && npm publish`)
|
|
706
|
+
3. Deploy Landing (`vercel --prod`)
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
*Ready for implementation. Spawn agents when approved.*
|