@chatroomcp/chatroom 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/application.d.ts +1 -0
- package/dist/app/application.js +4 -0
- package/dist/app/event-bus.d.ts +4 -0
- package/dist/auth/ingress-policy.d.ts +2 -0
- package/dist/auth/ingress-policy.js +7 -1
- package/dist/infrastructure/database/app-database.js +7 -0
- package/dist/infrastructure/http/http-server.js +5 -1
- package/dist/mcp/server/plugin-mcp-registrar.d.ts +6 -1
- package/dist/mcp/server/plugin-mcp-registrar.js +4 -4
- package/dist/mcp/server/request-context.d.ts +3 -0
- package/dist/mcp/server/request-context.js +8 -0
- package/dist/mcp/server/tool-support.d.ts +1 -1
- package/dist/mcp/server/tool-support.js +3 -1
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/Info.plist +14 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/_CodeSignature/CodeResources +115 -0
- package/dist/native/windows/chatroom-computer-helper.exe +0 -0
- package/dist/plugins/computer/audit.d.ts +34 -0
- package/dist/plugins/computer/audit.js +42 -0
- package/dist/plugins/computer/computer-native-backend.d.ts +11 -0
- package/dist/plugins/computer/computer-native-backend.js +58 -0
- package/dist/plugins/computer/computer-native-host.d.ts +26 -0
- package/dist/plugins/computer/computer-native-host.js +245 -0
- package/dist/plugins/computer/computer-protocol.d.ts +21 -0
- package/dist/plugins/computer/computer-protocol.js +72 -0
- package/dist/plugins/computer/computer-schemas.d.ts +317 -0
- package/dist/plugins/computer/computer-schemas.js +152 -0
- package/dist/plugins/computer/computer-service.d.ts +27 -0
- package/dist/plugins/computer/computer-service.js +108 -0
- package/dist/plugins/computer/computer-settings-repository.d.ts +8 -0
- package/dist/plugins/computer/computer-settings-repository.js +41 -0
- package/dist/plugins/computer/mcp.d.ts +3 -0
- package/dist/plugins/computer/mcp.js +126 -0
- package/dist/plugins/computer/plugin.d.ts +4 -0
- package/dist/plugins/computer/plugin.js +29 -0
- package/dist/plugins/computer/types.d.ts +174 -0
- package/dist/plugins/computer/types.js +1 -0
- package/dist/plugins/web/api-types.d.ts +15 -1
- package/dist/plugins/web/http/api-router.js +2 -0
- package/dist/plugins/web/http/computer-api-router.d.ts +5 -0
- package/dist/plugins/web/http/computer-api-router.js +68 -0
- package/dist/plugins/web/plugin.js +3 -1
- package/dist/plugins/web/runtime.d.ts +3 -1
- package/dist/plugins/web/runtime.js +3 -1
- package/dist/web/assets/index-BXlxEk8f.css +1 -0
- package/dist/web/assets/index-DUM6zVHS.js +26 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -3
- package/dist/web/assets/index-B7jL3mhD.css +0 -1
- package/dist/web/assets/index-L4-YdZVN.js +0 -26
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const computerPermissionStateSchema = z.enum([
|
|
3
|
+
"granted",
|
|
4
|
+
"denied",
|
|
5
|
+
"unknown",
|
|
6
|
+
"not-required",
|
|
7
|
+
]);
|
|
8
|
+
export const computerDisplaySchema = z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
name: z.string(),
|
|
11
|
+
width: z.number(),
|
|
12
|
+
height: z.number(),
|
|
13
|
+
scale: z.number(),
|
|
14
|
+
primary: z.boolean(),
|
|
15
|
+
});
|
|
16
|
+
export const computerElementSchema = z.object({
|
|
17
|
+
id: z.number().int(),
|
|
18
|
+
role: z.string(),
|
|
19
|
+
name: z.string().nullable(),
|
|
20
|
+
value: z.string().nullable(),
|
|
21
|
+
enabled: z.boolean(),
|
|
22
|
+
focused: z.boolean(),
|
|
23
|
+
selected: z.boolean(),
|
|
24
|
+
sensitive: z.boolean(),
|
|
25
|
+
bounds: z.tuple([z.number(), z.number(), z.number(), z.number()]).nullable(),
|
|
26
|
+
actions: z.array(z.string()),
|
|
27
|
+
});
|
|
28
|
+
export const computerScreenshotSchema = z.object({
|
|
29
|
+
mimeType: z.enum(["image/jpeg", "image/png"]),
|
|
30
|
+
data: z.string(),
|
|
31
|
+
});
|
|
32
|
+
export const computerNativeStatusSchema = z.object({
|
|
33
|
+
platform: z.enum(["macos", "windows", "unsupported"]),
|
|
34
|
+
helper: z.enum(["running", "stopped", "unavailable"]),
|
|
35
|
+
permissions: z.object({
|
|
36
|
+
accessibility: computerPermissionStateSchema,
|
|
37
|
+
screenRecording: computerPermissionStateSchema,
|
|
38
|
+
}),
|
|
39
|
+
displays: z.array(computerDisplaySchema),
|
|
40
|
+
});
|
|
41
|
+
export const computerSnapshotSchema = z.object({
|
|
42
|
+
snapshotId: z.string().min(1),
|
|
43
|
+
revision: z.number().int(),
|
|
44
|
+
display: computerDisplaySchema.nullable(),
|
|
45
|
+
activeApp: z.string().nullable(),
|
|
46
|
+
activeWindow: z.string().nullable(),
|
|
47
|
+
cursor: z.object({ x: z.number(), y: z.number() }).nullable(),
|
|
48
|
+
elements: z.array(computerElementSchema),
|
|
49
|
+
screenshot: computerScreenshotSchema.optional(),
|
|
50
|
+
});
|
|
51
|
+
export const computerSnapshotMetadataSchema = computerSnapshotSchema.omit({
|
|
52
|
+
screenshot: true,
|
|
53
|
+
});
|
|
54
|
+
export const computerActionSchema = z.discriminatedUnion("type", [
|
|
55
|
+
z.object({ type: z.literal("move"), x: z.number(), y: z.number() }),
|
|
56
|
+
z.object({
|
|
57
|
+
type: z.literal("click"),
|
|
58
|
+
x: z.number().optional(),
|
|
59
|
+
y: z.number().optional(),
|
|
60
|
+
elementId: z.number().int().optional(),
|
|
61
|
+
}),
|
|
62
|
+
z.object({
|
|
63
|
+
type: z.literal("double_click"),
|
|
64
|
+
x: z.number().optional(),
|
|
65
|
+
y: z.number().optional(),
|
|
66
|
+
elementId: z.number().int().optional(),
|
|
67
|
+
}),
|
|
68
|
+
z.object({
|
|
69
|
+
type: z.literal("right_click"),
|
|
70
|
+
x: z.number().optional(),
|
|
71
|
+
y: z.number().optional(),
|
|
72
|
+
elementId: z.number().int().optional(),
|
|
73
|
+
}),
|
|
74
|
+
z.object({
|
|
75
|
+
type: z.literal("drag"),
|
|
76
|
+
from: z.object({ x: z.number(), y: z.number() }),
|
|
77
|
+
to: z.object({ x: z.number(), y: z.number() }),
|
|
78
|
+
durationMs: z.number().int().min(0).max(10_000).optional(),
|
|
79
|
+
}),
|
|
80
|
+
z.object({
|
|
81
|
+
type: z.literal("scroll"),
|
|
82
|
+
deltaX: z.number().optional(),
|
|
83
|
+
deltaY: z.number(),
|
|
84
|
+
elementId: z.number().int().optional(),
|
|
85
|
+
}),
|
|
86
|
+
z.object({
|
|
87
|
+
type: z.literal("keypress"),
|
|
88
|
+
keys: z.array(z.string().min(1)).min(1).max(8),
|
|
89
|
+
}),
|
|
90
|
+
z.object({
|
|
91
|
+
type: z.literal("type_text"),
|
|
92
|
+
text: z.string().max(100_000),
|
|
93
|
+
elementId: z.number().int().optional(),
|
|
94
|
+
}),
|
|
95
|
+
z.object({ type: z.literal("invoke"), elementId: z.number().int() }),
|
|
96
|
+
z.object({
|
|
97
|
+
type: z.literal("set_value"),
|
|
98
|
+
elementId: z.number().int(),
|
|
99
|
+
value: z.string().max(100_000),
|
|
100
|
+
}),
|
|
101
|
+
z.object({
|
|
102
|
+
type: z.literal("select_text"),
|
|
103
|
+
elementId: z.number().int(),
|
|
104
|
+
start: z.number().int().min(0),
|
|
105
|
+
length: z.number().int().min(0),
|
|
106
|
+
}),
|
|
107
|
+
z.object({ type: z.literal("activate_app"), app: z.string().min(1) }),
|
|
108
|
+
z.object({ type: z.literal("activate_window"), elementId: z.number().int() }),
|
|
109
|
+
z.object({
|
|
110
|
+
type: z.literal("move_window"),
|
|
111
|
+
elementId: z.number().int(),
|
|
112
|
+
x: z.number(),
|
|
113
|
+
y: z.number(),
|
|
114
|
+
}),
|
|
115
|
+
z.object({
|
|
116
|
+
type: z.literal("resize_window"),
|
|
117
|
+
elementId: z.number().int(),
|
|
118
|
+
width: z.number().positive(),
|
|
119
|
+
height: z.number().positive(),
|
|
120
|
+
}),
|
|
121
|
+
z.object({
|
|
122
|
+
type: z.literal("wait"),
|
|
123
|
+
ms: z.number().int().min(0).max(30_000),
|
|
124
|
+
}),
|
|
125
|
+
]);
|
|
126
|
+
export const computerActionResultSchema = z.object({
|
|
127
|
+
success: z.literal(true),
|
|
128
|
+
revision: z.number().int(),
|
|
129
|
+
snapshot: computerSnapshotSchema.optional(),
|
|
130
|
+
executionMode: z.enum(["semantic", "background", "foreground", "mixed"]),
|
|
131
|
+
focusChanged: z.boolean(),
|
|
132
|
+
});
|
|
133
|
+
export const computerActionMetadataSchema = computerActionResultSchema.extend({
|
|
134
|
+
snapshot: computerSnapshotMetadataSchema.optional(),
|
|
135
|
+
});
|
|
136
|
+
export const computerSnapshotTargetSchema = z.discriminatedUnion("type", [
|
|
137
|
+
z.object({ type: z.literal("desktop") }),
|
|
138
|
+
z.object({
|
|
139
|
+
type: z.literal("display"),
|
|
140
|
+
displayId: z.string().optional(),
|
|
141
|
+
}),
|
|
142
|
+
z.object({ type: z.literal("app"), app: z.string().min(1) }),
|
|
143
|
+
z.object({ type: z.literal("window"), elementId: z.number().int() }),
|
|
144
|
+
z.object({
|
|
145
|
+
type: z.literal("region"),
|
|
146
|
+
displayId: z.string().optional(),
|
|
147
|
+
x: z.number(),
|
|
148
|
+
y: z.number(),
|
|
149
|
+
width: z.number().positive(),
|
|
150
|
+
height: z.number().positive(),
|
|
151
|
+
}),
|
|
152
|
+
]);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RuntimeEventBus } from "../../app/event-bus.js";
|
|
2
|
+
import type { ComputerSettingsRepository } from "./computer-settings-repository.js";
|
|
3
|
+
import type { ComputerAccessScope, ComputerActionRequest, ComputerActionResult, ComputerBackend, ComputerPermission, ComputerSettings, ComputerSnapshot, ComputerSnapshotRequest, ComputerStatus } from "./types.js";
|
|
4
|
+
type ComputerSettingsStore = Pick<ComputerSettingsRepository, "get" | "set">;
|
|
5
|
+
export declare class ComputerService {
|
|
6
|
+
private readonly backend;
|
|
7
|
+
private readonly settingsRepository;
|
|
8
|
+
private readonly events;
|
|
9
|
+
private revision;
|
|
10
|
+
private currentSnapshotId;
|
|
11
|
+
private latestSnapshotValue;
|
|
12
|
+
private queue;
|
|
13
|
+
constructor(backend: ComputerBackend, settingsRepository: ComputerSettingsStore, events: RuntimeEventBus);
|
|
14
|
+
settings(): ComputerSettings;
|
|
15
|
+
latestSnapshot(scope: ComputerAccessScope): ComputerSnapshot | null;
|
|
16
|
+
requestPermission(permission: ComputerPermission): Promise<ComputerStatus>;
|
|
17
|
+
status(): Promise<ComputerStatus>;
|
|
18
|
+
setSettings(patch: Partial<Pick<ComputerSettings, "enabled" | "remoteAccess">>): ComputerSettings;
|
|
19
|
+
snapshot(scope: ComputerAccessScope, request: ComputerSnapshotRequest): Promise<ComputerSnapshot>;
|
|
20
|
+
action(scope: ComputerAccessScope, request: ComputerActionRequest): Promise<ComputerActionResult>;
|
|
21
|
+
shutdown(): Promise<void>;
|
|
22
|
+
private assertActionBudget;
|
|
23
|
+
private assertAllowed;
|
|
24
|
+
private assertScopeAllowed;
|
|
25
|
+
private serial;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ChatRoomError } from "../../core/errors/chatroom-error.js";
|
|
3
|
+
export class ComputerService {
|
|
4
|
+
backend;
|
|
5
|
+
settingsRepository;
|
|
6
|
+
events;
|
|
7
|
+
revision = 0;
|
|
8
|
+
currentSnapshotId = null;
|
|
9
|
+
latestSnapshotValue = null;
|
|
10
|
+
queue = Promise.resolve();
|
|
11
|
+
constructor(backend, settingsRepository, events) {
|
|
12
|
+
this.backend = backend;
|
|
13
|
+
this.settingsRepository = settingsRepository;
|
|
14
|
+
this.events = events;
|
|
15
|
+
}
|
|
16
|
+
settings() {
|
|
17
|
+
return this.settingsRepository.get();
|
|
18
|
+
}
|
|
19
|
+
latestSnapshot(scope) {
|
|
20
|
+
const settings = this.settings();
|
|
21
|
+
if (!settings.enabled)
|
|
22
|
+
return null;
|
|
23
|
+
this.assertScopeAllowed(scope, settings);
|
|
24
|
+
return this.latestSnapshotValue;
|
|
25
|
+
}
|
|
26
|
+
requestPermission(permission) {
|
|
27
|
+
return this.serial(async () => {
|
|
28
|
+
const base = await this.backend.requestPermission(permission);
|
|
29
|
+
return { ...base, settings: this.settings() };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async status() {
|
|
33
|
+
const base = await this.backend.status();
|
|
34
|
+
return { ...base, settings: this.settings() };
|
|
35
|
+
}
|
|
36
|
+
setSettings(patch) {
|
|
37
|
+
const next = this.settingsRepository.set({ ...this.settings(), ...patch });
|
|
38
|
+
if (!next.enabled) {
|
|
39
|
+
this.currentSnapshotId = null;
|
|
40
|
+
this.latestSnapshotValue = null;
|
|
41
|
+
void this.backend.dispose().catch(() => undefined);
|
|
42
|
+
}
|
|
43
|
+
this.events.emit({ type: "computer-settings", settings: next });
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
snapshot(scope, request) {
|
|
47
|
+
return this.serial(async () => {
|
|
48
|
+
this.assertAllowed(scope);
|
|
49
|
+
this.currentSnapshotId = null;
|
|
50
|
+
const value = await this.backend.snapshot(request, this.revision);
|
|
51
|
+
this.currentSnapshotId = value.snapshotId || `snap_${randomUUID()}`;
|
|
52
|
+
value.snapshotId = this.currentSnapshotId;
|
|
53
|
+
value.revision = this.revision;
|
|
54
|
+
this.latestSnapshotValue = value;
|
|
55
|
+
return value;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
action(scope, request) {
|
|
59
|
+
return this.serial(async () => {
|
|
60
|
+
this.assertAllowed(scope);
|
|
61
|
+
this.assertActionBudget(request);
|
|
62
|
+
const referencesElements = request.actions.some((action) => "elementId" in action && action.elementId !== undefined);
|
|
63
|
+
if (referencesElements && !request.snapshotId)
|
|
64
|
+
throw new ChatRoomError("CONFLICT", "Actions using elementId require the latest snapshotId");
|
|
65
|
+
if (request.snapshotId && request.snapshotId !== this.currentSnapshotId)
|
|
66
|
+
throw new ChatRoomError("CONFLICT", "Computer snapshot is stale; take a new snapshot before acting");
|
|
67
|
+
this.currentSnapshotId = null;
|
|
68
|
+
const result = await this.backend.action(request, ++this.revision);
|
|
69
|
+
this.currentSnapshotId = result.snapshot?.snapshotId ?? null;
|
|
70
|
+
if (result.snapshot)
|
|
71
|
+
this.latestSnapshotValue = result.snapshot;
|
|
72
|
+
return result;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async shutdown() {
|
|
76
|
+
this.currentSnapshotId = null;
|
|
77
|
+
this.latestSnapshotValue = null;
|
|
78
|
+
await this.backend.dispose();
|
|
79
|
+
}
|
|
80
|
+
assertActionBudget(request) {
|
|
81
|
+
const durationMs = request.actions.reduce((total, action) => {
|
|
82
|
+
if (action.type === "wait")
|
|
83
|
+
return total + action.ms;
|
|
84
|
+
if (action.type === "drag")
|
|
85
|
+
return total + (action.durationMs ?? 0);
|
|
86
|
+
return total;
|
|
87
|
+
}, 0);
|
|
88
|
+
if (durationMs > 30_000)
|
|
89
|
+
throw new ChatRoomError("INVALID_INPUT", "Computer action batch exceeds the 30 second execution budget");
|
|
90
|
+
}
|
|
91
|
+
assertAllowed(scope) {
|
|
92
|
+
const settings = this.settings();
|
|
93
|
+
if (!settings.enabled)
|
|
94
|
+
throw new ChatRoomError("FORBIDDEN", "Computer Use is disabled");
|
|
95
|
+
this.assertScopeAllowed(scope, settings);
|
|
96
|
+
}
|
|
97
|
+
assertScopeAllowed(scope, settings) {
|
|
98
|
+
if (scope === "remote" && !settings.remoteAccess)
|
|
99
|
+
throw new ChatRoomError("FORBIDDEN", "Remote Computer Use is disabled", {
|
|
100
|
+
reason: "remote_computer_disabled",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
serial(action) {
|
|
104
|
+
const next = this.queue.then(action, action);
|
|
105
|
+
this.queue = next.then(() => undefined, () => undefined);
|
|
106
|
+
return next;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AppDatabase } from "../../infrastructure/database/app-database.js";
|
|
2
|
+
import type { ComputerSettings } from "./types.js";
|
|
3
|
+
export declare class ComputerSettingsRepository {
|
|
4
|
+
private readonly database;
|
|
5
|
+
constructor(database: AppDatabase);
|
|
6
|
+
get(): ComputerSettings;
|
|
7
|
+
set(value: Omit<ComputerSettings, "updatedAt"> | ComputerSettings): ComputerSettings;
|
|
8
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class ComputerSettingsRepository {
|
|
2
|
+
database;
|
|
3
|
+
constructor(database) {
|
|
4
|
+
this.database = database;
|
|
5
|
+
}
|
|
6
|
+
get() {
|
|
7
|
+
const row = this.database.raw
|
|
8
|
+
.prepare("SELECT enabled, remote_access, updated_at FROM computer_settings WHERE id = 1")
|
|
9
|
+
.get();
|
|
10
|
+
if (row)
|
|
11
|
+
return fromRow(row);
|
|
12
|
+
const value = {
|
|
13
|
+
enabled: false,
|
|
14
|
+
remoteAccess: true,
|
|
15
|
+
updatedAt: new Date().toISOString(),
|
|
16
|
+
};
|
|
17
|
+
this.set(value);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
set(value) {
|
|
21
|
+
const updatedAt = new Date().toISOString();
|
|
22
|
+
this.database.raw
|
|
23
|
+
.prepare(`
|
|
24
|
+
INSERT INTO computer_settings(id, enabled, remote_access, updated_at)
|
|
25
|
+
VALUES(1, ?, ?, ?)
|
|
26
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
27
|
+
enabled = excluded.enabled,
|
|
28
|
+
remote_access = excluded.remote_access,
|
|
29
|
+
updated_at = excluded.updated_at
|
|
30
|
+
`)
|
|
31
|
+
.run(value.enabled ? 1 : 0, value.remoteAccess ? 1 : 0, updatedAt);
|
|
32
|
+
return { ...value, updatedAt };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function fromRow(row) {
|
|
36
|
+
return {
|
|
37
|
+
enabled: Boolean(row.enabled),
|
|
38
|
+
remoteAccess: Boolean(row.remote_access),
|
|
39
|
+
updatedAt: String(row.updated_at),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -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,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" | "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 {};
|