The plan-schedule workflow needs to report agent runtime status (idle/busy/on_call/exhausted/offline) at the end of planning, but the cli had no wrapper for this — workflows were dropping inline curl in the middle of their procedure to hit the backend. This adds 'hf agent status --set <status> [--reason ...] [--recovery-at ...]'. The endpoint identifies the agent purely from X-Agent-ID + X-Claw-Identifier headers (no token), so the cli reads AGENT_ID from env and falls back to hostname() for CLAW_IDENTIFIER if it isn't set — same convention the openclaw plugin uses. Refuses to send if AGENT_ID env is missing, since this only makes sense from a pcexec/agent runtime context. Surface entry added so 'hf --help' lists it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
3.7 KiB
Go
141 lines
3.7 KiB
Go
// 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 <status>`.
|
|
//
|
|
// Supported statuses (mirrors backend `AgentStatus` enum):
|
|
// idle | busy | on_call | exhausted | offline
|
|
//
|
|
// For `exhausted`, an optional `--reason <rate_limit|billing>` and
|
|
// `--recovery-at <ISO-8601>` 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 <idle|busy|on_call|exhausted|offline> [--reason <rate_limit|billing>] [--recovery-at <iso>]")
|
|
}
|
|
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 <status> 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",
|
|
)
|
|
}
|