@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 CHANGED
@@ -3,7 +3,7 @@ FROM node:20-alpine AS base
3
3
  WORKDIR /app
4
4
 
5
5
  # Use corepack to get the pnpm version from the lockfile and speed up installs via cache
6
- RUN corepack enable
6
+ RUN corepack enable && apk add --no-cache python3 make g++
7
7
 
8
8
  # Copy package files
9
9
  COPY package*.json ./
@@ -28,6 +28,7 @@ RUN --mount=type=cache,target=/root/.pnpm-store \
28
28
 
29
29
  # Production stage
30
30
  FROM node:20-alpine AS production
31
+ RUN apk add --no-cache libc6-compat
31
32
  WORKDIR /app
32
33
 
33
34
  ENV NODE_ENV=production
@@ -1,88 +1,111 @@
1
- import fs from 'fs'
2
- import { NextResponse } from 'next/server'
3
- import path from 'path'
1
+ import fs from "fs";
2
+ import { NextResponse } from "next/server";
3
+ import path from "path";
4
4
 
5
- export const dynamic = 'force-dynamic'
5
+ export const dynamic = "force-dynamic";
6
6
 
7
7
  // Use env var for db-exporter URL (Docker internal vs local dev)
8
- const DB_EXPORTER_URL = process.env.DB_EXPORTER_URL || 'http://localhost:8000'
8
+ const DB_EXPORTER_URL = process.env.DB_EXPORTER_URL || "http://localhost:8000";
9
9
 
10
10
  // Load clients config for name normalization
11
- let clientsConfig: any[] = []
11
+ let clientsConfig: any[] = [];
12
12
  try {
13
- const configPath = path.join(process.cwd(), 'public', 'clients.json')
14
- clientsConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
13
+ const configPath = path.join(process.cwd(), "public", "clients.json");
14
+ clientsConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
15
15
  } catch (e) {
16
- console.error('Failed to load clients.json:', e)
16
+ console.error("Failed to load clients.json:", e);
17
17
  }
18
18
 
19
- // Normalize raw client name (e.g. "antigravity-client") to friendly name (e.g. "Antigravity")
19
+ // Normalize raw client name (e.g. "claude-ai") to friendly name (e.g. "Claude Desktop")
20
20
  function normalizeClientName(rawName: string): string {
21
- if (!rawName) return 'Unknown'
22
- const nameLower = rawName.toLowerCase()
21
+ if (!rawName) return "Unknown";
22
+ const nameLower = rawName.toLowerCase();
23
23
  const client = clientsConfig.find((c: any) => {
24
24
  try {
25
- return new RegExp(c.match, 'i').test(nameLower)
25
+ return new RegExp(c.match, "i").test(nameLower);
26
26
  } catch {
27
- return nameLower.includes(c.match)
27
+ return nameLower.includes(c.match);
28
28
  }
29
- })
30
- return client?.name || rawName
29
+ });
30
+ return client?.name || rawName;
31
31
  }
32
32
 
33
- const CLIENTS = ["Claude Code", "v0", "Cursor", "GitHub Copilot", "Windsurf"]
34
- const OPERATIONS = ["Read", "Write", "Update", "Delete", "Create"]
35
- const STATUSES = ["Success", "Success", "Success", "Warning", "Error"]
33
+ const CLIENTS = ["Claude Code", "v0", "Cursor", "GitHub Copilot", "Windsurf"];
34
+ const OPERATIONS = ["Read", "Write", "Update", "Delete", "Create"];
35
+ const STATUSES = ["Success", "Success", "Success", "Warning", "Error"];
36
36
  const DESCRIPTIONS = {
37
- "Success": ["Operation completed successfully", "Resource accessed", "Data synchronized"],
38
- "Warning": ["High latency detected", "Rate limit approaching", "Deprecation warning"],
39
- "Error": ["Unauthorized access", "Internal server error", "Timeout exceeded", "Validation failed"]
40
- }
37
+ Success: [
38
+ "Operation completed successfully",
39
+ "Resource accessed",
40
+ "Data synchronized",
41
+ ],
42
+ Warning: [
43
+ "High latency detected",
44
+ "Rate limit approaching",
45
+ "Deprecation warning",
46
+ ],
47
+ Error: [
48
+ "Unauthorized access",
49
+ "Internal server error",
50
+ "Timeout exceeded",
51
+ "Validation failed",
52
+ ],
53
+ };
41
54
 
