Initial MVP implementation of Claudia Docs CLI

- Auth commands: login, logout, status
- Document commands: create, list, get, update, delete
- Project commands: list, get
- Folder commands: list, create
- Tag commands: list, create, add
- Search command
- Reasoning save command
- JSON output format with --output text option
- Viper-based configuration management
- Cobra CLI framework
This commit is contained in:
Claudia CLI Bot
2026-03-31 01:25:15 +00:00
commit aca95d90f3
17 changed files with 3270 additions and 0 deletions

169
internal/auth/auth.go Normal file
View File

@@ -0,0 +1,169 @@
package auth
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/claudia/docs-cli/internal/api"
"github.com/claudia/docs-cli/internal/config"
"github.com/claudia/docs-cli/pkg/types"
)
// AuthClient handles authentication
type AuthClient struct {
client *api.Client
}
// NewAuthClient creates a new auth client
func NewAuthClient(c *api.Client) *AuthClient {
return &AuthClient{client: c}
}
// Login authenticates a user and returns the token
func (a *AuthClient) Login(username, password string) (*types.AuthResponse, error) {
req := types.LoginRequest{
Username: username,
Password: password,
}
resp, err := a.client.DoRequest(http.MethodPost, "/auth/login", req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return nil, handleAuthError(resp, body)
}
var authResp types.Response
if err := json.Unmarshal(body, &authResp); err != nil {
return nil, err
}
return mapToAuthResponse(authResp.Data), nil
}
// Register registers a new user
func (a *AuthClient) Register(username, password string) (*types.AuthResponse, error) {
req := types.RegisterRequest{
Username: username,
Password: password,
}
resp, err := a.client.DoRequest(http.MethodPost, "/auth/register", req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return nil, handleAuthError(resp, body)
}
var authResp types.Response
if err := json.Unmarshal(body, &authResp); err != nil {
return nil, err
}
return mapToAuthResponse(authResp.Data), nil
}
// Me returns the current user info
func (a *AuthClient) Me() (map[string]interface{}, error) {
resp, err := a.client.DoRequest(http.MethodGet, "/auth/me", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, handleAuthError(resp, body)
}
var authResp types.Response
if err := json.Unmarshal(body, &authResp); err != nil {
return nil, err
}
if m, ok := authResp.Data.(map[string]interface{}); ok {
return m, nil
}
return nil, fmt.Errorf("unexpected response format")
}
// Logout clears the saved token
func Logout() error {
return config.ClearToken()
}
// Status returns the current auth status
func Status() (map[string]interface{}, error) {
cfg := config.GetConfig()
if cfg == nil {
return map[string]interface{}{
"authenticated": false,
"server": "http://localhost:8000",
}, nil
}
token := cfg.Token
if token == "" {
return map[string]interface{}{
"authenticated": false,
"server": cfg.Server,
}, nil
}
return map[string]interface{}{
"authenticated": true,
"server": cfg.Server,
}, nil
}
func handleAuthError(resp *http.Response, body []byte) error {
var authResp types.Response
if err := json.Unmarshal(body, &authResp); err != nil {
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
if authResp.Error != nil {
return fmt.Errorf("%s: %s", authResp.Error.Code, authResp.Error.Message)
}
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
func mapToAuthResponse(data interface{}) *types.AuthResponse {
resp := &types.AuthResponse{}
if m, ok := data.(map[string]interface{}); ok {
if v, ok := m["access_token"].(string); ok {
resp.Token = v
}
if v, ok := m["token_type"].(string); ok {
resp.TokenType = v
}
if v, ok := m["expires_at"].(string); ok {
resp.ExpiresAt, _ = time.Parse(time.RFC3339, v)
} else if v, ok := m["expires_at"].(float64); ok {
resp.ExpiresAt = time.Unix(int64(v), 0)
}
}
return resp
}