@grec0/memory-bank-mcp 0.1.21 → 0.1.23

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.
@@ -1,307 +1,204 @@
1
- import * as fs from 'fs/promises';
2
- import * as path from 'path';
3
- import { LockManager } from './lockManager.js';
1
+ /**
2
+ * @fileoverview Agent Board - Unified interface for agent coordination
3
+ *
4
+ * This module provides backward-compatible API while using SQLite internally.
5
+ * The AgentBoard class maintains the same method signatures but delegates to
6
+ * AgentBoardSqlite for actual operations.
7
+ *
8
+ * Migration: MD-based storage is deprecated. All operations now use SQLite.
9
+ */
10
+ import { AgentBoardSqlite } from './agentBoardSqlite.js';
11
+ import { databaseManager } from './database.js';
12
+ import * as crypto from 'crypto';
13
+ /**
14
+ * AgentBoard - Facade for agent coordination
15
+ *
16
+ * Maintains backward-compatible API while using SQLite storage.
17
+ * The basePath parameter is kept for compatibility but ignored (DB is global).
18
+ */
4
19
  export class AgentBoard {
5
- basePath;
20
+ sqlite;
6
21
  projectId;
7
- lockManager;
8
22
  constructor(basePath, projectId) {
9
- this.basePath = basePath;
23
+ // basePath is ignored - SQLite DB is at ~/.memorybank/agentboard.db
10
24
  this.projectId = projectId;
11
- this.lockManager = new LockManager(basePath);
12
- }
25
+ this.sqlite = new AgentBoardSqlite(projectId);
26
+ }
27
+ // ========================================================================
28
+ // Board Content (Markdown export for compatibility)
29
+ // ========================================================================
30
+ /**
31
+ * Get board content as Markdown (for display/debugging)
32
+ */
13
33
  async getBoardContent() {
14
- await this.ensureBoardExists();
15
- return await fs.readFile(this.getBoardPath(), 'utf-8');
34
+ return this.sqlite.exportToMarkdown();
35
+ }
36
+ // ========================================================================
37
+ // Agent Management
38
+ // ========================================================================
39
+ /**
40
+ * Register an agent with optional session ID
41
+ * Note: For new flow, use registerAgentWithHash() which returns the generated ID
42
+ */
43
+ async registerAgent(agentId, sessionId) {
44
+ // Legacy method - agent ID already includes hash
45
+ // Just update/insert into SQLite
46
+ const db = databaseManager.getConnection();
47
+ const now = new Date().toISOString();
48
+ const effectiveSessionId = sessionId || crypto.randomUUID();
49
+ // Deactivate other agents for this project
50
+ db.prepare(`
51
+ UPDATE agents
52
+ SET status = 'INACTIVE', last_heartbeat = ?
53
+ WHERE project_id = ? AND status = 'ACTIVE' AND id != ?
54
+ `).run(now, this.projectId, agentId);
55
+ // Upsert this agent
56
+ db.prepare(`
57
+ INSERT INTO agents (id, project_id, session_id, status, focus, last_heartbeat)
58
+ VALUES (?, ?, ?, 'ACTIVE', '-', ?)
59
+ ON CONFLICT(id, project_id) DO UPDATE SET
60
+ session_id = excluded.session_id,
61
+ status = 'ACTIVE',
62
+ last_heartbeat = excluded.last_heartbeat
63
+ `).run(agentId, this.projectId, effectiveSessionId, now);
64
+ this.sqlite.logMessage(agentId, 'Agent registered');
65
+ }
66
+ /**
67
+ * Register agent and generate hash suffix (new flow)
68
+ * Client provides base ID (e.g., "Dev-VSCode-Gemini"), MCP generates full ID with hash
69
+ */
70
+ registerAgentWithHash(baseAgentId, sessionId) {
71
+ return this.sqlite.registerAgent(baseAgentId, sessionId);
72
+ }
73
+ /**
74
+ * Update agent status and current focus
75
+ */
76
+ async updateStatus(agentId, status, focus) {
77
+ this.sqlite.updateStatus(agentId, status, focus);
16
78
  }
