pi-app-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/dist/command-classification.d.ts +59 -0
  4. package/dist/command-classification.d.ts.map +1 -0
  5. package/dist/command-classification.js +78 -0
  6. package/dist/command-classification.js.map +7 -0
  7. package/dist/command-execution-engine.d.ts +118 -0
  8. package/dist/command-execution-engine.d.ts.map +1 -0
  9. package/dist/command-execution-engine.js +259 -0
  10. package/dist/command-execution-engine.js.map +7 -0
  11. package/dist/command-replay-store.d.ts +241 -0
  12. package/dist/command-replay-store.d.ts.map +1 -0
  13. package/dist/command-replay-store.js +306 -0
  14. package/dist/command-replay-store.js.map +7 -0
  15. package/dist/command-router.d.ts +25 -0
  16. package/dist/command-router.d.ts.map +1 -0
  17. package/dist/command-router.js +353 -0
  18. package/dist/command-router.js.map +7 -0
  19. package/dist/extension-ui.d.ts +139 -0
  20. package/dist/extension-ui.d.ts.map +1 -0
  21. package/dist/extension-ui.js +189 -0
  22. package/dist/extension-ui.js.map +7 -0
  23. package/dist/resource-governor.d.ts +254 -0
  24. package/dist/resource-governor.d.ts.map +1 -0
  25. package/dist/resource-governor.js +603 -0
  26. package/dist/resource-governor.js.map +7 -0
  27. package/dist/server-command-handlers.d.ts +120 -0
  28. package/dist/server-command-handlers.d.ts.map +1 -0
  29. package/dist/server-command-handlers.js +234 -0
  30. package/dist/server-command-handlers.js.map +7 -0
  31. package/dist/server-ui-context.d.ts +22 -0
  32. package/dist/server-ui-context.d.ts.map +1 -0
  33. package/dist/server-ui-context.js +221 -0
  34. package/dist/server-ui-context.js.map +7 -0
  35. package/dist/server.d.ts +82 -0
  36. package/dist/server.d.ts.map +1 -0
  37. package/dist/server.js +561 -0
  38. package/dist/server.js.map +7 -0
  39. package/dist/session-lock-manager.d.ts +100 -0
  40. package/dist/session-lock-manager.d.ts.map +1 -0
  41. package/dist/session-lock-manager.js +199 -0
  42. package/dist/session-lock-manager.js.map +7 -0
  43. package/dist/session-manager.d.ts +196 -0
  44. package/dist/session-manager.d.ts.map +1 -0
  45. package/dist/session-manager.js +1010 -0
  46. package/dist/session-manager.js.map +7 -0
  47. package/dist/session-store.d.ts +190 -0
  48. package/dist/session-store.d.ts.map +1 -0
  49. package/dist/session-store.js +446 -0
  50. package/dist/session-store.js.map +7 -0
  51. package/dist/session-version-store.d.ts +83 -0
  52. package/dist/session-version-store.d.ts.map +1 -0
  53. package/dist/session-version-store.js +117 -0
  54. package/dist/session-version-store.js.map +7 -0
  55. package/dist/type-guards.d.ts +59 -0
  56. package/dist/type-guards.d.ts.map +1 -0
  57. package/dist/type-guards.js +40 -0
  58. package/dist/type-guards.js.map +7 -0
  59. package/dist/types.d.ts +621 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +23 -0
  62. package/dist/types.js.map +7 -0
  63. package/dist/validation.d.ts +22 -0
  64. package/dist/validation.d.ts.map +1 -0
  65. package/dist/validation.js +323 -0
  66. package/dist/validation.js.map +7 -0
  67. package/package.json +135 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/server-ui-context.ts"],
