@magpie-community/opencode-trae-auth 0.2.8 → 0.2.10
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/README.md +17 -0
- package/index.mjs +119 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,23 @@ authorization page: `device_id` (19 digits) and `machine_id` (32 hex). Every
|
|
|
46
46
|
request then sends that same device. The relays saw requests dropped when the
|
|
47
47
|
device was new each time.
|
|
48
48
|
|
|
49
|
+
### Enterprise (SaaS) accounts
|
|
50
|
+
|
|
51
|
+
Since 0.2.10 the authorization page also offers Trae's enterprise (SaaS / SSO)
|
|
52
|
+
sign-in; before, the plugin asked the page to hide it (`hide_saas_login`), so
|
|
53
|
+
an enterprise subscription could only sign in as a personal account. A host
|
|
54
|
+
the sign-in names for the account is kept when it is an https host on Trae's
|
|
55
|
+
own domains (`trae.cn`, `trae.ai`, `mchost.guru`): a model host other than the
|
|
56
|
+
consumer site's own (`www`, `api`, `grow…`) takes the account's model
|
|
57
|
+
requests, and a token host the callback names (`authHost`) takes its
|
|
58
|
+
renewals. Any other host is ignored and the consumer hosts are used, as for a
|
|
59
|
+
personal account, which is unchanged. The account's tenant and login-type
|
|
60
|
+
fields are kept with the sign-in; nothing sends them.
|
|
61
|
+
|
|
62
|
+
**Not tried with a real enterprise account.** Which hosts and fields an
|
|
63
|
+
enterprise sign-in sends back is not known yet; if yours doesn't work, open an
|
|
64
|
+
issue with what the sign-in page showed.
|
|
65
|
+
|
|
49
66
|
### The international realm's region
|
|
50
67
|
|
|
51
68
|
Trae Global has two deployments. The sign-in's `AIRegion` (or a US auth host
|
package/index.mjs
CHANGED
|
@@ -79,11 +79,19 @@ const SITES = {
|
|
|
79
79
|
// the model host for an account: Trae CN's own hosts serve no models,
|
|
80
80
|
// so only a host that is one is taken, else the one gateway
|
|
81
81
|
apiOf: (a) => {
|
|
82
|
-
const h =
|
|
83
|
-
return
|
|
82
|
+
const h = traeHost(a?.api)
|
|
83
|
+
return h && SITES["trae-cn"].modelHost(h) ? h : SITES["trae-cn"].hosts.api
|
|
84
|
+
},
|
|
85
|
+
// whether a host named by a sign-in is one of the model hosts: an https
|
|
86
|
+
// host on mchost.guru or named trae-api-…, as the consumer gateway is,
|
|
87
|
+
// or another trae.cn host than the consumer site's own (www, api,
|
|
88
|
+
// grow…), which an enterprise (SaaS) sign-in may name for its tenant
|
|
89
|
+
modelHost: (h) => {
|
|
90
|
+
const t = traeHost(h)
|
|
91
|
+
if (!t) return false
|
|
92
|
+
const n = new URL(t).hostname
|
|
93
|
+
return /(^|\.)mchost\.guru$|^trae-api-/i.test(n) || (/\.trae\.cn$/i.test(n) && !/^(www|api|grow[^.]*)\./i.test(n))
|
|
84
94
|
},
|
|
85
|
-
// whether a host named by a sign-in is one of the model hosts
|
|
86
|
-
modelHost: (h) => /mchost\.guru|trae-api-/i.test(String(h ?? "")),
|
|
87
95
|
// ownPage: url is one of Trae CN's own JSON pages on api.trae.cn
|
|
88
96
|
// (/trae/api/…: the daily check-in, credits), which the account's fetch
|
|
89
97
|
// sends as the account rather than as a chat
|
|
@@ -166,15 +174,22 @@ const SITES = {
|
|
|
166
174
|
// the model host: the account's region (or a US host the sign-in named)
|
|
167
175
|
// selects the US deployment, else the SG one
|
|
168
176
|
apiOf: (a) => {
|
|
169
|
-
const h =
|
|
177
|
+
const h = traeHost(a?.api)
|
|
178
|
+
const n = h ? new URL(h).hostname : ""
|
|
170
179
|
const region = String(a?.region ?? "").toUpperCase()
|
|
171
|
-
if (/coreva/i.test(
|
|
172
|
-
if (/coresg/i.test(
|
|
180
|
+
if (/coreva/i.test(n) || /api-us(?:[-.]|$)/i.test(n) || /^US(?:[-_ ]|$)/.test(region)) return SITES["trae-global"].hosts.us
|
|
181
|
+
if (/coresg/i.test(n)) return h
|
|
182
|
+
// another trae.ai host than the site's own (www, api-…, grow…): one an
|
|
183
|
+
// enterprise (SaaS) sign-in named for its tenant
|
|
184
|
+
if (/\.trae\.ai$/i.test(n) && !/^(www\.|api[-.]|grow)/i.test(n)) return h
|
|
173
185
|
return SITES["trae-global"].hosts.api
|
|
174
186
|
},
|
|
175
187
|
// a sign-in names a host of Trae's, which can be an auth host
|
|
176
188
|
// (api-us-east), not the chat host
|
|
177
|
-
modelHost: (h) =>
|
|
189
|
+
modelHost: (h) => {
|
|
190
|
+
const t = traeHost(h)
|
|
191
|
+
return !!t && /(^|\.)trae\.ai$/i.test(new URL(t).hostname)
|
|
192
|
+
},
|
|
178
193
|
// Trae Global serves no own JSON pages through the account's fetch
|
|
179
194
|
ownPage: () => false,
|
|
180
195
|
// the account's region, from the sign-in's user info
|
|
@@ -213,6 +228,46 @@ const SITES = {
|
|
|
213
228
|
},
|
|
214
229
|
}
|
|
215
230
|
|
|
231
|
+
// traeHost is a host a sign-in named, when it is a well-formed https one
|
|
232
|
+
// on Trae's own domains (no port, no credentials), as origin plus any path;
|
|
233
|
+
// "" for anything else, which is then not taken
|
|
234
|
+
const TRAE_DOMAINS = ["trae.cn", "trae.ai", "mchost.guru"]
|
|
235
|
+
function traeHost(h) {
|
|
236
|
+
try {
|
|
237
|
+
const u = new URL(String(h ?? "").trim())
|
|
238
|
+
if (u.protocol !== "https:" || u.username || u.password || u.port || u.search || u.hash) return ""
|
|
239
|
+
const n = u.hostname.toLowerCase()
|
|
240
|
+
if (!TRAE_DOMAINS.some((d) => n === d || n.endsWith("." + d))) return ""
|
|
241
|
+
return (u.origin + u.pathname).replace(/\/+$/, "")
|
|
242
|
+
} catch {
|
|
243
|
+
return ""
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// authHostOf is the host a sign-in named for its tokens, when it named one
|
|
248
|
+
// on the site's own domain (an enterprise's may differ from the consumer
|
|
249
|
+
// one); "" when it named none, and the site's own is used
|
|
250
|
+
function authHostOf(h, site) {
|
|
251
|
+
const t = traeHost(h)
|
|
252
|
+
if (!t) return ""
|
|
253
|
+
const n = new URL(t).hostname
|
|
254
|
+
return n === site.webHost || n.endsWith("." + site.webHost) ? t : ""
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// accountOf keeps the account fields of the sign-in's user info that say
|
|
258
|
+
// what kind of account it is (tenant, enterprise, login type), plain values
|
|
259
|
+
// only and never anything that looks like a secret, for later use; nothing
|
|
260
|
+
// sends them
|
|
261
|
+
function accountOf(info) {
|
|
262
|
+
const out = {}
|
|
263
|
+
for (const [k, v] of Object.entries(info ?? {})) {
|
|
264
|
+
if (!/tenant|enterprise|saas|login|org|team|sso|^ent|usertype|accounttype/i.test(k)) continue
|
|
265
|
+
if (/token|secret|key|password|cookie|session|jwt/i.test(k)) continue
|
|
266
|
+
if (v === null || ["string", "number", "boolean"].includes(typeof v)) out[k] = v
|
|
267
|
+
}
|
|
268
|
+
return out
|
|
269
|
+
}
|
|
270
|
+
|
|
216
271
|
const SIGN_IN_TIMEOUT = 10 * 60 * 1000
|
|
217
272
|
const EARLY_MS = 2 * 60 * 1000 // a token this close to its end is renewed before a request
|
|
218
273
|
const LEAD_MS = 10 * 60 * 1000 // and this close, magpie renews it ahead of time (auth.refresh)
|
|
@@ -301,6 +356,11 @@ function toAuth(s, site) {
|
|
|
301
356
|
// the model host the sign-in named, if it named one
|
|
302
357
|
...(site.modelHost(s.api) ? { api: s.api } : {}),
|
|
303
358
|
...(s.region ? { region: s.region } : {}),
|
|
359
|
+
// the host the sign-in named, its own token host if it named one, and
|
|
360
|
+
// what kind of account it is (an enterprise one's tenant), kept as given
|
|
361
|
+
...(traeHost(s.host) ? { host: traeHost(s.host) } : {}),
|
|
362
|
+
...(authHostOf(s.authHost, site) ? { authHost: authHostOf(s.authHost, site) } : {}),
|
|
363
|
+
...(s.account && Object.keys(s.account).length ? { account: s.account } : {}),
|
|
304
364
|
}
|
|
305
365
|
}
|
|
306
366
|
|
|
@@ -319,6 +379,9 @@ function fromAuth(auth, site) {
|
|
|
319
379
|
devicePrivateKey: auth.devicePrivateKey || "",
|
|
320
380
|
api: auth.api || "",
|
|
321
381
|
region: site.regionOf(auth),
|
|
382
|
+
host: auth.host || "",
|
|
383
|
+
authHost: authHostOf(auth.authHost, site),
|
|
384
|
+
account: auth.account && typeof auth.account === "object" ? auth.account : {},
|
|
322
385
|
}
|
|
323
386
|
}
|
|
324
387
|
|
|
@@ -405,6 +468,7 @@ async function exchange(refresh, clientId, site, host = site.hosts.auth) {
|
|
|
405
468
|
name: String(info.ScreenName ?? info.screenName ?? ""),
|
|
406
469
|
api: info.Host ?? "",
|
|
407
470
|
region: site.regionOf(info),
|
|
471
|
+
account: accountOf(info),
|
|
408
472
|
}
|
|
409
473
|
}
|
|
410
474
|
const e = errorOf(text)
|
|
@@ -495,12 +559,14 @@ async function boundExchange(a, site, host = site.hosts.auth) {
|
|
|
495
559
|
// before 0.2.3 (exchange). A device key made for the account goes with
|
|
496
560
|
// either, so the next renewal signs with the same one.
|
|
497
561
|
async function renewal(a, site) {
|
|
498
|
-
|
|
562
|
+
// the token host the sign-in named, else the site's
|
|
563
|
+
const host = authHostOf(a.authHost, site) || site.hosts.auth
|
|
564
|
+
if (!site.deviceExchange) return exchange(a.refresh, a.clientId, site, host)
|
|
499
565
|
const keys = a.devicePrivateKey && a.devicePublicKey ? {} : newDeviceKey()
|
|
500
566
|
try {
|
|
501
|
-
return await boundExchange({ ...a, ...keys }, site)
|
|
567
|
+
return await boundExchange({ ...a, ...keys }, site, host)
|
|
502
568
|
} catch {}
|
|
503
|
-
return { ...(await exchange(a.refresh, a.clientId, site)), ...keys }
|
|
569
|
+
return { ...(await exchange(a.refresh, a.clientId, site, host)), ...keys }
|
|
504
570
|
}
|
|
505
571
|
|
|
506
572
|
// ---- the browser sign-in --------------------------------------------------------
|
|
@@ -527,10 +593,15 @@ async function signedIn(q, device, site) {
|
|
|
527
593
|
name: String(info.ScreenName ?? info.screenName ?? ""),
|
|
528
594
|
api: host,
|
|
529
595
|
region: site.regionOf(info),
|
|
596
|
+
host,
|
|
597
|
+
// a token host the callback named, if it named one
|
|
598
|
+
authHost: authHostOf(q.get("authHost") || q.get("auth_host") || info.AuthHost || info.authHost, site),
|
|
599
|
+
account: accountOf(info),
|
|
530
600
|
}
|
|
601
|
+
const authHost = s.authHost || site.hosts.auth
|
|
531
602
|
if (!s.token && s.refresh) {
|
|
532
|
-
const x = await exchange(s.refresh, s.clientId, site)
|
|
533
|
-
s = { ...s, ...x, uid: x.uid || s.uid, name: x.name || s.name, api: x.api || s.api, region: x.region || s.region }
|
|
603
|
+
const x = await exchange(s.refresh, s.clientId, site, authHost)
|
|
604
|
+
s = { ...s, ...x, uid: x.uid || s.uid, name: x.name || s.name, api: x.api || s.api, region: x.region || s.region, host: s.host || x.api, account: { ...x.account, ...s.account } }
|
|
534
605
|
}
|
|
535
606
|
if (!s.token) throw new Error(`${site.webHost} sent back no token`)
|
|
536
607
|
if (!s.refresh) throw new Error(`${site.webHost} sent back no refresh token`)
|
|
@@ -539,7 +610,7 @@ async function signedIn(q, device, site) {
|
|
|
539
610
|
// one at once, so a check-in takes it; when Trae won't, it does as is
|
|
540
611
|
if (site.deviceExchange) {
|
|
541
612
|
try {
|
|
542
|
-
const x = await boundExchange(s, site)
|
|
613
|
+
const x = await boundExchange(s, site, authHost)
|
|
543
614
|
s = { ...s, token: x.token, refresh: x.refresh, expires: x.expires || s.expires }
|
|
544
615
|
} catch {}
|
|
545
616
|
}
|
|
@@ -616,7 +687,9 @@ async function browserSignIn(site) {
|
|
|
616
687
|
x_env: "",
|
|
617
688
|
x_app_version: site.ideVersion,
|
|
618
689
|
x_app_type: "stable",
|
|
619
|
-
|
|
690
|
+
// the enterprise (SaaS) sign-in is offered beside the personal one: an
|
|
691
|
+
// enterprise subscription signs in there (@shad70w0 on X)
|
|
692
|
+
hide_saas_login: "false",
|
|
620
693
|
})
|
|
621
694
|
return {
|
|
622
695
|
url: `${site.hosts.web}/authorization?${q}`,
|
|
@@ -1067,6 +1140,15 @@ class TextTools {
|
|
|
1067
1140
|
this.after = true
|
|
1068
1141
|
}
|
|
1069
1142
|
if (end) {
|
|
1143
|
+
// an opener the stream ended inside (<tool_call name): a call cut
|
|
1144
|
+
// short, not the answer (plugins#38)
|
|
1145
|
+
const h = this.buf.search(OPEN_HOLD)
|
|
1146
|
+
if (h >= 0 && !quoted(this.sent + text + this.buf.slice(0, h))) {
|
|
1147
|
+
text += this.buf.slice(0, h)
|
|
1148
|
+
this.buf = ""
|
|
1149
|
+
this.sent += text
|
|
1150
|
+
return { text, calls, reasoning, error: CUT_SHORT }
|
|
1151
|
+
}
|
|
1070
1152
|
text += this.buf
|
|
1071
1153
|
this.buf = ""
|
|
1072
1154
|
} else {
|
|
@@ -1088,11 +1170,11 @@ class TextTools {
|
|
|
1088
1170
|
// gave as <tool_call name="…">.
|
|
1089
1171
|
block(body, named, end) {
|
|
1090
1172
|
if (!end && !body.trim()) return { wait: true }
|
|
1091
|
-
const close = body
|
|
1173
|
+
const [close, closeLen] = closeAt(body)
|
|
1092
1174
|
if (named) {
|
|
1093
1175
|
if (close < 0 && !end) return { wait: true }
|
|
1094
1176
|
const region = close >= 0 ? body.slice(0, close) : body
|
|
1095
|
-
const rest = close >= 0 ? body.slice(close +
|
|
1177
|
+
const rest = close >= 0 ? body.slice(close + closeLen) : ""
|
|
1096
1178
|
if (region.trimStart().startsWith("{")) {
|
|
1097
1179
|
const v = repairedJSON(region.trim())
|
|
1098
1180
|
if (!v) return { error: close >= 0 ? UNREADABLE : CUT_SHORT }
|
|
@@ -1116,7 +1198,7 @@ class TextTools {
|
|
|
1116
1198
|
const region = body.slice(ph[0].length, close >= 0 ? close : body.length)
|
|
1117
1199
|
const p = paramArgs(region, ph[1], this.tools)
|
|
1118
1200
|
if (close < 0 && !p.closed) return { error: CUT_SHORT }
|
|
1119
|
-
return { call: { name: ph[1], arguments: JSON.stringify(p.args) }, rest: close >= 0 ? body.slice(close +
|
|
1201
|
+
return { call: { name: ph[1], arguments: JSON.stringify(p.args) }, rest: close >= 0 ? body.slice(close + closeLen) : "" }
|
|
1120
1202
|
}
|
|
1121
1203
|
const o = objectEnd(body, 0)
|
|
1122
1204
|
if (o > 0 && !this.bad.has(":" + o)) {
|
|
@@ -1128,10 +1210,10 @@ class TextTools {
|
|
|
1128
1210
|
}
|
|
1129
1211
|
// not whole, or not JSON as it stands: read up to a close (a file's
|
|
1130
1212
|
// content may hold one), or at the end all of it
|
|
1131
|
-
for (let j = close; j >= 0; j = body
|
|
1213
|
+
for (let [j, n] = [close, closeLen]; j >= 0; [j, n] = closeAt(body, j + 1)) {
|
|
1132
1214
|
if (this.bad.has("<" + j)) continue
|
|
1133
1215
|
const v = callJSON(body.slice(0, j))
|
|
1134
|
-
if (v) return { call: { name: String(v.name), arguments: callArgs(v) }, rest: body.slice(j +
|
|
1216
|
+
if (v) return { call: { name: String(v.name), arguments: callArgs(v) }, rest: body.slice(j + n) }
|
|
1135
1217
|
this.bad.add("<" + j)
|
|
1136
1218
|
}
|
|
1137
1219
|
if (!end) return { wait: true }
|
|
@@ -1145,7 +1227,7 @@ class TextTools {
|
|
|
1145
1227
|
const region = close >= 0 ? body.slice(0, close) : body
|
|
1146
1228
|
const w = glmCall(region, this.tools)
|
|
1147
1229
|
// GLM's call, cut short at the end, is still the call
|
|
1148
|
-
if (w) return { call: { name: w.name, arguments: callArgs(w) }, rest: close >= 0 ? body.slice(close +
|
|
1230
|
+
if (w) return { call: { name: w.name, arguments: callArgs(w) }, rest: close >= 0 ? body.slice(close + closeLen) : "" }
|
|
1149
1231
|
if (/^\s*[\w.:-]{1,128}\s*<arg_key>/.test(body)) return { error: close >= 0 ? UNREADABLE : CUT_SHORT }
|
|
1150
1232
|
return { text: true }
|
|
1151
1233
|
}
|
|
@@ -1155,9 +1237,18 @@ const CUT_SHORT = "the model's tool call, written as text, was cut short; it is
|
|
|
1155
1237
|
const UNREADABLE = "the model's tool call, written as text, can't be read; it is not sent on as the answer"
|
|
1156
1238
|
|
|
1157
1239
|
// an opener: <tool_call>, one with attributes (<tool_call name="bash">),
|
|
1158
|
-
// or <tool_call right before its JSON; and one still being written
|
|
1159
|
-
|
|
1160
|
-
const
|
|
1240
|
+
// or <tool_call right before its JSON; and one still being written. GLM
|
|
1241
|
+
// also writes it without the underscore, <toolcall> (plugins#38)
|
|
1242
|
+
const OPEN_AT = /<tool_?call(?:(\s[^<>{}]*)?>|(?=\s*\{))/
|
|
1243
|
+
const OPEN_HOLD = /<tool_?call(?:\s[^<>{}\n]{0,200})?$/
|
|
1244
|
+
// closeAt is where the next close is from i, </tool_call> or </toolcall>,
|
|
1245
|
+
// and its length: [-1, 0] when there is none
|
|
1246
|
+
function closeAt(s, i = 0) {
|
|
1247
|
+
const re = /<\/tool_?call>/g
|
|
1248
|
+
re.lastIndex = i
|
|
1249
|
+
const m = re.exec(s)
|
|
1250
|
+
return m ? [m.index, m[0].length] : [-1, 0]
|
|
1251
|
+
}
|
|
1161
1252
|
// DeepSeek's half-and-half call: {"name":"x"} (or its brace or > left
|
|
1162
1253
|
// out), then <parameter name="k">v</parameter> or Qwen's <parameter=k>
|
|
1163
1254
|
const PARAM_HEAD = /^\s*\{\s*"name"\s*:\s*"([^"]+)"\s*\}?\s*>?\s*(?=<parameter\b)/
|
|
@@ -1303,7 +1394,7 @@ const GLM_ARG = /<arg_key>\s*([\s\S]*?)\s*<\/arg_key>\s*<arg_value>([\s\S]*?)<\/
|
|
|
1303
1394
|
// a value that isn't a string. A name alone is a call only to a tool the
|
|
1304
1395
|
// request has.
|
|
1305
1396
|
function glmCall(s, tools = []) {
|
|
1306
|
-
s = String(s ?? "").replace(/<\/
|
|
1397
|
+
s = String(s ?? "").replace(/<\/tool_?call>\s*$/, "")
|
|
1307
1398
|
const k = s.indexOf("<arg_key>")
|
|
1308
1399
|
const name = (k < 0 ? s : s.slice(0, k)).trim()
|
|
1309
1400
|
if (!/^[\w.:-]{1,128}$/.test(name)) return null
|
|
@@ -1373,9 +1464,9 @@ const DSML_CLOSE = new RegExp(String.raw`<\s*\/\s*[||][\s||]*DSML[\s||]*in
|
|
|
1373
1464
|
const DSML_PARAM = new RegExp(String.raw`<\s*[||][\s||]*DSML[\s||]*parameter\s+name\s*=\s*"([^"]+)"((?:\s+\w+\s*=\s*"[^"]*")*)\s*>([\s\S]*?)` + DSML + String.raw`parameter\s*>`, "g")
|
|
1374
1465
|
// what is left after a call: blanks, our close, DeepSeek's tags but the
|
|
1375
1466
|
// next call's
|
|
1376
|
-
const LEFTOVER = new RegExp(String.raw`^(?:\s+|<\/
|
|
1467
|
+
const LEFTOVER = new RegExp(String.raw`^(?:\s+|<\/tool_?calls?>|<tool_?calls>|` + DSML + String.raw`(?![\s||]*invoke\b)[^<>]*>)+`)
|
|
1377
1468
|
// what text may be the start of, so held back
|
|
1378
|
-
const MARKS = [OPEN, CLOSE, "<|DSML|", "<|DSML|", "</|DSML|", "</|DSML|", '{"reasoning_content"']
|
|
1469
|
+
const MARKS = [OPEN, CLOSE, "<toolcall>", "</toolcall>", "<|DSML|", "<|DSML|", "</|DSML|", "</|DSML|", '{"reasoning_content"']
|
|
1379
1470
|
// reasoning a model wrote into its text as JSON
|
|
1380
1471
|
const REASON_AT = /\{\s*"reasoning_content"\s*:/
|
|
1381
1472
|
const MARK_MAX = Math.max(...MARKS.map((m) => m.length))
|
|
@@ -2329,4 +2420,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2329
2420
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
2330
2421
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
2331
2422
|
|
|
2332
|
-
export const _internal = { HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, toolNamed, unwrapped, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, proofOf, SITES, SESSION, conversationID }
|
|
2423
|
+
export const _internal = { traeHost, accountOf, HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, toolNamed, unwrapped, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, proofOf, SITES, SESSION, conversationID }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-trae-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "OpenCode provider plugins for Trae: Trae CN (trae.cn) and Trae Global (trae.ai), free tier included, with Trae's own browser sign-in",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|