noumen 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +846 -51
- package/dist/a2a/index.d.ts +148 -0
- package/dist/a2a/index.js +579 -0
- package/dist/a2a/index.js.map +1 -0
- package/dist/acp/index.d.ts +129 -0
- package/dist/acp/index.js +498 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/agent-1nFVUP9E.d.ts +1332 -0
- package/dist/cache-DsRqxx6v.d.ts +38 -0
- package/dist/chunk-3HEYCV26.js +10 -0
- package/dist/chunk-3HEYCV26.js.map +1 -0
- package/dist/chunk-3SK5GCI6.js +75 -0
- package/dist/chunk-3SK5GCI6.js.map +1 -0
- package/dist/chunk-42PHHZUA.js +132 -0
- package/dist/chunk-42PHHZUA.js.map +1 -0
- package/dist/chunk-4HW6LN6D.js +10365 -0
- package/dist/chunk-4HW6LN6D.js.map +1 -0
- package/dist/chunk-4SQA2UCV.js +26 -0
- package/dist/chunk-4SQA2UCV.js.map +1 -0
- package/dist/chunk-5GEX6ZSB.js +179 -0
- package/dist/chunk-5GEX6ZSB.js.map +1 -0
- package/dist/chunk-5JN4SPI7.js +94 -0
- package/dist/chunk-5JN4SPI7.js.map +1 -0
- package/dist/chunk-AMYIJSAZ.js +57 -0
- package/dist/chunk-AMYIJSAZ.js.map +1 -0
- package/dist/chunk-BZSFUEWM.js +43 -0
- package/dist/chunk-BZSFUEWM.js.map +1 -0
- package/dist/chunk-CS6WNDCF.js +171 -0
- package/dist/chunk-CS6WNDCF.js.map +1 -0
- package/dist/chunk-D43BWEZA.js +346 -0
- package/dist/chunk-D43BWEZA.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-EKOGVTBT.js +472 -0
- package/dist/chunk-EKOGVTBT.js.map +1 -0
- package/dist/chunk-HEQQQGK5.js +131 -0
- package/dist/chunk-HEQQQGK5.js.map +1 -0
- package/dist/chunk-HL6JCRZJ.js +3112 -0
- package/dist/chunk-HL6JCRZJ.js.map +1 -0
- package/dist/chunk-JACGEMTF.js +43 -0
- package/dist/chunk-JACGEMTF.js.map +1 -0
- package/dist/chunk-JX7CLUCV.js +21 -0
- package/dist/chunk-JX7CLUCV.js.map +1 -0
- package/dist/chunk-KXDB56YW.js +39 -0
- package/dist/chunk-KXDB56YW.js.map +1 -0
- package/dist/chunk-L3L3FG5T.js +16 -0
- package/dist/chunk-L3L3FG5T.js.map +1 -0
- package/dist/chunk-OGXNFXFA.js +196 -0
- package/dist/chunk-OGXNFXFA.js.map +1 -0
- package/dist/chunk-UVSSQBDY.js +192 -0
- package/dist/chunk-UVSSQBDY.js.map +1 -0
- package/dist/chunk-Y45R3PQL.js +684 -0
- package/dist/chunk-Y45R3PQL.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +874 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/index.d.ts +64 -0
- package/dist/client/index.js +409 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client-CRRO2376.js +10 -0
- package/dist/client-CRRO2376.js.map +1 -0
- package/dist/headless-FFU2DESQ.js +142 -0
- package/dist/headless-FFU2DESQ.js.map +1 -0
- package/dist/history-snip-64GYP4ZL.js +12 -0
- package/dist/history-snip-64GYP4ZL.js.map +1 -0
- package/dist/index.d.ts +1459 -422
- package/dist/index.js +398 -1757
- package/dist/index.js.map +1 -1
- package/dist/jsonrpc/index.d.ts +54 -0
- package/dist/jsonrpc/index.js +34 -0
- package/dist/jsonrpc/index.js.map +1 -0
- package/dist/lsp/index.d.ts +36 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/index.js.map +1 -0
- package/dist/lsp-PS3BWIHC.js +8 -0
- package/dist/lsp-PS3BWIHC.js.map +1 -0
- package/dist/manager-DLXK63XC.js +8 -0
- package/dist/manager-DLXK63XC.js.map +1 -0
- package/dist/mcp/index.d.ts +111 -0
- package/dist/mcp/index.js +105 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp-auth-AEI2R4ZC.js +9 -0
- package/dist/mcp-auth-AEI2R4ZC.js.map +1 -0
- package/dist/provider-factory-KCLIF34X.js +20 -0
- package/dist/provider-factory-KCLIF34X.js.map +1 -0
- package/dist/providers/anthropic.d.ts +19 -0
- package/dist/providers/anthropic.js +35 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/bedrock.d.ts +39 -0
- package/dist/providers/bedrock.js +56 -0
- package/dist/providers/bedrock.js.map +1 -0
- package/dist/providers/gemini.d.ts +17 -0
- package/dist/providers/gemini.js +262 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/ollama.d.ts +13 -0
- package/dist/providers/ollama.js +20 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai.d.ts +21 -0
- package/dist/providers/openai.js +9 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/openrouter.d.ts +16 -0
- package/dist/providers/openrouter.js +24 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/vertex.d.ts +42 -0
- package/dist/providers/vertex.js +67 -0
- package/dist/providers/vertex.js.map +1 -0
- package/dist/render-GRN4ZSSW.js +14 -0
- package/dist/render-GRN4ZSSW.js.map +1 -0
- package/dist/resolve-4JA2BBDA.js +14 -0
- package/dist/resolve-4JA2BBDA.js.map +1 -0
- package/dist/server/index.d.ts +143 -0
- package/dist/server/index.js +695 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server-CHMxuWKq.d.ts +96 -0
- package/dist/spinner-OJNR6NFO.js +8 -0
- package/dist/spinner-OJNR6NFO.js.map +1 -0
- package/dist/types-2kTLUCnD.d.ts +107 -0
- package/dist/types-CD0rUKKT.d.ts +109 -0
- package/dist/types-LrU4LRmX.d.ts +575 -0
- package/dist/types-NIyVwQ4h.d.ts +109 -0
- package/dist/types-QwfylltH.d.ts +71 -0
- package/dist/types-RPKUTu1k.d.ts +645 -0
- package/dist/uuid-RVN2T26F.js +8 -0
- package/dist/uuid-RVN2T26F.js.map +1 -0
- package/dist/zod-7YXKWYMC.js +12 -0
- package/dist/zod-7YXKWYMC.js.map +1 -0
- package/package.json +141 -7
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import "../chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/server/index.ts
|
|
4
|
+
import { createServer as createHttpServer } from "http";
|
|
5
|
+
|
|
6
|
+
// src/server/session-state.ts
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
var DEFAULT_PENDING_TIMEOUT_MS = 12e4;
|
|
9
|
+
function createSessionState(sessions, requestedId, overrides) {
|
|
10
|
+
if (requestedId && sessions.has(requestedId)) {
|
|
11
|
+
throw new Error(`Session ${requestedId} already exists`);
|
|
12
|
+
}
|
|
13
|
+
const sessionId = requestedId ?? randomUUID();
|
|
14
|
+
const session = {
|
|
15
|
+
id: sessionId,
|
|
16
|
+
abortController: new AbortController(),
|
|
17
|
+
pendingPermission: null,
|
|
18
|
+
pendingInput: null,
|
|
19
|
+
pendingPermissionTimer: null,
|
|
20
|
+
pendingInputTimer: null,
|
|
21
|
+
lastActivity: Date.now(),
|
|
22
|
+
sseResponse: null,
|
|
23
|
+
sseKeepaliveTimer: null,
|
|
24
|
+
eventBuffer: [],
|
|
25
|
+
sequenceNum: 0,
|
|
26
|
+
done: false,
|
|
27
|
+
cwd: overrides.cwd
|
|
28
|
+
};
|
|
29
|
+
sessions.set(sessionId, session);
|
|
30
|
+
return session;
|
|
31
|
+
}
|
|
32
|
+
function destroySession(sessions, session) {
|
|
33
|
+
session.abortController.abort();
|
|
34
|
+
clearSseKeepalive(session);
|
|
35
|
+
clearPendingPermissionTimer(session);
|
|
36
|
+
clearPendingInputTimer(session);
|
|
37
|
+
if (session.pendingPermission) {
|
|
38
|
+
session.pendingPermission.reject(new Error("Session aborted"));
|
|
39
|
+
session.pendingPermission = null;
|
|
40
|
+
}
|
|
41
|
+
if (session.pendingInput) {
|
|
42
|
+
session.pendingInput.reject(new Error("Session aborted"));
|
|
43
|
+
session.pendingInput = null;
|
|
44
|
+
}
|
|
45
|
+
if (session.sseResponse) {
|
|
46
|
+
session.sseResponse.end();
|
|
47
|
+
session.sseResponse = null;
|
|
48
|
+
}
|
|
49
|
+
sessions.delete(session.id);
|
|
50
|
+
}
|
|
51
|
+
function reapIdleSessions(sessions, timeoutMs) {
|
|
52
|
+
if (!timeoutMs) return;
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
for (const session of sessions.values()) {
|
|
55
|
+
if (now - session.lastActivity > timeoutMs) {
|
|
56
|
+
destroySession(sessions, session);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function bridgePermission(sessions, sessionId, timeoutMs) {
|
|
61
|
+
const session = sessions.get(sessionId);
|
|
62
|
+
if (!session) return Promise.reject(new Error("Session not found"));
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
session.pendingPermission = { resolve, reject };
|
|
65
|
+
session.pendingPermissionTimer = setTimeout(() => {
|
|
66
|
+
session.pendingPermissionTimer = null;
|
|
67
|
+
if (session.pendingPermission) {
|
|
68
|
+
session.pendingPermission.reject(new Error("Permission request timed out"));
|
|
69
|
+
session.pendingPermission = null;
|
|
70
|
+
}
|
|
71
|
+
}, timeoutMs);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function bridgeUserInput(sessions, sessionId, timeoutMs) {
|
|
75
|
+
const session = sessions.get(sessionId);
|
|
76
|
+
if (!session) return Promise.reject(new Error("Session not found"));
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
session.pendingInput = { resolve, reject };
|
|
79
|
+
session.pendingInputTimer = setTimeout(() => {
|
|
80
|
+
session.pendingInputTimer = null;
|
|
81
|
+
if (session.pendingInput) {
|
|
82
|
+
session.pendingInput.reject(new Error("User input request timed out"));
|
|
83
|
+
session.pendingInput = null;
|
|
84
|
+
}
|
|
85
|
+
}, timeoutMs);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function clearPendingPermissionTimer(session) {
|
|
89
|
+
if (session.pendingPermissionTimer) {
|
|
90
|
+
clearTimeout(session.pendingPermissionTimer);
|
|
91
|
+
session.pendingPermissionTimer = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function clearPendingInputTimer(session) {
|
|
95
|
+
if (session.pendingInputTimer) {
|
|
96
|
+
clearTimeout(session.pendingInputTimer);
|
|
97
|
+
session.pendingInputTimer = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function clearSseKeepalive(session) {
|
|
101
|
+
if (session.sseKeepaliveTimer) {
|
|
102
|
+
clearInterval(session.sseKeepaliveTimer);
|
|
103
|
+
session.sseKeepaliveTimer = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/server/event-buffer.ts
|
|
108
|
+
var MAX_EVENT_BUFFER = 1e3;
|
|
109
|
+
function serializeEvent(event) {
|
|
110
|
+
if (event.type === "error") {
|
|
111
|
+
return { type: "error", error: { message: event.error.message, name: event.error.name } };
|
|
112
|
+
}
|
|
113
|
+
if (event.type === "retry_exhausted") {
|
|
114
|
+
return { ...event, error: { message: event.error.message, name: event.error.name } };
|
|
115
|
+
}
|
|
116
|
+
if (event.type === "retry_attempt") {
|
|
117
|
+
return { ...event, error: { message: event.error.message, name: event.error.name } };
|
|
118
|
+
}
|
|
119
|
+
return event;
|
|
120
|
+
}
|
|
121
|
+
function pushEvent(session, event) {
|
|
122
|
+
session.sequenceNum++;
|
|
123
|
+
const seq = session.sequenceNum;
|
|
124
|
+
if (session.eventBuffer.length >= MAX_EVENT_BUFFER) {
|
|
125
|
+
session.eventBuffer.shift();
|
|
126
|
+
}
|
|
127
|
+
session.eventBuffer.push({ seq, event });
|
|
128
|
+
if (session.sseResponse) {
|
|
129
|
+
writeSseEventRaw(session.sseResponse, seq, serializeEvent(event));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function getBufferedEventsAfter(buffer, afterSeq) {
|
|
133
|
+
if (!afterSeq) return [...buffer];
|
|
134
|
+
return buffer.filter((e) => e.seq > afterSeq);
|
|
135
|
+
}
|
|
136
|
+
function writeSseEventRaw(res, seq, data) {
|
|
137
|
+
res.write(`id: ${seq}
|
|
138
|
+
data: ${JSON.stringify(data)}
|
|
139
|
+
|
|
140
|
+
`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/server/ws-dispatch.ts
|
|
144
|
+
function parseWsMessage(raw) {
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(typeof raw === "string" ? raw : raw.toString());
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function handleWsMessage(msg, ctx, callbacks) {
|
|
152
|
+
const msgType = msg.type;
|
|
153
|
+
if (msgType === "run") {
|
|
154
|
+
if (ctx.maxSessions && ctx.currentSessionCount >= ctx.maxSessions) {
|
|
155
|
+
return { type: "error", error: "Maximum sessions reached" };
|
|
156
|
+
}
|
|
157
|
+
if (typeof msg.prompt !== "string" || !msg.prompt.trim()) {
|
|
158
|
+
return { type: "error", error: "Missing or empty prompt" };
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const sessionId = await callbacks.onRun(
|
|
162
|
+
msg.prompt,
|
|
163
|
+
msg.sessionId
|
|
164
|
+
);
|
|
165
|
+
return { type: "session_created", sessionId };
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return { type: "error", error: err instanceof Error ? err.message : String(err) };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (msgType === "message") {
|
|
171
|
+
if (typeof msg.prompt !== "string" || !msg.prompt.trim()) {
|
|
172
|
+
return { type: "error", error: "Missing or empty prompt" };
|
|
173
|
+
}
|
|
174
|
+
const sessionId = msg.sessionId;
|
|
175
|
+
if (!sessionId) {
|
|
176
|
+
return { type: "error", error: "Missing sessionId" };
|
|
177
|
+
}
|
|
178
|
+
callbacks.onMessage(sessionId, msg.prompt);
|
|
179
|
+
return { type: "ok" };
|
|
180
|
+
}
|
|
181
|
+
if (msgType === "permission_response") {
|
|
182
|
+
const sessionId = msg.sessionId;
|
|
183
|
+
const { sessionId: _sid, type: _type, ...response } = msg;
|
|
184
|
+
callbacks.onPermissionResponse(sessionId, response);
|
|
185
|
+
return { type: "ok" };
|
|
186
|
+
}
|
|
187
|
+
if (msgType === "input_response") {
|
|
188
|
+
const sessionId = msg.sessionId;
|
|
189
|
+
callbacks.onInputResponse(sessionId, msg.answer ?? "");
|
|
190
|
+
return { type: "ok" };
|
|
191
|
+
}
|
|
192
|
+
if (msgType === "abort") {
|
|
193
|
+
const sessionId = msg.sessionId;
|
|
194
|
+
if (sessionId) callbacks.onAbort(sessionId);
|
|
195
|
+
return { type: "ok" };
|
|
196
|
+
}
|
|
197
|
+
return { type: "ok" };
|
|
198
|
+
}
|
|
199
|
+
function wsSend(ws, data) {
|
|
200
|
+
if (ws.readyState === 1) ws.send(JSON.stringify(data));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/server/index.ts
|
|
204
|
+
var SSE_KEEPALIVE_INTERVAL_MS = 15e3;
|
|
205
|
+
var MAX_BODY_BYTES = 1048576;
|
|
206
|
+
var SHUTDOWN_DRAIN_MS = 500;
|
|
207
|
+
var WS_PING_INTERVAL_MS = 3e4;
|
|
208
|
+
var NoumenServer = class {
|
|
209
|
+
code;
|
|
210
|
+
options;
|
|
211
|
+
httpServer = null;
|
|
212
|
+
wss = null;
|
|
213
|
+
sessions = /* @__PURE__ */ new Map();
|
|
214
|
+
idleTimer = null;
|
|
215
|
+
constructor(code, options) {
|
|
216
|
+
this.code = code;
|
|
217
|
+
this.options = options;
|
|
218
|
+
}
|
|
219
|
+
async start() {
|
|
220
|
+
this.httpServer = createHttpServer((req, res) => this.handleRequest(req, res));
|
|
221
|
+
if (this.options.ws !== false) {
|
|
222
|
+
await this.initWebSocket();
|
|
223
|
+
}
|
|
224
|
+
this.ensureIdleReaper();
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
const host = this.options.host ?? "127.0.0.1";
|
|
227
|
+
this.httpServer.listen(this.options.port, host, () => resolve());
|
|
228
|
+
this.httpServer.once("error", reject);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async stop() {
|
|
232
|
+
if (this.idleTimer) {
|
|
233
|
+
clearInterval(this.idleTimer);
|
|
234
|
+
this.idleTimer = null;
|
|
235
|
+
}
|
|
236
|
+
for (const session of this.sessions.values()) {
|
|
237
|
+
session.abortController.abort();
|
|
238
|
+
}
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, SHUTDOWN_DRAIN_MS));
|
|
240
|
+
for (const session of this.sessions.values()) {
|
|
241
|
+
destroySession(this.sessions, session);
|
|
242
|
+
}
|
|
243
|
+
if (this.wss) {
|
|
244
|
+
await new Promise((resolve) => this.wss.close(() => resolve()));
|
|
245
|
+
this.wss = null;
|
|
246
|
+
}
|
|
247
|
+
if (this.httpServer) {
|
|
248
|
+
if (typeof this.httpServer.closeAllConnections === "function") {
|
|
249
|
+
this.httpServer.closeAllConnections();
|
|
250
|
+
}
|
|
251
|
+
await new Promise(
|
|
252
|
+
(resolve, reject) => this.httpServer.close((err) => err ? reject(err) : resolve())
|
|
253
|
+
);
|
|
254
|
+
this.httpServer = null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
getActiveSessions() {
|
|
258
|
+
const result = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const [id, s] of this.sessions) {
|
|
260
|
+
result.set(id, { id: s.id, lastActivity: s.lastActivity, done: s.done });
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
// -------------------------------------------------------------------------
|
|
265
|
+
// WebSocket setup
|
|
266
|
+
// -------------------------------------------------------------------------
|
|
267
|
+
async initWebSocket() {
|
|
268
|
+
let WsServerCtor;
|
|
269
|
+
try {
|
|
270
|
+
const ws = await import("ws");
|
|
271
|
+
WsServerCtor = ws.WebSocketServer ?? ws.default?.WebSocketServer;
|
|
272
|
+
} catch {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"noumen/server: WebSocket support requires the 'ws' package. Install it with: npm install ws\nOr disable WebSocket with { ws: false } in ServerOptions."
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
this.wss = new WsServerCtor({ server: this.httpServer });
|
|
278
|
+
this.wss.on("connection", (ws, req) => {
|
|
279
|
+
this.handleWsConnection(ws, req).catch(
|
|
280
|
+
(err) => this.options.onError?.(err instanceof Error ? err : new Error(String(err)))
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Handle an HTTP request. Used internally by `start()` and exposed for
|
|
286
|
+
* `createRequestHandler()` so the same logic can be mounted on an
|
|
287
|
+
* external Express / Fastify / Hono server.
|
|
288
|
+
*/
|
|
289
|
+
async handleRequest(req, res) {
|
|
290
|
+
this.ensureIdleReaper();
|
|
291
|
+
return this.handleHttp(req, res).catch((err) => {
|
|
292
|
+
this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
293
|
+
if (!res.headersSent) jsonResponse(res, 500, { error: "Internal server error" });
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
idleReaperStarted = false;
|
|
297
|
+
ensureIdleReaper() {
|
|
298
|
+
if (this.idleReaperStarted || !this.options.idleTimeoutMs) return;
|
|
299
|
+
this.idleReaperStarted = true;
|
|
300
|
+
const interval = Math.max(this.options.idleTimeoutMs / 2, 1e3);
|
|
301
|
+
this.idleTimer = setInterval(() => reapIdleSessions(this.sessions, this.options.idleTimeoutMs), interval);
|
|
302
|
+
this.idleTimer.unref();
|
|
303
|
+
}
|
|
304
|
+
// -------------------------------------------------------------------------
|
|
305
|
+
// HTTP routing
|
|
306
|
+
// -------------------------------------------------------------------------
|
|
307
|
+
async handleHttp(req, res) {
|
|
308
|
+
if (this.options.cors !== false) {
|
|
309
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
310
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
311
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Last-Event-ID");
|
|
312
|
+
}
|
|
313
|
+
const method = req.method ?? "GET";
|
|
314
|
+
if (method === "OPTIONS") {
|
|
315
|
+
res.writeHead(204);
|
|
316
|
+
res.end();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
320
|
+
const path = url.pathname;
|
|
321
|
+
if (path === "/health" && method === "GET") {
|
|
322
|
+
return jsonResponse(res, 200, { status: "ok", sessions: this.sessions.size });
|
|
323
|
+
}
|
|
324
|
+
if (this.options.auth) {
|
|
325
|
+
const authResult = await this.authenticate(req);
|
|
326
|
+
if (!authResult) {
|
|
327
|
+
return jsonResponse(res, 401, { error: "Unauthorized" });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (path === "/sessions" && method === "POST") {
|
|
331
|
+
return this.handleCreateSession(req, res);
|
|
332
|
+
}
|
|
333
|
+
if (path === "/sessions" && method === "GET") {
|
|
334
|
+
return this.handleListSessions(res);
|
|
335
|
+
}
|
|
336
|
+
const sessionMatch = path.match(/^\/sessions\/([^/]+)(?:\/(.+))?$/);
|
|
337
|
+
if (sessionMatch) {
|
|
338
|
+
const sessionId = sessionMatch[1];
|
|
339
|
+
const sub = sessionMatch[2] ?? "";
|
|
340
|
+
if (sub === "events" && method === "GET") return this.handleSseStream(sessionId, req, res);
|
|
341
|
+
if (sub === "permissions" && method === "POST") return this.handlePermissionResponse(sessionId, req, res);
|
|
342
|
+
if (sub === "input" && method === "POST") return this.handleInputResponse(sessionId, req, res);
|
|
343
|
+
if (sub === "messages" && method === "POST") return this.handleSendMessage(sessionId, req, res);
|
|
344
|
+
if (sub === "" && method === "DELETE") return this.handleDeleteSession(sessionId, res);
|
|
345
|
+
}
|
|
346
|
+
jsonResponse(res, 404, { error: "Not found" });
|
|
347
|
+
}
|
|
348
|
+
// -------------------------------------------------------------------------
|
|
349
|
+
// Auth
|
|
350
|
+
// -------------------------------------------------------------------------
|
|
351
|
+
async authenticate(req) {
|
|
352
|
+
const auth = this.options.auth;
|
|
353
|
+
if (!auth) return {};
|
|
354
|
+
if (auth.type === "bearer") {
|
|
355
|
+
const header = req.headers.authorization;
|
|
356
|
+
if (header === `Bearer ${auth.token}`) return {};
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
return auth.verify(req);
|
|
360
|
+
}
|
|
361
|
+
// -------------------------------------------------------------------------
|
|
362
|
+
// REST handlers
|
|
363
|
+
// -------------------------------------------------------------------------
|
|
364
|
+
async handleCreateSession(req, res) {
|
|
365
|
+
const body = await readBody(req);
|
|
366
|
+
const { prompt, sessionId: requestedId } = body;
|
|
367
|
+
if (!prompt || typeof prompt !== "string") {
|
|
368
|
+
return jsonResponse(res, 400, { error: "Missing required field: prompt" });
|
|
369
|
+
}
|
|
370
|
+
if (this.options.maxSessions && this.sessions.size >= this.options.maxSessions) {
|
|
371
|
+
return jsonResponse(res, 429, { error: "Maximum sessions reached" });
|
|
372
|
+
}
|
|
373
|
+
const overrides = await this.resolveConnectionOverrides(req);
|
|
374
|
+
const session = createSessionState(this.sessions, requestedId, overrides);
|
|
375
|
+
this.runAgentSse(session, prompt, false);
|
|
376
|
+
jsonResponse(res, 201, {
|
|
377
|
+
sessionId: session.id,
|
|
378
|
+
eventsUrl: `/sessions/${session.id}/events`
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
handleListSessions(res) {
|
|
382
|
+
const sessions = Array.from(this.sessions.values()).map((s) => ({
|
|
383
|
+
id: s.id,
|
|
384
|
+
lastActivity: s.lastActivity,
|
|
385
|
+
done: s.done
|
|
386
|
+
}));
|
|
387
|
+
jsonResponse(res, 200, sessions);
|
|
388
|
+
}
|
|
389
|
+
handleSseStream(sessionId, req, res) {
|
|
390
|
+
const session = this.sessions.get(sessionId);
|
|
391
|
+
if (!session) return jsonResponse(res, 404, { error: "Session not found" });
|
|
392
|
+
if (session.sseResponse) {
|
|
393
|
+
const oldRes = session.sseResponse;
|
|
394
|
+
writeSseEventRaw(oldRes, session.sequenceNum + 1, { type: "subscriber_replaced" });
|
|
395
|
+
oldRes.end();
|
|
396
|
+
clearSseKeepalive(session);
|
|
397
|
+
session.sseResponse = null;
|
|
398
|
+
}
|
|
399
|
+
res.writeHead(200, {
|
|
400
|
+
"Content-Type": "text/event-stream",
|
|
401
|
+
"Cache-Control": "no-cache",
|
|
402
|
+
"Connection": "keep-alive",
|
|
403
|
+
"X-Accel-Buffering": "no"
|
|
404
|
+
});
|
|
405
|
+
const lastEventId = req.headers["last-event-id"];
|
|
406
|
+
const resumeAfterSeq = lastEventId ? parseInt(lastEventId, 10) : 0;
|
|
407
|
+
const eventsToReplay = getBufferedEventsAfter(session.eventBuffer, resumeAfterSeq);
|
|
408
|
+
for (const buffered of eventsToReplay) {
|
|
409
|
+
writeSseEventRaw(res, buffered.seq, serializeEvent(buffered.event));
|
|
410
|
+
}
|
|
411
|
+
session.eventBuffer = [];
|
|
412
|
+
session.sseResponse = res;
|
|
413
|
+
this.startSseKeepalive(session);
|
|
414
|
+
res.on("close", () => {
|
|
415
|
+
if (session.sseResponse === res) {
|
|
416
|
+
clearSseKeepalive(session);
|
|
417
|
+
session.sseResponse = null;
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
async handlePermissionResponse(sessionId, req, res) {
|
|
422
|
+
const session = this.sessions.get(sessionId);
|
|
423
|
+
if (!session) return jsonResponse(res, 404, { error: "Session not found" });
|
|
424
|
+
if (!session.pendingPermission) return jsonResponse(res, 409, { error: "No pending permission request" });
|
|
425
|
+
const body = await readBody(req);
|
|
426
|
+
clearPendingPermissionTimer(session);
|
|
427
|
+
session.pendingPermission.resolve(body);
|
|
428
|
+
session.pendingPermission = null;
|
|
429
|
+
jsonResponse(res, 200, { ok: true });
|
|
430
|
+
}
|
|
431
|
+
async handleInputResponse(sessionId, req, res) {
|
|
432
|
+
const session = this.sessions.get(sessionId);
|
|
433
|
+
if (!session) return jsonResponse(res, 404, { error: "Session not found" });
|
|
434
|
+
if (!session.pendingInput) return jsonResponse(res, 409, { error: "No pending input request" });
|
|
435
|
+
const body = await readBody(req);
|
|
436
|
+
if (typeof body.answer !== "string") {
|
|
437
|
+
return jsonResponse(res, 400, { error: "Missing required field: answer" });
|
|
438
|
+
}
|
|
439
|
+
clearPendingInputTimer(session);
|
|
440
|
+
session.pendingInput.resolve(body.answer);
|
|
441
|
+
session.pendingInput = null;
|
|
442
|
+
jsonResponse(res, 200, { ok: true });
|
|
443
|
+
}
|
|
444
|
+
async handleSendMessage(sessionId, req, res) {
|
|
445
|
+
const session = this.sessions.get(sessionId);
|
|
446
|
+
if (!session) return jsonResponse(res, 404, { error: "Session not found" });
|
|
447
|
+
if (!session.done) return jsonResponse(res, 409, { error: "Session is still running" });
|
|
448
|
+
const body = await readBody(req);
|
|
449
|
+
if (!body.prompt || typeof body.prompt !== "string") {
|
|
450
|
+
return jsonResponse(res, 400, { error: "Missing required field: prompt" });
|
|
451
|
+
}
|
|
452
|
+
session.done = false;
|
|
453
|
+
session.abortController = new AbortController();
|
|
454
|
+
this.runAgentSse(session, body.prompt, true);
|
|
455
|
+
jsonResponse(res, 200, { ok: true });
|
|
456
|
+
}
|
|
457
|
+
handleDeleteSession(sessionId, res) {
|
|
458
|
+
const session = this.sessions.get(sessionId);
|
|
459
|
+
if (!session) return jsonResponse(res, 404, { error: "Session not found" });
|
|
460
|
+
destroySession(this.sessions, session);
|
|
461
|
+
jsonResponse(res, 200, { ok: true });
|
|
462
|
+
}
|
|
463
|
+
// -------------------------------------------------------------------------
|
|
464
|
+
// WebSocket handling
|
|
465
|
+
// -------------------------------------------------------------------------
|
|
466
|
+
async handleWsConnection(ws, req) {
|
|
467
|
+
if (this.options.auth) {
|
|
468
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
469
|
+
const tokenParam = url.searchParams.get("token");
|
|
470
|
+
if (tokenParam && this.options.auth.type === "bearer") {
|
|
471
|
+
if (tokenParam !== this.options.auth.token) {
|
|
472
|
+
ws.close();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
const authResult = await this.authenticate(req);
|
|
477
|
+
if (!authResult) {
|
|
478
|
+
ws.close();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const wsSessions = /* @__PURE__ */ new Set();
|
|
484
|
+
let pongReceived = true;
|
|
485
|
+
const pingTimer = setInterval(() => {
|
|
486
|
+
if (!pongReceived) {
|
|
487
|
+
ws.close();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
pongReceived = false;
|
|
491
|
+
try {
|
|
492
|
+
ws.ping();
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
}, WS_PING_INTERVAL_MS);
|
|
496
|
+
ws.on("pong", () => {
|
|
497
|
+
pongReceived = true;
|
|
498
|
+
});
|
|
499
|
+
const callbacks = {
|
|
500
|
+
onRun: async (prompt, requestedSessionId) => {
|
|
501
|
+
const overrides = await this.resolveConnectionOverrides(req);
|
|
502
|
+
const session = createSessionState(this.sessions, requestedSessionId, overrides);
|
|
503
|
+
wsSessions.add(session.id);
|
|
504
|
+
this.runAgentWs(session, prompt, ws, false);
|
|
505
|
+
return session.id;
|
|
506
|
+
},
|
|
507
|
+
onMessage: (sessionId, prompt) => {
|
|
508
|
+
const session = this.sessions.get(sessionId);
|
|
509
|
+
if (!session) {
|
|
510
|
+
wsSend(ws, { type: "error", error: "Session not found" });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (!session.done) {
|
|
514
|
+
wsSend(ws, { type: "error", error: "Session is still running" });
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
session.done = false;
|
|
518
|
+
session.abortController = new AbortController();
|
|
519
|
+
this.runAgentWs(session, prompt, ws, true);
|
|
520
|
+
},
|
|
521
|
+
onPermissionResponse: (sessionId, response) => {
|
|
522
|
+
const session = this.sessions.get(sessionId);
|
|
523
|
+
if (!session?.pendingPermission) return;
|
|
524
|
+
clearPendingPermissionTimer(session);
|
|
525
|
+
session.pendingPermission.resolve(response);
|
|
526
|
+
session.pendingPermission = null;
|
|
527
|
+
},
|
|
528
|
+
onInputResponse: (sessionId, answer) => {
|
|
529
|
+
const session = this.sessions.get(sessionId);
|
|
530
|
+
if (!session?.pendingInput) return;
|
|
531
|
+
clearPendingInputTimer(session);
|
|
532
|
+
session.pendingInput.resolve(answer);
|
|
533
|
+
session.pendingInput = null;
|
|
534
|
+
},
|
|
535
|
+
onAbort: (sessionId) => {
|
|
536
|
+
const session = this.sessions.get(sessionId);
|
|
537
|
+
if (session) destroySession(this.sessions, session);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
ws.on("message", async (raw) => {
|
|
541
|
+
try {
|
|
542
|
+
const msg = parseWsMessage(raw);
|
|
543
|
+
if (!msg) {
|
|
544
|
+
wsSend(ws, { type: "error", error: "Invalid JSON" });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const result = await handleWsMessage(msg, {
|
|
548
|
+
maxSessions: this.options.maxSessions,
|
|
549
|
+
currentSessionCount: this.sessions.size
|
|
550
|
+
}, callbacks);
|
|
551
|
+
if (result.type === "error") {
|
|
552
|
+
wsSend(ws, { type: "error", error: result.error });
|
|
553
|
+
} else if (result.type === "session_created") {
|
|
554
|
+
wsSend(ws, { type: "session_created", sessionId: result.sessionId });
|
|
555
|
+
}
|
|
556
|
+
} catch (err) {
|
|
557
|
+
wsSend(ws, { type: "error", error: String(err) });
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
ws.on("close", () => {
|
|
561
|
+
clearInterval(pingTimer);
|
|
562
|
+
for (const sid of wsSessions) {
|
|
563
|
+
const session = this.sessions.get(sid);
|
|
564
|
+
if (session) destroySession(this.sessions, session);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
ws.on("error", () => {
|
|
568
|
+
clearInterval(pingTimer);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
// -------------------------------------------------------------------------
|
|
572
|
+
// Agent runners
|
|
573
|
+
// -------------------------------------------------------------------------
|
|
574
|
+
async makeThread(session, resume) {
|
|
575
|
+
const timeoutMs = this.options.pendingTimeoutMs ?? DEFAULT_PENDING_TIMEOUT_MS;
|
|
576
|
+
const handlers = {
|
|
577
|
+
cwd: session.cwd,
|
|
578
|
+
permissionHandler: (_req) => bridgePermission(this.sessions, session.id, timeoutMs),
|
|
579
|
+
userInputHandler: (_q) => bridgeUserInput(this.sessions, session.id, timeoutMs)
|
|
580
|
+
};
|
|
581
|
+
return resume ? this.code.resumeThread(session.id, handlers) : this.code.createThread({ sessionId: session.id, ...handlers });
|
|
582
|
+
}
|
|
583
|
+
runAgentSse(session, prompt, resume) {
|
|
584
|
+
const run = async () => {
|
|
585
|
+
try {
|
|
586
|
+
const thread = await this.makeThread(session, resume);
|
|
587
|
+
for await (const event of thread.run(prompt, { signal: session.abortController.signal })) {
|
|
588
|
+
pushEvent(session, event);
|
|
589
|
+
session.lastActivity = Date.now();
|
|
590
|
+
}
|
|
591
|
+
} catch (err) {
|
|
592
|
+
if (err.name !== "AbortError") {
|
|
593
|
+
pushEvent(session, {
|
|
594
|
+
type: "error",
|
|
595
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
} finally {
|
|
599
|
+
session.done = true;
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
run().catch((err) => this.options.onError?.(err instanceof Error ? err : new Error(String(err))));
|
|
603
|
+
}
|
|
604
|
+
runAgentWs(session, prompt, ws, resume) {
|
|
605
|
+
const run = async () => {
|
|
606
|
+
try {
|
|
607
|
+
const thread = await this.makeThread(session, resume);
|
|
608
|
+
for await (const event of thread.run(prompt, { signal: session.abortController.signal })) {
|
|
609
|
+
session.sequenceNum++;
|
|
610
|
+
wsSend(ws, { ...serializeEvent(event), sessionId: session.id, seq: session.sequenceNum });
|
|
611
|
+
session.lastActivity = Date.now();
|
|
612
|
+
}
|
|
613
|
+
} catch (err) {
|
|
614
|
+
if (err.name !== "AbortError") {
|
|
615
|
+
wsSend(ws, { type: "error", sessionId: session.id, error: String(err) });
|
|
616
|
+
}
|
|
617
|
+
} finally {
|
|
618
|
+
session.done = true;
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
run().catch((err) => this.options.onError?.(err instanceof Error ? err : new Error(String(err))));
|
|
622
|
+
}
|
|
623
|
+
startSseKeepalive(session) {
|
|
624
|
+
clearSseKeepalive(session);
|
|
625
|
+
session.sseKeepaliveTimer = setInterval(() => {
|
|
626
|
+
if (session.sseResponse && !session.sseResponse.destroyed) {
|
|
627
|
+
session.sseResponse.write(":keepalive\n\n");
|
|
628
|
+
}
|
|
629
|
+
}, SSE_KEEPALIVE_INTERVAL_MS);
|
|
630
|
+
session.sseKeepaliveTimer.unref();
|
|
631
|
+
}
|
|
632
|
+
async resolveConnectionOverrides(req) {
|
|
633
|
+
if (!this.options.onConnection) return {};
|
|
634
|
+
const auth = await this.authenticate(req) ?? {};
|
|
635
|
+
return this.options.onConnection({ auth, remoteAddress: req.socket.remoteAddress });
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
function createServer(code, options) {
|
|
639
|
+
return new NoumenServer(code, options);
|
|
640
|
+
}
|
|
641
|
+
function createRequestHandler(code, options) {
|
|
642
|
+
const serverOpts = {
|
|
643
|
+
port: 0,
|
|
644
|
+
ws: false,
|
|
645
|
+
...options
|
|
646
|
+
};
|
|
647
|
+
const server = new NoumenServer(code, serverOpts);
|
|
648
|
+
return (req, res) => {
|
|
649
|
+
server.handleRequest(req, res);
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function jsonResponse(res, status, body) {
|
|
653
|
+
const json = JSON.stringify(body);
|
|
654
|
+
res.writeHead(status, {
|
|
655
|
+
"Content-Type": "application/json",
|
|
656
|
+
"Content-Length": Buffer.byteLength(json)
|
|
657
|
+
});
|
|
658
|
+
res.end(json);
|
|
659
|
+
}
|
|
660
|
+
function readBody(req) {
|
|
661
|
+
return new Promise((resolve, reject) => {
|
|
662
|
+
let totalBytes = 0;
|
|
663
|
+
let rejected = false;
|
|
664
|
+
const chunks = [];
|
|
665
|
+
req.on("data", (chunk) => {
|
|
666
|
+
if (rejected) return;
|
|
667
|
+
totalBytes += chunk.length;
|
|
668
|
+
if (totalBytes > MAX_BODY_BYTES) {
|
|
669
|
+
rejected = true;
|
|
670
|
+
req.destroy();
|
|
671
|
+
reject(new Error("Request body too large"));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
chunks.push(chunk);
|
|
675
|
+
});
|
|
676
|
+
req.on("end", () => {
|
|
677
|
+
if (rejected) return;
|
|
678
|
+
try {
|
|
679
|
+
const raw = Buffer.concat(chunks).toString();
|
|
680
|
+
resolve(raw ? JSON.parse(raw) : {});
|
|
681
|
+
} catch (err) {
|
|
682
|
+
reject(err);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
req.on("error", (err) => {
|
|
686
|
+
if (!rejected) reject(err);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
export {
|
|
691
|
+
NoumenServer,
|
|
692
|
+
createRequestHandler,
|
|
693
|
+
createServer
|
|
694
|
+
};
|
|
695
|
+
//# sourceMappingURL=index.js.map
|