api

package
v0.1.15 Latest Latest
Warning

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

Go to latest
Published: May 1, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package api implements the JSON HTTP handlers mounted under /api by internal/serve.Server. Handlers are pure http.HandlerFunc factories — auth wrapping happens at server boot, not inside the handler.

Package api — V13 /api/cost handler.

Returns a window of cost_points plus totals so the dashboard can render a cumulative cost chart that survives daemon restarts.

Required server.go wiring (coordinator owns this):

mux.Handle("GET /api/cost", authHF(api.Cost(s.cost)))

s.cost must satisfy api.CostSource (see below). The production implementation is store.CostStore — use a direct assignment; the interfaces match by duck-typing (Range + Totals).

Package api — /api/sessions/{name}/feed/history (V6).

Historical scroll past the in-memory 500-slot ring buffer. The hub's per-session ring is a cache; the on-disk JSONL log is the source of truth. This handler reads that log in reverse so a UI that has scrolled to the oldest ring entry can fetch older events on demand.

Mount (wired in server.go alongside the other /feed routes):

mux.Handle("GET /api/sessions/{name}/feed/history",
    authHF(api.FeedHistory(s.logDir, logsUUIDResolver{proj: s.proj})))

Shape (one response row per line):

{
  "events": [
    {"id":"<nano>-0", "session":"alpha", "type":"tool_call",
     "ts":"2026-04-21T14:33:09Z",
     "payload":{session,tool,input,summary,is_error,ts}},
    ...
  ],
  "has_more": true
}

Contract notes:

  • `before` query param is REQUIRED. The cursor is the opaque `<unix-nano>-<seq>` ID the hub assigns at Publish time; the client echoes back the oldest visible row's id.
  • Derived IDs use seq=0: JSONL lines don't carry hub sequence numbers, but monotonicity within this cursor window is preserved because rows come out of a single file in append-order.
  • Results are strictly less than `before` and returned newest-first so the UI can append directly below the ring view.
  • `has_more` is true when the backwards scan hit `limit` before exhausting the file — the UI shows the "Load older" button again.
  • Only `tool_call`-shaped lines are emitted (lines without a `tool_name` field are dropped). The on-disk log only contains tool_call payloads today, so this is effectively a schema guard.

Package api holds the v0.1 HTTP handlers for ctm serve. Only the liveness endpoints (/healthz, /health) live here in step 1 of the design spec; sessions, hooks, revert, and bootstrap arrive in subsequent steps.

Package api — /api/logs/usage surfaces disk usage of the JSONL tailer directory so users can notice when it's time to prune.

Walk is bounded (maxFilesLimit) to keep the handler cheap even on very old installs that have accumulated thousands of transcripts. Resolution of uuid → session name reuses the same "claudeDirToName" fallback already used for orphan UUID adoption in serve.Server.Run (see server.go for background).

Package api — pane.go: V24 live tmux pane capture SSE stream.

Route wiring (owned by coordinator in server.go — do NOT edit here):

mux.Handle("GET /events/session/{name}/pane", authHF(api.PaneStream(s.tmux)))

Package api hosts the HTTP handlers for ctm serve. Each file in this package owns one resource family; this file owns /api/sessions and /api/sessions/{name}.

Wiring lives in internal/serve/server.go (route registration is the caller's responsibility). Handlers here return enriched Session views per spec §6: fields the projection cannot yet populate (last_tool_call_at, context_pct, attention) are sourced through the SessionEnricher interface and OMITTED from the JSON when the enricher reports no value. Later steps in the ctm-serve plan will supply real enricher implementations.

Package api — V15: /api/sessions/{name}/subagents.

Returns the forest of subagents for a session, computed by replaying the session's JSONL log and grouping tool_call rows by their top-level `agent_id` field. Each unique (session, agent_id) pair produces one tree node; the `parent_id` field is reserved for a future Claude Code schema change (the current JSONL shape doesn't carry a parent pointer, so every node is a root today).

Mount (wired in server.go alongside the other /api/sessions/{name} routes — the coordinator pastes these lines into registerRoutes):

mux.Handle("GET /api/sessions/{name}/subagents",
    authHF(api.Subagents(s.logDir, logsUUIDResolver{proj: s.proj})))

