Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [Unreleased] — Domain Coverage Expansion (P0–P3)
## [Unreleased] — Domain Coverage Expansion (P0–P5)

### Added

Expand Down Expand Up @@ -34,9 +34,21 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `goclaw health` — uses WS RPC `health` when authenticated, retaining unauthenticated HTTP `/health` fallback.
- `goclaw traces list --since --agent --status --root-only --limit` — expanded filters for automation-friendly trace search.

**P4 — UX polish**
- `goclaw codex-pool activity --agent=<id>|--provider=<id>` — unified Codex pool activity lookup; legacy agent/provider commands remain as deprecated aliases.
- `goclaw api-keys rotate <id>` — create replacement key, show raw key once, then revoke old key with structured partial-failure reporting.
- `goclaw config defaults` — read-only WS passthrough for server default config values.
- `goclaw chat replay <agent> --session=<key>` and `goclaw chat sessions resume <agent> --session=<key>` — discoverability wrappers over existing chat session contracts.
- `goclaw tools invoke <name> --args=<json|@file>` — alias for `--params` with file-backed JSON support.

**P5 — Residual command fillers**
- `goclaw teams attachments download <team-id> <attachment-id> --output <file>` — authenticated attachment download with required output path and no-overwrite default.
- `goclaw agents evolution skill apply <agent-id> <suggestion-id> [--skill-draft @file]` — explicit wrapper for approving `skill_add` suggestions through the server evolution approval route.
- `goclaw agents evolution update` now maps `--action=accept|reject` to the server-compatible `status=approved|rejected` payload.

### Notes
- All new commands honor the AI-first ergonomics contract: `--output=json` envelope, central error handler, `--yes` for destructive ops, `--quiet` for CI.
- P4/P5 backlog was re-swept against the current CLI surface; already-covered items were removed from residual scope before the next implementation pass.
- P4/P5 backlog was re-swept against the current CLI surface; already-covered items were removed from residual scope before implementation.
- Out of scope: OpenAI-compatible `/chat/completions` and `/v1/responses` endpoints (client APIs, not admin CLI surface).

---
Expand Down
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,16 @@ echo "Analyze this log" | goclaw chat myagent
|---------|-------------|
| `auth` | Login, logout, device pairing, profile management |
| `profile` | List, create, switch, inspect, and delete CLI profiles |
| `agents` | CRUD, shares, delegation links, per-user instances |
| `agents` | CRUD, shares, delegation links, per-user instances, evolution |
| `chat` | Interactive or single-shot messaging with streaming |
| `sessions` | List, preview, delete, reset, label, compact |
| `codex-pool` | Unified Codex pool activity lookup for agents/providers |
| `skills` | Upload, manage, grant/revoke access |
| `mcp` | MCP server management, grants, access requests |
| `providers` | LLM provider CRUD, model listing, verification |
| `tools` | Custom + built-in tool management, invocation |
| `cron` | Scheduled jobs CRUD, trigger, run history |
| `teams` | Team management, task board, workspace |
| `teams` | Team management, task board, workspace, attachments |
| `channels` | Channel instances, contacts, pending messages |
| `traces` | LLM trace viewer, filters, export |
| `memory` | Memory documents, semantic search |
Expand All @@ -81,7 +82,7 @@ echo "Analyze this log" | goclaw chat myagent
| `tts` | Text-to-speech operations |
| `media` | Media upload/download |
| `activity` | Audit log |
| `api-keys` | API key management (create, list, revoke) |
| `api-keys` | API key management (create, list, revoke, rotate) |
| `system upgrade` | Gateway release upgrade status and trigger controls |
| `workstations` | Coding-agent workstation CRUD, permissions, activity, and agent links |
| `webhooks` | Webhook admin CRUD, secret rotation, and deletion |
Expand Down Expand Up @@ -300,10 +301,37 @@ goclaw api-keys list

# Revoke a key
goclaw api-keys revoke <key-id>

# Rotate a key by creating a replacement and revoking the old key
goclaw api-keys rotate <key-id> --name "ci-deploy-v2" --scopes "operator.read,operator.write" --yes
```

Available scopes: `operator.admin`, `operator.read`, `operator.write`, `operator.approvals`, `operator.pairing`

## UX Convenience Commands

```bash
# Unified Codex pool activity
goclaw codex-pool activity --agent=agent-123
goclaw codex-pool activity --provider=provider-123

# Resolved server defaults
goclaw config defaults -o json

# Replay or resume a known chat session
goclaw chat replay myagent --session=sess-123 -o json
goclaw chat sessions resume myagent --session=sess-123 -m "Continue" --no-stream

