setup

package
v0.4.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package setup connects logos to the agents that will use it.

The engine has never been the hard part of adopting this. The hard part is the nine steps between cloning the repo and an agent actually answering from your vault — install a runtime, pull models, choose a vault, index it, find the right config file for your host, and hand-write JSON full of absolute paths. Every one of those is a place to give up, and none of them is the part worth having.

So this package does the wiring. It finds the MCP hosts installed on the machine and registers logos with each one, preferring the host's own registration command where there is one and merging its config file where there is not.

Why the host's CLI comes first

Claude Code and Codex both ship a command for this. Using it means their config format stays their problem: when they change it, their command changes with it and logos keeps working. Hand-writing another application's config is a standing bet that its format will not move, and that bet is only worth taking when there is no alternative — which is the case for Claude Desktop and Cursor — or when the CLI would register at the wrong scope or cannot be exercised by a test, which is why Cline, Devin and GitHub Copilot are merged.

Where a file does have to be written, it is read, merged, backed up and then replaced atomically. Someone's other MCP servers are not ours to lose.

Index

Constants

View Source
const (
	PluginSource = "Coder8124/logos"
	PluginRef    = "logos@logos"
)

PluginSource is the marketplace `claude plugin marketplace add` takes, and PluginRef names the plugin inside it.

View Source
const AutoUpdateBackoff = 7 * 24 * time.Hour

AutoUpdateBackoff is how long an update that changed nothing is believed for. `claude plugin update` exits 0 with nothing to install, so a plugin the marketplace cannot move would otherwise reach the network every day for the rest of the install's life. A week still picks up a late release without making the failure a daily habit.

View Source
const AutoUpdateEvery = 24 * time.Hour

AutoUpdateEvery is how long a check is good for. A day: the plugin is not released more often than that, and a check that reached the network every session would be a network call in the one place that has to stay instant.

View Source
const Name = "logos"

Name is what logos calls itself in a host's server list.

View Source
const OldName = "brain"

OldName is what 0.4 setup registered the server as. Setup replaces such an entry through 0.4.x; this goes before 0.5.0.

Variables

This section is empty.

Functions

func AutoUpdateDisabled added in v0.4.5

func AutoUpdateDisabled() bool

AutoUpdateDisabled reports whether this machine has turned self-updating off.

Two ways in, because the two populations asking for it are different. A person who does not want their editor's plugins moving under them runs `logos plugin autoupdate off` once; someone rolling Logos out across machines they administer sets LOGOS_PLUGIN_AUTOUPDATE=off in the environment and needs it to hold without a per-machine command. The environment wins either way, including over a stored "on", so a managed setting cannot be locally undone.

func AutoUpdatePlugin added in v0.4.4

func AutoUpdatePlugin(version string, now time.Time) (updated bool, err error)

AutoUpdatePlugin updates the Logos plugin if it is behind version and has not been checked since now-AutoUpdateEvery. It reports whether it ran an update.

Everything that is not "the plugin is installed, connected, and older than this binary" is a reason to do nothing rather than to fail: a machine with no plugin, a disabled one, or a dev build with no release number to rank is not a machine with a problem to fix. The error is returned only when an update was attempted and the update itself failed — and that is recorded too, so the next session says so out loud rather than retrying in silence forever.

func InstallPluginSteps added in v0.4.4

func InstallPluginSteps() [][]string

InstallPluginSteps are the commands that install the Logos plugin: the marketplace has to be known before the plugin in it can be installed.

func LogosPlugin

func LogosPlugin() (version string, connects bool)

LogosPlugin reports whether the Logos plugin connects Claude Code, and at which version. The plugin carries its own MCP server, so registering logos with `claude mcp add` on top of it lists every tool twice and pays the per-session cost twice.

func Names

func Names(hosts []Host) []string

Names lists every host logos knows how to wire, for error messages.

func OnOtherVault added in v0.4.4

func OnOtherVault(hosts []Host, dir string) (names, vaults []string)

OnOtherVault names each detected host with a logos entry pinned to a vault other than dir. Setup records one vault for the whole machine but wires only the hosts it was asked to, so the rest can be left writing somewhere the CLI, the desktop app and the plugin's hooks no longer read.

func PinnedVault added in v0.4.5

func PinnedVault(hosts []Host, name string) string

PinnedVault returns the LOGOS_VAULT a named host has wired onto its logos server, or "" when there is none to read.