Shape:

{
  "subagents": [
    {
      "id": "ada78973e092dae52",
      "parent_id": null,
      "type": "Explore",
      "description": "cat README.md",
      "started_at": "2026-04-21T12:00:00Z",
      "stopped_at": "2026-04-21T12:02:10Z",
      "tool_calls": 7,
      "status": "completed"
    },
    ...
  ]
}

Newest root first; children newest-first (see orderNodes below — trees are flat today so this is a simple top-level sort). Cap at maxSubagentsPerSession (500).

`since` query param (RFC3339) — when present, rows with `started_at <= since` are elided server-side to reduce the payload on re-fetch after an SSE wake-up.

Completion status is inferred: a subagent is "running" when its last observed tool_call is within runningGrace (5 s) of now AND no tool_response error bit is set; "failed" when any of its tool calls returned an error; otherwise "completed".

Package api — V16: /api/sessions/{name}/teams.

A "team" is a group of subagents dispatched within a tight time window (teamWindow). The Claude Code JSONL doesn't carry an explicit `team_name` or `team_spawn` row today, so the team shape is inferred from the same replay as V15: any pair of subagents whose `started_at` timestamps fall inside teamWindow get merged into a single team. When the schema grows a dedicated team_name field, extend parseSubagentLine to surface it and switch the group key here.

Mount (coordinator pastes into registerRoutes in server.go):

mux.Handle("GET /api/sessions/{name}/teams",
    authHF(api.Teams(s.logDir, logsUUIDResolver{proj: s.proj})))

Shape:

{
  "teams": [
    {
      "id": "team-<earliest-agent-id>",
      "name": "Explore · 3 agents",
      "dispatched_at": "2026-04-21T12:00:00Z",
      "status": "completed",
      "summary": null,
      "members": [
        {"subagent_id":"abc","description":"...","status":"completed"},
        ...
      ]
    }
  ]
}

Status roll-up:

  • any member running → team is "running"
  • any member failed → team is "failed"
  • else → team is "completed"

Summary is currently always null (no `team_summary` event exists in the JSONL yet); the field stays in the contract so the UI can render a blockquote when one lands later.

Package api — /api/sessions/{name}/tool_calls/{id}/detail surfaces the full tool-call input (and, for Edit/MultiEdit/Write, a unified diff) on-demand so the Feed tab can expand any row without paying for a richer hub Event payload at ingest time.

Wiring (paste into internal/serve/server.go registerRoutes, next to the other /api/sessions/{name}/... handlers):

mux.Handle(
    "GET /api/sessions/{name}/tool_calls/{id}/detail",
    authHF(api.ToolCallDetail(api.NewJSONLLogReader(s.logDir, s.proj))),
)

`s.logDir` is the absolute path to ~/.config/ctm/logs (same value already passed to `api.LogsUsage` on line 463). `s.proj` is the ingest.Projection used to map human session name → Claude UUID.

Index

Constants

View Source
const DoctorDeadline = 5 * time.Second

DoctorDeadline caps how long the runner may spend. The CLI doctor shells out to tmux and checks tmux session liveness; 5 s is ample for that without letting a pathological box hang the HTTP response.

Variables

View Source
var ErrDetailNotFound = errors.New("tool call detail not found")

ErrDetailNotFound is the sentinel the LogReader returns when the requested id does not correspond to any line in the session's JSONL (either the file is missing, the id is too old to still be on disk, or the scan hit the 5 MB cap without a match).

Functions

func AttachURL

func AttachURL() http.HandlerFunc

AttachURL returns GET /api/sessions/{name}/attach-url. Produces a `ctm://attach?name=…` URL the OS can hand off to the ctm CLI's attach handler. Read-only aside from formatting the URL, but still gated by Origin so a rogue page can't enumerate session names by probing this endpoint.

func AuthLogin

func AuthLogin(store *auth.Store, limiter *auth.Limiter) http.HandlerFunc

AuthLogin returns POST /api/auth/login. The limiter protects the argon2id verify path from brute-force/DoS; a successful login resets the IP's window so legitimate users aren't locked out after a typo.

func AuthLogout

