dsh-github-router 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/lib/tunnel.js ADDED
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Zero-dependency proxy transport: HTTPS through an HTTP CONNECT tunnel
3
+ * (plus plain HTTP via absolute-URI proxying), built on node:http/tls.
4
+ *
5
+ * Why not global fetch + undici ProxyAgent: this plugin must install
6
+ * OFFLINE on machines whose package registry access is exactly what is
7
+ * broken. The tunnel client is small, carries its own cooperative timeout
8
+ * and caller abort, enforces the byte cap, and sends
9
+ * `accept-encoding: identity` so responses never need decompression.
10
+ * @module dsh-github-router/tunnel
11
+ */
12
+ import http from 'node:http'
13
+ import tls from 'node:tls'
14
+
15
+ const MAX_REDIRECTS = 3
16
+
17
+ /** Basic auth header value for a proxy URL carrying credentials, else null. */
18
+ export function proxyAuthHeader(proxyUrl) {
19
+ if (proxyUrl.username === '' && proxyUrl.password === '') return null
20
+ const user = decodeURIComponent(proxyUrl.username)
21
+ const pass = decodeURIComponent(proxyUrl.password)
22
+ if (user === '' && pass === '') return null
23
+ return 'Basic ' + Buffer.from(`${user}:${pass}`, 'utf8').toString('base64')
24
+ }
25
+
26
+ /** The CONNECT authority line for a target URL. */
27
+ export function connectTarget(target) {
28
+ return `${target.hostname}:${target.port !== '' && target.port !== null ? target.port : 443}`
29
+ }
30
+
31
+ /** Build the request-line + headers block written onto the tunnel socket. */
32
+ export function buildRequestHead(target, method, headers) {
33
+ const path = target.pathname + target.search
34
+ const lines = [`${method} ${path} HTTP/1.1`, `host: ${target.host}`]
35
+ for (const [key, value] of Object.entries(headers)) {
36
+ if (value === undefined || value === null) continue
37
+ lines.push(`${key}: ${value}`)
38
+ }
39
+ lines.push('accept-encoding: identity', 'connection: close', '', '')
40
+ return lines.join('\r\n')
41
+ }
42
+
43
+ /** Parse an HTTP response head into { status, headers }. */
44
+ export function parseResponseHead(headText) {
45
+ const lines = String(headText).split('\r\n')
46
+ const match = /^HTTP\/\d(?:\.\d)? (\d{3})/.exec(lines[0] ?? '')
47
+ if (match === null) throw new Error(`bad HTTP status line: ${(lines[0] ?? '').slice(0, 60)}`)
48
+ const headers = {}
49
+ for (const line of lines.slice(1)) {
50
+ const colon = line.indexOf(':')
51
+ if (colon > 0) headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim()
52
+ }
53
+ return { status: Number(match[1]), headers }
54
+ }
55
+
56
+ function openTunnel(target, proxyUrl, timeoutMs, signal) {
57
+ return new Promise((resolve, reject) => {
58
+ const auth = proxyAuthHeader(proxyUrl)
59
+ const request = http.request({
60
+ host: proxyUrl.hostname,
61
+ port: proxyUrl.port !== '' ? Number(proxyUrl.port) : 80,
62
+ method: 'CONNECT',
63
+ path: connectTarget(target),
64
+ headers: {
65
+ host: connectTarget(target),
66
+ ...(auth !== null ? { 'proxy-authorization': auth } : {}),
67
+ },
68
+ })
69
+ const onAbort = () => request.destroy(new DOMException('aborted', 'AbortError'))
70
+ if (signal !== undefined && signal !== null) {
71
+ if (signal.aborted) onAbort()
72
+ else signal.addEventListener('abort', onAbort, { once: true })
73
+ }
74
+ const timer = setTimeout(() => request.destroy(new DOMException('timed out', 'TimeoutError')), timeoutMs)
75
+ if (typeof timer.unref === 'function') timer.unref()
76
+ request.on('connect', (response, socket) => {
77
+ clearTimeout(timer)
78
+ if (response.statusCode !== 200) {
79
+ socket.destroy()
80
+ reject(new Error(`proxy CONNECT failed: HTTP ${response.statusCode}`))
81
+ return
82
+ }
83
+ const tlsSocket = tls.connect({ socket, servername: target.hostname }, () => {
84
+ if (signal !== undefined && signal !== null) signal.removeEventListener('abort', onAbort)
85
+ resolve(tlsSocket)
86
+ })
87
+ tlsSocket.on('error', reject)
88
+ })
89
+ request.on('error', (error) => {
90
+ clearTimeout(timer)
91
+ if (signal !== undefined && signal !== null) signal.removeEventListener('abort', onAbort)
92
+ reject(error)
93
+ })
94
+ request.end()
95
+ })
96
+ }
97
+
98
+ function readResponse(socket, maxBytes) {
99
+ return new Promise((resolve, reject) => {
100
+ const chunks = []
101
+ let total = 0
102
+ let headerBuf = Buffer.alloc(0)
103
+ let headerDone = false
104
+ let settled = false
105
+ let status = 0
106
+ let headers = {}
107
+ let truncated = false
108
+ const finish = (error) => {
109
+ if (settled) return
110
+ settled = true
111
+ socket.destroy()
112
+ if (error !== undefined) reject(error)
113
+ else resolve({ status, ok: status >= 200 && status < 300, body: Buffer.concat(chunks), truncated, headers })
114
+ }
115
+ const onBody = (buf) => {
116
+ total += buf.length
117
+ if (total > maxBytes) {
118
+ const keep = maxBytes - (total - buf.length)
119
+ if (keep > 0) chunks.push(buf.subarray(0, keep))
120
+ truncated = true
121
+ finish()
122
+ return
123
+ }
124
+ chunks.push(buf)
125
+ }
126
+ socket.on('data', (buf) => {
127
+ if (!headerDone) {
128
+ headerBuf = Buffer.concat([headerBuf, buf])
129
+ const idx = headerBuf.indexOf('\r\n\r\n')
130
+ if (idx === -1) {
131
+ if (headerBuf.length > 65536) finish(new Error('response headers too large'))
132
+ return
133
+ }
134
+ try {
135
+ const head = parseResponseHead(headerBuf.subarray(0, idx).toString('latin1'))
136
+ status = head.status
137
+ headers = head.headers
138
+ } catch (error) {
139
+ finish(error)
140
+ return
141
+ }
142
+ headerDone = true
143
+ const rest = headerBuf.subarray(idx + 4)
144
+ if (rest.length > 0) onBody(rest)
145
+ return
146
+ }
147
+ onBody(buf)
148
+ })
149
+ socket.on('end', () => {
150
+ if (headerDone) finish()
151
+ else finish(new Error('connection closed before response headers'))
152
+ })
153
+ socket.on('error', (error) => finish(error))
154
+ })
155
+ }
156
+
157
+ /**
158
+ * One proxied request: CONNECT tunnel for https targets, absolute-URI for
159
+ * http targets. Returns the same shape as the direct fetch path.
160
+ */
161
+ export async function tunnelRequest(targetUrl, options) {
162
+ const { method = 'GET', headers = {}, timeoutMs = 15000, proxy, signal, maxBytes = 1048576 } = options
163
+ let target = new URL(targetUrl)
164
+ let proxyUrl
165
+ try {
166
+ proxyUrl = new URL(String(proxy))
167
+ } catch {
168
+ // Never echo credentials: display only scheme://host:port.
169
+ const display = String(proxy).replace(/\/\/([^/@]+)@/, '//')
170
+ throw new Error(`invalid proxy URL: ${display.slice(0, 120)}`)
171
+ }
172
+
173
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {
174
+ if (signal !== undefined && signal !== null && signal.aborted) throw new DOMException('aborted', 'AbortError')
175
+
176
+ let socket
177
+ if (target.protocol === 'https:') {
178
+ socket = await openTunnel(target, proxyUrl, timeoutMs, signal)
179
+ } else if (target.protocol === 'http:') {
180
+ const auth = proxyAuthHeader(proxyUrl)
181
+ socket = await new Promise((resolve, reject) => {
182
+ const request = http.request({
183
+ host: proxyUrl.hostname,
184
+ port: proxyUrl.port !== '' ? Number(proxyUrl.port) : 80,
185
+ method,
186
+ path: target.toString(),
187
+ headers: {
188
+ host: target.host,
189
+ ...(auth !== null ? { 'proxy-authorization': auth } : {}),
190
+ ...headers,
191
+ },
192
+ })
193
+ const onAbort = () => request.destroy(new DOMException('aborted', 'AbortError'))
194
+ if (signal !== undefined && signal !== null) {
195
+ if (signal.aborted) onAbort()
196
+ else signal.addEventListener('abort', onAbort, { once: true })
197
+ }
198
+ const timer = setTimeout(() => request.destroy(new DOMException('timed out', 'TimeoutError')), timeoutMs)
199
+ if (typeof timer.unref === 'function') timer.unref()
200
+ request.on('response', (response) => {
201
+ clearTimeout(timer)
202
+ if (signal !== undefined && signal !== null) signal.removeEventListener('abort', onAbort)
203
+ resolve({ stream: response, status: response.statusCode ?? 0, headers: response.headers })
204
+ })
205
+ request.on('error', (error) => {
206
+ clearTimeout(timer)
207
+ if (signal !== undefined && signal !== null) signal.removeEventListener('abort', onAbort)
208
+ reject(error)
209
+ })
210
+ request.end()
211
+ }).then(({ stream, status, headers }) => {
212
+ // Collect the http-over-proxy body (rarely used by this plugin).
213
+ return new Promise((resolve, reject) => {
214
+ const chunks = []
215
+ let total = 0
216
+ let truncated = false
217
+ stream.on('data', (buf) => {
218
+ total += buf.length
219
+ if (total > maxBytes) {
220
+ const keep = maxBytes - (total - buf.length)
221
+ if (keep > 0) chunks.push(buf.subarray(0, keep))
222
+ truncated = true
223
+ stream.destroy()
224
+ } else chunks.push(buf)
225
+ })
226
+ stream.on('end', () => resolve({ status, ok: status >= 200 && status < 300, body: Buffer.concat(chunks), truncated, headers }))
227
+ stream.on('error', reject)
228
+ })
229
+ })
230
+ return socket
231
+ } else {
232
+ throw new Error(`unsupported target protocol: ${target.protocol}`)
233
+ }
234
+
235
+ if (target.protocol === 'https:') {
236
+ socket.write(buildRequestHead(target, method, headers))
237
+ const response = await readResponse(socket, maxBytes)
238
+ const location = response.headers.location
239
+ if (location !== undefined && [301, 302, 303, 307, 308].includes(response.status) && hop < MAX_REDIRECTS) {
240
+ target = new URL(location, target)
241
+ continue
242
+ }
243
+ return { status: response.status, ok: response.ok, body: response.body, truncated: response.truncated, headers: response.headers, url: target.toString() }
244
+ }
245
+ }
246
+ throw new Error(`too many redirects (${MAX_REDIRECTS})`)
247
+ }
package/lib/util.js ADDED
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Shared helpers: session cwd, subprocess execution (argv arrays only),
3
+ * input validation guards, and text shaping.
4
+ * @module dsh-github-router/util
5
+ */
6
+ import { createHash } from 'node:crypto'
7
+ import { homedir } from 'node:os'
8
+ import { join } from 'node:path'
9
+
10
+ export const DSH_HOME = process.env.DSH_HOME || join(homedir(), '.dsh')
11
+ export const STORAGE_DIR = join(DSH_HOME, 'storages', 'dsh-github-router')
12
+ export const CACHE_DIR = join(STORAGE_DIR, 'cache')
13
+ export const GIT_CACHE_DIR = join(STORAGE_DIR, 'git')
14
+
15
+ /** Session workspace cwd, with a process-cwd fallback. */
16
+ export function sessionCwd(exec) {
17
+ const header = exec && exec.agent && exec.agent.session && exec.agent.session.header
18
+ if (header && typeof header.cwd === 'string' && header.cwd.length > 0) return header.cwd
19
+ return process.cwd()
20
+ }
21
+
22
+ export function sha1(text) {
23
+ return createHash('sha1').update(String(text), 'utf8').digest('hex')
24
+ }
25
+
26
+ export function sleep(ms) {
27
+ return new Promise((resolve) => setTimeout(resolve, ms))
28
+ }
29
+
30
+ export function isAbortError(error) {
31
+ return (
32
+ (error instanceof DOMException && error.name === 'AbortError') ||
33
+ (error !== null && typeof error === 'object' && error.name === 'AbortError')
34
+ )
35
+ }
36
+
37
+ // ------------------------------------------------------------------ guards
38
+
39
+ /** GitHub owner/repo names: alphanumerics plus . _ -, not empty. */
40
+ const OWNER_REPO_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
41
+
42
+ export function guardOwnerRepo(value, label) {
43
+ const text = String(value ?? '')
44
+ if (text.length === 0 || text.length > 100 || !OWNER_REPO_RE.test(text)) {
45
+ throw new Error(`${label} must be a valid GitHub owner or repository name (got "${text}")`)
46
+ }
47
+ return text
48
+ }
49
+
50
+ export function guardPrNumber(value, label = 'number') {
51
+ const n = Number(value)
52
+ if (!Number.isInteger(n) || n < 1 || n > 100_000_000) {
53
+ throw new Error(`${label} must be a positive integer (got "${String(value)}")`)
54
+ }
55
+ return n
56
+ }
57
+
58
+ /**
59
+ * api.github.com relative paths only: starts with '/', no query/fragment,
60
+ * no control characters or spaces, no '..' segments. GET only — enforced
61
+ * by the client, which has no other verbs.
62
+ */
63
+ const API_PATH_RE = /^\/[A-Za-z0-9._~%/-]*$/
64
+
65
+ export function guardApiPath(path) {
66
+ const text = String(path ?? '')
67
+ if (text.length === 0 || text.length > 2048 || !API_PATH_RE.test(text)) {
68
+ throw new Error(`path must be a plain api.github.com relative path starting with "/" (got "${text}")`)
69
+ }
70
+ if (text.split('/').includes('..') || text.includes('%2e%2e') || text.toLowerCase().includes('%2e%2e')) {
71
+ throw new Error(`path must not contain ".." segments (got "${text}")`)
72
+ }
73
+ return text
74
+ }
75
+
76
+ /**
77
+ * Repo file path: relative, no leading slash, no ".." segments, no control
78
+ * characters, bounded length. It is passed to subprocesses as a single argv
79
+ * element (never interpolated into a shell command).
80
+ */
81
+ const FILE_PATH_RE = /^[^\u0000-\u001f\u007f\\:*?"<>|]+$/
82
+
83
+ export function guardFilePath(path) {
84
+ const text = String(path ?? '')
85
+ if (text.length === 0 || text.length > 1024) {
86
+ throw new Error(`path must be a non-empty relative repository file path (got "${text}")`)
87
+ }
88
+ if (text.startsWith('/') || !FILE_PATH_RE.test(text)) {
89
+ throw new Error(`path must be a relative repository file path without control characters (got "${text}")`)
90
+ }
91
+ if (text.split('/').includes('..')) {
92
+ throw new Error(`path must not contain ".." segments (got "${text}")`)
93
+ }
94
+ return text
95
+ }
96
+
97
+ export function guardRef(value, label = 'ref') {
98
+ const text = String(value ?? '').trim()
99
+ if (text.length === 0 || text.length > 512 || text.startsWith('-') || /[\u0000-\u001f\u007f~^:?*[\\]/.test(text) || text.includes('..')) {
100
+ throw new Error(`${label} must be a plain git ref name (branch, tag, or full commit sha) (got "${text}")`)
101
+ }
102
+ return text
103
+ }
104
+
105
+ export function guardByteCount(value, label, fallback, min, max) {
106
+ if (value === undefined || value === null) return fallback
107
+ const n = Number(value)
108
+ if (!Number.isInteger(n) || n < min || n > max) {
109
+ throw new Error(`${label} must be an integer between ${min} and ${max} (got "${String(value)}")`)
110
+ }
111
+ return n
112
+ }
113
+
114
+ // -------------------------------------------------------------- subprocess
115
+
116
+ const DEFAULT_STDOUT_CAP = 1_048_576
117
+ const DEFAULT_STDERR_CAP = 262_144
118
+
119
+ /**
120
+ * Run one subprocess to completion through the platform subprocess service
121
+ * (host token — the same channel the pyenv/graphlint plugins use), with
122
+ * argv arrays and no shell interpolation anywhere.
123
+ * @returns { exitCode, ok, timedOut, stdout: {text, truncated}, stderr: {...}, spawnError }
124
+ */
125
+ export async function runCaptured(subprocess, spec) {
126
+ const {
127
+ argv,
128
+ cwd,
129
+ env,
130
+ graceMs = 15_000,
131
+ signal,
132
+ maxStdout = DEFAULT_STDOUT_CAP,
133
+ maxStderr = DEFAULT_STDERR_CAP,
134
+ onSpawn,
135
+ } = spec
136
+ let handle
137
+ try {
138
+ handle = subprocess.spawn({
139
+ argv,
140
+ ...(cwd !== undefined ? { cwd } : {}),
141
+ ...(env !== undefined ? { env } : {}),
142
+ stdio: { stdin: 'ignore', stdout: { maxBytes: maxStdout }, stderr: { maxBytes: maxStderr } },
143
+ graceMs,
144
+ ...(signal !== undefined ? { signal } : {}),
145
+ })
146
+ } catch (error) {
147
+ return { exitCode: null, ok: false, timedOut: false, spawnError: String(error && error.message ? error.message : error), stdout: { text: '', truncated: false }, stderr: { text: '', truncated: false } }
148
+ }
149
+ if (onSpawn !== undefined) onSpawn(handle)
150
+ let outcome
151
+ try {
152
+ outcome = await handle.done
153
+ } catch (error) {
154
+ return { exitCode: null, ok: false, timedOut: false, spawnError: String(error && error.message ? error.message : error), stdout: { text: '', truncated: false }, stderr: { text: '', truncated: false } }
155
+ }
156
+ const shape = (reader) => {
157
+ if (reader === undefined || reader === null) return { text: '', truncated: false }
158
+ const read = reader.readFrom(0)
159
+ return { text: typeof read.text === 'string' ? read.text : '', truncated: !!read.lossy }
160
+ }
161
+ const exitCode = outcome.exitCode === undefined || outcome.exitCode === null ? null : outcome.exitCode
162
+ return {
163
+ exitCode,
164
+ ok: exitCode === 0,
165
+ timedOut: outcome.exitCode === null || outcome.exitCode === undefined,
166
+ spawnError: null,
167
+ stdout: shape(handle.collected && handle.collected.stdout),
168
+ stderr: shape(handle.collected && handle.collected.stderr),
169
+ }
170
+ }
171
+
172
+ /** Env for read-only git/gh children: never prompts, no color, plus the ambient env. */
173
+ export function childEnv(extra) {
174
+ return {
175
+ ...process.env,
176
+ GIT_TERMINAL_PROMPT: '0',
177
+ GH_PROMPT_DISABLED: '1',
178
+ NO_COLOR: '1',
179
+ ...extra,
180
+ }
181
+ }
182
+
183
+ // ---------------------------------------------------------------- shaping
184
+
185
+ /** Byte-bounded truncation with a marker; lossless for inputs under the cap. */
186
+ export function truncateText(text, maxBytes) {
187
+ const s = String(text ?? '')
188
+ const buf = Buffer.from(s, 'utf8')
189
+ if (buf.length <= maxBytes) return { text: s, truncated: false }
190
+ return { text: buf.subarray(0, maxBytes).toString('utf8'), truncated: true }
191
+ }
192
+
193
+ const HTML_ENTITIES = {
194
+ '&amp;': '&',
195
+ '&lt;': '<',
196
+ '&gt;': '>',
197
+ '&quot;': '"',
198
+ '&#39;': "'",
199
+ '&apos;': "'",
200
+ '&nbsp;': ' ',
201
+ '&#x27;': "'",
202
+ }
203
+
204
+ /** Crude tag-strip for bodies extracted from page payloads (display only). */
205
+ export function stripHtml(html) {
206
+ let text = String(html ?? '')
207
+ text = text.replace(/<script[\s\S]*?<\/script>/gi, ' ')
208
+ text = text.replace(/<style[\s\S]*?<\/style>/gi, ' ')
209
+ text = text.replace(/<br\s*\/?>/gi, '\n')
210
+ text = text.replace(/<\/p>/gi, '\n')
211
+ text = text.replace(/<li[^>]*>/gi, '\n- ')
212
+ text = text.replace(/<[^>]+>/g, '')
213
+ text = text.replace(/&(amp|lt|gt|quot|apos|nbsp|#39|#x27);/gi, (m) => HTML_ENTITIES[m.toLowerCase()] ?? m)
214
+ text = text.replace(/\u00a0/g, ' ')
215
+ text = text.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n')
216
+ return text.trim()
217
+ }
218
+
219
+ // --------------------------------------------------------------- git urls
220
+
221
+ /**
222
+ * Extract { owner, repo } from common GitHub remote URL shapes.
223
+ * Returns null when the URL does not look like a GitHub repo remote.
224
+ */
225
+ export function ownerRepoFromUrl(url) {
226
+ const text = String(url ?? '').trim().replace(/\\/g, '/')
227
+ let match
228
+ match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(text)
229
+ if (match) return { owner: match[1], repo: match[2] }
230
+ match = /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/.exec(text)
231
+ if (match) return { owner: match[1], repo: match[2] }
232
+ match = /^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/.exec(text)
233
+ if (match) return { owner: match[1], repo: match[2] }
234
+ return null
235
+ }
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "dsh-github-router",
3
+ "version": "0.1.0",
4
+ "description": "DeepSeek Harness plugin: read-only GitHub access for agents (github_probe / github_pr / github_issue / github_file / github_api). Routes every request inside the tool — api.github.com (direct or proxy), gh CLI, git protocol (plugin-owned fetch cache plus read-only local repo reads), PR/issue page HTML with strict JSON embeddedData extraction, and optional user-configured raw mirrors — so agents never burn turns fighting sandbox TLS/proxy failures in a shell. No write/push capability exists anywhere in the plugin.",
5
+ "keywords": [
6
+ "dsh-plugin",
7
+ "dsh",
8
+ "deepseek-harness",
9
+ "github",
10
+ "pull-request",
11
+ "issue",
12
+ "read-only",
13
+ "proxy",
14
+ "router"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Yutong Zou <yutong.zou.24@alumni.ucl.ac.uk> (https://github.com/AngelosZou)",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/AngelosZou/dsh-github-router.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/AngelosZou/dsh-github-router/issues"
24
+ },
25
+ "homepage": "https://github.com/AngelosZou/dsh-github-router#readme",
26
+ "type": "module",
27
+ "main": "lib/index.js",
28
+ "exports": {
29
+ ".": {
30
+ "default": "./lib/index.js"
31
+ },
32
+ "./client": {
33
+ "default": "./lib/client.js"
34
+ },
35
+ "./cordis.patch.yml": "./cordis.patch.yml",
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "lib",
40
+ "cordis.patch.yml",
41
+ "SECURITY.md",
42
+ "README.md",
43
+ "README.zh.md",
44
+ "CHANGELOG.md",
45
+ "CONTRIBUTING.md",
46
+ "docs"
47
+ ],
48
+ "dsh": {
49
+ "bundle": {
50
+ "patch": "./cordis.patch.yml"
51
+ },
52
+ "client": {
53
+ "platform": "web",
54
+ "inject": [
55
+ "@deepseek-ai/dsh-client-runtime"
56
+ ]
57
+ }
58
+ },
59
+ "engines": {
60
+ "node": ">=20.18.0"
61
+ },
62
+ "peerDependencies": {
63
+ "@deepseek-ai/cordis": "^4.0.1",
64
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
65
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
66
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
67
+ "@deepseek-ai/schemastery": "^2.0.0"
68
+ },
69
+ "devDependencies": {
70
+ "@deepseek-ai/cordis": "^4.0.1",
71
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ },
76
+ "scripts": {
77
+ "test": "node --test --test-isolation=none \"test/*.test.js\""
78
+ }
79
+ }