@cybermem/dashboard 0.5.16 → 0.8.5
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/Dockerfile +2 -1
- package/app/api/audit-logs/route.ts +79 -56
- package/app/api/auth/token/route.ts +59 -0
- package/app/api/health/route.ts +49 -22
- package/app/api/metrics/route.ts +272 -86
- package/app/api/reset/route.ts +1 -1
- package/app/api/restore/route.ts +1 -1
- package/app/api/settings/route.ts +37 -26
- package/app/api/system/restart/route.ts +1 -1
- package/app/layout.tsx +25 -17
- package/app/page.tsx +135 -110
- package/components/dashboard/audit-log-table.tsx +3 -3
- package/components/dashboard/mcp-config-modal.tsx +356 -247
- package/components/dashboard/metric-card.tsx +4 -7
- package/components/dashboard/settings-modal.tsx +64 -38
- package/e2e/crud-happy-path.spec.ts +243 -173
- package/lib/auth.ts +50 -0
- package/lib/data/dashboard-context.tsx +117 -70
- package/lib/data/production-strategy.ts +124 -85
- package/middleware.ts +53 -31
- package/next.config.mjs +12 -20
- package/package.json +4 -1
- package/playwright.config.ts +50 -15
- package/public/clients.json +7 -23
|
@@ -1,86 +1,103 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { Button } from "@/components/ui/button"
|
|
4
|
-
import { Input } from "@/components/ui/input"
|
|
5
|
-
import { Label } from "@/components/ui/label"
|
|
6
|
-
import { useDashboard } from "@/lib/data/dashboard-context"
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import { Label } from "@/components/ui/label";
|
|
6
|
+
import { useDashboard } from "@/lib/data/dashboard-context";
|
|
7
|
+
import {
|
|
8
|
+
Check,
|
|
9
|
+
Copy,
|
|
10
|
+
Eye,
|
|
11
|
+
EyeOff,
|
|
12
|
+
FileCode,
|
|
13
|
+
Info,
|
|
14
|
+
Monitor,
|
|
15
|
+
X,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
import Image from "next/image";
|
|
18
|
+
import { useEffect, useState } from "react";
|
|
10
19
|
|
|
11
20
|
export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
12
|
-
const { clientConfigs } = useDashboard()
|
|
13
|
-
const clients = clientConfigs
|
|
14
|
-
const [selectedClient, setSelectedClient] = useState("claude")
|
|
15
|
-
const [copiedId, setCopiedId] = useState<string | null>(null)
|
|
16
|
-
const [apiKey, setApiKey] = useState("")
|
|
17
|
-
const [baseUrl, setBaseUrl] = useState("http://localhost:8080")
|
|
18
|
-
const [isLoading, setIsLoading] = useState(true)
|
|
19
|
-
const [isKeyVisible, setIsKeyVisible] = useState(false)
|
|
20
|
-
const [showRegenConfirm, setShowRegenConfirm] = useState(false)
|
|
21
|
-
const [regenInputValue, setRegenInputValue] = useState("")
|
|
22
|
-
const [isManaged, setIsManaged] = useState(false) // true = local mode, no API key needed
|
|
21
|
+
const { clientConfigs } = useDashboard();
|
|
22
|
+
const clients = clientConfigs;
|
|
23
|
+
const [selectedClient, setSelectedClient] = useState("claude");
|
|
24
|
+
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
25
|
+
const [apiKey, setApiKey] = useState("");
|
|
26
|
+
const [baseUrl, setBaseUrl] = useState("http://localhost:8080");
|
|
27
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
28
|
+
const [isKeyVisible, setIsKeyVisible] = useState(false);
|
|
29
|
+
const [showRegenConfirm, setShowRegenConfirm] = useState(false);
|
|
30
|
+
const [regenInputValue, setRegenInputValue] = useState("");
|
|
31
|
+
const [isManaged, setIsManaged] = useState(false); // true = local mode, no API key needed
|
|
23
32
|
|
|
24
33
|
useEffect(() => {
|
|
25
34
|
// Try to get key from local storage first (simulating persistence)
|
|
26
|
-
const localKey = localStorage.getItem("om_api_key")
|
|
35
|
+
const localKey = localStorage.getItem("om_api_key");
|
|
27
36
|
if (localKey) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.then(res => res.json())
|
|
32
|
-
.then(data => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
setApiKey(localKey);
|
|
38
|
+
// We still fetch settings for the endpoint
|
|
39
|
+
fetch("/api/settings")
|
|
40
|
+
.then((res) => res.json())
|
|
41
|
+
.then((data) => {
|
|
42
|
+
let srvEndpoint = data.endpoint;
|
|
43
|
+
if (
|
|
44
|
+
srvEndpoint.includes("localhost") &&
|
|
45
|
+
typeof window !== "undefined" &&
|
|
46
|
+
!window.location.hostname.includes("localhost")
|
|
47
|
+
) {
|
|
48
|
+
const port = srvEndpoint.split(":").pop()?.split("/")[0] || "8626";
|
|
49
|
+
srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}`;
|
|
50
|
+
}
|
|
51
|
+
setBaseUrl(srvEndpoint);
|
|
52
|
+
setIsLoading(false);
|
|
40
53
|
})
|
|
41
|
-
.catch(err => setIsLoading(false))
|
|
54
|
+
.catch((err) => setIsLoading(false));
|
|
42
55
|
} else {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
fetch("/api/settings")
|
|
57
|
+
.then((res) => res.json())
|
|
58
|
+
.then((data) => {
|
|
59
|
+
setApiKey(data.apiKey !== "not-set" ? data.apiKey : "");
|
|
60
|
+
setIsManaged(data.isManaged || false);
|
|
61
|
+
let srvEndpoint = data.endpoint;
|
|
62
|
+
if (
|
|
63
|
+
srvEndpoint.includes("localhost") &&
|
|
64
|
+
typeof window !== "undefined" &&
|
|
65
|
+
!window.location.hostname.includes("localhost")
|
|
66
|
+
) {
|
|
67
|
+
const port = srvEndpoint.split(":").pop()?.split("/")[0] || "8626";
|
|
68
|
+
srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}`;
|
|
69
|
+
}
|
|
70
|
+
setBaseUrl(srvEndpoint);
|
|
71
|
+
setIsLoading(false);
|
|
72
|
+
})
|
|
73
|
+
.catch((err) => {
|
|
74
|
+
console.error("Failed to fetch settings:", err);
|
|
75
|
+
setIsLoading(false);
|
|
76
|
+
});
|
|
60
77
|
}
|
|
61
|
-
}, [])
|
|
78
|
+
}, []);
|
|
62
79
|
|
|
63
80
|
const generateApiKey = () => {
|
|
64
81
|
// Legacy - redirected to confirmRegenerate logic via UI state
|
|
65
|
-
}
|
|
82
|
+
};
|
|
66
83
|
|
|
67
84
|
const confirmRegenerate = async () => {
|
|
68
85
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
const res = await fetch("/api/settings/regenerate", { method: "POST" });
|
|
87
|
+
if (!res.ok) throw new Error("Failed to regenerate key");
|
|
88
|
+
const data = await res.json();
|
|
89
|
+
|
|
90
|
+
const newKey = data.apiKey;
|
|
91
|
+
setApiKey(newKey);
|
|
92
|
+
localStorage.setItem("om_api_key", newKey);
|
|
93
|
+
setIsKeyVisible(true);
|
|
94
|
+
setShowRegenConfirm(false);
|
|
95
|
+
setRegenInputValue("");
|
|
79
96
|
} catch (e) {
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
console.error(e);
|
|
98
|
+
alert("Failed to regenerate key on server.");
|
|
82
99
|
}
|
|
83
|
-
}
|
|
100
|
+
};
|
|
84
101
|
|
|
85
102
|
const getMcpConfig = (clientId: string) => {
|
|
86
103
|
// Local mode: use stdio (command-based) - no server needed, runs via npx
|
|
@@ -89,53 +106,59 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
89
106
|
mcpServers: {
|
|
90
107
|
cybermem: {
|
|
91
108
|
command: "npx",
|
|
92
|
-
args: ["@cybermem/mcp"]
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
109
|
+
args: ["@cybermem/mcp"],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
96
113
|
}
|
|
97
114
|
|
|
98
|
-
// Remote mode: use stdio with --url and --
|
|
115
|
+
// Remote mode: use stdio with --url and --token args (universal for all clients)
|
|
99
116
|
return {
|
|
100
117
|
mcpServers: {
|
|
101
118
|
cybermem: {
|
|
102
119
|
command: "npx",
|
|
103
120
|
args: [
|
|
104
|
-
"-y",
|
|
105
|
-
"
|
|
106
|
-
"--
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
"-y",
|
|
122
|
+
"@cybermem/mcp",
|
|
123
|
+
"--url",
|
|
124
|
+
baseUrl,
|
|
125
|
+
"--token",
|
|
126
|
+
apiKey || "sk-your-generated-token",
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
};
|
|
112
132
|
|
|
113
133
|
const getConfigContent = (maskKey = false) => {
|
|
114
|
-
const config = (clients as any[]).find(c => c.id === selectedClient);
|
|
115
|
-
const displayKey = maskKey
|
|
116
|
-
|
|
134
|
+
const config = (clients as any[]).find((c) => c.id === selectedClient);
|
|
135
|
+
const displayKey = maskKey
|
|
136
|
+
? "••••••••••••••••"
|
|
137
|
+
: apiKey || "sk-your-generated-token";
|
|
138
|
+
const actualKey = apiKey || "sk-your-generated-token";
|
|
117
139
|
|
|
118
140
|
// Handle TOML config (Codex)
|
|
119
|
-
if (config?.configType ===
|
|
141
|
+
if (config?.configType === "toml") {
|
|
120
142
|
if (isManaged) {
|
|
121
143
|
return `# CyberMem Configuration (Local Mode)\n[mcp]\ncommand = "npx"\nargs = ["@cybermem/mcp"]`;
|
|
122
144
|
}
|
|
123
|
-
return `# CyberMem Configuration (Remote Mode)\n[mcp]\ncommand = "npx"\nargs = ["@cybermem/mcp", "--url", "${baseUrl}", "--
|
|
145
|
+
return `# CyberMem Configuration (Remote Mode)\n[mcp]\ncommand = "npx"\nargs = ["@cybermem/mcp", "--url", "${baseUrl}", "--token", "${maskKey ? displayKey : actualKey}"]`;
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
// Handle command-based configs (Claude Code, Gemini CLI, etc.)
|
|
127
|
-
if (
|
|
149
|
+
if (config?.configType === "command" || config?.configType === "cmd") {
|
|
128
150
|
// Select command based on mode
|
|
129
151
|
let cmd = isManaged ? config?.localCommand : config?.remoteCommand;
|
|
130
152
|
|
|
131
153
|
// Fallback to legacy 'command' field if new fields not present
|
|
132
154
|
if (!cmd) {
|
|
133
|
-
cmd = config?.command?.replace("http://localhost:8080", baseUrl) ||
|
|
155
|
+
cmd = config?.command?.replace("http://localhost:8080", baseUrl) || "";
|
|
134
156
|
}
|
|
135
157
|
|
|
136
158
|
// Substitute placeholders with actual values
|
|
137
|
-
cmd = cmd.replace(
|
|
138
|
-
cmd = cmd.replace(
|
|
159
|
+
cmd = cmd.replace("{{ENDPOINT}}", baseUrl);
|
|
160
|
+
cmd = cmd.replace("{{API_KEY}}", maskKey ? displayKey : actualKey);
|
|
161
|
+
cmd = cmd.replace("{{TOKEN}}", maskKey ? displayKey : actualKey);
|
|
139
162
|
|
|
140
163
|
return cmd;
|
|
141
164
|
}
|
|
@@ -143,45 +166,50 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
143
166
|
// Default to JSON config
|
|
144
167
|
const jsonConfig = getMcpConfig(selectedClient);
|
|
145
168
|
if (!isManaged && maskKey) {
|
|
146
|
-
// Mask the
|
|
169
|
+
// Mask the token in args array
|
|
147
170
|
const args = (jsonConfig.mcpServers.cybermem as any).args;
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
150
|
-
args[
|
|
171
|
+
const tokenIdx = args.indexOf("--token");
|
|
172
|
+
if (tokenIdx !== -1 && args[tokenIdx + 1]) {
|
|
173
|
+
args[tokenIdx + 1] = displayKey;
|
|
151
174
|
}
|
|
152
175
|
}
|
|
153
176
|
return JSON.stringify(jsonConfig, null, 2);
|
|
154
|
-
}
|
|
177
|
+
};
|
|
155
178
|
|
|
156
179
|
const copyToClipboard = (text: string, id: string) => {
|
|
157
|
-
navigator.clipboard.writeText(text)
|
|
158
|
-
setCopiedId(id)
|
|
159
|
-
setTimeout(() => setCopiedId(null), 2000)
|
|
160
|
-
}
|
|
180
|
+
navigator.clipboard.writeText(text);
|
|
181
|
+
setCopiedId(id);
|
|
182
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
183
|
+
};
|
|
161
184
|
|
|
162
185
|
const highlightJSON = (obj: any) => {
|
|
163
|
-
const json = JSON.stringify(obj, null, 2)
|
|
164
|
-
return json.replace(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
186
|
+
const json = JSON.stringify(obj, null, 2);
|
|
187
|
+
return json.replace(
|
|
188
|
+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
189
|
+
(match) => {
|
|
190
|
+
let cls = "text-orange-300";
|
|
191
|
+
if (/^"/.test(match)) {
|
|
192
|
+
if (/:$/.test(match)) {
|
|
193
|
+
cls = "text-emerald-300";
|
|
194
|
+
} else {
|
|
195
|
+
cls = "text-yellow-200";
|
|
196
|
+
}
|
|
197
|
+
} else if (/true|false/.test(match)) {
|
|
198
|
+
cls = "text-blue-300";
|
|
199
|
+
} else if (/null/.test(match)) {
|
|
200
|
+
cls = "text-gray-400";
|
|
171
201
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
return `<span class="${cls}">${match}</span>`
|
|
178
|
-
})
|
|
179
|
-
}
|
|
202
|
+
return `<span class="${cls}">${match}</span>`;
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
};
|
|
180
206
|
|
|
181
|
-
const selectedConfig = (clients as any[]).find(
|
|
207
|
+
const selectedConfig = (clients as any[]).find(
|
|
208
|
+
(c) => c.id === selectedClient,
|
|
209
|
+
);
|
|
182
210
|
|
|
183
211
|
const renderInstructions = () => {
|
|
184
|
-
if (!selectedConfig) return null
|
|
212
|
+
if (!selectedConfig) return null;
|
|
185
213
|
|
|
186
214
|
return (
|
|
187
215
|
<div className="space-y-4 text-sm text-neutral-300">
|
|
@@ -189,19 +217,28 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
189
217
|
{selectedConfig.steps.length > 0 && (
|
|
190
218
|
<ol className="list-decimal list-inside space-y-2 ml-2 text-neutral-400">
|
|
191
219
|
{selectedConfig.steps.map((step: string, i: number) => (
|
|
192
|
-
<li
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
220
|
+
<li
|
|
221
|
+
key={i}
|
|
222
|
+
dangerouslySetInnerHTML={{
|
|
223
|
+
__html: step
|
|
224
|
+
.replace(
|
|
225
|
+
/\*\*(.*?)\*\*/g,
|
|
226
|
+
'<span class="text-white font-medium">$1</span>',
|
|
227
|
+
)
|
|
228
|
+
.replace(
|
|
229
|
+
/`([^`]+)`/g,
|
|
230
|
+
'<code class="text-emerald-400 bg-emerald-500/10 px-1 py-0.5 rounded">$1</code>',
|
|
231
|
+
),
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
197
234
|
))}
|
|
198
235
|
</ol>
|
|
199
236
|
)}
|
|
200
237
|
</div>
|
|
201
|
-
)
|
|
202
|
-
}
|
|
238
|
+
);
|
|
239
|
+
};
|
|
203
240
|
|
|
204
|
-
const configContent = getConfigContent()
|
|
241
|
+
const configContent = getConfigContent();
|
|
205
242
|
|
|
206
243
|
return (
|
|
207
244
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
|
@@ -213,16 +250,24 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
213
250
|
radial-gradient(circle at 100% 0%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%),
|
|
214
251
|
radial-gradient(circle at 100% 100%, oklch(0.65 0 0 / 0.05) 0%, transparent 50%),
|
|
215
252
|
radial-gradient(circle at 0% 100%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%)
|
|
216
|
-
|
|
253
|
+
`,
|
|
217
254
|
}}
|
|
218
255
|
>
|
|
219
256
|
{/* Header */}
|
|
220
257
|
<div className="flex items-center justify-between px-6 pt-6 pb-2 flex-none">
|
|
221
258
|
<div className="flex items-center gap-3">
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
259
|
+
<div className="p-2 bg-white/5 rounded-lg border border-white/10 shadow-inner">
|
|
260
|
+
<Image
|
|
261
|
+
src="/icons/mcp.png"
|
|
262
|
+
alt="MCP Logo"
|
|
263
|
+
width={20}
|
|
264
|
+
height={20}
|
|
265
|
+
className="drop-shadow-[0_0_5px_rgba(255,255,255,0.3)]"
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
<h2 className="text-xl font-semibold text-white text-shadow-sm">
|
|
269
|
+
Integrate MCP Client
|
|
270
|
+
</h2>
|
|
226
271
|
</div>
|
|
227
272
|
<Button
|
|
228
273
|
variant="ghost"
|
|
@@ -236,38 +281,46 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
236
281
|
|
|
237
282
|
{/* Content */}
|
|
238
283
|
<div className="p-6 space-y-6 overflow-y-auto flex-1 min-h-0">
|
|
239
|
-
|
|
240
284
|
{/* Client Selector */}
|
|
241
285
|
<section>
|
|
242
286
|
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
|
|
243
|
-
|
|
244
|
-
|
|
287
|
+
<Monitor className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
|
|
288
|
+
Select Client
|
|
245
289
|
</h3>
|
|
246
290
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
|
247
291
|
{clients.map((client) => (
|
|
248
292
|
<button
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
293
|
+
key={client.id}
|
|
294
|
+
onClick={() => setSelectedClient(client.id)}
|
|
295
|
+
className={`
|
|
252
296
|
relative group flex flex-col items-center justify-center p-3 rounded-xl transition-all duration-300 border
|
|
253
|
-
${
|
|
254
|
-
|
|
255
|
-
|
|
297
|
+
${
|
|
298
|
+
selectedClient === client.id
|
|
299
|
+
? "bg-emerald-500/10 border-emerald-500/50 shadow-[0_0_15px_rgba(16,185,129,0.1)] backdrop-blur-sm"
|
|
300
|
+
: "bg-white/5 border-white/5 hover:bg-white/10 hover:border-white/20 backdrop-blur-sm"
|
|
256
301
|
}
|
|
257
302
|
`}
|
|
303
|
+
>
|
|
304
|
+
<div className="mb-2 transition-transform duration-300 group-hover:scale-110">
|
|
305
|
+
{client.icon ? (
|
|
306
|
+
<Image
|
|
307
|
+
src={client.icon}
|
|
308
|
+
alt={client.name}
|
|
309
|
+
width={32}
|
|
310
|
+
height={32}
|
|
311
|
+
className="object-contain drop-shadow-lg"
|
|
312
|
+
/>
|
|
313
|
+
) : (
|
|
314
|
+
<div className="w-8 h-8 flex items-center justify-center text-white/50 bg-white/5 rounded-full border border-white/10 transition-transform duration-300">
|
|
315
|
+
<span className="text-sm font-bold">?</span>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
<span
|
|
320
|
+
className={`text-[10px] font-medium text-center transition-colors ${selectedClient === client.id ? "text-emerald-400 text-shadow-emerald" : "text-neutral-400 group-hover:text-white"}`}
|
|
258
321
|
>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<Image src={client.icon} alt={client.name} width={32} height={32} className="object-contain drop-shadow-lg" />
|
|
262
|
-
) : (
|
|
263
|
-
<div className="w-8 h-8 flex items-center justify-center text-white/50 bg-white/5 rounded-full border border-white/10 transition-transform duration-300">
|
|
264
|
-
<span className="text-sm font-bold">?</span>
|
|
265
|
-
</div>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
<span className={`text-[10px] font-medium text-center transition-colors ${selectedClient === client.id ? "text-emerald-400 text-shadow-emerald" : "text-neutral-400 group-hover:text-white"}`}>
|
|
269
|
-
{client.name}
|
|
270
|
-
</span>
|
|
322
|
+
{client.name}
|
|
323
|
+
</span>
|
|
271
324
|
</button>
|
|
272
325
|
))}
|
|
273
326
|
</div>
|
|
@@ -275,25 +328,41 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
275
328
|
|
|
276
329
|
{/* Instructions */}
|
|
277
330
|
<section>
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
331
|
+
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
|
|
332
|
+
<FileCode className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
|
|
333
|
+
{selectedClient === "codex"
|
|
334
|
+
? "Configuration"
|
|
335
|
+
: selectedClient === "other"
|
|
336
|
+
? "Configuration JSON"
|
|
337
|
+
: "Integration Instructions"}
|
|
338
|
+
</h3>
|
|
282
339
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
340
|
+
<div className="bg-white/5 border border-white/10 rounded-lg p-5 space-y-4 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm">
|
|
341
|
+
{selectedClient === "chatgpt" && (
|
|
342
|
+
<div className="px-3 py-2 rounded-lg bg-emerald-500/10 border border-emerald-500/20 text-xs text-emerald-200">
|
|
343
|
+
<p>
|
|
344
|
+
Requires Developer Mode.{" "}
|
|
345
|
+
<a
|
|
346
|
+
href="https://platform.openai.com/docs/guides/developer-mode"
|
|
347
|
+
target="_blank"
|
|
348
|
+
rel="noopener noreferrer"
|
|
349
|
+
className="underline hover:text-white"
|
|
350
|
+
>
|
|
351
|
+
Read OpenAI Documentation
|
|
352
|
+
</a>
|
|
353
|
+
</p>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
289
356
|
|
|
290
|
-
|
|
357
|
+
{renderInstructions()}
|
|
291
358
|
|
|
292
|
-
|
|
293
|
-
|
|
359
|
+
{/* API Key Control Row - Only show in remote mode */}
|
|
360
|
+
{!isManaged ? (
|
|
294
361
|
<div className="bg-white/5 border border-white/10 rounded-lg p-5 space-y-4 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm mb-4">
|
|
295
362
|
<div className="space-y-2">
|
|
296
|
-
<Label htmlFor="mcp-api-key" className="text-neutral-200">
|
|
363
|
+
<Label htmlFor="mcp-api-key" className="text-neutral-200">
|
|
364
|
+
Security Token
|
|
365
|
+
</Label>
|
|
297
366
|
<div className="flex gap-2">
|
|
298
367
|
<div className="relative flex-1">
|
|
299
368
|
<Input
|
|
@@ -303,116 +372,154 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
303
372
|
className="bg-black/40 border-white/10 text-white focus-visible:border-emerald-500/30 focus-visible:ring-emerald-500/10 placeholder:text-neutral-600 shadow-inner pr-10 font-mono"
|
|
304
373
|
type={isKeyVisible ? "text" : "password"}
|
|
305
374
|
/>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
375
|
+
<button
|
|
376
|
+
type="button"
|
|
377
|
+
onClick={() => setIsKeyVisible(!isKeyVisible)}
|
|
378
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white transition-colors"
|
|
379
|
+
>
|
|
380
|
+
{isKeyVisible ? (
|
|
381
|
+
<EyeOff className="w-4 h-4" />
|
|
382
|
+
) : (
|
|
383
|
+
<Eye className="w-4 h-4" />
|
|
384
|
+
)}
|
|
312
385
|
</button>
|
|
313
386
|
</div>
|
|
314
387
|
<Button
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
388
|
+
size="icon"
|
|
389
|
+
variant="ghost"
|
|
390
|
+
className="h-10 w-10 border border-white/10 bg-white/5 hover:bg-white/10 text-neutral-400 hover:text-white"
|
|
391
|
+
onClick={() => copyToClipboard(apiKey, "apikey")}
|
|
392
|
+
title="Copy API Key"
|
|
393
|
+
>
|
|
394
|
+
{copiedId === "apikey" ? (
|
|
395
|
+
<Check className="h-4 w-4 text-emerald-400" />
|
|
396
|
+
) : (
|
|
397
|
+
<Copy className="h-4 w-4" />
|
|
398
|
+
)}
|
|
322
399
|
</Button>
|
|
323
400
|
</div>
|
|
324
401
|
|
|
325
402
|
{/* Regeneration Controls */}
|
|
326
403
|
<div className="flex justify-end pt-2">
|
|
327
404
|
{showRegenConfirm ? (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
405
|
+
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-right-4 duration-200">
|
|
406
|
+
<span className="text-xs text-red-400 font-medium">
|
|
407
|
+
Warning: Disconnects clients.
|
|
408
|
+
</span>
|
|
409
|
+
<Input
|
|
410
|
+
value={regenInputValue}
|
|
411
|
+
onChange={(e) => setRegenInputValue(e.target.value)}
|
|
412
|
+
placeholder="Type 'agree'"
|
|
413
|
+
className="h-8 w-28 bg-red-500/10 border-red-500/30 text-red-200 text-xs placeholder:text-red-500/30 focus-visible:border-red-500/50"
|
|
414
|
+
/>
|
|
415
|
+
<Button
|
|
416
|
+
size="sm"
|
|
417
|
+
variant="ghost"
|
|
418
|
+
className="h-8 px-3 text-neutral-400 hover:text-white hover:bg-white/10"
|
|
419
|
+
onClick={() => {
|
|
420
|
+
setShowRegenConfirm(false);
|
|
421
|
+
setRegenInputValue("");
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
Cancel
|
|
425
|
+
</Button>
|
|
426
|
+
<Button
|
|
427
|
+
size="sm"
|
|
428
|
+
disabled={regenInputValue !== "agree"}
|
|
429
|
+
className="h-8 px-3 bg-red-500/20 text-red-400 hover:bg-red-500/30 border border-red-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
430
|
+
onClick={confirmRegenerate}
|
|
431
|
+
>
|
|
432
|
+
Confirm
|
|
433
|
+
</Button>
|
|
434
|
+
</div>
|
|
356
435
|
) : (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
436
|
+
<Button
|
|
437
|
+
size="sm"
|
|
438
|
+
variant="ghost"
|
|
439
|
+
className="h-8 px-2 text-neutral-400 hover:text-white hover:bg-white/10"
|
|
440
|
+
onClick={() => setShowRegenConfirm(true)}
|
|
441
|
+
>
|
|
442
|
+
Regenerate Key
|
|
443
|
+
</Button>
|
|
365
444
|
)}
|
|
366
445
|
</div>
|
|
367
446
|
</div>
|
|
368
447
|
</div>
|
|
369
|
-
|
|
448
|
+
) : (
|
|
370
449
|
<div className="bg-emerald-500/5 border border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
371
450
|
<div className="flex items-start gap-3">
|
|
372
451
|
<Info className="h-4 w-4 shrink-0 text-emerald-400 mt-0.5" />
|
|
373
452
|
<div className="space-y-1">
|
|
374
|
-
<p className="text-sm font-medium text-emerald-200">
|
|
375
|
-
|
|
453
|
+
<p className="text-sm font-medium text-emerald-200">
|
|
454
|
+
Local Mode Active
|
|
455
|
+
</p>
|
|
456
|
+
<p className="text-xs text-emerald-200/60">
|
|
457
|
+
No token required for connection from your laptop. Just
|
|
458
|
+
copy the config below.
|
|
459
|
+
</p>
|
|
376
460
|
</div>
|
|
377
461
|
</div>
|
|
378
462
|
</div>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
463
|
+
)}
|
|
464
|
+
|
|
465
|
+
<div className="relative group">
|
|
466
|
+
<div className="relative pl-5 py-5 pr-24 rounded-lg bg-[#0F161C] border border-white/10 font-mono text-xs md:text-sm text-white overflow-x-auto shadow-[0_0_20px_rgba(0,0,0,0.3)] inset-shadow">
|
|
467
|
+
<pre className="text-shadow-sm">
|
|
468
|
+
{(() => {
|
|
469
|
+
const config = (clients as any[]).find(
|
|
470
|
+
(c) => c.id === selectedClient,
|
|
471
|
+
);
|
|
472
|
+
if (config?.configType === "json") {
|
|
473
|
+
return (
|
|
474
|
+
<code
|
|
475
|
+
dangerouslySetInnerHTML={{
|
|
476
|
+
__html: highlightJSON(
|
|
477
|
+
JSON.parse(getConfigContent(!isKeyVisible)),
|
|
478
|
+
),
|
|
479
|
+
}}
|
|
480
|
+
/>
|
|
481
|
+
);
|
|
482
|
+
} else {
|
|
483
|
+
return getConfigContent(!isKeyVisible);
|
|
484
|
+
}
|
|
485
|
+
})()}
|
|
486
|
+
</pre>
|
|
487
|
+
</div>
|
|
488
|
+
<div className="absolute top-5 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
489
|
+
<Button
|
|
490
|
+
size="sm"
|
|
491
|
+
variant="ghost"
|
|
492
|
+
className="h-8 px-3 text-white bg-black/40 backdrop-blur border border-white/5 shadow-[0_0_10px_rgba(255,255,255,0.05)] hover:bg-white/10 hover:text-white font-medium"
|
|
493
|
+
onClick={() =>
|
|
494
|
+
copyToClipboard(getConfigContent(false), "config")
|
|
495
|
+
}
|
|
496
|
+
>
|
|
497
|
+
{copiedId === "config" ? (
|
|
498
|
+
<Check className="h-4 w-4 stroke-[2.5] text-emerald-400 mr-2 drop-shadow-[0_0_5px_rgba(52,211,153,0.5)]" />
|
|
499
|
+
) : (
|
|
500
|
+
<Copy className="h-4 w-4 stroke-[2.5] mr-2 text-white drop-shadow-[0_0_5px_rgba(255,255,255,0.3)]" />
|
|
501
|
+
)}
|
|
502
|
+
{copiedId === "config" ? (
|
|
503
|
+
<span className="text-emerald-400 text-shadow-sm">
|
|
504
|
+
Copied
|
|
505
|
+
</span>
|
|
506
|
+
) : (
|
|
507
|
+
<span className="text-white text-shadow-sm">Copy</span>
|
|
508
|
+
)}
|
|
509
|
+
</Button>
|
|
405
510
|
</div>
|
|
511
|
+
</div>
|
|
406
512
|
|
|
407
|
-
|
|
513
|
+
{!isManaged && (
|
|
408
514
|
<div className="flex items-start gap-3 p-3 rounded-lg bg-emerald-500/5 border border-emerald-500/10 text-emerald-200/70 text-xs mt-4">
|
|
409
515
|
<Info className="h-4 w-4 shrink-0 text-white mt-0.5" />
|
|
410
516
|
<p>
|
|
411
|
-
This configuration includes your generated
|
|
517
|
+
This configuration includes your generated Security Token.
|
|
518
|
+
Keep it secure and do not share it publicly.
|
|
412
519
|
</p>
|
|
413
520
|
</div>
|
|
414
|
-
|
|
415
|
-
|
|
521
|
+
)}
|
|
522
|
+
</div>
|
|
416
523
|
</section>
|
|
417
524
|
</div>
|
|
418
525
|
|
|
@@ -423,7 +530,9 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
423
530
|
variant="ghost"
|
|
424
531
|
className="bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 text-emerald-400 hover:text-emerald-300 mr-auto"
|
|
425
532
|
>
|
|
426
|
-
<a href="https://docs.cybermem.dev" target="_blank">
|
|
533
|
+
<a href="https://docs.cybermem.dev" target="_blank">
|
|
534
|
+
Read Documentation
|
|
535
|
+
</a>
|
|
427
536
|
</Button>
|
|
428
537
|
<Button
|
|
429
538
|
onClick={onClose}
|
|
@@ -434,5 +543,5 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
434
543
|
</div>
|
|
435
544
|
</div>
|
|
436
545
|
</div>
|
|
437
|
-
)
|
|
546
|
+
);
|
|
438
547
|
}
|