@magpie-community/opencode-workbuddy-auth 0.1.4 → 0.1.6

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 +51 -17
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -86,10 +86,11 @@ const api = (site) => site.endpoint + "/v2"
86
86
  // ---- WorkBuddy's API ------------------------------------------------------
87
87
 
88
88
  class WBError extends Error {
89
- constructor(message, code, status) {
89
+ constructor(message, code, status, said) {
90
90
  super(message)
91
91
  this.code = code
92
92
  this.status = status
93
+ this.said = said // as magpie's built-in (wbCall) worded it
93
94
  }
94
95
  }
95
96
 
@@ -112,11 +113,13 @@ async function call(site, method, path, headers = {}, body) {
112
113
  try {
113
114
  env = JSON.parse(text)
114
115
  } catch {}
116
+ const code = Number.parseInt(env.code, 10) || 0
115
117
  if (!res.ok) {
116
118
  const why = env.msg || env.message || env.error?.message || text.slice(0, 200) || res.statusText
117
- throw new WBError(`${site.name}: ${why} (HTTP ${res.status}${env.code ? ", code " + env.code : ""})`, env.code, res.status)
119
+ const said = code ? env.msg || `error ${code}` : env.msg || statusText(res.status)
120
+ throw new WBError(`${site.name}: ${why} (HTTP ${res.status}${env.code ? ", code " + env.code : ""})`, env.code, res.status, said)
118
121
  }
119
- if (env.code) throw new WBError(`${site.name}: ${env.msg || "error"} (${env.code})`, env.code, res.status)
122
+ if (env.code) throw new WBError(`${site.name}: ${env.msg || "error"} (${env.code})`, env.code, res.status, env.msg || `error ${code}`)
120
123
  return env.data
121
124
  }
122
125
 
@@ -145,13 +148,14 @@ function stale(a) {
145
148
 
146
149
  // fresh is the sign-in with an access token that isn't about to end:
147
150
  // refreshed (and saved) when it is. A refresh that fails, or a refresh
148
- // token that has ended, leaves the access token there is.
151
+ // token that has ended, leaves the access token there is. Its failures
152
+ // are worded as the built-in's (wbFresh), "WorkBuddy" on both sites.
149
153
  async function fresh(site, client, a) {
150
154
  if (!stale(a)) return a
151
155
  const refreshable = a.refresh && !(a.refreshExpiresAt && Date.now() >= a.refreshExpiresAt)
152
156
  if (!refreshable) {
153
157
  if (a.access) return a
154
- throw new Error(`this ${site.name} account is signed out; sign in again`)
158
+ throw new Error("this WorkBuddy account is signed out; sign in again")
155
159
  }
156
160
  try {
157
161
  const got = await call(site, "POST", "/v2/plugin/auth/token/refresh", {
@@ -159,13 +163,14 @@ async function fresh(site, client, a) {
159
163
  "X-Auth-Refresh-Source": "plugin",
160
164
  "X-Domain": domainOf(site, a),
161
165
  }, {})
162
- if (!got?.accessToken) throw new Error(`${site.name} gave no refreshed token`)
166
+ if (!got?.accessToken) throw Object.assign(new Error("WorkBuddy gave no refreshed token"), { bare: true })
163
167
  const next = merge(a, got)
164
168
  await client?.auth?.set?.({ path: { id: site.id }, body: next })
165
169
  return next
166
170
  } catch (e) {
167
171
  if (a.access) return a
168
- throw new Error(`${site.name} token refresh: ${e?.message ?? e}`)
172
+ if (e?.bare) throw e
173
+ throw new Error(`WorkBuddy token refresh: ${e?.said ?? e?.message ?? e}`)
169
174
  }
170
175
  }
171
176
 
@@ -460,18 +465,29 @@ function modelOf(site, provider, m) {
460
465
  variants: variants(m.efforts),
461
466
  // magpie's own: served at no cost to the plan's credits
462
467
  free: !!m.free,
468
+ // magpie's own: the credits a request costs, as a multiple (0: none)
469
+ rate: m.rate ?? 0,
463
470
  }
464
471
  }
465
472
 
466
473
  // liveModels asks WorkBuddy's product config for the plan's models: the
467
474
  // "cli" agent's list, each with its details. The CLI User-Agent picks
468
475
  // WorkBuddy's config (a bare WorkBuddy/<v> gets CodeBuddy IDE's, no cli).
476
+ // creditsOf reads a model's credits, the multiple WorkBuddy's picker shows
477
+ // by it: "x0.03", "0" or 0; "x0.00" is free. A rate it can't read (or
478
+ // none, or an infinite one) is undefined: the model is neither free nor
479
+ // rated. The same rule as magpie's built-in (wbCredits).
480
+ function creditsOf(c) {
481
+ if (c == null || c === "") return undefined
482
+ const s = String(c).trim().toLowerCase().replace(/^x/, "")
483
+ const n = Number(s)
484
+ return s !== "" && Number.isFinite(n) && n >= 0 ? n : undefined
485
+ }
486
+
469
487
  // freeCredits says whether a model's credits are none: "x0.00", "0" or 0.
470
488
  // A rate it can't read is not free.
471
489
  function freeCredits(c) {
472
- if (c == null || c === "") return false
473
- const s = String(c).trim().toLowerCase().replace(/^x/, "")
474
- return s !== "" && Number.isFinite(Number(s)) && Number(s) === 0
490
+ return creditsOf(c) === 0
475
491
  }
476
492
 
477
493
  async function liveModels(site, a) {
@@ -495,7 +511,11 @@ async function liveModels(site, a) {
495
511
  m.context = d.maxInputTokens
496
512
  m.output = d.maxOutputTokens
497
513
  m.images = d.supportsImages === true
498
- m.free = freeCredits(d.credits)
514
+ const r = creditsOf(d.credits)
515
+ if (r !== undefined) {
516
+ m.free = r === 0
517
+ m.rate = r
518
+ }
499
519
  const es = d.reasoning?.supportedEfforts ?? []
500
520
  if (es.length) {
501
521
  const canOff = d.reasoning?.canDisableThinking
@@ -570,20 +590,22 @@ function makePlugin(site) {
570
590
  let body = init?.body
571
591
  if (body === undefined && req) body = await req.clone().text()
572
592
  const res = await fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body: withSystem(body) })
573
- return res.status >= 400 ? explained(res) : res
593
+ return res.status >= 400 ? explained(res) : kept(res)
574
594
  },
575
595
  }
576
596
  },
577
- // the account's credits, as magpie's built-in showed them
597
+ // the account's credits, as magpie's built-in showed them; each
598
+ // read keeps the sign-in (signIn), as the built-in's neither marked
599
+ // an account lapsed nor cleared it
578
600
  async usage(getAuth) {
579
601
  const auth = await getAuth()
580
- if (auth?.type !== "oauth" || !(auth.access || auth.source)) return { error: "not signed in" }
602
+ if (auth?.type !== "oauth" || !(auth.access || auth.source)) return { error: "not signed in", signIn: "kept" }
581
603
  try {
582
604
  const a = await current(site, client, auth)
583
605
  // the meter has no /v2 prefix, on either site
584
- return usageOf(await meter(site, a, "/billing/meter/get-user-resource-summary"), auth.plan)
606
+ return { ...usageOf(await meter(site, a, "/billing/meter/get-user-resource-summary"), auth.plan), signIn: "kept" }
585
607
  } catch (e) {
586
- return { error: e?.message ?? String(e) }
608
+ return { error: e?.message ?? String(e), signIn: "kept" }
587
609
  }
588
610
  },
589
611
  methods: [
@@ -611,13 +633,24 @@ const REFUSED_HINT =
611
633
  "WorkBuddy refuses chats from Codex and Claude Code (their system prompt); use it from Hermes, OpenCode or Pi, or add another provider to this group"
612
634
  const REFUSED = /unapproved channel|illegal api invocation/i
613
635
 
636
+ // kept is an answer that went through, saying the sign-in is kept: the
637
+ // built-in never cleared a WorkBuddy account's mark, as it never set one.
638
+ function kept(res) {
639
+ const headers = new Headers(res.headers)
640
+ headers.set("X-Magpie-Sign-In", "kept")
641
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
642
+ }
643
+
614
644
  // explained is res with REFUSED_HINT added to that refusal's message.
645
+ // Each refusal says the sign-in is kept: the built-in passed WorkBuddy's
646
+ // 401 on and never marked a WorkBuddy account lapsed.
615
647
  async function explained(res) {
616
648
  const text = await res.text()
617
649
  const again = (b) => {
618
650
  const headers = new Headers(res.headers)
619
651
  headers.delete("content-length")
620
652
  headers.delete("content-encoding")
653
+ headers.set("X-Magpie-Sign-In", "kept")
621
654
  return new Response(b, { status: res.status, statusText: res.statusText, headers })
622
655
  }
623
656
  if (!REFUSED.test(text)) return again(text)
@@ -635,6 +668,7 @@ async function explained(res) {
635
668
  headers.set("content-type", "application/json")
636
669
  headers.delete("content-length")
637
670
  headers.delete("content-encoding")
671
+ headers.set("X-Magpie-Sign-In", "kept")
638
672
  return new Response(JSON.stringify({ error: { message: msg, type: "permission_error", code: null } }), { status: res.status, statusText: res.statusText, headers })
639
673
  }
640
674
 
@@ -642,4 +676,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
642
676
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
643
677
 
644
678
  // for tests
645
- export const _internal = { withSystem, usageOf, desktopHeld, explained, REFUSED_HINT, freeCredits }
679
+ export const _internal = { withSystem, usageOf, desktopHeld, explained, REFUSED_HINT, freeCredits, creditsOf, fresh, SITES }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-workbuddy-auth",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",