prism-mcp-server 3.0.0 → 3.0.1
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 +140 -5
- package/dist/dashboard/server.js +74 -1
- package/dist/dashboard/ui.js +291 -2
- package/dist/server.js +41 -10
- package/dist/storage/sqlite.js +12 -0
- package/dist/storage/supabase.js +12 -0
- package/dist/tools/agentRegistryHandlers.js +16 -11
- package/dist/tools/sessionMemoryHandlers.js +31 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
## Table of Contents
|
|
16
16
|
|
|
17
|
-
- [What's New (v3.0.
|
|
17
|
+
- [What's New (v3.0.1)](#whats-new-in-v301---agent-identity--brain-clean-up-)
|
|
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)
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
- [Use Cases](#use-cases)
|
|
23
23
|
- [Architecture](#architecture)
|
|
24
24
|
- [Tool Reference](#tool-reference)
|
|
25
|
+
- [Agent Hivemind — Role Usage](#agent-hivemind--role-usage)
|
|
25
26
|
- [LangChain / LangGraph Integration](#langchain--langgraph-integration)
|
|
26
27
|
- [Environment Variables](#environment-variables)
|
|
27
28
|
- [Boot Settings (Restart Required)](#-boot-settings-restart-required)
|
|
@@ -38,7 +39,17 @@
|
|
|
38
39
|
|
|
39
40
|
---
|
|
40
41
|
|
|
41
|
-
## What's New in v3.0.
|
|
42
|
+
## What's New in v3.0.1 — Agent Identity & Brain Clean-up 🧹
|
|
43
|
+
|
|
44
|
+
| Feature | Description |
|
|
45
|
+
|---|---|
|
|
46
|
+
| 🧹 **Brain Health Clean-up** | New **Fix Issues** button in the Mind Palace Dashboard's Brain Health card — detects orphaned handoffs, missing embeddings, and stale rollups, then cleans them up in one click without needing the MCP tool. |
|
|
47
|
+
| 👤 **Agent Identity Settings** | Dashboard Settings → Agent Identity panel lets you set a **Default Role** (`dev`, `qa`, `pm`…) and **Agent Name** (e.g. `Dmitri`). Both values auto-apply as fallbacks in all memory and Hivemind tools — no need to pass them per call. |
|
|
48
|
+
| 📜 **Role-Scoped Skills** | Each agent role can have its own persistent skill/rules document stored in the dashboard (⚙️ Settings → Skills). It is automatically injected into every `session_load_context` response so the agent boots with its rules pre-loaded. |
|
|
49
|
+
| 🔤 **Resource Formatting Fix** | `memory://{project}/handoff` resources now render as formatted plain text (Last Summary, TODOs, Keywords) instead of a raw JSON blob — readable in Claude Desktop's paperclip attach panel. |
|
|
50
|
+
|
|
51
|
+
<details>
|
|
52
|
+
<summary><strong>What's in v3.0.0 — Agent Hivemind 🐝</strong></summary>
|
|
42
53
|
|
|
43
54
|
| Feature | Description |
|
|
44
55
|
|---|---|
|
|
@@ -50,6 +61,9 @@
|
|
|
50
61
|
| 🔒 **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
62
|
| ✅ **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
63
|
|
|
64
|
+
</details>
|
|
65
|
+
|
|
66
|
+
|
|
53
67
|
<details>
|
|
54
68
|
<summary><strong>What's in v2.5.0 — Enterprise Memory 🏗️</strong></summary>
|
|
55
69
|
|
|
@@ -102,7 +116,7 @@
|
|
|
102
116
|
| Feature | Description |
|
|
103
117
|
|---|---|
|
|
104
118
|
| 🩺 **Brain Health Check** | `session_health_check` — like Unix `fsck` for your agent's memory. Detects missing embeddings, duplicate entries, orphaned handoffs, and stale rollups. Use `auto_fix: true` to repair automatically. |
|
|
105
|
-
| 📊 **Mind Palace Health** | Brain health indicator on the Mind Palace Dashboard — see your memory integrity at a glance. |
|
|
119
|
+
| 📊 **Mind Palace Health** | Brain health indicator on the Mind Palace Dashboard — see your memory integrity at a glance. **🧹 Fix Issues** button auto-deletes orphaned handoffs in one click. |
|
|
106
120
|
|
|
107
121
|
</details>
|
|
108
122
|
|
|
@@ -237,11 +251,15 @@ Open **`http://localhost:3000`** in your browser to see exactly what your AI age
|
|
|
237
251
|

|
|
238
252
|
|
|
239
253
|
- **Current State & TODOs** — See the exact context injected into the LLM's prompt
|
|
254
|
+
- **Agent Identity Chip** — Header shows your active role + name (e.g. `🛠️ dev · Antigravity`); click to open Settings
|
|
255
|
+
- **Brain Health 🩺** — Memory integrity status at a glance; **🧹 Fix Issues** button auto-cleans orphaned handoffs in one click
|
|
240
256
|
- **Git Drift Detection** — Alerts you if you've modified code outside the agent's view
|
|
241
257
|
- **Morning Briefing** — AI-synthesized action plan from your last sessions
|
|
242
258
|
- **Time Travel Timeline** — Browse historical handoff states and revert any version
|
|
243
259
|
- **Visual Memory Vault** — Browse UI screenshots and auto-captured HTML states
|
|
244
260
|
- **Session Ledger** — Full audit trail of every decision your agent has made
|
|
261
|
+
- **Neural Graph** — Force-directed visualization of project ↔ keyword associations
|
|
262
|
+
- **Hivemind Radar** — Real-time active agent roster with role, task, and heartbeat
|
|
245
263
|
|
|
246
264
|
The dashboard auto-discovers all your projects and updates in real time.
|
|
247
265
|
|
|
@@ -463,7 +481,15 @@ graph TB
|
|
|
463
481
|
|
|
464
482
|
| Tool | Purpose | Key Args | Returns |
|
|
465
483
|
|------|---------|----------|---------|
|
|
466
|
-
| `session_health_check` | Scan brain for integrity issues (`fsck`) | `auto_fix` (boolean) | Health report & auto-repairs |
|
|
484
|
+
| `session_health_check` | Scan brain for integrity issues (`fsck`) | `project`, `auto_fix` (boolean) | Health report & auto-repairs |
|
|
485
|
+
|
|
486
|
+
The **Mind Palace Dashboard** also shows a live **Brain Health 🩺** card for every project:
|
|
487
|
+
|
|
488
|
+
- **Status indicator** — `✅ Healthy` or `⚠️ Issues detected` with entry/handoff/rollup counts
|
|
489
|
+
- **🧹 Fix Issues button** — appears automatically when issues are detected; click to clean up orphaned handoffs and stale rollups in one click, no MCP tool call required
|
|
490
|
+
- **No issues found** — shown in green when memory integrity is confirmed
|
|
491
|
+
|
|
492
|
+
The tool and dashboard button both call the same repair logic — the dashboard button is simply a zero-friction shortcut for common maintenance.
|
|
467
493
|
|
|
468
494
|
### v2.5 Enterprise Memory Tools
|
|
469
495
|
|
|
@@ -492,6 +518,110 @@ Instead of writing custom JavaScript, pass a `template` name for instant extract
|
|
|
492
518
|
|
|
493
519
|
---
|
|
494
520
|
|
|
521
|
+
## Agent Hivemind — Role Usage
|
|
522
|
+
|
|
523
|
+
Role-scoped memory lets multiple agents work on the same project without stepping on each other's memory. Each role gets its own isolated memory lane. Defaults to `global` for full backward compatibility.
|
|
524
|
+
|
|
525
|
+
### Available Roles
|
|
526
|
+
|
|
527
|
+
| Role | Use for |
|
|
528
|
+
|------|---------|
|
|
529
|
+
| `dev` | Development agent |
|
|
530
|
+
| `qa` | Testing / QA agent |
|
|
531
|
+
| `pm` | Product management |
|
|
532
|
+
| `lead` | Tech lead / orchestrator |
|
|
533
|
+
| `security` | Security review |
|
|
534
|
+
| `ux` | Design / UX |
|
|
535
|
+
| `global` | Default — shared, no isolation |
|
|
536
|
+
|
|
537
|
+
Custom role strings are also supported (e.g. `"docs"`, `"ml"`).
|
|
538
|
+
|
|
539
|
+
### Using Roles with Memory Tools
|
|
540
|
+
|
|
541
|
+
Just add `"role"` to any of the core memory tools:
|
|
542
|
+
|
|
543
|
+
```json
|
|
544
|
+
// Save a ledger entry as the "dev" agent
|
|
545
|
+
{ "name": "session_save_ledger", "arguments": {
|
|
546
|
+
"project": "my-app",
|
|
547
|
+
"role": "dev",
|
|
548
|
+
"conversation_id": "abc123",
|
|
549
|
+
"summary": "Fixed the auth race condition"
|
|
550
|
+
}}
|
|
551
|
+
|
|
552
|
+
// Load context scoped to your role
|
|
553
|
+
// Also injects a Team Roster showing active teammates
|
|
554
|
+
{ "name": "session_load_context", "arguments": {
|
|
555
|
+
"project": "my-app",
|
|
556
|
+
"role": "dev",
|
|
557
|
+
"level": "standard"
|
|
558
|
+
}}
|
|
559
|
+
|
|
560
|
+
// Save handoff as the "qa" agent
|
|
561
|
+
{ "name": "session_save_handoff", "arguments": {
|
|
562
|
+
"project": "my-app",
|
|
563
|
+
"role": "qa",
|
|
564
|
+
"last_summary": "Ran regression suite — 2 failures in auth module"
|
|
565
|
+
}}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Hivemind Coordination Tools
|
|
569
|
+
|
|
570
|
+
> **Requires:** `PRISM_ENABLE_HIVEMIND=true` (Boot Setting — restart required)
|
|
571
|
+
|
|
572
|
+
```json
|
|
573
|
+
// Announce yourself to the team at session start
|
|
574
|
+
{ "name": "agent_register", "arguments": {
|
|
575
|
+
"project": "my-app",
|
|
576
|
+
"role": "dev",
|
|
577
|
+
"agent_name": "Dev Agent #1",
|
|
578
|
+
"current_task": "Refactoring auth module"
|
|
579
|
+
}}
|
|
580
|
+
|
|
581
|
+
// Pulse every ~5 min to stay visible (agents pruned after 30 min)
|
|
582
|
+
{ "name": "agent_heartbeat", "arguments": {
|
|
583
|
+
"project": "my-app",
|
|
584
|
+
"role": "dev",
|
|
585
|
+
"current_task": "Now writing tests"
|
|
586
|
+
}}
|
|
587
|
+
|
|
588
|
+
// See everyone on the team
|
|
589
|
+
{ "name": "agent_list_team", "arguments": {
|
|
590
|
+
"project": "my-app"
|
|
591
|
+
}}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### How Role Isolation Works
|
|
595
|
+
|
|
596
|
+
- `session_load_context` with `role: "dev"` only sees entries saved with `role: "dev"`
|
|
597
|
+
- The `global` role is a shared pool — anything saved without a role goes here
|
|
598
|
+
- When loading *with* a role, Prism auto-injects a **Team Roster** block listing active teammates, roles, and tasks — no extra tool call needed
|
|
599
|
+
- The Hivemind Radar widget in the Mind Palace dashboard shows agent activity in real time
|
|
600
|
+
|
|
601
|
+
### Setting Your Agent Identity
|
|
602
|
+
|
|
603
|
+
The easiest way to configure your role and name is via the **Mind Palace Dashboard ⚙️ Settings → Agent Identity**:
|
|
604
|
+
|
|
605
|
+
- **Default Role** — dropdown to select `dev`, `qa`, `pm`, `lead`, `security`, `ux`, or `global`
|
|
606
|
+
- **Agent Name** — free text for your display name (e.g. `Dmitri`, `Dev Alex`, `QA Bot`)
|
|
607
|
+
|
|
608
|
+
Once set, **all memory and Hivemind tools automatically use these values** as fallbacks — no need to pass `role` or `agent_name` in every tool call.
|
|
609
|
+
|
|
610
|
+
> **Priority order:** explicit tool arg → dashboard setting → `"global"` (default)
|
|
611
|
+
|
|
612
|
+
**Alternative — hardcode in your startup rules** (if you prefer prompt-level config):
|
|
613
|
+
|
|
614
|
+
```markdown
|
|
615
|
+
## Prism MCP Memory Auto-Load (CRITICAL)
|
|
616
|
+
At the start of every new session, call session_load_context with:
|
|
617
|
+
- project: "my-app", role: "dev"
|
|
618
|
+
- project: "my-other-project", role: "dev"
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
> **Tip:** For true multi-agent setups, each AI instance has its own Mind Palace dashboard — set a different identity per agent there rather than managing it in prompts.
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
495
625
|
## LangChain / LangGraph Integration
|
|
496
626
|
|
|
497
627
|
Prism MCP includes first-class Python adapters for the LangChain ecosystem, located in `examples/langgraph-agent/`:
|
|
@@ -979,14 +1109,19 @@ See [`vertex-ai/`](vertex-ai/) for setup and benchmarks.
|
|
|
979
1109
|
|
|
980
1110
|
> **[View the full project board →](https://github.com/users/dcostenco/projects/1/views/1)**
|
|
981
1111
|
|
|
1112
|
+
### ✅ v3.0.1 — Agent Identity & Brain Clean-up (Shipped!)
|
|
1113
|
+
|
|
1114
|
+
See [What's New in v3.0.1](#whats-new-in-v301---agent-identity--brain-clean-up-) above.
|
|
1115
|
+
|
|
982
1116
|
### ✅ v3.0 — Agent Hivemind (Shipped!)
|
|
983
1117
|
|
|
984
|
-
See [What's New in v3.0.0](#whats-new-in-v300---agent-hivemind-) above.
|
|
1118
|
+
See [What's New in v3.0.0 — Agent Hivemind](#whats-new-in-v300---agent-hivemind-) above.
|
|
985
1119
|
|
|
986
1120
|
### 🚀 Future Ideas
|
|
987
1121
|
|
|
988
1122
|
| Feature | Issue | Description |
|
|
989
1123
|
|---------|-------|-------------|
|
|
1124
|
+
| **Role-Scoped Skills & Rules** | — | Each agent role (`dev`, `qa`, `pm`, etc.) gets its own persistent skill/rules document. Preloaded automatically at session start via `session_load_context`. Skills editable and uploadable from the Mind Palace Dashboard (⚙️ → Skills tab per role). Stored in `configStorage` per-role key — backend already exists. |
|
|
990
1125
|
| OpenTelemetry SDK Integration | [#6](https://github.com/dcostenco/prism-mcp/issues/6) | W3C-compliant tracing with Jaeger/Zipkin export |
|
|
991
1126
|
| GDPR Right to Portability | [#7](https://github.com/dcostenco/prism-mcp/issues/7) | `session_export_memory` tool for Art. 20 compliance |
|
|
992
1127
|
| Multi-agent CRDT Conflict Resolution | [#9](https://github.com/dcostenco/prism-mcp/issues/9) | Conflict-free replicated data types for concurrent agent edits |
|
package/dist/dashboard/server.js
CHANGED
|
@@ -21,6 +21,7 @@ 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
|
+
import { getAllSettings, setSetting } from "../storage/configStorage.js";
|
|
24
25
|
const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);
|
|
25
26
|
/** Read HTTP request body as string */
|
|
26
27
|
function readBody(req) {
|
|
@@ -125,7 +126,7 @@ export async function startDashboardServer() {
|
|
|
125
126
|
return res.end(JSON.stringify({ context, ledger, history }));
|
|
126
127
|
}
|
|
127
128
|
// ─── API: Brain Health Check (v2.2.0) ───
|
|
128
|
-
if (url.pathname === "/api/health") {
|
|
129
|
+
if (url.pathname === "/api/health" && req.method === "GET") {
|
|
129
130
|
try {
|
|
130
131
|
const { runHealthCheck } = await import("../utils/healthCheck.js");
|
|
131
132
|
const stats = await storage.getHealthStats(PRISM_USER_ID);
|
|
@@ -146,6 +147,78 @@ export async function startDashboardServer() {
|
|
|
146
147
|
}));
|
|
147
148
|
}
|
|
148
149
|
}
|
|
150
|
+
// ─── API: Brain Health Cleanup (v3.1) ───
|
|
151
|
+
// Deletes orphaned handoffs (handoffs with no backing ledger entries).
|
|
152
|
+
if (url.pathname === "/api/health/cleanup" && req.method === "POST") {
|
|
153
|
+
try {
|
|
154
|
+
const { runHealthCheck } = await import("../utils/healthCheck.js");
|
|
155
|
+
const stats = await storage.getHealthStats(PRISM_USER_ID);
|
|
156
|
+
const report = runHealthCheck(stats);
|
|
157
|
+
// Collect orphaned handoff projects from the health issues
|
|
158
|
+
const orphaned = stats.orphanedHandoffs || [];
|
|
159
|
+
const cleaned = [];
|
|
160
|
+
for (const { project } of orphaned) {
|
|
161
|
+
try {
|
|
162
|
+
await storage.deleteHandoff(project, PRISM_USER_ID);
|
|
163
|
+
cleaned.push(project);
|
|
164
|
+
console.error(`[Dashboard] Cleaned up orphaned handoff: ${project}`);
|
|
165
|
+
}
|
|
166
|
+
catch (delErr) {
|
|
167
|
+
console.error(`[Dashboard] Failed to delete handoff for ${project}:`, delErr);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
171
|
+
return res.end(JSON.stringify({
|
|
172
|
+
ok: true,
|
|
173
|
+
cleaned,
|
|
174
|
+
count: cleaned.length,
|
|
175
|
+
message: cleaned.length > 0
|
|
176
|
+
? `Cleaned up ${cleaned.length} orphaned handoff(s): ${cleaned.join(", ")}`
|
|
177
|
+
: "No orphaned handoffs to clean up.",
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error("[Dashboard] Health cleanup error:", err);
|
|
182
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
183
|
+
return res.end(JSON.stringify({ ok: false, error: "Cleanup failed" }));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ─── API: Role-Scoped Skills (v3.1) ───
|
|
187
|
+
// GET /api/skills → { skills: { dev: "...", qa: "..." } }
|
|
188
|
+
if (url.pathname === "/api/skills" && req.method === "GET") {
|
|
189
|
+
const all = await getAllSettings();
|
|
190
|
+
const skills = {};
|
|
191
|
+
for (const [k, v] of Object.entries(all)) {
|
|
192
|
+
if (k.startsWith("skill:") && v) {
|
|
193
|
+
skills[k.replace("skill:", "")] = v;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
197
|
+
return res.end(JSON.stringify({ skills }));
|
|
198
|
+
}
|
|
199
|
+
// POST /api/skills → { role, content } saves skill:<role>
|
|
200
|
+
if (url.pathname === "/api/skills" && req.method === "POST") {
|
|
201
|
+
const body = await new Promise(resolve => {
|
|
202
|
+
let data = "";
|
|
203
|
+
req.on("data", c => data += c);
|
|
204
|
+
req.on("end", () => resolve(data));
|
|
205
|
+
});
|
|
206
|
+
const { role, content } = JSON.parse(body || "{}");
|
|
207
|
+
if (!role) {
|
|
208
|
+
res.writeHead(400);
|
|
209
|
+
return res.end(JSON.stringify({ error: "role required" }));
|
|
210
|
+
}
|
|
211
|
+
await setSetting(`skill:${role}`, content || "");
|
|
212
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
213
|
+
return res.end(JSON.stringify({ ok: true, role }));
|
|
214
|
+
}
|
|
215
|
+
// DELETE /api/skills/:role → clears skill:<role>
|
|
216
|
+
if (url.pathname.startsWith("/api/skills/") && req.method === "DELETE") {
|
|
217
|
+
const role = url.pathname.replace("/api/skills/", "");
|
|
218
|
+
await setSetting(`skill:${role}`, "");
|
|
219
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
220
|
+
return res.end(JSON.stringify({ ok: true, role }));
|
|
221
|
+
}
|
|
149
222
|
// ─── API: Knowledge Graph Data (v2.3.0) ───
|
|
150
223
|
if (url.pathname === "/api/graph") {
|
|
151
224
|
// Fetch recent ledger entries to build the graph
|
package/dist/dashboard/ui.js
CHANGED
|
@@ -295,6 +295,21 @@ export function renderDashboardHTML(version) {
|
|
|
295
295
|
.health-issues .issue-row {
|
|
296
296
|
padding: 0.3rem 0; display: flex; gap: 0.5rem; align-items: flex-start;
|
|
297
297
|
}
|
|
298
|
+
.cleanup-btn {
|
|
299
|
+
margin-left: auto; background: rgba(244,63,94,0.12); border: 1px solid rgba(244,63,94,0.3);
|
|
300
|
+
color: var(--accent-rose); cursor: pointer; font-size: 0.75rem; font-weight: 600;
|
|
301
|
+
padding: 0.2rem 0.65rem; border-radius: 6px; transition: all 0.2s;
|
|
302
|
+
}
|
|
303
|
+
.cleanup-btn:hover { background: rgba(244,63,94,0.25); border-color: var(--accent-rose); }
|
|
304
|
+
.cleanup-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
305
|
+
.toast-fixed {
|
|
306
|
+
position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 200;
|
|
307
|
+
padding: 0.65rem 1.2rem; border-radius: 10px; font-size: 0.85rem; font-weight: 500;
|
|
308
|
+
backdrop-filter: blur(10px); border: 1px solid var(--border-glow);
|
|
309
|
+
background: var(--bg-secondary); color: var(--text-primary);
|
|
310
|
+
opacity: 0; transition: opacity 0.3s; pointer-events: none;
|
|
311
|
+
}
|
|
312
|
+
.toast-fixed.show { opacity: 1; }
|
|
298
313
|
|
|
299
314
|
/* ─── Neural Graph (v2.3.0) ─── */
|
|
300
315
|
#network-container {
|
|
@@ -318,6 +333,45 @@ export function renderDashboardHTML(version) {
|
|
|
318
333
|
transition: all 0.2s;
|
|
319
334
|
}
|
|
320
335
|
.settings-btn:hover { border-color: var(--border-glow); color: var(--accent-purple); }
|
|
336
|
+
.identity-chip {
|
|
337
|
+
display: none; align-items: center; gap: 0.4rem;
|
|
338
|
+
padding: 0.35rem 0.75rem; border-radius: 999px;
|
|
339
|
+
background: rgba(139,92,246,0.12); border: 1px solid rgba(139,92,246,0.25);
|
|
340
|
+
color: var(--text-secondary); font-size: 0.8rem; font-weight: 500;
|
|
341
|
+
cursor: pointer; transition: all 0.2s;
|
|
342
|
+
}
|
|
343
|
+
.identity-chip:hover { border-color: var(--accent-purple); color: var(--accent-purple); background: rgba(139,92,246,0.2); }
|
|
344
|
+
.identity-chip .role-icon { font-size: 0.9rem; }
|
|
345
|
+
.identity-chip .identity-label { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
346
|
+
/* Settings modal tab bar */
|
|
347
|
+
.settings-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border-glass); margin: 0 -1.5rem 1.2rem; padding: 0 1.5rem; }
|
|
348
|
+
.s-tab { padding: 0.55rem 1.1rem; font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); cursor: pointer;
|
|
349
|
+
border-bottom: 2px solid transparent; transition: all 0.2s; background: none; border-top: none; border-left: none; border-right: none; }
|
|
350
|
+
.s-tab.active { color: var(--accent-purple); border-bottom-color: var(--accent-purple); }
|
|
351
|
+
.s-tab:hover:not(.active) { color: var(--text-primary); }
|
|
352
|
+
.s-tab-panel { display: none; } .s-tab-panel.active { display: block; }
|
|
353
|
+
/* Skills editor */
|
|
354
|
+
.skill-role-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; }
|
|
355
|
+
.skill-role-row label { font-size: 0.82rem; color: var(--text-secondary); }
|
|
356
|
+
.skill-role-select { padding: 0.3rem 0.6rem; background: var(--bg-hover); color: var(--text-primary);
|
|
357
|
+
border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); }
|
|
358
|
+
.skill-textarea { width: 100%; min-height: 220px; background: var(--bg-hover); color: var(--text-primary);
|
|
359
|
+
border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 0.75rem;
|
|
360
|
+
font-size: 0.82rem; font-family: var(--font-mono); line-height: 1.5; resize: vertical;
|
|
361
|
+
box-sizing: border-box; transition: border-color 0.2s; }
|
|
362
|
+
.skill-textarea:focus { outline: none; border-color: var(--accent-purple); }
|
|
363
|
+
.skill-char-count { font-size: 0.74rem; color: var(--text-muted); text-align: right; margin-top: 0.3rem; }
|
|
364
|
+
.skill-actions { display: flex; gap: 0.6rem; margin-top: 0.85rem; align-items: center; }
|
|
365
|
+
.skill-save-btn { background: var(--accent-purple); color: #fff; border: none; border-radius: var(--radius-sm);
|
|
366
|
+
padding: 0.45rem 1rem; font-size: 0.82rem; font-weight: 600; cursor: pointer; transition: opacity 0.2s; }
|
|
367
|
+
.skill-save-btn:hover { opacity: 0.85; }
|
|
368
|
+
.skill-upload-btn { background: none; border: 1px solid var(--border-glass); color: var(--text-secondary);
|
|
369
|
+
border-radius: var(--radius-sm); padding: 0.45rem 0.85rem; font-size: 0.82rem; cursor: pointer; transition: all 0.2s; }
|
|
370
|
+
.skill-upload-btn:hover { border-color: var(--accent-purple); color: var(--accent-purple); }
|
|
371
|
+
.skill-clear-btn { background: none; border: none; color: var(--text-muted); font-size: 0.8rem; cursor: pointer;
|
|
372
|
+
margin-left: auto; transition: color 0.2s; }
|
|
373
|
+
.skill-clear-btn:hover { color: #ef4444; }
|
|
374
|
+
.skill-hint { font-size: 0.78rem; color: var(--text-muted); margin-top: 0.6rem; line-height: 1.5; }
|
|
321
375
|
.modal-overlay {
|
|
322
376
|
display: none; position: fixed; inset: 0; z-index: 100;
|
|
323
377
|
background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
|
|
@@ -402,6 +456,7 @@ export function renderDashboardHTML(version) {
|
|
|
402
456
|
<span class="version-badge">v${version}</span>
|
|
403
457
|
</div>
|
|
404
458
|
<div class="selector">
|
|
459
|
+
<span class="identity-chip" id="identityChip" onclick="openSettings()" title="Agent Identity — click to change"></span>
|
|
405
460
|
<select id="projectSelect">
|
|
406
461
|
<option value="">Loading projects...</option>
|
|
407
462
|
</select>
|
|
@@ -440,7 +495,10 @@ export function renderDashboardHTML(version) {
|
|
|
440
495
|
|
|
441
496
|
<!-- Brain Health (v2.2.0) -->
|
|
442
497
|
<div class="card" id="healthCard" style="display:none">
|
|
443
|
-
<div class="card-title"
|
|
498
|
+
<div class="card-title">
|
|
499
|
+
<span class="dot" style="background:var(--accent-green)"></span> Brain Health 🩺
|
|
500
|
+
<button class="cleanup-btn" id="cleanupBtn" onclick="cleanupIssues()" style="display:none">🧹 Fix Issues</button>
|
|
501
|
+
</div>
|
|
444
502
|
<div class="health-status">
|
|
445
503
|
<div class="health-dot unknown" id="healthDot"></div>
|
|
446
504
|
<div>
|
|
@@ -512,6 +570,15 @@ export function renderDashboardHTML(version) {
|
|
|
512
570
|
<button class="modal-close" onclick="closeSettings()">✕</button>
|
|
513
571
|
<h2>⚙️ Settings</h2>
|
|
514
572
|
|
|
573
|
+
<!-- Tab bar -->
|
|
574
|
+
<div class="settings-tabs">
|
|
575
|
+
<button class="s-tab active" id="stab-settings" onclick="switchSettingsTab('settings')">⚙️ Settings</button>
|
|
576
|
+
<button class="s-tab" id="stab-skills" onclick="switchSettingsTab('skills')">📜 Skills</button>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<!-- Settings panel (existing content) -->
|
|
580
|
+
<div class="s-tab-panel active" id="spanel-settings">
|
|
581
|
+
|
|
515
582
|
<div class="setting-section">Runtime Settings</div>
|
|
516
583
|
|
|
517
584
|
<div class="setting-row">
|
|
@@ -565,12 +632,107 @@ export function renderDashboardHTML(version) {
|
|
|
565
632
|
<option value="supabase">Supabase</option>
|
|
566
633
|
</select>
|
|
567
634
|
</div>
|
|
635
|
+
|
|
636
|
+
<div class="setting-section">Agent Identity</div>
|
|
637
|
+
|
|
638
|
+
<div class="setting-row">
|
|
639
|
+
<div>
|
|
640
|
+
<div class="setting-label">Default Role</div>
|
|
641
|
+
<div class="setting-desc">Used when no role is passed to memory/Hivemind tools</div>
|
|
642
|
+
</div>
|
|
643
|
+
<select class="setting-select" id="select-default-role" onchange="saveSetting('default_role', this.value)">
|
|
644
|
+
<option value="global">global (shared)</option>
|
|
645
|
+
<option value="dev">dev</option>
|
|
646
|
+
<option value="qa">qa</option>
|
|
647
|
+
<option value="pm">pm</option>
|
|
648
|
+
<option value="lead">lead</option>
|
|
649
|
+
<option value="security">security</option>
|
|
650
|
+
<option value="ux">ux</option>
|
|
651
|
+
</select>
|
|
652
|
+
</div>
|
|
653
|
+
|
|
654
|
+
<div class="setting-row">
|
|
655
|
+
<div>
|
|
656
|
+
<div class="setting-label">Agent Name</div>
|
|
657
|
+
<div class="setting-desc">Display name shown in Hivemind Radar (e.g. Dmitri, Dev Alex)</div>
|
|
658
|
+
</div>
|
|
659
|
+
<input type="text" id="input-agent-name"
|
|
660
|
+
placeholder="e.g. Dmitri"
|
|
661
|
+
style="padding: 0.2rem 0.5rem; 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); width: 130px;"
|
|
662
|
+
onchange="saveSetting('agent_name', this.value)"
|
|
663
|
+
oninput="clearTimeout(this._t); this._t=setTimeout(()=>saveSetting('agent_name',this.value),800)" />
|
|
664
|
+
</div>
|
|
665
|
+
|
|
568
666
|
<span class="setting-saved" id="savedToast">Saved ✓</span>
|
|
667
|
+
</div><!-- /spanel-settings -->
|
|
668
|
+
|
|
669
|
+
<!-- Skills panel -->
|
|
670
|
+
<div class="s-tab-panel" id="spanel-skills">
|
|
671
|
+
<div class="skill-role-row">
|
|
672
|
+
<label>Role</label>
|
|
673
|
+
<select class="skill-role-select" id="skillRoleSelect" onchange="loadSkillForRole(this.value)">
|
|
674
|
+
<option value="global">🌐 global</option>
|
|
675
|
+
<option value="dev">🛠️ dev</option>
|
|
676
|
+
<option value="qa">🔍 qa</option>
|
|
677
|
+
<option value="pm">📋 pm</option>
|
|
678
|
+
<option value="lead">🏗️ lead</option>
|
|
679
|
+
<option value="security">🔒 security</option>
|
|
680
|
+
<option value="ux">🎨 ux</option>
|
|
681
|
+
</select>
|
|
682
|
+
</div>
|
|
683
|
+
<textarea class="skill-textarea" id="skillTextarea"
|
|
684
|
+
placeholder="Paste rules, conventions, or prompts for this role...
|
|
685
|
+
Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode\n- Log errors to console.error"
|
|
686
|
+
oninput="document.getElementById('skillCharCount').textContent = this.value.length + ' chars'">
|
|
687
|
+
</textarea>
|
|
688
|
+
<div class="skill-char-count" id="skillCharCount">0 chars</div>
|
|
689
|
+
<div class="skill-actions">
|
|
690
|
+
<button class="skill-save-btn" onclick="saveCurrentSkill()">💾 Save</button>
|
|
691
|
+
<label class="skill-upload-btn" title="Upload a .md or .txt file">
|
|
692
|
+
📎 Upload file
|
|
693
|
+
<input type="file" accept=".md,.txt,.markdown" style="display:none"
|
|
694
|
+
onchange="handleSkillUpload(this)">
|
|
695
|
+
</label>
|
|
696
|
+
<button class="skill-clear-btn" onclick="clearCurrentSkill()">🗑️ Clear</button>
|
|
697
|
+
</div>
|
|
698
|
+
<div class="skill-hint">
|
|
699
|
+
Skills are auto-injected into <code>session_load_context</code> responses for this role.<br>
|
|
700
|
+
Use Markdown. Changes take effect immediately — no restart needed.
|
|
701
|
+
</div>
|
|
702
|
+
</div><!-- /spanel-skills -->
|
|
703
|
+
|
|
569
704
|
</div>
|
|
570
705
|
</div>
|
|
571
706
|
</div>
|
|
572
707
|
|
|
708
|
+
<!-- Fixed toast for cleanup feedback -->
|
|
709
|
+
<div class="toast-fixed" id="fixedToast"></div>
|
|
710
|
+
|
|
573
711
|
<script>
|
|
712
|
+
// Role icon map
|
|
713
|
+
var ROLE_ICONS = {dev:'🛠️',qa:'🔍',pm:'📋',lead:'🏗️',security:'🔒',ux:'🎨',global:'🌐',cmo:'📢'};
|
|
714
|
+
|
|
715
|
+
// Load and render the identity chip from settings
|
|
716
|
+
async function loadIdentityChip() {
|
|
717
|
+
try {
|
|
718
|
+
var res = await fetch('/api/settings');
|
|
719
|
+
var data = await res.json();
|
|
720
|
+
var s = data.settings || {};
|
|
721
|
+
var role = s.default_role || '';
|
|
722
|
+
var name = s.agent_name || '';
|
|
723
|
+
var chip = document.getElementById('identityChip');
|
|
724
|
+
if (!chip) return;
|
|
725
|
+
if (role && role !== 'global' || name) {
|
|
726
|
+
var icon = ROLE_ICONS[role] || '🤖';
|
|
727
|
+
var label = name ? (role && role !== 'global' ? role + ' · ' + name : name) : role;
|
|
728
|
+
chip.innerHTML = '<span class="role-icon">' + icon + '</span><span class="identity-label">' + escapeHtml(label) + '</span>';
|
|
729
|
+
chip.style.display = 'flex';
|
|
730
|
+
} else {
|
|
731
|
+
chip.style.display = 'none';
|
|
732
|
+
}
|
|
733
|
+
} catch(e) { /* silently skip */ }
|
|
734
|
+
}
|
|
735
|
+
|
|
574
736
|
// Auto-load project list on page load
|
|
575
737
|
(async function() {
|
|
576
738
|
try {
|
|
@@ -586,6 +748,8 @@ export function renderDashboardHTML(version) {
|
|
|
586
748
|
} catch(e) {
|
|
587
749
|
document.getElementById('projectSelect').innerHTML = '<option value="">Error loading projects</option>';
|
|
588
750
|
}
|
|
751
|
+
// Load identity chip once settings are available
|
|
752
|
+
loadIdentityChip();
|
|
589
753
|
})();
|
|
590
754
|
|
|
591
755
|
async function loadProject() {
|
|
@@ -707,6 +871,7 @@ export function renderDashboardHTML(version) {
|
|
|
707
871
|
|
|
708
872
|
// Issue rows
|
|
709
873
|
var issues = healthData.issues || [];
|
|
874
|
+
var cleanupBtn = document.getElementById('cleanupBtn');
|
|
710
875
|
if (issues.length > 0) {
|
|
711
876
|
var sevIcons = { error: '🔴', warning: '🟡', info: '🔵' };
|
|
712
877
|
healthIssues.innerHTML = issues.map(function(i) {
|
|
@@ -715,8 +880,10 @@ export function renderDashboardHTML(version) {
|
|
|
715
880
|
'<span>' + escapeHtml(i.message) + '</span>' +
|
|
716
881
|
'</div>';
|
|
717
882
|
}).join('');
|
|
883
|
+
if (cleanupBtn) cleanupBtn.style.display = 'inline-block';
|
|
718
884
|
} else {
|
|
719
885
|
healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">🎉 No issues found</div>';
|
|
886
|
+
if (cleanupBtn) cleanupBtn.style.display = 'none';
|
|
720
887
|
}
|
|
721
888
|
|
|
722
889
|
healthCard.style.display = 'block';
|
|
@@ -832,6 +999,75 @@ export function renderDashboardHTML(version) {
|
|
|
832
999
|
if (e.target === this) closeSettings();
|
|
833
1000
|
});
|
|
834
1001
|
|
|
1002
|
+
// ─── Skills Tab JS ───────────────────────────────────────────
|
|
1003
|
+
var _skillsCache = {}; // role → content cache
|
|
1004
|
+
|
|
1005
|
+
function switchSettingsTab(tab) {
|
|
1006
|
+
['settings','skills'].forEach(function(t) {
|
|
1007
|
+
document.getElementById('stab-' + t).classList.toggle('active', t === tab);
|
|
1008
|
+
document.getElementById('spanel-' + t).classList.toggle('active', t === tab);
|
|
1009
|
+
});
|
|
1010
|
+
if (tab === 'skills') {
|
|
1011
|
+
// Load skill for whichever role is currently selected
|
|
1012
|
+
var role = document.getElementById('skillRoleSelect').value;
|
|
1013
|
+
loadSkillForRole(role);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async function loadSkillForRole(role) {
|
|
1018
|
+
try {
|
|
1019
|
+
var res = await fetch('/api/skills');
|
|
1020
|
+
var data = await res.json();
|
|
1021
|
+
_skillsCache = data.skills || {};
|
|
1022
|
+
var content = _skillsCache[role] || '';
|
|
1023
|
+
var ta = document.getElementById('skillTextarea');
|
|
1024
|
+
ta.value = content;
|
|
1025
|
+
document.getElementById('skillCharCount').textContent = content.length + ' chars';
|
|
1026
|
+
} catch(e) { console.warn('Skills load failed:', e); }
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async function saveCurrentSkill() {
|
|
1030
|
+
var role = document.getElementById('skillRoleSelect').value;
|
|
1031
|
+
var content = document.getElementById('skillTextarea').value;
|
|
1032
|
+
try {
|
|
1033
|
+
await fetch('/api/skills', {
|
|
1034
|
+
method: 'POST',
|
|
1035
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1036
|
+
body: JSON.stringify({ role: role, content: content })
|
|
1037
|
+
});
|
|
1038
|
+
_skillsCache[role] = content;
|
|
1039
|
+
showFixedToast('✅ Skill saved for ' + role, true);
|
|
1040
|
+
} catch(e) { showFixedToast('❌ Save failed', false); }
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function clearCurrentSkill() {
|
|
1044
|
+
var role = document.getElementById('skillRoleSelect').value;
|
|
1045
|
+
try {
|
|
1046
|
+
await fetch('/api/skills/' + role, { method: 'DELETE' });
|
|
1047
|
+
document.getElementById('skillTextarea').value = '';
|
|
1048
|
+
document.getElementById('skillCharCount').textContent = '0 chars';
|
|
1049
|
+
_skillsCache[role] = '';
|
|
1050
|
+
showFixedToast('🗑️ Skill cleared for ' + role, true);
|
|
1051
|
+
} catch(e) { showFixedToast('❌ Clear failed', false); }
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function handleSkillUpload(input) {
|
|
1055
|
+
var file = input.files[0];
|
|
1056
|
+
if (!file) return;
|
|
1057
|
+
var reader = new FileReader();
|
|
1058
|
+
reader.onload = async function(e) {
|
|
1059
|
+
var content = e.target.result;
|
|
1060
|
+
var ta = document.getElementById('skillTextarea');
|
|
1061
|
+
ta.value = content;
|
|
1062
|
+
document.getElementById('skillCharCount').textContent = content.length + ' chars';
|
|
1063
|
+
// Auto-save after upload
|
|
1064
|
+
await saveCurrentSkill();
|
|
1065
|
+
};
|
|
1066
|
+
reader.readAsText(file);
|
|
1067
|
+
input.value = ''; // reset so same file can be re-uploaded
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
|
|
835
1071
|
async function loadSettings() {
|
|
836
1072
|
try {
|
|
837
1073
|
var res = await fetch('/api/settings');
|
|
@@ -855,6 +1091,9 @@ export function renderDashboardHTML(version) {
|
|
|
855
1091
|
if (s.PRISM_STORAGE) {
|
|
856
1092
|
document.getElementById('storageBackendSelect').value = s.PRISM_STORAGE;
|
|
857
1093
|
}
|
|
1094
|
+
// Agent Identity
|
|
1095
|
+
if (s.default_role) document.getElementById('select-default-role').value = s.default_role;
|
|
1096
|
+
if (s.agent_name) document.getElementById('input-agent-name').value = s.agent_name;
|
|
858
1097
|
} catch(e) { console.warn('Settings load failed:', e); }
|
|
859
1098
|
}
|
|
860
1099
|
|
|
@@ -879,8 +1118,9 @@ export function renderDashboardHTML(version) {
|
|
|
879
1118
|
headers: { 'Content-Type': 'application/json' },
|
|
880
1119
|
body: JSON.stringify({ key: key, value: value })
|
|
881
1120
|
});
|
|
882
|
-
// Apply theme instantly on change
|
|
883
1121
|
if (key === 'dashboard_theme') applyTheme(value);
|
|
1122
|
+
// Refresh identity chip if role or name changed
|
|
1123
|
+
if (key === 'default_role' || key === 'agent_name') loadIdentityChip();
|
|
884
1124
|
showToast('Saved ✓');
|
|
885
1125
|
} catch(e) { console.error('Setting save failed:', e); }
|
|
886
1126
|
}
|
|
@@ -939,6 +1179,55 @@ export function renderDashboardHTML(version) {
|
|
|
939
1179
|
if (mins < 60) return mins + 'm ago';
|
|
940
1180
|
return Math.floor(mins/60) + 'h ago';
|
|
941
1181
|
}
|
|
1182
|
+
|
|
1183
|
+
// ─── Brain Health Cleanup (v3.1) ───
|
|
1184
|
+
async function cleanupIssues() {
|
|
1185
|
+
var btn = document.getElementById('cleanupBtn');
|
|
1186
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Cleaning...'; }
|
|
1187
|
+
try {
|
|
1188
|
+
var res = await fetch('/api/health/cleanup', { method: 'POST' });
|
|
1189
|
+
var data = await res.json();
|
|
1190
|
+
showFixedToast(data.message || (data.ok ? 'Cleanup complete.' : 'Cleanup failed.'), data.ok);
|
|
1191
|
+
// Re-run health check to refresh the card
|
|
1192
|
+
setTimeout(async function() {
|
|
1193
|
+
try {
|
|
1194
|
+
var healthRes = await fetch('/api/health');
|
|
1195
|
+
var healthData = await healthRes.json();
|
|
1196
|
+
var healthDot = document.getElementById('healthDot');
|
|
1197
|
+
var healthLabel = document.getElementById('healthLabel');
|
|
1198
|
+
var healthSummary = document.getElementById('healthSummary');
|
|
1199
|
+
var healthIssues = document.getElementById('healthIssues');
|
|
1200
|
+
var cleanupBtn = document.getElementById('cleanupBtn');
|
|
1201
|
+
var statusMap = { healthy: '✅ Healthy', degraded: '⚠️ Degraded', unhealthy: '🔴 Unhealthy' };
|
|
1202
|
+
healthDot.className = 'health-dot ' + (healthData.status || 'unknown');
|
|
1203
|
+
healthLabel.textContent = statusMap[healthData.status] || '❓ Unknown';
|
|
1204
|
+
var t = healthData.totals || {};
|
|
1205
|
+
healthSummary.textContent = (t.activeEntries || 0) + ' entries · ' + (t.handoffs || 0) + ' handoffs · ' + (t.rollups || 0) + ' rollups';
|
|
1206
|
+
var issues = healthData.issues || [];
|
|
1207
|
+
if (issues.length > 0) {
|
|
1208
|
+
var sevIcons = { error: '🔴', warning: '🟡', info: '🔵' };
|
|
1209
|
+
healthIssues.innerHTML = issues.map(function(i) {
|
|
1210
|
+
return '<div class="issue-row"><span>' + (sevIcons[i.severity] || '❓') + '</span><span>' + escapeHtml(i.message) + '</span></div>';
|
|
1211
|
+
}).join('');
|
|
1212
|
+
if (cleanupBtn) { cleanupBtn.disabled = false; cleanupBtn.textContent = '🧹 Fix Issues'; cleanupBtn.style.display = 'inline-block'; }
|
|
1213
|
+
} else {
|
|
1214
|
+
healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">🎉 No issues found</div>';
|
|
1215
|
+
if (cleanupBtn) cleanupBtn.style.display = 'none';
|
|
1216
|
+
}
|
|
1217
|
+
} catch(e) {}
|
|
1218
|
+
}, 400);
|
|
1219
|
+
} catch(e) {
|
|
1220
|
+
showFixedToast('Cleanup request failed.', false);
|
|
1221
|
+
if (btn) { btn.disabled = false; btn.textContent = '🧹 Fix Issues'; }
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function showFixedToast(msg, ok) {
|
|
1226
|
+
var t = document.getElementById('fixedToast');
|
|
1227
|
+
t.textContent = (ok === false ? '❌ ' : '✅ ') + msg;
|
|
1228
|
+
t.classList.add('show');
|
|
1229
|
+
setTimeout(function() { t.classList.remove('show'); }, 3500);
|
|
1230
|
+
}
|
|
942
1231
|
</script>
|
|
943
1232
|
</body>
|
|
944
1233
|
</html>`;
|
package/dist/server.js
CHANGED
|
@@ -348,6 +348,8 @@ export function createServer() {
|
|
|
348
348
|
}
|
|
349
349
|
});
|
|
350
350
|
// Read a specific project's handoff as a resource
|
|
351
|
+
// v3.1 FIX: Returns formatted text/plain (same layout as session_load_context)
|
|
352
|
+
// so MCP clients render it as readable text instead of a raw JSON blob.
|
|
351
353
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
352
354
|
const uri = request.params.uri;
|
|
353
355
|
const match = uri.match(/^memory:\/\/(.+)\/handoff$/);
|
|
@@ -356,20 +358,49 @@ export function createServer() {
|
|
|
356
358
|
}
|
|
357
359
|
const project = decodeURIComponent(match[1]);
|
|
358
360
|
try {
|
|
359
|
-
// v2.3.6 FIX: Use storage abstraction instead of direct supabaseRpc
|
|
360
361
|
const storage = await getStorage();
|
|
361
362
|
const data = await storage.loadContext(project, "standard", PRISM_USER_ID);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
363
|
+
if (!data) {
|
|
364
|
+
return {
|
|
365
|
+
contents: [{
|
|
366
|
+
uri,
|
|
367
|
+
mimeType: "text/plain",
|
|
368
|
+
text: `No session context found for project "${project}".\nThis project has no previous session history. Starting fresh.`,
|
|
369
|
+
}],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
// Format identically to sessionLoadContextHandler so the resource
|
|
373
|
+
// renders as readable text rather than a raw JSON dump.
|
|
374
|
+
const d = data;
|
|
375
|
+
let formattedContext = "";
|
|
376
|
+
if (d.last_summary)
|
|
377
|
+
formattedContext += `📝 Last Summary: ${d.last_summary}\n`;
|
|
378
|
+
if (d.active_branch)
|
|
379
|
+
formattedContext += `🌿 Active Branch: ${d.active_branch}\n`;
|
|
380
|
+
if (d.key_context)
|
|
381
|
+
formattedContext += `💡 Key Context: ${d.key_context}\n`;
|
|
382
|
+
if (d.pending_todo?.length) {
|
|
383
|
+
formattedContext += `\n✅ Open TODOs:\n` + d.pending_todo.map((t) => ` - ${t}`).join("\n") + `\n`;
|
|
384
|
+
}
|
|
385
|
+
if (d.active_decisions?.length) {
|
|
386
|
+
formattedContext += `\n⚖️ Active Decisions:\n` + d.active_decisions.map((dec) => ` - ${dec}`).join("\n") + `\n`;
|
|
367
387
|
}
|
|
388
|
+
if (d.keywords?.length) {
|
|
389
|
+
formattedContext += `\n🔑 Keywords: ${d.keywords.join(", ")}\n`;
|
|
390
|
+
}
|
|
391
|
+
if (d.recent_sessions?.length) {
|
|
392
|
+
formattedContext += `\n⏳ Recent Sessions:\n` + d.recent_sessions.map((s) => ` [${s.session_date?.split("T")[0]}] ${s.summary}`).join("\n") + `\n`;
|
|
393
|
+
}
|
|
394
|
+
const version = d.version;
|
|
395
|
+
const versionNote = version
|
|
396
|
+
? `\n\n🔑 Session version: ${version}. Pass expected_version: ${version} when saving handoff.\n` +
|
|
397
|
+
`_occ_instruction: When saving handoff state, you MUST pass expected_version: ${version} to prevent state collisions with other sessions.`
|
|
398
|
+
: "";
|
|
368
399
|
return {
|
|
369
400
|
contents: [{
|
|
370
|
-
uri
|
|
371
|
-
mimeType: "
|
|
372
|
-
text:
|
|
401
|
+
uri,
|
|
402
|
+
mimeType: "text/plain",
|
|
403
|
+
text: `📋 Session context for "${project}" (standard):\n\n${formattedContext.trim()}${versionNote}`,
|
|
373
404
|
}],
|
|
374
405
|
};
|
|
375
406
|
}
|
|
@@ -378,7 +409,7 @@ export function createServer() {
|
|
|
378
409
|
return {
|
|
379
410
|
isError: true,
|
|
380
411
|
contents: [{
|
|
381
|
-
uri
|
|
412
|
+
uri,
|
|
382
413
|
mimeType: "text/plain",
|
|
383
414
|
text: `Error reading resource: ${error instanceof Error ? error.message : String(error)}`,
|
|
384
415
|
}],
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -20,6 +20,7 @@ import * as fs from "fs";
|
|
|
20
20
|
import * as path from "path";
|
|
21
21
|
import * as os from "os";
|
|
22
22
|
import { randomUUID } from "crypto";
|
|
23
|
+
import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
|
|
23
24
|
import { debugLog } from "../utils/logger.js";
|
|
24
25
|
export class SqliteStorage {
|
|
25
26
|
db;
|
|
@@ -1130,4 +1131,15 @@ export class SqliteStorage {
|
|
|
1130
1131
|
});
|
|
1131
1132
|
debugLog(`[SqliteStorage] Agent deregistered: ${project}/${role}`);
|
|
1132
1133
|
}
|
|
1134
|
+
// ─── System Settings (v3.0 Dashboard) — proxy to configStorage ───
|
|
1135
|
+
async getSetting(key) {
|
|
1136
|
+
const val = await cfgGet(key, "");
|
|
1137
|
+
return val === "" ? null : val;
|
|
1138
|
+
}
|
|
1139
|
+
async setSetting(key, value) {
|
|
1140
|
+
await cfgSet(key, value);
|
|
1141
|
+
}
|
|
1142
|
+
async getAllSettings() {
|
|
1143
|
+
return cfgGetAll();
|
|
1144
|
+
}
|
|
1133
1145
|
}
|
package/dist/storage/supabase.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { supabasePost, supabaseGet, supabaseRpc, supabasePatch, supabaseDelete, } from "../utils/supabaseApi.js";
|
|
16
16
|
import { debugLog } from "../utils/logger.js";
|
|
17
|
+
import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
|
|
17
18
|
export class SupabaseStorage {
|
|
18
19
|
// ─── Lifecycle ─────────────────────────────────────────────
|
|
19
20
|
async initialize() {
|
|
@@ -284,4 +285,15 @@ export class SupabaseStorage {
|
|
|
284
285
|
role: `eq.${role}`,
|
|
285
286
|
});
|
|
286
287
|
}
|
|
288
|
+
// ─── System Settings (v3.0 Dashboard) — proxy to configStorage ───
|
|
289
|
+
async getSetting(key) {
|
|
290
|
+
const val = await cfgGet(key, "");
|
|
291
|
+
return val === "" ? null : val;
|
|
292
|
+
}
|
|
293
|
+
async setSetting(key, value) {
|
|
294
|
+
await cfgSet(key, value);
|
|
295
|
+
}
|
|
296
|
+
async getAllSettings() {
|
|
297
|
+
return cfgGetAll();
|
|
298
|
+
}
|
|
287
299
|
}
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
import { getStorage } from "../storage/index.js";
|
|
8
8
|
import { PRISM_USER_ID } from "../config.js";
|
|
9
9
|
import { getRoleIcon } from "./agentRegistryDefinitions.js";
|
|
10
|
+
import { getSetting } from "../storage/configStorage.js";
|
|
10
11
|
// ─── Type Guards ─────────────────────────────────────────────
|
|
11
12
|
function isAgentRegisterArgs(args) {
|
|
12
|
-
return typeof args.project === "string"
|
|
13
|
+
return typeof args.project === "string";
|
|
13
14
|
}
|
|
14
15
|
function isAgentHeartbeatArgs(args) {
|
|
15
|
-
return typeof args.project === "string"
|
|
16
|
+
return typeof args.project === "string";
|
|
16
17
|
}
|
|
17
18
|
function isAgentListTeamArgs(args) {
|
|
18
19
|
return typeof args.project === "string";
|
|
@@ -21,27 +22,30 @@ function isAgentListTeamArgs(args) {
|
|
|
21
22
|
export async function agentRegisterHandler(args) {
|
|
22
23
|
if (!isAgentRegisterArgs(args)) {
|
|
23
24
|
return {
|
|
24
|
-
content: [{ type: "text", text: "Missing required: project
|
|
25
|
+
content: [{ type: "text", text: "Missing required: project" }],
|
|
25
26
|
isError: true,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
29
|
+
// Fall back to dashboard-configured identity if not passed explicitly
|
|
30
|
+
const effectiveRole = args.role || await getSetting("default_role", "global");
|
|
31
|
+
const effectiveName = args.agent_name || await getSetting("agent_name", "") || null;
|
|
28
32
|
const storage = await getStorage();
|
|
29
33
|
const result = await storage.registerAgent({
|
|
30
34
|
project: args.project,
|
|
31
35
|
user_id: PRISM_USER_ID,
|
|
32
|
-
role:
|
|
33
|
-
agent_name:
|
|
36
|
+
role: effectiveRole,
|
|
37
|
+
agent_name: effectiveName,
|
|
34
38
|
status: "active",
|
|
35
39
|
current_task: args.current_task || null,
|
|
36
40
|
});
|
|
37
|
-
const icon = getRoleIcon(
|
|
41
|
+
const icon = getRoleIcon(effectiveRole);
|
|
38
42
|
return {
|
|
39
43
|
content: [{
|
|
40
44
|
type: "text",
|
|
41
45
|
text: `${icon} **Agent Registered**\n\n` +
|
|
42
46
|
`- **Project:** ${args.project}\n` +
|
|
43
|
-
`- **Role:** ${
|
|
44
|
-
(
|
|
47
|
+
`- **Role:** ${effectiveRole}\n` +
|
|
48
|
+
(effectiveName ? `- **Name:** ${effectiveName}\n` : "") +
|
|
45
49
|
(args.current_task ? `- **Task:** ${args.current_task}\n` : "") +
|
|
46
50
|
`\nOther agents will see you when they call \`agent_list_team\` or \`session_load_context\`.`,
|
|
47
51
|
}],
|
|
@@ -50,16 +54,17 @@ export async function agentRegisterHandler(args) {
|
|
|
50
54
|
export async function agentHeartbeatHandler(args) {
|
|
51
55
|
if (!isAgentHeartbeatArgs(args)) {
|
|
52
56
|
return {
|
|
53
|
-
content: [{ type: "text", text: "Missing required: project
|
|
57
|
+
content: [{ type: "text", text: "Missing required: project" }],
|
|
54
58
|
isError: true,
|
|
55
59
|
};
|
|
56
60
|
}
|
|
61
|
+
const effectiveRole = args.role || await getSetting("default_role", "global");
|
|
57
62
|
const storage = await getStorage();
|
|
58
|
-
await storage.heartbeatAgent(args.project, PRISM_USER_ID,
|
|
63
|
+
await storage.heartbeatAgent(args.project, PRISM_USER_ID, effectiveRole, args.current_task);
|
|
59
64
|
return {
|
|
60
65
|
content: [{
|
|
61
66
|
type: "text",
|
|
62
|
-
text: `💓 Heartbeat updated for **${
|
|
67
|
+
text: `💓 Heartbeat updated for **${effectiveRole}** on \`${args.project}\`.` +
|
|
63
68
|
(args.current_task ? ` Task: ${args.current_task}` : ""),
|
|
64
69
|
}],
|
|
65
70
|
};
|
|
@@ -20,6 +20,7 @@ import { getStorage } from "../storage/index.js";
|
|
|
20
20
|
import { toKeywordArray } from "../utils/keywordExtractor.js";
|
|
21
21
|
import { generateEmbedding } from "../utils/embeddingApi.js";
|
|
22
22
|
import { getCurrentGitState, getGitDrift } from "../utils/git.js";
|
|
23
|
+
import { getSetting } from "../storage/configStorage.js";
|
|
23
24
|
// ─── Phase 1: Explainability & Memory Lineage ────────────────
|
|
24
25
|
// These utilities provide structured tracing metadata for search operations.
|
|
25
26
|
// When `enable_trace: true` is passed to session_search_memory or knowledge_search,
|
|
@@ -54,6 +55,7 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
54
55
|
const keywords = toKeywordArray(combinedText);
|
|
55
56
|
debugLog(`[session_save_ledger] Extracted ${keywords.length} keywords: ${keywords.slice(0, 5).join(", ")}...`);
|
|
56
57
|
// Save via storage backend
|
|
58
|
+
const effectiveRole = role || await getSetting("default_role", "global");
|
|
57
59
|
const result = await storage.saveLedger({
|
|
58
60
|
project,
|
|
59
61
|
conversation_id,
|
|
@@ -63,7 +65,7 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
63
65
|
files_changed: files_changed || [],
|
|
64
66
|
decisions: decisions || [],
|
|
65
67
|
keywords,
|
|
66
|
-
role:
|
|
68
|
+
role: effectiveRole, // v3.0: Hivemind role scoping (dashboard fallback)
|
|
67
69
|
});
|
|
68
70
|
// ─── Fire-and-forget embedding generation ───
|
|
69
71
|
if (GOOGLE_API_KEY && result) {
|
|
@@ -125,6 +127,7 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
125
127
|
debugLog(`[session_save_handoff] Git state captured: branch=${gitState.branch}, sha=${gitState.commitSha?.substring(0, 8)}`);
|
|
126
128
|
}
|
|
127
129
|
// Save via storage backend (OCC-aware)
|
|
130
|
+
const effectiveRole = role || await getSetting("default_role", "global");
|
|
128
131
|
const data = await storage.saveHandoff({
|
|
129
132
|
project,
|
|
130
133
|
user_id: PRISM_USER_ID,
|
|
@@ -135,7 +138,7 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
135
138
|
key_context: key_context ?? null,
|
|
136
139
|
active_branch: active_branch ?? null,
|
|
137
140
|
metadata,
|
|
138
|
-
role:
|
|
141
|
+
role: effectiveRole, // v3.0: Hivemind role scoping (dashboard fallback)
|
|
139
142
|
}, expected_version ?? null);
|
|
140
143
|
// ─── Handle version conflict ───
|
|
141
144
|
if (data.status === "conflict") {
|
|
@@ -321,6 +324,7 @@ export async function sessionLoadContextHandler(args) {
|
|
|
321
324
|
throw new Error("Invalid arguments for session_load_context");
|
|
322
325
|
}
|
|
323
326
|
const { project, level = "standard", role } = args;
|
|
327
|
+
const agentName = await getSetting("agent_name", "");
|
|
324
328
|
const validLevels = ["quick", "standard", "deep"];
|
|
325
329
|
if (!validLevels.includes(level)) {
|
|
326
330
|
return {
|
|
@@ -333,7 +337,8 @@ export async function sessionLoadContextHandler(args) {
|
|
|
333
337
|
}
|
|
334
338
|
debugLog(`[session_load_context] Loading ${level} context for project="${project}"`);
|
|
335
339
|
const storage = await getStorage();
|
|
336
|
-
const
|
|
340
|
+
const effectiveRole = role || await getSetting("default_role", "") || undefined;
|
|
341
|
+
const data = await storage.loadContext(project, level, PRISM_USER_ID, effectiveRole); // v3.0: role with dashboard fallback
|
|
337
342
|
if (!data) {
|
|
338
343
|
return {
|
|
339
344
|
content: [{
|
|
@@ -471,10 +476,32 @@ export async function sessionLoadContextHandler(args) {
|
|
|
471
476
|
if (d.session_history?.length) {
|
|
472
477
|
formattedContext += `\n📂 Session History (${d.session_history.length} entries):\n` + d.session_history.map((s) => ` [${s.session_date?.split("T")[0]}] ${s.summary}`).join("\n") + `\n`;
|
|
473
478
|
}
|
|
479
|
+
// ─── Role-Scoped Skill Injection ─────────────────────────────
|
|
480
|
+
// If the active role has a skill document stored, append it so the
|
|
481
|
+
// agent loads its rules/conventions automatically at session start.
|
|
482
|
+
let skillBlock = "";
|
|
483
|
+
let skillLoaded = false;
|
|
484
|
+
if (effectiveRole) {
|
|
485
|
+
const skillContent = await getSetting(`skill:${effectiveRole}`, "");
|
|
486
|
+
if (skillContent && skillContent.trim()) {
|
|
487
|
+
skillBlock = `\n\n[📜 ROLE SKILL: ${effectiveRole}]\n${skillContent.trim()}`;
|
|
488
|
+
skillLoaded = true;
|
|
489
|
+
debugLog(`[session_load_context] Injecting skill for role="${effectiveRole}" (${skillContent.length} chars)`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// ─── Agent Greeting Block ────────────────────────────────────
|
|
493
|
+
// Shows agent identity (name + role) and skill status after briefing.
|
|
494
|
+
let greetingBlock = "";
|
|
495
|
+
if (agentName || effectiveRole) {
|
|
496
|
+
const namePart = agentName ? `👋 **${agentName}**` : `👋 **Agent**`;
|
|
497
|
+
const rolePart = effectiveRole ? ` · Role: \`${effectiveRole}\`` : "";
|
|
498
|
+
const skillPart = skillLoaded ? ` · 📜 \`${effectiveRole}\` skill loaded` : (effectiveRole ? " · 📜 No skill configured" : "");
|
|
499
|
+
greetingBlock = `\n\n[👤 AGENT IDENTITY]\n${namePart}${rolePart}${skillPart}`;
|
|
500
|
+
}
|
|
474
501
|
return {
|
|
475
502
|
content: [{
|
|
476
503
|
type: "text",
|
|
477
|
-
text: `📋 Session context for "${project}" (${level}):\n\n${formattedContext.trim()}${driftReport}${briefingBlock}${visualMemoryBlock}${versionNote}`,
|
|
504
|
+
text: `📋 Session context for "${project}" (${level}):\n\n${formattedContext.trim()}${driftReport}${briefingBlock}${greetingBlock}${visualMemoryBlock}${skillBlock}${versionNote}`,
|
|
478
505
|
}],
|
|
479
506
|
isError: false,
|
|
480
507
|
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-mcp",
|
|
5
5
|
"description": "The Mind Palace for AI Agents — local-first MCP server with persistent memory (SQLite/Supabase), visual dashboard, time travel, multi-agent sync, Morning Briefings, reality drift detection, code mode templates, semantic vector search, and Brave Search + Gemini analysis. Zero-config local mode.",
|
|
6
6
|
"module": "index.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "dist/server.js",
|
|
9
9
|
"bin": {
|
|
10
|
-
"prism-mcp-server": "
|
|
10
|
+
"prism-mcp-server": "dist/server.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist"
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"homepage": "https://github.com/dcostenco/prism-mcp",
|
|
69
69
|
"repository": {
|
|
70
70
|
"type": "git",
|
|
71
|
-
"url": "https://github.com/dcostenco/prism-mcp.git"
|
|
71
|
+
"url": "git+https://github.com/dcostenco/prism-mcp.git"
|
|
72
72
|
},
|
|
73
73
|
"author": "Dmitri Costenco",
|
|
74
74
|
"license": "MIT",
|