Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ agent-office setup # the first-start walkthrough again

Every option is in [docs/configuration.md](docs/configuration.md). Choosing models and providers per worker is in [docs/agents.md](docs/agents.md).

### Editorial JeyTid (optional)

This checkout also contains an optional private editorial subsystem for a single owner. It is
disabled by default so the base office keeps its Node 20 behavior. Enable it on Node 22.5+ with
`EDITORIAL_ENABLED=1`; it persists projects, revisions, tasks, artifacts, approvals, costs and
backups under the office data directory and exposes the authenticated `/editorial` view. Telegram,
OpenAI and Mastodon remain opt-in connectors and are never reported as connected without their
credentials and an observed response.

See [docs/editorial.md](docs/editorial.md) for setup, commands, recovery, capability limits and the
official sources used by the adapters. A local mock demo is available with `agent-office editorial demo`.

To run it from a clone instead:

```bash
Expand Down
17 changes: 17 additions & 0 deletions docs/editorial.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copy into the process environment; never commit real values.
EDITORIAL_ENABLED=1
TELEGRAM_BOT_TOKEN=<token-from-botfather>
TELEGRAM_OWNER_ID=<numeric-owner-id>
TELEGRAM_ALLOWED_CHAT_IDS=<numeric-chat-id>
EDITORIAL_PROVIDER=mock
EDITORIAL_AUTONOMOUS_MODE=0
OPENAI_API_KEY=<optional-api-key>
EDITORIAL_TEXT_MODEL=<configured-text-model>
EDITORIAL_IMAGE_MODEL=gpt-image-1
EDITORIAL_MAX_CONCURRENT=3
EDITORIAL_BUDGET_USD=5
EDITORIAL_BUDGET_WARNING_RATIO=0.8
EDITORIAL_AGENT_OFFICE_FLOOR=<optional-floor-id-or-name>
EDITORIAL_AGENT_OFFICE_PROVIDER=claude
MASTODON_BASE_URL=https://<your-instance>
MASTODON_ACCESS_TOKEN=<optional-user-token>
154 changes: 154 additions & 0 deletions docs/editorial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Editorial JeyTid

Editorial es un subsistema opcional de Agent Office para dirigir una biblioteca personal desde un
solo propietario. El código vive en `src/server/editorial/` y no cambia el comportamiento del
oficio base mientras `EDITORIAL_ENABLED` no sea `1`.

## Requisitos y activación

El oficio base admite Node 20+. Editorial usa `node:sqlite`, por lo que requiere Node 22.5 o una
versión posterior con ese módulo disponible. La base y los archivos privados quedan bajo:

```text
<agent-office-data>/.agent-office/editorial/
editorial.sqlite
files/projects/<project_id>/...
```

Ejemplo de configuración (no contiene credenciales):

```powershell
$env:EDITORIAL_ENABLED = "1"
$env:TELEGRAM_BOT_TOKEN = "<token entregado por BotFather>"
$env:TELEGRAM_OWNER_ID = "<id numérico del propietario>"
$env:TELEGRAM_ALLOWED_CHAT_IDS = "<id numérico del chat>"
$env:EDITORIAL_PROVIDER = "mock" # cambia a openai solo con OPENAI_API_KEY
$env:EDITORIAL_AUTONOMOUS_MODE = "0" # 1 ejecuta tareas elegibles sin pedir confirmación por tarea
$env:EDITORIAL_BUDGET_USD = "5"
$env:EDITORIAL_MAX_CONCURRENT = "3"
```

También se puede comprobar la instalación sin levantar el servidor:

```bash
agent-office editorial doctor --data-dir .agent-office
agent-office editorial demo --data-dir .agent-office
agent-office editorial snapshot --data-dir .agent-office
agent-office editorial backup --data-dir .agent-office --destination ./editorial-backup
agent-office editorial restore ./editorial-backup --data-dir ./restored-office
```

`demo` usa un proveedor mock determinista y nunca publica en KDP ni en una red social.

## Modelo operativo

La entrada de una obra crea un proyecto aislado, una revisión inicial del brief y un flujo durable
con once perfiles: dirección editorial, investigación, derechos/procedencia, escritura, edición de
desarrollo, corrección, arte, maquetación/KDP, auditoría independiente, publicación KDP y marketing.
Cada tarea guarda contrato, dependencias, intentos, generación, lease, heartbeat, checkpoints,
resumen, proveedor, modelo, versión de perfil, evidencias, costos y artefactos. No se almacena
razonamiento privado.

La cola reserva presupuesto dentro de una transacción SQLite, limita la concurrencia, recupera
leases vencidos y descarta respuestas tardías cuando la generación ya no coincide o la tarea fue
cancelada. Una revisión nueva revoca aprobaciones y marca releases preparados como `needs_changes`.
Los artefactos guardan SHA-256, revisión y procedencia; las rutas nunca vienen directamente del
usuario.

## Telegram

El bot usa long polling de `getUpdates`, conserva el offset y deduplica `update_id` en SQLite. Solo
acepta el `TELEGRAM_OWNER_ID` y los chats numéricos incluidos en
`TELEGRAM_ALLOWED_CHAT_IDS`. No se concede autoridad por nombre de usuario.

Comandos principales:

```text
/nuevo_libro <título>
/ideas [contexto]
/proyectos
/estado <project_id>
/tareas [project_id]
/modo
/ejecutar [project_id]
/agentes
/costos
/aprobaciones
/publicar <release_id>
/pausar <project_id> /continuar <project_id>
/cancelar <task_id>
```

En modo `supervisado` (`EDITORIAL_AUTONOMOUS_MODE=0`, valor predeterminado), crear un libro o una
tarea deja la cola pendiente; el propietario inicia el trabajo con `/ejecutar project_id` o
`/ejecutar` para toda la cola. En modo `autónomo` (`EDITORIAL_AUTONOMOUS_MODE=1`), el orquestador
revisa la cola periódicamente y ejecuta las tareas elegibles sin confirmación individual. La vista
`/editorial`, `/modo`, `/estado` y `/tareas` permiten observar el avance. El propietario puede cambiar
el modo del proceso actual con `/modo autonomo` o `/modo supervisado`; para conservar la elección
después de reiniciar, actualiza `EDITORIAL_AUTONOMOUS_MODE`. Este modo no autoriza por
sí mismo publicación externa, gasto publicitario, login en KDP ni publicación social: esas acciones
siguen sujetas a la política de aprobación del propietario.

Un documento adjunto debe llevar `project_id` al inicio del caption y no puede superar 20 MB. Los
botones de aprobación llevan solo el id opaco de la aprobación; el token secreto nunca se envía en
el callback. Aprobar cambia el estado a `approved`; `/publicar` vuelve a comprobar el hash exacto,
consume la aprobación una sola vez y deja el release en `assisted`. La acción externa KDP no se
ejecuta automáticamente.

## Documentos, derechos y release

El módulo conserva fuente Markdown editable, EPUB, PDF interior básico, render HTML, metadatos,
checklist KDP y ZIP con `MANIFEST.json`. EPUB y PDF tienen validación estructural local; el PDF no
pretende ser una preflight tipográfica profesional. La auditoría comprueba archivos, hashes,
formatos, derechos y hallazgos bloqueantes antes de marcar un candidato como aprobado.

La preparación KDP distingue `prepared` y `assisted` de `published`. El propietario todavía debe
revisar identidad, territorios, precio, páginas y cubierta, y completar login, MFA, CAPTCHA y envío.
No se realizan pruebas de publicación real.

## Marketing y conectores

El calendario social es durable, exportable y requiere aprobación del propietario antes de publicar.
Existe un conector opcional real para Mastodon: solo usa `MASTODON_BASE_URL` y
`MASTODON_ACCESS_TOKEN` en memoria, llama `POST /api/v1/statuses` y solo registra `published` si la
respuesta remota contiene un id. Sin esas variables, el estado es exportación asistida y no se
simula una publicación.

## Vista, seguridad y recuperación

Con el usuario del oficio autenticado, `/editorial` muestra proyectos, tareas, runs, revisiones,
artefactos, aprobaciones, hallazgos, capacidades, costos y conectores. Los endpoints son de sesión,
los secretos no aparecen en el snapshot, y la ruta de artefactos valida el registro privado antes de
leerlo. El almacenamiento usa directorios privados, escritura temporal + rename y hashes.

El respaldo copia SQLite y `files/` después de checkpoint WAL y crea `BACKUP-MANIFEST.json`. Se puede
restaurar en un directorio separado para verificar sin tocar la instalación activa; `--force` es
obligatorio para reemplazar un destino que ya tenga base.

## Matriz de capacidades actual

| Capacidad | Estado sin credenciales | Evidencia o límite |
| --- | --- | --- |
| Estado, revisiones, canon aislado | Implementada | SQLite + invalidación por revisión |
| Cola durable, leases, checkpoints | Implementada | Reserva y recuperación transaccional |
| Once perfiles y adaptador Agent Office | Implementada / requiere floor para despachar | El proveedor editorial local sigue siendo la autoridad del resultado |
| Texto e imagen real | Requiere configuración | Adaptadores HTTP; no hay llamada real en pruebas |
| Mock reproducible | Probada localmente | Texto e imagen marcados como mock |
| EPUB | Implementada y validada localmente | Contenedor y archivos requeridos |
| PDF | Limitada y validada localmente | PDF de texto básico, no preflight de imprenta |
| KDP automatizado | No soportada | Se requiere ruta asistida con propietario |
| Calendario social | Implementada | SQLite + exportación |
| Mastodon | Requiere configuración | Solo se confirma publicación con respuesta remota |
| Telegram | Requiere token e id | Polling, deduplicación, callbacks y adjuntos |

## Fuentes oficiales consultadas

- Agent Office: [README](https://github.com/AgentSystemLabs/agent-office), [cómo funciona](https://github.com/AgentSystemLabs/agent-office/blob/main/docs/how-it-works.md), [agentes](https://github.com/AgentSystemLabs/agent-office/blob/main/docs/agents.md).
- [Node.js SQLite](https://nodejs.org/api/sqlite.html).
- [Telegram Bot API](https://core.telegram.org/bots/api), incluyendo `getUpdates`, callbacks y límites de archivos.
- [OpenAI Responses API](https://developers.openai.com/api/reference/resources/responses) y [Images API](https://developers.openai.com/api/reference/resources/images).
- [KDP Help](https://kdp.amazon.com/en_US/help), [preparación de cubierta](https://kdp.amazon.com/en_US/help/topic/G202187850) y [plantillas/calculador](https://kdp.amazon.com/es_ES/cover-templates).
- [Mastodon: publicar un status](https://docs.joinmastodon.org/methods/statuses/) y [guías de API](https://docs.joinmastodon.org/api/guidelines/).

Estas fuentes describen APIs y políticas externas; la matriz anterior es el estado verificable de
esta instalación, no una promesa de capacidades de las plataformas.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@lydell/node-pty": "^1.2.0-beta.15",
"@xterm/addon-serialize": "^0.14.0",
"@xterm/headless": "^6.0.0",
"fflate": "^0.8.3",
"selfsigned": "^5.5.0",
"ws": "^8.21.3"
},
Expand Down
68 changes: 68 additions & 0 deletions src/client/editorial.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Editorial JeyTid</title>
<style>
:root { color-scheme: light; font-family: system-ui, sans-serif; background: #f6f1e8; color: #182033; }
body { margin: 0; padding: 24px; }
main { max-width: 1180px; margin: auto; }
header { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
h1 { margin: 0; font-size: 28px; }
a, button { color: inherit; }
.toolbar { display: flex; gap: 8px; align-items: center; }
button { border: 2px solid #182033; border-radius: 9px; padding: 7px 12px; background: #fff; cursor: pointer; font-weight: 700; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin-top: 20px; }
.card { background: #fff; border: 2px solid #182033; border-radius: 14px; padding: 16px; box-shadow: 4px 4px 0 #182033; }
.card h2 { margin: 0 0 10px; font-size: 16px; }
.muted { color: #5c6273; font-size: 13px; }
ul { padding-left: 20px; margin: 8px 0 0; }
li { margin: 5px 0; overflow-wrap: anywhere; }
code { font-size: 12px; }
.state { display: inline-block; border-radius: 999px; padding: 2px 8px; background: #e7f4dd; font-size: 12px; font-weight: 800; }
.warn { background: #fff1c9; }
.error { background: #ffe0df; }
pre { white-space: pre-wrap; }
</style>
</head>
<body>
<main>
<header><div><h1>📚 Editorial JeyTid</h1><p class="muted">Estado operativo, dependencias, artefactos, aprobaciones y conectores.</p></div><div class="toolbar"><a href="/">← Agent Office</a><button id="refresh">Actualizar</button></div></header>
<section id="status" class="card" style="margin-top:20px">Cargando…</section>
<div id="grid" class="grid"></div>
</main>
<script>
const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]));
const list = (items, render) => items.length ? `<ul>${items.map(render).join('')}</ul>` : '<p class="muted">Sin registros.</p>';
async function load() {
const status = document.getElementById('status');
const grid = document.getElementById('grid');
try {
const response = await fetch('/api/editorial/overview', { headers: { accept: 'application/json' } });
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'No disponible');
const configured = Object.values(data.connectors || {}).filter((value) => value.configured).length;
const mode = data.settings.autonomousMode ? 'autónomo' : 'supervisado';
status.innerHTML = `<strong>Modo:</strong> ${mode} · <strong>Proveedor:</strong> ${esc(data.settings.provider)} · <strong>Conexiones configuradas:</strong> ${configured} · <strong>Costos:</strong> $${Number(data.costs.observedUsd || 0).toFixed(4)} observados / $${Number(data.costs.estimatedUsd || 0).toFixed(4)} estimados`;
grid.innerHTML = [
`<article class="card"><h2>Libros</h2>${list(data.projects || [], (item) => `<li><strong>${esc(item.title)}</strong><br><code>${esc(item.id)}</code> · <span class="state">${esc(item.stage)}</span></li>`)}</article>`,
`<article class="card"><h2>Tareas y dependencias</h2>${list(data.tasks || [], (item) => `<li><strong>${esc(item.roleId)}</strong> · <span class="state ${item.state === 'failed' ? 'error' : item.state === 'waiting_user' ? 'warn' : ''}">${esc(item.state)}</span><br>${esc(item.objective)}</li>`)}</article>`,
`<article class="card"><h2>Agentes y runs</h2>${list(data.runs || [], (item) => `<li>${esc(item.roleId)} · ${esc(item.provider)} · ${esc(item.status)}<br><code>${esc(item.runId)}</code></li>`)}</article>`,
`<article class="card"><h2>Aprobaciones</h2>${list(data.approvals || [], (item) => `<li>${esc(item.action)} · ${esc(item.state)}<br><code>${esc(item.subjectId)}</code></li>`)}</article>`,
`<article class="card"><h2>Hallazgos</h2>${list(data.findings || [], (item) => `<li><strong>${esc(item.severity)}</strong> · ${esc(item.title)}<br>${esc(item.detail)}</li>`)}</article>`,
`<article class="card"><h2>Capacidades</h2>${list(data.capabilities || [], (item) => `<li>${esc(item.capability)} · <span class="state ${item.state === 'unsupported' ? 'error' : item.state === 'requires_config' ? 'warn' : ''}">${esc(item.state)}</span>${item.limitation ? `<br><span class="muted">${esc(item.limitation)}</span>` : ''}</li>`)}</article>`,
`<article class="card"><h2>Revisiones y archivos</h2><p><strong>${(data.revisions || []).length}</strong> revisiones · <strong>${(data.artifacts || []).length}</strong> artefactos</p>${list(data.artifacts || [], (item) => `<li>${esc(item.type)} · <code>${esc(item.id)}</code><br><span class="muted">${esc(item.sha256.slice(0, 16))}…</span></li>`)}</article>`,
`<article class="card"><h2>Releases</h2>${list(data.releases || [], (item) => `<li><strong>${esc(item.state)}</strong> · ${esc(item.auditStatus)}<br><code>${esc(item.id)}</code></li>`)}</article>`,
`<article class="card"><h2>Calendario social</h2>${list(data.socialPosts || [], (item) => `<li>${esc(item.scheduledAt || 'sin fecha')} · <span class="state ${item.state === 'published' ? '' : 'warn'}">${esc(item.state)}</span><br>${esc(item.content?.text || '')}</li>`)}</article>`
].join('');
} catch (error) {
status.innerHTML = `<strong>No disponible:</strong> ${esc(error.message)}`;
grid.innerHTML = '';
}
}
document.getElementById('refresh').addEventListener('click', load);
load();
</script>
</body>
</html>
4 changes: 4 additions & 0 deletions src/server/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { tildify } from './building.js';
import { openBrowser } from './browser.js';

const argv = process.argv.slice(2);
if (argv[0] === 'editorial') {
const { editorialCommand } = await import('./editorial/cli.js');
process.exit(await editorialCommand(argv.slice(1)));
}
if (argv[0] === 'prune') {
const { prune } = await import('./prune.js');
process.exit(await prune(argv.slice(1)));
Expand Down
Loading