@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,413 +1,274 @@
1
-
2
1
  import { createFileRoute } from '@tanstack/react-router';
3
2
  import { DashboardLayout } from '../components/DashboardLayout';
4
- import React, { useState, useMemo } from 'react';
5
- // import { useQuery, useMutation } from 'convex/react';
6
- // import { api } from '../../convex/_generated/api';
7
- import { Plug, Plus, RefreshCw, CheckCircle, XCircle, Trash2, MoreVertical, Edit, Search } from 'lucide-react';
8
-
9
- // --- Mock Data and Types ---
10
- type ConnectionStatus = 'connected' | 'disconnected' | 'testing';
11
- type ConnectionType = 'MCP' | 'API' | 'Webhook';
3
+ import { useState, useMemo } from 'react';
4
+ import { useQuery, useMutation } from 'convex/react';
5
+ import { api } from '@convex/_generated/api';
6
+ import { Plug, Plus, Trash2, Search, X, Check, ExternalLink, Globe, Database, Mail, MessageSquare, FileText, Code, Zap, Shield } from 'lucide-react';
12
7
 
13
- interface Connection {
14
- id: string;
15
- name: string;
16
- type: ConnectionType;
17
- status: ConnectionStatus;
18
- lastConnected: string | null;
19
- serverUrl: string;
20
- protocol: string;
21
- enabled: boolean;
22
- }
8
+ export const Route = createFileRoute('/connections')({ component: ConnectionsPage });
23
9
 