# Invoke a custom tool with JSON args from file
goclaw tools invoke weather --args=@payload.json

# Download a team task attachment to an explicit file
goclaw teams attachments download team-123 attachment-456 --output ./artifact.bin

# Approve a skill_add evolution suggestion, optionally overriding the draft
goclaw agents evolution skill apply agent-123 suggestion-456 --skill-draft @./SKILL.md
```

## API Docs

```bash
Expand Down
4 changes: 2 additions & 2 deletions cmd/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ var agentsListCmd = &cobra.Command{
return err
}
if cfg.OutputFormat != "table" {
printer.Print(unmarshalList(data))
printer.Print(unmarshalNamedList(data, "agents"))
return nil
}
tbl := output.NewTable("ID", "KEY", "NAME", "PROVIDER", "MODEL", "STATUS", "TYPE")
for _, a := range unmarshalList(data) {
for _, a := range unmarshalNamedList(data, "agents") {
tbl.AddRow(str(a, "id"), str(a, "agent_key"), str(a, "display_name"),
str(a, "provider"), str(a, "model"), str(a, "status"), str(a, "agent_type"))
}
Expand Down
86 changes: 83 additions & 3 deletions cmd/agents_evolution.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cmd

import (
"encoding/json"
"fmt"
"net/url"

"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -85,26 +87,104 @@ Example:
if err != nil {
return err
}
status := map[string]string{
"accept": "approved",
"reject": "rejected",
}[action]
_, err = c.Patch(
fmt.Sprintf("/v1/agents/%s/evolution/suggestions/%s", args[0], args[1]),
map[string]any{"action": action},
fmt.Sprintf(
"/v1/agents/%s/evolution/suggestions/%s",
url.PathEscape(args[0]),
url.PathEscape(args[1]),
),
map[string]any{"status": status},
)
if err != nil {
return err
}
printer.Success(fmt.Sprintf("Suggestion %s: %sd", args[1], action))
printer.Success(fmt.Sprintf("Suggestion %s %s", args[1], status))
return nil
},
}

var agentsEvolutionSkillCmd = &cobra.Command{
Use: "skill",
Short: "Apply skill evolution suggestions",
}

var agentsEvolutionSkillApplyCmd = &cobra.Command{
Use: "apply <id> <suggestionID>",
Short: "Approve a skill_add evolution suggestion",
Long: `Approve a skill_add evolution suggestion for an agent.

PATCH /v1/agents/{id}/evolution/suggestions/{suggestionID}

Example:
goclaw agents evolution skill apply agent-1 sugg-42
goclaw agents evolution skill apply agent-1 sugg-42 --skill-draft @./SKILL.md`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
body := map[string]any{"status": "approved"}
if cmd.Flags().Changed("skill-draft") {
draft, _ := cmd.Flags().GetString("skill-draft")
content, err := readContent(draft)
if err != nil {
return err
}
body["skill_draft"] = content
}
c, err := newHTTP()
if err != nil {
return err
}
if err := requireSkillAddSuggestion(c, args[0], args[1]); err != nil {
return err
}
data, err := c.Patch(
fmt.Sprintf(
"/v1/agents/%s/evolution/suggestions/%s",
url.PathEscape(args[0]),
url.PathEscape(args[1]),
),
body,
)
if err != nil {
return err
}
printer.Print(unmarshalMap(data))
return nil
},
}

func requireSkillAddSuggestion(c interface {
Get(path string) (json.RawMessage, error)
}, agentID, suggestionID string) error {
data, err := c.Get("/v1/agents/" + url.PathEscape(agentID) + "/evolution/suggestions?status=pending&limit=500")
if err != nil {
return err
}
for _, suggestion := range unmarshalList(data) {
if str(suggestion, "id") == suggestionID {
if str(suggestion, "suggestion_type") != "skill_add" {
return fmt.Errorf("suggestion %s is %q, not skill_add", suggestionID, str(suggestion, "suggestion_type"))
}
return nil
}
}
return fmt.Errorf("suggestion %s not found in agent evolution suggestions", suggestionID)
}

func init() {
agentsEvolutionUpdateCmd.Flags().String("action", "", "Action: accept or reject")
_ = agentsEvolutionUpdateCmd.MarkFlagRequired("action")
agentsEvolutionSkillApplyCmd.Flags().String("skill-draft", "", "Skill draft content or @file")
agentsEvolutionSkillCmd.AddCommand(agentsEvolutionSkillApplyCmd)

agentsEvolutionCmd.AddCommand(
agentsEvolutionMetricsCmd,
agentsEvolutionSuggestionsCmd,
agentsEvolutionUpdateCmd,
agentsEvolutionSkillCmd,
)
agentsCmd.AddCommand(agentsEvolutionCmd)
}
5 changes: 3 additions & 2 deletions cmd/agents_misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ Example:
}

