Skip to content

Commit 4229fae

Browse files
committed
feat: add toon formatting
1 parent 2948c08 commit 4229fae

7 files changed

Lines changed: 134 additions & 24 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ An OpenCloud CLI application that can be used by local AI assistants and agents
55
### Features
66

77
- Seamless integration with local AI assistants and agents via skills.
8-
- Efficient agent token usage, especially compared to tools from MCP servers.
8+
- Efficient agent token usage thanks to TOON and skill reference data.
99
- Fetching and managing OpenCloud resources.
10-
- Authentication with OpenCloud using OAuth2.
10+
- Authentication with OpenCloud using OIDC.
1111
- Single binary application for easy installation and usage.
1212

1313
### Requirements

cmd/opencloud-cli/api.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ var path string
1515
var method string
1616
var body string
1717
var verbose bool
18+
var statusOnly bool
19+
var jsonFormat bool
1820

1921
var apiCmd = &cobra.Command{
2022
Use: "api",
@@ -55,8 +57,30 @@ var apiCmd = &cobra.Command{
5557
return fmt.Errorf("error making request: %w", err)
5658
}
5759

60+
// Set up encoder
61+
encodingFormat := client.TOON
62+
if jsonFormat {
63+
encodingFormat = client.JSON
64+
}
65+
e := client.NewEncoder(encodingFormat)
66+
5867
// Print output
59-
fmt.Println(resp)
68+
if statusOnly {
69+
output, err := e.EncodeStatusCode(resp.StatusCode)
70+
if err != nil {
71+
return fmt.Errorf("failed to encode status code: %w", err)
72+
}
73+
74+
fmt.Println(output)
75+
return nil
76+
}
77+
78+
output, err := e.EncodeBody(resp.Body)
79+
if err != nil {
80+
return fmt.Errorf("failed to encode response body: %w", err)
81+
}
82+
83+
fmt.Println(output)
6084
return nil
6185
},
6286
}
@@ -67,5 +91,7 @@ func init() {
6791
apiCmd.Flags().StringVarP(&method, "method", "m", "GET", "HTTP method to use")
6892
apiCmd.Flags().StringVarP(&body, "body", "b", "", "JSON body to send with the request")
6993
apiCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output")
94+
apiCmd.Flags().BoolVar(&statusOnly, "status-only", false, "Only return the status code")
95+
apiCmd.Flags().BoolVar(&jsonFormat, "json-format", false, "Encode the output in JSON format")
7096
apiCmd.MarkFlagRequired("path")
7197
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/JammingBen/opencloud-skill-cli
33
go 1.25.0
44

55
require (
6+
github.com/alpkeskin/gotoon v0.1.1
67
github.com/fatih/color v1.18.0
78
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
89
github.com/spf13/cobra v1.10.2

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/alpkeskin/gotoon v0.1.1 h1:GQOVwMfWKINnfEA6slrXHJaJYDwnUFmrPlXOtnuja1w=
2+
github.com/alpkeskin/gotoon v0.1.1/go.mod h1:XRTz8RM4tz8M2nB37MNRN8rHF4YgeYd8nIXmoU0B0+M=
13
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
24
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
35
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=

internal/client/client.go

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package client
33
import (
44
"bytes"
55
"crypto/tls"
6-
"encoding/json"
76
"fmt"
87
"io"
98
"log/slog"
@@ -19,6 +18,11 @@ type Client struct {
1918
tokenSource oauth2.TokenSource
2019
}
2120

21+
type Response struct {
22+
StatusCode int
23+
Body string
24+
}
25+
2226
func NewClient(baseURL string, insecure bool, ts oauth2.TokenSource) *Client {
2327
return &Client{
2428
baseURL: baseURL,
@@ -27,16 +31,16 @@ func NewClient(baseURL string, insecure bool, ts oauth2.TokenSource) *Client {
2731
}
2832
}
2933

30-
// MakeRequest makes an HTTP request to the specified URL with the given method and returns the response
31-
func (c *Client) MakeRequest(path string, method string, body string) (string, error) {
34+
// MakeRequest makes an HTTP request to the specified URL with the given method and body and returns the response
35+
func (c *Client) MakeRequest(path string, method string, body string) (*Response, error) {
3236
fullURL, err := url.JoinPath(c.baseURL, "graph", path)
3337
if err != nil {
34-
return "", fmt.Errorf("failed to join path: %w", err)
38+
return nil, fmt.Errorf("failed to join path: %w", err)
3539
}
3640

3741
req, err := http.NewRequest(method, fullURL, nil)
3842
if err != nil {
39-
return "", fmt.Errorf("failed to create request: %w", err)
43+
return nil, fmt.Errorf("failed to create request: %w", err)
4044
}
4145

4246
// Set up http transport
@@ -48,7 +52,7 @@ func (c *Client) MakeRequest(path string, method string, body string) (string, e
4852
if c.tokenSource != nil {
4953
token, err := c.tokenSource.Token()
5054
if err != nil {
51-
return "", fmt.Errorf("failed to get token: %w", err)
55+
return nil, fmt.Errorf("failed to get token: %w", err)
5256
}
5357
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
5458
}
@@ -65,36 +69,39 @@ func (c *Client) MakeRequest(path string, method string, body string) (string, e
6569
client := &http.Client{Transport: tr}
6670
resp, err := client.Do(req)
6771
if err != nil {
68-
return "", fmt.Errorf("failed to execute request: %w", err)
72+
return nil, fmt.Errorf("failed to execute request: %w", err)
6973
}
7074

7175
slog.Debug("Received response", "status", resp.StatusCode)
7276

73-
// Read and return response body
77+
if resp.StatusCode >= 400 {
78+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
79+
}
80+
7481
return readBody(resp)
7582
}
7683

77-
// readBody reads the response body and returns it as a string.
78-
// It also checks for HTTP errors and returns an error if the status code is 400 or above.
79-
func readBody(resp *http.Response) (string, error) {
84+
// readBody reads the response and returns a Response struct containing the status code and body as a string
85+
func readBody(resp *http.Response) (*Response, error) {
8086
defer resp.Body.Close()
8187

82-
if resp.StatusCode >= 400 {
83-
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
88+
r := &Response{
89+
StatusCode: resp.StatusCode,
8490
}
8591

8692
if resp.Body == nil {
87-
return "", nil
93+
return r, nil
8894
}
8995

9096
bodyBytes, err := io.ReadAll(resp.Body)
9197
if err != nil {
92-
return "", fmt.Errorf("failed to read response body: %w", err)
98+
return r, fmt.Errorf("failed to read response body: %w", err)
9399
}
94-
dst := &bytes.Buffer{}
95-
if err := json.Indent(dst, bodyBytes, "", " "); err != nil {
96-
return "", err
100+
101+
if len(bodyBytes) == 0 {
102+
return r, nil
97103
}
98104

99-
return dst.String(), nil
105+
r.Body = string(bodyBytes)
106+
return r, nil
100107
}

internal/client/encoder.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package client
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/alpkeskin/gotoon"
8+
)
9+
10+
type Format int
11+
12+
const (
13+
TOON Format = iota
14+
JSON
15+
)
16+
17+
// Encoder is responsible for encoding the response body and status code in the specified format (TOON or JSON)
18+
type Encoder struct {
19+
format Format
20+
}
21+
22+
func NewEncoder(format Format) *Encoder {
23+
return &Encoder{format: format}
24+
}
25+
26+
// EncodeBody encodes the response body in the specified format (TOON or JSON)
27+
func (e *Encoder) EncodeBody(body string) (string, error) {
28+
var data any
29+
if err := json.Unmarshal([]byte(body), &data); err != nil {
30+
return "", fmt.Errorf("failed to unmarshal response body: %w", err)
31+
}
32+
33+
switch e.format {
34+
case JSON:
35+
encoded, err := json.MarshalIndent(data, "", " ")
36+
if err != nil {
37+
return "", fmt.Errorf("failed to marshal response body: %w", err)
38+
}
39+
return string(encoded), nil
40+
case TOON:
41+
encoded, err := gotoon.Encode(data)
42+
if err != nil {
43+
return "", fmt.Errorf("failed to encode response body: %w", err)
44+
}
45+
46+
return encoded, nil
47+
default:
48+
return "", fmt.Errorf("unsupported format: %d", e.format)
49+
}
50+
}
51+
52+
// EncodeStatusCode encodes only the status code in the specified format (TOON or JSON)
53+
func (e *Encoder) EncodeStatusCode(statusCode int) (string, error) {
54+
output := map[string]int{"status": statusCode}
55+
56+
switch e.format {
57+
case JSON:
58+
encoded, err := json.MarshalIndent(output, "", " ")
59+
if err != nil {
60+
return "", fmt.Errorf("failed to marshal status code: %w", err)
61+
}
62+
return string(encoded), nil
63+
case TOON:
64+
encoded, err := gotoon.Encode(output)
65+
if err != nil {
66+
return "", fmt.Errorf("failed to encode status code: %w", err)
67+
}
68+
69+
return encoded, nil
70+
default:
71+
return "", fmt.Errorf("unsupported format: %d", e.format)
72+
}
73+
}

skills/opencloud-cli/SKILL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ description: 'Manage files, folders and spaces of an OpenCloud server using the
77

88
## How to Use This Skill
99

10-
Use the `oc-cli` command to interact with the OpenCloud CLI. The `references` directory contains detailed documentation for all available resources and operations.
10+
Use the `oc-cli` command to interact with the OpenCloud CLI. The `references` directory contains detailed documentation for all available resources and operations. Responses get returned in TOON format.
1111

1212
### Examples
1313

@@ -21,7 +21,7 @@ oc-cli api --help
2121
# examples
2222
oc-cli api -p /v1beta1/me/drives -m GET # list all my drives
2323
oc-cli api -p /v1.0/drives/90eedea1-dea1-90ee-a1de-ee90a1deee90 -m GET # get a drive by its id
24-
oc-cli api -p /v1.0/drives/90eedea1-dea1-90ee-a1de-ee90a1deee90 -m PATCH -b '{"name": "New Drive Name"}' # update a drive
24+
oc-cli api -p /v1.0/drives/90eedea1-dea1-90ee-a1de-ee90a1deee90 -m PATCH -b '{"name": "New Drive Name"}' --status-only # update a drive
2525

2626
```
2727

@@ -76,3 +76,4 @@ references/
7676
## General tips
7777

7878
- prefer the `v1beta1` endpoints over the `v1.0` endpoints
79+
- use the `--status-only` flag when the response body is not needed, e.g. for update and delete operations

0 commit comments

Comments
 (0)