mcp-coordinator 0.1.0 → 0.2.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.
@@ -0,0 +1,146 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, readFileSync, writeFileSync, rmSync, statSync, } from "fs";
3
+ import { resolve } from "path";
4
+ import { getConfigDir } from "./config.js";
5
+ const SENTINEL = "<!-- mcp-coordinator:coordination-section -->";
6
+ export function createUninstallCommand() {
7
+ return new Command("uninstall")
8
+ .description("Remove mcp-coordinator integrations: the 'coordinator' MCP server entry from a .mcp.json, the coordination section from a CLAUDE.md, and (with --purge) the ~/.mcp-coordinator/ directory.")
9
+ .option("--mcp-config <path>", "Remove the 'coordinator' entry from <path>/.mcp.json (leaves other servers untouched). Removes the file if it ends up empty.")
10
+ .option("--claude-md <path>", "Remove the mcp-coordinator coordination section from <path>/CLAUDE.md (between the sentinel comments). Leaves other content untouched.")
11
+ .option("--purge", "Delete the ~/.mcp-coordinator/ directory entirely (config + data + logs + pid file). Asks for confirmation unless --force is set.")
12
+ .option("--force", "Skip the confirmation prompt for --purge. Useful in scripts.")
13
+ .action(async (opts) => {
14
+ let didSomething = false;
15
+ let exitCode = 0;
16
+ const validateDir = (p, label) => {
17
+ const abs = resolve(p);
18
+ if (!existsSync(abs)) {
19
+ console.error(`Error: ${label} path ${abs} does not exist.`);
20
+ return null;
21
+ }
22
+ const st = statSync(abs);
23
+ if (!st.isDirectory()) {
24
+ console.error(`Error: ${label} path ${abs} is not a directory.`);
25
+ return null;
26
+ }
27
+ return abs;
28
+ };
29
+ // 1. Remove coordinator from .mcp.json
30
+ if (opts.mcpConfig) {
31
+ const dirAbs = validateDir(opts.mcpConfig, "--mcp-config");
32
+ if (!dirAbs) {
33
+ exitCode = 1;
34
+ }
35
+ else {
36
+ const target = resolve(dirAbs, ".mcp.json");
37
+ if (!existsSync(target)) {
38
+ console.log(`No .mcp.json at ${target} — nothing to remove.`);
39
+ }
40
+ else {
41
+ try {
42
+ const json = JSON.parse(readFileSync(target, "utf-8"));
43
+ if (json.mcpServers && "coordinator" in json.mcpServers) {
44
+ delete json.mcpServers.coordinator;
45
+ if (Object.keys(json.mcpServers).length === 0) {
46
+ delete json.mcpServers;
47
+ }
48
+ if (Object.keys(json).length === 0) {
49
+ rmSync(target);
50
+ console.log(`Removed empty .mcp.json: ${target}`);
51
+ }
52
+ else {
53
+ writeFileSync(target, JSON.stringify(json, null, 2) + "\n");
54
+ console.log(`Removed coordinator entry from ${target}`);
55
+ }
56
+ didSomething = true;
57
+ }
58
+ else {
59
+ console.log(`No coordinator entry in ${target} — nothing to remove.`);
60
+ }
61
+ }
62
+ catch (e) {
63
+ console.error(`Error reading ${target}: ${e.message}`);
64
+ exitCode = 1;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ // 2. Remove coordination section from CLAUDE.md
70
+ if (opts.claudeMd) {
71
+ const dirAbs = validateDir(opts.claudeMd, "--claude-md");
72
+ if (!dirAbs) {
73
+ exitCode = 1;
74
+ }
75
+ else {
76
+ const target = resolve(dirAbs, "CLAUDE.md");
77
+ if (!existsSync(target)) {
78
+ console.log(`No CLAUDE.md at ${target} — nothing to remove.`);
79
+ }
80
+ else {
81
+ const content = readFileSync(target, "utf-8");
82
+ if (!content.includes(SENTINEL)) {
83
+ console.log(`No mcp-coordinator section in ${target} — nothing to remove.`);
84
+ }
85
+ else {
86
+ const re = new RegExp(`${SENTINEL}[\\s\\S]*?${SENTINEL}\\n?`, "g");
87
+ const cleaned = content.replace(re, "");
88
+ const tidied = cleaned.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "").replace(/\n+$/, "\n");
89
+ if (tidied.trim() === "" || tidied.trim() === "# CLAUDE.md") {
90
+ rmSync(target);
91
+ console.log(`Removed empty CLAUDE.md: ${target}`);
92
+ }
93
+ else {
94
+ writeFileSync(target, tidied);
95
+ console.log(`Removed coordination section from ${target}`);
96
+ }
97
+ didSomething = true;
98
+ }
99
+ }
100
+ }
101
+ }
102
+ // 3. Purge ~/.mcp-coordinator/
103
+ if (opts.purge) {
104
+ const dir = getConfigDir();
105
+ if (!existsSync(dir)) {
106
+ console.log(`No config dir at ${dir} — nothing to purge.`);
107
+ }
108
+ else {
109
+ if (!opts.force) {
110
+ // Read a single line from stdin
111
+ const ask = await new Promise((resolveP) => {
112
+ process.stdout.write(`Delete ${dir} (config, data, logs, pid)? [y/N] `);
113
+ process.stdin.once("data", (b) => resolveP(b.toString().trim()));
114
+ });
115
+ if (ask.toLowerCase() !== "y" && ask.toLowerCase() !== "yes") {
116
+ console.log("Aborted.");
117
+ if (exitCode !== 0)
118
+ process.exit(exitCode);
119
+ return;
120
+ }
121
+ }
122
+ rmSync(dir, { recursive: true, force: true });
123
+ console.log(`Purged config dir: ${dir}`);
124
+ didSomething = true;
125
+ }
126
+ }
127
+ if (!opts.mcpConfig && !opts.claudeMd && !opts.purge) {
128
+ console.log("Nothing to do. Pass --mcp-config <path>, --claude-md <path>, --purge, or any combination.");
129
+ console.log("");
130
+ console.log("Examples:");
131
+ console.log(" mcp-coordinator uninstall --mcp-config ~/my-repo");
132
+ console.log(" mcp-coordinator uninstall --claude-md ~/my-repo");
133
+ console.log(" mcp-coordinator uninstall --mcp-config ~/my-repo --claude-md ~/my-repo");
134
+ console.log(" mcp-coordinator uninstall --purge --force");
135
+ console.log("");
136
+ console.log("To remove the npm package itself: npm uninstall -g mcp-coordinator");
137
+ process.exit(0);
138
+ }
139
+ if (!didSomething) {
140
+ console.log("");
141
+ console.log("No changes made.");
142
+ }
143
+ if (exitCode !== 0)
144
+ process.exit(exitCode);
145
+ });
146
+ }
@@ -59,12 +59,12 @@ export class AgentActivityTracker {
59
59
  // ── Private ──
60
60
  upsert(agentId, status, file, thread) {
61
61
  const db = getDb();
62
- db.prepare(`INSERT INTO agent_activity_status (agent_id, activity_status, current_file, current_thread, last_activity_at)
63
- VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
64
- ON CONFLICT(agent_id) DO UPDATE SET
65
- activity_status = excluded.activity_status,
66
- current_file = excluded.current_file,
67
- current_thread = excluded.current_thread,
62
+ db.prepare(`INSERT INTO agent_activity_status (agent_id, activity_status, current_file, current_thread, last_activity_at)
63
+ VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
64
+ ON CONFLICT(agent_id) DO UPDATE SET
65
+ activity_status = excluded.activity_status,
66
+ current_file = excluded.current_file,
67
+ current_thread = excluded.current_thread,
68
68
  last_activity_at = CURRENT_TIMESTAMP`).run(agentId, status, file, thread);
69
69
  }
70
70
  }
@@ -2,12 +2,12 @@ import { getDb } from "./database.js";
2
2
  export class AgentRegistry {
3
3
  register(agentId, name, modules) {
4
4
  const db = getDb();
5
- db.prepare(`INSERT INTO agents (id, name, modules, status, registered_at, last_seen_at)
6
- VALUES (?, ?, ?, 'online', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
7
- ON CONFLICT(id) DO UPDATE SET
8
- name = excluded.name,
9
- modules = excluded.modules,
10
- status = 'online',
5
+ db.prepare(`INSERT INTO agents (id, name, modules, status, registered_at, last_seen_at)
6
+ VALUES (?, ?, ?, 'online', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
7
+ ON CONFLICT(id) DO UPDATE SET
8
+ name = excluded.name,
9
+ modules = excluded.modules,
10
+ status = 'online',
11
11
  last_seen_at = CURRENT_TIMESTAMP`).run(agentId, name, JSON.stringify(modules));
12
12
  return this.get(agentId);
13
13
  }
@@ -54,7 +54,7 @@ export class Consultation {
54
54
  const assignedTo = params.assigned_to ?? null;
55
55
  const keepOpen = params.keep_open || assignedTo !== null;
56
56
  const autoResolve = respondentIds.length === 0 && !keepOpen;
57
- db.prepare(`INSERT INTO threads (id, initiator_id, subject, plan, target_modules, target_files, status, expected_respondents, resolved_at, depends_on_files, exports_affected, timeout_seconds, assigned_to)
57
+ db.prepare(`INSERT INTO threads (id, initiator_id, subject, plan, target_modules, target_files, status, expected_respondents, resolved_at, depends_on_files, exports_affected, timeout_seconds, assigned_to)
58
58
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.agent_id, params.subject, params.plan || null, JSON.stringify(params.target_modules), JSON.stringify(params.target_files), autoResolve ? "resolved" : "open", JSON.stringify(respondentIds), autoResolve ? new Date().toISOString() : null, JSON.stringify(params.depends_on_files || []), JSON.stringify(params.exports_affected || []), keepOpen ? 0 : 600, assignedTo);
59
59
  this.log.info({
60
60
  thread_id: id,
@@ -81,7 +81,7 @@ export class Consultation {
81
81
  const id = randomUUID();
82
82
  // Simple token estimate: ~4 chars per token for English/French
83
83
  const tokenEstimate = Math.ceil(params.content.length / 4);
84
- db.prepare(`INSERT INTO thread_messages (id, thread_id, agent_id, agent_name, type, content, context_snapshot, in_reply_to, round, token_estimate)
84
+ db.prepare(`INSERT INTO thread_messages (id, thread_id, agent_id, agent_name, type, content, context_snapshot, in_reply_to, round, token_estimate)
85
85
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.thread_id, params.agent_id, params.agent_name || null, params.type, params.content, params.context_snapshot || null, params.in_reply_to || null, thread.round, tokenEstimate);
86
86
  this.log.debug({
87
87
  thread_id: params.thread_id,
@@ -195,21 +195,21 @@ export class Consultation {
195
195
  checkTimeouts() {
196
196
  const db = getDb();
197
197
  // Get threads that will be timed out (before updating them)
198
- const timedOut = db.prepare(`
199
- SELECT id FROM threads
200
- WHERE status IN ('open', 'resolving')
201
- AND timeout_seconds > 0
202
- AND datetime(created_at, '+' || (timeout_seconds * round) || ' seconds') < CURRENT_TIMESTAMP
198
+ const timedOut = db.prepare(`
199
+ SELECT id FROM threads
200
+ WHERE status IN ('open', 'resolving')
201
+ AND timeout_seconds > 0
202
+ AND datetime(created_at, '+' || (timeout_seconds * round) || ' seconds') < CURRENT_TIMESTAMP
203
203
  `).all();
204
204
  if (timedOut.length === 0)
205
205
  return;
206
- db.prepare(`
207
- UPDATE threads SET status = 'resolved',
208
- resolution_summary = 'Résolu par timeout — pas de réponse dans le délai',
209
- resolved_at = CURRENT_TIMESTAMP
210
- WHERE status IN ('open', 'resolving')
211
- AND timeout_seconds > 0
212
- AND datetime(created_at, '+' || (timeout_seconds * round) || ' seconds') < CURRENT_TIMESTAMP
206
+ db.prepare(`
207
+ UPDATE threads SET status = 'resolved',
208
+ resolution_summary = 'Résolu par timeout — pas de réponse dans le délai',
209
+ resolved_at = CURRENT_TIMESTAMP
210
+ WHERE status IN ('open', 'resolving')
211
+ AND timeout_seconds > 0
212
+ AND datetime(created_at, '+' || (timeout_seconds * round) || ' seconds') < CURRENT_TIMESTAMP
213
213
  `).run();
214
214
  this.log.info({ count: timedOut.length, thread_ids: timedOut.map(t => t.id) }, "Threads timed out");
215
215
  for (const t of timedOut) {
@@ -260,9 +260,9 @@ export class Consultation {
260
260
  }
261
261
  getThreadUpdates(agentId, since) {
262
262
  const db = getDb();
263
- let sql = `SELECT tm.* FROM thread_messages tm
264
- JOIN threads t ON tm.thread_id = t.id
265
- WHERE t.status IN ('open', 'resolving')
263
+ let sql = `SELECT tm.* FROM thread_messages tm
264
+ JOIN threads t ON tm.thread_id = t.id
265
+ WHERE t.status IN ('open', 'resolving')
266
266
  AND tm.agent_id != ?`;
267
267
  const params = [agentId];
268
268
  if (since) {
@@ -284,7 +284,7 @@ export class Consultation {
284
284
  logActionSummary(params) {
285
285
  const db = getDb();
286
286
  const id = randomUUID();
287
- db.prepare(`INSERT INTO action_summaries (id, session_id, agent_id, file_path, summary)
287
+ db.prepare(`INSERT INTO action_summaries (id, session_id, agent_id, file_path, summary)
288
288
  VALUES (?, ?, ?, ?, ?)`).run(id, params.session_id, params.agent_id, params.file_path || null, params.summary);
289
289
  return db.prepare("SELECT * FROM action_summaries WHERE id = ?").get(id);
290
290
  }
@@ -310,7 +310,7 @@ export class Consultation {
310
310
  const db = getDb();
311
311
  const thread = this.getThread(threadId);
312
312
  const id = randomUUID();
313
- db.prepare(`INSERT INTO thread_messages (id, thread_id, agent_id, type, content, round)
313
+ db.prepare(`INSERT INTO thread_messages (id, thread_id, agent_id, type, content, round)
314
314
  VALUES (?, ?, ?, ?, ?, ?)`).run(id, threadId, agentId, type, content, thread.round);
315
315
  }
316
316
  allRespondentsApproved(threadId) {
@@ -323,7 +323,7 @@ export class Consultation {
323
323
  // increments the round, and prior-round approvals must be re-collected
324
324
  // for the new proposal.
325
325
  const approvals = db
326
- .prepare(`SELECT DISTINCT agent_id FROM thread_messages
326
+ .prepare(`SELECT DISTINCT agent_id FROM thread_messages
327
327
  WHERE thread_id = ? AND type = 'approve' AND round = ?`)
328
328
  .all(threadId, thread.round);
329
329
  const approvedIds = new Set(approvals.map((a) => a.agent_id));
@@ -3,132 +3,132 @@ import { mkdirSync } from "fs";
3
3
  import { createRequire } from "module";
4
4
  const require = createRequire(import.meta.url);
5
5
  let db;
6
- const SCHEMA = `
7
- CREATE TABLE IF NOT EXISTS agents (
8
- id TEXT PRIMARY KEY,
9
- name TEXT NOT NULL,
10
- modules TEXT DEFAULT '[]',
11
- status TEXT DEFAULT 'offline',
12
- registered_at TEXT DEFAULT CURRENT_TIMESTAMP,
13
- last_seen_at TEXT DEFAULT CURRENT_TIMESTAMP
14
- );
15
-
16
- CREATE TABLE IF NOT EXISTS threads (
17
- id TEXT PRIMARY KEY,
18
- initiator_id TEXT NOT NULL,
19
- subject TEXT NOT NULL,
20
- plan TEXT,
21
- target_modules TEXT DEFAULT '[]',
22
- target_files TEXT DEFAULT '[]',
23
- status TEXT DEFAULT 'open',
24
- resolution_summary TEXT,
25
- conflicts TEXT,
26
- round INTEGER DEFAULT 1,
27
- max_rounds INTEGER DEFAULT 4,
28
- timeout_seconds INTEGER DEFAULT 600,
29
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
30
- resolved_at TEXT,
31
- expected_respondents TEXT,
32
- depends_on_files TEXT,
33
- exports_affected TEXT,
34
- claimed_by TEXT,
35
- claimed_at TEXT,
36
- FOREIGN KEY (initiator_id) REFERENCES agents(id)
37
- );
38
-
39
- CREATE TABLE IF NOT EXISTS thread_messages (
40
- id TEXT PRIMARY KEY,
41
- thread_id TEXT NOT NULL,
42
- agent_id TEXT NOT NULL,
43
- agent_name TEXT,
44
- type TEXT NOT NULL,
45
- content TEXT NOT NULL,
46
- context_snapshot TEXT,
47
- in_reply_to TEXT,
48
- round INTEGER NOT NULL,
49
- token_estimate INTEGER DEFAULT 0,
50
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
51
- FOREIGN KEY (thread_id) REFERENCES threads(id),
52
- FOREIGN KEY (agent_id) REFERENCES agents(id)
53
- );
54
-
55
- CREATE TABLE IF NOT EXISTS action_summaries (
56
- id TEXT PRIMARY KEY,
57
- session_id TEXT NOT NULL,
58
- agent_id TEXT NOT NULL,
59
- file_path TEXT,
60
- summary TEXT NOT NULL,
61
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
62
- FOREIGN KEY (agent_id) REFERENCES agents(id)
63
- );
64
-
65
- CREATE TABLE IF NOT EXISTS events (
66
- id INTEGER PRIMARY KEY AUTOINCREMENT,
67
- type TEXT NOT NULL,
68
- payload TEXT NOT NULL,
69
- created_at TEXT DEFAULT CURRENT_TIMESTAMP
70
- );
71
-
72
- CREATE TABLE IF NOT EXISTS dependency_map (
73
- module_id TEXT PRIMARY KEY,
74
- depends_on TEXT DEFAULT '[]',
75
- exports TEXT DEFAULT '[]',
76
- owners TEXT DEFAULT '[]'
77
- );
78
-
79
- CREATE TABLE IF NOT EXISTS file_activity (
80
- id INTEGER PRIMARY KEY AUTOINCREMENT,
81
- session_id TEXT NOT NULL,
82
- agent_id TEXT NOT NULL,
83
- agent_name TEXT,
84
- tool_name TEXT NOT NULL,
85
- file_path TEXT NOT NULL,
86
- module TEXT,
87
- created_at TEXT DEFAULT CURRENT_TIMESTAMP
88
- );
89
-
90
- CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status);
91
- CREATE INDEX IF NOT EXISTS idx_threads_initiator ON threads(initiator_id);
92
- CREATE INDEX IF NOT EXISTS idx_messages_thread ON thread_messages(thread_id);
93
- CREATE INDEX IF NOT EXISTS idx_messages_agent ON thread_messages(agent_id);
94
- CREATE INDEX IF NOT EXISTS idx_summaries_agent ON action_summaries(agent_id);
95
- CREATE INDEX IF NOT EXISTS idx_summaries_session ON action_summaries(session_id);
96
- CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
97
- CREATE INDEX IF NOT EXISTS idx_file_activity_agent ON file_activity(agent_id);
98
- CREATE INDEX IF NOT EXISTS idx_file_activity_path ON file_activity(file_path);
99
-
100
- CREATE TABLE IF NOT EXISTS introspections (
101
- id TEXT PRIMARY KEY,
102
- thread_id TEXT NOT NULL,
103
- agent_id TEXT NOT NULL,
104
- score INTEGER NOT NULL,
105
- reasons TEXT,
106
- status TEXT DEFAULT 'pending',
107
- response TEXT,
108
- concerned INTEGER DEFAULT 0,
109
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
110
- responded_at TEXT,
111
- FOREIGN KEY (thread_id) REFERENCES threads(id),
112
- FOREIGN KEY (agent_id) REFERENCES agents(id)
113
- );
114
-
115
- CREATE INDEX IF NOT EXISTS idx_introspections_agent ON introspections(agent_id);
116
- CREATE INDEX IF NOT EXISTS idx_introspections_status ON introspections(status);
117
-
118
- CREATE TABLE IF NOT EXISTS agent_activity_status (
119
- agent_id TEXT PRIMARY KEY,
120
- activity_status TEXT DEFAULT 'idle',
121
- current_file TEXT,
122
- current_thread TEXT,
123
- last_activity_at TEXT DEFAULT CURRENT_TIMESTAMP,
124
- FOREIGN KEY (agent_id) REFERENCES agents(id)
125
- );
126
-
127
- CREATE TABLE IF NOT EXISTS revoked_agents (
128
- agent_id TEXT PRIMARY KEY,
129
- revoked_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
130
- revoked_by TEXT NOT NULL
131
- );
6
+ const SCHEMA = `
7
+ CREATE TABLE IF NOT EXISTS agents (
8
+ id TEXT PRIMARY KEY,
9
+ name TEXT NOT NULL,
10
+ modules TEXT DEFAULT '[]',
11
+ status TEXT DEFAULT 'offline',
12
+ registered_at TEXT DEFAULT CURRENT_TIMESTAMP,
13
+ last_seen_at TEXT DEFAULT CURRENT_TIMESTAMP
14
+ );
15
+
16
+ CREATE TABLE IF NOT EXISTS threads (
17
+ id TEXT PRIMARY KEY,
18
+ initiator_id TEXT NOT NULL,
19
+ subject TEXT NOT NULL,
20
+ plan TEXT,
21
+ target_modules TEXT DEFAULT '[]',
22
+ target_files TEXT DEFAULT '[]',
23
+ status TEXT DEFAULT 'open',
24
+ resolution_summary TEXT,
25
+ conflicts TEXT,
26
+ round INTEGER DEFAULT 1,
27
+ max_rounds INTEGER DEFAULT 4,
28
+ timeout_seconds INTEGER DEFAULT 600,
29
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
30
+ resolved_at TEXT,
31
+ expected_respondents TEXT,
32
+ depends_on_files TEXT,
33
+ exports_affected TEXT,
34
+ claimed_by TEXT,
35
+ claimed_at TEXT,
36
+ FOREIGN KEY (initiator_id) REFERENCES agents(id)
37
+ );
38
+
39
+ CREATE TABLE IF NOT EXISTS thread_messages (
40
+ id TEXT PRIMARY KEY,
41
+ thread_id TEXT NOT NULL,
42
+ agent_id TEXT NOT NULL,
43
+ agent_name TEXT,
44
+ type TEXT NOT NULL,
45
+ content TEXT NOT NULL,
46
+ context_snapshot TEXT,
47
+ in_reply_to TEXT,
48
+ round INTEGER NOT NULL,
49
+ token_estimate INTEGER DEFAULT 0,
50
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
51
+ FOREIGN KEY (thread_id) REFERENCES threads(id),
52
+ FOREIGN KEY (agent_id) REFERENCES agents(id)
53
+ );
54
+
55
+ CREATE TABLE IF NOT EXISTS action_summaries (
56
+ id TEXT PRIMARY KEY,
57
+ session_id TEXT NOT NULL,
58
+ agent_id TEXT NOT NULL,
59
+ file_path TEXT,
60
+ summary TEXT NOT NULL,
61
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
62
+ FOREIGN KEY (agent_id) REFERENCES agents(id)
63
+ );
64
+
65
+ CREATE TABLE IF NOT EXISTS events (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ type TEXT NOT NULL,
68
+ payload TEXT NOT NULL,
69
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
70
+ );
71
+
72
+ CREATE TABLE IF NOT EXISTS dependency_map (
73
+ module_id TEXT PRIMARY KEY,
74
+ depends_on TEXT DEFAULT '[]',
75
+ exports TEXT DEFAULT '[]',
76
+ owners TEXT DEFAULT '[]'
77
+ );
78
+
79
+ CREATE TABLE IF NOT EXISTS file_activity (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ session_id TEXT NOT NULL,
82
+ agent_id TEXT NOT NULL,
83
+ agent_name TEXT,
84
+ tool_name TEXT NOT NULL,
85
+ file_path TEXT NOT NULL,
86
+ module TEXT,
87
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
88
+ );
89
+
90
+ CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status);
91
+ CREATE INDEX IF NOT EXISTS idx_threads_initiator ON threads(initiator_id);
92
+ CREATE INDEX IF NOT EXISTS idx_messages_thread ON thread_messages(thread_id);
93
+ CREATE INDEX IF NOT EXISTS idx_messages_agent ON thread_messages(agent_id);
94
+ CREATE INDEX IF NOT EXISTS idx_summaries_agent ON action_summaries(agent_id);
95
+ CREATE INDEX IF NOT EXISTS idx_summaries_session ON action_summaries(session_id);
96
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
97
+ CREATE INDEX IF NOT EXISTS idx_file_activity_agent ON file_activity(agent_id);
98
+ CREATE INDEX IF NOT EXISTS idx_file_activity_path ON file_activity(file_path);
99
+
100
+ CREATE TABLE IF NOT EXISTS introspections (
101
+ id TEXT PRIMARY KEY,
102
+ thread_id TEXT NOT NULL,
103
+ agent_id TEXT NOT NULL,
104
+ score INTEGER NOT NULL,
105
+ reasons TEXT,
106
+ status TEXT DEFAULT 'pending',
107
+ response TEXT,
108
+ concerned INTEGER DEFAULT 0,
109
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
110
+ responded_at TEXT,
111
+ FOREIGN KEY (thread_id) REFERENCES threads(id),
112
+ FOREIGN KEY (agent_id) REFERENCES agents(id)
113
+ );
114
+
115
+ CREATE INDEX IF NOT EXISTS idx_introspections_agent ON introspections(agent_id);
116
+ CREATE INDEX IF NOT EXISTS idx_introspections_status ON introspections(status);
117
+
118
+ CREATE TABLE IF NOT EXISTS agent_activity_status (
119
+ agent_id TEXT PRIMARY KEY,
120
+ activity_status TEXT DEFAULT 'idle',
121
+ current_file TEXT,
122
+ current_thread TEXT,
123
+ last_activity_at TEXT DEFAULT CURRENT_TIMESTAMP,
124
+ FOREIGN KEY (agent_id) REFERENCES agents(id)
125
+ );
126
+
127
+ CREATE TABLE IF NOT EXISTS revoked_agents (
128
+ agent_id TEXT PRIMARY KEY,
129
+ revoked_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
130
+ revoked_by TEXT NOT NULL
131
+ );
132
132
  `;
133
133
  function createBetterSqlite3(dataDir) {
134
134
  mkdirSync(dataDir, { recursive: true });
@@ -16,9 +16,9 @@ export class DependencyMapper {
16
16
  }
17
17
  setMap(map) {
18
18
  const db = getDb();
19
- const stmt = db.prepare(`INSERT INTO dependency_map (module_id, depends_on, exports, owners)
20
- VALUES (?, ?, ?, ?)
21
- ON CONFLICT(module_id) DO UPDATE SET
19
+ const stmt = db.prepare(`INSERT INTO dependency_map (module_id, depends_on, exports, owners)
20
+ VALUES (?, ?, ?, ?)
21
+ ON CONFLICT(module_id) DO UPDATE SET
22
22
  depends_on = excluded.depends_on, exports = excluded.exports, owners = excluded.owners`);
23
23
  const tx = db.transaction(() => {
24
24
  for (const [id, info] of Object.entries(map)) {
@@ -3,7 +3,7 @@ export class FileTracker {
3
3
  log(params) {
4
4
  const db = getDb();
5
5
  const module = this.fileToModule(params.file_path);
6
- db.prepare(`INSERT INTO file_activity (session_id, agent_id, agent_name, tool_name, file_path, module)
6
+ db.prepare(`INSERT INTO file_activity (session_id, agent_id, agent_name, tool_name, file_path, module)
7
7
  VALUES (?, ?, ?, ?, ?, ?)`).run(params.session_id, params.agent_id, params.agent_name || null, params.tool_name, params.file_path, module);
8
8
  }
9
9
  getBySession(sessionId) {
@@ -12,11 +12,11 @@ export class FileTracker {
12
12
  }
13
13
  getHotFiles(sinceMinutes = 30) {
14
14
  const db = getDb();
15
- const rows = db.prepare(`SELECT file_path, COUNT(DISTINCT agent_id) as agent_count, GROUP_CONCAT(DISTINCT agent_id) as agents
16
- FROM file_activity
17
- WHERE created_at > datetime('now', '-' || ? || ' minutes')
18
- GROUP BY file_path
19
- HAVING COUNT(DISTINCT agent_id) > 1
15
+ const rows = db.prepare(`SELECT file_path, COUNT(DISTINCT agent_id) as agent_count, GROUP_CONCAT(DISTINCT agent_id) as agents
16
+ FROM file_activity
17
+ WHERE created_at > datetime('now', '-' || ? || ' minutes')
18
+ GROUP BY file_path
19
+ HAVING COUNT(DISTINCT agent_id) > 1
20
20
  ORDER BY agent_count DESC`).all(sinceMinutes);
21
21
  return rows.map((r) => ({
22
22
  file_path: r.file_path,
@@ -26,8 +26,8 @@ export class FileTracker {
26
26
  }
27
27
  checkFileConflict(filePath, agentId, withinMinutes = 30) {
28
28
  const db = getDb();
29
- const rows = db.prepare(`SELECT DISTINCT agent_id FROM file_activity
30
- WHERE file_path = ? AND agent_id != ?
29
+ const rows = db.prepare(`SELECT DISTINCT agent_id FROM file_activity
30
+ WHERE file_path = ? AND agent_id != ?
31
31
  AND created_at > datetime('now', '-' || ? || ' minutes')`).all(filePath, agentId, withinMinutes);
32
32
  return { conflict: rows.length > 0, agents: rows.map((r) => r.agent_id) };
33
33
  }
@@ -4,7 +4,7 @@ export class IntrospectionManager {
4
4
  create(params) {
5
5
  const db = getDb();
6
6
  const id = randomUUID();
7
- db.prepare(`INSERT INTO introspections (id, thread_id, agent_id, score, reasons)
7
+ db.prepare(`INSERT INTO introspections (id, thread_id, agent_id, score, reasons)
8
8
  VALUES (?, ?, ?, ?, ?)`).run(id, params.thread_id, params.agent_id, params.score, JSON.stringify(params.reasons));
9
9
  return this.get(id);
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-coordinator",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Embedded MQTT broker + MCP server for multi-agent coordination",
5
5
  "type": "module",
6
6
  "license": "MIT",