mcp-coordinator 0.1.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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -0
  3. package/dashboard/Dockerfile +19 -0
  4. package/dashboard/public/index.html +1178 -0
  5. package/dist/cli/config.d.ts +14 -0
  6. package/dist/cli/config.js +58 -0
  7. package/dist/cli/dashboard.d.ts +2 -0
  8. package/dist/cli/dashboard.js +14 -0
  9. package/dist/cli/index.d.ts +2 -0
  10. package/dist/cli/index.js +13 -0
  11. package/dist/cli/server/index.d.ts +2 -0
  12. package/dist/cli/server/index.js +11 -0
  13. package/dist/cli/server/start.d.ts +2 -0
  14. package/dist/cli/server/start.js +57 -0
  15. package/dist/cli/server/status.d.ts +2 -0
  16. package/dist/cli/server/status.js +60 -0
  17. package/dist/cli/server/stop.d.ts +2 -0
  18. package/dist/cli/server/stop.js +59 -0
  19. package/dist/cli/version.d.ts +1 -0
  20. package/dist/cli/version.js +22 -0
  21. package/dist/src/agent-activity.d.ts +27 -0
  22. package/dist/src/agent-activity.js +70 -0
  23. package/dist/src/agent-registry.d.ts +10 -0
  24. package/dist/src/agent-registry.js +38 -0
  25. package/dist/src/auth.d.ts +22 -0
  26. package/dist/src/auth.js +91 -0
  27. package/dist/src/conflict-detector.d.ts +17 -0
  28. package/dist/src/conflict-detector.js +114 -0
  29. package/dist/src/consultation.d.ts +75 -0
  30. package/dist/src/consultation.js +332 -0
  31. package/dist/src/context-provider.d.ts +14 -0
  32. package/dist/src/context-provider.js +34 -0
  33. package/dist/src/database.d.ts +4 -0
  34. package/dist/src/database.js +194 -0
  35. package/dist/src/db-adapter.d.ts +15 -0
  36. package/dist/src/db-adapter.js +1 -0
  37. package/dist/src/dependency-map.d.ts +7 -0
  38. package/dist/src/dependency-map.js +76 -0
  39. package/dist/src/file-tracker.d.ts +21 -0
  40. package/dist/src/file-tracker.js +44 -0
  41. package/dist/src/impact-scorer.d.ts +31 -0
  42. package/dist/src/impact-scorer.js +112 -0
  43. package/dist/src/index.d.ts +2 -0
  44. package/dist/src/index.js +26 -0
  45. package/dist/src/introspection.d.ts +24 -0
  46. package/dist/src/introspection.js +28 -0
  47. package/dist/src/logger.d.ts +20 -0
  48. package/dist/src/logger.js +55 -0
  49. package/dist/src/mqtt-bridge.d.ts +40 -0
  50. package/dist/src/mqtt-bridge.js +173 -0
  51. package/dist/src/mqtt-broker.d.ts +23 -0
  52. package/dist/src/mqtt-broker.js +99 -0
  53. package/dist/src/plan-quality.d.ts +11 -0
  54. package/dist/src/plan-quality.js +30 -0
  55. package/dist/src/quota/credential-reader.d.ts +21 -0
  56. package/dist/src/quota/credential-reader.js +86 -0
  57. package/dist/src/quota/quota-cache.d.ts +93 -0
  58. package/dist/src/quota/quota-cache.js +177 -0
  59. package/dist/src/quota/quota.d.ts +47 -0
  60. package/dist/src/quota/quota.js +117 -0
  61. package/dist/src/serve-http.d.ts +5 -0
  62. package/dist/src/serve-http.js +775 -0
  63. package/dist/src/server-setup.d.ts +34 -0
  64. package/dist/src/server-setup.js +453 -0
  65. package/dist/src/sse-emitter.d.ts +10 -0
  66. package/dist/src/sse-emitter.js +35 -0
  67. package/dist/src/types.d.ts +121 -0
  68. package/dist/src/types.js +1 -0
  69. package/package.json +80 -0
