Skip to content

Commit 7201a48

Browse files
nervebandclaude
andcommitted
Add LLM-friendly CLI features
- Add info command with --test-permissions flag for diagnostics - Add structured API errors with categories and hints - Add --quiet and --json-errors global flags - Add auto-update notifications with 24h cache - Add help footer with documentation links - Enhance config command with detailed help text - Update README with LLM section, exit codes, and troubleshooting - Standardize exit codes: 0=success, 1=user error, 2=system error Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 09f37e6 commit 7201a48

9 files changed

Lines changed: 976 additions & 26 deletions

File tree

README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,35 @@ api_url: http://localhost:39867
7070
output_format: json # json, text, markdown
7171
```
7272
73+
### Configuration Fields
74+
75+
| Field | Type | Default | Description |
76+
|-------|------|---------|-------------|
77+
| `api_url` | string | `http://localhost:39867` | Beeper Desktop API endpoint URL |
78+
| `output_format` | string | `json` | Default output format: `json`, `text`, or `markdown` |
79+
80+
### Environment Variables
81+
82+
Environment variables override config file settings:
83+
84+
| Variable | Description |
85+
|----------|-------------|
86+
| `BEEPER_API_URL` | Override API URL |
87+
| `BEEPER_OUTPUT_FORMAT` | Override output format |
88+
| `BEEPER_TOKEN` | API authentication token (required for most operations) |
89+
90+
### Authentication
91+
92+
Generate an API token in Beeper Desktop settings and set it as an environment variable:
93+
94+
```bash
95+
export BEEPER_TOKEN="your-token-here"
96+
```
97+
98+
Token permissions:
99+
- **read**: Access messages, chats, and accounts
100+
- **write**: Send messages, archive chats, set reminders
101+
73102
## API Coverage
74103

