@goodandready/dsh-subscriptions 0.7.46 → 0.7.48
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/accounts.js +4 -0
- package/lib/adapter-manager.js +1 -0
- package/lib/adapter.js +6 -1
- package/lib/antigravity-reasoning.js +80 -0
- package/lib/claude-keychain.js +49 -0
- package/lib/images.js +6 -2
- package/lib/import-auth.js +9 -2
- package/lib/index.js +1 -0
- package/lib/media-reference.js +38 -0
- package/lib/native-tools.js +5 -0
- package/lib/vendors/antigravity.js +12 -0
- package/package.json +1 -1
package/lib/accounts.js
CHANGED
|
@@ -51,6 +51,7 @@ import { recordSwitch, recordExhaust, recordBroken, computeHealthScore, healthBa
|
|
|
51
51
|
import { executeWithRetry } from './backoff.js'
|
|
52
52
|
import { getVendor } from './vendors/index.js'
|
|
53
53
|
import { versionForProvider } from './client-version-service.js'
|
|
54
|
+
import { writeClaudeKeychain } from './claude-keychain.js'
|
|
54
55
|
|
|
55
56
|
const SKEW_MS = 60 * 1000
|
|
56
57
|
const USAGE_TTL_MS = 2 * 60 * 1000
|
|
@@ -508,6 +509,9 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
508
509
|
accountId: next.accountId || blob.accountId,
|
|
509
510
|
}
|
|
510
511
|
if (ref) await saveBlob(ref, merged)
|
|
512
|
+
if (ref && provider === 'claude' && blob.accessToken && merged.accessToken !== blob.accessToken) {
|
|
513
|
+
writeClaudeKeychain(merged, blob.accessToken).catch(() => {})
|
|
514
|
+
}
|
|
511
515
|
if (ref) refreshFailures.delete(ref)
|
|
512
516
|
return merged
|
|
513
517
|
} catch (e) {
|
package/lib/adapter-manager.js
CHANGED
|
@@ -56,6 +56,7 @@ export function createAdapterManager({
|
|
|
56
56
|
switchAtRemaining: () => live().switchAtRemaining,
|
|
57
57
|
rotationStrategy: () => live().rotationStrategy,
|
|
58
58
|
modelAccountRules: () => live().modelAccountRules || [],
|
|
59
|
+
subagentSpeed: () => live().codexSubagentSpeed,
|
|
59
60
|
rateLimitWait: () => live().rateLimitWait,
|
|
60
61
|
rateLimitMaxWaitMs: () => live().rateLimitMaxWaitMs,
|
|
61
62
|
enableSoftFailureSentinel: () => live().enableSoftFailureSentinel,
|
package/lib/adapter.js
CHANGED
|
@@ -6,6 +6,7 @@ import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm'
|
|
|
6
6
|
import { displayName } from './refs.js'
|
|
7
7
|
import { applyModelSettings, bestAccountPlan } from './model-settings.js'
|
|
8
8
|
import { getSpeedMode } from './speed-mode.js'
|
|
9
|
+
import { resolveSpeed } from './account-features.js'
|
|
9
10
|
import { getVendor } from './vendors/index.js'
|
|
10
11
|
import { modelCatalog } from './messages.js'
|
|
11
12
|
import { streamWithRotation } from './stream-rotate.js'
|
|
@@ -205,7 +206,11 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
205
206
|
const defaultRef = typeof deps.defaultAccountRef === 'function' ? deps.defaultAccountRef(provider) : null
|
|
206
207
|
const streamOptions = {
|
|
207
208
|
...hydratedOptions,
|
|
208
|
-
...(provider === 'codex' &&
|
|
209
|
+
...(provider === 'codex' && resolveSpeed({
|
|
210
|
+
childSpeed: options.speed,
|
|
211
|
+
sessionSpeed: getSpeedMode(sessionId),
|
|
212
|
+
subagentSpeed: (options.subagent || options.parentSessionId) && typeof deps.subagentSpeed === 'function' ? deps.subagentSpeed() : undefined,
|
|
213
|
+
}) === 'fast' ? { service_tier: 'priority' } : {}),
|
|
209
214
|
...(effectiveReasoning.effort ? { reasoningEffort: effectiveReasoning.effort } : {}),
|
|
210
215
|
...(effectiveReasoning.budgetTokens ? { thinkingBudget: effectiveReasoning.budgetTokens } : {}),
|
|
211
216
|
// #830: kill switch. With no default account, use a sentinel that
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const FAMILIES = [
|
|
2
|
+
{ re: /claude/i, budget: 1024, effort: 'high', levels: ['low', 'medium', 'high'] },
|
|
3
|
+
{ re: /gpt-oss/i, budget: 8192, effort: 'medium', levels: ['low', 'medium', 'high'] },
|
|
4
|
+
{ re: /gemini-3\./i, budgets: { low: 1024, medium: 8192, high: 24576 }, effort: 'medium', levels: ['low', 'medium', 'high'] },
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
function familyFor(model) {
|
|
8
|
+
return FAMILIES.find((family) => family.re.test(String(model || ''))) || null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function codedError(message, code) {
|
|
12
|
+
/** @type {Error & { code?: string }} */
|
|
13
|
+
const err = new Error(message)
|
|
14
|
+
err.code = code
|
|
15
|
+
return err
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** #826: family-specific Antigravity reasoning budget with explicit errors. */
|
|
19
|
+
export function antigravityReasoning(model, effort, maxTokens) {
|
|
20
|
+
const family = familyFor(model)
|
|
21
|
+
if (!family) return { supported: false, thinkingBudget: null, effort: null, maxTokens }
|
|
22
|
+
const requested = String(effort || family.effort).toLowerCase()
|
|
23
|
+
if (!family.levels.includes(requested)) {
|
|
24
|
+
throw codedError('unsupported reasoning effort for ' + model + ': ' + requested, 'UNSUPPORTED_REASONING_EFFORT')
|
|
25
|
+
}
|
|
26
|
+
const thinkingBudget = family.budgets ? family.budgets[requested] : family.budget
|
|
27
|
+
if (maxTokens != null && Number(maxTokens) <= thinkingBudget) {
|
|
28
|
+
throw codedError('maxTokens must exceed thinking budget', 'UNSUPPORTED_REASONING_EFFORT')
|
|
29
|
+
}
|
|
30
|
+
return { supported: true, thinkingBudget, effort: requested, maxTokens }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deref(schema, root, seen) {
|
|
34
|
+
if (!schema || typeof schema !== 'object') return schema
|
|
35
|
+
if (typeof schema.$ref === 'string') {
|
|
36
|
+
const ref = schema.$ref
|
|
37
|
+
if (!ref.startsWith('#/')) {
|
|
38
|
+
throw codedError('remote $ref is not supported', 'UNSUPPORTED_TOOL_SCHEMA')
|
|
39
|
+
}
|
|
40
|
+
if (seen.has(ref)) {
|
|
41
|
+
throw codedError('cyclic $ref is not supported', 'UNSUPPORTED_TOOL_SCHEMA')
|
|
42
|
+
}
|
|
43
|
+
const path = ref.slice(2).split('/').map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
44
|
+
let target = root
|
|
45
|
+
for (const key of path) target = target && target[key]
|
|
46
|
+
if (!target) {
|
|
47
|
+
throw codedError('missing local $ref: ' + ref, 'UNSUPPORTED_TOOL_SCHEMA')
|
|
48
|
+
}
|
|
49
|
+
return deref(target, root, new Set([...seen, ref]))
|
|
50
|
+
}
|
|
51
|
+
return schema
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function strip(schema, root, seen) {
|
|
55
|
+
if (Array.isArray(schema)) return schema.map((item) => strip(item, root, seen))
|
|
56
|
+
if (!schema || typeof schema !== 'object') return schema
|
|
57
|
+
const resolved = deref(schema, root, seen)
|
|
58
|
+
if (resolved !== schema) return strip(resolved, root, seen)
|
|
59
|
+
const out = {}
|
|
60
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
61
|
+
if (key === '$schema' || key === '$id' || key === '$defs' || key === 'definitions') continue
|
|
62
|
+
out[key] = strip(value, root, seen)
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(out.anyOf)) {
|
|
65
|
+
const variants = out.anyOf.filter((v) => v && typeof v === 'object')
|
|
66
|
+
if (variants.length === 1) return variants[0]
|
|
67
|
+
if (variants.every((v) => v.type === variants[0].type)) out.type = variants[0].type
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(out.oneOf)) {
|
|
70
|
+
const variants = out.oneOf.filter((v) => v && typeof v === 'object')
|
|
71
|
+
if (variants.length === 1) return variants[0]
|
|
72
|
+
if (variants.every((v) => v.type === variants[0].type)) out.type = variants[0].type
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** #826: expand local refs, reject cycles/remote refs, strip metadata, widen unions. */
|
|
78
|
+
export function normalizeAntigravityToolSchema(schema) {
|
|
79
|
+
return strip(schema, schema, new Set())
|
|
80
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile)
|
|
5
|
+
const SERVICE = 'Claude Code-credentials'
|
|
6
|
+
|
|
7
|
+
export function validClaudeBlob(blob) {
|
|
8
|
+
if (!blob || typeof blob !== 'object') return false
|
|
9
|
+
if (typeof blob.accessToken !== 'string' || !blob.accessToken) return false
|
|
10
|
+
if (typeof blob.refreshToken !== 'string' || !blob.refreshToken) return false
|
|
11
|
+
return Number.isFinite(Number(blob.expiresAt))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mergeClaudeCredentials(current, cli) {
|
|
15
|
+
if (!validClaudeBlob(current)) return validClaudeBlob(cli) ? { ...cli } : null
|
|
16
|
+
if (!validClaudeBlob(cli)) return { ...current }
|
|
17
|
+
const currentExpiry = Number(current.expiresAt) || 0
|
|
18
|
+
const cliExpiry = Number(cli.expiresAt) || 0
|
|
19
|
+
if (cli.accessToken && cli.accessToken !== current.accessToken && cliExpiry > currentExpiry) {
|
|
20
|
+
return { ...current, ...cli }
|
|
21
|
+
}
|
|
22
|
+
return { ...current }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function canWriteClaudeKeychain(currentBlob, expectedAccessToken) {
|
|
26
|
+
return Boolean(currentBlob && expectedAccessToken && currentBlob.accessToken === expectedAccessToken)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** macOS-only read; returns null on any failure so accounts stay usable. */
|
|
30
|
+
export async function readClaudeKeychain({ exec = execFileAsync, service = SERVICE } = {}) {
|
|
31
|
+
if (process.platform !== 'darwin') return null
|
|
32
|
+
try {
|
|
33
|
+
const { stdout } = await exec('/usr/bin/security', ['find-generic-password', '-s', service, '-w'])
|
|
34
|
+
const blob = JSON.parse(String(stdout || '').trim())
|
|
35
|
+
return validClaudeBlob(blob) ? blob : null
|
|
36
|
+
} catch { return null }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Write back only when the stored blob still owns the expected old access token. */
|
|
40
|
+
export async function writeClaudeKeychain(blob, expectedAccessToken, { exec = execFileAsync, service = SERVICE, account = 'claude' } = {}) {
|
|
41
|
+
if (process.platform !== 'darwin') return false
|
|
42
|
+
if (!validClaudeBlob(blob) || !expectedAccessToken) return false
|
|
43
|
+
const current = await readClaudeKeychain({ exec, service })
|
|
44
|
+
if (!canWriteClaudeKeychain(current, expectedAccessToken)) return false
|
|
45
|
+
try {
|
|
46
|
+
await exec('/usr/bin/security', ['add-generic-password', '-U', '-s', service, '-a', account, '-w', JSON.stringify(blob)])
|
|
47
|
+
return true
|
|
48
|
+
} catch { return false }
|
|
49
|
+
}
|
package/lib/images.js
CHANGED
|
@@ -28,7 +28,7 @@ const GROK_ASPECT = {
|
|
|
28
28
|
auto: 'auto',
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function codexBody({ prompt, size, quality }) {
|
|
31
|
+
export function codexBody({ prompt, size, quality, referenceImages = [] }) {
|
|
32
32
|
const text = String(prompt || '').trim()
|
|
33
33
|
if (!text) throw new Error('prompt must be a non-empty string')
|
|
34
34
|
return {
|
|
@@ -36,10 +36,13 @@ export function codexBody({ prompt, size, quality }) {
|
|
|
36
36
|
model: CODEX_MODEL,
|
|
37
37
|
...(size ? { size } : {}),
|
|
38
38
|
...(quality ? { quality } : {}),
|
|
39
|
+
...(Array.isArray(referenceImages) && referenceImages.length
|
|
40
|
+
? { images: referenceImages.map((ref) => ({ image_url: ref })) }
|
|
41
|
+
: {}),
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
export function grokBody({ prompt, size, quality }) {
|
|
45
|
+
export function grokBody({ prompt, size, quality, referenceImages = [] }) {
|
|
43
46
|
const text = String(prompt || '').trim()
|
|
44
47
|
if (!text) throw new Error('prompt must be a non-empty string')
|
|
45
48
|
// Grok has only two quality tiers: high is composed from medium.
|
|
@@ -52,6 +55,7 @@ export function grokBody({ prompt, size, quality }) {
|
|
|
52
55
|
response_format: 'b64_json',
|
|
53
56
|
...(size && GROK_ASPECT[size] ? { aspect_ratio: GROK_ASPECT[size] } : {}),
|
|
54
57
|
...(level ? { quality: level } : {}),
|
|
58
|
+
...(Array.isArray(referenceImages) && referenceImages.length ? { image_urls: referenceImages } : {}),
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
|
package/lib/import-auth.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { readClaudeCredentials } from './vendors/claude-cli.js'
|
|
6
|
+
import { readClaudeKeychain } from './claude-keychain.js'
|
|
6
7
|
import { readCopilotCredentials } from './vendors/copilot-cli.js'
|
|
7
8
|
import { normalizeExpiresAt } from './blob.js'
|
|
8
9
|
import { isGoogleClientId, extractGoogleClientIdFromJwt, emailFromToken } from './jwt.js'
|
|
@@ -250,11 +251,17 @@ async function discoverLocalCliSessionsInner() {
|
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
// 8. Claude CLI Direct Import
|
|
253
|
-
const
|
|
254
|
+
const fileCreds = await readClaudeCredentials(activeHome)
|
|
255
|
+
const keychainCreds = fileCreds ? null : await readClaudeKeychain()
|
|
256
|
+
const claudeCreds = fileCreds || (keychainCreds ? {
|
|
257
|
+
...keychainCreds,
|
|
258
|
+
sourcePath: 'macOS Keychain (Claude Code-credentials)',
|
|
259
|
+
email: keychainCreds.email || '',
|
|
260
|
+
} : null)
|
|
254
261
|
if (claudeCreds) {
|
|
255
262
|
const claudeEntry = {
|
|
256
263
|
provider: 'claude',
|
|
257
|
-
source: 'file',
|
|
264
|
+
source: fileCreds ? 'file' : 'keychain',
|
|
258
265
|
path: claudeCreds.sourcePath || homeFile('.claude', '.credentials.json'),
|
|
259
266
|
email: claudeCreds.email || 'Claude Code CLI User',
|
|
260
267
|
hasRefreshToken: !!claudeCreds.refreshToken,
|
package/lib/index.js
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const MAX_REFERENCE_IMAGES = 5
|
|
2
|
+
|
|
3
|
+
/** #801: validate attachment refs before any generation call. */
|
|
4
|
+
export function validateReferenceImages(refs) {
|
|
5
|
+
if (refs == null) return { ok: true, refs: [] }
|
|
6
|
+
if (!Array.isArray(refs)) {
|
|
7
|
+
return { ok: false, error: 'referenceImages must be an array' }
|
|
8
|
+
}
|
|
9
|
+
const clean = refs.map((ref) => String(ref || '').trim()).filter(Boolean)
|
|
10
|
+
if (!clean.length) return { ok: false, error: 'referenceImages must not be empty' }
|
|
11
|
+
if (clean.length > MAX_REFERENCE_IMAGES) return { ok: false, error: 'at most 5 reference images are allowed' }
|
|
12
|
+
if (new Set(clean).size !== clean.length) return { ok: false, error: 'duplicate reference images are not allowed' }
|
|
13
|
+
return { ok: true, refs: clean }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** #801: provider-specific image-edit request bodies. */
|
|
17
|
+
export function referenceImageRequest(provider, { prompt, references = [], model, size } = {}) {
|
|
18
|
+
const checked = validateReferenceImages(references)
|
|
19
|
+
if (!checked.ok) {
|
|
20
|
+
const err = new Error(checked.error)
|
|
21
|
+
err.code = 'INVALID_MEDIA_REFERENCE'
|
|
22
|
+
throw err
|
|
23
|
+
}
|
|
24
|
+
if (provider === 'grok') {
|
|
25
|
+
return {
|
|
26
|
+
model: model || 'grok-2-image',
|
|
27
|
+
prompt,
|
|
28
|
+
image_urls: checked.refs,
|
|
29
|
+
...(size ? { size } : {}),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
model: model || 'gpt-image-1',
|
|
34
|
+
prompt,
|
|
35
|
+
images: checked.refs.map((ref) => ({ image_url: ref })),
|
|
36
|
+
...(size ? { size } : {}),
|
|
37
|
+
}
|
|
38
|
+
}
|
package/lib/native-tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { validateReferenceImages } from './media-reference.js'
|
|
3
4
|
|
|
4
5
|
const TOOL_OUTPUT = {
|
|
5
6
|
schema: {
|
|
@@ -86,12 +87,15 @@ export function registerSubscriptionTools(ctx, deps, options = {}) {
|
|
|
86
87
|
provider: { type: 'string', enum: ['gpt', 'codex', 'grok'], description: 'Preferred provider' },
|
|
87
88
|
size: { type: 'string', description: 'Image size, e.g. 1024x1024' },
|
|
88
89
|
quality: { type: 'string', description: 'low, medium or high' },
|
|
90
|
+
referenceImages: { type: 'array', items: { type: 'string' }, maxItems: 5, description: '1–5 attachment refs for image editing' },
|
|
89
91
|
},
|
|
90
92
|
required: ['prompt'],
|
|
91
93
|
},
|
|
92
94
|
async execute(args, exec) {
|
|
93
95
|
const images = deps.images && deps.images()
|
|
94
96
|
if (!images || typeof images.generate !== 'function') throw new Error('subscription image service is unavailable')
|
|
97
|
+
const references = validateReferenceImages(args && args.referenceImages)
|
|
98
|
+
if (!references.ok) throw new Error(references.error)
|
|
95
99
|
const preferred = String((args && args.provider) || 'gpt').toLowerCase()
|
|
96
100
|
const order = preferred === 'grok' ? ['grok', 'codex'] : ['codex', 'grok']
|
|
97
101
|
let lastError = null
|
|
@@ -102,6 +106,7 @@ export function registerSubscriptionTools(ctx, deps, options = {}) {
|
|
|
102
106
|
prompt: args && args.prompt,
|
|
103
107
|
size: args && args.size,
|
|
104
108
|
quality: args && args.quality,
|
|
109
|
+
referenceImages: references.refs,
|
|
105
110
|
signal: exec && exec.signal,
|
|
106
111
|
})
|
|
107
112
|
const files = await saveImages(rows, exec)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
3
3
|
import { googleContents } from '../messages.js'
|
|
4
|
+
import { antigravityReasoning, normalizeAntigravityToolSchema } from '../antigravity-reasoning.js'
|
|
4
5
|
import { formTokenRequest, googleStream, throwHttpError, tokenBlobFromOAuth , readErrorBody } from '../wire.js'
|
|
5
6
|
import { fetchWithTimeout } from '../http.js'
|
|
6
7
|
import { emailFromToken, avatarFromToken, scopesFromToken } from '../jwt.js'
|
|
@@ -221,6 +222,17 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, signal, s
|
|
|
221
222
|
if (tiered) {
|
|
222
223
|
if (!request.generationConfig) request.generationConfig = {}
|
|
223
224
|
request.generationConfig.thinkingConfig = /** @type {any} */ ({ thinkingLevel: tiered.thinkingLevel })
|
|
225
|
+
} else {
|
|
226
|
+
const reasoning = antigravityReasoning(options.model, options.reasoningEffort, options.maxTokens)
|
|
227
|
+
if (reasoning.supported) {
|
|
228
|
+
if (!request.generationConfig) request.generationConfig = {}
|
|
229
|
+
request.generationConfig.thinkingConfig = /** @type {any} */ ({ thinkingBudget: reasoning.thinkingBudget })
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const declaration of (request.tools && request.tools[0] && request.tools[0].functionDeclarations) || []) {
|
|
233
|
+
if (declaration && declaration.parameters) {
|
|
234
|
+
declaration.parameters = normalizeAntigravityToolSchema(declaration.parameters)
|
|
235
|
+
}
|
|
224
236
|
}
|
|
225
237
|
const { envelope, sessionId } = streamEnvelope({
|
|
226
238
|
projectId: ready.projectId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.48",
|
|
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",
|