@magpie-community/opencode-workbuddy-auth 0.1.48 → 0.2.0

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 +23 -4
  2. package/index.mjs +106 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -397,11 +397,30 @@ entry. An already signed-in browser can simply renew another saved account.
397
397
 
398
398
  ## Not here
399
399
 
400
- magpie's built-in WorkBuddy does more than this package:
401
- - the daily check-in;
402
- - the credits meter (`/billing/meter/get-user-resource-summary`);
403
- - several accounts under one provider.
400
+ magpie's built-in WorkBuddy does more than this package: several accounts
401
+ under one provider.
404
402
 
405
403
  OpenCode's plugin API has no place for these.
406
404
 
407
405
  Credit multipliers accept the international catalog's `x0.34 credits` text as well as `x0.34`. An empty credits value (for example Auto) stays unknown, never free.
406
+
407
+ ## Daily check-in
408
+
409
+ The account's daily credits are claimed through the billing meter. magpie
410
+ can run it once a day per account when its WorkBuddy check-in switch is on;
411
+ Settings → Usage → Plugins has that switch and a *Check in now* button.
412
+ The activity status is read first: inactive is `inactive`, a day already
413
+ checked in is `done`, and only an active unchecked day is claimed. A claim
414
+ that WorkBuddy answers 1001, 1002 or 1003 is `done`, `ineligible` or
415
+ `inactive`. A claim turned away 401 while the account's credits still read
416
+ is `ineligible`: that check-in takes WorkBuddy's own app. A transient
417
+ billing failure is retried twice before the card shows the failure.
418
+ Only WorkBuddy (China) checks in: the AI build has no claim endpoint, and
419
+ its plugin carries no check-in hook.
420
+ A request that gets no answer at all leaves the hook as a thrown error, so
421
+ magpie retries it on its 1-minute offline backoff rather than the 30-minute
422
+ one it gives a failed answer (#149).
423
+
424
+ An `inactive` answer is kept for the whole Beijing day: the plugin host
425
+ gives it no recheck, and a manual refresh reads the kept one (tracked in
426
+ [yetone/magpie#2696](https://github.com/yetone/magpie/issues/2696)).
package/index.mjs CHANGED
@@ -1868,23 +1868,99 @@ function signedOut(site, a, why, expired, note) {
1868
1868
  // the same: WorkBuddy's message, else its code, else the HTTP status.
1869
1869
  async function meter(site, a, path, body = {}) {
1870
1870
  const res = await meterAsked(site, a, path, body)
1871
+ const raw = await res.text()
1871
1872
  let env = {}
1873
+ let parsed = true
1872
1874
  try {
1873
- env = JSON.parse(await res.text()) ?? {}
1874
- } catch {}
1875
+ env = JSON.parse(raw) ?? {}
1876
+ } catch {
1877
+ parsed = false
1878
+ }
1879
+ if (res.ok && !parsed) {
1880
+ const unreadable = raw.trim().slice(0, 120) || "(empty)"
1881
+ throw Object.assign(new Error(`unreadable reply ${unreadable}`), { status: res.status, code: 0, unreadable: true })
1882
+ }
1875
1883
  const code = Number.parseInt(env.code, 10) || 0
1884
+ // the status, the code and whether the token itself was refused go with
1885
+ // it, for usage's one renewal on a 401 (reissued) and check-in's outcomes
1886
+ const said = { status: res.status, code }
1887
+ if (!res.ok) said.invalidToken = invalidToken(res)
1888
+ const err = (message) => Object.assign(new Error(message), said)
1876
1889
  if (!res.ok) {
1877
- // the status and whether the token itself was refused go with it, for
1878
- // usage's one renewal on a 401 (reissued)
1879
- const said = { status: res.status, invalidToken: invalidToken(res) }
1880
- if (code) throw Object.assign(new Error(env.msg || `error ${code}`), said)
1881
- if (env.msg) throw Object.assign(new Error(env.msg), said)
1882
- throw Object.assign(new Error(statusText(res.status)), said)
1883
- }
1884
- if (code) throw new Error(env.msg || `error ${code}`)
1890
+ if (code) throw err(env.msg || `error ${code}`)
1891
+ if (env.msg) throw err(env.msg)
1892
+ throw err(statusText(res.status))
1893
+ }
1894
+ if (code) throw err(env.msg || `error ${code}`)
1885
1895
  return env.data ?? null
1886
1896
  }
1887
1897
 
1898
+ // checkinOf claims the account's daily credits. China has the /v2 meter;
1899
+ // after the activity status names an active, unchecked day. Transient
1900
+ // upstream failures are asked again twice (the bounded retry quota-hub
1901
+ // observed on the billing endpoint; callers may shorten the waits for a
1902
+ // test).
1903
+ async function checkinOf(site, a, { waits = [0, 0] } = {}) {
1904
+ let status
1905
+ try {
1906
+ status = await meter(site, a, "/v2/billing/meter/checkin-activity-status")
1907
+ } catch (e) {
1908
+ if (e?.code === 1001) return { outcome: "done" }
1909
+ // a reply WorkBuddy sent is the day's failed answer; only a request
1910
+ // that got no answer leaves as a throw, for magpie's offline backoff
1911
+ if (e?.status !== undefined || e?.code !== undefined) return { outcome: "failed", message: e.message }
1912
+ throw e
1913
+ }
1914
+ if (!status?.active) return { outcome: "inactive" }
1915
+ if (status.today_checked_in ?? status.todayCheckedIn) {
1916
+ // today_credit is what today actually gave, the built-in's word for a
1917
+ // day already in; daily_credit is the day's grant
1918
+ const credit = Number(status.today_credit ?? status.daily_credit)
1919
+ return { outcome: "done", ...(Number.isFinite(credit) && credit > 0 ? { credit } : {}) }
1920
+ }
1921
+ const paths = ["/v2/billing/meter/daily-checkin"]
1922
+ for (let attempt = 0; ; attempt++) {
1923
+ let i = 0
1924
+ while (i < paths.length) {
1925
+ try {
1926
+ const got = await meter(site, a, paths[i++])
1927
+ // the claim's own answer, as the built-in hands it on: the credit
1928
+ // and streak it granted and what it said, for the card
1929
+ const credit = Number(got?.credit)
1930
+ const streak = Number(got?.streak_days)
1931
+ return {
1932
+ outcome: "claimed",
1933
+ ...(Number.isFinite(credit) && credit > 0 ? { credit } : {}),
1934
+ ...(Number.isFinite(streak) && streak > 0 ? { streak } : {}),
1935
+ ...(got?.message ? { message: String(got.message) } : {}),
1936
+ }
1937
+ } catch (e) {
1938
+ if (e?.code === 1001) return { outcome: "done" }
1939
+ if (e?.code === 1002) return { outcome: "ineligible", message: e.message }
1940
+ if (e?.code === 1003) return { outcome: "inactive", message: e.message }
1941
+ if (e?.status === 401) {
1942
+ try {
1943
+ await meter(site, a, "/billing/meter/get-user-resource-summary")
1944
+ } catch {
1945
+ throw e
1946
+ }
1947
+ return { outcome: "ineligible", message: "WorkBuddy's check-in takes WorkBuddy's own app" }
1948
+ }
1949
+ if (e?.unreadable) return { outcome: "failed", message: `check-in: ${e.message}` }
1950
+ const transient = e?.status >= 500 || e?.code === 10000
1951
+ if (transient && attempt < waits.length) {
1952
+ await new Promise((r) => setTimeout(r, waits[attempt]))
1953
+ break
1954
+ }
1955
+ // a reply WorkBuddy sent is the day's failed answer; only a request
1956
+ // that got no answer leaves as a throw, for magpie's offline backoff
1957
+ if (e?.status !== undefined || e?.code !== undefined) return { outcome: "failed", message: e.message }
1958
+ throw e
1959
+ }
1960
+ }
1961
+ }
1962
+ }
1963
+
1888
1964
  // capacity is a capacity the billing API sends, as a string ("438.88")
1889
1965
  // or now and then a number; empty is 0.
1890
1966
  function capacity(v) {
@@ -3036,6 +3112,25 @@ function makePlugin(site) {
3036
3112
  // and is renewed as before.
3037
3113
  refreshLead: LEAD_MS,
3038
3114
  refresh: (auth) => refreshed(site, auth),
3115
+ // the account's daily check-in: magpie can run it once a day per
3116
+ // account, when its WorkBuddy check-in switch is on; the button in
3117
+ // Settings asks right away. China's meter only: the AI build has no
3118
+ // check-in (#47), so the hook stays off its plugin. Errors leave as
3119
+ // throws, not as failed answers: host.js answers a throwing hook to
3120
+ // magpie as an error, whose wbCheckinFailed keeps the offline words
3121
+ // and magpie's 1-minute backoff, where a returned failed waits the
3122
+ // full 30 minutes (magpie-community/plugins#149). checkinOf returns
3123
+ // an outcome only for answers it read and classified.
3124
+ ...(site.ai ? {} : {
3125
+ async checkin(getAuth) {
3126
+ const auth = await getAuth()
3127
+ // an API key reads no credits (see usage), so it has no day to claim
3128
+ if (auth?.type === "api") return { outcome: "ineligible", message: "check-in needs a signed-in account, not an API key" }
3129
+ if (auth?.type !== "oauth" || !(auth.access || auth.source)) return { outcome: "failed", message: "not signed in" }
3130
+ const a = await current(site, client, auth)
3131
+ return await checkinOf(site, a, { waits: [2000, 4000] })
3132
+ },
3133
+ }),
3039
3134
  options: [
3040
3135
  {
3041
3136
  key: FREE_ONLY,
@@ -3409,4 +3504,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
3409
3504
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
3410
3505
 
3411
3506
  // for tests
3412
- export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
3507
+ export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-workbuddy-auth",
3
- "version": "0.1.48",
3
+ "version": "0.2.0",
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",