1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
import { Type } from "typebox";
import { type Result, err, ok } from "../util/result";
import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool";
const ListTabsArgs = Type.Object({
includeInternal: Type.Optional(Type.Boolean({ default: true, description: "Include chrome:// pages" })),
});
export const listTabsTool = defineBrowserTool({
name: "browser_list_tabs",
label: "Browser List Tabs",
description: "List all open browser tabs (page targets).",
promptSnippet: "List browser tabs",
promptGuidelines: [
"Use to find a full targetId for browser_switch_tab.",
"Internal tabs (chrome://) included by default; pass includeInternal=false to exclude them.",
"Each tab shows its full 32-char hex targetId — use the exact value with browser_switch_tab.",
],
parameters: ListTabsArgs,
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
const r = await client.listTabs(args.includeInternal ?? true);
if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
const lines = r.data.map((t, i) => ` [${i}] ${t.targetId} ${t.url}\n ${t.title}`);
return ok({
text: `Tabs (${r.data.length}):\n${lines.join("\n")}`,
details: { tabs: r.data },
});
},
});
export const currentTabTool = defineBrowserTool({
name: "browser_current_tab",
label: "Browser Current Tab",
description: "Get info about the currently attached tab.",
promptSnippet: "Get current tab info",
promptGuidelines: ["Returns targetId, url, title."],
parameters: Type.Object({}),
async handler(_a, { client }): Promise<Result<ToolOk, ToolErr>> {
const cur = client.current();
if (!cur) return err({ kind: "invalid_state", message: "No tab attached." });
const ti = await client.session().callBrowser("Target.getTargetInfo", { targetId: cur.targetId });
if (!ti.success) return err({ kind: "cdp_error", message: ti.error.message });
const info = (ti.data as { targetInfo: { targetId: string; url: string; title: string } }).targetInfo; // CDP boundary cast: Target.getTargetInfo returns { targetInfo: { targetId, url, title } }
return ok({
text: `Current tab:\n ${info.targetId}\n ${info.url}\n ${info.title}`,
details: { targetId: info.targetId, url: info.url, title: info.title },
});
},
});
const SwitchTabArgs = Type.Object({
targetId: Type.String({ description: "Target ID from browser_list_tabs" }),
});
export const switchTabTool = defineBrowserTool({
name: "browser_switch_tab",
label: "Browser Switch Tab",
description: "Switch to and attach to a different tab by targetId.",
promptSnippet: "Switch tabs",
promptGuidelines: [
"Get a targetId via browser_list_tabs first.",
"Accepts exact targetId or a unique prefix of at least 8 hex characters.",
],
parameters: SwitchTabArgs,
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
// Try exact match first
let r = await client.switchTab(args.targetId);
if (r.success) {
return ok({ text: `Switched to ${args.targetId}`, details: { targetId: args.targetId } });
}
// If exact match failed and input looks like a short prefix (≥8 hex chars), try prefix resolution
const isHexPrefix = /^[0-9A-Fa-f]{8,}$/.test(args.targetId);
if (!isHexPrefix) {
return err({ kind: "cdp_error", message: r.error.message });
}
const tabs = await client.listTabs(true);
if (!tabs.success) return err({ kind: "cdp_error", message: r.error.message });
const matches = tabs.data.filter((t) => t.targetId.startsWith(args.targetId));
if (matches.length === 0) {
return err({ kind: "cdp_error", message: `No tab found with prefix "${args.targetId}"` });
}
if (matches.length > 1) {
const ids = matches.map((t) => t.targetId).join(", ");
return err({
kind: "invalid_state",
message: `Ambiguous prefix "${args.targetId}" matches ${matches.length} tabs: ${ids}`,
details: { matches: matches.map((t) => ({ targetId: t.targetId, url: t.url })) },
});
}
const resolved = matches[0]!.targetId;
r = await client.switchTab(resolved);
if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
return ok({
text: `Switched to ${resolved} (resolved from prefix "${args.targetId}")`,
details: { targetId: resolved, resolvedPrefix: args.targetId },
});
},
});
const NewTabArgs = Type.Object({
url: Type.Optional(Type.String({ description: "Optional URL to navigate to" })),
});
export const newTabTool = defineBrowserTool({
name: "browser_new_tab",
label: "Browser New Tab",
description: "Open a new tab and switch to it. Optionally navigate to a URL.",
promptSnippet: "Open a new tab",
promptGuidelines: ["Pass url to navigate immediately."],
parameters: NewTabArgs,
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
const r = await client.newTab(args.url);
if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
return ok({
text: `New tab: ${r.data}${args.url ? ` (${args.url})` : ""}`,
details: args.url !== undefined
? { targetId: r.data, url: args.url }
: { targetId: r.data },
});
},
});
|