@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,126 @@
1
+ import { z } from "zod";
2
+ import { closedRead, openWorldMutation, } from "../../mcp/server/tool-support.js";
3
+ import { currentMcpAccessScope } from "../../mcp/server/request-context.js";
4
+ import { auditActionOutput, auditComputerActions, auditSnapshotOutput, } from "./audit.js";
5
+ import { computerActionMetadataSchema, computerActionSchema, computerSnapshotMetadataSchema, computerSnapshotTargetSchema, } from "./computer-schemas.js";
6
+ export function registerComputerTools(mcp, service) {
7
+ mcp.registerTool("computer_snapshot", {
8
+ title: "Observe computer",
9
+ description: "Observe the current desktop using a screenshot and compact accessibility elements. Element IDs are valid only for the returned snapshotId.",
10
+ inputSchema: z.object({
11
+ target: computerSnapshotTargetSchema.optional(),
12
+ includeScreenshot: z.boolean().default(true),
13
+ includeElements: z.boolean().default(true),
14
+ }),
15
+ outputSchema: computerSnapshotMetadataSchema,
16
+ annotations: closedRead,
17
+ action: "snapshot",
18
+ audit: {
19
+ output: (value) => auditSnapshotOutput(value),
20
+ },
21
+ present: presentSnapshot,
22
+ }, (input) => service.snapshot(currentMcpAccessScope(), {
23
+ includeScreenshot: input.includeScreenshot,
24
+ includeElements: input.includeElements,
25
+ ...(input.target === undefined
26
+ ? {}
27
+ : { target: normalizeSnapshotTarget(input.target) }),
28
+ }));
29
+ mcp.registerTool("computer_action", {
30
+ title: "Control computer",
31
+ description: "Execute a bounded batch of semantic or coordinate desktop actions. Prefer elementId from the latest snapshot; elementId actions require that snapshotId.",
32
+ inputSchema: z.object({
33
+ snapshotId: z.string().optional(),
34
+ actions: z.array(computerActionSchema).min(1).max(50),
35
+ observeAfter: z.boolean().default(true),
36
+ }),
37
+ outputSchema: computerActionMetadataSchema,
38
+ annotations: openWorldMutation,
39
+ action: "action",
40
+ audit: {
41
+ input: (value) => auditComputerActions(value),
42
+ output: (value) => auditActionOutput(value),
43
+ },
44
+ present: presentAction,
45
+ }, (input) => service.action(currentMcpAccessScope(), {
46
+ actions: input.actions,
47
+ observeAfter: input.observeAfter,
48
+ ...(input.snapshotId === undefined
49
+ ? {}
50
+ : { snapshotId: input.snapshotId }),
51
+ }));
52
+ }
53
+ function presentSnapshot(value) {
54
+ const snapshot = value;
55
+ const { screenshot, ...metadata } = snapshot;
56
+ return {
57
+ content: [
58
+ { type: "text", text: JSON.stringify(metadata, null, 2) },
59
+ ...(screenshot
60
+ ? [
61
+ {
62
+ type: "image",
63
+ data: screenshot.data,
64
+ mimeType: screenshot.mimeType,
65
+ },
66
+ ]
67
+ : []),
68
+ ],
69
+ structuredContent: metadata,
70
+ };
71
+ }
72
+ function presentAction(value) {
73
+ const result = value;
74
+ const screenshot = result.snapshot?.screenshot;
75
+ const snapshot = result.snapshot
76
+ ? (({ screenshot: _screenshot, ...metadata }) => metadata)(result.snapshot)
77
+ : undefined;
78
+ const safe = {
79
+ success: result.success,
80
+ revision: result.revision,
81
+ executionMode: result.executionMode,
82
+ focusChanged: result.focusChanged,
83
+ ...(snapshot === undefined ? {} : { snapshot }),
84
+ };
85
+ return {
86
+ content: [
87
+ { type: "text", text: JSON.stringify(safe, null, 2) },
88
+ ...(screenshot
89
+ ? [
90
+ {
91
+ type: "image",
92
+ data: screenshot.data,
93
+ mimeType: screenshot.mimeType,
94
+ },
95
+ ]
96
+ : []),
97
+ ],
98
+ structuredContent: safe,
99
+ };
100
+ }
101
+ function normalizeSnapshotTarget(input) {
102
+ switch (input.type) {
103
+ case "desktop":
104
+ return { type: "desktop" };
105
+ case "display":
106
+ return input.displayId === undefined
107
+ ? { type: "display" }
108
+ : { type: "display", displayId: input.displayId };
109
+ case "app":
110
+ return { type: "app", app: input.app };
111
+ case "window":
112
+ return { type: "window", elementId: input.elementId };
113
+ case "region": {
114
+ const region = {
115
+ type: "region",
116
+ x: input.x,
117
+ y: input.y,
118
+ width: input.width,
119
+ height: input.height,
120
+ };
121
+ return input.displayId === undefined
122
+ ? region
123
+ : { ...region, displayId: input.displayId };
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,4 @@
1
+ import type { InternalPlugin } from "../types.js";
2
+ import { ComputerService } from "./computer-service.js";
3
+ export declare const ComputerServiceToken: import("../types.js").ServiceToken<ComputerService>;
4
+ export declare function createComputerPlugin(): InternalPlugin;
@@ -0,0 +1,29 @@
1
+ import { createServiceToken } from "../types.js";
2
+ import { NativeComputerBackend } from "./computer-native-backend.js";
3
+ import { ComputerService } from "./computer-service.js";
4
+ import { ComputerSettingsRepository } from "./computer-settings-repository.js";
5
+ import { registerComputerTools } from "./mcp.js";
6
+ import { currentMcpAccessScope } from "../../mcp/server/request-context.js";
7
+ export const ComputerServiceToken = createServiceToken("computer");
8
+ export function createComputerPlugin() {
9
+ let service = null;
10
+ return {
11
+ id: "computer",
12
+ activate(context) {
13
+ service = new ComputerService(new NativeComputerBackend(), new ComputerSettingsRepository(context.database), context.events);
14
+ context.services.provide(ComputerServiceToken, service);
15
+ },
16
+ registerMcp(mcp) {
17
+ if (!service)
18
+ throw new Error("Computer plugin is not active");
19
+ if (currentMcpAccessScope() === "remote" &&
20
+ !service.settings().remoteAccess)
21
+ return;
22
+ registerComputerTools(mcp, service);
23
+ },
24
+ async deactivate() {
25
+ await service?.shutdown();
26
+ service = null;
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,174 @@
1
+ export type ComputerAccessScope = "local" | "remote";
2
+ export type ComputerPlatform = "macos" | "windows" | "linux" | "unsupported";
3
+ export type ComputerHelperState = "running" | "stopped" | "unavailable";
4
+ export type ComputerPermissionState = "granted" | "denied" | "unknown" | "not-required";
5
+ export type ComputerPermission = "accessibility" | "screenRecording";
6
+ export interface ComputerSettings {
7
+ enabled: boolean;
8
+ remoteAccess: boolean;
9
+ updatedAt: string;
10
+ }
11
+ export interface ComputerPermissions {
12
+ accessibility: ComputerPermissionState;
13
+ screenRecording: ComputerPermissionState;
14
+ }
15
+ export interface ComputerDisplay {
16
+ id: string;
17
+ name: string;
18
+ width: number;
19
+ height: number;
20
+ scale: number;
21
+ primary: boolean;
22
+ }
23
+ export interface ComputerStatus {
24
+ platform: ComputerPlatform;
25
+ helper: ComputerHelperState;
26
+ permissions: ComputerPermissions;
27
+ displays: ComputerDisplay[];
28
+ settings: ComputerSettings;
29
+ }
30
+ export interface ComputerElement {
31
+ id: number;
32
+ role: string;
33
+ name: string | null;
34
+ value: string | null;
35
+ enabled: boolean;
36
+ focused: boolean;
37
+ selected: boolean;
38
+ sensitive: boolean;
39
+ bounds: [number, number, number, number] | null;
40
+ actions: string[];
41
+ }
42
+ export type ComputerSnapshotTarget = {
43
+ type: "desktop";
44
+ } | {
45
+ type: "display";
46
+ displayId?: string;
47
+ } | {
48
+ type: "app";
49
+ app: string;
50
+ } | {
51
+ type: "window";
52
+ elementId: number;
53
+ } | {
54
+ type: "region";
55
+ displayId?: string;
56
+ x: number;
57
+ y: number;
58
+ width: number;
59
+ height: number;
60
+ };
61
+ export interface ComputerSnapshotRequest {
62
+ target?: ComputerSnapshotTarget;
63
+ includeScreenshot: boolean;
64
+ includeElements: boolean;
65
+ }
66
+ export interface ComputerSnapshot {
67
+ snapshotId: string;
68
+ revision: number;
69
+ display: ComputerDisplay | null;
70
+ activeApp: string | null;
71
+ activeWindow: string | null;
72
+ cursor: {
73
+ x: number;
74
+ y: number;
75
+ } | null;
76
+ elements: ComputerElement[];
77
+ screenshot?: {
78
+ mimeType: "image/jpeg" | "image/png";
79
+ data: string;
80
+ };
81
+ }
82
+ export type ComputerAction = {
83
+ type: "move";
84
+ x: number;
85
+ y: number;
86
+ } | {
87
+ type: "click";
88
+ x?: number;
89
+ y?: number;
90
+ elementId?: number;
91
+ } | {
92
+ type: "double_click";
93
+ x?: number;
94
+ y?: number;
95
+ elementId?: number;
96
+ } | {
97
+ type: "right_click";
98
+ x?: number;
99
+ y?: number;
100
+ elementId?: number;
101
+ } | {
102
+ type: "drag";
103
+ from: {
104
+ x: number;
105
+ y: number;
106
+ };
107
+ to: {
108
+ x: number;
109
+ y: number;
110
+ };
111
+ durationMs?: number;
112
+ } | {
113
+ type: "scroll";
114
+ deltaX?: number;
115
+ deltaY: number;
116
+ elementId?: number;
117
+ } | {
118
+ type: "keypress";
119
+ keys: string[];
120
+ } | {
121
+ type: "type_text";
122
+ text: string;
123
+ elementId?: number;
124
+ } | {
125
+ type: "invoke";
126
+ elementId: number;
127
+ } | {
128
+ type: "set_value";
129
+ elementId: number;
130
+ value: string;
131
+ } | {
132
+ type: "select_text";
133
+ elementId: number;
134
+ start: number;
135
+ length: number;
136
+ } | {
137
+ type: "activate_app";
138
+ app: string;
139
+ } | {
140
+ type: "activate_window";
141
+ elementId: number;
142
+ } | {
143
+ type: "move_window";
144
+ elementId: number;
145
+ x: number;
146
+ y: number;
147
+ } | {
148
+ type: "resize_window";
149
+ elementId: number;
150
+ width: number;
151
+ height: number;
152
+ } | {
153
+ type: "wait";
154
+ ms: number;
155
+ };
156
+ export interface ComputerActionRequest {
157
+ snapshotId?: string;
158
+ actions: ComputerAction[];
159
+ observeAfter: boolean;
160
+ }
161
+ export interface ComputerActionResult {
162
+ success: true;
163
+ revision: number;
164
+ snapshot?: ComputerSnapshot;
165
+ executionMode: "semantic" | "background" | "foreground" | "mixed";
166
+ focusChanged: boolean;
167
+ }
168
+ export interface ComputerBackend {
169
+ status(): Promise<Omit<ComputerStatus, "settings">>;
170
+ requestPermission(permission: ComputerPermission): Promise<Omit<ComputerStatus, "settings">>;
171
+ snapshot(request: ComputerSnapshotRequest, revision: number): Promise<ComputerSnapshot>;
172
+ action(request: ComputerActionRequest, revision: number): Promise<ComputerActionResult>;
173
+ dispose(): Promise<void>;
174
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,9 +1,23 @@
1
1
  import type { Operation as DomainOperation } from "../../core/operations/types.js";
2
2
  import type { GitInfo } from "../workspace/domain/git.js";
3
3
  import type { ProcessSnapshot } from "../process/types.js";
4
+ import type { ComputerDisplay, ComputerPermission, ComputerSnapshot, ComputerStatus } from "../computer/types.js";
4
5
  import type { Workspace } from "../workspace/domain/workspace.js";
5
6
  import type { WorktreeApplyPreview, WorktreeFileDiff } from "../workspace/domain/review.js";
6
- export type { ProcessSnapshot, WorktreeApplyPreview, WorktreeFileDiff };
7
+ export type { ComputerDisplay, ComputerPermission, ComputerStatus, ProcessSnapshot, WorktreeApplyPreview, WorktreeFileDiff, };
8
+ export interface ComputerPreviewView {
9
+ snapshotId: string;
10
+ revision: number;
11
+ display: ComputerDisplay | null;
12
+ activeApp: string | null;
13
+ activeWindow: string | null;
14
+ cursor: {
15
+ x: number;
16
+ y: number;
17
+ } | null;
18
+ elementCount: number;
19
+ screenshot: ComputerSnapshot["screenshot"] | null;
20
+ }
7
21
  export type Operation = DomainOperation;
8
22
  export interface WorkspaceView extends Workspace {
9
23
  git?: GitInfo | null;
@@ -5,6 +5,7 @@ import { asyncRoute, parseCookie, requireString, } from "../../../presentation/h
5
5
  import { createProcessApiRouter } from "./process-api-router.js";
6
6
  import { createWorkspaceApiRouter } from "./workspace-api-router.js";
7
7
  import { createCloudApiRouter } from "./cloud-api-router.js";
8
+ import { createComputerApiRouter } from "./computer-api-router.js";
8
9
  const SESSION_COOKIE = "chatroom_session";
9
10
  export function createApiRouter(application, eventBus, auth, passkeys, ingress, cloud, runtimeStatus) {
10
11
  const router = Router();
@@ -97,6 +98,7 @@ export function createApiRouter(application, eventBus, auth, passkeys, ingress,
97
98
  });
98
99
  router.use(createWorkspaceApiRouter(application));
99
100
  router.use(createProcessApiRouter(application));
101
+ router.use(createComputerApiRouter(application.computer, application.operations, ingress));
100
102
  router.use(createCloudApiRouter(cloud, application.operations));
101
103
  router.get("/events", (req, res) => {
102
104
  res.status(200);
@@ -0,0 +1,5 @@
1
+ import { Router } from "express";
2
+ import type { ComputerService } from "../../computer/computer-service.js";
3
+ import type { OperationLog } from "../../../operations/operation-log.js";
4
+ import type { IngressPolicy } from "../../../auth/ingress-policy.js";
5
+ export declare function createComputerApiRouter(computer: ComputerService, operations: OperationLog, ingress: IngressPolicy): Router;
@@ -0,0 +1,68 @@
1
+ import { Router } from "express";
2
+ import { z } from "zod";
3
+ import { asyncRoute } from "../../../presentation/http/http-utils.js";
4
+ import { ChatRoomError } from "../../../core/errors/chatroom-error.js";
5
+ const settingsPatchSchema = z
6
+ .object({
7
+ enabled: z.boolean().optional(),
8
+ remoteAccess: z.boolean().optional(),
9
+ })
10
+ .strict();
11
+ export function createComputerApiRouter(computer, operations, ingress) {
12
+ const router = Router();
13
+ router.get("/computer/status", asyncRoute(async (_req, res) => res.json(await computer.status())));
14
+ router.post("/computer/permissions/accessibility/request", asyncRoute(async (req, res) => {
15
+ assertLocalPermissionRequest(req);
16
+ res.json(await computer.requestPermission("accessibility"));
17
+ }));
18
+ router.post("/computer/permissions/screen-recording/request", asyncRoute(async (req, res) => {
19
+ assertLocalPermissionRequest(req);
20
+ res.json(await computer.requestPermission("screenRecording"));
21
+ }));
22
+ function assertLocalPermissionRequest(req) {
23
+ if (ingress.isExternalWeb(req))
24
+ throw new ChatRoomError("FORBIDDEN", "Computer permissions can be requested only from the local WebUI");
25
+ }
26
+ router.patch("/computer/settings", asyncRoute(async (req, res) => {
27
+ const parsed = settingsPatchSchema.parse(req.body);
28
+ const patch = {
29
+ ...(parsed.enabled === undefined ? {} : { enabled: parsed.enabled }),
30
+ ...(parsed.remoteAccess === undefined
31
+ ? {}
32
+ : { remoteAccess: parsed.remoteAccess }),
33
+ };
34
+ res.json(await operations.run({
35
+ pluginId: "computer",
36
+ source: "gui",
37
+ action: "settings.set",
38
+ input: patch,
39
+ }, async () => computer.setSettings(patch)));
40
+ }));
41
+ router.get("/computer/preview", (req, res) => {
42
+ const scope = ingress.isExternalWeb(req) ? "remote" : "local";
43
+ res.json(presentPreview(computer.latestSnapshot(scope)));
44
+ });
45
+ router.post("/computer/snapshot", asyncRoute(async (req, res) => {
46
+ const scope = ingress.isExternalWeb(req) ? "remote" : "local";
47
+ const value = await computer.snapshot(scope, {
48
+ includeScreenshot: true,
49
+ includeElements: true,
50
+ });
51
+ res.json(presentPreview(value));
52
+ }));
53
+ return router;
54
+ }
55
+ function presentPreview(value) {
56
+ if (!value)
57
+ return null;
58
+ return {
59
+ snapshotId: value.snapshotId,
60
+ revision: value.revision,
61
+ display: value.display,
62
+ activeApp: value.activeApp,
63
+ activeWindow: value.activeWindow,
64
+ cursor: value.cursor,
65
+ elementCount: value.elements.length,
66
+ screenshot: value.screenshot ?? null,
67
+ };
68
+ }
@@ -1,6 +1,7 @@
1
1
  import { createServiceToken } from "../types.js";
2
2
  import { WorkspaceService } from "../workspace/plugin.js";
3
3
  import { ProcessService } from "../process/plugin.js";
4
+ import { ComputerServiceToken } from "../computer/plugin.js";
4
5
  import { WebRuntime } from "./runtime.js";
5
6
  export const WebServiceToken = createServiceToken("web");
6
7
  export function createWebPlugin() {
@@ -9,8 +10,9 @@ export function createWebPlugin() {
9
10
  activate(context) {
10
11
  const workspace = context.services.require(WorkspaceService);
11
12
  const processes = context.services.require(ProcessService);
13
+ const computer = context.services.require(ComputerServiceToken);
12
14
  context.services.provide(WebServiceToken, {
13
- application: new WebRuntime(workspace.workspaces, context.operations, processes, workspace.git),
15
+ application: new WebRuntime(workspace.workspaces, context.operations, processes, workspace.git, computer),
14
16
  });
15
17
  },
16
18
  };
@@ -2,12 +2,14 @@ import type { OperationLog } from "../../operations/operation-log.js";
2
2
  import type { WorkspaceService } from "../workspace/workspace-service.js";
3
3
  import type { ProcessSupervisor } from "../process/process-supervisor.js";
4
4
  import type { GitService } from "../workspace/git/git-service.js";
5
+ import type { ComputerService } from "../computer/computer-service.js";
5
6
  export declare class WebRuntime {
6
7
  readonly workspaces: WorkspaceService;
7
8
  readonly operations: OperationLog;
8
9
  readonly processes: ProcessSupervisor;
9
10
  readonly git: GitService;
10
- constructor(workspaces: WorkspaceService, operations: OperationLog, processes: ProcessSupervisor, git: GitService);
11
+ readonly computer: ComputerService;
12
+ constructor(workspaces: WorkspaceService, operations: OperationLog, processes: ProcessSupervisor, git: GitService, computer: ComputerService);
11
13
  previewWorktreeApply(workspaceId: string): Promise<import("./api-types.js").WorktreeApplyPreview>;
12
14
  previewWorktreeFileDiff(workspaceId: string, filePath: string): Promise<import("./api-types.js").WorktreeFileDiff>;
13
15
  applyWorktree(workspaceId: string, paths?: string[]): Promise<{
@@ -3,11 +3,13 @@ export class WebRuntime {
3
3
  operations;
4
4
  processes;
5
5
  git;
6
- constructor(workspaces, operations, processes, git) {
6
+ computer;
7
+ constructor(workspaces, operations, processes, git, computer) {
7
8
  this.workspaces = workspaces;
8
9
  this.operations = operations;
9
10
  this.processes = processes;
10
11
  this.git = git;
12
+ this.computer = computer;
11
13
  }
12
14
  previewWorktreeApply(workspaceId) {
13
15
  return this.workspaces.previewWorktreeApply(workspaceId);