@magpie-community/opencode-workbuddy-auth 0.1.21 → 0.1.23

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 (3) hide show
  1. package/README.md +8 -0
  2. package/index.mjs +157 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -95,6 +95,14 @@ models to each shape below and Kimi's, GPT's, Gemini's and GLM's to some:
95
95
  `allOf`) at the root are an object, the union's fields merged;
96
96
  - a `pattern`'s `\0` is `\u0000`, which matches the same;
97
97
  - a tuple's `items` (a list) is one schema any of them fits;
98
+ - an `enum` with no `type` takes its values' type (or that and `"null"`),
99
+ and an `enum` of several types is an `anyOf` of one typed `enum` for
100
+ each, at any depth; an empty `enum` is left out (Kimi and DeepSeek, an
101
+ MCP server's tools, #122);
102
+ - `true` and `false` where a schema goes are `{}` and `{"not": {}}`
103
+ (`additionalProperties` is left); a `required` that isn't a list, and a
104
+ `type` JSON Schema doesn't name, are left out, and `"String"` is
105
+ `"string"`;
98
106
  - a tool's name with a character other than letters, digits, `_` and `-`
99
107
  (`.`, `:`, `/`, a space, CJK) is sent with `_` there, and the model's
100
108
  calls come back under the tool's own name.
package/index.mjs CHANGED
@@ -636,6 +636,139 @@ function fitSchema(s, depth = 0) {
636
636
  return changed
637
637
  }
638
638
 
639
+ // Kimi (kimi-k3-1) answers 11133 too, to the tools of an MCP server or a
640
+ // plugin (clearmann, #122). Tried on the owner's account, 2026-10-14, each
641
+ // alone in an otherwise plain chat: Kimi turns away an enum with no type
642
+ // (["a","b"] as much as [1,"a",null]), an empty enum, an enum whose type
643
+ // lists two types besides null, a schema that is true or false where a
644
+ // schema goes (properties.x: true, items: true), a required that isn't a
645
+ // list, and a type it doesn't know ("any", "String"). It takes an enum
646
+ // typed T or [T, "null"], an anyOf of such enums, {"not": {}}, {}, and
647
+ // additionalProperties: true or false. fitShape gives each the shape it
648
+ // takes, meaning the same: none of it narrows or widens what a call may
649
+ // hold, except an empty enum (which nothing fits) is left out.
650
+ const TYPES = new Set(["string", "number", "integer", "boolean", "object", "array", "null"])
651
+
652
+ // SUB_ONE are the keywords whose value is a schema, SUB_LIST a list of
653
+ // schemas, SUB_MAP a map of names to schemas. additionalProperties and
654
+ // unevaluatedProperties are left as they are when true or false.
655
+ const SUB_ONE = ["items", "additionalItems", "unevaluatedItems", "contains", "not", "if", "then", "else", "propertyNames", "additionalProperties", "unevaluatedProperties"]
656
+ const SUB_LIST = ["anyOf", "oneOf", "allOf", "prefixItems"]
657
+ const SUB_MAP = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"]
658
+ const BOOL_OK = new Set(["additionalProperties", "unevaluatedProperties"])
659
+
660
+ // valueType is the JSON Schema type of an enum's value.
661
+ function valueType(v) {
662
+ if (v === null) return "null"
663
+ if (Array.isArray(v)) return "array"
664
+ if (typeof v === "number") return Number.isInteger(v) ? "integer" : "number"
665
+ return typeof v
666
+ }
667
+
668
+ // fitEnum gives s's enum a type: the one its values have, or that and
669
+ // "null"; values of several types are an anyOf of an enum for each. It
670
+ // reports whether it changed anything.
671
+ function fitEnum(s) {
672
+ if (!("enum" in s)) return false
673
+ if (!Array.isArray(s.enum) || s.enum.length === 0) {
674
+ delete s.enum
675
+ return true
676
+ }
677
+ const listed = Array.isArray(s.type) ? s.type : typeof s.type === "string" ? [s.type] : []
678
+ if (listed.filter((t) => t !== "null").length === 1) return false
679
+ const byType = new Map()
680
+ for (const v of s.enum) {
681
+ const t = valueType(v)
682
+ if (!byType.has(t)) byType.set(t, [])
683
+ byType.get(t).push(v)
684
+ }
685
+ if (byType.has("integer") && byType.has("number")) {
686
+ byType.get("number").push(...byType.get("integer"))
687
+ byType.delete("integer")
688
+ }
689
+ const hasNull = byType.delete("null")
690
+ if (byType.size <= 1) {
691
+ const [t] = byType.keys()
692
+ const want = t === undefined ? "null" : hasNull ? [t, "null"] : t
693
+ if (JSON.stringify(want) === JSON.stringify(s.type)) return false
694
+ s.type = want
695
+ return true
696
+ }
697
+ const split = [...byType].map(([t, vs]) => ({ type: t, enum: vs }))
698
+ if (hasNull) split.push({ type: "null" })
699
+ delete s.enum
700
+ delete s.type
701
+ if (Array.isArray(s.anyOf)) s.allOf = [...(Array.isArray(s.allOf) ? s.allOf : []), { anyOf: split }]
702
+ else s.anyOf = split
703
+ return true
704
+ }
705
+
706
+ // subSchema is v, a schema found under key, as an object Kimi takes: true
707
+ // is {}, false is {"not": {}}.
708
+ function subSchema(v, key) {
709
+ if (typeof v !== "boolean" || BOOL_OK.has(key)) return v
710
+ return v ? {} : { not: {} }
711
+ }
712
+
713
+ // fitShape mends s, a schema, and each schema under it, in place (see
714
+ // TYPES above); a name under properties is never read as a keyword. It
715
+ // reports whether it changed anything.
716
+ function fitShape(s, depth = 0) {
717
+ if (!s || typeof s !== "object" || Array.isArray(s) || depth > 64) return false
718
+ let changed = false
719
+ if ("type" in s) {
720
+ const list = (Array.isArray(s.type) ? s.type : [s.type]).map((t) => (typeof t === "string" ? t.toLowerCase() : t)).filter((t) => TYPES.has(t))
721
+ const uniq = [...new Set(list)]
722
+ const want = uniq.length === 0 ? undefined : uniq.length === 1 ? uniq[0] : uniq
723
+ if (JSON.stringify(want) !== JSON.stringify(s.type)) {
724
+ if (want === undefined) delete s.type
725
+ else s.type = want
726
+ changed = true
727
+ }
728
+ }
729
+ if ("required" in s && !Array.isArray(s.required)) {
730
+ delete s.required
731
+ changed = true
732
+ } else if (Array.isArray(s.required) && s.required.some((k) => typeof k !== "string")) {
733
+ s.required = s.required.filter((k) => typeof k === "string")
734
+ changed = true
735
+ }
736
+ if (fitEnum(s)) changed = true
737
+ for (const k of SUB_ONE) {
738
+ if (!(k in s)) continue
739
+ const v = subSchema(s[k], k)
740
+ if (v !== s[k]) {
741
+ s[k] = v
742
+ changed = true
743
+ }
744
+ if (fitShape(v, depth + 1)) changed = true
745
+ }
746
+ for (const k of SUB_LIST) {
747
+ if (!Array.isArray(s[k])) continue
748
+ s[k].forEach((v0, i) => {
749
+ const v = subSchema(v0, k)
750
+ if (v !== v0) {
751
+ s[k][i] = v
752
+ changed = true
753
+ }
754
+ if (fitShape(v, depth + 1)) changed = true
755
+ })
756
+ }
757
+ for (const k of SUB_MAP) {
758
+ const m = s[k]
759
+ if (!m || typeof m !== "object" || Array.isArray(m)) continue
760
+ for (const [name, v0] of Object.entries(m)) {
761
+ const v = subSchema(v0, k)
762
+ if (v !== v0) {
763
+ m[name] = v
764
+ changed = true
765
+ }
766
+ if (fitShape(v, depth + 1)) changed = true
767
+ }
768
+ }
769
+ return changed
770
+ }
771
+
639
772
  // localRef is the schema a local $ref in root names ("#/$defs/x",
640
773
  // "#/definitions/x"), or s itself.
641
774
  function localRef(root, s) {
@@ -698,8 +831,8 @@ function objectRoot(ps, root = ps, depth = 0) {
698
831
  }
699
832
 
700
833
  // fitTools mends b's function tools in place: their parameters (fitSchema,
701
- // objectRoot) and names. It reports whether it changed anything, and the
702
- // names it gave, each to the name the client knows (ours), so that the
834
+ // fitShape, objectRoot) and names. It reports whether it changed anything,
835
+ // and the names it gave, each to the name the client knows (ours), so that the
703
836
  // model's calls are named back. A call in the chat's history is named as
704
837
  // its tool now is.
705
838
  function fitTools(b) {
@@ -729,6 +862,7 @@ function fitTools(b) {
729
862
  continue
730
863
  }
731
864
  if (fitSchema(fn.parameters)) changed = true
865
+ if (fitShape(fn.parameters)) changed = true
732
866
  if (objectRoot(fn.parameters)) changed = true
733
867
  }
734
868
  if (theirs.size && Array.isArray(b.messages)) {
@@ -1075,6 +1209,24 @@ function enterpriseUsageOf(u) {
1075
1209
  return w
1076
1210
  }
1077
1211
 
1212
+ // spareFree has the credits windows not count the models that cost none
1213
+ // (notModels): a model at x0.00 draws no credits, and with the credits
1214
+ // used up WorkBuddy still serves it (deepseek-v4.1-flash at 560 / 560,
1215
+ // magpie-community/plugins#138, hheei). Counted, the window at 100% put
1216
+ // the account last, and behind every other, for its free models too. A
1217
+ // model free only some hours (a night promotion) is not free here, as in
1218
+ // the list (priceOf). A list that can't be read leaves the windows as they
1219
+ // are: every model counted, as before.
1220
+ async function spareFree(site, a, windows) {
1221
+ if (!windows.length) return windows
1222
+ let free = []
1223
+ try {
1224
+ free = (await liveModels(site, a)).filter((m) => m.free).map((m) => m.id)
1225
+ } catch {}
1226
+ if (free.length) for (const w of windows) w.notModels = free
1227
+ return windows
1228
+ }
1229
+
1078
1230
  // The packages' codes, as WorkBuddy's own client lists them when it asks
1079
1231
  // for each package's credits (its COMMODITY_CODES): the API refuses a
1080
1232
  // detail list without them (10001 "PackageCodes required").
@@ -1443,7 +1595,7 @@ function makePlugin(site) {
1443
1595
  try {
1444
1596
  ent = enterpriseUsageOf(await meter(site, a, "/billing/meter/get-enterprise-user-usage"))
1445
1597
  } catch {}
1446
- if (ent) return { plan: auth.plan || "Enterprise", windows: [ent], signIn: "kept" }
1598
+ if (ent) return { plan: auth.plan || "Enterprise", windows: await spareFree(site, a, [ent]), signIn: "kept" }
1447
1599
  }
1448
1600
  // when the credits left run out, for routing to spend them first;
1449
1601
  // a list that can't be read leaves it unknown, the card stands
@@ -1453,6 +1605,7 @@ function makePlugin(site) {
1453
1605
  Object.assign(credits, runsOutOf(sum, await packagesOf(site, a)) ?? {})
1454
1606
  } catch {}
1455
1607
  }
1608
+ await spareFree(site, a, out.windows)
1456
1609
  return { ...out, signIn: "kept" }
1457
1610
  } catch (e) {
1458
1611
  return { error: e?.message ?? String(e), signIn: "kept" }
@@ -1558,4 +1711,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
1558
1711
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
1559
1712
 
1560
1713
  // for tests
1561
- export const _internal = { replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
1714
+ export const _internal = { fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-workbuddy-auth",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "OpenCode provider plugins for WorkBuddy's plans: WorkBuddy (CodeBuddy, copilot.tencent.com) and WorkBuddy AI (www.workbuddy.ai)",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",