@cybermem/dashboard 0.13.17 → 0.14.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/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # @cybermem/dashboard
2
+
3
+ ## 0.14.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Automated release for patch version bump.
8
+
9
+ ## 0.14.4
10
+
11
+ ### Patch Changes
12
+
13
+ - [#86](https://github.com/mikhailkogan17/cybermem/pull/86) [`ffca5dd`](https://github.com/mikhailkogan17/cybermem/commit/ffca5dd374a50f594c1a935f1fe81ee1e1e3c6fe) Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Migrate version management to Changesets
14
+
15
+ - Remove custom bash scripts (version-bump.sh, release.sh)
16
+ - Add Changesets for automated versioning and changelog generation
17
+ - Update publish workflow to use Changesets
18
+ - Add documentation for new release process
package/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  # Base stage for both dev and prod
2
- FROM node:20-alpine AS base
2
+ FROM node:24-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
@@ -27,13 +27,13 @@ RUN --mount=type=cache,target=/root/.pnpm-store \
27
27
  pnpm build
28
28
 
29
29
  # Native stage for sqlite3 bindings and wrapper
30
- FROM node:20-alpine AS native-builder
30
+ FROM node:24-alpine AS native-builder
31
31
  RUN apk update && apk add --no-cache python3 python3-dev make g++
32
32
  WORKDIR /native
33
33
  RUN npm init -y && npm install sqlite3@5.1.7 sqlite@5.1.1
34
34
 
35
35
  # Production stage
36
- FROM node:20-alpine AS production
36
+ FROM node:24-alpine AS production
37
37
  RUN apk add --no-cache libc6-compat
38
38
  WORKDIR /app
39
39
 
@@ -67,37 +67,49 @@ export async function GET(request: Request) {
67
67
  let config: any;
68
68
  let configType = client?.configType || "json";
69
69
 
70
+ // Local envs (isManaged): use @cybermem/mcp directly
71
+ // Remote envs: use mcp-remote (standard stdio-to-HTTP bridge)
70
72
  if (configType === "toml") {
71
73
  if (isManaged) {
72
- config = `[mcpServers.cybermem]\ncommand = "npx"\nargs = ["@cybermem/mcp"]`;
74
+ const localArgs = isStaging
75
+ ? `["-y", "@cybermem/mcp", "--env", "staging"]`
76
+ : `["-y", "@cybermem/mcp"]`;
77
+ config = `[mcpServers.cybermem]\ncommand = "npx"\nargs = ${localArgs}`;
73
78
  } else {
74
79
  const keyVal = maskKey ? displayKey : actualKey;
75
- config = `[mcpServers.cybermem]\ncommand = "npx"\nargs = ["@cybermem/mcp", "--url", "${baseUrl}", "--token", "${keyVal}"]`;
80
+ config = `[mcpServers.cybermem]\ncommand = "npx"\nargs = ["-y", "mcp-remote", "${baseUrl}", "--header", "X-API-Key:${keyVal}"]`;
76
81
  }
77
82
  } else if (configType === "command" || configType === "cmd") {
78
- let cmd = isManaged ? client?.localCommand : client?.remoteCommand;
79
- if (!cmd) {
80
- cmd = client?.command?.replace("http://localhost:8080", baseUrl) || "";
83
+ // Generate command directly (don't rely on clients.json templates)
84
+ if (isManaged) {
85
+ const clientName = client?.id || "cybermem";
86
+ const cliPrefix = client?.id === "gemini-cli" ? "gemini" : "claude";
87
+ config = `${cliPrefix} mcp add ${clientName} npx -y @cybermem/mcp`;
88
+ } else {
89
+ const keyVal = maskKey ? displayKey : actualKey;
90
+ const clientName = client?.id || "cybermem";
91
+ const cliPrefix = client?.id === "gemini-cli" ? "gemini" : "claude";
92
+ config = `${cliPrefix} mcp add ${clientName} -- npx -y mcp-remote ${baseUrl} --header X-API-Key:${keyVal}`;
81
93
  }
82
- cmd = cmd.replace("{{ENDPOINT}}", baseUrl);
83
- cmd = cmd.replace("{{API_KEY}}", maskKey ? displayKey : actualKey);
84
- cmd = cmd.replace("{{TOKEN}}", maskKey ? displayKey : actualKey);
85
- config = cmd;
86
94
  } else {
87
95
  // JSON (default)
88
- const args = isManaged
89
- ? ["-y", "@cybermem/mcp"]
90
- : [
91
- "-y",
92
- "@cybermem/mcp",
93
- "--url",
94
- baseUrl,
95
- "--token",
96
- maskKey ? displayKey : actualKey,
97
- ];
96
+ let args: string[];
98
97
 
99
- if (isStaging && !isManaged) {
100
- args.push("--staging");
98
+ if (isManaged) {
99
+ // Local: use @cybermem/mcp directly (SDK mode)
100
+ args = ["-y", "@cybermem/mcp"];
101
+ if (isStaging) {
102
+ args.push("--env", "staging");
103
+ }
104
+ } else {
105
+ // Remote: use mcp-remote (standard stdio-to-HTTP bridge)
106
+ args = [
107
+ "-y",
108
+ "mcp-remote",
109
+ baseUrl,
110
+ "--header",
111
+ `X-API-Key:${maskKey ? displayKey : actualKey}`,
112
+ ];
101
113
  }
102
114
 
103
115
  config = {
@@ -120,11 +120,13 @@ export async function GET(request: NextRequest) {
120
120
  }
121
121
 
122
122
  let apiKey = process.env.OM_API_KEY || "not-set";
123
+ let tokenSource = "env";
123
124
 
124
125
  // Try to read from HttpOnly cookie first
125
126
  const cookieKey = request.cookies.get("cybermem_api_key")?.value;
126
127
  if (cookieKey) {
127
128
  apiKey = cookieKey;
129
+ tokenSource = "cookie";
128
130
  }
129
131
 
130
132
  // Fallback to config file
@@ -132,7 +134,10 @@ export async function GET(request: NextRequest) {
132
134
  if (fs.existsSync(CONFIG_PATH)) {
133
135
  const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
134
136
  const conf = JSON.parse(raw);
135
- if (conf.api_key && apiKey === "not-set") apiKey = conf.api_key;
137
+ if (conf.api_key && apiKey === "not-set") {
138
+ apiKey = conf.api_key;
139
+ tokenSource = "config";
140
+ }
136
141
  }
137
142
  } catch (e) {
138
143
  // ignore
@@ -143,7 +148,45 @@ export async function GET(request: NextRequest) {
143
148
  const secretPath = "/run/secrets/om_api_key";
144
149
  if (fs.existsSync(secretPath)) {
145
150
  const secret = fs.readFileSync(secretPath, "utf-8").trim();
146
- if (secret) apiKey = secret;
151
+ if (secret) {
152
+ if (secret.startsWith("sk-")) {
153
+ apiKey = secret;
154
+ tokenSource = "docker-secret";
155
+ } else {
156
+ // Fallback: support env-file format like "OM_API_KEY=sk-..."
157
+ const envMatch = secret.match(
158
+ /OM_API_KEY=["']?(sk-[a-zA-Z0-9]+)["']?/,
159
+ );
160
+ if (envMatch) {
161
+ apiKey = envMatch[1];
162
+ tokenSource = "docker-secret-env";
163
+ } else {
164
+ console.warn(
165
+ `[Settings API] Token at ${secretPath} doesn't match expected format (sk-* or OM_API_KEY=sk-*)`,
166
+ );
167
+ }
168
+ }
169
+ }
170
+ }
171
+ } catch (e) {
172
+ // ignore
173
+ }
174
+
175
+ // Fallback: Auto-generated token location
176
+ try {
177
+ const fallbackPath = "/data/.cybermem_token";
178
+ if (apiKey === "not-set" && fs.existsSync(fallbackPath)) {
179
+ const token = fs.readFileSync(fallbackPath, "utf-8").trim();
180
+ if (token) {
181
+ if (token.startsWith("sk-")) {
182
+ apiKey = token;
183
+ tokenSource = "fallback";
184
+ } else {
185
+ console.warn(
186
+ `[Settings API] Token at ${fallbackPath} doesn't match expected format (sk-*)`,
187
+ );
188
+ }
189
+ }
147
190
  }
148
191
  } catch (e) {
149
192
  // ignore
@@ -158,6 +201,16 @@ export async function GET(request: NextRequest) {
158
201
  // isManaged = Local Mode (localhost auto-login)
159
202
  const isManaged = isLocal;
160
203
 
204
+ // Mask the token for public display
205
+ const maskToken = (token: string) => {
206
+ if (!token || token === "not-set") return token;
207
+ if (token.length <= 10) return "****";
208
+ // sk-abcd...efgh
209
+ return `${token.slice(0, 7)}...${token.slice(-4)}`;
210
+ };
211
+
212
+ const maskedApiKey = maskToken(apiKey);
213
+
161
214
  // Read version from package.json
162
215
  let version = "v0.11.4"; // Default fallback
163
216
  try {
@@ -176,8 +229,9 @@ export async function GET(request: NextRequest) {
176
229
 
177
230
  return NextResponse.json(
178
231
  {
179
- token: apiKey,
180
232
  apiKey: apiKey,
233
+ apiKeyMasked: maskedApiKey,
234
+ tokenSource: apiKey === "not-set" ? "not-set" : tokenSource,
181
235
  endpoint,
182
236
  isManaged,
183
237
  isLocal,
@@ -7,6 +7,7 @@ import { Check, Copy, Eye, EyeOff, RotateCcw, Shield } from "lucide-react";
7
7
 
8
8
  interface AccessTokenSectionProps {
9
9
  apiKey: string;
10
+ apiKeyMasked: string;
10
11
  showApiKey: boolean;
11
12
  setShowApiKey: (show: boolean) => void;
12
13
  copiedId: string | null;
@@ -18,6 +19,7 @@ interface AccessTokenSectionProps {
18
19
 
19
20
  export default function AccessTokenSection({
20
21
  apiKey,
22
+ apiKeyMasked,
21
23
  showApiKey,
22
24
  setShowApiKey,
23
25
  copiedId,
@@ -40,7 +42,10 @@ export default function AccessTokenSection({
40
42
  <div className="relative flex-1">
41
43
  <Input
42
44
  id="access-token"
43
- value={apiKey || "Token not generated yet"}
45
+ value={
46
+ (showApiKey ? apiKey : apiKeyMasked) ||
47
+ "Token not generated yet"
48
+ }
44
49
  readOnly
45
50
  className="bg-black/40 border-[0.5px] border-white/10 text-white font-mono text-sm pr-10"
46
51
  type={showApiKey ? "text" : "password"}
@@ -12,6 +12,7 @@ import SystemInfoSection from "./settings/system-info-section";
12
12
 
13
13
  export default function SettingsModal({ onClose }: { onClose: () => void }) {
14
14
  const [apiKey, setApiKey] = useState("");
15
+ const [apiKeyMasked, setApiKeyMasked] = useState("");
15
16
  const [endpoint, setEndpoint] = useState("");
16
17
  const [isManaged, setIsManaged] = useState(false);
17
18
  const [instanceType, setInstanceType] = useState<"local" | "rpi" | "vps">(
@@ -58,8 +59,16 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
58
59
 
59
60
  if (localKey && !data.isManaged) {
60
61
  setApiKey(localKey);
62
+ // Mask the local key for display
63
+ const maskedLocal = localKey.length > 10
64
+ ? `${localKey.slice(0, 7)}...${localKey.slice(-4)}`
65
+ : "****";
66
+ setApiKeyMasked(maskedLocal);
61
67
  } else {
62
68
  setApiKey(data.apiKey !== "not-set" ? data.apiKey : "");
69
+ setApiKeyMasked(
70
+ data.apiKeyMasked !== "not-set" ? data.apiKeyMasked : "",
71
+ );
63
72
  }
64
73
 
65
74
  let srvEndpoint = data.endpoint;
@@ -227,6 +236,7 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
227
236
  <div className="flex-1 overflow-y-auto p-8 space-y-8 bg-[#05100F]">
228
237
  <AccessTokenSection
229
238
  apiKey={apiKey}
239
+ apiKeyMasked={apiKeyMasked}
230
240
  showApiKey={showApiKey}
231
241
  setShowApiKey={setShowApiKey}
232
242
  copiedId={copiedId}
package/e2e/api.spec.ts CHANGED
@@ -215,5 +215,28 @@ test.describe("Dashboard:E2E:API (Deep Verification)", () => {
215
215
  contentType: "text/plain",
216
216
  });
217
217
  });
218
+
219
+ await test.step("⚙️ Settings Fallback — Verify tokenSource and Masking", async () => {
220
+ console.log("⚙️ GET /api/settings (Fallback/Auto-gen Verification)");
221
+
222
+ const settingsResp = await request.get(`${DASHBOARD_URL}/api/settings`, {
223
+ headers: getHeaders("antigravity-client"),
224
+ });
225
+
226
+ const settings = await settingsResp.json();
227
+ console.log(` Token Source: ${settings.tokenSource}`);
228
+
229
+ expect(settingsResp.status()).toBe(200);
230
+ expect(settings).toHaveProperty("tokenSource");
231
+ // Verify apiKeyMasked is masked (not the raw apiKey field)
232
+ if (settings.apiKeyMasked && settings.apiKeyMasked !== "not-set") {
233
+ expect(settings.apiKeyMasked.length).toBeLessThanOrEqual(14); // 7 + 3 + 4 = 14
234
+ expect(settings.apiKeyMasked).toMatch(/^sk-.*\.\.\..*$/);
235
+ }
236
+ // Verify apiKey contains the raw token (for UI copy functionality)
237
+ if (settings.apiKey && settings.apiKey !== "not-set") {
238
+ expect(settings.apiKey).toMatch(/^sk-[a-f0-9]{32}$/);
239
+ }
240
+ });
218
241
  });
219
242
  });
package/e2e/ui.spec.ts CHANGED
@@ -199,8 +199,10 @@ test.describe("Dashboard:E2E:UI (High-Fidelity Mocks)", () => {
199
199
  contentType: "application/json",
200
200
  body: JSON.stringify({
201
201
  apiKey: MOCK_API_KEY,
202
+ apiKeyMasked: "sk-e2e-...2345",
202
203
  instanceId: "local-dev-mock",
203
204
  instanceType: "local",
205
+ isManaged: true,
204
206
  endpoint: "http://localhost:8626/mcp",
205
207
  }),
206
208
  });
@@ -210,7 +212,7 @@ test.describe("Dashboard:E2E:UI (High-Fidelity Mocks)", () => {
210
212
  description: `API Key: ${MOCK_API_KEY.substring(0, 10)}...`,
211
213
  });
212
214
 
213
- // 5. Mock MCP Config
215
+ // 5. Mock MCP Config (local env = @cybermem/mcp direct, no --url)
214
216
  await page.route("**/api/mcp-config*", async (route) => {
215
217
  await route.fulfill({
216
218
  status: 200,
@@ -219,19 +221,19 @@ test.describe("Dashboard:E2E:UI (High-Fidelity Mocks)", () => {
219
221
  configType: "json",
220
222
  config: {
221
223
  mcpServers: {
222
- "cybermem-mcp": {
224
+ cybermem: {
223
225
  command: "npx",
224
- args: ["@cybermem/mcp", "--url", "http://localhost:8626"],
225
- env: { X_CLIENT_NAME: MOCK_IDENTITY_WRITER },
226
+ args: ["-y", "@cybermem/mcp"],
226
227
  },
227
228
  },
228
229
  },
230
+ isManaged: true,
229
231
  }),
230
232
  });
231
233
  });
232
234
  appliedMocks.push({
233
235
  endpoint: "GET /api/mcp-config",
234
- description: "MCP config with npx @cybermem/mcp",
236
+ description: "MCP config with npx @cybermem/mcp (local)",
235
237
  });
236
238
  });
237
239
 
@@ -326,9 +328,9 @@ test.describe("Dashboard:E2E:UI (High-Fidelity Mocks)", () => {
326
328
  await expect(page.getByText(/ACCESS TOKEN/i).first()).toBeVisible();
327
329
  });
328
330
 
329
- await test.step(`Verify Token Matches Mock ${MOCK_API_KEY}`, async () => {
331
+ await test.step(`Verify Token Matches Masked Mock by Default`, async () => {
330
332
  const input = page.locator("input#access-token");
331
- await expect(input).toHaveValue(MOCK_API_KEY);
333
+ await expect(input).toHaveValue("sk-e2e-...2345");
332
334
  });
333
335
 
334
336
  await test.step("Toggle Token Visibility — password → text", async () => {
@@ -336,6 +338,7 @@ test.describe("Dashboard:E2E:UI (High-Fidelity Mocks)", () => {
336
338
  await expect(input).toHaveAttribute("type", "password");
337
339
  await page.getByTestId("toggle-visibility").click();
338
340
  await expect(input).toHaveAttribute("type", "text");
341
+ await expect(input).toHaveValue(MOCK_API_KEY);
339
342
  });
340
343
 
341
344
  await flushNetwork();
@@ -357,14 +360,14 @@ test.describe("Dashboard:E2E:UI (High-Fidelity Mocks)", () => {
357
360
  await page.getByRole("button", { name: MOCK_IDENTITY_WRITER }).click();
358
361
  });
359
362
 
360
- await test.step("Verify CLI Install Command — npx @cybermem/mcp", async () => {
363
+ await test.step("Verify CLI Install Command — npx @cybermem/mcp (local, no --url)", async () => {
361
364
  const codeBlock = page.locator("pre code");
362
365
  await expect(codeBlock).toContainText("npx");
363
366
  await expect(codeBlock).toContainText("@cybermem/mcp");
364
367
  });
365
368
 
366
369
  await testInfo.attach("🚀 CLI Installation Command", {
367
- body: `npx @cybermem/cli init\nnpx @cybermem/cli up\n\nMCP Server Config:\n"cybermem-mcp": {\n "command": "npx",\n "args": ["@cybermem/mcp", "--url", "http://localhost:8626"]\n}`,
370
+ body: `npx @cybermem/cli init\nnpx @cybermem/cli up\n\nMCP Server Config (local):\n"cybermem": {\n "command": "npx",\n "args": ["-y", "@cybermem/mcp"]\n}`,
368
371
  contentType: "text/plain",
369
372
  });
370
373
 
package/eslint.config.mjs CHANGED
@@ -1,13 +1,16 @@
1
- import nextConfig from 'eslint-config-next';
1
+ import nextConfig from "eslint-config-next";
2
2
 
3
3
  const eslintConfig = [
4
+ {
5
+ ignores: ["playwright-report/**", ".next/**", "test-results/**"],
6
+ },
4
7
  ...nextConfig,
5
8
  {
6
9
  rules: {
7
10
  // Disable false-positive for sessionStorage reads in useEffect
8
- 'react-hooks/set-state-in-effect': 'off',
11
+ "react-hooks/set-state-in-effect": "off",
9
12
  // Disable for shadcn/ui components that use Math.random() in useMemo
10
- 'react-hooks/purity': 'off',
13
+ "react-hooks/purity": "off",
11
14
  },
12
15
  },
13
16
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cybermem/dashboard",
3
- "version": "0.13.17",
3
+ "version": "0.14.5",
4
4
  "description": "CyberMem Monitoring Dashboard",
5
5
  "homepage": "https://cybermem.dev",
6
6
  "repository": {
@@ -94,7 +94,7 @@
94
94
  ],
95
95
  "configType": "command",
96
96
  "localCommand": "claude mcp add cybermem npx @cybermem/mcp",
97
- "remoteCommand": "claude mcp add cybermem -- npx @cybermem/mcp --url {{ENDPOINT}} --token {{TOKEN}}"
97
+ "remoteCommand": "claude mcp add cybermem -- npx -y mcp-remote {{ENDPOINT}} --header X-API-Key:{{TOKEN}}"
98
98
  },
99
99
  {
100
100
  "id": "chatgpt",
@@ -158,7 +158,7 @@
158
158
  ],
159
159
  "configType": "cmd",
160
160
  "localCommand": "gemini mcp add cybermem npx @cybermem/mcp",
161
- "remoteCommand": "gemini mcp add cybermem -- npx @cybermem/mcp --url {{ENDPOINT}} --token {{TOKEN}}"
161
+ "remoteCommand": "gemini mcp add cybermem -- npx -y mcp-remote {{ENDPOINT}} --header X-API-Key:{{TOKEN}}"
162
162
  },
163
163
  {
164
164
  "id": "other",