17
- // --- Task Management Methods ---
79
+ /**
80
+ * Resolve a base agent ID to the actual active agent ID
81
+ */
82
+ async resolveActiveAgentId(baseId) {
83
+ return this.sqlite.resolveActiveAgentId(baseId);
84
+ }
85
+ /**
86
+ * Get session ID for an agent
87
+ */
88
+ async getSessionId(agentId) {
89
+ return this.sqlite.getSessionId(agentId) || undefined;
90
+ }
91
+ /**
92
+ * Get the currently active agent for this project
93
+ */
94
+ getActiveAgent() {
95
+ return this.sqlite.getActiveAgent();
96
+ }
97
+ /**
98
+ * Get all agents (for session history)
99
+ */
100
+ getAllAgents() {
101
+ return this.sqlite.getAllAgents();
102
+ }
103
+ // ========================================================================
104
+ // Task Management
105
+ // ========================================================================
106
+ /**
107
+ * Create a task (project-centric)
108
+ * Note: assignedTo parameter is deprecated - tasks go to the project, not agent
109
+ */
18
110
  async createTask(title, fromAgentId, assignedTo, description) {
19
- const taskId = `TASK-${Date.now().toString().slice(-6)}`;
20
- await this.updateBoard((content) => {
21
- const tasks = this.parseTable(content, 'Pending Tasks');
22
- const now = new Date().toISOString();
23
- // Columns: ID, Title, Assigned To, From, Status, Created At
24
- tasks.push([taskId, title, assignedTo, fromAgentId, 'PENDING', now]);
25
- return this.updateTable(content, 'Pending Tasks', ['ID', 'Title', 'Assigned To', 'From', 'Status', 'Created At'], tasks);
26
- });
27
- await this.logMessage(fromAgentId, `Created task ${taskId}: ${title}`);
28
- return taskId;
111
+ // assignedTo is ignored in new model - tasks are project-centric
112
+ return this.sqlite.createTask(title, description, fromAgentId);
29
113
  }
114
+ /**
115
+ * Create an external task from another project
116
+ */
30
117
  async createExternalTask(title, fromProject, context) {
31
- const taskId = `EXT-${Date.now().toString().slice(-6)}`;
32
- await this.updateBoard((content) => {
33
- const requests = this.parseTable(content, 'External Requests');
34
- const now = new Date().toISOString();
35
- // Sanitize context to fit in a table cell (no newlines, escape pipes)
36
- const safeContext = context.replace(/[\r\n]+/g, '<br/>').replace(/\|/g, '\\|');
37
- // Columns: ID, Title, From Project, Context, Status, Received At
38
- requests.push([taskId, title, fromProject, safeContext, 'PENDING', now]);
39
- return this.updateTable(content, 'External Requests', ['ID', 'Title', 'From Project', 'Context', 'Status', 'Received At'], requests);
40
- });
41
- await this.logMessage('SYSTEM', `Received external prompt from project ${fromProject}: ${title}`);
42
- return taskId;
118
+ return this.sqlite.createExternalTask(title, fromProject, context);
43
119
  }
