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