cli

package
v0.0.0-...-27e3991 Latest Latest
Warning

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

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 34 Imported by: 0

Documentation

Overview

SessionStart hook logic for `wbt context session-start` (formerly the standalone wbt-context binary). Emits a JSON object with a "systemMessage" field that Claude Code injects into the first user message of a new session.

Exit semantics: this function returns nil unconditionally so the wbt dispatcher (and the wbt-context shim) always exit 0 — Claude Code MUST never be blocked by a hook error. All failures are logged via slog to a 0600 file in os.TempDir.

Stop hook + health snapshot logic for `wbt doctor` (formerly the standalone wbt-doctor binary). Connects to the wayneblacktea Postgres database and emits a JSON snapshot of personal-OS health (stuck in-progress tasks, pending proposal queue depth, due reviews) plus a list of human-readable "forgotten signals" — short strings flagging likely Claude omissions.

Designed to run as a Claude Code Stop hook so the next SessionStart can surface the previous session's open loops without depending on the live MCP process (which is gone by Stop time).

Stop hook upgrade (session lifecycle): if stdin contains a Claude Code transcript JSON (non-empty), the doctor:

  1. Calls Haiku to produce a ≤500-char plain-text summary.
  2. Writes the summary to session_handoffs.summary_text (best-effort).
  3. Saves the summary as a zettelkasten knowledge_item (type=zettelkasten, source=auto-summary) for long-term searchable recall.

Output:

  • JSON to stdout (parseable by SessionStart hook / claude-hud)
  • Forgotten signals also written to stderr in human-readable form
  • Always returns nil so wbt (and the wbt-doctor shim) exits 0

Package cli contains the logic for the wbt CLI subcommands. It is extracted from cmd/wbt/main.go so that the pure helper functions can be unit-tested without subprocess execution or file I/O.

PostToolUse hook logic for `wbt hook` (formerly the standalone wbt-hook binary).

Claude Code calls this subcommand after every tool execution (Bash, Edit, Write, Read, MCP, etc.) with a JSON payload on stdin.

Spec (Claude Code hooks):

stdin  — JSON: {"tool_name":..., "tool_input":..., "tool_response":{"text":...},
                "tool_use_id":..., "cwd":..., "session_id":..., "transcript_path":...}
stdout — optional JSON: {"additionalContext": "..."} (≤ 10 000 chars)
exit 0 — always; hook MUST NOT block the Claude Code session

