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]
|
discordID = filtered[1]
|
||||||
}
|
}
|
||||||
commands.RunUserUpdateDiscordID(filtered[0], discordID, tokenFlag)
|
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:
|
default:
|
||||||
if group, ok := findGroup(args[0]); ok {
|
if group, ok := findGroup(args[0]); ok {
|
||||||
handleGroup(group, args[1:])
|
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: "version", Description: "Show CLI version", Permitted: true},
|
||||||
{Name: "health", Description: "Check API health", Permitted: true},
|
{Name: "health", Description: "Check API health", Permitted: true},
|
||||||
{Name: "config", Description: "View and manage CLI configuration", 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",
|
Name: "user",
|
||||||
Description: "Manage users",
|
Description: "Manage users",
|
||||||
|
|||||||
Reference in New Issue
Block a user