server

package
v0.20.5 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 41 Imported by: 0

Documentation

Overview

Package server wires up musterd's HTTP+WS surface. server.go is the composition root only (docs/conventions.md § Composition roots): it builds Config, registers each feature once, and owns the core routes/lifecycle — every other endpoint is a feature method.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ClaudeCodeInfo

type ClaudeCodeInfo struct {
	Installed *string
	Floor     string
	Verified  string
	Status    string
}

ClaudeCodeInfo is the daemon's startup snapshot of the installed Claude Code against the canary-verified range, used to build the WS `hello` message (kb:anchor/ws.hello) and the issue-capture snapshot. Installed is nil iff Status is "unknown" (the startup version check failed, hung past its timeout, or was unparseable); Floor/Verified are always populated. A Config field (server.go, not ws.go): main computes it once, at startup, before New exists.

type ClaudeThemeInfo

type ClaudeThemeInfo struct {
	Family string `json:"family"`
}

ClaudeThemeInfo is the `claudeTheme` object inside a snapshot (kb:anchor/ws.snapshot / kb:anchor/ws.claude-theme) — the daemon's latest read of Claude Code's own theme family. Family is always present: "unknown" while polling is disabled (-claude-theme-poll 0) or no read has yet succeeded.

type Config

type Config struct {
	Store       *store.Store
	Logger      zerolog.Logger
	UIToken     string
	IngestToken string
	// WebDist, non-empty, serves the dashboard from disk instead of the embedded copy.
	WebDist         string
	DaemonVersion   string
	ClaudeCode      ClaudeCodeInfo
	IngestQueueSize int
	TmuxSocket      string
	// HTTPClient is shared by every feature with outbound HTTP calls (usage, issue,
	// update); nil defaults to http.DefaultClient, resolved once by New (like TmuxClient
	// and Attach below) since all three features need the same default.
	HTTPClient *http.Client
	// TmuxClient overrides the tmux client sessionLauncher/shellRegistry use; nil constructs tmux.New(cfg.TmuxSocket).
	TmuxClient paneSpawner
	// Attach overrides how a terminal socket attaches to a tmux target; nil uses termbridge.Attach.
	Attach attachFunc
	// ShellScroll overrides the shell socket's copy-mode driver (kb:anchor/terminal.shell-ws); nil
	// uses the real tmux client (the same one TmuxClient's override does not affect —
	// production always wants real tmux copy-mode commands regardless of a paneSpawner
	// test double).
	ShellScroll shellScroller
	// Locator resolves a dropped file's path (kb:anchor/sessions.locate); nil answers 500, never a panic.
	Locator *locate.Locator
	// InterruptChecker is handed to the session manager's interrupt sweep
	// (session.Config.InterruptChecker); nil disables it. cmd/musterd wires the Claude Code
	// transcript reader, so internal/server stays free of transcript knowledge.
	InterruptChecker session.InterruptChecker
	Launch           LaunchConfig
	Usage            UsageConfig
	Theme            ThemeConfig
	Issue            IssueConfig
	Update           UpdateConfig
	RepoRefresh      RepoRefreshConfig
}

Config wires everything a Server needs, built entirely in main (no init() magic, no package-level state). Core fields are shared by more than one feature or by the root itself; everything else groups into one sub-struct per feature, declared in that feature's own file.

type IssueConfig added in v0.12.2

type IssueConfig struct {
	// Repo is the GitHub repo issues are filed against.
	Repo string
	// APIURL is the GitHub API's base URL. main always passes the flag's non-empty
	// default, so this is the only place that URL is defined — no fallback constant
	// duplicates it here. Empty disables both new endpoints entirely: they 404
	// not_found (kb:anchor/issue.captures / kb:anchor/issue.create).
	APIURL string
	// TokenFile, when non-empty, reads the bearer token from this file's trimmed
	// contents instead of running `gh auth token` — a test seam that makes it
	// structurally impossible for a test using it to execute the real gh.
	TokenFile string
}

IssueConfig groups the file-an-issue button's config. Empty APIURL disables both endpoints entirely (mirrors UsageConfig.APIURL's shape): a zero-value Config must never reach the real GitHub API host or execute `gh`.

type LaunchConfig added in v0.12.2

