@chatroomcp/chatroom 0.1.7 → 0.2.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 (51) hide show
  1. package/dist/app/application.d.ts +1 -0
  2. package/dist/app/application.js +4 -0
  3. package/dist/app/event-bus.d.ts +4 -0
  4. package/dist/auth/ingress-policy.d.ts +2 -0
  5. package/dist/auth/ingress-policy.js +7 -1
  6. package/dist/infrastructure/database/app-database.js +7 -0
  7. package/dist/infrastructure/http/http-server.js +5 -1
  8. package/dist/mcp/server/plugin-mcp-registrar.d.ts +6 -1
  9. package/dist/mcp/server/plugin-mcp-registrar.js +4 -4
  10. package/dist/mcp/server/request-context.d.ts +3 -0
  11. package/dist/mcp/server/request-context.js +8 -0
  12. package/dist/mcp/server/tool-support.d.ts +1 -1
  13. package/dist/mcp/server/tool-support.js +3 -1
  14. package/dist/native/linux/chatroom-computer-helper +0 -0
  15. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/Info.plist +14 -0
  16. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  17. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/_CodeSignature/CodeResources +115 -0
  18. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  19. package/dist/plugins/computer/audit.d.ts +34 -0
  20. package/dist/plugins/computer/audit.js +42 -0
  21. package/dist/plugins/computer/computer-native-backend.d.ts +11 -0
  22. package/dist/plugins/computer/computer-native-backend.js +58 -0
  23. package/dist/plugins/computer/computer-native-host.d.ts +27 -0
  24. package/dist/plugins/computer/computer-native-host.js +335 -0
  25. package/dist/plugins/computer/computer-protocol.d.ts +21 -0
  26. package/dist/plugins/computer/computer-protocol.js +72 -0
  27. package/dist/plugins/computer/computer-schemas.d.ts +318 -0
  28. package/dist/plugins/computer/computer-schemas.js +152 -0
  29. package/dist/plugins/computer/computer-service.d.ts +27 -0
  30. package/dist/plugins/computer/computer-service.js +108 -0
  31. package/dist/plugins/computer/computer-settings-repository.d.ts +8 -0
  32. package/dist/plugins/computer/computer-settings-repository.js +41 -0
  33. package/dist/plugins/computer/mcp.d.ts +3 -0
  34. package/dist/plugins/computer/mcp.js +126 -0
  35. package/dist/plugins/computer/plugin.d.ts +4 -0
  36. package/dist/plugins/computer/plugin.js +29 -0
  37. package/dist/plugins/computer/types.d.ts +174 -0
  38. package/dist/plugins/computer/types.js +1 -0
  39. package/dist/plugins/web/api-types.d.ts +15 -1
  40. package/dist/plugins/web/http/api-router.js +2 -0
  41. package/dist/plugins/web/http/computer-api-router.d.ts +5 -0
  42. package/dist/plugins/web/http/computer-api-router.js +68 -0
  43. package/dist/plugins/web/plugin.js +3 -1
  44. package/dist/plugins/web/runtime.d.ts +3 -1
  45. package/dist/plugins/web/runtime.js +3 -1
  46. package/dist/web/assets/index-B4vO7wVS.css +1 -0
  47. package/dist/web/assets/index-BU7LCgKB.js +26 -0
  48. package/dist/web/index.html +2 -2
  49. package/package.json +6 -3
  50. package/dist/web/assets/index-B7jL3mhD.css +0 -1
  51. package/dist/web/assets/index-L4-YdZVN.js +0 -26
