@chatroomcp/chatroom 0.1.3

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 (159) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +50 -0
  3. package/assets/chatroom.svg +15 -0
  4. package/chatroom.config.example.json +24 -0
  5. package/dist/app/application.d.ts +18 -0
  6. package/dist/app/application.js +63 -0
  7. package/dist/app/event-bus.d.ts +23 -0
  8. package/dist/app/event-bus.js +12 -0
  9. package/dist/app/external-access-registry.d.ts +18 -0
  10. package/dist/app/external-access-registry.js +48 -0
  11. package/dist/app/lifecycle.d.ts +12 -0
  12. package/dist/app/lifecycle.js +41 -0
  13. package/dist/auth/auth-service.d.ts +88 -0
  14. package/dist/auth/auth-service.js +222 -0
  15. package/dist/auth/ingress-policy.d.ts +21 -0
  16. package/dist/auth/ingress-policy.js +57 -0
  17. package/dist/auth/passkey-service.d.ts +39 -0
  18. package/dist/auth/passkey-service.js +145 -0
  19. package/dist/cli/index.d.ts +2 -0
  20. package/dist/cli/index.js +10 -0
  21. package/dist/cli/run-cli.d.ts +1 -0
  22. package/dist/cli/run-cli.js +42 -0
  23. package/dist/config/load-config.d.ts +7 -0
  24. package/dist/config/load-config.js +182 -0
  25. package/dist/config/platform-paths.d.ts +6 -0
  26. package/dist/config/platform-paths.js +34 -0
  27. package/dist/config/types.d.ts +25 -0
  28. package/dist/config/types.js +1 -0
  29. package/dist/core/auth/passkey-repository.d.ts +18 -0
  30. package/dist/core/auth/passkey-repository.js +1 -0
  31. package/dist/core/auth/repository.d.ts +59 -0
  32. package/dist/core/auth/repository.js +7 -0
  33. package/dist/core/auth/session-repository.d.ts +6 -0
  34. package/dist/core/auth/session-repository.js +1 -0
  35. package/dist/core/errors/chatroom-error.d.ts +23 -0
  36. package/dist/core/errors/chatroom-error.js +46 -0
  37. package/dist/core/operations/bounded-value.d.ts +6 -0
  38. package/dist/core/operations/bounded-value.js +41 -0
  39. package/dist/core/operations/redactor.d.ts +4 -0
  40. package/dist/core/operations/redactor.js +65 -0
  41. package/dist/core/operations/repository.d.ts +19 -0
  42. package/dist/core/operations/repository.js +1 -0
  43. package/dist/core/operations/types.d.ts +31 -0
  44. package/dist/core/operations/types.js +9 -0
  45. package/dist/core/runtime/child-environment.d.ts +1 -0
  46. package/dist/core/runtime/child-environment.js +39 -0
  47. package/dist/core/runtime/command-runner.d.ts +15 -0
  48. package/dist/core/runtime/command-runner.js +39 -0
  49. package/dist/core/runtime/identity.d.ts +2 -0
  50. package/dist/core/runtime/identity.js +9 -0
  51. package/dist/infrastructure/database/app-database.d.ts +6 -0
  52. package/dist/infrastructure/database/app-database.js +104 -0
  53. package/dist/infrastructure/database/oauth-repository.d.ts +20 -0
  54. package/dist/infrastructure/database/oauth-repository.js +140 -0
  55. package/dist/infrastructure/database/operation-repository.d.ts +16 -0
  56. package/dist/infrastructure/database/operation-repository.js +129 -0
  57. package/dist/infrastructure/database/passkey-repository.d.ts +11 -0
  58. package/dist/infrastructure/database/passkey-repository.js +57 -0
  59. package/dist/infrastructure/database/web-session-repository.d.ts +10 -0
  60. package/dist/infrastructure/database/web-session-repository.js +26 -0
  61. package/dist/infrastructure/http/http-server.d.ts +26 -0
  62. package/dist/infrastructure/http/http-server.js +166 -0
  63. package/dist/mcp/server/create-mcp-server.d.ts +3 -0
  64. package/dist/mcp/server/create-mcp-server.js +16 -0
  65. package/dist/mcp/server/tool-support.d.ts +105 -0
  66. package/dist/mcp/server/tool-support.js +124 -0
  67. package/dist/operations/operation-log.d.ts +23 -0
  68. package/dist/operations/operation-log.js +103 -0
  69. package/dist/plugins/cloud/api-client.d.ts +56 -0
  70. package/dist/plugins/cloud/api-client.js +122 -0
  71. package/dist/plugins/cloud/controller.d.ts +48 -0
  72. package/dist/plugins/cloud/controller.js +282 -0
  73. package/dist/plugins/cloud/device-proof.d.ts +9 -0
  74. package/dist/plugins/cloud/device-proof.js +17 -0
  75. package/dist/plugins/cloud/plugin.d.ts +4 -0
  76. package/dist/plugins/cloud/plugin.js +17 -0
  77. package/dist/plugins/cloud/state-store.d.ts +7 -0
  78. package/dist/plugins/cloud/state-store.js +74 -0
  79. package/dist/plugins/cloud/tunnel-client.d.ts +36 -0
  80. package/dist/plugins/cloud/tunnel-client.js +293 -0
  81. package/dist/plugins/cloud/types.d.ts +70 -0
  82. package/dist/plugins/cloud/types.js +32 -0
  83. package/dist/plugins/plugin-manager.d.ts +11 -0
  84. package/dist/plugins/plugin-manager.js +24 -0
  85. package/dist/plugins/process/backend.d.ts +19 -0
  86. package/dist/plugins/process/backend.js +1 -0
  87. package/dist/plugins/process/head-tail-buffer.d.ts +18 -0
  88. package/dist/plugins/process/head-tail-buffer.js +49 -0
  89. package/dist/plugins/process/infrastructure/pipe-process-backend.d.ts +8 -0
  90. package/dist/plugins/process/infrastructure/pipe-process-backend.js +61 -0
  91. package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +8 -0
  92. package/dist/plugins/process/infrastructure/pty-process-backend.js +50 -0
  93. package/dist/plugins/process/mcp.d.ts +4 -0
  94. package/dist/plugins/process/mcp.js +69 -0
  95. package/dist/plugins/process/plugin.d.ts +4 -0
  96. package/dist/plugins/process/plugin.js +28 -0
  97. package/dist/plugins/process/process-supervisor.d.ts +36 -0
  98. package/dist/plugins/process/process-supervisor.js +232 -0
  99. package/dist/plugins/process/types.d.ts +34 -0
  100. package/dist/plugins/process/types.js +1 -0
  101. package/dist/plugins/types.d.ts +32 -0
  102. package/dist/plugins/types.js +17 -0
  103. package/dist/plugins/web/api-types.d.ts +10 -0
  104. package/dist/plugins/web/api-types.js +1 -0
  105. package/dist/plugins/web/http/api-router.d.ts +13 -0
  106. package/dist/plugins/web/http/api-router.js +146 -0
  107. package/dist/plugins/web/http/cloud-api-router.d.ts +4 -0
  108. package/dist/plugins/web/http/cloud-api-router.js +41 -0
  109. package/dist/plugins/web/http/process-api-router.d.ts +3 -0
  110. package/dist/plugins/web/http/process-api-router.js +16 -0
  111. package/dist/plugins/web/http/workspace-api-router.d.ts +3 -0
  112. package/dist/plugins/web/http/workspace-api-router.js +82 -0
  113. package/dist/plugins/web/plugin.d.ts +8 -0
  114. package/dist/plugins/web/plugin.js +17 -0
  115. package/dist/plugins/web/runtime.d.ts +36 -0
  116. package/dist/plugins/web/runtime.js +66 -0
  117. package/dist/plugins/workspace/domain/filesystem.d.ts +25 -0
  118. package/dist/plugins/workspace/domain/filesystem.js +1 -0
  119. package/dist/plugins/workspace/domain/git.d.ts +7 -0
  120. package/dist/plugins/workspace/domain/git.js +1 -0
  121. package/dist/plugins/workspace/domain/repository.d.ts +16 -0
  122. package/dist/plugins/workspace/domain/repository.js +1 -0
  123. package/dist/plugins/workspace/domain/review.d.ts +28 -0
  124. package/dist/plugins/workspace/domain/review.js +1 -0
  125. package/dist/plugins/workspace/domain/workspace.d.ts +23 -0
  126. package/dist/plugins/workspace/domain/workspace.js +1 -0
  127. package/dist/plugins/workspace/git/git-command-runner.d.ts +6 -0
  128. package/dist/plugins/workspace/git/git-command-runner.js +15 -0
  129. package/dist/plugins/workspace/git/git-service.d.ts +14 -0
  130. package/dist/plugins/workspace/git/git-service.js +107 -0
  131. package/dist/plugins/workspace/git/git-snapshot-service.d.ts +33 -0
  132. package/dist/plugins/workspace/git/git-snapshot-service.js +352 -0
  133. package/dist/plugins/workspace/infrastructure/workspace-fs.d.ts +38 -0
  134. package/dist/plugins/workspace/infrastructure/workspace-fs.js +340 -0
  135. package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +12 -0
  136. package/dist/plugins/workspace/infrastructure/workspace-repository.js +51 -0
  137. package/dist/plugins/workspace/mcp.d.ts +4 -0
  138. package/dist/plugins/workspace/mcp.js +168 -0
  139. package/dist/plugins/workspace/plugin.d.ts +10 -0
  140. package/dist/plugins/workspace/plugin.js +37 -0
  141. package/dist/plugins/workspace/workspace-helpers.d.ts +9 -0
  142. package/dist/plugins/workspace/workspace-helpers.js +67 -0
  143. package/dist/plugins/workspace/workspace-service.d.ts +46 -0
  144. package/dist/plugins/workspace/workspace-service.js +339 -0
  145. package/dist/plugins/workspace/worktree-review-service.d.ts +16 -0
  146. package/dist/plugins/workspace/worktree-review-service.js +51 -0
  147. package/dist/presentation/http/http-utils.d.ts +7 -0
  148. package/dist/presentation/http/http-utils.js +47 -0
  149. package/dist/presentation/http/oauth-router.d.ts +5 -0
  150. package/dist/presentation/http/oauth-router.js +163 -0
  151. package/dist/web/assets/chatroom-B5p-5n3y.svg +15 -0
  152. package/dist/web/assets/index-BF_mWl1D.js +26 -0
  153. package/dist/web/assets/index-JPZut9tc.css +1 -0
  154. package/dist/web/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  155. package/dist/web/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  156. package/dist/web/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  157. package/dist/web/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  158. package/dist/web/index.html +19 -0
  159. package/package.json +77 -0