42
55
  export async function GET(request: Request) {
43
56
  try {
44
- // Fetch logs from db-exporter service
45
- // timeout 2s to not hang
46
- const controller = new AbortController()
47
- const timeoutId = setTimeout(() => controller.abort(), 2000)
48
-
49
- const res = await fetch(`${DB_EXPORTER_URL}/api/logs?limit=100`, {
50
- signal: controller.signal,
51
- cache: 'no-store'
52
- })
53
- clearTimeout(timeoutId)
57
+ const homedir =
58
+ process.env.HOME || process.env.USER || "/Users/mikhailkogan";
59
+ const dbPath =
60
+ process.env.OM_DB_PATH ||
61
+ path.resolve(homedir, ".cybermem/data/openmemory.sqlite");
54
62
 
55
- if (!res.ok) {
56
- throw new Error(`Failed to fetch logs: ${res.statusText}`)
63
+ if (!fs.existsSync(dbPath)) {
64
+ console.error(`[AUDIT-LOGS-API] SQLite DB NOT FOUND at ${dbPath}`);
65
+ return NextResponse.json({ logs: [] });
57
66
  }
58
67
 
59
- const data = await res.json()
60
- const rawLogs = data.logs || []
68
+ console.error(`[AUDIT-LOGS-API] Reading logs from ${dbPath}`);
69
+ const sqlite3 = require("sqlite3").verbose();
70
+ const { open } = require("sqlite");
71
+ const db = await open({
72
+ filename: dbPath,
73
+ driver: sqlite3.Database,
74
+ });
75
+
76
+ const rawLogs = await db.all(
77
+ "SELECT * FROM cybermem_access_log ORDER BY timestamp DESC LIMIT 100",
78
+ );
79
+ await db.close();
80
+
81
+ console.error(`[AUDIT-LOGS-API] Found ${rawLogs.length} logs in SQLite`);
61
82
 
62
83
  const logs = rawLogs.map((log: any) => {
63
- const statusCode = parseInt(log.status) || 0
64
- let status = "Success"
65
- if (statusCode === 0 || statusCode >= 400) status = "Error"
66
- else if (statusCode >= 300) status = "Warning"
84
+ const statusCode = parseInt(log.status) || 0;
85
+ let status = "Success";
86
+ if (log.is_error === 1 || statusCode >= 400 || statusCode === 0)
87
+ status = "Error";
88
+ else if (statusCode >= 300) status = "Warning";
67
89
 
68
- // Capitalize operation
69
- const operation = log.operation.charAt(0).toUpperCase() + log.operation.slice(1)
90
+ // Capitalize operation
91
+ const operation =
92
+ log.operation.charAt(0).toUpperCase() + log.operation.slice(1);
70
93
 
71
- return {
72
- timestamp: log.timestamp,
73
- client: normalizeClientName(log.client_name),
74
- operation: operation,
75
- status: status,
76
- method: log.method,
77
- description: log.endpoint,
78
- rawStatus: log.status
79
- }
80
- })
94
+ return {
95
+ timestamp: log.timestamp,
96
+ client: normalizeClientName(log.client_name),
97
+ operation: operation,
98
+ status: status,
99
+ method: log.method,
100
+ description: log.endpoint,
101
+ rawStatus: log.status,
102
+ };
103
+ });
81
104
 
82
- return NextResponse.json({ logs })
105
+ return NextResponse.json({ logs });
83
106
  } catch (error) {
84
- console.error("Error fetching audit logs:", error)
107
+ console.error("[AUDIT-LOGS-API] Error fetching audit logs:", error);
85
108
  // Return empty list on error to avoid breaking UI with 500
86
- return NextResponse.json({ logs: [] })
109
+ return NextResponse.json({ logs: [] });
87
110
  }
