@restregular/dsh-visual-browser 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +123 -2
- package/client.js +835 -0
- package/cordis.patch.yml +17 -0
- package/icon.svg +21 -0
- package/index.js +43 -0
- package/lib/browser.js +963 -0
- package/lib/cdp.js +192 -0
- package/lib/format.js +87 -0
- package/lib/page-scripts.js +528 -0
- package/lib/routes.js +201 -0
- package/lib/tools.js +168 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +69 -4
package/lib/routes.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-origin HTTP API behind the Visual Browser panel.
|
|
3
|
+
*
|
|
4
|
+
* The panel polls `/state` for the timeline and pulls `/frame?seq=` for the
|
|
5
|
+
* picture; user clicks, wheel events and keys go back through the same
|
|
6
|
+
* BrowserManager methods the agent tools use. Frames stay on the host, so a
|
|
7
|
+
* 1 Hz picture never costs a session token.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BrowserError } from './browser.js';
|
|
11
|
+
|
|
12
|
+
const BODY_LIMIT = 64 * 1024;
|
|
13
|
+
|
|
14
|
+
class HttpError extends Error {
|
|
15
|
+
constructor(status, code, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sendJson(res, status, payload) {
|
|
23
|
+
const body = Buffer.from(JSON.stringify(payload), 'utf8');
|
|
24
|
+
res.statusCode = status;
|
|
25
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
26
|
+
res.setHeader('cache-control', 'no-store');
|
|
27
|
+
res.setHeader('content-length', String(body.length));
|
|
28
|
+
res.end(body);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sendFrame(res, frame) {
|
|
32
|
+
if (frame === null || frame === undefined) {
|
|
33
|
+
res.statusCode = 204;
|
|
34
|
+
res.end();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
res.statusCode = 200;
|
|
38
|
+
res.setHeader('content-type', 'image/jpeg');
|
|
39
|
+
res.setHeader('cache-control', 'no-store');
|
|
40
|
+
res.setHeader('content-length', String(frame.jpeg.length));
|
|
41
|
+
res.end(frame.jpeg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function readJsonBody(req) {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
let size = 0;
|
|
47
|
+
for await (const chunk of req) {
|
|
48
|
+
size += chunk.length;
|
|
49
|
+
if (size > BODY_LIMIT) throw new HttpError(413, 'BODY_TOO_LARGE', 'Request body is too large');
|
|
50
|
+
chunks.push(chunk);
|
|
51
|
+
}
|
|
52
|
+
if (size === 0) return {};
|
|
53
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(text);
|
|
56
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
57
|
+
} catch {
|
|
58
|
+
throw new HttpError(400, 'BAD_JSON', 'Request body must be JSON');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function failure(res, error) {
|
|
63
|
+
const status =
|
|
64
|
+
error instanceof HttpError
|
|
65
|
+
? error.status
|
|
66
|
+
: error instanceof BrowserError
|
|
67
|
+
? error.code === 'BAD_ACTION' || error.code === 'BAD_URL' || error.code === 'BAD_KEY'
|
|
68
|
+
? 400
|
|
69
|
+
: error.code === 'TARGET_NOT_FOUND' || error.code === 'STALE_REF'
|
|
70
|
+
? 409
|
|
71
|
+
: 500
|
|
72
|
+
: 500;
|
|
73
|
+
const code = error instanceof HttpError ? error.code : error instanceof BrowserError ? error.code : 'INTERNAL_ERROR';
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
sendJson(res, status, { ok: false, error: { code, message } });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Handle one request under `prefix`. Exported so a smoke test can drive it with
|
|
80
|
+
* a plain request/response pair and a stub manager.
|
|
81
|
+
*/
|
|
82
|
+
export async function handle(manager, prefix, req, res) {
|
|
83
|
+
const method = String(req.method ?? 'GET').toUpperCase();
|
|
84
|
+
let url;
|
|
85
|
+
try {
|
|
86
|
+
url = new URL(String(req.url ?? '/'), 'http://127.0.0.1');
|
|
87
|
+
} catch {
|
|
88
|
+
sendJson(res, 400, { ok: false, error: { code: 'BAD_URL', message: 'Malformed request URL' } });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let route = url.pathname;
|
|
92
|
+
if (route.startsWith(prefix)) route = route.slice(prefix.length);
|
|
93
|
+
if (route === '' || route === '/') route = '/';
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (method === 'GET' && route === '/') {
|
|
97
|
+
sendJson(res, 200, { ok: true, name: 'dsh-visual-browser', state: manager.state() });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (method === 'GET' && route === '/state') {
|
|
101
|
+
sendJson(res, 200, { ok: true, state: manager.state() });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (method === 'GET' && route === '/frame') {
|
|
105
|
+
const seq = Number(url.searchParams.get('seq') ?? '0');
|
|
106
|
+
sendFrame(res, manager.frameFor(seq));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (method === 'GET' && route === '/live') {
|
|
110
|
+
const state = await manager.refreshFrame();
|
|
111
|
+
const frame = manager.frameFor(state.frameSeq);
|
|
112
|
+
sendFrame(res, frame);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (method === 'POST' && route === '/start') {
|
|
116
|
+
const body = await readJsonBody(req);
|
|
117
|
+
const target = typeof body.url === 'string' ? body.url.trim() : '';
|
|
118
|
+
if (target !== '') await manager.open(target);
|
|
119
|
+
else await manager.ensureStarted();
|
|
120
|
+
sendJson(res, 200, { ok: true, state: manager.state() });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (method === 'POST' && route === '/stop') {
|
|
124
|
+
await manager.close();
|
|
125
|
+
sendJson(res, 200, { ok: true, state: manager.state() });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (method === 'POST' && route === '/capture') {
|
|
129
|
+
const state = await manager.refreshFrame();
|
|
130
|
+
sendJson(res, 200, { ok: true, state });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (method === 'POST' && route === '/click') {
|
|
134
|
+
const body = await readJsonBody(req);
|
|
135
|
+
await manager.clickAt({ x: body.x, y: body.y });
|
|
136
|
+
sendJson(res, 200, { ok: true, state: manager.state() });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (method === 'POST' && route === '/wheel') {
|
|
140
|
+
const body = await readJsonBody(req);
|
|
141
|
+
await manager.wheelAt({ deltaY: body.deltaY, deltaX: body.deltaX });
|
|
142
|
+
sendJson(res, 200, { ok: true, state: manager.state() });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (method === 'POST' && route === '/key') {
|
|
146
|
+
const body = await readJsonBody(req);
|
|
147
|
+
await manager.keyFromUser(String(body.key ?? 'Enter'));
|
|
148
|
+
sendJson(res, 200, { ok: true, state: manager.state() });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (method === 'POST' && route === '/nav') {
|
|
152
|
+
const body = await readJsonBody(req);
|
|
153
|
+
const action = String(body.action ?? '');
|
|
154
|
+
if (!['back', 'forward', 'reload'].includes(action)) throw new HttpError(400, 'BAD_ACTION', 'action must be back, forward, or reload');
|
|
155
|
+
await manager.act({ action });
|
|
156
|
+
sendJson(res, 200, { ok: true, state: manager.state() });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
sendJson(res, 404, { ok: false, error: { code: 'NOT_FOUND', message: `No route for ${method} ${route}` } });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
manager.noteError?.(error);
|
|
162
|
+
failure(res, error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Mount the API on the host web server; a deployment without it keeps the tools. */
|
|
167
|
+
export function mountRoutes(ctx, manager, options = {}) {
|
|
168
|
+
const prefix = typeof options.prefix === 'string' && options.prefix !== '' ? options.prefix : '/visual-browser/api';
|
|
169
|
+
try {
|
|
170
|
+
ctx.inject(['webServer'], (scoped) => {
|
|
171
|
+
const webServer = scoped.get('webServer');
|
|
172
|
+
if (webServer === undefined || typeof webServer.register !== 'function') {
|
|
173
|
+
ctx.logger?.info?.('[visual-browser] webServer unavailable; panel API not mounted');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
scoped.effect(
|
|
178
|
+
() =>
|
|
179
|
+
webServer.register({
|
|
180
|
+
kind: 'prefix',
|
|
181
|
+
path: prefix,
|
|
182
|
+
handler: (req, res) =>
|
|
183
|
+
handle(manager, prefix, req, res).catch((error) => {
|
|
184
|
+
try {
|
|
185
|
+
failure(res, error);
|
|
186
|
+
} catch {
|
|
187
|
+
/* response already sent */
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
}),
|
|
191
|
+
'dsh-visual-browser: api route',
|
|
192
|
+
);
|
|
193
|
+
ctx.logger?.info?.(`[visual-browser] panel API mounted at ${prefix}`);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
ctx.logger?.warn?.(`[visual-browser] could not mount ${prefix}: ${error?.message ?? error}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
ctx.logger?.warn?.(`[visual-browser] route mount failed: ${error?.message ?? error}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-facing tools.
|
|
3
|
+
*
|
|
4
|
+
* Five tools cover the loop: open, look, act, read, screenshot. Each one
|
|
5
|
+
* returns the compact numbered view again, so a model never has to re-read a
|
|
6
|
+
* page and never sees the DOM. Every action is also mirrored into the panel
|
|
7
|
+
* timeline by the manager itself.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BrowserError } from './browser.js';
|
|
11
|
+
import { formatSnapshot } from './format.js';
|
|
12
|
+
|
|
13
|
+
const TEXT_OUTPUT = {
|
|
14
|
+
schema: { type: 'string' },
|
|
15
|
+
render(_args, value) {
|
|
16
|
+
return [{ type: 'text', text: String(value ?? '') }];
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function hintFor(error) {
|
|
21
|
+
if (!(error instanceof BrowserError)) return error instanceof Error ? error.message : String(error);
|
|
22
|
+
switch (error.code) {
|
|
23
|
+
case 'BROWSER_NOT_FOUND':
|
|
24
|
+
return `${error.message} Install Google Chrome or Microsoft Edge, or set \`executablePath\` in the dsh-visual-browser config in cordis.patch.yml.`;
|
|
25
|
+
case 'STALE_REF':
|
|
26
|
+
return error.message;
|
|
27
|
+
case 'NOT_RUNNING':
|
|
28
|
+
return `${error.message} Call browser_open first.`;
|
|
29
|
+
default:
|
|
30
|
+
return error.message;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function snapshotText(snapshot, manager) {
|
|
35
|
+
return formatSnapshot(snapshot, { maxElements: manager.config.maxElements });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function browserTools(manager) {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
name: 'browser_open',
|
|
42
|
+
description:
|
|
43
|
+
'Open a URL in the visual browser: a real Chrome that the user can watch in the Visual Browser panel. Launches the browser on first use and returns the page\'s numbered view (interactive elements, headings, a short text excerpt) instead of the DOM, so the result stays small. Use the [n] numbers with browser_act.',
|
|
44
|
+
parameters: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
url: { type: 'string', description: 'Full URL, or a bare domain such as example.com (https is assumed).' },
|
|
48
|
+
},
|
|
49
|
+
required: ['url'],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
output: TEXT_OUTPUT,
|
|
53
|
+
async execute(args) {
|
|
54
|
+
try {
|
|
55
|
+
const snapshot = await manager.open(String(args?.url ?? ''));
|
|
56
|
+
return `OK: opened ${snapshot.url}\n${snapshotText(snapshot, manager)}`;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(hintFor(error));
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'browser_see',
|
|
64
|
+
description:
|
|
65
|
+
'Take a fresh numbered view of the current page: visible interactive elements with [n] numbers, headings and a short text excerpt. Element numbers are reassigned every time, so use numbers from the newest view only. Call this after the page changed on its own, or when an action result looked wrong.',
|
|
66
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
67
|
+
output: TEXT_OUTPUT,
|
|
68
|
+
async execute() {
|
|
69
|
+
try {
|
|
70
|
+
const snapshot = await manager.snapshot();
|
|
71
|
+
return snapshotText(snapshot, manager);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(hintFor(error));
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'browser_act',
|
|
79
|
+
description:
|
|
80
|
+
'Act on the current page, then return the new numbered view. Target an element with a [n] number from the newest view (preferred) or a CSS selector. Supported actions: click, type (ref + text, optional submit to press Enter), key, hover, scroll, select, back, forward, reload, wait, close. Every action is mirrored into the Visual Browser panel for the user.',
|
|
81
|
+
parameters: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
action: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
enum: ['click', 'type', 'key', 'hover', 'scroll', 'select', 'back', 'forward', 'reload', 'wait', 'close'],
|
|
87
|
+
description: 'What to do.',
|
|
88
|
+
},
|
|
89
|
+
ref: { type: 'number', description: 'Element number [n] from the newest view.' },
|
|
90
|
+
selector: { type: 'string', description: 'CSS selector, for when no numbered view is available.' },
|
|
91
|
+
text: { type: 'string', description: 'Text to type (type) or the option label/value (select).' },
|
|
92
|
+
key: { type: 'string', description: 'Key for action=key, such as Enter, Tab, Escape, ArrowDown, Control+a.' },
|
|
93
|
+
direction: { type: 'string', enum: ['up', 'down', 'left', 'right'], description: 'Scroll direction (default down).' },
|
|
94
|
+
amount: { type: 'number', description: 'Scroll amount in viewport heights (default 1).' },
|
|
95
|
+
value: { type: 'string', description: 'Option value or label for action=select.' },
|
|
96
|
+
ms: { type: 'number', description: 'Milliseconds for action=wait (default 800).' },
|
|
97
|
+
clear: { type: 'boolean', description: 'type: replace the field content (default true); false appends.' },
|
|
98
|
+
submit: { type: 'boolean', description: 'type: press Enter afterwards.' },
|
|
99
|
+
},
|
|
100
|
+
required: ['action'],
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
},
|
|
103
|
+
output: TEXT_OUTPUT,
|
|
104
|
+
async execute(args) {
|
|
105
|
+
try {
|
|
106
|
+
const result = await manager.act(args ?? {});
|
|
107
|
+
if (!result.snapshot) return `OK: ${result.summary}`;
|
|
108
|
+
return `OK: ${result.summary}\n${snapshotText(result.snapshot, manager)}`;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw new Error(hintFor(error));
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'browser_text',
|
|
116
|
+
description:
|
|
117
|
+
'Read text instead of guessing: omit ref/selector for the whole visible page text, or pass a [n] number to read one element (for a link it also returns the href). Use max_chars to raise the cap when the default excerpt is cut.',
|
|
118
|
+
parameters: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
ref: { type: 'number', description: 'Element number [n] from the newest view.' },
|
|
122
|
+
selector: { type: 'string', description: 'CSS selector instead of a number.' },
|
|
123
|
+
max_chars: { type: 'number', description: 'Character cap (default 4000).' },
|
|
124
|
+
},
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
},
|
|
127
|
+
output: TEXT_OUTPUT,
|
|
128
|
+
async execute(args) {
|
|
129
|
+
try {
|
|
130
|
+
const value = await manager.readText({
|
|
131
|
+
ref: args?.ref,
|
|
132
|
+
selector: args?.selector,
|
|
133
|
+
maxChars: args?.max_chars,
|
|
134
|
+
});
|
|
135
|
+
const header = `URL: ${value.url}${value.title ? `\nTITLE: ${value.title}` : ''}\nKIND: ${value.kind} · ${value.length} chars${value.truncated ? ' (truncated)' : ''}`;
|
|
136
|
+
const extra = [];
|
|
137
|
+
if (value.href) extra.push(`HREF: ${value.href}`);
|
|
138
|
+
if (value.value) extra.push(`VALUE: ${value.value}`);
|
|
139
|
+
return [header, ...extra, '', value.text || '(empty)'].join('\n');
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new Error(hintFor(error));
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'browser_screenshot',
|
|
147
|
+
description:
|
|
148
|
+
'Save a screenshot of the current page to a file and return its absolute path. Use it when pixel detail matters (layout, images, visual verification) and then open the file with read_image. Defaults to a temp directory and png.',
|
|
149
|
+
parameters: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
path: { type: 'string', description: 'Absolute path to write; defaults to a temp file.' },
|
|
153
|
+
format: { type: 'string', enum: ['png', 'jpeg'], description: 'Image format (default png).' },
|
|
154
|
+
},
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
},
|
|
157
|
+
output: TEXT_OUTPUT,
|
|
158
|
+
async execute(args) {
|
|
159
|
+
try {
|
|
160
|
+
const saved = await manager.screenshot({ path: args?.path, format: args?.format });
|
|
161
|
+
return `Saved ${saved.format.toUpperCase()} screenshot (${Math.round(saved.bytes / 1024)} KiB): ${saved.path}\nOpen it with read_image when the visual detail matters.`;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw new Error(hintFor(error));
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
}
|
package/locale/en.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "Visual Browser",
|
|
4
|
+
"description": "A real Chrome for the Agent plus a live view for you: the Agent reads a numbered, token-light list of interactive elements instead of the raw DOM, and every action lands in a panel with the page frame, a replayable timeline, and click-through control."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@restregular/dsh-visual-browser",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Visual browser for DeepSeek Harness: the Agent drives a real Chrome through a token-light numbered view of interactive elements (never the raw DOM), while the Web UI streams the live page frame, the click point of every action, and a replayable operation timeline the user can also take over by clicking the picture.",
|
|
5
|
+
"icon": "./icon.svg",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./client": "./client.js",
|
|
11
|
+
"./locale/*.json": "./locale/*.json",
|
|
12
|
+
"./icon": "./icon.svg",
|
|
13
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"index.js",
|
|
18
|
+
"client.js",
|
|
19
|
+
"lib/",
|
|
20
|
+
"locale/*.json",
|
|
21
|
+
"cordis.patch.yml",
|
|
22
|
+
"icon.svg",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"dsh": {
|
|
27
|
+
"bundle": {
|
|
28
|
+
"patch": "./cordis.patch.yml"
|
|
29
|
+
},
|
|
30
|
+
"client": {
|
|
31
|
+
"platform": "web",
|
|
32
|
+
"immediately": true,
|
|
33
|
+
"inject": [
|
|
34
|
+
"@deepseek-ai/dsh-client-locale",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/RestRegular/dsh-visual-browser.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/RestRegular/dsh-visual-browser#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/RestRegular/dsh-visual-browser/issues"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"dsh-plugin",
|
|
50
|
+
"deepseek-harness",
|
|
51
|
+
"deepseek",
|
|
52
|
+
"browser",
|
|
53
|
+
"browser-automation",
|
|
54
|
+
"cdp",
|
|
55
|
+
"chrome-devtools-protocol",
|
|
56
|
+
"screenshot",
|
|
57
|
+
"visualization"
|
|
58
|
+
],
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
61
|
+
"registry": "https://registry.npmjs.org/"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=22.13.0"
|
|
65
|
+
},
|
|
66
|
+
"license": "MIT",
|
|
67
|
+
"author": "RestRegular",
|
|
68
|
+
"scripts": {
|
|
69
|
+
"test": "node --check index.js && node --check client.js && node --check lib/browser.js && node test/snapshot.test.mjs && node test/scripts.test.mjs && node test/package.test.mjs"
|
|
70
|
+
}
|
|
71
|
+
}
|