@magpie-community/opencode-trae-auth 0.2.12 → 0.2.13
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 +10 -1
- package/index.mjs +192 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,7 +128,16 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
128
128
|
- GLM also writes its own template, `<tool_call>name<arg_key>k</arg_key>
|
|
129
129
|
<arg_value>v</arg_value></tool_call>` (a string kept as written, other
|
|
130
130
|
types read as JSON), and sometimes a whole call into a native call's
|
|
131
|
-
name; both are read as the call.
|
|
131
|
+
name; both are read as the call. A name that holds the wrapper with
|
|
132
|
+
GLM's tags where its service stopped mid-call — `{"name":"bash",
|
|
133
|
+
"arguments":{"command</arg_key><arg_value>ls</arg_value>` — is read too:
|
|
134
|
+
the tags are the service's own handoff, so what stands between them is
|
|
135
|
+
the model's text (a quote left unescaped read as the quote it is, a
|
|
136
|
+
comma inside a value kept, a field's closing tag ending the value before
|
|
137
|
+
it), and the fields the native call did carry are kept beside the ones
|
|
138
|
+
the name's own text fills in. A name whose tool the request doesn't
|
|
139
|
+
have, or one no field can be read from, goes on as written, as any other
|
|
140
|
+
unknown tool does.
|
|
132
141
|
- A block is read as models get it wrong too: every quote escaped
|
|
133
142
|
(`{\"name\":…}`), quotes or backslashes in a string left unescaped, an
|
|
134
143
|
opener with the name as an attribute (`<tool_call name="bash">` with
|
package/index.mjs
CHANGED
|
@@ -1493,6 +1493,165 @@ function glmCall(s, tools = []) {
|
|
|
1493
1493
|
return { name, arguments: args }
|
|
1494
1494
|
}
|
|
1495
1495
|
|
|
1496
|
+
// Trae reads a model's call for it and hands a call it could not read on as
|
|
1497
|
+
// the native call: the wrapper the client's prompt asked for, with GLM's own
|
|
1498
|
+
// template tags where the model wrote them and a field's close left behind
|
|
1499
|
+
// (``{"name":"bash","arguments":{"command</arg_key><arg_value>ls</arg_value>``).
|
|
1500
|
+
// The name then holds the call the model meant, and the call's own arguments
|
|
1501
|
+
// — where the model wrote them — hold the rest of it.
|
|
1502
|
+
const NAMECALL = /^\s*\{\s*"name"\s*:\s*"([^"\\]*)"\s*,\s*"(?:arguments|input|parameters)"\s*:\s*\{/
|
|
1503
|
+
// a wrapper the service put text in front of (`bash_abstract_command_0>{…}`):
|
|
1504
|
+
// the same shape, wherever it starts
|
|
1505
|
+
const NAMEHEAD = /\{\s*"name"\s*:\s*"([^"\\]*)"\s*,\s*"(?:arguments|input|parameters)"\s*:\s*\{/
|
|
1506
|
+
const NAMETAG = /<\/?arg_(?:key|value)\s*>/
|
|
1507
|
+
|
|
1508
|
+
// nameCall is the call a name holds when it is one the model wrote there
|
|
1509
|
+
// ({name, arguments}), null when it isn't: a name without the wrapper, one
|
|
1510
|
+
// whose tool the request doesn't have, or one no field could be read from.
|
|
1511
|
+
// The wrapper and the tags are the service's own handoff, so what stands
|
|
1512
|
+
// between them is the model's text as it wrote it — a quote it left
|
|
1513
|
+
// unescaped is read as the quote it is, the way repairedJSON reads a file's
|
|
1514
|
+
// content. A value is JSON where the tool's schema says the field is no
|
|
1515
|
+
// string and the text is one; else the text, cut where the value was cut.
|
|
1516
|
+
function nameCall(s, tools = []) {
|
|
1517
|
+
const text = String(s ?? "")
|
|
1518
|
+
// a wrapper the service put text in front of (`bash_abstract_command_0>{…}`)
|
|
1519
|
+
// is the call it holds, that text being no name the request has
|
|
1520
|
+
const at = text.indexOf("{")
|
|
1521
|
+
if (at < 0) return null
|
|
1522
|
+
const m = NAMECALL.exec(text.slice(at))
|
|
1523
|
+
if (!m) return null
|
|
1524
|
+
const name = m[1]
|
|
1525
|
+
const tool = tools.map((t) => t.function ?? t).find((t) => t?.name === name)
|
|
1526
|
+
if (!tool) return null
|
|
1527
|
+
const fields = nameFields(text.slice(at + m[0].length))
|
|
1528
|
+
const args = {}
|
|
1529
|
+
for (const [key, value] of fields) {
|
|
1530
|
+
// a field the schema says is no string is its JSON where it is one (a
|
|
1531
|
+
// number the model wrote, an object), else the text it is
|
|
1532
|
+
if (tool.parameters?.properties?.[key]?.type !== "string") {
|
|
1533
|
+
try {
|
|
1534
|
+
args[key] = JSON.parse(value.trim())
|
|
1535
|
+
continue
|
|
1536
|
+
} catch {}
|
|
1537
|
+
}
|
|
1538
|
+
args[key] = value
|
|
1539
|
+
}
|
|
1540
|
+
return Object.keys(args).length ? { name, arguments: args } : null
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// nameFields reads a wrapper's body as its fields: a key, then its value.
|
|
1544
|
+
// A tag stands where the service cut a colon or a quote, so a value ends at
|
|
1545
|
+
// the tag that ends it, at the next key, or at the structure that closes it
|
|
1546
|
+
// — and at a closing tag (`</description>`) whose name is a field's, because
|
|
1547
|
+
// what follows such a tag is the service's own text, not the model's.
|
|
1548
|
+
// What none of those reached is no field's text and is left out, so a call
|
|
1549
|
+
// cut in the middle keeps what it has.
|
|
1550
|
+
function nameFields(body) {
|
|
1551
|
+
const out = []
|
|
1552
|
+
let i = 0
|
|
1553
|
+
for (let n = 0; n < 64 && i < body.length; n++) {
|
|
1554
|
+
// between fields: blanks, a comma, or the tags that stand for either
|
|
1555
|
+
i += (body.slice(i).match(/^[\s,]+|^<\/?[a-z_]+\s*>/) || [""])[0].length
|
|
1556
|
+
if (body[i] !== '"') break
|
|
1557
|
+
const key = nameString(body, i, () => true)
|
|
1558
|
+
i = key.i
|
|
1559
|
+
if (!key.text) break
|
|
1560
|
+
const colon = body.slice(i).match(/^\s*:\s*/)
|
|
1561
|
+
if (colon) i += colon[0].length
|
|
1562
|
+
// a value the service cut at its tag: what stands after the tag is the
|
|
1563
|
+
// model's text (limit</arg_key><arg_value>55)
|
|
1564
|
+
i += (body.slice(i).match(/^(?:<\/?[a-z_]+\s*>)+/) || [""])[0].length
|
|
1565
|
+
let value
|
|
1566
|
+
if (body[i] === '"') {
|
|
1567
|
+
const v = nameString(body, i, (rest) => endsValue(rest))
|
|
1568
|
+
value = v.text
|
|
1569
|
+
i = v.i
|
|
1570
|
+
if (!value && !v.closed) break // an open quote with nothing in it
|
|
1571
|
+
} else {
|
|
1572
|
+
const rest = body.slice(i)
|
|
1573
|
+
const stop = valueEnd(rest)
|
|
1574
|
+
value = rest.slice(0, stop).replace(/^[\s,:]+/, "")
|
|
1575
|
+
// the field's own close: the structure that follows it, and a quote
|
|
1576
|
+
// the service cut it at where an escape isn't what stands before it
|
|
1577
|
+
value = value.replace(/[\s,}\]]+$/, "").replace(/(?<!\\)"$/, "").replace(/[\s,}\]]+$/, "")
|
|
1578
|
+
i += stop
|
|
1579
|
+
// nothing introduced a value (no colon, no quote, no tag) and none
|
|
1580
|
+
// stands: the wrapper is cut at the key, and an empty call read out
|
|
1581
|
+
// of it would be a call the model never wrote
|
|
1582
|
+
if (!value) break
|
|
1583
|
+
}
|
|
1584
|
+
out.push([key.text, value])
|
|
1585
|
+
}
|
|
1586
|
+
return out
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// endsValue is what may stand after a string's closing quote: the structure
|
|
1590
|
+
// that closes the field (`,`, `}`, `]`, the end), a tag that stands for one,
|
|
1591
|
+
// or the next field's key.
|
|
1592
|
+
function endsValue(rest) {
|
|
1593
|
+
if (/^\s*(?:[,}\]$]|<\/?[a-z_]+\s*>)/.test(rest)) return true
|
|
1594
|
+
return /^\s*"(?:[^"\\]|\\.)*"\s*(?::|<\/?[a-z_]+\s*>)/.test(rest)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// valueEnd is where an unquoted value runs out: the next key, a tag that
|
|
1598
|
+
// ends it, or the structure that closes the field. A comma inside it is the
|
|
1599
|
+
// model's own (sed -n 1,9p), so only a brace or a bracket closes a field.
|
|
1600
|
+
function valueEnd(rest) {
|
|
1601
|
+
const stops = [
|
|
1602
|
+
rest.search(/"(?:(?:[^"\\<>]*)"\s*(?::|,|\s*<\/?[a-z_]+\s*>)|[^"\\<>]*<\/?[a-z_]+\s*>)/),
|
|
1603
|
+
rest.search(/<\/?[a-z_]+\s*>/),
|
|
1604
|
+
rest.search(/[}\]]/),
|
|
1605
|
+
].filter((x) => x >= 0)
|
|
1606
|
+
return stops.length ? Math.min(...stops) : rest.length
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// nameString reads the string at s[i] === '"', its escapes kept as written.
|
|
1610
|
+
// ends says, for what follows a quote, whether that quote ended the string:
|
|
1611
|
+
// a quote the model left unescaped inside a value is escaped as read on.
|
|
1612
|
+
// `closed` is whether the model's own quote ended it, where the read ran
|
|
1613
|
+
// into a tag or the end of the text.
|
|
1614
|
+
function nameString(s, i, ends) {
|
|
1615
|
+
i++ // the opening quote
|
|
1616
|
+
let src = ""
|
|
1617
|
+
while (i < s.length) {
|
|
1618
|
+
const ch = s[i]
|
|
1619
|
+
if (ch === "\\") {
|
|
1620
|
+
src += s[i] + (s[i + 1] ?? "")
|
|
1621
|
+
i += 2
|
|
1622
|
+
continue
|
|
1623
|
+
}
|
|
1624
|
+
if (ch === "<") {
|
|
1625
|
+
// a tag ends the run: the service cut the value at it, whether it is
|
|
1626
|
+
// one of its own (`</arg_value>`) or a field's (`</description>`),
|
|
1627
|
+
// for neither is text the model wrote
|
|
1628
|
+
if (/^<\/?[a-z_][\w-]*\s*>/.test(s.slice(i))) break
|
|
1629
|
+
}
|
|
1630
|
+
if (ch === '"') {
|
|
1631
|
+
if (ends(s.slice(i + 1))) {
|
|
1632
|
+
i++
|
|
1633
|
+
return { text: decode(src), i, closed: true }
|
|
1634
|
+
}
|
|
1635
|
+
src += '\\"'
|
|
1636
|
+
i++
|
|
1637
|
+
continue
|
|
1638
|
+
}
|
|
1639
|
+
src += ch
|
|
1640
|
+
i++
|
|
1641
|
+
}
|
|
1642
|
+
return { text: decode(src), i, closed: false }
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// decode is a string's own source as the string: the escapes the model wrote
|
|
1646
|
+
// are the characters they stand for, a newline among them
|
|
1647
|
+
function decode(src) {
|
|
1648
|
+
try {
|
|
1649
|
+
return JSON.parse('"' + escapeRaw(src) + '"')
|
|
1650
|
+
} catch {
|
|
1651
|
+
return src
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1496
1655
|
// toolNamed is the request's own name for a tool a model named otherwise
|
|
1497
1656
|
// (Read for read, list-files for list_files); name itself when the request
|
|
1498
1657
|
// has none like it. A tool it has none of goes on as named: the agent tells
|
|
@@ -1684,12 +1843,21 @@ class NativeCalls {
|
|
|
1684
1843
|
}
|
|
1685
1844
|
take(tools = []) {
|
|
1686
1845
|
return this.calls.filter((c) => c.name).map(({ id, name, arguments: a }) => {
|
|
1687
|
-
// a name that holds the whole call, as
|
|
1688
|
-
// that call, its arguments the name's when it gave
|
|
1689
|
-
|
|
1846
|
+
// a name that holds the whole call, as the client's wrapper or in
|
|
1847
|
+
// GLM's template: that call, its arguments the name's when it gave
|
|
1848
|
+
// none of its own. The wrapper is read as JSON where it is whole —
|
|
1849
|
+
// its nesting is its own — and as the service's handoff where it was
|
|
1850
|
+
// left mid-call, a wrapper written with a space (`{ "name"`) whole
|
|
1851
|
+
// like any other
|
|
1852
|
+
const w = name.trimStart().startsWith("{") ? looseJSON(name) ?? nameCall(name, tools)
|
|
1853
|
+
: NAMEHEAD.test(name) ? nameCall(name, tools)
|
|
1854
|
+
: name.includes("<arg_key>") ? glmCall(name, tools) : null
|
|
1690
1855
|
if (w?.name) {
|
|
1691
1856
|
name = String(w.name)
|
|
1692
|
-
|
|
1857
|
+
// the name's own arguments are the model's; what the name read fills
|
|
1858
|
+
// the fields they left (the wrapper's text is the whole call where
|
|
1859
|
+
// Trae moved the arguments with it, and the rest where it did not)
|
|
1860
|
+
a = nameArgs(a, w)
|
|
1693
1861
|
}
|
|
1694
1862
|
const v = a && !done(a) ? looseJSON(a) : null
|
|
1695
1863
|
return { id: id || callId(), name, arguments: v ? JSON.stringify(v) : a || "{}" }
|
|
@@ -1697,6 +1865,25 @@ class NativeCalls {
|
|
|
1697
1865
|
}
|
|
1698
1866
|
}
|
|
1699
1867
|
|
|
1868
|
+
// nameArgs is the arguments of a call a name held: the native call's own
|
|
1869
|
+
// where it carried them, else the name's, and the two together where each
|
|
1870
|
+
// has a field the other hasn't — the service moves the arguments it read
|
|
1871
|
+
// into the native call and leaves the rest in the name. The name's side is
|
|
1872
|
+
// read the way main reads a whole name (`callArgs`: `arguments`, `input`,
|
|
1873
|
+
// `parameters`, arguments given as a string, or the flat fields), so what
|
|
1874
|
+
// main already read keeps reading the same. The two stand together only as
|
|
1875
|
+
// two sets of fields; a call's own arguments that are not that — text the
|
|
1876
|
+
// service cut, or a value that isn't a call's fields — go on as they stand,
|
|
1877
|
+
// which is what main passed on and what the agent can answer.
|
|
1878
|
+
function nameArgs(native, fromName) {
|
|
1879
|
+
const a = typeof native === "string" ? native.trim() : ""
|
|
1880
|
+
const named = callArgs(fromName)
|
|
1881
|
+
if (!a || a === "{}") return named
|
|
1882
|
+
const own = looseJSON(a), side = looseJSON(named)
|
|
1883
|
+
if (!own || !side || typeof own !== "object" || typeof side !== "object" || Array.isArray(own) || Array.isArray(side)) return a
|
|
1884
|
+
return JSON.stringify({ ...side, ...own })
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1700
1887
|
// resent says next is the call's whole arguments sent again after a
|
|
1701
1888
|
// partial snapshot, not a piece of them: Trae resends a snapshot whose
|
|
1702
1889
|
// punctuation isn't the first one's (− for -, ′ for '), so it doesn't
|
|
@@ -2496,4 +2683,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2496
2683
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
2497
2684
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
2498
2685
|
|
|
2499
|
-
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 }
|
|
2686
|
+
export const _internal = { 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 }
|
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.13",
|
|
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",
|