diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 654ad6ae6..816236b0f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -18,10 +18,10 @@ builds: - stdjson ldflags: - -s -w - - -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.version={{ .Version }} - - -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.gitCommit={{ .ShortCommit }} - - -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.buildTime={{ .Date }} - - -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.goVersion={{ .Env.GOVERSION }} + - -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }} + - -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }} + - -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }} + - -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ .Env.GOVERSION }} goos: - linux - windows diff --git a/Makefile b/Makefile index 955c1c966..98642703f 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,8 @@ VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev") BUILD_TIME=$(shell date +%FT%T%z) GO_VERSION=$(shell $(GO) version | awk '{print $$3}') -INTERNAL=github.com/sipeed/picoclaw/cmd/picoclaw/internal -LDFLAGS=-ldflags "-X $(INTERNAL).version=$(VERSION) -X $(INTERNAL).gitCommit=$(GIT_COMMIT) -X $(INTERNAL).buildTime=$(BUILD_TIME) -X $(INTERNAL).goVersion=$(GO_VERSION) -s -w" +CONFIG_PKG=github.com/sipeed/picoclaw/pkg/config +LDFLAGS=-ldflags "-X $(CONFIG_PKG).Version=$(VERSION) -X $(CONFIG_PKG).GitCommit=$(GIT_COMMIT) -X $(CONFIG_PKG).BuildTime=$(BUILD_TIME) -X $(CONFIG_PKG).GoVersion=$(GO_VERSION) -s -w" # Go variables GO?=CGO_ENABLED=0 go diff --git a/cmd/picoclaw/internal/helpers.go b/cmd/picoclaw/internal/helpers.go index f81d7013d..e04bccffb 100644 --- a/cmd/picoclaw/internal/helpers.go +++ b/cmd/picoclaw/internal/helpers.go @@ -1,23 +1,14 @@ package internal import ( - "fmt" "os" "path/filepath" - "runtime" "github.com/sipeed/picoclaw/pkg/config" ) const Logo = "🦞" -var ( - version = "dev" - gitCommit string - buildTime string - goVersion string -) - // GetPicoclawHome returns the picoclaw home directory. // Priority: $PICOCLAW_HOME > ~/.picoclaw func GetPicoclawHome() string { @@ -40,25 +31,19 @@ func LoadConfig() (*config.Config, error) { } // FormatVersion returns the version string with optional git commit +// Deprecated: Use pkg/config.FormatVersion instead func FormatVersion() string { - v := version - if gitCommit != "" { - v += fmt.Sprintf(" (git: %s)", gitCommit) - } - return v + return config.FormatVersion() } // FormatBuildInfo returns build time and go version info +// Deprecated: Use pkg/config.FormatBuildInfo instead func FormatBuildInfo() (string, string) { - build := buildTime - goVer := goVersion - if goVer == "" { - goVer = runtime.Version() - } - return build, goVer + return config.FormatBuildInfo() } // GetVersion returns the version string +// Deprecated: Use pkg/config.GetVersion instead func GetVersion() string { - return version + return config.GetVersion() } diff --git a/cmd/picoclaw/internal/helpers_test.go b/cmd/picoclaw/internal/helpers_test.go index 646be1ba1..583751781 100644 --- a/cmd/picoclaw/internal/helpers_test.go +++ b/cmd/picoclaw/internal/helpers_test.go @@ -40,65 +40,6 @@ func TestGetConfigPath_WithPICOCLAW_CONFIG(t *testing.T) { assert.Equal(t, want, got) } -func TestFormatVersion_NoGitCommit(t *testing.T) { - oldVersion, oldGit := version, gitCommit - t.Cleanup(func() { version, gitCommit = oldVersion, oldGit }) - - version = "1.2.3" - gitCommit = "" - - assert.Equal(t, "1.2.3", FormatVersion()) -} - -func TestFormatVersion_WithGitCommit(t *testing.T) { - oldVersion, oldGit := version, gitCommit - t.Cleanup(func() { version, gitCommit = oldVersion, oldGit }) - - version = "1.2.3" - gitCommit = "abc123" - - assert.Equal(t, "1.2.3 (git: abc123)", FormatVersion()) -} - -func TestFormatBuildInfo_UsesBuildTimeAndGoVersion_WhenSet(t *testing.T) { - oldBuildTime, oldGoVersion := buildTime, goVersion - t.Cleanup(func() { buildTime, goVersion = oldBuildTime, oldGoVersion }) - - buildTime = "2026-02-20T00:00:00Z" - goVersion = "go1.23.0" - - build, goVer := FormatBuildInfo() - - assert.Equal(t, buildTime, build) - assert.Equal(t, goVersion, goVer) -} - -func TestFormatBuildInfo_EmptyBuildTime_ReturnsEmptyBuild(t *testing.T) { - oldBuildTime, oldGoVersion := buildTime, goVersion - t.Cleanup(func() { buildTime, goVersion = oldBuildTime, oldGoVersion }) - - buildTime = "" - goVersion = "go1.23.0" - - build, goVer := FormatBuildInfo() - - assert.Empty(t, build) - assert.Equal(t, goVersion, goVer) -} - -func TestFormatBuildInfo_EmptyGoVersion_FallsBackToRuntimeVersion(t *testing.T) { - oldBuildTime, oldGoVersion := buildTime, goVersion - t.Cleanup(func() { buildTime, goVersion = oldBuildTime, oldGoVersion }) - - buildTime = "x" - goVersion = "" - - build, goVer := FormatBuildInfo() - - assert.Equal(t, "x", build) - assert.Equal(t, runtime.Version(), goVer) -} - func TestGetConfigPath_Windows(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("windows-specific HOME behavior varies; run on windows") @@ -112,17 +53,3 @@ func TestGetConfigPath_Windows(t *testing.T) { require.True(t, strings.EqualFold(got, want), "GetConfigPath() = %q, want %q", got, want) } - -func TestGetVersion(t *testing.T) { - assert.Equal(t, "dev", GetVersion()) -} - -func TestGetConfigPath_WithEnv(t *testing.T) { - t.Setenv("PICOCLAW_CONFIG", "/tmp/custom/config.json") - t.Setenv("HOME", "/tmp/home") // Also set home to ensure env is preferred - - got := GetConfigPath() - want := "/tmp/custom/config.json" - - assert.Equal(t, want, got) -} diff --git a/cmd/picoclaw/internal/status/helpers.go b/cmd/picoclaw/internal/status/helpers.go index ab28f4885..dd7063fe6 100644 --- a/cmd/picoclaw/internal/status/helpers.go +++ b/cmd/picoclaw/internal/status/helpers.go @@ -6,6 +6,7 @@ import ( "github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/pkg/auth" + "github.com/sipeed/picoclaw/pkg/config" ) func statusCmd() { @@ -18,8 +19,8 @@ func statusCmd() { configPath := internal.GetConfigPath() fmt.Printf("%s picoclaw Status\n", internal.Logo) - fmt.Printf("Version: %s\n", internal.FormatVersion()) - build, _ := internal.FormatBuildInfo() + fmt.Printf("Version: %s\n", config.FormatVersion()) + build, _ := config.FormatBuildInfo() if build != "" { fmt.Printf("Build: %s\n", build) } diff --git a/cmd/picoclaw/internal/version/command.go b/cmd/picoclaw/internal/version/command.go index 1cf686671..71c7dd2f8 100644 --- a/cmd/picoclaw/internal/version/command.go +++ b/cmd/picoclaw/internal/version/command.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" "github.com/sipeed/picoclaw/cmd/picoclaw/internal" + "github.com/sipeed/picoclaw/pkg/config" ) func NewVersionCommand() *cobra.Command { @@ -22,8 +23,8 @@ func NewVersionCommand() *cobra.Command { } func printVersion() { - fmt.Printf("%s picoclaw %s\n", internal.Logo, internal.FormatVersion()) - build, goVer := internal.FormatBuildInfo() + fmt.Printf("%s picoclaw %s\n", internal.Logo, config.FormatVersion()) + build, goVer := config.FormatBuildInfo() if build != "" { fmt.Printf(" Build: %s\n", build) } diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index fe4de8ecc..b82475905 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -22,10 +22,11 @@ import ( "github.com/sipeed/picoclaw/cmd/picoclaw/internal/skills" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/status" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/version" + "github.com/sipeed/picoclaw/pkg/config" ) func NewPicoclawCommand() *cobra.Command { - short := fmt.Sprintf("%s picoclaw - Personal AI Assistant v%s\n\n", internal.Logo, internal.GetVersion()) + short := fmt.Sprintf("%s picoclaw - Personal AI Assistant v%s\n\n", internal.Logo, config.GetVersion()) cmd := &cobra.Command{ Use: "picoclaw", diff --git a/cmd/picoclaw/main_test.go b/cmd/picoclaw/main_test.go index 3740ba358..e622675ee 100644 --- a/cmd/picoclaw/main_test.go +++ b/cmd/picoclaw/main_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/sipeed/picoclaw/cmd/picoclaw/internal" + "github.com/sipeed/picoclaw/pkg/config" ) func TestNewPicoclawCommand(t *testing.T) { @@ -16,7 +17,7 @@ func TestNewPicoclawCommand(t *testing.T) { require.NotNil(t, cmd) - short := fmt.Sprintf("%s picoclaw - Personal AI Assistant v%s\n\n", internal.Logo, internal.GetVersion()) + short := fmt.Sprintf("%s picoclaw - Personal AI Assistant v%s\n\n", internal.Logo, config.GetVersion()) assert.Equal(t, "picoclaw", cmd.Use) assert.Equal(t, short, cmd.Short) diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 92663a32d..33fe26001 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/skills" @@ -80,8 +81,10 @@ func NewContextBuilder(workspace string) *ContextBuilder { func (cb *ContextBuilder) getIdentity() string { workspacePath, _ := filepath.Abs(filepath.Join(cb.workspace)) toolDiscovery := cb.getDiscoveryRule() + version := config.FormatVersion() - return fmt.Sprintf(`# picoclaw 🦞 + return fmt.Sprintf( + `# picoclaw 🦞 (%s) You are picoclaw, a helpful AI assistant. @@ -102,7 +105,7 @@ Your workspace is at: %s 4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content. %s`, - workspacePath, workspacePath, workspacePath, workspacePath, workspacePath, toolDiscovery) + version, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath, toolDiscovery) } func (cb *ContextBuilder) getDiscoveryRule() string { diff --git a/pkg/config/config.go b/pkg/config/config.go index e3520faaf..13d5a7306 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -59,6 +59,16 @@ type Config struct { Tools ToolsConfig `json:"tools"` Heartbeat HeartbeatConfig `json:"heartbeat"` Devices DevicesConfig `json:"devices"` + // BuildInfo contains build-time version information + BuildInfo BuildInfo `json:"build_info,omitempty"` +} + +// BuildInfo contains build-time version information +type BuildInfo struct { + Version string `json:"version"` + GitCommit string `json:"git_commit"` + BuildTime string `json:"build_time"` + GoVersion string `json:"go_version"` } // MarshalJSON implements custom JSON marshaling for Config diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 88cb254ad..5bb3bd1d6 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -510,5 +510,11 @@ func DefaultConfig() *Config { Enabled: false, MonitorUSB: true, }, + BuildInfo: BuildInfo{ + Version: Version, + GitCommit: GitCommit, + BuildTime: BuildTime, + GoVersion: GoVersion, + }, } } diff --git a/pkg/config/version.go b/pkg/config/version.go new file mode 100644 index 000000000..b65d3cf33 --- /dev/null +++ b/pkg/config/version.go @@ -0,0 +1,44 @@ +package config + +import ( + "fmt" + "runtime" +) + +// Build-time variables injected via ldflags during build process. +// These are set by the Makefile or .goreleaser.yaml using the -X flag: +// +// -X github.com/sipeed/picoclaw/pkg/config.Version= +// -X github.com/sipeed/picoclaw/pkg/config.GitCommit= +// -X github.com/sipeed/picoclaw/pkg/config.BuildTime= +// -X github.com/sipeed/picoclaw/pkg/config.GoVersion= +var ( + Version = "dev" // Default value when not built with ldflags + GitCommit string // Git commit SHA (short) + BuildTime string // Build timestamp in RFC3339 format + GoVersion string // Go version used for building +) + +// FormatVersion returns the version string with optional git commit +func FormatVersion() string { + v := Version + if GitCommit != "" { + v += fmt.Sprintf(" (git: %s)", GitCommit) + } + return v +} + +// FormatBuildInfo returns build time and go version info +func FormatBuildInfo() (string, string) { + build := BuildTime + goVer := GoVersion + if goVer == "" { + goVer = runtime.Version() + } + return build, goVer +} + +// GetVersion returns the version string +func GetVersion() string { + return Version +} diff --git a/pkg/config/version_test.go b/pkg/config/version_test.go new file mode 100644 index 000000000..34bc906ce --- /dev/null +++ b/pkg/config/version_test.go @@ -0,0 +1,92 @@ +package config + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormatVersion_NoGitCommit(t *testing.T) { + oldVersion, oldGit := Version, GitCommit + t.Cleanup(func() { Version, GitCommit = oldVersion, oldGit }) + + Version = "1.2.3" + GitCommit = "" + + assert.Equal(t, "1.2.3", FormatVersion()) +} + +func TestFormatVersion_WithGitCommit(t *testing.T) { + oldVersion, oldGit := Version, GitCommit + t.Cleanup(func() { Version, GitCommit = oldVersion, oldGit }) + + Version = "1.2.3" + GitCommit = "abc123" + + assert.Equal(t, "1.2.3 (git: abc123)", FormatVersion()) +} + +func TestFormatBuildInfo_UsesBuildTimeAndGoVersion_WhenSet(t *testing.T) { + oldBuildTime, oldGoVersion := BuildTime, GoVersion + t.Cleanup(func() { BuildTime, GoVersion = oldBuildTime, oldGoVersion }) + + BuildTime = "2026-02-20T00:00:00Z" + GoVersion = "go1.23.0" + + build, goVer := FormatBuildInfo() + + assert.Equal(t, BuildTime, build) + assert.Equal(t, GoVersion, goVer) +} + +func TestFormatBuildInfo_EmptyBuildTime_ReturnsEmptyBuild(t *testing.T) { + oldBuildTime, oldGoVersion := BuildTime, GoVersion + t.Cleanup(func() { BuildTime, GoVersion = oldBuildTime, oldGoVersion }) + + BuildTime = "" + GoVersion = "go1.23.0" + + build, goVer := FormatBuildInfo() + + assert.Empty(t, build) + assert.Equal(t, GoVersion, goVer) +} + +func TestFormatBuildInfo_EmptyGoVersion_FallsBackToRuntimeVersion(t *testing.T) { + oldBuildTime, oldGoVersion := BuildTime, GoVersion + t.Cleanup(func() { BuildTime, GoVersion = oldBuildTime, oldGoVersion }) + + BuildTime = "x" + GoVersion = "" + + build, goVer := FormatBuildInfo() + + assert.Equal(t, "x", build) + assert.Equal(t, runtime.Version(), goVer) +} + +func TestGetVersion(t *testing.T) { + oldVersion := Version + t.Cleanup(func() { Version = oldVersion }) + + Version = "dev" + assert.Equal(t, "dev", GetVersion()) +} + +func TestGetVersion_Custom(t *testing.T) { + oldVersion := Version + t.Cleanup(func() { Version = oldVersion }) + + Version = "v1.0.0" + assert.Equal(t, "v1.0.0", GetVersion()) +} + +func TestVersion_DefaultIsDev(t *testing.T) { + // Reset to default values + oldVersion := Version + Version = "dev" + t.Cleanup(func() { Version = oldVersion }) + + assert.Equal(t, "dev", Version) +} diff --git a/workspace/IDENTITY.md b/workspace/IDENTITY.md index dabb0e14b..20e3e49fa 100644 --- a/workspace/IDENTITY.md +++ b/workspace/IDENTITY.md @@ -6,9 +6,6 @@ PicoClaw 🦞 ## Description Ultra-lightweight personal AI assistant written in Go, inspired by nanobot. -## Version -0.1.0 - ## Purpose - Provide intelligent AI assistance with minimal resource usage - Support multiple LLM providers (OpenAI, Anthropic, Zhipu, etc.)