func AuthLogout(store *auth.Store) http.HandlerFunc

AuthLogout returns POST /api/auth/logout.

func AuthSignup

func AuthSignup(store *auth.Store) http.HandlerFunc

AuthSignup returns POST /api/auth/signup. Refuses if user.json already exists; otherwise creates it, issues a session token, returns 201.

func AuthStatus

func AuthStatus(store *auth.Store) http.HandlerFunc

AuthStatus returns GET /api/auth/status. Never 401s — reports registered+authenticated as booleans so the UI can route.

func BearerFromRequest

func BearerFromRequest(r *http.Request) string

BearerFromRequest is the exported twin of bearerToken, used by internal/serve/server.go's authHF middleware.

func Bootstrap

func Bootstrap(version string, port int, hasWebhook bool) http.HandlerFunc

Bootstrap returns the GET /api/bootstrap handler. The response shape is the contract consumed by ui/ on the auth-paste screen: enough metadata for the SPA to decide whether to render webhook UI without leaking server internals.

Response: {"version":..., "port":..., "has_webhook":...}.

405 on any non-GET method.

func Checkpoints

func Checkpoints(resolveWorkdir func(name string) (string, bool), cache *CheckpointsCache) http.HandlerFunc

Checkpoints returns the GET handler for /api/sessions/{name}/checkpoints. resolveWorkdir lets the handler stay decoupled from the sessions package: it returns the workdir for `name` and false when the session is unknown. cache is shared with the revert handler's SHA-allowlist check (see server.go).

func ConfigGet

func ConfigGet(cfgPath string) http.HandlerFunc

ConfigGet returns the GET /api/config handler. The response is the same allowlisted shape that PATCH /api/config accepts, so the UI can seed the settings form with current values without parsing the full on-disk config.

Response body:

{
  "webhook_url":  "...",
  "webhook_auth": "...",
  "attention": {
    "error_rate_pct":         20,
    "error_rate_window":      20,
    "idle_minutes":           5,
    "quota_pct":              85,
    "context_pct":            90,
    "yolo_unchecked_minutes": 30
  }
}

Thresholds are always returned in their Resolved() form so the UI form shows the defaults the daemon is actually using rather than zeroes for fields the user has never set.

405 on any non-GET method. 500 on config-load failure (rare — Load auto-creates the file on first boot).

func ConfigUpdate

func ConfigUpdate(cfgPath string, shutdown func(reason string)) http.HandlerFunc

ConfigUpdate returns the PATCH /api/config handler.

Body shape (all top-level keys optional; unknown keys → 400):

{
  "webhook_url":  "https://...",
  "webhook_auth": "Bearer ...",
  "attention": {
    "error_rate_pct":         20,
    "error_rate_window":      20,
    "idle_minutes":           5,
    "quota_pct":              85,
    "context_pct":            90,
    "yolo_unchecked_minutes": 30
  }
}

On success, responds 202 Accepted with {"status":"restarting"} and schedules shutdown(reason) to run after shutdownDelay so the response flushes first. The caller (proc.EnsureServeRunning at the next attach/new/yolo) respawns the daemon.

Contract:

  • 405 on non-PATCH.
  • 400 on invalid JSON, unknown top-level keys, or out-of-range values.
  • 500 if the on-disk config cannot be loaded or atomically replaced.
  • 202 with {"status":"restarting"} on success.

Write is atomic: marshal → WriteFile(<path>.tmp) → Rename. A crash mid-write leaves the previous config intact.

func Cost

func Cost(src CostSource) http.HandlerFunc

Cost returns the GET /api/cost handler.

?session=<name>  — optional; omitted = aggregate across all sessions
?window=hour|day|week — default day; unknown = 400

func CreateSession

CreateSession returns POST /api/sessions.

func DefaultAllowedOrigins

func DefaultAllowedOrigins(port int) []string

DefaultAllowedOrigins is the baseline loopback allowlist. Future work may load a user-configurable extra list from config.Serve.AllowedOrigins; for v0.2 we hard-code the two loopback spellings the UI actually produces.

func Diff

func Diff(resolveWorkdir func(name string) (string, bool), cache *CheckpointsCache) http.HandlerFunc

