gclm-code 1.0.0 → 1.0.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.
- package/README.md +1 -1
- package/bin/gc.js +53 -25
- package/bin/install-runtime.js +253 -0
- package/package.json +10 -5
- package/vendor/manifest.json +92 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/package.json +9 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/bridgeClient.ts +1126 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/browserTools.ts +546 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/index.ts +15 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpServer.ts +96 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketClient.ts +493 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketPool.ts +327 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/toolCalls.ts +301 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/types.ts +134 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/package.json +9 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-jxa.js +341 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-swift.swift +417 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/implementation.js +204 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/index.js +5 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/package.json +11 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/deniedApps.ts +553 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/imageResize.ts +108 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/index.ts +69 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/keyBlocklist.ts +153 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/mcpServer.ts +313 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/pixelCompare.ts +171 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/sentinelApps.ts +43 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/subGates.ts +19 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/toolCalls.ts +3872 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/tools.ts +706 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/types.ts +635 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/package.json +9 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/driver-jxa.js +108 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/implementation.js +706 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/index.js +7 -0
- package/vendor/modules/node_modules/audio-capture-napi/package.json +8 -0
- package/vendor/modules/node_modules/audio-capture-napi/src/index.ts +226 -0
- package/vendor/modules/node_modules/image-processor-napi/package.json +11 -0
- package/vendor/modules/node_modules/image-processor-napi/src/index.ts +396 -0
- package/vendor/modules/node_modules/modifiers-napi/package.json +8 -0
- package/vendor/modules/node_modules/modifiers-napi/src/index.ts +79 -0
- package/vendor/modules/node_modules/url-handler-napi/package.json +8 -0
- package/vendor/modules/node_modules/url-handler-napi/src/index.ts +62 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMcpSocketClient,
|
|
3
|
+
SocketConnectionError,
|
|
4
|
+
} from "./mcpSocketClient.js";
|
|
5
|
+
import type { McpSocketClient } from "./mcpSocketClient.js";
|
|
6
|
+
import type {
|
|
7
|
+
ClaudeForChromeContext,
|
|
8
|
+
PermissionMode,
|
|
9
|
+
PermissionOverrides,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages connections to multiple Chrome native host sockets (one per Chrome profile).
|
|
14
|
+
* Routes tool calls to the correct socket based on tab ID.
|
|
15
|
+
*
|
|
16
|
+
* For `tabs_context_mcp`: queries all connected sockets and merges results.
|
|
17
|
+
* For other tools: routes based on the `tabId` argument using a routing table
|
|
18
|
+
* built from tabs_context_mcp responses.
|
|
19
|
+
*/
|
|
20
|
+
export class McpSocketPool {
|
|
21
|
+
private clients: Map<string, McpSocketClient> = new Map();
|
|
22
|
+
private tabRoutes: Map<number, string> = new Map();
|
|
23
|
+
private context: ClaudeForChromeContext;
|
|
24
|
+
private notificationHandler:
|
|
25
|
+
| ((notification: { method: string; params?: Record<string, unknown> }) => void)
|
|
26
|
+
| null = null;
|
|
27
|
+
|
|
28
|
+
constructor(context: ClaudeForChromeContext) {
|
|
29
|
+
this.context = context;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public setNotificationHandler(
|
|
33
|
+
handler: (notification: {
|
|
34
|
+
method: string;
|
|
35
|
+
params?: Record<string, unknown>;
|
|
36
|
+
}) => void,
|
|
37
|
+
): void {
|
|
38
|
+
this.notificationHandler = handler;
|
|
39
|
+
for (const client of this.clients.values()) {
|
|
40
|
+
client.setNotificationHandler(handler);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Discover available sockets and ensure at least one is connected.
|
|
46
|
+
*/
|
|
47
|
+
public async ensureConnected(): Promise<boolean> {
|
|
48
|
+
const { logger, serverName } = this.context;
|
|
49
|
+
|
|
50
|
+
this.refreshClients();
|
|
51
|
+
|
|
52
|
+
// Try to connect any disconnected clients
|
|
53
|
+
const connectPromises: Promise<boolean>[] = [];
|
|
54
|
+
for (const client of this.clients.values()) {
|
|
55
|
+
if (!client.isConnected()) {
|
|
56
|
+
connectPromises.push(
|
|
57
|
+
client.ensureConnected().catch(() => false),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (connectPromises.length > 0) {
|
|
63
|
+
await Promise.all(connectPromises);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const connectedCount = this.getConnectedClients().length;
|
|
67
|
+
if (connectedCount === 0) {
|
|
68
|
+
logger.info(`[${serverName}] No connected sockets in pool`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.info(`[${serverName}] Socket pool: ${connectedCount} connected`);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Call a tool, routing to the correct socket based on tab ID.
|
|
78
|
+
* For tabs_context_mcp, queries all sockets and merges results.
|
|
79
|
+
*/
|
|
80
|
+
public async callTool(
|
|
81
|
+
name: string,
|
|
82
|
+
args: Record<string, unknown>,
|
|
83
|
+
_permissionOverrides?: PermissionOverrides,
|
|
84
|
+
): Promise<unknown> {
|
|
85
|
+
if (name === "tabs_context_mcp") {
|
|
86
|
+
return this.callTabsContext(args);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Route by tabId if present
|
|
90
|
+
const tabId = args.tabId as number | undefined;
|
|
91
|
+
if (tabId !== undefined) {
|
|
92
|
+
const socketPath = this.tabRoutes.get(tabId);
|
|
93
|
+
if (socketPath) {
|
|
94
|
+
const client = this.clients.get(socketPath);
|
|
95
|
+
if (client?.isConnected()) {
|
|
96
|
+
return client.callTool(name, args);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Tab route not found or client disconnected — fall through to any connected
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fallback: use first connected client
|
|
103
|
+
const connected = this.getConnectedClients();
|
|
104
|
+
if (connected.length === 0) {
|
|
105
|
+
throw new SocketConnectionError(
|
|
106
|
+
`[${this.context.serverName}] No connected sockets available`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return connected[0]!.callTool(name, args);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public async setPermissionMode(
|
|
113
|
+
mode: PermissionMode,
|
|
114
|
+
allowedDomains?: string[],
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const connected = this.getConnectedClients();
|
|
117
|
+
await Promise.all(
|
|
118
|
+
connected.map((client) => client.setPermissionMode(mode, allowedDomains)),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public isConnected(): boolean {
|
|
123
|
+
return this.getConnectedClients().length > 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public disconnect(): void {
|
|
127
|
+
for (const client of this.clients.values()) {
|
|
128
|
+
client.disconnect();
|
|
129
|
+
}
|
|
130
|
+
this.clients.clear();
|
|
131
|
+
this.tabRoutes.clear();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private getConnectedClients(): McpSocketClient[] {
|
|
135
|
+
return [...this.clients.values()].filter((c) => c.isConnected());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Query all connected sockets for tabs and merge results.
|
|
140
|
+
* Updates the tab routing table.
|
|
141
|
+
*/
|
|
142
|
+
private async callTabsContext(
|
|
143
|
+
args: Record<string, unknown>,
|
|
144
|
+
): Promise<unknown> {
|
|
145
|
+
const { logger, serverName } = this.context;
|
|
146
|
+
const connected = this.getConnectedClients();
|
|
147
|
+
|
|
148
|
+
if (connected.length === 0) {
|
|
149
|
+
throw new SocketConnectionError(
|
|
150
|
+
`[${serverName}] No connected sockets available`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// If only one client, skip merging overhead
|
|
155
|
+
if (connected.length === 1) {
|
|
156
|
+
const result = await connected[0]!.callTool("tabs_context_mcp", args);
|
|
157
|
+
this.updateTabRoutes(result, this.getSocketPathForClient(connected[0]!));
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Query all connected clients in parallel
|
|
162
|
+
const results = await Promise.allSettled(
|
|
163
|
+
connected.map(async (client) => {
|
|
164
|
+
const result = await client.callTool("tabs_context_mcp", args);
|
|
165
|
+
const socketPath = this.getSocketPathForClient(client);
|
|
166
|
+
return { result, socketPath };
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Merge tab results
|
|
171
|
+
const mergedTabs: unknown[] = [];
|
|
172
|
+
this.tabRoutes.clear();
|
|
173
|
+
|
|
174
|
+
for (const settledResult of results) {
|
|
175
|
+
if (settledResult.status !== "fulfilled") {
|
|
176
|
+
logger.info(
|
|
177
|
+
`[${serverName}] tabs_context_mcp failed on one socket: ${settledResult.reason}`,
|
|
178
|
+
);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { result, socketPath } = settledResult.value;
|
|
183
|
+
this.updateTabRoutes(result, socketPath);
|
|
184
|
+
|
|
185
|
+
const tabs = this.extractTabs(result);
|
|
186
|
+
if (tabs) {
|
|
187
|
+
mergedTabs.push(...tabs);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Return merged result in the same format as the extension response
|
|
192
|
+
if (mergedTabs.length > 0) {
|
|
193
|
+
const tabListText = mergedTabs
|
|
194
|
+
.map((t) => {
|
|
195
|
+
const tab = t as { tabId: number; title: string; url: string };
|
|
196
|
+
return ` • tabId ${tab.tabId}: "${tab.title}" (${tab.url})`;
|
|
197
|
+
})
|
|
198
|
+
.join("\n");
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
result: {
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: "text",
|
|
205
|
+
text: JSON.stringify({ availableTabs: mergedTabs }),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: `\n\nTab Context:\n- Available tabs:\n${tabListText}`,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Fallback: return first successful result as-is
|
|
217
|
+
for (const settledResult of results) {
|
|
218
|
+
if (settledResult.status === "fulfilled") {
|
|
219
|
+
return settledResult.value.result;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw new SocketConnectionError(
|
|
224
|
+
`[${serverName}] All sockets failed for tabs_context_mcp`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extract tab objects from a tool response to update routing table.
|
|
230
|
+
*/
|
|
231
|
+
private updateTabRoutes(result: unknown, socketPath: string): void {
|
|
232
|
+
const tabs = this.extractTabs(result);
|
|
233
|
+
if (!tabs) return;
|
|
234
|
+
|
|
235
|
+
for (const tab of tabs) {
|
|
236
|
+
if (typeof tab === "object" && tab !== null && "tabId" in tab) {
|
|
237
|
+
const tabId = (tab as { tabId: number }).tabId;
|
|
238
|
+
this.tabRoutes.set(tabId, socketPath);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private extractTabs(result: unknown): unknown[] | null {
|
|
244
|
+
if (!result || typeof result !== "object") return null;
|
|
245
|
+
|
|
246
|
+
// Response format: { result: { content: [{ type: "text", text: "{\"availableTabs\":[...],\"tabGroupId\":...}" }] } }
|
|
247
|
+
const asResponse = result as {
|
|
248
|
+
result?: { content?: Array<{ type: string; text?: string }> };
|
|
249
|
+
};
|
|
250
|
+
const content = asResponse.result?.content;
|
|
251
|
+
if (!content || !Array.isArray(content)) return null;
|
|
252
|
+
|
|
253
|
+
for (const item of content) {
|
|
254
|
+
if (item.type === "text" && item.text) {
|
|
255
|
+
try {
|
|
256
|
+
const parsed = JSON.parse(item.text);
|
|
257
|
+
if (Array.isArray(parsed)) return parsed;
|
|
258
|
+
// Handle { availableTabs: [...] } format
|
|
259
|
+
if (parsed && Array.isArray(parsed.availableTabs)) {
|
|
260
|
+
return parsed.availableTabs;
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// Not JSON, skip
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private getSocketPathForClient(client: McpSocketClient): string {
|
|
271
|
+
for (const [path, c] of this.clients.entries()) {
|
|
272
|
+
if (c === client) return path;
|
|
273
|
+
}
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Scan for available sockets and create/remove clients as needed.
|
|
279
|
+
*/
|
|
280
|
+
private refreshClients(): void {
|
|
281
|
+
const socketPaths = this.getAvailableSocketPaths();
|
|
282
|
+
const { logger, serverName } = this.context;
|
|
283
|
+
|
|
284
|
+
// Add new clients for newly discovered sockets
|
|
285
|
+
for (const path of socketPaths) {
|
|
286
|
+
if (!this.clients.has(path)) {
|
|
287
|
+
logger.info(`[${serverName}] Adding socket to pool: ${path}`);
|
|
288
|
+
const clientContext: ClaudeForChromeContext = {
|
|
289
|
+
...this.context,
|
|
290
|
+
socketPath: path,
|
|
291
|
+
getSocketPath: undefined,
|
|
292
|
+
getSocketPaths: undefined,
|
|
293
|
+
};
|
|
294
|
+
const client = createMcpSocketClient(clientContext);
|
|
295
|
+
client.disableAutoReconnect = true;
|
|
296
|
+
if (this.notificationHandler) {
|
|
297
|
+
client.setNotificationHandler(this.notificationHandler);
|
|
298
|
+
}
|
|
299
|
+
this.clients.set(path, client);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Remove clients for sockets that no longer exist
|
|
304
|
+
for (const [path, client] of this.clients.entries()) {
|
|
305
|
+
if (!socketPaths.includes(path)) {
|
|
306
|
+
logger.info(`[${serverName}] Removing stale socket from pool: ${path}`);
|
|
307
|
+
client.disconnect();
|
|
308
|
+
this.clients.delete(path);
|
|
309
|
+
for (const [tabId, socketPath] of this.tabRoutes.entries()) {
|
|
310
|
+
if (socketPath === path) {
|
|
311
|
+
this.tabRoutes.delete(tabId);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private getAvailableSocketPaths(): string[] {
|
|
319
|
+
return this.context.getSocketPaths?.() ?? [];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function createMcpSocketPool(
|
|
324
|
+
context: ClaudeForChromeContext,
|
|
325
|
+
): McpSocketPool {
|
|
326
|
+
return new McpSocketPool(context);
|
|
327
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
|
|
3
|
+
import { SocketConnectionError } from "./mcpSocketClient.js";
|
|
4
|
+
import type {
|
|
5
|
+
ClaudeForChromeContext,
|
|
6
|
+
PermissionMode,
|
|
7
|
+
PermissionOverrides,
|
|
8
|
+
SocketClient,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
export const handleToolCall = async (
|
|
12
|
+
context: ClaudeForChromeContext,
|
|
13
|
+
socketClient: SocketClient,
|
|
14
|
+
name: string,
|
|
15
|
+
args: Record<string, unknown>,
|
|
16
|
+
permissionOverrides?: PermissionOverrides,
|
|
17
|
+
): Promise<CallToolResult> => {
|
|
18
|
+
// Handle permission mode changes locally (not forwarded to extension)
|
|
19
|
+
if (name === "set_permission_mode") {
|
|
20
|
+
return handleSetPermissionMode(socketClient, args);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle switch_browser outside the normal tool call flow (manages its own connection)
|
|
24
|
+
if (name === "switch_browser") {
|
|
25
|
+
return handleSwitchBrowser(context, socketClient);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const isConnected = await socketClient.ensureConnected();
|
|
30
|
+
|
|
31
|
+
context.logger.silly(
|
|
32
|
+
`[${context.serverName}] Server is connected: ${isConnected}. Received tool call: ${name} with args: ${JSON.stringify(args)}.`,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (isConnected) {
|
|
36
|
+
return await handleToolCallConnected(
|
|
37
|
+
context,
|
|
38
|
+
socketClient,
|
|
39
|
+
name,
|
|
40
|
+
args,
|
|
41
|
+
permissionOverrides,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return handleToolCallDisconnected(context);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
context.logger.info(`[${context.serverName}] Error calling tool:`, error);
|
|
48
|
+
|
|
49
|
+
if (error instanceof SocketConnectionError) {
|
|
50
|
+
return handleToolCallDisconnected(context);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: `Error calling tool, please try again. : ${error instanceof Error ? error.message : String(error)}`,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
async function handleToolCallConnected(
|
|
66
|
+
context: ClaudeForChromeContext,
|
|
67
|
+
socketClient: SocketClient,
|
|
68
|
+
name: string,
|
|
69
|
+
args: Record<string, unknown>,
|
|
70
|
+
permissionOverrides?: PermissionOverrides,
|
|
71
|
+
): Promise<CallToolResult> {
|
|
72
|
+
const response = await socketClient.callTool(name, args, permissionOverrides);
|
|
73
|
+
|
|
74
|
+
context.logger.silly(
|
|
75
|
+
`[${context.serverName}] Received result from socket bridge: ${JSON.stringify(response)}`,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (response === null || response === undefined) {
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text", text: "Tool execution completed" }],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Response will have either result or error field
|
|
85
|
+
const { result, error } = response as {
|
|
86
|
+
result?: { content: unknown[] | string };
|
|
87
|
+
error?: { content: unknown[] | string };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Determine which field has the content and whether it's an error
|
|
91
|
+
const contentData = error || result;
|
|
92
|
+
const isError = !!error;
|
|
93
|
+
|
|
94
|
+
if (!contentData) {
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: "Tool execution completed" }],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isError && isAuthenticationError(contentData.content)) {
|
|
101
|
+
context.onAuthenticationError();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { content } = contentData;
|
|
105
|
+
|
|
106
|
+
if (content && Array.isArray(content)) {
|
|
107
|
+
if (isError) {
|
|
108
|
+
return {
|
|
109
|
+
content: content.map((item: unknown) => {
|
|
110
|
+
if (typeof item === "object" && item !== null && "type" in item) {
|
|
111
|
+
return item;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { type: "text", text: String(item) };
|
|
115
|
+
}),
|
|
116
|
+
isError: true,
|
|
117
|
+
} as CallToolResult;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const convertedContent = content.map((item: unknown) => {
|
|
121
|
+
if (
|
|
122
|
+
typeof item === "object" &&
|
|
123
|
+
item !== null &&
|
|
124
|
+
"type" in item &&
|
|
125
|
+
"source" in item
|
|
126
|
+
) {
|
|
127
|
+
const typedItem = item;
|
|
128
|
+
if (
|
|
129
|
+
typedItem.type === "image" &&
|
|
130
|
+
typeof typedItem.source === "object" &&
|
|
131
|
+
typedItem.source !== null &&
|
|
132
|
+
"data" in typedItem.source
|
|
133
|
+
) {
|
|
134
|
+
return {
|
|
135
|
+
type: "image",
|
|
136
|
+
data: typedItem.source.data,
|
|
137
|
+
mimeType:
|
|
138
|
+
"media_type" in typedItem.source
|
|
139
|
+
? typedItem.source.media_type || "image/png"
|
|
140
|
+
: "image/png",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (typeof item === "object" && item !== null && "type" in item) {
|
|
146
|
+
return item;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { type: "text", text: String(item) };
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
content: convertedContent,
|
|
154
|
+
isError,
|
|
155
|
+
} as CallToolResult;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle string content
|
|
159
|
+
if (typeof content === "string") {
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: content }],
|
|
162
|
+
isError,
|
|
163
|
+
} as CallToolResult;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fallback for unexpected result format
|
|
167
|
+
context.logger.warn(
|
|
168
|
+
`[${context.serverName}] Unexpected result format from socket bridge`,
|
|
169
|
+
response,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
174
|
+
isError,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handleToolCallDisconnected(
|
|
179
|
+
context: ClaudeForChromeContext,
|
|
180
|
+
): CallToolResult {
|
|
181
|
+
const text = context.onToolCallDisconnected();
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text }],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Handle set_permission_mode tool call locally.
|
|
189
|
+
* This is security-sensitive as it controls whether permission prompts are shown.
|
|
190
|
+
*/
|
|
191
|
+
async function handleSetPermissionMode(
|
|
192
|
+
socketClient: SocketClient,
|
|
193
|
+
args: Record<string, unknown>,
|
|
194
|
+
): Promise<CallToolResult> {
|
|
195
|
+
// Validate permission mode at runtime
|
|
196
|
+
const validModes = [
|
|
197
|
+
"ask",
|
|
198
|
+
"skip_all_permission_checks",
|
|
199
|
+
"follow_a_plan",
|
|
200
|
+
] as const;
|
|
201
|
+
const mode = args.mode as string | undefined;
|
|
202
|
+
const permissionMode: PermissionMode =
|
|
203
|
+
mode && validModes.includes(mode as PermissionMode)
|
|
204
|
+
? (mode as PermissionMode)
|
|
205
|
+
: "ask";
|
|
206
|
+
|
|
207
|
+
if (socketClient.setPermissionMode) {
|
|
208
|
+
await socketClient.setPermissionMode(
|
|
209
|
+
permissionMode,
|
|
210
|
+
args.allowed_domains as string[] | undefined,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
content: [
|
|
216
|
+
{ type: "text", text: `Permission mode set to: ${permissionMode}` },
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Handle switch_browser tool call. Broadcasts a pairing request and blocks
|
|
223
|
+
* until a browser responds or timeout.
|
|
224
|
+
*/
|
|
225
|
+
async function handleSwitchBrowser(
|
|
226
|
+
context: ClaudeForChromeContext,
|
|
227
|
+
socketClient: SocketClient,
|
|
228
|
+
): Promise<CallToolResult> {
|
|
229
|
+
if (!context.bridgeConfig) {
|
|
230
|
+
return {
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: "Browser switching is only available with bridge connections.",
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
isError: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const isConnected = await socketClient.ensureConnected();
|
|
242
|
+
if (!isConnected) {
|
|
243
|
+
return handleToolCallDisconnected(context);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const result = (await socketClient.switchBrowser?.()) ?? null;
|
|
247
|
+
|
|
248
|
+
if (result === "no_other_browsers") {
|
|
249
|
+
return {
|
|
250
|
+
content: [
|
|
251
|
+
{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: "No other browsers available to switch to. Open Chrome with the Claude extension in another browser to switch.",
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
isError: true,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (result) {
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{ type: "text", text: `Connected to browser "${result.name}".` },
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
content: [
|
|
270
|
+
{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: "No browser responded within the timeout. Make sure Chrome is open with the Claude extension installed, then try again.",
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
isError: true,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if the error content indicates an authentication issue
|
|
281
|
+
*/
|
|
282
|
+
function isAuthenticationError(content: unknown[] | string): boolean {
|
|
283
|
+
const errorText = Array.isArray(content)
|
|
284
|
+
? content
|
|
285
|
+
.map((item) => {
|
|
286
|
+
if (typeof item === "string") return item;
|
|
287
|
+
if (
|
|
288
|
+
typeof item === "object" &&
|
|
289
|
+
item !== null &&
|
|
290
|
+
"text" in item &&
|
|
291
|
+
typeof item.text === "string"
|
|
292
|
+
) {
|
|
293
|
+
return item.text;
|
|
294
|
+
}
|
|
295
|
+
return "";
|
|
296
|
+
})
|
|
297
|
+
.join(" ")
|
|
298
|
+
: String(content);
|
|
299
|
+
|
|
300
|
+
return errorText.toLowerCase().includes("re-authenticated");
|
|
301
|
+
}
|