Skip to content

Commit 445c939

Browse files
authored
Merge pull request #18 from nextlevelbuilder/dev
release: promote dev to main (P3+P4+P5 + live API contracts)
2 parents 63b4adb + 143624d commit 445c939

42 files changed

Lines changed: 2260 additions & 260 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55

66
---
77

8-
## [Unreleased] — Domain Coverage Expansion (P0–P3)
8+
## [Unreleased] — Domain Coverage Expansion (P0–P5)
99

1010
### Added
1111

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

37+
**P4 — UX polish**
38+
- `goclaw codex-pool activity --agent=<id>|--provider=<id>` — unified Codex pool activity lookup; legacy agent/provider commands remain as deprecated aliases.
39+
- `goclaw api-keys rotate <id>` — create replacement key, show raw key once, then revoke old key with structured partial-failure reporting.
40+
- `goclaw config defaults` — read-only WS passthrough for server default config values.
41+
- `goclaw chat replay <agent> --session=<key>` and `goclaw chat sessions resume <agent> --session=<key>` — discoverability wrappers over existing chat session contracts.
42+
- `goclaw tools invoke <name> --args=<json|@file>` — alias for `--params` with file-backed JSON support.
43+
44+
**P5 — Residual command fillers**
45+
- `goclaw teams attachments download <team-id> <attachment-id> --output <file>` — authenticated attachment download with required output path and no-overwrite default.
46+
- `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.
47+
- `goclaw agents evolution update` now maps `--action=accept|reject` to the server-compatible `status=approved|rejected` payload.
48+
3749
### Notes
3850
- All new commands honor the AI-first ergonomics contract: `--output=json` envelope, central error handler, `--yes` for destructive ops, `--quiet` for CI.
39-
- P4/P5 backlog was re-swept against the current CLI surface; already-covered items were removed from residual scope before the next implementation pass.
51+
- P4/P5 backlog was re-swept against the current CLI surface; already-covered items were removed from residual scope before implementation.
4052
- Out of scope: OpenAI-compatible `/chat/completions` and `/v1/responses` endpoints (client APIs, not admin CLI surface).
4153

4254
---

README.md

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

301302
# Revoke a key
302303
goclaw api-keys revoke <key-id>
304+
305+
# Rotate a key by creating a replacement and revoking the old key
306+
goclaw api-keys rotate <key-id> --name "ci-deploy-v2" --scopes "operator.read,operator.write" --yes
303307
```
304308

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

311+
## UX Convenience Commands
312+
313+
```bash
314+
# Unified Codex pool activity
315+
goclaw codex-pool activity --agent=agent-123
316+
goclaw codex-pool activity --provider=provider-123
317+
318+
# Resolved server defaults
319+
goclaw config defaults -o json
320+
321+
# Replay or resume a known chat session
322+
goclaw chat replay myagent --session=sess-123 -o json
323+
goclaw chat sessions resume myagent --session=sess-123 -m "Continue" --no-stream
324+
325+
# Invoke a custom tool with JSON args from file
326+
goclaw tools invoke weather --args=@payload.json
327+
328+
# Download a team task attachment to an explicit file
329+
goclaw teams attachments download team-123 attachment-456 --output ./artifact.bin
330+
331+
# Approve a skill_add evolution suggestion, optionally overriding the draft
332+
goclaw agents evolution skill apply agent-123 suggestion-456 --skill-draft @./SKILL.md
333+
```
334+
307335
## API Docs
308336