type LaunchConfig struct {
	// ClaudeBin is the `claude` binary to spawn (default "claude").
	ClaudeBin string
	// HookScript/StatusLineScript are the absolute paths to the generated command-hook
	// wrapper scripts (internal/claudecode.WriteWrapperScripts) — every event, including
	// SessionStart, is registered against HookScript.
	HookScript       string
	StatusLineScript string
	// LegacyScripts lists prior wrapper paths MergeSettings must still recognise and
	// drop from an already-instrumented directory.
	LegacyScripts []string
	// BrowseRoot is the folder browser's root (kb:anchor/browse.get): GET /api/browse's
	// no-param default and the "Up" ceiling. Empty means the daemon user's home
	// directory.
	BrowseRoot string
	// ProjectsDir is Claude Code's transcript store (claudecode.ProjectsDir's default, or
	// -claude-projects-dir): the resume-from-list branch's and GET /api/past-sessions'
	// one read root, read-only.
	ProjectsDir string
}

LaunchConfig groups the launch path's config: the claude binary, the generated wrapper script paths, and the folder browser's root — every field sessionLauncher/browseFeature need, declared here since launcher.go is the launch service's home file.

type PrefsInfo

type PrefsInfo struct {
	View         string `json:"view"`
	Density      string `json:"density"`
	UsageModel   string `json:"usageModel"`
	RailSort     string `json:"railSort"`
	Theme        string `json:"theme"`
	UpdateCheck  bool   `json:"updateCheck"`
	RailDensity  string `json:"railDensity"`
	RailActivity string `json:"railActivity"`
}

PrefsInfo is the `prefs` object inside a snapshot (kb:anchor/prefs.put / kb:anchor/ws.snapshot), persisted in kv and broadcast on change. Density is the Tiles grid density. UsageModel is which modelScoped entry the masthead's third readout shows, default "Fable". RailSort is the rail's sort mode, "manual" | "attention", default "manual". Theme is opaque to the daemon beyond its pattern (kb:anchor/prefs.put), default "follow". UpdateCheck is whether the daemon checks GitHub Releases for a newer musterd, default true. RailDensity and RailActivity are the rail/Tiles-strip card density ("compact" | "comfortable" | "expanded", default "comfortable") and which text a card's activity line shows ("turn" | "prompt" | "reply" | "both", default "turn").

type RepoRefreshConfig added in v0.19.4

type RepoRefreshConfig struct {
	// Poll is the poll interval (-repo-poll). <= 0 runs no timer: the poll then runs once at
	// start and on a nudge only (kb:adr/lifecycle-branch-refreshed-by-repo-poll).
	Poll time.Duration
}

RepoRefreshConfig groups the repo poll's config.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server holds the HTTP mux and long-lived pieces, plus one field per feature.

func New

func New(cfg Config) *Server

New builds a Server and wires its routes. Nothing here starts a goroutine; call Start once the caller is ready to begin processing. Its length is one construction-plus-register line per feature (16 features) plus the four Config overrides below whose default is another root-built object (spawner, attach, httpClient, shellScroll) — every one of those four is resolved here, in this one place, and nowhere else (kb:adr/process-size-linters-warn-never-fail: the funlen warning this trips has that recorded reason; no split asked).

func (*Server) EndAllSessions

func (s *Server) EndAllSessions(ctx context.Context) int

EndAllSessions ends every alive session (the `-on-exit=kill` path), ahead of Shutdown. Returns how many were ended.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the root http.Handler to serve.

func (*Server) KillAllShells added in v0.14.1

func (s *Server) KillAllShells(ctx context.Context) (int, error)

KillAllShells kills every shell tmux session on the socket — the `-on-exit=kill` companion to EndAllSessions, run after it (sessions may hold the socket busy), per kb:adr/surfaces-shell-dies-at-kill-shutdown-too.

func (*Server) LiveSessionCount

func (s *Server) LiveSessionCount() int

LiveSessionCount reports how many sessions are alive — cmd/musterd's `-on-exit=ask` uses it to decide whether to prompt at all.

func (*Server) RestartRequests added in v0.12.0

func (s *Server) RestartRequests() <-chan struct{}

RestartRequests reports each in-place re-exec an apply requests (kb:anchor/update.apply). A disabled update feature yields a nil channel, so a select never fires on it.

func (*Server) ShellCount added in v0.14.1

func (s *Server) ShellCount(ctx context.Context) (int, error)

