feat(cli): hf user create --agent-id/--claw-identifier + hf user bind-agent

Implements NEXT_WAVE_DEV_DIRECTION.md §7.3 (was 4 lines of spec, never
shipped). Backend's POST /users has accepted agent_id+claw_identifier
since BE-CAL-003 but the cli never sent them, so historically every
agent user (zhi/lyn/mirror/sherlock/orion/nav on prod today) was
created with only the user row — agents table left empty, and all
downstream calendar/heartbeat/schedule-type flows that go through
_require_agent() returned 404.

## hf user create — new flags

  --agent-id <id>
  --claw-identifier <id>

Both required together (matches backend invariant). Either can come
from pcexec env: AGENT_ID env for agent-id, `openclaw config get
plugins.harbor-forge.identifier` for claw-identifier. Partial pair is
treated as "neither" so plain user creation (no binding intended) still
works without a 400.

## hf user bind-agent <username> — NEW subcommand

Backfills agents row for an existing user. PATCH
/users/{username}/bind-agent. Same accept --agent-id/--claw-identifier
flags + pcexec env fallback. requireBoth=true here — fail loudly if
the pair can't be resolved since the whole command is the binding.

## Wiring

- userCreatePayload gains AgentID + ClawIdentifier omitempty fields
- new userBindAgentPayload struct (both required)
- resolveAgentBinding helper shared by both commands
- main.go user create case parses --agent-id/--claw-identifier;
  new user bind-agent case parses positional username + the same flags
- surface.go lists bind-agent so `hf user` and `hf --help` show it

Build: clean. Smoke-tested both subcommand usage strings.

🤖 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:
hanghang zhang
2026-05-22 20:01:37 +01:00
parent e99b12ef08
commit 46d928782b
3 changed files with 190 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"strings"
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/commands"
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/help"
@@ -305,6 +306,7 @@ func handleUserCommand(subCmd string, args []string) {
commands.RunUserGet(filtered[0], tokenFlag)
case "create":
username, password, email, fullName, discordUserID := "", "", "", "", ""
agentID, clawID := "", ""
for i := 0; i < len(filtered); i++ {
switch filtered[i] {
case "--user":
@@ -332,14 +334,54 @@ func handleUserCommand(subCmd string, args []string) {
i++
discordUserID = filtered[i]
}
case "--agent-id":
if i+1 < len(filtered) {
i++
agentID = filtered[i]
}
case "--claw-identifier":
if i+1 < len(filtered) {
i++
clawID = filtered[i]
}
default:
output.Errorf("unknown flag: %s", filtered[i])
}
}
if username == "" {
output.Error("usage: hf user create --user <username>")
output.Error("usage: hf user create --user <username> [--agent-id <id> --claw-identifier <id>]")
}
commands.RunUserCreate(username, password, email, fullName, discordUserID, accMgrTokenFlag)
commands.RunUserCreate(username, password, email, fullName, discordUserID, agentID, clawID, accMgrTokenFlag)
case "bind-agent":
// `hf user bind-agent <username> [--agent-id <id>] [--claw-identifier <id>]`
// Backfills the agents row for an existing user. Pcexec env
// (AGENT_ID + `openclaw config get plugins.harbor-forge.identifier`)
// covers the flags when run from a pcexec session.
username, agentID, clawID := "", "", ""
for i := 0; i < len(filtered); i++ {
switch filtered[i] {
case "--agent-id":
if i+1 < len(filtered) {
i++
agentID = filtered[i]
}
case "--claw-identifier":
if i+1 < len(filtered) {
i++
clawID = filtered[i]
}
default:
if username == "" && !strings.HasPrefix(filtered[i], "--") {
username = filtered[i]
} else {
output.Errorf("unknown flag: %s", filtered[i])
}
}
}
if username == "" {
output.Error("usage: hf user bind-agent <username> [--agent-id <id>] [--claw-identifier <id>]")
}
commands.RunUserBindAgent(username, agentID, clawID, accMgrTokenFlag)
case "update":
if len(filtered) < 1 {
output.Error("usage: hf user update <username> [--email ...] [--full-name ...] [--pass ...] [--active ...]")