@magpie-community/opencode-trae-auth 0.1.9 → 0.1.11

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.
Files changed (2) hide show
  1. package/index.mjs +180 -11
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -534,25 +534,79 @@ const callId = () => "call_" + randomBytes(12).toString("hex") + (callSeq++).toS
534
534
 
535
535
  // TextTools splits the model's text into what the agent sees and the tool
536
536
  // calls it wrote as blocks, holding back the start of a block until it
537
- // is whole
537
+ // is whole. A block is ours (<tool_call>{…}</tool_call>), its JSON taken
538
+ // once whole even when DeepSeek closes it with its own tags in place of
539
+ // </tool_call>, or DeepSeek's (<|DSML|invoke name="…">, magpie#823); the
540
+ // tags left around a call are dropped.
538
541
  class TextTools {
539
542
  constructor() {
540
543
  this.buf = ""
544
+ this.after = false
541
545
  }
542
546
  push(s, end = false) {
543
547
  this.buf += s
544
548
  let text = ""
545
549
  const calls = []
546
550
  for (;;) {
551
+ if (this.after) {
552
+ const m = this.buf.match(LEFTOVER)
553
+ if (m) this.buf = this.buf.slice(m[0].length)
554
+ if (!this.buf || (!end && MARKS.some((k) => k.startsWith(this.buf)))) break
555
+ this.after = false
556
+ }
547
557
  const i = this.buf.indexOf(OPEN)
548
- if (i < 0) break
558
+ const d = this.buf.search(DSML_AT)
559
+ if (i < 0 && d < 0) break
560
+ if (d >= 0 && (i < 0 || d < i)) {
561
+ text += this.buf.slice(0, d)
562
+ this.buf = this.buf.slice(d)
563
+ const tag = this.buf.match(DSML_TAG)
564
+ if (!tag) {
565
+ // a tag not closed yet; at the end, or with another tag after it,
566
+ // a stray one dropped
567
+ const next = this.buf.indexOf("<", 1)
568
+ if (next < 0 && !end) break
569
+ this.buf = next < 0 ? "" : this.buf.slice(next)
570
+ continue
571
+ }
572
+ const inv = tag[0].match(DSML_INVOKE)
573
+ if (!inv) {
574
+ // function_calls, a parameter's close, any other tag: dropped,
575
+ // with the blanks after it
576
+ this.buf = this.buf.slice(tag[0].length)
577
+ this.after = true
578
+ continue
579
+ }
580
+ const body = this.buf.slice(tag[0].length)
581
+ const c = body.match(DSML_CLOSE)
582
+ const n = body.search(DSML_INVOKE_AT)
583
+ let stop = c ? c.index : n >= 0 ? n : -1
584
+ if (n >= 0 && n < stop) stop = n
585
+ if (stop < 0) {
586
+ if (!end) break
587
+ stop = body.length
588
+ }
589
+ calls.push({ name: inv[1], arguments: dsmlArgs(body.slice(0, stop)) })
590
+ this.buf = body.slice(c && c.index === stop ? stop + c[0].length : stop)
591
+ this.after = true
592
+ continue
593
+ }
594
+ const o = objectEnd(this.buf, i + OPEN.length)
595
+ const v = o > 0 ? looseJSON(this.buf.slice(i + OPEN.length, o)) : null
596
+ if (v?.name) {
597
+ text += this.buf.slice(0, i)
598
+ calls.push({ name: String(v.name), arguments: callArgs(v) })
599
+ this.buf = this.buf.slice(o)
600
+ this.after = true
601
+ continue
602
+ }
549
603
  const j = this.buf.indexOf(CLOSE, i + OPEN.length)
550
604
  if (j < 0) break
551
605
  text += this.buf.slice(0, i)
552
606
  const raw = this.buf.slice(i + OPEN.length, j).trim()
553
607
  this.buf = this.buf.slice(j + CLOSE.length)
554
- const v = parseJSON(raw)
555
- if (v.name) calls.push({ name: String(v.name), arguments: typeof v.arguments === "string" ? v.arguments : JSON.stringify(v.arguments ?? v.input ?? v.parameters ?? {}) })
608
+ const w = looseJSON(raw) ?? {}
609
+ if (w.name) calls.push({ name: String(w.name), arguments: callArgs(w) })
556
610
  else text += OPEN + raw + CLOSE
557
611
  }
558
612
  if (end) {
@@ -560,9 +614,9 @@ class TextTools {
560
614
  this.buf = ""
561
615
  } else {
562
616
  // keep back an opened block, or what may be the start of one
563
- const i = this.buf.indexOf(OPEN)
564
- let keep = i >= 0 ? this.buf.length - i : 0
565
- if (!keep) for (let k = Math.min(OPEN.length - 1, this.buf.length); k > 0; k--) if (OPEN.startsWith(this.buf.slice(-k))) { keep = k; break }
617
+ const at = [this.buf.indexOf(OPEN), this.buf.search(DSML_AT)].filter((x) => x >= 0)
618
+ let keep = at.length ? this.buf.length - Math.min(...at) : 0
619
+ if (!keep) for (let k = Math.min(MARK_MAX - 1, this.buf.length); k > 0; k--) if (MARKS.some((m) => m.startsWith(this.buf.slice(-k)))) { keep = k; break }
566
620
  text += this.buf.slice(0, this.buf.length - keep)
567
621
  this.buf = this.buf.slice(this.buf.length - keep)
568
622
  }
@@ -570,14 +624,120 @@ class TextTools {
570
624
  }
571
625
  }
572
626
 
627
+ // DeepSeek's tags, with its full-width bar or a plain one
628
+ const DSML = String.raw`<\s*\/?\s*[||][\s||]*DSML[\s||]*`
629
+ const DSML_AT = new RegExp(DSML)
630
+ const DSML_TAG = new RegExp("^" + DSML + "[^<>]*>")
631
+ const DSML_INVOKE = new RegExp(String.raw`^<\s*[||][\s||]*DSML[\s||]*invoke\s+name\s*=\s*"([^"]+)"\s*>`)
632
+ const DSML_INVOKE_AT = new RegExp(String.raw`<\s*[||][\s||]*DSML[\s||]*invoke\s+name\s*=`)
633
+ const DSML_CLOSE = new RegExp(String.raw`<\s*\/\s*[||][\s||]*DSML[\s||]*invoke\s*>`)
634
+ 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")
635
+ // what is left after a call: blanks, our close, DeepSeek's tags but the
636
+ // next call's
637
+ const LEFTOVER = new RegExp(String.raw`^(?:\s+|<\/tool_calls?>|<tool_calls>|` + DSML + String.raw`(?![\s||]*invoke\b)[^<>]*>)+`)
638
+ // what text may be the start of, so held back
639
+ const MARKS = [OPEN, CLOSE, "<|DSML|", "<|DSML|", "</|DSML|", "</|DSML|"]
640
+ const MARK_MAX = Math.max(...MARKS.map((m) => m.length))
641
+
642
+ // dsmlArgs is the arguments of a DSML invoke as JSON: a parameter marked
643
+ // string="true" is its text, any other its JSON (its text when not JSON)
644
+ function dsmlArgs(body) {
645
+ const args = {}
646
+ for (const p of body.matchAll(DSML_PARAM)) {
647
+ if (/string\s*=\s*"true"/.test(p[2])) {
648
+ args[p[1]] = p[3]
649
+ continue
650
+ }
651
+ try {
652
+ args[p[1]] = JSON.parse(p[3].trim())
653
+ } catch {
654
+ args[p[1]] = p[3]
655
+ }
656
+ }
657
+ return JSON.stringify(args)
658
+ }
659
+
660
+ // objectEnd is where the JSON object starting at from (after blanks) ends:
661
+ // past its closing brace, -1 when it isn't whole yet, -2 when there's none
662
+ function objectEnd(s, from) {
663
+ let k = from
664
+ while (k < s.length && /\s/.test(s[k])) k++
665
+ if (k >= s.length) return -1
666
+ if (s[k] !== "{") return -2
667
+ let depth = 0
668
+ let str = false
669
+ let esc = false
670
+ for (; k < s.length; k++) {
671
+ const ch = s[k]
672
+ if (str) {
673
+ if (esc) esc = false
674
+ else if (ch === "\\") esc = true
675
+ else if (ch === '"') str = false
676
+ continue
677
+ }
678
+ if (ch === '"') str = true
679
+ else if (ch === "{") depth++
680
+ else if (ch === "}" && --depth === 0) return k + 1
681
+ }
682
+ return -1
683
+ }
684
+
685
+ // looseJSON reads a JSON object as a model writes one: a string holding a
686
+ // raw newline or tab (a file's content written out, #799) is read as if
687
+ // they were escaped. null when it isn't one even so.
688
+ function looseJSON(s) {
689
+ for (const t of [s, escapeRaw(s)]) {
690
+ try {
691
+ const v = JSON.parse(t)
692
+ if (v && typeof v === "object" && !Array.isArray(v)) return v
693
+ } catch {}
694
+ }
695
+ return null
696
+ }
697
+
698
+ // escapeRaw escapes the control characters inside JSON strings
699
+ function escapeRaw(s) {
700
+ let out = ""
701
+ let str = false
702
+ let esc = false
703
+ for (const ch of String(s ?? "")) {
704
+ if (str && !esc && ch < " ") {
705
+ out += ch === "\n" ? "\\n" : ch === "\r" ? "\\r" : ch === "\t" ? "\\t" : "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0")
706
+ continue
707
+ }
708
+ if (str && ch === "\\" && !esc) esc = true
709
+ else {
710
+ if (ch === '"' && !esc) str = !str
711
+ esc = false
712
+ }
713
+ out += ch
714
+ }
715
+ return out
716
+ }
717
+
718
+ // callArgs is a tagged block's arguments as JSON: under arguments (an
719
+ // object, or a string of one), input or parameters, else the block's other
720
+ // keys, as a model that writes them beside the name means them (#799: a
721
+ // write that went out as {})
722
+ function callArgs(v) {
723
+ let a = v.arguments ?? v.input ?? v.parameters
724
+ if (a === undefined) {
725
+ const { name, type, id, ...rest } = v
726
+ a = rest
727
+ }
728
+ if (typeof a === "string") return a.trim() ? (looseJSON(a) ? JSON.stringify(looseJSON(a)) : a) : "{}"
729
+ return JSON.stringify(a ?? {})
730
+ }
731
+
573
732
  // nativeCall is a tool call as Trae's events carry one
574
733
  function nativeCall(tc) {
575
734
  const f = tc?.function ?? tc?.function_call ?? tc
576
735
  const name = f?.name ?? tc?.tool_name ?? ""
577
736
  const index = Number.isInteger(tc?.index) ? tc.index : null
578
- if (!name && index === null && !tc?.id) return null
579
737
  const a = f?.arguments ?? f?.args ?? tc?.params ?? tc?.input ?? tc?.parameters ?? ""
580
- return { id: tc?.id || tc?.tool_call_id || "", index, name: String(name), arguments: typeof a === "string" ? a : JSON.stringify(a) }
738
+ const args = typeof a === "string" ? a : JSON.stringify(a)
739
+ if (!name && index === null && !tc?.id && !args) return null
740
+ return { id: tc?.id || tc?.tool_call_id || "", index, name: String(name), arguments: args }
581
741
  }
582
742
 
583
743
  // NativeCalls puts together the tool calls Trae streams: a call comes in
@@ -594,6 +754,12 @@ class NativeCalls {
594
754
  let o = this.calls.find((x) => (c.id && x.id === c.id) || (!c.id && c.index !== null && x.index === c.index))
595
755
  if (!o && !c.id && c.index === null)
596
756
  o = this.calls.findLast((x) => x.name === c.name && (x.arguments === c.arguments || (!done(x.arguments) && c.arguments.startsWith(x.arguments))))
757
+ // a piece with nothing to say whose it is (or an index its call's
758
+ // first event didn't give) belongs to the call still being written
759
+ if (!o && !c.name && !c.id) {
760
+ o = this.calls.findLast((x) => !done(x.arguments))
761
+ if (o && c.index !== null && o.index !== null && o.index !== c.index) o = null
762
+ }
597
763
  if (!o) {
598
764
  if (!c.name) return
599
765
  this.calls.push({ ...c })
@@ -608,7 +774,10 @@ class NativeCalls {
608
774
  else o.arguments = prev + next // a piece
609
775
  }
610
776
  take() {
611
- return this.calls.filter((c) => c.name).map(({ id, name, arguments: a }) => ({ id: id || callId(), name, arguments: a || "{}" }))
777
+ return this.calls.filter((c) => c.name).map(({ id, name, arguments: a }) => {
778
+ const v = a && !done(a) ? looseJSON(a) : null
779
+ return { id: id || callId(), name, arguments: v ? JSON.stringify(v) : a || "{}" }
780
+ })
612
781
  }
613
782
  }
614
783
 
@@ -1154,4 +1323,4 @@ export const TraeCNAuthPlugin = async ({ client }) => {
1154
1323
  }
1155
1324
 
1156
1325
  // for tests
1157
- export const _internal = { HOSTS, MODELS, TextTools, traeMessages, chatBody, credits, whenOf, newDevice }
1326
+ export const _internal = { HOSTS, MODELS, TextTools, NativeCalls, looseJSON, traeMessages, chatBody, credits, whenOf, newDevice }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-trae-auth",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "OpenCode provider plugin for Trae CN (trae.cn): your Trae CN account's models, free tier included, with Trae's own browser sign-in",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",