Files
magpie/internal/agent/commandcode.go
T
yetone dd70a4055a wsl: OpenCode, Kimi Code, omp and the other CLIs magpie sets up through a file in the home are set up in WSL distros too (#515: on Windows only Codex, Pi, OmO and Claude Code were found in a distro) — fourteen agents join wslKinds, each found by its folder under the distro's $HOME or its command on the distro's own PATH, and listed as <name> · WSL <distro> (opencode@wsl:Ubuntu…): OpenCode (~/.config/opencode, opencode), MiMo Code (~/.config/mimocode, mimo), Kimi Code (~/.kimi-code, kimi; the old kimi-cli's ~/.kimi where that is all there is, found by the command), omp (~/.omp/agent, omp), Crush (~/.config/crush/crush.json, Linux's place, not Windows' LOCALAPPDATA), Hermes Agent (~/.hermes), Grok Build (~/.grok, by its folder alone, as on this machine), Droid (~/.factory), fx (~/.fx, by its folder alone: fx is also the JSON viewer), Command Code (~/.commandcode), MiniMax Code's mcode (~/.minimax), Muse Code (~/.config/muse), Qoder and Qoder CN (~/.qoder, ~/.qoder-cn). Each is its host constructor at a place (opencodeIn, kimiIn, ompIn, crushIn, hermesIn, grokIn, droidIn, fxIn, commandCodeIn, miniMaxIn, museIn, qoderIn/qoderCNIn; the host's opencode(home, cfg) and the rest wrap them), so it writes the same keys and schema as on this machine, but the gateway as the distro reaches it (place.gw/v1: 127.0.0.1 when mirrored, Windows' address under NAT) in its provider, its Check and its Sync, and its folder at the default, as the distro's variables (OPENCODE_CONFIG_DIR, KIMI_CODE_HOME, HERMES_HOME, FACTORY_HOME_OVERRIDE, MINIMAX_DATA_DIR, QODER_CONFIG_DIR…) aren't read (place.getenv is "" there); OpenChamber, a Windows app, isn't looked at for a distro's OpenCode, and omp there, whose version isn't known, gets xhigh rather than a max an older omp refuses. Their notices are the restart advice of their own, as for Codex. A stopped distro's agent is built without looking at its files (place.cold: exists/isDir are false, so the defaults are taken), its options read none (wslOwnAsleep, kimiOwnOptions/ompOwnOptions/miniMaxOwnOptions without a file), and a pick, once the distro is started, builds the agent again warm, so it writes the file the agent reads — an opencode.jsonc already there, ~/.kimi for kimi-cli — not the default. Left out: Gemini CLI and Antigravity (a key lent through .env, a launch command), dsh (profiles and .env), Devin (no gateway; its model list from the devin command on this machine), Goose (no magpie routing; its name is pressly's goose too), Cursor and Copilot (no magpie routing), and the apps (Cline, Hanako, WorkBuddy, ZCode, Claude Desktop, OpenChamber, Alma, Cindy); and the library's skills, MCP and instructions for the new WSL agents, which wslTargetOf still gives only Claude Code, Codex, Pi and OmO. Nothing ran on Windows or in a real WSL. wsl_more_test.go, on fake distro roots under temp dirs with the host's variables pointed elsewhere: TestWSLProbeFindsMoreAgents (each folder and command in the probe, none for grok and fx, kimi-cli by its command), TestWSLMoreAgentsDiscovered (all fourteen listed, no command, UA or aliases), TestWSLMoreAgentsPick (each agent, mirrored and NAT: its file in the distro names the distro's gateway, no 127.0.0.1 under NAT, Check passes, Sync keeps it, the notice; this machine's home and the folders its variables name untouched), TestWSLMoreAgentsStopped (nothing asked nor opened, the last value shown, options, a pick starts the distro and writes there) fail without the change; TestWSLStoppedPickFindsItsFiles fails without the warm rebuild (the pick wrote a second opencode.json beside the .jsonc). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet ./internal/..., GOOS=windows go test -c ./internal/agent/ ./internal/library/, and go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup race, passes on the rerun; untouched).
2026-10-02 14:16:38 +08:00

192 lines
6.1 KiB
Go

package agent
// Command Code (commandcode.ai) keeps its settings in ~/.commandcode/
// settings.json, the model as "provider/model", and takes providers of the
// user's own in ~/.commandcode/providers.json. magpie adds itself there as
// the provider "magpie", keyless, with the catalog as its models; a model
// through magpie is "magpie/<provider>/<model>", which Command Code splits
// at the first "/". Command Code still wants its own sign-in for these.
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"github.com/yetone/magpie/internal/edit"
)
// ccEfforts are the reasoning levels Command Code knows.
var ccEfforts = []string{"low", "medium", "high", "xhigh", "max"}
func commandCode(home string) *Agent { return commandCodeIn(here(home)) }
// commandCodeIn is Command Code at a place: this machine's home, or a WSL
// distro's (see wsl.go), its provider naming the gateway as the distro
// reaches it.
func commandCodeIn(at place) *Agent {
ccProviderJSON := func() any { return ccProviderJSONAt(at.v1()) }
dir := filepath.Join(at.home, ".commandcode")
path := filepath.Join(dir, "settings.json")
providers := filepath.Join(dir, "providers.json")
get := func(k string) string { v, _ := edit.GetJSON(path, k); return v }
dropMagpie := func() error {
if _, err := os.Stat(providers); err != nil {
return nil
}
return edit.DelJSON(providers, "provider."+magpieID)
}
return &Agent{
ID: "commandcode", Name: "Command Code", Icon: "commandcode", Aliases: []string{"command-code", "cmd"},
UA: []string{"command-code", "commandcode"},
Bin: "command-code", Dir: dir, Path: path,
Sync: func() error {
return syncJSON(providers, "provider."+magpieID, func() any { return ccProviderJSON() })
},
Notice: func() string {
notes := []string{"Command Code wants its own sign-in (cmd login) even for models through magpie."}
if Running(`(^|/)(cmd|cmdc|command-code|commandcode)( |$)`) {
notes = append(notes, "It reads its settings at start-up — restart open Command Code sessions to use this.")
}
return strings.Join(notes, " ")
},
Check: func() string {
if !usesMagpie(get("model")) {
return ""
}
if p := get("modelProvider"); p != magpieID {
return "Command Code's modelProvider (settings.json) is " + orDefault(p) + ", so it no longer asks magpie"
}
return wiringOff("Command Code", providers, func(k string) (string, bool) { return edit.GetJSON(providers, "provider."+magpieID+"."+k) },
"baseURL", at.v1())
},
Fields: []Field{{
Key: "model", Label: "model",
Get: func() string { return get("model") },
Set: func(v string) error {
if v == "" {
keys := []string{"model"}
if get("modelProvider") == magpieID {
keys = append(keys, "modelProvider")
}
if err := edit.DelJSON(path, keys...); err != nil {
return err
}
return dropMagpie()
}
if ref, ok := strings.CutPrefix(v, magpieID+"/"); ok && isMagpie(ref) {
if err := edit.SetJSON(providers, edit.KV{Path: "provider." + magpieID, Value: ccProviderJSON()}); err != nil {
return err
}
return edit.SetJSON(path, edit.KV{Path: "model", Value: v}, edit.KV{Path: "modelProvider", Value: magpieID})
}
if get("modelProvider") == magpieID {
if err := edit.DelJSON(path, "modelProvider"); err != nil {
return err
}
}
if err := edit.SetJSON(path, edit.KV{Path: "model", Value: v}); err != nil {
return err
}
return dropMagpie()
},
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("commandcode", magpieID+"/")...)
},
}, {
// settings.json's reasoningEffort keeps an effort for each model
// (Command Code's /effort saves it); this is the current model's
Key: "effort", Label: "effort",
Get: func() string { return ccEffortMap(path)[get("model")] },
Set: func(v string) error {
model := get("model")
if model == "" {
return fmt.Errorf("pick Command Code's model first; it keeps an effort for each model")
}
efforts := ccEffortMap(path)
if v == "" {
delete(efforts, model)
} else {
efforts[model] = v
}
if len(efforts) == 0 {
return edit.DelJSON(path, "reasoningEffort")
}
// written whole, as model ids hold dots
return edit.SetJSON(path, edit.KV{Path: "reasoningEffort", Value: efforts})
},
Options: func(cur map[string]string) []Option {
if ref, ok := strings.CutPrefix(cur["model"], magpieID+"/"); ok {
for _, m := range magpieModels("commandcode") {
if m.ID != ref {
continue
}
var efforts []string
for _, x := range ccEfforts {
if slices.Contains(m.Efforts, x) {
efforts = append(efforts, x)
}
}
if len(efforts) > 0 {
return static(efforts...)
}
}
}
return static(ccEfforts...)
},
}},
}
}
// ccProviderJSON is magpie's entry in providers.json. The key is false:
// the gateway takes any, and Command Code refuses one written out.
func ccProviderJSON() any { return ccProviderJSONAt(gatewayV1()) }
// ccProviderJSONAt is ccProviderJSON for a Command Code reaching the
// gateway's /v1 at v1.
func ccProviderJSONAt(v1 string) any {
ms := map[string]any{}
for _, m := range magpieModels("commandcode") {
e := map[string]any{"name": m.Name}
var efforts []string
for _, x := range m.Efforts {
if slices.Contains(ccEfforts, x) {
efforts = append(efforts, x)
}
}
if len(efforts) > 0 {
e["reasoning"] = true
e["reasoningEfforts"] = efforts
}
ms[m.ID] = e
}
return map[string]any{"name": "magpie", "api": "openai-completions", "baseURL": v1, "apiKey": false, "models": ms}
}
// ccEffortMap is settings.json's reasoningEffort, model id to effort.
func ccEffortMap(path string) map[string]string {
out := map[string]string{}
b, err := os.ReadFile(path)
if err != nil {
return out
}
var c struct {
Efforts map[string]any `json:"reasoningEffort"`
}
if json.Unmarshal(b, &c) != nil {
return out
}
for k, v := range c.Efforts {
if s, ok := v.(string); ok {
out[k] = s
}
}
return out
}