userconfig

package
v0.38.2 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package userconfig persists per-machine user preferences for the system tray (auto-start toggles, default project, self-update state) in a single JSON file under a hidden app directory in the user's home.

One installed binary = one config file. The directory is named after the running binary, so a user who installs the same app under two different names ("wick-manager", "client-tools") gets two separate configs without collision.

Path:

~/.<binary>/config.json

Settings here are machine-wide, not per-project. Per-project state (e.g., wick app data) still lives in the project's wick.db when launched from a project directory.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Dir added in v0.8.9

func Dir(name string) (string, error)

Dir returns the absolute per-app data directory. Empty name falls back to the running binary's basename.

func HiddenName added in v0.27.0

func HiddenName(name string) string

HiddenName turns an app name into a path-safe hidden directory name. Slugifies: lowercase, spaces → "-", strips chars that break Windows paths (< > : " / \ | ? *) and leading dots. "My App" → ".my-app". Exported so other packages derive the same ~/.<appName> dir from a resolved app name (e.g. the connector-plugin dir, which must match wick.db's tree).

func Path

func Path(name string) (string, error)

Path returns the absolute config file path for the given project name. Empty name falls back to the running binary's basename.

func ResolveDBPath

func ResolveDBPath(appName, customPath string)

ResolveDBPath determines the SQLite DB path and sets DATABASE_URL so config.Load() picks it up wherever it is called next.

Resolution order (first non-empty wins, never overwrites a higher priority):

  1. DATABASE_URL env already set (explicit env / CI override) → untouched
  2. cfg.DatabasePath set (user edited database_path in config.json)
  3. <binary_dir>/wick.db when wick.yml exists next to the binary (project mode)
  4. ~/.<appName>/wick.db (standalone / downloaded binary)

func ResolvePort

func ResolvePort(customPort int)

ResolvePort sets the PORT env from cfg.Port so config.Load() picks it up wherever it is called next.

Resolution order (first non-empty wins, never overwrites a higher priority):

  1. PORT env already set (explicit env / CI override) → untouched
  2. customPort > 0 (user edited port in config.json)
  3. fall through → env.go envDefault picks the built-in default (9425)

func Save

func Save(name string, cfg Config) error

Save writes the config atomically (write to temp, rename).

The temp file gets a unique name per write (via os.CreateTemp) rather than a fixed "config.json.tmp". Concurrent writers to the same config dir — e.g. a foreground Save racing the background rescan goroutine that Save itself spawns — would otherwise clobber each other's shared temp file, and one rename would fail with "no such file or directory" once the other consumed the temp first.

Types

type Config

type Config struct {
	// AutoStartApp registers the binary with the OS so it launches at
	// user login (Windows: Run registry, macOS: LaunchAgent plist,
	// Linux: XDG autostart .desktop). Toggle from Preferences ▶ Auto-start app.
	AutoStartApp bool `json:"auto_start_app"`

	// Tray auto-start toggles — applied at the next tray launch.
	AutoStartServer bool `json:"auto_start_server"`
	AutoStartWorker bool `json:"auto_start_worker"`

	// Self-update toggle.
	AutoUpdate bool `json:"auto_update"`

	// Port overrides the HTTP listen port. 0 = use env PORT or default 9425.
	// Set this in config.json to pin a custom port without touching .env.
	Port int `json:"port,omitempty"`

	// LogRetentionDays controls how many days of per-day log files are
	// kept. 0 = use built-in default (7). Set in config.json to override.
	LogRetentionDays int `json:"log_retention_days,omitempty"`

	// DatabasePath overrides the SQLite DB location. Empty = auto-detect.
	// Auto-detect: binary dir has wick.yml → <binary_dir>/wick.db,
	// otherwise ~/.<appName>/wick.db.
	// Set this manually in config.json if you need a custom location.
	DatabasePath string `json:"database_path,omitempty"`

	// Update state — managed by the updater, not user-facing.
	StagedUpdatePath    string `json:"staged_update_path,omitempty"`
	StagedUpdateVersion string `json:"staged_update_version,omitempty"`

	// Providers holds per-AI-provider overrides for the agents module
	// (claude / codex / gemini). Each provider keeps its own binary
	// path override + extra args. Empty / nil = full auto-detect via
	// PATH lookup.
	Providers ProvidersConfig `json:"providers,omitempty"`

	// ProviderStatuses caches the last-known Probe result per
	// instance, keyed `<type>/<name>`. Survives restart so the
	// Providers page renders instantly instead of waiting on cold
	// `--version` spawns. Refresh policy is owned by the agents
	// module — this layer is a dumb store. Empty / nil = no cache.
	ProviderStatuses map[string]ProviderStatus `json:"provider_statuses,omitempty"`
}

Config is the on-disk shape. Add fields with `json:"...,omitempty"` so older config files keep working when the binary upgrades.

func Load

func Load(name string) (Config, error)

Load reads the config file for the given project name. Missing file → defaults; parse errors surface to the caller.

type HookCapability added in v0.9.5

type HookCapability struct {
	Supported bool   `json:"supported,omitempty"`
	Verified  bool   `json:"verified,omitempty"`
	ProbedAt  string `json:"probed_at,omitempty"`
	Error     string `json:"error,omitempty"`
	Scope     string `json:"scope,omitempty"` // "bash+edit+mcp" | "shell-only" | "untested"
}

HookCapability is the persisted snapshot of one hook-event probe. Mirrors capability.Capability — kept here as a separate struct so the userconfig package stays self-contained (no import of internal/agents/capability, which would invert the dependency).

type HookInstanceConfig added in v0.9.5

type HookInstanceConfig struct {
	Enabled bool `json:"enabled,omitempty"`
}

HookInstanceConfig is the user's stored intent for one hook event on one provider instance. Kept as a struct (not just a bool) so we can grow per-event knobs (mode, allowlist, per-tool override) without another schema migration.

type ProviderInstance added in v0.9.0

type ProviderInstance struct {
	Name       string   `json:"name"`
	BinaryPath string   `json:"binary_path,omitempty"`
	Disabled   bool     `json:"disabled,omitempty"`
	ExtraArgs  []string `json:"extra_args,omitempty"`
	Env        []string `json:"env,omitempty"`

	// ModelSelect turns on the model picker for this CLI instance
	// (claude/codex/gemini). When on, the composer offers Models (or the
	// per-type seed defaults when Models is empty) and the chosen model is
	// passed to the CLI via --model on spawn. Off = the CLI's own default
	// model, no picker (unchanged behaviour). Ignored by wick (which has
	// its own WickModels).
	ModelSelect bool `json:"model_select,omitempty"`
	// Models is the user-curated list of models offered when ModelSelect is
	// on: each an alias plus an optional description. Empty → the per-type
	// seed defaults are used. CLIs can't enumerate their own models, so this
	// is seeded + user-editable rather than discovered. UserModelEntry
	// unmarshals both the current object form and the legacy plain-string
	// form so existing configs migrate transparently.
	Models []UserModelEntry `json:"models,omitempty"`

	// Hooks captures the user's intent per hook event: "do you want
	// wick to route this hook through the gate?". Keys are event
	// names (PreToolUse for the command gate today; future events
	// like SessionStart land as additional keys without schema
	// churn). Absent / Enabled=false means the provider's own
	// permission flow applies — no hook config gets installed on
	// spawn.
	Hooks map[string]HookInstanceConfig `json:"hooks,omitempty"`

	// SandboxMode sets --sandbox for codex spawns: "read-only",
	// "workspace-write", or "danger-full-access". Empty = danger-full-access.
	// Only meaningful for codex instances; ignored by claude/gemini.
	SandboxMode string `json:"sandbox_mode,omitempty"`

	// MaxConcurrent caps how many parallel spawns this instance may
	// have running at once. 0 = unlimited (follows the global pool cap).
	MaxConcurrent int `json:"max_concurrent,omitempty"`

	// SendMode overrides how this instance delivers a user message to its
	// CLI. Empty = the provider type's default (claude → "append", codex →
	// "queue"). Values: "append" (persistent stdin, one process; the CLI
	// queues input itself), "queue" (one-shot per turn, mid-turn sends
	// wait then run in order — every message processed, none lost), "spawn"
	// (one-shot, every send a fresh parallel process — no queue, contexts
	// independent). See provider.SendMode.
	SendMode string `json:"send_mode,omitempty"`

	// Storage configures credential/config file syncing for this
	// instance. nil = sync disabled.
	Storage *StorageConfig `json:"storage,omitempty"`

	// UseAIRouter routes this instance's CLI through an embedded AI router
	// proxy (base URL <wick-origin>/airouter/<id>/v1) instead of the
	// provider's own upstream. Only meaningful for claude/codex.
	//
	// JSON key kept as "use_9router" for backward compatibility with configs
	// written before the AI-router generalisation — existing 9router-routed
	// instances load unchanged and default to the 9router backend.
	UseAIRouter bool `json:"use_9router,omitempty"`

	// AIRouterProvider selects which registered router this instance routes
	// through ("9router", "omniroute", …). Empty = default (9router).
	AIRouterProvider string `json:"airouter_provider,omitempty"`

	// AIRouterModels maps a per-provider model slot key (e.g. "opus",
	// "sonnet", "haiku" for claude; "model", "subagent" for codex) to the
	// concrete model id chosen for it. Which slots exist is defined by the
	// selected router's SpawnHook. JSON key kept for back-compat.
	AIRouterModels map[string]string `json:"router9_models,omitempty"`

	// AIRouterAPIKey is a custom router API key (encrypted at rest via the
	// secret layer). Empty falls back to the router's default credential.
	// JSON key kept for back-compat.
	AIRouterAPIKey string `json:"router9_api_key,omitempty"`

	// AIRouterRawConfig is free-form extra config appended to the router spawn
	// (codex `-c` overrides / claude env), one entry per line.
	AIRouterRawConfig string `json:"airouter_raw_config,omitempty"`

	// WickModels is the custom-model registry for the built-in wick
	// provider — one entry per registered model (Gemini / OpenAI /
	// Anthropic / OpenRouter / other). Only meaningful for the wick
	// instance; ignored by claude/codex/gemini.
	WickModels []WickModel `json:"wick_models,omitempty"`

	// WickConfig holds the wick provider's instance-level settings
	// (tools, context budget, generation defaults). nil = defaults.
	WickConfig *WickConfig `json:"wick_config,omitempty"`
}

type ProviderStatus added in v0.9.0

type ProviderStatus struct {
	Path       string `json:"path"`
	PathFound  bool   `json:"path_found"`
	Version    string `json:"version,omitempty"`
	VersionErr string `json:"version_err,omitempty"`
	ScannedAt  string `json:"scanned_at,omitempty"`
	VersionAt  string `json:"version_at,omitempty"`

	// Hooks captures the runtime capability check per hook event name.
	// Keys are provider-agnostic event names ("PreToolUse",
	// "SessionStart", ...). Empty map = never probed, UI surfaces
	// "click Test to verify".
	Hooks map[string]HookCapability `json:"hooks,omitempty"`
}

ProviderStatus is the persisted shape of a Probe result.

Hooks holds per-event capability info (currently just "PreToolUse" for the command gate; future events like "SessionStart" or "UserPromptSubmit" land as additional map keys without struct churn). Persisting it here means the Providers page renders the gate-toggle state from disk without re-spawning the provider on every render — same TTL strategy as the version probe. Re-probe only fires when Version changes or the user clicks Rescan.

type ProvidersConfig added in v0.9.0

type ProvidersConfig struct {
	Claude []ProviderInstance `json:"claude,omitempty"`
	Codex  []ProviderInstance `json:"codex,omitempty"`
	Gemini []ProviderInstance `json:"gemini,omitempty"`

	// Wick is the built-in in-process provider. Single-instance by
	// design: the list never holds more than the one "wick" entry —
	// multiplicity lives in that instance's WickModels, not in extra
	// instances. Enforced by provider.Save/Rename, kept as a slice so
	// the ProvidersConfig shape stays uniform across types.
	Wick []ProviderInstance `json:"wick,omitempty"`
}

ProvidersConfig groups per-provider-type instance lists. One type (e.g. "claude") can hold multiple named instances so the user can run two different binaries / credential sets in parallel — typical case is a "work" claude on a corporate PAT next to a "personal" claude on a different PAT.

Bootstrap rule: on first boot the agents bootstrap auto-seeds one instance per supported type whose Name equals the type itself (`claude`, `codex`, `gemini`) with BinaryPath empty so LookPath resolves the canonical binary on PATH. Adding more instances is purely user-driven via the Providers page.

type StorageConfig added in v0.11.0

type StorageConfig struct {
	Mode            string `json:"mode"`             // "folder" | "single"
	SyncPath        string `json:"sync_path"`        // abs path to file or folder
	IntervalSeconds int    `json:"interval_seconds"` // 0 = startup only
}

StorageConfig defines how a provider instance syncs its credential files to the DB.

Mode "folder" syncs all files under SyncPath recursively. Mode "single" syncs only the file at SyncPath. IntervalSeconds controls how often the background ticker runs; 0 disables background sync (startup-only).

type UserModelEntry added in v0.35.0

type UserModelEntry struct {
	ID   string `json:"id"`
	Desc string `json:"desc,omitempty"`
}

ProviderInstance is one named configuration of a provider type. Name must be unique within a single type ("claude" can have one "work" + one "personal" but two "work" entries collide).

BinaryPath: absolute path to the CLI binary. Empty = LookPath the canonical type name on PATH.

Disabled: hide from new-session pickers and refuse to spawn. Useful when an instance is detected but known broken.

ExtraArgs: extra CLI flags appended after the canonical headless flags, before --resume. Forwarded to the provider's Spawner.

Env: extra `KEY=VALUE` pairs merged into the subprocess env on every spawn. The primary use case is per-instance credentials (different ANTHROPIC_API_KEY between work and personal claude) without leaking those into the user's global shell env. UserModelEntry is one curated model on a provider instance: an alias plus an optional description, both hand-editable. UnmarshalJSON accepts either the current object form ({"id":"opus","desc":"…"}) or the legacy plain string form ("opus") so configs written before the description column migrate transparently on first load.

func (*UserModelEntry) UnmarshalJSON added in v0.35.0

func (m *UserModelEntry) UnmarshalJSON(b []byte) error

type WickConfig added in v0.34.0

type WickConfig struct {
	// ShellToolDisabled turns the bash/cmd tool off. Stored inverted
	// so the zero value keeps the shell tool enabled (default on).
	ShellToolDisabled bool `json:"shell_tool_disabled,omitempty"`
	// HideCapabilities suppresses the model-capability chips in the pickers.
	// Stored inverted (like ShellToolDisabled) so the zero value SHOWS them —
	// capabilities are on by default; this only turns them off.
	HideCapabilities bool `json:"hide_capabilities,omitempty"`
	// StreamDisabled turns off SSE streaming of model output (falls back to
	// the one-shot JSON path). Stored inverted so the zero value STREAMS —
	// streaming is on by default; this only turns it off (e.g. a gateway that
	// doesn't support SSE).
	StreamDisabled bool `json:"stream_disabled,omitempty"`
	// CapabilityDisplayMode picks how chips render: "" / "icon" = icons +
	// tooltip (default), "label" = text labels.
	CapabilityDisplayMode string `json:"capability_display_mode,omitempty"`
	// Connectors limits which connector instances become tools.
	// Empty = all ready connectors.
	Connectors []string `json:"connectors,omitempty"`
	// MaxContextTokens is the history-replay budget. 0 = model default.
	MaxContextTokens int `json:"max_context_tokens,omitempty"`
	// MaxTurns caps the agentic loop per user turn. 0 = unlimited.
	MaxTurns int `json:"max_turns,omitempty"`
	// MaxConsecErrors cuts a turn after N consecutive all-error tool
	// rounds (a success resets the counter). 0 = default (20).
	MaxConsecErrors int `json:"max_consec_errors,omitempty"`
	// MaxTurnMinutes is the wall-clock ceiling for one turn. 0 = default (60).
	MaxTurnMinutes int `json:"max_turn_minutes,omitempty"`
	// MaxModelRetries is the total attempts for a failing model call (incl. the
	// first). 0 = default (3). 1 disables retries.
	MaxModelRetries int `json:"max_model_retries,omitempty"`
	// ModelCallTimeoutSec bounds one model-call attempt. 0 = default (120s).
	ModelCallTimeoutSec int `json:"model_call_timeout_sec,omitempty"`
	// GenConfig is the default generation config for models without
	// their own override.
	GenConfig *WickGenConfig `json:"gen_config,omitempty"`
	// RawConfig is raw ADK config (JSON) merged into the runner config
	// — the escape hatch that keeps every adk-go knob reachable before
	// a structured field exists for it.
	RawConfig string `json:"raw_config,omitempty"`
}

WickConfig is the instance-level settings block for the wick provider ("Provider settings" card in the UI).

type WickGenConfig added in v0.34.0

type WickGenConfig struct {
	Temperature    *float64 `json:"temperature,omitempty"`
	TopP           *float64 `json:"top_p,omitempty"`
	ThinkingBudget *int     `json:"thinking_budget,omitempty"` // tokens; 0 = off, nil = model default
	// ReasoningEffort is a vendor-agnostic reasoning level ("low"|"medium"|
	// "high"), preferred over ThinkingBudget where the vendor speaks an effort
	// enum. Empty = use ThinkingBudget / model default.
	ReasoningEffort string `json:"reasoning_effort,omitempty"`
	MaxOutputTokens int    `json:"max_output_tokens,omitempty"`
}

WickGenConfig mirrors the common genai.GenerateContentConfig knobs. Pointer fields distinguish "not set" from zero. The long tail of options rides WickConfig.RawConfig / WickModel.RawConfig.

type WickModel added in v0.34.0

type WickModel struct {
	// ID is the stable identifier ("m_" + random) referenced by the
	// Default flag and by sessions that pinned a specific model.
	ID string `json:"id"`
	// Kind is the vendor family: google | openai | anthropic |
	// openrouter | other.
	Kind string `json:"kind"`
	// Label is the display name; empty = show Model.
	Label string `json:"label,omitempty"`
	// Model is the vendor model id (gemini-flash-latest, gpt-5.2, …).
	Model string `json:"model"`
	// APIKey is encrypted at rest (wick_cenc_ token).
	APIKey string `json:"api_key,omitempty"`
	// BaseURL overrides the vendor endpoint; required for kind=other.
	BaseURL string `json:"base_url,omitempty"`
	// APIFormat picks the wire protocol: gemini | openai_chat |
	// openai_responses | anthropic_messages. Empty = derived from Kind.
	APIFormat string `json:"api_format,omitempty"`
	// MaxOutputTokens caps the response size. 0 = vendor default.
	MaxOutputTokens int `json:"max_output_tokens,omitempty"`
	// Default marks the model sessions use unless they pin another.
	// Exactly one entry per instance holds true (enforced on save).
	Default bool `json:"default,omitempty"`
	// Disabled hides the model from the composer's model picker and
	// from default-selection, without deleting its (possibly hard-won)
	// config. Stays visible, greyed out, in the Models table.
	Disabled bool `json:"disabled,omitempty"`
	// GenConfig holds per-model generation overrides; nil = the
	// instance-level WickConfig.GenConfig applies.
	GenConfig *WickGenConfig `json:"gen_config,omitempty"`
	// RawConfig is per-model raw ADK config (JSON), merged last.
	RawConfig string `json:"raw_config,omitempty"`
	// LiveSet marks this entry as a LIVE model set: at picker time the
	// vendor's model list is fetched and (optionally) filtered by
	// DiscoveryFilter, rather than pinning one model. Set independently of
	// DiscoveryFilter so a live set can have an EMPTY filter (= match all)
	// without a sentinel — the presence of the filter no longer implies
	// live-set-ness. Model may be empty for a live set.
	LiveSet bool `json:"live_set,omitempty"`
	// DiscoveryFilter narrows a live set's fetched model list (tiny grammar:
	// space-separated terms; `term` = contains, `-term`/`!term` = exclude —
	// matched over id+label). Empty = match all. Only meaningful when LiveSet.
	DiscoveryFilter string `json:"discovery_filter,omitempty"`
	// DefaultVendorModel is the sticky default vendor model id WITHIN a live
	// set (only meaningful when DiscoveryFilter is set). When this live set is
	// picked without an explicit "@vendor" override, the spawn uses this id if
	// it's still present in the freshly-fetched list; if it has vanished, the
	// picker/spawn auto-fall back to the top of the filtered list. Empty = no
	// pin, top-of-list is the effective default.
	DefaultVendorModel string `json:"default_vendor_model,omitempty"`
}

WickModel is one registered custom model on the wick provider. See internal/planning/in-progress/wick-provider/plan.md.

Jump to

Keyboard shortcuts

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