@zhanguym/chatroom 1.1.0 → 1.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/app/application.d.ts +2 -0
  2. package/dist/app/application.js +7 -1
  3. package/dist/app/lifecycle.js +6 -0
  4. package/dist/auth/auth-service.js +5 -1
  5. package/dist/core/runtime/child-environment.js +3 -2
  6. package/dist/infrastructure/database/custom-mcp-repository.d.ts +9 -0
  7. package/dist/infrastructure/database/custom-mcp-repository.js +37 -0
  8. package/dist/infrastructure/database/mcp-tool-settings-repository.d.ts +1 -0
  9. package/dist/infrastructure/database/mcp-tool-settings-repository.js +6 -0
  10. package/dist/infrastructure/database/schema.d.ts +1 -1
  11. package/dist/infrastructure/database/schema.js +9 -0
  12. package/dist/infrastructure/http/http-server.js +7 -1
  13. package/dist/mcp/custom/manager.d.ts +54 -0
  14. package/dist/mcp/custom/manager.js +494 -0
  15. package/dist/mcp/custom/oauth.d.ts +38 -0
  16. package/dist/mcp/custom/oauth.js +116 -0
  17. package/dist/mcp/custom/types.d.ts +42 -0
  18. package/dist/mcp/custom/types.js +1 -0
  19. package/dist/mcp/server/create-mcp-server.d.ts +2 -1
  20. package/dist/mcp/server/create-mcp-server.js +4 -3
  21. package/dist/mcp/server/tool-control.d.ts +5 -0
  22. package/dist/mcp/server/tool-control.js +17 -0
  23. package/dist/native/linux/chatroom-computer-helper +0 -0
  24. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  25. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  26. package/dist/plugins/cloud/tunnel-client.d.ts +2 -84
  27. package/dist/plugins/cloud/tunnel-client.js +2 -426
  28. package/dist/plugins/cloud/v2/stream.d.ts +48 -0
  29. package/dist/plugins/cloud/v2/stream.js +156 -0
  30. package/dist/plugins/cloud/v2/tunnel-client.d.ts +45 -0
  31. package/dist/plugins/cloud/v2/tunnel-client.js +426 -0
  32. package/dist/plugins/computer/computer-schemas.d.ts +8 -8
  33. package/dist/plugins/process/backend.d.ts +3 -0
  34. package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +2 -0
  35. package/dist/plugins/process/infrastructure/pty-process-backend.js +5 -2
  36. package/dist/plugins/process/infrastructure/terminal-shell.d.ts +4 -0
  37. package/dist/plugins/process/infrastructure/terminal-shell.js +10 -0
  38. package/dist/plugins/process/process-supervisor.d.ts +6 -0
  39. package/dist/plugins/process/process-supervisor.js +26 -0
  40. package/dist/plugins/process/types.d.ts +2 -0
  41. package/dist/plugins/types.d.ts +2 -0
  42. package/dist/plugins/web/api-types.d.ts +2 -0
  43. package/dist/plugins/web/http/api-router.d.ts +2 -1
  44. package/dist/plugins/web/http/api-router.js +8 -1
  45. package/dist/plugins/web/http/custom-mcp-api-router.d.ts +6 -0
  46. package/dist/plugins/web/http/custom-mcp-api-router.js +114 -0
  47. package/dist/plugins/web/http/logs-api-router.js +2 -2
  48. package/dist/plugins/web/http/process-api-router.js +48 -0
  49. package/dist/plugins/web/http/rtc-api-router.d.ts +4 -0
  50. package/dist/plugins/web/http/rtc-api-router.js +316 -0
  51. package/dist/plugins/web/http/runtime-api-router.js +2 -2
  52. package/dist/plugins/web/http/terminal-api-router.d.ts +4 -0
  53. package/dist/plugins/web/http/terminal-api-router.js +132 -0
  54. package/dist/plugins/web/plugin.js +1 -1
  55. package/dist/plugins/web/runtime.d.ts +3 -1
  56. package/dist/plugins/web/runtime.js +3 -1
  57. package/dist/web/assets/AgentView-BEA5mQVv.js +4 -0
  58. package/dist/web/assets/{AgentView-BGLIBoxF.css → AgentView-CMxIn3KB.css} +1 -1
  59. package/dist/web/assets/CloudView-BGcVSE22.js +1 -0
  60. package/dist/web/assets/CloudView-Bgp5G965.css +1 -0
  61. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-DfPmlGC0.js +3 -0
  62. package/dist/web/assets/ComputerView-Bz2Wsz2X.css +1 -0
  63. package/dist/web/assets/ComputerView-D6_NSXKb.js +1 -0
  64. package/dist/web/assets/{MarkdownContent-CkjR5fs7.js → MarkdownContent-DCkAALOF.js} +2 -2
  65. package/dist/web/assets/McpToolsView--8UloObb.js +2 -0
  66. package/dist/web/assets/McpToolsView-DdYOoY9_.css +1 -0
  67. package/dist/web/assets/OperationsView-CqgAPnSL.js +4 -0
  68. package/dist/web/assets/ProcessesView-BmNdR3C0.css +1 -0
  69. package/dist/web/assets/ProcessesView-DC0tjiYU.js +5 -0
  70. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-D_LKEtqT.js +1 -0
  71. package/dist/web/assets/SystemLogsView-COvwamo5.css +1 -0
  72. package/dist/web/assets/SystemLogsView-DkTeUr2k.js +1 -0
  73. package/dist/web/assets/TerminalView-Coze7IMj.css +1 -0
  74. package/dist/web/assets/TerminalView-DEP1U0aW.js +16 -0
  75. package/dist/web/assets/VSelect-B5-t612C.js +1 -0
  76. package/dist/web/assets/VSheet-CNayLacf.js +1 -0
  77. package/dist/web/assets/VSpacer-BYe3Z99W.js +1 -0
  78. package/dist/web/assets/VSwitch-cVRCnipJ.js +1 -0
  79. package/dist/web/assets/VTabs-HKX286IT.js +1 -0
  80. package/dist/web/assets/VTextarea-ypCBuKI-.js +1 -0
  81. package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +1 -0
  82. package/dist/web/assets/WorkspaceDownloadPreview-qwbTkU59.js +1 -0
  83. package/dist/web/assets/WorkspaceMediaPreview-D28Yf2yb.js +4 -0
  84. package/dist/web/assets/{WorkspacePdfPreview-CO-ZtDHG.js → WorkspacePdfPreview-BfZSPcR-.js} +2 -2
  85. package/dist/web/assets/WorkspacesView-CsCGtk5H.js +2 -0
  86. package/dist/web/assets/index-BCUxnASh.css +1 -0
  87. package/dist/web/assets/index-CFhV1jOj.js +19 -0
  88. package/dist/web/assets/{useMasterDetailFocus-D1mQ8Qjk.js → useMasterDetailFocus-CMJXBPPp.js} +1 -1
  89. package/dist/web/assets/useOperationLabels-DSbMa9Eb.js +1 -0
  90. package/dist/web/index.html +2 -2
  91. package/dist/web/transport-sw.js +417 -0
  92. package/package.json +10 -4
  93. package/scripts/ensure-pty-helper.mjs +28 -0
  94. package/dist/web/assets/AgentView-Dd6zCbr-.js +0 -4
  95. package/dist/web/assets/CloudView-DVVvwuWW.css +0 -1
  96. package/dist/web/assets/CloudView-DquY1HDZ.js +0 -1
  97. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-ekOImmBP.js +0 -3
  98. package/dist/web/assets/ComputerView-DWzfSYJ7.css +0 -1
  99. package/dist/web/assets/ComputerView-RD2gMxJZ.js +0 -1
  100. package/dist/web/assets/McpToolsView-Dg2-iaDP.js +0 -1
  101. package/dist/web/assets/McpToolsView-DjxPbTHj.css +0 -1
  102. package/dist/web/assets/OperationsView-C8yCX7WZ.js +0 -4
  103. package/dist/web/assets/ProcessesView-C834LC-f.js +0 -4
  104. package/dist/web/assets/ProcessesView-Ce6iVwjA.css +0 -1
  105. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-By2gilBw.js +0 -1
  106. package/dist/web/assets/SystemLogsView-B6KcZv1J.js +0 -1
  107. package/dist/web/assets/SystemLogsView-CofP4Jvi.css +0 -1
  108. package/dist/web/assets/VSelect-BtAJX-XJ.js +0 -1
  109. package/dist/web/assets/VSpacer-M8bpkhTH.js +0 -1
  110. package/dist/web/assets/VSwitch-C0VTnwLJ.js +0 -1
  111. package/dist/web/assets/VTabs-g61mykvY.js +0 -1
  112. package/dist/web/assets/VTextarea-CRYQYRqg.js +0 -1
  113. package/dist/web/assets/WorkspaceDownloadPreview-BrESF-82.js +0 -1
  114. package/dist/web/assets/WorkspaceDownloadPreview-DnxhH5Q7.css +0 -1
  115. package/dist/web/assets/WorkspaceMediaPreview-HYajqgZ3.js +0 -4
  116. package/dist/web/assets/WorkspacesView-BJCnzBLj.js +0 -2
  117. package/dist/web/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
  118. package/dist/web/assets/index-CZ-4tJlF.css +0 -1
  119. package/dist/web/assets/index-u-aADQP-.js +0 -19
  120. package/dist/web/assets/useOperationLabels-BjPIVnJW.js +0 -1
  121. package/dist/web/assets/utils-rtPwWNel.js +0 -1
  122. /package/dist/web/assets/{CodeViewer-CH1DEJge.css → VSheet-CH1DEJge.css} +0 -0
