dsh-tabbit 0.2.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/installer.js ADDED
@@ -0,0 +1,568 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { constants } from 'node:fs'
3
+ import { access, mkdir, open, rename, rm } from 'node:fs/promises'
4
+ import { homedir } from 'node:os'
5
+ import { basename, join } from 'node:path'
6
+
7
+ const MAX_INSTALLER_BYTES = 1024 * 1024 * 1024
8
+ export const MINIMUM_TABBIT_VERSION = '1.9.0'
9
+ const ALLOWED_DOWNLOAD_HOSTS = new Set([
10
+ 'www.tabbit.com',
11
+ 'pkg.tabbit.com',
12
+ 'releases.tabbit.com',
13
+ 'www.tabbit.ai',
14
+ 'pkg.tabbit.ai',
15
+ 'releases.tabbit.ai',
16
+ ])
17
+
18
+ const INSTALLER_ORIGINS = {
19
+ domestic: 'https://www.tabbit.com',
20
+ international: 'https://www.tabbit.ai',
21
+ }
22
+
23
+ const DOWNLOADS = {
24
+ 'win32:x64': {
25
+ platform: 'windows',
26
+ arch: 'x86_64',
27
+ extension: '.exe',
28
+ fallbackName: 'Tabbit Browser Installer.exe',
29
+ },
30
+ 'darwin:arm64': {
31
+ platform: 'mac',
32
+ arch: 'ARM_64',
33
+ extension: '.dmg',
34
+ fallbackName: 'Tabbit Browser Installer ARM64.dmg',
35
+ },
36
+ 'darwin:x64': {
37
+ platform: 'mac',
38
+ arch: 'x86_64',
39
+ extension: '.dmg',
40
+ fallbackName: 'Tabbit Browser Installer Intel.dmg',
41
+ },
42
+ }
43
+
44
+ const MAC_APPLICATIONS = [
45
+ {
46
+ name: 'Tabbit',
47
+ bundleId: 'com.tabbit-ai.Tabbit',
48
+ edition: 'international',
49
+ channel: 'stable',
50
+ },
51
+ {
52
+ name: 'Tabbit Browser',
53
+ bundleId: 'com.tab-browser.Tabbit',
54
+ edition: 'domestic',
55
+ channel: 'stable',
56
+ },
57
+ ]
58
+
59
+ const WINDOWS_DISPLAY_NAMES = new Map([
60
+ ['Tabbit', { edition: 'international', channel: 'stable' }],
61
+ ['Tabbit Browser', { edition: 'domestic', channel: 'stable' }],
62
+ ])
63
+
64
+ export function normalizeRegionCode(value) {
65
+ const locale = String(value ?? '')
66
+ .trim()
67
+ .replace(/^['"]|['"]$/g, '')
68
+ .split('@', 1)[0]
69
+ if (/^[a-z]{2}$/i.test(locale)) return locale.toUpperCase()
70
+ return locale.match(/(?:_|-)([a-z]{2})$/i)?.[1].toUpperCase()
71
+ }
72
+
73
+ export function detectSystemRegion({
74
+ platform = process.platform,
75
+ run = spawnSync,
76
+ } = {}) {
77
+ if (platform === 'darwin') {
78
+ const result = run('/usr/bin/defaults', ['read', '-g', 'AppleLocale'], {
79
+ encoding: 'utf8',
80
+ })
81
+ return result.status === 0 ? normalizeRegionCode(result.stdout) : undefined
82
+ }
83
+
84
+ if (platform === 'win32') {
85
+ const script = '([System.Globalization.RegionInfo]::new((Get-WinHomeLocation).GeoId)).TwoLetterISORegionName'
86
+ const result = run(
87
+ 'powershell.exe',
88
+ ['-NoProfile', '-NonInteractive', '-Command', script],
89
+ { encoding: 'utf8', windowsHide: true },
90
+ )
91
+ return result.status === 0 ? normalizeRegionCode(result.stdout) : undefined
92
+ }
93
+
94
+ return undefined
95
+ }
96
+
97
+ export function installerDistributionForRegion(regionCode) {
98
+ return normalizeRegionCode(regionCode) === 'CN' ? 'domestic' : 'international'
99
+ }
100
+
101
+ export function installerUrl(spec, distribution = 'international') {
102
+ const origin = INSTALLER_ORIGINS[distribution]
103
+ if (!origin) throw new Error(`Unknown Tabbit installer distribution: ${distribution}.`)
104
+ const query = new URLSearchParams({
105
+ platform: spec.platform,
106
+ arch: spec.arch,
107
+ tab_brand: 'dshr',
108
+ utm_source: 'dsh',
109
+ })
110
+ return `${origin}/api/v0/upgrade/installer?${query}`
111
+ }
112
+
113
+ export function detectPlatformSpec({
114
+ platform = process.platform,
115
+ arch = process.arch,
116
+ env = process.env,
117
+ run = spawnSync,
118
+ } = {}) {
119
+ let nativeArch = arch
120
+
121
+ if (platform === 'darwin' && arch === 'x64') {
122
+ const result = run('/usr/sbin/sysctl', ['-n', 'hw.optional.arm64'], {
123
+ encoding: 'utf8',
124
+ })
125
+ if (result.status === 0 && result.stdout.trim() === '1') nativeArch = 'arm64'
126
+ }
127
+
128
+ if (platform === 'win32') {
129
+ const reported = String(
130
+ env.PROCESSOR_ARCHITEW6432 ?? env.PROCESSOR_ARCHITECTURE ?? arch,
131
+ ).toLowerCase()
132
+ nativeArch = reported === 'amd64' || reported === 'x86_64' ? 'x64' : arch
133
+ }
134
+
135
+ const spec = DOWNLOADS[`${platform}:${nativeArch}`]
136
+ if (!spec) {
137
+ throw new Error(`Tabbit Browser installer is unavailable for ${platform}/${nativeArch}.`)
138
+ }
139
+ return { ...spec }
140
+ }
141
+
142
+ async function exists(path, mode = constants.F_OK) {
143
+ try {
144
+ await access(path, mode)
145
+ return true
146
+ } catch {
147
+ return false
148
+ }
149
+ }
150
+
151
+ function readPlistValue(plistPath, key, run = spawnSync) {
152
+ const result = run(
153
+ '/usr/bin/plutil',
154
+ ['-extract', key, 'raw', '-o', '-', plistPath],
155
+ { encoding: 'utf8' },
156
+ )
157
+ return result.status === 0 ? result.stdout.trim() : undefined
158
+ }
159
+
160
+ export async function detectMacInstallations({
161
+ userHome = homedir(),
162
+ run = spawnSync,
163
+ } = {}) {
164
+ const roots = ['/Applications', join(userHome, 'Applications')]
165
+ const installations = []
166
+ const seenBundleIds = new Set()
167
+
168
+ for (const app of MAC_APPLICATIONS) {
169
+ for (const root of roots) {
170
+ const path = join(root, `${app.name}.app`)
171
+ const plistPath = join(path, 'Contents', 'Info.plist')
172
+ if (!(await exists(plistPath))) continue
173
+ const actualBundleId = readPlistValue(plistPath, 'CFBundleIdentifier', run)
174
+ if (actualBundleId !== app.bundleId || seenBundleIds.has(app.bundleId)) continue
175
+ const version = readPlistValue(plistPath, 'CFBundleShortVersionString', run)
176
+ installations.push({
177
+ ...app,
178
+ path,
179
+ ...(version ? { version } : {}),
180
+ })
181
+ seenBundleIds.add(app.bundleId)
182
+ }
183
+ }
184
+
185
+ return installations
186
+ }
187
+
188
+ export function parseWindowsUninstallRegistry(output) {
189
+ const installations = []
190
+ let record = undefined
191
+
192
+ const commit = () => {
193
+ if (!record) return
194
+ const identity = WINDOWS_DISPLAY_NAMES.get(record.DisplayName)
195
+ if (!identity) return
196
+ const icon = record.DisplayIcon
197
+ ?.replace(/,\s*-?\d+$/, '')
198
+ .replace(/^"(.*)"$/, '$1')
199
+ installations.push({
200
+ name: record.DisplayName,
201
+ ...identity,
202
+ ...(record.InstallLocation || icon ? { path: record.InstallLocation || icon } : {}),
203
+ ...(icon ? { executable: icon } : {}),
204
+ ...(record.DisplayVersion ? { version: record.DisplayVersion } : {}),
205
+ registryKey: record.registryKey,
206
+ })
207
+ }
208
+
209
+ for (const line of String(output).split(/\r?\n/)) {
210
+ if (/^HKEY_/i.test(line.trim())) {
211
+ commit()
212
+ record = { registryKey: line.trim() }
213
+ continue
214
+ }
215
+ if (!record) continue
216
+ const match = line.match(/^\s+(DisplayName|DisplayVersion|InstallLocation|DisplayIcon)\s+REG_\w+\s+(.*)$/i)
217
+ if (match) record[match[1]] = match[2].trim()
218
+ }
219
+ commit()
220
+ return installations
221
+ }
222
+
223
+ export function detectWindowsInstallations({ run = spawnSync } = {}) {
224
+ const roots = [
225
+ 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
226
+ 'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
227
+ ]
228
+ const targetedKeys = roots.flatMap(root => (
229
+ ['Tabbit', 'Tabbit Browser'].map(name => `${root}\\${name}`)
230
+ ))
231
+ const installations = []
232
+ const seen = new Set()
233
+
234
+ const collect = (output) => {
235
+ for (const item of parseWindowsUninstallRegistry(output)) {
236
+ const key = `${item.name}\0${item.path ?? ''}`
237
+ if (seen.has(key)) continue
238
+ seen.add(key)
239
+ installations.push(item)
240
+ }
241
+ }
242
+
243
+ for (const key of targetedKeys) {
244
+ for (const view of ['64', '32']) {
245
+ const result = run('reg.exe', ['query', key, `/reg:${view}`], {
246
+ encoding: 'utf8',
247
+ windowsHide: true,
248
+ maxBuffer: 16 * 1024 * 1024,
249
+ })
250
+ if (result.status === 0) collect(result.stdout)
251
+ }
252
+ }
253
+ if (installations.length > 0) return installations
254
+
255
+ // Installer technologies may use generated uninstall subkeys. Preserve the
256
+ // broad scan as a compatibility fallback when the stable well-known keys are
257
+ // absent.
258
+ for (const root of roots) {
259
+ for (const view of ['64', '32']) {
260
+ const result = run('reg.exe', ['query', root, '/s', `/reg:${view}`], {
261
+ encoding: 'utf8',
262
+ windowsHide: true,
263
+ maxBuffer: 16 * 1024 * 1024,
264
+ })
265
+ if (result.status === 0) collect(result.stdout)
266
+ }
267
+ }
268
+ return installations
269
+ }
270
+
271
+ async function detectCli({
272
+ userHome = homedir(),
273
+ platform = process.platform,
274
+ env = process.env,
275
+ } = {}) {
276
+ const path = platform === 'win32'
277
+ ? join(env.LOCALAPPDATA || join(userHome, 'AppData', 'Local'), 'Tabbit', 'LocalAgent', 'bin', 'tabbit-cli.exe')
278
+ : join(userHome, '.local', 'bin', 'tabbit-cli')
279
+ return await exists(path, platform === 'win32' ? constants.F_OK : constants.X_OK)
280
+ ? { ready: true, path }
281
+ : { ready: false }
282
+ }
283
+
284
+ function numericVersion(version) {
285
+ const match = String(version ?? '').trim().match(/^v?(\d+(?:\.\d+)*)/i)
286
+ return match ? match[1].split('.').map(Number) : undefined
287
+ }
288
+
289
+ export function isVersionAtLeast(version, minimum = MINIMUM_TABBIT_VERSION) {
290
+ const actual = numericVersion(version)
291
+ const required = numericVersion(minimum)
292
+ if (!actual || !required) return false
293
+ const length = Math.max(actual.length, required.length)
294
+ for (let index = 0; index < length; index += 1) {
295
+ const left = actual[index] ?? 0
296
+ const right = required[index] ?? 0
297
+ if (left !== right) return left > right
298
+ }
299
+ return true
300
+ }
301
+
302
+ function isTabbitRuntimeProcess(name, command) {
303
+ const value = `${name ?? ''} ${command ?? ''}`
304
+ return /(?:^|[\\/"'\s])browser-runtime-service\.mjs(?=$|["'\s])/i.test(value)
305
+ || /(?:^|[\\/"'\s])nodejs-playwright-runtime\.mjs(?=$|["'\s])/i.test(value)
306
+ }
307
+
308
+ export function parseUnixProcessList(output) {
309
+ const processes = []
310
+ for (const line of String(output).split(/\r?\n/)) {
311
+ const match = line.match(/^\s*(\d+)\s+(\S+)\s+(.*)$/)
312
+ if (!match || !isTabbitRuntimeProcess(match[2], match[3])) continue
313
+ processes.push({ pid: Number(match[1]), name: match[2] })
314
+ }
315
+ return processes
316
+ }
317
+
318
+ export function parseWindowsProcessList(output) {
319
+ if (!String(output).trim()) return []
320
+ let records
321
+ try {
322
+ records = JSON.parse(output)
323
+ } catch {
324
+ return []
325
+ }
326
+ if (!Array.isArray(records)) records = [records]
327
+ return records
328
+ .filter(record => isTabbitRuntimeProcess(record.Name, record.CommandLine))
329
+ .map(record => ({
330
+ pid: Number(record.ProcessId),
331
+ name: String(record.Name ?? ''),
332
+ }))
333
+ }
334
+
335
+ export function detectTabbitPlaywrightProcesses({
336
+ platform = process.platform,
337
+ run = spawnSync,
338
+ } = {}) {
339
+ if (platform === 'win32') {
340
+ const script = `Get-CimInstance Win32_Process -Filter "CommandLine LIKE '%browser-runtime-service.mjs%' OR CommandLine LIKE '%nodejs-playwright-runtime.mjs%'" | Select-Object ProcessId,Name,CommandLine | ConvertTo-Json -Compress`
341
+ const result = run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
342
+ encoding: 'utf8',
343
+ windowsHide: true,
344
+ maxBuffer: 16 * 1024 * 1024,
345
+ })
346
+ return result.status === 0 ? parseWindowsProcessList(result.stdout) : []
347
+ }
348
+
349
+ const result = run('ps', ['-axo', 'pid=,comm=,args='], {
350
+ encoding: 'utf8',
351
+ maxBuffer: 16 * 1024 * 1024,
352
+ })
353
+ return result.status === 0 ? parseUnixProcessList(result.stdout) : []
354
+ }
355
+
356
+ export function summarizeTabbitRuntime(playwrightProcesses) {
357
+ const instanceCount = Array.isArray(playwrightProcesses) ? playwrightProcesses.length : 0
358
+ return {
359
+ instanceCount,
360
+ running: instanceCount > 0,
361
+ ambiguous: instanceCount > 1,
362
+ }
363
+ }
364
+
365
+ export async function detectTabbit({
366
+ platform = process.platform,
367
+ userHome = homedir(),
368
+ env = process.env,
369
+ run = spawnSync,
370
+ minimumVersion = MINIMUM_TABBIT_VERSION,
371
+ } = {}) {
372
+ const installations = platform === 'darwin'
373
+ ? await detectMacInstallations({ userHome, run })
374
+ : platform === 'win32'
375
+ ? detectWindowsInstallations({ run })
376
+ : []
377
+ const cli = await detectCli({ userHome, platform, env })
378
+ const supportedInstallations = installations.filter(item => (
379
+ isVersionAtLeast(item.version, minimumVersion)
380
+ ))
381
+ const playwrightProcesses = supportedInstallations.length > 0
382
+ ? detectTabbitPlaywrightProcesses({ platform, run })
383
+ : []
384
+ const runtime = summarizeTabbitRuntime(playwrightProcesses)
385
+ const recommendation = supportedInstallations.length === 0
386
+ ? 'download'
387
+ : runtime.running && cli.ready
388
+ ? 'ready'
389
+ : 'restart-required'
390
+ return {
391
+ platform,
392
+ minimumVersion,
393
+ cliReady: cli.ready,
394
+ cliPath: cli.path,
395
+ installations,
396
+ supportedInstallations,
397
+ playwrightProcessRunning: runtime.running,
398
+ playwrightInstanceCount: runtime.instanceCount,
399
+ playwrightRuntimeAmbiguous: runtime.ambiguous,
400
+ playwrightProcesses,
401
+ recommendation,
402
+ }
403
+ }
404
+
405
+ function filenameFromResponse(response, spec) {
406
+ const disposition = response.headers.get('content-disposition') ?? ''
407
+ const encoded = disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1]
408
+ const plain = disposition.match(/filename="?([^";]+)"?/i)?.[1]
409
+ let candidate
410
+ try {
411
+ candidate = encoded ? decodeURIComponent(encoded) : plain
412
+ } catch {
413
+ candidate = plain
414
+ }
415
+ if (!candidate) {
416
+ try {
417
+ candidate = decodeURIComponent(basename(new URL(response.url).pathname))
418
+ } catch {
419
+ candidate = undefined
420
+ }
421
+ }
422
+ const safe = basename(candidate || spec.fallbackName)
423
+ .replace(/[<>:"/\\|?*\u0000-\u001f]/g, '_')
424
+ .trim()
425
+ return safe.toLowerCase().endsWith(spec.extension) ? safe : spec.fallbackName
426
+ }
427
+
428
+ async function uniqueDestination(directory, filename) {
429
+ const extensionIndex = filename.toLowerCase().lastIndexOf('.')
430
+ const stem = extensionIndex > 0 ? filename.slice(0, extensionIndex) : filename
431
+ const extension = extensionIndex > 0 ? filename.slice(extensionIndex) : ''
432
+ for (let index = 0; index < 1000; index += 1) {
433
+ const candidate = join(directory, index === 0 ? filename : `${stem} (${index})${extension}`)
434
+ if (!(await exists(candidate))) return candidate
435
+ }
436
+ throw new Error('Could not allocate a unique installer filename.')
437
+ }
438
+
439
+ async function verifyInstaller(path, spec, bytes) {
440
+ const handle = await open(path, 'r')
441
+ try {
442
+ if (spec.extension === '.exe') {
443
+ const header = Buffer.alloc(2)
444
+ await handle.read(header, 0, 2, 0)
445
+ if (header.toString('ascii') !== 'MZ') throw new Error('Downloaded file is not a Windows executable.')
446
+ return
447
+ }
448
+ const trailer = Buffer.alloc(Math.min(512, bytes))
449
+ await handle.read(trailer, 0, trailer.length, Math.max(0, bytes - trailer.length))
450
+ if (trailer.subarray(0, 4).toString('ascii') !== 'koly') {
451
+ throw new Error('Downloaded file is not a valid DMG image.')
452
+ }
453
+ } finally {
454
+ await handle.close()
455
+ }
456
+ }
457
+
458
+ export async function downloadInstaller({
459
+ signal,
460
+ onProgress = () => {},
461
+ outputDirectory = join(homedir(), 'Downloads'),
462
+ fetchImpl = fetch,
463
+ platformOptions,
464
+ } = {}) {
465
+ const spec = detectPlatformSpec(platformOptions)
466
+ const regionCode = detectSystemRegion(platformOptions)
467
+ const distribution = installerDistributionForRegion(regionCode)
468
+ const sourceUrl = installerUrl(spec, distribution)
469
+ await mkdir(outputDirectory, { recursive: true })
470
+
471
+ const response = await fetchImpl(sourceUrl, { redirect: 'follow', signal })
472
+ if (!response.ok || !response.body) throw new Error(`Installer download failed with HTTP ${response.status}.`)
473
+ const finalUrl = new URL(response.url || sourceUrl)
474
+ if (finalUrl.protocol !== 'https:' || !ALLOWED_DOWNLOAD_HOSTS.has(finalUrl.hostname)) {
475
+ throw new Error(`Installer redirected to an untrusted host: ${finalUrl.hostname || finalUrl.href}`)
476
+ }
477
+
478
+ const expectedBytes = Number(response.headers.get('content-length')) || undefined
479
+ if (expectedBytes && expectedBytes > MAX_INSTALLER_BYTES) throw new Error('Installer exceeds the 1 GiB safety limit.')
480
+ const filename = filenameFromResponse(response, spec)
481
+ const destination = await uniqueDestination(outputDirectory, filename)
482
+ const partial = `${destination}.${process.pid}.${Date.now()}.part`
483
+ const handle = await open(partial, 'wx', 0o600)
484
+ let receivedBytes = 0
485
+ let lastPercent = -1
486
+ let lastReportAt = 0
487
+
488
+ try {
489
+ for await (const chunk of response.body) {
490
+ if (signal?.aborted) throw signal.reason ?? new Error('Download cancelled.')
491
+ receivedBytes += chunk.byteLength
492
+ if (receivedBytes > MAX_INSTALLER_BYTES) throw new Error('Installer exceeds the 1 GiB safety limit.')
493
+ await handle.write(chunk)
494
+
495
+ const now = Date.now()
496
+ const percent = expectedBytes ? Math.floor((receivedBytes / expectedBytes) * 100) : undefined
497
+ if ((percent !== undefined && percent > lastPercent) || now - lastReportAt >= 1000) {
498
+ lastPercent = percent ?? lastPercent
499
+ lastReportAt = now
500
+ onProgress({ receivedBytes, expectedBytes, percent })
501
+ }
502
+ }
503
+ await handle.sync()
504
+ } catch (error) {
505
+ await handle.close().catch(() => {})
506
+ await rm(partial, { force: true })
507
+ throw error
508
+ }
509
+ await handle.close()
510
+
511
+ try {
512
+ if (expectedBytes !== undefined && receivedBytes !== expectedBytes) {
513
+ throw new Error(`Installer download was incomplete: received ${receivedBytes} of ${expectedBytes} bytes.`)
514
+ }
515
+ await verifyInstaller(partial, spec, receivedBytes)
516
+ await rename(partial, destination)
517
+ } catch (error) {
518
+ await rm(partial, { force: true })
519
+ throw error
520
+ }
521
+
522
+ return {
523
+ path: destination,
524
+ bytes: receivedBytes,
525
+ platform: spec.platform,
526
+ arch: spec.arch,
527
+ region: regionCode ?? 'unknown',
528
+ distribution,
529
+ sourceUrl,
530
+ }
531
+ }
532
+
533
+ export function createDownloadJob(options = {}) {
534
+ const controller = new AbortController()
535
+ let pendingOutput = ''
536
+ const append = (line) => {
537
+ pendingOutput += `${line}\n`
538
+ }
539
+ const formatProgress = ({ receivedBytes, expectedBytes, percent }) => {
540
+ append(`TABBIT_DOWNLOAD_PROGRESS ${JSON.stringify({ receivedBytes, expectedBytes, percent })}`)
541
+ }
542
+
543
+ const done = downloadInstaller({
544
+ ...options,
545
+ signal: controller.signal,
546
+ onProgress: formatProgress,
547
+ }).then(result => {
548
+ append(`TABBIT_INSTALLER_READY ${JSON.stringify(result)}`)
549
+ return { status: 'completed', detail: `installer saved to ${result.path}` }
550
+ }).catch(error => {
551
+ if (controller.signal.aborted) {
552
+ append('Tabbit Browser installer download was cancelled.')
553
+ return { status: 'killed', detail: 'download cancelled' }
554
+ }
555
+ append(`Tabbit Browser installer download failed: ${error instanceof Error ? error.message : String(error)}`)
556
+ return { status: 'failed', detail: 'download failed' }
557
+ }).finally(() => options.onSettled?.())
558
+
559
+ return {
560
+ cancel: reason => controller.abort(new Error(reason || 'Download cancelled.')),
561
+ done,
562
+ readOutput() {
563
+ const output = pendingOutput
564
+ pendingOutput = ''
565
+ return output
566
+ },
567
+ }
568
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "dsh-tabbit",
3
+ "version": "0.2.0",
4
+ "description": "DSH bundle that packages the Tabbit Browser skill and background installer",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Tabbit-Browser/dsh-tabbit.git"
8
+ },
9
+ "type": "module",
10
+ "main": "./index.js",
11
+ "exports": {
12
+ ".": "./index.js"
13
+ },
14
+ "files": [
15
+ "index.js",
16
+ "installer.js",
17
+ "update-check.js",
18
+ "cordis.patch.yml",
19
+ "skills/**",
20
+ "README.md",
21
+ "README.zh-CN.md"
22
+ ],
23
+ "scripts": {
24
+ "test": "node --test"
25
+ },
26
+ "dsh": {
27
+ "bundle": {
28
+ "patch": "./cordis.patch.yml"
29
+ }
30
+ },
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "peerDependencies": {
35
+ "@deepseek-ai/cordis": ">=4.0.0",
36
+ "@deepseek-ai/dsh-jobs": ">=0.1.0-rc.5",
37
+ "@deepseek-ai/dsh-skill": ">=0.1.0-rc.5",
38
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.5"
39
+ },
40
+ "license": "MIT"
41
+ }