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>
594 lines
17 KiB
Go
594 lines
17 KiB
Go
package commands
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"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/mode"
|
|
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/output"
|
|
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/passmgr"
|
|
)
|
|
|
|
// userResponse matches the backend UserResponse schema.
|
|
type userResponse struct {
|
|
ID int `json:"id"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
FullName *string `json:"full_name"`
|
|
IsActive bool `json:"is_active"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
RoleID *int `json:"role_id"`
|
|
RoleName *string `json:"role_name"`
|
|
DiscordUserID *string `json:"discord_user_id"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
// RunUserList implements `hf user list`.
|
|
func RunUserList(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("/users")
|
|
if err != nil {
|
|
output.Errorf("failed to list users: %v", err)
|
|
}
|
|
|
|
if output.JSONMode {
|
|
var raw json.RawMessage
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
output.Errorf("invalid JSON response: %v", err)
|
|
}
|
|
output.PrintJSON(raw)
|
|
return
|
|
}
|
|
|
|
var users []userResponse
|
|
if err := json.Unmarshal(data, &users); err != nil {
|
|
output.Errorf("cannot parse user list: %v", err)
|
|
}
|
|
|
|
headers := []string{"USERNAME", "EMAIL", "FULL NAME", "ROLE", "ACTIVE", "ADMIN"}
|
|
var rows [][]string
|
|
for _, u := range users {
|
|
fullName := ""
|
|
if u.FullName != nil {
|
|
fullName = *u.FullName
|
|
}
|
|
roleName := ""
|
|
if u.RoleName != nil {
|
|
roleName = *u.RoleName
|
|
}
|
|
active := "yes"
|
|
if !u.IsActive {
|
|
active = "no"
|
|
}
|
|
admin := ""
|
|
if u.IsAdmin {
|
|
admin = "yes"
|
|
}
|
|
rows = append(rows, []string{u.Username, u.Email, fullName, roleName, active, admin})
|
|
}
|
|
output.PrintTable(headers, rows)
|
|
}
|
|
|
|
// RunUserGet implements `hf user get <username>`.
|
|
func RunUserGet(username, 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("/users/" + username)
|
|
if err != nil {
|
|
output.Errorf("failed to get user: %v", err)
|
|
}
|
|
|
|
if output.JSONMode {
|
|
var raw json.RawMessage
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
output.Errorf("invalid JSON response: %v", err)
|
|
}
|
|
output.PrintJSON(raw)
|
|
return
|
|
}
|
|
|
|
var u userResponse
|
|
if err := json.Unmarshal(data, &u); err != nil {
|
|
output.Errorf("cannot parse user: %v", err)
|
|
}
|
|
|
|
fullName := ""
|
|
if u.FullName != nil {
|
|
fullName = *u.FullName
|
|
}
|
|
roleName := ""
|
|
if u.RoleName != nil {
|
|
roleName = *u.RoleName
|
|
}
|
|
active := "yes"
|
|
if !u.IsActive {
|
|
active = "no"
|
|
}
|
|
admin := ""
|
|
if u.IsAdmin {
|
|
admin = "yes"
|
|
}
|
|
output.PrintKeyValue(
|
|
"username", u.Username,
|
|
"email", u.Email,
|
|
"full-name", fullName,
|
|
"role", roleName,
|
|
"active", active,
|
|
"admin", admin,
|
|
"created", u.CreatedAt,
|
|
)
|
|
}
|
|
|
|
// userCreatePayload is the JSON body for POST /users.
|
|
type userCreatePayload struct {
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
FullName *string `json:"full_name,omitempty"`
|
|
Password *string `json:"password,omitempty"`
|
|
DiscordUserID *string `json:"discord_user_id,omitempty"`
|
|
// Agent binding — both fields go together or both stay nil.
|
|
// Backend rejects (400) if only one is set.
|
|
AgentID *string `json:"agent_id,omitempty"`
|
|
ClawIdentifier *string `json:"claw_identifier,omitempty"`
|
|
}
|
|
|
|
// userBindAgentPayload is the JSON body for PATCH /users/{id}/bind-agent.
|
|
type userBindAgentPayload struct {
|
|
AgentID string `json:"agent_id"`
|
|
ClawIdentifier string `json:"claw_identifier"`
|
|
}
|
|
|
|
// resolveAgentBinding picks the final (agent_id, claw_identifier) pair
|
|
// to send to the backend.
|
|
//
|
|
// Explicit flags win. If either is empty, fall back to the pcexec env
|
|
// (AGENT_ID) and to `openclaw config get plugins.harbor-forge.identifier`
|
|
// for the claw — same convention as the openclaw plugin's heartbeat.
|
|
//
|
|
// Honours the backend's "both or neither" invariant: if only one side
|
|
// can be resolved, returns (nil, nil) — caller's intent of creating a
|
|
// non-agent user is preserved instead of producing a 400. When the
|
|
// caller actually NEEDS the binding (e.g. `hf user bind-agent` is the
|
|
// whole point of the command), set `requireBoth=true` to fail loudly
|
|
// instead.
|
|
func resolveAgentBinding(explicitAgentID, explicitClawID string, requireBoth bool) (*string, *string) {
|
|
agentID := strings.TrimSpace(explicitAgentID)
|
|
clawID := strings.TrimSpace(explicitClawID)
|
|
|
|
if agentID == "" {
|
|
agentID = strings.TrimSpace(os.Getenv("AGENT_ID"))
|
|
}
|
|
if clawID == "" {
|
|
if v, err := exec.Command("openclaw", "config", "get", "plugins.harbor-forge.identifier").Output(); err == nil {
|
|
clawID = strings.TrimSpace(string(v))
|
|
}
|
|
}
|
|
|
|
if agentID == "" && clawID == "" {
|
|
if requireBoth {
|
|
output.Error("--agent-id and --claw-identifier required (AGENT_ID env and `openclaw config get plugins.harbor-forge.identifier` both empty)")
|
|
}
|
|
return nil, nil
|
|
}
|
|
if agentID == "" || clawID == "" {
|
|
if requireBoth {
|
|
output.Errorf(
|
|
"could not resolve agent binding pair: agent_id=%q claw_identifier=%q (need both)",
|
|
agentID, clawID,
|
|
)
|
|
}
|
|
return nil, nil
|
|
}
|
|
return &agentID, &clawID
|
|
}
|
|
|
|
func maybeResolveDiscordUserID(explicit string, requireEnv bool) (string, bool, error) {
|
|
if strings.TrimSpace(explicit) != "" {
|
|
return strings.TrimSpace(explicit), true, nil
|
|
}
|
|
agentID := strings.TrimSpace(os.Getenv("AGENT_ID"))
|
|
agentVerify := strings.TrimSpace(os.Getenv("AGENT_VERIFY"))
|
|
if agentID == "" || agentVerify == "" {
|
|
if requireEnv {
|
|
return "", false, fmt.Errorf("discord id not provided and AGENT_ID/AGENT_VERIFY are missing")
|
|
}
|
|
return "", false, nil
|
|
}
|
|
cmd := exec.Command("ego-mgr", "get", "discord-id")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
if requireEnv {
|
|
return "", false, fmt.Errorf("failed to resolve discord id from ego-mgr: %w", err)
|
|
}
|
|
return "", false, nil
|
|
}
|
|
value := strings.TrimSpace(string(out))
|
|
if value == "" {
|
|
if requireEnv {
|
|
return "", false, fmt.Errorf("ego-mgr returned empty discord id")
|
|
}
|
|
return "", false, nil
|
|
}
|
|
return value, true, nil
|
|
}
|
|
|
|
// RunUserCreate implements `hf user create`.
|
|
//
|
|
// Agent binding: if `agentIDFlag` + `clawIDFlag` are both set (or both
|
|
// resolvable from env), the backend creates the matching agents row in
|
|
// the same transaction. Partial pair → treated as "neither" so callers
|
|
// who didn't want a binding still get a normal user.
|
|
func RunUserCreate(username, password, email, fullName, discordUserID, agentIDFlag, clawIDFlag, accMgrTokenFlag string) {
|
|
// Resolve account-manager token
|
|
var accMgrToken string
|
|
if mode.IsPaddedCell() {
|
|
if accMgrTokenFlag != "" {
|
|
output.Error("padded-cell installed, --acc-mgr-token flag disabled, use command directly")
|
|
}
|
|
tok, err := passmgr.GetAccountManagerToken()
|
|
if err != nil {
|
|
output.Error("--acc-mgr-token <token> required or execute with pcexec")
|
|
}
|
|
accMgrToken = tok
|
|
} else {
|
|
if accMgrTokenFlag == "" {
|
|
output.Error("--acc-mgr-token <token> required or execute with pcexec")
|
|
}
|
|
accMgrToken = accMgrTokenFlag
|
|
}
|
|
|
|
// Resolve password
|
|
if password == "" && mode.IsPaddedCell() {
|
|
pw, err := passmgr.GeneratePassword("hf", username)
|
|
if err != nil {
|
|
output.Error("--pass <password> required or execute with pcexec")
|
|
}
|
|
password = pw
|
|
}
|
|
if password == "" && !mode.IsPaddedCell() {
|
|
output.Error("--pass <password> required or execute with pcexec")
|
|
}
|
|
|
|
// Resolve email (default to username@harborforge.local if not provided)
|
|
if email == "" {
|
|
email = username + "@harborforge.local"
|
|
}
|
|
|
|
payload := userCreatePayload{
|
|
Username: username,
|
|
Email: email,
|
|
Password: &password,
|
|
}
|
|
if resolvedDiscordID, ok, err := maybeResolveDiscordUserID(discordUserID, false); err != nil {
|
|
output.Errorf("failed to resolve discord user id: %v", err)
|
|
} else if ok {
|
|
payload.DiscordUserID = &resolvedDiscordID
|
|
}
|
|
if fullName != "" {
|
|
payload.FullName = &fullName
|
|
}
|
|
|
|
// Agent binding — resolve from flags + pcexec env fallback.
|
|
if agentID, clawID := resolveAgentBinding(agentIDFlag, clawIDFlag, false); agentID != nil && clawID != nil {
|
|
payload.AgentID = agentID
|
|
payload.ClawIdentifier = clawID
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
output.Errorf("cannot marshal payload: %v", err)
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
output.Errorf("config error: %v", err)
|
|
}
|
|
c := client.NewWithAPIKey(cfg.BaseURL, accMgrToken)
|
|
data, err := c.Post("/users", bytes.NewReader(body))
|
|
if err != nil {
|
|
output.Errorf("failed to create user: %v", err)
|
|
}
|
|
|
|
if output.JSONMode {
|
|
var raw json.RawMessage
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
output.Errorf("invalid JSON response: %v", err)
|
|
}
|
|
output.PrintJSON(raw)
|
|
return
|
|
}
|
|
|
|
var u userResponse
|
|
if err := json.Unmarshal(data, &u); err != nil {
|
|
output.Errorf("cannot parse response: %v", err)
|
|
}
|
|
fmt.Printf("user created: %s\n", u.Username)
|
|
}
|
|
|
|
// RunUserUpdateDiscordID updates a user's discord_user_id field.
|
|
func RunUserUpdateDiscordID(username, discordUserID, tokenFlag string) {
|
|
token := ResolveToken(tokenFlag)
|
|
resolvedDiscordID, _, err := maybeResolveDiscordUserID(discordUserID, true)
|
|
if err != nil {
|
|
output.Errorf("failed to resolve discord user id: %v", err)
|
|
}
|
|
body, err := json.Marshal(map[string]interface{}{"discord_user_id": resolvedDiscordID})
|
|
if err != nil {
|
|
output.Errorf("cannot marshal payload: %v", err)
|
|
}
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
output.Errorf("config error: %v", err)
|
|
}
|
|
c := client.New(cfg.BaseURL, token)
|
|
if _, err := c.Patch("/users/"+username, bytes.NewReader(body)); err != nil {
|
|
output.Errorf("failed to update discord id: %v", err)
|
|
}
|
|
fmt.Printf("discord id updated: %s\n", username)
|
|
}
|
|
|
|
// RunUserUpdate implements `hf user update <username>`.
|
|
func RunUserUpdate(username string, args []string, tokenFlag string) {
|
|
token := ResolveToken(tokenFlag)
|
|
|
|
payload := make(map[string]interface{})
|
|
for i := 0; i < len(args); i++ {
|
|
switch args[i] {
|
|
case "--email":
|
|
if i+1 >= len(args) {
|
|
output.Error("--email requires a value")
|
|
}
|
|
i++
|
|
payload["email"] = args[i]
|
|
case "--full-name":
|
|
if i+1 >= len(args) {
|
|
output.Error("--full-name requires a value")
|
|
}
|
|
i++
|
|
payload["full_name"] = args[i]
|
|
case "--pass":
|
|
if i+1 >= len(args) {
|
|
output.Error("--pass requires a value")
|
|
}
|
|
i++
|
|
payload["password"] = args[i]
|
|
case "--active":
|
|
if i+1 >= len(args) {
|
|
output.Error("--active requires true or false")
|
|
}
|
|
i++
|
|
payload["is_active"] = strings.ToLower(args[i]) == "true"
|
|
default:
|
|
output.Errorf("unknown flag: %s", args[i])
|
|
}
|
|
}
|
|
|
|
if len(payload) == 0 {
|
|
output.Error("nothing to update — provide at least one flag")
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
output.Errorf("cannot marshal payload: %v", err)
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
output.Errorf("config error: %v", err)
|
|
}
|
|
c := client.New(cfg.BaseURL, token)
|
|
data, err := c.Patch("/users/"+username, bytes.NewReader(body))
|
|
if err != nil {
|
|
output.Errorf("failed to update user: %v", err)
|
|
}
|
|
|
|
if output.JSONMode {
|
|
var raw json.RawMessage
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
output.Errorf("invalid JSON response: %v", err)
|
|
}
|
|
output.PrintJSON(raw)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("user updated: %s\n", username)
|
|
}
|
|
|
|
// RunUserActivate implements `hf user activate <username>`.
|
|
func RunUserActivate(username, tokenFlag string) {
|
|
token := ResolveToken(tokenFlag)
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
output.Errorf("config error: %v", err)
|
|
}
|
|
body, _ := json.Marshal(map[string]interface{}{"is_active": true})
|
|
c := client.New(cfg.BaseURL, token)
|
|
_, err = c.Patch("/users/"+username, bytes.NewReader(body))
|
|
if err != nil {
|
|
output.Errorf("failed to activate user: %v", err)
|
|
}
|
|
fmt.Printf("user activated: %s\n", username)
|
|
}
|
|
|
|
// RunUserDeactivate implements `hf user deactivate <username>`.
|
|
func RunUserDeactivate(username, tokenFlag string) {
|
|
token := ResolveToken(tokenFlag)
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
output.Errorf("config error: %v", err)
|
|
}
|
|
body, _ := json.Marshal(map[string]interface{}{"is_active": false})
|
|
c := client.New(cfg.BaseURL, token)
|
|
_, err = c.Patch("/users/"+username, bytes.NewReader(body))
|
|
if err != nil {
|
|
output.Errorf("failed to deactivate user: %v", err)
|
|
}
|
|
fmt.Printf("user deactivated: %s\n", username)
|
|
}
|
|
|
|
// RunUserDelete implements `hf user delete <username>`.
|
|
func RunUserDelete(username, tokenFlag string) {
|
|
token := ResolveToken(tokenFlag)
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
output.Errorf("config error: %v", err)
|
|
}
|
|
c := client.New(cfg.BaseURL, token)
|
|
_, err = c.Delete("/users/" + username)
|
|
if err != nil {
|
|
output.Errorf("failed to delete user: %v", err)
|
|
}
|
|
fmt.Printf("user deleted: %s\n", username)
|
|
}
|
|
|
|
// resetAPIKeyResponse matches the backend reset-apikey response.
|
|
type resetAPIKeyResponse struct {
|
|
UserID int `json:"user_id"`
|
|
Username string `json:"username"`
|
|
APIKey string `json:"api_key"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// RunUserResetAPIKey implements `hf user reset-apikey <username>`.
|
|
func RunUserResetAPIKey(username, tokenFlag, accMgrTokenFlag string) {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
output.Errorf("config error: %v", err)
|
|
}
|
|
|
|
// Try acc-mgr-token first (allows provisioning without existing user token)
|
|
var c *client.Client
|
|
if accMgrTokenFlag != "" {
|
|
c = client.NewWithAPIKey(cfg.BaseURL, accMgrTokenFlag)
|
|
} else if mode.IsPaddedCell() {
|
|
if tok, err := passmgr.GetAccountManagerToken(); err == nil && tok != "" {
|
|
c = client.NewWithAPIKey(cfg.BaseURL, tok)
|
|
} else {
|
|
token := ResolveToken(tokenFlag)
|
|
c = client.New(cfg.BaseURL, token)
|
|
}
|
|
} else {
|
|
token := ResolveToken(tokenFlag)
|
|
c = client.New(cfg.BaseURL, token)
|
|
}
|
|
|
|
data, err := c.Post("/users/"+username+"/reset-apikey", nil)
|
|
if err != nil {
|
|
output.Errorf("failed to reset API key: %v", err)
|
|
}
|
|
|
|
if output.JSONMode {
|
|
var raw json.RawMessage
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
output.Errorf("invalid JSON response: %v", err)
|
|
}
|
|
output.PrintJSON(raw)
|
|
return
|
|
}
|
|
|
|
var r resetAPIKeyResponse
|
|
if err := json.Unmarshal(data, &r); err != nil {
|
|
fmt.Printf("API key reset for: %s\n", username)
|
|
return
|
|
}
|
|
output.PrintKeyValue(
|
|
"username", r.Username,
|
|
"api-key", r.APIKey,
|
|
"message", r.Message,
|
|
)
|
|
}
|
|
|
|
// RunUserBindAgent implements `hf user bind-agent <username>`.
|
|
//
|
|
// Backfills the agents row for an existing user that was created via
|
|
// the old `hf user create` (which did not accept --agent-id /
|
|
// --claw-identifier). Hits PATCH /users/{username}/bind-agent.
|
|
//
|
|
// Uses an account-manager token like `hf user create` does — same
|
|
// permission surface (`account.create`).
|
|
func RunUserBindAgent(username, agentIDFlag, clawIDFlag, accMgrTokenFlag string) {
|
|
if username == "" {
|
|
output.Error("usage: hf user bind-agent <username> [--agent-id <id>] [--claw-identifier <id>]")
|
|
}
|
|
|
|
// Resolve account-manager token (same as RunUserCreate).
|
|
var accMgrToken string
|
|
if mode.IsPaddedCell() {
|
|
if accMgrTokenFlag != "" {
|
|
output.Error("padded-cell installed, --acc-mgr-token flag disabled, use command directly")
|
|
}
|
|
tok, err := passmgr.GetAccountManagerToken()
|
|
if err != nil {
|
|
output.Error("--acc-mgr-token <token> required or execute with pcexec")
|
|
}
|
|
accMgrToken = tok
|
|
} else {
|
|
if accMgrTokenFlag == "" {
|
|
output.Error("--acc-mgr-token <token> required or execute with pcexec")
|
|
}
|
|
accMgrToken = accMgrTokenFlag
|
|
}
|
|
|
|
// Resolve agent binding — REQUIRED for this command (errors out on partial).
|
|
agentID, clawID := resolveAgentBinding(agentIDFlag, clawIDFlag, true)
|
|
if agentID == nil || clawID == nil {
|
|
output.Error("--agent-id and --claw-identifier could not be resolved (bug — requireBoth should have errored already)")
|
|
}
|
|
|
|
payload := userBindAgentPayload{
|
|
AgentID: *agentID,
|
|
ClawIdentifier: *clawID,
|
|
}
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
output.Errorf("cannot marshal payload: %v", err)
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
output.Errorf("config error: %v", err)
|
|
}
|
|
c := client.NewWithAPIKey(cfg.BaseURL, accMgrToken)
|
|
data, err := c.Patch("/users/"+username+"/bind-agent", bytes.NewReader(body))
|
|
if err != nil {
|
|
output.Errorf("failed to bind agent: %v", err)
|
|
}
|
|
|
|
if output.JSONMode {
|
|
var raw json.RawMessage
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
output.Errorf("invalid JSON response: %v", err)
|
|
}
|
|
output.PrintJSON(raw)
|
|
return
|
|
}
|
|
|
|
var u userResponse
|
|
if err := json.Unmarshal(data, &u); err != nil {
|
|
fmt.Printf("agent bound to %s (agent_id=%s, claw=%s)\n", username, *agentID, *clawID)
|
|
return
|
|
}
|
|
output.PrintKeyValue(
|
|
"username", u.Username,
|
|
"user_id", fmt.Sprint(u.ID),
|
|
"agent_id", *agentID,
|
|
"claw_identifier", *clawID,
|
|
"message", "agent binding written",
|
|
)
|
|
}
|