@magpie-community/opencode-trae-auth 0.2.14 → 0.2.16
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 +25 -6
- package/index.mjs +101 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,9 +42,11 @@ An account of one realm does not sign in to the other: trae.cn's accounts are
|
|
|
42
42
|
not trae.ai's.
|
|
43
43
|
|
|
44
44
|
At sign-in the plugin creates the account's device and names it to the
|
|
45
|
-
authorization page: `device_id`
|
|
45
|
+
authorization page: `device_id` and `machine_id` (32 hex). Every
|
|
46
46
|
request then sends that same device. The relays saw requests dropped when the
|
|
47
|
-
device was new each time.
|
|
47
|
+
device was new each time. A Trae CN `device_id` is 16 digits since 0.2.16, the
|
|
48
|
+
shape of the ids Trae's device service gives its clients (15 or 16 digits);
|
|
49
|
+
Trae Global's stays 19.
|
|
48
50
|
|
|
49
51
|
### Enterprise (SaaS) accounts
|
|
50
52
|
|
|
@@ -151,7 +153,15 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
151
153
|
request's (`read`); any other goes on as written, so the agent can say
|
|
152
154
|
the tool doesn't exist.
|
|
153
155
|
- Earlier calls and their results go back in as text.
|
|
154
|
-
- Images
|
|
156
|
+
- Images: Trae's lists mark each model `multimodal` true or false (the
|
|
157
|
+
Doubao-Seed models, Kimi K3 and K2.6, Qwen 3.7 Plus and MiniMax M3 take
|
|
158
|
+
images; GLM-5 and DeepSeek V4 don't). A model marked true is listed
|
|
159
|
+
as one that takes images, and an image sent to it goes in the
|
|
160
|
+
message's content as an OpenAI `image_url` part beside the text, as
|
|
161
|
+
TRAE's own agent sends one (plugins#161). A model marked false gets
|
|
162
|
+
"[an image, which this model can't see]" in its place. A model whose
|
|
163
|
+
list hasn't been read is sent its images, unless the fallback list
|
|
164
|
+
says it takes none.
|
|
155
165
|
|
|
156
166
|
The model list is the account's own, from
|
|
157
167
|
`/api/ide/v1/batch_get_detail_param`, every function's list in one ask, as
|
|
@@ -306,11 +316,20 @@ answered it 9074 even with the client's headers.
|
|
|
306
316
|
macOS `~/Library/Application Support/Trae CN/…`, Windows
|
|
307
317
|
`%APPDATA%\Trae CN\…` (or `trae-cn`), Linux `~/.config/trae-cn/…`
|
|
308
318
|
(or `Trae CN`; not seen on a real Linux install).
|
|
319
|
+
- **Every other account's device (0.2.16):** its own device id, in the
|
|
320
|
+
client's shape. Trae CN answered a claim on the plugin's 19-digit id
|
|
321
|
+
9074, while the same account and token on a 16-digit id were taken
|
|
322
|
+
(magpie-community/plugins#194). An account signed in since 0.2.16 has a
|
|
323
|
+
16-digit id and checks in on it. One signed in before checks in on a
|
|
324
|
+
16-digit id made from its own device id and uid: the same id every day,
|
|
325
|
+
never rotated, and no other account's. Its chat requests and renewals
|
|
326
|
+
keep the 19-digit device it signed in with.
|
|
309
327
|
- **What comes back:** Trae's answer as it is. A 401 or code 1001 marks
|
|
310
328
|
the account for a new sign-in. Code 9095 means this device has checked
|
|
311
|
-
in today. No device id is ever
|
|
312
|
-
a 9095 or a 9074, and no account is given
|
|
313
|
-
own client's
|
|
329
|
+
in today, and that holds for the day. No device id is ever changed or
|
|
330
|
+
rotated to get round a 9095 or a 9074, and no account is given another
|
|
331
|
+
account's device or its own client's device unless the client is signed
|
|
332
|
+
in as that account.
|
|
314
333
|
|
|
315
334
|
Only `api.trae.cn`'s `/trae/api/` pages are sent this way; any other URL
|
|
316
335
|
that isn't a chat request is still refused (400). Needs 0.1.4 or later.
|
package/index.mjs
CHANGED
|
@@ -15,6 +15,10 @@ import { createServer, STATUS_CODES } from "node:http"
|
|
|
15
15
|
import { homedir, release, version as osVersion } from "node:os"
|
|
16
16
|
import { join } from "node:path"
|
|
17
17
|
|
|
18
|
+
// SEES is a fallback model that takes images, as Trae's lists mark it
|
|
19
|
+
// (multimodal: true)
|
|
20
|
+
const SEES = { attachment: true, modalities: { input: ["text", "image"], output: ["text"] } }
|
|
21
|
+
|
|
18
22
|
const SITES = {
|
|
19
23
|
// Trae CN (trae.cn): the authorization page sends the browser back to a
|
|
20
24
|
// callback on 127.0.0.1 with the account's Cloud-IDE-JWT and refresh
|
|
@@ -74,8 +78,8 @@ const SITES = {
|
|
|
74
78
|
models: {
|
|
75
79
|
"glm-5.2": { name: "GLM-5.2", limit: { context: 200_000, output: 32_000 } },
|
|
76
80
|
"glm-5": { name: "GLM-5" },
|
|
77
|
-
"kimi-k2.6": { name: "Kimi K2.6", limit: { context: 256_000, output: 0 } },
|
|
78
|
-
"qwen-3.7-plus": { name: "Qwen 3.7 Plus" },
|
|
81
|
+
"kimi-k2.6": { name: "Kimi K2.6", limit: { context: 256_000, output: 0 }, ...SEES },
|
|
82
|
+
"qwen-3.7-plus": { name: "Qwen 3.7 Plus", ...SEES },
|
|
79
83
|
"DeepSeek-V4-Pro": { name: "DeepSeek V4 Pro" },
|
|
80
84
|
"DeepSeek-V4-Flash": { name: "DeepSeek V4 Flash" },
|
|
81
85
|
},
|
|
@@ -292,8 +296,32 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
292
296
|
|
|
293
297
|
// device is the machine an account signs in from, made once at sign-in and
|
|
294
298
|
// named to trae.cn's authorization page, then sent with every request: a
|
|
295
|
-
// fresh one each time is what the relays saw requests dropped for.
|
|
296
|
-
|
|
299
|
+
// fresh one each time is what the relays saw requests dropped for. Trae
|
|
300
|
+
// CN's id has the shape its clients' have, 16 digits: the device service
|
|
301
|
+
// gives the IDE a 15- or 16-digit one (iCubeAuthInfo://icube-dc:<id>, see
|
|
302
|
+
// clientDevice), and Trae CN answers a check-in claim on a 19-digit id
|
|
303
|
+
// 9074 where the same account and token on a 16-digit one are taken
|
|
304
|
+
// (magpie-community/plugins#194). Trae's global site keeps the 19 digits
|
|
305
|
+
// it was signed in with: nothing says its ids are shaped otherwise.
|
|
306
|
+
const newDevice = (site) => ({ deviceId: digits(site?.id === "trae-cn" ? 16 : 19), machineId: randomBytes(16).toString("hex"), ...newDeviceKey() })
|
|
307
|
+
|
|
308
|
+
// ugDevice is the device id a Trae CN account's check-in goes on when this
|
|
309
|
+
// machine's client isn't signed in as it: its own when that already has a
|
|
310
|
+
// client's shape (15 or 16 digits), else, for an account signed in before
|
|
311
|
+
// 0.2.16 with 19 digits, a 16-digit id made from its own device id and
|
|
312
|
+
// uid. That one is the same on every check-in of the account, never
|
|
313
|
+
// rotated and never another account's: it is the account's own device in
|
|
314
|
+
// the shape Trae CN takes (#194), not a new device to get past a refusal,
|
|
315
|
+
// and a 9095 (this device is in for today) on it stays the day's answer.
|
|
316
|
+
// The chat's pages and the renewal keep the device the account signed in
|
|
317
|
+
// with.
|
|
318
|
+
function ugDevice(a) {
|
|
319
|
+
const id = String(a.deviceId ?? "")
|
|
320
|
+
if (/^\d{15,16}$/.test(id)) return id
|
|
321
|
+
if (!id && !a.uid) return id
|
|
322
|
+
const n = BigInt("0x" + createHash("sha256").update(`trae-cn-device\0${id}\0${a.uid ?? ""}`).digest("hex").slice(0, 30))
|
|
323
|
+
return String(1_000_000_000_000_000n + (n % 9_000_000_000_000_000n))
|
|
324
|
+
}
|
|
297
325
|
|
|
298
326
|
// newDeviceKey is the device's key pair, an ECDSA P-256 one as the clients
|
|
299
327
|
// keep (iCubeAuthInfo://icube-dc:<device>), whose public half goes with a
|
|
@@ -623,7 +651,7 @@ async function signedIn(q, device, site) {
|
|
|
623
651
|
// browserSignIn is the IDE's sign-in: trae.cn's authorization page, back
|
|
624
652
|
// to a callback on 127.0.0.1 (the page takes no other kind)
|
|
625
653
|
async function browserSignIn(site) {
|
|
626
|
-
const device = newDevice()
|
|
654
|
+
const device = newDevice(site)
|
|
627
655
|
const trace = randomUUID()
|
|
628
656
|
let over = false
|
|
629
657
|
let settle
|
|
@@ -915,15 +943,36 @@ const partText = (content) => {
|
|
|
915
943
|
.join("\n")
|
|
916
944
|
}
|
|
917
945
|
|
|
918
|
-
//
|
|
919
|
-
|
|
946
|
+
// imageOf is an image part of a message as Trae's chat takes one: OpenAI's
|
|
947
|
+
// image_url part, {url} in it, which is what TRAE's ai-agent sends
|
|
948
|
+
// (LLMRawMultiContent's image_url, LLMRawChatImageURL) and what the
|
|
949
|
+
// open-source relays pass llm_utils_chat unchanged. null for a part that
|
|
950
|
+
// isn't an image, or one with no url.
|
|
951
|
+
function imageOf(p) {
|
|
952
|
+
if (p?.type !== "image_url" && p?.type !== "input_image") return null
|
|
953
|
+
const url = typeof p.image_url === "string" ? p.image_url : p.image_url?.url ?? p.url
|
|
954
|
+
return typeof url === "string" && url ? { type: "image_url", image_url: { url } } : null
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// partsOf is a message's text and, for a model that sees, its images apart
|
|
958
|
+
// from it; a model that doesn't is told in the text that one was there
|
|
959
|
+
const partsOf = (content, sees) => {
|
|
960
|
+
if (!sees || !Array.isArray(content)) return { text: partText(content), images: [] }
|
|
961
|
+
const images = content.map(imageOf).filter(Boolean)
|
|
962
|
+
return { text: partText(content.filter((p) => !imageOf(p))), images }
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// traeMessages is the conversation as Trae's chat takes it. sees is whether
|
|
966
|
+
// the model takes images: Trae's list says so of each model (multimodal),
|
|
967
|
+
// and a model it says doesn't is sent the placeholder, not the image.
|
|
968
|
+
function traeMessages(req, sees = false) {
|
|
920
969
|
const tools = Array.isArray(req.tools) && req.tools.length && req.tool_choice !== "none" ? req.tools : []
|
|
921
970
|
const names = new Map()
|
|
922
971
|
const out = []
|
|
923
972
|
if (tools.length) out.push({ role: "system", text: toolPrompt(tools, req.tool_choice, req.parallel_tool_calls) })
|
|
924
973
|
for (const m of req.messages ?? []) {
|
|
925
974
|
const role = m.role === "developer" ? "system" : m.role
|
|
926
|
-
let text =
|
|
975
|
+
let { text, images } = partsOf(m.content, sees)
|
|
927
976
|
if (role === "assistant") {
|
|
928
977
|
const calls = (m.tool_calls ?? []).map((c) => {
|
|
929
978
|
names.set(c.id, c.function?.name)
|
|
@@ -936,20 +985,24 @@ function traeMessages(req) {
|
|
|
936
985
|
text = [text, ...calls].filter(Boolean).join("\n")
|
|
937
986
|
out.push({ role: "assistant", text })
|
|
938
987
|
} else if (role === "tool") {
|
|
939
|
-
out.push({ role: "user", text: `Result of ${names.get(m.tool_call_id) || m.name || "the tool"} (call ${m.tool_call_id ?? "?"}):\n${text}
|
|
988
|
+
out.push({ role: "user", text: `Result of ${names.get(m.tool_call_id) || m.name || "the tool"} (call ${m.tool_call_id ?? "?"}):\n${text}`, images })
|
|
940
989
|
} else {
|
|
941
|
-
out.push({ role: role === "system" ? "system" : "user", text })
|
|
990
|
+
out.push({ role: role === "system" ? "system" : "user", text, images })
|
|
942
991
|
}
|
|
943
992
|
}
|
|
944
|
-
// Trae takes no empty message, nor two of one role in a row
|
|
993
|
+
// Trae takes no empty message, nor two of one role in a row; a message of
|
|
994
|
+
// an image alone is not empty
|
|
945
995
|
const merged = []
|
|
946
996
|
for (const m of out) {
|
|
947
|
-
|
|
997
|
+
const images = m.images ?? []
|
|
998
|
+
if (!m.text && !images.length) continue
|
|
948
999
|
const last = merged.at(-1)
|
|
949
|
-
if (last && last.role === m.role && m.role !== "system")
|
|
950
|
-
|
|
1000
|
+
if (last && last.role === m.role && m.role !== "system") {
|
|
1001
|
+
last.text = [last.text, m.text].filter(Boolean).join("\n\n")
|
|
1002
|
+
last.images = [...last.images, ...images]
|
|
1003
|
+
} else merged.push({ role: m.role, text: m.text, images })
|
|
951
1004
|
}
|
|
952
|
-
return merged.map((m) => ({ role: m.role, content: [{ type: "text", text: m.text }] }))
|
|
1005
|
+
return merged.map((m) => ({ role: m.role, content: [...(m.text ? [{ type: "text", text: m.text }] : []), ...m.images] }))
|
|
953
1006
|
}
|
|
954
1007
|
|
|
955
1008
|
// nativeTools are the tools as Trae names them: parameters a JSON string
|
|
@@ -1024,10 +1077,10 @@ function sessionOf(headers) {
|
|
|
1024
1077
|
// tool result sent back, a retry, another function answering) shows up in the
|
|
1025
1078
|
// usage detail as several, each under no title at all. request_id stays a
|
|
1026
1079
|
// fresh id per request, being that request's own.
|
|
1027
|
-
function chatBody(req, fn, modelName, most = 0, max = null, session = "") {
|
|
1080
|
+
function chatBody(req, fn, modelName, most = 0, max = null, session = "", sees = false) {
|
|
1028
1081
|
const config = max?.base ?? req.model
|
|
1029
1082
|
const body = {
|
|
1030
|
-
messages: traeMessages(req),
|
|
1083
|
+
messages: traeMessages(req, sees),
|
|
1031
1084
|
function: fn,
|
|
1032
1085
|
config_name: config,
|
|
1033
1086
|
model: config,
|
|
@@ -2270,6 +2323,12 @@ function chatModel(m, site) {
|
|
|
2270
2323
|
return true
|
|
2271
2324
|
}
|
|
2272
2325
|
|
|
2326
|
+
// seesOf is whether a list's entry says its model takes images: Trae's
|
|
2327
|
+
// lists mark each one multimodal true or false (Doubao-Seed, Kimi K3,
|
|
2328
|
+
// Qwen 3.7 Plus take them; GLM-5 and DeepSeek V4 don't). null when the
|
|
2329
|
+
// entry doesn't say.
|
|
2330
|
+
const seesOf = (m) => (typeof m?.multimodal === "boolean" ? m.multimodal : typeof m?.display_config?.multimodal === "boolean" ? m.display_config.multimodal : null)
|
|
2331
|
+
|
|
2273
2332
|
// devModel is the model an entry serves requests with: its __dev one
|
|
2274
2333
|
const devModel = (m) => (Array.isArray(m?.model_detail_list) ? m.model_detail_list : []).find((d) => /__dev$/.test(String(d?.model_name ?? "")))
|
|
2275
2334
|
|
|
@@ -2352,6 +2411,8 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2352
2411
|
const modelNames = new Map()
|
|
2353
2412
|
// the Max each model the list last read has (null: none)
|
|
2354
2413
|
const maxes = new Map()
|
|
2414
|
+
// whether each model the list last read takes images (its multimodal)
|
|
2415
|
+
const sight = new Map()
|
|
2355
2416
|
|
|
2356
2417
|
const save = async (auth) => {
|
|
2357
2418
|
try {
|
|
@@ -2454,6 +2515,9 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2454
2515
|
// other account keeps its own device; none is given the client's.
|
|
2455
2516
|
const deviceId = ug && site.id === "trae-cn" ? await clientDevice(a.uid) : undefined
|
|
2456
2517
|
const own = deviceId ? { deviceId, ...thisMachine() } : undefined
|
|
2518
|
+
// every other Trae CN account checks in on its own device, in the
|
|
2519
|
+
// client's shape (ugDevice, #194)
|
|
2520
|
+
if (ug && site.id === "trae-cn" && !own) a = { ...a, deviceId: ugDevice(a) }
|
|
2457
2521
|
const res = await fetch(url, {
|
|
2458
2522
|
method,
|
|
2459
2523
|
headers: ug ? ugHeaders(a, site, { Accept: "application/json" }, own) : ideHeaders(a, site, { Accept: "application/json" }),
|
|
@@ -2595,6 +2659,9 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2595
2659
|
}
|
|
2596
2660
|
for (const [id, { m, fn }] of out) {
|
|
2597
2661
|
listedBy.set(id, fn)
|
|
2662
|
+
const sees = seesOf(m)
|
|
2663
|
+
if (sees === null) sight.delete(id)
|
|
2664
|
+
else sight.set(id, sees)
|
|
2598
2665
|
const dev = devModel(m)
|
|
2599
2666
|
if (dev) modelNames.set(id, { fn, name: dev.model_name, most: Number(dev.max_tokens) || 0 })
|
|
2600
2667
|
else modelNames.delete(id)
|
|
@@ -2633,16 +2700,24 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2633
2700
|
const out = Number(devModel(m)?.max_tokens) || was.limit?.output || 0
|
|
2634
2701
|
const name = m.display_config?.display_name || m.display_name || m.display_model_name || was.name || id
|
|
2635
2702
|
const input = (k) => (MODEL.modalities?.input ?? []).includes(k)
|
|
2703
|
+
// the list's multimodal says whether the model takes images, over the
|
|
2704
|
+
// fallback list's word and MODEL's
|
|
2705
|
+
const sees = seesOf(m)
|
|
2706
|
+
const inputs = was.capabilities?.input ?? { text: true, image: input("image"), audio: input("audio"), video: input("video"), pdf: input("pdf") }
|
|
2707
|
+
const sight = sees === null ? {} : {
|
|
2708
|
+
modalities: { input: [...new Set([...((was.modalities ?? MODEL.modalities).input ?? ["text"]).filter((k) => k !== "image"), ...(sees ? ["image"] : [])])], output: (was.modalities ?? MODEL.modalities).output ?? ["text"] },
|
|
2709
|
+
attachment: sees,
|
|
2710
|
+
}
|
|
2636
2711
|
return {
|
|
2637
|
-
...MODEL, ...was, id, providerID: site.id, name: String(name), limit: { context: ctx, output: out },
|
|
2712
|
+
...MODEL, ...was, ...sight, id, providerID: site.id, name: String(name), limit: { context: ctx, output: out },
|
|
2638
2713
|
api: was.api ?? { id, url: site.hosts.api, npm: "@ai-sdk/openai-compatible" },
|
|
2639
2714
|
capabilities: {
|
|
2640
2715
|
...(was.capabilities ?? {}),
|
|
2641
2716
|
reasoning: was.capabilities?.reasoning ?? MODEL.reasoning,
|
|
2642
2717
|
temperature: was.capabilities?.temperature ?? MODEL.temperature,
|
|
2643
|
-
attachment: was.capabilities?.attachment ?? MODEL.attachment,
|
|
2718
|
+
attachment: sees ?? was.capabilities?.attachment ?? MODEL.attachment,
|
|
2644
2719
|
toolcall: was.capabilities?.toolcall ?? MODEL.tool_call,
|
|
2645
|
-
input:
|
|
2720
|
+
input: sees === null ? inputs : { ...inputs, image: sees },
|
|
2646
2721
|
output: was.capabilities?.output ?? { text: true, image: false, audio: false, video: false, pdf: false },
|
|
2647
2722
|
},
|
|
2648
2723
|
// Trae's request takes no effort or thinking level, so a model has
|
|
@@ -2739,6 +2814,10 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2739
2814
|
if (named) fns.unshift(named.fn)
|
|
2740
2815
|
}
|
|
2741
2816
|
const max = named?.max ?? null
|
|
2817
|
+
// a model the list hasn't said of is sent its images: the agent
|
|
2818
|
+
// sends them only to a model it was told takes them
|
|
2819
|
+
const base = max?.base ?? String(req.model)
|
|
2820
|
+
const sees = sight.get(base) ?? (site.models[base]?.modalities?.input?.includes("image") ?? true)
|
|
2742
2821
|
// the answer's own signal: the caller's abort (a cancelled
|
|
2743
2822
|
// request) and the stream's cancel both stop Trae's fetch, so
|
|
2744
2823
|
// no read is left hanging on a connection the agent walked
|
|
@@ -2758,7 +2837,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2758
2837
|
const res = await fetch(apiOf(a, site) + "/api/agent/v3/llm_utils_chat", {
|
|
2759
2838
|
method: "POST",
|
|
2760
2839
|
headers: ideHeaders(a, site, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
|
|
2761
|
-
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session)),
|
|
2840
|
+
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session, sees)),
|
|
2762
2841
|
signal: ac.signal,
|
|
2763
2842
|
})
|
|
2764
2843
|
if (!res.ok) {
|
|
@@ -2809,4 +2888,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2809
2888
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
2810
2889
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
2811
2890
|
|
|
2812
|
-
export const _internal = { ownClient, clientStoragePaths, clientDevice, clientDecode, thisMachine, traeHost, accountOf, HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, nameCall, toolNamed, unwrapped, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, proofOf, SITES, SESSION, conversationID }
|
|
2891
|
+
export const _internal = { ownClient, clientStoragePaths, clientDevice, clientDecode, thisMachine, traeHost, accountOf, HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, nameCall, toolNamed, unwrapped, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, ugDevice, 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.16",
|
|
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",
|