@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.
- package/dist/default/convex/agents.ts +204 -0
- package/dist/default/convex/apiKeys.ts +133 -0
- package/dist/default/convex/cronJobs.ts +224 -0
- package/dist/default/convex/files.ts +103 -0
- package/dist/default/convex/folders.ts +110 -0
- package/dist/default/convex/heartbeat.ts +371 -0
- package/dist/default/convex/logs.ts +66 -0
- package/dist/default/convex/mastraIntegration.ts +185 -0
- package/dist/default/convex/mcpConnections.ts +127 -0
- package/dist/default/convex/messages.ts +90 -0
- package/dist/default/convex/projects.ts +114 -0
- package/dist/default/convex/schema.ts +150 -83
- package/dist/default/convex/sessions.ts +174 -0
- package/dist/default/convex/settings.ts +79 -0
- package/dist/default/convex/skills.ts +178 -0
- package/dist/default/convex/threads.ts +100 -0
- package/dist/default/convex/usage.ts +195 -0
- package/dist/default/convex/vault.ts +397 -0
- package/dist/default/dashboard/app/main.tsx +7 -3
- package/dist/default/dashboard/app/routes/agents.tsx +103 -161
- package/dist/default/dashboard/app/routes/chat.tsx +163 -317
- package/dist/default/dashboard/app/routes/connections.tsx +247 -386
- package/dist/default/dashboard/app/routes/cron.tsx +127 -286
- package/dist/default/dashboard/app/routes/files.tsx +184 -167
- package/dist/default/dashboard/app/routes/index.tsx +63 -96
- package/dist/default/dashboard/app/routes/projects.tsx +106 -225
- package/dist/default/dashboard/app/routes/sessions.tsx +87 -253
- package/dist/default/dashboard/app/routes/settings.tsx +316 -532
- package/dist/default/dashboard/app/routes/skills.tsx +329 -216
- package/dist/default/dashboard/app/routes/usage.tsx +107 -150
- package/dist/default/dashboard/tsconfig.json +3 -2
- package/dist/default/dashboard/vite.config.ts +6 -0
- package/dist/index.js +256 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/convex/agents.ts +204 -0
- package/templates/default/convex/apiKeys.ts +133 -0
- package/templates/default/convex/cronJobs.ts +224 -0
- package/templates/default/convex/files.ts +103 -0
- package/templates/default/convex/folders.ts +110 -0
- package/templates/default/convex/heartbeat.ts +371 -0
- package/templates/default/convex/logs.ts +66 -0
- package/templates/default/convex/mastraIntegration.ts +185 -0
- package/templates/default/convex/mcpConnections.ts +127 -0
- package/templates/default/convex/messages.ts +90 -0
- package/templates/default/convex/projects.ts +114 -0
- package/templates/default/convex/schema.ts +150 -83
- package/templates/default/convex/sessions.ts +174 -0
- package/templates/default/convex/settings.ts +79 -0
- package/templates/default/convex/skills.ts +178 -0
- package/templates/default/convex/threads.ts +100 -0
- package/templates/default/convex/usage.ts +195 -0
- package/templates/default/convex/vault.ts +397 -0
- package/templates/default/dashboard/app/main.tsx +7 -3
- package/templates/default/dashboard/app/routes/agents.tsx +103 -161
- package/templates/default/dashboard/app/routes/chat.tsx +163 -317
- package/templates/default/dashboard/app/routes/connections.tsx +247 -386
- package/templates/default/dashboard/app/routes/cron.tsx +127 -286
- package/templates/default/dashboard/app/routes/files.tsx +184 -167
- package/templates/default/dashboard/app/routes/index.tsx +63 -96
- package/templates/default/dashboard/app/routes/projects.tsx +106 -225
- package/templates/default/dashboard/app/routes/sessions.tsx +87 -253
- package/templates/default/dashboard/app/routes/settings.tsx +316 -532
- package/templates/default/dashboard/app/routes/skills.tsx +329 -216
- package/templates/default/dashboard/app/routes/usage.tsx +107 -150
- package/templates/default/dashboard/tsconfig.json +3 -2
- 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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { Plug, Plus,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
<
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
connection={editingConnection}
|
|
410
|
-
/>
|
|
411
|
-
</DashboardLayout>
|
|
412
|
-
);
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
</DashboardLayout>
|
|
273
|
+
);
|
|
413
274
|
}
|