309337
```bash

cmd/agents.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ var agentsListCmd = &cobra.Command{
4242
return err
4343
}
4444
if cfg.OutputFormat != "table" {
45-
printer.Print(unmarshalList(data))
45+
printer.Print(unmarshalNamedList(data, "agents"))
4646
return nil
4747
}
4848
tbl := output.NewTable("ID", "KEY", "NAME", "PROVIDER", "MODEL", "STATUS", "TYPE")
49-
for _, a := range unmarshalList(data) {
49+
for _, a := range unmarshalNamedList(data, "agents") {
5050
tbl.AddRow(str(a, "id"), str(a, "agent_key"), str(a, "display_name"),
5151
str(a, "provider"), str(a, "model"), str(a, "status"), str(a, "agent_type"))
5252
}

cmd/agents_evolution.go

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package cmd
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
"net/url"
57

68
"github.com/spf13/cobra"
79
)
@@ -85,26 +87,104 @@ Example:
8587
if err != nil {
8688
return err
8789
}
90+
status := map[string]string{
91+
"accept": "approved",
92+
"reject": "rejected",
93+
}[action]
8894
_, err = c.Patch(
89-
fmt.Sprintf("/v1/agents/%s/evolution/suggestions/%s", args[0], args[1]),
90-
map[string]any{"action": action},
95+
fmt.Sprintf(
96+
"/v1/agents/%s/evolution/suggestions/%s",
97+
url.PathEscape(args[0]),
98+
url.PathEscape(args[1]),
99+
),
100+
map[string]any{"status": status},
91101
)
92102
if err != nil {
93103
return err
94104
}
95-
printer.Success(fmt.Sprintf("Suggestion %s: %sd", args[1], action))
105+
printer.Success(fmt.Sprintf("Suggestion %s %s", args[1], status))
96106
return nil
97107
},
98108
}
99109

110+
var agentsEvolutionSkillCmd = &cobra.Command{
111+
Use: "skill",
112+
Short: "Apply skill evolution suggestions",
113+
}
114+
115+
var agentsEvolutionSkillApplyCmd = &cobra.Command{
116+
Use: "apply <id> <suggestionID>",
117+
Short: "Approve a skill_add evolution suggestion",
118+
Long: `Approve a skill_add evolution suggestion for an agent.
119+
120+
PATCH /v1/agents/{id}/evolution/suggestions/{suggestionID}
121+
122+
Example:
123+
goclaw agents evolution skill apply agent-1 sugg-42
124+
goclaw agents evolution skill apply agent-1 sugg-42 --skill-draft @./SKILL.md`,
125+
Args: cobra.ExactArgs(2),
126+
RunE: func(cmd *cobra.Command, args []string) error {
127+
body := map[string]any{"status": "approved"}
128+
if cmd.Flags().Changed("skill-draft") {
129+
draft, _ := cmd.Flags().GetString("skill-draft")
130+
content, err := readContent(draft)
131+
if err != nil {
132+
return err
133+
}
134+
body["skill_draft"] = content
135+
}
136+
c, err := newHTTP()
137+
if err != nil {
138+
return err
139+
}
140+
if err := requireSkillAddSuggestion(c, args[0], args[1]); err != nil {
141+
return err
142+
}
143+
data, err := c.Patch(
144+
fmt.Sprintf(
145+
"/v1/agents/%s/evolution/suggestions/%s",
146+
url.PathEscape(args[0]),
147+
url.PathEscape(args[1]),
148+
),
149+
body,
150+
)
151+
if err != nil {
152+
return err
153+
}
154+
printer.Print(unmarshalMap(data))
155+
return nil
156+
},
157+
}
158+
159+
func requireSkillAddSuggestion(c interface {
160+
Get(path string) (json.RawMessage, error)
161+
}, agentID, suggestionID string) error {
162+
data, err := c.Get("/v1/agents/" + url.PathEscape(agentID) + "/evolution/suggestions?status=pending&limit=500")
163+
if err != nil {
164+
return err
165+
}
166+
for _, suggestion := range unmarshalList(data) {
167+
if str(suggestion, "id") == suggestionID {
168+
if str(suggestion, "suggestion_type") != "skill_add" {
169+
return fmt.Errorf("suggestion %s is %q, not skill_add", suggestionID, str(suggestion, "suggestion_type"))
170+
}
171+
return nil
172+
}
173+
}
174+
return fmt.Errorf("suggestion %s not found in agent evolution suggestions", suggestionID)
175+
}
176+
100177
func init() {
101178
agentsEvolutionUpdateCmd.Flags().String("action", "", "Action: accept or reject")
102179
_ = agentsEvolutionUpdateCmd.MarkFlagRequired("action")
180+
agentsEvolutionSkillApplyCmd.Flags().String("skill-draft", "", "Skill draft content or @file")
181+
agentsEvolutionSkillCmd.AddCommand(agentsEvolutionSkillApplyCmd)
103182

104183
agentsEvolutionCmd.AddCommand(
105184
agentsEvolutionMetricsCmd,
106185
agentsEvolutionSuggestionsCmd,
107186
agentsEvolutionUpdateCmd,
187+
agentsEvolutionSkillCmd,
108188
)
109189
agentsCmd.AddCommand(agentsEvolutionCmd)
110190
}

cmd/agents_misc.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ Example:
3535
}
3636

3737
var agentsCodexPoolActivityCmd = &cobra.Command{
38-
Use: "codex-pool-activity <id>",
39-
Short: "Get codex pool activity for an agent",
38+
Use: "codex-pool-activity <id>",
39+
Short: "Get codex pool activity for an agent",
40+
Deprecated: "use 'goclaw codex-pool activity --agent=<id>' instead",
4041
Long: `Retrieve recent codex (context pool) activity for an agent.
4142
4243
GET /v1/agents/{id}/codex-pool-activity

