fix: build entire cmd/pulse package, not just main.go

The static binary build was only compiling main.go, missing bootstrap.go
and config.go which define osExit, bootstrapTokenCmd, and configCmd.
This commit is contained in:
rcourtman 2026-01-13 09:06:21 +00:00
parent d389345153
commit 4e064aa0cc
7 changed files with 100 additions and 3 deletions

View file

@ -47,7 +47,7 @@ jobs:
cp -r frontend-modern/dist internal/api/frontend-modern/
- name: Build static binary
run: CGO_ENABLED=0 go build -ldflags="-s -w" -o pulse cmd/pulse/main.go
run: CGO_ENABLED=0 go build -ldflags="-s -w" -o pulse ./cmd/pulse/
- name: Tailscale
uses: tailscale/github-action@v2

View file

@ -48,7 +48,7 @@ import { TokenRevealDialog } from './components/TokenRevealDialog';
import { useAlertsActivation } from './stores/alertsActivation';
import { UpdateProgressModal } from './components/UpdateProgressModal';
import type { UpdateStatus } from './api/updates';
import { AIChat } from './components/AI/AIChat';
import { AIChat } from './components/AI/Chat';
import { AIStatusIndicator } from './components/AI/AIStatusIndicator';
import { aiChatStore } from './stores/aiChat';
import { useResourcesAsLegacy } from './hooks/useResources';

View file

@ -14,7 +14,7 @@ const backendPort = Number(
process.env.PULSE_DEV_API_PORT ??
process.env.FRONTEND_PORT ??
process.env.PORT ??
7654,
7655,
);
const backendUrl =

View file