#95: setup writes the vault twice — into each host's config as LOGOS_VAULT, and once for the machine as the recorded pointer. A tool call arrives through the host and gets the pin; the plugin's hooks run with a bare environment and get the pointer. When those two disagree the same session restores from one vault and checkpoints into another, each side silently consistent with itself, and the user reports that their work has stopped being saved. The pin is the more specific answer for anything running inside that host, so this is how such a caller asks for it instead of re-resolving.

Only a logos entry counts. Other MCP servers set environment of their own, and taking one for the vault would point logos at somebody else's directory. An unreadable host, an unknown name and a config with no environment in it are all "" — this is a question a host may simply not be able to answer, which is not a failure.

The answer comes off the config file, never from running the host's own listing command. `claude mcp list` prints name, command and status and no environment, so it could not answer this anyway — and it is a Node process that connects to every server it lists. Asking it here, before argument dispatch, on a binary the plugin's per-tool-call hooks run, meant a logos started by the plugin started a claude that started a logos.

func PluginCommand added in v0.4.4

func PluginCommand(step []string) string

PluginCommand renders one step the way a user would type it, for a dry run.

func PluginConnectionSkipped added in v0.4.5

func PluginConnectionSkipped(now time.Time) (time.Duration, bool)

PluginConnectionSkipped reports how much of that window is left. Claude Code records a plugin server's failed start in its own cache and then skips that server in every session started for the next fifteen minutes, saying so in a line most people never see. Without this, the machine simply has no Logos and doctor's other checks all pass: the plugin is installed, enabled, current.

func RemoveServerEntry added in v0.4.4

func RemoveServerEntry() error

RemoveServerEntry removes a user-scope MCP entry an earlier setup wrote in Claude Code. The plugin carries the same server, so the entry left behind would load logos twice.

func RenderConfig

func RenderConfig(s Server, format string) (string, error)

RenderConfig renders the server block a host's config needs, in the requested format, for printing rather than writing. It exists for the same reason MergeFile does: a host logos does not know how to find can still be wired, by hand, if the user can see what a working entry looks like. An empty format means json — the shape most MCP hosts actually use, and the one this package already writes for Claude Desktop and Cursor.

func RunPluginSteps added in v0.4.4

func RunPluginSteps(steps [][]string) error

RunPluginSteps runs the steps in order and stops at the first failure, naming the command that failed and what it printed: a half-done install reported as success is how someone ends up with a marketplace and no plugin.

func SetAutoUpdate added in v0.4.5

func SetAutoUpdate(on bool) error

SetAutoUpdate records this machine's answer, under the lock, so it does not land on top of a check writing its outcome at the same moment.

func SupportsPluginCommands added in v0.4.4

func SupportsPluginCommands() bool

SupportsPluginCommands says whether the Claude Code on this machine can install plugins from the command line. `claude plugin` arrived in 2.1; on an older one setup still has to fall back to `claude mcp add` and printed instructions.

func UninstallPluginSteps added in v0.4.5

func UninstallPluginSteps() [][]string

UninstallPluginSteps take the plugin back out. The way in is automated and the way out was a printed instruction, which is homework handed to someone at the worst possible moment — and a plugin left behind keeps starting its own MCP server, running its session-start hooks and updating itself daily after a run that reported success.

func UpdateNotice added in v0.4.4

func UpdateNotice() string

UpdateNotice is the line the next session says, and says once — an empty string when there is nothing to announce. Reading it marks it announced, so a second session does not repeat a week-old update as though it just happened.

func UpdatePluginSteps added in v0.4.4

func UpdatePluginSteps() [][]string

UpdatePluginSteps refresh a plugin older than this binary. Claude Code does not refresh a third-party marketplace on its own, so updating the plugin without updating the marketplace first finds nothing newer.

Types

type Host

