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.
- package/README.md +779 -36
- package/dashboard/Dockerfile +19 -19
- package/dashboard/public/index.html +1178 -1178
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +220 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +199 -0
- package/dist/cli/server/index.js +2 -0
- package/dist/cli/server/logs.d.ts +2 -0
- package/dist/cli/server/logs.js +83 -0
- package/dist/cli/uninstall.d.ts +2 -0
- package/dist/cli/uninstall.js +146 -0
- package/dist/src/agent-activity.js +6 -6
- package/dist/src/agent-registry.js +6 -6
- package/dist/src/consultation.js +20 -20
- package/dist/src/database.js +126 -126
- package/dist/src/dependency-map.js +3 -3
- package/dist/src/file-tracker.js +8 -8
- package/dist/src/introspection.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
}
|
package/dist/src/consultation.js
CHANGED
|
@@ -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));
|
package/dist/src/database.js
CHANGED
|
@@ -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)) {
|
package/dist/src/file-tracker.js
CHANGED
|
@@ -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
|
}
|