@lolkda/dsh-chrome-devtools-mcp 0.0.0-stage → 0.3.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/lib/state.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Persistent plugin state.
3
+ *
4
+ * The launcher and the Host half share one small JSON contract. Everything the
5
+ * browser can change lives here; the profile patch stays static and owns the
6
+ * entry id only.
7
+ */
8
+
9
+ import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises'
10
+ import { homedir } from 'node:os'
11
+ import { dirname, join } from 'node:path'
12
+
13
+ export const DEFAULT_STATE = Object.freeze({ mode: 'latest', version: null })
14
+
15
+ /** A version is a plain release SemVer without a `v` prefix. */
16
+ export const VERSION_PATTERN = /^\d+\.\d+\.\d+$/
17
+
18
+ /** Resolve the DSH home the same way the Host does, with an explicit override for tests. */
19
+ export function resolveDshHome(env = process.env) {
20
+ const configured = typeof env.DSH_HOME === 'string' ? env.DSH_HOME.trim() : ''
21
+ return configured || join(homedir(), '.dsh')
22
+ }
23
+
24
+ /** The single directory this plugin owns under DSH_HOME. */
25
+ export function stateDir(dshHome = resolveDshHome()) {
26
+ return join(dshHome, 'dsh-chrome-devtools-mcp')
27
+ }
28
+
29
+ export function filePaths(dshHome = resolveDshHome()) {
30
+ const dir = stateDir(dshHome)
31
+ return {
32
+ dir,
33
+ state: join(dir, 'state.json'),
34
+ runtime: join(dir, 'runtime.json'),
35
+ cache: join(dir, 'releases-cache.json'),
36
+ }
37
+ }
38
+
39
+ /** A selection is valid only when it is `latest` or a plain release version. */
40
+ export function normalizeSelection(value) {
41
+ if (!value || typeof value !== 'object') throw new Error('版本选择必须是对象')
42
+ if (value.mode === 'latest') return { mode: 'latest', version: null }
43
+ if (value.mode === 'pinned') {
44
+ if (typeof value.version !== 'string' || !VERSION_PATTERN.test(value.version)) {
45
+ throw new Error('固定版本必须是 x.y.z 形式')
46
+ }
47
+ return { mode: 'pinned', version: value.version }
48
+ }
49
+ throw new Error('模式必须是 latest 或 pinned')
50
+ }
51
+
52
+ async function readJson(path) {
53
+ try {
54
+ return JSON.parse(await readFile(path, 'utf8'))
55
+ } catch (error) {
56
+ if (error && error.code === 'ENOENT') return undefined
57
+ throw error
58
+ }
59
+ }
60
+
61
+ async function writeJsonAtomic(path, value) {
62
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 })
63
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`
64
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
65
+ await rename(temporary, path)
66
+ await chmod(path, 0o600).catch(() => {})
67
+ }
68
+
69
+ /** Read the selected version, falling back to `latest` for a missing or malformed file. */
70
+ export async function readState(dshHome) {
71
+ const paths = filePaths(dshHome)
72
+ let raw
73
+ try {
74
+ raw = await readJson(paths.state)
75
+ } catch (error) {
76
+ return { ...DEFAULT_STATE, warning: `读取状态失败:${error.message}` }
77
+ }
78
+ if (raw === undefined) return { ...DEFAULT_STATE }
79
+ try {
80
+ return normalizeSelection(raw)
81
+ } catch (error) {
82
+ return { ...DEFAULT_STATE, warning: `状态文件无效:${error.message}` }
83
+ }
84
+ }
85
+
86
+ /** Validate, then write the selection. Invalid input never touches the existing file. */
87
+ export async function writeState(dshHome, selection) {
88
+ const normalized = normalizeSelection(selection)
89
+ await writeJsonAtomic(filePaths(dshHome).state, {
90
+ ...normalized,
91
+ updatedAt: new Date().toISOString(),
92
+ })
93
+ return normalized
94
+ }
95
+
96
+ /** Read the launcher's runtime record. Missing or malformed records simply mean "unknown". */
97
+ export async function readRuntime(dshHome) {
98
+ const paths = filePaths(dshHome)
99
+ try {
100
+ const raw = await readJson(paths.runtime)
101
+ if (!raw || typeof raw !== 'object') return undefined
102
+ const pid = Number(raw.pid)
103
+ if (!Number.isSafeInteger(pid) || pid <= 0) return undefined
104
+ return {
105
+ pid,
106
+ spec: typeof raw.spec === 'string' ? raw.spec : undefined,
107
+ startedAt: typeof raw.startedAt === 'string' ? raw.startedAt : undefined,
108
+ endedAt: typeof raw.endedAt === 'string' ? raw.endedAt : undefined,
109
+ exitCode: Number.isInteger(raw.exitCode) ? raw.exitCode : undefined,
110
+ }
111
+ } catch {
112
+ return undefined
113
+ }
114
+ }
115
+
116
+ /** The launcher writes this before it spawns npx, and again when the child exits. */
117
+ export async function writeRuntime(dshHome, runtime) {
118
+ await writeJsonAtomic(filePaths(dshHome).runtime, runtime)
119
+ }
120
+
121
+ export async function readReleasesCache(dshHome) {
122
+ const paths = filePaths(dshHome)
123
+ try {
124
+ return await readJson(paths.cache)
125
+ } catch {
126
+ return undefined
127
+ }
128
+ }
129
+
130
+ export async function writeReleasesCache(dshHome, cache) {
131
+ await writeJsonAtomic(filePaths(dshHome).cache, cache)
132
+ }
package/lib/status.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Runtime status inference.
3
+ *
4
+ * mcp-client owns the child process and exposes no status RPC, so the launcher
5
+ * leaves a PID record and the Host combines it with a direct CDP probe.
6
+ */
7
+
8
+ import { readRuntime, readState } from './state.js'
9
+
10
+ export const DEFAULT_BROWSER_URL = 'http://127.0.0.1:9222'
11
+
12
+ export function normalizeBrowserUrl(value) {
13
+ if (typeof value !== 'string' || value.trim() === '') return DEFAULT_BROWSER_URL
14
+ const trimmed = value.trim().replace(/\/+$/, '')
15
+ return /^https?:\/\/[^\s]+$/i.test(trimmed) ? trimmed : DEFAULT_BROWSER_URL
16
+ }
17
+
18
+ /** `EPERM` still means the process exists but belongs to someone else. */
19
+ export function isPidAlive(pid) {
20
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false
21
+ try {
22
+ process.kill(pid, 0)
23
+ return true
24
+ } catch (error) {
25
+ return Boolean(error && error.code === 'EPERM')
26
+ }
27
+ }
28
+
29
+ export async function checkCdp(browserUrl = DEFAULT_BROWSER_URL, { fetchImpl = fetch, timeoutMs = 1500 } = {}) {
30
+ const endpoint = `${normalizeBrowserUrl(browserUrl)}/json/version`
31
+ try {
32
+ const response = await fetchImpl(endpoint, {
33
+ headers: { accept: 'application/json' },
34
+ signal: AbortSignal.timeout(timeoutMs),
35
+ })
36
+ if (!response.ok) {
37
+ return { reachable: false, endpoint, error: `HTTP ${response.status}` }
38
+ }
39
+ const body = await response.json().catch(() => null)
40
+ if (!body || typeof body !== 'object') {
41
+ return { reachable: false, endpoint, error: '响应不是 JSON 对象' }
42
+ }
43
+ return {
44
+ reachable: true,
45
+ endpoint,
46
+ browser: typeof body.Browser === 'string' ? body.Browser : null,
47
+ protocolVersion: typeof body['Protocol-Version'] === 'string' ? body['Protocol-Version'] : null,
48
+ }
49
+ } catch (error) {
50
+ const message = error && error.name === 'TimeoutError' ? '连接超时' : String(error?.message ?? error)
51
+ return { reachable: false, endpoint, error: message }
52
+ }
53
+ }
54
+
55
+ export async function getStatus({ dshHome, browserUrl = DEFAULT_BROWSER_URL, fetchImpl = fetch, timeoutMs = 1500 } = {}) {
56
+ const [configured, runtime] = await Promise.all([readState(dshHome), readRuntime(dshHome)])
57
+ const alive = runtime ? isPidAlive(runtime.pid) : false
58
+ const running = runtime
59
+ ? {
60
+ state: alive ? 'running' : 'stopped',
61
+ pid: runtime.pid,
62
+ spec: runtime.spec ?? null,
63
+ startedAt: runtime.startedAt ?? null,
64
+ endedAt: runtime.endedAt ?? null,
65
+ exitCode: runtime.exitCode ?? null,
66
+ ...(alive ? {} : { reason: 'launcher 进程已退出' }),
67
+ }
68
+ : { state: 'unknown', reason: '暂无启动记录' }
69
+
70
+ return {
71
+ configured: { mode: configured.mode, version: configured.version },
72
+ ...(configured.warning ? { configuredWarning: configured.warning } : {}),
73
+ running,
74
+ cdp: await checkCdp(browserUrl, { fetchImpl, timeoutMs }),
75
+ browserUrl: normalizeBrowserUrl(browserUrl),
76
+ }
77
+ }
package/lib/tunnel.js ADDED
@@ -0,0 +1,86 @@
1
+ /** Commands shown by the settings page. They are generated, never executed. */
2
+
3
+ import { networkInterfaces } from 'node:os'
4
+
5
+ export const DEFAULT_SSH_PORT = 22
6
+ export const DEFAULT_CDP_PORT = 9222
7
+
8
+ /** The first non-loopback IPv4 address, used when the browser is opened on localhost. */
9
+ export function defaultHost() {
10
+ const interfaces = networkInterfaces()
11
+ for (const entries of Object.values(interfaces)) {
12
+ for (const entry of entries ?? []) {
13
+ const family = entry.family === 4 || entry.family === 'IPv4'
14
+ if (family && !entry.internal && entry.address) return entry.address
15
+ }
16
+ }
17
+ return '127.0.0.1'
18
+ }
19
+
20
+ function cleanToken(value, label) {
21
+ const text = typeof value === 'string' ? value.trim() : ''
22
+ if (!text || /[\s"'`$\\]/.test(text)) throw new Error(`${label}包含不允许的字符`)
23
+ return text
24
+ }
25
+
26
+ export function buildSshCommand({
27
+ host,
28
+ user = '<user>',
29
+ sshPort = DEFAULT_SSH_PORT,
30
+ remotePort = DEFAULT_CDP_PORT,
31
+ localPort = DEFAULT_CDP_PORT,
32
+ } = {}) {
33
+ const target = cleanToken(host, '主机地址')
34
+ const account = cleanToken(user, 'SSH 用户名')
35
+ const port = Number(sshPort)
36
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('SSH 端口无效')
37
+ const remote = Number(remotePort)
38
+ const local = Number(localPort)
39
+ if (!Number.isInteger(remote) || remote < 1 || remote > 65535 || !Number.isInteger(local) || local < 1 || local > 65535) {
40
+ throw new Error('转发端口无效')
41
+ }
42
+ const portArgument = port === DEFAULT_SSH_PORT ? '' : ` -p ${port}`
43
+ return `ssh -N -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes${portArgument} -R ${remote}:127.0.0.1:${local} ${account}@${target}`
44
+ }
45
+
46
+ export const WINDOWS_OPENSSH_CLIENT = Object.freeze([
47
+ {
48
+ label: '检查 OpenSSH Client',
49
+ command: "Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Client*'",
50
+ },
51
+ {
52
+ label: '安装 OpenSSH Client',
53
+ command: 'Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0',
54
+ },
55
+ ])
56
+
57
+ export const WINDOWS_OPENSSH_SERVER = Object.freeze([
58
+ {
59
+ label: '检查 OpenSSH Server',
60
+ command: "Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'",
61
+ },
62
+ {
63
+ label: '安装 OpenSSH Server',
64
+ command: 'Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0',
65
+ },
66
+ {
67
+ label: '启动并设为开机自启',
68
+ command: 'Start-Service sshd\nSet-Service -Name sshd -StartupType Automatic',
69
+ },
70
+ {
71
+ label: '放行 22 端口',
72
+ command: "New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22",
73
+ },
74
+ ])
75
+
76
+ export function tunnelInfo({ host = defaultHost(), sshPort = DEFAULT_SSH_PORT, remotePort = DEFAULT_CDP_PORT, localPort = DEFAULT_CDP_PORT } = {}) {
77
+ return {
78
+ host,
79
+ sshPort,
80
+ remotePort,
81
+ localPort,
82
+ commandTemplate: buildSshCommand({ host, user: '<user>', sshPort, remotePort, localPort }),
83
+ windowsClient: WINDOWS_OPENSSH_CLIENT,
84
+ windowsServer: WINDOWS_OPENSSH_SERVER,
85
+ }
86
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Upstream release discovery.
3
+ *
4
+ * GitHub Releases are the user-facing source of truth; the npm registry is the
5
+ * fallback because that is what `npx` will actually resolve. Both are cached so
6
+ * a rate-limited GitHub API never leaves the settings page blank.
7
+ */
8
+
9
+ import { readReleasesCache, writeReleasesCache } from './state.js'
10
+
11
+ export const GITHUB_REPO = 'ChromeDevTools/chrome-devtools-mcp'
12
+ export const GITHUB_RELEASES_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases?per_page=40`
13
+ export const NPM_PACKUMENT_URL = 'https://registry.npmjs.org/chrome-devtools-mcp'
14
+ export const RELEASE_URL_PREFIX = `https://github.com/${GITHUB_REPO}/releases/tag`
15
+ export const CACHE_TTL_MS = 10 * 60 * 1000
16
+ const USER_AGENT = 'dsh-chrome-devtools-mcp/0.3.0'
17
+
18
+ export function parseVersion(value) {
19
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(value))
20
+ if (!match) return undefined
21
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) }
22
+ }
23
+
24
+ export function compareVersions(left, right) {
25
+ const a = parseVersion(left)
26
+ const b = parseVersion(right)
27
+ if (!a || !b) return 0
28
+ if (a.major !== b.major) return a.major - b.major
29
+ if (a.minor !== b.minor) return a.minor - b.minor
30
+ return a.patch - b.patch
31
+ }
32
+
33
+ /** Strip a leading `v` or the project's release prefix and split a SemVer tag. */
34
+ export function normalizeTag(value) {
35
+ if (typeof value !== 'string') return undefined
36
+ const tag = value.trim()
37
+ const match = /^(?:chrome-devtools-mcp-)?v?(\d+\.\d+\.\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(tag)
38
+ if (!match) return undefined
39
+ return {
40
+ version: match[1],
41
+ tag,
42
+ prerelease: match[2] !== undefined,
43
+ }
44
+ }
45
+
46
+ export function sortVersionsDescending(items) {
47
+ return [...items].sort((left, right) => compareVersions(right.version, left.version))
48
+ }
49
+
50
+ export function fromGithubReleases(releases) {
51
+ if (!Array.isArray(releases)) throw new Error('GitHub Releases 响应格式错误')
52
+ const byVersion = new Map()
53
+ let prereleaseCount = 0
54
+ for (const release of releases) {
55
+ if (!release || release.draft === true) continue
56
+ const parsed = normalizeTag(release.tag_name)
57
+ if (!parsed) continue
58
+ if (parsed.prerelease || release.prerelease === true) {
59
+ prereleaseCount += 1
60
+ continue
61
+ }
62
+ const item = {
63
+ version: parsed.version,
64
+ tag: parsed.tag,
65
+ url: typeof release.html_url === 'string' ? release.html_url : `${RELEASE_URL_PREFIX}/${parsed.tag}`,
66
+ publishedAt: typeof release.published_at === 'string' ? release.published_at : null,
67
+ prerelease: false,
68
+ }
69
+ const previous = byVersion.get(item.version)
70
+ if (!previous || String(item.publishedAt ?? '') > String(previous.publishedAt ?? '')) byVersion.set(item.version, item)
71
+ }
72
+ return { items: sortVersionsDescending([...byVersion.values()]), prereleaseCount }
73
+ }
74
+
75
+ export function fromNpmPackument(packument) {
76
+ if (!packument || typeof packument !== 'object' || !packument.versions) {
77
+ throw new Error('npm 版本列表响应格式错误')
78
+ }
79
+ const times = packument.time && typeof packument.time === 'object' ? packument.time : {}
80
+ const items = []
81
+ for (const [version, metadata] of Object.entries(packument.versions)) {
82
+ if (!parseVersion(version)) continue
83
+ if (metadata && metadata.deprecated) continue
84
+ items.push({
85
+ version,
86
+ tag: `v${version}`,
87
+ url: `${RELEASE_URL_PREFIX}/v${version}`,
88
+ publishedAt: typeof times[version] === 'string' ? times[version] : null,
89
+ prerelease: false,
90
+ })
91
+ }
92
+ return { items: sortVersionsDescending(items), prereleaseCount: 0 }
93
+ }
94
+
95
+ async function fetchJson(url, { fetchImpl = fetch, timeoutMs = 6000 } = {}) {
96
+ const response = await fetchImpl(url, {
97
+ headers: {
98
+ accept: 'application/vnd.github+json, application/json',
99
+ 'user-agent': USER_AGENT,
100
+ },
101
+ signal: AbortSignal.timeout(timeoutMs),
102
+ })
103
+ if (!response.ok) throw new Error(`GET ${url} 失败:HTTP ${response.status}`)
104
+ return response.json()
105
+ }
106
+
107
+ export async function fetchGithubVersions(options = {}) {
108
+ const releases = await fetchJson(GITHUB_RELEASES_URL, options)
109
+ return fromGithubReleases(releases)
110
+ }
111
+
112
+ export async function fetchNpmVersions(options = {}) {
113
+ const packument = await fetchJson(NPM_PACKUMENT_URL, options)
114
+ return fromNpmPackument(packument)
115
+ }
116
+
117
+ function responseOf(source, result, fetchedAt, cached, warning) {
118
+ return {
119
+ source,
120
+ cached,
121
+ fetchedAt,
122
+ latest: result.items[0]?.version ?? null,
123
+ items: result.items,
124
+ prereleaseCount: result.prereleaseCount ?? 0,
125
+ ...(warning ? { warning } : {}),
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Resolve the version list, preferring a fresh cache, then GitHub, then npm.
131
+ * A stale cache is still better than an empty selector when both networks fail.
132
+ */
133
+ export async function loadVersions({ dshHome, refresh = false, fetchImpl = fetch, now = Date.now() } = {}) {
134
+ const stored = await readReleasesCache(dshHome)
135
+ const cache = stored && Array.isArray(stored.items) && stored.items.length > 0 ? stored : undefined
136
+ const cachedAt = cache && typeof cache.fetchedAt === 'string' ? Date.parse(cache.fetchedAt) : NaN
137
+ const fresh = Number.isFinite(cachedAt) && now - cachedAt < CACHE_TTL_MS
138
+ if (!refresh && fresh && cache) {
139
+ return {
140
+ ...responseOf('cache', { items: cache.items, prereleaseCount: cache.prereleaseCount }, cache.fetchedAt, true, undefined),
141
+ upstreamSource: cache.source,
142
+ }
143
+ }
144
+
145
+ let warning
146
+ let result
147
+ let source
148
+ try {
149
+ result = await fetchGithubVersions({ fetchImpl, timeoutMs: 6000 })
150
+ if (result.items.length === 0) throw new Error('GitHub Releases 中没有可识别的稳定版本')
151
+ source = 'github'
152
+ } catch (githubError) {
153
+ warning = `GitHub 版本列表不可用:${githubError.message}`
154
+ try {
155
+ result = await fetchNpmVersions({ fetchImpl, timeoutMs: 8000 })
156
+ source = 'npm'
157
+ } catch (npmError) {
158
+ if (cache) {
159
+ return {
160
+ ...responseOf('cache', { items: cache.items, prereleaseCount: cache.prereleaseCount }, cache.fetchedAt ?? null, true, `${warning};npm 回退也失败:${npmError.message};使用缓存`),
161
+ upstreamSource: cache.source,
162
+ }
163
+ }
164
+ throw new Error(`${warning};npm 回退也失败:${npmError.message}`)
165
+ }
166
+ }
167
+
168
+ const fetchedAt = new Date(now).toISOString()
169
+ const payload = responseOf(source, result, fetchedAt, false, warning)
170
+ await writeReleasesCache(dshHome, { ...payload, source }).catch(() => {})
171
+ return payload
172
+ }
173
+
174
+ /** Verify an uncached manual version against npm before writing it. */
175
+ export async function assertVersionExists(version, { fetchImpl = fetch, knownVersions = [], timeoutMs = 5000 } = {}) {
176
+ if (!parseVersion(version)) throw new Error('版本必须是 x.y.z 形式')
177
+ if (knownVersions.includes(version)) return
178
+ const response = await fetchImpl(`${NPM_PACKUMENT_URL}/${encodeURIComponent(version)}`, {
179
+ headers: { accept: 'application/json', 'user-agent': USER_AGENT },
180
+ signal: AbortSignal.timeout(timeoutMs),
181
+ })
182
+ if (response.status === 404) throw new Error(`版本 ${version} 不存在于 npm`)
183
+ if (!response.ok) throw new Error(`无法验证版本 ${version}:HTTP ${response.status}`)
184
+ }
185
+
186
+ /** The stable versions from the last cache, used when a manual version is saved. */
187
+ export async function cachedVersionNames(dshHome) {
188
+ const cache = await readReleasesCache(dshHome)
189
+ return Array.isArray(cache?.items) ? cache.items.map((item) => item.version).filter((version) => parseVersion(version)) : []
190
+ }
package/package.json CHANGED
@@ -1,6 +1,79 @@
1
1
  {
2
2
  "name": "@lolkda/dsh-chrome-devtools-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "Chrome DevTools MCP bundle and settings page for DeepSeek Harness",
5
+ "license": "MIT",
6
+ "author": "lolkda",
7
+ "type": "module",
8
+ "main": "lib/index.js",
9
+ "homepage": "https://github.com/lolkda/dsh-chrome-devtools-mcp#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/lolkda/dsh-chrome-devtools-mcp.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/lolkda/dsh-chrome-devtools-mcp/issues"
16
+ },
17
+ "keywords": [
18
+ "dsh",
19
+ "deepseek-harness",
20
+ "chrome-devtools",
21
+ "mcp",
22
+ "browser",
23
+ "cdp"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org"
28
+ },
29
+ "files": [
30
+ "lib",
31
+ "client",
32
+ "bin",
33
+ "docs",
34
+ "cordis.patch.yml",
35
+ "README.md",
36
+ "CHANGELOG.md",
37
+ "LICENSE"
38
+ ],
39
+ "exports": {
40
+ ".": "./lib/index.js",
41
+ "./client": "./client/client.js",
42
+ "./cordis.patch.yml": "./cordis.patch.yml",
43
+ "./package.json": "./package.json"
44
+ },
45
+ "dsh": {
46
+ "bundle": {
47
+ "patch": "./cordis.patch.yml"
48
+ },
49
+ "client": {
50
+ "platform": "web",
51
+ "inject": [
52
+ "@deepseek-ai/dsh-client-ui-settings"
53
+ ]
54
+ },
55
+ "compatibility": {
56
+ "dsh": ">=0.2.1-rc.2 <0.3.0",
57
+ "profiles": [
58
+ "web"
59
+ ]
60
+ }
61
+ },
62
+ "engines": {
63
+ "node": ">=22",
64
+ "dsh": ">=0.2.1-rc.2 <0.3.0"
65
+ },
66
+ "scripts": {
67
+ "test": "node --check client/client.js && node --test test/*.test.mjs",
68
+ "pack:check": "npm pack --dry-run",
69
+ "verify": "npm test && npm run pack:check",
70
+ "prepublishOnly": "npm test"
71
+ },
72
+ "devDependencies": {
73
+ "yaml": "^2.9.1"
74
+ },
75
+ "chromeDevtoolsMcp": {
76
+ "channel": "latest",
77
+ "source": "https://github.com/ChromeDevTools/chrome-devtools-mcp"
78
+ }
79
+ }