type Host struct {
	Name string
	// Detect reports whether this host is installed. Presence-based: a command
	// on PATH, or a config directory that exists. A host nobody has installed
	// is skipped, never failed — "not installed" is not an error.
	Detect func() bool
	// Where is the config path or command shown in the report.
	Where func() string
	// Config is the file this host's registration rewrites, so it can be
	// backed up first. Empty means the file does not exist yet or logos does
	// not know where it lives — a host registered through its own CLI still
	// rewrites a file, and knowing which one is the only way back from a wrong
	// --vault.
	Config func() string
	// Register points the host at this server.
	Register func(Server) (Outcome, error)
	// List reports every MCP server this host currently has registered, read
	// back rather than assumed. Nil means this host exposes no way to read
	// that back — not an error, just a question this host cannot answer.
	List func() ([]Registration, error)
	// Hooks installs this host's user-level session-start hook, so a session
	// starts with the last checkpoint instead of waiting to be asked. Nil for a
	// host with no hooks, and for Claude Code, whose plugin carries its own.
	Hooks func(s Server) (Outcome, error)
	// Remove removes the entry registered under name when it runs logos, and
	// reports whether there was one. Setup uses it for OldName, uninstall for
	// both names. Nil for a host logos cannot take an entry back out of.
	Remove func(name string) (bool, error)
}

A Host is one application that can talk to an MCP server.

func Detected added in v0.4.5

func Detected(hosts []Host) []Host

Detected keeps the hosts that are actually present, for a message that has to say what is here rather than what logos knows about.

func Hosts

func Hosts() []Host

Hosts returns every MCP host logos knows how to connect to, in the order they are reported.

Each entry is a promise to keep working as somebody else's application changes, so a host is added when people ask for it, not speculatively. Aider is not here because it has no MCP client to register with; `logos setup --print-config` is the answer for a client that is not on the list.

func Only

func Only(hosts []Host, names []string) (kept []Host, unmatched []string)

Only keeps the hosts the user named, matched case-insensitively on a prefix so `--host claude-code`, `--host "Claude Code"` and `--host codex` all work. An unmatched name is returned so the caller can say so rather than silently wiring nothing.

A prefix several hosts share keeps all of them, which is what `--host cline` plainly means with four Clines on the list. It used to keep the first and break, so which Cline you got was decided by the order of Hosts() and nothing said the others existed. An exact name still wins outright: `--host cline` on a machine that has Cline itself is about Cline, not also about Cline in Cursor.

type Outcome

type Outcome string

Outcome says what happened to one host.

const (
	// Registered means the host now points at this logos.
	Registered Outcome = "registered"
	// Updated means it already had a logos entry and it was replaced.
	Updated Outcome = "updated"
	// Unchanged means the host was already pointed at exactly this logos, so
	// the registration rewrote its config with the bytes already in it. Setup
	// is run more than once — after moving a vault, after an update, or just to
	// check — and calling that "updated" reports work that did not happen.
	Unchanged Outcome = "already connected"
	// Skipped means the host is not installed here.
	Skipped Outcome = "not installed"
	// Failed means it is installed and something went wrong.
	Failed Outcome = "failed"
	// Pending means the host is installed and would be wired. Only Plan produces
	// it — it is what the user is being asked to approve.
	Pending Outcome = "would wire"
)

func MergeFile

func MergeFile(path string, s Server) (Outcome, error)

MergeFile is mergeJSON, exported for `logos setup --config <path>` — a host whose location on disk logos has no convention for, but whose file is the same mcpServers-keyed JSON that Claude Desktop and Cursor already read. The internal hosts in Hosts() are a closed, curated list on purpose (see the package doc); this is the escape hatch for every MCP client that is not on it, so that "not one of the four we special-case" does not mean "logos cannot help you connect this".

type PluginRecord added in v0.4.4

type PluginRecord struct {
	Version   string
	Installed bool // some logos@ install is recorded
	Connects  bool // it gives Claude Code Logos in every project
	Why       string
}

PluginRecord is what Claude Code's own files say about the Logos plugin.

func LogosPluginRecord added in v0.4.4

func LogosPluginRecord() PluginRecord

LogosPluginRecord reads the plugin's install record. Claude Code records installed plugins in installed_plugins.json, keyed "<plugin>@<marketplace>", and the on/off switch separately in settings.json's enabledPlugins.

Any logos@ key used to count as connected, so a plugin turned off in /plugin, or installed for a single project, made setup skip Claude Code and leave it with no Logos anywhere else. Only an install at user (or managed) scope that the user settings do not disable counts. A disable in one project's settings cannot be seen from here, which Why says.

type Registration

type Registration struct {
	Name    string
	Command string
	// Vault is the LOGOS_VAULT the entry sets, empty when the host's listing
	// does not show environment (`claude mcp list` does not) or none is set.
	Vault string
}

Registration is one server as a host currently reports it — the name it was given and the command line the host will actually run.

type Removal added in v0.4.4

