@magpie-community/opencode-workbuddy-auth 0.2.16 → 0.2.18
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/index.mjs +82 -18
- package/opencode.mjs +5 -0
- package/opencode2.mjs +594 -0
- package/package.json +5 -3
package/index.mjs
CHANGED
|
@@ -2884,9 +2884,30 @@ function pinning() {
|
|
|
2884
2884
|
// spent and extra ones are left.
|
|
2885
2885
|
function usageOf(sum, plan, site) {
|
|
2886
2886
|
const out = { plan: plan || (sum?.IsPaidUser ? "Pro" : "Free plan"), windows: [] }
|
|
2887
|
+
const { plan: mine, extra, left, bags } = kindsOf(sum, site)
|
|
2888
|
+
if (mine.total > 0 && extra.total > 0) {
|
|
2889
|
+
const credits = creditWindow("Credits", mine), more = creditWindow("Extra credits", extra)
|
|
2890
|
+
if (mine.used >= mine.total && extra.used < extra.total) credits.aside = true
|
|
2891
|
+
else more.aside = true
|
|
2892
|
+
out.windows.push(credits, more)
|
|
2893
|
+
} else {
|
|
2894
|
+
const total = mine.total + extra.total, used = mine.used + extra.used
|
|
2895
|
+
if (total > 0) out.windows.push(creditWindow("Credits", { used, total }))
|
|
2896
|
+
}
|
|
2897
|
+
const total = mine.total + extra.total
|
|
2898
|
+
if (bags > 0 && (total === 0 || left > 0)) out.windows.push({ name: "Prepaid credits", balance: left, display: `${compact(left)} credits`, unit: "credits" })
|
|
2899
|
+
return out
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
// creditWindow is a window of credits used of a total.
|
|
2903
|
+
const creditWindow = (name, { used, total }) => ({ name, used: (100 * used) / total, display: `${compact(used)} / ${compact(total)}`, amount: used, limit: total, unit: "credits" })
|
|
2904
|
+
|
|
2905
|
+
// kindsOf is the summary's credits, the plan's and the extra ones (see
|
|
2906
|
+
// usageOf), and what is left in its packages with no total (a prepaid
|
|
2907
|
+
// balance).
|
|
2908
|
+
function kindsOf(sum, site) {
|
|
2887
2909
|
const ofPlan = planCodes(sum, site)
|
|
2888
|
-
const kinds = { plan: { total: 0, used: 0 }, extra: { total: 0, used: 0 } }
|
|
2889
|
-
let left = 0, bags = 0
|
|
2910
|
+
const kinds = { plan: { total: 0, used: 0 }, extra: { total: 0, used: 0 }, left: 0, bags: 0 }
|
|
2890
2911
|
const hasTotal = (p) => p?.CycleTotalCapacity != null && p.CycleTotalCapacity !== ""
|
|
2891
2912
|
const add = (k, p) => ((k.total += capacity(p.CycleTotalCapacity)), (k.used += capacity(p?.CycleUsedCapacity)))
|
|
2892
2913
|
const groups = (Array.isArray(sum?.CreditGroups) ? sum.CreditGroups : []).filter(hasTotal)
|
|
@@ -2895,24 +2916,45 @@ function usageOf(sum, plan, site) {
|
|
|
2895
2916
|
if (hasTotal(p)) {
|
|
2896
2917
|
if (!groups.length) add(kinds[ofPlan(p?.PackageCode) ? "plan" : "extra"], p)
|
|
2897
2918
|
} else if (p?.CycleRemainCapacity != null && p.CycleRemainCapacity !== "") {
|
|
2898
|
-
left += capacity(p.CycleRemainCapacity)
|
|
2899
|
-
bags++
|
|
2919
|
+
kinds.left += capacity(p.CycleRemainCapacity)
|
|
2920
|
+
kinds.bags++
|
|
2900
2921
|
}
|
|
2901
2922
|
}
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2923
|
+
return kinds
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
// listedExtra is the extra credits of the detail lists (packagesOf) that
|
|
2927
|
+
// the summary doesn't list at all, as WorkBuddy's own usage panel counts
|
|
2928
|
+
// them (jiabubu on Discord, magpie 0.1.2774, a China-site free account:
|
|
2929
|
+
// magpie's card read "0 / 500" alone, while the panel shows the extra
|
|
2930
|
+
// kinds beside the plan's). The panel's groups come from the detail
|
|
2931
|
+
// lists, not the summary (buildCreditGroups over fetchUserResources' paid
|
|
2932
|
+
// and free lists), and leave out a package past its end; a slice package
|
|
2933
|
+
// (CapacityType 4) counts the day's slice, as resolveResourceCapacity
|
|
2934
|
+
// does. null when there are none with credits.
|
|
2935
|
+
function listedExtra(sum, packages, site, now = Date.now()) {
|
|
2936
|
+
const summed = new Set((sum?.Packages ?? []).map((p) => p?.PackageCode))
|
|
2937
|
+
const codes = site?.ai ? [...EXTRA_CODES, ...EXTRA_AI] : EXTRA_CODES
|
|
2938
|
+
let total = 0, left = 0, at = 0, lapsing = 0
|
|
2939
|
+
for (const p of packages ?? []) {
|
|
2940
|
+
const code = p?.PackageCode
|
|
2941
|
+
if (!codes.includes(code) || summed.has(code)) continue
|
|
2942
|
+
const end = endOf(p)
|
|
2943
|
+
if (end > 0 && end <= now) continue
|
|
2944
|
+
const slice = String(p?.CapacityType) === SLICE ? p?.SlicePeriodUsageDetails?.[0] : null
|
|
2945
|
+
const of = (v) => { const n = Number(v); return Number.isFinite(n) && n > 0 ? n : 0 }
|
|
2946
|
+
const t = of(slice ? slice.SlicePeriodCapacitySizePrecise : p?.CycleCapacitySizePrecise)
|
|
2947
|
+
const l = Math.min(t, of(slice ? slice.SlicePeriodCapacityRemainPrecise : p?.CycleCapacityRemainPrecise))
|
|
2948
|
+
if (!(t > 0)) continue
|
|
2949
|
+
total += t
|
|
2950
|
+
left += l
|
|
2951
|
+
if (l > 0 && end > 0 && !slice) {
|
|
2952
|
+
if (!at || end < at) at = end, lapsing = l
|
|
2953
|
+
else if (end === at) lapsing += l
|
|
2954
|
+
}
|
|
2912
2955
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
return out
|
|
2956
|
+
if (!(total > 0)) return null
|
|
2957
|
+
return { total, used: total - left, ...(at ? { runsOutAt: new Date(at).toISOString(), runsOut: lapsing } : {}) }
|
|
2916
2958
|
}
|
|
2917
2959
|
|
|
2918
2960
|
// enterpriseUsageOf is an enterprise (team) account's credits, from
|
|
@@ -2980,6 +3022,16 @@ const PLAN_CODES = [CODE("002", "AkiJS3ZHF5"), CODE("005", "maRGyrHhw1"), CODE("
|
|
|
2980
3022
|
CODE("039", "KRcQj7wUat"), CODE("040", "mi9rCYg46x")]
|
|
2981
3023
|
const FREE_MON = CODE("008", "cfWoLwvjU4")
|
|
2982
3024
|
|
|
3025
|
+
// EXTRA_CODES are the kinds of extra credits WorkBuddy's own usage panel
|
|
3026
|
+
// shows beside the plan's base (WorkBuddy 5.5.6, the China build's
|
|
3027
|
+
// resolveCreditGroup): the plan's bonus (bonus28, bonusIntl), top-ups
|
|
3028
|
+
// (extra 009, extra38 038, extraIntl 036) and platform rewards (activity
|
|
3029
|
+
// 007, bonus29, bonus30), and on WorkBuddy AI the free month (008) and
|
|
3030
|
+
// gift (006) too. The China build puts gift in no group, so it shows none.
|
|
3031
|
+
const EXTRA_CODES = [CODE("028", "NtpWi0jzXs"), CODE("037", "WxOD3MpI2o"), CODE("009", "0XmEQc2xOf"), CODE("038", "OhvqZtiPKr"),
|
|
3032
|
+
CODE("036", "lupO5WgNdG"), CODE("007", "nzdH5h4Nl0"), CODE("029", "6wCGEWquYy"), CODE("030", "BjSt89qTvr")]
|
|
3033
|
+
const EXTRA_AI = [FREE_MON, CODE("006", "DbXS0lrypC")]
|
|
3034
|
+
|
|
2983
3035
|
// planCodes says whether a package of the summary is the plan's own: one
|
|
2984
3036
|
// of PLAN_CODES on that site, or the subscription the summary names.
|
|
2985
3037
|
function planCodes(sum, site) {
|
|
@@ -3816,6 +3868,18 @@ function makePlugin(site) {
|
|
|
3816
3868
|
Object.assign(stops, runsOutOf(sum, packages) ?? {})
|
|
3817
3869
|
const aside = extra ? (stops === credits ? extra : credits) : null
|
|
3818
3870
|
if (aside) Object.assign(aside, runsOutOf(sum, packages, Date.now(), (code) => ofPlan(code) === (aside === credits)) ?? {})
|
|
3871
|
+
// the summary of the plan's credits alone: the extra ones
|
|
3872
|
+
// only the detail lists have (listedExtra) are their window,
|
|
3873
|
+
// always set aside. Whether WorkBuddy spends them once the
|
|
3874
|
+
// plan's are gone isn't seen, so the plan's window stops the
|
|
3875
|
+
// account as it did before.
|
|
3876
|
+
const kinds = !extra && kindsOf(sum, site)
|
|
3877
|
+
const more = kinds && kinds.plan.total > 0 && kinds.extra.total === 0 ? listedExtra(sum, packages, site) : null
|
|
3878
|
+
if (more) {
|
|
3879
|
+
const { runsOutAt, runsOut, ...counts } = more
|
|
3880
|
+
const w = { ...creditWindow("Extra credits", counts), aside: true, ...(runsOutAt ? { runsOutAt, runsOut } : {}) }
|
|
3881
|
+
out.windows.splice(out.windows.indexOf(credits) + 1, 0, w)
|
|
3882
|
+
}
|
|
3819
3883
|
} catch {}
|
|
3820
3884
|
}
|
|
3821
3885
|
await spareFree(site, a, out.windows)
|
|
@@ -3973,4 +4037,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
3973
4037
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
3974
4038
|
|
|
3975
4039
|
// for tests
|
|
3976
|
-
export const _internal = { planCodes, idFor, OFF_TAKEN, replyAfterToolsOn, withDefaultEffort, SMALL_BUDGET, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unsaid, FILTER_SAYS, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, REFUSED_DOC, commaPaused, commaPausedOn, opened, 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, pinning, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_CN, FREE_WINDOW_AI, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
|
4040
|
+
export const _internal = { planCodes, listedExtra, idFor, OFF_TAKEN, replyAfterToolsOn, withDefaultEffort, SMALL_BUDGET, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unsaid, FILTER_SAYS, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, REFUSED_DOC, commaPaused, commaPausedOn, opened, 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, pinning, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_CN, FREE_WINDOW_AI, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
package/opencode.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The package's OpenCode 1.x plugins, as OpenCode 1.x and magpie's host
|
|
2
|
+
// load them: its plugin functions and nothing else (1.x refuses a
|
|
3
|
+
// package whose exports include anything but plugins). OpenCode 2.x
|
|
4
|
+
// loads opencode2.mjs, which runs these.
|
|
5
|
+
export { WorkBuddyAIAuthPlugin, WorkBuddyAuthPlugin } from "./index.mjs"
|
package/opencode2.mjs
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
// The OpenCode 2.x face of a package whose plugins are written for
|
|
2
|
+
// OpenCode 1.x (and magpie's plugin host).
|
|
3
|
+
//
|
|
4
|
+
// OpenCode 2.x loads a package's `exports["."]` and wants its default to be
|
|
5
|
+
// {id, setup(ctx)}; it has no fallback for a 1.x plugin's named functions
|
|
6
|
+
// (plugins#657). This file is that default. It runs each 1.x plugin the
|
|
7
|
+
// package's opencode.mjs exports and hands 2.x what the plugin's hooks say:
|
|
8
|
+
//
|
|
9
|
+
// auth.methods → the integration's sign-in methods
|
|
10
|
+
// config() → the provider, its package, base URL and models
|
|
11
|
+
// provider.models → the signed-in account's own model list
|
|
12
|
+
// auth.loader → each request's base URL, key, headers and fetch
|
|
13
|
+
//
|
|
14
|
+
// What 2.x has no place for is left out: chat.headers and chat.params
|
|
15
|
+
// (2.x's sdk hook doesn't carry the session), magpie's usage and checkin,
|
|
16
|
+
// tools and events.
|
|
17
|
+
//
|
|
18
|
+
// scripts/opencode2.mjs is the one copy that is edited. Every package that
|
|
19
|
+
// has it carries the same bytes as packages/<name>/opencode2.mjs, and
|
|
20
|
+
// scripts/check.mjs fails when a copy differs.
|
|
21
|
+
|
|
22
|
+
import { readFileSync } from "node:fs"
|
|
23
|
+
import * as v1 from "./opencode.mjs"
|
|
24
|
+
|
|
25
|
+
const pkg = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8"))
|
|
26
|
+
|
|
27
|
+
// The key the SDK is made with: the account's own goes on each request in
|
|
28
|
+
// its place (see wrapFetch), so a key read when the SDK was made can't go
|
|
29
|
+
// out after the account changed.
|
|
30
|
+
const PLACEHOLDER = "opencode2-v1-key"
|
|
31
|
+
|
|
32
|
+
// The SDK package a 1.x npm name runs as on 2.x. A version keeps 2.x from
|
|
33
|
+
// swapping @ai-sdk/* for its native providers, which never call the sdk
|
|
34
|
+
// hook below; openai-compatible's is the one 2.x ships, the others are AI
|
|
35
|
+
// SDK v6's dist-tag, the line 2.x's bridge runs.
|
|
36
|
+
export function packageOf(npm) {
|
|
37
|
+
const name = npm || "@ai-sdk/openai-compatible"
|
|
38
|
+
if (name === "@ai-sdk/openai-compatible") return "aisdk:@ai-sdk/openai-compatible@2.0.41"
|
|
39
|
+
if (/^@ai-sdk\/[^/@]+$/.test(name)) return `aisdk:${name}@ai-v6`
|
|
40
|
+
return `aisdk:${name}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The plugins a 1.x module exports, as magpie's host and OpenCode 1.x read
|
|
44
|
+
// them: a default {server} is the one plugin, else every function export
|
|
45
|
+
// and every {server} object.
|
|
46
|
+
export function pluginsOf(mod) {
|
|
47
|
+
const d = mod?.default
|
|
48
|
+
if (d && typeof d === "object" && ("server" in d || "id" in d || "tui" in d))
|
|
49
|
+
return typeof d.server === "function" ? [d.server] : []
|
|
50
|
+
const out = []
|
|
51
|
+
for (const value of Object.values(mod ?? {})) {
|
|
52
|
+
const fn = typeof value === "function" ? value : value && typeof value === "object" && typeof value.server === "function" ? value.server : null
|
|
53
|
+
if (fn && !out.includes(fn)) out.push(fn)
|
|
54
|
+
}
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const int = (n, fallback = 0) => {
|
|
59
|
+
const v = Math.trunc(Number(n))
|
|
60
|
+
return Number.isFinite(v) && v >= 0 ? v : fallback
|
|
61
|
+
}
|
|
62
|
+
const money = (n) => (Number.isFinite(Number(n)) && Number(n) >= 0 ? Number(n) : 0)
|
|
63
|
+
const record = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {})
|
|
64
|
+
const json = (v) => {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(JSON.stringify(v ?? null))
|
|
67
|
+
} catch {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const strings = (v) => Object.fromEntries(Object.entries(record(v)).filter(([, s]) => typeof s === "string"))
|
|
72
|
+
const nonEmpty = (o) => (Object.keys(o).length ? o : undefined)
|
|
73
|
+
|
|
74
|
+
// connKey names a connection the way 2.x does.
|
|
75
|
+
const connKey = (c) => (!c ? "" : c.type === "credential" ? `credential:${c.id}` : `env:${c.name}`)
|
|
76
|
+
|
|
77
|
+
// ---- sign-in ------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
// fields is a 1.x method's prompts as a 2.x form.
|
|
80
|
+
export function fieldsOf(prompts) {
|
|
81
|
+
const out = []
|
|
82
|
+
for (const p of prompts ?? []) {
|
|
83
|
+
if (!p?.key) continue
|
|
84
|
+
const f = { key: String(p.key), type: "string", title: String(p.message ?? p.key) }
|
|
85
|
+
if (p.placeholder) f.placeholder = String(p.placeholder)
|
|
86
|
+
if (p.type === "select" && Array.isArray(p.options))
|
|
87
|
+
f.options = p.options.map((o) => ({ value: String(o.value), label: String(o.label ?? o.value), ...(o.hint ? { description: String(o.hint) } : {}) }))
|
|
88
|
+
// a prompt shown only when another answer says so can't be required
|
|
89
|
+
if (!p.condition) f.required = true
|
|
90
|
+
out.push(f)
|
|
91
|
+
}
|
|
92
|
+
return out.length ? out : undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// v1Saved is what a 1.x sign-in's success is saved as, the way 1.x writes
|
|
96
|
+
// auth.json: the result without its type and provider.
|
|
97
|
+
function v1Saved(r) {
|
|
98
|
+
const { type: _t, provider: _p, ...rest } = r
|
|
99
|
+
if ("key" in r) return { type: "api", ...rest }
|
|
100
|
+
return { type: "oauth", ...rest }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// credentialOf is a 1.x sign-in's success as a 2.x credential: OAuth for
|
|
104
|
+
// both kinds, the 1.x auth kept whole in its metadata. No refresh is
|
|
105
|
+
// registered, so 2.x hands the credential over as it is and the plugin's
|
|
106
|
+
// own loader renews it, as it does on 1.x.
|
|
107
|
+
export function credentialOf(methodID, r) {
|
|
108
|
+
if (!r || r.type !== "success") throw new Error(r?.error || r?.message || "the sign-in failed")
|
|
109
|
+
const saved = json(v1Saved(r))
|
|
110
|
+
if (saved.type === "api")
|
|
111
|
+
return { type: "oauth", methodID, access: String(saved.key ?? ""), refresh: "", expires: 0, metadata: { v1: saved } }
|
|
112
|
+
return {
|
|
113
|
+
type: "oauth",
|
|
114
|
+
methodID,
|
|
115
|
+
access: String(saved.access ?? ""),
|
|
116
|
+
refresh: String(saved.refresh ?? ""),
|
|
117
|
+
expires: int(saved.expires),
|
|
118
|
+
metadata: { v1: saved },
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// authOfValue is the 1.x auth a 2.x credential stands for.
|
|
123
|
+
export function authOfValue(value) {
|
|
124
|
+
if (!value) return null
|
|
125
|
+
if (value.type === "oauth") {
|
|
126
|
+
const v1 = record(value.metadata).v1
|
|
127
|
+
if (v1 && typeof v1 === "object") return v1
|
|
128
|
+
return { type: "oauth", access: value.access, refresh: value.refresh, expires: value.expires }
|
|
129
|
+
}
|
|
130
|
+
if (value.type === "key") {
|
|
131
|
+
const meta = value.metadata && Object.keys(value.metadata).length ? { metadata: value.metadata } : {}
|
|
132
|
+
return { type: "api", key: value.key, ...meta }
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const fingerprint = (a) => JSON.stringify([a?.type, a?.access ?? a?.key ?? "", a?.refresh ?? ""])
|
|
138
|
+
|
|
139
|
+
// ---- models -------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
const MODALITIES = ["text", "audio", "image", "video", "pdf"]
|
|
142
|
+
|
|
143
|
+
// internalOf is a config model as 1.x hands it to provider.models: what
|
|
144
|
+
// fromModelsDevModel makes of it, with the config's own variants.
|
|
145
|
+
export function internalOf(providerID, conf, key, m) {
|
|
146
|
+
const mods = record(m.modalities)
|
|
147
|
+
const has = (list, k) => Array.isArray(list) && list.includes(k)
|
|
148
|
+
const cost = record(m.cost)
|
|
149
|
+
return {
|
|
150
|
+
...m,
|
|
151
|
+
id: key,
|
|
152
|
+
providerID,
|
|
153
|
+
name: m.name ?? key,
|
|
154
|
+
api: { id: m.id ?? key, url: m.provider?.api ?? conf.api ?? conf.options?.baseURL ?? "", npm: m.provider?.npm ?? conf.npm ?? "@ai-sdk/openai-compatible" },
|
|
155
|
+
status: m.status ?? "active",
|
|
156
|
+
headers: m.headers ?? {},
|
|
157
|
+
options: m.options ?? {},
|
|
158
|
+
cost: { input: money(cost.input), output: money(cost.output), cache: { read: money(cost.cache_read), write: money(cost.cache_write) } },
|
|
159
|
+
limit: { context: int(m.limit?.context), input: m.limit?.input, output: int(m.limit?.output) },
|
|
160
|
+
capabilities: {
|
|
161
|
+
temperature: m.temperature ?? false,
|
|
162
|
+
reasoning: m.reasoning ?? false,
|
|
163
|
+
attachment: m.attachment ?? false,
|
|
164
|
+
toolcall: m.tool_call ?? true,
|
|
165
|
+
input: Object.fromEntries(MODALITIES.map((k) => [k, mods.input ? has(mods.input, k) : k === "text"])),
|
|
166
|
+
output: Object.fromEntries(MODALITIES.map((k) => [k, mods.output ? has(mods.output, k) : k === "text"])),
|
|
167
|
+
interleaved: m.interleaved ?? false,
|
|
168
|
+
},
|
|
169
|
+
release_date: m.release_date ?? "",
|
|
170
|
+
variants: record(m.variants),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// modelOf is a 1.x model, in config or internal form, as a 2.x Model.Info.
|
|
175
|
+
export function modelOf(providerID, provider, key, m) {
|
|
176
|
+
const caps = record(m.capabilities)
|
|
177
|
+
const listFrom = (obj, list) => {
|
|
178
|
+
if (obj && typeof obj === "object") return MODALITIES.filter((k) => obj[k])
|
|
179
|
+
if (Array.isArray(list)) return list.map(String)
|
|
180
|
+
return ["text"]
|
|
181
|
+
}
|
|
182
|
+
const input = listFrom(caps.input, m.modalities?.input)
|
|
183
|
+
const output = listFrom(caps.output, m.modalities?.output)
|
|
184
|
+
const c = record(m.cost)
|
|
185
|
+
const cache = record(c.cache)
|
|
186
|
+
const npm = m.api?.npm ?? m.provider?.npm
|
|
187
|
+
const url = m.api?.url ?? m.provider?.api
|
|
188
|
+
const settings = { ...record(json(m.options)) }
|
|
189
|
+
if (url && url !== provider.baseURL) settings.baseURL = url
|
|
190
|
+
const variants = Object.entries(record(m.variants))
|
|
191
|
+
.filter(([, v]) => !record(v).disabled)
|
|
192
|
+
.map(([id, v]) => {
|
|
193
|
+
const { disabled: _d, ...rest } = record(json(v))
|
|
194
|
+
return nonEmpty(rest) ? { id, settings: rest } : { id }
|
|
195
|
+
})
|
|
196
|
+
const status = ["alpha", "beta", "deprecated", "active"].includes(m.status) ? m.status : "active"
|
|
197
|
+
const out = {
|
|
198
|
+
id: key,
|
|
199
|
+
modelID: String(m.api?.id ?? m.id ?? key),
|
|
200
|
+
providerID,
|
|
201
|
+
name: String(m.name ?? key),
|
|
202
|
+
capabilities: {
|
|
203
|
+
tools: caps.toolcall ?? m.tool_call ?? true,
|
|
204
|
+
input: input.length ? input : ["text"],
|
|
205
|
+
output: output.length ? output : ["text"],
|
|
206
|
+
...(typeof (caps.temperature ?? m.temperature) === "boolean" ? { temperature: caps.temperature ?? m.temperature } : {}),
|
|
207
|
+
},
|
|
208
|
+
variants,
|
|
209
|
+
time: { released: Date.parse(m.release_date ?? "") || 0 },
|
|
210
|
+
cost: [{ input: money(c.input), output: money(c.output), cache: { read: money(cache.read ?? c.cache_read), write: money(cache.write ?? c.cache_write) } }],
|
|
211
|
+
status,
|
|
212
|
+
enabled: true,
|
|
213
|
+
limit: { context: int(m.limit?.context, 128000) || 128000, output: int(m.limit?.output, 8192) || 8192 },
|
|
214
|
+
}
|
|
215
|
+
if (m.limit?.input) out.limit.input = int(m.limit.input)
|
|
216
|
+
if (npm && npm !== provider.npm) out.package = packageOf(npm)
|
|
217
|
+
if (nonEmpty(settings)) out.settings = settings
|
|
218
|
+
const headers = strings(m.headers)
|
|
219
|
+
if (nonEmpty(headers)) out.headers = headers
|
|
220
|
+
return out
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---- requests -----------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
const urlOf = (input) => (typeof input === "string" ? input : input instanceof URL ? input.href : input?.url ?? String(input))
|
|
226
|
+
|
|
227
|
+
// wrapFetch sends a request the SDK made from the provider's settings the
|
|
228
|
+
// way 1.x would have sent it with the loader's options: the loader's base
|
|
229
|
+
// URL in place of the configured one, the account's key in place of the
|
|
230
|
+
// placeholder, the loader's headers added, through the loader's fetch.
|
|
231
|
+
export function wrapFetch(next, base, options) {
|
|
232
|
+
return async (input, init = {}) => {
|
|
233
|
+
const o = await options()
|
|
234
|
+
let url = urlOf(input)
|
|
235
|
+
const from = String(base ?? "").replace(/\/+$/, "")
|
|
236
|
+
const to = o.baseURL ? String(o.baseURL).replace(/\/+$/, "") : ""
|
|
237
|
+
if (from && to && from !== to && url.startsWith(from)) url = to + url.slice(from.length)
|
|
238
|
+
const req = input instanceof Request ? input : null
|
|
239
|
+
const headers = new Headers(init.headers ?? req?.headers ?? undefined)
|
|
240
|
+
for (const [k, v] of [...headers.entries()]) {
|
|
241
|
+
if (!v.includes(PLACEHOLDER)) continue
|
|
242
|
+
if (o.key) headers.set(k, v.replaceAll(PLACEHOLDER, o.key))
|
|
243
|
+
else headers.delete(k)
|
|
244
|
+
}
|
|
245
|
+
for (const [k, v] of Object.entries(strings(o.headers))) headers.set(k, v)
|
|
246
|
+
const send = typeof o.fetch === "function" ? o.fetch : next ?? fetch
|
|
247
|
+
if (req && init.body === undefined) {
|
|
248
|
+
const body = req.method === "GET" || req.method === "HEAD" ? undefined : await req.text()
|
|
249
|
+
return send(url, { method: req.method, headers, body, signal: init.signal ?? req.signal })
|
|
250
|
+
}
|
|
251
|
+
return send(url, { ...init, headers })
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---- one plugin ---------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
async function setupPlugin(ctx, fn, live) {
|
|
258
|
+
const directory = ctx?.location?.directory ?? process.cwd()
|
|
259
|
+
// the active account each provider was last read for, and the renewed
|
|
260
|
+
// auth a plugin saved for it (2.x has no way for a plugin to write a
|
|
261
|
+
// credential back, so it is kept in the plugin's storage, under the
|
|
262
|
+
// credential it renews and the token it was renewed from)
|
|
263
|
+
const current = new Map()
|
|
264
|
+
const saved = new Map()
|
|
265
|
+
const signingIn = new Map()
|
|
266
|
+
const storeKey = (id, key) => `v1auth/${id}/${key}`
|
|
267
|
+
|
|
268
|
+
const client = {
|
|
269
|
+
auth: {
|
|
270
|
+
async set(arg) {
|
|
271
|
+
const id = arg?.path?.id
|
|
272
|
+
const body = json(arg?.body)
|
|
273
|
+
const cur = id && current.get(id)
|
|
274
|
+
// a sign-in's own saves belong to the credential it is about to
|
|
275
|
+
// make, not the one that was active when it started
|
|
276
|
+
if (!cur || !body || signingIn.get(id) > 0) return { data: true }
|
|
277
|
+
const entry = { fp: cur.fp, auth: body }
|
|
278
|
+
saved.set(storeKey(id, cur.key), entry)
|
|
279
|
+
await Promise.resolve()
|
|
280
|
+
.then(() => ctx.storage?.set?.(storeKey(id, cur.key), entry))
|
|
281
|
+
.catch(() => {})
|
|
282
|
+
return { data: true }
|
|
283
|
+
},
|
|
284
|
+
async get() {
|
|
285
|
+
return { data: undefined }
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
app: { log: async () => ({ data: true }) },
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const hooks = (await fn({ client, directory, worktree: directory, project: {}, serverUrl: undefined, $: undefined }, {})) ?? {}
|
|
292
|
+
const cfg = { provider: {} }
|
|
293
|
+
try {
|
|
294
|
+
await hooks.config?.(cfg)
|
|
295
|
+
} catch {}
|
|
296
|
+
const auth = hooks.auth
|
|
297
|
+
const authID = auth?.provider ? String(auth.provider) : undefined
|
|
298
|
+
const providers = record(cfg.provider)
|
|
299
|
+
|
|
300
|
+
// getAuth reads the active account: the credential 2.x holds, or what the
|
|
301
|
+
// plugin last saved renewing it.
|
|
302
|
+
const getAuth = async (id) => {
|
|
303
|
+
const connection = await ctx.integration.connection.active(id).catch(() => undefined)
|
|
304
|
+
if (!connection) {
|
|
305
|
+
current.delete(id)
|
|
306
|
+
return undefined
|
|
307
|
+
}
|
|
308
|
+
const value = await ctx.integration.connection.resolve(connection).catch(() => undefined)
|
|
309
|
+
const base = authOfValue(value)
|
|
310
|
+
if (!base) {
|
|
311
|
+
current.delete(id)
|
|
312
|
+
return undefined
|
|
313
|
+
}
|
|
314
|
+
const key = connKey(connection)
|
|
315
|
+
const fp = fingerprint(base)
|
|
316
|
+
// ours: a credential one of this plugin's own sign-ins made
|
|
317
|
+
const ours = value?.type === "oauth" && !!record(value.metadata).v1
|
|
318
|
+
current.set(id, { key, fp, ours })
|
|
319
|
+
const sk = storeKey(id, key)
|
|
320
|
+
let entry = saved.get(sk)
|
|
321
|
+
if (!entry) {
|
|
322
|
+
entry = await Promise.resolve()
|
|
323
|
+
.then(() => ctx.storage?.get?.(sk))
|
|
324
|
+
.catch(() => undefined)
|
|
325
|
+
if (entry) saved.set(sk, entry)
|
|
326
|
+
}
|
|
327
|
+
return entry && entry.fp === fp && entry.auth ? entry.auth : base
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// the sign-in methods
|
|
331
|
+
if (authID && Array.isArray(auth.methods) && auth.methods.length) {
|
|
332
|
+
const label = (c) => {
|
|
333
|
+
const a = authOfValue(c)
|
|
334
|
+
const name = a?.name ?? a?.email ?? a?.metadata?.name ?? a?.metadata?.email ?? a?.account
|
|
335
|
+
return typeof name === "string" && name ? name : undefined
|
|
336
|
+
}
|
|
337
|
+
const begin = () => signingIn.set(authID, (signingIn.get(authID) ?? 0) + 1)
|
|
338
|
+
const end = () => signingIn.set(authID, Math.max(0, (signingIn.get(authID) ?? 1) - 1))
|
|
339
|
+
const title = record(providers[authID]).name
|
|
340
|
+
// 2.x runs a transform again on every reload, from nothing
|
|
341
|
+
await ctx.integration.transform((editor) => {
|
|
342
|
+
let keyed = false
|
|
343
|
+
// the integration 2.x makes for an id it doesn't know is named by the
|
|
344
|
+
// id; the provider's own name reads better in the sign-in list
|
|
345
|
+
if (typeof title === "string" && title)
|
|
346
|
+
try {
|
|
347
|
+
editor.update?.(authID, (i) => {
|
|
348
|
+
if (i && (!i.name || i.name === authID)) i.name = title
|
|
349
|
+
})
|
|
350
|
+
} catch {}
|
|
351
|
+
auth.methods.forEach((m, i) => {
|
|
352
|
+
if (!m) return
|
|
353
|
+
const methodID = `v1-${i}`
|
|
354
|
+
const form = fieldsOf(m.prompts)
|
|
355
|
+
if (m.type === "api" && typeof m.authorize !== "function") {
|
|
356
|
+
if (keyed) return
|
|
357
|
+
keyed = true
|
|
358
|
+
editor.method.update({ integrationID: authID, method: { type: "key", label: String(m.label ?? "API key"), ...(form ? { form } : {}) } })
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
if (typeof m.authorize !== "function") return
|
|
362
|
+
editor.method.update({
|
|
363
|
+
integrationID: authID,
|
|
364
|
+
method: { id: methodID, type: "oauth", label: String(m.label ?? authID), ...(form ? { form } : {}) },
|
|
365
|
+
async authorize(answer) {
|
|
366
|
+
begin()
|
|
367
|
+
let ended = false
|
|
368
|
+
const done = () => {
|
|
369
|
+
if (!ended) end()
|
|
370
|
+
ended = true
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const inputs = Object.fromEntries(Object.entries(record(answer)).map(([k, v]) => [k, String(v)]))
|
|
374
|
+
const r = await m.authorize(inputs)
|
|
375
|
+
if (m.type === "api") {
|
|
376
|
+
done()
|
|
377
|
+
return { url: "", instructions: "", mode: "auto", callback: Promise.resolve().then(() => credentialOf(methodID, r)) }
|
|
378
|
+
}
|
|
379
|
+
if (r?.method === "code")
|
|
380
|
+
return {
|
|
381
|
+
url: String(r.url ?? ""),
|
|
382
|
+
instructions: String(r.instructions ?? ""),
|
|
383
|
+
mode: "code",
|
|
384
|
+
callback: async (code) => {
|
|
385
|
+
try {
|
|
386
|
+
return credentialOf(methodID, await r.callback(code))
|
|
387
|
+
} finally {
|
|
388
|
+
done()
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
const callback = Promise.resolve()
|
|
393
|
+
.then(() => r.callback())
|
|
394
|
+
.then((res) => credentialOf(methodID, res))
|
|
395
|
+
.finally(done)
|
|
396
|
+
callback.catch(() => {})
|
|
397
|
+
return { url: String(r?.url ?? ""), instructions: String(r?.instructions ?? ""), mode: "auto", callback }
|
|
398
|
+
} catch (e) {
|
|
399
|
+
done()
|
|
400
|
+
throw e
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
label,
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// the loader's options for the account signed in now, read again when
|
|
410
|
+
// the account changes
|
|
411
|
+
let loaded = null
|
|
412
|
+
const loaderOptions = async (id) => {
|
|
413
|
+
if (id !== authID || typeof auth?.loader !== "function") {
|
|
414
|
+
const a = authID === id ? await getAuth(id) : undefined
|
|
415
|
+
return { key: a?.type === "api" ? a.key : undefined }
|
|
416
|
+
}
|
|
417
|
+
const a = await getAuth(id)
|
|
418
|
+
if (!a) return {}
|
|
419
|
+
const fp = current.get(id)?.key + "\0" + fingerprint(a)
|
|
420
|
+
if (loaded?.fp !== fp) {
|
|
421
|
+
const p = Promise.resolve()
|
|
422
|
+
.then(() => auth.loader(() => getAuth(id), providerOf(id)))
|
|
423
|
+
.then((o) => record(o))
|
|
424
|
+
loaded = { fp, p }
|
|
425
|
+
p.catch(() => {
|
|
426
|
+
if (loaded?.fp === fp) loaded = null
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
const o = await loaded.p
|
|
430
|
+
const key = typeof o.apiKey === "string" && o.apiKey ? o.apiKey : a.type === "api" ? a.key : undefined
|
|
431
|
+
return { baseURL: o.baseURL, key, headers: o.headers, fetch: o.fetch }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// the providers, with the list the account's own read last gave each
|
|
435
|
+
const source = new Map()
|
|
436
|
+
// providerOf is a provider as 1.x hands it to provider.models and
|
|
437
|
+
// auth.loader
|
|
438
|
+
const providerOf = (id) => {
|
|
439
|
+
const conf = record(providers[id])
|
|
440
|
+
const models = Object.fromEntries(Object.entries(record(conf.models)).map(([k, m]) => [k, internalOf(id, conf, k, record(m))]))
|
|
441
|
+
return { id, name: conf.name ?? id, source: "config", env: [], options: record(conf.options), models }
|
|
442
|
+
}
|
|
443
|
+
const listed = new Set()
|
|
444
|
+
for (const [id, conf0] of Object.entries(providers)) {
|
|
445
|
+
const conf = record(conf0)
|
|
446
|
+
// a provider entry that only names the sign-in (no package, no models)
|
|
447
|
+
// is not a provider of models
|
|
448
|
+
if (!conf.npm && !Object.keys(record(conf.models)).length) continue
|
|
449
|
+
const options = record(conf.options)
|
|
450
|
+
const { apiKey: _k, baseURL: _b, fetch: _f, headers: optHeaders, ...rest } = options
|
|
451
|
+
// with no base URL the config leaves it to the loader: a name that
|
|
452
|
+
// can't resolve stands in for it, and each request swaps in the
|
|
453
|
+
// loader's (wrapFetch)
|
|
454
|
+
const baseURL = options.baseURL ?? conf.api ?? `https://${id.replace(/[^a-z0-9-]/gi, "-")}.opencode2-v1.invalid`
|
|
455
|
+
const npm = conf.npm ?? "@ai-sdk/openai-compatible"
|
|
456
|
+
const info = {
|
|
457
|
+
id,
|
|
458
|
+
...(id === authID ? { integrationID: id } : {}),
|
|
459
|
+
name: String(conf.name ?? id),
|
|
460
|
+
activation: "auto",
|
|
461
|
+
package: packageOf(npm),
|
|
462
|
+
settings: { ...record(json(rest)), ...(baseURL ? { baseURL: String(baseURL) } : {}) },
|
|
463
|
+
}
|
|
464
|
+
const headers = { ...strings(conf.headers), ...strings(optHeaders) }
|
|
465
|
+
if (nonEmpty(headers)) info.headers = headers
|
|
466
|
+
const prov = { baseURL, npm }
|
|
467
|
+
const fromConfig = () =>
|
|
468
|
+
Object.entries(record(conf.models)).map(([key, m]) => modelOf(id, prov, key, internalOf(id, conf, key, record(m))))
|
|
469
|
+
listed.add(id)
|
|
470
|
+
await ctx.provider.transform((editor) => {
|
|
471
|
+
const s = source.get(id)
|
|
472
|
+
editor.add({ info, models: s?.models ?? fromConfig(), sourceConnection: s?.connection })
|
|
473
|
+
})
|
|
474
|
+
source.set(id, { conf, prov, models: null, connection: undefined })
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// each request: the placeholder key, and the fetch that swaps in the
|
|
478
|
+
// account's. A provider the package doesn't list (one 2.x has built in
|
|
479
|
+
// that the plugin only signs in to) is left to 2.x unless the plugin's
|
|
480
|
+
// loader serves it and one of the plugin's own sign-ins is active.
|
|
481
|
+
const own = authID && !listed.has(authID) && typeof auth?.loader === "function" ? [authID] : []
|
|
482
|
+
for (const id of new Set([...listed, ...own])) {
|
|
483
|
+
const base = source.get(id)?.prov.baseURL
|
|
484
|
+
await ctx.aisdk.hook(
|
|
485
|
+
"sdk",
|
|
486
|
+
async (evt) => {
|
|
487
|
+
const opts = evt.options
|
|
488
|
+
if (!opts || opts.__opencode2) return
|
|
489
|
+
if (!listed.has(id)) {
|
|
490
|
+
await getAuth(id)
|
|
491
|
+
if (!current.get(id)?.ours) return
|
|
492
|
+
}
|
|
493
|
+
opts.__opencode2 = true
|
|
494
|
+
opts.apiKey = PLACEHOLDER
|
|
495
|
+
opts.fetch = wrapFetch(opts.fetch, opts.baseURL ?? base, () => loaderOptions(id))
|
|
496
|
+
},
|
|
497
|
+
{ providerID: id },
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// refresh reads the account's own list after a sign-in or a switch
|
|
502
|
+
const listID = hooks.provider?.id ? String(hooks.provider.id) : undefined
|
|
503
|
+
let refreshing = null
|
|
504
|
+
let again = false
|
|
505
|
+
// the account the list was last read for: a credential.updated that
|
|
506
|
+
// leaves it as it was reads nothing again
|
|
507
|
+
let readFor = null
|
|
508
|
+
const stateOf = async () => {
|
|
509
|
+
const c = await ctx.integration.connection.active(authID).catch(() => undefined)
|
|
510
|
+
const a = c ? await getAuth(authID) : undefined
|
|
511
|
+
return connKey(c) + "\0" + (a ? fingerprint(a) : "")
|
|
512
|
+
}
|
|
513
|
+
const refresh = () => {
|
|
514
|
+
if (refreshing) {
|
|
515
|
+
again = true
|
|
516
|
+
return refreshing
|
|
517
|
+
}
|
|
518
|
+
refreshing = (async () => {
|
|
519
|
+
try {
|
|
520
|
+
if (!authID || !source.has(authID)) return
|
|
521
|
+
const s = source.get(authID)
|
|
522
|
+
const connection = await ctx.integration.connection.active(authID).catch(() => undefined)
|
|
523
|
+
const a = connection ? await getAuth(authID) : undefined
|
|
524
|
+
readFor = connKey(connection) + "\0" + (a ? fingerprint(a) : "")
|
|
525
|
+
if (!a || listID !== authID || typeof hooks.provider?.models !== "function") {
|
|
526
|
+
if (s.connection || s.models) {
|
|
527
|
+
s.connection = undefined
|
|
528
|
+
s.models = null
|
|
529
|
+
await ctx.provider.reload()
|
|
530
|
+
}
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
const provider = providerOf(authID)
|
|
534
|
+
let got
|
|
535
|
+
try {
|
|
536
|
+
got = await hooks.provider.models(provider, { auth: a })
|
|
537
|
+
} catch {
|
|
538
|
+
got = null
|
|
539
|
+
}
|
|
540
|
+
if (connKey(await ctx.integration.connection.active(authID).catch(() => undefined)) !== connKey(connection)) {
|
|
541
|
+
again = true
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
s.connection = connection
|
|
545
|
+
s.models = got && typeof got === "object" ? Object.entries(got).map(([k, m]) => modelOf(authID, s.prov, k, record(m))) : null
|
|
546
|
+
await ctx.provider.reload()
|
|
547
|
+
await ctx.model?.reload?.()
|
|
548
|
+
} finally {
|
|
549
|
+
refreshing = null
|
|
550
|
+
if (again && live.on) {
|
|
551
|
+
again = false
|
|
552
|
+
void refresh()
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
})()
|
|
556
|
+
return refreshing
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (authID && ctx.event?.subscribe)
|
|
560
|
+
void (async () => {
|
|
561
|
+
try {
|
|
562
|
+
// a switch names the integration; an update (a sign-in, a removal)
|
|
563
|
+
// names nothing, so it reads again only when this account changed
|
|
564
|
+
const types = ["credential.switched", "credential.updated"]
|
|
565
|
+
for await (const event of ctx.event.subscribe({ types })) {
|
|
566
|
+
if (!live.on) return
|
|
567
|
+
if (event?.type === "credential.switched" && event?.data?.integrationID === authID) {
|
|
568
|
+
loaded = null
|
|
569
|
+
await refresh()
|
|
570
|
+
} else if (event?.type === "credential.updated" && (await stateOf()) !== readFor) {
|
|
571
|
+
await refresh()
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
} catch {}
|
|
575
|
+
})()
|
|
576
|
+
void refresh()
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export async function setup(ctx, mod = v1) {
|
|
580
|
+
const live = { on: true }
|
|
581
|
+
for (const fn of pluginsOf(mod)) {
|
|
582
|
+
try {
|
|
583
|
+
await setupPlugin(ctx, fn, live)
|
|
584
|
+
} catch (e) {
|
|
585
|
+
// one plugin failing to start leaves the package's others working
|
|
586
|
+
console.error(`${pkg.name}: ${e?.message ?? e}`)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return () => {
|
|
590
|
+
live.on = false
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export default { id: pkg.name, setup: (ctx) => setup(ctx) }
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-workbuddy-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
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
|
-
"main": "./
|
|
7
|
-
"exports": "./
|
|
6
|
+
"main": "./opencode.mjs",
|
|
7
|
+
"exports": { ".": "./opencode2.mjs" },
|
|
8
8
|
"files": [
|
|
9
9
|
"index.mjs",
|
|
10
|
+
"opencode.mjs",
|
|
11
|
+
"opencode2.mjs",
|
|
10
12
|
"README.md"
|
|
11
13
|
],
|
|
12
14
|
"keywords": [
|