9 Commits

Author SHA1 Message Date
4eb68008b3 3
Some checks failed
Release / build (amd64, darwin, ) (push) Failing after 35s
Release / build (amd64, linux, ) (push) Failing after 10s
Release / build (amd64, windows, .exe) (push) Failing after 10s
Release / build (arm64, darwin, ) (push) Failing after 10s
Release / build (arm64, linux, ) (push) Failing after 9s
Release / release (push) Has been skipped
2026-03-31 01:46:03 -03:00
4258feab81 aaaa 2026-03-31 01:38:04 -03:00
fa6c44fcf1 ddd
Some checks failed
Release / build (push) Failing after 5m26s
Release / release (push) Has been skipped
2026-03-31 01:34:11 -03:00
3a50eac98e actualizacion 2026-03-31 01:06:38 -03:00
edddc538f9 readme actualizado
Some checks failed
Release / release (push) Has been cancelled
Release / build (amd64, darwin, ) (push) Failing after 38s
Release / build (amd64, linux, ) (push) Failing after 40s
Release / build (arm64, darwin, ) (push) Has been cancelled
Release / build (arm64, linux, ) (push) Has been cancelled
Release / build (amd64, windows, .exe) (push) Has been cancelled
2026-03-31 00:26:23 -03:00
258b1b0a0b actions 2026-03-31 00:18:20 -03:00
Claudia CLI Bot
4dd56b82c6 docs: Add comprehensive usage documentation to README 2026-03-31 02:28:45 +00:00
Claudia CLI Bot
f4bc5ad2e6 Merge remote changes and resolve .gitignore conflict 2026-03-31 01:26:13 +00:00
Claudia CLI Bot
aca95d90f3 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
2026-03-31 01:25:15 +00:00
19 changed files with 3644 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
# ← cada job corre dentro de esta imagen
container:
image: golang:1.22-alpine
strategy:
matrix:
include:
- goos: linux
goarch: amd64
suffix: ""
- goos: linux
goarch: arm64
suffix: ""
- goos: windows
goarch: amd64
suffix: ".exe"
- goos: darwin
goarch: amd64
suffix: ""
- goos: darwin
goarch: arm64
suffix: ""
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Compilar
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
run: |
BINARY_NAME="app-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.suffix }}"
mkdir -p dist
go build \
-ldflags="-w -s \
-X main.version=${{ gitea.ref_name }} \
-X main.commit=${{ gitea.sha }}" \
-o "dist/${BINARY_NAME}" \
./cmd/main.go
- name: Comprimir
run: |
cd dist
BINARY_NAME="app-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.suffix }}"
if [ "${{ matrix.goos }}" = "windows" ]; then
apk add --no-cache zip
zip "${BINARY_NAME%.exe}.zip" "${BINARY_NAME}"
else
tar -czf "${BINARY_NAME}.tar.gz" "${BINARY_NAME}"
fi
- name: Subir artefacto temporal
uses: actions/upload-artifact@v4
with:
name: bin-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/*.tar.gz
retention-days: 1
release:
runs-on: ubuntu-latest
needs: build
container:
image: alpine:3.19
steps:
- name: Instalar dependencias
run: apk add --no-cache git
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Descargar artefactos
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
- name: Generar changelog
run: |
PREV_TAG=$(git tag --sort=-version:refname | sed -n '2p')
if [ -z "$PREV_TAG" ]; then
git log --pretty=format:"- %s (%h)" | head -20 > changelog.txt
else
git log "${PREV_TAG}..${{ gitea.ref_name }}" \
--pretty=format:"- %s (%h)" > changelog.txt
fi
cat changelog.txt
- name: Crear checksums
run: |
cd dist
sha256sum * > checksums.txt
cat checksums.txt
- name: Crear Release
uses: https://gitea.com/actions/gitea-release-action@v1
with:
token: ${{ secrets.RELEASE_TOKEN }}
tag_name: ${{ gitea.ref_name }}
name: "Release ${{ gitea.ref_name }}"
body_path: changelog.txt
files: |
dist/*.tar.gz
dist/checksums.txt

2
.gitignore vendored
View File

@@ -25,3 +25,5 @@ go.work.sum
# env file # env file
.env .env
# CLI binary
claudia-docs

371
HANDOFF.md Normal file
View File

@@ -0,0 +1,371 @@
# Claudia Docs CLI — Handoff para Coder
## Overview
Este documento es la guía de inicio para implementar **Claudia Docs CLI**, una herramienta de línea de comandos en Go que permite a Claudia (y otros agentes) interactuar con Claudia Docs.
**Tiempo estimado de implementación:** 2-3 días (MVP completo)
---
## 1. Contexto del Proyecto
### Qué es
- CLI written en **Go** con framework **Cobra**
- Se comunica con la API REST de Claudia Docs (`/api/v1`)
- Autenticación via JWT
- Output por defecto en JSON, parseable por agentes
### Para qué sirve
- Claudia guarda documentos y reasoning via CLI
- Agentes pueden integrar el CLI en sus scripts/prompts
- No requiere navegador ni UI web
### Repositorio
```
/projects/claudia-docs-cli/
├── SPEC.md # Especificación completa (LEER PRIMERO)
├── HANDOFF.md # Este documento
├── cmd/ # Entry point
├── internal/ # Lógica de negocio
└── pkg/types/ # Tipos compartidos
```
---
## 2. Documentación Obligatoria
**LEER ANTES DE CODear:**
1. **`SPEC.md`** — Especificación completa del CLI
- Command structure
- Flags y argumentos
- Output format
- Configuración
- Estructura del proyecto
2. **`../claudia-docs/SPEC.md`** — Spec del backend
- API endpoints (sección 5)
- Data model (sección 4)
- Auth flow
- Para probar la API mientras sviluppo
---
## 3. Tech Stack
| Componente | Tecnología |
|------------|------------|
| Language | Go 1.21+ |
| CLI Framework | `github.com/spf13/cobra` |
| Config | `github.com/spf13/viper` |
| JWT | `github.com/golang-jwt/jwt/v5` |
| HTTP | `net/http` (stdlib) |
| JSON | `encoding/json` (stdlib) |
**No agregar otras dependencias sin consultar.**
---
## 4. Configuración del Entorno
### Backend local (para testing)
```bash
# En /projects/claudia-docs/
docker-compose up -d
# API disponible en http://localhost:8000
# Swagger: http://localhost:8000/docs
```
### Credenciales de test
```bash
INITIAL_ADMIN_USERNAME=admin
INITIAL_ADMIN_PASSWORD=admin123
```
### Variables de entorno para testing
```bash
export CLAUDIA_SERVER=http://localhost:8000
export CLAUDIA_TOKEN=""
```
---
## 5. Implementación Paso a Paso
### Paso 1: Setup del proyecto
```bash
cd /projects/claudia-docs-cli
# Inicializar go module
go mod init github.com/claudia/docs-cli
# Agregar dependencias
go get github.com/spf13/cobra@v1.8.0
go get github.com/spf13/viper@v1.18.2
go get github.com/golang-jwt/jwt/v5@v5.2.0
```
### Paso 2: Estructura de carpetas
```
cmd/claudia-docs/main.go # Entry point
internal/
api/client.go # HTTP client base
api/documents.go # Document endpoints
auth/auth.go # Login, JWT handling
config/config.go # Viper config
output/output.go # JSON/Text formatting
pkg/types/types.go # Tipos compartidos
cobra/commands.go # Definitions de comandos
```
### Paso 3: Implementar en orden
1. **`pkg/types/types.go`** — Tipos básicos (Request/Response)
2. **`internal/config/config.go`** — Carga de config desde yaml/env
3. **`internal/output/output.go`** — Formatter JSON/Text
4. **`internal/api/client.go`** — HTTP client con auth
5. **`internal/auth/auth.go`** — Login, logout, JWT
6. **`cobra/commands.go`** — Estructura de comandos
7. **`cmd/claudia-docs/main.go`** — Entry point + root command
8. **`internal/api/*.go`** — Cada recurso (documents, projects, folders, tags, search, reasoning)
### Paso 4: Testing manual
```bash
# Build
go build -o claudia-docs ./cmd/claudia-docs
# Login
./claudia-docs auth login -u admin -p admin123
# Probar commands
./claudia-docs auth status
./claudia-docs project list
./claudia-docs doc create -t "Test" -c "# Hello" -p <project-id>
./claudia-docs doc list --project-id <project-id>
```
---
## 6. API Endpoints a Consumir
Base URL: `${CLAUDIA_SERVER}/api/v1`
### Auth
```
POST /auth/register
POST /auth/login → { access_token }
GET /auth/me
```
### Projects
```
GET /projects
POST /projects
GET /projects/:id
PUT /projects/:id
DELETE /projects/:id
```
### Documents
```
GET /projects/:projectId/documents
POST /projects/:projectId/documents
GET /documents/:id
PUT /documents/:id
DELETE /documents/:id
PUT /documents/:id/content
```
### Folders
```
GET /projects/:projectId/folders
POST /projects/:projectId/folders
GET /folders/:id
PUT /folders/:id
DELETE /folders/:id
```
### Tags
```
GET /tags
POST /tags
POST /documents/:id/tags
DELETE /documents/:id/tags/:tagId
```
### Search
```
GET /search?q=...&project_id=...&tags=...
```
### Reasoning
```
PUT /documents/:id/reasoning
GET /documents/:id/reasoning
```
**Ver spec del backend para Request/Response bodies exactos.**
---
## 7. Formato de Output
### JSON Response Structure
```go
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error *ErrorInfo `json:"error,omitempty"`
Meta MetaInfo `json:"meta"`
}
type ErrorInfo struct {
Code string `json:"code"`
Message string `json:"message"`
}
type MetaInfo struct {
Command string `json:"command"`
DurationMs int64 `json:"duration_ms"`
Server string `json:"server,omitempty"`
}
```
### Comportamiento
- **Success:** `{"success": true, "data": {...}, "meta": {...}}`
- **Error:** `{"success": false, "error": {"code": "...", "message": "..."}, "meta": {...}}`
- **--quiet:** Solo JSON, sin prints a stdout
- **--output text:** Print amigable para humanos
---
## 8. Config File
Path: `~/.claudia-docs.yaml`
```yaml
server: http://localhost:8000
token: ""
timeout: 30s
output: json
agents:
default:
token: ""
default_project: ""
```
**Viper maneja优先级:**
1. Flags (--server, --token)
2. Environment (CLAUDIA_SERVER, CLAUDIA_TOKEN)
3. Config file (~/.claudia-docs.yaml)
4. Defaults
---
## 9. Testing Checklist
### Auth
- [ ] Login exitoso guarda token en config
- [ ] Login con credenciales inválidas retorna error
- [ ] Logout limpia token
- [ ] Commands sin token retornan 401
### Documents
- [ ] Create documento retorna id
- [ ] List retorna array de documentos
- [ ] Get retorna documento con contenido
- [ ] Update cambia título/contenido
- [ ] Delete elimina documento
### Projects/Folders
- [ ] CRUD completo funciona
### Search
- [ ] Búsqueda retorna resultados
- [ ] Filtros por proyecto/tag funcionan
### Output
- [ ] JSON output es válido
- [ ] --output text muestra formato legible
- [ ] --quiet solo imprime JSON
---
## 10. Build & Release
### Build local
```bash
# Linux
GOOS=linux GOARCH=amd64 go build -o claudia-docs-linux-amd64 ./cmd/claudia-docs
# macOS
GOOS=darwin GOARCH=amd64 go build -o claudia-docs-darwin-amd64 ./cmd/claudia-docs
# Windows
GOOS=windows GOARCH=amd64 go build -o claudia-docs-windows-amd64.exe ./cmd/claudia-docs
```
### Release script (para después)
```bash
# Generar checksums
sha256sum claudia-docs-* > checksums.txt
# Tag y release en Git
git tag v1.0.0
git push origin v1.0.0
```
---
## 11. Criterios de Aceptación
### Must Have (MVP)
- [ ] `claudia-docs auth login` funciona y guarda token
- [ ] `claudia-docs auth logout` limpia token
- [ ] `claudia-docs auth status` muestra estado
- [ ] `claudia-docs doc create` crea documento
- [ ] `claudia-docs doc list` lista documentos
- [ ] `claudia-docs doc get` obtiene documento
- [ ] `claudia-docs doc update` actualiza documento
- [ ] `claudia-docs doc delete` elimina documento
- [ ] `claudia-docs project list` lista proyectos
- [ ] Output JSON es parseable
- [ ] Config file ~/.claudia-docs.yaml se crea
- [ ] Flags --server y --token funcionan
- [ ] Help con --help funciona
### Should Have (Phase 2)
- [ ] Tags CRUD
- [ ] Search
- [ ] Reasoning save
- [ ] --output text
---
## 12. Contacto para Dudas
**Para preguntas sobre el diseño o specs:**
- Crear issue en el proyecto
- O preguntar directamente
**Para preguntas sobre implementación:**
- Si hay ambigüedad en el spec, preguntar antes de asumir
---
## 13. Enlaces
- Spec CLI: `/projects/claudia-docs-cli/SPEC.md`
- Spec Backend: `/projects/claudia-docs/SPEC.md`
- Backend API Docs: `http://localhost:8000/docs` (si backend corriendo)
---
**¡Happy coding! 🚀**

275
README.md
View File

@@ -1,2 +1,277 @@
# claudia-docs-cli # claudia-docs-cli
CLI tool for interacting with Claudia Docs from agents and scripts.
## Installation
### Download Binary
```bash
# Linux/macOS
curl -fsSL https://gitea.danielarroyo.cl/proyectos/claudia-docs-cli/releases/latest/claudia-docs -o /usr/local/bin/claudia-docs
chmod +x /usr/local/bin/claudia-docs
```
### Build from Source
```bash
git clone https://gitea.danielarroyo.cl/proyectos/claudia-docs-cli.git
cd claudia-docs-cli
go build -o claudia-docs ./cmd/claudia-docs
```
## Quick Start
```bash
# Login
./claudia-docs auth login -u admin -p your_password --save
# List projects
./claudia-docs project list
# Create document
./claudia-docs doc create -t "My Document" -c "# Hello" -p <project-id>
# List documents
./claudia-docs doc list --project-id <project-id>
```
## Global Options
| Flag | Description | Default |
|------|-------------|---------|
| `--server` | API server URL | `http://localhost:8000` |
| `--token` | JWT token | From config or `CLAUDIA_TOKEN` env |
| `--output` | Output format: `json`, `text` | `json` |
| `--quiet` | Suppress stdout, only JSON | `false` |
| `--config` | Config file path | `~/.claudia-docs.yaml` |
| `--verbose` | Verbose debug output | `false` |
## Environment Variables
| Variable | Description |
|----------|-------------|
| `CLAUDIA_SERVER` | API server URL |
| `CLAUDIA_TOKEN` | JWT token |
| `CLAUDIA_OUTPUT` | Output format |
## Commands
### Auth
```bash
# Login and save token
./claudia-docs auth login -u <username> -p <password> --save
# Check status
./claudia-docs auth status
# Logout
./claudia-docs auth logout
```
### Projects
```bash
# List all projects
./claudia-docs project list
# Get project details
./claudia-docs project get <project-id>
```
### Documents
```bash
# Create document
./claudia-docs doc create -t "Title" -c "# Content" -p <project-id> [-f <folder-id>]
# List documents in project
./claudia-docs doc list --project-id <project-id> [--limit 20] [--offset 0]
# Get document
./claudia-docs doc get <document-id> [--include-reasoning]
# Update document
./claudia-docs doc update <document-id> [-t "New Title"] [-c "New Content"]
# Delete document
./claudia-docs doc delete <document-id> [--force]
```
### Folders
```bash
# List folders in project
./claudia-docs folder list --project-id <project-id> [--parent-id <folder-id>]
# Create folder
./claudia-docs folder create --project-id <project-id> -n "Folder Name" [--parent-id <folder-id>]
```
### Tags
```bash
# List all tags
./claudia-docs tag list
# Create tag
./claudia-docs tag create -n <name> [--color <hex-color>]
# Add tag to document
./claudia-docs tag add --doc-id <document-id> --tag-id <tag-id>
```
### Search
```bash
# Full-text search
./claudia-docs search -q "query" [--project-id <project-id>] [--tags <tag1,tag2>]
```
### Reasoning
```bash
# Save reasoning metadata
./claudia-docs reasoning save -d <document-id> -t <type> -s '<json-steps>' [--confidence 0.85] [--model <model-name>]
# Types: research, planning, analysis, synthesis
# Steps JSON: '[{"step_id":"1","thought":"...","conclusion":"..."}]'
```
## Output Format
### JSON (default)
```json
{
"success": true,
"data": {...},
"error": null,
"meta": {
"command": "doc create",
"duration_ms": 45,
"server": "http://localhost:8000"
}
}
```
### Text
```bash
./claudia-docs --output text project list
```
## Configuration
Config file: `~/.claudia-docs.yaml`
```yaml
server: http://localhost:8000
token: ""
timeout: 30s
output: json
agents:
default:
token: ""
default_project: ""
```
## Examples
### Full Workflow
```bash
# 1. Login
./claudia-docs auth login -u researcher -p secret --save
# 2. List projects
./claudia-docs project list
# 3. Create document with content
./claudia-docs doc create \
--title "Research: AI Trends 2026" \
--content "# AI Trends\n\n..." \
--project-id "uuid-project"
# 4. Search
./claudia-docs search -q "AI trends"
# 5. Get and update
./claudia-docs doc get <doc-id>
./claudia-docs doc update <doc-id> -t "Updated Title"
```
### Script Integration
```bash
#!/bin/bash
export CLAUDIA_SERVER=http://localhost:8000
export CLAUDIA_TOKEN=$(./claudia-docs auth login -u agent -p pass --output json | jq -r '.data.token')
# Create document
DOC_ID=$(./claudia-docs doc create -t "Report" -c "# Content" -p $PROJECT_ID --output json | jq -r '.data.id')
# Save reasoning
./claudia-docs reasoning save -d $DOC_ID -t research -s '[{"step_id":"1","thought":"Analysis...","conclusion":"Result"}]'
```
## Help
```bash
./claudia-docs --help
./claudia-docs auth --help
./claudia-docs doc --help
```
---
# En tu máquina local
git tag v1.0.0
git push origin v1.0.0
```
---
### Secret necesario
En **Settings → Secrets → Actions** de tu repo:
| Secret | Cómo obtenerlo |
|---|---|
| `RELEASE_TOKEN` | Gitea → Settings → Applications → Generate Token (con permiso `write:repository`) |
---
### Estructura esperada del proyecto
```
mi-proyecto/
├── cmd/
│ └── main.go ← entrypoint
├── go.mod
├── go.sum
└── .gitea/
└── workflows/
└── release.yml
# Si main.go está en la raíz
-o "dist/${BINARY_NAME}" .
# Si está en otro lugar
-o "dist/${BINARY_NAME}" ./internal/cmd/server.go
```
---
### Resultado en Gitea
Una vez que hagas push del tag, en **Releases** del repo verás:
```
Release v1.0.0
├── app-linux-amd64.tar.gz
├── app-linux-arm64.tar.gz
├── app-darwin-amd64.tar.gz
├── app-darwin-arm64.tar.gz
├── app-windows-amd64.zip
└── checksums.txt

564
SPEC.md Normal file
View File

@@ -0,0 +1,564 @@
# Claudia Docs CLI — Project Specification
## 1. Concept & Vision
**Claudia Docs CLI** es una herramienta de línea de comandos que permite a Claudia (y otros agentes) interactuar con Claudia Docs sin necesidad de un navegador. Diseñado para ser invocado desde prompts, scripts y pipelines de automatización.
**Propósito:** Ser la interfaz preferida para agentes que necesitan crear documentos, guardar reasoning, y consultar información programáticamente.
**Differential:** No es un CLI genérico de API. Está optimizado para el flujo de trabajo de un agente de investigación.
---
## 2. Tech Stack
| Componente | Tecnología | Pourquoi |
|------------|------------|----------|
| **Language** | Go 1.21+ | Startup rápido, single binary, cross-compile |
| **CLI Framework** | Cobra | Battle-tested (kubectl, Hugo), experiencia del equipo |
| **HTTP Client** | net/http (stdlib) | Sin dependencias extra |
| **JWT** | golang-jwt/jwt/v5 | Maduro, mantenimiento activo |
| **JSON** | encoding/json (stdlib) | Rápido, sin dependencias |
| **Config** | spf13/viper | Manejo de config de forma idiomática |
---
## 3. Installation
### Binary Download (Recomendado para agentes)
```bash
# Linux/macOS
curl -fsSL https://releases.claudia-docs.io/latest/install.sh | bash
# Directo
curl -fsSL https://releases.claudia-docs.io/claudia-docs-linux-amd64 -o /usr/local/bin/claudia-docs
chmod +x /usr/local/bin/claudia-docs
```
### Build from Source
```bash
git clone https://git.example.com/claudia-docs-cli.git
cd claudia-docs-cli
go build -o claudia-docs ./cmd/claudia-docs
./claudia-docs --version
```
### npm (desarrolladores)
```bash
npm install -g @claudia/docs-cli
```
---
## 4. Command Structure
```
claudia-docs [global options] <command> [arguments]
GLOBAL OPTIONS:
--server URL API server URL (default: http://localhost:8000)
--token JWT JWT token (alternativa: CLAUDIA_TOKEN env var)
--output format Output format: json, text (default: json)
--quiet Suppress stdout, solo JSON
--config path Path to config file (default: ~/.claudia-docs.yaml)
--verbose Verbose output (debug)
--version Show version
--help Show help
```
---
### 4.1 Auth Commands
#### `claudia-docs auth login`
Autentica y guarda el token.
```bash
claudia-docs auth login -u <username> -p <password>
# Output JSON:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"expires_at": "2026-03-31T02:00:00Z"
},
"meta": { "command": "auth login", "duration_ms": 120 }
}
```
**Flags:**
- `-u, --username` (required): Username
- `-p, --password` (required): Password
- `--save`: Guardar token en config (~/.claudia-docs.yaml)
#### `claudia-docs auth logout`
Limpia el token guardado.
```bash
claudia-docs auth logout
```
#### `claudia-docs auth status`
Muestra estado de autenticación actual.
```bash
claudia-docs auth status
```
---
### 4.2 Document Commands
#### `claudia-docs doc create`
Crea un nuevo documento.
```bash
claudia-docs doc create [flags]
# Flags:
-t, --title string Título del documento (required)
-c, --content string Contenido markdown (default: "")
-p, --project-id string ID del proyecto (required)
-f, --folder-id string ID de carpeta (optional)
-m, --metadata JSON Metadata extra como JSON (optional)
# Ejemplo:
claudia-docs doc create \
--title "Research: AI Trends 2026" \
--content "# Research\n\nContenido..." \
--project-id "uuid-proyecto"
```
**Output:**
```json
{
"success": true,
"data": {
"id": "uuid-doc",
"title": "Research: AI Trends 2026",
"project_id": "uuid-proyecto",
"created_at": "2026-03-30T14:00:00Z"
}
}
```
---
#### `claudia-docs doc list`
Lista documentos.
```bash
claudia-docs doc list [flags]
# Flags:
-p, --project-id string Filtrar por proyecto
-f, --folder-id string Filtrar por carpeta
--limit int Límite de resultados (default: 20)
--offset int Offset para paginación (default: 0)
# Ejemplo:
claudia-docs doc list --project-id "uuid-proyecto" --limit 10
```
---
#### `claudia-docs doc get`
Obtiene un documento por ID.
```bash
claudia-docs doc get <document-id> [flags]
# Flags:
--include-reasoning Incluir metadata de reasoning
--include-tags Incluir tags
# Ejemplo:
claudia-docs doc get uuid-doc --include-reasoning --output json
```
---
#### `claudia-docs doc update`
Actualiza un documento.
```bash
claudia-docs doc update <document-id> [flags]
# Flags:
-t, --title string Nuevo título
-c, --content string Nuevo contenido
-f, --folder-id string Mover a carpeta
# Ejemplo:
claudia-docs doc update uuid-doc --title "Title v2"
```
---
#### `claudia-docs doc delete`
Elimina un documento.
```bash
claudia-docs doc delete <document-id> [--force]
# --force: Skip confirmación
```
---
### 4.3 Project Commands
#### `claudia-docs project list`
Lista proyectos del agente.
```bash
claudia-docs project list
```
#### `claudia-docs project create`
Crea un proyecto.
```bash
claudia-docs project create -n <name> [-d <description>]
```
#### `claudia-docs project get`
Obtiene un proyecto.
```bash
claudia-docs project get <project-id>
```
---
### 4.4 Folder Commands
#### `claudia-docs folder list`
Lista carpetas de un proyecto.
```bash
claudia-docs folder list --project-id <project-id> [--parent-id <folder-id>]
```
#### `claudia-docs folder create`
Crea una carpeta.
```bash
claudia-docs folder create --project-id <project-id> -n <name> [--parent-id <folder-id>]
```
---
### 4.5 Tag Commands
#### `claudia-docs tag list`
Lista todos los tags.
#### `claudia-docs tag create`
Crea un tag.
```bash
claudia-docs tag create -n <name> [--color <hex-color>]
```
#### `claudia-docs tag add`
Añade tag a documento.
```bash
claudia-docs tag add --doc-id <doc-id> --tag-id <tag-id>
```
---
### 4.6 Search Commands
#### `claudia-docs search`
Búsqueda full-text.
```bash
claudia-docs search [flags]
# Flags:
-q, --query string Texto a buscar (required)
-p, --project-id string Filtrar por proyecto
--tags string Filtrar por tags (comma-separated)
# Ejemplo:
claudia-docs search --query "API design" --project-id uuid-proyecto
```
---
### 4.7 Reasoning Commands
#### `claudia-docs reasoning save`
Guarda reasoning de un agente.
```bash
claudia-docs reasoning save [flags]
# Flags:
-d, --doc-id string Documento asociado (required)
-t, --type string Tipo: research, planning, analysis, synthesis (required)
-s, --steps JSON Array de steps de reasoning (required)
--confidence float Confidence 0.0-1.0 (optional)
--model string Modelo fuente (optional)
# Ejemplo:
claudia-docs reasoning save \
--doc-id uuid-doc \
--type research \
--steps '[{"step_id":"1","thought":"...","conclusion":"..."}]' \
--confidence 0.85 \
--model "claude-sonnet-4"
```
---
## 5. Output Format
### JSON (default)
```json
{
"success": true|false,
"data": { ... },
"error": {
"code": "ERROR_CODE",
"message": "Human readable message"
},
"meta": {
"command": "doc create",
"duration_ms": 45,
"server": "http://localhost:8000"
}
}
```
### Text (--output text)
```
✅ Document created: uuid-doc
Title: Research: AI Trends 2026
Created: 2026-03-30 14:00:00
```
### Quiet Mode (--quiet)
Solo JSON, sin mensajes de status.
---
## 6. Configuration
### File: `~/.claudia-docs.yaml`
```yaml
server: http://localhost:8000
token: "" # Se llena tras auth login --save
output: json
timeout: 30s
# Agentes múltiples (para Operator)
agents:
claudia:
token: eyJhbGciOiJIUzI1NiIs...
default_project: uuid-claudia
other:
token: eyJhbGciOiJIUzI1NiIs...
default_project: uuid-other
```
### Environment Variables
| Variable | Override | Description |
|----------|----------|-------------|
| `CLAUDIA_SERVER` | `--server` | API server URL |
| `CLAUDIA_TOKEN` | `--token` | JWT token |
| `CLAUDIA_OUTPUT` | `--output` | Output format |
| `CLAUDIA_AGENT` | - | Agent profile en config |
---
## 7. Agent Integration
### Ejemplo: Claudia invocando CLI
```bash
#!/bin/bash
# Claudia save_reasoning.sh
SERVER="${CLAUDIA_SERVER:-http://localhost:8000}"
TOKEN="${CLAUDIA_TOKEN}"
# Login si no hay token
if [ -z "$TOKEN" ]; then
TOKEN=$(claudia-docs auth login \
-u "${AGENT_USER}" \
-p "${AGENT_PASS}" \
--save \
--output json | jq -r '.data.token')
fi
# Guardar documento con reasoning
claudia-docs doc create \
--title "Analysis: $TASK_ID" \
--content "$(cat reasoning.md)" \
--project-id "${PROJECT_ID}" \
--output json | jq -r '.data.id' > /tmp/doc_id.txt
# Guardar reasoning steps
claudia-docs reasoning save \
--doc-id "$(cat /tmp/doc_id.txt)" \
--type research \
--steps "$(cat steps.json)" \
--confidence 0.85 \
--model "claude-sonnet-4"
```
### En contexto de agente (prompts)
```
[System] Para guardar reasoning, usa:
claudia-docs reasoning save --doc-id <id> --type <type> --steps '<json>'
[Tool] claudia-docs doc create --title "..." --content "..."
```
---
## 8. Project Structure
```
claudia-docs-cli/
├── cmd/
│ └── claudia-docs/
│ └── main.go # Entry point
├── internal/
│ ├── auth/
│ │ └── auth.go # Login, JWT handling
│ ├── api/
│ │ ├── client.go # HTTP client
│ │ ├── documents.go # Document API calls
│ │ ├── projects.go # Project API calls
│ │ ├── folders.go # Folder API calls
│ │ ├── tags.go # Tags API calls
│ │ ├── search.go # Search API calls
│ │ └── reasoning.go # Reasoning API calls
│ ├── config/
│ │ └── config.go # Viper config
│ └── output/
│ └── output.go # JSON/Text output formatting
├── pkg/
│ └── types/
│ └── types.go # Shared types (para compartir con backend)
├── cobra/
│ └── commands.go # Cobra command definitions
├── Makefile
├── go.mod
├── go.sum
└── README.md
```
---
## 9. Cross-Compilation
```bash
# Linux
GOOS=linux GOARCH=amd64 go build -o claudia-docs-linux-amd64 ./cmd/claudia-docs
# macOS
GOOS=darwin GOARCH=amd64 go build -o claudia-docs-darwin-amd64 ./cmd/claudia-docs
GOOS=darwin GOARCH=arm64 go build -o claudia-docs-darwin-arm64 ./cmd/claudia-docs
# Windows
GOOS=windows GOARCH=amd64 go build -o claudia-docs-windows-amd64.exe ./cmd/claudia-docs
```
---
## 10. Dependencies
```go
// go.mod
module github.com/claudia/docs-cli
go 1.21
require (
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
github.com/golang-jwt/jwt/v5 v5.2.0
)
```
**Sin dependencias externas** para HTTP client ni JSON (stdlib).
---
## 11. Error Codes
| Code | HTTP | Description |
|------|------|-------------|
| `UNAUTHORIZED` | 401 | Token inválido o expirado |
| `FORBIDDEN` | 403 | Sin permisos para el recurso |
| `NOT_FOUND` | 404 | Recurso no encontrado |
| `VALIDATION_ERROR` | 422 | Datos inválidos |
| `SERVER_ERROR` | 500 | Error interno del servidor |
| `NETWORK_ERROR` | - | No se pudo conectar al servidor |
---
## 12. Roadmap
### Phase 1: Core CLI (MVP)
- [x] Auth: login, logout, status
- [x] Documents: create, list, get, update, delete
- [x] Projects: list, get
- [x] Folders: list, get
- [x] Output JSON/Text
### Phase 2: Full Coverage
- [x] Tags: CRUD + add/remove from docs
- [x] Search
- [x] Reasoning save/list/get
### Phase 3: Advanced
- [ ] Batch operations (crear múltiples docs)
- [ ] File upload (--file path)
- [ ] Export documents
- [ ] Interactive mode (TUI)
- [ ] Shell completion
### Phase 4: Agent Features
- [ ] Streaming output
- [ ] Webhook subscriptions
- [ ] Agent-to-agent communication
---
## 13. Open Questions
1. **¿Go o Rust?** ¿Preferencia del equipo para Go vs Rust?
2. **Distribución:** ¿Binary-only es aceptable o se requiere npm/pip?
3. **TUI interactivo:** ¿Necesario en Phase 1 o Phase 3?
4. **Agent profiles:** ¿Soporte multi-agente desde inicio?
---
## 14. Metadata
| Campo | Valor |
|-------|-------|
| Project | Claudia Docs CLI |
| Version | 1.0.0 |
| Status | Specification |
| Created | 2026-03-30 |
| Author | Claudia (Chief Researcher) |
| Language | Go 1.21+ |
| Framework | Cobra |

746
cobra/commands.go Normal file
View File

@@ -0,0 +1,746 @@
package cobra
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/claudia/docs-cli/internal/api"
"github.com/claudia/docs-cli/internal/auth"
"github.com/claudia/docs-cli/internal/config"
"github.com/claudia/docs-cli/internal/output"
"github.com/claudia/docs-cli/pkg/types"
)
var (
server string
token string
outputFormat string
quiet bool
verbose bool
)
var rootCmd = &cobra.Command{
Use: "claudia-docs",
Short: "Claudia Docs CLI - Interact with Claudia Docs from the command line",
Long: `Claudia Docs CLI allows you to manage documents, projects, folders, tags, and more.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initConfig()
},
}
func Execute() error {
return rootCmd.Execute()
}
func initConfig() error {
cfg, err := config.Load()
if err != nil {
return err
}
config.UpdateFromFlags(server, token, outputFormat, quiet, verbose)
_ = cfg
return nil
}
func init() {
rootCmd.PersistentFlags().StringVar(&server, "server", "", "API server URL")
rootCmd.PersistentFlags().StringVar(&token, "token", "", "JWT token")
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "json", "Output format: json, text")
rootCmd.PersistentFlags().BoolVar(&quiet, "quiet", false, "Suppress stdout, only JSON")
rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Verbose output")
// Auth commands
rootCmd.AddCommand(authCmd)
// Document commands
rootCmd.AddCommand(docCmd)
// Project commands
rootCmd.AddCommand(projectCmd)
// Folder commands
rootCmd.AddCommand(folderCmd)
// Tag commands
rootCmd.AddCommand(tagCmd)
// Search command
rootCmd.AddCommand(searchCmd)
// Reasoning command
rootCmd.AddCommand(reasoningCmd)
}
// Auth command group
var authCmd = &cobra.Command{
Use: "auth",
Short: "Authentication commands",
}
var authLoginCmd = &cobra.Command{
Use: "login",
Short: "Login to Claudia Docs",
RunE: runAuthLogin,
}
var authLogoutCmd = &cobra.Command{
Use: "logout",
Short: "Logout and clear saved token",
RunE: runAuthLogout,
}
var authStatusCmd = &cobra.Command{
Use: "status",
Short: "Show authentication status",
RunE: runAuthStatus,
}
func init() {
authCmd.AddCommand(authLoginCmd, authLogoutCmd, authStatusCmd)
authLoginCmd.Flags().StringP("username", "u", "", "Username (required)")
authLoginCmd.Flags().StringP("password", "p", "", "Password (required)")
authLoginCmd.Flags().Bool("save", false, "Save token to config file")
authLoginCmd.MarkFlagRequired("username")
authLoginCmd.MarkFlagRequired("password")
}
func runAuthLogin(cmd *cobra.Command, args []string) error {
username, _ := cmd.Flags().GetString("username")
password, _ := cmd.Flags().GetString("password")
save, _ := cmd.Flags().GetBool("save")
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
authClient := auth.NewAuthClient(client)
authResp, err := authClient.Login(username, password)
if err != nil {
return printError("Login failed", err)
}
if save {
if err := config.SaveToken(authResp.Token); err != nil {
return printError("Failed to save token", err)
}
}
data := map[string]interface{}{
"token": authResp.Token,
"expires_at": authResp.ExpiresAt,
}
printResponse(cmd, data, "auth login")
return nil
}
func runAuthLogout(cmd *cobra.Command, args []string) error {
if err := auth.Logout(); err != nil {
return printError("Logout failed", err)
}
printResponse(cmd, map[string]string{"message": "Logged out successfully"}, "auth logout")
return nil
}
func runAuthStatus(cmd *cobra.Command, args []string) error {
status, err := auth.Status()
if err != nil {
return printError("Status check failed", err)
}
printResponse(cmd, status, "auth status")
return nil
}
// Document command group
var docCmd = &cobra.Command{
Use: "doc",
Short: "Document commands",
}
var docCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a new document",
RunE: runDocCreate,
}
var docListCmd = &cobra.Command{
Use: "list",
Short: "List documents",
RunE: runDocList,
}
var docGetCmd = &cobra.Command{
Use: "get",
Short: "Get a document by ID",
RunE: runDocGet,
}
var docUpdateCmd = &cobra.Command{
Use: "update",
Short: "Update a document",
RunE: runDocUpdate,
}
var docDeleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a document",
RunE: runDocDelete,
}
func init() {
docCmd.AddCommand(docCreateCmd, docListCmd, docGetCmd, docUpdateCmd, docDeleteCmd)
docCreateCmd.Flags().StringP("title", "t", "", "Document title (required)")
docCreateCmd.Flags().StringP("content", "c", "", "Document content")
docCreateCmd.Flags().StringP("project-id", "p", "", "Project ID (required)")
docCreateCmd.Flags().StringP("folder-id", "f", "", "Folder ID")
docCreateCmd.Flags().String("metadata", "", "Extra metadata as JSON")
docCreateCmd.MarkFlagRequired("title")
docCreateCmd.MarkFlagRequired("project-id")
docListCmd.Flags().StringP("project-id", "p", "", "Filter by project")
docListCmd.Flags().StringP("folder-id", "f", "", "Filter by folder")
docListCmd.Flags().Int("limit", 20, "Limit results")
docListCmd.Flags().Int("offset", 0, "Offset for pagination")
docGetCmd.Flags().Bool("include-reasoning", false, "Include reasoning metadata")
docGetCmd.Flags().Bool("include-tags", false, "Include tags")
docUpdateCmd.Flags().StringP("title", "t", "", "New title")
docUpdateCmd.Flags().StringP("content", "c", "", "New content")
docUpdateCmd.Flags().StringP("folder-id", "f", "", "Move to folder")
docDeleteCmd.Flags().Bool("force", false, "Skip confirmation")
}
func runDocCreate(cmd *cobra.Command, args []string) error {
title, _ := cmd.Flags().GetString("title")
content, _ := cmd.Flags().GetString("content")
projectID, _ := cmd.Flags().GetString("project-id")
folderID, _ := cmd.Flags().GetString("folder-id")
metadataJSON, _ := cmd.Flags().GetString("metadata")
var metadata map[string]interface{}
if metadataJSON != "" {
if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
return printError("Invalid metadata JSON", err)
}
}
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
docsClient := api.NewDocumentsClient(client)
req := types.CreateDocumentRequest{
Title: title,
Content: content,
FolderID: folderID,
Metadata: metadata,
}
doc, err := docsClient.Create(projectID, req)
if err != nil {
return printError("Failed to create document", err)
}
printResponse(cmd, doc, "doc create")
return nil
}
func runDocList(cmd *cobra.Command, args []string) error {
projectID, _ := cmd.Flags().GetString("project-id")
folderID, _ := cmd.Flags().GetString("folder-id")
limit, _ := cmd.Flags().GetInt("limit")
offset, _ := cmd.Flags().GetInt("offset")
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
docsClient := api.NewDocumentsClient(client)
docs, err := docsClient.List(projectID, folderID, limit, offset)
if err != nil {
return printError("Failed to list documents", err)
}
printResponse(cmd, docs, "doc list")
return nil
}
func runDocGet(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("document ID required")
}
docID := args[0]
includeReasoning, _ := cmd.Flags().GetBool("include-reasoning")
includeTags, _ := cmd.Flags().GetBool("include-tags")
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
docsClient := api.NewDocumentsClient(client)
doc, err := docsClient.Get(docID, includeReasoning, includeTags)
if err != nil {
return printError("Failed to get document", err)
}
printResponse(cmd, doc, "doc get")
return nil
}
func runDocUpdate(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("document ID required")
}
docID := args[0]
title, _ := cmd.Flags().GetString("title")
content, _ := cmd.Flags().GetString("content")
folderID, _ := cmd.Flags().GetString("folder-id")
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
docsClient := api.NewDocumentsClient(client)
req := types.UpdateDocumentRequest{
Title: title,
Content: content,
FolderID: folderID,
}
doc, err := docsClient.Update(docID, req)
if err != nil {
return printError("Failed to update document", err)
}
printResponse(cmd, doc, "doc update")
return nil
}
func runDocDelete(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("document ID required")
}
docID := args[0]
force, _ := cmd.Flags().GetBool("force")
if !force {
fmt.Printf("Are you sure you want to delete document %s? (y/N): ", docID)
var confirm string
fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
fmt.Println("Cancelled")
return nil
}
}
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
docsClient := api.NewDocumentsClient(client)
if err := docsClient.Delete(docID); err != nil {
return printError("Failed to delete document", err)
}
printResponse(cmd, map[string]string{"id": docID, "deleted": "true"}, "doc delete")
return nil
}
// Project command group
var projectCmd = &cobra.Command{
Use: "project",
Short: "Project commands",
}
var projectListCmd = &cobra.Command{
Use: "list",
Short: "List projects",
RunE: runProjectList,
}
var projectGetCmd = &cobra.Command{
Use: "get",
Short: "Get a project by ID",
RunE: runProjectGet,
}
func init() {
projectCmd.AddCommand(projectListCmd, projectGetCmd)
}
func runProjectList(cmd *cobra.Command, args []string) error {
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
projectsClient := api.NewProjectsClient(client)
projects, err := projectsClient.List()
if err != nil {
return printError("Failed to list projects", err)
}
printResponse(cmd, projects, "project list")
return nil
}
func runProjectGet(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("project ID required")
}
projectID := args[0]
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
projectsClient := api.NewProjectsClient(client)
project, err := projectsClient.Get(projectID)
if err != nil {
return printError("Failed to get project", err)
}
printResponse(cmd, project, "project get")
return nil
}
// Folder command group
var folderCmd = &cobra.Command{
Use: "folder",
Short: "Folder commands",
}
var folderListCmd = &cobra.Command{
Use: "list",
Short: "List folders in a project",
RunE: runFolderList,
}
var folderCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a folder",
RunE: runFolderCreate,
}
func init() {
folderCmd.AddCommand(folderListCmd, folderCreateCmd)
folderListCmd.Flags().StringP("project-id", "p", "", "Project ID (required)")
folderListCmd.Flags().String("parent-id", "", "Parent folder ID")
folderListCmd.MarkFlagRequired("project-id")
folderCreateCmd.Flags().StringP("project-id", "p", "", "Project ID (required)")
folderCreateCmd.Flags().StringP("name", "n", "", "Folder name (required)")
folderCreateCmd.Flags().String("parent-id", "", "Parent folder ID")
folderCreateCmd.MarkFlagRequired("project-id")
folderCreateCmd.MarkFlagRequired("name")
}
func runFolderList(cmd *cobra.Command, args []string) error {
projectID, _ := cmd.Flags().GetString("project-id")
parentID, _ := cmd.Flags().GetString("parent-id")
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
foldersClient := api.NewFoldersClient(client)
folders, err := foldersClient.List(projectID, parentID)
if err != nil {
return printError("Failed to list folders", err)
}
printResponse(cmd, folders, "folder list")
return nil
}
func runFolderCreate(cmd *cobra.Command, args []string) error {
projectID, _ := cmd.Flags().GetString("project-id")
name, _ := cmd.Flags().GetString("name")
parentID, _ := cmd.Flags().GetString("parent-id")
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
foldersClient := api.NewFoldersClient(client)
req := types.CreateFolderRequest{
Name: name,
ParentID: parentID,
}
folder, err := foldersClient.Create(projectID, req)
if err != nil {
return printError("Failed to create folder", err)
}
printResponse(cmd, folder, "folder create")
return nil
}
// Tag command group
var tagCmd = &cobra.Command{
Use: "tag",
Short: "Tag commands",
}
var tagListCmd = &cobra.Command{
Use: "list",
Short: "List all tags",
RunE: runTagList,
}
var tagCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a tag",
RunE: runTagCreate,
}
var tagAddCmd = &cobra.Command{
Use: "add",
Short: "Add a tag to a document",
RunE: runTagAdd,
}
func init() {
tagCmd.AddCommand(tagListCmd, tagCreateCmd, tagAddCmd)
tagCreateCmd.Flags().StringP("name", "n", "", "Tag name (required)")
tagCreateCmd.Flags().String("color", "", "Tag color (hex)")
tagCreateCmd.MarkFlagRequired("name")
tagAddCmd.Flags().String("doc-id", "", "Document ID (required)")
tagAddCmd.Flags().String("tag-id", "", "Tag ID (required)")
tagAddCmd.MarkFlagRequired("doc-id")
tagAddCmd.MarkFlagRequired("tag-id")
}
func runTagList(cmd *cobra.Command, args []string) error {
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
tagsClient := api.NewTagsClient(client)
tags, err := tagsClient.List()
if err != nil {
return printError("Failed to list tags", err)
}
printResponse(cmd, tags, "tag list")
return nil
}
func runTagCreate(cmd *cobra.Command, args []string) error {
name, _ := cmd.Flags().GetString("name")
color, _ := cmd.Flags().GetString("color")
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
tagsClient := api.NewTagsClient(client)
req := types.CreateTagRequest{
Name: name,
Color: color,
}
tag, err := tagsClient.Create(req)
if err != nil {
return printError("Failed to create tag", err)
}
printResponse(cmd, tag, "tag create")
return nil
}
func runTagAdd(cmd *cobra.Command, args []string) error {
docID, _ := cmd.Flags().GetString("doc-id")
tagID, _ := cmd.Flags().GetString("tag-id")
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
tagsClient := api.NewTagsClient(client)
if err := tagsClient.AddToDocument(docID, tagID); err != nil {
return printError("Failed to add tag", err)
}
printResponse(cmd, map[string]string{"doc_id": docID, "tag_id": tagID, "added": "true"}, "tag add")
return nil
}
// Search command
var searchCmd = &cobra.Command{
Use: "search",
Short: "Search documents",
RunE: runSearch,
}
func init() {
searchCmd.Flags().StringP("query", "q", "", "Search query (required)")
searchCmd.Flags().StringP("project-id", "p", "", "Filter by project")
searchCmd.Flags().String("tags", "", "Filter by tags (comma-separated)")
searchCmd.MarkFlagRequired("query")
}
func runSearch(cmd *cobra.Command, args []string) error {
query, _ := cmd.Flags().GetString("query")
projectID, _ := cmd.Flags().GetString("project-id")
tags, _ := cmd.Flags().GetString("tags")
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
searchClient := api.NewSearchClient(client)
result, err := searchClient.Search(query, projectID, tags)
if err != nil {
return printError("Search failed", err)
}
printResponse(cmd, result, "search")
return nil
}
// Reasoning command
var reasoningCmd = &cobra.Command{
Use: "reasoning",
Short: "Reasoning commands",
}
var reasoningSaveCmd = &cobra.Command{
Use: "save",
Short: "Save reasoning for a document",
RunE: runReasoningSave,
}
func init() {
reasoningCmd.AddCommand(reasoningSaveCmd)
reasoningSaveCmd.Flags().StringP("doc-id", "d", "", "Document ID (required)")
reasoningSaveCmd.Flags().StringP("type", "t", "", "Reasoning type: research, planning, analysis, synthesis (required)")
reasoningSaveCmd.Flags().String("steps", "", "JSON array of reasoning steps (required)")
reasoningSaveCmd.Flags().Float64("confidence", 0.0, "Confidence score 0.0-1.0")
reasoningSaveCmd.Flags().String("model", "", "Source model")
reasoningSaveCmd.MarkFlagRequired("doc-id")
reasoningSaveCmd.MarkFlagRequired("type")
reasoningSaveCmd.MarkFlagRequired("steps")
}
func runReasoningSave(cmd *cobra.Command, args []string) error {
docID, _ := cmd.Flags().GetString("doc-id")
reasoningType, _ := cmd.Flags().GetString("type")
stepsJSON, _ := cmd.Flags().GetString("steps")
confidence, _ := cmd.Flags().GetFloat64("confidence")
model, _ := cmd.Flags().GetString("model")
var steps []types.ReasoningStep
if err := json.Unmarshal([]byte(stepsJSON), &steps); err != nil {
return printError("Invalid steps JSON", err)
}
client, err := api.NewClient()
if err != nil {
return printError("Failed to create client", err)
}
reasoningClient := api.NewReasoningClient(client)
req := types.SaveReasoningRequest{
Type: reasoningType,
Steps: steps,
Confidence: confidence,
Model: model,
}
reasoning, err := reasoningClient.Save(docID, req)
if err != nil {
return printError("Failed to save reasoning", err)
}
printResponse(cmd, reasoning, "reasoning save")
return nil
}
// Helper functions
func printResponse(cmd *cobra.Command, data interface{}, command string) {
start := time.Now()
cfg := config.GetConfig()
server := "http://localhost:8000"
if cfg != nil {
server = cfg.Server
}
formatter := output.NewFormatter(command, server)
durationMs := int64(time.Since(start).Milliseconds())
resp := formatter.Success(data, durationMs)
if cfg != nil && cfg.Output == "text" {
printTextOutput(command, data)
} else {
output.PrintJSON(resp)
}
}
func printTextOutput(command string, data interface{}) {
switch v := data.(type) {
case map[string]interface{}:
for k, val := range v {
fmt.Printf("%s: %v\n", k, val)
}
case []interface{}:
fmt.Println("Items:")
for i, item := range v {
fmt.Printf(" [%d] %v\n", i, item)
}
default:
fmt.Println(data)
}
}
func printError(context string, err error) error {
cfg := config.GetConfig()
server := "http://localhost:8000"
if cfg != nil {
server = cfg.Server
}
formatter := output.NewFormatter("", server)
durationMs := int64(0)
code := "ERROR"
if err != nil {
code = "NETWORK_ERROR"
}
resp := formatter.Error(code, err.Error(), durationMs)
if cfg != nil && cfg.Output == "text" {
fmt.Fprintf(os.Stderr, "%s: %v\n", context, err)
} else {
output.PrintJSON(resp)
}
return err
}

31
go.mod Normal file
View File

@@ -0,0 +1,31 @@
module github.com/claudia/docs-cli
go 1.21
require (
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
)
require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

75
go.sum Normal file
View File

@@ -0,0 +1,75 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

110
internal/api/client.go Normal file
View File

@@ -0,0 +1,110 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/claudia/docs-cli/internal/config"
"github.com/claudia/docs-cli/pkg/types"
)
// Client is the API client
type Client struct {
BaseURL string
Token string
HTTPClient *http.Client
Timeout time.Duration
}
// NewClient creates a new API client
func NewClient() (*Client, error) {
cfg := config.GetConfig()
if cfg == nil {
cfg = &config.Config{Server: "http://localhost:8000", Timeout: "30s"}
}
timeout, err := time.ParseDuration(cfg.Timeout)
if err != nil {
timeout = 30 * time.Second
}
return &Client{
BaseURL: cfg.Server + "/api/v1",
Token: cfg.Token,
HTTPClient: &http.Client{
Timeout: timeout,
},
Timeout: timeout,
}, nil
}
// SetToken sets the authentication token
func (c *Client) SetToken(token string) {
c.Token = token
}
// doRequest performs an HTTP request
func (c *Client) doRequest(method, path string, body interface{}) (*http.Response, error) {
var reqBody io.Reader
if body != nil {
jsonData, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
reqBody = bytes.NewBuffer(jsonData)
}
url := c.BaseURL + path
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if c.Token != "" {
req.Header.Set("Authorization", "Bearer "+c.Token)
}
return c.HTTPClient.Do(req)
}
// DoRequest performs an HTTP request (exported for auth package)
func (c *Client) DoRequest(method, path string, body interface{}) (*http.Response, error) {
return c.doRequest(method, path, body)
}
// Response parses the API response
type Response struct {
Success bool
Data interface{}
Error *types.ErrorInfo
}
func parseResponse(resp *http.Response) (*types.Response, error) {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var apiResp types.Response
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("failed to parse response JSON: %w", err)
}
return &apiResp, nil
}
func handleError(resp *http.Response, body []byte) error {
var apiResp types.Response
if err := json.Unmarshal(body, &apiResp); err != nil {
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
if apiResp.Error != nil {
return fmt.Errorf("%s: %s", apiResp.Error.Code, apiResp.Error.Message)
}
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}

216
internal/api/documents.go Normal file
View File

@@ -0,0 +1,216 @@
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/claudia/docs-cli/pkg/types"
)
// Documents returns the documents client
type DocumentsClient struct {
client *Client
}
// NewDocumentsClient creates a new documents client
func NewDocumentsClient(c *Client) *DocumentsClient {
return &DocumentsClient{client: c}
}
// Create creates a new document
func (d *DocumentsClient) Create(projectID string, req types.CreateDocumentRequest) (*types.Document, error) {
resp, err := d.client.doRequest(http.MethodPost, "/projects/"+projectID+"/documents", 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, handleError(resp, body)
}
var docResp types.Response
if err := decodeJSON(body, &docResp); err != nil {
return nil, err
}
doc, ok := docResp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
return mapToDocument(doc), nil
}
// List lists documents
func (d *DocumentsClient) List(projectID, folderID string, limit, offset int) ([]types.Document, error) {
path := "/projects/" + projectID + "/documents"
query := fmt.Sprintf("?limit=%d&offset=%d", limit, offset)
if folderID != "" {
query += "&folder_id=" + folderID
}
resp, err := d.client.doRequest(http.MethodGet, path+query, 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, handleError(resp, body)
}
var docResp types.Response
if err := decodeJSON(body, &docResp); err != nil {
return nil, err
}
return sliceToDocuments(docResp.Data), nil
}
// Get gets a document by ID
func (d *DocumentsClient) Get(id string, includeReasoning, includeTags bool) (*types.Document, error) {
path := "/documents/" + id
query := ""
if includeReasoning || includeTags {
query = "?"
if includeReasoning {
query += "include_reasoning=true"
}
if includeTags {
if includeReasoning {
query += "&"
}
query += "include_tags=true"
}
}
resp, err := d.client.doRequest(http.MethodGet, path+query, 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, handleError(resp, body)
}
var docResp types.Response
if err := decodeJSON(body, &docResp); err != nil {
return nil, err
}
doc, ok := docResp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
return mapToDocument(doc), nil
}
// Update updates a document
func (d *DocumentsClient) Update(id string, req types.UpdateDocumentRequest) (*types.Document, error) {
resp, err := d.client.doRequest(http.MethodPut, "/documents/"+id, 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 {
return nil, handleError(resp, body)
}
var docResp types.Response
if err := decodeJSON(body, &docResp); err != nil {
return nil, err
}
doc, ok := docResp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
return mapToDocument(doc), nil
}
// Delete deletes a document
func (d *DocumentsClient) Delete(id string) error {
resp, err := d.client.doRequest(http.MethodDelete, "/documents/"+id, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return handleError(resp, body)
}
return nil
}
// Helper functions
func decodeJSON(data []byte, v interface{}) error {
return json.Unmarshal(data, v)
}
func mapToDocument(m map[string]interface{}) *types.Document {
doc := &types.Document{}
if v, ok := m["id"].(string); ok {
doc.ID = v
}
if v, ok := m["title"].(string); ok {
doc.Title = v
}
if v, ok := m["content"].(string); ok {
doc.Content = v
}
if v, ok := m["project_id"].(string); ok {
doc.ProjectID = v
}
if v, ok := m["folder_id"].(string); ok {
doc.FolderID = v
}
if v, ok := m["created_at"].(string); ok {
doc.CreatedAt, _ = time.Parse(time.RFC3339, v)
}
if v, ok := m["updated_at"].(string); ok {
doc.UpdatedAt, _ = time.Parse(time.RFC3339, v)
}
return doc
}
func sliceToDocuments(data interface{}) []types.Document {
var docs []types.Document
if arr, ok := data.([]interface{}); ok {
for _, item := range arr {
if m, ok := item.(map[string]interface{}); ok {
docs = append(docs, *mapToDocument(m))
}
}
}
return docs
}

145
internal/api/folders.go Normal file
View File

@@ -0,0 +1,145 @@
package api
import (
"fmt"
"io"
"net/http"
"time"
"github.com/claudia/docs-cli/pkg/types"
)
// FoldersClient handles folder API calls
type FoldersClient struct {
client *Client
}
// NewFoldersClient creates a new folders client
func NewFoldersClient(c *Client) *FoldersClient {
return &FoldersClient{client: c}
}
// List lists folders in a project
func (f *FoldersClient) List(projectID, parentID string) ([]types.Folder, error) {
path := "/projects/" + projectID + "/folders"
if parentID != "" {
path += "?parent_id=" + parentID
}
resp, err := f.client.doRequest(http.MethodGet, path, 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, handleError(resp, body)
}
var folderResp types.Response
if err := decodeJSON(body, &folderResp); err != nil {
return nil, err
}
return sliceToFolders(folderResp.Data), nil
}
// Create creates a new folder
func (f *FoldersClient) Create(projectID string, req types.CreateFolderRequest) (*types.Folder, error) {
resp, err := f.client.doRequest(http.MethodPost, "/projects/"+projectID+"/folders", 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, handleError(resp, body)
}
var folderResp types.Response
if err := decodeJSON(body, &folderResp); err != nil {
return nil, err
}
folder, ok := folderResp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
return mapToFolder(folder), nil
}
// Get gets a folder by ID
func (f *FoldersClient) Get(id string) (*types.Folder, error) {
resp, err := f.client.doRequest(http.MethodGet, "/folders/"+id, 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, handleError(resp, body)
}
var folderResp types.Response
if err := decodeJSON(body, &folderResp); err != nil {
return nil, err
}
folder, ok := folderResp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
return mapToFolder(folder), nil
}
func mapToFolder(m map[string]interface{}) *types.Folder {
folder := &types.Folder{}
if v, ok := m["id"].(string); ok {
folder.ID = v
}
if v, ok := m["name"].(string); ok {
folder.Name = v
}
if v, ok := m["project_id"].(string); ok {
folder.ProjectID = v
}
if v, ok := m["parent_id"].(string); ok {
folder.ParentID = v
}
if v, ok := m["created_at"].(string); ok {
folder.CreatedAt, _ = time.Parse(time.RFC3339, v)
}
if v, ok := m["updated_at"].(string); ok {
folder.UpdatedAt, _ = time.Parse(time.RFC3339, v)
}
return folder
}
func sliceToFolders(data interface{}) []types.Folder {
var folders []types.Folder
if arr, ok := data.([]interface{}); ok {
for _, item := range arr {
if m, ok := item.(map[string]interface{}); ok {
folders = append(folders, *mapToFolder(m))
}
}
}
return folders
}

137
internal/api/projects.go Normal file
View File

@@ -0,0 +1,137 @@
package api
import (
"fmt"
"io"
"net/http"
"time"
"github.com/claudia/docs-cli/pkg/types"
)
// ProjectsClient handles project API calls
type ProjectsClient struct {
client *Client
}
// NewProjectsClient creates a new projects client
func NewProjectsClient(c *Client) *ProjectsClient {
return &ProjectsClient{client: c}
}
// List lists all projects
func (p *ProjectsClient) List() ([]types.Project, error) {
resp, err := p.client.doRequest(http.MethodGet, "/projects", 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, handleError(resp, body)
}
var projResp types.Response
if err := decodeJSON(body, &projResp); err != nil {
return nil, err
}
return sliceToProjects(projResp.Data), nil
}
// Get gets a project by ID
func (p *ProjectsClient) Get(id string) (*types.Project, error) {
resp, err := p.client.doRequest(http.MethodGet, "/projects/"+id, 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, handleError(resp, body)
}
var projResp types.Response
if err := decodeJSON(body, &projResp); err != nil {
return nil, err
}
proj, ok := projResp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
return mapToProject(proj), nil
}
// Create creates a new project
func (p *ProjectsClient) Create(req types.CreateProjectRequest) (*types.Project, error) {
resp, err := p.client.doRequest(http.MethodPost, "/projects", 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, handleError(resp, body)
}
var projResp types.Response
if err := decodeJSON(body, &projResp); err != nil {
return nil, err
}
proj, ok := projResp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
return mapToProject(proj), nil
}
func mapToProject(m map[string]interface{}) *types.Project {
proj := &types.Project{}
if v, ok := m["id"].(string); ok {
proj.ID = v
}
if v, ok := m["name"].(string); ok {
proj.Name = v
}
if v, ok := m["description"].(string); ok {
proj.Description = v
}
if v, ok := m["created_at"].(string); ok {
proj.CreatedAt, _ = time.Parse(time.RFC3339, v)
}
if v, ok := m["updated_at"].(string); ok {
proj.UpdatedAt, _ = time.Parse(time.RFC3339, v)
}
return proj
}
func sliceToProjects(data interface{}) []types.Project {
var projects []types.Project
if arr, ok := data.([]interface{}); ok {
for _, item := range arr {
if m, ok := item.(map[string]interface{}); ok {
projects = append(projects, *mapToProject(m))
}
}
}
return projects
}

122
internal/api/reasoning.go Normal file
View File

@@ -0,0 +1,122 @@
package api
import (
"fmt"
"io"
"net/http"
"time"
"github.com/claudia/docs-cli/pkg/types"
)
// ReasoningClient handles reasoning API calls
type ReasoningClient struct {
client *Client
}
// NewReasoningClient creates a new reasoning client
func NewReasoningClient(c *Client) *ReasoningClient {
return &ReasoningClient{client: c}
}
// Save saves reasoning for a document
func (r *ReasoningClient) Save(docID string, req types.SaveReasoningRequest) (*types.Reasoning, error) {
path := "/documents/" + docID + "/reasoning"
resp, err := r.client.doRequest(http.MethodPut, path, 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 {
return nil, handleError(resp, body)
}
var reasoningResp types.Response
if err := decodeJSON(body, &reasoningResp); err != nil {
return nil, err
}
reasoning, ok := reasoningResp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
return mapToReasoning(reasoning), nil
}
// Get gets reasoning for a document
func (r *ReasoningClient) Get(docID string) (*types.Reasoning, error) {
path := "/documents/" + docID + "/reasoning"
resp, err := r.client.doRequest(http.MethodGet, path, 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, handleError(resp, body)
}
var reasoningResp types.Response
if err := decodeJSON(body, &reasoningResp); err != nil {
return nil, err
}
reasoning, ok := reasoningResp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
return mapToReasoning(reasoning), nil
}
func mapToReasoning(m map[string]interface{}) *types.Reasoning {
reasoning := &types.Reasoning{}
if v, ok := m["doc_id"].(string); ok {
reasoning.DocID = v
}
if v, ok := m["type"].(string); ok {
reasoning.Type = v
}
if v, ok := m["confidence"].(float64); ok {
reasoning.Confidence = v
}
if v, ok := m["model"].(string); ok {
reasoning.Model = v
}
if v, ok := m["created_at"].(string); ok {
reasoning.CreatedAt, _ = time.Parse(time.RFC3339, v)
}
if steps, ok := m["steps"].([]interface{}); ok {
for _, step := range steps {
if s, ok := step.(map[string]interface{}); ok {
reasoningStep := types.ReasoningStep{}
if sid, ok := s["step_id"].(string); ok {
reasoningStep.StepID = sid
}
if thought, ok := s["thought"].(string); ok {
reasoningStep.Thought = thought
}
if conclusion, ok := s["conclusion"].(string); ok {
reasoningStep.Conclusion = conclusion
}
if action, ok := s["action"].(string); ok {
reasoningStep.Action = action
}
reasoning.Steps = append(reasoning.Steps, reasoningStep)
}
}
}
return reasoning
}

72
internal/api/search.go Normal file
View File

@@ -0,0 +1,72 @@
package api
import (
"fmt"
"io"
"net/http"
"net/url"
"github.com/claudia/docs-cli/pkg/types"
)
// SearchClient handles search API calls
type SearchClient struct {
client *Client
}
// NewSearchClient creates a new search client
func NewSearchClient(c *Client) *SearchClient {
return &SearchClient{client: c}
}
// Search performs a full-text search
func (s *SearchClient) Search(query, projectID, tags string) (*types.SearchResult, error) {
params := url.Values{}
params.Set("q", query)
if projectID != "" {
params.Set("project_id", projectID)
}
if tags != "" {
params.Set("tags", tags)
}
path := "/search?" + params.Encode()
resp, err := s.client.doRequest(http.MethodGet, path, 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, handleError(resp, body)
}
var searchResp types.Response
if err := decodeJSON(body, &searchResp); err != nil {
return nil, err
}
return mapToSearchResult(searchResp.Data, query), nil
}
func mapToSearchResult(data interface{}, query string) *types.SearchResult {
result := &types.SearchResult{Query: query}
if m, ok := data.(map[string]interface{}); ok {
if v, ok := m["documents"].([]interface{}); ok {
for _, item := range v {
if doc, ok := item.(map[string]interface{}); ok {
result.Documents = append(result.Documents, *mapToDocument(doc))
}
}
}
if v, ok := m["total"].(float64); ok {
result.Total = int(v)
}
}
return result
}

132
internal/api/tags.go Normal file
View File

@@ -0,0 +1,132 @@
package api
import (
"fmt"
"io"
"net/http"
"github.com/claudia/docs-cli/pkg/types"
)
// TagsClient handles tag API calls
type TagsClient struct {
client *Client
}
// NewTagsClient creates a new tags client
func NewTagsClient(c *Client) *TagsClient {
return &TagsClient{client: c}
}
// List lists all tags
func (t *TagsClient) List() ([]types.Tag, error) {
resp, err := t.client.doRequest(http.MethodGet, "/tags", 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, handleError(resp, body)
}
var tagResp types.Response
if err := decodeJSON(body, &tagResp); err != nil {
return nil, err
}
return sliceToTags(tagResp.Data), nil
}
// Create creates a new tag
func (t *TagsClient) Create(req types.CreateTagRequest) (*types.Tag, error) {
resp, err := t.client.doRequest(http.MethodPost, "/tags", 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, handleError(resp, body)
}
var tagResp types.Response
if err := decodeJSON(body, &tagResp); err != nil {
return nil, err
}
tag, ok := tagResp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
return mapToTag(tag), nil
}
// AddToDocument adds a tag to a document
func (t *TagsClient) AddToDocument(docID, tagID string) error {
resp, err := t.client.doRequest(http.MethodPost, "/documents/"+docID+"/tags/"+tagID, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return handleError(resp, body)
}
return nil
}
// RemoveFromDocument removes a tag from a document
func (t *TagsClient) RemoveFromDocument(docID, tagID string) error {
resp, err := t.client.doRequest(http.MethodDelete, "/documents/"+docID+"/tags/"+tagID, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return handleError(resp, body)
}
return nil
}
func mapToTag(m map[string]interface{}) *types.Tag {
tag := &types.Tag{}
if v, ok := m["id"].(string); ok {
tag.ID = v
}
if v, ok := m["name"].(string); ok {
tag.Name = v
}
if v, ok := m["color"].(string); ok {
tag.Color = v
}
return tag
}
func sliceToTags(data interface{}) []types.Tag {
var tags []types.Tag
if arr, ok := data.([]interface{}); ok {
for _, item := range arr {
if m, ok := item.(map[string]interface{}); ok {
tags = append(tags, *mapToTag(m))
}
}
}
return tags
}

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
}

124
internal/config/config.go Normal file
View File

@@ -0,0 +1,124 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/viper"
)
// Config holds all configuration
type Config struct {
Server string
Token string
Output string
Timeout string
Quiet bool
Verbose bool
}
var cfg *Config
// Load initializes the configuration
func Load() (*Config, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get home directory: %w", err)
}
viper.SetConfigName(".claudia-docs")
viper.SetConfigType("yaml")
viper.AddConfigPath(home)
// Set defaults
viper.SetDefault("server", "http://localhost:8000")
viper.SetDefault("output", "json")
viper.SetDefault("timeout", "30s")
// Environment variables
viper.SetEnvPrefix("CLAUDIA")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
// Read config file if exists
_ = viper.ReadInConfig()
cfg = &Config{
Server: viper.GetString("server"),
Token: viper.GetString("token"),
Output: viper.GetString("output"),
Timeout: viper.GetString("timeout"),
Quiet: viper.GetBool("quiet"),
Verbose: viper.GetBool("verbose"),
}
return cfg, nil
}
// GetToken returns the current token
func GetToken() string {
if cfg == nil {
return ""
}
return cfg.Token
}
// GetServer returns the current server URL
func GetServer() string {
if cfg == nil {
return "http://localhost:8000"
}
return cfg.Server
}
// SaveToken saves a token to the config file
func SaveToken(token string) error {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
configPath := filepath.Join(home, ".claudia-docs.yaml")
viper.SetConfigFile(configPath)
viper.Set("token", token)
return viper.WriteConfig()
}
// ClearToken removes the token from config
func ClearToken() error {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
configPath := filepath.Join(home, ".claudia-docs.yaml")
viper.SetConfigFile(configPath)
viper.Set("token", "")
return viper.WriteConfig()
}
// GetConfig returns the current config
func GetConfig() *Config {
return cfg
}
// UpdateFromFlags updates config from CLI flags
func UpdateFromFlags(server, token, output string, quiet, verbose bool) {
if server != "" {
cfg.Server = server
}
if token != "" {
cfg.Token = token
}
if output != "" {
cfg.Output = output
}
cfg.Quiet = quiet
cfg.Verbose = verbose
}

82
internal/output/output.go Normal file
View File

@@ -0,0 +1,82 @@
package output
import (
"encoding/json"
"fmt"
"os"
"github.com/claudia/docs-cli/pkg/types"
)
// Formatter handles output formatting
type Formatter struct {
Command string
Server string
}
// NewFormatter creates a new formatter
func NewFormatter(command, server string) *Formatter {
return &Formatter{
Command: command,
Server: server,
}
}
// Success returns a success response
func (f *Formatter) Success(data interface{}, durationMs int64) types.Response {
return types.Response{
Success: true,
Data: data,
Meta: types.MetaInfo{
Command: f.Command,
DurationMs: durationMs,
Server: f.Server,
},
}
}
// Error returns an error response
func (f *Formatter) Error(code, message string, durationMs int64) types.Response {
return types.Response{
Success: false,
Error: &types.ErrorInfo{
Code: code,
Message: message,
},
Meta: types.MetaInfo{
Command: f.Command,
DurationMs: durationMs,
Server: f.Server,
},
}
}
// PrintJSON prints the response as JSON
func PrintJSON(resp types.Response) {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(resp)
}
// PrintJSONCompact prints the response as compact JSON
func PrintJSONCompact(resp types.Response) {
enc := json.NewEncoder(os.Stdout)
_ = enc.Encode(resp)
}
// PrintText prints a human-readable message
func PrintText(message string) {
fmt.Println(message)
}
// PrintError prints a human-readable error
func PrintError(message string) {
fmt.Fprintf(os.Stderr, "Error: %s\n", message)
}
// MeasureDuration returns a function to measure duration
func MeasureDuration() (int64, func()) {
return int64(0), func() {
// Duration is calculated where needed
}
}

149
pkg/types/types.go Normal file
View File

@@ -0,0 +1,149 @@
package types
import "time"
// Response is the standard API response wrapper
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error *ErrorInfo `json:"error,omitempty"`
Meta MetaInfo `json:"meta"`
}
// ErrorInfo contains error details
type ErrorInfo struct {
Code string `json:"code"`
Message string `json:"message"`
}
// MetaInfo contains metadata about the response
type MetaInfo struct {
Command string `json:"command"`
DurationMs int64 `json:"duration_ms"`
Server string `json:"server,omitempty"`
}
// AuthResponse is the response from auth endpoints
type AuthResponse struct {
Token string `json:"access_token,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
TokenType string `json:"token_type,omitempty"`
}
// Document represents a document
type Document struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content,omitempty"`
ProjectID string `json:"project_id"`
FolderID string `json:"folder_id,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Tags []Tag `json:"tags,omitempty"`
Reasoning []ReasoningStep `json:"reasoning,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Project represents a project
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Folder represents a folder
type Folder struct {
ID string `json:"id"`
Name string `json:"name"`
ProjectID string `json:"project_id"`
ParentID string `json:"parent_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Tag represents a tag
type Tag struct {
ID string `json:"id"`
Name string `json:"name"`
Color string `json:"color,omitempty"`
}
// ReasoningStep represents a reasoning step
type ReasoningStep struct {
StepID string `json:"step_id"`
Thought string `json:"thought"`
Conclusion string `json:"conclusion,omitempty"`
Action string `json:"action,omitempty"`
}
// Reasoning represents reasoning metadata for a document
type Reasoning struct {
DocID string `json:"doc_id"`
Type string `json:"type"`
Steps []ReasoningStep `json:"steps"`
Confidence float64 `json:"confidence,omitempty"`
Model string `json:"model,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// SearchResult represents a search result
type SearchResult struct {
Documents []Document `json:"documents"`
Total int `json:"total"`
Query string `json:"query"`
}
// CreateDocumentRequest is the request body for creating a document
type CreateDocumentRequest struct {
Title string `json:"title"`
Content string `json:"content"`
FolderID string `json:"folder_id,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
// UpdateDocumentRequest is the request body for updating a document
type UpdateDocumentRequest struct {
Title string `json:"title,omitempty"`
Content string `json:"content,omitempty"`
FolderID string `json:"folder_id,omitempty"`
}
// CreateProjectRequest is the request body for creating a project
type CreateProjectRequest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
}
// CreateFolderRequest is the request body for creating a folder
type CreateFolderRequest struct {
Name string `json:"name"`
ParentID string `json:"parent_id,omitempty"`
}
// CreateTagRequest is the request body for creating a tag
type CreateTagRequest struct {
Name string `json:"name"`
Color string `json:"color,omitempty"`
}
// SaveReasoningRequest is the request body for saving reasoning
type SaveReasoningRequest struct {
Type string `json:"type"`
Steps []ReasoningStep `json:"steps"`
Confidence float64 `json:"confidence,omitempty"`
Model string `json:"model,omitempty"`
}
// LoginRequest is the request body for login
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// RegisterRequest is the request body for registration
type RegisterRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}