Diff returns the GET handler for /api/sessions/{name}/checkpoints/{sha}/diff.

The handler chains two guards that together prevent arbitrary `git show` exposure:

  1. resolveWorkdir maps a session name to its workdir (false → 404).
  2. cache.IsCheckpoint confirms sha is one of the *full* commit SHAs currently listed under the session's checkpoints. A SHA that doesn't appear — including abbreviated forms — yields 404.

On success the unified diff is streamed as text/plain so the UI can render it in a <pre> without JSON envelope overhead.

func Doctor

func Doctor(run DoctorRunner) http.HandlerFunc

Doctor returns the GET /api/doctor handler.

Response shape (wire contract):

{
  "checks": [
    {"name": "dep:tmux", "status": "ok", "message": "/usr/bin/tmux"},
    {"name": "env:PATH", "status": "ok", "message": "set"},
    {"name": "serve:token", "status": "warn", "message": "...", "remediation": "..."}
  ]
}

Auth wrapping happens at server boot (see server.go).

func Feed

func Feed(src FeedSource, filter string) http.HandlerFunc

Feed returns the GET /api/feed handler (filter == "" → global ring) or, when filter is non-empty, GET /api/sessions/{filter}/feed.

Emits ONLY `tool_call` events. Other event types live in the same ring (quota_update, attention_*, session lifecycle) but the feed is a human-readable tool-call transcript — filtering here keeps the contract narrow.

Response shape: array of the same payload the SSE tool_call event carries, newest-first ordering so the client does not have to reverse when it appends new live events.

[
  {"session":"ctm","tool":"Edit","input":"...","summary":"...",
   "is_error":false,"ts":"2026-04-21T14:33:09Z"},
  ...
]

Returns 200 with an empty array when the ring is empty — lets the client distinguish "no history yet" from an error.

func FeedHistory

func FeedHistory(logDir string, resolver UUIDNameResolver) http.HandlerFunc

FeedHistory returns the GET /api/sessions/{name}/feed/history handler. Reads the session's <uuid>.jsonl (uuid resolved via `resolver`) in reverse and returns up to `limit` tool_call events strictly older than `before`.

func Forget

func Forget(store SessionStore, proj ProjRefresher) http.HandlerFunc

Forget returns POST /api/sessions/{name}/forget. Removes the session from sessions.json but keeps the JSONL log so the user can still search history. Body must be `{"confirm":"<name>"}` (B).

func Get

Get returns GET /api/sessions/{name} — a single sessionView, or 404. Uses the Go 1.22+ http.ServeMux path-pattern variable {name}.

func Health

func Health(version, headerName string, startedAt time.Time, hub HealthHubStats) http.HandlerFunc

Health returns the rich, component-level health endpoint. Surfaces hub stats (subscriber count, publish/drop totals, ring sizes) so "is anyone subscribed to /events/all?" is observable from outside.

func Healthz

func Healthz(version, headerName string, startedAt time.Time) http.HandlerFunc

Healthz returns the unauthenticated liveness endpoint. The headerName (typically "X-Ctm-Serve") is set to version on every response so the single-instance guard can identify a sibling daemon portably without /proc/<pid>/cmdline.

func Hooks

func Hooks(mgr *ingest.TailerManager, hub *events.Hub) http.HandlerFunc

Hooks returns the handler for `POST /api/hooks/:event`. It accepts form-encoded payloads from `proc.PostEvent` (the in-process helper added in Step 7) co-located with each `fireHook` call site.

The handler:

  • Spawns a tailer on `session_new` (if `name` and `uuid` are present).
  • Stops the tailer on `session_killed`.
  • Republishes every event onto the hub so SSE clients see the lifecycle in their feed.

func Input

Input returns POST /api/sessions/{name}/input. Gated on mode=yolo + tmux_alive; refuses otherwise with a structured error.

func Kill

func Kill(store SessionStore, tmuxClient TmuxMutator, proj ProjRefresher) http.HandlerFunc

Kill returns POST /api/sessions/{name}/kill. Body must be `{"confirm":"<session-name>"}` matching the path param (B in the design doc). Runs tmux kill-session and triggers a projection refresh. Returns 200 + the updated session JSON.

