dsh-builtin-browser 0.1.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.
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Model-facing browser tools over `ctx.browser`: `browser_open`,
3
+ * `browser_snapshot`, `browser_execute`, `browser_content`,
4
+ * `browser_screenshot`, and tab management (`browser_list_tabs`,
5
+ * `browser_switch_tab`, `browser_close_tab`, `browser_reset`).
6
+ *
7
+ * The tool layer owns only the model-facing schema, argument validation, and
8
+ * result formatting 鈥?never provider selection or page driving, which belong
9
+ * to the seam. Session lifecycle is owned here at the plugin level: the
10
+ * first `browser_open` (or any tool when no session exists) opens a session;
11
+ * later tools reuse it. Per-agent isolation is a follow-up.
12
+ * @module dsh-browser/tool-browser
13
+ */
14
+ import { defineTool } from '@deepseek-ai/dsh-tools';
15
+ /** Plugin name used by loader diagnostics. */
16
+ export const name = 'tool-browser';
17
+ /** The tool registry, browser seam, and system-prompt registry this tool layer consumes. */
18
+ export const inject = ['tools', 'browser', 'systemPrompt'];
19
+ /** One plugin-level browser session, opened lazily. */
20
+ let currentSession;
21
+ /**
22
+ * Resolve the active session, opening one on first use.
23
+ * @param browser - the seam service.
24
+ * @returns the active session id.
25
+ */
26
+ async function ensureSession(browser) {
27
+ if (currentSession === undefined) {
28
+ currentSession = await browser.open();
29
+ }
30
+ return currentSession;
31
+ }
32
+ /** Format a snapshot element list for the model. */
33
+ function formatSnapshot(snapshot) {
34
+ const lines = snapshot.elements.map(el => `[${el.ref}] ${el.kind}: ${el.label} (${el.x},${el.y})`);
35
+ const header = `URL: ${snapshot.url}${snapshot.title !== undefined ? `\nTitle: ${snapshot.title}` : ''}`;
36
+ const body = lines.length > 0 ? lines.join('\n') : '(no interactive elements found)';
37
+ const tail = snapshot.truncated === true ? '\n(snapshot truncated)' : '';
38
+ return `${header}\n\n${body}${tail}`;
39
+ }
40
+ /** Register all browser tools with `ctx.tools`. */
41
+ export function apply(ctx, config = {}) {
42
+ const timeoutMs = config.timeoutMs ?? 60_000;
43
+ ctx.systemPrompt.section({
44
+ name: 'tool:browser',
45
+ order: 110,
46
+ text: 'Use the browser_* tools to operate a real shared browser the human can see and take over. Locate elements by snapshot reference numbers (browser_snapshot) and drive them with browser_execute (DOM-referenced JS, native setters for framework inputs). browser_screenshot is for visual confirmation, not primary targeting. Keep the human informed of what you are doing on the page.',
47
+ });
48
+ ctx.tools.register(defineTool({
49
+ name: 'browser_open',
50
+ description: 'Open a URL in the shared browser. Opens a new session on first use; optionally opens in a new tab. Returns the resulting page snapshot.',
51
+ parameters: {
52
+ url: { type: 'string', required: true, description: 'The URL to open (HTTP/HTTPS).' },
53
+ newTab: { type: 'boolean', description: 'Open in a new tab instead of the active one.' },
54
+ },
55
+ output: {
56
+ schema: {
57
+ type: 'object',
58
+ additionalProperties: false,
59
+ properties: {
60
+ url: { type: 'string', required: true },
61
+ title: { type: 'string' },
62
+ elements: {
63
+ type: 'array',
64
+ required: true,
65
+ items: {
66
+ type: 'object',
67
+ additionalProperties: false,
68
+ properties: {
69
+ ref: { type: 'number', required: true },
70
+ kind: { type: 'string', required: true },
71
+ label: { type: 'string', required: true },
72
+ x: { type: 'number', required: true },
73
+ y: { type: 'number', required: true },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ },
79
+ render: (_args, value) => [{ type: 'text', text: formatSnapshot(value) }],
80
+ },
81
+ timeoutMs,
82
+ isConcurrencySafe: () => true,
83
+ async execute(args, exec) {
84
+ const browser = ctx.get('browser');
85
+ if (browser === undefined)
86
+ throw new Error('tool-browser: browser service unavailable');
87
+ const session = await ensureSession(browser);
88
+ await browser.openUrl(session, {
89
+ url: args.url,
90
+ ...args.newTab === true ? { newTab: true } : {},
91
+ }, exec.signal);
92
+ const snapshot = await browser.snapshot(session, exec.signal);
93
+ return {
94
+ url: snapshot.url,
95
+ ...snapshot.title !== undefined ? { title: snapshot.title } : {},
96
+ elements: snapshot.elements.map(el => ({ ref: el.ref, kind: el.kind, label: el.label, x: el.x, y: el.y })),
97
+ truncated: snapshot.truncated,
98
+ };
99
+ },
100
+ }));
101
+ ctx.tools.register(defineTool({
102
+ name: 'browser_snapshot',
103
+ description: 'Return an AI-friendly snapshot of the current shared-browser page: numbered interactive elements (inputs, buttons, links) the model can cite. Use this to understand an interactive page before driving it.',
104
+ parameters: {},
105
+ output: {
106
+ schema: {
107
+ type: 'object',
108
+ additionalProperties: false,
109
+ properties: {
110
+ url: { type: 'string', required: true },
111
+ title: { type: 'string' },
112
+ elements: {
113
+ type: 'array',
114
+ required: true,
115
+ items: {
116
+ type: 'object',
117
+ additionalProperties: false,
118
+ properties: {
119
+ ref: { type: 'number', required: true },
120
+ kind: { type: 'string', required: true },
121
+ label: { type: 'string', required: true },
122
+ x: { type: 'number', required: true },
123
+ y: { type: 'number', required: true },
124
+ },
125
+ },
126
+ },
127
+ },
128
+ },
129
+ render: (_args, value) => [{ type: 'text', text: formatSnapshot(value) }],
130
+ },
131
+ timeoutMs,
132
+ isConcurrencySafe: () => true,
133
+ async execute(_args, exec) {
134
+ const browser = ctx.get('browser');
135
+ if (browser === undefined)
136
+ throw new Error('tool-browser: browser service unavailable');
137
+ const session = await ensureSession(browser);
138
+ const snapshot = await browser.snapshot(session, exec.signal);
139
+ return {
140
+ url: snapshot.url,
141
+ ...snapshot.title !== undefined ? { title: snapshot.title } : {},
142
+ elements: snapshot.elements.map(el => ({ ref: el.ref, kind: el.kind, label: el.label, x: el.x, y: el.y })),
143
+ truncated: snapshot.truncated,
144
+ };
145
+ },
146
+ }));
147
+ ctx.tools.register(defineTool({
148
+ name: 'browser_execute',
149
+ description: 'Execute JavaScript in the shared-browser page context. This is the primary way to interact with page elements: focus, fill inputs (use the native value setter for framework-controlled inputs, then dispatch an input event), click buttons (element.click() or a constructed MouseEvent). Returns the evaluation result by value, or the exception text.',
150
+ parameters: {
151
+ script: { type: 'string', required: true, description: 'The JavaScript expression to evaluate in the page context.' },
152
+ args: { type: 'array', items: { type: 'string' }, description: 'Optional arguments injected into the script scope as arguments[0..n].' },
153
+ },
154
+ output: {
155
+ schema: {
156
+ type: 'object',
157
+ additionalProperties: false,
158
+ properties: {
159
+ ok: { type: 'boolean', required: true },
160
+ value: { type: 'string' },
161
+ exception: { type: 'string' },
162
+ },
163
+ },
164
+ render: (_args, value) => [{ type: 'text', text: value.ok ? `Result: ${String(value.value)}` : `Exception: ${value.exception}` }],
165
+ },
166
+ timeoutMs,
167
+ isConcurrencySafe: () => false, // page JS can be stateful
168
+ async execute(args, exec) {
169
+ const browser = ctx.get('browser');
170
+ if (browser === undefined)
171
+ throw new Error('tool-browser: browser service unavailable');
172
+ const session = await ensureSession(browser);
173
+ const result = await browser.execute(session, {
174
+ script: args.script,
175
+ args: args.args ?? [],
176
+ }, exec.signal);
177
+ if (result.ok) {
178
+ const raw = result.value;
179
+ const value = typeof raw === 'string' ? raw : JSON.stringify(raw ?? null);
180
+ return { ok: true, value };
181
+ }
182
+ return { ok: false, exception: result.exception };
183
+ },
184
+ }));
185
+ ctx.tools.register(defineTool({
186
+ name: 'browser_content',
187
+ description: 'Fetch the current shared-browser page content in a chosen format: html (raw DOM), markdown (structured reading), txt (plain text), or json. Optionally scope to a CSS selector and cap the length. Use this to read page content, not to interact.',
188
+ parameters: {
189
+ format: { type: 'string', required: true, enum: ['html', 'markdown', 'txt', 'json'], description: 'Output format.' },
190
+ selector: { type: 'string', description: 'CSS selector limiting the fetch to one region (e.g. #main).' },
191
+ maxChars: { type: 'number', description: 'Maximum characters of returned content.' },
192
+ timeoutMs: { type: 'number', description: 'Evaluation timeout in ms (default 30000).' },
193
+ },
194
+ output: {
195
+ schema: {
196
+ type: 'object',
197
+ additionalProperties: false,
198
+ properties: {
199
+ content: { type: 'string', required: true },
200
+ truncated: { type: 'boolean', required: true },
201
+ },
202
+ },
203
+ render: (_args, value) => [{ type: 'text', text: value.content + (value.truncated ? '\n(truncated)' : '') }],
204
+ },
205
+ timeoutMs,
206
+ isConcurrencySafe: () => true,
207
+ async execute(args, exec) {
208
+ const browser = ctx.get('browser');
209
+ if (browser === undefined)
210
+ throw new Error('tool-browser: browser service unavailable');
211
+ const session = await ensureSession(browser);
212
+ const result = await browser.content(session, {
213
+ format: args.format,
214
+ ...args.selector !== undefined ? { selector: args.selector } : {},
215
+ ...args.maxChars !== undefined ? { maxChars: args.maxChars } : {},
216
+ ...args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {},
217
+ }, exec.signal);
218
+ return { content: result.content, truncated: result.truncated };
219
+ },
220
+ }));
221
+ ctx.tools.register(defineTool({
222
+ name: 'browser_screenshot',
223
+ description: 'Capture the current shared-browser page as a PNG screenshot. Use for visual confirmation of layout, charts, designs, or CAPTCHAs 鈥?not as the primary way to locate elements (use browser_snapshot for that). Supports optional full-page capture.',
224
+ parameters: {
225
+ fullPage: { type: 'boolean', description: 'Capture the full scrollable page instead of the viewport (default false).' },
226
+ },
227
+ output: {
228
+ schema: {
229
+ type: 'object',
230
+ additionalProperties: false,
231
+ properties: {
232
+ dataUrl: { type: 'string', required: true, description: 'Base64 PNG data URL of the screenshot.' },
233
+ },
234
+ },
235
+ render: (_args, value) => [{ type: 'text', text: `Screenshot captured (${Math.round(value.dataUrl.length * 3 / 4 / 1024)} KiB). View it in the shared browser panel or open the data URL.` }],
236
+ },
237
+ timeoutMs,
238
+ isConcurrencySafe: () => true,
239
+ async execute(args, exec) {
240
+ const browser = ctx.get('browser');
241
+ if (browser === undefined)
242
+ throw new Error('tool-browser: browser service unavailable');
243
+ const session = await ensureSession(browser);
244
+ const shot = await browser.screenshot(session, {
245
+ ...args.fullPage === true ? { fullPage: true } : {},
246
+ }, exec.signal);
247
+ return { dataUrl: shot.dataUrl };
248
+ },
249
+ }));
250
+ if (config.tabTools !== false) {
251
+ ctx.tools.register(defineTool({
252
+ name: 'browser_list_tabs',
253
+ description: 'List the shared-browser session\'s tabs with their URLs and which is active.',
254
+ parameters: {},
255
+ output: {
256
+ schema: {
257
+ type: 'object',
258
+ additionalProperties: false,
259
+ properties: {
260
+ tabs: {
261
+ type: 'array',
262
+ required: true,
263
+ items: {
264
+ type: 'object',
265
+ additionalProperties: false,
266
+ properties: {
267
+ id: { type: 'string', required: true },
268
+ url: { type: 'string', required: true },
269
+ active: { type: 'boolean', required: true },
270
+ },
271
+ },
272
+ },
273
+ },
274
+ },
275
+ render: (_args, value) => [{
276
+ type: 'text',
277
+ text: value.tabs
278
+ .map(t => `${t.active ? '*' : ' '} ${t.id} ${t.url}`).join('\n'),
279
+ }],
280
+ },
281
+ timeoutMs,
282
+ isConcurrencySafe: () => true,
283
+ async execute(_args, _exec) {
284
+ const browser = ctx.get('browser');
285
+ if (browser === undefined)
286
+ throw new Error('tool-browser: browser service unavailable');
287
+ const session = await ensureSession(browser);
288
+ const tabs = await browser.listTabs(session);
289
+ return { tabs: tabs.map(t => ({ id: t.id, url: t.url, active: t.active })) };
290
+ },
291
+ }));
292
+ ctx.tools.register(defineTool({
293
+ name: 'browser_switch_tab',
294
+ description: 'Switch the shared browser to a tab by id (from browser_list_tabs).',
295
+ parameters: {
296
+ tabId: { type: 'string', required: true, description: 'The tab id to switch to.' },
297
+ },
298
+ output: {
299
+ schema: { type: 'object', additionalProperties: false, properties: { switched: { type: 'boolean', required: true } } },
300
+ render: (_args, value) => [{ type: 'text', text: value.switched ? 'Switched.' : 'Tab not found.' }],
301
+ },
302
+ timeoutMs,
303
+ isConcurrencySafe: () => true,
304
+ async execute(args, _exec) {
305
+ const browser = ctx.get('browser');
306
+ if (browser === undefined)
307
+ throw new Error('tool-browser: browser service unavailable');
308
+ const session = await ensureSession(browser);
309
+ await browser.switchTab(session, args.tabId);
310
+ return { switched: true };
311
+ },
312
+ }));
313
+ ctx.tools.register(defineTool({
314
+ name: 'browser_close_tab',
315
+ description: 'Close a tab in the shared browser by id. Closing the active tab activates the next.',
316
+ parameters: {
317
+ tabId: { type: 'string', required: true, description: 'The tab id to close.' },
318
+ },
319
+ output: {
320
+ schema: { type: 'object', additionalProperties: false, properties: { closed: { type: 'boolean', required: true } } },
321
+ render: (_args, value) => [{ type: 'text', text: value.closed ? 'Closed.' : 'Tab not found.' }],
322
+ },
323
+ timeoutMs,
324
+ isConcurrencySafe: () => true,
325
+ async execute(args, _exec) {
326
+ const browser = ctx.get('browser');
327
+ if (browser === undefined)
328
+ throw new Error('tool-browser: browser service unavailable');
329
+ const session = await ensureSession(browser);
330
+ await browser.closeTab(session, args.tabId);
331
+ return { closed: true };
332
+ },
333
+ }));
334
+ ctx.tools.register(defineTool({
335
+ name: 'browser_reset',
336
+ description: 'Close every tab in the shared browser and start fresh with one blank tab.',
337
+ parameters: {},
338
+ output: {
339
+ schema: { type: 'object', additionalProperties: false, properties: { reset: { type: 'boolean', required: true } } },
340
+ render: (_args, value) => [{ type: 'text', text: value.reset ? 'Browser reset.' : 'Failed.' }],
341
+ },
342
+ timeoutMs,
343
+ isConcurrencySafe: () => true,
344
+ async execute(_args, _exec) {
345
+ const browser = ctx.get('browser');
346
+ if (browser === undefined)
347
+ throw new Error('tool-browser: browser service unavailable');
348
+ const session = await ensureSession(browser);
349
+ await browser.reset(session);
350
+ return { reset: true };
351
+ },
352
+ }));
353
+ }
354
+ }
355
+ /** Test hook: clear the plugin-level session (used by tests and on reset). */
356
+ export const internals = {
357
+ get session() { return currentSession; },
358
+ clearSession() { currentSession = undefined; },
359
+ };
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "dsh-builtin-browser",
3
+ "version": "0.1.0",
4
+ "description": "Shared real browser capability for DeepSeek Harness: a visible browser the human watches and can take over, driven by the agent over CDP (snapshot/execute/content/tab management); host-side seam, provider, and browser_* tools",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./browser": {
14
+ "types": "./lib/browser/runtime.d.ts",
15
+ "default": "./lib/browser/runtime.js"
16
+ },
17
+ "./browser-electron": {
18
+ "types": "./lib/browser-electron/entry.d.ts",
19
+ "default": "./lib/browser-electron/entry.js"
20
+ },
21
+ "./tool-browser": {
22
+ "types": "./lib/tool-browser/index.d.ts",
23
+ "default": "./lib/tool-browser/index.js"
24
+ },
25
+ "./cordis.patch.yml": "./cordis.patch.yml",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib",
30
+ "cordis.patch.yml",
31
+ "README.md",
32
+ "README.en.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json"
37
+ },
38
+ "keywords": [
39
+ "dsh-plugin",
40
+ "deepseek-harness",
41
+ "browser",
42
+ "cdp"
43
+ ],
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/wqty123/dsh-browser.git"
48
+ },
49
+ "homepage": "https://github.com/wqty123/dsh-browser",
50
+ "bugs": {
51
+ "url": "https://github.com/wqty123/dsh-browser/issues"
52
+ },
53
+ "engines": {
54
+ "node": ">=22.19"
55
+ },
56
+ "dependencies": {
57
+ "@deepseek-ai/cordis": "^4.0.1",
58
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
59
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
60
+ "@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.1",
61
+ "@deepseek-ai/schemastery": "^3.18.1"
62
+ },
63
+ "peerDependencies": {
64
+ "@deepseek-ai/cordis": "^4.0.1",
65
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
66
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1"
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "^22.0.0",
70
+ "typescript": "^5.5.0"
71
+ },
72
+ "dsh": {
73
+ "bundle": {
74
+ "patch": "./cordis.patch.yml"
75
+ }
76
+ }
77
+ }