From 6c5b6a5af7eef8b6df2c4b66b0854bcb45aa2342 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Thu, 25 Dec 2025 23:23:46 +0800 Subject: [PATCH 1/3] fix(model): normalize CCOtel debug flag to use consistent truthy variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- model/config.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/model/config.go b/model/config.go index f9b2bdb..bc0c837 100644 --- a/model/config.go +++ b/model/config.go @@ -182,6 +182,10 @@ func (cs *configService) ReadConfigFile(ctx context.Context, opts ...ReadConfigO if config.CCOtel != nil && config.CCOtel.GRPCPort == 0 { config.CCOtel.GRPCPort = 54027 // default OTEL gRPC port } + + if config.CCOtel != nil && config.CCOtel.Debug != nil && *config.CCOtel.Debug { + config.CCOtel.Debug = &truthy + } if config.SocketPath == "" { config.SocketPath = DefaultSocketPath } From e9344c49ea8086e545bece6631d2a4ad6a213b8d Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Thu, 25 Dec 2025 23:39:15 +0800 Subject: [PATCH 2/3] feat(daemon): add version command with config debug info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add -v/--version flags to shelltime-daemon that display version info and current config state without starting any services. This helps diagnose configuration issues by showing: - Version, commit, build date - Go version and OS/Arch - Socket path and API endpoint - CCOtel, CCUsage, LogCleanup, CodeTracking config states Also add debug log when writing CCOtel debug files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/daemon/main.go | 83 ++++++++++++++++++++++++++++++++++++++ daemon/ccotel_processor.go | 1 + 2 files changed, 84 insertions(+) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 0e2a693..59993ff 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -2,10 +2,12 @@ package main import ( "context" + "flag" "fmt" "log/slog" "os" "os/signal" + "runtime" "syscall" "github.com/ThreeDotsLabs/watermill" @@ -26,6 +28,16 @@ var ( ) func main() { + // Handle version flag first, before any service initialization + showVersion := flag.Bool("v", false, "Show version information") + showVersionLong := flag.Bool("version", false, "Show version information") + flag.Parse() + + if *showVersion || *showVersionLong { + printVersionInfo() + return + } + l := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ AddSource: true, Level: slog.LevelDebug, @@ -154,3 +166,74 @@ func main() { pubsub.Close() processor.Stop() } + +func printVersionInfo() { + fmt.Printf("shelltime-daemon %s\n", version) + fmt.Printf(" Commit: %s\n", commit) + fmt.Printf(" Build Date: %s\n", date) + fmt.Printf(" Go Version: %s\n", runtime.Version()) + fmt.Printf(" OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH) + + // Read config and show debug info + ctx := context.Background() + configFile := os.ExpandEnv(fmt.Sprintf("%s/%s/%s", "$HOME", model.COMMAND_BASE_STORAGE_FOLDER, "config.toml")) + configService := model.NewConfigService(configFile) + cfg, err := configService.ReadConfigFile(ctx) + if err != nil { + fmt.Printf("\nConfig: error reading config: %v\n", err) + return + } + + fmt.Println("\nConfig:") + fmt.Printf(" Socket Path: %s\n", cfg.SocketPath) + fmt.Printf(" API Endpoint: %s\n", cfg.APIEndpoint) + + // CCOtel config + if cfg.CCOtel != nil { + fmt.Println("\nCCOtel:") + if cfg.CCOtel.Enabled != nil { + fmt.Printf(" Enabled: %v\n", *cfg.CCOtel.Enabled) + } else { + fmt.Printf(" Enabled: \n") + } + fmt.Printf(" gRPC Port: %d\n", cfg.CCOtel.GRPCPort) + if cfg.CCOtel.Debug != nil { + fmt.Printf(" Debug: %v\n", *cfg.CCOtel.Debug) + } else { + fmt.Printf(" Debug: \n") + } + } else { + fmt.Println("\nCCOtel: ") + } + + // CCUsage config + if cfg.CCUsage != nil { + fmt.Println("\nCCUsage:") + if cfg.CCUsage.Enabled != nil { + fmt.Printf(" Enabled: %v\n", *cfg.CCUsage.Enabled) + } else { + fmt.Printf(" Enabled: \n") + } + } + + // LogCleanup config + if cfg.LogCleanup != nil { + fmt.Println("\nLogCleanup:") + if cfg.LogCleanup.Enabled != nil { + fmt.Printf(" Enabled: %v\n", *cfg.LogCleanup.Enabled) + } else { + fmt.Printf(" Enabled: \n") + } + fmt.Printf(" ThresholdMB: %d\n", cfg.LogCleanup.ThresholdMB) + } + + // CodeTracking config + if cfg.CodeTracking != nil { + fmt.Println("\nCodeTracking:") + if cfg.CodeTracking.Enabled != nil { + fmt.Printf(" Enabled: %v\n", *cfg.CodeTracking.Enabled) + } else { + fmt.Printf(" Enabled: \n") + } + } +} diff --git a/daemon/ccotel_processor.go b/daemon/ccotel_processor.go index 0b562dc..d44300c 100644 --- a/daemon/ccotel_processor.go +++ b/daemon/ccotel_processor.go @@ -74,6 +74,7 @@ func (p *CCOtelProcessor) writeDebugFile(filename string, data interface{}) { if _, err := f.WriteString(fmt.Sprintf("\n--- %s ---\n%s\n", timestamp, jsonData)); err != nil { slog.Error("CCOtel: Failed to write debug data", "error", err) } + slog.Debug("CCOtel: Wrote debug data", "path", filePath) } // ProcessMetrics receives OTEL metrics and forwards to backend immediately From 59e5d9aa30db4c21605a80e43fb290c450f0f267 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Fri, 26 Dec 2025 00:03:24 +0800 Subject: [PATCH 3/3] feat(daemon): add socket-based status query with uptime tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance `shelltime daemon status` command to query the running daemon via socket for real-time status including: - Version and build info - Uptime (tracked from daemon start) - Go version and platform The daemon now tracks its start time in daemon.Init() and responds to status requests via the Unix socket, providing accurate runtime info. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/daemon/main.go | 65 ++------------------------------------- commands/daemon.status.go | 39 ++++++++++++++++++++--- daemon/base.go | 16 +++++++++- daemon/socket.go | 53 ++++++++++++++++++++++++++++--- 4 files changed, 100 insertions(+), 73 deletions(-) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 59993ff..1dfbef2 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -173,67 +173,6 @@ func printVersionInfo() { fmt.Printf(" Build Date: %s\n", date) fmt.Printf(" Go Version: %s\n", runtime.Version()) fmt.Printf(" OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH) - - // Read config and show debug info - ctx := context.Background() - configFile := os.ExpandEnv(fmt.Sprintf("%s/%s/%s", "$HOME", model.COMMAND_BASE_STORAGE_FOLDER, "config.toml")) - configService := model.NewConfigService(configFile) - cfg, err := configService.ReadConfigFile(ctx) - if err != nil { - fmt.Printf("\nConfig: error reading config: %v\n", err) - return - } - - fmt.Println("\nConfig:") - fmt.Printf(" Socket Path: %s\n", cfg.SocketPath) - fmt.Printf(" API Endpoint: %s\n", cfg.APIEndpoint) - - // CCOtel config - if cfg.CCOtel != nil { - fmt.Println("\nCCOtel:") - if cfg.CCOtel.Enabled != nil { - fmt.Printf(" Enabled: %v\n", *cfg.CCOtel.Enabled) - } else { - fmt.Printf(" Enabled: \n") - } - fmt.Printf(" gRPC Port: %d\n", cfg.CCOtel.GRPCPort) - if cfg.CCOtel.Debug != nil { - fmt.Printf(" Debug: %v\n", *cfg.CCOtel.Debug) - } else { - fmt.Printf(" Debug: \n") - } - } else { - fmt.Println("\nCCOtel: ") - } - - // CCUsage config - if cfg.CCUsage != nil { - fmt.Println("\nCCUsage:") - if cfg.CCUsage.Enabled != nil { - fmt.Printf(" Enabled: %v\n", *cfg.CCUsage.Enabled) - } else { - fmt.Printf(" Enabled: \n") - } - } - - // LogCleanup config - if cfg.LogCleanup != nil { - fmt.Println("\nLogCleanup:") - if cfg.LogCleanup.Enabled != nil { - fmt.Printf(" Enabled: %v\n", *cfg.LogCleanup.Enabled) - } else { - fmt.Printf(" Enabled: \n") - } - fmt.Printf(" ThresholdMB: %d\n", cfg.LogCleanup.ThresholdMB) - } - - // CodeTracking config - if cfg.CodeTracking != nil { - fmt.Println("\nCodeTracking:") - if cfg.CodeTracking.Enabled != nil { - fmt.Printf(" Enabled: %v\n", *cfg.CodeTracking.Enabled) - } else { - fmt.Printf(" Enabled: \n") - } - } + fmt.Println() + fmt.Println("Use 'shelltime daemon status' to check the running daemon status.") } diff --git a/commands/daemon.status.go b/commands/daemon.status.go index 3f2ff63..13f35a5 100644 --- a/commands/daemon.status.go +++ b/commands/daemon.status.go @@ -1,12 +1,14 @@ package commands import ( + "encoding/json" "fmt" "net" "os" "time" "github.com/gookit/color" + "github.com/malamtime/cli/daemon" "github.com/malamtime/cli/model" "github.com/urfave/cli/v2" ) @@ -37,8 +39,9 @@ func commandDaemonStatus(c *cli.Context) error { printError(fmt.Sprintf("Socket file does not exist at %s", socketPath)) } - // Check 2: Socket connectivity - connected, latency, connErr := checkSocketConnection(socketPath, 2*time.Second) + // Check 2: Socket connectivity and get status + statusResp, latency, connErr := requestDaemonStatus(socketPath, 2*time.Second) + connected := statusResp != nil if connected { printSuccess(fmt.Sprintf("Daemon is responding (latency: %v)", latency.Round(time.Microsecond))) } else { @@ -59,6 +62,15 @@ func commandDaemonStatus(c *cli.Context) error { } } + // Daemon info section (if connected) + if statusResp != nil { + printSectionHeader("Daemon Info") + fmt.Printf(" Version: %s\n", statusResp.Version) + fmt.Printf(" Uptime: %s (since %s)\n", statusResp.Uptime, statusResp.StartedAt.Format("2006-01-02 15:04:05")) + fmt.Printf(" Go Version: %s\n", statusResp.GoVersion) + fmt.Printf(" Platform: %s\n", statusResp.Platform) + } + // Configuration section printSectionHeader("Configuration") fmt.Printf(" Socket Path: %s\n", socketPath) @@ -97,13 +109,30 @@ func checkSocketFileExists(socketPath string) bool { return err == nil } -func checkSocketConnection(socketPath string, timeout time.Duration) (bool, time.Duration, error) { +func requestDaemonStatus(socketPath string, timeout time.Duration) (*daemon.StatusResponse, time.Duration, error) { start := time.Now() conn, err := net.DialTimeout("unix", socketPath, timeout) if err != nil { - return false, 0, err + return nil, 0, err } defer conn.Close() + + // Send status request + msg := daemon.SocketMessage{ + Type: daemon.SocketMessageTypeStatus, + } + encoder := json.NewEncoder(conn) + if err := encoder.Encode(msg); err != nil { + return nil, 0, err + } + + // Read response + var response daemon.StatusResponse + decoder := json.NewDecoder(conn) + if err := decoder.Decode(&response); err != nil { + return nil, 0, err + } + latency := time.Since(start) - return true, latency, nil + return &response, latency, nil } diff --git a/daemon/base.go b/daemon/base.go index db7331d..82787c1 100644 --- a/daemon/base.go +++ b/daemon/base.go @@ -1,9 +1,14 @@ package daemon -import "github.com/malamtime/cli/model" +import ( + "time" + + "github.com/malamtime/cli/model" +) var stConfig model.ConfigService var version string +var startedAt time.Time const ( PubSubTopic = "socket" @@ -12,4 +17,13 @@ const ( func Init(cs model.ConfigService, vs string) { stConfig = cs version = vs + startedAt = time.Now() +} + +func GetStartedAt() time.Time { + return startedAt +} + +func GetVersion() string { + return version } diff --git a/daemon/socket.go b/daemon/socket.go index eec5c00..9d3e182 100644 --- a/daemon/socket.go +++ b/daemon/socket.go @@ -2,9 +2,12 @@ package daemon import ( "encoding/json" + "fmt" "log/slog" "net" "os" + "runtime" + "time" "github.com/ThreeDotsLabs/watermill" "github.com/ThreeDotsLabs/watermill/message" @@ -16,8 +19,18 @@ type SocketMessageType string const ( SocketMessageTypeSync SocketMessageType = "sync" SocketMessageTypeHeartbeat SocketMessageType = "heartbeat" + SocketMessageTypeStatus SocketMessageType = "status" ) +// StatusResponse contains daemon status information +type StatusResponse struct { + Version string `json:"version"` + StartedAt time.Time `json:"startedAt"` + Uptime string `json:"uptime"` + GoVersion string `json:"goVersion"` + Platform string `json:"platform"` +} + type SocketMessage struct { Type SocketMessageType `json:"type"` // if parse from buffer, it will be the map[any]any @@ -99,10 +112,8 @@ func (p *SocketHandler) handleConnection(conn net.Conn) { } switch msg.Type { - // case "status": - // p.handleStatus(conn) - // case "track": - // p.handleTrack(conn, msg.Payload) + case SocketMessageTypeStatus: + p.handleStatus(conn) case SocketMessageTypeSync: buf, err := json.Marshal(msg) if err != nil { @@ -133,3 +144,37 @@ func (p *SocketHandler) handleConnection(conn net.Conn) { slog.Error("Unknown message type:", slog.String("messageType", string(msg.Type))) } } + +func (p *SocketHandler) handleStatus(conn net.Conn) { + uptime := time.Since(startedAt) + response := StatusResponse{ + Version: version, + StartedAt: startedAt, + Uptime: formatDuration(uptime), + GoVersion: runtime.Version(), + Platform: runtime.GOOS + "/" + runtime.GOARCH, + } + + encoder := json.NewEncoder(conn) + if err := encoder.Encode(response); err != nil { + slog.Error("Error encoding status response", slog.Any("err", err)) + } +} + +func formatDuration(d time.Duration) string { + days := int(d.Hours() / 24) + hours := int(d.Hours()) % 24 + minutes := int(d.Minutes()) % 60 + seconds := int(d.Seconds()) % 60 + + if days > 0 { + return fmt.Sprintf("%dd %dh %dm %ds", days, hours, minutes, seconds) + } + if hours > 0 { + return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds) + } + if minutes > 0 { + return fmt.Sprintf("%dm %ds", minutes, seconds) + } + return fmt.Sprintf("%ds", seconds) +}