func List

List returns GET /api/sessions — the full sessionView slice.

func LogsUsage

func LogsUsage(logDir string, resolver UUIDNameResolver) http.HandlerFunc

LogsUsage returns the GET /api/logs/usage handler. logDir is the absolute path of the directory the tailer watches (normally ~/.config/ctm/logs). resolver maps log UUID → human session name; pass nil to disable resolution (every row falls back to the "uuid:<short>" placeholder).

func PaneStream

func PaneStream(tmux TmuxPaneCapturer) http.HandlerFunc

PaneStream returns a GET /events/session/{name}/pane handler that streams a live capture of the named tmux pane as SSE.

Behaviour:

  • Emits one `event: pane` frame per tick (1 Hz) whose `data` is a JSON-encoded string containing the raw capture (escape sequences preserved).
  • Debounces identical payloads — a tick whose capture matches the last emitted payload is skipped. Keeps the stream quiet when the pane is idle.
  • Emits a single initial frame on connect so the UI has something to render immediately (no 1s blank state).
  • Exits cleanly when the client disconnects (r.Context().Done()) or when the pane disappears (CapturePaneHistory returns an error twice in a row — we tolerate one transient miss).
  • `?history=<N>` query param overrides the default scrollback window. Clamped to [0, maxPaneScrollback]. 0 = visible-only.

func Quota

func Quota(src QuotaSource) http.HandlerFunc

Quota returns the GET /api/quota handler. Shape mirrors the SSE `quota_update` global payload exactly so the SPA can feed the same TanStack cache key from both sources:

{"weekly_pct":46,"five_hr_pct":3,
 "weekly_resets_at":"2026-04-22T13:00:00Z",
 "five_hr_resets_at":"2026-04-21T18:00:00Z"}

If no statusline dump has populated rate limits yet, responds 204 No Content so the client leaves the cache null and renders "—" placeholders — matches spec §5 ("bars render before first event").

func Rename

func Rename(store SessionStore, tmuxClient TmuxMutator, proj ProjRefresher) http.HandlerFunc

Rename returns POST /api/sessions/{name}/rename. Body: `{"to":"<new-name>"}`. Does not require type-to-confirm — rename is recoverable. Performs the tmux rename-session first so a name collision fails loudly before sessions.json gets touched.

func RequireOrigin

func RequireOrigin(allowed []string, h http.Handler) http.Handler

RequireOrigin wraps h with a check that the Origin request header matches one of allowed. An empty Origin is rejected — same-origin fetches set it reliably in modern browsers, and non-browser callers (curl, ctm CLI) can set it explicitly. Missing/mismatched → 403.

func RequireOriginFunc

func RequireOriginFunc(allowed []string, h http.HandlerFunc) http.HandlerFunc

RequireOriginFunc is the HandlerFunc-flavoured variant — convenient when wrapping the return of another HandlerFunc inline.

func Revert

func Revert(
	resolveWorkdir func(name string) (string, bool),
	allowedSHA func(name, sha string) bool,
) http.HandlerFunc

Revert returns the POST handler for /api/sessions/{name}/revert.

resolveWorkdir maps a session name to its workdir (false → 404). allowedSHA reports whether `sha` appears in the corresponding `/checkpoints` listing for `name` — the sole guard preventing a caller from `git reset --hard` to an arbitrary SHA.

func Subagents

func Subagents(logDir string, resolver UUIDNameResolver) http.HandlerFunc

Subagents returns the GET handler for /api/sessions/{name}/subagents. logDir is the JSONL tailer directory; resolver maps session name → log UUID via the same `claudeDirToName` fallback used elsewhere in the api package.

func Teams

func Teams(logDir string, resolver UUIDNameResolver) http.HandlerFunc

Teams returns GET /api/sessions/{name}/teams.

func ToolCallDetail

func ToolCallDetail(reader LogReader) http.HandlerFunc

ToolCallDetail returns the handler for GET /api/sessions/{name}/tool_calls/{id}/detail.

reader is the seam described on LogReader. Responses are always application/json. Errors map as:

  • 405 on non-GET
  • 400 on empty name / id
  • 404 on ErrDetailNotFound
  • 500 on any other reader error

