@cybermem/dashboard 0.8.5 → 0.8.9
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 +1 -1
- package/app/api/environment/route.ts +73 -0
- package/app/api/settings/route.ts +53 -13
- package/app/layout.tsx +2 -0
- package/app/page.tsx +2 -37
- package/components/dashboard/login-modal.tsx +108 -36
- package/components/dashboard/mcp-config-modal.tsx +17 -18
- package/components/dashboard/settings-modal.tsx +142 -239
- package/components/ui/confirmation-modal.tsx +116 -0
- package/components/ui/tint-button.tsx +91 -0
- package/middleware.ts +1 -1
- package/next.config.mjs +7 -2
- package/package.json +1 -1
- package/components/dashboard/password-alert-modal.tsx +0 -72
package/Dockerfile
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Environment detection API for dynamic MCP URL configuration
|
|
5
|
+
*
|
|
6
|
+
* Priority:
|
|
7
|
+
* 1. Tailscale hostname (if TAILSCALE_HOSTNAME env var is set)
|
|
8
|
+
* 2. LAN .local domain (if raspberrypi.local or similar)
|
|
9
|
+
* 3. VPS public URL (if CYBERMEM_PUBLIC_URL env var is set)
|
|
10
|
+
* 4. localhost:8626 (fallback)
|
|
11
|
+
*/
|
|
12
|
+
export async function GET(request: Request) {
|
|
13
|
+
const MCP_PORT = process.env.MCP_PORT || "8626";
|
|
14
|
+
|
|
15
|
+
// Check for Tailscale hostname first (highest priority for remote access)
|
|
16
|
+
const tailscaleHostname = process.env.TAILSCALE_HOSTNAME;
|
|
17
|
+
if (tailscaleHostname) {
|
|
18
|
+
return NextResponse.json({
|
|
19
|
+
url: `https://${tailscaleHostname}/cybermem`,
|
|
20
|
+
type: "tailscale",
|
|
21
|
+
editable: false,
|
|
22
|
+
hint: "Using Tailscale Funnel for secure remote access",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for VPS public URL
|
|
27
|
+
const publicUrl = process.env.CYBERMEM_PUBLIC_URL;
|
|
28
|
+
if (publicUrl) {
|
|
29
|
+
const formattedUrl = publicUrl.endsWith("/")
|
|
30
|
+
? publicUrl.slice(0, -1)
|
|
31
|
+
: publicUrl;
|
|
32
|
+
return NextResponse.json({
|
|
33
|
+
url: `${formattedUrl}/cybermem`,
|
|
34
|
+
type: "vps",
|
|
35
|
+
editable: true,
|
|
36
|
+
hint: "Configure CYBERMEM_PUBLIC_URL to change this URL",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Detect from request headers (for LAN access)
|
|
41
|
+
const host = request.headers.get("host") || "";
|
|
42
|
+
|
|
43
|
+
// Check if accessing via .local domain (Raspberry Pi LAN)
|
|
44
|
+
if (host.includes(".local")) {
|
|
45
|
+
const hostname = host.split(":")[0];
|
|
46
|
+
return NextResponse.json({
|
|
47
|
+
url: `http://${hostname}:${MCP_PORT}/cybermem`,
|
|
48
|
+
type: "lan",
|
|
49
|
+
editable: false,
|
|
50
|
+
hint: "Detected LAN access via mDNS (.local)",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if accessing via IP address (likely LAN or VPS)
|
|
55
|
+
const ipMatch = host.match(/^(\d+\.\d+\.\d+\.\d+)/);
|
|
56
|
+
if (ipMatch) {
|
|
57
|
+
const protocol = request.headers.get("x-forwarded-proto") || "http";
|
|
58
|
+
return NextResponse.json({
|
|
59
|
+
url: `${protocol}://${ipMatch[1]}:${MCP_PORT}/cybermem`,
|
|
60
|
+
type: "ip",
|
|
61
|
+
editable: true,
|
|
62
|
+
hint: "Detected IP-based access",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Default to localhost
|
|
67
|
+
return NextResponse.json({
|
|
68
|
+
url: `http://localhost:${MCP_PORT}/cybermem`,
|
|
69
|
+
type: "local",
|
|
70
|
+
editable: false,
|
|
71
|
+
hint: "Running locally",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -6,6 +6,51 @@ export const dynamic = "force-dynamic";
|
|
|
6
6
|
|
|
7
7
|
const CONFIG_PATH = "/data/config.json";
|
|
8
8
|
|
|
9
|
+
// Detect the correct MCP endpoint based on request host
|
|
10
|
+
function getMcpEndpoint(request: NextRequest): {
|
|
11
|
+
endpoint: string;
|
|
12
|
+
isLocal: boolean;
|
|
13
|
+
} {
|
|
14
|
+
const host = request.headers.get("host") || "localhost:3000";
|
|
15
|
+
const hostname = host.split(":")[0];
|
|
16
|
+
|
|
17
|
+
// Priority 1: Explicit env var
|
|
18
|
+
if (process.env.CYBERMEM_URL) {
|
|
19
|
+
return { endpoint: process.env.CYBERMEM_URL, isLocal: false };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Priority 2: Tailscale domain (from env or detect)
|
|
23
|
+
const tailscaleDomain = process.env.TAILSCALE_DOMAIN;
|
|
24
|
+
|
|
25
|
+
// Priority 3: Based on request host
|
|
26
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
27
|
+
// Local development
|
|
28
|
+
return { endpoint: "http://localhost:8626/mcp", isLocal: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (hostname.endsWith(".local")) {
|
|
32
|
+
// LAN access (raspberrypi.local)
|
|
33
|
+
// If Tailscale domain is available, prefer it for remote config
|
|
34
|
+
if (tailscaleDomain) {
|
|
35
|
+
return {
|
|
36
|
+
endpoint: `https://${tailscaleDomain}/cybermem/mcp`,
|
|
37
|
+
isLocal: false,
|
|
38
|
+
// Also provide LAN endpoint as fallback
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { endpoint: `http://${hostname}:8626/mcp`, isLocal: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (hostname.includes(".ts.net")) {
|
|
45
|
+
// Tailscale Funnel access
|
|
46
|
+
return { endpoint: `https://${hostname}/cybermem/mcp`, isLocal: false };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fallback: use request host
|
|
50
|
+
const protocol = request.headers.get("x-forwarded-proto") || "http";
|
|
51
|
+
return { endpoint: `${protocol}://${hostname}:8626/mcp`, isLocal: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
9
54
|
export async function GET(request: NextRequest) {
|
|
10
55
|
// Rate limiting check
|
|
11
56
|
const rateLimit = checkRateLimit(request);
|
|
@@ -32,17 +77,11 @@ export async function GET(request: NextRequest) {
|
|
|
32
77
|
// ignore
|
|
33
78
|
}
|
|
34
79
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
// isManaged = Local Mode (No Auth). Only if NO URL and NO API KEY.
|
|
41
|
-
// If API Key is present (RPi), we are in "Secure/Legacy" mode, not Local.
|
|
42
|
-
// In local development, rawEndpoint might be unset, but we still want to not be "managed" if we want to test auth flows.
|
|
43
|
-
const isManaged =
|
|
44
|
-
!rawEndpoint &&
|
|
45
|
-
(!process.env.OM_API_KEY || process.env.OM_API_KEY === "dev-secret-key");
|
|
80
|
+
// Get dynamic endpoint based on request host
|
|
81
|
+
const { endpoint, isLocal } = getMcpEndpoint(request);
|
|
82
|
+
|
|
83
|
+
// isManaged = Local Mode (localhost auto-login)
|
|
84
|
+
const isManaged = isLocal;
|
|
46
85
|
|
|
47
86
|
return NextResponse.json(
|
|
48
87
|
{
|
|
@@ -50,8 +89,9 @@ export async function GET(request: NextRequest) {
|
|
|
50
89
|
apiKey: apiKey,
|
|
51
90
|
endpoint,
|
|
52
91
|
isManaged,
|
|
53
|
-
|
|
54
|
-
|
|
92
|
+
isLocal,
|
|
93
|
+
dashboardVersion: "v0.8.0",
|
|
94
|
+
mcpVersion: "v0.8.0",
|
|
55
95
|
},
|
|
56
96
|
{
|
|
57
97
|
headers: {
|
package/app/layout.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Analytics } from "@vercel/analytics/next";
|
|
|
3
3
|
import type { Metadata } from "next";
|
|
4
4
|
import { Exo_2, Geist, Geist_Mono } from "next/font/google";
|
|
5
5
|
import type React from "react";
|
|
6
|
+
import { Toaster } from "sonner";
|
|
6
7
|
import "./globals.css";
|
|
7
8
|
|
|
8
9
|
const _geist = Geist({ subsets: ["latin"] });
|
|
@@ -38,6 +39,7 @@ export default async function RootLayout({
|
|
|
38
39
|
<DashboardProvider initialAuth={initialAuth}>
|
|
39
40
|
{children}
|
|
40
41
|
</DashboardProvider>
|
|
42
|
+
<Toaster richColors theme="dark" position="bottom-right" />
|
|
41
43
|
<Analytics />
|
|
42
44
|
</body>
|
|
43
45
|
</html>
|
package/app/page.tsx
CHANGED
|
@@ -6,7 +6,6 @@ import DashboardHeader from "@/components/dashboard/header";
|
|
|
6
6
|
import LoginModal from "@/components/dashboard/login-modal";
|
|
7
7
|
import MCPConfigModal from "@/components/dashboard/mcp-config-modal";
|
|
8
8
|
import MetricsGrid from "@/components/dashboard/metrics-grid";
|
|
9
|
-
import PasswordAlertModal from "@/components/dashboard/password-alert-modal";
|
|
10
9
|
import SettingsModal from "@/components/dashboard/settings-modal";
|
|
11
10
|
import { useDashboard } from "@/lib/data/dashboard-context";
|
|
12
11
|
import { DashboardData } from "@/lib/data/types";
|
|
@@ -27,8 +26,6 @@ export default function Dashboard() {
|
|
|
27
26
|
|
|
28
27
|
const [showMCPConfig, setShowMCPConfig] = useState(false);
|
|
29
28
|
const [showSettings, setShowSettings] = useState(false);
|
|
30
|
-
const [showPasswordAlert, setShowPasswordAlert] = useState(false);
|
|
31
|
-
const [focusPasswordField, setFocusPasswordField] = useState(false);
|
|
32
29
|
|
|
33
30
|
// Data State
|
|
34
31
|
const [data, setData] = useState<DashboardData>({
|
|
@@ -53,27 +50,8 @@ export default function Dashboard() {
|
|
|
53
50
|
|
|
54
51
|
// Auth is handled by context (Layout passes initial state from headers)
|
|
55
52
|
|
|
56
|
-
const handleLogin = (
|
|
53
|
+
const handleLogin = (token: string) => {
|
|
57
54
|
login();
|
|
58
|
-
|
|
59
|
-
// Check if using default password and warning not dismissed
|
|
60
|
-
const hasCustomPassword = localStorage.getItem("adminPassword") !== null;
|
|
61
|
-
const warningDismissed =
|
|
62
|
-
localStorage.getItem("hidePasswordWarning") === "true";
|
|
63
|
-
|
|
64
|
-
if (!hasCustomPassword && !warningDismissed) {
|
|
65
|
-
setShowPasswordAlert(true);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const handlePasswordAlertChange = () => {
|
|
70
|
-
setShowPasswordAlert(false);
|
|
71
|
-
setFocusPasswordField(true);
|
|
72
|
-
setShowSettings(true);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const handlePasswordAlertDismiss = () => {
|
|
76
|
-
setShowPasswordAlert(false);
|
|
77
55
|
};
|
|
78
56
|
|
|
79
57
|
// Fetch Data Effect - Reacts to strategy change or refresh signal
|
|
@@ -183,20 +161,7 @@ export default function Dashboard() {
|
|
|
183
161
|
{showMCPConfig && (
|
|
184
162
|
<MCPConfigModal onClose={() => setShowMCPConfig(false)} />
|
|
185
163
|
)}
|
|
186
|
-
{showSettings && (
|
|
187
|
-
<SettingsModal
|
|
188
|
-
onClose={() => {
|
|
189
|
-
setShowSettings(false);
|
|
190
|
-
setFocusPasswordField(false);
|
|
191
|
-
}}
|
|
192
|
-
/>
|
|
193
|
-
)}
|
|
194
|
-
{showPasswordAlert && (
|
|
195
|
-
<PasswordAlertModal
|
|
196
|
-
onChangePassword={handlePasswordAlertChange}
|
|
197
|
-
onDismiss={handlePasswordAlertDismiss}
|
|
198
|
-
/>
|
|
199
|
-
)}
|
|
164
|
+
{showSettings && <SettingsModal onClose={() => setShowSettings(false)} />}
|
|
200
165
|
</div>
|
|
201
166
|
);
|
|
202
167
|
}
|
|
@@ -1,32 +1,62 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
3
|
+
import { Input } from "@/components/ui/input";
|
|
4
|
+
import { Label } from "@/components/ui/label";
|
|
5
|
+
import { TintButton } from "@/components/ui/tint-button";
|
|
6
|
+
import {
|
|
7
|
+
Tooltip,
|
|
8
|
+
TooltipContent,
|
|
9
|
+
TooltipTrigger,
|
|
10
|
+
} from "@/components/ui/tooltip";
|
|
11
|
+
import { HelpCircle, Key, LogIn } from "lucide-react";
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
8
13
|
|
|
9
14
|
interface LoginModalProps {
|
|
10
|
-
onLogin: (
|
|
15
|
+
onLogin: (token: string) => void;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export default function LoginModal({ onLogin }: LoginModalProps) {
|
|
14
|
-
const [
|
|
15
|
-
const [error, setError] = useState("")
|
|
19
|
+
const [token, setToken] = useState("");
|
|
20
|
+
const [error, setError] = useState("");
|
|
21
|
+
const [isLocal, setIsLocal] = useState(true);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
// Auto-login on localhost
|
|
25
|
+
const hostname = window.location.hostname;
|
|
26
|
+
const local =
|
|
27
|
+
hostname === "localhost" ||
|
|
28
|
+
hostname === "127.0.0.1" ||
|
|
29
|
+
hostname.includes("raspberrypi.local") ||
|
|
30
|
+
hostname.endsWith(".local");
|
|
31
|
+
setIsLocal(local);
|
|
32
|
+
|
|
33
|
+
if (local) {
|
|
34
|
+
// Auto-login for local access
|
|
35
|
+
onLogin("local-bypass");
|
|
36
|
+
}
|
|
37
|
+
}, [onLogin]);
|
|
16
38
|
|
|
17
39
|
const handleSubmit = (e: React.FormEvent) => {
|
|
18
|
-
e.preventDefault()
|
|
40
|
+
e.preventDefault();
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
if (!token.trim()) {
|
|
43
|
+
setError("Please enter your API token");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
22
46
|
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} else {
|
|
27
|
-
setError("Incorrect password")
|
|
28
|
-
setPassword("")
|
|
47
|
+
if (!token.startsWith("sk-")) {
|
|
48
|
+
setError("Token should start with 'sk-'");
|
|
49
|
+
return;
|
|
29
50
|
}
|
|
51
|
+
|
|
52
|
+
// For remote access, use the token
|
|
53
|
+
onLogin(token);
|
|
54
|
+
setError("");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Don't show modal for local access (auto-login)
|
|
58
|
+
if (isLocal) {
|
|
59
|
+
return null;
|
|
30
60
|
}
|
|
31
61
|
|
|
32
62
|
return (
|
|
@@ -39,45 +69,87 @@ export default function LoginModal({ onLogin }: LoginModalProps) {
|
|
|
39
69
|
radial-gradient(circle at 100% 0%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%),
|
|
40
70
|
radial-gradient(circle at 100% 100%, oklch(0.65 0 0 / 0.05) 0%, transparent 50%),
|
|
41
71
|
radial-gradient(circle at 0% 100%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%)
|
|
42
|
-
|
|
72
|
+
`,
|
|
43
73
|
}}
|
|
44
74
|
>
|
|
45
75
|
{/* Header */}
|
|
46
76
|
<div className="px-6 pt-8 pb-6 text-center">
|
|
47
77
|
<div className="inline-flex p-3 bg-emerald-500/10 rounded-full border border-emerald-500/20 mb-4">
|
|
48
|
-
<
|
|
78
|
+
<Key className="w-8 h-8 text-emerald-400 drop-shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
|
|
49
79
|
</div>
|
|
50
|
-
<h2 className="text-2xl font-bold text-white text-shadow-sm mb-2">
|
|
51
|
-
|
|
80
|
+
<h2 className="text-2xl font-bold text-white text-shadow-sm mb-2">
|
|
81
|
+
CyberMem Dashboard
|
|
82
|
+
</h2>
|
|
83
|
+
<p className="text-neutral-400 text-sm">
|
|
84
|
+
Remote access requires API token authentication
|
|
85
|
+
</p>
|
|
52
86
|
</div>
|
|
53
87
|
|
|
54
88
|
{/* Content */}
|
|
55
89
|
<form onSubmit={handleSubmit} className="px-6 pb-6 space-y-4">
|
|
56
90
|
<div className="space-y-2">
|
|
57
|
-
<
|
|
91
|
+
<div className="flex items-center gap-2">
|
|
92
|
+
<Label htmlFor="token" className="text-neutral-200">
|
|
93
|
+
API Token
|
|
94
|
+
</Label>
|
|
95
|
+
<Tooltip>
|
|
96
|
+
<TooltipTrigger asChild>
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
className="text-neutral-500 hover:text-emerald-400 transition-colors"
|
|
100
|
+
>
|
|
101
|
+
<HelpCircle className="w-4 h-4" />
|
|
102
|
+
</button>
|
|
103
|
+
</TooltipTrigger>
|
|
104
|
+
<TooltipContent className="max-w-xs bg-neutral-900 border-neutral-700 text-neutral-200">
|
|
105
|
+
<p className="text-xs">
|
|
106
|
+
Find your token in{" "}
|
|
107
|
+
<code className="text-emerald-400">~/.cybermem/.env</code>{" "}
|
|
108
|
+
or check the terminal output from{" "}
|
|
109
|
+
<code className="text-emerald-400">cybermem up</code>
|
|
110
|
+
</p>
|
|
111
|
+
</TooltipContent>
|
|
112
|
+
</Tooltip>
|
|
113
|
+
</div>
|
|
58
114
|
<Input
|
|
59
|
-
id="
|
|
115
|
+
id="token"
|
|
60
116
|
type="password"
|
|
61
|
-
value={
|
|
62
|
-
onChange={(e) =>
|
|
63
|
-
placeholder="
|
|
64
|
-
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"
|
|
117
|
+
value={token}
|
|
118
|
+
onChange={(e) => setToken(e.target.value)}
|
|
119
|
+
placeholder="sk-xxxxxxxxxxxxxxxx"
|
|
120
|
+
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 font-mono"
|
|
65
121
|
autoFocus
|
|
66
122
|
/>
|
|
67
|
-
{error &&
|
|
68
|
-
<p className="text-red-400 text-sm mt-2">{error}</p>
|
|
69
|
-
)}
|
|
123
|
+
{error && <p className="text-red-400 text-sm mt-2">{error}</p>}
|
|
70
124
|
</div>
|
|
71
125
|
|
|
72
|
-
<
|
|
126
|
+
<TintButton
|
|
73
127
|
type="submit"
|
|
74
|
-
|
|
128
|
+
tint="emerald"
|
|
129
|
+
variant="solid"
|
|
130
|
+
className="w-full h-11"
|
|
75
131
|
>
|
|
76
132
|
<LogIn className="w-4 h-4" />
|
|
77
|
-
Login
|
|
78
|
-
</
|
|
133
|
+
Login with Token
|
|
134
|
+
</TintButton>
|
|
135
|
+
|
|
136
|
+
<div className="text-xs text-neutral-500 text-center pt-2 space-y-1">
|
|
137
|
+
<p>
|
|
138
|
+
<strong className="text-neutral-400">
|
|
139
|
+
Where to find your token:
|
|
140
|
+
</strong>
|
|
141
|
+
</p>
|
|
142
|
+
<p>
|
|
143
|
+
1. Open Dashboard from{" "}
|
|
144
|
+
<code className="text-emerald-400/80">localhost:3000</code> or{" "}
|
|
145
|
+
<code className="text-emerald-400/80">
|
|
146
|
+
raspberrypi.local:3000
|
|
147
|
+
</code>
|
|
148
|
+
</p>
|
|
149
|
+
<p>2. Go to Settings → Access Token → Copy</p>
|
|
150
|
+
</div>
|
|
79
151
|
</form>
|
|
80
152
|
</div>
|
|
81
153
|
</div>
|
|
82
|
-
)
|
|
154
|
+
);
|
|
83
155
|
}
|
|
@@ -5,17 +5,18 @@ import { Input } from "@/components/ui/input";
|
|
|
5
5
|
import { Label } from "@/components/ui/label";
|
|
6
6
|
import { useDashboard } from "@/lib/data/dashboard-context";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
Check,
|
|
9
|
+
Copy,
|
|
10
|
+
Eye,
|
|
11
|
+
EyeOff,
|
|
12
|
+
FileCode,
|
|
13
|
+
Info,
|
|
14
|
+
Monitor,
|
|
15
|
+
X,
|
|
16
16
|
} from "lucide-react";
|
|
17
17
|
import Image from "next/image";
|
|
18
18
|
import { useEffect, useState } from "react";
|
|
19
|
+
import { toast } from "sonner";
|
|
19
20
|
|
|
20
21
|
export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
21
22
|
const { clientConfigs } = useDashboard();
|
|
@@ -93,9 +94,14 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
93
94
|
setIsKeyVisible(true);
|
|
94
95
|
setShowRegenConfirm(false);
|
|
95
96
|
setRegenInputValue("");
|
|
97
|
+
toast.success("Token Regenerated!", {
|
|
98
|
+
description: "All existing client connections will need to be updated.",
|
|
99
|
+
});
|
|
96
100
|
} catch (e) {
|
|
97
101
|
console.error(e);
|
|
98
|
-
|
|
102
|
+
toast.error("Failed to regenerate token", {
|
|
103
|
+
description: "Please check if the server is running.",
|
|
104
|
+
});
|
|
99
105
|
}
|
|
100
106
|
};
|
|
101
107
|
|
|
@@ -112,7 +118,7 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
112
118
|
};
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
// Remote mode: use stdio with --url and --token
|
|
121
|
+
// Remote mode: use stdio with --url and --token
|
|
116
122
|
return {
|
|
117
123
|
mcpServers: {
|
|
118
124
|
cybermem: {
|
|
@@ -165,14 +171,6 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
165
171
|
|
|
166
172
|
// Default to JSON config
|
|
167
173
|
const jsonConfig = getMcpConfig(selectedClient);
|
|
168
|
-
if (!isManaged && maskKey) {
|
|
169
|
-
// Mask the token in args array
|
|
170
|
-
const args = (jsonConfig.mcpServers.cybermem as any).args;
|
|
171
|
-
const tokenIdx = args.indexOf("--token");
|
|
172
|
-
if (tokenIdx !== -1 && args[tokenIdx + 1]) {
|
|
173
|
-
args[tokenIdx + 1] = displayKey;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
174
|
return JSON.stringify(jsonConfig, null, 2);
|
|
177
175
|
};
|
|
178
176
|
|
|
@@ -180,6 +178,7 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
180
178
|
navigator.clipboard.writeText(text);
|
|
181
179
|
setCopiedId(id);
|
|
182
180
|
setTimeout(() => setCopiedId(null), 2000);
|
|
181
|
+
toast.success("Copied to clipboard!");
|
|
183
182
|
};
|
|
184
183
|
|
|
185
184
|
const highlightJSON = (obj: any) => {
|