CRM API — Referencia de endpoints

Arc OS — The Orchestration System for AI Teams

Información general

Parámetro Valor
Base URL https://arc-os.co/api/crm
Autorización Authorization: Bearer <JWT> o ?token=<JWT> (para SSE/WebSocket)
Content-Type application/json
Algoritmo JWT HMAC-SHA256
TTL JWT 24 horas

Autenticación

Todos los endpoints (excepto /docs/*) requieren un token JWT en el header Authorization: Bearer <token>.

Para conexiones SSE y WebSocket el token se pasa mediante el query parameter ?token=<JWT>.

Errores de autorización

Código Descripción
401 Token ausente o inválido
403 Sin acceso al proyecto (multi-tenancy)

Endpoints por categoría

Cuenta y ajustes

Método Ruta Descripción
GET /account/settings Obtener ajustes de cuenta
PUT /account/settings Actualizar ajustes de cuenta

Onboarding + Trial Credits (Phase 50.1)

Método Ruta Descripción
POST /onboarding/setup Crear el primer proyecto. Body multipart: config (JSON) + files. El campo anthropicKey ahora es opcional — si está vacío + el usuario tiene email verificado + no ha recibido prueba gratuita antes, el proyecto se crea en trial_mode=1 con 100K free tokens. Response: { ok, project, trial_activated }. Phase 51: devuelve 402 con {error:"plan_limit_reached", reason, current, limit, plan} cuando el usuario supera el límite de proyectos del plan.
GET /account/trial-status Estado de prueba gratuita para el banner de UI. Response: { email, email_verified, trial_granted, has_trial_active, total_remaining, total_granted, projects: [...] }
GET /account/usage Historial de uso de tokens para el usuario autorizado (Phase 63, #148). Response: { rows: [ { project_name, worker_id, input_tokens, output_tokens, cache_tokens, total_tokens, created_at } × hasta 200 ], totals: { total, input, output } }. Lee token_usage_log por owner_id. Mostrado en UserDropdown (UsageCard) y BillingPage (sección Token Usage).
GET /account/billing-summary Resumen consolidado de billing (#309). Response: { arc: { plan, status, tokens_this_month, tokens_input, tokens_output, renewal_date }, anthropic: { connected, key_prefix, credit_balance_usd, spend_month_usd, tokens_this_week } }. La sección Anthropic se rellena server-side vía la Anthropic API con la account_settings.anthropic_key del usuario (fallback → PLATFORM_ANTHROPIC_KEY). Mostrado en la UsageCard del UserDropdown.

Onboarding Checklist (Phase 54.1, issue #56)

Checklist de engagement post-wizard con 5 pasos. Cada paso (workers, cli, skill, bot, issue) acepta el estado completed o skipped. Las mutaciones son idempotentes: un POST idéntico repetido devuelve el mismo estado sin escribir un duplicado en activity_log. El replay no reinicia el estado, solo elimina dismissed_at — la UI vuelve a mostrar el panel con el mismo progreso.

Método Ruta Descripción
GET /onboarding/progress Estado actual para el usuario autenticado. Response: { steps:["workers","cli","skill","bot","issue"], state:{<step>:<status>}, completed_count, total_steps:5, completed_at, dismissed_at, source, started_at, updated_at }. Usuario sin actividad → ceros/null sin crear fila.
POST /onboarding/event Registrar transición de paso. Body: { step: "workers"|"cli"|"skill"|"bot"|"issue", status: "completed"|"skipped", source?: "web"|"cli" }. Validación por whitelist → 400 para step/status desconocido. Response: mismo shape que GET. Emite onboarding_step_completed/onboarding_step_skipped en activity_log solo al changed; al llegar a 5/5 emite adicionalmente onboarding_completed con duration_ms.
POST /onboarding/dismiss Cerrar el panel (dismissed_at = now). Idempotente. Emite onboarding_dismissed en la primera llamada con payload {completed_count}.
POST /onboarding/replay Volver a abrir el panel cerrado (dismissed_at = NULL). El estado de los pasos no se modifica. Emite onboarding_replayed al limpiar el evento.
POST /projects/:name/active-issue Issue #115. Vincular la sesión web actual a un issue. Body: { issue_id: number, title?: string }. Escribe evento session_active_issue en activity_log (source=web).
GET /projects/:name/active-issue Issue #115. Último issue vinculado de este owner en los últimos 7 días. Response: { active_issue_id, title, ts }.
GET /onboarding/cli-status Phase 54.3 (issue #58). ¿El usuario inició sesión con arc login en los últimos 30 días? Response: { installed: boolean, last_cli_at: string|null }. SSOT — filas en activity_log con event_type='cli_invocation' y actor=chatId. El checklist de onboarding del frontend hace polling a este endpoint cada 10s mientras el paso CLI esté pendiente; cuando installed=true — marca automáticamente el paso cli como completado.
GET /analytics/onboarding-funnel Phase 54.6 (issue #61). Estadísticas agregadas del funnel en ventana deslizante. Query: hours=168 (1-720, default 7d). Response: { hours, total_steps:5, started_users, completed_users, completion_rate, per_step: [{step, completed, skipped}…], duration_p50_ms, duration_p90_ms, ttfc_p50_ms, ttfc_sample_size }. SSOT — eventos de activity_log onboarding_step_* + onboarding_completed + cli_invocation. TTFC = time-to-first-arc (delta julianday desde el primer onboarding-step hasta el primer cli_invocation por actor).

El SSOT para las métricas del funnel (Phase 54.6 / issue #61) son los eventos en activity_log (event_type LIKE 'onboarding_%'). La tabla onboarding_progress es un caché derivado: la UI se renderiza con una sola consulta en lugar de agregar eventos.

Beta Feedback (Phase 53.3)

Método Ruta Descripción
POST /feedback Enviar feedback beta. Body: {type: "bug"|"feature"|"other", title, description, project?, browser?}. Escribe en activity_log (event_type=feedback_report) y hace ping al CEO en Telegram.
GET /admin/feedback Lista de submissions recientes (solo admin). Query: limit=50 (máx 500). Response: {items: [...], count}.
POST /feedback/translation Enviar un issue de traducción (Phase 59.4). Body: {locale, msgid, suggestion, severity: "minor"|"major"|"wrong", current_translation?, page_url?}. Se guarda en translation_feedback.
GET /admin/translations Lista de translation feedback (admin). Query: locale, status=open|accepted|rejected|all, limit. Response: {items, count}.
GET /admin/translations/stats Estadísticas de salud por locale (admin). Response: {stats: [{locale, total, open_count, accepted, rejected, critical_open}]}.
POST /admin/translations/:id/accept Aceptar la sugerencia — parchea el archivo .po en disco. Body: {note?}. Response: {ok, po_patched, glossary_suggestion}.
POST /admin/translations/:id/reject Rechazar la sugerencia. Body: {note?}. Response: {ok}.

POST /feedback/translation — valida: locale ∈ {uk,de,es,fr,pl,pt-BR,ru}, msgid ≤1000, suggestion ≤2000, severity ∈ {minor,major,wrong}. Tras 3+ sugerencias aceptadas para el mismo msgid → glossary_suggestion: true en la respuesta de accept.

El widget flotante de FeedbackWidget.jsx ahora tiene un 4.º tipo «Translation» — auto-rellena el locale desde i18n.locale y captura msgid + suggestion + severity.

Arc Help AI Chat (Phase 61, #147)

Método Ruta Descripción
POST /help/chat Q&A con IA dentro de la app. Body: {message, history: [{role,text}]}. Response: {reply, sources: string[], remaining, limit}. Rate limit: 30/day/user.
GET /help/usage Límite actual. Response: {remaining, limit, used}.

POST /help/chat — pipeline: (1) verificación de rate limit (429 si se supera), (2) RAG vía shared/rag.ts (Cohere + sqlite-vec, Phase 71) fusionando hits del proyecto + skills _global_ → fallback de búsqueda por keywords en docs/public/, (3) Claude Haiku con system prompt + contexto de docs + history. message ≤2000 chars. Responde en el idioma de la pregunta.

Beta Invites (Phase 52.1, solo admin)

Método Ruta Descripción
GET /admin/dashboard System Dashboard (Phase 60.9, #145). Solo admin. Devuelve: CPU/RAM/Disco desde /proc, usuarios por plan, flota de containers, últimos 50 eventos de actividad, estadísticas de waitlist + proyectos + issues.
GET /admin/wipe-metrics Dashboard de telemetría WIP-E (#308). Solo admin. Devuelve: {render: {count, avg_ms, p50_ms, p95_ms, max_ms}, interaction: {count, avg_per_session, p95_per_session, max_per_session, sessions_zero}, by_worker: [{worker_id, render_count, avg_render_ms, session_count, avg_interactions}], daily: [{date, renders, interactions, avg_render_ms}], recent: [...]}.
GET /admin/waitlist Lista de todas las solicitudes de waitlist. Solo admin. Response: {entries: [{id, email, message, status, created_at}]}.
POST /admin/waitlist/:id/approve Aprobar la solicitud — genera invite code (arc-XXXX-XXXX), envía email con el código, actualiza status→approved. Response: {ok, invite_code, email_sent}.
POST /admin/waitlist/:id/reject Rechazar la solicitud. Response: {ok}.
GET /admin/invites Lista de todos los códigos de invitación + conteos (total_active, total_used). Solo admin.
POST /admin/invites Generar N códigos. Body: {count: N, note?: string}. Solo admin. Response: {ok, codes, count}.
DELETE /admin/invites/:code Revocar código de invitación no utilizado.
/admin/notebooklm/* Eliminados en Phase 71.8 junto con el NotebookLM Bridge. La búsqueda semántica ahora funciona mediante el RAG self-hosted (rag-architecture.md).

Actualización del flujo de auth: POST /api/auth/register ahora requiere el campo invite_code (closed beta Phase 52.1). Sin código → 403 {error: "invite_required"}. Código inválido/usado → 403 {error: "invalid_invite"}.

Standard Cloud — WebSocket Terminal + SSE Logs (Phase 60 #139)

Protocolo Ruta Descripción
WS /ws/cloud/:userId/terminal?token=<JWT> Proxy a docker exec -i <containerId> /bin/bash. IDOR: userId debe coincidir con el chatId del JWT. Un container en pausa se reanuda automáticamente. Frames WS entrantes → stdin del container; stdout+stderr → frames WS.
SSE /api/sse/cloud/:userId/logs docker logs -f --tail 50 para el container del usuario. Auth: Bearer JWT. IDOR: userId === chatId. Eventos: data: {"line": "..."} por línea, data: {"closed": true} al salir.

Standard Cloud (Phase 60)

Método Ruta Descripción
POST /cloud/claude-verify Verifica claude --version en el container (shell-quoted transport-safe vía SSH en modo remote-host, #329). Establece claude_authed=true. Response: { ok, output }
POST /cloud/ssh-keygen Genera una clave ed25519 en el container (idempotente). Response: { public_key }
POST /cloud/ssh-verify ssh -T [email protected] en el container. Establece github_authed=true si tiene éxito. Response: { ok, output }
POST /cloud/provision Provisión de un Docker container para el usuario. Requiere plan cloud, 402 en caso contrario. Idempotente: si el container ya existe — devuelve el estado actual. Response: { container_id, status, server_ip, port, claude_authed, github_authed }
GET /cloud/status Estado del container + reconciliación con docker inspect en vivo. Response: { container_id, status, server_ip, internal_port, claude_authed, github_authed, docker_running, last_active, created_at } o { status: "none" }
POST /cloud/deprovision Detener + eliminar el container (docker stop + docker rm -f + docker network rm arc-net-{id}). Actualiza status=deleted en DB. Response: { ok: true, container_id }

Estados del container: provisioningreadypausedsuspended / deleted.

Seguridad (SEC-60 #152, #154, #155, #156): cada container está aislado en su propia red arc-net-{id} (prevención de lateral movement). Conexión SSH Contabo→Hetzner mediante el usuario dedicado arcapi (grupo docker, sin root) con wrapper docker-only — los comandos no-docker quedan bloqueados a nivel de authorized_keys. ARC_TOKEN se inyecta con docker exec después del arranque (no visible en docker inspect). git clone limitado con timeout 60. Idle timeout de WebSocket: 120s. SSE docker logs limitado a --since 1h. Prevención de IDOR: todos los endpoints comprueban container.user_id === req.userId. Flags de seguridad en docker run: --cap-drop=ALL --security-opt=no-new-privileges --cpus=1.5 --memory=2g --pids-limit=200. Volumes: arc-{id}-workspace:/workspace, arc-{id}-claude:/home/arcuser/.claude, arc-{id}-ssh:/home/arcuser/.ssh. Lifecycle (#141): GET /cloud/status siempre actualiza last_active. 30 min idle → docker pause (cron cada 5 min, scripts/cloud-lifecycle-cron.ts). Wake: mensaje CRM, mensaje TG, upgrade WS → docker unpause automático.

Waitlist (#134):

Método Ruta Descripción
POST /cloud/waitlist Unirse a la cola. Idempotente. Response: { position, status, joined_at, message }. 409 si ya está en el plan cloud o ya tiene un container.
GET /cloud/waitlist/status Estado propio en la cola. Response: { position, status, joined_at, invited_at } o { status: "not_joined" }.
GET /cloud/waitlist Solo admin. Lista completa + stats. Response: { stats: { total, waiting, invited, activated }, list: [...] }.
POST /cloud/waitlist/invite Solo admin. Invitar a un usuario. Body: { user_id }. Establece status=invited + sube automáticamente el plan a cloud. Response: { ok, user_id, position }.

Billing (Phase 51 → #202 Plata by mono)

Phase #202: Stripe fue sustituido por Plata by mono (internet acquiring de monobank). Suscripciones recurrentes mediante tokenization (la tarjeta se guarda en el primer pago).

Método Ruta Descripción
GET /billing/status Plan actual, límites, usage, features. Response: { plan, status, current_period_end, next_billing_date, plata_masked_pan, limits, usage, features, pricing, can_upgrade, plata_ready }
POST /billing/checkout-session Crea un invoice de Plata con tokenization. Body: { plan: "min"|"cloud", success_url?, cancel_url? }. Response: { url, invoice_id, plan, amount_uah }. 503 si PLATA_MERCHANT_TOKEN no está en el vault.
POST /billing/webhook Callback de Plata (SIN auth CRM — verificado por el header X-Token). Estados: success (activa el plan + guarda cardToken), failure/expired (incrementa billing_failures, 3+ → downgrade a free). Idempotente vía la tabla plata_events.
POST /billing/cancel Cancelar la suscripción (downgrade a free). Pausa el Docker container en el plan cloud. Response: { ok, plan: "free" }.

#205 (2026-05-26): La ruta legacy /billing/portal-session se eliminó junto con el código muerto de Stripe. Usa /billing/cancel para cancelar una suscripción.

Límites del plan (semántica OR):

Respuesta 402 en POST /onboarding/setup o POST /projects/:name/workers cuando se supera el límite: { error: "plan_limit_reached", reason: "projects_limit"|"workers_limit", current, limit, plan, message }

Los usuarios admin (role=admin) omiten completamente la verificación de límites del plan — son operadores, no tenants de pago.

Los beta-testers (subscriptions.plan='beta', Phase 52 F&F) también la omiten — proyectos y workers ilimitados más todas las features Max. Se asigna manualmente: UPDATE subscriptions SET plan='beta' WHERE user_id=?.

Bugfix (issue #25): POST /projects/create (Quick Start, Phase 50.2) antes fallaba con ownerChatId is not defined por un typo — corregido, el actor del audit ahora se registra correctamente.

Bugfix (issue #26): allocatePort() para nuevos proyectos ahora prueba los bindings TCP reales (ss -tln), no solo el registry. Antes podía devolver un puerto ocupado por un servicio fuera del registry (NotebookLM bridge :19213, internal bridges) → el workspace bot fallaba con EADDRINUSE.

Flujo de auth (Phase 50.1): /api/auth/register y /api/auth/login ahora devuelven JWT incluso para email no verificado + flag needs_verification: true. Las acciones sensibles (trial grant, billing, invites) verifican email_verified por separado. Rate limit en signup: 3 / IP / 24h.


Proyectos (9 endpoints)

Método Ruta Descripción
GET /projects Lista de proyectos del usuario
POST /projects/create Crear proyecto — body: {displayName, projectName, niche?, teamPreset?}; para usuarios trial establece automáticamente trial_mode=1 e inyecta PLATFORM_ANTHROPIC_KEY
POST /projects/create-with-team Creación atómica de proyecto + workers + (opc.) bot de TG en una sola petición — body: {project, workers[], telegram?}; rollback en caso de error
GET /projects/suggest-preset Sugerencia de preset según el nicho — query: niche=<text>; devuelve {preset_id} basado en un keyword map
GET /projects/:name Detalles del proyecto
GET /projects/:name/config Configuración del proyecto
PUT /projects/:name/config Actualizar configuración
POST /projects/:name/upload-icon Subir icono PNG/GIF del proyecto
POST /projects/:name/workers/:id/upload-icon Subir icono PNG/GIF del worker
GET /projects/:name/protocol Protocolo del proyecto
PUT /projects/:name/protocol Actualizar protocolo
GET /projects/:name/logs Logs del proyecto
GET /projects/:name/metrics Métricas del proyecto

POST /projects/create — body:

{
  "technical_name": "string",
  "displayName": "string",
  "description": "string",
  "icon": "string",
  "color": "string"
}

GET /projects/:name/logs — query: category, lines

GET /projects/:name/metrics — query: since, until


Workers (11 endpoints)

Método Ruta Descripción
GET /workers #304 Phase A — todos los workers de todos los proyectos del usuario actual. Response: { workers: [{ id, label, icon, type, model, tools, context_assets, project_name }] }. Filtrado por owner_id (multi-tenancy). El CEO ve todos los proyectos.
GET /workers/presets #228 — biblioteca global de presets (project-agnostic). Devuelve 13 workers del canónico config/workers_registry.json: { presets: [{ id, label, icon, type, model, max_turns, tools, system_prompt, context_assets, focus_dirs, prompt_style }] }. Lo usa el WorkerCreationWizard para el Step 1.
GET /workers/templates #304 Phase I — plantillas del usuario actual. Response: { templates: [{ id, name, description, config, is_public, created_at }] }.
POST /workers/templates #304 Phase I — guardar/actualizar plantilla. Body: { name, description?, config }. Response: { ok, id }.
DELETE /workers/templates/:id #304 Phase I — eliminar plantilla (solo el propietario). Response: { ok }.
GET /projects/:name/workers Lista de workers
POST /projects/:name/workers Crear worker
POST /projects/:name/workers/reorder Phase 53.8 — reordenar workers. Body: {order: [id1, id2, ...]}. Reescribe workers_registry.json de forma atómica. Los workers ausentes en order se añaden al final (protección contra pérdida de datos). Response: {ok, count, order}.
PUT /projects/:name/workers/:id Actualizar worker
DELETE /projects/:name/workers/:id Eliminar worker
POST /projects/:name/workers/generate-prompt Generar prompt de sistema
GET /projects/:name/workers/:id/telegram-token Obtener token de Telegram
POST /projects/:name/workers/:id/telegram-token Phase 53.4 — valida el token con Telegram getMe, guarda bot_username en el vault, rechaza si el mismo bot ya está vinculado a otro worker (409). Response: {ok, started, bot_username}.
DELETE /projects/:name/workers/:id/telegram-token Eliminar token de Telegram
POST /projects/:name/workers/:id/avatar #304 Phase D — subir avatar (multipart file, JPEG/PNG/WebP, máx 2 MB). Verificación de magic bytes. Se guarda en data/worker-avatars/, se registra en worker_avatars (migration 043). Response: { ok, url }.
GET /projects/:name/workers/:id/avatar #304 Phase D — obtener el avatar en binario (Content-Type según el MIME). 404 si no hay avatar subido.
DELETE /projects/:name/workers/:id/avatar #304 Phase D — eliminar avatar, restablecer avatar_pack='role' en el JSON del worker.
GET /projects/:name/workers/:id/activity #306 — feed de actividad del worker (últimos 50 eventos). Merged: activity_log (actor=workerId) + project_issues.activity (author=workerId) + token_usage_log (snapshots diarios). Response: { events: [{ type, title, detail, when }] }. Types: git_commit, skill_loaded, skill_unloaded, issue_pick, issue_close, issue_log, token_budget, session_start.
GET /projects/:name/workers/:id/runtime #306 — estado runtime del worker. Response: { status: 'working'|'idle', status_started_at, tokens_today, tokens_pct, tokens_cap, current_skill }. Lee primero de workers_runtime_state (migration 045); fallback por staleness: status='working' + tmux muerto + updated_at > 10 min → idle (detección de crash). Tope diario según el plan vía lookup de subscriptions.plan: free=100K, starter=400K, starter_cloud=2M, beta=sin límite (devuelve tokens_cap: null, tokens_pct: 0). Intervalo de polling 15s.
POST /projects/:name/workers/:id/notify Phase 53.2 — enviar ping de evento TG ({event?, text, buttons?}). No-op silencioso si no hay token vinculado o CRM_DISABLE_TG_NOTIFY=1.
POST /projects/:name/workers/:id/suggest-bot-username 53.11.1 (issue #48) — devuelve 5 candidatos de username TG para el wizard de creación de bot con formato <project>_<worker>_bot + fallbacks numerados. Slugify elimina guiones, trunca a 32 chars (la parte del worker se trunca primero). Response: {candidates: string[]}.
POST /metrics/wizard 53.11.1 (issue #48) — sink de telemetría para el wizard de creación de bot. Body: {action, duration_ms?, attempts?, success?, project?, worker_id?, locale?} (#124: eventos locale_active/locale_switch). Escribe en activity_log (event_type=wizard_metric), best-effort.
GET /analytics/wizard-metrics?hours=168 53.11.1 (issue #48) — resumen del funnel: {starts, completions, abandons, success_rate, avg_duration_ms_completed, avg_attempts_completed, by_action}. Default 7 días, clamp 1-720h.
POST /projects/:name/restart Reiniciar worker
GET /projects/:name/active-role Rol activo actual
POST /projects/:name/active-role Cambiar rol activo

POST /projects/:name/workers — body:

{
  "label": "string",
  "icon": "string",
  "type": "terminal | telegram",
  "model": "string",
  "max_turns": 20,
  "tools": ["Read", "Write", "Bash"],
  "system_prompt": "string",
  "focus_dirs": ["src/", "docs/"]
}

max_turns por defecto es 20 (antes era 5, lo que causaba el error "Reached max turns" en diálogos multi-paso con tool calls).

POST /projects/:name/restart — query: worker_id


Archivos y almacenamiento (8 endpoints)

Método Ruta Descripción
GET /projects/:name/files Árbol de archivos
POST /projects/:name/files/upload Subir archivo (multipart, máx 100MB)
POST /projects/:name/files/mkdir Crear directorio
POST /projects/:name/files/create Crear archivo
GET /projects/:name/files/read Leer archivo
PUT /projects/:name/files/save Guardar archivo
DELETE /projects/:name/files/delete Eliminar archivo
POST /projects/:name/files/clone Git clone de repositorio

GET /projects/:name/files — query: path

GET /projects/:name/files/read — query: path, raw


Skills (18 endpoints)

Skills del proyecto

Método Ruta Descripción
GET /projects/:name/skills Lista de skills del proyecto. Devuelve las globales (owner_project=NULL) + las skills de este proyecto (owner_project=name). Las skills de otros proyectos no se incluyen (#157).
POST /projects/:name/skills Crear skill. Se guarda con owner_project=name, visible solo para este proyecto.
PUT /projects/:name/skills/:id Actualizar skill
DELETE /projects/:name/skills/:id Eliminar skill

#210 (2026-05-26): la DB (skills_global) es ahora el escritor SSOT. Los guardados desde la UI van primero a la DB; .claude/skills/<name>/SKILL.md se escribe como artefacto para que Claude Code CLI auto-descubra las skills. Las escrituras legacy a skills/<name>.md se eliminaron — los archivos existentes ya no se leen ni se mantienen. Helper de migración: scripts/migrate-skills-to-db.ts.

Marketplace global

Método Ruta Descripción
GET /skills Lista de skills globales
POST /skills Publicar skill
GET /skills/:id Detalles de skill
PUT /skills/:id Actualizar skill
DELETE /skills/:id Eliminar skill

Evolución y actualizaciones

Método Ruta Descripción
GET /skills/:id/evolution Historial de evolución de la skill
GET /skill-updates Lista de actualizaciones disponibles
POST /skill-updates/:id/approve Aceptar actualización
POST /skill-updates/:id/reject Rechazar actualización

Forks de skills

Método Ruta Descripción
GET /projects/:name/skill-forks Lista de forks
POST /projects/:name/skill-forks Crear fork
PUT /projects/:name/skill-forks/:id Actualizar fork
DELETE /projects/:name/skill-forks/:id Eliminar fork

Chat y mensajes

Método Ruta Descripción
POST /projects/:name/chat Enviar mensaje al chat
GET /projects/:name/chat/history Historial de chat
POST /projects/:name/message Enviar mensaje al worker (Phase 48.6: wake-up automático del worker inactivo, ~2-4s cold start; Phase 48.6.1: el wake-up ahora funciona también en proyectos single-mode, no solo parallel)
GET /projects/:name/pins Lista de notas (pins)
POST /projects/:name/pins Crear nota
DELETE /projects/:name/pins/:id Eliminar nota

Wiki (4 endpoints)

Método Ruta Descripción
GET /projects/:name/wiki/tree Árbol de páginas wiki
GET /projects/:name/wiki/file Leer página wiki
PUT /projects/:name/wiki/save Guardar página wiki. Phase 71.5: dispara syncWiki → re-embed con Cohere (fire-and-forget; los fallos se loguean, el write no falla).
GET /projects/:name/wiki/download Descargar wiki como archivo ZIP

Analytics (4 endpoints)

Método Ruta Descripción
GET /analytics/activity Feed de actividad
GET /analytics/sidebar Datos para el panel lateral
GET /analytics/phases Lista de fases del proyecto
POST /analytics/phases Actualizar fases del proyecto

Marketplace y Sage (8 endpoints)

Método Ruta Descripción
GET /sage/scout/categories Categorías del marketplace
POST /sage/scout Buscar skills
POST /sage/scout/quick-scan Escaneo rápido
POST /sage/scout/analyze Análisis profundo de skill
POST /sage/scout/install Instalar skill
POST /sage/analyze Análisis de Sage
GET /sage/status Estado del servicio Sage
POST /sage/benchmark Ejecutar benchmark

Memoria y Knowledge

Método Ruta Descripción
GET /projects/:name/rag/search?q=...&k=6&include_global=true&doc_types=wiki,issue,skill,transcript Phase 71.7 (#364): búsqueda semántica sobre embeddings + embeddings_vec (Cohere + sqlite-vec). Parámetros: q (texto de la consulta), k (1-25, default 6), include_global (default true — merge con el namespace de skills _global_), doc_types (subconjunto separado por comas; Phase 73.6 tipo adicional: transcript). Response: `{ query, project, hits: [{rank, doc_type, doc_id, chunk_ix, distance, scope: 'project'
POST /projects/:name/memory/refresh Phase 71.8 (#365): re-embed de MANIFEST + ROADMAP + archivos clave en el RAG store (antes — sync a NotebookLM). Mismo endpoint, nueva semántica.
POST /projects/:name/memory/fetch-artifact Eliminado en Phase 71.8 (el audio overview no tiene equivalente en RAG) — devuelve 410 Gone.
GET /projects/:name/learnings Lista de learnings
POST /projects/:name/learnings Añadir learning
GET /projects/:name/knowledge-graph Grafo de conocimiento del proyecto

Documentación (global, sin auth)

Método Ruta Descripción
GET /docs/tree?lang=<lang> Árbol de documentación; lang opcional (en/uk), default en
GET /docs/file?path=<p>&lang=<lang> Leer archivo de documentación con language fallback

GET /docs/tree — query: lang (opcional)

GET /docs/file — query: path (obligatorio), lang (opcional)


Sistema

Método Ruta Descripción
GET /system/configs Obtener configuraciones del sistema
PUT /system/configs Actualizar configuraciones del sistema

Códigos de error

Código Significado
200 Éxito
201 Creado
400 Solicitud inválida
401 No autorizado
403 Prohibido (multi-tenancy)
404 No encontrado
409 Conflicto (duplicado)
429 Demasiadas solicitudes
500 Error del servidor

GitHub Integration (Phase 49.3)

Endpoint Method Descripción
/api/crm/projects/:name/github GET Lista de repos de GitHub vinculados al proyecto
/api/crm/projects/:name/github POST Vincular repo (body: {owner, repo}) — devuelve webhook URL + secret + instrucciones de configuración
/api/crm/projects/:name/github/:id DELETE Desvincular repo
/api/crm/projects/:name/github/events GET Lista de GitHub events recientes (Phase 49.3.1, query: ?limit=50)
/api/webhooks/github POST Receptor público de webhooks (validado con HMAC-SHA256, rate-limit 100/min)

Eventos soportados: push, pull_request, workflow_run, issues. Las notificaciones se enrutan al Telegram del propietario del proyecto.

Account Security (Phase 45.4)

Endpoint Method Descripción
/api/crm/account/recovery GET Lista de recovery keys activas
/api/crm/account/recovery POST Crear recovery key (body: encryptedKey, keyHint)
/api/crm/account/recovery DELETE Revocar recovery key(s) (body: { id } o {} para todas)
/api/crm/account/recovery/restore GET Obtener encrypted master key para recuperación

Seguridad


Phase 53.13 — baseline de type-safety (2026-05-10)

Sin cambios de comportamiento en endpoints — solo tipos internos. tsc --noEmit ahora bloquea push/CI:


Sentinel Pentest Remediation (2026-06-10, #433–#444)

Sprint de pentest white-box — cambios de comportamiento de los endpoints tras corregir 3×P1 + 4×P2 + 3×P3:


Phase 53.15 — Sentinel Sprint 1 (2026-05-10)

Cambios de comportamiento en endpoints de auth + admin (correcciones P0 del audit de Sentinel):


Phase 53.21 — Sentinel P2 batch 2 (2026-05-12)

Phase 63 — Consolidación UI/UX + Seguimiento de Uso de Tokens (2026-05-21, #148)

Nuevo endpoint:

Cambios en claude-runner.ts:

Cambios de UI (no API):

Phase 53.18 — corrección de filtración de secretos en tmux (2026-05-11)

Sin cambios de comportamiento en endpoints — solo refactor de rutas internas de spawn.

Phase 53.16 — Sentinel Sprint 2 (2026-05-10)

Cambios de comportamiento en endpoints tras el hardening de 13 × P1:

Phase 55 — Cosmic Editorial login (2026-05-13)

Nuevos endpoints para inicio de sesión por magic-link:

El union EphemeralTokenType se extendió: ahora incluye "magic_link" junto a los existentes oauth_state / password_reset / email_verification / tfa_challenge.

El frontend (CosmicCard.jsx) gestiona el estado magic (cuenta regresiva de reenvío de 60s) y el parámetro URL ?magic_token= (auto-consume → login → animación de éxito).

Phase 56 — AI Interop / Project Context Export (2026-05-13)

Exportación exclusiva para owners de un snapshot saneado del proyecto como .md para transferirlo a una IA externa (Gemini / ChatGPT / Perplexity / Claude.ai).

Alerta: cuando el owner supera 3 exportaciones en 24h Y prefs.notify_on_export = true (por defecto OFF) — logActivity("export_alert", ...) pasa por el pipeline de notificación TG de Phase 53.10 (alertFired: true en el body de la respuesta).

El scanner multi-nivel (shared/secret-scanner.ts) — Tier 1 regex (PATTERN_REGISTRY del sanitizador PII), Tier 2 entropía Shannon ≥4.5 bits/char en cadenas de ≥20 chars, Tier 3 heurísticas de contexto (key=/token:/secret=/password=). Whitelist: UUID / git SHA / SHA-256 / chars repetidos / hex corto / base58 de baja entropía. Niveles de severidad (critical/high/medium/low). Rendimiento: <500 ms / 1 MB.

Migración DB 024 — tablas export_audit_log + export_preferences.


Phase 57 — Platform Settings (seguimiento Sentinel #103, 2026-05-15)

Gestión de secretos super-admin desde la UI de CRM en lugar de ssh/editar-.env/pegar-en-chat. Backend MVP (Stage 1 de 4 stages). Todos los endpoints requieren requireAdmin (Phase 53.15) — devuelven 403 Forbidden — admin only para no-admin, 401 Unauthorized sin JWT.

Lista de exclusión fija NEVER_EXPOSE: CRM_SECRET (firma JWT) + SECRET_ENCRYPTION_KEY (meta-clave del vault) — incluso una solicitud admin con token válido devuelve 400 "not managed". El log de auditoría es append-only (sin handler UPDATE/DELETE), cada acción (incluidas las fallidas) escribe una fila con IP + UA + email.

Migración DB 026 — tabla platform_audit_log. Stage 2 (frontend PlatformSettings.jsx) — publicado el 2026-05-15 (cbc8bac): grid de tarjetas solo para admin + modal de rotación (<input type="password"> + confirmación de reescritura) + drawer de auditoría; entrada en sidebar filtrada por userRole === "admin" obtenido de /api/auth/me.

Polish (2026-05-15, commit 56191b0) — Restructura de la UI de Platform Settings. Los items de la respuesta de GET /api/crm/platform/settings ganan 5 nuevos campos: category (anthropic|oauth|telegram|email), usedIn (string[] — archivos/flujos que consumen la clave), getFromUrl (dónde obtener un valor nuevo), effectAfterRotate, riskIfLeaked. El frontend los utiliza para renderizar 4 grupos de tarjetas por sección + panel de ayuda colapsable por tarjeta con contexto estructurado (Used in / Get from / Effect / Risk). Sin cambios de comportamiento en los endpoints mutadores (PUT/POST/restart/test).

Refactor (2026-05-16) — cleanup interno de shared/routes/platform.ts. Se eliminaron 39 líneas (16 añadidas), sin cambios en la superficie pública de la API. Las firmas y respuestas de los endpoints PUT/POST/restart/test/audit no cambian. Documentado aquí solo porque el gate pre-push de cobertura de documentación se activa ante cualquier diff en shared/routes/*.ts.

Actividad backdated (#117, 2026-05-16)POST /api/mcp/issues/:project/:id/log ahora acepta el campo opcional ts (string ISO-8601). Lo usa arc retro en la reconstrucción para que las entradas históricas queden en sus timestamps originales. Los valores con fecha futura se recortan silenciosamente a now dentro de addActivity() (defensa contra errores de tipeo). ISO inválido → 400.

Stage 3 (2026-05-15) — recarga en caliente de secretos OAuth + Resend sin reinicio. shared/auth.ts loadOAuthConfig() ahora lee getSecret("GITHUB_CLIENT_ID/SECRET" | "GOOGLE_CLIENT_ID/SECRET") por llamada en lugar de process.env. Los callsites en master-bot/routes/auth.ts ya invocaban getOAuthConfig() por request → 0 cambios en callsites. RESEND_API_KEY ya tiene hot-reload mediante shared/email.ts:47. Cambio de comportamiento: PUT /api/crm/platform/settings/{GITHUB_CLIENT_ID|GITHUB_CLIENT_SECRET|GOOGLE_CLIENT_ID|GOOGLE_CLIENT_SECRET|RESEND_API_KEY} ahora surte efecto en la siguiente solicitud, sin necesidad de reinicio. restartTargets para estas 5 claves está vacío → el botón Restart en la UI está oculto. Caso límite: un flujo OAuth con state-token emitido antes de la rotación puede recibir un 400 en el callback durante el code-exchange — el usuario puede resolver reintentando. ANTHROPIC_API_KEY, PLATFORM_ANTHROPIC_KEY, MASTER_BOT_TOKEN, CITADEL_BOT_TOKEN siguen requiriendo reinicio (se leen al hacer spawn del child-bot / inicio del long-poll TG).

Cleanup Phase 57.3.5 (2026-05-16) — allowlist MANAGED_KEYS reducida de 9 a 6. Eliminadas: ANTHROPIC_API_KEY (los operadores ahora usan PLATFORM_ANTHROPIC_KEY tanto para trial-credits como para inferencia de plataforma; el fallback a .env sigue funcionando para rutas de código legado hasta que Sage/Karpathy migren), CITADEL_BOT_TOKEN (el bot por proyecto pertenece a las entradas child:<name>:token del vault, gestionadas por el flujo de onboarding de workers — no en Platform Settings). MASTER_BOT_TOKEN reutilizado: label → "Telegram — System Monitor Bot", descripción → "Server health alerts + on-demand status probes (admin-only, not a chat bot)". La Phase 58 añadirá el loop de monitoreo (alertas push para crash de worker / disco / RAM / brute-force SSH / bypass CF + comandos /status, /health, /errors, /restart). Conjunto final: PLATFORM_ANTHROPIC_KEY + GITHUB×2 + GOOGLE×2 + MASTER_BOT_TOKEN + RESEND_API_KEY (refs #103).

Arc Help (Phase 61 / #147)

Historial (Phase 61 / #153):

GDPR / Compliance (Sprint 1+2, #161–#174, 2026-05-22)

Right to Erasure — DELETE /api/auth/account (#162)

Elimina permanentemente al usuario autenticado y todos sus datos (RGPD Art. 17).

Password Version / Token Invalidation (#174)

La migración 035 añade password_version INTEGER NOT NULL DEFAULT 0 a users. Al cambiar la contraseña, password_version se incrementa. El payload JWT incluye el campo pv. crmAuthMiddleware valida pv contra la DB en cada solicitud, rechazando tokens emitidos antes del último cambio de contraseña (401 "Token invalidated — please log in again"). Fail-open si la DB no está disponible.

Data Retention Cron (#168)

El master bot ejecuta una purga diaria al arrancar + cada 24h. Límites de retención: chat_messages 180 días (por timestamp), activity_log 365 días (por created_at), auth_events 90 días (por ts), token_usage_log 730 días (por created_at unixepoch), export_audit_log 365 días (por exported_at). No fatal — la purga no bloquea el arranque.

Email Compliance (#167)

Todos los emails transaccionales salientes (password reset, verificación, magic-link) ahora incluyen:

Security — HIBP Breached Password Check (#171)

En POST /api/auth/register y POST /api/auth/reset-password, la contraseña enviada se comprueba contra la API de k-anonimato de HaveIBeenPwned antes de almacenarse. Solo se envían a HIBP los primeros 5 caracteres hex del hash SHA-1 — la contraseña completa nunca sale del servidor. Si la contraseña aparece en alguna base de filtraciones con count > 0, la solicitud se rechaza con HTTP 400: "This password was found in a known data breach. Please choose a different password." Fail-open en timeout/error de HIBP (timeout de 4s) — un HIBP caído no bloquea el registro.

Data Portability — GET /api/auth/export (#163)

RGPD Art. 20 — derecho a la portabilidad de los datos. Devuelve un archivo JSON estructurado con todos los datos personales que Arc OS guarda sobre el usuario autenticado.

Arc Help — Hardened System Prompt + Anti-Injection (#151)

Cambios de comportamiento de POST /api/crm/help/chat (sin cambios en la superficie de la API):

Worker Discipline Hardening (#187, #188, #189, 2026-05-23)

Issue Status Expansion (#187)

PUT /api/mcp/issues/:project/:id ahora acepta valores de status extendidos:

Status Significado
open Aún no iniciado
in_progress En trabajo activo (lo establece arc issue take)
blocked Esperando una dependencia externa
deferred Pospuesto (antes solo se guardaba como texto)
closed Hecho

Nuevo campo assignee: los issues ahora tienen assignee: string | null. Se establece con arc issue take <id> o --assignee <worker_id> en arc issue update.

Migración 036: ALTER TABLE project_issues ADD COLUMN assignee TEXT (nullable, auto-aplicada al arrancar el servidor).

Comando CLI arc issue take <id> (#187)

Atajo para reclamar un issue: establece assignee = current_worker_id, status = in_progress, registra la actividad, escribe el session state. Equivalente a:

arc issue update <id> --status in_progress --assignee developer
arc issue log <id> "Taken by developer — status set to in_progress"

Validación del hook commit-msg (#187)

.githooks/commit-msg ahora valida los issues #N referenciados contra el issues/issues.json local:

Inyección de PROJECT_MANIFEST.md en el bridge (#188)

handleCliInit (shared/cli-routes.ts) ahora lee PROJECT_MANIFEST.md desde la raíz del proyecto y lo inyecta en el bloque CITADEL bajo ## Project Context. Límite: 8000 chars. Esto da a los workers de bridge (que corren en máquinas de clientes vía arc) acceso a la arquitectura compacta, patrones de seguridad, estructura de archivos y learnings clave del CLAUDE.md completo.

Ubicación: después de PROJECT_RULES.md, antes de la lista de skills.

Campo de configuración context_assets del worker (#189)

La configuración del worker en workers_registry.json admite el campo opcional context_assets: string[] — lista de nombres de skills que se inyectan automáticamente en cada sesión de bridge de ese worker (sin requerir arc skill <name>):

{
  "id": "developer",
  "context_assets": ["crm-api-reference", "archivist_system"]
}

El contenido de cada skill se inyecta bajo ### Auto-Loaded Skills → #### Skill: <name>, truncado a 3000 chars cada uno.

Phase 62 — Voice Input (#373, 2026-06-05)

Transcripción de voz en tiempo real proxied a través del servidor whisper.cpp self-hosted (arc-whisper.service, puerto 19214, modelo ggml-base precargado).

POST /api/crm/voice/transcribe (#373, Phase 62.4)

Transcribe clips de voz cortos (dictado en el chat). Hace proxy del audio al whisper-server local y devuelve texto.

Auth: Bearer token (o query ?token=).

Body: multipart/form-data

Campo Tipo Notas
audio Blob webm / ogg / wav. Máx 25 MB.
locale string BCP-47, p. ej. uk-UA, en-US. Se pasa a whisper como parámetro language.

Response 200:

{ "transcript": "Що ти зробив вчора?" }

Códigos de error:

Código Significado
400 Falta el campo audio o locale
413 Audio mayor de 25 MB
429 Cuota diaria alcanzada (60 min/user/day) O servidor ocupado (máx 2 transcripciones concurrentes)
502 El whisper-server devolvió un no-200
500 Fallo inesperado

Rate limit: voice_usage_log (migration 051) registra segundos aproximados por (usuario, día) usando el tamaño en bytes de la subida como proxy (asume codec de voz ~32 kbps, precisión ±30%). Tope duro: 3600 s / día. Las solicitudes que superarían el tope devuelven 429 antes de reenviar a whisper.

Nota de arquitectura: whisper corre solo en Contabo (no dentro de los containers per-user de Hetzner). Los bytes de audio nunca salen de Contabo; el texto resultante es lo que ve el cloud-chat routing de Phase 70. arc-whisper.service mantiene precargado el modelo ggml-base de modo que el coste por llamada es pura inferencia (~3.4 s warm para 11 s de audio, 3.1× tiempo real en el actual servidor EPYC de 6 vCPU).


Phase 73 — Meeting Transcription + Analysis (#377-#384, 2026-06-05)

Sube audio/vídeo de una reunión a un proyecto, obtén transcripción con whisper + resumen de Claude, opcionalmente embebido en RAG. Todas las rutas están protegidas por canAccessProject (owner o admin).

POST /api/crm/projects/:name/transcripts/upload (#377, Phase 73.1)

Upload multipart, devuelve 202 con transcript_id + job_id + status:'queued'. El job lo recoge la cola in-process (máx 1 concurrente).

Campos del body:

Límites: 1 GB máx por subida, allow-list de MIME (mp3/wav/m4a/aac/ogg/opus/flac + mp4/mov/webm/mkv).

Errores: 400 (campo ausente / MIME inválido), 401, 413 (sobre el tope), 500 (escritura en disco).

GET /api/crm/projects/:name/transcripts (#379, Phase 73.3)

Lista los transcripts del proyecto, con paginación por cursor. Query: ?limit=20&cursor=<id>. Devuelve {items: TranscriptSummary[], next_cursor: number|null}.

GET /api/crm/projects/:name/transcripts/:id (#379)

Fila completa incluyendo transcript_text, summary_json (parseado a objeto) y frames_json (parseado cuando Phase 73.4 esté disponible).

GET /api/crm/projects/:name/transcripts/job/:jobId/progress (#379)

Stream SSE del progreso del job. Envía event: progress con {status, progress_pct, step_label, error} cuando cualquier campo cambia, además de heartbeats de comentario : keep-alive cada 1s para que el idleTimeout de 10s de Bun no mate las ejecuciones largas de whisper. Cierra con event: end cuando el estado es terminal.

Auth: el EventSource del navegador añade ?token=<bearer> (no puede establecer el header Authorization).

Estados terminales: done (tras el embed RAG de Phase 73.6 + limpieza de archivos), failed. Nota: summarized es un paso transitorio — el SSE permanece abierto a través de embeddingdone. El botón de envío del frontend se desbloquea en summarized (no espera al RAG).

Máquina de estados (Phases 73.1-73.6)

queued
  → extracting_audio    (ffmpeg → 16 kHz mono WAV)
  → transcribing        (whisper-cli -t 4)
  → (video) extracting_frames → frames_extracted   (ffmpeg scene-change)
            → vision_analyzing → vision_analyzed   (Phase 73.4 Claude vision per frame)
  → (audio) transcribed
  → summarizing         (Claude Sonnet → summary_json, receives vision frames as context)
  → summarized
  → embedding           (Phase 73.6 Cohere upsert via shared/rag.ts, skipped if embed_to_rag=0)
  → done                (source file + frames dir deleted — CEO decision D4)

Forma del JSON de vision frames (Phase 73.4, #380)

Se guarda como string JSON en transcripts.frames_json (parseado de vuelta a objeto por GET /transcripts/:id).

[
  { "ts_ms": 3000, "description": "Slide titled 'Q3 Revenue' with bar chart showing 30% growth." },
  { "ts_ms": 6000, "description": "Architecture diagram with three boxes labeled API/Worker/DB." }
]

Tope duro MAX_FRAMES=50 por transcript (~$0.15 en el peor caso con el pricing típico de vision de Sonnet). Los frames que superan el tope se descartan silenciosamente; la última descripción conservada recibe el sufijo [+N more frames dropped]. Los fallos por frame se convierten en strings [vision failed: <msg>] — no abortan la pasada. Los frames descritos como "No informational content" son solo webcam o decorativos.

Forma del JSON de summary (Phase 73.5, #381)

Se guarda como string JSON en transcripts.summary_json. Parseado de vuelta a objeto por GET /transcripts/:id.

{
  "tldr": "1-2 sentence executive summary",
  "key_points": ["..."],
  "action_items": [{"task": "...", "owner": "name or null"}],
  "decisions": ["..."],
  "topics": ["..."],
  "model": "claude-sonnet-4-5",
  "generated_at": "2026-06-05T20:04:15.573Z"
}

La resolución de la clave Anthropic refleja shared/worker-spawn.ts: BYOK account_settings.anthropic_key (descifrada si está cifrada), fallback PLATFORM_ANTHROPIC_KEY para owners en trial-mode. Los fallos del summary no son fatales — transcript_text queda intacto, el estado retrocede a transcribed/frames_extracted para que el usuario pueda reintentar tras corregir su clave.

Phase 78 — Notes: Knowledge Collections (#394–#404, 2026-06-08)

Notas por proyecto al estilo NotebookLM. Cada nota es una colección de fuentes (vídeo, audio, YouTube, web, PDF, DOCX, TXT, imagen) con un índice RAG compartido y un chat.

GET /api/crm/projects/:name/notes

Devuelve todas las notas del proyecto. Auth requerido + canAccessProject.

Response 200:

[{ "id": 1, "title": "Sprint planning", "description": null, "created_at": "...", "source_count": 3 }]

POST /api/crm/projects/:name/notes

Crear una nota nueva.

Body: { "title": "string", "description": "string?" }
Response 201: { "id": 1, "title": "Sprint planning" }

GET /api/crm/projects/:name/notes/:id

Detalle de la nota con fuentes, enlaces a issues e historial de chat.

Response 200:

{
  "id": 1, "title": "Sprint planning",
  "sources": [{ "id": 1, "source_type": "youtube", "title": "My video", "url": "...", "status": "done", "duration_seconds": 3600 }],
  "issue_links": [{ "issue_id": 42, "title": "Issue title" }],
  "chats": [{ "role": "user", "content": "Summarize", "created_at": "..." }]
}

DELETE /api/crm/projects/:name/notes/:id

Elimina la nota y todas las fuentes/chats. Cascada a note_sources, note_chats, note_issue_links.

POST /api/crm/projects/:name/notes/:id/sources

Añadir una fuente (subida de archivo o URL).

Content-Type: multipart/form-data O application/json

Response 201: { "source_id": 5, "status": "queued" }

El procesamiento es asíncrono. Haz polling a GET /notes/:id hasta que source.status === "done".

PATCH /api/crm/projects/:name/notes/:id/sources/:sourceId

Renombrar una fuente (edición inline del título).

Body: { "title": "New name" }
Response 200: {}
Pasa una cadena vacía o null para restablecer el default (nombre de archivo/URL).

DELETE /api/crm/projects/:name/notes/:id/sources/:sourceId

Eliminar una fuente y su contenido.

GET /api/crm/projects/:name/notes/:id/sources/:sourceId/progress

Stream SSE del progreso de procesamiento de la fuente.

Eventos: progress { "status": "processing"|"done"|"error", "message": "..." }

POST /api/crm/projects/:name/notes/:id/chat

Enviar un mensaje al chat de la nota. Respuesta en stream SSE.

Body:

{
  "message": "Summarize all sources",
  "selectedSourceIds": [1, 3]
}

selectedSourceIds es opcional — omítelo para incluir todas las fuentes.

Eventos SSE:

Estrategia RAG: búsqueda sqlite-vec sobre los embeddings de note_source → fallback de inyección directa de content_text (máx 80 K chars) cuando la búsqueda vectorial no está disponible o no hay resultados. Se inyecta un guard anti-alucinación en el system prompt cuando se incluyen fuentes sin procesar.

Tool use — create_issue: Claude puede crear issues del proyecto desde el chat. Multi-turn: el turno 1 hace streaming hasta la tool call, el backend la ejecuta (issueQueries.nextId + issueQueries.insert), el turno 2 reanuda el streaming con el resultado de la tool inyectado.

Máquina de estados del status de la fuente

queued → processing → done
                    ↘ error

Valores del campo status de la fuente:

Estrategia de transcripción de YouTube (Phase 78.3)

  1. npm youtube-transcript: cascada de idiomas ["en", "en-US", "en-GB"] → fallback a cualquiera
  2. API de Supadata.ai: GET https://api.supadata.ai/v1/youtube/transcript?url=...&text=true&lang=en → fallback sin el parámetro lang
  3. yt-dlp + Whisper: fallback final para vídeos sin subtítulos

Prioridad: preferir subtítulos en inglés para evitar transcripciones auto-traducidas al árabe u otros idiomas.