var agentsCodexPoolActivityCmd = &cobra.Command{
Use: "codex-pool-activity <id>",
Short: "Get codex pool activity for an agent",
Use: "codex-pool-activity <id>",
Short: "Get codex pool activity for an agent",
Deprecated: "use 'goclaw codex-pool activity --agent=<id>' instead",
Long: `Retrieve recent codex (context pool) activity for an agent.

GET /v1/agents/{id}/codex-pool-activity
Expand Down
13 changes: 3 additions & 10 deletions cmd/api_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,9 @@ var apiKeysCreateCmd = &cobra.Command{
scopesRaw, _ := cmd.Flags().GetString("scopes")
expiresIn, _ := cmd.Flags().GetInt("expires-in")

// Parse comma-separated scopes into slice
var scopes []string
for _, s := range strings.Split(scopesRaw, ",") {
s = strings.TrimSpace(s)
if s != "" {
scopes = append(scopes, s)
}
}
if len(scopes) == 0 {
return fmt.Errorf("at least one scope is required")
scopes, err := parseAPIKeyScopes(scopesRaw)
if err != nil {
return err
}

body := buildBody("name", name, "scopes", scopes)
Expand Down
20 changes: 20 additions & 0 deletions cmd/api_keys_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package cmd

import (
"fmt"
"strings"
)

func parseAPIKeyScopes(scopesRaw string) ([]string, error) {
var scopes []string
for _, s := range strings.Split(scopesRaw, ",") {
s = strings.TrimSpace(s)
if s != "" {
scopes = append(scopes, s)
}
}
if len(scopes) == 0 {
return nil, fmt.Errorf("at least one scope is required")
}
return scopes, nil
}
85 changes: 85 additions & 0 deletions cmd/api_keys_rotate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cmd

import (
"fmt"
"net/url"

"github.com/nextlevelbuilder/goclaw-cli/internal/output"
"github.com/nextlevelbuilder/goclaw-cli/internal/tui"
"github.com/spf13/cobra"
)

var apiKeysRotateCmd = &cobra.Command{
Use: "rotate <id>",
Short: "Create a replacement API key and revoke the old key",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !tui.Confirm("Rotate this API key?", cfg.Yes) {
return nil
}
c, err := newHTTP()
if err != nil {
return err
}
name, _ := cmd.Flags().GetString("name")
scopesRaw, _ := cmd.Flags().GetString("scopes")
expiresIn, _ := cmd.Flags().GetInt("expires-in")
scopes, err := parseAPIKeyScopes(scopesRaw)
if err != nil {
return err
}

body := buildBody("name", name, "scopes", scopes)
if expiresIn > 0 {
body["expires_in"] = expiresIn
}

data, err := c.Post("/v1/api-keys", body)
if err != nil {
return err
}
result := unmarshalMap(data)
printAPIKeyRotateResult(args[0], result)

_, err = c.Post("/v1/api-keys/"+url.PathEscape(args[0])+"/revoke", nil)
if err != nil {
return apiKeyRotatePartialError(args[0], result, err)
}
return nil
},
}

func printAPIKeyRotateResult(oldKeyID string, result map[string]any) {
if cfg.OutputFormat == "table" {
fmt.Printf("API key rotated: %s\n", str(result, "id"))
fmt.Println("--- IMPORTANT: Copy your replacement API key now. It will not be shown again. ---")
fmt.Printf("Key: %s\n", str(result, "key"))
return
}
result["old_key_id"] = oldKeyID
result["old_revoke_status"] = "pending"
printer.Print(result)
}

func apiKeyRotatePartialError(oldKeyID string, result map[string]any, revokeErr error) error {
details := map[string]any{
"new_key_id": str(result, "id"),
"old_key_id": oldKeyID,
"old_revoke_status": "failed",
"revoke_error": revokeErr.Error(),
}
return &output.ErrorDetail{
Code: "INTERNAL",
Message: "replacement API key was created, but revoking the old key failed",
Details: details,
}
}

func init() {
apiKeysRotateCmd.Flags().String("name", "", "Human-readable replacement key name")
_ = apiKeysRotateCmd.MarkFlagRequired("name")
apiKeysRotateCmd.Flags().String("scopes", "", "Comma-separated replacement key scopes")
_ = apiKeysRotateCmd.MarkFlagRequired("scopes")
apiKeysRotateCmd.Flags().Int("expires-in", 0, "TTL in seconds (0 = no expiry)")
apiKeysCmd.AddCommand(apiKeysRotateCmd)
}
Loading
Loading