ShellCount counts every shell tmux session on the socket — the on-exit prompt's count, per kb:adr/surfaces-shell-dies-at-kill-shutdown-too.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context)

Shutdown closes every open WS connection (unblocking hijacked-connection goroutines http.Server.Shutdown cannot reach) and terminal socket, stops the liveness poll, then stops every lifecycle feature in Start's registration order.

func (*Server) Start

func (s *Server) Start()

Start reloads and reconciles sessions, then starts every lifecycle feature in registration order. Call once, after New — Reconcile runs synchronously here so neither `/ws` nor `GET /api/state` can observe a pre-reconcile session list.

func (*Server) StopLivenessPoll added in v0.14.1

func (s *Server) StopLivenessPoll(ctx context.Context)

StopLivenessPoll stops the session manager's background liveness poll and the repo poll that reads the same rows, and waits for them to exit, without touching any other feature. Call this first, before any other shutdown step (ShellCount's tmux round trip, the on-exit prompt, EndAllSessions/KillAllShells): those steps run for up to several seconds after the shutdown signal arrives, and the poll's own 5s ticker is not otherwise paused during that window. A pane that dies right then would otherwise race its own liveness tick against the fresh process's next-startup Reconcile — the still-shutting-down process marking+persisting alive:false before the row is ever handed to Reconcile makes Reconcile sweep it instead of keeping it ended (kb:adr/lifecycle-reconcile-converges-with-the-socket only keeps a row that was still alive:true at Reconcile time). Deliberately narrower than Shutdown, which also closes WS/terminal connections and stops every other feature that EndAllSessions/KillAllShells still need; Shutdown stops the poll again itself (idempotent) for callers, such as the auto-update restart path, that go straight to it without this earlier call.

func (*Server) TmuxSocket

func (s *Server) TmuxSocket() string

TmuxSocket returns the dedicated tmux socket this daemon instance is bound to.

type Snapshot

type Snapshot struct {
	Sessions    []sessionWire   `json:"sessions"`
	Usage       UsageInfo       `json:"usage"`
	Prefs       PrefsInfo       `json:"prefs"`
	ClaudeTheme ClaudeThemeInfo `json:"claudeTheme"`
	// Update (kb:anchor/ws.update): always present, even on a daemon with updates disabled
	// entirely.
	Update UpdateInfo `json:"update"`
	// ShellsBusy (kb:anchor/ws.shell-activity): session ids whose shell is busy right now,
	// always present as [] when none — never null — so a reconnecting client re-syncs
	// without waiting for a shellActivity transition.
	ShellsBusy []int64 `json:"shellsBusy"`
	// Groups and Ungrouped (kb:anchor/ws.groups): the rail's sections, always present — Groups
	// as [] when there are none.
	Groups    []groupWire   `json:"groups"`
	Ungrouped ungroupedWire `json:"ungrouped"`
}

Snapshot is the daemon's full state, shared verbatim (aside from the WS "type" wrapper) between GET /api/state and the WS `snapshot` message (kb:anchor/ws.snapshot) — one function builds it so the two can never drift apart.

type ThemeConfig added in v0.12.2

type ThemeConfig struct {
	// Poll is the poll interval. <= 0 disables polling entirely: snapshot.claudeTheme.family
	// stays "unknown" forever.
	Poll time.Duration
	// ConfigFile is Claude Code's global config file to poll
	// (claudecode.DefaultConfigPath() in production) — a test seam like UsageConfig.TokenFile.
	ConfigFile string
}

ThemeConfig groups the Claude-theme poller's config. Poll <= 0 means the poller is never constructed at all (mirrors UsageConfig.Poll's shape) — a zero-value Config never opens ConfigFile.

type UpdateApplyInfo added in v0.12.0

type UpdateApplyInfo struct {
	Phase   string  `json:"phase"`
	Version *string `json:"version"`
	Error   *string `json:"error"`
}

UpdateApplyInfo is the `update.apply` object (kb:anchor/ws.update). Error is non-nil iff Phase is "failed"; Version is nil iff Phase is "idle".

type UpdateConfig added in v0.12.2

