dsh-plugin-weapp-gateway 0.8.23 → 0.8.34
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 -21
- package/PROTOCOL.md +1545 -1486
- package/README.md +503 -464
- package/THIRD-PARTY-NOTICES/dsh-cost-meter.txt +31 -31
- package/bin/setup-ip.mjs +474 -474
- package/cordis.patch.yml +50 -50
- package/docs/agent-team-app-integration.md +945 -0
- package/docs/blog-mobile-gateway.md +542 -542
- package/docs/dsh-0.1.5-rc.2-compatibility-audit.md +255 -255
- package/docs/dsh-0.1.6-alpha.1-compatibility-audit.md +122 -122
- package/docs/dsh-0.1.7-rc.1-compatibility-audit.md +52 -52
- package/docs/dsh-0.2.0-rc.2-plugin-management-mobile-integration.md +109 -109
- package/docs/dsh-rc2-mobile-integration.md +155 -155
- package/docs/file-upload-app-integration.md +250 -0
- package/docs/multi-gateway-app-integration.md +141 -125
- package/docs/multi-gateway-phase1-acceptance.md +179 -179
- package/docs/multi-gateway-todo.md +137 -137
- package/docs/remote-gateway-refactor-plan.md +52 -52
- package/docs/runtime-architecture.architecture.json +264 -264
- package/docs/runtime-architecture.html +15001 -15001
- package/docs/runtime-architecture.visual-check.html +32 -32
- package/docs/runtime-architecture.visual-check.json +548 -548
- package/docs/session-agent-preset-app-integration.md +93 -93
- package/docs/turn-outline-app-integration.md +196 -0
- package/docs/typert-remote-gateway-feature-checklist.md +294 -294
- package/docs/voice-input-implementation.md +219 -219
- package/docs/workspace-changes-mobile-integration.md +32 -32
- package/helper/dsh_mobile_gateway_helper.py +227 -227
- package/lib/account-bridge.mjs +57 -57
- package/lib/client.js +1409 -1361
- package/lib/cloudflare-tunnel.mjs +314 -314
- package/lib/cloudflared-binary.mjs +135 -135
- package/lib/context-bridge.mjs +3338 -1715
- package/lib/cost-meter-bridge.mjs +794 -794
- package/lib/cost-meter-dashboard-bridge.mjs +1655 -1449
- package/lib/devices.js +273 -245
- package/lib/dsh-host-adapter.mjs +508 -426
- package/lib/file-upload.mjs +205 -0
- package/lib/gateway-probe.mjs +6 -6
- package/lib/gateway-state.mjs +57 -57
- package/lib/gateway-tools.mjs +36 -36
- package/lib/history-cache.mjs +66 -0
- package/lib/index.mjs +4390 -3803
- package/lib/plugin-management.mjs +251 -233
- package/lib/public-endpoint-check.mjs +61 -61
- package/lib/relay-connector.mjs +213 -181
- package/lib/relay-state.mjs +83 -76
- package/lib/session-follower.mjs +140 -136
- package/lib/session-resume-bridge.mjs +86 -0
- package/lib/session-target.mjs +29 -0
- package/lib/speech-input.mjs +192 -192
- package/lib/vendor/cost-meter/aliyun-balance.mjs +123 -123
- package/lib/vendor/cost-meter/coding-plans.mjs +1668 -1668
- package/lib/vendor/cost-meter/credentials-shim.mjs +8 -8
- package/lib/vendor/cost-meter/custom-balance.mjs +281 -281
- package/lib/vendor/cost-meter/factory-quota.mjs +69 -69
- package/lib/vendor/cost-meter/gateway-quota-adapters.mjs +1006 -1006
- package/lib/vendor/cost-meter/gateway-quotas.mjs +664 -664
- package/lib/vendor/cost-meter/home-paths-shim.mjs +13 -13
- package/lib/vendor/cost-meter/net.mjs +183 -183
- package/lib/vendor/cost-meter/opendesign.mjs +204 -204
- package/lib/vendor/cost-meter/tokenrhythm-balance.mjs +95 -95
- package/lib/vendor/cost-meter/workbuddy-accounts.mjs +44 -44
- package/lib/vendor/cost-meter/workbuddy-transport.mjs +38 -38
- package/lib/vendor/cost-meter/workbuddy.mjs +356 -356
- package/lib/web-restart.mjs +47 -47
- package/lib/wire-json.mjs +7 -7
- package/lib/workspace-changes-bridge.mjs +32 -32
- package/package.json +54 -54
- package/relay/agent-connector.js +588 -569
- package/relay/vendor/tweetnacl.js +2391 -2391
package/lib/web-restart.mjs
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
// Detached handoff process: only launched by the loopback-only management API.
|
|
2
|
-
import { spawn } from 'node:child_process'
|
|
3
|
-
import { openSync, closeSync, appendFileSync } from 'node:fs'
|
|
4
|
-
|
|
5
|
-
const logFile = '/tmp/mobile-gateway.log'
|
|
6
|
-
const note = (message) => {
|
|
7
|
-
try { appendFileSync(logFile, `[${new Date().toISOString()}] web restart: ${message}\n`) } catch {}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
const spec = JSON.parse(Buffer.from(process.argv[2] || '', 'base64url').toString())
|
|
12
|
-
if (!Number.isSafeInteger(spec.pid) || spec.pid <= 0 || typeof spec.execPath !== 'string'
|
|
13
|
-
|| !Array.isArray(spec.execArgv) || !Array.isArray(spec.argv) || typeof spec.cwd !== 'string') {
|
|
14
|
-
throw new Error('invalid restart handoff')
|
|
15
|
-
}
|
|
16
|
-
const deadline = Date.now() + 30_000
|
|
17
|
-
while (Date.now() < deadline) {
|
|
18
|
-
try { process.kill(spec.pid, 0) } catch (error) {
|
|
19
|
-
if (error.code === 'ESRCH') break
|
|
20
|
-
throw error
|
|
21
|
-
}
|
|
22
|
-
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
process.kill(spec.pid, 0)
|
|
26
|
-
throw new Error('old DSH Web process did not exit within 30 seconds')
|
|
27
|
-
} catch (error) {
|
|
28
|
-
if (error.code !== 'ESRCH') throw error
|
|
29
|
-
}
|
|
30
|
-
const fd = openSync(logFile, 'a')
|
|
31
|
-
try {
|
|
32
|
-
const child = spawn(spec.execPath, [...spec.execArgv, ...spec.argv], {
|
|
33
|
-
cwd: spec.cwd, env: process.env, detached: true, stdio: ['ignore', fd, fd],
|
|
34
|
-
})
|
|
35
|
-
await new Promise((resolve, reject) => {
|
|
36
|
-
child.once('spawn', resolve)
|
|
37
|
-
child.once('error', reject)
|
|
38
|
-
})
|
|
39
|
-
child.unref()
|
|
40
|
-
note(`started DSH Web pid=${child.pid}`)
|
|
41
|
-
} finally {
|
|
42
|
-
closeSync(fd)
|
|
43
|
-
}
|
|
44
|
-
} catch (error) {
|
|
45
|
-
note(error?.message || String(error))
|
|
46
|
-
process.exitCode = 1
|
|
47
|
-
}
|
|
1
|
+
// Detached handoff process: only launched by the loopback-only management API.
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { openSync, closeSync, appendFileSync } from 'node:fs'
|
|
4
|
+
|
|
5
|
+
const logFile = '/tmp/mobile-gateway.log'
|
|
6
|
+
const note = (message) => {
|
|
7
|
+
try { appendFileSync(logFile, `[${new Date().toISOString()}] web restart: ${message}\n`) } catch {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const spec = JSON.parse(Buffer.from(process.argv[2] || '', 'base64url').toString())
|
|
12
|
+
if (!Number.isSafeInteger(spec.pid) || spec.pid <= 0 || typeof spec.execPath !== 'string'
|
|
13
|
+
|| !Array.isArray(spec.execArgv) || !Array.isArray(spec.argv) || typeof spec.cwd !== 'string') {
|
|
14
|
+
throw new Error('invalid restart handoff')
|
|
15
|
+
}
|
|
16
|
+
const deadline = Date.now() + 30_000
|
|
17
|
+
while (Date.now() < deadline) {
|
|
18
|
+
try { process.kill(spec.pid, 0) } catch (error) {
|
|
19
|
+
if (error.code === 'ESRCH') break
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
process.kill(spec.pid, 0)
|
|
26
|
+
throw new Error('old DSH Web process did not exit within 30 seconds')
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code !== 'ESRCH') throw error
|
|
29
|
+
}
|
|
30
|
+
const fd = openSync(logFile, 'a')
|
|
31
|
+
try {
|
|
32
|
+
const child = spawn(spec.execPath, [...spec.execArgv, ...spec.argv], {
|
|
33
|
+
cwd: spec.cwd, env: process.env, detached: true, stdio: ['ignore', fd, fd],
|
|
34
|
+
})
|
|
35
|
+
await new Promise((resolve, reject) => {
|
|
36
|
+
child.once('spawn', resolve)
|
|
37
|
+
child.once('error', reject)
|
|
38
|
+
})
|
|
39
|
+
child.unref()
|
|
40
|
+
note(`started DSH Web pid=${child.pid}`)
|
|
41
|
+
} finally {
|
|
42
|
+
closeSync(fd)
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
note(error?.message || String(error))
|
|
46
|
+
process.exitCode = 1
|
|
47
|
+
}
|
package/lib/wire-json.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Host 的摘要截断可能留下单个 UTF-16 代理项。JSON.stringify 会将其转义,
|
|
2
|
-
// 但 Swift/Foundation 仍拒绝整帧;在传输边界统一输出有效 Unicode。
|
|
3
|
-
// 不修改原对象、历史正文、有效 emoji 或字面量反斜杠转义文本。
|
|
4
|
-
export function stringifyWireFrame(frame) {
|
|
5
|
-
return JSON.stringify(frame, (_key, value) =>
|
|
6
|
-
typeof value === 'string' ? value.replace(/[\uD800-\uDFFF]/gu, '\uFFFD') : value)
|
|
7
|
-
}
|
|
1
|
+
// Host 的摘要截断可能留下单个 UTF-16 代理项。JSON.stringify 会将其转义,
|
|
2
|
+
// 但 Swift/Foundation 仍拒绝整帧;在传输边界统一输出有效 Unicode。
|
|
3
|
+
// 不修改原对象、历史正文、有效 emoji 或字面量反斜杠转义文本。
|
|
4
|
+
export function stringifyWireFrame(frame) {
|
|
5
|
+
return JSON.stringify(frame, (_key, value) =>
|
|
6
|
+
typeof value === 'string' ? value.replace(/[\uD800-\uDFFF]/gu, '\uFFFD') : value)
|
|
7
|
+
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
export const WORKSPACE_CHANGES_TYPES = new Set(['workspace-changes-summary', 'workspace-changes-diff'])
|
|
2
|
-
|
|
3
|
-
export async function handleWorkspaceChanges(api, msg) {
|
|
4
|
-
const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined
|
|
5
|
-
const failure = (code, message) => ({ kind: 'error', requestType: msg.type, requestId, code, message })
|
|
6
|
-
if (!requestId || requestId.length > 128 || typeof msg.sessionId !== 'string' || !msg.sessionId.trim() ||
|
|
7
|
-
msg.sessionId.length > 512 || !Number.isSafeInteger(msg.seq) || msg.seq < 0 ||
|
|
8
|
-
(msg.type === 'workspace-changes-diff' && (!Number.isSafeInteger(msg.index) || msg.index < 0))) {
|
|
9
|
-
return failure('bad-request', 'Invalid workspace change coordinates')
|
|
10
|
-
}
|
|
11
|
-
const service = api.workspaceChanges
|
|
12
|
-
if (!service) return failure('workspace-changes-unavailable', 'Workspace changes are unavailable on this host')
|
|
13
|
-
try {
|
|
14
|
-
const summary = service.summary(msg.sessionId, msg.seq)
|
|
15
|
-
let value = null
|
|
16
|
-
if (summary) {
|
|
17
|
-
if (msg.type === 'workspace-changes-summary') {
|
|
18
|
-
// Do not expose host working directories or internal snapshot locations.
|
|
19
|
-
const { turn, total, added, deleted } = summary
|
|
20
|
-
value = { turn, total, added, deleted, files: summary.files.map(file => ({
|
|
21
|
-
path: file.path, display: file.display, added: file.added, deleted: file.deleted,
|
|
22
|
-
...(file.binary === true ? { binary: true } : {}),
|
|
23
|
-
...(file.oversized === true ? { oversized: true } : {})
|
|
24
|
-
})) }
|
|
25
|
-
} else if (msg.index < summary.files.length) {
|
|
26
|
-
value = await service.diff(msg.sessionId, msg.seq, msg.index, AbortSignal.timeout(15000)) ?? null
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return { kind: msg.type, requestId, sessionId: msg.sessionId, seq: msg.seq,
|
|
30
|
-
...(msg.type === 'workspace-changes-diff' ? { index: msg.index } : {}), value }
|
|
31
|
-
} catch (_) { return failure('workspace-changes-failed', 'Unable to read workspace change snapshot') }
|
|
32
|
-
}
|
|
1
|
+
export const WORKSPACE_CHANGES_TYPES = new Set(['workspace-changes-summary', 'workspace-changes-diff'])
|
|
2
|
+
|
|
3
|
+
export async function handleWorkspaceChanges(api, msg) {
|
|
4
|
+
const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined
|
|
5
|
+
const failure = (code, message) => ({ kind: 'error', requestType: msg.type, requestId, code, message })
|
|
6
|
+
if (!requestId || requestId.length > 128 || typeof msg.sessionId !== 'string' || !msg.sessionId.trim() ||
|
|
7
|
+
msg.sessionId.length > 512 || !Number.isSafeInteger(msg.seq) || msg.seq < 0 ||
|
|
8
|
+
(msg.type === 'workspace-changes-diff' && (!Number.isSafeInteger(msg.index) || msg.index < 0))) {
|
|
9
|
+
return failure('bad-request', 'Invalid workspace change coordinates')
|
|
10
|
+
}
|
|
11
|
+
const service = api.workspaceChanges
|
|
12
|
+
if (!service) return failure('workspace-changes-unavailable', 'Workspace changes are unavailable on this host')
|
|
13
|
+
try {
|
|
14
|
+
const summary = service.summary(msg.sessionId, msg.seq)
|
|
15
|
+
let value = null
|
|
16
|
+
if (summary) {
|
|
17
|
+
if (msg.type === 'workspace-changes-summary') {
|
|
18
|
+
// Do not expose host working directories or internal snapshot locations.
|
|
19
|
+
const { turn, total, added, deleted } = summary
|
|
20
|
+
value = { turn, total, added, deleted, files: summary.files.map(file => ({
|
|
21
|
+
path: file.path, display: file.display, added: file.added, deleted: file.deleted,
|
|
22
|
+
...(file.binary === true ? { binary: true } : {}),
|
|
23
|
+
...(file.oversized === true ? { oversized: true } : {})
|
|
24
|
+
})) }
|
|
25
|
+
} else if (msg.index < summary.files.length) {
|
|
26
|
+
value = await service.diff(msg.sessionId, msg.seq, msg.index, AbortSignal.timeout(15000)) ?? null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { kind: msg.type, requestId, sessionId: msg.sessionId, seq: msg.seq,
|
|
30
|
+
...(msg.type === 'workspace-changes-diff' ? { index: msg.index } : {}), value }
|
|
31
|
+
} catch (_) { return failure('workspace-changes-failed', 'Unable to read workspace change snapshot') }
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-plugin-weapp-gateway",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "鲸尾移动网关:DeepSeek Harness 移动客户端连接与会话同步,支持安全配对、远程访问、上下文、编队、额度和语音输入",
|
|
5
|
-
"main": "lib/index.mjs",
|
|
6
|
-
"files": [
|
|
7
|
-
"bin",
|
|
8
|
-
"helper",
|
|
9
|
-
"lib",
|
|
10
|
-
"relay",
|
|
11
|
-
"docs",
|
|
12
|
-
"THIRD-PARTY-NOTICES",
|
|
13
|
-
"locale/*.json",
|
|
14
|
-
"cordis.patch.yml",
|
|
15
|
-
"PROTOCOL.md"
|
|
16
|
-
],
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/huiikeung/dsh-plugin-weapp-gateway.git"
|
|
20
|
-
},
|
|
21
|
-
"homepage": "https://github.com/huiikeung/dsh-plugin-weapp-gateway#readme",
|
|
22
|
-
"bugs": {
|
|
23
|
-
"url": "https://github.com/huiikeung/dsh-plugin-weapp-gateway/issues"
|
|
24
|
-
},
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"@deepseek-ai/schemastery": "^3.18.1",
|
|
27
|
-
"ws": "^8.18.0",
|
|
28
|
-
"qrcode": "^1.5.4"
|
|
29
|
-
},
|
|
30
|
-
"bin": "bin/setup-ip.mjs",
|
|
31
|
-
"scripts": {
|
|
32
|
-
"test": "node test/wire-json.test.mjs && node test/setup-ip.test.mjs && node test/host-adapter.test.mjs && node test/account-bridge.test.mjs && node test/context-bridge.test.mjs && node test/cost-meter.test.mjs && node --test test/cost-meter-dashboard.test.mjs && node test/clawbot-service.test.mjs && node test/speech-input.test.mjs && node test/speech-official.test.mjs && node test/plugin-management.test.mjs && node test/plugin-updates.test.mjs && node test/session-preset.test.mjs && node test/session-follower.test.mjs && node test/completion-notifications.test.mjs && node --test test/workspace-changes.test.mjs && node test/gateway.test.mjs && node test/auth.test.mjs && node test/lan.test.mjs && node test/cloudflared-binary.test.mjs && node test/cloudflare-tunnel.test.mjs && node --test test/public-endpoint-check.test.mjs && node test/gateway-tools.test.mjs && node test/relay-connector.test.mjs && node test/cloudflare-linux.test.mjs && node test/multi-gateway.test.mjs"
|
|
33
|
-
},
|
|
34
|
-
"exports": {
|
|
35
|
-
".": "./lib/index.mjs",
|
|
36
|
-
"./client": "./lib/client.js",
|
|
37
|
-
"./locale/*.json": "./locale/*.json",
|
|
38
|
-
"./package.json": "./package.json"
|
|
39
|
-
},
|
|
40
|
-
"dsh": {
|
|
41
|
-
"bundle": {
|
|
42
|
-
"patch": "./cordis.patch.yml"
|
|
43
|
-
},
|
|
44
|
-
"client": {
|
|
45
|
-
"platform": "web",
|
|
46
|
-
"inject": [
|
|
47
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
48
|
-
"@deepseek-ai/dsh-client-ui-layout",
|
|
49
|
-
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
50
|
-
]
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
"license": "MIT"
|
|
54
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-weapp-gateway",
|
|
3
|
+
"version": "0.8.34",
|
|
4
|
+
"description": "鲸尾移动网关:DeepSeek Harness 移动客户端连接与会话同步,支持安全配对、远程访问、上下文、编队、额度和语音输入",
|
|
5
|
+
"main": "lib/index.mjs",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin",
|
|
8
|
+
"helper",
|
|
9
|
+
"lib",
|
|
10
|
+
"relay",
|
|
11
|
+
"docs",
|
|
12
|
+
"THIRD-PARTY-NOTICES",
|
|
13
|
+
"locale/*.json",
|
|
14
|
+
"cordis.patch.yml",
|
|
15
|
+
"PROTOCOL.md"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/huiikeung/dsh-plugin-weapp-gateway.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/huiikeung/dsh-plugin-weapp-gateway#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/huiikeung/dsh-plugin-weapp-gateway/issues"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
27
|
+
"ws": "^8.18.0",
|
|
28
|
+
"qrcode": "^1.5.4"
|
|
29
|
+
},
|
|
30
|
+
"bin": "bin/setup-ip.mjs",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node test/wire-json.test.mjs && node --test test/history-cache.test.mjs && node test/session-target.test.mjs && node test/setup-ip.test.mjs && node test/host-adapter.test.mjs && node test/file-upload-adapter.test.mjs && node test/file-upload.test.mjs && node test/agent-team.test.mjs && node test/account-bridge.test.mjs && node test/context-bridge.test.mjs && node test/cost-meter.test.mjs && node --test test/cost-meter-dashboard.test.mjs && node test/clawbot-service.test.mjs && node test/speech-input.test.mjs && node test/speech-official.test.mjs && node test/plugin-management.test.mjs && node test/plugin-updates.test.mjs && node test/session-preset.test.mjs && node test/session-follower.test.mjs && node test/completion-notifications.test.mjs && node --test test/workspace-changes.test.mjs && node test/gateway.test.mjs && node test/auth.test.mjs && node test/lan.test.mjs && node test/cloudflared-binary.test.mjs && node test/cloudflare-tunnel.test.mjs && node --test test/public-endpoint-check.test.mjs && node test/gateway-tools.test.mjs && node test/relay-connector.test.mjs && node test/restart-reconnect.test.mjs && node test/cloudflare-linux.test.mjs && node test/multi-gateway.test.mjs"
|
|
33
|
+
},
|
|
34
|
+
"exports": {
|
|
35
|
+
".": "./lib/index.mjs",
|
|
36
|
+
"./client": "./lib/client.js",
|
|
37
|
+
"./locale/*.json": "./locale/*.json",
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"dsh": {
|
|
41
|
+
"bundle": {
|
|
42
|
+
"patch": "./cordis.patch.yml"
|
|
43
|
+
},
|
|
44
|
+
"client": {
|
|
45
|
+
"platform": "web",
|
|
46
|
+
"inject": [
|
|
47
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"license": "MIT"
|
|
54
|
+
}
|