cmd/api_keys.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,9 @@ var apiKeysCreateCmd = &cobra.Command{
5959
scopesRaw, _ := cmd.Flags().GetString("scopes")
6060
expiresIn, _ := cmd.Flags().GetInt("expires-in")
6161

62-
// Parse comma-separated scopes into slice
63-
var scopes []string
64-
for _, s := range strings.Split(scopesRaw, ",") {
65-
s = strings.TrimSpace(s)
66-
if s != "" {
67-
scopes = append(scopes, s)
68-
}
69-
}
70-
if len(scopes) == 0 {
71-
return fmt.Errorf("at least one scope is required")
62+
scopes, err := parseAPIKeyScopes(scopesRaw)
63+
if err != nil {
64+
return err
7265
}
7366

7467
body := buildBody("name", name, "scopes", scopes)

cmd/api_keys_helpers.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
func parseAPIKeyScopes(scopesRaw string) ([]string, error) {
9+
var scopes []string
10+
for _, s := range strings.Split(scopesRaw, ",") {
11+
s = strings.TrimSpace(s)
12+
if s != "" {
13+
scopes = append(scopes, s)
14+
}
15+
}
16+
if len(scopes) == 0 {
17+
return nil, fmt.Errorf("at least one scope is required")
18+
}
19+
return scopes, nil
20+
}

cmd/api_keys_rotate.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
7+
"github.com/nextlevelbuilder/goclaw-cli/internal/output"
8+
"github.com/nextlevelbuilder/goclaw-cli/internal/tui"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var apiKeysRotateCmd = &cobra.Command{
13+
Use: "rotate <id>",
14+
Short: "Create a replacement API key and revoke the old key",
15+
Args: cobra.ExactArgs(1),
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
if !tui.Confirm("Rotate this API key?", cfg.Yes) {
18+
return nil
19+
}
20+
c, err := newHTTP()
21+
if err != nil {
22+
return err
23+
}
24+
name, _ := cmd.Flags().GetString("name")
25+
scopesRaw, _ := cmd.Flags().GetString("scopes")
26+
expiresIn, _ := cmd.Flags().GetInt("expires-in")
27+
scopes, err := parseAPIKeyScopes(scopesRaw)
28+
if err != nil {
29+
return err
30+
}
31+
32+
body := buildBody("name", name, "scopes", scopes)
33+
if expiresIn > 0 {
34+
body["expires_in"] = expiresIn
35+
}
36+
37+
data, err := c.Post("/v1/api-keys", body)
38+
if err != nil {
39+
return err
40+
}
41+
result := unmarshalMap(data)
42+
printAPIKeyRotateResult(args[0], result)
43+
44+
_, err = c.Post("/v1/api-keys/"+url.PathEscape(args[0])+"/revoke", nil)
45+
if err != nil {
46+
return apiKeyRotatePartialError(args[0], result, err)
47+
}
48+
return nil
49+
},
50+
}
51+
52+
func printAPIKeyRotateResult(oldKeyID string, result map[string]any) {
53+
if cfg.OutputFormat == "table" {
54+
fmt.Printf("API key rotated: %s\n", str(result, "id"))
55+
fmt.Println("--- IMPORTANT: Copy your replacement API key now. It will not be shown again. ---")
56+
fmt.Printf("Key: %s\n", str(result, "key"))
57+
return
58+
}
59+
result["old_key_id"] = oldKeyID
60+
result["old_revoke_status"] = "pending"
61+
printer.Print(result)
62+
}
63+
64+
func apiKeyRotatePartialError(oldKeyID string, result map[string]any, revokeErr error) error {
65+
details := map[string]any{
66+
"new_key_id": str(result, "id"),
67+
"old_key_id": oldKeyID,
68+
"old_revoke_status": "failed",
69+
"revoke_error": revokeErr.Error(),
70+
}
71+
return &output.ErrorDetail{
72+
Code: "INTERNAL",
73+
Message: "replacement API key was created, but revoking the old key failed",
74+
Details: details,
75+
}
76+
}
77+
78+
func init() {
79+
apiKeysRotateCmd.Flags().String("name", "", "Human-readable replacement key name")
80+
_ = apiKeysRotateCmd.MarkFlagRequired("name")
81+
apiKeysRotateCmd.Flags().String("scopes", "", "Comma-separated replacement key scopes")
82+
_ = apiKeysRotateCmd.MarkFlagRequired("scopes")
83+
apiKeysRotateCmd.Flags().Int("expires-in", 0, "TTL in seconds (0 = no expiry)")
84+
apiKeysCmd.AddCommand(apiKeysRotateCmd)
85+
}

0 commit comments

Comments
 (0)