@goodandready/dsh-subscriptions 0.6.7 → 0.6.9

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/http.js CHANGED
@@ -30,6 +30,13 @@ export function readBody(req, maxBytes = 64 * 1024) {
30
30
  })
31
31
  }
32
32
 
33
+ export function isLoopback(value) {
34
+ const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
35
+ return address === 'localhost' || address === 'localhost.' || address === '::1'
36
+ || address?.startsWith('127.') === true
37
+ || address?.startsWith('::ffff:127.') === true
38
+ }
39
+
33
40
  /**
34
41
  * @param {import('node:http').IncomingMessage} request
35
42
  * @returns {boolean}
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Format a future timestamp as a relative remaining duration down to the minute.
3
- * Supports en and ru.
3
+ * Supports en and zh.
4
4
  */
5
5
 
6
6
  export const RELATIVE_UNITS = {
@@ -12,6 +12,14 @@ export const RELATIVE_UNITS = {
12
12
  hour: '{n}h',
13
13
  day: '{n}d',
14
14
  },
15
+ zh: {
16
+ soon: '刚刚',
17
+ prefix: '',
18
+ suffix: '后',
19
+ minute: '{n}分',
20
+ hour: '{n}小时',
21
+ day: '{n}天',
22
+ },
15
23
  ru: {
16
24
  soon: 'только что',
17
25
  prefix: 'через ',
@@ -32,10 +40,10 @@ function fill(template, n) {
32
40
  * @param {number} [now]
33
41
  * @returns {string} human-readable remaining time, down to the minute
34
42
  */
35
- export function formatRelativeReset(resetAt, lang = 'ru', now = Date.now()) {
43
+ export function formatRelativeReset(resetAt, lang = 'en', now = Date.now()) {
36
44
  if (typeof resetAt !== 'number' || !Number.isFinite(resetAt) || resetAt <= 0) return ''
37
45
  const delta = resetAt - now
38
- const units = RELATIVE_UNITS[lang] || RELATIVE_UNITS.ru
46
+ const units = RELATIVE_UNITS[lang] || RELATIVE_UNITS.en
39
47
  if (delta <= 0) return units.soon
40
48
 
41
49
  const totalMinutes = Math.max(1, Math.round(delta / 60_000))
@@ -16,8 +16,8 @@ export function registerStatusRoutes(ctx, state) {
16
16
  store,
17
17
  history,
18
18
  diagnosticsReport,
19
- pmL,
20
- pmE,
19
+ pmL = (s) => s,
20
+ pmE = (s) => s,
21
21
  } = state
22
22
 
23
23
  async function configResponse() {
@@ -177,25 +177,27 @@ export function registerStatusRoutes(ctx, state) {
177
177
  writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
178
178
  return
179
179
  }
180
- const provider = payload.provider
180
+ const provider = payload.provider || payload.vendor
181
+ const index = payload.index || (payload.accountIndex != null ? payload.accountIndex + 1 : 1)
181
182
  if (!isProvider(provider)) {
182
183
  writeJson(res, 200, { ok: false, provider, error: { code: 'provider', message: 'unknown provider' } })
183
184
  return
184
185
  }
185
- const ref = oauthRef(provider, payload.index)
186
+ const t0 = Date.now()
187
+ const ref = oauthRef(provider, index)
186
188
  const info = await store.describeRef(ref)
187
189
  if (!info.configured) {
188
- writeJson(res, 200, { ok: false, provider, index: payload.index, ref, error: { code: 'not_connected', message: 'not connected' } })
190
+ writeJson(res, 200, { ok: false, provider, index: index, ref, error: { code: 'not_connected', message: 'not connected' } })
189
191
  return
190
192
  }
191
193
  let blob
192
194
  try { blob = await store.loadBlob(ref) } catch (e) {
193
- writeJson(res, 200, { ok: false, provider, index: payload.index, ref, error: { code: 'auth', message: String(e && e.message || e) } })
195
+ writeJson(res, 200, { ok: false, provider, index: index, ref, error: { code: 'auth', message: String(e && e.message || e) } })
194
196
  return
195
197
  }
196
198
  try { blob = await store.ensureFresh(provider, blob, ref) } catch (e) {
197
199
  writeJson(res, 200, {
198
- ok: false, provider, index: payload.index, ref,
200
+ ok: false, provider, index: index, ref,
199
201
  email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
200
202
  quota: info.quota || null,
201
203
  error: { code: 'refresh', message: String(e && e.message || e) },
@@ -204,12 +206,14 @@ export function registerStatusRoutes(ctx, state) {
204
206
  }
205
207
  const vendor = getVendor(provider)
206
208
  if (typeof vendor.check !== 'function') {
207
- writeJson(res, 200, { ok: true, provider, index: payload.index, ref, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: info.quota || null })
209
+ writeJson(res, 200, { ok: true, provider, index: index, ref, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: info.quota || null })
208
210
  return
209
211
  }
210
212
  let capturedQuota = null
211
- const probeFetch = async (url, init) => {
212
- const res2 = await fetch(url, init)
213
+ const probeFetch = async (url, init = {}) => {
214
+ const timeoutSignal = AbortSignal.timeout(15_000)
215
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
216
+ const res2 = await fetch(url, { ...init, signal })
213
217
  try {
214
218
  const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
215
219
  if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
@@ -218,10 +222,12 @@ export function registerStatusRoutes(ctx, state) {
218
222
  }
219
223
  try {
220
224
  await vendor.check(blob, vendorConfig(provider, live()), probeFetch)
221
- writeJson(res, 200, { ok: true, provider, index: payload.index, ref, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: capturedQuota || info.quota || null, usagePercent: info.usagePercent ?? null })
225
+ const latencyMs = Date.now() - t0;
226
+ writeJson(res, 200, { ok: true, provider, index: index, ref, latencyMs, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: capturedQuota || info.quota || null, usagePercent: info.usagePercent ?? null })
222
227
  } catch (e) {
223
228
  writeJson(res, 200, {
224
- ok: false, provider, index: payload.index, ref,
229
+ ok: false, provider, index: index, ref,
230
+ latencyMs: Date.now() - t0,
225
231
  email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
226
232
  quota: capturedQuota || info.quota || null,
227
233
  error: { code: e && e.code ? e.code : 'VENDOR', message: String(e && e.message || e).slice(0, 300) },
@@ -318,8 +324,10 @@ export function registerStatusRoutes(ctx, state) {
318
324
  const vendor = getVendor(provider)
319
325
  if (typeof vendor.check === 'function') {
320
326
  let capturedQuota = null
321
- const probeFetch = async (url, init) => {
322
- const res2 = await fetch(url, init)
327
+ const probeFetch = async (url, init = {}) => {
328
+ const timeoutSignal = AbortSignal.timeout(15_000)
329
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
330
+ const res2 = await fetch(url, { ...init, signal })
323
331
  try {
324
332
  const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
325
333
  if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
package/lib/routes.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { publicConfig } from './config-schema.js'
2
2
  import { PROVIDERS, displayName } from './refs.js'
3
- import { escapeHtml, writeHtml, writeJson } from './http.js'
3
+ import { escapeHtml, isLoopback, writeHtml, writeJson } from './http.js'
4
4
  import { registerStatusRoutes } from './routes/status.js'
5
5
  import { registerOauthRoutes } from './routes/oauth.js'
6
6
  import { registerAccountsRoutes } from './routes/accounts.js'
7
7
  import { registerProxyRoutes } from './routes/proxy.js'
8
+ import { registerPluginUpdater } from './updater.js'
8
9
 
9
10
  export function registerRoutes(ctx, state) {
10
11
  const {
@@ -22,6 +23,11 @@ export function registerRoutes(ctx, state) {
22
23
  }
23
24
  }
24
25
 
26
+ ctx.effect(() => registerPluginUpdater(ctx, {
27
+ endpoint: '/dsh-subscriptions/update',
28
+ packageName: '@goodandready/dsh-subscriptions',
29
+ manifestUrl: new URL('../package.json', import.meta.url),
30
+ }), 'dsh-subscriptions: /update')
25
31
 
26
32
  ctx.effect(() => ctx.webServer.register({
27
33
  kind: 'exact',
@@ -32,7 +38,8 @@ export function registerRoutes(ctx, state) {
32
38
  return
33
39
  }
34
40
  const host = (req.headers.host || '').split(':')[0]
35
- if (host !== 'localhost' && host !== '127.0.0.1') {
41
+ const remote = req.socket?.remoteAddress
42
+ if (!isLoopback(host) || (remote && !isLoopback(remote))) {
36
43
  writeHtml(res, 403, '<!doctype html><meta charset="utf-8"><p>Subscriptions overview is localhost-only.</p>')
37
44
  return
38
45
  }
package/lib/updater.js ADDED
@@ -0,0 +1,260 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { homedir } from 'node:os'
5
+ import { basename, dirname, isAbsolute, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { writeJson } from './http.js'
8
+
9
+ const UPDATE_HEADER = 'x-dsh-plugin-update'
10
+ const UPDATE_TIMEOUT_MS = 10 * 60_000
11
+ const VERSION_CACHE_MS = 5 * 60_000
12
+ let latestCache
13
+
14
+ function header(request, name) {
15
+ const value = request.headers?.[name]
16
+ return Array.isArray(value) ? value[0] : value
17
+ }
18
+
19
+ export function isLoopback(value) {
20
+ const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
21
+ return address === 'localhost' || address === 'localhost.' || address === '::1'
22
+ || address?.startsWith('127.') === true
23
+ || address?.startsWith('::ffff:127.') === true
24
+ }
25
+
26
+ export function isTrustedUpdateRequest(request) {
27
+ const customHeader = header(request, UPDATE_HEADER)
28
+ if (customHeader !== '1') {
29
+ // In browser fetch, allow if sec-fetch-site is same-origin or same-site
30
+ const site = header(request, 'sec-fetch-site')
31
+ if (site === 'cross-site') return false
32
+ }
33
+ const remote = request.socket?.remoteAddress
34
+ if (remote && !isLoopback(remote)) return false
35
+ const origin = header(request, 'origin')
36
+ const host = header(request, 'host')
37
+ if (origin && host) {
38
+ try {
39
+ const url = new URL(origin)
40
+ if (url.host !== host && !isLoopback(url.hostname)) return false
41
+ } catch {
42
+ return false
43
+ }
44
+ }
45
+ return true
46
+ }
47
+
48
+ function validProfileName(value) {
49
+ return typeof value === 'string' && value !== '' && value !== '.' && value !== '..'
50
+ && !value.includes('/') && !value.includes('\\') && !/[\0-\x1f\x7f]/.test(value)
51
+ }
52
+
53
+ function profileNameFromArgv(argv) {
54
+ for (let index = 2; index < argv.length; index += 1) {
55
+ if (argv[index] === '--profile') return argv[index + 1]
56
+ if (argv[index]?.startsWith('--profile=')) return argv[index].slice('--profile='.length)
57
+ }
58
+ return argv[2] === 'web' ? 'web' : undefined
59
+ }
60
+
61
+ function findDshCliEntry() {
62
+ const value = process.argv[1]
63
+ if (value === undefined || value === '') return undefined
64
+ const entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value)
65
+ if (!existsSync(entry)) return undefined
66
+ for (let directory = dirname(entry); ; directory = dirname(directory)) {
67
+ const manifestPath = resolve(directory, 'package.json')
68
+ if (existsSync(manifestPath)) {
69
+ try {
70
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
71
+ const bin = typeof manifest.bin === 'string'
72
+ ? manifest.bin
73
+ : typeof manifest.bin === 'object' && manifest.bin !== null
74
+ ? manifest.bin.dsh
75
+ : undefined
76
+ if (manifest.name === '@deepseek-ai/dsh' && typeof bin === 'string'
77
+ && !isAbsolute(bin) && resolve(directory, bin) === resolve(entry)) return entry
78
+ } catch {
79
+ // Continue searching parent package directories.
80
+ }
81
+ }
82
+ const parent = dirname(directory)
83
+ if (parent === directory) return undefined
84
+ }
85
+ }
86
+
87
+ function getRuntime() {
88
+ const profileDir = resolve(process.env.DSH_PROFILE_DIR
89
+ ?? resolve(homedir(), '.dsh', 'profiles', 'web'))
90
+ const selected = profileNameFromArgv(process.argv)
91
+ const profileName = validProfileName(selected)
92
+ ? selected
93
+ : validProfileName(basename(profileDir)) ? basename(profileDir) : 'web'
94
+ const cliEntry = findDshCliEntry()
95
+ return cliEntry === undefined ? { profileName, profileDir } : { profileName, profileDir, cliEntry }
96
+ }
97
+
98
+ export function parseSemver(value) {
99
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value)
100
+ if (match === null) return undefined
101
+ return {
102
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
103
+ prerelease: match[4]?.split('.') ?? [],
104
+ }
105
+ }
106
+
107
+ function comparePrerelease(left, right) {
108
+ if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1
109
+ const length = Math.max(left.length, right.length)
110
+ for (let index = 0; index < length; index += 1) {
111
+ const a = left[index]
112
+ const b = right[index]
113
+ if (a === undefined || b === undefined) return a === b ? 0 : a === undefined ? -1 : 1
114
+ if (a === b) continue
115
+ const aNumeric = /^\d+$/.test(a)
116
+ const bNumeric = /^\d+$/.test(b)
117
+ if (aNumeric && bNumeric) {
118
+ const aNumber = BigInt(a)
119
+ const bNumber = BigInt(b)
120
+ if (aNumber !== bNumber) return aNumber > bNumber ? 1 : -1
121
+ continue
122
+ }
123
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1
124
+ return a > b ? 1 : -1
125
+ }
126
+ return 0
127
+ }
128
+
129
+ export function isNewerVersion(currentValue, candidateValue) {
130
+ const current = parseSemver(currentValue)
131
+ const candidate = parseSemver(candidateValue)
132
+ if (current === undefined || candidate === undefined) return false
133
+ for (let index = 0; index < 3; index += 1) {
134
+ if (candidate.core[index] !== current.core[index]) return candidate.core[index] > current.core[index]
135
+ }
136
+ return comparePrerelease(candidate.prerelease, current.prerelease) > 0
137
+ }
138
+
139
+ export async function fetchLatestVersion(packageName, registry = 'https://registry.npmjs.org') {
140
+ if (latestCache?.packageName === packageName && latestCache.registry === registry && Date.now() < latestCache.expiresAt) {
141
+ return latestCache.version
142
+ }
143
+ try {
144
+ const response = await fetch(`${registry.replace(/\/$/, '')}/${encodeURIComponent(packageName)}/latest`, {
145
+ signal: AbortSignal.timeout(8_000),
146
+ })
147
+ if (!response.ok) return undefined
148
+ const value = await response.json()
149
+ if (typeof value?.version !== 'string' || value.version === '') return undefined
150
+ latestCache = { packageName, registry, version: value.version, expiresAt: Date.now() + VERSION_CACHE_MS }
151
+ return value.version
152
+ } catch {
153
+ return undefined
154
+ }
155
+ }
156
+
157
+ export async function readCurrentVersion(manifestUrl) {
158
+ const value = JSON.parse(await readFile(manifestUrl, 'utf8'))
159
+ if (typeof value?.version !== 'string' || value.version === '') throw new Error('Cannot read current plugin version.')
160
+ return value.version
161
+ }
162
+
163
+ export async function getUpdaterStatus(options, target = getRuntime()) {
164
+ const current = await readCurrentVersion(options.manifestUrl)
165
+ const latest = await fetchLatestVersion(options.packageName, options.registry ?? 'https://registry.npmjs.org')
166
+ return {
167
+ ok: true,
168
+ packageName: options.packageName,
169
+ currentVersion: current,
170
+ ...(latest === undefined ? {} : { latestVersion: latest }),
171
+ latestCheckFailed: latest === undefined,
172
+ updateAvailable: latest !== undefined && isNewerVersion(current, latest),
173
+ profileName: target.profileName,
174
+ canAutoUpdate: target.cliEntry !== undefined,
175
+ }
176
+ }
177
+
178
+ export async function installExactPackage(target, packageSpec, options) {
179
+ if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.')
180
+ await new Promise((resolvePromise, reject) => {
181
+ const child = spawn(process.execPath, [
182
+ target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
183
+ '--config.minimumReleaseAge=0', packageSpec,
184
+ `--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
185
+ ], {
186
+ cwd: target.profileDir,
187
+ windowsHide: true,
188
+ stdio: ['ignore', 'pipe', 'pipe'],
189
+ env: { ...process.env, NO_COLOR: '1' },
190
+ })
191
+ let detail = ''
192
+ child.stdout?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000) })
193
+ child.stderr?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000) })
194
+ const timer = setTimeout(() => {
195
+ child.kill()
196
+ reject(new Error('Update timed out; use the normal DSH update flow.'))
197
+ }, UPDATE_TIMEOUT_MS)
198
+ child.once('error', error => { clearTimeout(timer); reject(error) })
199
+ child.once('exit', code => {
200
+ clearTimeout(timer)
201
+ if (code === 0) resolvePromise()
202
+ else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
203
+ })
204
+ })
205
+ }
206
+
207
+ export function registerPluginUpdater(ctx, options) {
208
+ let installing = false
209
+ return ctx.webServer.register({
210
+ kind: 'exact',
211
+ path: options.endpoint,
212
+ handler: async (request, response) => {
213
+ try {
214
+ const target = getRuntime()
215
+ if (request.method === 'GET' || request.method === 'HEAD') {
216
+ const payload = await getUpdaterStatus(options, target)
217
+ writeJson(response, 200, payload)
218
+ return
219
+ }
220
+ if (request.method !== 'POST') {
221
+ response.writeHead(405, { allow: 'GET, HEAD, POST' })
222
+ response.end()
223
+ return
224
+ }
225
+ if (!isTrustedUpdateRequest(request)) {
226
+ writeJson(response, 403, { ok: false, error: { code: 'forbidden', message: 'Rejected non-local or cross-origin update request.' } })
227
+ return
228
+ }
229
+ if (installing) {
230
+ writeJson(response, 409, { ok: false, error: { code: 'busy', message: 'This plugin is already updating.' } })
231
+ return
232
+ }
233
+ installing = true
234
+ try {
235
+ const before = await getUpdaterStatus(options, target)
236
+ if (before.latestVersion === undefined) {
237
+ writeJson(response, 503, { ok: false, error: { code: 'unavailable', message: 'The latest version is temporarily unavailable.' } })
238
+ return
239
+ }
240
+ if (!before.updateAvailable) {
241
+ writeJson(response, 200, { ok: true, ...before })
242
+ return
243
+ }
244
+ await installExactPackage(target, `${options.packageName}@${before.latestVersion}`, options)
245
+ writeJson(response, 200, {
246
+ ok: true,
247
+ ...before,
248
+ updatedVersion: before.latestVersion,
249
+ restartRequired: true,
250
+ })
251
+ } finally {
252
+ installing = false
253
+ }
254
+ } catch (error) {
255
+ ctx.logger?.warn?.(`plugin updater failed: ${String(error)}`)
256
+ writeJson(response, 503, { ok: false, error: { code: 'update_failed', message: String(error?.message || error) } })
257
+ }
258
+ },
259
+ })
260
+ }
package/lib/usage.js CHANGED
@@ -52,22 +52,22 @@ export function asUsageSnapshot(percent) {
52
52
  // Named limit windows reported by vendors (five_hour, seven_day, primary...).
53
53
  // Label map is a data table: extend it when a vendor adds a window name.
54
54
  const WINDOW_LABELS = {
55
- five_hour: { ru: "5ч", en: "5h" },
56
- seven_day: { ru: "7д", en: "7d" },
57
- seven_day_oauth_apps: { ru: "7д прил.", en: "7d apps" },
58
- weekly_limit_7_days: { ru: "7д", en: "7d" },
59
- primary: { ru: "осн.", en: "primary" },
60
- secondary: { ru: "втор.", en: "secondary" },
55
+ five_hour: { zh: "5小时", ru: "5ч", en: "5h" },
56
+ seven_day: { zh: "7天", ru: "7д", en: "7d" },
57
+ seven_day_oauth_apps: { zh: "7天应用", ru: "7д прил.", en: "7d apps" },
58
+ weekly_limit_7_days: { zh: "7天", ru: "7д", en: "7d" },
59
+ primary: { zh: "主配额", ru: "осн.", en: "primary" },
60
+ secondary: { zh: "次配额", ru: "втор.", en: "secondary" },
61
61
  // codex: the vendor API returns primary_window/secondary_window - surfaced as 5h/7d in the UI
62
- primary_window: { ru: "5ч", en: "5h" },
63
- secondary_window: { ru: "7д", en: "7d" },
64
- monthly: { ru: "месяц", en: "month" },
65
- credits: { ru: "кредиты", en: "credits" },
62
+ primary_window: { zh: "5小时", ru: "5ч", en: "5h" },
63
+ secondary_window: { zh: "7天", ru: "7д", en: "7d" },
64
+ monthly: { zh: "月度", ru: "месяц", en: "month" },
65
+ credits: { zh: "点数", ru: "кредиты", en: "credits" },
66
66
  }
67
67
 
68
68
  export function windowLabel(id) {
69
69
  const known = WINDOW_LABELS[id]
70
- return known || { ru: id, en: id }
70
+ return known || { zh: id, ru: id, en: id }
71
71
  }
72
72
 
73
73
  // Walk vendor usage JSON; every node carrying a utilization-style number
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,6 +34,10 @@
34
34
  "bugs": {
35
35
  "url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
36
36
  },
37
+ "scripts": {
38
+ "test": "eslint lib/ && node --test test/*.test.mjs",
39
+ "typecheck": "tsc -p tsconfig.checkjs.json"
40
+ },
37
41
  "dsh": {
38
42
  "bundle": {
39
43
  "patch": "./cordis.patch.yml"
@@ -62,9 +66,5 @@
62
66
  "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
63
67
  "@types/node": "^22.20.2",
64
68
  "typescript": "^7.0.2"
65
- },
66
- "scripts": {
67
- "test": "eslint lib/ && node --test test/*.test.mjs",
68
- "typecheck": "tsc -p tsconfig.checkjs.json"
69
69
  }
70
- }
70
+ }