Compare commits
14 Commits
136204a1fd
...
v0.1.8
| Author | SHA1 | Date | |
|---|---|---|---|
| 65293d0961 | |||
| f9464c9def | |||
|
|
b6df678845 | ||
| 0d23a8b8b0 | |||
|
|
17c9393b98 | ||
| 4eb68008b3 | |||
| 4258feab81 | |||
| fa6c44fcf1 | |||
| 3a50eac98e | |||
| edddc538f9 | |||
| 258b1b0a0b | |||
|
|
4dd56b82c6 | ||
|
|
f4bc5ad2e6 | ||
|
|
aca95d90f3 |
209
.gitea/workflows/release.yml
Normal file
209
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version tag (e.g., v1.0.0)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
env:
|
||||||
|
BINARY_NAME: claudia-docs
|
||||||
|
GO_VERSION: '1.22'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
ext: ''
|
||||||
|
- goos: linux
|
||||||
|
goarch: arm64
|
||||||
|
ext: ''
|
||||||
|
- goos: windows
|
||||||
|
goarch: amd64
|
||||||
|
ext: '.exe'
|
||||||
|
- goos: darwin
|
||||||
|
goarch: amd64
|
||||||
|
ext: ''
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm64
|
||||||
|
ext: ''
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ gitea.event.inputs.version }}" ]; then
|
||||||
|
VERSION="${{ gitea.event.inputs.version }}"
|
||||||
|
else
|
||||||
|
VERSION="${{ gitea.ref_name }}"
|
||||||
|
fi
|
||||||
|
echo "version=${VERSION#v}" >> $GITEA_OUTPUT
|
||||||
|
echo "tag=${VERSION}" >> $GITEA_OUTPUT
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
run: |
|
||||||
|
BINARY="${{ env.BINARY_NAME }}-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}"
|
||||||
|
mkdir -p dist
|
||||||
|
|
||||||
|
go build \
|
||||||
|
-ldflags="-w -s \
|
||||||
|
-X main.version=${{ steps.version.outputs.version }} \
|
||||||
|
-X main.commit=$(git rev-parse --short HEAD) \
|
||||||
|
-X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
|
-o "dist/${BINARY}" \
|
||||||
|
./cmd/claudia-docs
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
run: |
|
||||||
|
cd dist
|
||||||
|
BINARY="${{ env.BINARY_NAME }}-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}"
|
||||||
|
case "${{ matrix.goos }}" in
|
||||||
|
windows)
|
||||||
|
zip "${BINARY%.exe}.zip" "${BINARY}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
tar -czf "${BINARY}.tar.gz" "${BINARY}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
path: dist/*
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: startsWith(gitea.ref, 'refs/tags/v') || gitea.event.inputs.version != ''
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ gitea.event.inputs.version }}" ]; then
|
||||||
|
VERSION="${{ gitea.event.inputs.version }}"
|
||||||
|
else
|
||||||
|
VERSION="${{ gitea.ref_name }}"
|
||||||
|
fi
|
||||||
|
echo "version=${VERSION#v}" >> $GITEA_OUTPUT
|
||||||
|
echo "tag=${VERSION}" >> $GITEA_OUTPUT
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: dist/
|
||||||
|
# ← sin merge-multiple, quedan en subdirectorios:
|
||||||
|
# dist/linux-amd64/claudia-docs-linux-amd64.tar.gz
|
||||||
|
# dist/windows-amd64/claudia-docs-windows-amd64.zip
|
||||||
|
# etc.
|
||||||
|
|
||||||
|
- name: Mover artefactos a dist/
|
||||||
|
run: |
|
||||||
|
find dist/ -mindepth 2 -type f \
|
||||||
|
\( -name "*.tar.gz" -o -name "*.zip" \) \
|
||||||
|
-exec mv {} dist/ \;
|
||||||
|
# Eliminar subdirectorios vacíos
|
||||||
|
find dist/ -mindepth 1 -type d -delete
|
||||||
|
ls -lh dist/
|
||||||
|
|
||||||
|
- name: Generate changelog
|
||||||
|
run: |
|
||||||
|
PREV_TAG=$(git tag --sort=-version:refname | sed -n '2p')
|
||||||
|
if [ -z "$PREV_TAG" ]; then
|
||||||
|
echo "# Changes (full history)" > changelog.md
|
||||||
|
git log --pretty=format:"- %s (%h)" -20 >> changelog.md
|
||||||
|
else
|
||||||
|
echo "# Changes since ${PREV_TAG}" > changelog.md
|
||||||
|
git log "${PREV_TAG}..${{ steps.version.outputs.tag }}" \
|
||||||
|
--pretty=format:"- %s (%h)" >> changelog.md
|
||||||
|
fi
|
||||||
|
cat changelog.md
|
||||||
|
|
||||||
|
- name: Generate checksums
|
||||||
|
run: |
|
||||||
|
cd dist
|
||||||
|
sha256sum *.tar.gz *.zip > checksums.txt
|
||||||
|
cat checksums.txt
|
||||||
|
|
||||||
|
- name: Generate install script
|
||||||
|
run: |
|
||||||
|
cat > install.sh << 'INSTALL_SCRIPT'
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VERSION="${VERSION:-latest}"
|
||||||
|
BINARY="claudia-docs"
|
||||||
|
|
||||||
|
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64) ARCH="amd64" ;;
|
||||||
|
aarch64|arm64) ARCH="arm64" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
URL="https://gitea.danielarroyo.cl/proyectos/claudia-docs-cli/releases/download/${VERSION}/claudia-docs-${OS}-${ARCH}.tar.gz"
|
||||||
|
|
||||||
|
echo "Installing Claudia Docs CLI ${VERSION}..."
|
||||||
|
echo "Downloading from: ${URL}"
|
||||||
|
|
||||||
|
curl -fsSL "${URL}" | tar -xz
|
||||||
|
|
||||||
|
chmod +x "${BINARY}-${OS}-${ARCH}"
|
||||||
|
mv "${BINARY}-${OS}-${ARCH}" "${BINARY}"
|
||||||
|
|
||||||
|
if [ -w /usr/local/bin ]; then
|
||||||
|
mv "${BINARY}" /usr/local/bin/
|
||||||
|
echo "Installed to /usr/local/bin/claudia-docs"
|
||||||
|
else
|
||||||
|
echo "Run: sudo mv ${BINARY} /usr/local/bin/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done! Run: claudia-docs --version"
|
||||||
|
INSTALL_SCRIPT
|
||||||
|
chmod +x install.sh
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
tag_name: ${{ steps.version.outputs.tag }}
|
||||||
|
name: "Claudia Docs CLI ${{ steps.version.outputs.tag }}"
|
||||||
|
body_path: changelog.md
|
||||||
|
files: |
|
||||||
|
dist/*.tar.gz
|
||||||
|
dist/*.zip
|
||||||
|
dist/checksums.txt
|
||||||
|
install.sh
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,3 +25,5 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# CLI binary
|
||||||
|
claudia-docs
|
||||||
|
|||||||
371
HANDOFF.md
Normal file
371
HANDOFF.md
Normal 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
275
README.md
@@ -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
564
SPEC.md
Normal 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 |
|
||||||
13
cmd/claudia-docs/main.go
Normal file
13
cmd/claudia-docs/main.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/claudia/docs-cli/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := cobra.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
746
cobra/commands.go
Normal file
746
cobra/commands.go
Normal 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
31
go.mod
Normal 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
75
go.sum
Normal 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
110
internal/api/client.go
Normal 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
216
internal/api/documents.go
Normal 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
145
internal/api/folders.go
Normal 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
137
internal/api/projects.go
Normal 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
122
internal/api/reasoning.go
Normal 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
72
internal/api/search.go
Normal 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
132
internal/api/tags.go
Normal 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
169
internal/auth/auth.go
Normal 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
124
internal/config/config.go
Normal 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
82
internal/output/output.go
Normal 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
149
pkg/types/types.go
Normal 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"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user