@agentforge-ai/cli 0.4.3 → 0.5.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.
Files changed (67) hide show
  1. package/dist/default/convex/agents.ts +204 -0
  2. package/dist/default/convex/apiKeys.ts +133 -0
  3. package/dist/default/convex/cronJobs.ts +224 -0
  4. package/dist/default/convex/files.ts +103 -0
  5. package/dist/default/convex/folders.ts +110 -0
  6. package/dist/default/convex/heartbeat.ts +371 -0
  7. package/dist/default/convex/logs.ts +66 -0
  8. package/dist/default/convex/mastraIntegration.ts +185 -0
  9. package/dist/default/convex/mcpConnections.ts +127 -0
  10. package/dist/default/convex/messages.ts +90 -0
  11. package/dist/default/convex/projects.ts +114 -0
  12. package/dist/default/convex/schema.ts +150 -83
  13. package/dist/default/convex/sessions.ts +174 -0
  14. package/dist/default/convex/settings.ts +79 -0
  15. package/dist/default/convex/skills.ts +178 -0
  16. package/dist/default/convex/threads.ts +100 -0
  17. package/dist/default/convex/usage.ts +195 -0
  18. package/dist/default/convex/vault.ts +397 -0
  19. package/dist/default/dashboard/app/main.tsx +7 -3
  20. package/dist/default/dashboard/app/routes/agents.tsx +103 -161
  21. package/dist/default/dashboard/app/routes/chat.tsx +163 -317
  22. package/dist/default/dashboard/app/routes/connections.tsx +247 -386
  23. package/dist/default/dashboard/app/routes/cron.tsx +127 -286
  24. package/dist/default/dashboard/app/routes/files.tsx +184 -167
  25. package/dist/default/dashboard/app/routes/index.tsx +63 -96
  26. package/dist/default/dashboard/app/routes/projects.tsx +106 -225
  27. package/dist/default/dashboard/app/routes/sessions.tsx +87 -253
  28. package/dist/default/dashboard/app/routes/settings.tsx +316 -532
  29. package/dist/default/dashboard/app/routes/skills.tsx +329 -216
  30. package/dist/default/dashboard/app/routes/usage.tsx +107 -150
  31. package/dist/default/dashboard/tsconfig.json +3 -2
  32. package/dist/default/dashboard/vite.config.ts +6 -0
  33. package/dist/index.js +256 -49
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -1
  36. package/templates/default/convex/agents.ts +204 -0
  37. package/templates/default/convex/apiKeys.ts +133 -0
  38. package/templates/default/convex/cronJobs.ts +224 -0
  39. package/templates/default/convex/files.ts +103 -0
  40. package/templates/default/convex/folders.ts +110 -0
  41. package/templates/default/convex/heartbeat.ts +371 -0
  42. package/templates/default/convex/logs.ts +66 -0
  43. package/templates/default/convex/mastraIntegration.ts +185 -0
  44. package/templates/default/convex/mcpConnections.ts +127 -0
  45. package/templates/default/convex/messages.ts +90 -0
  46. package/templates/default/convex/projects.ts +114 -0
  47. package/templates/default/convex/schema.ts +150 -83
  48. package/templates/default/convex/sessions.ts +174 -0
  49. package/templates/default/convex/settings.ts +79 -0
  50. package/templates/default/convex/skills.ts +178 -0
  51. package/templates/default/convex/threads.ts +100 -0
  52. package/templates/default/convex/usage.ts +195 -0
  53. package/templates/default/convex/vault.ts +397 -0
  54. package/templates/default/dashboard/app/main.tsx +7 -3
  55. package/templates/default/dashboard/app/routes/agents.tsx +103 -161
  56. package/templates/default/dashboard/app/routes/chat.tsx +163 -317
  57. package/templates/default/dashboard/app/routes/connections.tsx +247 -386
  58. package/templates/default/dashboard/app/routes/cron.tsx +127 -286
  59. package/templates/default/dashboard/app/routes/files.tsx +184 -167
  60. package/templates/default/dashboard/app/routes/index.tsx +63 -96
  61. package/templates/default/dashboard/app/routes/projects.tsx +106 -225
  62. package/templates/default/dashboard/app/routes/sessions.tsx +87 -253
  63. package/templates/default/dashboard/app/routes/settings.tsx +316 -532
  64. package/templates/default/dashboard/app/routes/skills.tsx +329 -216
  65. package/templates/default/dashboard/app/routes/usage.tsx +107 -150
  66. package/templates/default/dashboard/tsconfig.json +3 -2
  67. package/templates/default/dashboard/vite.config.ts +6 -0