@@ -0,0 +1,316 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import http from "node:http";
3
+ import { Router } from "express";
4
+ import { z } from "zod";
5
+ import { CreditSender, INITIAL_CREDIT, creditFrame, decodeFrame, metaFrame, parseMeta, readCredit, } from "#plugins/cloud/v2/stream";
6
+ const offerSchema = z
7
+ .object({
8
+ type: z.literal("offer"),
9
+ sdp: z.string().min(1).max(64_000),
10
+ })
11
+ .strict();
12
+ const openSchema = z
13
+ .object({
14
+ method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
15
+ path: z.string().max(8192),
16
+ headers: z.record(z.string(), z.string()).default({}),
17
+ })
18
+ .strict();
19
+ const STUN = ["stun:stun.cloudflare.com:3478", "stun:stun.l.google.com:19302"];
20
+ const MAX_PEERS = 8;
21
+ const MAX_STREAMS = 32;
22
+ const MAX_UPLOAD = 8 * 1024 * 1024;
23
+ const MAX_IDLE_MS = 30 * 60 * 1000;
24
+ const HEADER_NAMES = new Set([
25
+ "accept",
26
+ "content-type",
27
+ "range",
28
+ "if-none-match",
29
+ "if-modified-since",
30
+ "x-chatroom-mime-type",
31
+ ]);
32
+ const RESPONSE_HEADERS = new Set([
33
+ "content-type",
34
+ "content-disposition",
35
+ "content-length",
36
+ "content-range",
37
+ "accept-ranges",
38
+ "cache-control",
39
+ "etag",
40
+ "last-modified",
41
+ ]);
42
+ /** V2 DataChannel HTTP transport. Cloud and self-hosting use the same HTTPS signaling API. */
43
+ export function createRtcApiRouter(config) {
44
+ const router = Router();
45
+ const peers = new Map();
46
+ router.post("/rtc/offer", async (req, res) => {
47
+ const parsed = offerSchema.safeParse(req.body);
48
+ if (!parsed.success ||
49
+ !parsed.data.sdp.includes("m=application ") ||
50
+ !parsed.data.sdp.includes("a=fingerprint:") ||
51
+ !parsed.data.sdp.includes("a=ice-ufrag:")) {
52
+ res.status(400).json({ error: "invalid_offer" });
53
+ return;
54
+ }
55
+ if (peers.size >= MAX_PEERS) {
56
+ res.status(429).json({ error: "rtc_capacity" });
57
+ return;
58
+ }
59
+ let library;
60
+ try {
61
+ library = await import("node-datachannel");
62
+ }
63
+ catch {
64
+ res.status(503).json({ error: "rtc_unavailable" });
65
+ return;
66
+ }
67
+ const id = randomUUID();
68
+ const pc = new library.PeerConnection(id, { iceServers: STUN });
69
+ peers.set(id, pc);
70
+ const streams = new Map();
71
+ let dc = null;
72
+ let closed = false;
73
+ const cookie = req.headers.cookie ?? "";
74
+ const host = req.headers.host ?? "localhost";
75
+ const origin = req.headers.origin ?? "";
76
+ const cleanup = () => {
77
+ if (closed)
78
+ return;
79
+ closed = true;
80
+ clearTimeout(expiry);
81
+ for (const stream of streams.values()) {
82
+ stream.sender?.cancel();
83
+ stream.request.destroy();
84
+ }
85
+ streams.clear();
86
+ peers.delete(id);
87
+ try {
88
+ dc?.close();
89
+ }
90
+ catch {
91
+ /* connection already closed */
92
+ }
93
+ pc.close();
94
+ };
95
+ let expiry;
96
+ const activity = () => {
97
+ clearTimeout(expiry);
98
+ expiry = setTimeout(cleanup, MAX_IDLE_MS);
99
+ expiry.unref();
100
+ };
101
+ activity();
102
+ pc.onStateChange((state) => {
103
+ if (state === "closed" || state === "failed")
104
+ cleanup();
105
+ });
106
+ pc.onDataChannel((channel) => {
107
+ if (closed || dc || channel.getLabel() !== "chatroom-v2") {
108
+ channel.close();
109
+ return;
110
+ }
111
+ dc = channel;
112
+ channel.setBufferedAmountLowThreshold(512 * 1024);
113
+ channel.onBufferedAmountLow(() => {
114
+ for (const stream of streams.values())
115
+ stream.sender?.writable();
116
+ });
117
+ const send = (frame) => {
118
+ try {
119
+ if (!channel.isOpen() ||
120
+ channel.bufferedAmount() > 4 * 1024 * 1024 ||
121
+ !channel.sendMessageBinary(Buffer.from(frame))) {
122
+ cleanup();
123
+ return;
124
+ }
125
+ activity();
126
+ }
127
+ catch {
128
+ cleanup();
129
+ }
130
+ };
131
+ const abort = (streamId) => {
132
+ const stream = streams.get(streamId);
133
+ if (!stream)
134
+ return;
135
+ streams.delete(streamId);
136
+ stream.sender?.cancel();
137
+ stream.request.destroy();
138
+ };
139
+ const reset = (streamId, reason) => {
140
+ if (!streams.has(streamId))
141
+ return;
142
+ abort(streamId);
143
+ send(metaFrame(5 /* Op.RESET */, streamId, { reason }));
144
+ };
145
+ channel.onMessage((raw) => {
146
+ if (closed || typeof raw === "string") {
147
+ cleanup();
148
+ return;
149
+ }
150
+ try {
151
+ activity();
152
+ const frame = decodeFrame(new Uint8Array(raw));
153
+ if (!frame.id)
154
+ throw Error("Reserved RTC control stream");
155
+ if (frame.op === 1 /* Op.OPEN */) {
156
+ if (streams.has(frame.id) || streams.size >= MAX_STREAMS) {
157
+ reset(frame.id, "capacity");
158
+ return;
159
+ }
160
+ const open = openSchema.parse(parseMeta(frame.payload));
161
+ if (!(open.path.startsWith("/api/") ||
162
+ open.path.startsWith("/assets/")) ||
163
+ open.path.startsWith("/api/rtc/") ||
164
+ (open.path.startsWith("/assets/") && open.method !== "GET") ||
165
+ open.path.includes("#")) {
166
+ reset(frame.id, "invalid_path");
167
+ return;
168
+ }
169
+ const headers = { host, cookie };
170
+ if (origin)
171
+ headers.origin = origin;
172
+ for (const [name, value] of Object.entries(open.headers))
173
+ if (HEADER_NAMES.has(name.toLowerCase()))
174
+ headers[name.toLowerCase()] = value;
175
+ const request = http.request({
176
+ host: ["0.0.0.0", "::"].includes(config.server.host)
177
+ ? "127.0.0.1"
178
+ : config.server.host,
179
+ port: config.server.port,
180
+ method: open.method,
181
+ path: open.path,
182
+ headers,
183
+ }, (response) => {
184
+ const stream = streams.get(frame.id);
185
+ if (!stream) {
186
+ response.destroy();
187
+ return;
188
+ }
189
+ const responseHeaders = Object.fromEntries(Object.entries(response.headers).filter(([name, value]) => RESPONSE_HEADERS.has(name) && typeof value === "string"));
190
+ send(metaFrame(2 /* Op.META */, frame.id, {
191
+ status: response.statusCode ?? 502,
192
+ headers: responseHeaders,
193
+ }));
194
+ const writer = new CreditSender(frame.id, send, () => response.pause(), () => response.resume(), () => {
195
+ streams.delete(frame.id);
196
+ }, () => channel.bufferedAmount() < 1024 * 1024);
197
+ stream.sender = writer;
198
+ response.on("data", (chunk) => {
199
+ if (!streams.has(frame.id))
200
+ return;
201
+ try {
202
+ writer.enqueue(chunk);
203
+ }
204
+ catch {
205
+ reset(frame.id, "send_overflow");
206
+ response.destroy();
207
+ }
208
+ });
209
+ response.on("end", () => writer.end());
210
+ response.on("error", () => reset(frame.id, "response_error"));
211
+ response.on("close", () => {
212
+ if (!response.complete && streams.has(frame.id))
213
+ reset(frame.id, "response_disconnected");
214
+ });
215
+ });
216
+ const stream = {
217
+ request,
218
+ sender: null,
219
+ requestBytes: 0,
220
+ receiveCredit: INITIAL_CREDIT,
221
+ consumed: 0,
222
+ drainArmed: false,
223
+ ended: false,
224
+ };
225
+ streams.set(frame.id, stream);
226
+ request.on("error", () => {
227
+ if (streams.has(frame.id))
228
+ reset(frame.id, "request_failed");
229
+ });
230
+ }
231
+ else if (frame.op === 3 /* Op.DATA */) {
232
+ const stream = streams.get(frame.id);
233
+ if (!stream)
234
+ return;
235
+ stream.requestBytes += frame.payload.byteLength;
236
+ stream.receiveCredit -= frame.payload.byteLength;
237
+ if (stream.ended ||
238
+ stream.requestBytes > MAX_UPLOAD ||
239
+ stream.receiveCredit < 0) {
240
+ reset(frame.id, "request_limit");
241
+ return;
242
+ }
243
+ if (stream.request.write(frame.payload)) {
244
+ stream.receiveCredit += frame.payload.byteLength;
245
+ send(creditFrame(frame.id, frame.payload.byteLength));
246
+ }
247
+ else {
248
+ stream.consumed += frame.payload.byteLength;
249
+ if (!stream.drainArmed) {
250
+ stream.drainArmed = true;
251
+ stream.request.once("drain", () => {
252
+ stream.drainArmed = false;
253
+ if (!streams.has(frame.id))
254
+ return;
255
+ stream.receiveCredit += stream.consumed;
256
+ send(creditFrame(frame.id, stream.consumed));
257
+ stream.consumed = 0;
258
+ });
259
+ }
260
+ }
261
+ }
262
+ else if (frame.op === 4 /* Op.FIN */) {
263
+ const stream = streams.get(frame.id);
264
+ if (!stream || stream.ended)
265
+ return;
266
+ stream.ended = true;
267
+ stream.request.end();
268
+ }
269
+ else if (frame.op === 6 /* Op.CREDIT */) {
270
+ streams.get(frame.id)?.sender?.addCredit(readCredit(frame.payload));
271
+ }
272
+ else if (frame.op === 5 /* Op.RESET */) {
273
+ abort(frame.id);
274
+ }
275
+ else
276
+ throw Error("Unexpected RTC frame");
277
+ }
278
+ catch {
279
+ cleanup();
280
+ }
281
+ });
282
+ channel.onClosed(cleanup);
283
+ channel.onError(cleanup);
284
+ });
285
+ try {
286
+ const answer = await new Promise((resolve, reject) => {
287
+ const timeout = setTimeout(() => reject(Error("RTC answer timeout")), 12_000);
288
+ const finish = () => {
289
+ const description = pc.localDescription();
290
+ if (!description || description.type !== "answer")
291
+ return;
292
+ clearTimeout(timeout);
293
+ resolve({ type: "answer", sdp: description.sdp });
294
+ };
295
+ pc.onGatheringStateChange((state) => {
296
+ if (state === "complete")
297
+ finish();
298
+ });
299
+ pc.onLocalDescription(() => {
300
+ if (pc.gatheringState() === "complete")
301
+ finish();
302
+ });
303
+ pc.setRemoteDescription(parsed.data.sdp, "offer");
304
+ });
305
+ if (closed)
306
+ throw Error("RTC negotiation ended");
307
+ res.json(answer);
308
+ }
309
+ catch {
310
+ cleanup();
311
+ if (!res.headersSent)
312
+ res.status(503).json({ error: "rtc_negotiation_failed" });
313
+ }
314
+ });
315
+ return router;
316
+ }
@@ -13,7 +13,7 @@ export function createRuntimeApiRouter(application, eventBus, runtimeStatus) {
13
13
  throw new ChatRoomError("INVALID_INPUT", "enabled must be a boolean");
14
14
  res.json(application.mcpTools.setEnabled(requireString(req.params.toolName, "toolName"), body.enabled));
15
15
  });
