362 lines
9.8 KiB
Go
362 lines
9.8 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/yetone/magpie/internal/plugin"
|
|
)
|
|
|
|
// A plugin's provider is signed in to as many times as the user likes, as
|
|
// a built-in subscription is: plugin-auth.json keeps each account's
|
|
// sign-in (under the provider's id, then id#slot), logins.json which is
|
|
// first and which are on, as it does for the built-ins, the account's key
|
|
// in its Home.
|
|
|
|
// pluginAgent is what logins.json lists a plugin provider's accounts as.
|
|
func pluginAgent(pp plugin.Provider) string { return "plugin:" + pp.ID }
|
|
|
|
// pluginLabels names each account: its id, else the end of its key, else
|
|
// what it signed in with; a name two share gets the account's slot.
|
|
func pluginLabels(pp plugin.Provider) map[string]string {
|
|
out := map[string]string{}
|
|
seen := map[string]int{}
|
|
for _, a := range pp.Accounts {
|
|
l := a.AccountID
|
|
if l == "" && a.Hint != "" {
|
|
l = "API key …" + a.Hint
|
|
}
|
|
if l == "" {
|
|
l = map[string]string{"api": "API key", "oauth": "Signed in"}[a.Type]
|
|
}
|
|
if l == "" {
|
|
l = "Signed in"
|
|
}
|
|
out[a.Key] = l
|
|
seen[strings.ToLower(l)]++
|
|
}
|
|
for _, a := range pp.Accounts {
|
|
if seen[strings.ToLower(out[a.Key])] > 1 {
|
|
if _, slot, ok := strings.Cut(a.Key, "#"); ok {
|
|
out[a.Key] += " (" + slot + ")"
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type pluginLogin struct {
|
|
sideLogin
|
|
acct plugin.Account
|
|
}
|
|
|
|
// pluginOwn says a plugin sign-in is the agent's own (its CLI's, its
|
|
// app's), which the plugin reads where the agent keeps it. The movers mark
|
|
// it so (migrate_side.go, migrate_kiro.go, migrate_workbuddy.go,
|
|
// migrate_zcode.go), as the plugins' own "CLI's sign-in" ways do.
|
|
func pluginOwn(id string, auth map[string]any) bool {
|
|
md, _ := auth["metadata"].(map[string]any)
|
|
switch id {
|
|
case "devin", CommandCodePlanID:
|
|
return md["cli"] == true
|
|
case "grok":
|
|
return samePath(str(auth["refresh"]), GrokHome())
|
|
case "cursor":
|
|
return str(auth["refresh"]) == cursorCLIMark
|
|
case "kiro", "workbuddy", WorkBuddyAIID:
|
|
return str(auth["source"]) != ""
|
|
case "zcode":
|
|
var s struct{ Source string }
|
|
_ = json.Unmarshal([]byte(str(auth["refresh"])), &s)
|
|
return s.Source == "zcode"
|
|
}
|
|
return false
|
|
}
|
|
|
|
// pluginOwnUser is who the agent itself is signed in to now, "" when it
|
|
// can't be told: the own account is named after it, as the built-in's
|
|
// was, so the name follows a switch made in the agent.
|
|
var pluginOwnUser = func(id string) string {
|
|
switch id {
|
|
case "devin":
|
|
if u, _, ok := devinIdentity(); ok {
|
|
return u
|
|
}
|
|
case "grok":
|
|
if c, ok := readGrokCredential(GrokHome()); ok {
|
|
return c.Email
|
|
}
|
|
case CommandCodePlanID:
|
|
if u, _, ok := cmdOwn(); ok {
|
|
return u
|
|
}
|
|
case "cursor":
|
|
if CursorExecutable() != "" && !cursorSignedOut() {
|
|
if u, _, ok := cursorIdentity(); ok {
|
|
return u
|
|
}
|
|
}
|
|
case "kiro":
|
|
return kiroOwnUser()
|
|
case "zcode":
|
|
if u, _, ok := zcodeOwn(); ok {
|
|
return u
|
|
}
|
|
case "workbuddy", WorkBuddyAIID:
|
|
if u, _, ok := wbOwn(wbSiteOf(id)); ok {
|
|
return u
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// pluginLogins are the provider's accounts, the first in use first. What
|
|
// plugin-auth.json keeps is the truth: logins.json follows it. The agent's
|
|
// own account is named as the agent is signed in now; removed in magpie,
|
|
// it is only hidden, and shows again once the agent signs in anew.
|
|
func pluginLogins(pp plugin.Provider) []pluginLogin {
|
|
agent := pluginAgent(pp)
|
|
labels := pluginLabels(pp)
|
|
byKey := map[string]plugin.Account{}
|
|
for _, a := range pp.Accounts {
|
|
byKey[a.Key] = a
|
|
}
|
|
own := map[string]bool{}
|
|
for k, a := range plugin.Auths(pp.ID) {
|
|
if _, ok := byKey[k]; ok && pluginOwn(pp.ID, a) {
|
|
own[k] = true
|
|
}
|
|
}
|
|
mark := ""
|
|
if len(own) > 0 {
|
|
if u := pluginOwnUser(pp.ID); u != "" {
|
|
for k := range own {
|
|
labels[k] = u
|
|
}
|
|
}
|
|
mark = ownMark(pp.ID)
|
|
}
|
|
loginsMu.Lock()
|
|
ls := readLogins()
|
|
changed := false
|
|
have := map[string]bool{}
|
|
keep := ls[:0]
|
|
for _, l := range ls {
|
|
if l.Agent == agent {
|
|
if _, ok := byKey[l.Home]; !ok || have[l.Home] {
|
|
changed = true // signed out, or listed twice
|
|
continue
|
|
}
|
|
have[l.Home] = true
|
|
renamed := l.User != labels[l.Home]
|
|
if renamed {
|
|
l.User, changed = labels[l.Home], true
|
|
}
|
|
// the agent's own, hidden, shows again once the agent signs
|
|
// in anew: told by its sign-in's mark, else by another account
|
|
if l.Hidden != "" && own[l.Home] && (mark != "" && mark != l.Hidden || mark == "" && l.Hidden == hiddenNoMark && renamed) {
|
|
l.Hidden, changed = "", true
|
|
}
|
|
}
|
|
keep = append(keep, l)
|
|
}
|
|
ls = keep
|
|
for _, a := range pp.Accounts {
|
|
if !have[a.Key] {
|
|
ls = append(ls, savedLogin{Agent: agent, User: labels[a.Key], Home: a.Key, On: true, Seen: time.Now().UTC().Truncate(time.Second)})
|
|
changed = true
|
|
}
|
|
}
|
|
if changed {
|
|
_ = writeLogins(ls)
|
|
}
|
|
loginsMu.Unlock()
|
|
var out []pluginLogin
|
|
for _, l := range sideLogins(agent, "", func(l savedLogin) bool { _, ok := byKey[l.Home]; return ok && l.Hidden == "" }) {
|
|
l.Lapsed = l.saved.Lapsed // a refused sign-in shows on the account
|
|
l.Own = own[l.saved.Home]
|
|
out = append(out, pluginLogin{l, byKey[l.saved.Home]})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func pluginSide(pp plugin.Provider) []sideLogin {
|
|
var out []sideLogin
|
|
for _, l := range pluginLogins(pp) {
|
|
out = append(out, l.sideLogin)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// pluginOfAgent is the plugin provider the accounts page names by its
|
|
// magpie id.
|
|
func pluginOfAgent(agent string) (plugin.Provider, bool) {
|
|
if strings.HasPrefix(agent, "plugin:") {
|
|
id := strings.TrimPrefix(agent, "plugin:")
|
|
for _, pp := range plugin.Cached() {
|
|
if pp.ID == id {
|
|
return pp, true
|
|
}
|
|
}
|
|
return plugin.Provider{}, false
|
|
}
|
|
return PluginOf(agent)
|
|
}
|
|
|
|
func pluginLoginList(pp plugin.Provider) []Login { return loginsOf(pluginSide(pp)) }
|
|
|
|
func switchPluginLogin(pp plugin.Provider, user string) error {
|
|
return switchSideLogin(pluginAgent(pp), user, pluginSide(pp))
|
|
}
|
|
|
|
func setPluginLoginOn(pp plugin.Provider, user string, on bool) error {
|
|
return setSideLoginOn(pluginAgent(pp), user, on, pluginSide(pp))
|
|
}
|
|
|
|
// forgetPluginLogin signs the account out: its sign-in is magpie's own.
|
|
// The agent's own is only hidden, as the built-in hid it: the agent stays
|
|
// signed in, and the account shows again once it signs in anew. Qoder's
|
|
// first goes as the built-in's did, the next put first.
|
|
func forgetPluginLogin(pp plugin.Provider, user string) error {
|
|
agent := pluginAgent(pp)
|
|
ls := pluginSide(pp)
|
|
i := slices.IndexFunc(ls, func(l sideLogin) bool { return strings.EqualFold(l.User, user) })
|
|
if i < 0 {
|
|
return fmt.Errorf("no %s account %q", pp.Name, user)
|
|
}
|
|
own, first := ls[i].Own, ls[i].Active
|
|
if !own && !(first && firstGoes(pp.ID)) {
|
|
var err error
|
|
ferr := forgetSideLogin(agent, user, ls, func(l savedLogin) { err = signOutPlugin(pp, l.Home) })
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
return err
|
|
}
|
|
next := ""
|
|
if first {
|
|
for _, l := range ls {
|
|
if !strings.EqualFold(l.User, user) {
|
|
next = l.User
|
|
break
|
|
}
|
|
}
|
|
}
|
|
key := ls[i].saved.Home
|
|
err := editSideLogin(agent, user, func(saved []savedLogin, j int) ([]savedLogin, error) {
|
|
for k := range saved {
|
|
if saved[k].Agent == agent && (first || k == j) {
|
|
saved[k].First = next != "" && strings.EqualFold(saved[k].User, next)
|
|
}
|
|
}
|
|
if own {
|
|
saved[j].Hidden = firstNonEmpty(ownMark(pp.ID), hiddenNoMark)
|
|
return saved, nil
|
|
}
|
|
return append(saved[:j], saved[j+1:]...), nil
|
|
})
|
|
if err != nil || own {
|
|
return err
|
|
}
|
|
return signOutPlugin(pp, key)
|
|
}
|
|
|
|
// firstGoes says the account in use first can be removed, the next taking
|
|
// its place, as the built-in let it (Qoder's).
|
|
func firstGoes(id string) bool { return id == "qoder" || id == QoderCNID }
|
|
|
|
func signOutPlugin(pp plugin.Provider, key string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
return plugin.SignOut(ctx, pp.ID, key)
|
|
}
|
|
|
|
// pluginAlsoOn is the provider's accounts in use behind the first.
|
|
func pluginAlsoOn(pp plugin.Provider) []Provider {
|
|
var out []Provider
|
|
for _, l := range pluginLogins(pp) {
|
|
if !l.Active && l.On {
|
|
out = append(out, pluginProvider(pp, l))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// keepPluginPlan keeps the plan an account's allowance told on its row,
|
|
// for the accounts list to show it as a built-in's.
|
|
func keepPluginPlan(pp plugin.Provider, key, plan string) {
|
|
if plan == "" {
|
|
return
|
|
}
|
|
loginsMu.Lock()
|
|
defer loginsMu.Unlock()
|
|
ls := readLogins()
|
|
for i, l := range ls {
|
|
if l.Agent == pluginAgent(pp) && l.Home == key {
|
|
if l.Plan != plan {
|
|
ls[i].Plan = plan
|
|
_ = writeLogins(ls)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// notePluginLapse marks the account at key lapsed when its vendor refused
|
|
// a request (401), as a built-in's refused sign-in is marked, and clears
|
|
// the mark once one goes through.
|
|
func notePluginLapse(pp plugin.Provider, key string, status int) {
|
|
refused := status == http.StatusUnauthorized
|
|
if !refused && (status < 200 || status > 299) {
|
|
return
|
|
}
|
|
loginsMu.Lock()
|
|
defer loginsMu.Unlock()
|
|
ls := readLogins()
|
|
for i, l := range ls {
|
|
if l.Agent == pluginAgent(pp) && l.Home == key {
|
|
want := ""
|
|
if refused {
|
|
want = lapsedText(pp, l.User)
|
|
}
|
|
if l.Lapsed != want {
|
|
ls[i].Lapsed = want
|
|
_ = writeLogins(ls)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// lapsedText is what a plugin account the vendor refused says.
|
|
func lapsedText(pp plugin.Provider, user string) string {
|
|
name, _ := pluginCard(pp)
|
|
return user + "'s " + name + " sign-in has expired — sign in again"
|
|
}
|
|
|
|
// clearPluginLapse takes the mark off an account signed in again, and
|
|
// brings back the agent's own account removed in magpie, as signing in to
|
|
// the built-in's did (#320).
|
|
func clearPluginLapse(saved plugin.Saved) {
|
|
for _, pp := range plugin.Cached() {
|
|
if pp.ID == saved.Provider {
|
|
notePluginLapse(pp, saved.Account, http.StatusOK)
|
|
}
|
|
}
|
|
loginsMu.Lock()
|
|
defer loginsMu.Unlock()
|
|
ls := readLogins()
|
|
for i, l := range ls {
|
|
if l.Agent == "plugin:"+saved.Provider && l.Home == saved.Account && l.Hidden != "" {
|
|
ls[i].Hidden = ""
|
|
_ = writeLogins(ls)
|
|
return
|
|
}
|
|
}
|
|
}
|