75104
### Read Operations
@@ -201,6 +230,66 @@ go test ./...
201230
./build.sh
202231
```
203232

233+
## For LLMs and Automated Agents
234+
235+
This CLI is designed for programmatic access by LLMs and automation tools.
236+
237+
### Recommended Workflow
238+
239+
1. **Check connectivity**: `beeper info` to verify API is reachable
240+
2. **Test permissions**: `beeper info --test-permissions` to verify token works
241+
3. **List chats**: `beeper chats list -o json` to get available chat IDs
242+
4. **Read messages**: `beeper messages list --chat-id <ID> -o json`
243+
5. **Send messages**: `beeper send --chat-id <ID> --message "text"`
244+
245+
### Error Handling
246+
247+
Use `--json-errors` for machine-readable error output:
248+
249+
```bash
250+
beeper chats list --json-errors 2>&1
251+
```
252+
253+
Error JSON format:
254+
```json
255+
{
256+
"error": "error message",
257+
"code": "error_code",
258+
"category": "auth|config|permission|not_found|network|validation|server|unknown",
259+
"operation": "operation_name",
260+
"hint": "actionable suggestion"
261+
}
262+
```
263+
264+
### Updating
265+
266+
The CLI checks for updates automatically and notifies when a new version is available. To upgrade:
267+
268+
```bash
269+
beeper upgrade
270+
```
271+
272+
Use `--quiet` to suppress update notifications in automated environments.
273+
274+
### Troubleshooting
275+
276+
| Issue | Solution |
277+
|-------|----------|
278+
| "connection refused" | Start Beeper Desktop and enable API |
279+
| "unauthorized" | Set `BEEPER_TOKEN` environment variable |
280+
| "not found" | Verify chat/message ID with `beeper chats list` |
281+
| "timeout" | Check network, restart Beeper Desktop |
282+
283+
Run `beeper info` for diagnostic information.
284+
285+
## Exit Codes
286+
287+
| Code | Meaning |
288+
|------|---------|
289+
| 0 | Success |
290+
| 1 | User/Application error (invalid arguments, missing resources, permission denied) |
291+
| 2 | System/Network error (connection failed, timeout, server error) |
292+
204293
## Requirements
205294

206295
- Beeper Desktop application installed and running

cmd/config.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,35 @@ import (
1010
var configCmd = &cobra.Command{
1111
Use: "config",
1212
Short: "Manage configuration",
13-
Long: `View and modify Beeper CLI configuration settings.`,
13+
Long: `View and modify Beeper CLI configuration settings.
14+
15+
Configuration File:
16+
Location: ~/.beeper-api-cli/config.yaml
17+
18+
Configuration Fields:
19+
api_url The Beeper Desktop API URL (default: http://localhost:39867)
20+
output_format Default output format: json, text, or markdown (default: json)
21+
22+
Environment Variables (override config file):
23+
BEEPER_API_URL Override api_url
24+
BEEPER_OUTPUT_FORMAT Override output_format
25+
BEEPER_TOKEN API authentication token (required for most operations)
26+
27+
Example config.yaml:
28+
api_url: http://localhost:39867
29+
output_format: json
30+
31+
Manual Editing:
32+
You can edit the config file directly with any text editor.
33+
Changes take effect on the next command execution.`,
1434
}
1535

1636
var configShowCmd = &cobra.Command{
1737
Use: "show",
1838
Short: "Display current configuration",
1939
RunE: func(cmd *cobra.Command, args []string) error {
20-
fmt.Printf("API URL: %s\n", cfg.APIURL)
40+
fmt.Printf("Config File: %s\n", config.GetConfigPath())
41+
fmt.Printf("API URL: %s\n", cfg.APIURL)
2142
fmt.Printf("Output Format: %s\n", cfg.OutputFormat)
2243
return nil
2344
},

cmd/errors.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"strings"
9+
10+
"github.com/nerveband/beeper-api-cli/internal/api"
11+
)
12+
13+
// JSONError represents an error in JSON format for --json-errors output
14+
type JSONError struct {
15+
Error string `json:"error"`
16+
Code string `json:"code,omitempty"`
17+
Category string `json:"category,omitempty"`
18+
Operation string `json:"operation,omitempty"`
19+
Hint string `json:"hint,omitempty"`
20+
}
21+
22+
// formatError formats an error for display, adding hints if available
23+
func formatError(err error) string {
24+
var apiErr *api.APIError
25+
if errors.As(err, &apiErr) {
26+
var sb strings.Builder
27+
sb.WriteString("Error: ")
28+
sb.WriteString(apiErr.Message)
29+
sb.WriteString("\n")
30+
31+
if apiErr.Hint != "" && !quietMode {
32+
sb.WriteString("\nHint: ")
33+
sb.WriteString(apiErr.Hint)
34+
sb.WriteString("\n")
35+
}
36+
37+
return sb.String()
38+
}
39+
40+
// For non-API errors, try to provide helpful hints based on error content
41+
errMsg := err.Error()
42+
hint := getGenericHint(errMsg)
43+
44+
if hint != "" && !quietMode {
45+
return fmt.Sprintf("Error: %s\n\nHint: %s\n", errMsg, hint)
46+
}
47+
48+
return fmt.Sprintf("Error: %s\n", errMsg)
49+
}
50+
51+
// formatErrorAsJSON formats an error as JSON for --json-errors output
52+
func formatErrorAsJSON(err error) string {
53+
jsonErr := JSONError{}
54+
55+
var apiErr *api.APIError
56+
if errors.As(err, &apiErr) {
57+
jsonErr.Error = apiErr.Message
58+
jsonErr.Code = apiErr.Code
59+
jsonErr.Category = string(apiErr.Category)
60+
jsonErr.Operation = apiErr.Operation
61+
jsonErr.Hint = apiErr.Hint
62+
} else {
63+
jsonErr.Error = err.Error()
64+
jsonErr.Category = "unknown"
65+
jsonErr.Hint = getGenericHint(err.Error())
66+
}
67+
68+
data, _ := json.Marshal(jsonErr)
69+
return string(data)
70+
}
71+
72+
// getGenericHint returns a hint based on common error patterns
73+
func getGenericHint(errMsg string) string {
74+
errLower := strings.ToLower(errMsg)
75+
76+
switch {
77+
case strings.Contains(errLower, "connection refused"):
78+
return "Beeper Desktop may not be running. Start Beeper Desktop and ensure the API is enabled."
79+
case strings.Contains(errLower, "no such host"):
80+
return "Could not resolve the API host. Check your network connection and API URL configuration."
81+
case strings.Contains(errLower, "timeout"):
82+
return "The request timed out. Check if Beeper Desktop is responding and your network is stable."
83+
case strings.Contains(errLower, "unauthorized") || strings.Contains(errLower, "401"):
84+
return "Authentication required. Set BEEPER_TOKEN environment variable with a valid API token."
85+
case strings.Contains(errLower, "forbidden") || strings.Contains(errLower, "403"):
86+
return "Access denied. Your token may lack the required permissions."
87+
case strings.Contains(errLower, "not found") || strings.Contains(errLower, "404"):
88+
return "Resource not found. Verify the ID is correct using 'beeper chats list'."
89+
case strings.Contains(errLower, "config"):
90+
return "Configuration error. Check your settings with 'beeper config show'."
91+
default:
92+
return ""
93+
}
94+
}
95+
96+
// handleError processes an error and exits with appropriate code
97+
// Returns the exit code (for testing purposes)
98+
func handleError(err error) int {
99+
if err == nil {
100+
return 0
101+
}
102+
103+
// Determine exit code based on error type
104+
exitCode := 1 // Default: user/application error
105+
106+
var apiErr *api.APIError
107+
if errors.As(err, &apiErr) {
108+
switch apiErr.Category {
109+
case api.CategoryNetwork:
110+
exitCode = 2 // System/network error
111+
case api.CategoryServer:
112+
exitCode = 2 // Server error (system)
113+
}
114+
} else {
115+
// Check for network-related errors in message
116+
errMsg := strings.ToLower(err.Error())
117+
if strings.Contains(errMsg, "connection refused") ||
118+
strings.Contains(errMsg, "no such host") ||
119+
strings.Contains(errMsg, "timeout") {
120+
exitCode = 2
121+
}
122+
}
123+
124+
// Output error
125+
if jsonErrors {
126+
fmt.Fprintln(os.Stderr, formatErrorAsJSON(err))
127+
} else {
128+
fmt.Fprint(os.Stderr, formatError(err))
129+
}
130+
131+
return exitCode
132+
}
133+
134+
// exitWithError handles an error and exits the process
135+
func exitWithError(err error) {
136+
code := handleError(err)
137+
os.Exit(code)
138+
}
139+
140+
// ExitCodes documents the exit codes used by the CLI
141+
var ExitCodes = map[int]string{
142+
0: "Success",
143+
1: "User/Application error (invalid arguments, missing resources, permission denied)",
144+
2: "System/Network error (connection failed, timeout, server error)",
145+
}

0 commit comments

Comments
 (0)