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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,16 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `goclaw activity aggregate --group-by {action|actor_type|entity_type|actor_id} [--from --to --limit --actor-type --actor-id --action --entity-type --entity-id]` — group audit-log activity by dimension with bucket counts (`GET /v1/activity/aggregate`). Attached as subcommand of existing `activity` parent.
- `goclaw logs aggregate [--group-by {level|source}] [--level --source --from]` — summarize the runtime log ring buffer (`GET /v1/logs/runtime/aggregate`, admin-only). Distinct from `logs tail`. Epoch-millis `last_seen` rendered as RFC3339, never scientific notation.

**Runtime & Packages parity**
- `goclaw credentials agent-credentials` — list/get/set/delete per-agent credential material for secure CLI credentials.
- `goclaw packages updates apply-all [packages...]` — accepts positional package specs in addition to `--packages`.

### Fixed

- `goclaw packages list` now decodes current server grouped payloads `{system,pip,npm,github}` in table mode while preserving raw object payloads for JSON/YAML.
- `goclaw packages install` and `goclaw packages uninstall` now send the server-compatible `package` key; legacy `--runtime python|node` translates to `pip:`/`npm:` specs.
- `goclaw packages runtimes`, `packages deny-groups`, and `packages github-releases --repo --limit` now match current server envelopes and required query parameters.
- `goclaw credentials list`, `credentials presets`, `credentials agent-grants list`, and `credentials user-credentials list` now decode current server envelope payloads.
- `goclaw traces list` now decodes the current server payload `{traces,total,limit,offset}`. JSON/YAML mode preserves that envelope; table mode renders rows from `traces` using `id`, `total_input_tokens`, `total_output_tokens`, and `total_cost`.
- `goclaw traces get <id>` — TTY mode now renders a human-readable summary (header card + span tree) instead of dumping raw JSON. JSON-mode payload unchanged. Decode failures surface as wrapped errors instead of an empty `{}`. Trace ids are validated against `^[A-Za-z0-9._-]+$` and reserved tokens (`.`, `..`) are rejected before any HTTP call. Distinct exit codes per failure: not-found → 3, permission-denied → 2, malformed-id → 4, server-failure → 5. Latent retry-body bug in `internal/client/http.go` fixed: the final 5xx/429 response body is now preserved so the typed `APIError` reaches the caller (previously collapsed to exit 1). Closes #17.
- `goclaw traces get <id>` now handles the current server detail payload `{trace,spans}` while preserving the server envelope in JSON/YAML mode.
Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ echo "Analyze this log" | goclaw chat myagent
| `storage` | Workspace file browser |
| `approvals` | Execution approval management |
| `delegations` | Delegation history |
| `credentials` | CLI credential store |
| `packages` | Runtime package inventory, installs, updates, runtimes, deny groups, GitHub releases |
| `credentials` | CLI credential store, grants, user credentials, agent credentials |
| `tts` | Text-to-speech operations |
| `media` | Media upload/download |
| `activity` | Audit log |
Expand Down Expand Up @@ -132,6 +133,40 @@ goclaw logs aggregate [--group-by <level|source>] [--level <l>] [--source <s>] [

All are one-shot HTTP — no watch loops or WS streams. `logs aggregate` is admin-only on the server; `activity aggregate --group-by actor_id` is also admin-only (server-enforced).

### Runtime & Packages

```bash
# Runtime inventory grouped by system, pip, npm, and GitHub package sources
goclaw packages list

# Install or uninstall with legacy runtime flags translated to server package specs
goclaw packages install pandas --runtime python
goclaw packages uninstall typescript --runtime node --yes

# Runtime readiness, deny groups, GitHub release lookup, and update lifecycle
goclaw packages runtimes
goclaw packages deny-groups
goclaw packages github-releases --repo cli/cli --limit 10
goclaw packages updates list
goclaw packages updates apply pip:pandas
goclaw packages updates apply-all pip:pandas npm:typescript
```

### CLI Credentials

```bash
# Server-side secure CLI credential store
goclaw credentials list
goclaw credentials presets
goclaw credentials create --body '{"preset":"git"}'

# Access grants and per-principal credential material
goclaw credentials agent-grants list <credential-id>
goclaw credentials user-credentials list <credential-id>
goclaw credentials agent-credentials list <credential-id>
goclaw credentials agent-credentials set <credential-id> <agent-id> --body '{"credential_type":"pat","env":{"GITHUB_TOKEN":"..."}}'
```

### Reading a Trace by ID

```bash
Expand Down
49 changes: 21 additions & 28 deletions cmd/admin_credentials.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package cmd

import (
"encoding/json"
"fmt"

"github.com/nextlevelbuilder/goclaw-cli/internal/output"
"github.com/nextlevelbuilder/goclaw-cli/internal/tui"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -33,14 +29,10 @@ var adminCredentialsListCmd = &cobra.Command{
return err
}
if cfg.OutputFormat != "table" {
printer.Print(unmarshalList(data))
printer.Print(rawPayload(data))
return nil
}
tbl := output.NewTable("ID", "NAME", "CREATED")
for _, cr := range unmarshalList(data) {
tbl.AddRow(str(cr, "id"), str(cr, "name"), str(cr, "created_at"))
}
printer.Print(tbl)
printer.Print(cliCredentialsTable(data))
return nil
},
}
Expand All @@ -53,8 +45,11 @@ var adminCredentialsCreateCmd = &cobra.Command{
if err != nil {
return err
}
name, _ := cmd.Flags().GetString("name")
data, err := c.Post("/v1/cli-credentials", map[string]any{"name": name})
body, err := credentialCreateBody(cmd)
if err != nil {
return err
}
data, err := c.Post("/v1/cli-credentials", body)
if err != nil {
return err
}
Expand All @@ -68,13 +63,9 @@ var adminCredentialsUpdateCmd = &cobra.Command{
Short: "Update a CLI credential",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
bodyJSON, _ := cmd.Flags().GetString("body")
if bodyJSON == "" {
return fmt.Errorf("--body is required (JSON object)")
}
var body map[string]any
if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {
return fmt.Errorf("invalid --body JSON: %w", err)
body, err := jsonObjectFlag(cmd, "body", true)
if err != nil {
return err
}
c, err := newHTTP()
if err != nil {
Expand Down Expand Up @@ -140,7 +131,11 @@ var adminCredentialsPresetsCmd = &cobra.Command{
if err != nil {
return err
}
printer.Print(unmarshalList(data))
if cfg.OutputFormat != "table" {
printer.Print(rawPayload(data))
return nil
}
printer.Print(credentialPresetsTable(data))
return nil
},
}
Expand All @@ -149,12 +144,9 @@ var adminCredentialsCheckBinaryCmd = &cobra.Command{
Use: "check-binary",
Short: "Verify a CLI binary is accessible on the server",
RunE: func(cmd *cobra.Command, args []string) error {
bodyJSON, _ := cmd.Flags().GetString("body")
var body map[string]any
if bodyJSON != "" {
if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {
return fmt.Errorf("invalid --body JSON: %w", err)
}
body, err := jsonObjectFlag(cmd, "body", false)
if err != nil {
return err
}
c, err := newHTTP()
if err != nil {
Expand All @@ -170,8 +162,9 @@ var adminCredentialsCheckBinaryCmd = &cobra.Command{
}

func init() {
adminCredentialsCreateCmd.Flags().String("name", "", "Credential name (required)")
_ = adminCredentialsCreateCmd.MarkFlagRequired("name")
adminCredentialsCreateCmd.Flags().String("name", "", "Credential binary name")
adminCredentialsCreateCmd.Flags().String("preset", "", "Credential preset name")
adminCredentialsCreateCmd.Flags().String("body", "", "Create payload as JSON object")

adminCredentialsUpdateCmd.Flags().String("body", "", "Update payload as JSON object (required)")
adminCredentialsCheckBinaryCmd.Flags().String("body", "", "Check payload as JSON object")
Expand Down
110 changes: 110 additions & 0 deletions cmd/admin_credentials_agents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package cmd

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

// admin_credentials_agents.go adds per-agent credential material management.
// Routes: GET/PUT/DELETE /v1/cli-credentials/{id}/agent-credentials[/{agentId}]

var adminCredAgentCredentialsCmd = &cobra.Command{
Use: "agent-credentials",
Short: "Manage per-agent credentials for a CLI credential",
}

var adminCredAgentCredentialsListCmd = &cobra.Command{
Use: "list <credID>",
Short: "List agent credentials for a CLI credential",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newHTTP()
if err != nil {
return err
}
data, err := c.Get("/v1/cli-credentials/" + args[0] + "/agent-credentials")
if err != nil {
return err
}
if cfg.OutputFormat != "table" {
printer.Print(rawPayload(data))
return nil
}
printer.Print(agentCredentialsTable(data))
return nil
},
}

var adminCredAgentCredentialsGetCmd = &cobra.Command{
Use: "get <credID> <agentID>",
Short: "Get an agent credential entry",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newHTTP()
if err != nil {
return err
}
data, err := c.Get("/v1/cli-credentials/" + args[0] + "/agent-credentials/" + args[1])
if err != nil {
return err
}
printer.Print(unmarshalMap(data))
return nil
},
}

var adminCredAgentCredentialsSetCmd = &cobra.Command{
Use: "set <credID> <agentID>",
Short: "Create or update an agent credential entry",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
body, err := jsonObjectFlag(cmd, "body", true)
if err != nil {
return err
}
c, err := newHTTP()
if err != nil {
return err
}
_, err = c.Put("/v1/cli-credentials/"+args[0]+"/agent-credentials/"+args[1], body)
if err != nil {
return err
}
printer.Success("Agent credential set")
return nil
},
}

var adminCredAgentCredentialsDeleteCmd = &cobra.Command{
Use: "delete <credID> <agentID>",
Short: "Delete an agent credential entry (requires --yes)",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
if !tui.Confirm("Delete agent credential?", cfg.Yes) {
return nil
}
c, err := newHTTP()
if err != nil {
return err
}
_, err = c.Delete("/v1/cli-credentials/" + args[0] + "/agent-credentials/" + args[1])
if err != nil {
return err
}
printer.Success("Agent credential deleted")
return nil
},
}

func init() {
adminCredAgentCredentialsSetCmd.Flags().String("body", "", "Credential payload as JSON object (required)")
_ = adminCredAgentCredentialsSetCmd.MarkFlagRequired("body")

adminCredAgentCredentialsCmd.AddCommand(
adminCredAgentCredentialsListCmd,
adminCredAgentCredentialsGetCmd,
adminCredAgentCredentialsSetCmd,
adminCredAgentCredentialsDeleteCmd,
)
adminCredentialsCmd.AddCommand(adminCredAgentCredentialsCmd)
}
27 changes: 11 additions & 16 deletions cmd/admin_credentials_grants.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cmd

import (
"encoding/json"
"fmt"

"github.com/nextlevelbuilder/goclaw-cli/internal/tui"
Expand Down Expand Up @@ -29,7 +28,11 @@ var adminCredGrantsListCmd = &cobra.Command{
if err != nil {
return err
}
printer.Print(unmarshalList(data))
if cfg.OutputFormat != "table" {
printer.Print(rawPayload(data))
return nil
}
printer.Print(credentialGrantsTable(data))
return nil
},
}
Expand All @@ -39,13 +42,9 @@ var adminCredGrantsCreateCmd = &cobra.Command{
Short: "Create an agent grant for a CLI credential",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
bodyJSON, _ := cmd.Flags().GetString("body")
if bodyJSON == "" {
return fmt.Errorf("--body is required (JSON object)")
}
var body map[string]any
if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {
return fmt.Errorf("invalid --body JSON: %w", err)
body, err := jsonObjectFlag(cmd, "body", true)
if err != nil {
return err
}
c, err := newHTTP()
if err != nil {
Expand Down Expand Up @@ -84,13 +83,9 @@ var adminCredGrantsUpdateCmd = &cobra.Command{
Short: "Update an agent grant",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
bodyJSON, _ := cmd.Flags().GetString("body")
if bodyJSON == "" {
return fmt.Errorf("--body is required (JSON object)")
}
var body map[string]any
if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {
return fmt.Errorf("invalid --body JSON: %w", err)
body, err := jsonObjectFlag(cmd, "body", true)
if err != nil {
return err
}
c, err := newHTTP()
if err != nil {
Expand Down
Loading
Loading