agent-ws 1.0.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/dist/agent.js ADDED
@@ -0,0 +1,760 @@
1
+ // src/server/websocket.ts
2
+ import { WebSocketServer, WebSocket } from "ws";
3
+
4
+ // src/process/claude-runner.ts
5
+ import { spawn } from "node:child_process";
6
+ import { mkdirSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ import { createInterface } from "node:readline";
10
+ var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
11
+ var ClaudeRunner = class {
12
+ process = null;
13
+ timeout = null;
14
+ disposed = false;
15
+ killed = false;
16
+ claudePath;
17
+ timeoutMs;
18
+ log;
19
+ sessionDir;
20
+ constructor(options) {
21
+ this.claudePath = options.claudePath ?? "claude";
22
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
23
+ this.log = options.logger;
24
+ this.sessionDir = options.sessionDir ?? "agent-ws-sessions";
25
+ }
26
+ get isRunning() {
27
+ return this.process !== null;
28
+ }
29
+ run(options, handlers) {
30
+ if (this.disposed) {
31
+ handlers.onError("Runner has been disposed", options.requestId);
32
+ return;
33
+ }
34
+ this.kill();
35
+ const { prompt, model, systemPrompt, projectId, requestId, thinkingTokens } = options;
36
+ const args = [
37
+ "--print",
38
+ "--output-format",
39
+ "stream-json",
40
+ "--max-turns",
41
+ "1",
42
+ // Single-turn text output, no agentic loops
43
+ "--tools",
44
+ ""
45
+ // Disable tool use — we only want generated text
46
+ ];
47
+ if (projectId) {
48
+ args.push("--continue");
49
+ }
50
+ if (model) {
51
+ args.push("--model", model);
52
+ }
53
+ if (systemPrompt) {
54
+ args.push("--append-system-prompt", systemPrompt);
55
+ }
56
+ args.push("-");
57
+ this.log.info({ requestId, model, promptLength: prompt.length }, "Spawning Claude process");
58
+ this.killed = false;
59
+ let cwd;
60
+ if (projectId) {
61
+ const base = resolve(tmpdir(), this.sessionDir);
62
+ cwd = resolve(base, projectId);
63
+ if (!cwd.startsWith(base + "/") && cwd !== base) {
64
+ handlers.onError("Invalid projectId", requestId);
65
+ return;
66
+ }
67
+ mkdirSync(cwd, { recursive: true });
68
+ }
69
+ try {
70
+ const ALLOWED_ENV_KEYS = [
71
+ "PATH",
72
+ "HOME",
73
+ "USER",
74
+ "SHELL",
75
+ "TERM",
76
+ "LANG",
77
+ "LC_ALL",
78
+ "ANTHROPIC_API_KEY",
79
+ "NODE_PATH",
80
+ "XDG_CONFIG_HOME"
81
+ ];
82
+ const env = {};
83
+ if (thinkingTokens !== void 0) {
84
+ env["MAX_THINKING_TOKENS"] = String(thinkingTokens);
85
+ }
86
+ for (const key of ALLOWED_ENV_KEYS) {
87
+ if (process.env[key]) env[key] = process.env[key];
88
+ }
89
+ this.process = spawn(this.claudePath, args, {
90
+ stdio: ["pipe", "pipe", "pipe"],
91
+ cwd,
92
+ env
93
+ });
94
+ } catch (err) {
95
+ const message = err instanceof Error ? err.message : "Failed to start Claude";
96
+ this.log.error({ err, requestId }, "Failed to spawn Claude process");
97
+ handlers.onError(message, requestId);
98
+ return;
99
+ }
100
+ this.log.debug({ pid: this.process.pid, requestId }, "Claude process spawned");
101
+ if (this.process.stdin) {
102
+ this.process.stdin.write(prompt);
103
+ this.process.stdin.end();
104
+ }
105
+ let handlersDone = false;
106
+ const finish = (cb) => {
107
+ if (handlersDone) return;
108
+ handlersDone = true;
109
+ this.clearTimeout();
110
+ cb();
111
+ };
112
+ this.timeout = setTimeout(() => {
113
+ this.log.warn({ requestId }, "Claude process timed out");
114
+ this.kill();
115
+ finish(() => handlers.onError("Process timed out", requestId));
116
+ }, this.timeoutMs);
117
+ if (this.process.stdout) {
118
+ const rl = createInterface({ input: this.process.stdout });
119
+ rl.on("line", (line) => {
120
+ this.parseStreamLine(line, handlers, requestId);
121
+ });
122
+ }
123
+ if (this.process.stderr) {
124
+ const stderrRl = createInterface({ input: this.process.stderr });
125
+ stderrRl.on("line", (line) => {
126
+ if (line.trim()) {
127
+ this.log.warn({ requestId, stderr: line }, "Claude stderr");
128
+ }
129
+ });
130
+ }
131
+ this.process.on("exit", (exitCode, signal) => {
132
+ this.process = null;
133
+ if (this.killed) {
134
+ this.log.debug({ requestId }, "Claude process was killed");
135
+ return;
136
+ }
137
+ if (exitCode === 0) {
138
+ this.log.info({ requestId }, "Claude process completed successfully");
139
+ finish(() => handlers.onComplete(requestId));
140
+ } else {
141
+ const reason = exitCode !== null ? `Claude CLI exited with code ${exitCode}` : `Claude CLI killed by signal ${signal ?? "unknown"}`;
142
+ this.log.warn({ requestId, exitCode, signal }, reason);
143
+ finish(() => handlers.onError(reason, requestId));
144
+ }
145
+ });
146
+ this.process.on("error", (err) => {
147
+ this.process = null;
148
+ this.log.error({ err, requestId }, "Claude process error");
149
+ finish(() => handlers.onError(err.message, requestId));
150
+ });
151
+ }
152
+ /**
153
+ * Parse a single NDJSON line from Claude CLI's stream-json output.
154
+ *
155
+ * The stream-json format can emit several event types. We look for content
156
+ * in these known patterns (in priority order):
157
+ *
158
+ * 1. Raw Anthropic API event: { type: "content_block_delta", delta: { type: "text_delta"|"thinking_delta", text|thinking } }
159
+ * 2. Wrapped stream event: { type: "stream_event", event: { type: "content_block_delta", ... } }
160
+ * 3. Complete assistant msg: { type: "assistant", message: { content: [{ type: "text"|"thinking", text|thinking }] } }
161
+ */
162
+ parseStreamLine(line, handlers, requestId) {
163
+ if (!line.trim()) return;
164
+ try {
165
+ const event = JSON.parse(line);
166
+ if (event.type === "content_block_delta") {
167
+ if (event.delta?.type === "text_delta" && event.delta.text) {
168
+ handlers.onChunk(event.delta.text, requestId);
169
+ } else if (event.delta?.type === "thinking_delta" && event.delta.thinking) {
170
+ handlers.onChunk(event.delta.thinking, requestId, true);
171
+ }
172
+ return;
173
+ }
174
+ if (event.type === "stream_event" && event.event) {
175
+ const inner = event.event;
176
+ if (inner.type === "content_block_delta") {
177
+ if (inner.delta?.type === "text_delta" && inner.delta.text) {
178
+ handlers.onChunk(inner.delta.text, requestId);
179
+ } else if (inner.delta?.type === "thinking_delta" && inner.delta.thinking) {
180
+ handlers.onChunk(inner.delta.thinking, requestId, true);
181
+ }
182
+ }
183
+ return;
184
+ }
185
+ if (event.type === "assistant" && Array.isArray(event.message?.content)) {
186
+ for (const block of event.message.content) {
187
+ if (block.type === "text" && block.text) {
188
+ handlers.onChunk(block.text, requestId);
189
+ } else if (block.type === "thinking" && block.thinking) {
190
+ handlers.onChunk(block.thinking, requestId, true);
191
+ }
192
+ }
193
+ return;
194
+ }
195
+ if (event.type === "result") {
196
+ return;
197
+ }
198
+ } catch {
199
+ }
200
+ }
201
+ kill() {
202
+ this.clearTimeout();
203
+ if (this.process) {
204
+ this.log.debug({ pid: this.process.pid }, "Killing Claude process");
205
+ this.killed = true;
206
+ try {
207
+ this.process.kill();
208
+ } catch {
209
+ }
210
+ this.process = null;
211
+ }
212
+ }
213
+ dispose() {
214
+ this.disposed = true;
215
+ this.kill();
216
+ }
217
+ clearTimeout() {
218
+ if (this.timeout) {
219
+ clearTimeout(this.timeout);
220
+ this.timeout = null;
221
+ }
222
+ }
223
+ };
224
+
225
+ // src/process/codex-runner.ts
226
+ import { spawn as spawn2 } from "node:child_process";
227
+ import { mkdirSync as mkdirSync2 } from "node:fs";
228
+ import { resolve as resolve2 } from "node:path";
229
+ import { tmpdir as tmpdir2 } from "node:os";
230
+ import { createInterface as createInterface2 } from "node:readline";
231
+ var DEFAULT_TIMEOUT_MS2 = 5 * 60 * 1e3;
232
+ var CodexRunner = class {
233
+ process = null;
234
+ timeout = null;
235
+ disposed = false;
236
+ killed = false;
237
+ codexPath;
238
+ timeoutMs;
239
+ log;
240
+ sessionDir;
241
+ constructor(options) {
242
+ this.codexPath = options.codexPath ?? "codex";
243
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
244
+ this.log = options.logger;
245
+ this.sessionDir = options.sessionDir ?? "agent-ws-sessions";
246
+ }
247
+ get isRunning() {
248
+ return this.process !== null;
249
+ }
250
+ run(options, handlers) {
251
+ if (this.disposed) {
252
+ handlers.onError("Runner has been disposed", options.requestId);
253
+ return;
254
+ }
255
+ this.kill();
256
+ const { prompt, model, systemPrompt, projectId, requestId } = options;
257
+ let fullPrompt = prompt;
258
+ if (systemPrompt) {
259
+ fullPrompt = `${systemPrompt}
260
+
261
+ ---
262
+
263
+ ${prompt}`;
264
+ }
265
+ const args = ["--json"];
266
+ if (model) {
267
+ args.push("--model", model);
268
+ }
269
+ args.push("-");
270
+ this.log.info({ requestId, model, promptLength: prompt.length }, "Spawning Codex process");
271
+ this.killed = false;
272
+ let cwd;
273
+ if (projectId) {
274
+ const base = resolve2(tmpdir2(), this.sessionDir);
275
+ cwd = resolve2(base, projectId);
276
+ if (!cwd.startsWith(base + "/") && cwd !== base) {
277
+ handlers.onError("Invalid projectId", requestId);
278
+ return;
279
+ }
280
+ mkdirSync2(cwd, { recursive: true });
281
+ }
282
+ const ALLOWED_ENV_KEYS = [
283
+ "PATH",
284
+ "HOME",
285
+ "USER",
286
+ "SHELL",
287
+ "TERM",
288
+ "LANG",
289
+ "LC_ALL",
290
+ "OPENAI_API_KEY",
291
+ "NODE_PATH",
292
+ "XDG_CONFIG_HOME"
293
+ ];
294
+ const env = {};
295
+ for (const key of ALLOWED_ENV_KEYS) {
296
+ if (process.env[key]) env[key] = process.env[key];
297
+ }
298
+ try {
299
+ this.process = spawn2(this.codexPath, args, {
300
+ stdio: ["pipe", "pipe", "pipe"],
301
+ cwd,
302
+ env
303
+ });
304
+ } catch (err) {
305
+ const message = err instanceof Error ? err.message : "Failed to start Codex";
306
+ this.log.error({ err, requestId }, "Failed to spawn Codex process");
307
+ handlers.onError(message, requestId);
308
+ return;
309
+ }
310
+ this.log.debug({ pid: this.process.pid, requestId }, "Codex process spawned");
311
+ if (this.process.stdin) {
312
+ this.process.stdin.write(fullPrompt);
313
+ this.process.stdin.end();
314
+ }
315
+ let handlersDone = false;
316
+ const finish = (cb) => {
317
+ if (handlersDone) return;
318
+ handlersDone = true;
319
+ this.clearTimeout();
320
+ cb();
321
+ };
322
+ this.timeout = setTimeout(() => {
323
+ this.log.warn({ requestId }, "Codex process timed out");
324
+ this.kill();
325
+ finish(() => handlers.onError("Process timed out", requestId));
326
+ }, this.timeoutMs);
327
+ if (this.process.stdout) {
328
+ const rl = createInterface2({ input: this.process.stdout });
329
+ rl.on("line", (line) => {
330
+ this.parseStreamLine(line, handlers, requestId);
331
+ });
332
+ }
333
+ if (this.process.stderr) {
334
+ const stderrRl = createInterface2({ input: this.process.stderr });
335
+ stderrRl.on("line", (line) => {
336
+ if (line.trim()) {
337
+ this.log.warn({ requestId, stderr: line }, "Codex stderr");
338
+ }
339
+ });
340
+ }
341
+ this.process.on("exit", (exitCode, signal) => {
342
+ this.process = null;
343
+ if (this.killed) {
344
+ this.log.debug({ requestId }, "Codex process was killed");
345
+ return;
346
+ }
347
+ if (exitCode === 0) {
348
+ this.log.info({ requestId }, "Codex process completed successfully");
349
+ finish(() => handlers.onComplete(requestId));
350
+ } else {
351
+ const reason = exitCode !== null ? `Codex CLI exited with code ${exitCode}` : `Codex CLI killed by signal ${signal ?? "unknown"}`;
352
+ this.log.warn({ requestId, exitCode, signal }, reason);
353
+ finish(() => handlers.onError(reason, requestId));
354
+ }
355
+ });
356
+ this.process.on("error", (err) => {
357
+ this.process = null;
358
+ this.log.error({ err, requestId }, "Codex process error");
359
+ finish(() => handlers.onError(err.message, requestId));
360
+ });
361
+ }
362
+ /**
363
+ * Parse JSONL output from Codex CLI.
364
+ * Looks for text content in response events.
365
+ */
366
+ parseStreamLine(line, handlers, requestId) {
367
+ if (!line.trim()) return;
368
+ try {
369
+ const event = JSON.parse(line);
370
+ if (event.type === "message" && event.role === "assistant") {
371
+ if (Array.isArray(event.content)) {
372
+ for (const block of event.content) {
373
+ if (block.type === "output_text" && block.text) {
374
+ handlers.onChunk(block.text, requestId);
375
+ } else if (block.type === "text" && block.text) {
376
+ handlers.onChunk(block.text, requestId);
377
+ }
378
+ }
379
+ } else if (typeof event.content === "string" && event.content) {
380
+ handlers.onChunk(event.content, requestId);
381
+ }
382
+ return;
383
+ }
384
+ if (event.type === "response.completed" || event.type === "item.completed") {
385
+ return;
386
+ }
387
+ } catch {
388
+ }
389
+ }
390
+ kill() {
391
+ this.clearTimeout();
392
+ if (this.process) {
393
+ this.log.debug({ pid: this.process.pid }, "Killing Codex process");
394
+ this.killed = true;
395
+ try {
396
+ this.process.kill();
397
+ } catch {
398
+ }
399
+ this.process = null;
400
+ }
401
+ }
402
+ dispose() {
403
+ this.disposed = true;
404
+ this.kill();
405
+ }
406
+ clearTimeout() {
407
+ if (this.timeout) {
408
+ clearTimeout(this.timeout);
409
+ this.timeout = null;
410
+ }
411
+ }
412
+ };
413
+
414
+ // src/server/protocol.ts
415
+ var MAX_PROMPT_BYTES = 512 * 1024;
416
+ var MAX_SYSTEM_PROMPT_BYTES = 64 * 1024;
417
+ var MAX_PROJECT_ID_LENGTH = 128;
418
+ var PROJECT_ID_PATTERN = /^[a-zA-Z0-9._-]+$/;
419
+ function parseClientMessage(raw) {
420
+ let data;
421
+ try {
422
+ data = JSON.parse(raw);
423
+ } catch {
424
+ return { ok: false, error: "Invalid JSON" };
425
+ }
426
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
427
+ return { ok: false, error: "Message must be a JSON object" };
428
+ }
429
+ const obj = data;
430
+ const type = obj["type"];
431
+ if (typeof type !== "string") {
432
+ return { ok: false, error: "Missing or invalid 'type' field" };
433
+ }
434
+ switch (type) {
435
+ case "prompt": {
436
+ const prompt = obj["prompt"];
437
+ if (typeof prompt !== "string" || prompt.length === 0) {
438
+ return { ok: false, error: "Missing or empty 'prompt' field" };
439
+ }
440
+ if (new TextEncoder().encode(prompt).byteLength > MAX_PROMPT_BYTES) {
441
+ return { ok: false, error: `Prompt exceeds maximum size of ${MAX_PROMPT_BYTES} bytes` };
442
+ }
443
+ const requestId = obj["requestId"];
444
+ if (typeof requestId !== "string" || requestId.length === 0) {
445
+ return { ok: false, error: "Missing or empty 'requestId' field" };
446
+ }
447
+ const model = obj["model"];
448
+ const systemPrompt = obj["systemPrompt"];
449
+ const projectId = obj["projectId"];
450
+ const provider = obj["provider"];
451
+ const thinkingTokens = obj["thinkingTokens"];
452
+ if (typeof systemPrompt === "string" && new TextEncoder().encode(systemPrompt).byteLength > MAX_SYSTEM_PROMPT_BYTES) {
453
+ return { ok: false, error: `System prompt exceeds maximum size of ${MAX_SYSTEM_PROMPT_BYTES} bytes` };
454
+ }
455
+ if (typeof projectId === "string") {
456
+ if (projectId.length > MAX_PROJECT_ID_LENGTH) {
457
+ return { ok: false, error: `projectId exceeds maximum length of ${MAX_PROJECT_ID_LENGTH}` };
458
+ }
459
+ if (!PROJECT_ID_PATTERN.test(projectId)) {
460
+ return { ok: false, error: "projectId contains invalid characters (allowed: alphanumeric, hyphens, underscores, dots)" };
461
+ }
462
+ }
463
+ return {
464
+ ok: true,
465
+ message: {
466
+ type: "prompt",
467
+ prompt,
468
+ model: typeof model === "string" ? model : void 0,
469
+ systemPrompt: typeof systemPrompt === "string" ? systemPrompt : void 0,
470
+ projectId: typeof projectId === "string" ? projectId : void 0,
471
+ requestId,
472
+ provider: provider === "codex" ? "codex" : "claude",
473
+ thinkingTokens: typeof thinkingTokens === "number" && thinkingTokens >= 0 ? thinkingTokens : void 0
474
+ }
475
+ };
476
+ }
477
+ case "cancel": {
478
+ const requestId = obj["requestId"];
479
+ return {
480
+ ok: true,
481
+ message: {
482
+ type: "cancel",
483
+ requestId: typeof requestId === "string" ? requestId : void 0
484
+ }
485
+ };
486
+ }
487
+ default:
488
+ return { ok: false, error: `Unknown message type: ${String(type).slice(0, 50)}` };
489
+ }
490
+ }
491
+ function serializeMessage(message) {
492
+ return JSON.stringify(message);
493
+ }
494
+
495
+ // src/server/websocket.ts
496
+ var HEARTBEAT_INTERVAL_MS = 3e4;
497
+ var DEFAULT_MAX_PAYLOAD = 1024 * 1024;
498
+ var AgentWebSocketServer = class {
499
+ wss = null;
500
+ heartbeatInterval = null;
501
+ connections = /* @__PURE__ */ new Map();
502
+ log;
503
+ options;
504
+ constructor(options) {
505
+ this.options = options;
506
+ this.log = options.logger;
507
+ }
508
+ start() {
509
+ return new Promise((resolve3, reject) => {
510
+ this.wss = new WebSocketServer({
511
+ port: this.options.port,
512
+ host: this.options.host,
513
+ maxPayload: this.options.maxPayload ?? DEFAULT_MAX_PAYLOAD
514
+ });
515
+ this.wss.on("listening", () => {
516
+ this.log.info({ port: this.options.port, host: this.options.host }, "WebSocket server started");
517
+ this.startHeartbeat();
518
+ resolve3();
519
+ });
520
+ this.wss.on("error", (err) => {
521
+ if (err.code === "EADDRINUSE") {
522
+ this.log.fatal({ port: this.options.port }, "Port already in use");
523
+ } else {
524
+ this.log.error({ err }, "WebSocket server error");
525
+ }
526
+ reject(err);
527
+ });
528
+ this.wss.on("connection", (ws, req) => this.handleConnection(ws, req));
529
+ });
530
+ }
531
+ stop() {
532
+ if (this.heartbeatInterval) {
533
+ clearInterval(this.heartbeatInterval);
534
+ this.heartbeatInterval = null;
535
+ }
536
+ for (const [ws, state] of this.connections) {
537
+ state.runner.dispose();
538
+ ws.terminate();
539
+ }
540
+ this.connections.clear();
541
+ if (this.wss) {
542
+ this.wss.close();
543
+ this.wss = null;
544
+ }
545
+ this.log.info("WebSocket server stopped");
546
+ }
547
+ handleConnection(ws, req) {
548
+ if (this.options.allowedOrigins && this.options.allowedOrigins.length > 0) {
549
+ const origin = req.headers.origin;
550
+ if (!origin || !this.options.allowedOrigins.includes(origin)) {
551
+ this.log.warn({ origin: origin ?? "(none)" }, "Rejected connection: origin not in allowlist");
552
+ ws.close(4003, "Origin not allowed");
553
+ return;
554
+ }
555
+ }
556
+ const clientIp = req.socket.remoteAddress;
557
+ this.log.info({ clientIp }, "Client connected");
558
+ const runner = this.createRunner();
559
+ const state = { runner, isAlive: true, activeRequestId: null };
560
+ this.connections.set(ws, state);
561
+ this.sendMessage(ws, {
562
+ type: "connected",
563
+ version: "1.0",
564
+ agent: this.options.agentName ?? "agent-ws"
565
+ });
566
+ ws.on("pong", () => {
567
+ state.isAlive = true;
568
+ });
569
+ ws.on("message", (data) => {
570
+ const raw = data.toString();
571
+ this.handleMessage(ws, state, raw);
572
+ });
573
+ ws.on("close", () => {
574
+ this.log.info({ clientIp }, "Client disconnected");
575
+ state.runner.dispose();
576
+ this.connections.delete(ws);
577
+ });
578
+ ws.on("error", (err) => {
579
+ this.log.error({ err, clientIp }, "WebSocket error");
580
+ });
581
+ }
582
+ handleMessage(ws, state, raw) {
583
+ const result = parseClientMessage(raw);
584
+ if (!result.ok) {
585
+ this.sendMessage(ws, { type: "error", message: result.error });
586
+ return;
587
+ }
588
+ const { message } = result;
589
+ switch (message.type) {
590
+ case "prompt":
591
+ this.handlePrompt(ws, state, message);
592
+ break;
593
+ case "cancel":
594
+ this.handleCancel(ws, state);
595
+ break;
596
+ }
597
+ }
598
+ handlePrompt(ws, state, message) {
599
+ if (state.activeRequestId !== null) {
600
+ this.sendMessage(ws, {
601
+ type: "error",
602
+ message: "Request already in progress",
603
+ requestId: message.requestId
604
+ });
605
+ return;
606
+ }
607
+ state.activeRequestId = message.requestId;
608
+ if (message.provider === "codex") {
609
+ if (!(state.runner instanceof CodexRunner)) {
610
+ try {
611
+ state.runner.dispose();
612
+ } catch {
613
+ }
614
+ state.runner = new CodexRunner({
615
+ timeoutMs: this.options.timeoutMs,
616
+ logger: this.log.child({ component: "codex-runner" }),
617
+ sessionDir: this.options.sessionDir
618
+ });
619
+ }
620
+ } else {
621
+ if (!(state.runner instanceof ClaudeRunner)) {
622
+ try {
623
+ state.runner.dispose();
624
+ } catch {
625
+ }
626
+ state.runner = this.createRunner();
627
+ }
628
+ }
629
+ const handlers = {
630
+ onChunk: (content, requestId, thinking) => {
631
+ try {
632
+ this.sendMessage(ws, { type: "chunk", content, requestId, ...thinking ? { thinking: true } : {} });
633
+ } catch (err) {
634
+ this.log.warn({ err, requestId }, "Error in onChunk handler");
635
+ }
636
+ },
637
+ onComplete: (requestId) => {
638
+ try {
639
+ state.activeRequestId = null;
640
+ this.sendMessage(ws, { type: "complete", requestId });
641
+ } catch (err) {
642
+ this.log.warn({ err, requestId }, "Error in onComplete handler");
643
+ }
644
+ },
645
+ onError: (errorMessage, requestId) => {
646
+ try {
647
+ state.activeRequestId = null;
648
+ this.sendMessage(ws, { type: "error", message: errorMessage, requestId });
649
+ } catch (err) {
650
+ this.log.warn({ err, requestId }, "Error in onError handler");
651
+ }
652
+ }
653
+ };
654
+ state.runner.run(
655
+ { prompt: message.prompt, model: message.model, systemPrompt: message.systemPrompt, projectId: message.projectId, requestId: message.requestId, thinkingTokens: message.thinkingTokens },
656
+ handlers
657
+ );
658
+ }
659
+ handleCancel(ws, state) {
660
+ state.runner.kill();
661
+ const requestId = state.activeRequestId;
662
+ state.activeRequestId = null;
663
+ this.log.info({ requestId }, "Request cancelled");
664
+ }
665
+ sendMessage(ws, message) {
666
+ if (ws.readyState === WebSocket.OPEN) {
667
+ ws.send(serializeMessage(message));
668
+ } else {
669
+ this.log.warn({ messageType: message.type, readyState: ws.readyState }, "Dropping message, WebSocket not OPEN");
670
+ }
671
+ }
672
+ createRunner() {
673
+ if (this.options.runnerFactory) {
674
+ return this.options.runnerFactory(this.log);
675
+ }
676
+ const runnerOptions = {
677
+ claudePath: this.options.claudePath,
678
+ timeoutMs: this.options.timeoutMs,
679
+ logger: this.log.child({ component: "runner" }),
680
+ sessionDir: this.options.sessionDir
681
+ };
682
+ return new ClaudeRunner(runnerOptions);
683
+ }
684
+ startHeartbeat() {
685
+ this.heartbeatInterval = setInterval(() => {
686
+ for (const [ws, state] of this.connections) {
687
+ if (!state.isAlive) {
688
+ this.log.debug("Terminating dead connection");
689
+ state.runner.dispose();
690
+ this.connections.delete(ws);
691
+ ws.terminate();
692
+ continue;
693
+ }
694
+ state.isAlive = false;
695
+ try {
696
+ ws.ping();
697
+ } catch {
698
+ this.log.debug("Ping failed, terminating connection");
699
+ state.runner.dispose();
700
+ this.connections.delete(ws);
701
+ ws.terminate();
702
+ }
703
+ }
704
+ }, HEARTBEAT_INTERVAL_MS);
705
+ }
706
+ };
707
+
708
+ // src/utils/logger.ts
709
+ import pino from "pino";
710
+ function createLogger(options = {}) {
711
+ const { level = "info", pretty = process.env["NODE_ENV"] !== "production" } = options;
712
+ if (pretty) {
713
+ try {
714
+ return pino({
715
+ level,
716
+ transport: {
717
+ target: "pino-pretty",
718
+ options: {
719
+ colorize: true,
720
+ translateTime: "HH:MM:ss",
721
+ ignore: "pid,hostname"
722
+ }
723
+ }
724
+ });
725
+ } catch {
726
+ }
727
+ }
728
+ return pino({ level });
729
+ }
730
+
731
+ // src/agent.ts
732
+ var AgentWS = class {
733
+ server;
734
+ log;
735
+ constructor(options = {}) {
736
+ this.log = createLogger({ level: options.logLevel ?? "info" });
737
+ const serverOptions = {
738
+ port: options.port ?? 9999,
739
+ host: options.host ?? "localhost",
740
+ logger: this.log,
741
+ claudePath: options.claudePath,
742
+ timeoutMs: options.timeoutMs,
743
+ allowedOrigins: options.allowedOrigins,
744
+ runnerFactory: options.runnerFactory,
745
+ agentName: options.agentName,
746
+ sessionDir: options.sessionDir
747
+ };
748
+ this.server = new AgentWebSocketServer(serverOptions);
749
+ }
750
+ async start() {
751
+ await this.server.start();
752
+ }
753
+ stop() {
754
+ this.server.stop();
755
+ }
756
+ };
757
+ export {
758
+ AgentWS
759
+ };
760
+ //# sourceMappingURL=agent.js.map