@@ -1,252 +1,365 @@
1
1
  import { createFileRoute } from '@tanstack/react-router';
2
2
  import { DashboardLayout } from '../components/DashboardLayout';
3
3
  import { useState, useMemo } from 'react';
4
- // import { useQuery, useMutation } from 'convex/react';
5
- // import { api } from '../../convex/_generated/api';
6
- import { Sparkles, Download, Trash2, Plus, Search, Filter, X } from 'lucide-react';
7
-
8
- // --- Mock Data and Types ---
9
- type SkillCategory = 'Tools' | 'Knowledge' | 'Workflows' | 'Integrations';
10
-
11
- interface Skill {
12
- id: string;
13
- name: string;
14
- description: string;
15
- category: SkillCategory;
16
- version: string;
17
- isInstalled: boolean;
18
- }
4
+ import { useQuery, useMutation } from 'convex/react';
5
+ import { api } from '@convex/_generated/api';
6
+ import { Sparkles, Download, Trash2, Plus, Search, X, Check, Code, Globe, Calculator, FileText, Database, Mail, Wrench } from 'lucide-react';
19
7
 
20
- const mockSkills: Skill[] = [
21
- { id: '1', name: 'Web Scraper', description: 'Extracts data from websites using CSS selectors.', category: 'Tools', version: '1.2.0', isInstalled: true },
22
- { id: '2', name: 'Sentiment Analysis', description: 'Analyzes text to determine emotional tone.', category: 'Knowledge', version: '2.0.1', isInstalled: false },
23
- { id: '3', name: 'Daily Report', description: 'Generates a daily summary of activities.', category: 'Workflows', version: '1.0.0', isInstalled: true },
24
- { id: '4', name: 'GitHub Integration', description: 'Connects to GitHub to manage repositories.', category: 'Integrations', version: '1.5.3', isInstalled: false },
25
- { id: '5', name: 'Image Resizer', description: 'Resizes images to specified dimensions.', category: 'Tools', version: '1.1.0', isInstalled: false },
26
- { id: '6', name: 'Company Financials', description: 'Provides financial data for public companies.', category: 'Knowledge', version: '3.1.0', isInstalled: true },
27
- ];
8
+ export const Route = createFileRoute('/skills')({ component: SkillsPage });
28
9
 