Types

type Attention

type Attention struct {
	State   string    `json:"state"`
	Since   time.Time `json:"since"`
	Details string    `json:"details,omitempty"`
}

Attention mirrors the spec §6 Session.attention sub-object. Values come from a future attention engine; for now the enricher always reports "no value" and the field is omitted.

type CheckpointsCache

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

CheckpointsCache wraps the per-session 5 s TTL cache used by the /checkpoints handler so other callers — notably the revert SHA-allowlist check — can reuse the same cached list rather than each spinning up its own `git log` subprocess.

The zero value is ready to use; NewCheckpointsCache exists for symmetry with the rest of the api package.

func NewCheckpointsCache

func NewCheckpointsCache() *CheckpointsCache

NewCheckpointsCache constructs a fresh cache. Lister selection is deferred to call time so tests can swap the package-level `checkpointsLister` after construction and still see the change.

func (*CheckpointsCache) Get

func (c *CheckpointsCache) Get(workdir, name string, limit int) ([]git.Checkpoint, error)

Get returns the cached checkpoint list for (name, limit) when fresh, otherwise re-runs the underlying lister against workdir, caches, and returns. Errors propagate without poisoning the cache.

func (*CheckpointsCache) IsCheckpoint

func (c *CheckpointsCache) IsCheckpoint(workdir, name, sha string) bool

IsCheckpoint reports whether sha is one of the *full* commit SHAs returned for (workdir, name) by Get(workdir, name, 200). Comparison is exact-match only — abbreviated SHAs are intentionally rejected because prefix matching would expand the allowlist's blast radius.

type CostPoint

type CostPoint struct {
	TS            time.Time
	Session       string
	InputTokens   int64
	OutputTokens  int64
	CacheTokens   int64
	CostUSDMicros int64
}

CostPoint mirrors store.Point in wire form. Exported so the server.go adapter can type-assert cleanly.

type CostSource

type CostSource interface {
	Range(session string, since, until time.Time) ([]CostPoint, error)
	Totals(since time.Time) (CostTotals, error)
}

CostSource is the subset of store.CostStore the Cost handler depends on. Accepting an interface keeps this package decoupled from the store package (mirrors the QuotaSource / LogsUsage patterns) and lets tests swap in a fake without opening a SQLite file.

type CostTotals

type CostTotals struct {
	InputTokens   int64
	OutputTokens  int64
	CacheTokens   int64
	CostUSDMicros int64
}

CostTotals mirrors store.Totals for the same decoupling reason.

type CreateLookPath

type CreateLookPath interface {
	LookPath(file string) (string, error)
}

CreateLookPath is the seam used for the "claude on PATH" check. exec.LookPath satisfies it in production.

type CreateSpawner

type CreateSpawner interface {
	Spawn(name, workdir string) (session.Session, error)
	// SendInitialPrompt fires a one-shot prompt into the new session
	// after claude has had time to boot. Fire-and-forget; errors are
	// logged but don't fail the create response.
	SendInitialPrompt(name, text string)
}

CreateSpawner is the thin seam into session.Yolo. Keeping it behind an interface lets create_test.go exercise the handler without spawning real tmux + claude.

type Detail

type Detail struct {
	Tool          string `json:"tool"`
	InputJSON     string `json:"input_json"`
	OutputExcerpt string `json:"output_excerpt"`
	TS            string `json:"ts"`
	IsError       bool   `json:"is_error"`
	Diff          string `json:"diff,omitempty"`
}

Detail is the JSON response shape for GET /api/sessions/{name}/tool_calls/{id}/detail.

InputJSON is always the raw `tool_input` sub-object re-encoded as compact JSON so the UI can render it as a code block without having to re-marshal. Diff is only populated when Tool ∈ {Edit,MultiEdit, Write}; empty otherwise.

type DoctorRunner

type DoctorRunner func(ctx context.Context) []doctor.Check

DoctorRunner is the seam the handler uses to produce a []doctor.Check. Tests stub it to force arbitrary status rows; production injects a function that calls doctor.Run(ctx, cfg) with the daemon's Config.

func DefaultDoctorRunner

func DefaultDoctorRunner(cfg config.Config) DoctorRunner