88
111
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Token Authentication Endpoint
3
+ *
4
+ * Validates JWT token and sets cookie for browser sessions.
5
+ * Called by: cybermem-cli dashboard (opens browser with ?token=xxx)
6
+ */
7
+
8
+ import { verifyToken } from "@/lib/auth";
9
+ import { NextRequest } from "next/server";
10
+
11
+ export async function GET(req: NextRequest) {
12
+ const token = req.nextUrl.searchParams.get("token");
13
+ const error = req.nextUrl.searchParams.get("error");
14
+
15
+ // Check for error redirect
16
+ if (error) {
17
+ return new Response(
18
+ `<!DOCTYPE html>
19
+ <html>
20
+ <head><title>Auth Error</title></head>
21
+ <body style="background:#0a0a0a;color:#fff;font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
22
+ <div style="text-align:center">
23
+ <h1 style="color:#ef4444">Authentication Required</h1>
24
+ <p style="color:#888">Run: <code style="background:#222;padding:4px 8px;border-radius:4px">npx @cybermem/cli dashboard</code></p>
25
+ </div>
26
+ </body>
27
+ </html>`,
28
+ { status: 401, headers: { "Content-Type": "text/html" } },
29
+ );
30
+ }
31
+
32
+ if (!token) {
33
+ return new Response(JSON.stringify({ error: "No token provided" }), {
34
+ status: 400,
35
+ headers: { "Content-Type": "application/json" },
36
+ });
37
+ }
38
+
39
+ // Validate JWT
40
+ const payload = await verifyToken(token);
41
+
42
+ if (!payload) {
43
+ return new Response(JSON.stringify({ error: "Invalid token" }), {
44
+ status: 401,
45
+ headers: { "Content-Type": "application/json" },
46
+ });
47
+ }
48
+
49
+ // Set cookie and redirect to dashboard home
50
+ const response = new Response(null, {
51
+ status: 302,
52
+ headers: {
53
+ Location: "/",
54
+ "Set-Cookie": `cybermem_token=${token}; HttpOnly; Path=/; Max-Age=${30 * 24 * 60 * 60}; SameSite=Lax`,
55
+ },
56
+ });
57
+
58
+ return response;
59
+ }
@@ -0,0 +1,70 @@
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}`,
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
+ return NextResponse.json({
30
+ url: publicUrl,
31
+ type: "vps",
32
+ editable: true,
33
+ hint: "Configure CYBERMEM_PUBLIC_URL to change this URL",
34
+ });
35
+ }
36
+
37
+ // Detect from request headers (for LAN access)
38
+ const host = request.headers.get("host") || "";
39
+
40
+ // Check if accessing via .local domain (Raspberry Pi LAN)
41
+ if (host.includes(".local")) {
42
+ const hostname = host.split(":")[0];
43
+ return NextResponse.json({
44
+ url: `http://${hostname}:${MCP_PORT}`,
45
+ type: "lan",
46
+ editable: false,
47
+ hint: "Detected LAN access via mDNS (.local)",
48
+ });
49
+ }
50
+
51
+ // Check if accessing via IP address (likely LAN or VPS)
52
+ const ipMatch = host.match(/^(\d+\.\d+\.\d+\.\d+)/);
53
+ if (ipMatch) {
54
+ const protocol = request.headers.get("x-forwarded-proto") || "http";
55
+ return NextResponse.json({
56
+ url: `${protocol}://${ipMatch[1]}:${MCP_PORT}`,
57
+ type: "ip",
58
+ editable: true,
59
+ hint: "Detected IP-based access",
60
+ });
61
+ }
62
+
63
+ // Default to localhost
64
+ return NextResponse.json({
65
+ url: `http://localhost:${MCP_PORT}`,
66
+ type: "local",
67
+ editable: false,
68
+ hint: "Running locally",
69
+ });
70
+ }
@@ -1,5 +1,7 @@
1
1
  import { checkRateLimit, rateLimitResponse } from "@/lib/rate-limit";
