@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.
- package/bin/claude-coordinator-server.js +13 -9
- package/index.d.ts +106 -129
- package/index.js +5 -1
- package/package.json +11 -18
- package/router.js +149 -0
- package/server.js +45 -0
- package/state.js +112 -0
- package/tsdoc-metadata.json +11 -0
- package/36.js +0 -263
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
7
|
+
port: Number(process.env.PORT) || DEFAULT_PORT,
|
|
8
|
+
host: process.env.HOST ?? DEFAULT_HOST
|
|
8
9
|
});
|
|
9
|
-
const shutdown = ()=>{
|
|
10
|
-
|
|
11
|
-
|
|
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(
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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.
|
|
37
|
-
"ws": "^8.
|
|
38
|
-
"zod": "^4.3
|
|
38
|
+
"@trpc/server": "^11.18.0",
|
|
39
|
+
"ws": "^8.21.0",
|
|
40
|
+
"zod": "^4.4.3"
|
|
39
41
|
},
|
|
40
42
|
"engines": {
|
|
41
|
-
"node": ">=
|
|
42
|
-
}
|
|
43
|
-
|
|
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 };
|