16
- router.get("/events", (req, res) => {
16
+ router.get("/events", (_req, res) => {
17
17
  res.status(200);
18
18
  res.setHeader("Content-Type", "text/event-stream");
19
19
  res.setHeader("Cache-Control", "no-cache, no-transform");
@@ -25,7 +25,7 @@ export function createRuntimeApiRouter(application, eventBus, runtimeStatus) {
25
25
  });
26
26
  const keepalive = setInterval(() => res.write(": keepalive\n\n"), 15000);
27
27
  keepalive.unref();
28
- req.on("close", () => {
28
+ res.on("close", () => {
29
29
  clearInterval(keepalive);
30
30
  unsubscribe();
31
31
  });
@@ -0,0 +1,4 @@
1
+ import { Router } from "express";
2
+ import type { WebRuntime } from "#plugins/web/runtime";
3
+ /** PTY I/O over the existing V2 HTTP stream transport; no extra socket/protocol. */
4
+ export declare function createTerminalApiRouter(app: WebRuntime): Router;
@@ -0,0 +1,132 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import { Router } from "express";
4
+ import { ChatRoomError } from "#core/errors/chatroom-error";
5
+ import { asyncRoute, bodyRecord, requireString, } from "#presentation/http/http-utils";
6
+ import { defaultTerminalShell, defaultTerminalArgs, } from "#plugins/process/infrastructure/terminal-shell";
7
+ const MAX_RUNNING = 8;
8
+ const MAX_SESSIONS = 24;
9
+ const MAX_INPUT_BYTES = 16 * 1024;
10
+ const MAX_BACKLOG = 512 * 1024;
11
+ const validSize = (n) => Number.isInteger(n) && n >= 2 && n <= 500;
12
+ /** PTY I/O over the existing V2 HTTP stream transport; no extra socket/protocol. */
13
+ export function createTerminalApiRouter(app) {
14
+ const router = Router();
15
+ const sessions = new Map();
16
+ const describe = (s) => ({
17
+ ...s,
18
+ state: app.processes.read(s.processId).state,
19
+ });
20
+ function requireSession(id) {
21
+ const session = sessions.get(id);
22
+ if (!session)
23
+ throw new ChatRoomError("NOT_FOUND", "Terminal not found");
24
+ return session;
25
+ }
26
+ router.get("/terminal/sessions", (_req, res) => {
27
+ res.json([...sessions.values()].reverse().map(describe));
28
+ });
29
+ router.post("/terminal/sessions", asyncRoute(async (req, res) => {
30
+ const body = bodyRecord(req.body);
31
+ const cols = body.cols ?? 80;
32
+ const rows = body.rows ?? 24;
33
+ if (!validSize(cols) || !validSize(rows))
34
+ throw new ChatRoomError("INVALID_INPUT", "Invalid terminal dimensions");
35
+ const running = [...sessions.values()].filter((s) => app.processes.read(s.processId).state === "running").length;
36
+ if (running >= MAX_RUNNING || sessions.size >= MAX_SESSIONS)
37
+ throw new ChatRoomError("CONFLICT", "Too many terminal sessions");
38
+ const shell = defaultTerminalShell();
39
+ const snapshot = await app.processes.start({
40
+ command: shell,
41
+ args: defaultTerminalArgs(),
42
+ cwd: homedir(),
43
+ pty: true,
44
+ cols,
45
+ rows,
46
+ env: {
47
+ ...process.env,
48
+ TERM: "xterm-256color",
49
+ COLORTERM: "truecolor",
50
+ },
51
+ timeoutMs: 0,
52
+ }, { source: "gui", action: "start" });
53
+ const session = {
54
+ id: randomUUID(),
55
+ processId: snapshot.processId,
56
+ shell,
57
+ createdAt: snapshot.startedAt,
58
+ };
59
+ sessions.set(session.id, session);
60
+ res.status(201).json(describe(session));
61
+ }));
62
+ router.post("/terminal/sessions/:id/input", (req, res) => {
63
+ const session = requireSession(requireString(req.params.id, "id"));
64
+ const body = bodyRecord(req.body);
65
+ const data = requireString(body.data, "data");
66
+ if (Buffer.byteLength(data, "utf8") > MAX_INPUT_BYTES)
67
+ throw new ChatRoomError("INVALID_INPUT", "Terminal input too large");
68
+ app.processes.write(session.processId, data);
69
+ res.status(204).end();
70
+ });
71
+ router.post("/terminal/sessions/:id/resize", (req, res) => {
72
+ const session = requireSession(requireString(req.params.id, "id"));
73
+ const body = bodyRecord(req.body);
74
+ if (!validSize(body.cols) || !validSize(body.rows))
75
+ throw new ChatRoomError("INVALID_INPUT", "Invalid terminal dimensions");
76
+ app.processes.resize(session.processId, body.cols, body.rows);
77
+ res.status(204).end();
78
+ });
79
+ router.delete("/terminal/sessions/:id", asyncRoute(async (req, res) => {
80
+ const session = requireSession(requireString(req.params.id, "id"));
81
+ if (app.processes.read(session.processId).state === "running")
82
+ await app.processKill(session.processId, false);
83
+ sessions.delete(session.id);
84
+ res.status(204).end();
85
+ }));
86
+ router.get("/terminal/sessions/:id/stream", (req, res) => {
87
+ const session = requireSession(requireString(req.params.id, "id"));
88
+ res.status(200);
89
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
90
+ res.setHeader("Cache-Control", "private, no-store, no-transform");
91
+ res.setHeader("X-Accel-Buffering", "no");
92
+ res.flushHeaders();
93
+ let finished = false;
94
+ const write = (type, payload) => {
95
+ if (finished || res.destroyed)
96
+ return;
97
+ if (res.writableLength > MAX_BACKLOG) {
98
+ res.destroy(new Error("Terminal stream is too slow"));
99
+ return;
100
+ }
101
+ res.write("event: " + type + "\n" + "data: " + JSON.stringify(payload) + "\n\n");
102
+ };
103
+ // Attach before taking snapshot: no output gap between backlog and live bytes.
104
+ const { snapshot, unsubscribe } = app.processes.subscribeOutput(session.processId, (chunk) => {
105
+ if (!chunk) {
106
+ write("exit", describe(session));
107
+ res.end();
108
+ return;
109
+ }
110
+ for (let offset = 0; offset < chunk.length; offset += 16 * 1024)
111
+ write("output", chunk.subarray(offset, offset + 16 * 1024).toString("base64"));
112
+ });
113
+ write("snapshot", {
114
+ output: Buffer.from(snapshot.stdout, "utf8").toString("base64"),
115
+ state: snapshot.state,
116
+ });
117
+ if (snapshot.state !== "running") {
118
+ write("exit", describe(session));
119
+ unsubscribe();
120
+ res.end();
121
+ return;
122
+ }
123
+ const heartbeat = setInterval(() => write("ping", null), 15000);
124
+ heartbeat.unref();
125
+ res.on("close", () => {
126
+ finished = true;
127
+ clearInterval(heartbeat);
128
+ unsubscribe();
129
+ });
130
+ });
131
+ return router;
132
+ }
@@ -11,7 +11,7 @@ export function createWebPlugin() {
11
11
  id: "web",
12
12
  activate(context) {
13
13
  context.services.provide(WebServiceToken, {
14
- application: new WebRuntime(context.services.require(WorkspaceServiceToken), context.services.require(GitAccessToken), context.operations, context.services.require(ProcessServiceToken), context.services.require(ComputerServiceToken), context.services.require(AgentServiceToken), context.mcpTools),
14
+ application: new WebRuntime(context.services.require(WorkspaceServiceToken), context.services.require(GitAccessToken), context.operations, context.services.require(ProcessServiceToken), context.services.require(ComputerServiceToken), context.services.require(AgentServiceToken), context.mcpTools, context.customMcp),
15
15
  });
16
16
  },
17
17
  };
@@ -4,6 +4,7 @@ import type { GitAccess } from "#app/git-access";
4
4
  import type { ProcessSupervisor } from "#plugins/process/process-supervisor";
5
5
  import type { WorkspaceService } from "#plugins/workspace/workspace-service";
6
6
  import type { AgentRuntime } from "#plugins/agent/runtime";
7
+ import type { CustomMcpManager } from "#mcp/custom/manager";
7
8
  import type { McpToolControl } from "#mcp/server/tool-control";
8
9
  export declare class WebRuntime {
9
10
  readonly workspaces: WorkspaceService;
@@ -13,7 +14,8 @@ export declare class WebRuntime {
13
14
  readonly computer: ComputerService;
14
15
  readonly agents: AgentRuntime;
15
16
  readonly mcpTools: McpToolControl;
16
- constructor(workspaces: WorkspaceService, git: GitAccess, operations: OperationLog, processes: ProcessSupervisor, computer: ComputerService, agents: AgentRuntime, mcpTools: McpToolControl);
17
+ readonly customMcp: CustomMcpManager;
18
+ constructor(workspaces: WorkspaceService, git: GitAccess, operations: OperationLog, processes: ProcessSupervisor, computer: ComputerService, agents: AgentRuntime, mcpTools: McpToolControl, customMcp: CustomMcpManager);
17
19
  processKill(processId: string, force?: boolean): Promise<import("./api-types.js").ProcessSnapshot>;
18
20
  listProcesses(): import("./api-types.js").ProcessSummary[];
19
21
  getProcess(processId: string): import("./api-types.js").ProcessSnapshot;
@@ -6,7 +6,8 @@ export class WebRuntime {
6
6
  computer;
7
7
  agents;
8
8
  mcpTools;
9
- constructor(workspaces, git, operations, processes, computer, agents, mcpTools) {
9
+ customMcp;
10
+ constructor(workspaces, git, operations, processes, computer, agents, mcpTools, customMcp) {
10
11
  this.workspaces = workspaces;
11
12
  this.git = git;
12
13
  this.operations = operations;
@@ -14,6 +15,7 @@ export class WebRuntime {
14
15
  this.computer = computer;
15
16
  this.agents = agents;
16
17
  this.mcpTools = mcpTools;
18
+ this.customMcp = customMcp;
17
19
  }
18
20
  processKill(processId, force = false) {
19
21
  return this.operations.run({