@spencerbeggs/claude-coordinator-server 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.
package/36.js ADDED
@@ -0,0 +1,263 @@
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 };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # @spencerbeggs/claude-coordinator-server
2
+
3
+ tRPC WebSocket server enabling real-time coordination between Claude Code
4
+ instances through session management, context sharing, Q&A, and decision
5
+ logging.
6
+
7
+ ## Features
8
+
9
+ - **Real-time communication** - WebSocket-based with tRPC subscriptions
10
+ - **Session management** - Agents join/leave coordinated sessions
11
+ - **Context sharing** - Share and retrieve key-value context entries
12
+ - **Q&A system** - Ask and answer questions between agents
13
+ - **Decision logging** - Track decisions made during coordination
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @spencerbeggs/claude-coordinator-server
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### Start the Server
24
+
25
+ ```bash
26
+ # Using npx
27
+ npx @spencerbeggs/claude-coordinator-server
28
+
29
+ # With custom port
30
+ PORT=3031 npx @spencerbeggs/claude-coordinator-server
31
+
32
+ # With custom host
33
+ HOST=0.0.0.0 PORT=3030 npx @spencerbeggs/claude-coordinator-server
34
+ ```
35
+
36
+ ### Use Programmatically
37
+
38
+ ```typescript
39
+ import { createServer } from "@spencerbeggs/claude-coordinator-server";
40
+
41
+ const server = createServer({
42
+ port: 3030,
43
+ host: "localhost",
44
+ });
45
+
46
+ console.log("Server started on ws://localhost:3030");
47
+
48
+ // Graceful shutdown
49
+ process.on("SIGTERM", () => {
50
+ server.close();
51
+ });
52
+ ```
53
+
54
+ ## API Overview
55
+
56
+ The server exposes tRPC procedures organized by domain:
57
+
58
+ ### Session
59
+
60
+ | Procedure | Type | Description |
61
+ | --------- | ---- | ----------- |
62
+ | `session.join` | mutation | Join session as an agent |
63
+ | `session.leave` | mutation | Leave current session |
64
+ | `session.list` | query | List connected agents |
65
+ | `session.onAgentChange` | subscription | Real-time agent updates |
66
+
67
+ ### Context
68
+
69
+ | Procedure | Type | Description |
70
+ | --------- | ---- | ----------- |
71
+ | `context.share` | mutation | Share a context entry |
72
+ | `context.get` | query | Get context by key |
73
+ | `context.list` | query | List context entries |
74
+ | `context.onContextChange` | subscription | Real-time context updates |
75
+
76
+ ### Questions
77
+
78
+ | Procedure | Type | Description |
79
+ | --------- | ---- | ----------- |
80
+ | `questions.ask` | mutation | Ask a question |
81
+ | `questions.answer` | mutation | Answer a question |
82
+ | `questions.listPending` | query | List unanswered questions |
83
+ | `questions.onQuestion` | subscription | Real-time Q&A updates |
84
+
85
+ ### Decisions
86
+
87
+ | Procedure | Type | Description |
88
+ | --------- | ---- | ----------- |
89
+ | `decisions.log` | mutation | Log a decision |
90
+ | `decisions.list` | query | List all decisions |
91
+
92
+ ## Configuration
93
+
94
+ | Environment Variable | Default | Description |
95
+ | -------------------- | ------- | ----------- |
96
+ | `PORT` | `3030` | Server port |
97
+ | `HOST` | `localhost` | Server host |
98
+
99
+ ## Documentation
100
+
101
+ - [Architecture Design Doc](../../.claude/design/claude-coordinator-server/architecture.md)
102
+ - [Core Schemas Package](../claude-coordinator-core/README.md)
103
+ - [MCP Bridge Package](../claude-coordinator-mcp/README.md)
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,15 @@
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;
5
+ const server = createServer({
6
+ port: port,
7
+ host: host
8
+ });
9
+ const shutdown = ()=>{
10
+ server.close();
11
+ process.exit(0);
12
+ };
13
+ process.on("SIGINT", shutdown);
14
+ process.on("SIGTERM", shutdown);
15
+ console.error("[coordinator] Server started. Press Ctrl+C to stop.");
package/index.d.ts ADDED
@@ -0,0 +1,129 @@
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 { }
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export { CoordinatorState, appRouter, createContext, createServer, getOrCreateState, resetState } from "./36.js";
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@spencerbeggs/claude-coordinator-server",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "tRPC WebSocket server for Claude Coordinator",
6
+ "keywords": [
7
+ "claude",
8
+ "coordinator",
9
+ "trpc",
10
+ "websocket",
11
+ "server"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/spencerbeggs/claude-design-coordinator.git",
16
+ "directory": "pkgs/claude-coordinator-server"
17
+ },
18
+ "license": "MIT",
19
+ "author": {
20
+ "name": "C. Spencer Beggs",
21
+ "email": "spencer@beggs.codes",
22
+ "url": "https://spencerbeg.gs"
23
+ },
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./index.d.ts",
28
+ "import": "./index.js"
29
+ }
30
+ },
31
+ "bin": {
32
+ "claude-coordinator-server": "./bin/claude-coordinator-server.js"
33
+ },
34
+ "dependencies": {
35
+ "@spencerbeggs/claude-coordinator-core": "0.1.0",
36
+ "@trpc/server": "^11.8.1",
37
+ "ws": "^8.19.0",
38
+ "zod": "^4.3.5"
39
+ },
40
+ "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
+ }