120
+ /**
121
+ * Complete a task
122
+ */
44
123
  async completeTask(taskId, agentId) {
45
- await this.updateBoard((content) => {
46
- let tasks = this.parseTable(content, 'Pending Tasks');
47
- const initialCount = tasks.length;
48
- tasks = tasks.filter(t => t[0] !== taskId);
49
- if (tasks.length < initialCount) {
50
- return this.updateTable(content, 'Pending Tasks', ['ID', 'Title', 'Assigned To', 'From', 'Status', 'Created At'], tasks);
51
- }
52
- // Check external requests if not found in pending
53
- let requests = this.parseTable(content, 'External Requests');
54
- requests = requests.filter(t => t[0] !== taskId);
55
- return this.updateTable(content, 'External Requests', ['ID', 'Title', 'From Project', 'Context', 'Status', 'Received At'], requests);
56
- });
57
- await this.logMessage(agentId, `Completed task ${taskId}`);
58
- }
59
- async logMessage(agentId, message) {
60
- await this.updateBoard((content) => {
61
- const lines = content.split('\n');
62
- let msgSectionIdx = -1;
63
- for (let i = 0; i < lines.length; i++) {
64
- if (lines[i].trim().startsWith('## Agent Messages')) {
65
- msgSectionIdx = i;
66
- break;
67
- }
68
- }
69
- const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
70
- const msgLine = `- [${timestamp}] **${agentId}**: ${message}`;
71
- if (msgSectionIdx !== -1) {
72
- // Determine insertion point (after header)
73
- lines.splice(msgSectionIdx + 1, 0, msgLine);
74
- // Truncate logs if too long (keep last 20)
75
- let nextHeaderIdx = -1;
76
- for (let j = msgSectionIdx + 2; j < lines.length; j++) {
77
- if (lines[j].startsWith('## ')) {
78
- nextHeaderIdx = j;
79
- break;
80
- }
81
- }
82
- const endOfMessages = nextHeaderIdx === -1 ? lines.length : nextHeaderIdx;
83
- if (endOfMessages - (msgSectionIdx + 1) > 20) {
84
- lines.splice(endOfMessages - 1, 1);
85
- }
86
- }
87
- return lines.join('\n');
88
- });
89
- }
90
- getBoardPath() {
91
- return path.join(this.basePath, '.memorybank', 'projects', this.projectId, 'docs', 'agentBoard.md');
92
- }
93
- async ensureBoardExists() {
94
- const boardPath = this.getBoardPath();
95
- try {
96
- await fs.access(boardPath);
97
- }
98
- catch {
99
- const initialContent = `# Multi-Agent Board
100
-
101
- ## Active Agents
102
- | Agent ID | Status | Current Focus | Session ID | Last Heartbeat |
103
- |---|---|---|---|---|
104
-
105
- ## Pending Tasks
106
- | ID | Title | Assigned To | From | Status | Created At |
107
- |---|---|---|---|---|---|
108
-
109
- ## External Requests
110
- | ID | Title | From Project | Context | Status | Received At |
111
- |---|---|---|---|---|---|
112
-
113
- ## File Locks
114
- | File Pattern | Claimed By | Since |
115
- |---|---|---|
116
-
117
- ## Agent Messages
118
- - [System]: Board initialized
119
- `;
120
- await fs.mkdir(path.dirname(boardPath), { recursive: true });
121
- await fs.writeFile(boardPath, initialContent, 'utf-8');
122
- }
123
- }
124
- async registerAgent(agentId, sessionId) {
125
- await this.updateBoard((content) => {
126
- const agents = this.parseTable(content, 'Active Agents');
127
- const existing = agents.findIndex(a => a[0]?.trim() === agentId);
128
- const now = new Date().toISOString();
129
- const session = sessionId || (existing >= 0 ? (agents[existing][3] || '') : ''); // Use col 3 as SessionID based on previous read
130
- if (existing >= 0) {
131
- // Check if it's already 5 cols or 4
132
- if (agents[existing].length < 5) {
133
- agents[existing] = [agentId, 'ACTIVE', '-', session, now];
134
- }
135
- else {
136
- // preserve existing fields if needed, but update timestamp
137
- agents[existing] = [agentId, 'ACTIVE', '-', session, now];
138
- }
139
- }
140
- else {
141
- agents.push([agentId, 'ACTIVE', '-', session, now]);
142
- }
143
- return this.updateTable(content, 'Active Agents', ['Agent ID', 'Status', 'Current Focus', 'Session ID', 'Last Heartbeat'], agents);
144
- });
145
- }
146
- async updateStatus(agentId, status, focus) {
147
- await this.updateBoard((content) => {
148
- let agents = this.parseTable(content, 'Active Agents');
149
- // Migration: Ensure 5 columns
150
- agents = agents.map(row => {
151
- if (row.length === 4) {
152
- return [row[0], row[1], row[2], '', row[3]];
153
- }
154
- return row;
155
- });
156
- const idx = agents.findIndex(a => a[0]?.trim() === agentId);
157
- const now = new Date().toISOString();
158
- if (idx >= 0) {
159
- // Keep existing session ID
160
- const currentSession = agents[idx][3] || '';
161
- agents[idx] = [agentId, status, focus, currentSession, now];
162
- }
163
- else {
164
- agents.push([agentId, status, focus, '', now]);
165
- }
166
- return this.updateTable(content, 'Active Agents', ['Agent ID', 'Status', 'Current Focus', 'Session ID', 'Last Heartbeat'], agents);
167
- });
168
- }
169
- async getSessionId(agentId) {
170
- const content = await this.getBoardContent();
171
- const agents = this.parseTable(content, 'Active Agents');
172
- const agent = agents.find(a => a[0]?.trim() === agentId);
173
- if (agent) {
174
- // Handle 5 cols
175
- if (agent.length >= 5)
176
- return agent[3].trim();
177
- // Handle 4 cols (legacy) - no session ID
178
- return undefined;
179
- }
180
- return undefined;
181
- }
124
+ this.sqlite.completeTask(taskId, agentId);
125
+ }
126
+ /**
127
+ * Get pending tasks for this project
128
+ */
129
+ getPendingTasks() {
130
+ return this.sqlite.getPendingTasks();
131
+ }
132
+ /**
133
+ * Claim a task
134
+ */
135
+ claimTask(taskId, agentId) {
136
+ return this.sqlite.claimTask(taskId, agentId);
137
+ }
138
+ // ========================================================================
139
+ // Resource Locks
140
+ // ========================================================================
141
+ /**
142
+ * Claim a resource lock
143
+ */
182
144
  async claimResource(agentId, resource) {
183
- let success = false;
184
- await this.updateBoard((content) => {
185
- const locks = this.parseTable(content, 'File Locks');
186
- // Check if already locked by someone else
187
- const existing = locks.find(l => l[0]?.trim() === resource);
188
- if (existing && existing[1]?.trim() !== agentId) {
189
- success = false;
190
- return content; // No change
191
- }
192
- // Add or update lock
193
- const now = new Date().toISOString();
194
- if (existing) {
195
- existing[2] = now; // Renew timestamp
196
- }
197
- else {
198
- locks.push([resource, agentId, now]);
199
- }
200
- success = true;
201
- return this.updateTable(content, 'File Locks', ['File Pattern', 'Claimed By', 'Since'], locks);
202
- });
203
- return success;
145
+ return this.sqlite.claimResource(agentId, resource);
204
146
  }