24
- const mockConnections: Connection[] = [
10
+ // ─── MCP Integrations Catalog ─────────────────────────────────────
11
+ const MCP_CATALOG = [
12
+ {
13
+ name: 'GitHub',
14
+ description: 'Access repositories, issues, pull requests, and code search. Automate GitHub workflows from your agents.',
15
+ serverUrl: 'npx -y @modelcontextprotocol/server-github',
16
+ protocol: 'mcp',
17
+ category: 'Development',
18
+ icon: Code,
19
+ authFields: [{ key: 'GITHUB_PERSONAL_ACCESS_TOKEN', label: 'Personal Access Token', placeholder: 'ghp_xxxxxxxxxxxxxxxxxxxx', helpUrl: 'https://github.com/settings/tokens' }],
20
+ capabilities: ['repos', 'issues', 'pull_requests', 'code_search', 'actions'],
21
+ },
25
22
  {
26
- id: '1',
27
- name: 'Cloudflare MCP',
28
- type: 'MCP',
29
- status: 'connected',
30
- lastConnected: new Date(Date.now() - 86400000).toISOString(),
31
- serverUrl: 'https://mcp.cloudflare.com',
23
+ name: 'Slack',
24
+ description: 'Send messages, read channels, manage conversations, and automate Slack workflows.',
25
+ serverUrl: 'npx -y @modelcontextprotocol/server-slack',
32
26
  protocol: 'mcp',
33
- enabled: true,
27
+ category: 'Communication',
28
+ icon: MessageSquare,
29
+ authFields: [{ key: 'SLACK_BOT_TOKEN', label: 'Bot Token', placeholder: 'xoxb-xxxxxxxxxxxx', helpUrl: 'https://api.slack.com/apps' }],
30
+ capabilities: ['send_messages', 'read_channels', 'manage_users'],
34
31
  },
35
32
  {
36
- id: '2',
37
- name: 'Stripe API',
38
- type: 'API',
39
- status: 'disconnected',
40
- lastConnected: new Date(Date.now() - 604800000).toISOString(),
41
- serverUrl: 'https://api.stripe.com',
42
- protocol: 'https',
43
- enabled: true,
33
+ name: 'Google Drive',
34
+ description: 'Search, read, and manage files in Google Drive. Access documents, spreadsheets, and presentations.',
35
+ serverUrl: 'npx -y @modelcontextprotocol/server-gdrive',
36
+ protocol: 'mcp',
37
+ category: 'Productivity',
38
+ icon: FileText,
39
+ authFields: [{ key: 'GOOGLE_CLIENT_ID', label: 'Client ID', placeholder: 'xxxxx.apps.googleusercontent.com', helpUrl: 'https://console.cloud.google.com/apis/credentials' }, { key: 'GOOGLE_CLIENT_SECRET', label: 'Client Secret', placeholder: 'GOCSPX-xxxxxxxxxxxx' }],
40
+ capabilities: ['search_files', 'read_files', 'create_files', 'manage_permissions'],
44
41
  },
45
42
  {
46
- id: '3',
47
- name: 'GitHub Webhook',
48
- type: 'Webhook',
49
- status: 'connected',
50
- lastConnected: new Date().toISOString(),
51
- serverUrl: 'https://api.github.com/webhooks',
52
- protocol: 'https',
53
- enabled: false,
43
+ name: 'PostgreSQL',
44
+ description: 'Query and manage PostgreSQL databases. Run read-only queries, inspect schemas, and analyze data.',
45
+ serverUrl: 'npx -y @modelcontextprotocol/server-postgres',
46
+ protocol: 'mcp',
47
+ category: 'Database',
48
+ icon: Database,
49
+ authFields: [{ key: 'POSTGRES_CONNECTION_STRING', label: 'Connection String', placeholder: 'postgresql://user:pass@host:5432/db' }],
50
+ capabilities: ['query', 'schema_inspection', 'data_analysis'],
51
+ },
52
+ {
53
+ name: 'Brave Search',
54
+ description: 'Web and local search powered by Brave. Get real-time search results with privacy-focused indexing.',
55
+ serverUrl: 'npx -y @modelcontextprotocol/server-brave-search',
56
+ protocol: 'mcp',
57
+ category: 'Search',
58
+ icon: Globe,
59
+ authFields: [{ key: 'BRAVE_API_KEY', label: 'API Key', placeholder: 'BSAxxxxxxxxxxxxxxxxxxxx', helpUrl: 'https://brave.com/search/api/' }],
60
+ capabilities: ['web_search', 'local_search', 'news_search'],
61
+ },
62
+ {
63
+ name: 'Notion',
64
+ description: 'Search, read, and create pages in Notion. Manage databases, blocks, and workspace content.',
65
+ serverUrl: 'npx -y @modelcontextprotocol/server-notion',
66
+ protocol: 'mcp',
67
+ category: 'Productivity',
68
+ icon: FileText,
69
+ authFields: [{ key: 'NOTION_API_KEY', label: 'Integration Token', placeholder: 'ntn_xxxxxxxxxxxxxxxxxxxx', helpUrl: 'https://www.notion.so/my-integrations' }],
70
+ capabilities: ['search_pages', 'read_pages', 'create_pages', 'manage_databases'],
71
+ },
72
+ {
73
+ name: 'Sentry',
74
+ description: 'Monitor errors, performance issues, and releases. Query issues and analyze stack traces.',
75
+ serverUrl: 'npx -y @modelcontextprotocol/server-sentry',
76
+ protocol: 'mcp',
77
+ category: 'Development',
78
+ icon: Shield,
79
+ authFields: [{ key: 'SENTRY_AUTH_TOKEN', label: 'Auth Token', placeholder: 'sntrys_xxxxxxxxxxxxxxxxxxxx', helpUrl: 'https://sentry.io/settings/auth-tokens/' }],
80
+ capabilities: ['list_issues', 'get_issue_details', 'search_events'],
81
+ },
82
+ {
83
+ name: 'Filesystem',
84
+ description: 'Read, write, and manage files on the local filesystem. Useful for agents that need file access.',
85
+ serverUrl: 'npx -y @modelcontextprotocol/server-filesystem /path/to/allowed/dir',
86
+ protocol: 'mcp',
87
+ category: 'System',
88
+ icon: FileText,
89
+ authFields: [],
90
+ capabilities: ['read_files', 'write_files', 'list_directories', 'search_files'],
54
91
  },
55
92
  ];
56
93
 
57
- // --- Reusable UI Components (assuming these are in a components/ui folder) ---
58
- const Button = ({ children, className, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
59
- <button className={`px-4 py-2 rounded-md font-semibold transition-colors ${className}`} {...props}>
60
- {children}
61
- </button>
62
- );
63
-
64
- const Input = (props: React.InputHTMLAttributes<HTMLInputElement>) => (
65
- <input {...props} className={`w-full px-3 py-2 bg-background border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary ${props.className}`} />
66
- );
67
-
68
- const Select = ({ children, ...props }: React.SelectHTMLAttributes<HTMLSelectElement>) => (
69
- <select {...props} className={`w-full px-3 py-2 bg-background border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary ${props.className}`}>
70
- {children}
71
- </select>
72
- );
73
-
74
- const Card = ({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
75
- <div className={`bg-card border border-border rounded-lg shadow-sm ${className}`} {...props}>
76
- {children}
77
- </div>
78
- );
94
+ const CATEGORIES = ['All', 'Development', 'Communication', 'Productivity', 'Database', 'Search', 'System'];
79
95
 
80
- const Dialog = ({ open, onClose, children }: { open: boolean; onClose: () => void; children: React.ReactNode }) => {
81
- if (!open) return null;
82
- return (
83
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
84
- <div className="bg-card border border-border rounded-lg shadow-xl w-full max-w-md m-4">
85
- {children}
86
- </div>
96
+ function ConnectionsPage() {
97
+ const connections = useQuery(api.mcpConnections.list, {}) ?? [];
98
+ const createConnection = useMutation(api.mcpConnections.create);
99
+ const removeConnection = useMutation(api.mcpConnections.remove);
100
+ const toggleConnection = useMutation(api.mcpConnections.toggleEnabled);
101
+
102
+ const [searchQuery, setSearchQuery] = useState('');
103
+ const [categoryFilter, setCategoryFilter] = useState('All');
104
+ const [tab, setTab] = useState<'catalog' | 'connected'>('catalog');
105
+ const [connectingItem, setConnectingItem] = useState<typeof MCP_CATALOG[0] | null>(null);
106
+ const [authValues, setAuthValues] = useState<Record<string, string>>({});
107
+
108
+ const connectedNames = new Set(connections.map((c: any) => c.name));
109
+
110
+ const filteredCatalog = useMemo(() => {
111
+ let result = MCP_CATALOG;
112
+ if (categoryFilter !== 'All') result = result.filter(s => s.category === categoryFilter);
113
+ if (searchQuery) {
114
+ const q = searchQuery.toLowerCase();
115
+ result = result.filter(s => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
116
+ }
117
+ return result;
118
+ }, [searchQuery, categoryFilter]);
119
+
120
+ const handleConnect = async () => {
121
+ if (!connectingItem) return;
122
+ await createConnection({
123
+ name: connectingItem.name,
124
+ serverUrl: connectingItem.serverUrl,
125
+ protocol: connectingItem.protocol,
126
+ credentials: connectingItem.authFields.length > 0 ? authValues : undefined,
127
+ capabilities: connectingItem.capabilities,
128
+ });
129
+ setConnectingItem(null);
130
+ setAuthValues({});
131
+ setTab('connected');
132
+ };
133
+
134
+ const handleDisconnect = async (id: any) => {
135
+ if (confirm('Disconnect this integration?')) {
136
+ await removeConnection({ id });
137
+ }
138
+ };
139
+
140
+ return (
141
+ <DashboardLayout>
142
+ <div className="space-y-6">
143
+ <div>
144
+ <h1 className="text-3xl font-bold">Integrations</h1>
145
+ <p className="text-muted-foreground">Connect MCP servers and external services to extend your agents.</p>
87
146
  </div>
88
- );
89
- };
90
-
91
- const DialogHeader = ({ children }: { children: React.ReactNode }) => <div className="p-4 border-b border-border">{children}</div>;
92
- const DialogTitle = ({ children }: { children: React.ReactNode }) => <h2 className="text-lg font-semibold text-foreground">{children}</h2>;
93
- const DialogContent = ({ children }: { children: React.ReactNode }) => <div className="p-4 space-y-4">{children}</div>;
94
- const DialogFooter = ({ children }: { children: React.ReactNode }) => <div className="p-4 border-t border-border flex justify-end space-x-2">{children}</div>;
95
-
96
- const Switch = ({ checked, onCheckedChange }: { checked: boolean; onCheckedChange: (checked: boolean) => void }) => (
97
- <button
98
- type="button"
99
- role="switch"
100
- aria-checked={checked}
101
- onClick={() => onCheckedChange(!checked)}
102
- className={`${checked ? 'bg-primary' : 'bg-muted'} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-background`}
103
- >
104
- <span className={`${checked ? 'translate-x-5' : 'translate-x-0'} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`} />
105
- </button>
106
- );
107
147
 
108
- // --- Page Specific Components ---
148
+ <div className="flex gap-1 bg-muted p-1 rounded-lg w-fit">
149
+ <button onClick={() => setTab('catalog')} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${tab === 'catalog' ? 'bg-card shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>
150
+ <Plug className="w-4 h-4 inline mr-2" />Catalog ({MCP_CATALOG.length})
151
+ </button>
152
+ <button onClick={() => setTab('connected')} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${tab === 'connected' ? 'bg-card shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>
153
+ <Check className="w-4 h-4 inline mr-2" />Connected ({connections.length})
154
+ </button>
155
+ </div>
109
156
 
110
- function ConnectionCard({ connection, onTest, onEdit, onDelete, onToggle }: {
111
- connection: Connection;
112
- onTest: (id: string) => void;
113
- onEdit: (connection: Connection) => void;
114
- onDelete: (id: string) => void;
115
- onToggle: (id: string, enabled: boolean) => void;
116
- }) {
117
- const StatusIndicator = () => {
118
- switch (connection.status) {
119
- case 'connected': return <CheckCircle className="h-5 w-5 text-green-500" />;
120
- case 'disconnected': return <XCircle className="h-5 w-5 text-red-500" />;
121
- case 'testing': return <RefreshCw className="h-5 w-5 text-yellow-500 animate-spin" />;
122
- }
123
- };
157
+ <div className="flex flex-col sm:flex-row gap-3">
158
+ <div className="relative flex-1 max-w-sm">
159
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
160
+ <input type="text" placeholder="Search integrations..." 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" />
161
+ </div>
162
+ <div className="flex gap-2 flex-wrap">
163
+ {CATEGORIES.map(cat => (
164
+ <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>
165
+ ))}
166
+ </div>
167
+ </div>
124
168
 
125
- return (
126
- <Card className="flex flex-col justify-between">
127
- <div className="p-4">
128
- <div className="flex justify-between items-start">
129
- <div className="flex items-center space-x-3">
130
- <Plug className="h-8 w-8 text-primary" />
131
- <div>
132
- <h3 className="font-bold text-foreground">{connection.name}</h3>
133
- <p className="text-sm text-muted-foreground">{connection.type}</p>
134
- </div>
135
- </div>
136
- <Switch checked={connection.enabled} onCheckedChange={(checked) => onToggle(connection.id, checked)} />
137
- </div>
138
- <div className="mt-4 space-y-2 text-sm">
139
- <div className="flex items-center space-x-2">
140
- <StatusIndicator />
141
- <span className="capitalize text-muted-foreground">{connection.status}</span>
142
- </div>
143
- <div className="flex items-center space-x-2 text-muted-foreground">
144
- <RefreshCw className="h-4 w-4" />
145
- <span>Last connected: {connection.lastConnected ? new Date(connection.lastConnected).toLocaleDateString() : 'Never'}</span>
169
+ {tab === 'catalog' ? (
170
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
171
+ {filteredCatalog.map(item => {
172
+ const isConnected = connectedNames.has(item.name);
173
+ const Icon = item.icon;
174
+ return (
175
+ <div key={item.name} className="bg-card border border-border rounded-lg p-5 shadow-sm hover:shadow-md transition-shadow">
176
+ <div className="flex items-start justify-between mb-3">
177
+ <div className="flex items-center gap-3">
178
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center"><Icon className="w-5 h-5 text-primary" /></div>
179
+ <div>
180
+ <h3 className="font-semibold text-foreground">{item.name}</h3>
181
+ <p className="text-xs text-muted-foreground">{item.category}</p>
182
+ </div>
146
183
  </div>
184
+ </div>
185
+ <p className="text-sm text-muted-foreground mb-3 line-clamp-2">{item.description}</p>
186
+ <div className="flex flex-wrap gap-1 mb-4">
187
+ {item.capabilities.slice(0, 3).map(cap => (
188
+ <span key={cap} className="text-xs bg-muted px-2 py-0.5 rounded">{cap.replace(/_/g, ' ')}</span>
189
+ ))}
190
+ {item.capabilities.length > 3 && <span className="text-xs text-muted-foreground">+{item.capabilities.length - 3} more</span>}
191
+ </div>
192
+ <div className="pt-3 border-t border-border">
193
+ {isConnected ? (
194
+ <span className="text-xs text-green-500 flex items-center gap-1"><Check className="w-3.5 h-3.5" /> Connected</span>
195
+ ) : (
196
+ <button onClick={() => { setConnectingItem(item); setAuthValues({}); }} className="w-full bg-primary text-primary-foreground px-3 py-2 rounded-lg text-sm hover:bg-primary/90 flex items-center justify-center gap-2">
197
+ <Plug className="w-4 h-4" /> Connect
198
+ </button>
199
+ )}
200
+ </div>
147
201
  </div>
202
+ );
203
+ })}
204
+ </div>
205
+ ) : (
206
+ connections.length === 0 ? (
207
+ <div className="text-center py-16 bg-card border border-border rounded-lg">
208
+ <Plug className="w-16 h-16 text-muted-foreground/30 mx-auto mb-4" />
209
+ <h3 className="text-lg font-semibold mb-2">No integrations connected</h3>
210
+ <p className="text-muted-foreground">Browse the catalog to connect your first integration.</p>
148
211
  </div>
149
- <div className="p-4 bg-background/50 border-t border-border flex items-center justify-end space-x-2">
150
- <Button onClick={() => onTest(connection.id)} className="bg-secondary text-secondary-foreground hover:bg-secondary/80 text-sm">Test</Button>
151
- <Button onClick={() => onEdit(connection)} className="bg-secondary text-secondary-foreground hover:bg-secondary/80 text-sm"><Edit className="h-4 w-4" /></Button>
152
- <Button onClick={() => onDelete(connection.id)} className="bg-destructive text-destructive-foreground hover:bg-destructive/80 text-sm"><Trash2 className="h-4 w-4" /></Button>
212
+ ) : (
213
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
214
+ {connections.map((conn: any) => (
215
+ <div key={conn._id} className="bg-card border border-border rounded-lg p-5 shadow-sm">
216
+ <div className="flex items-start justify-between mb-3">
217
+ <h3 className="font-semibold text-foreground">{conn.name}</h3>
218
+ <span className={`text-xs px-2 py-0.5 rounded-full ${conn.isEnabled ? 'bg-green-500/10 text-green-500' : 'bg-muted text-muted-foreground'}`}>{conn.isEnabled ? 'Active' : 'Disabled'}</span>
219
+ </div>
220
+ <p className="text-xs text-muted-foreground mb-1 font-mono truncate">{conn.serverUrl}</p>
221
+ <p className="text-xs text-muted-foreground mb-4">Protocol: {conn.protocol}</p>
222
+ <div className="flex items-center justify-between pt-3 border-t border-border">
223
+ <button onClick={() => toggleConnection({ id: conn._id })} className="text-xs text-muted-foreground hover:text-foreground">{conn.isEnabled ? 'Disable' : 'Enable'}</button>
224
+ <button onClick={() => handleDisconnect(conn._id)} className="p-1.5 rounded hover:bg-destructive/10"><Trash2 className="w-4 h-4 text-destructive" /></button>
225
+ </div>
226
+ </div>
227
+ ))}
153
228
  </div>
154
- </Card>
155
- );
156
- }
157
-
158
- function ConnectionFormModal({ open, onClose, onSave, connection: initialConnection }: {
159
- open: boolean;
160
- onClose: () => void;
161
- onSave: (connection: Omit<Connection, 'id' | 'status' | 'lastConnected'> & { id?: string }) => void;
162
- connection: Connection | null;
163
- }) {
164
- const [name, setName] = useState('');
165
- const [type, setType] = useState<ConnectionType>('MCP');
166
- const [serverUrl, setServerUrl] = useState('');
167
- const [credentials, setCredentials] = useState('');
168
- const [isTesting, setIsTesting] = useState(false);
169
- const [testStatus, setTestStatus] = useState<'success' | 'error' | null>(null);
170
-
171
- React.useEffect(() => {
172
- if (initialConnection) {
173
- setName(initialConnection.name);
174
- setType(initialConnection.type);
175
- setServerUrl(initialConnection.serverUrl);
176
- setCredentials('********'); // Don't expose credentials
177
- } else {
178
- setName('');
179
- setType('MCP');
180
- setServerUrl('');
181
- setCredentials('');
182
- }
183
- setTestStatus(null);
184
- }, [initialConnection, open]);
185
-
186
- const handleTest = async () => {
187
- setIsTesting(true);
188
- setTestStatus(null);
189
- // const testConnection = useMutation(api.mcpConnections.test);
190
- // In a real app, you'd call the mutation:
191
- // try {
192
- // await testConnection({ serverUrl, credentials });
193
- // setTestStatus('success');
194
- // } catch (error) {
195
- // setTestStatus('error');
196
- // }
197
- await new Promise(resolve => setTimeout(resolve, 1500)); // Simulate network delay
198
- if (serverUrl.includes('fail')) {
199
- setTestStatus('error');
200
- } else {
201
- setTestStatus('success');
202
- }
203
- setIsTesting(false);
204
- };
205
-
206
- const handleSubmit = (e: React.FormEvent) => {
207
- e.preventDefault();
208
- onSave({
209
- id: initialConnection?.id,
210
- name,
211
- type,
212
- serverUrl,
213
- protocol: type === 'MCP' ? 'mcp' : 'https',
214
- enabled: initialConnection?.enabled ?? true,
215
- // Credentials would be handled securely, not passed like this
216
- });
217
- onClose();
218
- };
219
-
220
- return (
221
- <Dialog open={open} onClose={onClose}>
222
- <form onSubmit={handleSubmit}>
223
- <DialogHeader>
224
- <DialogTitle>{initialConnection ? 'Edit Connection' : 'Add New Connection'}</DialogTitle>
225
- </DialogHeader>
226
- <DialogContent>
227
- <div className="space-y-2">
228
- <label htmlFor="name" className="text-sm font-medium text-muted-foreground">Name</label>
229
- <Input id="name" value={name} onChange={e => setName(e.target.value)} placeholder="My Awesome API" required />
230
- </div>
231
- <div className="space-y-2">
232
- <label htmlFor="type" className="text-sm font-medium text-muted-foreground">Type</label>
233
- <Select id="type" value={type} onChange={e => setType(e.target.value as ConnectionType)} required>
234
- <option value="MCP">MCP</option>
235
- <option value="API">API</option>
236
- <option value="Webhook">Webhook</option>
237
- </Select>
238
- </div>
239
- <div className="space-y-2">
240
- <label htmlFor="serverUrl" className="text-sm font-medium text-muted-foreground">Server URL</label>
241
- <Input id="serverUrl" value={serverUrl} onChange={e => setServerUrl(e.target.value)} placeholder="https://example.com/api" required />
242
- </div>
243
- <div className="space-y-2">
244
- <label htmlFor="credentials" className="text-sm font-medium text-muted-foreground">Credentials (e.g., API Key)</label>
245
- <Input id="credentials" type="password" value={credentials} onChange={e => setCredentials(e.target.value)} placeholder={initialConnection ? 'Enter new key to update' : 'Your secret key'} />
246
- </div>
247
- {testStatus && (
248
- <div className={`flex items-center space-x-2 text-sm p-2 rounded-md ${testStatus === 'success' ? 'bg-green-900/50 text-green-400' : 'bg-red-900/50 text-red-400'}`}>
249
- {testStatus === 'success' ? <CheckCircle className="h-4 w-4" /> : <XCircle className="h-4 w-4" />}
250
- <span>{testStatus === 'success' ? 'Connection successful!' : 'Connection failed.'}</span>
251
- </div>
252
- )}
253
- </DialogContent>
254
- <DialogFooter>
255
- <Button type="button" onClick={handleTest} className="bg-secondary text-secondary-foreground hover:bg-secondary/80" disabled={isTesting}>
256
- {isTesting ? <><RefreshCw className="h-4 w-4 mr-2 animate-spin" /> Testing...</> : 'Test Connection'}
257
- </Button>
258
- <Button type="button" onClick={onClose} className="bg-muted text-muted-foreground hover:bg-muted/80">Cancel</Button>
259
- <Button type="submit" className="bg-primary text-primary-foreground hover:bg-primary/80">{initialConnection ? 'Save Changes' : 'Add Connection'}</Button>
260
- </DialogFooter>
261
- </form>
262
- </Dialog>
263
- );
264
- }
265
-
266
- // --- Main Page Component ---
267
-
268
- export const Route = createFileRoute('/connections')({ component: ConnectionsPage });
269
-
270
- function ConnectionsPage() {
271
- // --- Convex Hooks (Commented Out) ---
272
- // const connections = useQuery(api.mcpConnections.list) ?? [];
273
- // const createConnection = useMutation(api.mcpConnections.create);
274
- // const updateConnection = useMutation(api.mcpConnections.update);
275
- // const deleteConnection = useMutation(api.mcpConnections.delete);
276
- // const testConnection = useMutation(api.mcpConnections.test);
277
- // const toggleConnection = useMutation(api.mcpConnections.toggle);
278
-
279
- // --- Local State Management ---
280
- const [connections, setConnections] = useState<Connection[]>(mockConnections);
281
- const [isLoading, setIsLoading] = useState(false); // For initial load
282
- const [error, setError] = useState<string | null>(null);
283
- const [searchTerm, setSearchTerm] = useState('');
284
- const [isModalOpen, setIsModalOpen] = useState(false);
285
- const [editingConnection, setEditingConnection] = useState<Connection | null>(null);
286
-
287
- const filteredConnections = useMemo(() =>
288
- connections.filter(c => c.name.toLowerCase().includes(searchTerm.toLowerCase())),
289
- [connections, searchTerm]
290
- );
291
-
292
- const handleAdd = () => {
293
- setEditingConnection(null);
294
- setIsModalOpen(true);
295
- };
296
-
297
- const handleEdit = (connection: Connection) => {
298
- setEditingConnection(connection);
299
- setIsModalOpen(true);
300
- };
301
-
302
- const handleDelete = (id: string) => {
303
- if (window.confirm('Are you sure you want to delete this connection?')) {
304
- // await deleteConnection({ id });
305
- setConnections(prev => prev.filter(c => c.id !== id));
306
- }
307
- };
308
-
309
- const handleSave = (data: Omit<Connection, 'id' | 'status' | 'lastConnected'> & { id?: string }) => {
310
- if (data.id) { // Update
311
- // await updateConnection({ id: data.id, ...data });
312
- setConnections(prev => prev.map(c => c.id === data.id ? { ...c, ...data } : c));
313
- } else { // Create
314
- const newId = (Math.random() * 100000).toString();
315
- // const newId = await createConnection(data);
316
- const newConnection: Connection = {
317
- ...data,
318
- id: newId,
319
- status: 'disconnected',
320
- lastConnected: null,
321
- };
322
- setConnections(prev => [newConnection, ...prev]);
323
- }
324
- };
325
-
326
- const handleTest = async (id: string) => {
327
- setConnections(prev => prev.map(c => c.id === id ? { ...c, status: 'testing' } : c));
328
- // const result = await testConnection({ id });
329
- await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate test
330
- const result = { success: Math.random() > 0.3 }; // Simulate success/fail
331
-
332
- setConnections(prev => prev.map(c => c.id === id ? {
333
- ...c,
334
- status: result.success ? 'connected' : 'disconnected',
335
- lastConnected: result.success ? new Date().toISOString() : c.lastConnected,
336
- } : c));
337
- };
338
-
339
- const handleToggle = (id: string, enabled: boolean) => {
340
- // await toggleConnection({ id, enabled });
341
- setConnections(prev => prev.map(c => c.id === id ? { ...c, enabled } : c));
342
- };
343
-
344
- return (
345
- <DashboardLayout>
346
- <div className="bg-background text-foreground p-4 sm:p-6 lg:p-8">
347
- <header className="flex flex-col sm:flex-row justify-between sm:items-center mb-6 gap-4">
348
- <div>
349
- <h1 className="text-2xl font-bold">Connections</h1>
350
- <p className="text-muted-foreground">Manage your MCP, API, and Webhook connections.</p>
351
- </div>
352
- <div className="flex items-center gap-2">
353
- <div className="relative w-full sm:w-auto">
354
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
355
- <Input
356
- type="text"
357
- placeholder="Search connections..."
358
- value={searchTerm}
359
- onChange={(e) => setSearchTerm(e.target.value)}
360
- className="pl-10 w-full sm:w-64"
361
- />
229
+ )
230
+ )}
231
+
232
+ {/* Connect Modal */}
233
+ {connectingItem && (
234
+ <div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
235
+ <div className="bg-card border border-border rounded-lg shadow-xl w-full max-w-lg">
236
+ <div className="flex justify-between items-center p-4 border-b border-border">
237
+ <h2 className="text-lg font-bold">Connect {connectingItem.name}</h2>
238
+ <button onClick={() => setConnectingItem(null)} className="text-muted-foreground hover:text-foreground"><X className="h-5 w-5" /></button>
239
+ </div>
240
+ <div className="p-6 space-y-4">
241
+ <p className="text-sm text-muted-foreground">{connectingItem.description}</p>
242
+ {connectingItem.authFields.length > 0 ? (
243
+ <div className="space-y-3">
244
+ <h3 className="text-sm font-semibold">Authentication</h3>
245
+ {connectingItem.authFields.map(field => (
246
+ <div key={field.key}>
247
+ <div className="flex items-center justify-between mb-1">
248
+ <label className="text-sm font-medium">{field.label}</label>
249
+ {field.helpUrl && <a href={field.helpUrl} target="_blank" rel="noopener noreferrer" className="text-xs text-primary hover:underline flex items-center gap-1">Get token <ExternalLink className="w-3 h-3" /></a>}
362
250
  </div>
363
- <Button onClick={handleAdd} className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2">
364
- <Plus className="h-4 w-4" />
365
- <span>Add Connection</span>
366
- </Button>
367
- </div>
368
- </header>
369
-
370
- {isLoading ? (
371
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
372
- {[...Array(3)].map((_, i) => <Card key={i} className="h-48 animate-pulse"></Card>)}
373
- </div>
374
- ) : error ? (
375
- <div className="flex flex-col items-center justify-center text-center h-64 bg-card rounded-lg">
376
- <XCircle className="h-12 w-12 text-destructive mb-4" />
377
- <h3 className="text-xl font-semibold">Failed to load connections</h3>
378
- <p className="text-muted-foreground">{error}</p>
379
- </div>
380
- ) : filteredConnections.length > 0 ? (
381
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
382
- {filteredConnections.map(conn => (
383
- <ConnectionCard
384
- key={conn.id}
385
- connection={conn}
386
- onTest={handleTest}
387
- onEdit={handleEdit}
388
- onDelete={handleDelete}
389
- onToggle={handleToggle}
390
- />
391
- ))}
392
- </div>
251
+ <input type="password" placeholder={field.placeholder} value={authValues[field.key] || ''} onChange={(e) => setAuthValues(prev => ({ ...prev, [field.key]: e.target.value }))} className="w-full bg-background border border-border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary font-mono" />
252
+ </div>
253
+ ))}
254
+ </div>
393
255
  ) : (
394
- <div className="flex flex-col items-center justify-center text-center h-64 bg-card rounded-lg border-2 border-dashed border-border">
395
- <Plug className="h-12 w-12 text-muted-foreground mb-4" />
396
- <h3 className="text-xl font-semibold">No Connections Found</h3>
397
- <p className="text-muted-foreground mb-4">Get started by adding your first connection.</p>
398
- <Button onClick={handleAdd} className="bg-primary text-primary-foreground hover:bg-primary/90">
399
- Add Connection
400
- </Button>
401
- </div>
256
+ <p className="text-sm text-green-500">No authentication required for this integration.</p>
402
257
  )}
258
+ <div className="bg-muted/50 rounded-lg p-3">
259
+ <p className="text-xs text-muted-foreground font-mono">{connectingItem.serverUrl}</p>
260
+ </div>
261
+ </div>
262
+ <div className="p-4 border-t border-border flex justify-end gap-2">
263
+ <button onClick={() => setConnectingItem(null)} className="px-4 py-2 rounded-lg bg-muted text-muted-foreground text-sm">Cancel</button>
264
+ <button onClick={handleConnect} disabled={connectingItem.authFields.length > 0 && connectingItem.authFields.some(f => !authValues[f.key])} className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2">
265
+ <Plug className="w-4 h-4" /> Connect
266
+ </button>
267
+ </div>
403
268
  </div>
404
-
405
- <ConnectionFormModal
406
- open={isModalOpen}
407
- onClose={() => setIsModalOpen(false)}
408
- onSave={handleSave}
409
- connection={editingConnection}
410
- />
411
- </DashboardLayout>
412
- );
269
+ </div>
270
+ )}
271
+ </div>
272
+ </DashboardLayout>
273
+ );
413
274
  }