type UpdateConfig struct {
	// BaseURL is the GitHub Releases base URL; "" disables checking and apply entirely.
	BaseURL string
	// CheckInterval is how often the daemon re-checks for a newer release.
	CheckInterval time.Duration
	// PublicKey is the minisign public key file's raw bytes verification trusts
	// (overrides the embedded selfupdate.PublicKey() — a test seam; production always
	// passes the embedded key).
	PublicKey []byte
	// Install is the startup install classification (selfupdate.Classify), computed
	// once in cmd/musterd from the resolved executable path. installer/unmanaged are
	// re-derived at the start of every check by the Reclassify closure below, from
	// exePath/home/the write probe, not from this field; Install only decides whether
	// that re-derivation runs (dev/homebrew stay fixed for the daemon's life).
	Install selfupdate.Install
	// ExePath is the resolved (os.Executable + filepath.EvalSymlinks) real path of the
	// running binary — where an apply installs the new one and what swap detection stats.
	ExePath string
	// Reclassify re-derives installer/unmanaged (kb:adr/update-install-rechecked-on-every-check),
	// built in cmd/musterd from the same exePath/home/access inputs that produced Install.
	// Nil (every test daemon that doesn't exercise reclassification) means installer/
	// unmanaged never change after startup.
	Reclassify func() selfupdate.Install
}

UpdateConfig groups auto-update's checker/apply config. BaseURL empty means no updateManager is constructed at all (mirrors UsageConfig's nil-when-disabled shape) — a zero-value Config must never reach github.com.

type UpdateInfo added in v0.12.0

type UpdateInfo struct {
	Running   string          `json:"running"`
	Install   string          `json:"install"`
	Remedy    *string         `json:"remedy"`
	CanCheck  bool            `json:"canCheck"`
	Available *string         `json:"available"`
	CheckedAt *string         `json:"checkedAt"`
	Installed *string         `json:"installed"`
	Apply     UpdateApplyInfo `json:"apply"`
}

UpdateInfo is the `update` object, shared verbatim by the WS `update` broadcast and snapshot.update (kb:anchor/ws.update) — always present, even on a daemon with updates disabled entirely (-update-base-url "").

type UsageBucket

type UsageBucket struct {
	UsedPct  float64 `json:"usedPct"`
	ResetsAt string  `json:"resetsAt"`
}

UsageBucket is one of the five-hour/seven-day usage readouts (kb:anchor/ws.usage).

type UsageConfig added in v0.12.2

type UsageConfig struct {
	// Poll is the poll interval. <= 0 disables polling entirely: POST /api/usage/refresh
	// then 404s (kb:anchor/usage.refresh).
	Poll time.Duration
	// APIURL is the per-model usage endpoint's base URL. main always passes the flag's
	// non-empty default, so this is the *only* place that URL is defined — there is
	// deliberately no fallback constant here duplicating it. Empty disables the poller
	// entirely (same fail-safe shape as Poll <= 0).
	APIURL string
	// TokenFile, when non-empty, reads the Claude Code OAuth token from this file
	// instead of the macOS Keychain — a test seam that makes it structurally impossible
	// for a test using it to fall through to the real Keychain.
	TokenFile string
	// KeychainUser is the account name `security` looks the Keychain item up under
	// (main passes os/user.Current()'s value) — used only when TokenFile is empty.
	KeychainUser string
}

UsageConfig groups the per-model usage poller's config. Poll <= 0 means the poller is never constructed at all — a zero-value Config never reaches api.anthropic.com or the Keychain.

type UsageInfo

type UsageInfo struct {
	FiveHour          *UsageBucket       `json:"fiveHour"`
	SevenDay          *UsageBucket       `json:"sevenDay"`
	SampledAt         *string            `json:"sampledAt"`
	Source            string             `json:"source"`
	ModelScoped       []UsageModelWindow `json:"modelScoped"`
	ModelScopedAt     *string            `json:"modelScopedAt"`
	ModelScopedError  *string            `json:"modelScopedError"`
	ModelScopedSource string             `json:"modelScopedSource"`
}

type UsageModelWindow

type UsageModelWindow struct {
	DisplayName string  `json:"displayName"`
	UsedPct     float64 `json:"usedPct"`
	ResetsAt    string  `json:"resetsAt"`
}

UsageModelWindow is one entry of the `usage.modelScoped` list (kb:anchor/ws.usage, kb:adr/usage-masthead-one-selectable-model-window) — one per-model weekly usage window.

Jump to

Keyboard shortcuts

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