Compare commits
6 Commits
docs/readm
...
e99b12ef08
| Author | SHA1 | Date | |
|---|---|---|---|
| e99b12ef08 | |||
|
|
6ace6f2594 | ||
| 8dd58bad43 | |||
| 34a5512009 | |||
| ce532bdf15 | |||
| dbc599171f |
@@ -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:])
|
||||
@@ -224,6 +237,12 @@ func handleGroup(group help.Group, args []string) {
|
||||
case "monitor":
|
||||
handleMonitorCommand(sub.Name, remaining)
|
||||
return
|
||||
case "schedule-type":
|
||||
handleScheduleTypeCommand(sub.Name, remaining)
|
||||
return
|
||||
case "assign-schedule-type":
|
||||
handleAssignScheduleType(remaining)
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) > 0 && args[0] == "update-discord-id" {
|
||||
@@ -1138,3 +1157,47 @@ func handleMonitorAPIKeyCommand(args []string, tokenFlag string) {
|
||||
output.Errorf("unknown monitor api-key subcommand: %s", subCmd)
|
||||
}
|
||||
}
|
||||
|
||||
func handleScheduleTypeCommand(subCmd string, args []string) {
|
||||
tokenFlag := ""
|
||||
var filtered []string
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--token":
|
||||
if i+1 < len(args) {
|
||||
i++
|
||||
tokenFlag = args[i]
|
||||
}
|
||||
default:
|
||||
filtered = append(filtered, args[i])
|
||||
}
|
||||
}
|
||||
|
||||
switch subCmd {
|
||||
case "list":
|
||||
commands.RunScheduleTypeList(tokenFlag)
|
||||
case "create":
|
||||
commands.RunScheduleTypeCreate(filtered, tokenFlag)
|
||||
case "delete":
|
||||
commands.RunScheduleTypeDelete(filtered, tokenFlag)
|
||||
default:
|
||||
output.Errorf("hf schedule-type %s is not implemented yet", subCmd)
|
||||
}
|
||||
}
|
||||
|
||||
func handleAssignScheduleType(args []string) {
|
||||
tokenFlag := ""
|
||||
var filtered []string
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--token":
|
||||
if i+1 < len(args) {
|
||||
i++
|
||||
tokenFlag = args[i]
|
||||
}
|
||||
default:
|
||||
filtered = append(filtered, args[i])
|
||||
}
|
||||
}
|
||||
commands.RunAssignScheduleType(filtered, tokenFlag)
|
||||
}
|
||||
|
||||
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",
|
||||
)
|
||||
}
|
||||
165
internal/commands/schedule_type.go
Normal file
165
internal/commands/schedule_type.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/client"
|
||||
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/config"
|
||||
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/output"
|
||||
)
|
||||
|
||||
type scheduleTypeResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
WorkFrom int `json:"work_from"`
|
||||
WorkTo int `json:"work_to"`
|
||||
EntertainmentFrom int `json:"entertainment_from"`
|
||||
EntertainmentTo int `json:"entertainment_to"`
|
||||
}
|
||||
|
||||
// RunScheduleTypeList implements `hf schedule-type list`.
|
||||
func RunScheduleTypeList(tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
output.Errorf("config error: %v", err)
|
||||
}
|
||||
c := client.New(cfg.BaseURL, token)
|
||||
data, err := c.Get("/schedule-types/")
|
||||
if err != nil {
|
||||
output.Errorf("failed to list schedule types: %v", err)
|
||||
}
|
||||
|
||||
var types []scheduleTypeResponse
|
||||
if err := json.Unmarshal(data, &types); err != nil {
|
||||
output.Errorf("invalid response: %v", err)
|
||||
}
|
||||
|
||||
if output.JSONMode {
|
||||
output.PrintJSON(types)
|
||||
return
|
||||
}
|
||||
|
||||
if len(types) == 0 {
|
||||
fmt.Println("No schedule types defined.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%-4s %-20s %-12s %-12s\n", "ID", "Name", "Work", "Entertainment")
|
||||
fmt.Printf("%-4s %-20s %-12s %-12s\n", "----", "--------------------", "------------", "------------")
|
||||
for _, t := range types {
|
||||
fmt.Printf("%-4d %-20s %02d:00-%02d:00 %02d:00-%02d:00\n",
|
||||
t.ID, t.Name, t.WorkFrom, t.WorkTo, t.EntertainmentFrom, t.EntertainmentTo)
|
||||
}
|
||||
}
|
||||
|
||||
// RunScheduleTypeCreate implements `hf schedule-type create <name> --work <from>-<to> --entertainment <from>-<to>`.
|
||||
func RunScheduleTypeCreate(args []string, tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
|
||||
if len(args) < 1 {
|
||||
output.Error("usage: hf schedule-type create <name> --work <from>-<to> --entertainment <from>-<to>")
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
workFrom, workTo, entFrom, entTo := -1, -1, -1, -1
|
||||
|
||||
for i := 1; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--work":
|
||||
if i+1 < len(args) {
|
||||
i++
|
||||
fmt.Sscanf(args[i], "%d-%d", &workFrom, &workTo)
|
||||
}
|
||||
case "--entertainment":
|
||||
if i+1 < len(args) {
|
||||
i++
|
||||
fmt.Sscanf(args[i], "%d-%d", &entFrom, &entTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if workFrom < 0 || workTo < 0 || entFrom < 0 || entTo < 0 {
|
||||
output.Error("usage: hf schedule-type create <name> --work <from>-<to> --entertainment <from>-<to>\n e.g.: hf schedule-type create standard --work 8-18 --entertainment 19-23")
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"name": name,
|
||||
"work_from": workFrom,
|
||||
"work_to": workTo,
|
||||
"entertainment_from": entFrom,
|
||||
"entertainment_to": entTo,
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
output.Errorf("config error: %v", err)
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
c := client.New(cfg.BaseURL, token)
|
||||
data, err := c.Post("/schedule-types/", bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
output.Errorf("failed to create schedule type: %v", err)
|
||||
}
|
||||
|
||||
var resp scheduleTypeResponse
|
||||
json.Unmarshal(data, &resp)
|
||||
fmt.Printf("Created schedule type: %s (id=%d, work=%02d:00-%02d:00, entertainment=%02d:00-%02d:00)\n",
|
||||
resp.Name, resp.ID, resp.WorkFrom, resp.WorkTo, resp.EntertainmentFrom, resp.EntertainmentTo)
|
||||
}
|
||||
|
||||
// RunScheduleTypeDelete implements `hf schedule-type delete <id>`.
|
||||
func RunScheduleTypeDelete(args []string, tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
|
||||
if len(args) < 1 {
|
||||
output.Error("usage: hf schedule-type delete <id>")
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
output.Errorf("config error: %v", err)
|
||||
}
|
||||
|
||||
c := client.New(cfg.BaseURL, token)
|
||||
_, err = c.Delete("/schedule-types/" + args[0])
|
||||
if err != nil {
|
||||
output.Errorf("failed to delete schedule type: %v", err)
|
||||
}
|
||||
fmt.Printf("Deleted schedule type %s\n", args[0])
|
||||
}
|
||||
|
||||
// RunAssignScheduleType implements `hf assign-schedule-type <agent-id> <schedule-type-name>`.
|
||||
func RunAssignScheduleType(args []string, tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
|
||||
if len(args) < 2 {
|
||||
output.Error("usage: hf assign-schedule-type <agent-id> <schedule-type-name>")
|
||||
}
|
||||
|
||||
agentID := args[0]
|
||||
scheduleName := args[1]
|
||||
|
||||
body := map[string]string{
|
||||
"schedule_type_name": scheduleName,
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
output.Errorf("config error: %v", err)
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
c := client.New(cfg.BaseURL, token)
|
||||
data, err := c.Put("/schedule-types/agent/"+agentID+"/assign", bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
output.Errorf("failed to assign schedule type: %v", err)
|
||||
}
|
||||
|
||||
var resp map[string]any
|
||||
json.Unmarshal(data, &resp)
|
||||
fmt.Printf("Assigned schedule type '%s' to agent '%s'\n", scheduleName, agentID)
|
||||
}
|
||||
@@ -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",
|
||||
@@ -181,6 +188,16 @@ func CommandSurface() []Group {
|
||||
{Name: "api-key", Description: "Manage monitor API keys", Permitted: has(perms, "monitor.manage")},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "schedule-type",
|
||||
Description: "Manage work/entertainment schedule types",
|
||||
SubCommands: []Command{
|
||||
{Name: "list", Description: "List schedule types", Permitted: has(perms, "schedule_type.read")},
|
||||
{Name: "create", Description: "Create a schedule type", Permitted: has(perms, "schedule_type.manage")},
|
||||
{Name: "delete", Description: "Delete a schedule type", Permitted: has(perms, "schedule_type.manage")},
|
||||
},
|
||||
},
|
||||
{Name: "assign-schedule-type", Description: "Assign a schedule type to an agent: assign-schedule-type <agent-id> <type-name>", Permitted: has(perms, "schedule_type.manage")},
|
||||
}
|
||||
|
||||
for i := range groups {
|
||||
|
||||
Reference in New Issue
Block a user