// Package commands — agent runtime-status command (`hf agent status`). // // Wraps the plugin-facing `POST /calendar/agent/status` endpoint so agents // driven from `pcexec` (which sets AGENT_ID/CLAW_IDENTIFIER env) can report // their status from a workflow without writing curl in the middle of a // `flow.md` procedure. // // The endpoint itself is unauthenticated at the HTTP layer — it identifies // the agent purely from X-Agent-ID + X-Claw-Identifier headers — so this // command does NOT call `ResolveToken`. Calling it from outside a pcexec // session will fail because AGENT_ID/CLAW_IDENTIFIER won't be set. package commands import ( "bytes" "encoding/json" "fmt" "net/http" "os" "strings" "time" "git.hangman-lab.top/zhi/HarborForge.Cli/internal/config" "git.hangman-lab.top/zhi/HarborForge.Cli/internal/output" ) // RunAgentStatus implements `hf agent status --set `. // // Supported statuses (mirrors backend `AgentStatus` enum): // idle | busy | on_call | exhausted | offline // // For `exhausted`, an optional `--reason ` and // `--recovery-at ` can be provided. func RunAgentStatus(args []string) { target := "" reason := "" recoveryAt := "" for i := 0; i < len(args); i++ { switch args[i] { case "--set": if i+1 >= len(args) { output.Error("usage: hf agent status --set [--reason ] [--recovery-at ]") } target = args[i+1] i++ case "--reason": if i+1 >= len(args) { output.Error("--reason requires a value") } reason = args[i+1] i++ case "--recovery-at": if i+1 >= len(args) { output.Error("--recovery-at requires an ISO-8601 timestamp") } recoveryAt = args[i+1] i++ default: output.Errorf("unknown flag: %s", args[i]) } } if target == "" { output.Error("--set is required") } agentID := strings.TrimSpace(os.Getenv("AGENT_ID")) clawID := strings.TrimSpace(os.Getenv("CLAW_IDENTIFIER")) if clawID == "" { // Match the plugin convention: hostname fallback when CLAW_IDENTIFIER // is unset. Most pcexec callers won't have it set in env. if h, err := os.Hostname(); err == nil { clawID = h } } if agentID == "" { output.Error("AGENT_ID env is not set — run via pcexec or export AGENT_ID first") } if clawID == "" { output.Error("CLAW_IDENTIFIER env not set and hostname() failed — set CLAW_IDENTIFIER explicitly") } cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } body := map[string]interface{}{ "agent_id": agentID, "claw_identifier": clawID, "status": target, } if reason != "" { body["exhaust_reason"] = reason } if recoveryAt != "" { body["recovery_at"] = recoveryAt } payload, err := json.Marshal(body) if err != nil { output.Errorf("cannot marshal payload: %v", err) } url := strings.TrimRight(cfg.BaseURL, "/") + "/calendar/agent/status" req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload)) if err != nil { output.Errorf("cannot build request: %v", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Agent-ID", agentID) req.Header.Set("X-Claw-Identifier", clawID) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { output.Errorf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode/100 != 2 { buf := new(bytes.Buffer) _, _ = buf.ReadFrom(resp.Body) output.Errorf("backend returned %d: %s", resp.StatusCode, buf.String()) } if output.JSONMode { buf := new(bytes.Buffer) _, _ = buf.ReadFrom(resp.Body) fmt.Println(buf.String()) return } output.PrintKeyValue( "agent_id", agentID, "claw_identifier", clawID, "status", target, "ok", "true", ) }