@magpie-community/opencode-trae-auth 0.2.23 → 0.2.25
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 +19 -0
- package/index.mjs +198 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -134,6 +134,13 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
134
134
|
error (plugins#38). Trae takes no two messages of one role in a row,
|
|
135
135
|
so an assistant turn's text is folded into the calls turn after it
|
|
136
136
|
and consecutive calls turns join one `tool_calls` list.
|
|
137
|
+
- For a Kimi (Moonshot) model the tools' schemas are first rewritten for
|
|
138
|
+
Moonshot's "moonshot flavored json schema", which Trae's 4027 "tool call
|
|
139
|
+
failed" quotes when it refuses them: an `enum` or `const` with no `type`
|
|
140
|
+
gets the one its values have, a `$ref` beside a `type` and a `type` beside
|
|
141
|
+
an `anyOf` are written as Kimi holds them (the same rules as kimi-work's).
|
|
142
|
+
The request's own tools are not changed, and a schema Kimi takes as it is
|
|
143
|
+
goes byte for byte. Other models' tools go as they are.
|
|
137
144
|
- A call with no name is dropped, and the `tool` result answering it
|
|
138
145
|
with it: Trae requires the name, and an empty `tool_calls` list with a
|
|
139
146
|
result naming no call is not a turn it takes.
|
|
@@ -441,6 +448,18 @@ answered it 9074 even with the client's headers.
|
|
|
441
448
|
changed or rotated to get round a 9095 or a 9074, and no account is
|
|
442
449
|
given another account's device or its own client's device unless a
|
|
443
450
|
client is signed in as that account.
|
|
451
|
+
- **Which device, said (0.2.25):** a 9074 or 9095 message ends with
|
|
452
|
+
the device the claim went on: this machine's Trae CN client's, signed in
|
|
453
|
+
as the account, or magpie's own for it, with how many client profiles
|
|
454
|
+
were found and that none is signed in as the account. A client device
|
|
455
|
+
that another profile, signed in as another account, also has is said
|
|
456
|
+
too: both accounts check in on one device, which Trae takes once a day.
|
|
457
|
+
No uid or device id is named. On plugins#436 two accounts shared one
|
|
458
|
+
machine whose two profiles (`Trae CN`, `TRAE SOLO CN`) were both the
|
|
459
|
+
first account's; it checked in and the second got 9074, which took
|
|
460
|
+
running `clientDevice` by hand to explain. A profile started with its
|
|
461
|
+
own `--user-data-dir` is still not read: whether TRAE CN keeps a
|
|
462
|
+
separate, registered device there is not known yet (plugins#436).
|
|
444
463
|
- **A claim's code 0 is not the answer (0.2.22):** Trae answers a
|
|
445
464
|
claim code 0 for one that registered nothing, so the status is read back
|
|
446
465
|
after every claim and only a day that is in is reported as `claimed`.
|
package/index.mjs
CHANGED
|
@@ -871,6 +871,15 @@ function clientStoragePaths(platform, home, env) {
|
|
|
871
871
|
// seen (and one unreadable file hid every profile after it).
|
|
872
872
|
async function clientDevice(uid) {
|
|
873
873
|
if (!uid) return undefined
|
|
874
|
+
return (await clientProfiles()).find((p) => String(p.uid) === String(uid))?.device
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// clientProfiles are this machine's Trae CN client profiles that can be
|
|
878
|
+
// made out, in ownClient.paths' order: the account each is signed in as
|
|
879
|
+
// and its one device. A file that isn't there, can't be read or parsed,
|
|
880
|
+
// names no single device or has a sign-in that doesn't decode is left out.
|
|
881
|
+
async function clientProfiles() {
|
|
882
|
+
const out = []
|
|
874
883
|
for (const p of ownClient.paths()) {
|
|
875
884
|
let text
|
|
876
885
|
try {
|
|
@@ -888,12 +897,34 @@ async function clientDevice(uid) {
|
|
|
888
897
|
const ids = Object.keys(doc ?? {}).filter((k) => k.startsWith(DC_KEY)).map((k) => k.slice(DC_KEY.length))
|
|
889
898
|
if (ids.length !== 1 || !/^\d{8,20}$/.test(ids[0])) continue
|
|
890
899
|
const who = clientAuth(doc[AUTH_KEY])?.userId
|
|
891
|
-
if (who != null && String(who)
|
|
900
|
+
if (who != null && String(who) !== "") out.push({ uid: String(who), device: ids[0] })
|
|
892
901
|
} catch {
|
|
893
902
|
continue // a sign-in that doesn't decode: the next profile may be
|
|
894
903
|
}
|
|
895
904
|
}
|
|
896
|
-
return
|
|
905
|
+
return out
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// deviceNote says which device a Trae CN check-in went on, for the message
|
|
909
|
+
// of a claim Trae refused (9074) or found already in (9095): the device of
|
|
910
|
+
// a client on this machine signed in as uid, or magpie's own for the
|
|
911
|
+
// account and why — how many client profiles were found, and that none is
|
|
912
|
+
// signed in as it. It names no uid and no device id. A client's device
|
|
913
|
+
// that another profile, signed in as another account, also has is said:
|
|
914
|
+
// both accounts check in on one device, and Trae takes one a day
|
|
915
|
+
// (plugins#436). It only states what was read; it promises no fix.
|
|
916
|
+
async function deviceNote(uid) {
|
|
917
|
+
const all = await clientProfiles()
|
|
918
|
+
const mine = all.find((p) => String(p.uid) === String(uid ?? ""))
|
|
919
|
+
if (mine) {
|
|
920
|
+
const shared = all.some((p) => p.device === mine.device && String(p.uid) !== String(uid))
|
|
921
|
+
return shared
|
|
922
|
+
? "sent on this machine's Trae CN client's device, signed in as this account — another profile here, signed in as another account, has the same device"
|
|
923
|
+
: "sent on this machine's Trae CN client's device, signed in as this account"
|
|
924
|
+
}
|
|
925
|
+
if (!all.length) return "sent on magpie's own device for this account: no Trae CN client profile was found on this machine"
|
|
926
|
+
const n = all.length
|
|
927
|
+
return `sent on magpie's own device for this account: ${n} Trae CN client profile${n === 1 ? " is" : "s are"} on this machine, none signed in as this account`
|
|
897
928
|
}
|
|
898
929
|
|
|
899
930
|
// clientAuth is the client's sign-in envelope, as stored or decoded
|
|
@@ -1126,11 +1157,157 @@ function traeMessages(req, sees = false) {
|
|
|
1126
1157
|
})
|
|
1127
1158
|
}
|
|
1128
1159
|
|
|
1129
|
-
//
|
|
1130
|
-
|
|
1160
|
+
// Moonshot (Kimi) validates tools against its own "moonshot flavored json
|
|
1161
|
+
// schema" and refuses the whole request (Trae answers code 4027, "tool call
|
|
1162
|
+
// failed: Invalid request: tools.function.parameters is not a valid moonshot
|
|
1163
|
+
// flavored json schema, details: <At path 'properties.ops.items.properties.op':
|
|
1164
|
+
// type is not defined>") for shapes a JSON Schema allows and agents send: an
|
|
1165
|
+
// enum or const with no type (Claude Code's tools carry them), a $ref beside a
|
|
1166
|
+
// type, a type beside an anyOf. Trae passes the tools to Kimi as they are, so
|
|
1167
|
+
// they are rewritten here for the rules Kimi has been seen to hold, for a
|
|
1168
|
+
// Kimi/Moonshot model only (moonshotModel). This is kimi-work's moonshotSchema,
|
|
1169
|
+
// which carries the reasons for each rule, ported. The tools sent are
|
|
1170
|
+
// a copy: the request's own stay as they were, and a schema Kimi takes as it is
|
|
1171
|
+
// goes byte for byte.
|
|
1172
|
+
const jsIsObj=x=>x!==null&&typeof x==='object'&&!Array.isArray(x);
|
|
1173
|
+
const msMAP_KEYS=['properties','$defs','definitions','dependentSchemas','patternProperties'];
|
|
1174
|
+
const msSUB_KEYS=['items','additionalProperties','contains','propertyNames','not','if','then','else','allOf','anyOf','oneOf','prefixItems','unevaluatedProperties','unevaluatedItems','contentSchema'];
|
|
1175
|
+
const msALL_TYPES=['string','number','integer','boolean','object','array','null'];
|
|
1176
|
+
|
|
1177
|
+
function msEachSchema(node,f) {
|
|
1178
|
+
if(Array.isArray(node)) {for(const v of node)msEachSchema(v,f);return;}
|
|
1179
|
+
if(!jsIsObj(node))return;
|
|
1180
|
+
f(node);
|
|
1181
|
+
for(const k of msMAP_KEYS) if(jsIsObj(node[k])) for(const v of Object.values(node[k])) msEachSchema(v,f);
|
|
1182
|
+
for(const k of msSUB_KEYS) if(k in node) msEachSchema(node[k],f);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function msRefTarget(root,ref) {
|
|
1186
|
+
if(typeof ref!=='string'||(ref!=='#'&&!ref.startsWith('#/')))return undefined;
|
|
1187
|
+
let cur=root;
|
|
1188
|
+
for(let tok of ref.split('/').slice(1)) {
|
|
1189
|
+
tok=tok.replace(/~1/g,'/').replace(/~0/g,'~');
|
|
1190
|
+
try{tok=decodeURIComponent(tok);}catch{}
|
|
1191
|
+
if(!jsIsObj(cur)&&!Array.isArray(cur))return undefined;
|
|
1192
|
+
if(!Object.hasOwn(cur,tok))return undefined;
|
|
1193
|
+
cur=cur[tok];
|
|
1194
|
+
}
|
|
1195
|
+
return cur;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const msHasRefInside=s=>JSON.stringify(s).includes('"$ref"');
|
|
1199
|
+
|
|
1200
|
+
function msValueType(v) {
|
|
1201
|
+
if(v===null)return 'null';
|
|
1202
|
+
switch(typeof v){
|
|
1203
|
+
case 'string':return 'string';
|
|
1204
|
+
case 'boolean':return 'boolean';
|
|
1205
|
+
case 'number':return Number.isInteger(v)?'integer':'number';
|
|
1206
|
+
}
|
|
1207
|
+
return Array.isArray(v)?'array':'object';
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// the type an enum of vals is given: the one all non-null values have, with
|
|
1211
|
+
// "null" when a null is among them; undefined when there are none or they
|
|
1212
|
+
// are of more than one kind
|
|
1213
|
+
function msEnumType(vals) {
|
|
1214
|
+
let t,nul=false;
|
|
1215
|
+
for(const v of vals) {
|
|
1216
|
+
const vt=msValueType(v);
|
|
1217
|
+
if(vt==='null'){nul=true;continue;}
|
|
1218
|
+
if(vt==='array'||vt==='object')return undefined;
|
|
1219
|
+
if(t===undefined||t===vt)t=vt;
|
|
1220
|
+
else if((t==='integer'&&vt==='number')||(t==='number'&&vt==='integer'))t='number';
|
|
1221
|
+
else return undefined;
|
|
1222
|
+
}
|
|
1223
|
+
if(t===undefined)return undefined;
|
|
1224
|
+
return nul?[t,'null']:t;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// the types s can have, as its type, keywords, values or branches say;
|
|
1228
|
+
// undefined when they don't say
|
|
1229
|
+
function msSchemaTypes(root,s,depth=0) {
|
|
1230
|
+
if(depth>16||!jsIsObj(s))return undefined;
|
|
1231
|
+
if('type' in s)return new Set([].concat(s.type).map(String));
|
|
1232
|
+
if('$ref' in s){const t=msRefTarget(root,s.$ref);return t===undefined?undefined:msSchemaTypes(root,t,depth+1);}
|
|
1233
|
+
if(Array.isArray(s.enum)||'const' in s)return new Set(('const' in s?[s.const]:s.enum).map(msValueType));
|
|
1234
|
+
for(const k of ['oneOf','anyOf','allOf']) {
|
|
1235
|
+
if(!Array.isArray(s[k]))continue;
|
|
1236
|
+
const ts=new Set();
|
|
1237
|
+
for(const b of s[k]) {
|
|
1238
|
+
const bt=msSchemaTypes(root,b,depth+1);
|
|
1239
|
+
if(!bt){if(k!=='allOf')return undefined;continue;}
|
|
1240
|
+
for(const t of bt)ts.add(t);
|
|
1241
|
+
}
|
|
1242
|
+
if(ts.size)return ts;
|
|
1243
|
+
}
|
|
1244
|
+
const has=(...ks)=>ks.some(k=>k in s);
|
|
1245
|
+
if(has('properties','required','additionalProperties','patternProperties','minProperties','maxProperties','propertyNames','dependentRequired','dependentSchemas','unevaluatedProperties'))return new Set(['object']);
|
|
1246
|
+
if(has('items','prefixItems','minItems','maxItems','uniqueItems','contains','unevaluatedItems'))return new Set(['array']);
|
|
1247
|
+
if(has('minLength','maxLength','pattern','format','contentEncoding','contentMediaType'))return new Set(['string']);
|
|
1248
|
+
if(has('minimum','maximum','exclusiveMinimum','exclusiveMaximum','multipleOf'))return new Set(['number']);
|
|
1249
|
+
return undefined;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function msTypeFor(root,s) {
|
|
1253
|
+
if(Array.isArray(s.enum))return msEnumType(s.enum);
|
|
1254
|
+
if('const' in s)return msEnumType([s.const]);
|
|
1255
|
+
const ts=msSchemaTypes(root,s);
|
|
1256
|
+
if(!ts||!ts.size)return [...msALL_TYPES];
|
|
1257
|
+
if(ts.has('integer')&&ts.has('number'))ts.delete('integer');
|
|
1258
|
+
const names=msALL_TYPES.filter(t=>ts.has(t));
|
|
1259
|
+
return names.length===1?names[0]:names;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// the same object with type written first, as an agent would have
|
|
1263
|
+
const msWithType=(s,type)=>{const rest={...s};delete rest.type;for(const k of Object.keys(s))delete s[k];Object.assign(s,{type},rest);};
|
|
1264
|
+
|
|
1265
|
+
function moonshotSchema(root) {
|
|
1266
|
+
let changed=false;
|
|
1267
|
+
// a $ref beside a type, and a type beside an anyOf
|
|
1268
|
+
msEachSchema(root,s=>{
|
|
1269
|
+
if('$ref' in s&&'type' in s) {
|
|
1270
|
+
const t=msRefTarget(root,s.$ref);
|
|
1271
|
+
if(jsIsObj(t)&&!msHasRefInside(t)) {
|
|
1272
|
+
const own={...s};delete own.$ref;
|
|
1273
|
+
for(const k of Object.keys(s))delete s[k];
|
|
1274
|
+
Object.assign(s,structuredClone(t),own);
|
|
1275
|
+
} else delete s.type;
|
|
1276
|
+
changed=true;
|
|
1277
|
+
}
|
|
1278
|
+
if(Array.isArray(s.anyOf)&&'type' in s){delete s.type;changed=true;}
|
|
1279
|
+
});
|
|
1280
|
+
// an enum or const with no type
|
|
1281
|
+
msEachSchema(root,s=>{
|
|
1282
|
+
if('type' in s||!(Array.isArray(s.enum)||'const' in s))return;
|
|
1283
|
+
const t=msEnumType(Array.isArray(s.enum)?s.enum:[s.const]);
|
|
1284
|
+
if(t!==undefined){msWithType(s,t);changed=true;}
|
|
1285
|
+
});
|
|
1286
|
+
// a $ref's target with no type
|
|
1287
|
+
msEachSchema(root,s=>{
|
|
1288
|
+
if(typeof s.$ref!=='string')return;
|
|
1289
|
+
const t=msRefTarget(root,s.$ref);
|
|
1290
|
+
if(!jsIsObj(t)||'type' in t||'anyOf' in t||'$ref' in t||!Object.keys(t).length)return;
|
|
1291
|
+
const typ=msTypeFor(root,t);
|
|
1292
|
+
if(typ!==undefined){msWithType(t,typ);changed=true;}
|
|
1293
|
+
});
|
|
1294
|
+
return changed;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
const moonshotModel = (...names) => names.some((n) => /kimi|moonshot/i.test(String(n ?? "")))
|
|
1299
|
+
|
|
1300
|
+
// nativeTools are the tools as Trae names them: parameters a JSON string.
|
|
1301
|
+
// moonshot is whether a Kimi model takes them (moonshotSchema)
|
|
1302
|
+
const nativeTools = (tools, moonshot = false) =>
|
|
1131
1303
|
tools.map((t) => {
|
|
1132
1304
|
const f = t.function ?? t
|
|
1133
|
-
|
|
1305
|
+
let p = f.parameters
|
|
1306
|
+
if (moonshot && jsIsObj(p)) {
|
|
1307
|
+
p = structuredClone(p)
|
|
1308
|
+
moonshotSchema(p)
|
|
1309
|
+
}
|
|
1310
|
+
return { type: "function", function: { name: f.name, description: f.description ?? "", parameters: typeof p === "string" ? p : JSON.stringify(p ?? {}) } }
|
|
1134
1311
|
})
|
|
1135
1312
|
|
|
1136
1313
|
// SESSION carries the conversation magpie (or OpenCode) names a request by,
|
|
@@ -1225,7 +1402,7 @@ function chatBody(req, fn, modelName, most = 0, max = null, session = "", sees =
|
|
|
1225
1402
|
}
|
|
1226
1403
|
if (typeof req.temperature === "number") body.temperature = req.temperature
|
|
1227
1404
|
if (Array.isArray(req.tools) && req.tools.length && req.tool_choice !== "none") {
|
|
1228
|
-
body.tools = nativeTools(req.tools)
|
|
1405
|
+
body.tools = nativeTools(req.tools, moonshotModel(config, modelName, req.model))
|
|
1229
1406
|
const c = req.tool_choice
|
|
1230
1407
|
if (c && typeof c === "object") body.tool_choice = c.function?.name || c.name || "auto"
|
|
1231
1408
|
else if (typeof c === "string") body.tool_choice = c
|
|
@@ -2594,9 +2771,12 @@ const MAX = "-max"
|
|
|
2594
2771
|
// same names, as reasoning_effort, so a level an agent picks is sent as Trae
|
|
2595
2772
|
// spells it. OpenCode knows the three as low/high/xhigh, so the map goes both
|
|
2596
2773
|
// ways. Trae's client names two more, medium and max (its bundle's
|
|
2597
|
-
// reasoning_effort.{low,medium,high,extra_high,max}), handed out per model
|
|
2598
|
-
//
|
|
2599
|
-
//
|
|
2774
|
+
// reasoning_effort.{low,medium,high,extra_high,max}), handed out per model,
|
|
2775
|
+
// so they are spelled here too, and offered only for a model whose entry
|
|
2776
|
+
// names them. No list seen yet names either: TRAE CN's app offers DeepSeek
|
|
2777
|
+
// V4.1 Flash 轻/高/极高 (light/high/extra_high) alone, and its "Max" is the
|
|
2778
|
+
// 更大上下文 (Max) 1M-window switch, which is the model id + MAX below, not a
|
|
2779
|
+
// level (#434's screenshot).
|
|
2600
2780
|
//
|
|
2601
2781
|
// An entry without the config takes no level: Trae serves it at its own
|
|
2602
2782
|
// default and nothing is sent. The plugin used to send none for every model,
|
|
@@ -2929,7 +3109,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2929
3109
|
const ce = errorOf(answer)
|
|
2930
3110
|
if (claim.status === 401 || lapsedCode(ce.code, site)) return { outcome: "failed", message: ce.message || `${a0.name || a0.uid}: Trae's sign-in has expired` }
|
|
2931
3111
|
// 9095: this device is in for today, whoever took it — the day's answer
|
|
2932
|
-
if (String(ce.code) === "9095") return { outcome: "ineligible", message: ce.message || "this device has checked in today" }
|
|
3112
|
+
if (String(ce.code) === "9095") return { outcome: "ineligible", message: `${ce.message || "this device has checked in today"} (${await deviceNote(a0.uid)})` }
|
|
2933
3113
|
// 9074: Trae did not take this claim for this account's device. Its own
|
|
2934
3114
|
// words say the pool is busy, which measurements do not bear out: the
|
|
2935
3115
|
// same account, token and headers were taken on a 16-digit id where a
|
|
@@ -2943,8 +3123,13 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2943
3123
|
// it in. Nothing is said of signing one in: it is the one thing seen
|
|
2944
3124
|
// to change a 9074, but it hasn't been tried on a real account, and
|
|
2945
3125
|
// on a claim that already rode a client's device it is what the user
|
|
2946
|
-
// did — the advice would only send them back to do it again.
|
|
2947
|
-
|
|
3126
|
+
// did — the advice would only send them back to do it again. What it
|
|
3127
|
+
// does say, after both 9074 and 9095, is which device the claim went
|
|
3128
|
+
// on and why (deviceNote): moxuanchen (plugins#436) had to run
|
|
3129
|
+
// clientDevice by hand to learn that the account Trae CN took (A, 100
|
|
3130
|
+
// credits) rode the client's device and the one it refused 9074 (B)
|
|
3131
|
+
// rode magpie's own, since both of that machine's profiles were A's.
|
|
3132
|
+
if (String(ce.code) === "9074") return { outcome: "ineligible", message: `${ce.message} (9074) — Trae CN did not take this check-in for this account's device (${await deviceNote(a0.uid)})` }
|
|
2948
3133
|
if (claim.status !== 200 || (ce.code && String(ce.code) !== "0")) return { outcome: "failed", message: ce.message || statusLine(claim.status) }
|
|
2949
3134
|
if (!answer || typeof answer !== "object" || Array.isArray(answer)) return { outcome: "failed", message: `check-in claim: unreadable reply ${claimText.trim().slice(0, 120) || "(empty)"}` }
|
|
2950
3135
|
// a claim's own code 0 is not proof the day is in: Trae answers it for
|
|
@@ -3396,4 +3581,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
3396
3581
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
3397
3582
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
3398
3583
|
|
|
3399
|
-
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, LEVELS, levelsOf, variantsOf, effortOf }
|
|
3584
|
+
export const _internal = { ownClient, clientStoragePaths, clientDevice, clientProfiles, deviceNote, clientDecode, thisMachine, traeHost, accountOf, HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, nameCall, toolNamed, unwrapped, traeMessages, chatBody, nativeTools, moonshotSchema, credits, dollarUsageOf, whenOf, newDevice, ugDevice, proofOf, SITES, SESSION, conversationID, LEVELS, levelsOf, variantsOf, effortOf }
|
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.25",
|
|
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",
|