diff --git a/.github/workflows/deploy-gce.yml b/.github/workflows/deploy-gce.yml index dc963d7c3..c799b1b88 100644 --- a/.github/workflows/deploy-gce.yml +++ b/.github/workflows/deploy-gce.yml @@ -28,13 +28,16 @@ jobs: sudo apt-get install -y libolm-dev - name: Build Go backend - run: make build + run: | + make build + make build-launcher-tui - name: Determine binary name id: binary run: | # The Makefile outputs a binary named picoclaw-linux-amd64 echo "path=build/picoclaw-linux-amd64" >> $GITHUB_OUTPUT + echo "tui_path=build/picoclaw-launcher-tui-linux-amd64" >> $GITHUB_OUTPUT - id: 'auth' name: 'Authenticate to Google Cloud' @@ -47,7 +50,7 @@ jobs: uses: 'google-github-actions/setup-gcloud@v2' - name: Verify build artifact - run: ls -lh ${{ steps.binary.outputs.path }} + run: ls -lh ${{ steps.binary.outputs.path }} ${{ steps.binary.outputs.tui_path }} - name: 'Deploy binary to GCE' run: | @@ -57,13 +60,16 @@ jobs: --project=${{ secrets.GCP_PROJECT_ID }} \ --command="mkdir -p ~/.local/bin" - # Copy the binary to the instance + # Copy the binaries to the instance gcloud compute scp ${{ steps.binary.outputs.path }} ${{ secrets.GCE_INSTANCE }}:~/.local/bin/picoclaw \ --zone=${{ secrets.GCE_ZONE }} \ --project=${{ secrets.GCP_PROJECT_ID }} + gcloud compute scp ${{ steps.binary.outputs.tui_path }} ${{ secrets.GCE_INSTANCE }}:~/.local/bin/picoclaw-launcher-tui \ + --zone=${{ secrets.GCE_ZONE }} \ + --project=${{ secrets.GCP_PROJECT_ID }} - # Make it executable and restart the service + # Make it executable and restart the gateway service via command line gcloud compute ssh ${{ secrets.GCE_INSTANCE }} \ --zone=${{ secrets.GCE_ZONE }} \ --project=${{ secrets.GCP_PROJECT_ID }} \ - --command="chmod +x ~/.local/bin/picoclaw && sudo systemctl restart picoclaw" + --command="chmod +x ~/.local/bin/picoclaw ~/.local/bin/picoclaw-launcher-tui && pkill -f 'picoclaw gate -E' || true && nohup ~/.local/bin/picoclaw gate -E > /dev/null 2>&1 &" diff --git a/pkg/config/config.go b/pkg/config/config.go index c1a48974f..fed12db17 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1048,14 +1048,14 @@ type SearXNGConfig struct { } type GLMSearchConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` apiKey string secDirty bool BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"` // SearchEngine specifies the search backend: "search_std" (default), // "search_pro", "search_pro_sogou", or "search_pro_quark". SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"` - MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"` + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"` } // APIKey returns the GLM search API key @@ -1766,7 +1766,7 @@ func SaveConfig(path string, cfg *Config) error { m.secModelName = newName } - + cfg.security.ModelList = newModelList if cfg.Channels.Pico.secDirty { cfg.security.Channels.Pico = &PicoSecurity{ diff --git a/pkg/config/security.go b/pkg/config/security.go index f5fc51b5c..3603d1ab0 100644 --- a/pkg/config/security.go +++ b/pkg/config/security.go @@ -239,6 +239,7 @@ func (c *Config) MergeAndApplySecurity(newer *SecurityConfig) error { c.security = mergeSecurityConfig(c.security, newer) return applySecurityConfig(c, c.security) } + // mergeSecurityConfig merges two SecurityConfig instances, preferring non-empty values from 'newer'. // This is used during config migration to preserve existing security data while adding new entries. func mergeSecurityConfig(existing, newer *SecurityConfig) *SecurityConfig { @@ -335,7 +336,8 @@ func mergeChannelsSecurity(dst, src *ChannelsSecurity) { if src.Pico != nil && src.Pico.Token != "" { dst.Pico = src.Pico } - if src.IRC != nil && (src.IRC.Password != "" || src.IRC.NickServPassword != "" || src.IRC.SASLPassword != "") { + if src.IRC != nil && + (src.IRC.Password != "" || src.IRC.NickServPassword != "" || src.IRC.SASLPassword != "") { dst.IRC = src.IRC } } diff --git a/pkg/providers/anthropic_messages/provider.go b/pkg/providers/anthropic_messages/provider.go index 0debffc1b..6894b6d61 100644 --- a/pkg/providers/anthropic_messages/provider.go +++ b/pkg/providers/anthropic_messages/provider.go @@ -58,8 +58,8 @@ func NewProviderWithTimeout(apiKey, apiBase string, timeoutSeconds int) *Provide } return &Provider{ - apiKey: apiKey, - apiBase: baseURL, + apiKey: apiKey, + apiBase: baseURL, httpClient: common.NewHTTPClientWithTimeout("", timeout), } } @@ -102,7 +102,10 @@ func (p *Provider) Chat( // Set headers req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-API-Key", p.apiKey) //nolint:canonicalheader // Anthropic API requires exact header name + req.Header.Set( + "X-Api-Key", + p.apiKey, + ) req.Header.Set("Anthropic-Version", defaultAPIVersion) // Execute request @@ -134,7 +137,11 @@ func (p *Provider) Chat( return nil, fmt.Errorf("service unavailable (503): %s", string(body)) default: if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf( + "API request failed with status %d: %s", + resp.StatusCode, + string(body), + ) } } @@ -194,7 +201,8 @@ func buildRequestBody( "content": msg.Content, } if len(apiMessages) > 0 { - if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && prev["role"] == "user" { + if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && + prev["role"] == "user" { if content, ok := prev["content"].([]map[string]any); ok { prev["content"] = append(content, toolResultBlock) continue @@ -266,7 +274,8 @@ func buildRequestBody( "content": msg.Content, } if len(apiMessages) > 0 { - if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && prev["role"] == "user" { + if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && + prev["role"] == "user" { if content, ok := prev["content"].([]map[string]any); ok { prev["content"] = append(content, toolResultBlock) continue @@ -318,7 +327,10 @@ func parseResponseBody(body []byte) (*LLMResponse, error) { // Extract content and tool calls var content strings.Builder - toolCalls := make([]ToolCall, 0) // Initialize as empty slice (not nil) for consistent JSON serialization + toolCalls := make( + []ToolCall, + 0, + ) // Initialize as empty slice (not nil) for consistent JSON serialization for _, block := range resp.Content { switch block.Type { diff --git a/pkg/providers/common/http_logger.go b/pkg/providers/common/http_logger.go index c999d93ac..e916a9f2f 100644 --- a/pkg/providers/common/http_logger.go +++ b/pkg/providers/common/http_logger.go @@ -48,8 +48,8 @@ func (lrt *LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, er // Optional: Log Response Body if res.Body != nil { - bodyBytes, err := io.ReadAll(res.Body) - if err == nil { + bodyBytes, readErr := io.ReadAll(res.Body) + if readErr == nil { res.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Truncate response body if too long to avoid flooding logs respStr := string(bodyBytes) diff --git a/web/backend/api/config.go b/web/backend/api/config.go index 20aebef30..5aa615f89 100644 --- a/web/backend/api/config.go +++ b/web/backend/api/config.go @@ -67,7 +67,7 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { // Intercept explicitly provided security tokens from JSON payload that json.Unmarshal drops. var incomingSec config.SecurityConfig - if err := json.Unmarshal(body, &incomingSec); err == nil { + if err := json.Unmarshal(body, &incomingSec); err == nil { //nolint:musttag // SecurityConfig uses yaml tags cfg.MergeAndApplySecurity(&incomingSec) } else { cfg.ApplySecurity() @@ -164,9 +164,9 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) { // Restore security fields from existing config and merge explicitly provided overrides. newCfg.SecurityCopyFrom(cfg) - var incomingSec config.SecurityConfig - if err := json.Unmarshal(patchBody, &incomingSec); err == nil { - newCfg.MergeAndApplySecurity(&incomingSec) + var patchSec config.SecurityConfig + if err := json.Unmarshal(patchBody, &patchSec); err == nil { //nolint:musttag // SecurityConfig uses yaml tags + newCfg.MergeAndApplySecurity(&patchSec) } else { newCfg.ApplySecurity() }