feat(cli): add 'hf agent status' wrapper for POST /calendar/agent/status
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>
This commit is contained in:
@@ -54,6 +54,19 @@ func main() {
|
||||
discordID = filtered[1]
|
||||
}
|
||||
commands.RunUserUpdateDiscordID(filtered[0], discordID, tokenFlag)
|
||||
case "agent":
|
||||
// `hf agent <sub>` — currently only `status` is implemented (wraps
|
||||
// `POST /calendar/agent/status`, identifies caller via
|
||||
// AGENT_ID/CLAW_IDENTIFIER env, no token needed).
|
||||
if len(args) < 2 {
|
||||
output.Error("usage: hf agent status --set <idle|busy|on_call|exhausted|offline>")
|
||||
}
|
||||
switch args[1] {
|
||||
case "status":
|
||||
commands.RunAgentStatus(args[2:])
|
||||
default:
|
||||
output.Errorf("unknown agent subcommand: %s", args[1])
|
||||
}
|
||||
default:
|
||||
if group, ok := findGroup(args[0]); ok {
|
||||
handleGroup(group, args[1:])
|
||||
|
||||
140
internal/commands/agent.go
Normal file
140
internal/commands/agent.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// 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",
|
||||
)
|
||||
}
|
||||
@@ -29,6 +29,13 @@ func CommandSurface() []Group {
|
||||
{Name: "version", Description: "Show CLI version", Permitted: true},
|
||||
{Name: "health", Description: "Check API health", Permitted: true},
|
||||
{Name: "config", Description: "View and manage CLI configuration", Permitted: true},
|
||||
{
|
||||
Name: "agent",
|
||||
Description: "Runtime status reporting for the calling agent (uses AGENT_ID/CLAW_IDENTIFIER env)",
|
||||
SubCommands: []Command{
|
||||
{Name: "status", Description: "Report runtime status: hf agent status --set <idle|busy|on_call|exhausted|offline>", Permitted: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "user",
|
||||
Description: "Manage users",
|
||||
|
||||
Reference in New Issue
Block a user