DefaultDoctorRunner is the production adapter: calls doctor.Run with the live Config under ctx. Kept out of Doctor() so tests can inject a deterministic stub via DoctorRunner.

type FeedSource

type FeedSource interface {
	Snapshot(filter string) []events.Event
}

FeedSource is the subset of events.Hub that the Feed handler needs. Accepting an interface keeps the api package decoupled from the hub for tests and avoids a circular import if events ever grows to depend on api.

type HealthHubStats

type HealthHubStats interface {
	Stats() any
}

HealthHubStats lets the daemon inject live hub statistics into the /health response without health.go importing the events package (cycle). server.go wires it via Health().

type InputSessionSource

type InputSessionSource interface {
	Get(name string) (session.Session, bool)
	TmuxAlive(name string) bool
}

InputSessionSource is the narrow slice of the sessions projection the Input handler needs. Get returns the snapshot; TmuxAlive is sourced from the attention engine / live reconcile layer (see internal/serve/attention/engine.go SessionSource for prior art).

type InputTmux

type InputTmux interface {
	SendKeys(target, keys string) error
	SendEnter(target string) error
}

InputTmux is the narrow slice of *tmux.Client the Input handler needs.

type JSONLLogReader

type JSONLLogReader struct {
	LogDir   string
	Resolver UUIDResolver
}

JSONLLogReader is the production LogReader. It maps session name → Claude UUID, scans ~/.config/ctm/logs/<uuid>.jsonl from end-to-start matching on the hub Event.ID's nanosecond prefix against each line's `ctm_timestamp` (or, lacking that, by falling back to the newest line within the scan cap).

func NewJSONLLogReader

func NewJSONLLogReader(logDir string, proj *ingest.Projection) *JSONLLogReader

NewJSONLLogReader wires a production reader against the tailer log directory and the ingest projection.

func (*JSONLLogReader) ReadDetail

func (r *JSONLLogReader) ReadDetail(sessionName, id string) (Detail, error)

ReadDetail implements LogReader. Errors other than ErrDetailNotFound are I/O-level and surface as 500.

type LogReader

type LogReader interface {
	// ReadDetail returns the Detail for (sessionName, id). On a
	// clean miss it must return ErrDetailNotFound so the handler can
	// emit a 404 without logging a 5xx.
	ReadDetail(sessionName, id string) (Detail, error)
}

LogReader is the seam the handler talks to. Production wires JSONLLogReader; tests pass a fake. Keeping this narrow means the handler test doesn't need to touch the filesystem.

type NoopEnricher

type NoopEnricher struct{}

NoopEnricher reports no values for every field. Useful as the default while the underlying ingestors are still being built.

func (NoopEnricher) Attention

func (NoopEnricher) Attention(string) (Attention, bool)

func (NoopEnricher) ContextPct

func (NoopEnricher) ContextPct(string) (int, bool)

func (NoopEnricher) LastToolCallAt

func (NoopEnricher) LastToolCallAt(string) (time.Time, bool)

func (NoopEnricher) Tokens

func (NoopEnricher) Tokens(string) (TokenUsage, bool)

type ProjRefresher

type ProjRefresher interface {
	Reload()
}

ProjRefresher triggers a projection reload after state mutations so /api/sessions reflects the new truth without waiting for the next polling tick.

type QuotaSnapshot

type QuotaSnapshot struct {
	WeeklyPct       int
	FiveHourPct     int
	WeeklyResetsAt  time.Time
	FiveHourResetAt time.Time
	Known           bool
}

QuotaSnapshot mirrors ingest.GlobalSnapshot so the api package doesn't need to import internal/serve/ingest (mirrors the pattern used by hubStatsAdapter in server.go). Exported because the quotaSourceAdapter in server.go converts between the ingest snapshot and this shape at the package boundary.

type QuotaSource

type QuotaSource interface {
	// Snapshot returns WeeklyPct, FiveHourPct, WeeklyResetsAt,
	// FiveHourResetAt, Known — matching ingest.GlobalSnapshot field
	// order via a struct return.
	Snapshot() QuotaSnapshot
}

QuotaSource is the subset of ingest.QuotaIngester the Quota handler depends on. Accepting an interface lets tests inject a fake without touching fsnotify.

