@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.
@@ -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.*