2
+ import fs from "fs";
2
3
  import { NextRequest, NextResponse } from "next/server";
4
+ import path from "path";
3
5
 
4
6
  export const dynamic = "force-dynamic";
5
7
 
@@ -38,7 +40,21 @@ async function checkService(
38
40
  const latencyMs = Date.now() - start;
39
41
 
40
42
  if (res.ok) {
41
- return { name, status: "ok", latencyMs };
43
+ try {
44
+ const data = await res.json();
45
+ if (data.ok === false || data.status === "error") {
46
+ return {
47
+ name,
48
+ status: "error",
49
+ message: data.error || data.message || "Service reported error",
50
+ latencyMs,
51
+ };
52
+ }
53
+ return { name, status: "ok", latencyMs };
54
+ } catch {
55
+ // If not JSON but OK, assume OK (legacy services)
56
+ return { name, status: "ok", latencyMs };
57
+ }
42
58
  }
43
59
  return {
44
60
  name,
@@ -65,29 +81,40 @@ export async function GET(request: NextRequest) {
65
81
  return rateLimitResponse(rateLimit.resetIn);
66
82
  }
67
83
 
68
- // Use environment variables with sensible defaults for local Docker stack
69
- const dbExporterUrl = process.env.DB_EXPORTER_URL || "http://localhost:8000";
70
- // OpenMemory API is optional in SDK-based architecture
71
- // Only check if explicitly configured (SDK mode doesn't have HTTP API)
72
- const openMemoryUrl = process.env.OPENMEMORY_URL || process.env.CYBERMEM_URL;
73
- const vectorUrl = process.env.VECTOR_URL; // Vector is optional
74
-
75
- const checks: Promise<ServiceStatus>[] = [
76
- checkService("Database", `${dbExporterUrl}/health`),
77
- ];
78
-
79
- // Only check OpenMemory API if explicitly configured
80
- // In SDK mode, there's no HTTP API - memory is handled via MCP
81
- if (openMemoryUrl) {
82
- checks.push(checkService("OpenMemory API", `${openMemoryUrl}/health`));
84
+ // Check SQLite Database File
85
+ const homedir = process.env.HOME || process.env.USERPROFILE || "";
86
+ const dbPath =
87
+ process.env.OM_DB_PATH ||
88
+ path.resolve(homedir, ".cybermem/data/openmemory.sqlite");
89
+
90
+ const dbExists = fs.existsSync(dbPath);
91
+ console.error(
92
+ `[HEALTH-API] SQLite Check: ${dbExists ? "OK" : "MISSING"} (${dbPath})`,
93
+ );
94
+
95
+ const dbStatus: ServiceStatus = dbExists
96
+ ? { name: "Database", status: "ok" }
97
+ : { name: "Database", status: "error", message: "SQLite file not found" };
98
+
99
+ const checks: ServiceStatus[] = [dbStatus];
100
+
101
+ // Check Core API if explicitly configured
102
+ const apiEndpoint =
103
+ process.env.CYBERMEM_URL ||
104
+ process.env.OPENMEMORY_URL ||
105
+ "http://localhost:8626";
106
+
107
+ if (apiEndpoint) {
108
+ console.error(`[HEALTH-API] Checking API at ${apiEndpoint}/health`);
109
+ const apiStatus = await checkService(
110
+ "CyberMem (MCP) API",
111
+ `${apiEndpoint}/health`,
112
+ );
113
+ console.error(`[HEALTH-API] API Status: ${apiStatus.status}`);
114
+ checks.push(apiStatus);
83
115
  }
84
116
 
85
- // Only check Vector if configured
86
- if (vectorUrl) {
87
- checks.push(checkService("Vector", `${vectorUrl}/health`));
88
- }
89
-
90
- const services = await Promise.all(checks);
117
+ const services = checks;
91
118
 
92
119
  // Determine overall status
93
120
  const hasError = services.some((s) => s.status === "error");