@@ -0,0 +1,335 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { spawn, spawnSync, } from "node:child_process";
3
+ import { chmodSync, existsSync, rmSync } from "node:fs";
4
+ import { createServer } from "node:net";
5
+ import { createInterface } from "node:readline";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { ChatRoomError } from "../../core/errors/chatroom-error.js";
9
+ import { COMPUTER_NATIVE_PROTOCOL_VERSION, nativeError, parseNativeEnvelope, } from "./computer-protocol.js";
10
+ export class ComputerNativeHost {
11
+ child = null;
12
+ socket = null;
13
+ socketServer = null;
14
+ socketPath = null;
15
+ lines = null;
16
+ starting = null;
17
+ pending = new Map();
18
+ sequence = 0;
19
+ get platform() {
20
+ if (process.platform === "darwin")
21
+ return "macos";
22
+ if (process.platform === "win32")
23
+ return "windows";
24
+ if (process.platform === "linux")
25
+ return "linux";
26
+ return "unsupported";
27
+ }
28
+ get idle() {
29
+ return this.pending.size === 0;
30
+ }
31
+ async request(method, params) {
32
+ await this.ensureStarted();
33
+ const id = `computer_${++this.sequence}`;
34
+ const timeoutMs = requestTimeoutMs(method);
35
+ const promise = new Promise((resolve, reject) => {
36
+ const timer = setTimeout(() => {
37
+ this.pending.delete(id);
38
+ const error = new ChatRoomError("INTERNAL", `Computer helper ${method} request timed out after ${timeoutMs} ms`);
39
+ reject(error);
40
+ this.reset(error);
41
+ }, timeoutMs);
42
+ timer.unref();
43
+ this.pending.set(id, { resolve, reject, timer });
44
+ });
45
+ const line = `${JSON.stringify({
46
+ protocol: COMPUTER_NATIVE_PROTOCOL_VERSION,
47
+ id,
48
+ method,
49
+ params,
50
+ })}\n`;
51
+ if (this.platform === "macos")
52
+ this.socket.write(line);
53
+ else
54
+ this.child.stdin.write(line);
55
+ return promise;
56
+ }
57
+ restart(reason = "Computer helper restarting") {
58
+ this.reset(new Error(reason));
59
+ }
60
+ async dispose() {
61
+ this.reset(new Error("Computer helper stopped"));
62
+ }
63
+ async ensureStarted() {
64
+ const platform = this.platform;
65
+ if (platform === "macos" && this.socket && !this.socket.destroyed)
66
+ return;
67
+ if ((platform === "windows" || platform === "linux") &&
68
+ this.child &&
69
+ !this.child.killed)
70
+ return;
71
+ if (this.starting)
72
+ return this.starting;
73
+ this.starting = this.startHelper(platform);
74
+ try {
75
+ await this.starting;
76
+ }
77
+ finally {
78
+ this.starting = null;
79
+ }
80
+ }
81
+ startHelper(platform) {
82
+ switch (platform) {
83
+ case "macos":
84
+ return this.startMacHelper();
85
+ case "windows":
86
+ return this.startPipeHelper(windowsHelperPath(), "Windows");
87
+ case "linux":
88
+ return this.startPipeHelper(linuxHelperPath(), "Linux X11", linuxDesktopEnvironment());
89
+ case "unsupported":
90
+ return Promise.reject(new ChatRoomError("UNSUPPORTED", "Computer Use is supported only on macOS, Windows, and Linux X11"));
91
+ }
92
+ }
93
+ async startMacHelper() {
94
+ const app = macHelperAppPath();
95
+ if (!app)
96
+ throw new ChatRoomError("UNSUPPORTED", "macOS Computer helper is missing");
97
+ this.cleanupSocketPath();
98
+ const socketPath = path.join("/tmp", `chatroom-c-${process.pid}-${randomUUID().slice(0, 8)}.sock`);
99
+ this.socketPath = socketPath;
100
+ await new Promise((resolve, reject) => {
101
+ const server = createServer();
102
+ this.socketServer = server;
103
+ let settled = false;
104
+ const timer = setTimeout(() => fail(new Error("Computer helper connection timed out")), 10_000);
105
+ timer.unref();
106
+ const cleanupServer = () => {
107
+ clearTimeout(timer);
108
+ if (this.socketServer === server)
109
+ this.socketServer = null;
110
+ server.close();
111
+ };
112
+ const fail = (error) => {
113
+ if (settled)
114
+ return;
115
+ settled = true;
116
+ cleanupServer();
117
+ this.cleanupSocketPath();
118
+ reject(error);
119
+ };
120
+ server.once("error", fail);
121
+ server.once("connection", (socket) => {
122
+ if (settled) {
123
+ socket.destroy();
124
+ return;
125
+ }
126
+ settled = true;
127
+ server.removeListener("error", fail);
128
+ cleanupServer();
129
+ this.attachSocket(socket);
130
+ resolve();
131
+ });
132
+ server.listen(socketPath, () => {
133
+ chmodSync(socketPath, 0o600);
134
+ const launcher = spawn("/usr/bin/open", ["-n", "-g", app, "--args", "--connect", socketPath], { stdio: "ignore" });
135
+ launcher.once("error", fail);
136
+ });
137
+ });
138
+ }
139
+ attachSocket(socket) {
140
+ this.socket = socket;
141
+ this.lines = createInterface({ input: socket });
142
+ this.lines.on("line", (line) => this.handleLine(line));
143
+ socket.once("error", (error) => this.failTransport(socket, error));
144
+ socket.once("close", () => this.failTransport(socket, new Error("Computer helper exited")));
145
+ }
146
+ async startPipeHelper(executable, platformName, env = process.env) {
147
+ if (!executable)
148
+ throw new ChatRoomError("UNSUPPORTED", `${platformName} Computer helper is missing`);
149
+ const child = spawn(executable, [], {
150
+ stdio: ["pipe", "pipe", "pipe"],
151
+ windowsHide: process.platform === "win32",
152
+ env,
153
+ });
154
+ this.child = child;
155
+ this.lines = createInterface({ input: child.stdout });
156
+ this.lines.on("line", (line) => this.handleLine(line));
157
+ child.stderr.on("data", (chunk) => console.error("[computer-helper]", String(chunk).trimEnd()));
158
+ child.once("error", (error) => this.failTransport(child, error));
159
+ child.once("exit", () => this.failTransport(child, new Error("Computer helper exited")));
160
+ }
161
+ failTransport(transport, error) {
162
+ const isCurrentSocket = transport === this.socket;
163
+ const isCurrentChild = transport === this.child;
164
+ if (!isCurrentSocket && !isCurrentChild)
165
+ return;
166
+ if (isCurrentSocket)
167
+ this.socket = null;
168
+ if (isCurrentChild)
169
+ this.child = null;
170
+ this.lines?.close();
171
+ this.lines = null;
172
+ this.cleanupSocketPath();
173
+ this.rejectPending(error);
174
+ }
175
+ reset(error) {
176
+ this.lines?.close();
177
+ this.lines = null;
178
+ this.socket?.destroy();
179
+ this.socket = null;
180
+ this.socketServer?.close();
181
+ this.socketServer = null;
182
+ this.child?.kill();
183
+ this.child = null;
184
+ this.cleanupSocketPath();
185
+ this.rejectPending(error);
186
+ }
187
+ rejectPending(error) {
188
+ for (const item of this.pending.values()) {
189
+ clearTimeout(item.timer);
190
+ item.reject(error);
191
+ }
192
+ this.pending.clear();
193
+ }
194
+ cleanupSocketPath() {
195
+ if (!this.socketPath)
196
+ return;
197
+ rmSync(this.socketPath, { force: true });
198
+ this.socketPath = null;
199
+ }
200
+ handleLine(line) {
201
+ let raw;
202
+ try {
203
+ raw = JSON.parse(line);
204
+ }
205
+ catch {
206
+ return;
207
+ }
208
+ const rawId = raw && typeof raw === "object" && "id" in raw
209
+ ? raw.id
210
+ : undefined;
211
+ if (typeof rawId !== "string")
212
+ return;
213
+ const pending = this.pending.get(rawId);
214
+ if (!pending)
215
+ return;
216
+ let message;
217
+ try {
218
+ message = parseNativeEnvelope(raw);
219
+ }
220
+ catch (error) {
221
+ this.pending.delete(rawId);
222
+ clearTimeout(pending.timer);
223
+ pending.reject(error);
224
+ return;
225
+ }
226
+ this.pending.delete(message.id);
227
+ clearTimeout(pending.timer);
228
+ if (message.error)
229
+ pending.reject(nativeError(message.error));
230
+ else
231
+ pending.resolve(message.result);
232
+ }
233
+ }
234
+ function requestTimeoutMs(method) {
235
+ switch (method) {
236
+ case "status":
237
+ return 5_000;
238
+ case "snapshot":
239
+ return 15_000;
240
+ case "requestPermission":
241
+ return 30_000;
242
+ case "action":
243
+ return 60_000;
244
+ }
245
+ }
246
+ function projectRoot() {
247
+ const current = fileURLToPath(import.meta.url);
248
+ return path.resolve(path.dirname(current), "../../..");
249
+ }
250
+ function macHelperAppPath() {
251
+ return nativeHelperPath("macos", "ChatRoomComputerHelper.app");
252
+ }
253
+ function windowsHelperPath() {
254
+ return nativeHelperPath("windows", "chatroom-computer-helper.exe");
255
+ }
256
+ function linuxHelperPath() {
257
+ return nativeHelperPath("linux", "chatroom-computer-helper");
258
+ }
259
+ function nativeHelperPath(...parts) {
260
+ const target = path.join(projectRoot(), "dist", "native", ...parts);
261
+ return existsSync(target) ? target : null;
262
+ }
263
+ function linuxDesktopEnvironment() {
264
+ const env = { ...process.env };
265
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
266
+ if (!env.XDG_RUNTIME_DIR && uid !== null) {
267
+ const runtimeDir = `/run/user/${uid}`;
268
+ if (existsSync(runtimeDir))
269
+ env.XDG_RUNTIME_DIR = runtimeDir;
270
+ }
271
+ if (!env.DISPLAY) {
272
+ const display = activeX11Display(uid);
273
+ if (display) {
274
+ env.DISPLAY = display;
275
+ env.XDG_SESSION_TYPE = "x11";
276
+ }
277
+ }
278
+ if (!env.XAUTHORITY) {
279
+ const candidates = [
280
+ env.HOME ? path.join(env.HOME, ".Xauthority") : null,
281
+ env.XDG_RUNTIME_DIR
282
+ ? path.join(env.XDG_RUNTIME_DIR, "gdm", "Xauthority")
283
+ : null,
284
+ ];
285
+ const authority = candidates.find((candidate) => !!candidate && existsSync(candidate));
286
+ if (authority)
287
+ env.XAUTHORITY = authority;
288
+ }
289
+ if (!env.DBUS_SESSION_BUS_ADDRESS && env.XDG_RUNTIME_DIR) {
290
+ const bus = path.join(env.XDG_RUNTIME_DIR, "bus");
291
+ if (existsSync(bus))
292
+ env.DBUS_SESSION_BUS_ADDRESS = `unix:path=${bus}`;
293
+ }
294
+ return env;
295
+ }
296
+ function activeX11Display(uid) {
297
+ if (process.platform !== "linux" || uid === null)
298
+ return null;
299
+ const loginctl = existsSync("/usr/bin/loginctl")
300
+ ? "/usr/bin/loginctl"
301
+ : "loginctl";
302
+ const sessions = spawnSync(loginctl, ["list-sessions", "--no-legend", "--no-pager"], {
303
+ encoding: "utf8",
304
+ timeout: 2_000,
305
+ });
306
+ if (sessions.status !== 0 || !sessions.stdout)
307
+ return null;
308
+ const userId = String(uid);
309
+ for (const line of sessions.stdout.split("\n")) {
310
+ const [sessionId, sessionUid] = line.trim().split(/\s+/, 3);
311
+ if (!sessionId || sessionUid !== userId)
312
+ continue;
313
+ const details = spawnSync(loginctl, [
314
+ "show-session",
315
+ sessionId,
316
+ "--no-pager",
317
+ "-p",
318
+ "Type",
319
+ "-p",
320
+ "Active",
321
+ "-p",
322
+ "Display",
323
+ ], { encoding: "utf8", timeout: 2_000 });
324
+ if (details.status !== 0 || !details.stdout)
325
+ continue;
326
+ const values = Object.fromEntries(details.stdout
327
+ .split("\n")
328
+ .map((item) => item.split("=", 2))
329
+ .filter((item) => item.length === 2));
330
+ if (values.Type === "x11" && values.Active === "yes" && values.Display) {
331
+ return values.Display;
332
+ }
333
+ }
334
+ return null;
335
+ }
@@ -0,0 +1,21 @@
1
+ import { ChatRoomError } from "../../core/errors/chatroom-error.js";
2
+ import type { ComputerActionResult, ComputerSnapshot, ComputerStatus } from "./types.js";
3
+ export declare const COMPUTER_NATIVE_PROTOCOL_VERSION = 1;
4
+ export type ComputerNativeMethod = "status" | "requestPermission" | "snapshot" | "action";
5
+ export interface ComputerNativeResultMap {
6
+ status: Omit<ComputerStatus, "settings">;
7
+ requestPermission: Omit<ComputerStatus, "settings">;
8
+ snapshot: ComputerSnapshot;
9
+ action: ComputerActionResult;
10
+ }
11
+ export interface ComputerNativeError {
12
+ code?: string;
13
+ message: string;
14
+ }
15
+ export declare function parseNativeEnvelope(value: unknown): {
16
+ id: string;
17
+ result?: unknown;
18
+ error?: ComputerNativeError;
19
+ };
20
+ export declare function parseNativeResult<M extends ComputerNativeMethod>(method: M, value: unknown): ComputerNativeResultMap[M];
21
+ export declare function nativeError(error: ComputerNativeError): ChatRoomError;
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ import { ChatRoomError, } from "../../core/errors/chatroom-error.js";
3
+ import { computerActionResultSchema, computerNativeStatusSchema, computerSnapshotSchema, } from "./computer-schemas.js";
4
+ export const COMPUTER_NATIVE_PROTOCOL_VERSION = 1;
5
+ const responseEnvelopeSchema = z
6
+ .object({
7
+ protocol: z.literal(COMPUTER_NATIVE_PROTOCOL_VERSION).optional(),
8
+ id: z.string(),
9
+ result: z.unknown().optional(),
10
+ error: z
11
+ .object({
12
+ code: z.string().optional(),
13
+ message: z.string(),
14
+ })
15
+ .optional(),
16
+ })
17
+ .refine((value) => value.result !== undefined || value.error !== undefined, {
18
+ message: "Native response must contain result or error",
19
+ });
20
+ export function parseNativeEnvelope(value) {
21
+ const parsed = responseEnvelopeSchema.safeParse(value);
22
+ if (!parsed.success) {
23
+ throw new ChatRoomError("INTERNAL", "Computer helper returned an invalid protocol response", { issues: parsed.error.issues });
24
+ }
25
+ return {
26
+ id: parsed.data.id,
27
+ ...(parsed.data.result === undefined ? {} : { result: parsed.data.result }),
28
+ ...(parsed.data.error === undefined
29
+ ? {}
30
+ : {
31
+ error: {
32
+ message: parsed.data.error.message,
33
+ ...(parsed.data.error.code === undefined
34
+ ? {}
35
+ : { code: parsed.data.error.code }),
36
+ },
37
+ }),
38
+ };
39
+ }
40
+ export function parseNativeResult(method, value) {
41
+ const schema = method === "status" || method === "requestPermission"
42
+ ? computerNativeStatusSchema
43
+ : method === "snapshot"
44
+ ? computerSnapshotSchema
45
+ : computerActionResultSchema;
46
+ const parsed = schema.safeParse(value);
47
+ if (!parsed.success) {
48
+ throw new ChatRoomError("INTERNAL", `Computer helper returned an invalid ${method} result`, { issues: parsed.error.issues });
49
+ }
50
+ return parsed.data;
51
+ }
52
+ export function nativeError(error) {
53
+ return new ChatRoomError(nativeErrorCode(error.code), error.message, {
54
+ nativeCode: error.code ?? null,
55
+ });
56
+ }
57
+ function nativeErrorCode(code) {
58
+ switch (code) {
59
+ case "invalid_request":
60
+ return "INVALID_INPUT";
61
+ case "not_found":
62
+ return "NOT_FOUND";
63
+ case "permission_required":
64
+ return "FORBIDDEN";
65
+ case "stale_snapshot":
66
+ return "CONFLICT";
67
+ case "unsupported":
68
+ return "UNSUPPORTED";
69
+ default:
70
+ return "INTERNAL";
71
+ }
72
+ }