147
+ /**
148
+ * Release a resource lock
149
+ */
205
150
  async releaseResource(agentId, resource) {
206
- await this.updateBoard((content) => {
207
- let locks = this.parseTable(content, 'File Locks');
208
- // Filter out locks for this resource by this agent
209
- locks = locks.filter(l => !(l[0]?.trim() === resource && l[1]?.trim() === agentId));
210
- return this.updateTable(content, 'File Locks', ['File Pattern', 'Claimed By', 'Since'], locks);
211
- });
212
- }
213
- // [Duplicate getBoardContent removed]
214
- // --- Helpers ---
215
- async updateBoard(mutator) {
216
- await this.ensureBoardExists();
217
- const locked = await this.lockManager.acquire('agentBoard');
218
- if (!locked) {
219
- throw new Error('Could not acquire lock for Agent Board');
220
- }
221
- try {
222
- const current = await fs.readFile(this.getBoardPath(), 'utf-8');
223
- const newContent = mutator(current);
224
- await fs.writeFile(this.getBoardPath(), newContent, 'utf-8');
225
- }
226
- finally {
227
- await this.lockManager.release('agentBoard');
228
- }
229
- }
230
- parseTable(content, headerName) {
231
- const lines = content.split('\n');
232
- const result = [];
233
- let inTable = false;
234
- let colCount = 0;
235
- for (let i = 0; i < lines.length; i++) {
236
- const line = lines[i].trim();
237
- if (line.startsWith(`## ${headerName}`)) {
238
- inTable = true;
239
- continue;
240
- }
241
- if (inTable) {
242
- if (line.startsWith('## '))
243
- break; // New section
244
- if (!line.includes('|'))
245
- continue;
246
- if (line.includes('---'))
247
- continue; // Separator
248
- // Parse row
249
- const cols = line.split('|').map(c => c.trim()).filter(c => c !== '');
250
- if (cols.length > 0) {
251
- // Check if it's the header row
252
- if (result.length === 0) {
253
- const firstCol = cols[0].toLowerCase();
254
- if (firstCol.includes('agent id') || firstCol.includes('file pattern') || firstCol === 'id') {
255
- // skip header
256
- }
257
- else {
258
- result.push(cols);
259
- }
260
- }
261
- else {
262
- result.push(cols);
263
- }
264
- }
265
- }
266
- }
267
- return result;
268
- }
269
- updateTable(content, headerName, headers, rows) {
270
- const lines = content.split('\n');
271
- let startIdx = -1;
272
- let endIdx = -1;
273
- // Validar rows (limpiar arrays vacíos o mal formados)
274
- const cleanRows = rows.filter(r => r.length > 0);
275
- for (let i = 0; i < lines.length; i++) {
276
- if (lines[i].trim().startsWith(`## ${headerName}`)) {
277
- startIdx = i;
278
- // Find end of section (next ## or end of file)
279
- for (let j = i + 1; j < lines.length; j++) {
280
- if (lines[j].trim().startsWith('## ')) {
281
- endIdx = j;
282
- break;
283
- }
284
- }
285
- if (endIdx === -1)
286
- endIdx = lines.length;
287
- break;
288
- }
289
- }
290
- const newTable = [
291
- `## ${headerName}`,
292
- `| ${headers.join(' | ')} |`,
293
- `| ${headers.map(() => '---').join(' | ')} |`,
294
- ...cleanRows.map(row => `| ${row.join(' | ')} |`)
295
- ].join('\n');
296
- if (startIdx === -1) {
297
- // Append if not found
298
- return content + '\n\n' + newTable;
299
- }
300
- else {
301
- // Replace section
302
- const before = lines.slice(0, startIdx);
303
- const after = lines.slice(endIdx);
304
- return [...before, newTable, '', ...after].join('\n');
305
- }
151
+ this.sqlite.releaseResource(agentId, resource);
152
+ }
153
+ /**
154
+ * Get all locks for this project
155
+ */
156
+ getLocks() {
157
+ return this.sqlite.getLocks();
158
+ }
159
+ /**
160
+ * Cleanup orphaned locks
161
+ */
162
+ cleanupOrphanedLocks() {
163
+ return this.sqlite.cleanupOrphanedLocks();
164
+ }
165
+ // ========================================================================
166
+ // Messages
167
+ // ========================================================================
168
+ /**
169
+ * Log a message to the agent board
170
+ */
171
+ async logMessage(agentId, message) {
172
+ this.sqlite.logMessage(agentId, message);
173
+ }
174
+ /**
175
+ * Get recent messages
176
+ */
177
+ getMessages(limit = 20) {
178
+ return this.sqlite.getMessages(limit);
179
+ }
180
+ // ========================================================================
181
+ // Session Events
182
+ // ========================================================================
183
+ /**
184
+ * Log a session event
185
+ */
186
+ logSessionEvent(sessionId, eventType, eventData, agentId) {
187
+ this.sqlite.logSessionEvent(sessionId, eventType, eventData, agentId);
188
+ }
189
+ /**
190
+ * Get session history
191
+ */
192
+ getSessionHistory(sessionId) {
193
+ return this.sqlite.getSessionHistory(sessionId);
194
+ }
195
+ /**
196
+ * Get all sessions for this project
197
+ */
198
+ getProjectSessions() {
199
+ return this.sqlite.getProjectSessions();
306
200
  }
307
201
  }
202
+ // Re-export SQLite implementation for direct access if needed
203
+ export { AgentBoardSqlite } from './agentBoardSqlite.js';
204
+ export { cleanupStaleAgents, cleanupAllOrphanedLocks } from './agentBoardSqlite.js';