4
+ "sourcesContent": ["/**\n * Server UI Context - implements ExtensionUIContext for remote clients.\n *\n * Extension UI requests (select, confirm, input, etc.) are:\n * 1. Broadcast to all subscribed clients via extension_ui_request event\n * 2. Tracked as pending requests in ExtensionUIManager\n * 3. Resolved when a client sends extension_ui_response command\n *\n * This enables skills, prompt templates, and custom tools that need user input\n * to work over the WebSocket/stdio transport.\n */\n\nimport type { ExtensionUIContext, TerminalInputHandler } from \"@mariozechner/pi-coding-agent\";\nimport type { ExtensionUIManager } from \"./extension-ui.js\";\nimport {\n isSelectResponse,\n isConfirmResponse,\n isInputResponse,\n isEditorResponse,\n} from \"./extension-ui.js\";\n\n/**\n * Create an ExtensionUIContext that routes UI requests to remote clients.\n *\n * @param sessionId The session ID for request routing\n * @param extensionUI The manager that tracks pending requests\n * @param broadcast Function to broadcast events to subscribers\n */\nexport function createServerUIContext(\n sessionId: string,\n extensionUI: ExtensionUIManager,\n broadcast: (sessionId: string, event: any) => void\n): ExtensionUIContext {\n return {\n async select(\n title: string,\n options: string[],\n opts?: { signal?: AbortSignal; timeout?: number }\n ): Promise<string | undefined> {\n const request = extensionUI.createPendingRequest(sessionId, \"select\", {\n title,\n options,\n timeout: opts?.timeout,\n });\n\n // If limit reached, return undefined (no selection possible)\n if (!request) return undefined;\n\n extensionUI.broadcastUIRequest(sessionId, request.requestId, \"select\", {\n title,\n options,\n timeout: opts?.timeout,\n });\n\n try {\n const response = await raceWithAbortAndSignal(request.promise, opts?.signal, () =>\n extensionUI.cancelRequest(request.requestId)\n );\n if (response.method === \"cancelled\") return undefined;\n if (isSelectResponse(response)) return response.value;\n return undefined;\n } catch {\n // Timeout or abort - return undefined (no selection)\n return undefined;\n }\n },\n\n async confirm(\n title: string,\n message: string,\n opts?: { signal?: AbortSignal; timeout?: number }\n ): Promise<boolean> {\n const request = extensionUI.createPendingRequest(sessionId, \"confirm\", {\n title,\n message,\n timeout: opts?.timeout,\n });\n\n // If limit reached, return false (not confirmed)\n if (!request) return false;\n\n extensionUI.broadcastUIRequest(sessionId, request.requestId, \"confirm\", {\n title,\n message,\n timeout: opts?.timeout,\n });\n\n try {\n const response = await raceWithAbortAndSignal(request.promise, opts?.signal, () =>\n extensionUI.cancelRequest(request.requestId)\n );\n if (response.method === \"cancelled\") return false;\n if (isConfirmResponse(response)) return response.confirmed;\n return false;\n } catch {\n // Timeout or abort - return false (not confirmed)\n return false;\n }\n },\n\n async input(\n title: string,\n placeholder?: string,\n opts?: { signal?: AbortSignal; timeout?: number }\n ): Promise<string | undefined> {\n const request = extensionUI.createPendingRequest(sessionId, \"input\", {\n title,\n placeholder,\n timeout: opts?.timeout,\n });\n\n // If limit reached, return undefined (no input possible)\n if (!request) return undefined;\n\n extensionUI.broadcastUIRequest(sessionId, request.requestId, \"input\", {\n title,\n placeholder,\n timeout: opts?.timeout,\n });\n\n try {\n const response = await raceWithAbortAndSignal(request.promise, opts?.signal, () =>\n extensionUI.cancelRequest(request.requestId)\n );\n if (response.method === \"cancelled\") return undefined;\n if (isInputResponse(response)) return response.value;\n return undefined;\n } catch {\n // Timeout or abort - return undefined\n return undefined;\n }\n },\n\n async editor(title: string, prefill?: string): Promise<string | undefined> {\n const request = extensionUI.createPendingRequest(sessionId, \"editor\", {\n title,\n prefill,\n });\n\n // If limit reached, return undefined (no editor input possible)\n if (!request) return undefined;\n\n extensionUI.broadcastUIRequest(sessionId, request.requestId, \"editor\", {\n title,\n prefill,\n });\n\n try {\n const response = await raceWithAbortAndSignal(\n request.promise,\n undefined, // editor doesn't typically use abort signal\n () => extensionUI.cancelRequest(request.requestId)\n );\n if (response.method === \"cancelled\") return undefined;\n if (isEditorResponse(response)) return response.value;\n return undefined;\n } catch {\n // Timeout - return undefined\n return undefined;\n }\n },\n\n notify(message: string, type?: \"info\" | \"warning\" | \"error\"): void {\n // Broadcast notification to all subscribers\n broadcast(sessionId, {\n type: \"extension_ui_request\",\n requestId: `notify-${Date.now()}`, // Ephemeral, no response expected\n method: \"notify\",\n message,\n notifyType: type ?? \"info\",\n });\n },\n\n onTerminalInput(_handler: TerminalInputHandler): () => void {\n // Terminal input not available in server mode\n // Return a no-op unsubscribe function\n return () => {};\n },\n\n setStatus(key: string, text: string | undefined): void {\n // Broadcast status update to subscribers\n broadcast(sessionId, {\n type: \"extension_ui_request\",\n requestId: `status-${key}-${Date.now()}`,\n method: \"setStatus\",\n key,\n text,\n });\n },\n\n setWorkingMessage(message?: string): void {\n // Broadcast working message update\n broadcast(sessionId, {\n type: \"extension_ui_request\",\n requestId: `working-${Date.now()}`,\n method: \"setWorkingMessage\",\n message,\n });\n },\n\n setWidget(\n key: string,\n content: any,\n options?: { placement?: \"aboveEditor\" | \"belowEditor\" }\n ): void {\n // Only support string[] content (not component factories)\n // Component factories can't be serialized for remote clients\n if (Array.isArray(content) || content === undefined) {\n broadcast(sessionId, {\n type: \"extension_ui_request\",\n requestId: `widget-${key}-${Date.now()}`,\n method: \"setWidget\",\n key,\n content,\n placement: options?.placement,\n });\n }\n // If content is a function (component factory), ignore it silently\n // Remote clients can't render server-side components\n },\n\n setFooter(_factory: any): void {\n // Custom footer not supported in server mode\n // This would require Component serialization\n },\n\n setHeader(_factory: any): void {\n // Custom header not supported in server mode\n },\n\n setTitle(title: string): void {\n // Broadcast title update\n broadcast(sessionId, {\n type: \"extension_ui_request\",\n requestId: `title-${Date.now()}`,\n method: \"setTitle\",\n title,\n });\n },\n\n async custom<T>(_factory: any, _options?: any): Promise<T> {\n // Custom components not supported in server mode\n // Would require serializing component factories\n throw new Error(\"Custom components are not supported in server mode\");\n },\n\n pasteToEditor(_text: string): void {\n // Editor paste not available in server mode\n },\n\n setEditorText(_text: string): void {\n // Editor text setting not available in server mode\n },\n\n getEditorText(): string {\n // Editor not available in server mode\n return \"\";\n },\n\n setEditorComponent(_factory: any): void {\n // Custom editor not supported in server mode\n },\n\n // Theme methods - return stubs since themes are TUI-specific\n get theme(): any {\n return {\n // Minimal theme stub for extensions that check theme properties\n colors: {},\n styles: {},\n };\n },\n\n getAllThemes(): { name: string; path: string | undefined }[] {\n // Themes not available in server mode\n return [];\n },\n\n getTheme(_name: string): any {\n return undefined;\n },\n\n setTheme(_theme: string | any): { success: boolean; error?: string } {\n return { success: false, error: \"Themes not supported in server mode\" };\n },\n\n getToolsExpanded(): boolean {\n return false;\n },\n\n setToolsExpanded(_expanded: boolean): void {\n // Tool expansion state not tracked in server mode\n },\n };\n}\n\n/**\n * Race a promise against abort signal and optional timeout.\n * Calls onCancel if aborted or timed out.\n */\nasync function raceWithAbortAndSignal<T>(\n promise: Promise<T>,\n signal: AbortSignal | undefined,\n onCancel: () => void\n): Promise<T> {\n if (!signal) {\n return promise;\n }\n\n return new Promise<T>((resolve, reject) => {\n const abortHandler = () => {\n onCancel();\n reject(new Error(\"Aborted\"));\n };\n\n signal.addEventListener(\"abort\", abortHandler);\n\n promise\n .then((result) => {\n signal.removeEventListener(\"abort\", abortHandler);\n resolve(result);\n })\n .catch((error) => {\n signal.removeEventListener(\"abort\", abortHandler);\n reject(error);\n });\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pi-app-server - Session multiplexer for pi-coding-agent
4
+ *
5
+ * Exposes N independent AgentSessions through dual transports:
6
+ * - WebSocket on port 3141
7
+ * - stdio (JSON lines)
8
+ *
9
+ * The protocol IS the architecture.
10
+ */
11
+ import { PiSessionManager } from "./session-manager.js";
12
+ import { type AuthProvider } from "./auth.js";
13
+ import { MetricsEmitter, type MetricsSink, type ThresholdConfig, type Alert } from "./metrics-index.js";
14
+ import { type Logger, type LogLevel } from "./logger-index.js";
15
+ export interface PiServerOptions {
16
+ /** Authentication provider (default: AllowAllAuthProvider) */
17
+ authProvider?: AuthProvider;
18
+ /** Metrics sink(s) for observability. Can be a single sink or CompositeSink. */
19
+ metricsSink?: MetricsSink;
20
+ /** Include MemorySink automatically for get_metrics command (default: true) */
21
+ includeMemoryMetrics?: boolean;
22
+ /** Logger for structured logging (default: ConsoleLogger with 'info' level) */
23
+ logger?: Logger;
24
+ /** Log level for default ConsoleLogger (ignored if logger is provided) */
25
+ logLevel?: LogLevel;
26
+ /** Alert thresholds for automatic monitoring (default: built-in thresholds) */
27
+ alertThresholds?: Record<string, ThresholdConfig>;
28
+ /** Called when an alert fires (default: console logging) */
29
+ onAlert?: (alert: Alert) => void | Promise<void>;
30
+ /** Called when an alert clears (optional) */
31
+ onAlertClear?: (alert: Alert) => void | Promise<void>;
32
+ }
33
+ export declare class PiServer {
34
+ private sessionManager;
35
+ private wss;
36
+ private stdinInterface;
37
+ private authProvider;
38
+ private serverStartTime;
39
+ private metrics;
40
+ private logger;
41
+ /** Memory sink for get_metrics command (if included) */
42
+ private memorySink;
43
+ /** Stdio backpressure state */
44
+ private stdioState;
45
+ constructor(options?: PiServerOptions);
46
+ start(port?: number): Promise<void>;
47
+ /**
48
+ * Check if server is shutting down.
49
+ */
50
+ isInShutdown(): boolean;
51
+ /**
52
+ * Get session manager for external access.
53
+ */
54
+ getSessionManager(): PiSessionManager;
55
+ /**
56
+ * Get metrics emitter for external access.
57
+ */
58
+ getMetrics(): MetricsEmitter;
59
+ /**
60
+ * Get logger for external access.
61
+ */
62
+ getLogger(): Logger;
63
+ /**
64
+ * Get memory sink metrics (for get_metrics command).
65
+ * Returns undefined if includeMemoryMetrics was false.
66
+ */
67
+ getMemoryMetrics(): Record<string, unknown> | undefined;
68
+ /**
69
+ * Graceful shutdown.
70
+ * 1. Stop accepting new connections
71
+ * 2. Broadcast shutdown notification
72
+ * 3. Drain in-flight commands
73
+ * 4. Close all WebSocket connections
74
+ * 5. Close stdin
75
+ * 6. Dispose all sessions
76
+ */
77
+ stop(timeoutMs?: number): Promise<void>;
78
+ private setupWebSocket;
79
+ private setupStdio;
80
+ private handleCommand;
81
+ }
82
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGxD,OAAO,EAAE,KAAK,YAAY,EAA0C,MAAM,WAAW,CAAC;AACtF,OAAO,EACL,cAAc,EACd,KAAK,WAAW,EAMhB,KAAK,eAAe,EACpB,KAAK,KAAK,EACX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,MAAM,EAAiB,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAyO9E,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,gFAAgF;IAChF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,+EAA+E;IAC/E,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAClD,4DAA4D;IAC5D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,6CAA6C;IAC7C,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,cAAc,CAAmC;IACzD,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,wDAAwD;IACxD,OAAO,CAAC,UAAU,CAA2B;IAC7C,+BAA+B;IAC/B,OAAO,CAAC,UAAU,CAIhB;gBAEU,OAAO,GAAE,eAAoB;IAoEnC,KAAK,CAAC,IAAI,GAAE,MAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAsDvD;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;OAEG;IACH,iBAAiB,IAAI,gBAAgB;IAIrC;;OAEG;IACH,UAAU,IAAI,cAAc;IAI5B;;OAEG;IACH,SAAS,IAAI,MAAM;IAInB;;;OAGG;IACH,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAIvD;;;;;;;;OAQG;IACG,IAAI,CAAC,SAAS,SAA8B,GAAG,OAAO,CAAC,IAAI,CAAC;IA2FlE,OAAO,CAAC,cAAc;IAuJtB,OAAO,CAAC,UAAU;YAqEJ,aAAa;CAmC5B"}
package/dist/server.js ADDED
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env node
2
+ import * as readline from "readline";
3
+ import { WebSocketServer, WebSocket } from "ws";
4
+ import { PiSessionManager } from "./session-manager.js";
5
+ import { getSessionId as getSessionIdFromCmd, isCreateSessionResponse } from "./types.js";
6
+ import { AllowAllAuthProvider } from "./auth.js";
7
+ import {
8
+ MetricsEmitter,
9
+ NoOpSink,
10
+ MemorySink,
11
+ CompositeSink,
12
+ MetricNames,
13
+ ThresholdAlertSink
14
+ } from "./metrics-index.js";
15
+ import { ConsoleLogger } from "./logger-index.js";
16
+ const SERVER_VERSION = "0.1.0";
17
+ const PROTOCOL_VERSION = "1.0.0";
18
+ const DEFAULT_PORT = 3141;
19
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 3e4;
20
+ const BACKPRESSURE_THRESHOLD_BYTES = 64 * 1024;
21
+ const BACKPRESSURE_CRITICAL_BYTES = 1024 * 1024;
22
+ const _STDIO_BACKPRESSURE_THRESHOLD_BYTES = 256 * 1024;
23
+ const HEARTBEAT_INTERVAL_MS = 30 * 1e3;
24
+ const HEARTBEAT_TIMEOUT_MS = 10 * 1e3;
25
+ function sendWithBackpressure(ws, data, options = {}) {
26
+ const { isCritical = false } = options;
27
+ if (ws.readyState !== WebSocket.OPEN) {
28
+ return { ok: false, reason: "closed" };
29
+ }
30
+ const buffered = ws.bufferedAmount;
31
+ if (buffered > BACKPRESSURE_CRITICAL_BYTES) {
32
+ try {
33
+ ws.close(1011, "Server overloaded - backpressure critical");
34
+ } catch {
35
+ }
36
+ return { ok: false, reason: "backpressure" };
37
+ }
38
+ if (buffered > BACKPRESSURE_THRESHOLD_BYTES && !isCritical) {
39
+ return { ok: false, reason: "backpressure" };
40
+ }
41
+ try {
42
+ ws.send(data);
43
+ return { ok: true };
44
+ } catch (error) {
45
+ return {
46
+ ok: false,
47
+ reason: "error",
48
+ error: error instanceof Error ? error : new Error(String(error))
49
+ };
50
+ }
51
+ }
52
+ function sendWithStdioBackpressure(data, state, options = {}) {
53
+ const { isCritical = false } = options;
54
+ try {
55
+ const canWrite = process.stdout.write(data + "\n");
56
+ if (!canWrite) {
57
+ state.hasBackpressure = true;
58
+ if (!state.drainHandlerRegistered) {
59
+ state.drainHandlerRegistered = true;
60
+ process.stdout.once("drain", () => {
61
+ state.hasBackpressure = false;
62
+ });
63
+ }
64
+ }
65
+ return true;
66
+ } catch (error) {
67
+ if (isCritical) {
68
+ console.error(`[stdio] Critical message failed:`, error);
69
+ }
70
+ return false;
71
+ }
72
+ }
73
+ function startHeartbeat(ws, state) {
74
+ state.lastPongAt = Date.now();
75
+ state.waitingForPong = false;
76
+ state.cleanedUp = false;
77
+ state.heartbeatTimer = setInterval(() => {
78
+ if (state.cleanedUp || ws.readyState !== WebSocket.OPEN) {
79
+ stopHeartbeat(state);
80
+ return;
81
+ }
82
+ if (state.waitingForPong) {
83
+ const elapsed = Date.now() - state.lastPongAt;
84
+ console.error(`[WebSocket] No pong received after ${elapsed}ms, closing connection`);
85
+ stopHeartbeat(state);
86
+ try {
87
+ ws.close(1001, "Heartbeat timeout");
88
+ } catch {
89
+ }
90
+ return;
91
+ }
92
+ state.waitingForPong = true;
93
+ try {
94
+ ws.ping();
95
+ } catch {
96
+ stopHeartbeat(state);
97
+ }
98
+ state.pongTimeoutTimer = setTimeout(() => {
99
+ if (state.cleanedUp) return;
100
+ if (state.waitingForPong && ws.readyState === WebSocket.OPEN) {
101
+ console.error(
102
+ `[WebSocket] Pong timeout after ${HEARTBEAT_TIMEOUT_MS}ms, closing connection`
103
+ );
104
+ stopHeartbeat(state);
105
+ try {
106
+ ws.close(1001, "Heartbeat timeout");
107
+ } catch {
108
+ }
109
+ }
110
+ }, HEARTBEAT_TIMEOUT_MS);
111
+ }, HEARTBEAT_INTERVAL_MS);
112
+ }
113
+ function stopHeartbeat(state) {
114
+ state.cleanedUp = true;
115
+ if (state.heartbeatTimer) {
116
+ clearInterval(state.heartbeatTimer);
117
+ state.heartbeatTimer = null;
118
+ }
119
+ if (state.pongTimeoutTimer) {
120
+ clearTimeout(state.pongTimeoutTimer);
121
+ state.pongTimeoutTimer = null;
122
+ }
123
+ state.waitingForPong = false;
124
+ }
125
+ class PiServer {
126
+ sessionManager = new PiSessionManager();
127
+ wss = null;
128
+ stdinInterface = null;
129
+ authProvider;
130
+ serverStartTime = Date.now();
131
+ metrics;
132
+ logger;
133
+ /** Memory sink for get_metrics command (if included) */
134
+ memorySink = null;
135
+ /** Stdio backpressure state */
136
+ stdioState = {
137
+ hasBackpressure: false,
138
+ droppedCount: 0,
139
+ drainHandlerRegistered: false
140
+ };
141
+ constructor(options = {}) {
142
+ this.authProvider = options.authProvider ?? new AllowAllAuthProvider();
143
+ this.logger = options.logger ?? new ConsoleLogger({
144
+ level: options.logLevel ?? "info",
145
+ component: "pi-server"
146
+ });
147
+ const defaultAlertThresholds = {
148
+ [MetricNames.RATE_LIMIT_GENERATION_COUNTER]: {
149
+ info: 1e12,
150
+ // 1 trillion - start paying attention
151
+ warn: 1e14,
152
+ // 100 trillion - concerning
153
+ critical: 1e15
154
+ // 1 quadrillion - action needed
155
+ }
156
+ };
157
+ const includeMemory = options.includeMemoryMetrics ?? true;
158
+ const sinks = [];
159
+ if (options.metricsSink) {
160
+ sinks.push(options.metricsSink);
161
+ }
162
+ if (includeMemory) {
163
+ this.memorySink = new MemorySink({ maxEvents: 1e3 });
164
+ sinks.push(this.memorySink);
165
+ }
166
+ const baseSink = sinks.length > 0 ? new CompositeSink(sinks) : new NoOpSink();
167
+ const alertThresholds = options.alertThresholds ?? defaultAlertThresholds;
168
+ const onAlert = options.onAlert ?? ((alert) => {
169
+ const levelStr = `[${alert.level.toUpperCase()}]`;
170
+ if (alert.level === "critical") {
171
+ console.error(`${levelStr} ${alert.message}`);
172
+ } else {
173
+ console.log(`${levelStr} ${alert.message}`);
174
+ }
175
+ });
176
+ const alertSink = new ThresholdAlertSink({
177
+ sink: baseSink,
178
+ thresholds: alertThresholds,
179
+ onAlert,
180
+ onClear: options.onAlertClear,
181
+ maxAlertStates: 1e3
182
+ });
183
+ this.metrics = new MetricsEmitter({ sink: alertSink });
184
+ this.sessionManager.getGovernor().setMetrics(this.metrics);
185
+ if (this.memorySink) {
186
+ this.sessionManager.setMemoryMetricsProvider(() => this.memorySink.getMetrics());
187
+ }
188
+ }
189
+ async start(port = DEFAULT_PORT) {
190
+ this.wss = new WebSocketServer({ port });
191
+ this.setupWebSocket(this.wss);
192
+ await new Promise((resolve, reject) => {
193
+ const onListening = () => {
194
+ this.wss?.off("error", onError);
195
+ resolve();
196
+ };
197
+ const onError = (error) => {
198
+ this.wss?.off("listening", onListening);
199
+ reject(error);
200
+ };
201
+ this.wss?.once("listening", onListening);
202
+ this.wss?.once("error", onError);
203
+ }).catch((error) => {
204
+ throw new Error(
205
+ `Failed to start WebSocket server on port ${port}: ${error instanceof Error ? error.message : String(error)}`
206
+ );
207
+ });
208
+ this.stdinInterface = this.setupStdio();
209
+ this.sessionManager.startSessionCleanup(36e5);
210
+ this.sessionManager.getGovernor().startPeriodicCleanup(3e5);
211
+ const readyEvent = {
212
+ type: "server_ready",
213
+ data: {
214
+ serverVersion: SERVER_VERSION,
215
+ protocolVersion: PROTOCOL_VERSION,
216
+ transports: ["websocket", "stdio"]
217
+ }
218
+ };
219
+ this.sessionManager.broadcast(JSON.stringify(readyEvent));
220
+ this.metrics.event(MetricNames.EVENT_SESSION_CREATED, { event: "server_ready" });
221
+ this.logger.info("Server started", {
222
+ version: SERVER_VERSION,
223
+ protocol: PROTOCOL_VERSION,
224
+ port,
225
+ transports: ["websocket", "stdio"]
226
+ });
227
+ }
228
+ /**
229
+ * Check if server is shutting down.
230
+ */
231
+ isInShutdown() {
232
+ return this.sessionManager.isInShutdown();
233
+ }
234
+ /**
235
+ * Get session manager for external access.
236
+ */
237
+ getSessionManager() {
238
+ return this.sessionManager;
239
+ }
240
+ /**
241
+ * Get metrics emitter for external access.
242
+ */
243
+ getMetrics() {
244
+ return this.metrics;
245
+ }
246
+ /**
247
+ * Get logger for external access.
248
+ */
249
+ getLogger() {
250
+ return this.logger;
251
+ }
252
+ /**
253
+ * Get memory sink metrics (for get_metrics command).
254
+ * Returns undefined if includeMemoryMetrics was false.
255
+ */
256
+ getMemoryMetrics() {
257
+ return this.memorySink?.getMetrics();
258
+ }
259
+ /**
260
+ * Graceful shutdown.
261
+ * 1. Stop accepting new connections
262
+ * 2. Broadcast shutdown notification
263
+ * 3. Drain in-flight commands
264
+ * 4. Close all WebSocket connections
265
+ * 5. Close stdin
266
+ * 6. Dispose all sessions
267
+ */
268
+ async stop(timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
269
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
270
+ timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS;
271
+ }
272
+ if (this.sessionManager.isInShutdown()) {
273
+ return;
274
+ }
275
+ this.logger.info("Graceful shutdown initiated");
276
+ this.sessionManager.stopSessionCleanup();
277
+ this.sessionManager.getGovernor().stopPeriodicCleanup();
278
+ if (this.authProvider.dispose) {
279
+ try {
280
+ await Promise.resolve(this.authProvider.dispose());
281
+ } catch (error) {
282
+ this.logger.logError(
283
+ "Auth provider dispose failed",
284
+ error instanceof Error ? error : new Error(String(error))
285
+ );
286
+ }
287
+ }
288
+ if (this.wss) {
289
+ this.wss.close(() => {
290
+ this.logger.debug("WebSocket server closed (no new connections)");
291
+ });
292
+ }
293
+ if (this.stdinInterface) {
294
+ this.stdinInterface.close();
295
+ this.stdinInterface = null;
296
+ this.logger.debug("Stdin closed");
297
+ }
298
+ const result = await this.sessionManager.initiateShutdown(timeoutMs);
299
+ if (result.timedOut) {
300
+ this.logger.warn("Shutdown timed out", {
301
+ timeoutMs,
302
+ drained: result.drained,
303
+ pending: this.sessionManager.getInFlightCount()
304
+ });
305
+ } else {
306
+ this.logger.info("All in-flight commands completed", { count: result.drained });
307
+ }
308
+ if (this.wss) {
309
+ const clients = [...this.wss.clients];
310
+ this.logger.info("Closing WebSocket connections", { count: clients.length });
311
+ for (const ws of clients) {
312
+ try {
313
+ ws.close(1001, "Server shutting down");
314
+ } catch {
315
+ }
316
+ }
317
+ }
318
+ const disposeResult = this.sessionManager.disposeAllSessions();
319
+ this.logger.info("Sessions disposed", {
320
+ disposed: disposeResult.disposed,
321
+ failed: disposeResult.failed
322
+ });
323
+ const uptimeMs = Date.now() - this.serverStartTime;
324
+ this.metrics.gauge(MetricNames.SESSION_LIFETIME_SECONDS, Math.floor(uptimeMs / 1e3));
325
+ await this.metrics.flush();
326
+ this.logger.info("Shutdown complete", { uptimeMs });
327
+ }
328
+ // ==========================================================================
329
+ // WEBSOCKET TRANSPORT
330
+ // ==========================================================================
331
+ setupWebSocket(wss) {
332
+ wss.on("connection", async (ws, request) => {
333
+ const connResult = this.sessionManager.getGovernor().canAcceptConnection();
334
+ if (!connResult.allowed) {
335
+ this.logger.warn("Connection rejected", { reason: connResult.reason });
336
+ try {
337
+ ws.close(1013, connResult.reason);
338
+ } catch {
339
+ }
340
+ return;
341
+ }
342
+ const authContext = {
343
+ request,
344
+ websocket: {
345
+ remoteAddress: request.socket?.remoteAddress,
346
+ secure: request.socket?.encrypted ?? false
347
+ },
348
+ serverStartTime: this.serverStartTime,
349
+ connectionCount: this.sessionManager.getGovernor().getConnectionCount()
350
+ };
351
+ const authResult = await Promise.resolve(this.authProvider.authenticate(authContext));
352
+ if (!authResult.allowed) {
353
+ this.logger.warn("Authentication failed", { reason: authResult.reason });
354
+ try {
355
+ ws.close(1008, authResult.reason);
356
+ } catch {
357
+ }
358
+ return;
359
+ }
360
+ this.sessionManager.getGovernor().registerConnection();
361
+ this.metrics.counter(MetricNames.CONNECTIONS_TOTAL, 1);
362
+ this.metrics.gauge(
363
+ MetricNames.CONNECTIONS_ACTIVE,
364
+ this.sessionManager.getGovernor().getConnectionCount()
365
+ );
366
+ const heartbeatState = {
367
+ waitingForPong: false,
368
+ lastPongAt: Date.now(),
369
+ heartbeatTimer: null,
370
+ pongTimeoutTimer: null,
371
+ cleanedUp: false
372
+ };
373
+ const subscriber = {
374
+ send: (data) => {
375
+ sendWithBackpressure(ws, data, { isCritical: false });
376
+ },
377
+ subscribedSessions: /* @__PURE__ */ new Set()
378
+ };
379
+ const readyEvent = {
380
+ type: "server_ready",
381
+ data: {
382
+ serverVersion: SERVER_VERSION,
383
+ protocolVersion: PROTOCOL_VERSION,
384
+ transports: ["websocket", "stdio"]
385
+ }
386
+ };
387
+ sendWithBackpressure(ws, JSON.stringify(readyEvent), { isCritical: true });
388
+ this.sessionManager.addSubscriber(subscriber);
389
+ let cleanedUp = false;
390
+ const cleanupConnection = () => {
391
+ if (cleanedUp) return;
392
+ cleanedUp = true;
393
+ stopHeartbeat(heartbeatState);
394
+ this.sessionManager.removeSubscriber(subscriber);
395
+ this.sessionManager.getGovernor().unregisterConnection();
396
+ this.metrics.gauge(
397
+ MetricNames.CONNECTIONS_ACTIVE,
398
+ this.sessionManager.getGovernor().getConnectionCount()
399
+ );
400
+ };
401
+ startHeartbeat(ws, heartbeatState);
402
+ ws.on("pong", () => {
403
+ heartbeatState.waitingForPong = false;
404
+ heartbeatState.lastPongAt = Date.now();
405
+ if (heartbeatState.pongTimeoutTimer) {
406
+ clearTimeout(heartbeatState.pongTimeoutTimer);
407
+ heartbeatState.pongTimeoutTimer = null;
408
+ }
409
+ });
410
+ ws.on("message", async (data) => {
411
+ const sizeResult = this.sessionManager.getGovernor().canAcceptMessage(data.length);
412
+ if (!sizeResult.allowed) {
413
+ const errorResponse = {
414
+ type: "response",
415
+ command: "unknown",
416
+ success: false,
417
+ error: sizeResult.reason
418
+ };
419
+ sendWithBackpressure(ws, JSON.stringify(errorResponse), { isCritical: true });
420
+ return;
421
+ }
422
+ try {
423
+ const command = JSON.parse(data.toString());
424
+ await this.handleCommand(command, subscriber, (response) => {
425
+ sendWithBackpressure(ws, JSON.stringify(response), { isCritical: true });
426
+ });
427
+ } catch (error) {
428
+ const errorResponse = {
429
+ type: "response",
430
+ command: "unknown",
431
+ success: false,
432
+ error: error instanceof Error ? error.message : "Invalid JSON"
433
+ };
434
+ sendWithBackpressure(ws, JSON.stringify(errorResponse), { isCritical: true });
435
+ }
436
+ });
437
+ ws.on("close", () => {
438
+ cleanupConnection();
439
+ });
440
+ ws.on("error", (error) => {
441
+ console.error(`[WebSocket] Connection error:`, error);
442
+ cleanupConnection();
443
+ });
444
+ });
445
+ }
446
+ // ==========================================================================
447
+ // STDIO TRANSPORT
448
+ // ==========================================================================
449
+ setupStdio() {
450
+ const rl = readline.createInterface({
451
+ input: process.stdin,
452
+ output: process.stdout,
453
+ terminal: false
454
+ });
455
+ const subscriber = {
456
+ send: (data) => {
457
+ if (!sendWithStdioBackpressure(data, this.stdioState, { isCritical: false })) {
458
+ this.stdioState.droppedCount++;
459
+ }
460
+ },
461
+ subscribedSessions: /* @__PURE__ */ new Set()
462
+ };
463
+ this.sessionManager.addSubscriber(subscriber);
464
+ rl.on("line", async (line) => {
465
+ const messageBytes = Buffer.byteLength(line, "utf8");
466
+ const sizeResult = this.sessionManager.getGovernor().canAcceptMessage(messageBytes);
467
+ if (!sizeResult.allowed) {
468
+ const errorResponse = {
469
+ type: "response",
470
+ command: "unknown",
471
+ success: false,
472
+ error: sizeResult.reason
473
+ };
474
+ sendWithStdioBackpressure(JSON.stringify(errorResponse), this.stdioState, {
475
+ isCritical: true
476
+ });
477
+ return;
478
+ }
479
+ try {
480
+ const command = JSON.parse(line);
481
+ await this.handleCommand(command, subscriber, (response) => {
482
+ sendWithStdioBackpressure(JSON.stringify(response), this.stdioState, {
483
+ isCritical: true
484
+ });
485
+ });
486
+ } catch (error) {
487
+ const errorResponse = {
488
+ type: "response",
489
+ command: "unknown",
490
+ success: false,
491
+ error: error instanceof Error ? error.message : "Invalid JSON"
492
+ };
493
+ sendWithStdioBackpressure(JSON.stringify(errorResponse), this.stdioState, {
494
+ isCritical: true
495
+ });
496
+ }
497
+ });
498
+ rl.on("close", () => {
499
+ this.sessionManager.removeSubscriber(subscriber);
500
+ });
501
+ return rl;
502
+ }
503
+ // ==========================================================================
504
+ // COMMAND HANDLING
505
+ // ==========================================================================
506
+ async handleCommand(command, subscriber, respond) {
507
+ const response = await this.sessionManager.executeCommand(command);
508
+ respond(response);
509
+ if (command.type === "switch_session" && response.success) {
510
+ const sessionId = getSessionIdFromCmd(command);
511
+ if (sessionId) {
512
+ this.sessionManager.subscribeToSession(subscriber, sessionId);
513
+ }
514
+ }
515
+ if (command.type === "create_session" && isCreateSessionResponse(response)) {
516
+ const broadcast = {
517
+ type: "session_created",
518
+ data: {
519
+ sessionId: response.data.sessionId,
520
+ sessionInfo: response.data.sessionInfo
521
+ }
522
+ };
523
+ this.sessionManager.broadcast(JSON.stringify(broadcast));
524
+ } else if (command.type === "delete_session" && response.success) {
525
+ const broadcast = {
526
+ type: "session_deleted",
527
+ data: { sessionId: getSessionIdFromCmd(command) }
528
+ };
529
+ this.sessionManager.broadcast(JSON.stringify(broadcast));
530
+ }
531
+ }
532
+ }
533
+ async function main() {
534
+ const portEnv = process.env.PI_SERVER_PORT;
535
+ const port = portEnv ? parseInt(portEnv, 10) : DEFAULT_PORT;
536
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
537
+ console.error(`Invalid PI_SERVER_PORT: "${portEnv}". Must be 1-65535.`);
538
+ process.exit(1);
539
+ return;
540
+ }
541
+ const server = new PiServer();
542
+ await server.start(port);
543
+ const handleShutdown = async (signal) => {
544
+ console.error(`
545
+ [${signal}] Received, initiating shutdown...`);
546
+ await server.stop(DEFAULT_SHUTDOWN_TIMEOUT_MS);
547
+ process.exit(0);
548
+ };
549
+ process.on("SIGINT", () => handleShutdown("SIGINT"));
550
+ process.on("SIGTERM", () => handleShutdown("SIGTERM"));
551
+ }
552
+ if (import.meta.url === `file://${process.argv[1]}`) {
553
+ main().catch((error) => {
554
+ console.error("Fatal error:", error);
555
+ process.exit(1);
556
+ });
557
+ }
558
+ export {
559
+ PiServer
560
+ };
561
+ //# sourceMappingURL=server.js.map