29
- const CATEGORIES: SkillCategory[] = ['Tools', 'Knowledge', 'Workflows', 'Integrations'];
30
-
31
- // --- Components ---
32
-
33
- function CreateSkillDialog({ open, onOpenChange, onCreateSkill }: { open: boolean; onOpenChange: (open: boolean) => void; onCreateSkill: (skill: Omit<Skill, 'id' | 'isInstalled'>) => void; }) {
34
- const [name, setName] = useState('');
35
- const [description, setDescription] = useState('');
36
- const [category, setCategory] = useState<SkillCategory>('Tools');
37
- const [version, setVersion] = useState('1.0.0');
38
-
39
- const handleSubmit = (e: React.FormEvent) => {
40
- e.preventDefault();
41
- if (!name || !description || !version) return;
42
- onCreateSkill({ name, description, category, version });
43
- onOpenChange(false);
44
- setName('');
45
- setDescription('');
46
- setCategory('Tools');
47
- setVersion('1.0.0');
48
- };
10
+ // ─── Prebuilt Skills Catalog ─────────────────────────────────────
11
+ const PREBUILT_SKILLS = [
12
+ {
13
+ name: 'web-search',
14
+ displayName: 'Web Search',
15
+ description: 'Search the web using DuckDuckGo and return structured results. Supports query refinement and result filtering.',
16
+ category: 'Tools',
17
+ version: '1.0.0',
18
+ author: 'AgentForge',
19
+ icon: Globe,
20
+ code: `import { createTool } from '@mastra/core';
21
+ import { z } from 'zod';
49
22
 
50
- if (!open) return null;
23
+ export const webSearch = createTool({
24
+ id: 'web-search',
25
+ description: 'Search the web and return results',
26
+ inputSchema: z.object({
27
+ query: z.string().describe('Search query'),
28
+ maxResults: z.number().default(5).describe('Maximum results to return'),
29
+ }),
30
+ execute: async ({ context }) => {
31
+ const url = \`https://api.duckduckgo.com/?q=\${encodeURIComponent(context.query)}&format=json&no_redirect=1\`;
32
+ const res = await fetch(url);
33
+ const data = await res.json();
34
+ return { results: data.RelatedTopics?.slice(0, context.maxResults) || [], source: 'duckduckgo' };
35
+ },
36
+ });`,
37
+ },
38
+ {
39
+ name: 'code-executor',
40
+ displayName: 'Code Executor',
41
+ description: 'Safely execute JavaScript/TypeScript code snippets in a sandboxed environment and return the output.',
42
+ category: 'Tools',
43
+ version: '1.0.0',
44
+ author: 'AgentForge',
45
+ icon: Code,
46
+ code: `import { createTool } from '@mastra/core';
47
+ import { z } from 'zod';
51
48
 
52
- return (
53
- <div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
54
- <div className="bg-card p-6 rounded-lg shadow-xl w-full max-w-md border border-border" onClick={(e) => e.stopPropagation()}>
55
- <div className="flex justify-between items-center mb-4">
56
- <h2 className="text-xl font-semibold text-foreground">Create Custom Skill</h2>
57
- <button onClick={() => onOpenChange(false)} className="text-muted-foreground hover:text-foreground"><X size={20} /></button>
58
- </div>
59
- <form onSubmit={handleSubmit} className="space-y-4">
60
- <div>
61
- <label htmlFor="name" className="block text-sm font-medium text-muted-foreground mb-1">Skill Name</label>
62
- <input id="name" value={name} onChange={(e) => setName(e.target.value)} required className="w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary" />
63
- </div>
64
- <div>
65
- <label htmlFor="description" className="block text-sm font-medium text-muted-foreground mb-1">Description</label>
66
- <textarea id="description" value={description} onChange={(e) => setDescription(e.target.value)} required rows={3} className="w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary"></textarea>
67
- </div>
68
- <div className="grid grid-cols-2 gap-4">
69
- <div>
70
- <label htmlFor="category" className="block text-sm font-medium text-muted-foreground mb-1">Category</label>
71
- <select id="category" value={category} onChange={(e) => setCategory(e.target.value as SkillCategory)} className="w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary">
72
- {CATEGORIES.map(cat => <option key={cat} value={cat}>{cat}</option>)}
73
- </select>
74
- </div>
75
- <div>
76
- <label htmlFor="version" className="block text-sm font-medium text-muted-foreground mb-1">Version</label>
77
- <input id="version" value={version} onChange={(e) => setVersion(e.target.value)} required className="w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary" />
78
- </div>
79
- </div>
80
- <div className="flex justify-end gap-2 pt-4">
81
- <button type="button" onClick={() => onOpenChange(false)} className="px-4 py-2 rounded-md border border-border text-foreground hover:bg-zinc-800 transition-colors">Cancel</button>
82
- <button type="submit" className="px-4 py-2 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">Create Skill</button>
83
- </div>
84
- </form>
85
- </div>
86
- </div>
87
- );
88
- }
49
+ export const codeExecutor = createTool({
50
+ id: 'code-executor',
51
+ description: 'Execute JavaScript code and return the result',
52
+ inputSchema: z.object({
53
+ code: z.string().describe('JavaScript code to execute'),
54
+ }),
55
+ execute: async ({ context }) => {
56
+ try {
57
+ const fn = new Function('return (async () => {' + context.code + '})()');
58
+ const result = await fn();
59
+ return { success: true, output: String(result) };
60
+ } catch (error: any) {
61
+ return { success: false, error: error.message };
62
+ }
63
+ },
64
+ });`,
65
+ },
66
+ {
67
+ name: 'calculator',
68
+ displayName: 'Calculator',
69
+ description: 'Perform mathematical calculations. Supports basic arithmetic, trigonometry, and common math functions.',
70
+ category: 'Tools',
71
+ version: '1.0.0',
72
+ author: 'AgentForge',
73
+ icon: Calculator,
74
+ code: `import { createTool } from '@mastra/core';
75
+ import { z } from 'zod';
89
76
 
90
- function SkillCard({ skill, onToggleInstall }: { skill: Skill; onToggleInstall: (id: string) => void; }) {
91
- // const uninstallSkill = useMutation(api.skills.uninstall);
92
- // const installSkill = useMutation(api.skills.install);
93
-
94
- const handleToggle = () => {
95
- onToggleInstall(skill.id);
96
- // if (skill.isInstalled) {
97
- // uninstallSkill({ id: skill.id });
98
- // } else {
99
- // installSkill({ id: skill.id });
100
- // }
101
- };
77
+ export const calculator = createTool({
78
+ id: 'calculator',
79
+ description: 'Evaluate a mathematical expression',
80
+ inputSchema: z.object({
81
+ expression: z.string().describe('Math expression to evaluate, e.g. "2 + 2 * 3"'),
82
+ }),
83
+ execute: async ({ context }) => {
84
+ try {
85
+ const sanitized = context.expression.replace(/[^0-9+\\-*/().%\\s]/g, '');
86
+ const result = Function('"use strict"; return (' + sanitized + ')')();
87
+ return { result: Number(result), expression: context.expression };
88
+ } catch (error: any) {
89
+ return { error: 'Invalid expression: ' + error.message };
90
+ }
91
+ },
92
+ });`,
93
+ },
94
+ {
95
+ name: 'text-summarizer',
96
+ displayName: 'Text Summarizer',
97
+ description: 'Summarize long text into concise bullet points or paragraphs using extractive summarization.',
98
+ category: 'Knowledge',
99
+ version: '1.0.0',
100
+ author: 'AgentForge',
101
+ icon: FileText,
102
+ code: `import { createTool } from '@mastra/core';
103
+ import { z } from 'zod';
102
104
 
103
- return (
104
- <div className="bg-card border border-border rounded-lg p-4 flex flex-col h-full transition-shadow hover:shadow-lg hover:shadow-primary/10">
105
- <div className="flex-grow">
106
- <div className="flex items-start justify-between mb-2">
107
- <div className="flex items-center gap-3">
108
- <div className="bg-primary/10 p-2 rounded-md">
109
- <Sparkles className="text-primary" size={20} />
110
- </div>
111
- <div>
112
- <h3 className="font-semibold text-foreground">{skill.name}</h3>
113
- <p className="text-xs text-muted-foreground">v{skill.version}</p>
114
- </div>
115
- </div>
116
- <span className="text-xs bg-zinc-800 border border-zinc-700 text-muted-foreground px-2 py-0.5 rounded-full whitespace-nowrap">{skill.category}</span>
117
- </div>
118
- <p className="text-sm text-muted-foreground mt-3">
119
- {skill.description}
120
- </p>
121
- </div>
122
- <button
123
- onClick={handleToggle}
124
- className={`mt-4 w-full flex items-center justify-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${skill.isInstalled ? 'bg-destructive/10 text-destructive hover:bg-destructive/20' : 'bg-primary/10 text-primary hover:bg-primary/20'}`}>
125
- {skill.isInstalled ? <Trash2 size={16} /> : <Download size={16} />}
126
- {skill.isInstalled ? 'Uninstall' : 'Install'}
127
- </button>
128
- </div>
129
- );
130
- }
105
+ export const textSummarizer = createTool({
106
+ id: 'text-summarizer',
107
+ description: 'Summarize text into key points',
108
+ inputSchema: z.object({
109
+ text: z.string().describe('Text to summarize'),
110
+ maxSentences: z.number().default(3).describe('Maximum sentences in summary'),
111
+ }),
112
+ execute: async ({ context }) => {
113
+ const sentences = context.text.match(/[^.!?]+[.!?]+/g) || [context.text];
114
+ const scored = sentences.map((s, i) => ({
115
+ text: s.trim(),
116
+ score: s.split(' ').length * (i === 0 ? 1.5 : 1),
117
+ }));
118
+ scored.sort((a, b) => b.score - a.score);
119
+ return { summary: scored.slice(0, context.maxSentences).map(s => s.text).join(' ') };
120
+ },
121
+ });`,
122
+ },
123
+ {
124
+ name: 'json-transformer',
125
+ displayName: 'JSON Transformer',
126
+ description: 'Parse, validate, and transform JSON data. Extract fields, flatten nested objects, and convert formats.',
127
+ category: 'Tools',
128
+ version: '1.0.0',
129
+ author: 'AgentForge',
130
+ icon: Database,
131
+ code: `import { createTool } from '@mastra/core';
132
+ import { z } from 'zod';
131
133
 
132
- export const Route = createFileRoute('/skills')({ component: SkillsPage });
134
+ export const jsonTransformer = createTool({
135
+ id: 'json-transformer',
136
+ description: 'Parse and transform JSON data',
137
+ inputSchema: z.object({
138
+ json: z.string().describe('JSON string to transform'),
139
+ operation: z.enum(['parse', 'flatten', 'extract']).describe('Operation to perform'),
140
+ path: z.string().optional().describe('JSON path for extraction (e.g. "data.users")'),
141
+ }),
142
+ execute: async ({ context }) => {
143
+ const data = JSON.parse(context.json);
144
+ if (context.operation === 'parse') return { result: data };
145
+ if (context.operation === 'extract' && context.path) {
146
+ const keys = context.path.split('.');
147
+ let val = data;
148
+ for (const k of keys) val = val?.[k];
149
+ return { result: val };
150
+ }
151
+ if (context.operation === 'flatten') {
152
+ const flat: Record<string, any> = {};
153
+ const recurse = (obj: any, prefix = '') => {
154
+ for (const [k, v] of Object.entries(obj)) {
155
+ const key = prefix ? prefix + '.' + k : k;
156
+ if (typeof v === 'object' && v !== null && !Array.isArray(v)) recurse(v, key);
157
+ else flat[key] = v;
158
+ }
159
+ };
160
+ recurse(data);
161
+ return { result: flat };
162
+ }
163
+ return { error: 'Unknown operation' };
164
+ },
165
+ });`,
166
+ },
167
+ {
168
+ name: 'email-drafter',
169
+ displayName: 'Email Drafter',
170
+ description: 'Generate professional email drafts based on context, tone, and recipient information.',
171
+ category: 'Workflows',
172
+ version: '1.0.0',
173
+ author: 'AgentForge',
174
+ icon: Mail,
175
+ code: `import { createTool } from '@mastra/core';
176
+ import { z } from 'zod';
177
+
178
+ export const emailDrafter = createTool({
179
+ id: 'email-drafter',
180
+ description: 'Draft a professional email',
181
+ inputSchema: z.object({
182
+ to: z.string().describe('Recipient name or role'),
183
+ subject: z.string().describe('Email subject'),
184
+ context: z.string().describe('What the email should convey'),
185
+ tone: z.enum(['formal', 'casual', 'friendly']).default('formal'),
186
+ }),
187
+ execute: async ({ context: ctx }) => {
188
+ const greeting = ctx.tone === 'formal' ? 'Dear' : ctx.tone === 'friendly' ? 'Hi' : 'Hello';
189
+ const closing = ctx.tone === 'formal' ? 'Best regards' : ctx.tone === 'friendly' ? 'Cheers' : 'Thanks';
190
+ return {
191
+ draft: \`Subject: \${ctx.subject}\\n\\n\${greeting} \${ctx.to},\\n\\n\${ctx.context}\\n\\n\${closing}\`,
192
+ metadata: { tone: ctx.tone, to: ctx.to },
193
+ };
194
+ },
195
+ });`,
196
+ },
197
+ ];
198
+
199
+ const CATEGORIES = ['All', 'Tools', 'Knowledge', 'Workflows'];
133
200
 
134
201
  function SkillsPage() {
135
- const [skills, setSkills] = useState(mockSkills);
202
+ const installedSkills = useQuery(api.skills.list, {}) ?? [];
203
+ const installSkill = useMutation(api.skills.create);
204
+ const removeSkill = useMutation(api.skills.remove);
205
+ const toggleSkill = useMutation(api.skills.toggleEnabled);
206
+
136
207
  const [searchQuery, setSearchQuery] = useState('');
137
- const [selectedCategories, setSelectedCategories] = useState<SkillCategory[]>([]);
138
- const [isCreateDialogOpen, setCreateDialogOpen] = useState(false);
139
-
140
- // const allSkillsQuery = useQuery(api.skills.list);
141
- // const createSkillMutation = useMutation(api.skills.create);
142
- // const isLoading = allSkillsQuery === undefined;
143
- // const skillsData = allSkillsQuery || [];
144
-
145
- const handleToggleCategory = (category: SkillCategory) => {
146
- setSelectedCategories(prev =>
147
- prev.includes(category)
148
- ? prev.filter(c => c !== category)
149
- : [...prev, category]
150
- );
151
- };
208
+ const [categoryFilter, setCategoryFilter] = useState('All');
209
+ const [tab, setTab] = useState<'installed' | 'marketplace'>('marketplace');
210
+ const [selectedSkill, setSelectedSkill] = useState<typeof PREBUILT_SKILLS[0] | null>(null);
152
211
 
153
- const handleCreateSkill = (newSkillData: Omit<Skill, 'id' | 'isInstalled'>) => {
154
- const newSkill: Skill = {
155
- ...newSkillData,
156
- id: `custom-${Date.now()}`,
157
- isInstalled: false,
158
- };
159
- setSkills(prev => [newSkill, ...prev]);
160
- // createSkillMutation(newSkillData).catch(console.error);
161
- };
212
+ const installedNames = new Set(installedSkills.map((s: any) => s.name));
162
213
 
163
- const handleToggleInstall = (id: string) => {
164
- setSkills(prev => prev.map(s => s.id === id ? { ...s, isInstalled: !s.isInstalled } : s));
165
- // Note: The actual mutation would be called in SkillCard
166
- };
214
+ const filteredMarketplace = useMemo(() => {
215
+ let result = PREBUILT_SKILLS;
216
+ if (categoryFilter !== 'All') result = result.filter(s => s.category === categoryFilter);
217
+ if (searchQuery) {
218
+ const q = searchQuery.toLowerCase();
219
+ result = result.filter(s => s.displayName.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
220
+ }
221
+ return result;
222
+ }, [searchQuery, categoryFilter]);
223
+
224
+ const filteredInstalled = useMemo(() => {
225
+ let result = installedSkills;
226
+ if (categoryFilter !== 'All') result = result.filter((s: any) => s.category === categoryFilter);
227
+ if (searchQuery) {
228
+ const q = searchQuery.toLowerCase();
229
+ result = result.filter((s: any) => s.displayName.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
230
+ }
231
+ return result;
232
+ }, [installedSkills, searchQuery, categoryFilter]);
167
233
 
168
- const filteredSkills = useMemo(() => {
169
- // Replace `skills` with `skillsData` when using Convex
170
- return skills.filter(skill =>
171
- skill.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
172
- (selectedCategories.length === 0 || selectedCategories.includes(skill.category))
173
- );
174
- }, [skills, searchQuery, selectedCategories]); // Replace `skills` with `skillsData`
234
+ const handleInstall = async (skill: typeof PREBUILT_SKILLS[0]) => {
235
+ await installSkill({
236
+ name: skill.name,
237
+ displayName: skill.displayName,
238
+ description: skill.description,
239
+ category: skill.category,
240
+ version: skill.version,
241
+ author: skill.author,
242
+ code: skill.code,
243
+ });
244
+ };
175
245
 
176
- const isLoading = false; // When using Convex, this would be `allSkillsQuery === undefined`
246
+ const handleUninstall = async (id: any) => {
247
+ if (confirm('Uninstall this skill?')) {
248
+ await removeSkill({ id });
249
+ }
250
+ };
177
251
 
178
252
  return (
179
253
  <DashboardLayout>
180
- <div className="p-4 sm:p-6 lg:p-8 bg-background text-foreground min-h-screen">
181
- <header className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
254
+ <div className="space-y-6">
255
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
182
256
  <div>
183
- <h1 className="text-2xl font-bold tracking-tight">Skills Marketplace</h1>
184
- <p className="text-muted-foreground">Discover, install, and create new capabilities for your agents.</p>
257
+ <h1 className="text-3xl font-bold">Skills</h1>
258
+ <p className="text-muted-foreground">Install prebuilt skills or create your own to extend agent capabilities.</p>
185
259
  </div>
186
- <button onClick={() => setCreateDialogOpen(true)} className="flex items-center justify-center gap-2 px-4 py-2 rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">
187
- <Plus size={16} /> Create Skill
260
+ </div>
261
+
262
+ {/* Tabs */}
263
+ <div className="flex gap-1 bg-muted p-1 rounded-lg w-fit">
264
+ <button onClick={() => setTab('marketplace')} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${tab === 'marketplace' ? 'bg-card shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>
265
+ <Sparkles className="w-4 h-4 inline mr-2" />Marketplace ({PREBUILT_SKILLS.length})
266
+ </button>
267
+ <button onClick={() => setTab('installed')} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${tab === 'installed' ? 'bg-card shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>
268
+ <Check className="w-4 h-4 inline mr-2" />Installed ({installedSkills.length})
188
269
  </button>
189
- </header>
190
-
191
- <div className="mb-6 space-y-4">
192
- <div className="relative">
193
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={20} />
194
- <input
195
- type="text"
196
- placeholder="Search skills by name..."
197
- value={searchQuery}
198
- onChange={(e) => setSearchQuery(e.target.value)}
199
- className="w-full bg-card border border-border rounded-md pl-10 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary"
200
- />
270
+ </div>
271
+
272
+ {/* Filters */}
273
+ <div className="flex flex-col sm:flex-row gap-3">
274
+ <div className="relative flex-1 max-w-sm">
275
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
276
+ <input type="text" placeholder="Search skills..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pl-9 pr-3 py-2 bg-card border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary w-full" />
201
277
  </div>
202
- <div className="flex items-center gap-2 flex-wrap">
203
- <Filter size={16} className="text-muted-foreground" />
204
- <span className="text-sm font-medium text-muted-foreground">Filter by:</span>
205
- {CATEGORIES.map(category => (
206
- <button
207
- key={category}
208
- onClick={() => handleToggleCategory(category)}
209
- className={`px-3 py-1 text-sm rounded-full border transition-colors ${selectedCategories.includes(category) ? 'bg-primary text-primary-foreground border-primary' : 'bg-card hover:bg-zinc-800 border-border'}`}>
210
- {category}
211
- </button>
278
+ <div className="flex gap-2">
279
+ {CATEGORIES.map(cat => (
280
+ <button key={cat} onClick={() => setCategoryFilter(cat)} className={`px-3 py-1.5 rounded-lg text-sm ${categoryFilter === cat ? 'bg-primary text-primary-foreground' : 'bg-card border border-border text-muted-foreground hover:text-foreground'}`}>{cat}</button>
212
281
  ))}
213
- {selectedCategories.length > 0 && (
214
- <button onClick={() => setSelectedCategories([])} className="text-sm text-muted-foreground hover:text-foreground underline">Clear</button>
215
- )}
216
282
  </div>
217
283
  </div>
218
284
 
219
- {isLoading ? (
220
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
221
- {[...Array(8)].map((_, i) => (
222
- <div key={i} className="bg-card border border-border rounded-lg p-4 animate-pulse">
223
- <div className="h-8 bg-zinc-800 rounded w-12 mb-3"></div>
224
- <div className="h-5 bg-zinc-800 rounded w-3/4 mb-2"></div>
225
- <div className="h-4 bg-zinc-800 rounded w-1/4 mb-4"></div>
226
- <div className="h-4 bg-zinc-800 rounded w-full mb-1"></div>
227
- <div className="h-4 bg-zinc-800 rounded w-5/6"></div>
228
- <div className="h-10 bg-zinc-800 rounded mt-4"></div>
229
- </div>
230
- ))}
231
- </div>
232
- ) : filteredSkills.length > 0 ? (
233
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
234
- {filteredSkills.map(skill => (
235
- <SkillCard key={skill.id} skill={skill} onToggleInstall={handleToggleInstall} />
236
- ))}
285
+ {/* Grid */}
286
+ {tab === 'marketplace' ? (
287
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
288
+ {filteredMarketplace.map(skill => {
289
+ const isInstalled = installedNames.has(skill.name);
290
+ const Icon = skill.icon;
291
+ return (
292
+ <div key={skill.name} className="bg-card border border-border rounded-lg p-5 shadow-sm hover:shadow-md transition-shadow">
293
+ <div className="flex items-start justify-between mb-3">
294
+ <div className="flex items-center gap-2">
295
+ <div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center"><Icon className="w-4 h-4 text-primary" /></div>
296
+ <div>
297
+ <h3 className="font-semibold text-foreground">{skill.displayName}</h3>
298
+ <p className="text-xs text-muted-foreground">v{skill.version} by {skill.author}</p>
299
+ </div>
300
+ </div>
301
+ <span className="text-xs bg-muted px-2 py-0.5 rounded">{skill.category}</span>
302
+ </div>
303
+ <p className="text-sm text-muted-foreground mb-4 line-clamp-2">{skill.description}</p>
304
+ <div className="flex items-center justify-between pt-3 border-t border-border">
305
+ <button onClick={() => setSelectedSkill(skill)} className="text-xs text-primary hover:underline">View Code</button>
306
+ {isInstalled ? (
307
+ <span className="text-xs text-green-500 flex items-center gap-1"><Check className="w-3.5 h-3.5" /> Installed</span>
308
+ ) : (
309
+ <button onClick={() => handleInstall(skill)} className="bg-primary text-primary-foreground px-3 py-1.5 rounded-lg text-xs hover:bg-primary/90 flex items-center gap-1">
310
+ <Download className="w-3.5 h-3.5" /> Install
311
+ </button>
312
+ )}
313
+ </div>
314
+ </div>
315
+ );
316
+ })}
237
317
  </div>
238
318
  ) : (
239
- <div className="text-center py-16 border-2 border-dashed border-border rounded-lg bg-card">
240
- <Sparkles className="mx-auto h-12 w-12 text-muted-foreground" />
241
- <h3 className="mt-2 text-lg font-medium text-foreground">No Skills Found</h3>
242
- <p className="mt-1 text-sm text-muted-foreground">Your search or filter criteria did not match any skills. Try a different query.</p>
243
- <button onClick={() => { setSearchQuery(''); setSelectedCategories([]); }} className="mt-4 px-4 py-2 rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">
244
- Clear Filters
245
- </button>
319
+ filteredInstalled.length === 0 ? (
320
+ <div className="text-center py-16 bg-card border border-border rounded-lg">
321
+ <Wrench className="w-16 h-16 text-muted-foreground/30 mx-auto mb-4" />
322
+ <h3 className="text-lg font-semibold mb-2">No skills installed</h3>
323
+ <p className="text-muted-foreground">Browse the marketplace to install skills for your agents.</p>
324
+ </div>
325
+ ) : (
326
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
327
+ {filteredInstalled.map((skill: any) => (
328
+ <div key={skill._id} className="bg-card border border-border rounded-lg p-5 shadow-sm">
329
+ <div className="flex items-start justify-between mb-3">
330
+ <h3 className="font-semibold text-foreground">{skill.displayName}</h3>
331
+ <span className={`text-xs px-2 py-0.5 rounded-full ${skill.isEnabled ? 'bg-green-500/10 text-green-500' : 'bg-muted text-muted-foreground'}`}>{skill.isEnabled ? 'Enabled' : 'Disabled'}</span>
332
+ </div>
333
+ <p className="text-sm text-muted-foreground mb-4 line-clamp-2">{skill.description}</p>
334
+ <div className="flex items-center justify-between pt-3 border-t border-border">
335
+ <button onClick={() => toggleSkill({ id: skill._id })} className="text-xs text-muted-foreground hover:text-foreground">{skill.isEnabled ? 'Disable' : 'Enable'}</button>
336
+ <button onClick={() => handleUninstall(skill._id)} className="p-1.5 rounded hover:bg-destructive/10"><Trash2 className="w-4 h-4 text-destructive" /></button>
337
+ </div>
338
+ </div>
339
+ ))}
340
+ </div>
341
+ )
342
+ )}
343
+
344
+ {/* Code Preview Modal */}
345
+ {selectedSkill && (
346
+ <div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
347
+ <div className="bg-card border border-border rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col">
348
+ <div className="flex justify-between items-center p-4 border-b border-border">
349
+ <h2 className="text-lg font-bold">{selectedSkill.displayName} — Source Code</h2>
350
+ <button onClick={() => setSelectedSkill(null)} className="text-muted-foreground hover:text-foreground"><X className="h-5 w-5" /></button>
351
+ </div>
352
+ <pre className="flex-1 overflow-auto p-4 text-xs font-mono bg-background text-foreground">{selectedSkill.code}</pre>
353
+ <div className="p-4 border-t border-border flex justify-end gap-2">
354
+ <button onClick={() => setSelectedSkill(null)} className="px-4 py-2 rounded-lg bg-muted text-muted-foreground text-sm">Close</button>
355
+ {!installedNames.has(selectedSkill.name) && (
356
+ <button onClick={() => { handleInstall(selectedSkill); setSelectedSkill(null); }} className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm hover:bg-primary/90">Install Skill</button>
357
+ )}
358
+ </div>
359
+ </div>
246
360
  </div>
247
361
  )}
248
362
  </div>
249
- <CreateSkillDialog open={isCreateDialogOpen} onOpenChange={setCreateDialogOpen} onCreateSkill={handleCreateSkill} />
250
363
  </DashboardLayout>
251
364
  );
252
365
  }