From f4f8fb90eb0cb40957443ace05a11e399cb3d04f Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Sun, 4 Jan 2026 13:51:29 +0800 Subject: [PATCH 1/4] feat(cli): add config view command with JSON output support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new `config view` subcommand to display the current configuration. Supports both table (default) and JSON output formats via --format/-f flag. Table format masks sensitive token fields for security. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/cli/main.go | 1 + commands/config.go | 11 +++ commands/config_view.go | 167 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 commands/config.go create mode 100644 commands/config_view.go diff --git a/cmd/cli/main.go b/cmd/cli/main.go index d9ffc8b..a8e8225 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -110,6 +110,7 @@ func main() { commands.CodexCommand, commands.SchemaCommand, commands.GrepCommand, + commands.ConfigCommand, } err = app.Run(os.Args) if err != nil { diff --git a/commands/config.go b/commands/config.go new file mode 100644 index 0000000..d4545b4 --- /dev/null +++ b/commands/config.go @@ -0,0 +1,11 @@ +package commands + +import "github.com/urfave/cli/v2" + +var ConfigCommand *cli.Command = &cli.Command{ + Name: "config", + Usage: "manage shelltime configuration", + Subcommands: []*cli.Command{ + ConfigViewCommand, + }, +} diff --git a/commands/config_view.go b/commands/config_view.go new file mode 100644 index 0000000..7fadaef --- /dev/null +++ b/commands/config_view.go @@ -0,0 +1,167 @@ +package commands + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "strings" + + "github.com/gookit/color" + "github.com/malamtime/cli/model" + "github.com/olekukonko/tablewriter" + "github.com/urfave/cli/v2" + "go.opentelemetry.io/otel/trace" +) + +var ConfigViewCommand *cli.Command = &cli.Command{ + Name: "view", + Usage: "view current configuration", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "format", + Aliases: []string{"f"}, + Value: "table", + Usage: "output format (table/json)", + }, + }, + Action: configView, + OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error { + color.Red.Println(err.Error()) + return nil + }, +} + +func configView(c *cli.Context) error { + ctx, span := commandTracer.Start(c.Context, "config.view", trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + SetupLogger(os.ExpandEnv("$HOME/" + model.COMMAND_BASE_STORAGE_FOLDER)) + + format := c.String("format") + if format != "table" && format != "json" { + return fmt.Errorf("unsupported format: %s. Use 'table' or 'json'", format) + } + + cfg, err := configService.ReadConfigFile(ctx) + if err != nil { + return fmt.Errorf("failed to read config: %w", err) + } + + if format == "json" { + return outputConfigJSON(cfg) + } + return outputConfigTable(cfg) +} + +func outputConfigJSON(cfg model.ShellTimeConfig) error { + jsonData, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + fmt.Println(string(jsonData)) + return nil +} + +func outputConfigTable(cfg model.ShellTimeConfig) error { + w := tablewriter.NewWriter(os.Stdout) + w.Header([]string{"KEY", "VALUE"}) + + pairs := flattenConfig(cfg, "") + for _, pair := range pairs { + w.Append([]string{pair.key, pair.value}) + } + + w.Render() + return nil +} + +type keyValuePair struct { + key string + value string +} + +func flattenConfig(v interface{}, prefix string) []keyValuePair { + var pairs []keyValuePair + + val := reflect.ValueOf(v) + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return pairs + } + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + return pairs + } + + typ := val.Type() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + // Get JSON tag for the key name + jsonTag := fieldType.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + continue + } + // Parse the tag to get just the name part + tagParts := strings.Split(jsonTag, ",") + keyName := tagParts[0] + + fullKey := keyName + if prefix != "" { + fullKey = prefix + "." + keyName + } + + // Handle pointer types + if field.Kind() == reflect.Ptr { + if field.IsNil() { + pairs = append(pairs, keyValuePair{key: fullKey, value: ""}) + continue + } + field = field.Elem() + } + + switch field.Kind() { + case reflect.Struct: + // Recursively flatten nested structs + nestedPairs := flattenConfig(field.Interface(), fullKey) + pairs = append(pairs, nestedPairs...) + case reflect.Slice: + if field.Len() == 0 { + pairs = append(pairs, keyValuePair{key: fullKey, value: "[]"}) + } else { + // Marshal slice to JSON for display + jsonBytes, err := json.Marshal(field.Interface()) + if err != nil { + pairs = append(pairs, keyValuePair{key: fullKey, value: fmt.Sprintf("<%d items>", field.Len())}) + } else { + pairs = append(pairs, keyValuePair{key: fullKey, value: string(jsonBytes)}) + } + } + case reflect.Bool: + pairs = append(pairs, keyValuePair{key: fullKey, value: fmt.Sprintf("%v", field.Bool())}) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + pairs = append(pairs, keyValuePair{key: fullKey, value: fmt.Sprintf("%d", field.Int())}) + case reflect.String: + value := field.String() + if value == "" { + value = "" + } else if strings.Contains(fullKey, "token") || strings.Contains(strings.ToLower(fullKey), "token") { + // Mask sensitive fields + if len(value) > 8 { + value = value[:4] + "****" + value[len(value)-4:] + } else { + value = "****" + } + } + pairs = append(pairs, keyValuePair{key: fullKey, value: value}) + default: + pairs = append(pairs, keyValuePair{key: fullKey, value: fmt.Sprintf("%v", field.Interface())}) + } + } + + return pairs +} From b8d3b919a38d15e15811db290d5fb5e66438f0e0 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Sun, 4 Jan 2026 13:53:11 +0800 Subject: [PATCH 2/4] style(commands): apply go fmt formatting 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 --- commands/auth.go | 2 +- commands/dotfiles.go | 2 +- commands/grep.go | 2 +- commands/query.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/commands/auth.go b/commands/auth.go index 7da635c..f3962cb 100644 --- a/commands/auth.go +++ b/commands/auth.go @@ -8,10 +8,10 @@ import ( "os" "time" - "github.com/malamtime/cli/stloader" "github.com/gookit/color" "github.com/invopop/jsonschema" "github.com/malamtime/cli/model" + "github.com/malamtime/cli/stloader" "github.com/pkg/browser" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/trace" diff --git a/commands/dotfiles.go b/commands/dotfiles.go index a867022..28a61ed 100644 --- a/commands/dotfiles.go +++ b/commands/dotfiles.go @@ -41,4 +41,4 @@ var DotfilesCommand *cli.Command = &cli.Command{ OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error { return nil }, -} \ No newline at end of file +} diff --git a/commands/grep.go b/commands/grep.go index 9e05aa7..f30f2d8 100644 --- a/commands/grep.go +++ b/commands/grep.go @@ -8,9 +8,9 @@ import ( "strconv" "time" - "github.com/malamtime/cli/stloader" "github.com/gookit/color" "github.com/malamtime/cli/model" + "github.com/malamtime/cli/stloader" "github.com/olekukonko/tablewriter" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/trace" diff --git a/commands/query.go b/commands/query.go index b924265..9c0ecd2 100644 --- a/commands/query.go +++ b/commands/query.go @@ -9,9 +9,9 @@ import ( "runtime" "strings" - "github.com/malamtime/cli/stloader" "github.com/gookit/color" "github.com/malamtime/cli/model" + "github.com/malamtime/cli/stloader" "github.com/urfave/cli/v2" ) From 0e89dd0b572f2d5cd819459ccb2b8ab6c39e2e0f Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Sun, 4 Jan 2026 14:11:28 +0800 Subject: [PATCH 3/4] fix(daemon): resolve race condition in cc_info timer startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap GetCachedCost() and NotifyActivity() call order to ensure activeRanges is populated before the timer goroutine runs fetchActiveRanges(). Also add debug log for cc_info socket events. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- daemon/socket.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/daemon/socket.go b/daemon/socket.go index 607f242..b496a87 100644 --- a/daemon/socket.go +++ b/daemon/socket.go @@ -194,6 +194,8 @@ func (p *SocketHandler) handleStatus(conn net.Conn) { } func (p *SocketHandler) handleCCInfo(conn net.Conn, msg SocketMessage) { + slog.Debug("cc_info socket event received") + // Parse time range from payload, default to "today" timeRange := CCInfoTimeRangeToday if payload, ok := msg.Payload.(map[string]interface{}); ok { @@ -202,9 +204,9 @@ func (p *SocketHandler) handleCCInfo(conn net.Conn, msg SocketMessage) { } } - // Notify activity and get cached cost - p.ccInfoTimer.NotifyActivity() + // Get cached cost first (marks range as active), then notify activity (starts timer) cache := p.ccInfoTimer.GetCachedCost(timeRange) + p.ccInfoTimer.NotifyActivity() response := CCInfoResponse{ TotalCostUSD: cache.TotalCostUSD, From bdd34fe19491d1140f8567cff72acd01f073fd7c Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Sun, 4 Jan 2026 14:12:18 +0800 Subject: [PATCH 4/4] chore: release 0.1.53 Release-As: 0.1.53