mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
77f5f02510
march-improvements
275 lines
7.4 KiB
Go
275 lines
7.4 KiB
Go
// Package main provides the statusline hook for Claude Code.
|
|
// This binary outputs a status line showing claude-mnemonic metrics.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/lukaszraczylo/claude-mnemonic/pkg/hooks"
|
|
)
|
|
|
|
// StatusInput is the JSON input from Claude Code's statusline feature.
|
|
type StatusInput struct {
|
|
HookEventName string `json:"hook_event_name"`
|
|
SessionID string `json:"session_id"`
|
|
CWD string `json:"cwd"`
|
|
Model struct {
|
|
ID string `json:"id"`
|
|
DisplayName string `json:"display_name"`
|
|
} `json:"model"`
|
|
Workspace struct {
|
|
CurrentDir string `json:"current_dir"`
|
|
ProjectDir string `json:"project_dir"`
|
|
} `json:"workspace"`
|
|
Version string `json:"version"`
|
|
Cost struct {
|
|
TotalCostUSD float64 `json:"total_cost_usd"`
|
|
TotalDurationMS int64 `json:"total_duration_ms"`
|
|
TotalAPIDurationMS int64 `json:"total_api_duration_ms"`
|
|
TotalLinesAdded int `json:"total_lines_added"`
|
|
TotalLinesRemoved int `json:"total_lines_removed"`
|
|
} `json:"cost"`
|
|
ContextWindow struct {
|
|
TotalInputTokens int `json:"total_input_tokens"`
|
|
TotalOutputTokens int `json:"total_output_tokens"`
|
|
ContextWindowSize int `json:"context_window_size"`
|
|
} `json:"context_window"`
|
|
}
|
|
|
|
// WorkerStats is the response from the worker's /api/stats endpoint.
|
|
type WorkerStats struct {
|
|
Uptime string `json:"uptime"`
|
|
Project string `json:"project,omitempty"`
|
|
Retrieval struct {
|
|
TotalRequests int64 `json:"TotalRequests"`
|
|
ObservationsServed int64 `json:"ObservationsServed"`
|
|
SearchRequests int64 `json:"SearchRequests"`
|
|
ContextInjections int64 `json:"ContextInjections"`
|
|
} `json:"retrieval"`
|
|
ActiveSessions int `json:"activeSessions"`
|
|
QueueDepth int `json:"queueDepth"`
|
|
ConnectedClients int `json:"connectedClients"`
|
|
SessionsToday int `json:"sessionsToday"`
|
|
ProjectObservations int `json:"projectObservations,omitempty"`
|
|
IsProcessing bool `json:"isProcessing"`
|
|
Ready bool `json:"ready"`
|
|
}
|
|
|
|
// ANSI color codes
|
|
const (
|
|
colorReset = "\033[0m"
|
|
colorGreen = "\033[32m"
|
|
colorYellow = "\033[33m"
|
|
colorCyan = "\033[36m"
|
|
colorGray = "\033[90m"
|
|
colorRed = "\033[31m"
|
|
)
|
|
|
|
func main() {
|
|
hooks.RunStatuslineHook(handleStatusline)
|
|
}
|
|
|
|
func handleStatusline(input *StatusInput, port int) string {
|
|
// Handle error cases (nil input)
|
|
if input == nil {
|
|
return formatOffline()
|
|
}
|
|
|
|
// Determine project directory
|
|
projectDir := input.Workspace.ProjectDir
|
|
if projectDir == "" {
|
|
projectDir = input.Workspace.CurrentDir
|
|
}
|
|
if projectDir == "" {
|
|
projectDir = input.CWD
|
|
}
|
|
|
|
// Generate project ID
|
|
project := ""
|
|
if projectDir != "" {
|
|
project = hooks.ProjectIDWithName(projectDir)
|
|
}
|
|
|
|
// Get worker stats
|
|
stats := getWorkerStats(port, project)
|
|
|
|
// Format and return statusline
|
|
return formatStatusLine(stats, *input)
|
|
}
|
|
|
|
// getWorkerStats fetches stats from the worker service.
|
|
func getWorkerStats(port int, project string) *WorkerStats {
|
|
// Build URL with optional project parameter
|
|
endpoint := fmt.Sprintf("http://127.0.0.1:%d/api/stats", port)
|
|
if project != "" {
|
|
endpoint += "?project=" + url.QueryEscape(project)
|
|
}
|
|
|
|
// Create HTTP client with short timeout (statusline must be fast)
|
|
client := &http.Client{Timeout: 100 * time.Millisecond}
|
|
|
|
resp, err := client.Get(endpoint)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil
|
|
}
|
|
|
|
var stats WorkerStats
|
|
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
|
|
return nil
|
|
}
|
|
|
|
return &stats
|
|
}
|
|
|
|
// formatStatusLine formats the status line output.
|
|
func formatStatusLine(stats *WorkerStats, input StatusInput) string {
|
|
// Check if colors are enabled (default: yes, unless TERM is dumb or NO_COLOR is set)
|
|
useColors := os.Getenv("NO_COLOR") == "" && os.Getenv("TERM") != "dumb"
|
|
if os.Getenv("CLAUDE_MNEMONIC_STATUSLINE_COLORS") == "false" {
|
|
useColors = false
|
|
} else if os.Getenv("CLAUDE_MNEMONIC_STATUSLINE_COLORS") == "true" {
|
|
useColors = true
|
|
}
|
|
|
|
// Check format preference
|
|
format := os.Getenv("CLAUDE_MNEMONIC_STATUSLINE_FORMAT")
|
|
if format == "" {
|
|
format = "default"
|
|
}
|
|
|
|
if stats == nil {
|
|
return formatOfflineColored(useColors)
|
|
}
|
|
|
|
if !stats.Ready {
|
|
return formatStartingColored(useColors)
|
|
}
|
|
|
|
switch format {
|
|
case "compact":
|
|
return formatCompact(stats, useColors)
|
|
case "minimal":
|
|
return formatMinimal(stats, useColors)
|
|
default:
|
|
return formatDefault(stats, useColors)
|
|
}
|
|
}
|
|
|
|
// formatDefault returns the default status line format.
|
|
func formatDefault(stats *WorkerStats, useColors bool) string {
|
|
// [mnemonic] ● served:42 | injected:5 | searches:3 | project:28 memories
|
|
var prefix, indicator, reset string
|
|
if useColors {
|
|
prefix = colorCyan + "[mnemonic]" + colorReset
|
|
indicator = colorGreen + "●" + colorReset
|
|
reset = colorReset
|
|
} else {
|
|
prefix = "[mnemonic]"
|
|
indicator = "●"
|
|
}
|
|
|
|
// Build status parts with clear labels
|
|
parts := []string{
|
|
prefix,
|
|
indicator,
|
|
}
|
|
|
|
// Add retrieval stats if available
|
|
if stats.Retrieval.ObservationsServed > 0 {
|
|
parts = append(parts, fmt.Sprintf("served:%d", stats.Retrieval.ObservationsServed))
|
|
}
|
|
if stats.Retrieval.ContextInjections > 0 {
|
|
parts = append(parts, fmt.Sprintf("injected:%d", stats.Retrieval.ContextInjections))
|
|
}
|
|
if stats.Retrieval.SearchRequests > 0 {
|
|
parts = append(parts, fmt.Sprintf("searches:%d", stats.Retrieval.SearchRequests))
|
|
}
|
|
|
|
// Add project-specific observation count if available
|
|
if stats.ProjectObservations > 0 {
|
|
parts = append(parts, fmt.Sprintf("project:%d memories", stats.ProjectObservations))
|
|
}
|
|
|
|
// Join with separators
|
|
result := parts[0] + " " + parts[1]
|
|
if len(parts) > 2 {
|
|
for i := 2; i < len(parts); i++ {
|
|
if useColors {
|
|
result += colorGray + " | " + reset + parts[i]
|
|
} else {
|
|
result += " | " + parts[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// formatCompact returns a compact status line format.
|
|
func formatCompact(stats *WorkerStats, useColors bool) string {
|
|
// [m] ● 42/5/3
|
|
var prefix, indicator string
|
|
if useColors {
|
|
prefix = colorCyan + "[m]" + colorReset
|
|
indicator = colorGreen + "●" + colorReset
|
|
} else {
|
|
prefix = "[m]"
|
|
indicator = "●"
|
|
}
|
|
|
|
return fmt.Sprintf("%s %s %d/%d/%d",
|
|
prefix, indicator,
|
|
stats.Retrieval.ObservationsServed,
|
|
stats.Retrieval.ContextInjections,
|
|
stats.Retrieval.SearchRequests)
|
|
}
|
|
|
|
// formatMinimal returns a minimal status line format.
|
|
func formatMinimal(stats *WorkerStats, useColors bool) string {
|
|
// ● 28 memories
|
|
var indicator string
|
|
if useColors {
|
|
indicator = colorGreen + "●" + colorReset
|
|
} else {
|
|
indicator = "●"
|
|
}
|
|
|
|
if stats.ProjectObservations > 0 {
|
|
return fmt.Sprintf("%s %d memories", indicator, stats.ProjectObservations)
|
|
}
|
|
|
|
return fmt.Sprintf("%s mnemonic ready", indicator)
|
|
}
|
|
|
|
// formatOffline returns status for when worker is offline.
|
|
func formatOffline() string {
|
|
useColors := os.Getenv("NO_COLOR") == "" && os.Getenv("TERM") != "dumb"
|
|
return formatOfflineColored(useColors)
|
|
}
|
|
|
|
// formatOfflineColored returns colored offline status.
|
|
func formatOfflineColored(useColors bool) string {
|
|
if useColors {
|
|
return colorGray + "[mnemonic]" + colorReset + " " + colorGray + "○" + colorReset + " offline"
|
|
}
|
|
return "[mnemonic] ○ offline"
|
|
}
|
|
|
|
// formatStartingColored returns colored starting status.
|
|
func formatStartingColored(useColors bool) string {
|
|
if useColors {
|
|
return colorYellow + "[mnemonic]" + colorReset + " " + colorYellow + "○" + colorReset + " starting..."
|
|
}
|
|
return "[mnemonic] ○ starting..."
|
|
}
|