feat(architecture): add complete technical architecture for SimpleNote

- ARCHITECTURE.md: main architecture document
- api-spec.yaml: full OpenAPI 3.0 spec
- folder-structure.md: detailed folder layout
- data-format.md: JSON schemas for .meta.json, .library.json, .tag-index.json
- env-template.md: environment variables documentation
- cli-protocol.md: CLI-to-API communication protocol
This commit is contained in:
Bulma
2026-03-28 03:18:25 +00:00
commit 90e4dd0807
6 changed files with 2195 additions and 0 deletions

413
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,413 @@
# SimpleNote - Arquitectura Técnica
## 1. Visión General
Sistema de gestión de documentos basado en archivos Markdown con API REST. Diseñado para reemplazar Joplin con una arquitectura más simple.
```
┌─────────────────┐ ┌──────────────────┐
│ simplenote-cli │◄────►│ simplenote-web │
│ (Commander.js) │ │ (Express + API) │
└─────────────────┘ └────────┬─────────┘
┌────────▼─────────┐
│ File System │
│ (Markdown + JSON)│
└───────────────────┘
```
## 2. Componentes
### 2.1 simplenote-web
- **Rol**: API REST + Frontend web
- **Puerto default**: 3000
- **Estructura interna**: Express.js con routers modulares
### 2.2 simplenote-cli
- **Rol**: Cliente de línea de comandos
- **Conexión**: HTTP al API de simplenote-web
- **Config**: `~/.config/simplenote/config.json`
## 3. Arquitectura de Datos
### 3.1 Estructura de Librerías (Filesystem)
```
data/ # DATA_ROOT configurable
└── libraries/
└── {library-id}/
├── .library.json # Metadata de librería
├── documents/
│ └── {document-id}/
│ ├── index.md # Contenido
│ └── .meta.json # Metadata del documento
└── sub-libraries/
└── {child-lib-id}/... # Anidamiento recursivo
```
### 3.2 JSON Manifests
**`.library.json`** — Metadata de librería:
```json
{
"id": "uuid-v4",
"name": "Nombre de Librería",
"parentId": "parent-uuid | null",
"path": "/libraries/uuid",
"createdAt": "ISO8601",
"updatedAt": "ISO8601"
}
```
**`.meta.json`** — Metadata de documento:
```json
{
"id": "uuid-v4",
"title": "string",
"tags": ["tag1", "tag2"],
"type": "requirement|note|spec|general",
"status": "draft|approved|implemented",
"priority": "high|medium|low",
"createdBy": "agent-id",
"createdAt": "ISO8601",
"updatedAt": "ISO8601",
"libraryId": "uuid"
}
```
**`.tag-index.json`** — Índice global de tags (en DATA_ROOT):
```json
{
"version": 1,
"updatedAt": "ISO8601",
"tags": {
"backend": ["doc-1", "doc-2"],
"api": ["doc-1", "doc-3"],
"auth": ["doc-2"]
}
}
```
### 3.3 Formato de Documento (Markdown)
```markdown
---
id: REQ-001
title: Título del Requerimiento
type: requirement
priority: high
status: draft
tags: [backend, api]
createdBy: agent-id
createdAt: 2026-03-28
---
# Título
## Descripción
Descripción del requerimiento.
## Criterios de Aceptación
- [ ] Criterio 1
- [ ] Criterio 2
```
## 4. API REST
### 4.1 Base URL
```
/api/v1
```
### 4.2 Autenticación
- **Método**: Token Bearer en header `Authorization`
- **Formato**: `Authorization: Bearer <token>`
- **Admin tokens**: Generados en setup inicial, almacenados en `.auth-tokens.json`
### 4.3 Endpoints
#### Auth
| Method | Endpoint | Descripción |
|--------|----------|-------------|
| POST | `/api/v1/auth/token` | Generar token (admin) |
| GET | `/api/v1/auth/verify` | Verificar token válido |
#### Documents
| Method | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/api/v1/documents` | Listar (filtros: tag, library, type) |
| GET | `/api/v1/documents/:id` | Obtener documento + metadata |
| POST | `/api/v1/documents` | Crear documento |
| PUT | `/api/v1/documents/:id` | Actualizar documento |
| DELETE | `/api/v1/documents/:id` | Eliminar documento |
| GET | `/api/v1/documents/:id/export` | Exportar como Markdown |
#### Libraries
| Method | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/api/v1/libraries` | Listar librerías raíz |
| GET | `/api/v1/libraries/:id` | Ver contenido de librería |
| POST | `/api/v1/libraries` | Crear librería |
| GET | `/api/v1/libraries/:id/tree` | Árbol completo de sublibrerías |
| DELETE | `/api/v1/libraries/:id` | Eliminar librería |
#### Tags
| Method | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/api/v1/tags` | Listar todos los tags |
| GET | `/api/v1/tags/:tag/documents` | Docs con tag específico |
| POST | `/api/v1/documents/:id/tags` | Agregar tags a documento |
## 5. Middleware de Auth
### 5.1 Flujo de Tokens
1. **Setup inicial**: Se genera admin token (almacenado en `.auth-tokens.json`)
2. **CLI login**: `simplenote auth login <token>` → almacena token en config local
3. **Requests**: Token enviado en header `Authorization: Bearer <token>`
4. **Verificación**: Middleware busca token en `.auth-tokens.json`
### 5.2 Implementación
```javascript
// authMiddleware.js
async function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Token required' });
}
const tokens = await readJSON('.auth-tokens.json');
if (!tokens.valid.includes(token)) {
return res.status(401).json({ error: 'Invalid token' });
}
req.token = token;
next();
}
```
### 5.3 Archivo `.auth-tokens.json`
```json
{
"version": 1,
"tokens": [
{
"token": "snk_xxxxx",
"label": "cli-default",
"createdAt": "ISO8601"
}
]
}
```
## 6. Estrategia de Indexación de Tags
### 6.1write-through
Cuando se crea/actualiza/elimina un documento:
1. Se actualiza su `.meta.json` con los tags
2. Se reconstruye el `.tag-index.json` global
### 6.2 Optimización
- El índice se rebuild en memoria al iniciar (rápido para <10k docs)
- Para sistemas grandes: rebuild incremental (solo afecta docs modificados)
- El índice es un archivo JSON plano para búsqueda O(1) por tag
### 6.3 API de Búsqueda
```javascript
// GET /api/v1/tags/backend
// → Lee .tag-index.json → returns ["doc-1", "doc-2"]
// GET /api/v1/documents?tag=backend
// → Busca en índice → filtra docs en memoria → retorna
```
## 7. CLI API Client
### 7.1 Configuración Local
```json
// ~/.config/simplenote/config.json
{
"apiUrl": "http://localhost:3000/api/v1",
"token": "snk_xxxxx",
"activeLibrary": "default"
}
```
### 7.2 Cliente HTTP
```javascript
// cli/src/api/client.js
class SimpleNoteClient {
constructor(baseUrl, token) {
this.baseUrl = baseUrl;
this.token = token;
}
async request(method, path, body) {
const res = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: body ? JSON.stringify(body) : undefined
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json();
}
// Documents
listDocuments(params) { return this.request('GET', '/documents', null); }
getDocument(id) { return this.request('GET', `/documents/${id}`); }
createDocument(data) { return this.request('POST', '/documents', data); }
updateDocument(id, data) { return this.request('PUT', `/documents/${id}`, data); }
deleteDocument(id) { return this.request('DELETE', `/documents/${id}`); }
// Libraries
listLibraries() { return this.request('GET', '/libraries'); }
createLibrary(data) { return this.request('POST', '/libraries', data); }
// Tags
listTags() { return this.request('GET', '/tags'); }
getTagDocuments(tag) { return this.request('GET', `/tags/${tag}/documents`); }
// Auth
verifyToken() { return this.request('GET', '/auth/verify'); }
}
```
## 8. Dependencias NPM
### simplenote-web
```json
{
"dependencies": {
"express": "^4.18.2",
"uuid": "^9.0.0",
"marked": "^11.0.0",
"gray-matter": "^4.0.3"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}
```
### simplenote-cli
```json
{
"dependencies": {
"commander": "^11.1.0",
"axios": "^1.6.0"
}
}
```
## 9. Variables de Entorno
| Variable | Default | Descripción |
|----------|---------|-------------|
| `PORT` | 3000 | Puerto del servidor |
| `DATA_ROOT` | `./data` | Raíz de documentos |
| `HOST` | `0.0.0.0` | Host de binding |
| `LOG_LEVEL` | `info` | Nivel de logging |
| `CORS_ORIGIN` | `*` | Orígenes CORS permitidos |
## 10. Estructura de Archivos del Proyecto
```
simplenote-web/
├── src/
│ ├── index.js # Entry point
│ ├── app.js # Express setup
│ ├── routes/
│ │ ├── documents.js
│ │ ├── libraries.js
│ │ ├── tags.js
│ │ └── auth.js
│ ├── services/
│ │ ├── documentService.js
│ │ ├── libraryService.js
│ │ └── tagService.js
│ ├── middleware/
│ │ └── auth.js
│ ├── utils/
│ │ ├── markdown.js
│ │ ├── fsHelper.js
│ │ └── uuid.js
│ └── indexers/
│ └── tagIndexer.js
├── data/ # .gitkeep
├── tests/
├── package.json
└── README.md
simplenote-cli/
├── src/
│ ├── index.js # Entry point (Commander)
│ ├── commands/
│ │ ├── doc.js
│ │ ├── lib.js
│ │ └── tag.js
│ ├── api/
│ │ └── client.js
│ └── config/
│ └── loader.js
├── package.json
└── README.md
```
## 11. Flujo de Operaciones
### 11.1 Crear Documento (CLI)
```
simplenote doc create --title "REQ-001" --tags "backend,api"
→ POST /api/v1/documents
→ Service: genera UUID, crea /data/libraries/{lib}/documents/{id}/index.md
→ Service: crea /data/libraries/{lib}/documents/{id}/.meta.json
→ Service: rebuild .tag-index.json
→ Response: { id, path, tags, ... }
```
### 11.2 Crear Documento (Web)
```
Form POST → /api/v1/documents
→ Mismo flujo que CLI
→ Web reconstruye lista con filtros
```
### 11.3 Búsqueda por Tag
```
GET /api/v1/tags/backend
→ tagIndexer: lee .tag-index.json
→ Returns: ["doc-id-1", "doc-id-2"]
GET /api/v1/documents?tag=backend
→ Busca en índice → filtra docs → retorna con metadata
```
## 12. Seguridad
- Tokens con prefijo `snk_` para identificación fácil
- Tokens almacenados en texto plano (sistema multi-usuario confiado)
- Para producción futura: hash bcrypt + salt
- CORS configurable para restringir acceso web
## 13. Líneas de Código Estimadas
| Componente | LOC aprox |
|------------|-----------|
| API REST (Express) | ~600 |
| Services (document, library, tag) | ~400 |
| Middleware auth | ~50 |
| Tag indexer | ~100 |
| CLI client | ~300 |
| CLI commands | ~400 |
| **Total** | ~1850 |

877
api-spec.yaml Normal file
View File

@@ -0,0 +1,877 @@
openapi: 3.0.3
info:
title: SimpleNote API
description: REST API for SimpleNote document management system
version: 1.0.0
contact:
name: SimpleNote Team
servers:
- url: http://localhost:3000/api/v1
description: Local development server
tags:
- name: Auth
description: Authentication and token management
- name: Documents
description: Document CRUD operations
- name: Libraries
description: Library (folder) management
- name: Tags
description: Tag-based search and management
paths:
# ============ AUTH ============
/auth/token:
post:
tags: [Auth]
summary: Generate a new API token
description: Admin-only endpoint to generate a new bearer token
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [label]
properties:
label:
type: string
description: Human-readable label for the token
example: "cli-default"
responses:
'200':
description: Token generated successfully
content:
application/json:
schema:
type: object
properties:
token:
type: string
example: "snk_a1b2c3d4e5f6..."
label:
type: string
example: "cli-default"
createdAt:
type: string
format: date-time
'401':
description: Unauthorized - invalid admin token
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'400':
description: Missing required fields
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/auth/verify:
get:
tags: [Auth]
summary: Verify current token is valid
security:
- BearerAuth: []
responses:
'200':
description: Token is valid
content:
application/json:
schema:
type: object
properties:
valid:
type: boolean
example: true
token:
type: string
example: "snk_a1b2c3d4e5f6..."
'401':
description: Token is invalid or missing
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ============ DOCUMENTS ============
/documents:
get:
tags: [Documents]
summary: List all documents
security:
- BearerAuth: []
parameters:
- name: tag
in: query
description: Filter by tag
schema:
type: string
example: "backend"
- name: library
in: query
description: Filter by library ID
schema:
type: string
example: "550e8400-e29b-41d4-a716-446655440000"
- name: type
in: query
description: Filter by document type
schema:
type: string
enum: [requirement, note, spec, general]
example: "requirement"
- name: status
in: query
description: Filter by status
schema:
type: string
enum: [draft, approved, implemented]
example: "draft"
- name: limit
in: query
description: Max results to return
schema:
type: integer
default: 50
example: 20
- name: offset
in: query
description: Skip first N results
schema:
type: integer
default: 0
example: 0
responses:
'200':
description: List of documents
content:
application/json:
schema:
type: object
properties:
documents:
type: array
items:
$ref: '#/components/schemas/Document'
total:
type: integer
example: 42
limit:
type: integer
example: 20
offset:
type: integer
example: 0
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
tags: [Documents]
summary: Create a new document
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [title, libraryId]
properties:
title:
type: string
description: Document title
example: "API Authentication Design"
libraryId:
type: string
description: Target library ID
example: "550e8400-e29b-41d4-a716-446655440000"
content:
type: string
description: Markdown content (optional, defaults to template)
example: "# API Authentication\n\n## Description\n..."
tags:
type: array
items:
type: string
example: ["backend", "api", "auth"]
type:
type: string
enum: [requirement, note, spec, general]
default: general
priority:
type: string
enum: [high, medium, low]
default: medium
status:
type: string
enum: [draft, approved, implemented]
default: draft
responses:
'201':
description: Document created
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
'400':
description: Invalid request body
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Library not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/documents/{id}:
get:
tags: [Documents]
summary: Get a document by ID
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
description: Document UUID
schema:
type: string
example: "550e8400-e29b-41d4-a716-446655440001"
responses:
'200':
description: Document found
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
'404':
description: Document not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
put:
tags: [Documents]
summary: Update a document
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
description: Document UUID
schema:
type: string
example: "550e8400-e29b-41d4-a716-446655440001"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
title:
type: string
example: "Updated Title"
content:
type: string
example: "# Updated Content\n\nNew markdown..."
tags:
type: array
items:
type: string
example: ["backend", "api"]
type:
type: string
enum: [requirement, note, spec, general]
priority:
type: string
enum: [high, medium, low]
status:
type: string
enum: [draft, approved, implemented]
responses:
'200':
description: Document updated
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
'400':
description: Invalid request body
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Document not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
tags: [Documents]
summary: Delete a document
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
description: Document UUID
schema:
type: string
example: "550e8400-e29b-41d4-a716-446655440001"
responses:
'200':
description: Document deleted
content:
application/json:
schema:
type: object
properties:
deleted:
type: boolean
example: true
id:
type: string
example: "550e8400-e29b-41d4-a716-446655440001"
'404':
description: Document not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/documents/{id}/export:
get:
tags: [Documents]
summary: Export document as raw Markdown
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
description: Document UUID
schema:
type: string
example: "550e8400-e29b-41d4-a716-446655440001"
responses:
'200':
description: Raw Markdown file
content:
text/markdown:
schema:
type: string
example: |
---
id: REQ-001
title: API Authentication
---
# API Authentication
## Description
...
'404':
description: Document not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/documents/{id}/tags:
post:
tags: [Tags]
summary: Add tags to a document
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
description: Document UUID
schema:
type: string
example: "550e8400-e29b-41d4-a716-446655440001"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [tags]
properties:
tags:
type: array
items:
type: string
example: ["new-tag", "another-tag"]
responses:
'200':
description: Tags added
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
'400':
description: Invalid tags array
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Document not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ============ LIBRARIES ============
/libraries:
get:
tags: [Libraries]
summary: List root-level libraries
security:
- BearerAuth: []
responses:
'200':
description: List of root libraries
content:
application/json:
schema:
type: object
properties:
libraries:
type: array
items:
$ref: '#/components/schemas/Library'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
tags: [Libraries]
summary: Create a new library
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name]
properties:
name:
type: string
description: Library name
example: "Backend Requirements"
parentId:
type: string
nullable: true
description: Parent library ID for nesting (null for root)
example: "550e8400-e29b-41d4-a716-446655440000"
responses:
'201':
description: Library created
content:
application/json:
schema:
$ref: '#/components/schemas/Library'
'400':
description: Invalid request body
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Parent library not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/libraries/{id}:
get:
tags: [Libraries]
summary: Get library contents (documents and sub-libraries)
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
description: Library UUID
schema:
type: string
example: "550e8400-e29b-41d4-a716-446655440000"
responses:
'200':
description: Library contents
content:
application/json:
schema:
type: object
properties:
library:
$ref: '#/components/schemas/Library'
documents:
type: array
items:
$ref: '#/components/schemas/Document'
subLibraries:
type: array
items:
$ref: '#/components/schemas/Library'
'404':
description: Library not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/libraries/{id}/tree:
get:
tags: [Libraries]
summary: Get full subtree of a library
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
description: Library UUID
schema:
type: string
example: "550e8400-e29b-41d4-a716-446655440000"
responses:
'200':
description: Full library tree
content:
application/json:
schema:
$ref: '#/components/schemas/LibraryTree'
'404':
description: Library not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/libraries/{id}/documents:
get:
tags: [Documents]
summary: List documents in a specific library
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
description: Library UUID
schema:
type: string
example: "550e8400-e29b-41d4-a716-446655440000"
responses:
'200':
description: List of documents in library
content:
application/json:
schema:
type: object
properties:
libraryId:
type: string
documents:
type: array
items:
$ref: '#/components/schemas/Document'
'404':
description: Library not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ============ TAGS ============
/tags:
get:
tags: [Tags]
summary: List all tags with document counts
security:
- BearerAuth: []
responses:
'200':
description: All tags with counts
content:
application/json:
schema:
type: object
properties:
tags:
type: array
items:
type: object
properties:
name:
type: string
example: "backend"
count:
type: integer
example: 5
total:
type: integer
example: 15
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/tags/{tag}:
get:
tags: [Tags]
summary: Get all documents with a specific tag
security:
- BearerAuth: []
parameters:
- name: tag
in: path
required: true
description: Tag name
schema:
type: string
example: "backend"
responses:
'200':
description: Documents with tag
content:
application/json:
schema:
type: object
properties:
tag:
type: string
example: "backend"
documents:
type: array
items:
$ref: '#/components/schemas/Document'
count:
type: integer
example: 5
'404':
description: Tag not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: API token with `snk_` prefix
schemas:
Error:
type: object
properties:
error:
type: string
example: "Document not found"
code:
type: string
example: "NOT_FOUND"
Document:
type: object
properties:
id:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440001"
title:
type: string
example: "API Authentication Design"
path:
type: string
example: "/libraries/550e8400/.../documents/550e8401/index.md"
content:
type: string
description: Raw markdown content
example: "# API Authentication\n\n## Description\n..."
tags:
type: array
items:
type: string
example: ["backend", "api", "auth"]
type:
type: string
enum: [requirement, note, spec, general]
example: "requirement"
status:
type: string
enum: [draft, approved, implemented]
example: "draft"
priority:
type: string
enum: [high, medium, low]
example: "high"
libraryId:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
createdBy:
type: string
description: Agent or user ID who created the document
example: "agent-001"
createdAt:
type: string
format: date-time
example: "2026-03-28T10:00:00Z"
updatedAt:
type: string
format: date-time
example: "2026-03-28T12:30:00Z"
Library:
type: object
properties:
id:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
name:
type: string
example: "Backend Requirements"
parentId:
type: string
nullable: true
format: uuid
description: Parent library ID, null for root
example: null
path:
type: string
description: Absolute path to library folder
example: "/data/libraries/550e8400"
documentCount:
type: integer
description: Total documents in this library (excludes sub-libraries)
example: 12
createdAt:
type: string
format: date-time
example: "2026-03-28T09:00:00Z"
updatedAt:
type: string
format: date-time
example: "2026-03-28T09:00:00Z"
LibraryTree:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
documents:
type: array
items:
type: object
properties:
id:
type: string
title:
type: string
subLibraries:
type: array
items:
$ref: '#/components/schemas/LibraryTree'

274
cli-protocol.md Normal file
View File

@@ -0,0 +1,274 @@
# SimpleNote CLI - Protocolo de Comunicación
## 1. Overview
El CLI (`simplenote-cli`) se comunica con el servidor (`simplenote-web`) exclusivamente
vía HTTP REST usando el API documentado en `api-spec.yaml`.
No hay comunicación peer-to-peer ni protocolos binarios. Todo es JSON sobre HTTP.
```
┌──────────────────┐ HTTP/REST ┌─────────────────┐
│ simplenote-cli │ ◄──────────────────► │ simplenote-web │
│ (Commander.js) │ Bearer token auth │ (Express.js) │
└──────────────────┘ └─────────────────┘
│ │
└──── ~/.config/simplenote/config.json ──┘
```
## 2. Cliente HTTP
### 2.1 Clase `SimpleNoteClient`
```javascript
// src/api/client.js
const axios = require('axios');
class SimpleNoteClient {
constructor({ baseUrl, token }) {
this.baseUrl = baseUrl.replace(/\/$/, ''); // strip trailing slash
this.token = token;
this._axios = axios.create({
baseURL: this.baseUrl,
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
});
}
_authHeaders() {
return this.token ? { Authorization: `Bearer ${this.token}` } : {};
}
async _request(method, path, data) {
try {
const res = await this._axios.request({
method,
url: path,
data,
headers: this._authHeaders()
});
return res.data;
} catch (err) {
if (err.response) {
const msg = err.response.data?.error || err.message;
throw new Error(`API Error ${err.response.status}: ${msg}`);
}
throw err;
}
}
// Auth
async verifyToken() { return this._request('GET', '/auth/verify'); }
// Documents
async listDocuments(params) {
const qs = new URLSearchParams(params).toString();
return this._request('GET', `/documents${qs ? '?' + qs : ''}`);
}
async getDocument(id) { return this._request('GET', `/documents/${id}`); }
async createDocument(data) { return this._request('POST', '/documents', data); }
async updateDocument(id, data) { return this._request('PUT', `/documents/${id}`, data); }
async deleteDocument(id) { return this._request('DELETE', `/documents/${id}`); }
async exportDocument(id) { return this._request('GET', `/documents/${id}/export`); }
// Documents > Tags
async addTagsToDocument(id, tags) {
return this._request('POST', `/documents/${id}/tags`, { tags });
}
// Libraries
async listLibraries() { return this._request('GET', '/libraries'); }
async getLibrary(id) { return this._request('GET', `/libraries/${id}`); }
async createLibrary(data) { return this._request('POST', '/libraries', data); }
async getLibraryTree(id) { return this._request('GET', `/libraries/${id}/tree`); }
async listLibraryDocuments(id) {
return this._request('GET', `/libraries/${id}/documents`);
}
// Tags
async listTags() { return this._request('GET', '/tags'); }
async getTagDocuments(tag) { return this._request('GET', `/tags/${tag}`); }
}
```
## 3. Flujo de Auth
### 3.1 Login Inicial
```bash
simplenote auth login snk_a1b2c3d4e5f6...
```
Flujo:
1. CLI guarda token en `~/.config/simplenote/config.json`
2. CLI llama `GET /api/v1/auth/verify` para validar
3. Si 200 → login exitoso. Si 401 → token inválido.
### 3.2 Requests Subsecuentes
Todas las requests incluyen:
```
Authorization: Bearer <token>
```
### 3.3 Verificación de Status
```bash
simplenote auth status
```
`GET /auth/verify` → muestra si token es válido.
## 4. Comandos CLI Detallados
### 4.1 Documents
```bash
# Listar con filtros
simplenote doc list --tag backend --library 550e8400... --type requirement
simplenote doc list --tag api --limit 10
# Ver documento
simplenote doc get 770e8400-e29b-41d4-a716-446655440002
# Crear
simplenote doc create \
--title "API Authentication" \
--content "# API Authentication\n\n..." \
--tags "backend,api,auth" \
--type requirement \
--priority high \
--library 550e8400-e29b-41d4-a716-446655440000
# Actualizar
simplenote doc update 770e8400... --title "Nuevo título" --content "..."
simplenote doc update 770e8400... --status approved
# Eliminar
simplenote doc delete 770e8400...
# Exportar como markdown
simplenote doc export 770e8400...
# Agregar tags
simplenote doc add-tags 770e8400... --tags "new-tag,another"
```
### 4.2 Libraries
```bash
# Listar librerías raíz
simplenote lib list
# Listar con padre
simplenote lib list --parent 550e8400...
# Ver contenido
simplenote lib get 550e8400...
# Crear
simplenote lib create --name "API Specs"
simplenote lib create --name "Sub Librería" --parent 550e8400...
# Ver árbol completo
simplenote lib tree 550e8400...
```
### 4.3 Tags
```bash
# Listar todos los tags
simplenote tag list
# Ver docs con tag
simplenote tag docs backend
```
### 4.4 Auth
```bash
# Login con token
simplenote auth login snk_xxxxx
# Verificar status
simplenote auth status
```
## 5. Manejo de Errores
```javascript
// Errores de API se transforman en mensajes claros
try {
await client.getDocument('non-existent-id');
} catch (err) {
console.error(err.message);
// "API Error 404: Document not found"
}
```
Códigos de error CLI:
- `1` — Error general (network, parse, etc)
- `2` — Token inválido / auth fallida
- `3` — Recurso no encontrado (404)
- `4` — Validación de input
## 6. Configuración de Conexión
```javascript
// ~/.config/simplenote/config.json
{
"apiUrl": "http://localhost:3000/api/v1",
"token": "snk_xxxxx",
"activeLibrary": "550e8400-e29b-41d4-a716-446655440000"
}
```
Override por línea de comandos:
```bash
simplenote --api-url http://custom:3000/api/v1 doc list
```
## 7. Dependencias CLI
```json
// package.json
{
"dependencies": {
"commander": "^11.1.0",
"axios": "^1.6.0",
"chalk": "^5.3.0",
"inquirer": "^9.2.0"
}
}
```
## 8. Ejemplo de Sesión Completa
```bash
$ simplenote auth login snk_a1b2c3d4e5f6
✓ Token verified. Logged in.
$ simplenote lib list
[
{ "id": "550e8400...", "name": "Backend Requirements", "documentCount": 5 }
]
$ simplenote doc create \
--title "Token Auth" \
--tags "backend,auth" \
--type requirement \
--library 550e8400...
{
"id": "770e8400...",
"title": "Token Auth",
"tags": ["backend", "auth"],
...
}
$ simplenote tag docs backend
[
{ "id": "770e8400...", "title": "Token Auth", ... }
]
$ simplenote doc get 770e8400...
# Muestra documento formateado con content + metadata
```

345
data-format.md Normal file
View File

@@ -0,0 +1,345 @@
# SimpleNote - Formato de Datos
## 1. Archivos JSON de Metadata
Todos los archivos de metadata son JSON planos, almacenados junto a su contenido en el filesystem.
---
## 2. `.library.json`
Define una librería (equivalente a una carpeta/directorio).
**Ubicación**: `data/libraries/{library-uuid}/.library.json`
**Schema**:
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "name", "path", "createdAt", "updatedAt"],
"properties": {
"id": {
"type": "string",
"format": "uuid",
"description": "UUID único de la librería"
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 255,
"description": "Nombre visible de la librería"
},
"parentId": {
"type": ["string", "null"],
"format": "uuid",
"description": "ID del padre directo. Null para root."
},
"path": {
"type": "string",
"description": "Ruta relativa desde DATA_ROOT (ej: libraries/uuid)"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Timestamp de creación ISO8601"
},
"updatedAt": {
"type": "string",
"format": "date-time",
"description": "Timestamp de última modificación ISO8601"
}
}
}
```
**Ejemplo**:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Backend Requirements",
"parentId": null,
"path": "libraries/550e8400",
"createdAt": "2026-03-28T09:00:00Z",
"updatedAt": "2026-03-28T09:00:00Z"
}
```
**Ejemplo anidado**:
```json
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "API Specs",
"parentId": "550e8400-e29b-41d4-a716-446655440000",
"path": "libraries/550e8400/sub-libraries/660e8400",
"createdAt": "2026-03-28T10:00:00Z",
"updatedAt": "2026-03-28T10:00:00Z"
}
```
---
## 3. `.meta.json`
Metadata de un documento individual.
**Ubicación**: `data/libraries/{lib-uuid}/documents/{doc-uuid}/.meta.json`
**Schema**:
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "title", "tags", "type", "libraryId", "createdAt", "updatedAt"],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 500
},
"tags": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true
},
"type": {
"type": "string",
"enum": ["requirement", "note", "spec", "general"]
},
"status": {
"type": "string",
"enum": ["draft", "approved", "implemented"]
},
"priority": {
"type": "string",
"enum": ["high", "medium", "low"]
},
"libraryId": {
"type": "string",
"format": "uuid"
},
"createdBy": {
"type": "string",
"description": "Agent ID o user ID que creó el documento"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
}
}
```
**Ejemplo**:
```json
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"title": "API Authentication Design",
"tags": ["backend", "api", "auth"],
"type": "requirement",
"status": "draft",
"priority": "high",
"libraryId": "550e8400-e29b-41d4-a716-446655440000",
"createdBy": "agent-001",
"createdAt": "2026-03-28T10:00:00Z",
"updatedAt": "2026-03-28T10:00:00Z"
}
```
---
## 4. `.tag-index.json`
Índice global de tags. Rebuild completo o incremental.
**Ubicación**: `data/.tag-index.json`
**Schema**:
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["version", "updatedAt", "tags"],
"properties": {
"version": {
"type": "integer",
"const": 1,
"description": "Versión del formato de índice"
},
"updatedAt": {
"type": "string",
"format": "date-time",
"description": "Última rebuild del índice"
},
"tags": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": { "type": "string", "format": "uuid" },
"uniqueItems": true
},
"description": "Map tag → array de document IDs"
}
}
}
```
**Ejemplo**:
```json
{
"version": 1,
"updatedAt": "2026-03-28T12:00:00Z",
"tags": {
"backend": [
"770e8400-e29b-41d4-a716-446655440002",
"880e8400-e29b-41d4-a716-446655440003"
],
"api": [
"770e8400-e29b-41d4-a716-446655440002"
],
"auth": [
"770e8400-e29b-41d4-a716-446655440002",
"990e8400-e29b-41d4-a716-446655440004"
]
}
}
```
---
## 5. `.auth-tokens.json`
Tokens de API válidos.
**Ubicación**: `data/.auth-tokens.json`
**Schema**:
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["version", "tokens"],
"properties": {
"version": {
"type": "integer",
"const": 1
},
"tokens": {
"type": "array",
"items": {
"type": "object",
"required": ["token", "label", "createdAt"],
"properties": {
"token": {
"type": "string",
"pattern": "^snk_"
},
"label": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
}
}
}
}
}
}
```
**Ejemplo**:
```json
{
"version": 1,
"tokens": [
{
"token": "snk_a1b2c3d4e5f6g7h8i9j0",
"label": "cli-default",
"createdAt": "2026-03-28T00:00:00Z"
},
{
"token": "snk_k9j8i7h6g5f4e3d2c1b",
"label": "hiro-workstation",
"createdAt": "2026-03-28T01:00:00Z"
}
]
}
```
---
## 6. Documento Markdown (`index.md`)
Archivo de contenido con frontmatter YAML.
### 6.1 Frontmatter
```yaml
---
id: REQ-001
title: Título del Requerimiento
type: requirement # requirement | note | spec | general
priority: high # high | medium | low
status: draft # draft | approved | implemented
tags: [backend, api]
createdBy: agent-001
createdAt: 2026-03-28
---
```
### 6.2 Body
Markdown standard con headers, listas, código, etc.
### 6.3 Ejemplo Completo
```markdown
---
id: REQ-001
title: API Authentication Design
type: requirement
priority: high
status: draft
tags: [backend, api, auth]
createdBy: agent-001
createdAt: 2026-03-28
---
# API Authentication Design
## Descripción
El sistema debe soportar autenticación via tokens Bearer para todas las rutas
protegidas bajo `/api/v1/*` excepto `/api/v1/auth/token`.
## Criterios de Aceptación
- [ ] Endpoint POST /api/v1/auth/token acepta credenciales y retorna token
- [ ] Middleware extrae token del header `Authorization: Bearer <token>`
- [ ] Middleware retorna 401 si header ausente o token inválido
- [ ] Token tiene prefijo `snk_` para identificación fácil
## Notas
Tokens en esta versión son secretos compartidos. Para producción se recomienda
OAuth2 o JWT firmados.
```
---
## 7. Tabla Resumen de Archivos
| Archivo | Ubicación | Propósito |
|---------|-----------|-----------|
| `.library.json` | `libraries/{id}/` | Metadata de librería |
| `.meta.json` | `libraries/{lib}/documents/{id}/` | Metadata de documento |
| `index.md` | `libraries/{lib}/documents/{id}/` | Contenido del documento |
| `.tag-index.json` | `DATA_ROOT/` | Índice global tag → docs |
| `.auth-tokens.json` | `DATA_ROOT/` | Tokens API válidos |
| `config.json` | `~/.config/simplenote/` | Config local del CLI |

116
env-template.md Normal file
View File

@@ -0,0 +1,116 @@
# SimpleNote - Variables de Entorno
## Template: `.env.example`
Copia este archivo a `.env` en la raíz de `simplenote-web/`.
```env
# ============ SERVER ============
PORT=3000
HOST=0.0.0.0
# ============ DATA ============
# Raíz donde se almacenan documentos y archivos de índice
# Default: ./data (relativo al proyecto)
DATA_ROOT=./data
# ============ AUTH ============
# Token admin inicial (generado en setup). No exponer en cliente.
ADMIN_TOKEN=snk_initial_admin_token_change_me
# ============ LOGGING ============
LOG_LEVEL=info
# Opciones: trace, debug, info, warn, error, fatal
# ============ CORS ============
# Orígenes permitidos para requests cross-origin
# Default: * (permitir todos)
CORS_ORIGIN=*
# Para desarrollo local:
# CORS_ORIGIN=http://localhost:5173
# Para producción:
# CORS_ORIGIN=https://simplenote.example.com
# ============ API ============
# Versión del API en URLs
API_PREFIX=/api/v1
# ============ RATE LIMITING (opcional) ============
# Requests por minuto por IP
# RATE_LIMIT_ENABLED=true
# RATE_LIMIT_WINDOW_MS=60000
# RATE_LIMIT_MAX=100
```
## Detalle de Variables
### `PORT`
- **Tipo**: integer
- **Default**: `3000`
- **Descripción**: Puerto TCP donde Express escucha conexiones entrantes.
### `HOST`
- **Tipo**: string
- **Default**: `0.0.0.0`
- **Descripción**: Host de binding. `0.0.0.0` = todas las interfaces.
### `DATA_ROOT`
- **Tipo**: string (ruta)
- **Default**: `./data`
- **Descripción**: Directorio raíz donde se guardan:
- `libraries/` — estructura de carpetas con documentos
- `.tag-index.json` — índice global de tags
- `.auth-tokens.json` — tokens de API
### `LOG_LEVEL`
- **Tipo**: enum
- **Default**: `info`
- **Opciones**: `trace`, `debug`, `info`, `warn`, `error`, `fatal`
- **Descripción**: Nivel mínimo de logs que se emiten.
### `CORS_ORIGIN`
- **Tipo**: string
- **Default**: `*`
- **Descripción**: Valor del header `Access-Control-Allow-Origin`. Usar dominio
específico en producción para seguridad.
### `API_PREFIX`
- **Tipo**: string
- **Default**: `/api/v1`
- **Descripción**: Prefijo para todas las rutas del API. Cambiar permite versionado.
---
## CLI Config: `~/.config/simplenote/config.json`
```json
{
"apiUrl": "http://localhost:3000/api/v1",
"token": "snk_your_token_here",
"activeLibrary": "550e8400-e29b-41d4-a716-446655440000"
}
```
| Campo | Descripción |
|-------|-------------|
| `apiUrl` | URL base del API (sin trailing slash) |
| `token` | Token Bearer para autenticación |
| `activeLibrary` | ID de librería activa por defecto |
---
## Flags de Línea de Comandos (CLI)
```bash
# Override apiUrl
simplenote --api-url http://custom:3000/api/v1 doc list
# Modo verbose
simplenote --verbose doc list
# Help
simplenote help
simplenote doc help create
```

170
folder-structure.md Normal file
View File

@@ -0,0 +1,170 @@
# SimpleNote - Estructura de Carpetas
## Estructura General del Repositorio
```
simplenote-web/
├── src/
│ ├── index.js # Entry point (bind port, listen)
│ ├── app.js # Express app setup, middleware, routes
│ ├── config/
│ │ └── index.js # Env vars loader y defaults
│ ├── routes/
│ │ ├── index.js # Router principal (mount /api/v1/*)
│ │ ├── documents.js # /documents routes
│ │ ├── libraries.js # /libraries routes
│ │ ├── tags.js # /tags routes
│ │ └── auth.js # /auth routes
│ ├── services/
│ │ ├── documentService.js # Lógica de documentos
│ │ ├── libraryService.js # Lógica de librerías
│ │ └── tagService.js # Lógica de tags e indexación
│ ├── middleware/
│ │ ├── auth.js # Middleware de autenticación Bearer
│ │ └── errorHandler.js # Handler global de errores
│ ├── utils/
│ │ ├── markdown.js # Parseo y serialización de Markdown
│ │ ├── fsHelper.js # Helpers de filesystem (safe paths, etc)
│ │ ├── uuid.js # Wrapper de uuid/v4
│ │ └── errors.js # Clases de errores custom (NotFound, etc)
│ └── indexers/
│ └── tagIndexer.js # Rebuild y query del .tag-index.json
├── data/ # Raíz de documentos (DATA_ROOT)
│ ├── .tag-index.json # Índice global de tags
│ ├── .auth-tokens.json # Tokens de API válidos
│ └── libraries/
│ └── {uuid}/
│ ├── .library.json
│ ├── documents/
│ │ └── {uuid}/
│ │ ├── index.md
│ │ └── .meta.json
│ └── sub-libraries/
│ └── {child-uuid}/...
├── tests/
│ ├── unit/
│ │ ├── services/
│ │ │ ├── documentService.test.js
│ │ │ ├── libraryService.test.js
│ │ │ └── tagService.test.js
│ │ └── utils/
│ │ └── markdown.test.js
│ └── integration/
│ └── api.test.js
├── scripts/
│ └── init-data.js # Script de inicialización (crea default lib)
├── package.json
├── .env.example
└── README.md
simplenote-cli/
├── src/
│ ├── index.js # Entry point (Commander program)
│ ├── api/
│ │ └── client.js # SimpleNoteClient (axios-based)
│ ├── commands/
│ │ ├── index.js # Registra todos los subcommands
│ │ ├── doc.js # simplenote doc <subcmd>
│ │ ├── lib.js # simplenote lib <subcmd>
│ │ └── tag.js # simplenote tag <subcmd>
│ └── config/
│ ├── loader.js # Carga ~/.config/simplenote/config.json
│ └── errors.js # Errores CLI custom
├── package.json
└── README.md
```
## Estructura de Datos en Disco (DATA_ROOT)
```
data/ # DATA_ROOT (default: ./data)
├── .tag-index.json # Tag index global
├── .auth-tokens.json # Auth tokens
└── libraries/
└── {library-uuid}/
├── .library.json
├── documents/
│ └── {doc-uuid}/
│ ├── index.md # Contenido Markdown
│ └── .meta.json # Metadata (tags, timestamps)
└── sub-libraries/
└── {child-uuid}/
├── .library.json
└── documents/
└── ... (recursivo)
```
## Archivos de Metadata
### `.library.json` (por librería)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Backend Requirements",
"parentId": null,
"path": "libraries/550e8400",
"createdAt": "2026-03-28T09:00:00Z",
"updatedAt": "2026-03-28T09:00:00Z"
}
```
### `.meta.json` (por documento)
```json
{
"id": "550e8401-e29b-41d4-a716-446655440001",
"title": "API Authentication",
"tags": ["backend", "api", "auth"],
"type": "requirement",
"status": "draft",
"priority": "high",
"libraryId": "550e8400-e29b-41d4-a716-446655440000",
"createdBy": "agent-001",
"createdAt": "2026-03-28T10:00:00Z",
"updatedAt": "2026-03-28T10:00:00Z"
}
```
### `index.md` (contenido)
```markdown
---
id: REQ-001
title: API Authentication
type: requirement
priority: high
status: draft
tags: [backend, api, auth]
createdBy: agent-001
createdAt: 2026-03-28
---
# API Authentication
## Descripción
El sistema debe soportar autenticación via tokens Bearer.
## Criterios de Aceptación
- [ ] Endpoint POST /api/auth/token
- [ ] Middleware valida Bearer token
- [ ] Retorna 401 si token inválido
```
## Archivos de Configuración Local (CLI)
```
~/.config/simplenote/
└── config.json # Config CLI local
{
"apiUrl": "http://localhost:3000/api/v1",
"token": "snk_xxxxx",
"activeLibrary": "default"
}
```
## Archivos de Configuración del Servidor
```
simplenote-web/
├── .env.example # Template de variables de entorno
├── .env # No commitear (contiene secrets)
└── .gitignore # Ignora .env, data/, node_modules/
```