Files
yetone 6f65ffb5ff agent: CodeBuddy Code, Cline, Gemini CLI, AtomCode, Goose and Reasonix installed in a WSL distro are found and wired there (lgtm on Discord: magpie 现在无法检测到安装在wsl中的codebuddy呀?)
wslKinds had none of these CLIs, so each was set up only on Windows.
Each now has a place-aware constructor (codebuddyIn, clineIn, geminiIn,
atomcodeIn, gooseIn, reasonixIn). It writes the distro's own config with
the gateway as the distro reaches it (mirrored or NAT) and the key it
takes from there. A distro reads none of Windows' variables
(CODEBUDDY_CONFIG_DIR, CLINE_DIR, APPDATA...). Kinds are found by their
folder or their command. Gemini CLI is found by its command alone,
because Antigravity keeps ~/.gemini too. Goose is found by its folder
alone, because a goose command can be pressly's. Reasonix also needs a
native version (new wslKind.accept). Gemini CLI also counts a .env that
names an old address with magpie's key as magpie's own. Before this, a
WSL restart that moved the distro's address made it read "custom", and
picking the model again stashed magpie's old address as the user's.

Verified:
- On the Windows test box (WSL2 magpie-desktop, NAT), with a sandboxed
  nogui magpie.exe (own USERPROFILE/APPDATA, port 3441), real CodeBuddy
  Code 2.161.4, Gemini CLI 0.63.0 and Cline CLI 3.0.68 were installed in
  the distro. `magpie ls` listed each as "· WSL magpie-desktop".
  Picking relay/glm-4.6 wrote ~/.codebuddy/models.json, ~/.gemini/.env +
  settings.json and ~/.cline/data/settings/providers.json in the distro
  at http://172.17.32.1:3441, and wrote nothing in the sandbox home's
  agent folders. The installs were removed afterwards.
- Not tried for real: AtomCode, Goose and Reasonix in a distro, and a
  chat request from a WSL CLI through the gateway.
- Tests: gofmt; go vet ./...; -race on internal/agent; the full
  `go test -tags nogui ./...`; GOOS=windows and GOOS=linux builds.
  TestWSLProbeFindsMoreAgents and the rest of TestWSLMore* panic with
  "no WSL agent codebuddy" on the old wsl.go.
  TestWSLGeminiAddressMoved fails ("auth custom") without the gemini
  routed change.
2026-10-07 05:37:27 +08:00

100 lines
3.5 KiB
Go

package agent
// CodeBuddy Code (Tencent's CLI, npm @tencent-ai/codebuddy-code: codebuddy,
// cbc) is one package for the international edition (codebuddy.ai) and the
// China one (codebuddy.cn, CODEBUDDY_INTERNET_ENVIRONMENT=internal). Both
// take models of one's own from ~/.codebuddy/models.json ($CODEBUDDY_CONFIG_DIR
// if set), in WorkBuddy's format (workbuddy.go):
//
// {"models":[{"id":"deepseek/pro","name":…,"vendor":"magpie",
// "apiKey":"magpie-codebuddy","url":"http://127.0.0.1:3425/v1/chat/completions",
// "maxInputTokens":…,"maxOutputTokens":…,"supportsToolCall":true,…}],
// "availableModels":[…]}
//
// and pick up a change to it without a restart. Such a model needs no
// CodeBuddy sign-in: the CLI asks url for it under Bearer apiKey. The model
// a session starts on is settings.json's "model" (an id from that list, or
// one of CodeBuddy's own), read when a session starts. Its requests say
// "CLI/<ver> CodeBuddy/<ver>", so they are told apart by their key.
import (
"encoding/json"
"path/filepath"
"slices"
"strings"
"github.com/yetone/magpie/internal/edit"
)
func codebuddy(home string) *Agent { return codebuddyIn(here(home)) }
// codebuddyIn is CodeBuddy Code at a place: this machine's home, or a WSL
// distro's (see wsl.go), where CODEBUDDY_CONFIG_DIR isn't read and its
// models name the gateway as the distro reaches it, with the key it takes
// from there.
func codebuddyIn(at place) *Agent {
dir := at.getenv("CODEBUDDY_CONFIG_DIR")
if dir == "" {
dir = filepath.Join(at.home, ".codebuddy")
}
path := filepath.Join(dir, "models.json")
write := func(on bool) error { return buddyWriteAt(path, "codebuddy", on, at) }
settings := filepath.Join(dir, "settings.json")
model := func() string { v, _ := edit.GetJSON(settings, "model"); return v }
// ours is whether id is one of magpie's models in models.json
ours := func(id string) bool {
d, _ := workbuddyRead(path)
return slices.ContainsFunc(d.models, func(raw json.RawMessage) bool { e, ok := workbuddyMine(raw); return ok && e.ID == id })
}
return &Agent{
ID: "codebuddy", Name: "CodeBuddy Code", Icon: "codebuddy-color", Aliases: []string{"codebuddy-code", "cbc"},
UA: []string{"codebuddy"}, Spelled: prefixed,
Bin: "codebuddy", Dir: dir, Path: path,
Sync: func() error {
if !workbuddyWired(path) {
return nil
}
return write(true)
},
Notice: func() string {
if Running(`(^|/)(codebuddy|cbc|codebuddy-code)( |$)`) {
return "CodeBuddy Code reads its model when a session starts — restart open codebuddy sessions, or run /clear in them, to use this."
}
return ""
},
Fields: []Field{{
Key: "model", Label: "model",
Get: func() string {
m := model()
if m != "" && ours(m) {
return magpieID + "/" + m
}
return m
},
Set: func(v string) error {
if ref, ok := strings.CutPrefix(v, magpieID+"/"); ok && isMagpie(ref) {
if err := write(true); err != nil {
return err
}
return edit.SetJSON(settings, edit.KV{Path: "model", Value: ref})
}
// CodeBuddy's own: magpie's models come out of its list
if err := write(false); err != nil {
return err
}
if v == "" {
return edit.DelJSON(settings, "model")
}
return edit.SetJSON(settings, edit.KV{Path: "model", Value: v})
},
Options: func(cur map[string]string) []Option {
var out []Option
if v := cur["model"]; v != "" && !usesMagpie(v) {
out = append(out, Option{Value: v, Icon: modelIcon("", v)})
}
return append(out, viaMagpie("codebuddy", magpieID+"/")...)
},
}},
}
}