@@ -0,0 +1,293 @@
1
+ import { createPrivateKey, sign } from "node:crypto";
2
+ import http from "node:http";
3
+ import WebSocket from "ws";
4
+ import { cloudServiceForPublicService, } from "./types.js";
5
+ const PROTOCOL = "chatroom-tunnel-v1";
6
+ const HIGH_WATER = 8 * 1024 * 1024;
7
+ const LOW_WATER = 2 * 1024 * 1024;
8
+ export class CloudTunnelClient {
9
+ lease;
10
+ identity;
11
+ local;
12
+ callbacks;
13
+ socket = null;
14
+ reconnectTimer = null;
15
+ stopped = true;
16
+ attempts = 0;
17
+ closeWhenIdle = null;
18
+ acceptingServices;
19
+ streams = new Map();
20
+ constructor(lease, identity, local, callbacks) {
21
+ this.lease = lease;
22
+ this.identity = identity;
23
+ this.local = local;
24
+ this.callbacks = callbacks;
25
+ this.acceptingServices = new Set(lease.services);
26
+ }
27
+ start() {
28
+ if (!this.stopped)
29
+ return;
30
+ this.stopped = false;
31
+ this.closeWhenIdle = null;
32
+ this.acceptingServices = new Set(this.lease.services);
33
+ this.connect();
34
+ }
35
+ stop() {
36
+ this.stopped = true;
37
+ this.closeWhenIdle = null;
38
+ this.acceptingServices.clear();
39
+ if (this.reconnectTimer)
40
+ clearTimeout(this.reconnectTimer);
41
+ this.reconnectTimer = null;
42
+ this.socket?.close();
43
+ this.socket = null;
44
+ for (const stream of this.streams.values())
45
+ stream.request.destroy();
46
+ this.streams.clear();
47
+ }
48
+ drainAndStop() {
49
+ this.stopped = true;
50
+ this.acceptingServices.clear();
51
+ if (this.reconnectTimer)
52
+ clearTimeout(this.reconnectTimer);
53
+ this.reconnectTimer = null;
54
+ if (this.streams.size === 0) {
55
+ this.closeWhenIdle = null;
56
+ this.socket?.close();
57
+ return;
58
+ }
59
+ this.closeWhenIdle = "stop";
60
+ }
61
+ updateLease(lease) {
62
+ const previousServices = this.lease.services;
63
+ const reconnect = lease.token !== this.lease.token ||
64
+ lease.tunnelUrl !== this.lease.tunnelUrl;
65
+ const removesService = previousServices.some((service) => !lease.services.includes(service));
66
+ this.lease = lease;
67
+ this.acceptingServices = new Set(lease.services);
68
+ if (!reconnect || this.stopped)
69
+ return;
70
+ if (!removesService || this.streams.size === 0) {
71
+ this.socket?.close();
72
+ return;
73
+ }
74
+ this.closeWhenIdle = "reconnect";
75
+ }
76
+ connect() {
77
+ if (this.stopped || this.socket)
78
+ return;
79
+ const socket = new WebSocket(this.lease.tunnelUrl, PROTOCOL);
80
+ this.socket = socket;
81
+ socket.binaryType = "nodebuffer";
82
+ socket.on("message", (data, isBinary) => {
83
+ try {
84
+ if (isBinary)
85
+ this.handleBinary(Buffer.from(data));
86
+ else
87
+ this.handleControl(JSON.parse(data.toString()));
88
+ }
89
+ catch (error) {
90
+ this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
91
+ socket.close();
92
+ }
93
+ });
94
+ socket.on("close", () => {
95
+ if (this.socket === socket)
96
+ this.socket = null;
97
+ this.closeWhenIdle = null;
98
+ for (const stream of this.streams.values())
99
+ stream.request.destroy();
100
+ this.streams.clear();
101
+ this.callbacks.onDisconnected();
102
+ if (!this.stopped)
103
+ this.scheduleReconnect();
104
+ });
105
+ socket.on("error", (error) => this.callbacks.onError(error));
106
+ }
107
+ handleControl(message) {
108
+ if (message.type === "challenge") {
109
+ this.send({
110
+ type: "authenticate",
111
+ lease: this.lease.token,
112
+ signature: signChallenge(message.nonce, this.lease.token, this.identity.devicePrivateKey),
113
+ });
114
+ return;
115
+ }
116
+ if (message.type === "ready") {
117
+ this.attempts = 0;
118
+ this.callbacks.onConnected();
119
+ return;
120
+ }
121
+ if (message.type === "open") {
122
+ this.openStream(message);
123
+ return;
124
+ }
125
+ const stream = this.streams.get(message.streamId);
126
+ if (!stream)
127
+ return;
128
+ if (message.type === "end")
129
+ stream.request.end();
130
+ else if (message.type === "abort") {
131
+ stream.request.destroy();
132
+ stream.response?.destroy();
133
+ this.finishStream(message.streamId);
134
+ }
135
+ else if (message.type === "pause")
136
+ stream.response?.pause();
137
+ else if (message.type === "resume")
138
+ stream.response?.resume();
139
+ }
140
+ openStream(message) {
141
+ const service = cloudServiceForPublicService(message.service);
142
+ if (!this.acceptingServices.has(service) ||
143
+ !allowedPath(message.service, message.path)) {
144
+ this.send({
145
+ type: "response",
146
+ streamId: message.streamId,
147
+ status: 404,
148
+ headers: { "content-type": "text/plain" },
149
+ });
150
+ this.send({ type: "response-end", streamId: message.streamId });
151
+ return;
152
+ }
153
+ const headers = sanitizeHeaders(message.headers);
154
+ const request = http.request({
155
+ host: this.local.host,
156
+ port: this.local.port,
157
+ method: message.method,
158
+ path: message.path,
159
+ headers,
160
+ }, (response) => {
161
+ const stream = this.streams.get(message.streamId);
162
+ if (stream)
163
+ stream.response = response;
164
+ this.send({
165
+ type: "response",
166
+ streamId: message.streamId,
167
+ status: response.statusCode ?? 502,
168
+ headers: responseHeaders(response.headers),
169
+ });
170
+ response.on("data", (chunk) => {
171
+ if (!this.sendBinary(message.streamId, Buffer.from(chunk))) {
172
+ response.pause();
173
+ const timer = setInterval(() => {
174
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
175
+ clearInterval(timer);
176
+ return;
177
+ }
178
+ if (this.socket.bufferedAmount <= LOW_WATER) {
179
+ clearInterval(timer);
180
+ response.resume();
181
+ }
182
+ }, 10);
183
+ timer.unref();
184
+ }
185
+ });
186
+ response.on("end", () => {
187
+ this.send({ type: "response-end", streamId: message.streamId });
188
+ this.finishStream(message.streamId);
189
+ });
190
+ response.on("error", () => {
191
+ this.send({ type: "abort", streamId: message.streamId });
192
+ this.finishStream(message.streamId);
193
+ });
194
+ });
195
+ request.on("drain", () => this.send({ type: "resume", streamId: message.streamId }));
196
+ request.on("error", () => {
197
+ this.send({
198
+ type: "response",
199
+ streamId: message.streamId,
200
+ status: 502,
201
+ headers: {},
202
+ });
203
+ this.send({ type: "response-end", streamId: message.streamId });
204
+ this.finishStream(message.streamId);
205
+ });
206
+ this.streams.set(message.streamId, { request, response: null });
207
+ }
208
+ finishStream(streamId) {
209
+ this.streams.delete(streamId);
210
+ if (this.streams.size !== 0 || !this.closeWhenIdle)
211
+ return;
212
+ const action = this.closeWhenIdle;
213
+ this.closeWhenIdle = null;
214
+ if (action === "stop") {
215
+ this.socket?.close();
216
+ return;
217
+ }
218
+ this.socket?.close();
219
+ }
220
+ handleBinary(frame) {
221
+ if (frame.length < 4)
222
+ throw new Error("Invalid tunnel data frame");
223
+ const streamId = frame.readUInt32BE(0);
224
+ const stream = this.streams.get(streamId);
225
+ if (!stream)
226
+ return;
227
+ if (!stream.request.write(frame.subarray(4)))
228
+ this.send({ type: "pause", streamId });
229
+ }
230
+ send(message) {
231
+ if (this.socket?.readyState === WebSocket.OPEN)
232
+ this.socket.send(JSON.stringify(message));
233
+ }
234
+ sendBinary(streamId, chunk) {
235
+ const socket = this.socket;
236
+ if (!socket || socket.readyState !== WebSocket.OPEN)
237
+ return false;
238
+ const frame = Buffer.allocUnsafe(4 + chunk.length);
239
+ frame.writeUInt32BE(streamId, 0);
240
+ chunk.copy(frame, 4);
241
+ socket.send(frame, { binary: true });
242
+ return socket.bufferedAmount < HIGH_WATER;
243
+ }
244
+ scheduleReconnect() {
245
+ if (this.reconnectTimer || this.stopped)
246
+ return;
247
+ const delay = Math.min(30_000, 500 * 2 ** Math.min(this.attempts++, 6)) *
248
+ (0.8 + Math.random() * 0.4);
249
+ this.reconnectTimer = setTimeout(() => {
250
+ this.reconnectTimer = null;
251
+ this.connect();
252
+ }, delay);
253
+ this.reconnectTimer.unref();
254
+ }
255
+ }
256
+ function signChallenge(nonce, lease, privateKey) {
257
+ const key = createPrivateKey({
258
+ key: Buffer.from(privateKey, "base64url"),
259
+ format: "der",
260
+ type: "pkcs8",
261
+ });
262
+ return sign(null, Buffer.from(`chatroom-tunnel-v1\n${nonce}\n${lease}`), key).toString("base64url");
263
+ }
264
+ function allowedPath(service, value) {
265
+ const pathname = new URL(value, "http://localhost").pathname;
266
+ if (service === "mcp")
267
+ return (pathname === "/mcp" ||
268
+ pathname.startsWith("/.well-known/oauth-") ||
269
+ pathname.startsWith("/oauth/"));
270
+ return (pathname !== "/mcp" &&
271
+ !pathname.startsWith("/oauth/") &&
272
+ !pathname.startsWith("/.well-known/oauth-"));
273
+ }
274
+ function sanitizeHeaders(headers) {
275
+ const blocked = new Set([
276
+ "connection",
277
+ "proxy-connection",
278
+ "keep-alive",
279
+ "transfer-encoding",
280
+ "upgrade",
281
+ ]);
282
+ return Object.fromEntries(Object.entries(headers).filter(([key]) => !blocked.has(key.toLowerCase())));
283
+ }
284
+ function responseHeaders(headers) {
285
+ const result = {};
286
+ for (const [key, value] of Object.entries(headers)) {
287
+ if (value === undefined ||
288
+ ["connection", "keep-alive", "transfer-encoding", "upgrade"].includes(key.toLowerCase()))
289
+ continue;
290
+ result[key] = Array.isArray(value) ? value.join(", ") : value;
291
+ }
292
+ return result;
293
+ }
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ export declare const CLOUD_SERVICES: readonly ["remote_mcp", "remote_web"];
3
+ export declare const CLOUD_SERVICE_SCHEMA: z.ZodEnum<{
4
+ remote_mcp: "remote_mcp";
5
+ remote_web: "remote_web";
6
+ }>;
7
+ export type CloudServiceId = (typeof CLOUD_SERVICES)[number];
8
+ declare const PUBLIC_SERVICE_TO_CLOUD_SERVICE: {
9
+ readonly mcp: "remote_mcp";
10
+ readonly web: "remote_web";
11
+ };
12
+ export type PublicService = keyof typeof PUBLIC_SERVICE_TO_CLOUD_SERVICE;
13
+ export declare const CLOUD_DESIRED_SERVICES_SCHEMA: z.ZodObject<{
14
+ remote_mcp: z.ZodBoolean;
15
+ remote_web: z.ZodBoolean;
16
+ }, z.core.$strict>;
17
+ export declare function cloudServiceForPublicService(service: PublicService): CloudServiceId;
18
+ export declare const CLOUD_ENTITLEMENT_SCHEMA: z.ZodObject<{
19
+ service: z.ZodEnum<{
20
+ remote_mcp: "remote_mcp";
21
+ remote_web: "remote_web";
22
+ }>;
23
+ status: z.ZodLiteral<"active">;
24
+ sourceProvider: z.ZodString;
25
+ sourceId: z.ZodString;
26
+ validUntil: z.ZodNullable<z.ZodString>;
27
+ }, z.core.$strict>;
28
+ export type CloudEntitlement = z.infer<typeof CLOUD_ENTITLEMENT_SCHEMA>;
29
+ export declare const CLOUD_LEASE_SCHEMA: z.ZodObject<{
30
+ token: z.ZodString;
31
+ expiresAt: z.ZodString;
32
+ tunnelUrl: z.ZodString;
33
+ mcpBaseUrl: z.ZodNullable<z.ZodString>;
34
+ webBaseUrl: z.ZodNullable<z.ZodString>;
35
+ services: z.ZodArray<z.ZodEnum<{
36
+ remote_mcp: "remote_mcp";
37
+ remote_web: "remote_web";
38
+ }>>;
39
+ }, z.core.$strict>;
40
+ export type CloudLeaseState = z.infer<typeof CLOUD_LEASE_SCHEMA>;
41
+ interface ManagementSessionState {
42
+ purchaseToken: string;
43
+ managementUrl: string;
44
+ expiresAt: string;
45
+ }
46
+ export interface CloudPersistedState {
47
+ schemaVersion: 1;
48
+ installationId: string;
49
+ devicePublicKey: string;
50
+ devicePrivateKey: string;
51
+ customerId: string | null;
52
+ publicPrefix: string | null;
53
+ desiredServices: Record<CloudServiceId, boolean>;
54
+ entitlements: CloudEntitlement[];
55
+ managementSession: ManagementSessionState | null;
56
+ lease: CloudLeaseState | null;
57
+ }
58
+ export interface CloudStatus {
59
+ installationId: string;
60
+ customerId: string | null;
61
+ publicPrefix: string | null;
62
+ desiredServices: Record<CloudServiceId, boolean>;
63
+ entitlements: CloudEntitlement[];
64
+ managementSessionActive: boolean;
65
+ connection: "inactive" | "connecting" | "connected" | "disconnected" | "error";
66
+ mcpUrl: string | null;
67
+ webUrl: string | null;
68
+ lastError: string | null;
69
+ }
70
+ export {};
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ export const CLOUD_SERVICES = ["remote_mcp", "remote_web"];
3
+ export const CLOUD_SERVICE_SCHEMA = z.enum(CLOUD_SERVICES);
4
+ const PUBLIC_SERVICE_TO_CLOUD_SERVICE = {
5
+ mcp: "remote_mcp",
6
+ web: "remote_web",
7
+ };
8
+ export const CLOUD_DESIRED_SERVICES_SCHEMA = z
9
+ .object({ remote_mcp: z.boolean(), remote_web: z.boolean() })
10
+ .strict();
11
+ export function cloudServiceForPublicService(service) {
12
+ return PUBLIC_SERVICE_TO_CLOUD_SERVICE[service];
13
+ }
14
+ export const CLOUD_ENTITLEMENT_SCHEMA = z
15
+ .object({
16
+ service: CLOUD_SERVICE_SCHEMA,
17
+ status: z.literal("active"),
18
+ sourceProvider: z.string().min(1),
19
+ sourceId: z.string().min(1),
20
+ validUntil: z.string().datetime().nullable(),
21
+ })
22
+ .strict();
23
+ export const CLOUD_LEASE_SCHEMA = z
24
+ .object({
25
+ token: z.string().min(1),
26
+ expiresAt: z.string().datetime(),
27
+ tunnelUrl: z.string().url(),
28
+ mcpBaseUrl: z.string().url().nullable(),
29
+ webBaseUrl: z.string().url().nullable(),
30
+ services: z.array(CLOUD_SERVICE_SCHEMA),
31
+ })
32
+ .strict();
@@ -0,0 +1,11 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
+ import type { InternalPlugin, PluginContext } from "./types.js";
3
+ export declare class PluginManager {
4
+ private readonly context;
5
+ private readonly plugins;
6
+ private readonly active;
7
+ constructor(context: PluginContext, plugins: InternalPlugin[]);
8
+ start(): Promise<void>;
9
+ registerMcp(server: McpServer): void;
10
+ stop(): Promise<void>;
11
+ }
@@ -0,0 +1,24 @@
1
+ export class PluginManager {
2
+ context;
3
+ plugins;
4
+ active = [];
5
+ constructor(context, plugins) {
6
+ this.context = context;
7
+ this.plugins = plugins;
8
+ }
9
+ async start() {
10
+ for (const plugin of this.plugins) {
11
+ await plugin.activate(this.context);
12
+ this.active.push(plugin);
13
+ }
14
+ }
15
+ registerMcp(server) {
16
+ for (const plugin of this.active)
17
+ plugin.registerMcp?.(server);
18
+ }
19
+ async stop() {
20
+ for (const plugin of [...this.active].reverse())
21
+ await plugin.deactivate?.();
22
+ this.active.length = 0;
23
+ }
24
+ }
@@ -0,0 +1,19 @@
1
+ /** OS/process-backend port consumed by ProcessSupervisor. */
2
+ export interface BackendProcess {
3
+ readonly pid: number | null;
4
+ write(data: string): void;
5
+ kill(signal?: NodeJS.Signals): void;
6
+ onStdout(listener: (chunk: Buffer) => void): void;
7
+ onStderr(listener: (chunk: Buffer) => void): void;
8
+ onExit(listener: (exit: {
9
+ exitCode: number | null;
10
+ signal: string | null;
11
+ }) => void): void;
12
+ }
13
+ export interface ProcessBackend {
14
+ readonly kind: "pipe" | "pty";
15
+ start(command: string, args: string[], options: {
16
+ cwd: string;
17
+ env: NodeJS.ProcessEnv;
18
+ }): Promise<BackendProcess>;
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ /** Bounded process-output buffer that preserves useful context from both ends of a stream. */
2
+ interface BufferSnapshot {
3
+ text: string;
4
+ bytesSeen: number;
5
+ outputTruncated: boolean;
6
+ }
7
+ export declare class HeadTailBuffer {
8
+ private readonly headLimit;
9
+ private readonly tailLimit;
10
+ private head;
11
+ private tail;
12
+ private bytesSeen;
13
+ constructor(totalLimitBytes?: number);
14
+ append(chunk: Buffer | string): void;
15
+ snapshot(): BufferSnapshot;
16
+ private pushTail;
17
+ }
18
+ export {};
@@ -0,0 +1,49 @@
1
+ export class HeadTailBuffer {
2
+ headLimit;
3
+ tailLimit;
4
+ head = Buffer.alloc(0);
5
+ tail = Buffer.alloc(0);
6
+ bytesSeen = 0;
7
+ constructor(totalLimitBytes = 256 * 1024) {
8
+ if (totalLimitBytes < 128)
9
+ throw new RangeError("Buffer limit must be at least 128 bytes");
10
+ this.headLimit = Math.floor(totalLimitBytes / 2);
11
+ this.tailLimit = totalLimitBytes - this.headLimit;
12
+ }
13
+ append(chunk) {
14
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
15
+ this.bytesSeen += data.length;
16
+ if (this.head.length < this.headLimit) {
17
+ const needed = this.headLimit - this.head.length;
18
+ this.head = Buffer.concat([this.head, data.subarray(0, needed)]);
19
+ if (data.length <= needed)
20
+ return;
21
+ this.pushTail(data.subarray(needed));
22
+ return;
23
+ }
24
+ this.pushTail(data);
25
+ }
26
+ snapshot() {
27
+ const stored = this.head.length + this.tail.length;
28
+ const truncated = this.bytesSeen > stored;
29
+ if (!truncated) {
30
+ return {
31
+ text: Buffer.concat([this.head, this.tail]).toString("utf8"),
32
+ bytesSeen: this.bytesSeen,
33
+ outputTruncated: false,
34
+ };
35
+ }
36
+ return {
37
+ text: `${this.head.toString("utf8")}\n…[${this.bytesSeen - stored} bytes truncated]…\n${this.tail.toString("utf8")}`,
38
+ bytesSeen: this.bytesSeen,
39
+ outputTruncated: true,
40
+ };
41
+ }
42
+ pushTail(data) {
43
+ const combined = Buffer.concat([this.tail, data]);
44
+ this.tail =
45
+ combined.length > this.tailLimit
46
+ ? combined.subarray(combined.length - this.tailLimit)
47
+ : combined;
48
+ }
49
+ }
@@ -0,0 +1,8 @@
1
+ import type { BackendProcess, ProcessBackend } from "../backend.js";
2
+ export declare class PipeProcessBackend implements ProcessBackend {
3
+ readonly kind: "pipe";
4
+ start(command: string, args: string[], options: {
5
+ cwd: string;
6
+ env: NodeJS.ProcessEnv;
7
+ }): Promise<BackendProcess>;
8
+ }
@@ -0,0 +1,61 @@
1
+ /** Pipe-based ProcessBackend implemented with Node child_process.spawn. */
2
+ import { spawn } from "node:child_process";
3
+ export class PipeProcessBackend {
4
+ kind = "pipe";
5
+ async start(command, args, options) {
6
+ const child = spawn(command, args, {
7
+ cwd: options.cwd,
8
+ env: options.env,
9
+ stdio: ["pipe", "pipe", "pipe"],
10
+ detached: process.platform !== "win32",
11
+ windowsHide: true,
12
+ });
13
+ await new Promise((resolve, reject) => {
14
+ child.once("spawn", resolve);
15
+ child.once("error", reject);
16
+ });
17
+ return new PipeBackendProcess(child);
18
+ }
19
+ }
20
+ class PipeBackendProcess {
21
+ child;
22
+ constructor(child) {
23
+ this.child = child;
24
+ }
25
+ get pid() {
26
+ return this.child.pid ?? null;
27
+ }
28
+ write(data) {
29
+ this.child.stdin.write(data);
30
+ }
31
+ kill(signal = "SIGTERM") {
32
+ if (!this.child.pid)
33
+ return;
34
+ if (process.platform === "win32") {
35
+ const args = ["/PID", String(this.child.pid), "/T"];
36
+ if (signal === "SIGKILL")
37
+ args.push("/F");
38
+ const killer = spawn("taskkill.exe", args, {
39
+ stdio: "ignore",
40
+ windowsHide: true,
41
+ });
42
+ killer.once("error", () => this.child.kill(signal));
43
+ return;
44
+ }
45
+ try {
46
+ process.kill(-this.child.pid, signal);
47
+ }
48
+ catch {
49
+ this.child.kill(signal);
50
+ }
51
+ }
52
+ onStdout(listener) {
53
+ this.child.stdout.on("data", listener);
54
+ }
55
+ onStderr(listener) {
56
+ this.child.stderr.on("data", listener);
57
+ }
58
+ onExit(listener) {
59
+ this.child.once("close", (code, signal) => listener({ exitCode: code, signal }));
60
+ }
61
+ }
@@ -0,0 +1,8 @@
1
+ import type { BackendProcess, ProcessBackend } from "../backend.js";
2
+ export declare class PtyProcessBackend implements ProcessBackend {
3
+ readonly kind: "pty";
4
+ start(command: string, args: string[], options: {
5
+ cwd: string;
6
+ env: NodeJS.ProcessEnv;
7
+ }): Promise<BackendProcess>;
8
+ }
@@ -0,0 +1,50 @@
1
+ /** Optional PTY ProcessBackend backed by node-pty when native PTY support is installed. */
2
+ import { ChatRoomError } from "../../../core/errors/chatroom-error.js";
3
+ export class PtyProcessBackend {
4
+ kind = "pty";
5
+ async start(command, args, options) {
6
+ let pty;
7
+ try {
8
+ pty = (await import("node-pty"));
9
+ }
10
+ catch (error) {
11
+ throw new ChatRoomError("UNSUPPORTED", "PTY support is unavailable; install optional dependency node-pty", undefined, { cause: error });
12
+ }
13
+ const env = Object.fromEntries(Object.entries(options.env).filter((entry) => typeof entry[1] === "string"));
14
+ const processHandle = pty.spawn(command, args, {
15
+ name: env.TERM ?? "xterm-256color",
16
+ cols: 120,
17
+ rows: 30,
18
+ cwd: options.cwd,
19
+ env,
20
+ });
21
+ return new PtyBackendProcess(processHandle);
22
+ }
23
+ }
24
+ class PtyBackendProcess {
25
+ processHandle;
26
+ constructor(processHandle) {
27
+ this.processHandle = processHandle;
28
+ }
29
+ get pid() {
30
+ return this.processHandle.pid;
31
+ }
32
+ write(data) {
33
+ this.processHandle.write(data);
34
+ }
35
+ kill(signal = "SIGTERM") {
36
+ this.processHandle.kill(signal);
37
+ }
38
+ onStdout(listener) {
39
+ this.processHandle.onData((data) => listener(Buffer.from(data)));
40
+ }
41
+ onStderr(_listener) {
42
+ /* PTYs expose a merged stream. */
43
+ }
44
+ onExit(listener) {
45
+ this.processHandle.onExit((event) => listener({
46
+ exitCode: event.exitCode,
47
+ signal: event.signal ? String(event.signal) : null,
48
+ }));
49
+ }
50
+ }
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
+ import type { OperationLog } from "../../operations/operation-log.js";
3
+ import type { ProcessSupervisor } from "./process-supervisor.js";
4
+ export declare function registerProcessTools(server: McpServer, processes: ProcessSupervisor, operations: OperationLog): void;