@@ -0,0 +1,14 @@
1
+ export interface CoordinatorConfig {
2
+ server: {
3
+ port: number;
4
+ data_dir: string;
5
+ };
6
+ defaults: {
7
+ coordinator_url: string;
8
+ };
9
+ }
10
+ export declare function getConfigDir(): string;
11
+ export declare function ensureConfigDir(): string;
12
+ export declare function loadConfig(configDir?: string): CoordinatorConfig;
13
+ export declare function saveConfig(config: CoordinatorConfig, configDir?: string): void;
14
+ export declare function resolveValue(flag: unknown, envVar: string | undefined, configValue: unknown, defaultValue: unknown): unknown;
@@ -0,0 +1,58 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ const DEFAULT_CONFIG = {
5
+ server: {
6
+ port: 3100,
7
+ data_dir: join(homedir(), ".mcp-coordinator", "data"),
8
+ },
9
+ defaults: {
10
+ coordinator_url: "http://localhost:3100",
11
+ },
12
+ };
13
+ export function getConfigDir() {
14
+ return join(homedir(), ".mcp-coordinator");
15
+ }
16
+ export function ensureConfigDir() {
17
+ const dir = getConfigDir();
18
+ mkdirSync(dir, { recursive: true });
19
+ mkdirSync(join(dir, "data"), { recursive: true });
20
+ mkdirSync(join(dir, "logs"), { recursive: true });
21
+ return dir;
22
+ }
23
+ export function loadConfig(configDir) {
24
+ const dir = configDir ?? getConfigDir();
25
+ const configPath = join(dir, "config.json");
26
+ if (!existsSync(configPath)) {
27
+ return { ...DEFAULT_CONFIG };
28
+ }
29
+ try {
30
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
31
+ return {
32
+ server: {
33
+ port: raw.server?.port ?? DEFAULT_CONFIG.server.port,
34
+ data_dir: raw.server?.data_dir ?? DEFAULT_CONFIG.server.data_dir,
35
+ },
36
+ defaults: {
37
+ coordinator_url: raw.defaults?.coordinator_url ?? DEFAULT_CONFIG.defaults.coordinator_url,
38
+ },
39
+ };
40
+ }
41
+ catch {
42
+ return { ...DEFAULT_CONFIG };
43
+ }
44
+ }
45
+ export function saveConfig(config, configDir) {
46
+ const dir = configDir ?? getConfigDir();
47
+ mkdirSync(dir, { recursive: true });
48
+ writeFileSync(join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n");
49
+ }
50
+ export function resolveValue(flag, envVar, configValue, defaultValue) {
51
+ if (flag !== undefined)
52
+ return flag;
53
+ if (envVar !== undefined)
54
+ return envVar;
55
+ if (configValue !== undefined)
56
+ return configValue;
57
+ return defaultValue;
58
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createDashboardCommand(): Command;
@@ -0,0 +1,14 @@
1
+ import { Command } from "commander";
2
+ import { exec } from "child_process";
3
+ export function createDashboardCommand() {
4
+ return new Command("dashboard")
5
+ .description("Open the real-time dashboard")
6
+ .action(() => {
7
+ const url = "http://localhost:3100/dashboard";
8
+ console.log(`Dashboard: ${url}`);
9
+ const cmd = process.platform === "darwin"
10
+ ? `open "${url}"`
11
+ : `xdg-open "${url}" 2>/dev/null`;
12
+ exec(cmd, () => { });
13
+ });
14
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { createServerProgram } from "./server/index.js";
4
+ import { createDashboardCommand } from "./dashboard.js";
5
+ import { getVersion } from "./version.js";
6
+ const program = new Command();
7
+ program
8
+ .name("mcp-coordinator")
9
+ .description("Embedded MQTT broker + MCP server for multi-agent coordination")
10
+ .version(getVersion());
11
+ program.addCommand(createServerProgram());
12
+ program.addCommand(createDashboardCommand());
13
+ program.parse();
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createServerProgram(): Command;
@@ -0,0 +1,11 @@
1
+ import { Command } from "commander";
2
+ import { createServerStartCommand } from "./start.js";
3
+ import { createServerStopCommand } from "./stop.js";
4
+ import { createServerStatusCommand } from "./status.js";
5
+ export function createServerProgram() {
6
+ const server = new Command("server").description("Manage the coordination server");
7
+ server.addCommand(createServerStartCommand());
8
+ server.addCommand(createServerStopCommand());
9
+ server.addCommand(createServerStatusCommand());
10
+ return server;
11
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createServerStartCommand(): Command;
@@ -0,0 +1,57 @@
1
+ import { Command } from "commander";
2
+ import { writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { loadConfig, ensureConfigDir } from "../config.js";
5
+ export function createServerStartCommand() {
6
+ return new Command("start")
7
+ .description("Start the coordination server")
8
+ .option("--port <port>", "Server port")
9
+ .option("--data-dir <path>", "Data directory")
10
+ .option("--daemon", "Run as background daemon")
11
+ .action(async (opts) => {
12
+ const config = loadConfig();
13
+ const port = parseInt(opts.port ?? process.env.PORT ?? String(config.server.port), 10);
14
+ const dataDir = opts.dataDir ?? process.env.COORDINATOR_DATA_DIR ?? config.server.data_dir;
15
+ const configDir = ensureConfigDir();
16
+ if (opts.daemon) {
17
+ // Daemon mode: spawn self, redirect logs via shell
18
+ const { spawn } = await import("child_process");
19
+ const { openSync } = await import("fs");
20
+ const logPath = join(configDir, "logs", "server.log");
21
+ const logFd = openSync(logPath, "a");
22
+ // In compiled binary: process.execPath IS the binary, no argv[1] needed
23
+ // In dev (tsx): process.execPath is node, argv[1] is the script
24
+ const isBun = typeof globalThis.Bun !== "undefined";
25
+ const cmd = isBun ? process.execPath : process.execPath;
26
+ const args = isBun ? ["server", "start"] : [process.argv[1], "server", "start"];
27
+ const child = spawn(cmd, args, {
28
+ detached: true,
29
+ stdio: ["ignore", logFd, logFd],
30
+ env: { ...process.env, PORT: String(port), COORDINATOR_DATA_DIR: dataDir },
31
+ });
32
+ // Write PID file
33
+ writeFileSync(join(configDir, "server.pid"), String(child.pid));
34
+ child.unref();
35
+ console.log(`Coordinator started in background (PID ${child.pid}, port ${port})`);
36
+ console.log(` Logs: ${logPath}`);
37
+ console.log(` Stop: mcp-coordinator server stop`);
38
+ process.exit(0);
39
+ }
40
+ // Foreground mode: start server in-process
41
+ // Write PID file for server stop support
42
+ writeFileSync(join(configDir, "server.pid"), String(process.pid));
43
+ // Graceful shutdown
44
+ const { unlinkSync } = await import("fs");
45
+ const cleanup = () => {
46
+ try {
47
+ unlinkSync(join(configDir, "server.pid"));
48
+ }
49
+ catch { }
50
+ };
51
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
52
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
53
+ // Import and start server in-process
54
+ const { startServer } = await import("../../src/serve-http.js");
55
+ await startServer({ port, dataDir });
56
+ });
57
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createServerStatusCommand(): Command;
@@ -0,0 +1,60 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { execSync } from "child_process";
5
+ import { getConfigDir, loadConfig } from "../config.js";
6
+ export function createServerStatusCommand() {
7
+ return new Command("status")
8
+ .description("Show coordinator status")
9
+ .action(() => {
10
+ const configDir = getConfigDir();
11
+ const pidPath = join(configDir, "server.pid");
12
+ const config = loadConfig();
13
+ const port = config.server.port;
14
+ if (!existsSync(pidPath)) {
15
+ console.log("Coordinator: stopped");
16
+ return;
17
+ }
18
+ const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
19
+ let processAlive = false;
20
+ try {
21
+ process.kill(pid, 0);
22
+ processAlive = true;
23
+ }
24
+ catch { }
25
+ if (!processAlive) {
26
+ console.log("Coordinator: stopped (stale PID file)");
27
+ return;
28
+ }
29
+ // Health check
30
+ let health = {};
31
+ try {
32
+ const raw = execSync(`curl -s --max-time 3 http://localhost:${port}/health`, {
33
+ encoding: "utf-8",
34
+ stdio: ["pipe", "pipe", "pipe"],
35
+ });
36
+ health = JSON.parse(raw);
37
+ }
38
+ catch { }
39
+ if (health.status === "ok") {
40
+ let status = {};
41
+ try {
42
+ const raw = execSync(`curl -s --max-time 3 -X POST http://localhost:${port}/api/status`, {
43
+ encoding: "utf-8",
44
+ stdio: ["pipe", "pipe", "pipe"],
45
+ });
46
+ status = JSON.parse(raw);
47
+ }
48
+ catch { }
49
+ console.log(`Coordinator: running (PID ${pid}, port ${port})`);
50
+ if (status.online !== undefined) {
51
+ console.log(`Agents: ${status.online} online`);
52
+ console.log(`Threads: ${status.open_threads} open`);
53
+ }
54
+ console.log(`Dashboard: http://localhost:${port}/dashboard`);
55
+ }
56
+ else {
57
+ console.log(`Coordinator: running (PID ${pid}) but health check failed`);
58
+ }
59
+ });
60
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createServerStopCommand(): Command;
@@ -0,0 +1,59 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync, unlinkSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { getConfigDir } from "../config.js";
5
+ export function createServerStopCommand() {
6
+ return new Command("stop")
7
+ .description("Stop the coordination server")
8
+ .action(() => {
9
+ const pidPath = join(getConfigDir(), "server.pid");
10
+ if (!existsSync(pidPath)) {
11
+ console.error("No server PID file found. Is the server running?");
12
+ process.exit(1);
13
+ }
14
+ const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
15
+ if (isNaN(pid)) {
16
+ console.error("Invalid PID file. Removing.");
17
+ unlinkSync(pidPath);
18
+ process.exit(1);
19
+ }
20
+ // Check if process is alive
21
+ try {
22
+ process.kill(pid, 0);
23
+ }
24
+ catch {
25
+ console.log(`Server (PID ${pid}) is not running. Cleaning up PID file.`);
26
+ unlinkSync(pidPath);
27
+ return;
28
+ }
29
+ // Send SIGTERM
30
+ console.log(`Stopping coordinator (PID ${pid})...`);
31
+ process.kill(pid, "SIGTERM");
32
+ // Wait up to 5 seconds
33
+ const deadline = Date.now() + 5000;
34
+ const check = () => {
35
+ try {
36
+ process.kill(pid, 0);
37
+ if (Date.now() < deadline) {
38
+ setTimeout(check, 200);
39
+ }
40
+ else {
41
+ console.log("Server did not stop gracefully. Sending SIGKILL.");
42
+ process.kill(pid, "SIGKILL");
43
+ try {
44
+ unlinkSync(pidPath);
45
+ }
46
+ catch { }
47
+ }
48
+ }
49
+ catch {
50
+ console.log("Coordinator stopped.");
51
+ try {
52
+ unlinkSync(pidPath);
53
+ }
54
+ catch { }
55
+ }
56
+ };
57
+ check();
58
+ });
59
+ }
@@ -0,0 +1 @@
1
+ export declare function getVersion(): string;
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from "fs";
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, resolve } from "path";
4
+ export function getVersion() {
5
+ // dist/cli/version.js -> ../../package.json
6
+ // cli/version.ts (tsx) -> ../package.json
7
+ // Wrap fileURLToPath in the try as well — under Bun --compile, import.meta.url
8
+ // may be a synthetic non-file URL that throws TypeError on fileURLToPath.
9
+ try {
10
+ const here = dirname(fileURLToPath(import.meta.url));
11
+ for (const candidate of [resolve(here, "..", "package.json"), resolve(here, "..", "..", "package.json")]) {
12
+ try {
13
+ const json = JSON.parse(readFileSync(candidate, "utf-8"));
14
+ if (json.version)
15
+ return json.version;
16
+ }
17
+ catch { }
18
+ }
19
+ }
20
+ catch { }
21
+ return "0.0.0";
22
+ }
@@ -0,0 +1,27 @@
1
+ import type { AgentRegistry } from "./agent-registry.js";
2
+ import type { AgentActivity } from "./types.js";
3
+ interface HeartbeatPayload {
4
+ currentFile: string | null;
5
+ currentThread: string | null;
6
+ }
7
+ interface GetActivityOptions {
8
+ idleAfterMinutes?: number;
9
+ }
10
+ export declare class AgentActivityTracker {
11
+ private registry;
12
+ constructor(registry: AgentRegistry);
13
+ /** Report file edit activity → status becomes "working" */
14
+ reportFileActivity(agentId: string, filePath: string): void;
15
+ /** Report agent is waiting on a consultation thread */
16
+ reportWaiting(agentId: string, threadId: string): void;
17
+ /** Report agent went offline → clear all activity */
18
+ reportOffline(agentId: string): void;
19
+ /** Enriched heartbeat — derives status from current state */
20
+ heartbeat(agentId: string, payload: HeartbeatPayload): void;
21
+ /** Get activity for a single agent, with optional idle timeout */
22
+ getActivity(agentId: string, options?: GetActivityOptions): AgentActivity;
23
+ /** List activity for all online agents */
24
+ listAll(options?: GetActivityOptions): AgentActivity[];
25
+ private upsert;
26
+ }
27
+ export {};
@@ -0,0 +1,70 @@
1
+ import { getDb } from "./database.js";
2
+ export class AgentActivityTracker {
3
+ registry;
4
+ constructor(registry) {
5
+ this.registry = registry;
6
+ }
7
+ /** Report file edit activity → status becomes "working" */
8
+ reportFileActivity(agentId, filePath) {
9
+ this.upsert(agentId, "working", filePath, null);
10
+ }
11
+ /** Report agent is waiting on a consultation thread */
12
+ reportWaiting(agentId, threadId) {
13
+ this.upsert(agentId, "waiting", null, threadId);
14
+ }
15
+ /** Report agent went offline → clear all activity */
16
+ reportOffline(agentId) {
17
+ this.upsert(agentId, "offline", null, null);
18
+ }
19
+ /** Enriched heartbeat — derives status from current state */
20
+ heartbeat(agentId, payload) {
21
+ let status;
22
+ if (payload.currentFile) {
23
+ status = "working";
24
+ }
25
+ else if (payload.currentThread) {
26
+ status = "waiting";
27
+ }
28
+ else {
29
+ status = "idle";
30
+ }
31
+ this.upsert(agentId, status, payload.currentFile, payload.currentThread);
32
+ }
33
+ /** Get activity for a single agent, with optional idle timeout */
34
+ getActivity(agentId, options) {
35
+ const agent = this.registry.get(agentId);
36
+ if (!agent || agent.status === "offline") {
37
+ return { agent_id: agentId, activity_status: "offline", current_file: null, current_thread: null, last_activity_at: new Date().toISOString() };
38
+ }
39
+ const db = getDb();
40
+ const row = db.prepare("SELECT * FROM agent_activity_status WHERE agent_id = ?").get(agentId);
41
+ if (!row) {
42
+ return { agent_id: agentId, activity_status: "idle", current_file: null, current_thread: null, last_activity_at: new Date().toISOString() };
43
+ }
44
+ // Check idle timeout: if working but no activity for X minutes → idle
45
+ if (row.activity_status === "working" && options?.idleAfterMinutes) {
46
+ const lastActivity = new Date(row.last_activity_at.replace(" ", "T") + "Z").getTime();
47
+ const threshold = options.idleAfterMinutes * 60 * 1000;
48
+ if (Date.now() - lastActivity > threshold) {
49
+ return { ...row, activity_status: "idle" };
50
+ }
51
+ }
52
+ return row;
53
+ }
54
+ /** List activity for all online agents */
55
+ listAll(options) {
56
+ const onlineAgents = this.registry.listOnline();
57
+ return onlineAgents.map((agent) => this.getActivity(agent.id, options));
58
+ }
59
+ // ── Private ──
60
+ upsert(agentId, status, file, thread) {
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,
68
+ last_activity_at = CURRENT_TIMESTAMP`).run(agentId, status, file, thread);
69
+ }
70
+ }
@@ -0,0 +1,10 @@
1
+ import type { Agent } from "./types.js";
2
+ export declare class AgentRegistry {
3
+ register(agentId: string, name: string, modules: string[]): Agent;
4
+ get(agentId: string): Agent | undefined;
5
+ listOnline(): Agent[];
6
+ listAll(): Agent[];
7
+ setOnline(agentId: string): void;
8
+ setOffline(agentId: string): void;
9
+ heartbeat(agentId: string): void;
10
+ }
@@ -0,0 +1,38 @@
1
+ import { getDb } from "./database.js";
2
+ export class AgentRegistry {
3
+ register(agentId, name, modules) {
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',
11
+ last_seen_at = CURRENT_TIMESTAMP`).run(agentId, name, JSON.stringify(modules));
12
+ return this.get(agentId);
13
+ }
14
+ get(agentId) {
15
+ const db = getDb();
16
+ return db.prepare("SELECT * FROM agents WHERE id = ?").get(agentId);
17
+ }
18
+ listOnline() {
19
+ const db = getDb();
20
+ return db.prepare("SELECT * FROM agents WHERE status = 'online' ORDER BY name").all();
21
+ }
22
+ listAll() {
23
+ const db = getDb();
24
+ return db.prepare("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
25
+ }
26
+ setOnline(agentId) {
27
+ const db = getDb();
28
+ db.prepare("UPDATE agents SET status = 'online', last_seen_at = CURRENT_TIMESTAMP WHERE id = ?").run(agentId);
29
+ }
30
+ setOffline(agentId) {
31
+ const db = getDb();
32
+ db.prepare("UPDATE agents SET status = 'offline', last_seen_at = CURRENT_TIMESTAMP WHERE id = ?").run(agentId);
33
+ }
34
+ heartbeat(agentId) {
35
+ const db = getDb();
36
+ db.prepare("UPDATE agents SET last_seen_at = CURRENT_TIMESTAMP WHERE id = ?").run(agentId);
37
+ }
38
+ }
@@ -0,0 +1,22 @@
1
+ import type { IncomingMessage } from "http";
2
+ import { type Logger } from "./logger.js";
3
+ export declare function setAuthLogger(logger: Logger): void;
4
+ export interface AuthClaims {
5
+ sub: string;
6
+ role: "agent" | "admin";
7
+ }
8
+ export declare function initAuth(secret: string, expiry?: string): void;
9
+ export declare function createToken(agentId: string, role: "agent" | "admin", expiry?: string): Promise<string>;
10
+ export declare function verifyToken(token: string): Promise<AuthClaims>;
11
+ export declare function refreshToken(token: string, gracePeriod?: string): Promise<string>;
12
+ export declare function isRevoked(agentId: string): boolean;
13
+ export declare function revokeAgent(agentId: string, revokedBy: string): void;
14
+ export type AuthResult = {
15
+ ok: true;
16
+ claims: AuthClaims;
17
+ } | {
18
+ ok: false;
19
+ status: 401 | 403;
20
+ error: string;
21
+ };
22
+ export declare function authenticateRequest(req: IncomingMessage): Promise<AuthResult>;
@@ -0,0 +1,91 @@
1
+ import { SignJWT, jwtVerify, errors } from "jose";
2
+ import { randomUUID } from "crypto";
3
+ import { getDb } from "./database.js";
4
+ import { silentLogger } from "./logger.js";
5
+ let signingKey;
6
+ let defaultExpiry = "24h";
7
+ let log = silentLogger;
8
+ export function setAuthLogger(logger) {
9
+ log = logger;
10
+ }
11
+ export function initAuth(secret, expiry) {
12
+ signingKey = new TextEncoder().encode(secret);
13
+ if (expiry)
14
+ defaultExpiry = expiry;
15
+ }
16
+ export async function createToken(agentId, role, expiry) {
17
+ return new SignJWT({ role })
18
+ .setProtectedHeader({ alg: "HS256" })
19
+ .setSubject(agentId)
20
+ .setJti(randomUUID())
21
+ .setIssuedAt()
22
+ .setExpirationTime(expiry || defaultExpiry)
23
+ .sign(signingKey);
24
+ }
25
+ export async function verifyToken(token) {
26
+ const { payload } = await jwtVerify(token, signingKey);
27
+ if (!payload.sub)
28
+ throw new Error("Missing sub claim in token");
29
+ const role = payload.role;
30
+ if (role !== "agent" && role !== "admin")
31
+ throw new Error("Invalid role in token");
32
+ return { sub: payload.sub, role };
33
+ }
34
+ export async function refreshToken(token, gracePeriod = "1h") {
35
+ let claims;
36
+ try {
37
+ claims = await verifyToken(token);
38
+ }
39
+ catch (err) {
40
+ if (err instanceof errors.JWTExpired) {
41
+ const { payload } = await jwtVerify(token, signingKey, {
42
+ clockTolerance: gracePeriod,
43
+ });
44
+ if (!payload.sub)
45
+ throw new Error("Missing sub claim in token");
46
+ const role = payload.role;
47
+ if (role !== "agent" && role !== "admin")
48
+ throw new Error("Invalid role in token");
49
+ claims = { sub: payload.sub, role };
50
+ }
51
+ else {
52
+ throw err;
53
+ }
54
+ }
55
+ return createToken(claims.sub, claims.role);
56
+ }
57
+ export function isRevoked(agentId) {
58
+ const db = getDb();
59
+ const row = db.prepare("SELECT 1 FROM revoked_agents WHERE agent_id = ?").get(agentId);
60
+ return !!row;
61
+ }
62
+ export function revokeAgent(agentId, revokedBy) {
63
+ const db = getDb();
64
+ db.prepare("INSERT OR IGNORE INTO revoked_agents (agent_id, revoked_by) VALUES (?, ?)").run(agentId, revokedBy);
65
+ }
66
+ const ADMIN_ONLY_ROUTES = ["/api/auth/revoke", "/api/reset"];
67
+ export async function authenticateRequest(req) {
68
+ const authHeader = req.headers.authorization;
69
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
70
+ return { ok: false, status: 401, error: "Missing or invalid Authorization header" };
71
+ }
72
+ const token = authHeader.slice(7);
73
+ let claims;
74
+ try {
75
+ claims = await verifyToken(token);
76
+ }
77
+ catch (err) {
78
+ log.error({ err }, "JWT verification error");
79
+ return { ok: false, status: 401, error: "Invalid or expired token" };
80
+ }
81
+ if (isRevoked(claims.sub)) {
82
+ return { ok: false, status: 403, error: "Agent has been revoked" };
83
+ }
84
+ const url = req.url || "";
85
+ // Strip query string and hash before matching — "/api/reset?x=1" must hit the check
86
+ const pathOnly = url.split(/[?#]/)[0];
87
+ if (ADMIN_ONLY_ROUTES.some((r) => pathOnly === r) && claims.role !== "admin") {
88
+ return { ok: false, status: 403, error: "Admin access required" };
89
+ }
90
+ return { ok: true, claims };
91
+ }
@@ -0,0 +1,17 @@
1
+ import { type Logger } from "./logger.js";
2
+ import type { ConflictReport } from "./types.js";
3
+ import type { Consultation } from "./consultation.js";
4
+ import type { DependencyMapper } from "./dependency-map.js";
5
+ import type { FileTracker } from "./file-tracker.js";
6
+ export declare class ConflictDetector {
7
+ private consultation;
8
+ private depMap;
9
+ private fileTracker;
10
+ private log;
11
+ constructor(consultation: Consultation, depMap: DependencyMapper, fileTracker: FileTracker, logger?: Logger);
12
+ detect(params: {
13
+ agent_id: string;
14
+ target_modules: string[];
15
+ target_files: string[];
16
+ }): ConflictReport[];
17
+ }