prism-mcp-server 2.5.1 → 3.0.0

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/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  ## Table of Contents
16
16
 
17
- - [What's New (v2.5.0)](#whats-new-in-v250---enterprise-memory-)
17
+ - [What's New (v3.0.0)](#whats-new-in-v300---agent-hivemind-)
18
18
  - [How Prism Compares](#how-prism-compares)
19
19
  - [Quick Start](#quick-start-zero-config--local-mode)
20
20
  - [Mind Palace Dashboard](#-the-mind-palace-dashboard)
@@ -24,6 +24,7 @@
24
24
  - [Tool Reference](#tool-reference)
25
25
  - [LangChain / LangGraph Integration](#langchain--langgraph-integration)
26
26
  - [Environment Variables](#environment-variables)
27
+ - [Boot Settings (Restart Required)](#-boot-settings-restart-required)
27
28
  - [Progressive Context Loading](#progressive-context-loading)
28
29
  - [Time Travel](#time-travel-version-history)
29
30
  - [Agent Telepathy](#agent-telepathy-multi-client-sync)
@@ -37,7 +38,20 @@
37
38
 
38
39
  ---
39
40
 
40
- ## What's New in v2.5.0 — Enterprise Memory 🏗️
41
+ ## What's New in v3.0.0 — Agent Hivemind 🐝
42
+
43
+ | Feature | Description |
44
+ |---|---|
45
+ | 🐝 **Role-Scoped Memory** | Optional `role` parameter on ledger, handoff, and context loading — each agent role (dev, qa, pm, lead, security, ux) gets its own isolated memory lane within a project. Defaults to `'global'` for full backward compatibility. |
46
+ | 👥 **Agent Registry** | New `agent_register`, `agent_heartbeat`, `agent_list_team` tools — agents announce their presence, pulse their status, and discover who else is working on the team. Stale agents are auto-pruned after 30 minutes. |
47
+ | 🎯 **Team Roster Injection** | When loading context with a role, Prism automatically injects a "Team Roster" showing active teammates, their roles, current tasks, and last heartbeat — true multi-agent awareness without extra tool calls. |
48
+ | ⚙️ **Dashboard Settings** | New Settings modal with runtime toggles (auto-capture, theme, context depth) backed by a persistent `system_settings` key-value store. Environment variables override DB settings for safety. |
49
+ | 📡 **Hivemind Radar** | New dashboard widget showing active agents, their roles (with icons), current tasks, and heartbeat timestamps — a real-time team coordination dashboard. |
50
+ | 🔒 **Conditional Tool Registration** | `PRISM_ENABLE_HIVEMIND` env var gates Hivemind tools — users who don't need multi-agent features keep the same lean tool count as v2.x. |
51
+ | ✅ **Test Suite** | 58 tests across 4 suites (storage, tools, dashboard, load) with Vitest — includes concurrent write stress tests, role isolation verification, and 0.2ms/write performance benchmarks. |
52
+
53
+ <details>
54
+ <summary><strong>What's in v2.5.0 — Enterprise Memory 🏗️</strong></summary>
41
55
 
42
56
  | Feature | Description |
43
57
  |---|---|
@@ -46,6 +60,8 @@
46
60
  | 🔗 **LangChain Integration (Phase 3)** | `PrismMemoryRetriever` and `PrismKnowledgeRetriever` — async-first `BaseRetriever` subclasses that wrap Prism MCP's traced search endpoints. Trace metadata flows automatically into `Document.metadata["trace"]` for LangSmith visibility. |
47
61
  | 🧩 **LangGraph Research Agent** | Full example in `examples/langgraph-agent/` — a 5-node agentic research loop with MCP bridge, persistent memory, and `EnsembleRetriever` hybrid search. |
48
62
 
63
+ </details>
64
+
49
65
  <details>
50
66
  <summary><strong>What's in v2.5.1 — Version Sync & Embedding Safety</strong></summary>
51
67
 
@@ -528,7 +544,8 @@ The retrievers use `_aget_relevant_documents` as the primary path with `asyncio.
528
544
  | Variable | Required | Description |
529
545
  |----------|----------|-------------|
530
546
  | `BRAVE_API_KEY` | No | Brave Search Pro API key (enables web/local search tools) |
531
- | `PRISM_STORAGE` | No | `"local"` (default) or `"supabase"` |
547
+ | `PRISM_STORAGE` | No | `"local"` (default) or `"supabase"` — **requires restart** |
548
+ | `PRISM_ENABLE_HIVEMIND` | No | Set `"true"` to enable multi-agent Hivemind tools — **requires restart** |
532
549
  | `GOOGLE_API_KEY` | No | Google AI / Gemini — enables paper analysis, Morning Briefings, compaction |
533
550
  | `BRAVE_ANSWERS_API_KEY` | No | Separate Brave Answers key for AI-grounded answers |
534
551
  | `SUPABASE_URL` | If cloud mode | Supabase project URL |
@@ -540,6 +557,39 @@ The retrievers use `_aget_relevant_documents` as the primary path with `asyncio.
540
557
 
541
558
  ---
542
559
 
560
+ ## ⚡ Boot Settings (Restart Required)
561
+
562
+ Some settings affect how Prism **initializes at startup** and cannot be changed at runtime. Prism stores these in a lightweight, dedicated SQLite database (`~/.prism-mcp/prism-config.db`) that is read **before** the main storage backend is selected — solving the chicken-and-egg problem of needing config before the config store is ready.
563
+
564
+ > **⚠️ You must restart the Prism MCP server after changing any Boot Setting.** The Mind Palace dashboard labels these with a **"Restart Required"** badge.
565
+
566
+ | Setting | Dashboard Control | Environment Override | Description |
567
+ |---------|------------------|---------------------|-------------|
568
+ | `PRISM_STORAGE` | ⚙️ Storage Backend dropdown | `PRISM_STORAGE=supabase` | Switch between `local` (SQLite) and `supabase` (cloud) |
569
+ | `PRISM_ENABLE_HIVEMIND` | ⚙️ Hivemind Mode toggle | `PRISM_ENABLE_HIVEMIND=true` | Enable/disable multi-agent coordination tools |
570
+
571
+ ### How Boot Settings Work
572
+
573
+ 1. **Dashboard saves the setting** → written to `~/.prism-mcp/prism-config.db` immediately
574
+ 2. **You restart the MCP server** → server reads the config DB at startup, selects backend/features
575
+ 3. **Environment variables always win** → if `PRISM_STORAGE` is set in your MCP config JSON, it overrides the dashboard value
576
+
577
+ ```
578
+ Priority: env var in MCP config JSON > Dashboard (prism-config.db) > default (local)
579
+ ```
580
+
581
+ ### Runtime Settings (no restart needed)
582
+
583
+ These settings take effect immediately without a restart:
584
+
585
+ | Setting | Description |
586
+ |---------|-------------|
587
+ | Dashboard Theme | Visual theme for the Mind Palace (`dark`, `midnight`, `purple`) |
588
+ | Context Depth | Default level for `session_load_context` (`quick`, `standard`, `deep`) |
589
+ | Auto-Capture HTML | Snapshot local dev server HTML on every handoff save |
590
+
591
+ ---
592
+
543
593
  ## Progressive Context Loading
544
594
 
545
595
  Load only what you need — saves tokens and speeds up boot:
@@ -880,6 +930,7 @@ See [`vertex-ai/`](vertex-ai/) for setup and benchmarks.
880
930
  │ │ ├── interface.ts # StorageBackend abstraction (+ GDPR delete methods)
881
931
  │ │ ├── sqlite.ts # SQLite local storage (libSQL + F32_BLOB + deleted_at migration)
882
932
  │ │ ├── supabase.ts # Supabase cloud storage (+ soft/hard delete)
933
+ │ │ ├── configStorage.ts # Boot config micro-DB (~/.prism-mcp/prism-config.db)
883
934
  │ │ └── index.ts # Backend factory (auto-selects based on PRISM_STORAGE)
884
935
  │ ├── sync/
885
936
  │ │ ├── interface.ts # SyncBus abstraction (Telepathy)
@@ -928,6 +979,10 @@ See [`vertex-ai/`](vertex-ai/) for setup and benchmarks.
928
979
 
929
980
  > **[View the full project board →](https://github.com/users/dcostenco/projects/1/views/1)**
930
981
 
982
+ ### ✅ v3.0 — Agent Hivemind (Shipped!)
983
+
984
+ See [What's New in v3.0.0](#whats-new-in-v300---agent-hivemind-) above.
985
+
931
986
  ### 🚀 Future Ideas
932
987
 
933
988
  | Feature | Issue | Description |
package/dist/config.js CHANGED
@@ -106,6 +106,14 @@ export const PRISM_CAPTURE_PORTS = (process.env.PRISM_CAPTURE_PORTS || "3000,300
106
106
  // Optionally enable verbose output (stderr) for Prism initialization,
107
107
  // memory indexing, and background tasks.
108
108
  export const PRISM_DEBUG_LOGGING = process.env.PRISM_DEBUG_LOGGING === "true";
109
+ // ─── v3.0: Agent Hivemind Feature Flag ───────────────────────
110
+ // When enabled, registers 3 additional MCP tools for multi-agent
111
+ // coordination: agent_register, agent_heartbeat, agent_list_team.
112
+ // The role parameter on existing tools (session_save_ledger, etc.)
113
+ // is always available regardless of this flag — adding a parameter
114
+ // doesn't increase tool count.
115
+ // Set PRISM_ENABLE_HIVEMIND=true to unlock the Agent Registry tools.
116
+ export const PRISM_ENABLE_HIVEMIND = process.env.PRISM_ENABLE_HIVEMIND === "true";
109
117
  if (PRISM_AUTO_CAPTURE) {
110
118
  // Use console.error instead of debugLog here to prevent circular dependency
111
119
  if (PRISM_DEBUG_LOGGING) {
@@ -17,54 +17,69 @@
17
17
  * ═══════════════════════════════════════════════════════════════════
18
18
  */
19
19
  import * as http from "http";
20
- import { execSync } from "child_process";
20
+ import { exec } from "child_process";
21
21
  import { getStorage } from "../storage/index.js";
22
22
  import { PRISM_USER_ID, SERVER_CONFIG } from "../config.js";
23
23
  import { renderDashboardHTML } from "./ui.js";
24
24
  const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);
25
+ /** Read HTTP request body as string */
26
+ function readBody(req) {
27
+ return new Promise((resolve, reject) => {
28
+ let data = "";
29
+ req.on("data", chunk => { data += chunk; });
30
+ req.on("end", () => resolve(data));
31
+ req.on("error", reject);
32
+ });
33
+ }
25
34
  /**
26
35
  * Kill any existing process holding the dashboard port.
27
36
  * This prevents zombie dashboard processes from surviving IDE restarts
28
37
  * and serving stale versions of the UI.
38
+ *
39
+ * CRITICAL: Uses async exec() instead of execSync() to avoid blocking
40
+ * the Node.js event loop. Blocking during startup prevents the MCP
41
+ * stdio transport from responding to the initialize handshake in time,
42
+ * causing Antigravity to report MCP_SERVER_INIT_ERROR.
29
43
  */
30
- function killPortHolder(port) {
31
- try {
32
- // lsof returns PIDs listening on the port; -t gives terse (PID-only) output
33
- const pids = execSync(`lsof -ti tcp:${port}`, { encoding: "utf-8" })
34
- .trim()
35
- .split("\n")
36
- .filter(Boolean);
37
- if (pids.length === 0)
38
- return;
39
- // Don't kill ourselves
40
- const myPid = String(process.pid);
41
- const stalePids = pids.filter(p => p !== myPid);
42
- if (stalePids.length > 0) {
43
- console.error(`[Dashboard] Killing stale process(es) on port ${port}: ${stalePids.join(", ")}`);
44
- execSync(`kill ${stalePids.join(" ")}`, { encoding: "utf-8" });
45
- // Brief pause to let the OS release the port
46
- execSync("sleep 0.3");
47
- }
48
- }
49
- catch (err) {
50
- // lsof exits with code 1 when no matches found — that's expected.
51
- // Any other failure (lsof missing, permission denied, etc.) gets a warning.
52
- const isNoMatch = err instanceof Error &&
53
- "status" in err &&
54
- err.status === 1;
55
- if (!isNoMatch) {
56
- console.error(`[Dashboard] killPortHolder: could not check port ${port} (lsof may not be installed) — skipping.`);
57
- }
58
- }
44
+ async function killPortHolder(port) {
45
+ return new Promise((resolve) => {
46
+ exec(`lsof -ti tcp:${port}`, { encoding: "utf-8" }, (err, stdout) => {
47
+ if (err) {
48
+ // lsof exits with code 1 when no matches found — that's expected.
49
+ // Any other failure (lsof missing, permission denied, etc.) gets a warning.
50
+ const isNoMatch = err.code === 1;
51
+ if (!isNoMatch) {
52
+ console.error(`[Dashboard] killPortHolder: could not check port ${port} (lsof may not be installed) — skipping.`);
53
+ }
54
+ return resolve();
55
+ }
56
+ const pids = stdout.trim().split("\n").filter(Boolean);
57
+ if (pids.length === 0)
58
+ return resolve();
59
+ // Don't kill ourselves
60
+ const myPid = String(process.pid);
61
+ const stalePids = pids.filter(p => p !== myPid);
62
+ if (stalePids.length > 0) {
63
+ console.error(`[Dashboard] Killing stale process(es) on port ${port}: ${stalePids.join(", ")}`);
64
+ exec(`kill ${stalePids.join(" ")}`, () => {
65
+ // Brief pause to let the OS release the port
66
+ setTimeout(resolve, 300);
67
+ });
68
+ }
69
+ else {
70
+ resolve();
71
+ }
72
+ });
73
+ });
59
74
  }
60
75
  export async function startDashboardServer() {
61
76
  // Clean up any zombie dashboard process from a previous session
62
- killPortHolder(PORT);
77
+ await killPortHolder(PORT);
63
78
  const storage = await getStorage();
64
79
  const httpServer = http.createServer(async (req, res) => {
65
80
  // CORS headers for local dev
66
81
  res.setHeader("Access-Control-Allow-Origin", "*");
67
- res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
82
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
68
83
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
69
84
  if (req.method === "OPTIONS") {
70
85
  res.writeHead(204);
@@ -193,6 +208,55 @@ export async function startDashboardServer() {
193
208
  res.writeHead(200, { "Content-Type": "application/json" });
194
209
  return res.end(JSON.stringify({ nodes, edges }));
195
210
  }
211
+ // ─── API: Hivemind Team Roster (v3.0) ───
212
+ if (url.pathname === "/api/team") {
213
+ const projectName = url.searchParams.get("project");
214
+ if (!projectName) {
215
+ res.writeHead(400, { "Content-Type": "application/json" });
216
+ return res.end(JSON.stringify({ error: "Missing ?project= parameter" }));
217
+ }
218
+ try {
219
+ const team = await storage.listTeam(projectName, PRISM_USER_ID);
220
+ res.writeHead(200, { "Content-Type": "application/json" });
221
+ return res.end(JSON.stringify({ team }));
222
+ }
223
+ catch {
224
+ res.writeHead(200, { "Content-Type": "application/json" });
225
+ return res.end(JSON.stringify({ team: [] }));
226
+ }
227
+ }
228
+ // ─── API: Settings — GET (v3.0 Dashboard Settings) ───
229
+ if (url.pathname === "/api/settings" && req.method === "GET") {
230
+ try {
231
+ const { getAllSettings } = await import("../storage/configStorage.js");
232
+ const settings = await getAllSettings();
233
+ res.writeHead(200, { "Content-Type": "application/json" });
234
+ return res.end(JSON.stringify({ settings }));
235
+ }
236
+ catch {
237
+ res.writeHead(200, { "Content-Type": "application/json" });
238
+ return res.end(JSON.stringify({ settings: {} }));
239
+ }
240
+ }
241
+ // ─── API: Settings — POST (v3.0 Dashboard Settings) ───
242
+ if (url.pathname === "/api/settings" && req.method === "POST") {
243
+ try {
244
+ const body = await readBody(req);
245
+ const parsed = JSON.parse(body);
246
+ if (parsed.key && parsed.value !== undefined) {
247
+ const { setSetting } = await import("../storage/configStorage.js");
248
+ await setSetting(parsed.key, String(parsed.value));
249
+ res.writeHead(200, { "Content-Type": "application/json" });
250
+ return res.end(JSON.stringify({ ok: true, key: parsed.key, value: parsed.value }));
251
+ }
252
+ res.writeHead(400, { "Content-Type": "application/json" });
253
+ return res.end(JSON.stringify({ error: "Missing key or value" }));
254
+ }
255
+ catch (err) {
256
+ res.writeHead(400, { "Content-Type": "application/json" });
257
+ return res.end(JSON.stringify({ error: "Invalid JSON body" }));
258
+ }
259
+ }
196
260
  // ─── 404 ───
197
261
  res.writeHead(404, { "Content-Type": "text/plain" });
198
262
  res.end("Not found");
@@ -27,7 +27,8 @@ export function renderDashboardHTML(version) {
27
27
  <style>
28
28
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
29
29
 
30
- :root {
30
+ /* ─── Theme: Dark (Default) ─── */
31
+ :root, [data-theme="dark"] {
31
32
  --bg-primary: #0a0e1a;
32
33
  --bg-secondary: #111827;
33
34
  --bg-glass: rgba(17, 24, 39, 0.6);
@@ -49,6 +50,44 @@ export function renderDashboardHTML(version) {
49
50
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
50
51
  }
51
52
 
53
+ /* ─── Theme: Midnight — deeper blacks, blue-shifted accents ─── */
54
+ [data-theme="midnight"] {
55
+ --bg-primary: #020617;
56
+ --bg-secondary: #0f172a;
57
+ --bg-glass: rgba(2, 6, 23, 0.7);
58
+ --border-glass: rgba(59, 130, 246, 0.15);
59
+ --border-glow: rgba(59, 130, 246, 0.35);
60
+ --text-primary: #e2e8f0;
61
+ --text-secondary: #94a3b8;
62
+ --text-muted: #475569;
63
+ --accent-purple: #818cf8;
64
+ --accent-blue: #60a5fa;
65
+ --accent-cyan: #22d3ee;
66
+ --accent-green: #34d399;
67
+ --accent-amber: #fbbf24;
68
+ --accent-rose: #fb7185;
69
+ --gradient-hero: linear-gradient(135deg, #818cf8 0%, #60a5fa 50%, #22d3ee 100%);
70
+ }
71
+
72
+ /* ─── Theme: Purple Haze — warm violet tones ─── */
73
+ [data-theme="purple"] {
74
+ --bg-primary: #0c0515;
75
+ --bg-secondary: #1a0a2e;
76
+ --bg-glass: rgba(26, 10, 46, 0.65);
77
+ --border-glass: rgba(168, 85, 247, 0.2);
78
+ --border-glow: rgba(168, 85, 247, 0.4);
79
+ --text-primary: #f5f3ff;
80
+ --text-secondary: #c4b5fd;
81
+ --text-muted: #7c3aed;
82
+ --accent-purple: #a855f7;
83
+ --accent-blue: #7c3aed;
84
+ --accent-cyan: #c084fc;
85
+ --accent-green: #a78bfa;
86
+ --accent-amber: #e879f9;
87
+ --accent-rose: #f472b6;
88
+ --gradient-hero: linear-gradient(135deg, #a855f7 0%, #7c3aed 50%, #c084fc 100%);
89
+ }
90
+
52
91
  body {
53
92
  background: var(--bg-primary);
54
93
  color: var(--text-primary);
@@ -270,6 +309,87 @@ export function renderDashboardHTML(version) {
270
309
  transition: color 0.2s;
271
310
  }
272
311
  .refresh-btn:hover { color: var(--accent-purple); }
312
+
313
+ /* ─── Settings Modal (v3.0) ─── */
314
+ .settings-btn {
315
+ background: none; border: 1px solid var(--border-glass);
316
+ color: var(--text-secondary); cursor: pointer; font-size: 1.1rem;
317
+ padding: 0.4rem 0.7rem; border-radius: var(--radius-sm);
318
+ transition: all 0.2s;
319
+ }
320
+ .settings-btn:hover { border-color: var(--border-glow); color: var(--accent-purple); }
321
+ .modal-overlay {
322
+ display: none; position: fixed; inset: 0; z-index: 100;
323
+ background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
324
+ justify-content: center; align-items: center;
325
+ }
326
+ .modal-overlay.active { display: flex; }
327
+ .modal {
328
+ background: var(--bg-secondary); border: 1px solid var(--border-glow);
329
+ border-radius: var(--radius); padding: 2rem; width: 480px; max-width: 90vw;
330
+ max-height: 85vh; overflow-y: auto; position: relative;
331
+ }
332
+ .modal h2 { font-size: 1.1rem; margin-bottom: 1.5rem; display: flex; align-items: center; gap: 0.5rem; }
333
+ .modal-close {
334
+ position: absolute; top: 1rem; right: 1rem; background: none;
335
+ border: none; color: var(--text-muted); cursor: pointer; font-size: 1.25rem;
336
+ }
337
+ .modal-close:hover { color: var(--text-primary); }
338
+ .setting-row {
339
+ display: flex; justify-content: space-between; align-items: center;
340
+ padding: 0.75rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
341
+ }
342
+ .setting-row:last-child { border-bottom: none; }
343
+ .setting-label { font-size: 0.85rem; color: var(--text-secondary); }
344
+ .setting-desc { font-size: 0.7rem; color: var(--text-muted); margin-top: 0.2rem; }
345
+ .toggle {
346
+ position: relative; width: 44px; height: 24px;
347
+ background: rgba(100,116,139,0.3); border-radius: 12px;
348
+ cursor: pointer; transition: background 0.3s; flex-shrink: 0;
349
+ }
350
+ .toggle.active { background: var(--accent-purple); }
351
+ .toggle::after {
352
+ content: ''; position: absolute; top: 2px; left: 2px;
353
+ width: 20px; height: 20px; border-radius: 50%;
354
+ background: white; transition: transform 0.3s;
355
+ }
356
+ .toggle.active::after { transform: translateX(20px); }
357
+ .setting-select {
358
+ background: var(--bg-primary); border: 1px solid var(--border-glass);
359
+ color: var(--text-primary); padding: 0.4rem 0.6rem;
360
+ border-radius: 6px; font-size: 0.8rem; font-family: var(--font-sans);
361
+ }
362
+ .setting-section {
363
+ font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
364
+ letter-spacing: 0.1em; color: var(--accent-purple); margin: 1rem 0 0.5rem;
365
+ }
366
+ .setting-saved {
367
+ font-size: 0.75rem; color: var(--accent-green); opacity: 0;
368
+ transition: opacity 0.3s; margin-left: 0.5rem;
369
+ }
370
+ .setting-saved.show { opacity: 1; }
371
+ .boot-badge {
372
+ font-size: 0.6rem; padding: 0.15rem 0.5rem; border-radius: 4px;
373
+ background: rgba(245,158,11,0.15); color: var(--accent-amber);
374
+ font-weight: 600; text-transform: uppercase;
375
+ }
376
+
377
+ /* ─── Hivemind Radar (v3.0) ─── */
378
+ .team-list { list-style: none; padding: 0; }
379
+ .team-item {
380
+ display: flex; align-items: center; gap: 0.75rem;
381
+ padding: 0.6rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
382
+ font-size: 0.85rem;
383
+ }
384
+ .team-item:last-child { border-bottom: none; }
385
+ .team-role { font-weight: 600; color: var(--text-primary); min-width: 60px; }
386
+ .team-task { color: var(--text-secondary); flex: 1; }
387
+ .team-heartbeat { font-size: 0.7rem; color: var(--text-muted); font-family: var(--font-mono); }
388
+ .pulse-dot {
389
+ width: 8px; height: 8px; border-radius: 50%; background: var(--accent-green);
390
+ flex-shrink: 0; animation: pulseDot 2s ease-in-out infinite;
391
+ }
392
+ @keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
273
393
  </style>
274
394
  </head>
275
395
  <body>
@@ -286,6 +406,7 @@ export function renderDashboardHTML(version) {
286
406
  <option value="">Loading projects...</option>
287
407
  </select>
288
408
  <button onclick="loadProject()">Inspect</button>
409
+ <button class="settings-btn" onclick="openSettings()" title="Settings">⚙️</button>
289
410
  </div>
290
411
  </header>
291
412
 
@@ -367,6 +488,84 @@ export function renderDashboardHTML(version) {
367
488
  <div class="card-title"><span class="dot" style="background:var(--accent-amber)"></span> Session Ledger</div>
368
489
  <div class="timeline" id="ledgerTimeline"></div>
369
490
  </div>
491
+ </div>
492
+
493
+ <!-- Hivemind Radar (v3.0) -->
494
+ <div class="card" id="hivemindCard" style="display:none">
495
+ <div class="card-title">
496
+ <span class="dot" style="background:var(--accent-cyan)"></span>
497
+ Hivemind Radar 🐝
498
+ <button onclick="loadTeam()" class="refresh-btn">↻</button>
499
+ </div>
500
+ <ul class="team-list" id="teamList">
501
+ <li style="color:var(--text-muted);font-size:0.85rem;text-align:center;padding:1rem">
502
+ No active agents. Set PRISM_ENABLE_HIVEMIND=true to enable.
503
+ </li>
504
+ </ul>
505
+ </div>
506
+ </div>
507
+ </div>
508
+
509
+ <!-- Settings Modal (v3.0) -->
510
+ <div class="modal-overlay" id="settingsModal">
511
+ <div class="modal">
512
+ <button class="modal-close" onclick="closeSettings()">✕</button>
513
+ <h2>⚙️ Settings</h2>
514
+
515
+ <div class="setting-section">Runtime Settings</div>
516
+
517
+ <div class="setting-row">
518
+ <div>
519
+ <div class="setting-label">Auto-Capture HTML</div>
520
+ <div class="setting-desc">Capture local dev server UI on handoff save</div>
521
+ </div>
522
+ <div class="toggle" id="toggle-auto-capture" onclick="toggleSetting('auto_capture', this)"></div>
523
+ </div>
524
+
525
+ <div class="setting-row">
526
+ <div>
527
+ <div class="setting-label">Dashboard Theme</div>
528
+ <div class="setting-desc">Visual theme for Mind Palace</div>
529
+ </div>
530
+ <select class="setting-select" id="select-theme" onchange="saveSetting('dashboard_theme', this.value)">
531
+ <option value="dark">Dark (Default)</option>
532
+ <option value="midnight">Midnight</option>
533
+ <option value="purple">Purple Haze</option>
534
+ </select>
535
+ </div>
536
+
537
+ <div class="setting-row">
538
+ <div>
539
+ <div class="setting-label">Context Depth</div>
540
+ <div class="setting-desc">Default level for session_load_context</div>
541
+ </div>
542
+ <select class="setting-select" id="select-context-depth" onchange="saveSetting('default_context_depth', this.value)">
543
+ <option value="standard">Standard (~200 tokens)</option>
544
+ <option value="quick">Quick (~50 tokens)</option>
545
+ <option value="deep">Deep (~1000+ tokens)</option>
546
+ </select>
547
+ </div>
548
+
549
+ <div class="setting-section">Boot Settings <span class="boot-badge">Restart Required</span></div>
550
+
551
+ <div class="setting-row">
552
+ <div>
553
+ <div class="setting-label">Hivemind Mode</div>
554
+ <div class="setting-desc">Multi-agent coordination (PRISM_ENABLE_HIVEMIND)</div>
555
+ </div>
556
+ <div class="toggle" id="toggle-hivemind" onclick="toggleBootSetting('hivemind_enabled', this)"></div>
557
+ </div>
558
+ <div class="setting-row">
559
+ <div>
560
+ <div class="setting-label">Storage Backend</div>
561
+ <div class="setting-desc">Switch between SQLite and Supabase</div>
562
+ </div>
563
+ <select id="storageBackendSelect" onchange="window.saveBootSetting('PRISM_STORAGE', this.value)" style="padding: 0.2rem 0.4rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); cursor: pointer;">
564
+ <option value="local">SQLite</option>
565
+ <option value="supabase">Supabase</option>
566
+ </select>
567
+ </div>
568
+ <span class="setting-saved" id="savedToast">Saved ✓</span>
370
569
  </div>
371
570
  </div>
372
571
  </div>
@@ -528,6 +727,7 @@ export function renderDashboardHTML(version) {
528
727
 
529
728
  document.getElementById('content').className = 'grid grid-main fade-in';
530
729
  document.getElementById('content').style.display = 'grid';
730
+ loadTeam(); // v3.0: auto-load Hivemind team
531
731
  } catch(e) {
532
732
  alert('Failed to load project data: ' + e.message);
533
733
  } finally {
@@ -618,6 +818,127 @@ export function renderDashboardHTML(version) {
618
818
 
619
819
  // Initialize the graph on page load
620
820
  loadGraph();
821
+
822
+ // ─── Settings Modal (v3.0) ───
823
+ function openSettings() {
824
+ document.getElementById('settingsModal').classList.add('active');
825
+ loadSettings();
826
+ }
827
+ function closeSettings() {
828
+ document.getElementById('settingsModal').classList.remove('active');
829
+ }
830
+ // Close on overlay click
831
+ document.getElementById('settingsModal').addEventListener('click', function(e) {
832
+ if (e.target === this) closeSettings();
833
+ });
834
+
835
+ async function loadSettings() {
836
+ try {
837
+ var res = await fetch('/api/settings');
838
+ var data = await res.json();
839
+ var s = data.settings || {};
840
+ // Runtime toggles
841
+ if (s.auto_capture === 'true') document.getElementById('toggle-auto-capture').classList.add('active');
842
+ else document.getElementById('toggle-auto-capture').classList.remove('active');
843
+ // Context depth
844
+ if (s.default_context_depth) document.getElementById('select-context-depth').value = s.default_context_depth;
845
+ // Theme
846
+ if (s.dashboard_theme) {
847
+ document.getElementById('select-theme').value = s.dashboard_theme;
848
+ applyTheme(s.dashboard_theme);
849
+ }
850
+ // Boot toggles
851
+ if (s.hivemind_enabled === 'true') document.getElementById('toggle-hivemind').classList.add('active');
852
+ else document.getElementById('toggle-hivemind').classList.remove('active');
853
+
854
+ // Storage Backend
855
+ if (s.PRISM_STORAGE) {
856
+ document.getElementById('storageBackendSelect').value = s.PRISM_STORAGE;
857
+ }
858
+ } catch(e) { console.warn('Settings load failed:', e); }
859
+ }
860
+
861
+ function toggleSetting(key, el) {
862
+ var isActive = el.classList.toggle('active');
863
+ saveSetting(key, isActive ? 'true' : 'false');
864
+ }
865
+ function toggleBootSetting(key, el) {
866
+ var isActive = el.classList.toggle('active');
867
+ saveSetting(key, isActive ? 'true' : 'false');
868
+ showToast('Saved. Restart your AI client for this to take effect.');
869
+ }
870
+ function saveBootSetting(key, value) {
871
+ saveSetting(key, value);
872
+ showToast('Saved. Restart your AI client for this to take effect.');
873
+ }
874
+
875
+ async function saveSetting(key, value) {
876
+ try {
877
+ await fetch('/api/settings', {
878
+ method: 'POST',
879
+ headers: { 'Content-Type': 'application/json' },
880
+ body: JSON.stringify({ key: key, value: value })
881
+ });
882
+ // Apply theme instantly on change
883
+ if (key === 'dashboard_theme') applyTheme(value);
884
+ showToast('Saved ✓');
885
+ } catch(e) { console.error('Setting save failed:', e); }
886
+ }
887
+
888
+ /**
889
+ * applyTheme — sets the data-theme attribute on <html>
890
+ * CSS custom properties in [data-theme="..."] blocks
891
+ * override :root defaults instantly, no page reload needed.
892
+ */
893
+ function applyTheme(theme) {
894
+ document.documentElement.setAttribute('data-theme', theme || 'dark');
895
+ }
896
+
897
+ function showToast(msg) {
898
+ var toast = document.getElementById('savedToast');
899
+ toast.textContent = msg || 'Saved ✓';
900
+ toast.classList.add('show');
901
+ setTimeout(function() { toast.classList.remove('show'); }, 2000);
902
+ }
903
+
904
+ // ─── Hivemind Radar (v3.0) ───
905
+ async function loadTeam() {
906
+ var project = document.getElementById('projectSelect').value;
907
+ if (!project) return;
908
+ var card = document.getElementById('hivemindCard');
909
+ try {
910
+ var res = await fetch('/api/team?project=' + encodeURIComponent(project));
911
+ var data = await res.json();
912
+ var team = data.team || [];
913
+ var list = document.getElementById('teamList');
914
+ if (team.length > 0) {
915
+ var roleIcons = {dev:'🛠️',qa:'🔍',pm:'📋',lead:'🏗️',security:'🔒',ux:'🎨',cmo:'📢'};
916
+ list.innerHTML = team.map(function(a) {
917
+ var icon = roleIcons[a.role] || '🤖';
918
+ var ago = a.last_heartbeat ? timeAgo(a.last_heartbeat) : '?';
919
+ return '<li class="team-item">' +
920
+ '<span class="pulse-dot"></span>' +
921
+ '<span class="team-role">' + icon + ' ' + escapeHtml(a.role) + '</span>' +
922
+ '<span class="team-task">' + escapeHtml(a.current_task || 'idle') + '</span>' +
923
+ '<span class="team-heartbeat">' + ago + '</span></li>';
924
+ }).join('');
925
+ card.style.display = 'block';
926
+ } else {
927
+ list.innerHTML = '<li style="color:var(--text-muted);font-size:0.85rem;text-align:center;padding:1rem">No active agents on this project.</li>';
928
+ card.style.display = 'block';
929
+ }
930
+ } catch(e) {
931
+ console.warn('Team load failed:', e);
932
+ }
933
+ }
934
+
935
+ function timeAgo(iso) {
936
+ var diff = Date.now() - new Date(iso).getTime();
937
+ var mins = Math.floor(diff / 60000);
938
+ if (mins < 1) return 'just now';
939
+ if (mins < 60) return mins + 'm ago';
940
+ return Math.floor(mins/60) + 'h ago';
941
+ }
621
942
  </script>
622
943
  </body>
623
944
  </html>`;