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 +58 -3
- package/dist/config.js +8 -0
- package/dist/dashboard/server.js +96 -32
- package/dist/dashboard/ui.js +322 -1
- package/dist/server.js +35 -28
- package/dist/storage/configStorage.js +73 -0
- package/dist/storage/index.js +8 -5
- package/dist/storage/sqlite.js +237 -20
- package/dist/storage/supabase.js +84 -106
- package/dist/tools/agentRegistryDefinitions.js +104 -0
- package/dist/tools/agentRegistryHandlers.js +114 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/sessionMemoryDefinitions.js +12 -0
- package/dist/tools/sessionMemoryHandlers.js +7 -4
- package/dist/utils/googleAi.js +1 -1
- package/package.json +8 -3
package/dist/storage/supabase.js
CHANGED
|
@@ -17,30 +17,25 @@ import { debugLog } from "../utils/logger.js";
|
|
|
17
17
|
export class SupabaseStorage {
|
|
18
18
|
// ─── Lifecycle ─────────────────────────────────────────────
|
|
19
19
|
async initialize() {
|
|
20
|
-
// Supabase is always ready — connection is stateless (REST API).
|
|
21
|
-
// The SUPABASE_URL and SUPABASE_KEY are validated at import time
|
|
22
|
-
// by supabaseApi.ts's guard clause.
|
|
23
20
|
debugLog("[SupabaseStorage] Initialized (REST API, stateless)");
|
|
24
21
|
}
|
|
25
22
|
async close() {
|
|
26
|
-
// No-op for Supabase — connections are stateless.
|
|
27
23
|
debugLog("[SupabaseStorage] Closed (no-op for REST)");
|
|
28
24
|
}
|
|
29
25
|
// ─── Ledger Operations ─────────────────────────────────────
|
|
30
26
|
async saveLedger(entry) {
|
|
31
|
-
// Direct mapping from sessionSaveLedgerHandler line 95
|
|
32
27
|
const record = {
|
|
33
28
|
project: entry.project,
|
|
34
29
|
conversation_id: entry.conversation_id,
|
|
35
30
|
summary: entry.summary,
|
|
36
31
|
user_id: entry.user_id,
|
|
32
|
+
role: entry.role || "global", // v3.0: include role
|
|
37
33
|
todos: entry.todos || [],
|
|
38
34
|
files_changed: entry.files_changed || [],
|
|
39
35
|
decisions: entry.decisions || [],
|
|
40
36
|
keywords: entry.keywords || [],
|
|
41
37
|
...(entry.is_rollup !== undefined && { is_rollup: entry.is_rollup }),
|
|
42
38
|
...(entry.rollup_count !== undefined && { rollup_count: entry.rollup_count }),
|
|
43
|
-
// Compaction handler also sets title, agent_name for rollup entries
|
|
44
39
|
...(entry.is_rollup && {
|
|
45
40
|
title: `Session Rollup (${entry.rollup_count || 0} entries)`,
|
|
46
41
|
agent_name: "prism-compactor",
|
|
@@ -49,63 +44,36 @@ export class SupabaseStorage {
|
|
|
49
44
|
return supabasePost("session_ledger", record);
|
|
50
45
|
}
|
|
51
46
|
async patchLedger(id, data) {
|
|
52
|
-
// Direct mapping from sessionSaveLedgerHandler line 115 (embedding patch)
|
|
53
|
-
// and compactionHandler line 292 (archive patch)
|
|
54
47
|
await supabasePatch("session_ledger", data, { id: `eq.${id}` });
|
|
55
48
|
}
|
|
56
49
|
async getLedgerEntries(params) {
|
|
57
|
-
// Direct mapping from:
|
|
58
|
-
// - compactionHandler line 143 (count entries for project)
|
|
59
|
-
// - compactionHandler line 211 (fetch oldest entries)
|
|
60
|
-
// - backfillEmbeddingsHandler line 700 (find missing embeddings)
|
|
61
|
-
// - knowledgeForgetHandler line 479 (dry run count)
|
|
62
50
|
const result = await supabaseGet("session_ledger", params);
|
|
63
51
|
return Array.isArray(result) ? result : [];
|
|
64
52
|
}
|
|
65
53
|
async deleteLedger(params) {
|
|
66
|
-
// Direct mapping from knowledgeForgetHandler line 482
|
|
67
54
|
const result = await supabaseDelete("session_ledger", params);
|
|
68
55
|
return Array.isArray(result) ? result : [];
|
|
69
56
|
}
|
|
70
57
|
// ─── Phase 2: GDPR-Compliant Memory Deletion ──────────────
|
|
71
|
-
//
|
|
72
|
-
// These methods are SURGICAL — they operate on a single entry by ID.
|
|
73
|
-
// They MUST verify user_id ownership to prevent cross-user deletion.
|
|
74
|
-
//
|
|
75
|
-
// softDeleteLedger: Sets deleted_at + deleted_reason. Entry stays in
|
|
76
|
-
// DB for audit trail. Supabase RPCs and TypeScript queries filter
|
|
77
|
-
// it out via "WHERE deleted_at IS NULL". Reversible.
|
|
78
|
-
//
|
|
79
|
-
// hardDeleteLedger: Physical DELETE. Irreversible. For GDPR Article 17
|
|
80
|
-
// "right to erasure" when the audit trail must also be removed.
|
|
81
58
|
async softDeleteLedger(id, userId, reason) {
|
|
82
|
-
// PATCH (not DELETE): sets tombstone fields while preserving the row.
|
|
83
|
-
// The deleted_at timestamp is set server-side for consistency.
|
|
84
|
-
// deleted_reason captures the GDPR justification (e.g., "User requested",
|
|
85
|
-
// "Data retention policy", "GDPR Article 17 request").
|
|
86
59
|
await supabasePatch("session_ledger", {
|
|
87
60
|
deleted_at: new Date().toISOString(),
|
|
88
61
|
deleted_reason: reason || null,
|
|
89
62
|
}, {
|
|
90
63
|
id: `eq.${id}`,
|
|
91
|
-
user_id: `eq.${userId}`,
|
|
64
|
+
user_id: `eq.${userId}`,
|
|
92
65
|
});
|
|
93
66
|
debugLog(`[SupabaseStorage] Soft-deleted ledger entry ${id} (reason: ${reason || "none"})`);
|
|
94
67
|
}
|
|
95
68
|
async hardDeleteLedger(id, userId) {
|
|
96
|
-
// Physical DELETE — row is permanently removed from the database.
|
|
97
|
-
// This is irreversible. The FTS5 index (if any) is cleaned up by
|
|
98
|
-
// Supabase's built-in trigger handling.
|
|
99
69
|
await supabaseDelete("session_ledger", {
|
|
100
70
|
id: `eq.${id}`,
|
|
101
|
-
user_id: `eq.${userId}`,
|
|
71
|
+
user_id: `eq.${userId}`,
|
|
102
72
|
});
|
|
103
73
|
debugLog(`[SupabaseStorage] Hard-deleted ledger entry ${id}`);
|
|
104
74
|
}
|
|
105
75
|
// ─── Handoff Operations ────────────────────────────────────
|
|
106
76
|
async saveHandoff(handoff, expectedVersion) {
|
|
107
|
-
// Direct mapping from sessionSaveHandoffHandler line 214
|
|
108
|
-
// Calls the save_handoff_with_version RPC for OCC
|
|
109
77
|
const result = await supabaseRpc("save_handoff_with_version", {
|
|
110
78
|
p_project: handoff.project,
|
|
111
79
|
p_expected_version: expectedVersion ?? null,
|
|
@@ -116,6 +84,7 @@ export class SupabaseStorage {
|
|
|
116
84
|
p_key_context: handoff.key_context ?? null,
|
|
117
85
|
p_active_branch: handoff.active_branch ?? null,
|
|
118
86
|
p_user_id: handoff.user_id,
|
|
87
|
+
p_role: handoff.role || "global", // v3.0: pass role to RPC
|
|
119
88
|
});
|
|
120
89
|
const data = Array.isArray(result) ? result[0] : result;
|
|
121
90
|
if (data?.status === "conflict") {
|
|
@@ -130,25 +99,24 @@ export class SupabaseStorage {
|
|
|
130
99
|
};
|
|
131
100
|
}
|
|
132
101
|
async deleteHandoff(project, userId) {
|
|
133
|
-
// Direct mapping from knowledgeForgetHandler line 486
|
|
134
102
|
await supabaseDelete("session_handoffs", {
|
|
135
103
|
project: `eq.${project}`,
|
|
136
104
|
user_id: `eq.${userId}`,
|
|
137
105
|
});
|
|
138
106
|
}
|
|
139
|
-
async loadContext(project, level, userId
|
|
140
|
-
|
|
107
|
+
async loadContext(project, level, userId, role // v3.0: optional role filter
|
|
108
|
+
) {
|
|
141
109
|
const result = await supabaseRpc("get_session_context", {
|
|
142
110
|
p_project: project,
|
|
143
111
|
p_level: level,
|
|
144
112
|
p_user_id: userId,
|
|
113
|
+
p_role: role || "global", // v3.0: pass role to RPC
|
|
145
114
|
});
|
|
146
115
|
const data = Array.isArray(result) ? result[0] : result;
|
|
147
116
|
return data ?? null;
|
|
148
117
|
}
|
|
149
118
|
// ─── Search Operations ─────────────────────────────────────
|
|
150
119
|
async searchKnowledge(params) {
|
|
151
|
-
// Direct mapping from knowledgeSearchHandler line 388
|
|
152
120
|
const result = await supabaseRpc("search_knowledge", {
|
|
153
121
|
p_project: params.project || null,
|
|
154
122
|
p_keywords: params.keywords,
|
|
@@ -164,7 +132,6 @@ export class SupabaseStorage {
|
|
|
164
132
|
return data;
|
|
165
133
|
}
|
|
166
134
|
async searchMemory(params) {
|
|
167
|
-
// Direct mapping from sessionSearchMemoryHandler line 583
|
|
168
135
|
const result = await supabaseRpc("semantic_search_ledger", {
|
|
169
136
|
p_query_embedding: params.queryEmbedding,
|
|
170
137
|
p_project: params.project || null,
|
|
@@ -176,7 +143,6 @@ export class SupabaseStorage {
|
|
|
176
143
|
}
|
|
177
144
|
// ─── Compaction ────────────────────────────────────────────
|
|
178
145
|
async getCompactionCandidates(threshold, keepRecent, userId) {
|
|
179
|
-
// Direct mapping from compactionHandler line 165
|
|
180
146
|
const result = await supabaseRpc("get_compaction_candidates", {
|
|
181
147
|
p_threshold: threshold,
|
|
182
148
|
p_keep_recent: keepRecent,
|
|
@@ -210,100 +176,112 @@ export class SupabaseStorage {
|
|
|
210
176
|
order: "project.asc",
|
|
211
177
|
});
|
|
212
178
|
const rows = Array.isArray(data) ? data : [];
|
|
213
|
-
// Deduplicate on the client side since Supabase doesn't support DISTINCT via REST
|
|
214
179
|
return [...new Set(rows.map((r) => r.project))];
|
|
215
180
|
}
|
|
216
181
|
// ─── v2.2.0 Health Check (fsck) ─────────────────────────────
|
|
217
|
-
/**
|
|
218
|
-
* Gather raw health statistics via Supabase REST API.
|
|
219
|
-
*
|
|
220
|
-
* Supabase REST (PostgREST) doesn't support complex JOINs,
|
|
221
|
-
* so we fetch raw data and let healthCheck.ts do the analysis
|
|
222
|
-
* in pure JS — same approach as SQLite for consistency.
|
|
223
|
-
*/
|
|
224
182
|
async getHealthStats(userId) {
|
|
225
|
-
// ── Check 1: Entries missing embeddings ────────────────────
|
|
226
|
-
// Fetch active entries where embedding column is null.
|
|
227
|
-
// PostgREST filter: archived_at=is.null AND embedding=is.null
|
|
228
183
|
const missingData = await supabaseGet("session_ledger", {
|
|
229
|
-
select: "id",
|
|
230
|
-
user_id: `eq.${userId}`,
|
|
231
|
-
archived_at: "is.null",
|
|
232
|
-
embedding: "is.null",
|
|
184
|
+
select: "id",
|
|
185
|
+
user_id: `eq.${userId}`,
|
|
186
|
+
archived_at: "is.null",
|
|
187
|
+
embedding: "is.null",
|
|
233
188
|
});
|
|
234
|
-
// Count the returned rows (PostgREST returns array)
|
|
235
189
|
const missingEmbeddings = Array.isArray(missingData) ? missingData.length : 0;
|
|
236
|
-
// ── Check 2: All active summaries for JS duplicate detection ─
|
|
237
|
-
// Pull id + project + summary so healthCheck.ts can run
|
|
238
|
-
// Jaccard similarity comparison in-memory.
|
|
239
190
|
const summData = await supabaseGet("session_ledger", {
|
|
240
|
-
select: "id,project,summary",
|
|
241
|
-
user_id: `eq.${userId}`,
|
|
242
|
-
archived_at: "is.null",
|
|
191
|
+
select: "id,project,summary",
|
|
192
|
+
user_id: `eq.${userId}`,
|
|
193
|
+
archived_at: "is.null",
|
|
243
194
|
});
|
|
244
|
-
// Map to typed array for the health check engine
|
|
245
195
|
const activeLedgerSummaries = (Array.isArray(summData) ? summData : []).map((r) => ({
|
|
246
|
-
id: r.id,
|
|
247
|
-
project: r.project,
|
|
248
|
-
summary: r.summary,
|
|
196
|
+
id: r.id,
|
|
197
|
+
project: r.project,
|
|
198
|
+
summary: r.summary,
|
|
249
199
|
}));
|
|
250
|
-
// ── Check 3: Find orphaned handoffs ──────────────────────────
|
|
251
|
-
// Fetch all handoff projects, then all ledger projects.
|
|
252
|
-
// Difference = orphaned handoffs (handoff but no ledger entries).
|
|
253
200
|
const handoffData = await supabaseGet("session_handoffs", {
|
|
254
|
-
select: "project",
|
|
255
|
-
user_id: `eq.${userId}`,
|
|
201
|
+
select: "project",
|
|
202
|
+
user_id: `eq.${userId}`,
|
|
256
203
|
});
|
|
257
|
-
const handoffProjects = new Set(
|
|
258
|
-
(Array.isArray(handoffData) ? handoffData : [])
|
|
204
|
+
const handoffProjects = new Set((Array.isArray(handoffData) ? handoffData : [])
|
|
259
205
|
.map((r) => r.project));
|
|
260
206
|
const ledgerData = await supabaseGet("session_ledger", {
|
|
261
|
-
select: "project",
|
|
262
|
-
user_id: `eq.${userId}`,
|
|
263
|
-
archived_at: "is.null",
|
|
207
|
+
select: "project",
|
|
208
|
+
user_id: `eq.${userId}`,
|
|
209
|
+
archived_at: "is.null",
|
|
264
210
|
});
|
|
265
|
-
const ledgerProjects = new Set(
|
|
266
|
-
(Array.isArray(ledgerData) ? ledgerData : [])
|
|
211
|
+
const ledgerProjects = new Set((Array.isArray(ledgerData) ? ledgerData : [])
|
|
267
212
|
.map((r) => r.project));
|
|
268
|
-
// Orphaned = in handoffs but NOT in ledger
|
|
269
213
|
const orphanedHandoffs = [...handoffProjects]
|
|
270
|
-
.filter(p => !ledgerProjects.has(p))
|
|
271
|
-
.map(project => ({ project }));
|
|
272
|
-
// ── Check 4: Count stale rollups ─────────────────────────────
|
|
273
|
-
// PostgREST can't do self-joins. Fetch rollups and archived
|
|
274
|
-
// entries separately, then compute in JS.
|
|
214
|
+
.filter(p => !ledgerProjects.has(p))
|
|
215
|
+
.map(project => ({ project }));
|
|
275
216
|
const rollupData = await supabaseGet("session_ledger", {
|
|
276
|
-
select: "id,project",
|
|
277
|
-
user_id: `eq.${userId}`,
|
|
278
|
-
is_rollup: "eq.true",
|
|
279
|
-
archived_at: "is.null",
|
|
217
|
+
select: "id,project",
|
|
218
|
+
user_id: `eq.${userId}`,
|
|
219
|
+
is_rollup: "eq.true",
|
|
220
|
+
archived_at: "is.null",
|
|
280
221
|
});
|
|
281
222
|
const archivedData = await supabaseGet("session_ledger", {
|
|
282
|
-
select: "project",
|
|
283
|
-
user_id: `eq.${userId}`,
|
|
284
|
-
"archived_at": "not.is.null",
|
|
223
|
+
select: "project",
|
|
224
|
+
user_id: `eq.${userId}`,
|
|
225
|
+
"archived_at": "not.is.null",
|
|
285
226
|
});
|
|
286
|
-
// Build a set of projects that have archived entries
|
|
287
227
|
const archivedProjects = new Set((Array.isArray(archivedData) ? archivedData : [])
|
|
288
228
|
.map((r) => r.project));
|
|
289
|
-
// Stale = rollup exists but project has no archived originals
|
|
290
229
|
const rollups = Array.isArray(rollupData) ? rollupData : [];
|
|
291
|
-
const staleRollups = rollups.filter((r) => !archivedProjects.has(r.project)
|
|
292
|
-
).length;
|
|
293
|
-
// ── Totals ───────────────────────────────────────────────────
|
|
294
|
-
// Reuse data already fetched above to avoid extra API calls
|
|
230
|
+
const staleRollups = rollups.filter((r) => !archivedProjects.has(r.project)).length;
|
|
295
231
|
const totalActiveEntries = activeLedgerSummaries.length;
|
|
296
232
|
const totalHandoffs = handoffProjects.size;
|
|
297
233
|
const totalRollups = rollups.length;
|
|
298
|
-
// ── Return raw health stats for the JS engine ────────────────
|
|
299
234
|
return {
|
|
300
|
-
missingEmbeddings,
|
|
301
|
-
activeLedgerSummaries,
|
|
302
|
-
orphanedHandoffs,
|
|
303
|
-
staleRollups,
|
|
304
|
-
totalActiveEntries,
|
|
305
|
-
totalHandoffs,
|
|
306
|
-
totalRollups,
|
|
235
|
+
missingEmbeddings,
|
|
236
|
+
activeLedgerSummaries,
|
|
237
|
+
orphanedHandoffs,
|
|
238
|
+
staleRollups,
|
|
239
|
+
totalActiveEntries,
|
|
240
|
+
totalHandoffs,
|
|
241
|
+
totalRollups,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// ─── v3.0: Agent Registry (Hivemind) ───────────────────────
|
|
245
|
+
// Supabase users need to run the 017_agent_hivemind.sql migration
|
|
246
|
+
async registerAgent(entry) {
|
|
247
|
+
const record = {
|
|
248
|
+
project: entry.project,
|
|
249
|
+
user_id: entry.user_id,
|
|
250
|
+
role: entry.role,
|
|
251
|
+
agent_name: entry.agent_name ?? null,
|
|
252
|
+
status: entry.status || "active",
|
|
253
|
+
current_task: entry.current_task ?? null,
|
|
307
254
|
};
|
|
255
|
+
const result = await supabasePost("agent_registry", record);
|
|
256
|
+
const data = Array.isArray(result) ? result[0] : result;
|
|
257
|
+
return { ...entry, id: data?.id, status: entry.status || "active" };
|
|
258
|
+
}
|
|
259
|
+
async heartbeatAgent(project, userId, role, currentTask) {
|
|
260
|
+
const patchData = {
|
|
261
|
+
last_heartbeat: new Date().toISOString(),
|
|
262
|
+
};
|
|
263
|
+
if (currentTask !== undefined) {
|
|
264
|
+
patchData.current_task = currentTask;
|
|
265
|
+
}
|
|
266
|
+
await supabasePatch("agent_registry", patchData, {
|
|
267
|
+
project: `eq.${project}`,
|
|
268
|
+
user_id: `eq.${userId}`,
|
|
269
|
+
role: `eq.${role}`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async listTeam(project, userId, _staleMinutes = 30) {
|
|
273
|
+
const data = await supabaseGet("agent_registry", {
|
|
274
|
+
project: `eq.${project}`,
|
|
275
|
+
user_id: `eq.${userId}`,
|
|
276
|
+
order: "last_heartbeat.desc",
|
|
277
|
+
});
|
|
278
|
+
return (Array.isArray(data) ? data : []);
|
|
279
|
+
}
|
|
280
|
+
async deregisterAgent(project, userId, role) {
|
|
281
|
+
await supabaseDelete("agent_registry", {
|
|
282
|
+
project: `eq.${project}`,
|
|
283
|
+
user_id: `eq.${userId}`,
|
|
284
|
+
role: `eq.${role}`,
|
|
285
|
+
});
|
|
308
286
|
}
|
|
309
287
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Registry Tool Definitions (v3.0 — Agent Hivemind)
|
|
3
|
+
*
|
|
4
|
+
* Three new MCP tools for multi-agent coordination:
|
|
5
|
+
* - agent_register: Register an agent with project + role
|
|
6
|
+
* - agent_heartbeat: Update heartbeat + current task
|
|
7
|
+
* - agent_list_team: List active agents on a project
|
|
8
|
+
*
|
|
9
|
+
* These tools are ONLY registered when PRISM_ENABLE_HIVEMIND=true
|
|
10
|
+
* (see server.ts). This prevents increasing the tool count for
|
|
11
|
+
* users who don't need multi-agent features.
|
|
12
|
+
*/
|
|
13
|
+
// ─── Role Icons (for dashboard + responses) ──────────────────
|
|
14
|
+
export const ROLE_ICONS = {
|
|
15
|
+
dev: "🛠️",
|
|
16
|
+
qa: "🔍",
|
|
17
|
+
pm: "📋",
|
|
18
|
+
lead: "🏗️",
|
|
19
|
+
security: "🔒",
|
|
20
|
+
ux: "🎨",
|
|
21
|
+
cmo: "📢",
|
|
22
|
+
global: "🌐",
|
|
23
|
+
};
|
|
24
|
+
/** Get icon for a role, with fallback for custom roles (Pro-Tip 4) */
|
|
25
|
+
export function getRoleIcon(role) {
|
|
26
|
+
return ROLE_ICONS[role.toLowerCase()] || "🤖";
|
|
27
|
+
}
|
|
28
|
+
// ─── Tool Definitions ────────────────────────────────────────
|
|
29
|
+
export const AGENT_REGISTER_TOOL = {
|
|
30
|
+
name: "agent_register",
|
|
31
|
+
description: "Register this agent with the Hivemind team for a project. " +
|
|
32
|
+
"Announces your role and current task to other agents. " +
|
|
33
|
+
"If already registered, updates the existing entry. " +
|
|
34
|
+
"Other agents will see you when they call agent_list_team or session_load_context.",
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
project: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "Project identifier (e.g., 'prism-mcp').",
|
|
41
|
+
},
|
|
42
|
+
role: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Your agent role. Common roles: 'dev', 'qa', 'pm', 'lead', 'security', 'ux'. " +
|
|
45
|
+
"Custom roles are also supported (e.g., 'translator', 'docs').",
|
|
46
|
+
},
|
|
47
|
+
agent_name: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Optional human-readable name for this agent (e.g., 'Backend Dev #1').",
|
|
50
|
+
},
|
|
51
|
+
current_task: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "Optional description of what you're currently working on.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ["project", "role"],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
export const AGENT_HEARTBEAT_TOOL = {
|
|
60
|
+
name: "agent_heartbeat",
|
|
61
|
+
description: "Update your heartbeat and optionally your current task. " +
|
|
62
|
+
"Call this periodically to stay visible to the team. " +
|
|
63
|
+
"Agents that haven't sent a heartbeat in 30 minutes are auto-pruned.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
project: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Project identifier.",
|
|
70
|
+
},
|
|
71
|
+
role: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Your agent role.",
|
|
74
|
+
},
|
|
75
|
+
current_task: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Optional updated description of your current task.",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
required: ["project", "role"],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
export const AGENT_LIST_TEAM_TOOL = {
|
|
84
|
+
name: "agent_list_team",
|
|
85
|
+
description: "List all active agents on a project. Shows role, status, current task, " +
|
|
86
|
+
"and last heartbeat time. Automatically prunes agents that haven't " +
|
|
87
|
+
"reported in 30+ minutes.",
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
project: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Project identifier.",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ["project"],
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
/** All Hivemind agent registry tools — conditionally registered */
|
|
100
|
+
export const AGENT_REGISTRY_TOOLS = [
|
|
101
|
+
AGENT_REGISTER_TOOL,
|
|
102
|
+
AGENT_HEARTBEAT_TOOL,
|
|
103
|
+
AGENT_LIST_TEAM_TOOL,
|
|
104
|
+
];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Registry Handlers (v3.0 — Agent Hivemind)
|
|
3
|
+
*
|
|
4
|
+
* Handler implementations for the 3 agent registry MCP tools.
|
|
5
|
+
* These are only called when PRISM_ENABLE_HIVEMIND=true.
|
|
6
|
+
*/
|
|
7
|
+
import { getStorage } from "../storage/index.js";
|
|
8
|
+
import { PRISM_USER_ID } from "../config.js";
|
|
9
|
+
import { getRoleIcon } from "./agentRegistryDefinitions.js";
|
|
10
|
+
// ─── Type Guards ─────────────────────────────────────────────
|
|
11
|
+
function isAgentRegisterArgs(args) {
|
|
12
|
+
return typeof args.project === "string" && typeof args.role === "string";
|
|
13
|
+
}
|
|
14
|
+
function isAgentHeartbeatArgs(args) {
|
|
15
|
+
return typeof args.project === "string" && typeof args.role === "string";
|
|
16
|
+
}
|
|
17
|
+
function isAgentListTeamArgs(args) {
|
|
18
|
+
return typeof args.project === "string";
|
|
19
|
+
}
|
|
20
|
+
// ─── Handlers ────────────────────────────────────────────────
|
|
21
|
+
export async function agentRegisterHandler(args) {
|
|
22
|
+
if (!isAgentRegisterArgs(args)) {
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: "Missing required: project, role" }],
|
|
25
|
+
isError: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const storage = await getStorage();
|
|
29
|
+
const result = await storage.registerAgent({
|
|
30
|
+
project: args.project,
|
|
31
|
+
user_id: PRISM_USER_ID,
|
|
32
|
+
role: args.role,
|
|
33
|
+
agent_name: args.agent_name || null,
|
|
34
|
+
status: "active",
|
|
35
|
+
current_task: args.current_task || null,
|
|
36
|
+
});
|
|
37
|
+
const icon = getRoleIcon(args.role);
|
|
38
|
+
return {
|
|
39
|
+
content: [{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: `${icon} **Agent Registered**\n\n` +
|
|
42
|
+
`- **Project:** ${args.project}\n` +
|
|
43
|
+
`- **Role:** ${args.role}\n` +
|
|
44
|
+
(args.agent_name ? `- **Name:** ${args.agent_name}\n` : "") +
|
|
45
|
+
(args.current_task ? `- **Task:** ${args.current_task}\n` : "") +
|
|
46
|
+
`\nOther agents will see you when they call \`agent_list_team\` or \`session_load_context\`.`,
|
|
47
|
+
}],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export async function agentHeartbeatHandler(args) {
|
|
51
|
+
if (!isAgentHeartbeatArgs(args)) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: "Missing required: project, role" }],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const storage = await getStorage();
|
|
58
|
+
await storage.heartbeatAgent(args.project, PRISM_USER_ID, args.role, args.current_task);
|
|
59
|
+
return {
|
|
60
|
+
content: [{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: `💓 Heartbeat updated for **${args.role}** on \`${args.project}\`.` +
|
|
63
|
+
(args.current_task ? ` Task: ${args.current_task}` : ""),
|
|
64
|
+
}],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function agentListTeamHandler(args) {
|
|
68
|
+
if (!isAgentListTeamArgs(args)) {
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: "Missing required: project" }],
|
|
71
|
+
isError: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const storage = await getStorage();
|
|
75
|
+
const team = await storage.listTeam(args.project, PRISM_USER_ID);
|
|
76
|
+
if (team.length === 0) {
|
|
77
|
+
return {
|
|
78
|
+
content: [{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: `No active agents on \`${args.project}\`. Use \`agent_register\` to join the team.`,
|
|
81
|
+
}],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const lines = team.map(agent => {
|
|
85
|
+
const icon = getRoleIcon(agent.role);
|
|
86
|
+
const ago = agent.last_heartbeat
|
|
87
|
+
? getTimeAgo(agent.last_heartbeat)
|
|
88
|
+
: "unknown";
|
|
89
|
+
return (`${icon} **${agent.role}**` +
|
|
90
|
+
(agent.agent_name ? ` (${agent.agent_name})` : "") +
|
|
91
|
+
` — ${agent.status}` +
|
|
92
|
+
(agent.current_task ? ` | Task: ${agent.current_task}` : "") +
|
|
93
|
+
` | Last seen: ${ago}`);
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
content: [{
|
|
97
|
+
type: "text",
|
|
98
|
+
text: `## 🐝 Active Hivemind Team — \`${args.project}\`\n\n` +
|
|
99
|
+
lines.join("\n") +
|
|
100
|
+
`\n\n_${team.length} agent(s) active. Stale agents (>30min) auto-pruned._`,
|
|
101
|
+
}],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
105
|
+
function getTimeAgo(isoString) {
|
|
106
|
+
const diff = Date.now() - new Date(isoString).getTime();
|
|
107
|
+
const mins = Math.floor(diff / 60000);
|
|
108
|
+
if (mins < 1)
|
|
109
|
+
return "just now";
|
|
110
|
+
if (mins < 60)
|
|
111
|
+
return `${mins}m ago`;
|
|
112
|
+
const hours = Math.floor(mins / 60);
|
|
113
|
+
return `${hours}h ago`;
|
|
114
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -33,3 +33,8 @@ export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContext
|
|
|
33
33
|
// more complex than the other session memory handlers (chunked Gemini
|
|
34
34
|
// API calls, recursive summarization, etc.).
|
|
35
35
|
export { compactLedgerHandler } from "./compactionHandler.js";
|
|
36
|
+
// ── Agent Registry Tools (v3.0 — Hivemind, Optional) ──
|
|
37
|
+
// These tools are only registered when PRISM_ENABLE_HIVEMIND=true.
|
|
38
|
+
// server.ts handles the conditional registration.
|
|
39
|
+
export { AGENT_REGISTRY_TOOLS, getRoleIcon } from "./agentRegistryDefinitions.js";
|
|
40
|
+
export { agentRegisterHandler, agentHeartbeatHandler, agentListTeamHandler } from "./agentRegistryHandlers.js";
|
|
@@ -35,6 +35,10 @@ export const SESSION_SAVE_LEDGER_TOOL = {
|
|
|
35
35
|
items: { type: "string" },
|
|
36
36
|
description: "Optional list of key decisions made during this session.",
|
|
37
37
|
},
|
|
38
|
+
role: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "v3.0: Agent role for Hivemind scoping (e.g., 'dev', 'qa', 'pm'). Defaults to 'global'.",
|
|
41
|
+
},
|
|
38
42
|
},
|
|
39
43
|
required: ["project", "conversation_id", "summary"],
|
|
40
44
|
},
|
|
@@ -84,6 +88,10 @@ export const SESSION_SAVE_HANDOFF_TOOL = {
|
|
|
84
88
|
type: "string",
|
|
85
89
|
description: "Free-form critical context the next session needs to know.",
|
|
86
90
|
},
|
|
91
|
+
role: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "v3.0: Agent role for Hivemind scoping (e.g., 'dev', 'qa', 'pm'). Defaults to 'global'.",
|
|
94
|
+
},
|
|
87
95
|
},
|
|
88
96
|
required: ["project"],
|
|
89
97
|
},
|
|
@@ -109,6 +117,10 @@ export const SESSION_LOAD_CONTEXT_TOOL = {
|
|
|
109
117
|
enum: ["quick", "standard", "deep"],
|
|
110
118
|
description: "How much context to load: 'quick' (just TODOs), 'standard' (recommended — includes recent summaries), or 'deep' (full history). Default: standard.",
|
|
111
119
|
},
|
|
120
|
+
role: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "v3.0: Agent role for Hivemind scoping (e.g., 'dev', 'qa', 'pm'). Defaults to 'global'. When set, also injects active_team roster.",
|
|
123
|
+
},
|
|
112
124
|
},
|
|
113
125
|
required: ["project"],
|
|
114
126
|
},
|
|
@@ -46,7 +46,7 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
46
46
|
if (!isSessionSaveLedgerArgs(args)) {
|
|
47
47
|
throw new Error("Invalid arguments for session_save_ledger");
|
|
48
48
|
}
|
|
49
|
-
const { project, conversation_id, summary, todos, files_changed, decisions } = args;
|
|
49
|
+
const { project, conversation_id, summary, todos, files_changed, decisions, role } = args;
|
|
50
50
|
const storage = await getStorage();
|
|
51
51
|
debugLog(`[session_save_ledger] Saving ledger entry for project="${project}"`);
|
|
52
52
|
// Auto-extract keywords from summary + decisions for knowledge accumulation
|
|
@@ -63,6 +63,7 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
63
63
|
files_changed: files_changed || [],
|
|
64
64
|
decisions: decisions || [],
|
|
65
65
|
keywords,
|
|
66
|
+
role: role || "global", // v3.0: Hivemind role scoping
|
|
66
67
|
});
|
|
67
68
|
// ─── Fire-and-forget embedding generation ───
|
|
68
69
|
if (GOOGLE_API_KEY && result) {
|
|
@@ -104,7 +105,8 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
104
105
|
if (!isSessionSaveHandoffArgs(args)) {
|
|
105
106
|
throw new Error("Invalid arguments for session_save_handoff");
|
|
106
107
|
}
|
|
107
|
-
const { project, expected_version, open_todos, active_branch, last_summary, key_context,
|
|
108
|
+
const { project, expected_version, open_todos, active_branch, last_summary, key_context, role, // v3.0: Hivemind role
|
|
109
|
+
} = args;
|
|
108
110
|
const storage = await getStorage();
|
|
109
111
|
debugLog(`[session_save_handoff] Saving handoff for project="${project}" ` +
|
|
110
112
|
`(expected_version=${expected_version ?? "none"})`);
|
|
@@ -133,6 +135,7 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
133
135
|
key_context: key_context ?? null,
|
|
134
136
|
active_branch: active_branch ?? null,
|
|
135
137
|
metadata,
|
|
138
|
+
role: role || "global", // v3.0: Hivemind role scoping
|
|
136
139
|
}, expected_version ?? null);
|
|
137
140
|
// ─── Handle version conflict ───
|
|
138
141
|
if (data.status === "conflict") {
|
|
@@ -317,7 +320,7 @@ export async function sessionLoadContextHandler(args) {
|
|
|
317
320
|
if (!isSessionLoadContextArgs(args)) {
|
|
318
321
|
throw new Error("Invalid arguments for session_load_context");
|
|
319
322
|
}
|
|
320
|
-
const { project, level = "standard" } = args;
|
|
323
|
+
const { project, level = "standard", role } = args;
|
|
321
324
|
const validLevels = ["quick", "standard", "deep"];
|
|
322
325
|
if (!validLevels.includes(level)) {
|
|
323
326
|
return {
|
|
@@ -330,7 +333,7 @@ export async function sessionLoadContextHandler(args) {
|
|
|
330
333
|
}
|
|
331
334
|
debugLog(`[session_load_context] Loading ${level} context for project="${project}"`);
|
|
332
335
|
const storage = await getStorage();
|
|
333
|
-
const data = await storage.loadContext(project, level, PRISM_USER_ID);
|
|
336
|
+
const data = await storage.loadContext(project, level, PRISM_USER_ID, role); // v3.0: pass role
|
|
334
337
|
if (!data) {
|
|
335
338
|
return {
|
|
336
339
|
content: [{
|
package/dist/utils/googleAi.js
CHANGED
|
@@ -38,7 +38,7 @@ export async function createMcpClient() {
|
|
|
38
38
|
command: "node",
|
|
39
39
|
args: ["index.js"], // Server entry point
|
|
40
40
|
});
|
|
41
|
-
const client = new Client({ name: "gemini-mcp-client", version: "1.0.0" }
|
|
41
|
+
const client = new Client({ name: "gemini-mcp-client", version: "1.0.0" });
|
|
42
42
|
await client.connect(transport);
|
|
43
43
|
return { client, transport };
|
|
44
44
|
}
|