type SessionEnricher

type SessionEnricher interface {
	LastToolCallAt(name string) (time.Time, bool)
	ContextPct(name string) (int, bool)
	Attention(name string) (Attention, bool)
	// Tokens returns the live per-session token breakdown from the
	// last statusline dump's current_usage. ok=false means no dump
	// has been ingested yet for this session.
	Tokens(name string) (TokenUsage, bool)
}

SessionEnricher supplies the per-session fields that the sessions projection cannot derive on its own. Implementations return ok=false to signal "no value yet"; handlers omit those fields from the JSON.

Stable interface — later steps plug in real implementations (tool-call tailer, statusline-dump quota ingest, attention engine) without changing this signature.

type SessionStore

type SessionStore interface {
	Get(name string) (*session.Session, error)
	Delete(name string) error
	Rename(oldName, newName string) error
}

SessionStore is the narrow slice of *session.Store the mutation handlers need. A package-local interface keeps the api package decoupled from the concrete store and makes the handlers trivially faked in tests.

type SubagentNode

type SubagentNode struct {
	ID          string     `json:"id"`
	ParentID    *string    `json:"parent_id"`
	Type        string     `json:"type"`
	Description string     `json:"description"`
	StartedAt   time.Time  `json:"started_at"`
	StoppedAt   *time.Time `json:"stopped_at,omitempty"`
	ToolCalls   int        `json:"tool_calls"`
	Status      string     `json:"status"`
}

SubagentNode is a single row in the /subagents response. ParentID is a pointer so JSON emits `null` when absent (rather than the zero string "").

type Team

type Team struct {
	ID           string       `json:"id"`
	Name         string       `json:"name"`
	DispatchedAt time.Time    `json:"dispatched_at"`
	Status       string       `json:"status"`
	Summary      *string      `json:"summary,omitempty"`
	Members      []TeamMember `json:"members"`
}

Team is a single row in the /teams response.

type TeamMember

type TeamMember struct {
	SubagentID  string `json:"subagent_id"`
	Description string `json:"description"`
	Status      string `json:"status"`
}

TeamMember mirrors a single row in team.members.

type TmuxMutator

type TmuxMutator interface {
	KillSession(name string) error
	RenameSession(oldName, newName string) error
}

TmuxMutator is the narrow slice of *tmux.Client that kill / rename need. Mirrors the same decoupling pattern used by TmuxPaneCapturer.

type TmuxPaneCapturer

type TmuxPaneCapturer interface {
	// CapturePaneHistory returns the raw output of
	//   tmux capture-pane -e -p -J -t <name> -S -<scrollback>
	// scrollback lines above the visible pane, with -e preserving
	// SGR, -p writing to stdout, and -J joining wrapped lines.
	CapturePaneHistory(name string, scrollback int) (string, error)
}

TmuxPaneCapturer is the narrow slice of *tmux.Client this handler needs. A package-local interface keeps the api package decoupled from internal/tmux (which would otherwise pull os/exec into every api test binary) and makes the handler trivially faked.

type TokenUsage

type TokenUsage struct {
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`
	// CacheTokens is creation + read from the statusline dump; the UI
	// doesn't distinguish, so we collapse them at ingest time.
	CacheTokens int `json:"cache_tokens"`
}

TokenUsage mirrors the statusline dump's `context_window.current_usage` payload the ingester captures per session. All three fields are current-turn counts, not cumulative session totals.

type UUIDNameResolver

type UUIDNameResolver interface {
	ResolveUUID(uuid string) (name string, ok bool)
}

UUIDNameResolver returns the human session name for a given log UUID. Implementations should encapsulate the uuidToName + claudeDirToName fallback lookup so the handler stays decoupled from ingest internals.

ok=false signals "unknown UUID"; the handler falls back to a "uuid:<short>" placeholder identical to the orphan-adoption path.

type UUIDResolver

type UUIDResolver interface {
	ResolveName(sessionName string) (uuid string, ok bool)
}

UUIDResolver maps a human session name to its Claude session UUID. Narrower than ingest.Projection so the reader stays testable.

Jump to

Keyboard shortcuts

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