type Removal struct {
	Host  string
	Where string
	// Removed names the entries taken out: logos, and brain if 0.4 left one.
	Removed []string
	// Backup is the config as it was before, kept only when it changed.
	Backup string
	// Unhooked is set when this host's session-start hook was taken out too. A
	// hook left behind runs a logos that uninstall just removed, and the host
	// reports the failure at the user on every session.
	Unhooked bool
	Err      error
}

Removal says what uninstall took out of one host.

func Uninstall added in v0.4.4

func Uninstall(hosts []Host) []Removal

Uninstall takes every logos entry back out of each host that is present. Only entries that run `mcp serve` go, under either name, so someone else's server called brain survives, and a host failing does not stop the others.

type Result

type Result struct {
	Host    string
	Outcome Outcome
	Where   string // config path or command, for the report
	// Backup is the copy of this host's config taken before it was changed,
	// empty when there was nothing to copy. Reported, because a backup nobody
	// is told about is a backup nobody uses.
	Backup string
	// CommentsOnlyInBackup is set when the original config had comments.
	// Writing it back as JSON drops them, and the backup is the one place
	// they still exist.
	CommentsOnlyInBackup bool
	Err                  error
	// Tier is how much of Logos this host ends up with, and Hooked whether its
	// session-start hook went in. HookErr does not fail the host: the server is
	// registered either way, and a hook that could not be written is reported
	// rather than swallowed.
	Tier    Tier
	Hooked  Outcome
	HookErr error
	// Replaced is set when the entry 0.4 setup wrote under OldName was
	// removed, and ReplaceErr when that was tried and failed. Neither fails the
	// host: logos is registered either way, and the leftover is reported.
	Replaced   bool
	ReplaceErr error
}

A Result is one host's line in the report.

func Install

func Install(s Server, hosts []Host) []Result

Install registers the server with every host that is present.

One host failing does not stop the others: a user with a broken Cursor config still wants Claude wired up, and the report tells them which is which.

func Plan

func Plan(hosts []Host) []Result

Plan reports what Install would do, without doing any of it.

Registering logos with every AI tool on someone's machine is a large action for someone evaluating one integration, and it used to happen with no gate at all — not even --yes. Showing the list first costs one function and turns an imposition into a choice.

It does not distinguish "would register" from "would update": finding that out means reading a config the user has not yet agreed to have touched, and for the CLI-based hosts it means asking their command, which is not free. Pending is the honest answer to "what will happen here".

type Server

type Server struct {
	// Bin is the absolute path to the logos binary. Absolute because a host
	// launches it from a working directory nobody chose.
	Bin  string
	Args []string
	// Env is what the host must set. LOGOS_VAULT belongs here and must be
	// absolute for the same reason Bin is.
	Env map[string]string
}

A Server is the command a host should run to reach this logos.

type Tier added in v0.4.4

type Tier string

Tier names how much of Logos a host actually gets. Registering the MCP server is the floor: it gives a model tools it can choose to call. What makes continuity happen without anyone remembering to ask for it is a session-start hook, and only some hosts have one — Claude Code through the plugin, Cursor and Codex through a user-level hooks file. The tier is printed per host because "connected" meant three different things and setup said the same word for all of them.

const (
	TierPlugin Tier = "plugin — restores on its own"
	TierHooks  Tier = "MCP + session hooks"
	TierMCP    Tier = "MCP only — resume by asking"
)

func TierOf added in v0.4.4

func TierOf(h Host) Tier

TierOf reports the richest integration this host supports.

type UpdateRecord added in v0.4.4

type UpdateRecord struct {
	Checked   time.Time `json:"checked"`
	From      string    `json:"from,omitempty"`
	To        string    `json:"to,omitempty"`
	Stayed    string    `json:"stayed,omitempty"`
	Failed    string    `json:"failed,omitempty"`
	Announced bool      `json:"announced,omitempty"`
	Off       bool      `json:"off,omitempty"`
}

An UpdateRecord is the outcome of the last check. Announced is what keeps the announcement to one session: the notice is read once and then marked. Stayed is set when an update ran and moved nothing: it holds the version this binary wanted, with From still the version the plugin is on. That pair used to be written as a completed update. Off is the user having said no. It lives in the stamp rather than in the vault for the same reason the rest of the record does: which plugin this machine's Claude Code runs, and whether this machine is allowed to move it, is a fact about the machine — a vault carried to a second machine must not silently disable updates there too.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL