@spencerbeggs/claude-coordinator-server 0.1.0 → 0.1.1

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,15 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_PORT, createServer, DEFAULT_HOST } from "../36.js";
3
- const port = Number(process.env.PORT) || DEFAULT_PORT;
4
- const host = process.env.HOST ?? DEFAULT_HOST;
2
+ import { createServer } from "../server.js";
3
+ import { DEFAULT_HOST, DEFAULT_PORT } from "@spencerbeggs/claude-coordinator-core";
4
+
5
+ //#region src/bin/cli.ts
5
6
  const server = createServer({
6
- port: port,
7
- host: host
7
+ port: Number(process.env.PORT) || DEFAULT_PORT,
8
+ host: process.env.HOST ?? DEFAULT_HOST
8
9
  });
9
- const shutdown = ()=>{
10
- server.close();
11
- process.exit(0);
10
+ const shutdown = () => {
11
+ server.close();
12
+ process.exit(0);
12
13
  };
13
14
  process.on("SIGINT", shutdown);
14
15
  process.on("SIGTERM", shutdown);
15
- console.error("[coordinator] Server started. Press Ctrl+C to stop.");
16
+ console.error(`[coordinator] Server started. Press Ctrl+C to stop.`);
17
+
18
+ //#endregion
19
+ export { };
package/index.d.ts CHANGED
@@ -1,129 +1,106 @@
1
- /**
2
- * \@spencerbeggs/claude-coordinator-server
3
- *
4
- * tRPC WebSocket server for the Claude Coordinator system.
5
- * Provides real-time communication between Claude Code instances.
6
- *
7
- * @packageDocumentation
8
- */
9
-
10
- import type { Agent } from '@spencerbeggs/claude-coordinator-core';
11
- import type { ContextEntry } from '@spencerbeggs/claude-coordinator-core';
12
- import type { Decision } from '@spencerbeggs/claude-coordinator-core';
13
- import { EventEmitter } from 'node:events';
14
- import type { Question } from '@spencerbeggs/claude-coordinator-core';
15
- import { TRPCDefaultErrorShape } from '@trpc/server';
16
- import { TRPCRootObject } from '@trpc/server';
17
- import { TRPCRuntimeConfigOptions } from '@trpc/server';
18
- import { WebSocketServer } from 'ws';
19
-
20
- /**
21
- * Type exports for clients
22
- */
23
- export declare type AppRouter = typeof appRouter;
24
-
25
- /**
26
- * Main application router
27
- */
28
- export declare const appRouter: typeof t.router extends (...args: any) => infer R ? R : unknown;
29
-
30
- /**
31
- * Context passed to each tRPC procedure
32
- */
33
- export declare interface Context {
34
- state: CoordinatorState;
35
- agentId?: string;
36
- }
37
-
38
- /**
39
- * Server instance with cleanup method
40
- */
41
- export declare interface CoordinatorServer {
42
- wss: WebSocketServer;
43
- close: () => void;
44
- }
45
-
46
- /**
47
- * EventEmitter-based state manager for the coordinator
48
- */
49
- export declare class CoordinatorState extends EventEmitter<CoordinatorStateEvents> {
50
- private sessionId;
51
- private agents;
52
- private context;
53
- private questions;
54
- private decisions;
55
- constructor(sessionId: string);
56
- getSessionId(): string;
57
- addAgent(agent: Agent): void;
58
- removeAgent(agentId: string): boolean;
59
- getAgent(agentId: string): Agent | undefined;
60
- listAgents(): Agent[];
61
- setContext(entry: ContextEntry): void;
62
- getContext(key: string): ContextEntry | undefined;
63
- listContext(filters?: {
64
- tags?: string[];
65
- createdBy?: string;
66
- }): ContextEntry[];
67
- addQuestion(question: Question): void;
68
- answerQuestion(questionId: string, answer: string, answeredBy: string): Question | undefined;
69
- getQuestion(questionId: string): Question | undefined;
70
- listPendingQuestions(forAgentId?: string): Question[];
71
- addDecision(decision: Decision): void;
72
- listDecisions(): Decision[];
73
- }
74
-
75
- /**
76
- * Events emitted by the coordinator state
77
- */
78
- export declare interface CoordinatorStateEvents {
79
- agentChange: [agents: Agent[]];
80
- contextChange: [entry: ContextEntry];
81
- question: [question: Question];
82
- answer: [question: Question];
83
- }
84
-
85
- /**
86
- * Create context for a request
87
- */
88
- export declare function createContext(): Context;
89
-
90
- /**
91
- * Create and start the coordinator WebSocket server
92
- */
93
- export declare function createServer(options?: ServerOptions): CoordinatorServer;
94
-
95
- export declare function getOrCreateState(sessionId?: string): CoordinatorState;
96
-
97
- /**
98
- * Resets the global coordinator state singleton to null.
99
- *
100
- * @remarks
101
- * Call this in test teardown (e.g., `afterEach` or `afterAll`) to ensure clean
102
- * state between test suites. This prevents state from leaking across tests when
103
- * using the singleton pattern.
104
- *
105
- * @example
106
- * ```ts
107
- * afterEach(() => {
108
- * resetState();
109
- * });
110
- * ```
111
- */
112
- export declare function resetState(): void;
113
-
114
- /**
115
- * Options for creating the coordinator server
116
- */
117
- export declare interface ServerOptions {
118
- port?: number;
119
- host?: string;
120
- }
121
-
122
- declare const t: TRPCRootObject<Context, object, TRPCRuntimeConfigOptions<Context, object>, {
123
- ctx: Context;
124
- meta: object;
125
- errorShape: TRPCDefaultErrorShape;
126
- transformer: false;
127
- }>;
128
-
129
- export { }
1
+ import { EventEmitter } from "node:events";
2
+ import { Agent, ContextEntry, Decision, ListContextInput, Question } from "@spencerbeggs/claude-coordinator-core";
3
+ import { WebSocketServer } from "ws";
4
+
5
+ //#region src/state.d.ts
6
+ /**
7
+ * Events emitted by the coordinator state
8
+ */
9
+ interface CoordinatorStateEvents {
10
+ agentChange: [agents: Agent[]];
11
+ contextChange: [entry: ContextEntry];
12
+ question: [question: Question];
13
+ answer: [question: Question];
14
+ }
15
+ /**
16
+ * EventEmitter-based state manager for the coordinator
17
+ */
18
+ declare class CoordinatorState extends EventEmitter<CoordinatorStateEvents> {
19
+ private sessionId;
20
+ private agents;
21
+ private context;
22
+ private questions;
23
+ private decisions;
24
+ constructor(sessionId: string);
25
+ getSessionId(): string;
26
+ addAgent(agent: Agent): void;
27
+ removeAgent(agentId: string): boolean;
28
+ getAgent(agentId: string): Agent | undefined;
29
+ listAgents(): Agent[];
30
+ setContext(entry: ContextEntry): void;
31
+ getContext(key: string): ContextEntry | undefined;
32
+ listContext(filters?: ListContextInput): ContextEntry[];
33
+ addQuestion(question: Question): void;
34
+ answerQuestion(questionId: string, answer: string, answeredBy: string): Question | undefined;
35
+ getQuestion(questionId: string): Question | undefined;
36
+ listPendingQuestions(forAgentId?: string): Question[];
37
+ addDecision(decision: Decision): void;
38
+ listDecisions(): Decision[];
39
+ }
40
+ declare function getOrCreateState(sessionId?: string): CoordinatorState;
41
+ /**
42
+ * Resets the global coordinator state singleton to null.
43
+ *
44
+ * @remarks
45
+ * Call this in test teardown (e.g., `afterEach` or `afterAll`) to ensure clean
46
+ * state between test suites. This prevents state from leaking across tests when
47
+ * using the singleton pattern.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * afterEach(() => {
52
+ * resetState();
53
+ * });
54
+ * ```
55
+ */
56
+ declare function resetState(): void;
57
+ //#endregion
58
+ //#region src/router.d.ts
59
+ /**
60
+ * Context passed to each tRPC procedure
61
+ */
62
+ interface Context {
63
+ state: CoordinatorState;
64
+ agentId?: string;
65
+ }
66
+ declare const t: import("@trpc/server").TRPCRootObject<Context, object, import("@trpc/server").TRPCRuntimeConfigOptions<Context, object>, {
67
+ ctx: Context;
68
+ meta: object;
69
+ errorShape: import("@trpc/server").TRPCDefaultErrorShape;
70
+ transformer: false;
71
+ }>;
72
+ /**
73
+ * Main application router
74
+ */
75
+ declare const appRouter: typeof t.router extends ((...args: any) => infer R) ? R : unknown;
76
+ /**
77
+ * Type exports for clients
78
+ */
79
+ type AppRouter = typeof appRouter;
80
+ /**
81
+ * Create context for a request
82
+ */
83
+ declare function createContext(): Context;
84
+ //#endregion
85
+ //#region src/server.d.ts
86
+ /**
87
+ * Options for creating the coordinator server
88
+ */
89
+ interface ServerOptions {
90
+ port?: number;
91
+ host?: string;
92
+ }
93
+ /**
94
+ * Server instance with cleanup method
95
+ */
96
+ interface CoordinatorServer {
97
+ wss: WebSocketServer;
98
+ close: () => void;
99
+ }
100
+ /**
101
+ * Create and start the coordinator WebSocket server
102
+ */
103
+ declare function createServer(options?: ServerOptions): CoordinatorServer;
104
+ //#endregion
105
+ export { type AppRouter, type Context, type CoordinatorServer, CoordinatorState, type CoordinatorStateEvents, type ServerOptions, appRouter, createContext, createServer, getOrCreateState, resetState };
106
+ //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1 +1,5 @@
1
- export { CoordinatorState, appRouter, createContext, createServer, getOrCreateState, resetState } from "./36.js";
1
+ import { CoordinatorState, getOrCreateState, resetState } from "./state.js";
2
+ import { appRouter, createContext } from "./router.js";
3
+ import { createServer } from "./server.js";
4
+
5
+ export { CoordinatorState, appRouter, createContext, createServer, getOrCreateState, resetState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spencerbeggs/claude-coordinator-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "description": "tRPC WebSocket server for Claude Coordinator",
6
6
  "keywords": [
@@ -21,32 +21,25 @@
21
21
  "email": "spencer@beggs.codes",
22
22
  "url": "https://spencerbeg.gs"
23
23
  },
24
+ "sideEffects": false,
24
25
  "type": "module",
25
26
  "exports": {
26
27
  ".": {
27
28
  "types": "./index.d.ts",
28
29
  "import": "./index.js"
29
- }
30
+ },
31
+ "./package.json": "./package.json"
30
32
  },
31
33
  "bin": {
32
- "claude-coordinator-server": "./bin/claude-coordinator-server.js"
34
+ "claude-coordinator-server": "bin/claude-coordinator-server.js"
33
35
  },
34
36
  "dependencies": {
35
37
  "@spencerbeggs/claude-coordinator-core": "0.1.0",
36
- "@trpc/server": "^11.8.1",
37
- "ws": "^8.19.0",
38
- "zod": "^4.3.5"
38
+ "@trpc/server": "^11.18.0",
39
+ "ws": "^8.21.0",
40
+ "zod": "^4.4.3"
39
41
  },
40
42
  "engines": {
41
- "node": ">=20.0.0"
42
- },
43
- "files": [
44
- "36.js",
45
- "LICENSE",
46
- "README.md",
47
- "bin/claude-coordinator-server.js",
48
- "index.d.ts",
49
- "index.js",
50
- "package.json"
51
- ]
52
- }
43
+ "node": ">=24.11.0"
44
+ }
45
+ }
package/router.js ADDED
@@ -0,0 +1,149 @@
1
+ import { getOrCreateState } from "./state.js";
2
+ import { AnswerInputSchema, AskInputSchema, GetContextInputSchema, JoinInputSchema, ListContextInputSchema, LogDecisionInputSchema, ShareContextInputSchema } from "@spencerbeggs/claude-coordinator-core";
3
+ import { initTRPC } from "@trpc/server";
4
+ import { observable } from "@trpc/server/observable";
5
+ import { z } from "zod";
6
+
7
+ //#region src/router.ts
8
+ const t = initTRPC.context().create();
9
+ const publicProcedure = t.procedure;
10
+ const sessionRouter = t.router({
11
+ join: publicProcedure.input(JoinInputSchema).mutation(({ input, ctx }) => {
12
+ const agent = {
13
+ id: crypto.randomUUID(),
14
+ name: input.name,
15
+ role: input.role,
16
+ repoPath: input.repoPath,
17
+ connectedAt: /* @__PURE__ */ new Date()
18
+ };
19
+ ctx.state.addAgent(agent);
20
+ return {
21
+ agent,
22
+ sessionId: ctx.state.getSessionId()
23
+ };
24
+ }),
25
+ leave: publicProcedure.input(z.object({ agentId: z.string().uuid() })).mutation(({ input, ctx }) => {
26
+ return { success: ctx.state.removeAgent(input.agentId) };
27
+ }),
28
+ list: publicProcedure.query(({ ctx }) => {
29
+ return ctx.state.listAgents();
30
+ }),
31
+ onAgentChange: publicProcedure.subscription(({ ctx }) => {
32
+ return observable((emit) => {
33
+ const handler = (agents) => {
34
+ emit.next(agents);
35
+ };
36
+ ctx.state.on("agentChange", handler);
37
+ emit.next(ctx.state.listAgents());
38
+ return () => {
39
+ ctx.state.off("agentChange", handler);
40
+ };
41
+ });
42
+ })
43
+ });
44
+ const contextRouter = t.router({
45
+ share: publicProcedure.input(ShareContextInputSchema.extend({ agentId: z.string().uuid() })).mutation(({ input, ctx }) => {
46
+ const now = /* @__PURE__ */ new Date();
47
+ const existing = ctx.state.getContext(input.key);
48
+ const entry = {
49
+ id: existing?.id ?? crypto.randomUUID(),
50
+ key: input.key,
51
+ value: input.value,
52
+ tags: input.tags ?? [],
53
+ createdBy: existing?.createdBy ?? input.agentId,
54
+ createdAt: existing?.createdAt ?? now,
55
+ updatedAt: now
56
+ };
57
+ ctx.state.setContext(entry);
58
+ return entry;
59
+ }),
60
+ get: publicProcedure.input(GetContextInputSchema).query(({ input, ctx }) => {
61
+ return ctx.state.getContext(input.key) ?? null;
62
+ }),
63
+ list: publicProcedure.input(ListContextInputSchema.optional()).query(({ input, ctx }) => {
64
+ return ctx.state.listContext(input);
65
+ }),
66
+ onContextChange: publicProcedure.subscription(({ ctx }) => {
67
+ return observable((emit) => {
68
+ const handler = (entry) => {
69
+ emit.next(entry);
70
+ };
71
+ ctx.state.on("contextChange", handler);
72
+ return () => {
73
+ ctx.state.off("contextChange", handler);
74
+ };
75
+ });
76
+ })
77
+ });
78
+ const questionsRouter = t.router({
79
+ ask: publicProcedure.input(AskInputSchema.extend({ agentId: z.string().uuid() })).mutation(({ input, ctx }) => {
80
+ const question = {
81
+ id: crypto.randomUUID(),
82
+ question: input.question,
83
+ from: input.agentId,
84
+ to: input.to,
85
+ status: "pending",
86
+ createdAt: /* @__PURE__ */ new Date()
87
+ };
88
+ ctx.state.addQuestion(question);
89
+ return question;
90
+ }),
91
+ answer: publicProcedure.input(AnswerInputSchema.extend({ agentId: z.string().uuid() })).mutation(({ input, ctx }) => {
92
+ const answered = ctx.state.answerQuestion(input.questionId, input.answer, input.agentId);
93
+ if (!answered) throw new Error(`Question not found: ${input.questionId}`);
94
+ return answered;
95
+ }),
96
+ listPending: publicProcedure.input(z.object({ agentId: z.string().uuid().optional() }).optional()).query(({ input, ctx }) => {
97
+ return ctx.state.listPendingQuestions(input?.agentId);
98
+ }),
99
+ onQuestion: publicProcedure.subscription(({ ctx }) => {
100
+ return observable((emit) => {
101
+ const questionHandler = (question) => {
102
+ emit.next(question);
103
+ };
104
+ const answerHandler = (question) => {
105
+ emit.next(question);
106
+ };
107
+ ctx.state.on("question", questionHandler);
108
+ ctx.state.on("answer", answerHandler);
109
+ return () => {
110
+ ctx.state.off("question", questionHandler);
111
+ ctx.state.off("answer", answerHandler);
112
+ };
113
+ });
114
+ })
115
+ });
116
+ const decisionsRouter = t.router({
117
+ log: publicProcedure.input(LogDecisionInputSchema.extend({ agentId: z.string().uuid() })).mutation(({ input, ctx }) => {
118
+ const decision = {
119
+ id: crypto.randomUUID(),
120
+ decision: input.decision,
121
+ rationale: input.rationale,
122
+ by: input.agentId,
123
+ createdAt: /* @__PURE__ */ new Date()
124
+ };
125
+ ctx.state.addDecision(decision);
126
+ return decision;
127
+ }),
128
+ list: publicProcedure.query(({ ctx }) => {
129
+ return ctx.state.listDecisions();
130
+ })
131
+ });
132
+ /**
133
+ * Main application router
134
+ */
135
+ const appRouter = t.router({
136
+ session: sessionRouter,
137
+ context: contextRouter,
138
+ questions: questionsRouter,
139
+ decisions: decisionsRouter
140
+ });
141
+ /**
142
+ * Create context for a request
143
+ */
144
+ function createContext() {
145
+ return { state: getOrCreateState() };
146
+ }
147
+
148
+ //#endregion
149
+ export { appRouter, createContext };
package/server.js ADDED
@@ -0,0 +1,45 @@
1
+ import { appRouter, createContext } from "./router.js";
2
+ import { applyWSSHandler } from "@trpc/server/adapters/ws";
3
+ import { WebSocketServer } from "ws";
4
+
5
+ //#region src/server.ts
6
+ /**
7
+ * Create and start the coordinator WebSocket server
8
+ */
9
+ function createServer(options = {}) {
10
+ const port = options.port ?? 3030;
11
+ const host = options.host ?? "localhost";
12
+ const wss = new WebSocketServer({
13
+ port,
14
+ host
15
+ });
16
+ const handler = applyWSSHandler({
17
+ wss,
18
+ router: appRouter,
19
+ createContext,
20
+ keepAlive: {
21
+ enabled: true,
22
+ pingMs: 3e4,
23
+ pongWaitMs: 5e3
24
+ }
25
+ });
26
+ console.error(`[coordinator] WebSocket server listening on ws://${host}:${port}`);
27
+ wss.on("connection", (ws) => {
28
+ console.error(`[coordinator] Client connected (${wss.clients.size} total)`);
29
+ ws.on("close", () => {
30
+ console.error(`[coordinator] Client disconnected (${wss.clients.size} total)`);
31
+ });
32
+ });
33
+ const close = () => {
34
+ console.error("[coordinator] Shutting down server...");
35
+ handler.broadcastReconnectNotification();
36
+ wss.close();
37
+ };
38
+ return {
39
+ wss,
40
+ close
41
+ };
42
+ }
43
+
44
+ //#endregion
45
+ export { createServer };
package/state.js ADDED
@@ -0,0 +1,112 @@
1
+ import { EventEmitter } from "node:events";
2
+
3
+ //#region src/state.ts
4
+ /**
5
+ * EventEmitter-based state manager for the coordinator
6
+ */
7
+ var CoordinatorState = class extends EventEmitter {
8
+ sessionId;
9
+ agents = /* @__PURE__ */ new Map();
10
+ context = /* @__PURE__ */ new Map();
11
+ questions = /* @__PURE__ */ new Map();
12
+ decisions = [];
13
+ constructor(sessionId) {
14
+ super();
15
+ this.sessionId = sessionId;
16
+ }
17
+ getSessionId() {
18
+ return this.sessionId;
19
+ }
20
+ addAgent(agent) {
21
+ this.agents.set(agent.id, agent);
22
+ this.emit("agentChange", this.listAgents());
23
+ }
24
+ removeAgent(agentId) {
25
+ const removed = this.agents.delete(agentId);
26
+ if (removed) this.emit("agentChange", this.listAgents());
27
+ return removed;
28
+ }
29
+ getAgent(agentId) {
30
+ return this.agents.get(agentId);
31
+ }
32
+ listAgents() {
33
+ return Array.from(this.agents.values());
34
+ }
35
+ setContext(entry) {
36
+ this.context.set(entry.key, entry);
37
+ this.emit("contextChange", entry);
38
+ }
39
+ getContext(key) {
40
+ return this.context.get(key);
41
+ }
42
+ listContext(filters) {
43
+ let entries = Array.from(this.context.values());
44
+ const filterTags = filters?.tags;
45
+ if (filterTags && filterTags.length > 0) entries = entries.filter((entry) => filterTags.every((tag) => entry.tags.includes(tag)));
46
+ if (filters?.createdBy) entries = entries.filter((entry) => entry.createdBy === filters.createdBy);
47
+ return entries;
48
+ }
49
+ addQuestion(question) {
50
+ this.questions.set(question.id, question);
51
+ this.emit("question", question);
52
+ }
53
+ answerQuestion(questionId, answer, answeredBy) {
54
+ const question = this.questions.get(questionId);
55
+ if (!question) return;
56
+ const answered = {
57
+ ...question,
58
+ answer,
59
+ answeredBy,
60
+ status: "answered",
61
+ answeredAt: /* @__PURE__ */ new Date()
62
+ };
63
+ this.questions.set(questionId, answered);
64
+ this.emit("answer", answered);
65
+ return answered;
66
+ }
67
+ getQuestion(questionId) {
68
+ return this.questions.get(questionId);
69
+ }
70
+ listPendingQuestions(forAgentId) {
71
+ return Array.from(this.questions.values()).filter((q) => {
72
+ if (q.status !== "pending") return false;
73
+ if (forAgentId && q.to && q.to !== forAgentId) return false;
74
+ return true;
75
+ });
76
+ }
77
+ addDecision(decision) {
78
+ this.decisions.push(decision);
79
+ }
80
+ listDecisions() {
81
+ return [...this.decisions];
82
+ }
83
+ };
84
+ /**
85
+ * Global state instance (singleton per server)
86
+ */
87
+ let globalState = null;
88
+ function getOrCreateState(sessionId) {
89
+ if (!globalState) globalState = new CoordinatorState(sessionId ?? crypto.randomUUID());
90
+ return globalState;
91
+ }
92
+ /**
93
+ * Resets the global coordinator state singleton to null.
94
+ *
95
+ * @remarks
96
+ * Call this in test teardown (e.g., `afterEach` or `afterAll`) to ensure clean
97
+ * state between test suites. This prevents state from leaking across tests when
98
+ * using the singleton pattern.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * afterEach(() => {
103
+ * resetState();
104
+ * });
105
+ * ```
106
+ */
107
+ function resetState() {
108
+ globalState = null;
109
+ }
110
+
111
+ //#endregion
112
+ export { CoordinatorState, getOrCreateState, resetState };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.58.9"
9
+ }
10
+ ]
11
+ }
package/36.js DELETED
@@ -1,263 +0,0 @@
1
- import { AnswerInputSchema, AskInputSchema, DEFAULT_HOST, DEFAULT_PORT, GetContextInputSchema, JoinInputSchema, ListContextInputSchema, LogDecisionInputSchema, ShareContextInputSchema } from "@spencerbeggs/claude-coordinator-core";
2
- import { initTRPC } from "@trpc/server";
3
- import { observable } from "@trpc/server/observable";
4
- import { z } from "zod";
5
- import { EventEmitter } from "node:events";
6
- import { applyWSSHandler } from "@trpc/server/adapters/ws";
7
- import { WebSocketServer } from "ws";
8
- class CoordinatorState extends EventEmitter {
9
- sessionId;
10
- agents = new Map();
11
- context = new Map();
12
- questions = new Map();
13
- decisions = [];
14
- constructor(sessionId){
15
- super();
16
- this.sessionId = sessionId;
17
- }
18
- getSessionId() {
19
- return this.sessionId;
20
- }
21
- addAgent(agent) {
22
- this.agents.set(agent.id, agent);
23
- this.emit("agentChange", this.listAgents());
24
- }
25
- removeAgent(agentId) {
26
- const removed = this.agents.delete(agentId);
27
- if (removed) this.emit("agentChange", this.listAgents());
28
- return removed;
29
- }
30
- getAgent(agentId) {
31
- return this.agents.get(agentId);
32
- }
33
- listAgents() {
34
- return Array.from(this.agents.values());
35
- }
36
- setContext(entry) {
37
- this.context.set(entry.key, entry);
38
- this.emit("contextChange", entry);
39
- }
40
- getContext(key) {
41
- return this.context.get(key);
42
- }
43
- listContext(filters) {
44
- let entries = Array.from(this.context.values());
45
- const filterTags = filters?.tags;
46
- if (filterTags && filterTags.length > 0) entries = entries.filter((entry)=>filterTags.every((tag)=>entry.tags.includes(tag)));
47
- if (filters?.createdBy) entries = entries.filter((entry)=>entry.createdBy === filters.createdBy);
48
- return entries;
49
- }
50
- addQuestion(question) {
51
- this.questions.set(question.id, question);
52
- this.emit("question", question);
53
- }
54
- answerQuestion(questionId, answer, answeredBy) {
55
- const question = this.questions.get(questionId);
56
- if (!question) return;
57
- const answered = {
58
- ...question,
59
- answer,
60
- answeredBy,
61
- status: "answered",
62
- answeredAt: new Date()
63
- };
64
- this.questions.set(questionId, answered);
65
- this.emit("answer", answered);
66
- return answered;
67
- }
68
- getQuestion(questionId) {
69
- return this.questions.get(questionId);
70
- }
71
- listPendingQuestions(forAgentId) {
72
- return Array.from(this.questions.values()).filter((q)=>{
73
- if ("pending" !== q.status) return false;
74
- if (forAgentId && q.to && q.to !== forAgentId) return false;
75
- return true;
76
- });
77
- }
78
- addDecision(decision) {
79
- this.decisions.push(decision);
80
- }
81
- listDecisions() {
82
- return [
83
- ...this.decisions
84
- ];
85
- }
86
- }
87
- let globalState = null;
88
- function getOrCreateState(sessionId) {
89
- if (!globalState) globalState = new CoordinatorState(sessionId ?? crypto.randomUUID());
90
- return globalState;
91
- }
92
- function resetState() {
93
- globalState = null;
94
- }
95
- const t = initTRPC.context().create();
96
- const publicProcedure = t.procedure;
97
- const sessionRouter = t.router({
98
- join: publicProcedure.input(JoinInputSchema).mutation(({ input, ctx })=>{
99
- const agent = {
100
- id: crypto.randomUUID(),
101
- name: input.name,
102
- role: input.role,
103
- repoPath: input.repoPath,
104
- connectedAt: new Date()
105
- };
106
- ctx.state.addAgent(agent);
107
- return {
108
- agent,
109
- sessionId: ctx.state.getSessionId()
110
- };
111
- }),
112
- leave: publicProcedure.input(z.object({
113
- agentId: z.string().uuid()
114
- })).mutation(({ input, ctx })=>{
115
- const removed = ctx.state.removeAgent(input.agentId);
116
- return {
117
- success: removed
118
- };
119
- }),
120
- list: publicProcedure.query(({ ctx })=>ctx.state.listAgents()),
121
- onAgentChange: publicProcedure.subscription(({ ctx })=>observable((emit)=>{
122
- const handler = (agents)=>{
123
- emit.next(agents);
124
- };
125
- ctx.state.on("agentChange", handler);
126
- emit.next(ctx.state.listAgents());
127
- return ()=>{
128
- ctx.state.off("agentChange", handler);
129
- };
130
- }))
131
- });
132
- const contextRouter = t.router({
133
- share: publicProcedure.input(ShareContextInputSchema.extend({
134
- agentId: z.string().uuid()
135
- })).mutation(({ input, ctx })=>{
136
- const now = new Date();
137
- const existing = ctx.state.getContext(input.key);
138
- const entry = {
139
- id: existing?.id ?? crypto.randomUUID(),
140
- key: input.key,
141
- value: input.value,
142
- tags: input.tags ?? [],
143
- createdBy: existing?.createdBy ?? input.agentId,
144
- createdAt: existing?.createdAt ?? now,
145
- updatedAt: now
146
- };
147
- ctx.state.setContext(entry);
148
- return entry;
149
- }),
150
- get: publicProcedure.input(GetContextInputSchema).query(({ input, ctx })=>ctx.state.getContext(input.key) ?? null),
151
- list: publicProcedure.input(ListContextInputSchema.optional()).query(({ input, ctx })=>ctx.state.listContext(input)),
152
- onContextChange: publicProcedure.subscription(({ ctx })=>observable((emit)=>{
153
- const handler = (entry)=>{
154
- emit.next(entry);
155
- };
156
- ctx.state.on("contextChange", handler);
157
- return ()=>{
158
- ctx.state.off("contextChange", handler);
159
- };
160
- }))
161
- });
162
- const questionsRouter = t.router({
163
- ask: publicProcedure.input(AskInputSchema.extend({
164
- agentId: z.string().uuid()
165
- })).mutation(({ input, ctx })=>{
166
- const question = {
167
- id: crypto.randomUUID(),
168
- question: input.question,
169
- from: input.agentId,
170
- to: input.to,
171
- status: "pending",
172
- createdAt: new Date()
173
- };
174
- ctx.state.addQuestion(question);
175
- return question;
176
- }),
177
- answer: publicProcedure.input(AnswerInputSchema.extend({
178
- agentId: z.string().uuid()
179
- })).mutation(({ input, ctx })=>{
180
- const answered = ctx.state.answerQuestion(input.questionId, input.answer, input.agentId);
181
- if (!answered) throw new Error(`Question not found: ${input.questionId}`);
182
- return answered;
183
- }),
184
- listPending: publicProcedure.input(z.object({
185
- agentId: z.string().uuid().optional()
186
- }).optional()).query(({ input, ctx })=>ctx.state.listPendingQuestions(input?.agentId)),
187
- onQuestion: publicProcedure.subscription(({ ctx })=>observable((emit)=>{
188
- const questionHandler = (question)=>{
189
- emit.next(question);
190
- };
191
- const answerHandler = (question)=>{
192
- emit.next(question);
193
- };
194
- ctx.state.on("question", questionHandler);
195
- ctx.state.on("answer", answerHandler);
196
- return ()=>{
197
- ctx.state.off("question", questionHandler);
198
- ctx.state.off("answer", answerHandler);
199
- };
200
- }))
201
- });
202
- const decisionsRouter = t.router({
203
- log: publicProcedure.input(LogDecisionInputSchema.extend({
204
- agentId: z.string().uuid()
205
- })).mutation(({ input, ctx })=>{
206
- const decision = {
207
- id: crypto.randomUUID(),
208
- decision: input.decision,
209
- rationale: input.rationale,
210
- by: input.agentId,
211
- createdAt: new Date()
212
- };
213
- ctx.state.addDecision(decision);
214
- return decision;
215
- }),
216
- list: publicProcedure.query(({ ctx })=>ctx.state.listDecisions())
217
- });
218
- const appRouter = t.router({
219
- session: sessionRouter,
220
- context: contextRouter,
221
- questions: questionsRouter,
222
- decisions: decisionsRouter
223
- });
224
- function createContext() {
225
- return {
226
- state: getOrCreateState()
227
- };
228
- }
229
- function createServer(options = {}) {
230
- const port = options.port ?? 3030;
231
- const host = options.host ?? "localhost";
232
- const wss = new WebSocketServer({
233
- port,
234
- host
235
- });
236
- const handler = applyWSSHandler({
237
- wss,
238
- router: appRouter,
239
- createContext: createContext,
240
- keepAlive: {
241
- enabled: true,
242
- pingMs: 30000,
243
- pongWaitMs: 5000
244
- }
245
- });
246
- console.error(`[coordinator] WebSocket server listening on ws://${host}:${port}`);
247
- wss.on("connection", (ws)=>{
248
- console.error(`[coordinator] Client connected (${wss.clients.size} total)`);
249
- ws.on("close", ()=>{
250
- console.error(`[coordinator] Client disconnected (${wss.clients.size} total)`);
251
- });
252
- });
253
- const close = ()=>{
254
- console.error("[coordinator] Shutting down server...");
255
- handler.broadcastReconnectNotification();
256
- wss.close();
257
- };
258
- return {
259
- wss,
260
- close
261
- };
262
- }
263
- export { CoordinatorState, DEFAULT_HOST, DEFAULT_PORT, appRouter, createContext, createServer, getOrCreateState, resetState };