@ -970,6 +970,13 @@ func (s *Service) GetConfig() *config.AIConfig {
return &cfg
}
// GetCommandPolicy returns the command policy for security checks
func (s *Service) GetCommandPolicy() CommandPolicy {
s.mu.RLock()
defer s.mu.RUnlock()
return s.policy
}
// GetDebugContext returns debug information about what context would be sent to the AI
func (s *Service) GetDebugContext(req ExecuteRequest) map[string]interface{} {
s.mu.RLock()
@ -3225,6 +3232,12 @@ This is a 3-command job. Don't over-investigate.`
return prompt
}
// BuildSystemPromptForOpenCode builds a system prompt for use with OpenCode integration.
// This is a public wrapper around the internal buildSystemPrompt for the OpenCode service.
func (s *Service) BuildSystemPromptForOpenCode(req ExecuteRequest) string {
return s.buildSystemPrompt(req)
}
// formatContextKey converts snake_case keys to readable labels
func formatContextKey(key string) string {
replacements := map[string]string{

View file

@ -57,6 +57,7 @@ type Router struct {
temperatureProxyHandlers *TemperatureProxyHandlers
systemSettingsHandler *SystemSettingsHandler
aiSettingsHandler *AISettingsHandler
aiHandler *AIHandler
resourceHandlers *ResourceHandlers
reportingHandlers *ReportingHandlers
configProfileHandler *ConfigProfileHandler
@ -1331,6 +1332,62 @@ func (r *Router) setupRoutes() {
})
}
}
// OpenCode-powered AI handler for chat
r.aiHandler = NewAIHandler(r.config, r.persistence, r.agentExecServer)
// OpenCode chat endpoints (new SSE-based chat)
r.mux.HandleFunc("/api/ai/chat", RequireAuth(r.config, r.aiHandler.HandleChat))
r.mux.HandleFunc("/api/ai/status", RequireAuth(r.config, r.aiHandler.HandleStatus))
r.mux.HandleFunc("/api/ai/sessions", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
r.aiHandler.HandleSessions(w, req)
case http.MethodPost:
r.aiHandler.HandleCreateSession(w, req)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}))
r.mux.HandleFunc("/api/ai/sessions/", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) {
// Extract session ID from path: /api/ai/sessions/{id}[/messages|/abort]
path := strings.TrimPrefix(req.URL.Path, "/api/ai/sessions/")
parts := strings.SplitN(path, "/", 2)
sessionID := parts[0]
if sessionID == "" {
http.Error(w, "Missing session ID", http.StatusBadRequest)
return
}
if len(parts) == 1 {
// /api/ai/sessions/{id}
if req.Method == http.MethodDelete {
r.aiHandler.HandleDeleteSession(w, req, sessionID)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
return
}
// /api/ai/sessions/{id}/messages or /api/ai/sessions/{id}/abort
switch parts[1] {
case "messages":
if req.Method == http.MethodGet {
r.aiHandler.HandleMessages(w, req, sessionID)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
case "abort":
if req.Method == http.MethodPost {
r.aiHandler.HandleAbort(w, req, sessionID)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
default:
http.Error(w, "Not found", http.StatusNotFound)
}
}))
r.mux.HandleFunc("/api/settings/ai", RequirePermission(r.config, r.authorizer, auth.ActionRead, auth.ResourceSettings, RequireScope(config.ScopeSettingsRead, r.aiSettingsHandler.HandleGetAISettings)))
r.mux.HandleFunc("/api/settings/ai/update", RequirePermission(r.config, r.authorizer, auth.ActionWrite, auth.ResourceSettings, RequireScope(config.ScopeSettingsWrite, r.aiSettingsHandler.HandleUpdateAISettings)))
r.mux.HandleFunc("/api/ai/test", RequirePermission(r.config, r.authorizer, auth.ActionWrite, auth.ResourceSettings, RequireScope(config.ScopeSettingsWrite, r.aiSettingsHandler.HandleTestAIConnection)))
@ -1870,6 +1927,24 @@ func (r *Router) StopPatrol() {
}
}
// StartOpenCode starts the OpenCode-powered AI service
func (r *Router) StartOpenCode(ctx context.Context) {
if r.aiHandler != nil && r.monitor != nil {
if err := r.aiHandler.Start(ctx, r.monitor); err != nil {
log.Error().Err(err).Msg("Failed to start OpenCode AI service")
}
}
}
// StopOpenCode stops the OpenCode AI service
func (r *Router) StopOpenCode(ctx context.Context) {
if r.aiHandler != nil {
if err := r.aiHandler.Stop(ctx); err != nil {
log.Error().Err(err).Msg("Failed to stop OpenCode AI service")
}
}
}
// startBaselineLearning runs a background loop that learns baselines from metrics history
// This enables anomaly detection by understanding what "normal" looks like for each resource
func (r *Router) startBaselineLearning(ctx context.Context, store *ai.BaselineStore, metricsHistory *monitoring.MetricsHistory) {

View file

@ -61,6 +61,12 @@ type AIConfig struct {
// AI cost controls
// Budget is expressed as an estimated USD amount over a 30-day window (pro-rated in UI for other ranges).
CostBudgetUSD30d float64 `json:"cost_budget_usd_30d,omitempty"`
// OpenCode integration - experimental feature to use OpenCode as the AI backend
// When enabled, chat and patrol use OpenCode instead of the built-in provider system
UseOpenCode bool `json:"use_opencode,omitempty"` // Enable OpenCode backend (experimental)
OpenCodeDataDir string `json:"opencode_data_dir,omitempty"` // Data directory for OpenCode (default: ~/.opencode)
OpenCodePort int `json:"opencode_port,omitempty"` // Port for OpenCode server (0 = auto-assign)
}
// AIProvider constants

View file

@ -289,6 +289,9 @@ func Run(ctx context.Context, version string) error {
// Start AI patrol service for background infrastructure monitoring
router.StartPatrol(ctx)
// Start OpenCode-powered AI chat service
router.StartOpenCode(ctx)
// Wire alert-triggered AI analysis
router.WireAlertTriggeredAI()