Safety constraints:

  • Read at most 300 bytes from stdin (claude-mem bug #1220 workaround)
  • Total execution time budget: 50 ms (enqueue only, no DB / LLM wait)
  • POST to wayneblacktea server with 200 ms timeout
  • Always returns nil; errors are slog'd, never propagated

reembed_cmd.go — wbt reembed subcommand

Idempotent backfill command that re-embeds historical rows in session_handoffs and decisions whose embedding is NULL or whose embedding_provider does not match the currently-configured provider.

Usage:

wbt reembed [--table session_handoffs|decisions|all] [--batch N] [--dry-run]

Design notes:

  • Resumable: uses "WHERE embedding IS NULL OR embedding_provider <> $target" with ORDER BY created_at so interrupted runs restart cleanly.
  • Rate-limited: configurable batch size + per-batch sleep to avoid 429s.
  • Fail-soft: a per-row embed or DB error is logged and skipped; the loop continues to the next row.
  • NOT run automatically: the operator invokes this manually against prod Aiven AFTER setting GEMINI_API_KEY and DATABASE_URL in the environment.
  • This file DOES NOT connect to any live DB during build/task check — all DB I/O is behind a --dry-run guard that bypasses real connections.

Index

Constants

View Source
const HookMaxStdinBytes = hookMaxStdinBytes

HookMaxStdinBytes is exported for test access — verifies the truncation cap invariant from the legacy cmd/wbt-hook test suite.

Variables

This section is empty.

Functions

func BuildHookNotes

func BuildHookNotes(toolInput string) string

BuildHookNotes returns a SHA256 hex hash of toolInput, or a length-capped raw input when WBT_HOOK_RAW=1 is set (only for trusted dev environments). Exported so the hook_cmd_test.go file in the same package can exercise it (matches the legacy cmd/wbt-hook test contract).

SECURITY: raw mode caps at hookRawNotesMaxLen (500 chars) so file contents written by Edit/Write/Bash that include secrets cannot be harvested wholesale. (security audit M-3)

func CollectAPIKey

func CollectAPIKey(r *bufio.Reader) (string, error)

CollectAPIKey asks for an API key or generates one if the user leaves it empty.

func ConfigPath

func ConfigPath() (string, error)

ConfigPath returns the canonical path to the global config file: $XDG_CONFIG_HOME/wayneblacktea/config.yaml (defaults to ~/.config/…). The parent directory is created if it does not exist.

func DSNFromFallback

func DSNFromFallback() string

DSNFromFallback reads DATABASE_URL from one of the user-config fallback locations:

  • ~/.wayneblacktea/.env.local
  • ~/.wayneblacktea/.env

Returns "" when no file is found or no DATABASE_URL line is present.

func DetectDoctorSignals

func DetectDoctorSignals(s DoctorSnapshot) []string

DetectDoctorSignals returns the forgotten-signal strings for snapshot s. Exported for test access.

func EmitContext

func EmitContext(msg string)

EmitContext writes the SessionStart hook JSON envelope to stdout. Exported for test access.

func GenerateEnvFile

func GenerateEnvFile(apiKey, port string, db DBConfig) string

GenerateEnvFile returns the contents of the .env file produced by wbt init.

The output contains:

  • API_KEY, PORT, STORAGE_BACKEND (always present)
  • DATABASE_URL (postgres backend only)
  • SQLITE_PATH (sqlite backend only)
  • ALLOWED_ORIGINS defaulting to localhost on the given port
  • Commented hints for optional AI provider keys

func HealthOK

func HealthOK(port int) bool

HealthOK is the exported probeHealth helper for tests. It performs one GET /health and returns true when the response is 200. Not intended for production callers; setup.go uses probeHealth directly.

func HomeDir

func HomeDir() string

HomeDir returns the user home directory, falling back to "." on error.

func InitHookSlog

func InitHookSlog(name string)

InitHookSlog redirects slog to a 0600 file in os.TempDir so a hook never writes to stderr (Claude Code surfaces stderr as terminal warnings). Falls back to io.Discard if the log file cannot be opened. The name parameter is the basename of the hook (e.g. "wbt-context", "wbt-hook", "wbt-doctor") and becomes the prefix of the log file under os.TempDir.

Shared by the wbt context|hook|doctor subcommands (formerly the standalone wbt-context / wbt-hook / wbt-doctor binaries, now thin shims that exec `wbt <subcmd>`).

func LoadGlobalConfig

func LoadGlobalConfig() error

LoadGlobalConfig reads ~/.config/wayneblacktea/config.yaml and sets env vars for any keys present in the file that are not already set.

Missing file is silently ignored. Parse errors are returned.

func OpenBrowser

func OpenBrowser(url string)

OpenBrowser opens url in the system default browser. Best-effort: errors are silently swallowed because a browser launch failure (headless env, no default browser configured) must not abort the server start flow.

func ParseDuration

func ParseDuration(s string) (time.Duration, error)

ParseDuration extends time.ParseDuration to accept "d" for days.

func PrintHookSnippet

func PrintHookSnippet(apiKey, port string)

PrintHookSnippet prints a copy-pasteable ~/.claude/settings.json snippet that registers wbt-hook as a Claude Code PostToolUse global hook.

func Prompt

func Prompt(r *bufio.Reader, question string) (string, error)

Prompt prints the question and returns the trimmed answer from r.

func PromptRequired

func PromptRequired(r *bufio.Reader, question, emptyErrMsg string) (string, error)

PromptRequired calls Prompt and returns an error if the result is empty.

func PromptWithDefault

func PromptWithDefault(r *bufio.Reader, question, defaultVal string) (string, error)

PromptWithDefault calls Prompt and returns defaultVal if the user input is empty.

func RandomHex

func RandomHex(n int) (string, error)

RandomHex generates n cryptographically random bytes encoded as hex.

func ReadEnvPort

func ReadEnvPort() string

ReadEnvPort reads the PORT env var and returns "8420" as the default.

func RegisterClaudeMCP

func RegisterClaudeMCP(name, urlOrEmpty string, transport MCPTransport) error

RegisterClaudeMCP runs `claude mcp remove <name>` (best-effort) then `claude mcp add ...` to register the wayneblacktea MCP server with the user's Claude Code installation at user-global scope (default; no --scope project).

transport=http URL is the http endpoint (e.g. http://localhost:8420/mcp). transport=stdio invokes `wbt mcp`.

If the `claude` binary is not in PATH the function prints copy-paste instructions to stdout and returns nil — registration is a convenience, not a hard requirement.

The CLAUDE_BIN env var overrides the binary location for tests only.

func ResolveAllowedOrigins

func ResolveAllowedOrigins(allowedOrigins, appEnv, port string) (string, error)

ResolveAllowedOrigins determines the CORS allowed-origins string for the .env file. This is the wbt-init variant (reads env vars directly so it can be tested without subprocess wiring).

Rules (mirrors cmd/server resolveAllowedOrigins):

  • ALLOWED_ORIGINS="*" is always rejected.
  • ALLOWED_ORIGINS non-empty → returned as-is.
  • ALLOWED_ORIGINS empty + APP_ENV="production" → error.
  • ALLOWED_ORIGINS empty + any other APP_ENV → default to localhost on port.

func RunContext

func RunContext(args []string) error

RunContext dispatches `wbt context <subcmd>`. args is os.Args[2:] (subcmd onward). Currently supports only `session-start`. Returns nil so wbt always exits 0 — Claude Code MUST never be blocked by a hook error.

func RunDoctor

func RunDoctor(_ []string) error

RunDoctor dispatches `wbt doctor`. args is unused but kept for the standard Run<X>(args []string) error signature. Always returns nil so wbt (and the wbt-doctor shim) exits 0 — the Stop hook MUST never block Claude Code.

func RunGuard

func RunGuard(args []string) error

RunGuard dispatches wbt guard subcommands.

func RunHook

func RunHook(_ []string) error

RunHook dispatches `wbt hook` (PostToolUse). args is unused but kept for the standard Run<X>(args []string) error signature. Always returns nil so the wbt dispatcher (and wbt-hook shim) exits 0 — Claude Code MUST never be blocked.

func RunInit

func RunInit() error

RunInit runs the interactive wizard and writes .env + .mcp.json.

func RunInstallGitHook

func RunInstallGitHook(args []string) error

RunInstallGitHook implements `wbt install-git-hook`.

func RunMCP

func RunMCP() error

RunMCP serves MCP stdio by delegating to the shared mcprunner package (also used by cmd/mcp). Reads .env from CWD if present so users do not need to set DATABASE_URL / CLAUDE_API_KEY in the environment that Claude Code launches the hook from.

func RunPostMergeLocal

func RunPostMergeLocal(args []string) error

RunPostMergeLocal implements `wbt post-merge-local`. It is designed to run from a git post-merge hook in an arbitrary repo and is ALWAYS fail-open: it returns nil on every non-flag error so the hook never aborts a merge.

func RunReconcile

func RunReconcile(args []string) error

RunReconcile implements the `wbt reconcile` subcommand. args is everything after "wbt reconcile" on the command line. Returns nil on success or a fatal error; gh-missing / gh-unauth is a warning, not an error.

func RunReembed

func RunReembed(args []string) error

RunReembed dispatches `wbt reembed`. args is os.Args[2:]. Always returns nil so wbt exits 0 on dry-run; propagates errors otherwise.

func RunRestart

func RunRestart(args []string) error

RunRestart is stop + setup. We do not short-circuit on stop errors so that "stop failed because already stopped" still triggers a fresh setup attempt.

func RunServe

func RunServe(args []string) error

RunServe loads config and runs wayneblacktea-server. Config is loaded in priority order (later source wins unless env var already set):

  1. ~/.config/wayneblacktea/config.yaml (global, written by `wbt init`)
  2. .env in CWD (legacy / per-project override)
  3. Existing environment variables (Railway, CI, etc.)

args is the slice of arguments after "serve" (i.e. os.Args[2:]).

Flags:

--no-browser   suppress automatic browser launch (also suppressed by WBT_NO_BROWSER=1)

func RunSetup

func RunSetup(args []string) error

RunSetup orchestrates the one-command install. See package doc for the step ordering. args is the slice of CLI arguments after "setup".

Supported flags:

--port=<n>      override port (default: WBT_PORT env or 8420 from config)
--server-bin=<path>  override wayneblacktea-server path (for tests)
--mcp-name=<n>  override MCP server name (default: wayneblacktea)
--no-mcp        skip claude mcp registration entirely

func RunStatus

func RunStatus(args []string) error

RunStatus implements `wbt status`. It reads the PID file, checks liveness, probes /health on the configured port, and prints the result.

Flags:

--format plain   (default) one-line human summary
--format json    JSON shape per StatusReport

Exit code: 0 when the server is healthy; 1 when not running OR PID file references a dead process; 2 on probe failure (port unreachable but PID alive — caller may want to inspect logs).

func RunStop

func RunStop(args []string) error

RunStop terminates the background wayneblacktea server identified by the PID file and removes the PID file on success. The operation is idempotent: missing PID file is reported as "already stopped" with exit code 0.

args is reserved for future flags (--force, --timeout) and is currently validated to be empty.

func SetIfAbsent

func SetIfAbsent(key, value string)

SetIfAbsent sets the env var key to value only when value is non-empty and the env var is not already set.

func SortBySimDesc

func SortBySimDesc[T any](s []T, score func(T) float64)

SortBySimDesc sorts a slice of any type by descending similarity score. Uses a simple insertion-sort-style swap (table sizes <= 200, acceptable). Exported for test access.

func ValidateGuardBypassFlags

func ValidateGuardBypassFlags(scope, target, reason string, iUnderstandGlobal bool) error

ValidateGuardBypassFlags exhaustively validates the (scope, target, reason) triple plus the global confirmation.

Validation rules:

  • scope MUST be in {file, dir, repo, global}.
  • When scope=global, target MUST be the literal "global" AND the --i-understand-this-is-global flag MUST be set.
  • When scope in {file, dir}, target MUST be an absolute path AND MUST NOT be one of the overly-broad system roots ("/", "/home", "/Users").
  • reason MUST be non-empty (non-whitespace).

func WorkspaceFromEnv

func WorkspaceFromEnv() *uuid.UUID

WorkspaceFromEnv reads WORKSPACE_ID from the environment and returns a *uuid.UUID, or nil if the env var is missing/invalid. Used by the context and doctor hooks to scope DB queries.

func WriteGlobalConfig

func WriteGlobalConfig(cfg WbtConfig) error

WriteGlobalConfig serialises cfg to ~/.config/wayneblacktea/config.yaml with mode 0600 (credential-bearing file).

func WriteMCPJSON

func WriteMCPJSON(db DBConfig) ([]byte, error)

WriteMCPJSON marshals the .mcp.json content and returns the bytes.

The MCP entry points at `wbt mcp` rather than the standalone `wayneblacktea-mcp` binary so that `go install .../cmd/wbt@latest` installs everything an end user needs.

Types

type DBConfig

type DBConfig struct {
	StorageBackend string
	DatabaseURL    string
	SQLitePath     string
}

DBConfig holds the database configuration collected during init.

func CollectDBConfig

func CollectDBConfig(r *bufio.Reader) (DBConfig, error)

CollectDBConfig asks the user whether to use SQLite or Postgres.

func CollectPostgresConfig

func CollectPostgresConfig(r *bufio.Reader) (DBConfig, error)

CollectPostgresConfig collects Postgres connection details.

func CollectSQLiteConfig

func CollectSQLiteConfig(r *bufio.Reader) (DBConfig, error)

CollectSQLiteConfig collects SQLite file path details.

type DoctorSnapshot

type DoctorSnapshot struct {
	GeneratedAt      time.Time `json:"generated_at"`
	Workspace        string    `json:"workspace,omitempty"`
	StuckTasks       []string  `json:"stuck_task_ids,omitempty"`
	StuckCount       int       `json:"stuck_count"`
	InProgressCount  int       `json:"in_progress_count"`
	PendingProposals int       `json:"pending_proposals"`
	DueReviews       int       `json:"due_reviews"`
	ForgottenSignals []string  `json:"forgotten_signals,omitempty"`
	SessionSummary   string    `json:"session_summary,omitempty"`

	// GoalsDue and TopPending are the OPS-1 delivery-visibility fields,
	// consumed by _project/.claude/hooks/session-start.sh. Both are
	// deliberately NOT `omitempty`: GoalsDue MUST always render as "[]"
	// (never omitted/null) and TopPending MUST always render as an object
	// or explicit "null" — see newDoctorSnapshot for the always-non-nil
	// GoalsDue initialisation that makes this hold even on fail-soft paths
	// that never reach the DB. Title + deadline only; no goal/task IDs,
	// descriptions, or other context is exposed (backend-security-design.md
	// §3.2 data minimisation).
	GoalsDue   []gtd.DeliveryGoal `json:"goals_due"`
	TopPending *gtd.DeliveryTask  `json:"top_pending"`
}

DoctorSnapshot is the JSON envelope written to stdout by `wbt doctor`. Exported so tests can construct fixtures without leaving the package.

type MCPConfig

type MCPConfig struct {
	MCPServers map[string]MCPServer `json:"mcpServers"`
}

MCPConfig is the shape of .mcp.json written for Claude Desktop / MCP clients.

type MCPServer

type MCPServer struct {
	Command string            `json:"command"`
	Args    []string          `json:"args,omitempty"`
	Env     map[string]string `json:"env"`
}

MCPServer represents a single MCP server entry in .mcp.json.

type MCPTransport

type MCPTransport string

MCPTransport names the transport claude uses to reach wayneblacktea.

const (
	// MCPTransportHTTP registers an HTTP endpoint (preferred for `wbt setup`
	// since the server runs in the background; one process serves many
	// Claude sessions).
	MCPTransportHTTP MCPTransport = "http"
	// MCPTransportStdio registers a fork-on-each-launch stdio binary.
	// Retained for backward compatibility with `wbt init`.
	MCPTransportStdio MCPTransport = "stdio"
)

type SessionStartOutput

type SessionStartOutput struct {
	SystemMessage string `json:"systemMessage"`
}

SessionStartOutput matches the Claude Code hook spec: a JSON object with a "systemMessage" string that is prepended to the first user message.

type StatusReport

type StatusReport struct {
	PID       int        `json:"pid"`
	Port      int        `json:"port"`
	Transport string     `json:"transport"`
	Healthy   bool       `json:"healthy"`
	PIDFile   string     `json:"pid_file"`
	StartedAt *time.Time `json:"started_at,omitempty"`
	Version   string     `json:"version,omitempty"`
}

StatusReport is the JSON shape printed by `wbt status --format json`. StartedAt is a pointer so json:"omitempty" actually drops the field when we don't know the start time (PID-file-only path).

type WbtConfig

type WbtConfig struct {
	APIKey      string `yaml:"api_key"`
	DatabaseURL string `yaml:"database_url"`
	Port        string `yaml:"port"`
	// storage
	StorageBackend string `yaml:"storage_backend"`
	SQLitePath     string `yaml:"sqlite_path"`
}

WbtConfig is the schema for ~/.config/wayneblacktea/config.yaml. All fields are optional strings so that partial configs are accepted on read without overriding env vars that are already set.

func DBConfigToWbtConfig

func DBConfigToWbtConfig(apiKey, port string, db DBConfig) WbtConfig

DBConfigToWbtConfig converts wizard-collected DBConfig into WbtConfig.

Jump to

Keyboard shortcuts

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