sagadeck 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,15 +14,22 @@ Ele vem com 11 temas (incluindo estilo Keynote Apple em prata clean e respiráve
14
14
  ## Instalação
15
15
 
16
16
  ```bash
17
- pip install sagadeck
17
+ npm install -g sagadeck
18
18
  ```
19
- Requer **Node.js 18+** e **Chrome ou Edge** instalados (o motor é JavaScript e vem empacotado no pacote Python).
19
+ Requer **Node.js 18+** e **Chrome ou Edge** instalados (o PDF, o PNG e as conferências visuais usam o navegador).
20
+ É tudo Node: não precisa de Python. (O pacote do PyPI parou na 1.5.0.)
21
+
22
+ ## Comece aqui
20
23
 
21
- Quem já vive no Node pode instalar direto, sem Python e sem bundle (vai a fonte + dependências):
22
24
  ```bash
23
- npm install -g sagadeck
25
+ sagadeck studio
24
26
  ```
25
- A IA funciona apontando para um modelrelay ou gateway compatível (`SAGADECK_LLM_URL`, veja abaixo); o `pip install "sagadeck[ia]"` continua sendo o caminho com o modelrelay junto.
27
+ e abra **http://127.0.0.1:3517** no navegador. **Essa é a página do sagadeck**: a biblioteca das suas
28
+ apresentações, o editor (estilo PowerPoint) e o chat com a IA. Não há outra interface para abrir nem para criar.
29
+
30
+ Para usar a IA (gerar apresentações, conversar com o chat), clique em **Configurar IA** no alto da biblioteca (ou
31
+ em **IA desligada**, no editor), escolha o provedor, cole a chave, clique em **Testar** e **Salvar**. Detalhes em
32
+ [IA de verdade](#ia-de-verdade-llm).
26
33
 
27
34
  ## Como usar
28
35
 
@@ -69,64 +76,52 @@ O sagadeck foi projetado para ser usado por humanos e por agentes de IA:
69
76
  2. **Pelo Estúdio Visual**: Execute `sagadeck studio palestra.yaml` para abrir a interface web estilo PowerPoint, onde você pode editar visualmente no canvas 16:9 e conversar com a IA no chat lateral. A IA se auto-corrige e nunca deixa elementos sobrepostos ou fora das margens.
70
77
  3. **Pelo Protocolo MCP**: Execute `sagadeck mcp` para que IDEs agênticos (Cursor, Windsurf, Cline, Roo Code) descubram e invoquem diretamente as ferramentas de criação, leitura, fiscalização e auto-cura de apresentações.
71
78
 
72
- Se o código de uma ferramenta em Python quiser chamar o sagadeck diretamente (sem montar comandos de terminal), há uma API mínima:
73
-
74
- ```python
75
- import sagadeck
76
- sagadeck.autofix("palestra.yaml") # corrige sobreposições e margens automaticamente
77
- sagadeck.studio() # abre a biblioteca (Documentos/sagadeck no Windows)
78
- sagadeck.studio("palestra.yaml", port=3517) # abre direto o editor de um deck
79
- sagadeck.export("palestra.yaml", out="saida") # {'html': …, 'pptx': …, 'pdf': …, 'roteiro': …}
80
- print(sagadeck.check("palestra.yaml")) # relatório do fiscal em texto
81
- contexto = sagadeck.reference() # referência do YAML para colocar no prompt
82
- ```
83
-
84
79
  ## IA de verdade (LLM)
85
80
 
86
- O chat lateral do Studio, o Napkin (texto → slide), o **"✨ Deck com IA"** e a geração de imagens usam um LLM quando há um disponível; sem LLM, o chat e o Napkin continuam funcionando com as regras locais.
87
-
88
- O sagadeck fala com qualquer endpoint compatível com OpenAI (`/v1/chat/completions`). O caminho recomendado é o [modelrelay](https://github.com/naruminho/modelrelay), que decide pela configuração dele para onde as chamadas vão (OpenRouter, OpenAI, gateway corporativo), sem nada disso no sagadeck:
89
-
90
- ```bash
91
- pip install "sagadeck[ia]" # o sagadeck com o modelrelay na versão que ele exige
92
- # ou só o modelrelay: pip install -U modelrelay
93
- ```
94
-
95
- Com um modelrelay mais velho do que o exigido, o sagadeck avisa ao abrir e mostra o comando para atualizar.
96
- Quem desenvolve o modelrelay e o instalou direto do clone (`pip install -e .` na pasta dele, a "instalação
97
- editável": o Python lê o código da pasta, sem cópia) atualiza só com `git pull` ali, e o aviso diz isso, com a
98
- pasta. `pip install -e .` de novo só quando o modelrelay ganhar uma dependência nova.
81
+ O chat lateral do Studio, o Napkin (texto → slide), o **"Deck com IA"** e a geração de imagens usam um LLM quando há um configurado; sem ele, o chat e o Napkin continuam funcionando com as regras locais.
99
82
 
100
- O jeito mais fácil de configurar é pela tela: abra `sagadeck studio` e clique em **modelrelay** no alto da
101
- biblioteca (ou abra http://127.0.0.1:8765/ com um `modelrelay serve` rodando). Lá você escolhe o provedor
102
- (OpenRouter, OpenAI, Google, DeepSeek, o gateway da empresa…), cola a chave, testa e diz qual modelo faz
103
- **texto** e qual faz **imagem**. A tela grava o `~/.modelrelay/config.toml`, que também dá para editar à mão
104
- (`modelrelay init` cria um modelo comentado). O botão só aparece quando o modelrelay roda nesta máquina; no
105
- modo multiusuário, quem configura é o admin.
83
+ O sagadeck fala direto com qualquer provedor compatível com a API da OpenAI (`/v1/chat/completions`): OpenRouter,
84
+ OpenAI, o proxy de IA da empresa. Não há serviço à parte para instalar nem para manter rodando.
106
85
 
107
- À mão, no `~/.modelrelay/config.toml`, os apelidos que o sagadeck usa são:
86
+ **Pela tela** (o jeito mais fácil): **Configurar IA**, no alto da biblioteca, ou **IA desligada / IA ligada**, no
87
+ editor. Escolha o provedor (OpenRouter, OpenAI ou "outro compatível", com o endereço), cole a chave e diga o modelo
88
+ de cada papel:
108
89
 
109
- ```toml
110
- [models]
111
- "text" = "google/gemini-2.5-flash" # chat, Napkin e geração de deck
112
- "image" = "google/gemini-2.5-flash-image" # imagens
90
+ | papel | para quê | |
91
+ |---|---|---|
92
+ | texto | chat, Napkin, geração e revisão de decks | obrigatório |
93
+ | visão | ver o slide renderizado e as imagens coladas | opcional; vazio = o de texto |
94
+ | imagem | ilustrações (`image_prompt`) | opcional |
95
+ | busca | pesquisa na web da geração (no OpenRouter, um modelo `:online`) | opcional; sem ele, buscadores diretos |
96
+
97
+ **Testar** faz uma pergunta curta com o que está na tela antes de salvar. A configuração fica só nesta máquina, em
98
+ `~/.sagadeck/ia.json` (fora do código e fora da biblioteca, que pode sincronizar com a nuvem); a chave nunca volta
99
+ inteira para a página. No modo multiusuário, quem configura é quem administra o servidor.
100
+
101
+ **À mão**, o mesmo arquivo:
102
+
103
+ ```json
104
+ {
105
+ "provider": "openrouter",
106
+ "url": "https://openrouter.ai/api/v1",
107
+ "keyEnv": "OPENROUTER_API_KEY",
108
+ "models": { "text": "deepseek/deepseek-v4.1-flash", "image": "google/gemini-3.1-flash-image" }
109
+ }
113
110
  ```
111
+ A chave vai em `key` ou numa variável de ambiente indicada em `keyEnv`. `headers` acrescenta cabeçalhos fixos.
114
112
 
115
- O sagadeck se identifica para o modelrelay (cabeçalho `X-Modelrelay-App: sagadeck`), então dá para usar
116
- modelos diferentes só nele, sem afetar outros apps que usam o mesmo modelrelay:
113
+ **Provedor que não segue a API da OpenAI** (outro jeito de autenticar, outro endereço): em vez de mexer no sagadeck,
114
+ aponte `"adaptador"` para um arquivo `.mjs` seu cujo `export default` recebe `(url, init)` e devolve a resposta (como
115
+ o `fetch`). O resto do pedido segue no formato da OpenAI.
117
116
 
118
- ```toml
119
- [apps.sagadeck.models] # só o que muda para o sagadeck; o resto vem de [models]
120
- "text" = "deepseek/deepseek-v4-flash"
121
- ```
122
-
123
- Confira com `modelrelay show --app sagadeck`. Detalhes na seção *Per-app models* do README do modelrelay.
117
+ **Quem usava o modelrelay**: na primeira vez, o sagadeck traz a configuração dele (`~/.modelrelay/config.toml`:
118
+ provedor, chave e os modelos do sagadeck) para o `~/.sagadeck/ia.json`. Depois disso o modelrelay não é mais usado.
124
119
 
125
120
  **Modelo sem visão** (ex.: DeepSeek V4 Flash): o assistente manda uma foto do slide e as imagens que você cola.
126
121
  Se o modelo recusar imagem, o sagadeck refaz o pedido sem as imagens, avisa na resposta ("não enxerga imagens")
127
122
  e não insiste nesse modelo até reiniciar. Tudo funciona, só que a IA não vê o slide renderizado.
128
123
 
129
- Pronto: com o modelrelay instalado no mesmo Python, `sagadeck studio`, `new`, `napkin` e `imagens` sobem um `modelrelay serve` sozinhos enquanto rodam. Rodando o motor Node direto (`node bin/sagadeck.js`), deixe um `modelrelay serve` aberto em outro terminal.
124
+ Na linha de comando, `new`, `napkin` e `imagens` usam a mesma configuração:
130
125
 
131
126
  ```bash
132
127
  sagadeck new palestra --prompt "Palestra de 15 min para gerentes sobre IA com segurança. Ilustre onde fizer sentido." --slides=10
@@ -134,14 +129,15 @@ sagadeck napkin "1) cliente abre chamado 2) triagem por IA 3) analista revisa"
134
129
  sagadeck imagens palestra.yaml # gera as imagens pedidas com image_prompt: no YAML
135
130
  ```
136
131
 
137
- | variável | padrão | |
138
- |---|---|---|
139
- | `SAGADECK_LLM_URL` | `http://127.0.0.1:8765/v1` | qualquer API compatível com OpenAI (ex.: `https://openrouter.ai/api/v1`) |
140
- | `SAGADECK_LLM_KEY` | — | bearer token, se apontar direto para um provedor |
141
- | `SAGADECK_TEXT_MODEL` / `SAGADECK_IMAGE_MODEL` | `text` / `image` | nomes dos modelos |
142
- | `SAGADECK_LLM_TIMEOUT` | `180` | segundos por chamada |
143
- | `SAGADECK_NO_RELAY` | — | `1` impede o pacote Python de subir o modelrelay |
144
- | `SAGADECK_APP` | `sagadeck` | nome com que o sagadeck se identifica ao modelrelay (`[apps.<nome>.models]`) |
132
+ As variáveis abaixo valem por cima da configuração (útil em servidor e em testes):
133
+
134
+ | variável | |
135
+ |---|---|
136
+ | `SAGADECK_LLM_URL` | qualquer API compatível com OpenAI (ex.: `https://openrouter.ai/api/v1`) |
137
+ | `SAGADECK_LLM_KEY` | a chave (bearer token) |
138
+ | `SAGADECK_TEXT_MODEL` / `SAGADECK_VISION_MODEL` / `SAGADECK_IMAGE_MODEL` / `SAGADECK_SEARCH_MODEL` | os modelos de cada papel |
139
+ | `SAGADECK_LLM_TIMEOUT` | segundos por chamada (padrão `180`) |
140
+ | `SAGADECK_IA` | outro arquivo de configuração (padrão `~/.sagadeck/ia.json`) |
145
141
 
146
142
  **Imagens geradas: você pede no texto**, no chat, no briefing do "Deck com IA" ou no `--prompt`; não há caixa para marcar.
147
143
  "Com fotos em todos os slides" ilustra todos; "você decide onde ilustrar" deixa a IA escolher só os slides em que uma imagem
@@ -156,15 +152,13 @@ Todo YAML vindo do LLM é validado (renderiza cada slide); se falhar, o erro vol
156
152
  ```bash
157
153
  npm install
158
154
  node bin/sagadeck.js build templates/exemplo.yaml
159
- npm run bundle # empacota o motor em python/sagadeck/engine
160
- npm run build:py # gera dist/*.whl e dist/*.tar.gz
161
155
  npm test # suíte inteira: motor + runtime + Studio (clicando num Chrome headless)
162
156
  npm run test:unit # só o motor (rápido, sem navegador)
163
- SAGADECK_LIVE=1 npm test # inclui os testes que chamam o LLM de verdade (modelrelay)
157
+ SAGADECK_LIVE=1 npm test # inclui os testes que chamam o LLM de verdade (o configurado em Configurar IA)
164
158
  ```
165
159
  **Toda funcionalidade nova entra com teste** em `test/` — é o que garante que um refactor não apague o que já funciona. Veja [CLAUDE.md](CLAUDE.md).
166
160
 
167
- Publicação: crie uma release `vX.Y.Z` no GitHub (o workflow `.github/workflows/publish.yml` publica no PyPI via *trusted publishing*). A versão fica em `package.json` e `python/sagadeck/__init__.py` (o bundle confere se são iguais).
161
+ Publicação: com a CI verde, crie uma release `vX.Y.Z` no GitHub; o workflow `.github/workflows/publish.yml` publica no npm (*trusted publishing*). A versão fica em `package.json`.
168
162
 
169
163
  ## Arquivo `.sagadeck` (a apresentação inteira)
170
164
 
@@ -193,7 +187,7 @@ formato avisa para atualizar o sagadeck. Se um filtro de e-mail barrar a extens
193
187
  ## Biblioteca
194
188
 
195
189
  `sagadeck studio` sem arquivo abre a biblioteca: suas apresentações organizadas em tópicos, com capa, busca,
196
- lixeira (um dia) e o menu **⋯** de cada cartão (apresentar, renomear, duplicar, mover, baixar tudo/.sagadeck/PPTX/PDF).
190
+ lixeira (30 dias; o prazo se muda em Preferências) e o menu **⋯** de cada cartão (apresentar, renomear, duplicar, mover, baixar tudo/.sagadeck/PPTX/PDF).
197
191
 
198
192
  **Nova apresentação** tem três caminhos: *Descrever com IA* (o assunto, quanto tempo você tem, que vira o número de
199
193
  slides, o estilo e, se quiser, material de apoio: PDF, Word, PowerPoint, Excel ou um link), *A partir de um arquivo
@@ -301,8 +295,6 @@ src/export/ PowerPoint, PDF, imagens, roteiro e o fiscal
301
295
  templates/exemplo.yaml deck de exemplo com todos os layouts
302
296
  docs/REFERENCIA.md referência completa do YAML
303
297
  SKILL.md instruções para IAs gerarem decks com o sagadeck
304
- python/sagadeck/ pacote Python (CLI + API) que carrega o motor empacotado
305
- scripts/bundle.mjs empacota o motor para o pip
306
298
  tools/ppt-render.ps1 renderiza um .pptx pelo PowerPoint (para conferir fidelidade)
307
299
  tools/test-live.mjs teste automático do modo apresentação e dos widgets
308
300
  ```
package/SKILL.md CHANGED
@@ -7,15 +7,30 @@ description: Gera apresentações bonitas (HTML animado + PowerPoint editável +
7
7
 
8
8
  O sagadeck gera apresentações profissionais a partir de um arquivo YAML, oferecendo tanto linha de comando (CLI), estúdio web visual no estilo PowerPoint, quanto protocolo MCP para integração com IDEs agênticos (Claude Code, Cursor, Windsurf, Cline, Roo Code).
9
9
 
10
+ ## Para agentes: "deixar pronto para usar"
11
+
12
+ Quando a pessoa pede para instalar o sagadeck e deixar pronto:
13
+
14
+ 1. `npm install -g sagadeck` (Node 18+; não precisa de Python).
15
+ 2. `sagadeck studio` e diga para ela abrir **http://127.0.0.1:3517** (a primeira linha que o comando escreve é
16
+ esse endereço). **O Studio é a interface do sagadeck**: biblioteca, editor e chat com IA.
17
+ 3. **Nunca crie uma página, um servidor ou uma interface própria** para o sagadeck, nem "suba as APIs" à mão: tudo
18
+ que a pessoa precisa está no Studio.
19
+ 4. Para a IA, mostre onde configurar: **Configurar IA** no alto da biblioteca (ou **IA desligada** no editor):
20
+ provedor, chave, Testar e Salvar. A configuração fica em `~/.sagadeck/ia.json`; nunca grave a chave no código nem
21
+ na pasta das apresentações.
22
+ 5. As apresentações moram na biblioteca (`SAGADECK_HOME`, senão Documentos\sagadeck no Windows e `~/sagadeck` fora
23
+ dele). Não crie decks em outra pasta.
24
+
10
25
  ## Comandos Disponíveis
11
26
 
12
27
  | comando | o que faz |
13
28
  |---|---|
14
- | `sagadeck studio [deck.yaml] [--port=3000]` | **Abre o estúdio web estilo PowerPoint** com canvas visual 16:9, edição WYSIWYG direta, detector de sobreposição em tempo real e **chat lateral com IA**. |
29
+ | `sagadeck studio [deck.yaml] [--port=3517]` | **Abre o estúdio web estilo PowerPoint** com canvas visual 16:9, edição WYSIWYG direta, detector de sobreposição em tempo real e **chat lateral com IA**. |
15
30
  | `sagadeck autofix <deck.yaml>` | **Auto-cura do layout**: detecta e repara automaticamente sobreposição entre elementos, elementos fora das margens seguras (120px) e excesso de texto. |
16
31
  | `sagadeck search "<termo>"` | **Pesquisa web sem bloqueio (DuckDuckGo)**: busca dados reais, números e referências confiáveis para enriquecer os slides. |
17
32
  | `sagadeck mcp` | **Servidor MCP**: expõe ferramentas padronizadas (JSON-RPC) para IDEs agênticos criarem, lerem, pesquisarem, fiscalizarem e editarem decks. |
18
- | `sagadeck new deck.yaml [--theme=prata]` | cria um deck de exemplo estruturado |
33
+ | `sagadeck new "<nome>" [--topic=T] [--theme=prata]` | cria um deck de exemplo na biblioteca |
19
34
  | `sagadeck build deck.yaml` | compila `deck.html` standalone (abre no navegador; tecla P = modo apresentador) |
20
35
  | `sagadeck check deck.yaml` | fiscal: texto estourado, sobreposição, contraste, fonte pequena, excesso de texto |
21
36
  | `sagadeck pptx deck.yaml` | gera `deck.pptx` **totalmente editável** no PowerPoint com formas e caixas nativas |
@@ -99,14 +114,6 @@ O agente no terminal deve seguir o fluxo:
99
114
  3. Rodar `sagadeck check deck.yaml` para confirmar ausência de avisos.
100
115
  4. Rodar `sagadeck all deck.yaml` para gerar todas as entregas.
101
116
 
102
- ### 3. Via API Python
103
- ```python
104
- import sagadeck
105
- sagadeck.autofix("palestra.yaml")
106
- sagadeck.build("palestra.yaml")
107
- sagadeck.studio("palestra.yaml", port=3000)
108
- ```
109
-
110
117
  ---
111
118
 
112
119
  ## Decisão de Arquitetura: Mesmo Repositório vs. Repositório Separado
@@ -114,7 +121,7 @@ sagadeck.studio("palestra.yaml", port=3000)
114
121
  ### Por que o Estúdio deve ficar no **mesmo repositório** (`sagadeck`):
115
122
  1. **Fonte Única da Verdade**: Os layouts (incluindo `stats` para KPIs e `steps` para processos), os 11 temas visuais (incluindo `prata` estilo Keynote Apple, `rabisco` artesanal pintado à mão, `oceano` azul elétrico vivo, `pop` alegre/chiclete e `aurora` neon), as regras de tipografia e as rotinas de verificação geométrica residem no mesmo código (`src/layouts.js`, `src/themes.js`, `src/fiscal/autofix.js`). Qualquer alteração ou novo layout adicionado ao compilador fica disponível **instantaneamente** no estúdio visual.
116
123
  2. **Zero Descompasso de Versões**: Em repositórios separados, quando o formato YAML ou os temas do SagaDeck evoluem, a interface visual quebra ou fica desatualizada até que alguém publique e atualize dependências externas.
117
- 3. **Experiência de Uso (DX) Imediata**: O desenvolvedor ou agente clona o repositório ou roda `pip install sagadeck` / `npm install` e tem **tudo num só lugar**: linha de comando (`sagadeck build`), estúdio visual (`sagadeck studio`), servidor de agentes (`sagadeck mcp`) e fiscal (`sagadeck check`).
124
+ 3. **Experiência de Uso (DX) Imediata**: O desenvolvedor ou agente clona o repositório ou roda `npm install -g sagadeck` e tem **tudo num só lugar**: linha de comando (`sagadeck build`), estúdio visual (`sagadeck studio`), servidor de agentes (`sagadeck mcp`) e fiscal (`sagadeck check`).
118
125
  4. **Leve e Modular**: A interface web foi construída com tecnologias nativas leves (Vanilla JS, CSS moderno e servidor HTTP embutido do Node.js sem frameworks pesados). Isso mantém o pacote pequeno (~1.1 MB empacotado) sem impactar o desempenho do compilador.
119
126
 
120
127
  ---
package/bin/sagadeck.js CHANGED
@@ -394,22 +394,15 @@ async function main() {
394
394
  process.exit(1);
395
395
  }
396
396
  const agentUsers = typeof flags.agentes === "string" ? flags.agentes.split(",").map((x) => x.trim()).filter(Boolean) : undefined;
397
- // IA: sobe o `modelrelay serve` junto, se ele estiver instalado e ninguém escutar na porta dele (como o sagadeck do pip)
398
- const { startRelay } = await import("../src/ai/relay.js");
399
- const relay = multiuser ? null : await startRelay();
400
- if (relay) {
401
- console.log(`✓ IA: modelrelay em ${relay.url} (configuração: http://127.0.0.1:8765/)`);
402
- for (const sig of ["exit", "SIGINT", "SIGTERM"]) process.on(sig, () => { relay.stop(); if (sig !== "exit") process.exit(0); });
403
- }
404
397
  const server = createStudioServer(deckFile, { port, host, library, multiuser, userHeader: flags["user-header"], agentUsers });
405
398
  server.on("error", (e) => {
406
399
  if (e.code !== "EADDRINUSE") throw e;
407
400
  console.error(`A porta ${port} está ocupada por outro programa. Use --port=OUTRA.`);
408
401
  process.exit(1);
409
402
  });
410
- server.listen(port, host, () => {
411
- console.log(`✓ SagaDeck Studio ${VERSION} em http://${shownHost}:${port}${deckFile ? "" : " (biblioteca)"} (processo ${process.pid})`);
412
- console.log(multiuser ? ` multiusuário: bibliotecas em ${path.join(library, "usuarios")}` : ` biblioteca: ${library}`);
403
+ server.listen(port, host, async () => {
404
+ const { studioBanner } = await import("../src/studio/banner.js");
405
+ for (const line of studioBanner({ version: VERSION, url: `http://${shownHost}:${port}/`, library, ai: (await import("../src/ai/llm.js")).llmConfig(), multiuser, usersDir: path.join(library, "usuarios"), isLibrary: !deckFile })) console.log(line);
413
406
  });
414
407
  break;
415
408
  }
package/docs/ANDAMENTO.md CHANGED
@@ -12,7 +12,7 @@ começar. Trabalho em curso vai para o GitHub (commit + push) a cada etapa, nunc
12
12
 
13
13
  **Para quem pega isto numa sessão nova, sem a conversa de origem.** O projeto é o **sagadeck**:
14
14
  - Repositório GitHub `naruminho/sagadeck`, clonado em `C:\Users\narum\src\sagadeck`, branch `main`.
15
- - Publicado no npm como `sagadeck` (1.5.0, com as mudanças de 08 e 09/10).
15
+ - Publicado no npm como `sagadeck` (1.6.0: IA embutida, só npm; o PyPI parou na 1.5.0).
16
16
  - Ferramenta Node (ESM) que transforma apresentações escritas em YAML em HTML/PPTX.
17
17
  - Tem um editor web, o Studio (`sagadeck studio`, porta 3517).
18
18
  - Tem uma IA (via modelrelay em `http://127.0.0.1:8765/v1`) que cria e edita decks, inclusive a partir de PDFs de
@@ -35,13 +35,43 @@ datadas abaixo). O resto fica para depois e só entra se for pedido. Antes de co
35
35
 
36
36
  **OneDrive (a biblioteca agora mora em `C:\Users\narum\OneDrive\Documents\sagadeck` e sincroniza)**
37
37
 
38
- 2. **Cache fora da nuvem.** A lixeira passou a durar um dia (seção "Lixeira de um dia" abaixo). Sobram `.cache`
38
+ 2. **Cache fora da nuvem.** O prazo da lixeira virou preferência (seção "Lixeira com prazo nas Preferências"). Sobram `.cache`
39
39
  (5 MB) e `.historico` indo para o OneDrive; só pesa se a pasta Documentos estiver no OneDrive (comum no Windows 11
40
40
  e em empresa, mas não universal). O histórico de versões talvez deva continuar junto do deck (portável).
41
41
  3. **Arquivos só na nuvem (Files On-Demand).** Com espaço liberado, o OneDrive deixa só o marcador do arquivo, e
42
42
  ler o conteúdo força o download. Conferir se listar a biblioteca (`openLibrary`, `/api/library`) lê o YAML de
43
43
  cada deck; se lê, listar só pelo nome/metadado e abrir o conteúdo sob demanda.
44
44
 
45
+ ## CI verde de novo (e a regra: merge só com a CI verde) — 09/10/2026
46
+
47
+ A CI do GitHub estava vermelha havia dias (desde antes da 1.4.0) e 13 PRs foram mergeados e a 1.5.0 publicada assim.
48
+ Regra nova no CLAUDE.md: merge e release só com a CI verde nas duas máquinas. As falhas, uma por uma:
49
+
50
+ - **Python 3.12** (Ubuntu): escape inválido numa docstring; corrigido antes da 1.5.0 (seção abaixo).
51
+ - **Cópia de mídia entre apresentações** (Windows): defeito real. `copySlideAssets` comparava o caminho real do
52
+ arquivo com o caminho da pasta sem resolver; deck aberto por junção (o `C:\Users\narum\sagadeck` antigo é uma) ou
53
+ por nome curto do Windows (`RUNNER~1`, a pasta temporária da CI) recusava toda mídia como "fora da apresentação".
54
+ Compara os dois resolvidos. Teste com uma junção de verdade (`test/studio-chat-jobs.test.js`).
55
+ - **Animações paradas** (Windows): o Windows Server vem com "mostrar animações" desligado, o Chrome informa
56
+ `prefers-reduced-motion: reduce` e as cenas ficam paradas, de propósito. Os testes herdavam a preferência da máquina:
57
+ `newPage` (`test/helpers.js`) fixa "sem preferência", e quem testa o movimento reduzido pede `reducedMotion: "reduce"`.
58
+ - **Defeito real achado no caminho**: com o sistema pedindo movimento reduzido, a cena às vezes animava sem parar. O
59
+ iframe dizia "ready" antes de o script da página escutar; a página nunca respondia (nem "slide atual", nem "movimento
60
+ reduzido"). A página agora fala com cada cena assim que se liga a ela (`src/runtime/motion.js`) e a cena acompanha a
61
+ mudança da preferência (`src/motion.js`). Teste novo: sistema com movimento reduzido deixa o holograma parado
62
+ (falhava 3 em 4 sem a correção; 5 em 5 com).
63
+ - **"Parar a transformação"** (os dois): o teste esperava um texto do aviso de progresso para recarregar a página "no
64
+ meio". Agora espera o modelo (falso) ter recebido o pedido de escrever os slides.
65
+ - **Animações paradas no Windows da CI, a causa de verdade** (achada com o diagnóstico que os testes passaram a
66
+ imprimir, inclusive o estado interno da cena, `window.__sagaMotion`): na CI a cena ficava com "parada" ligado para
67
+ sempre, embora a preferência dissesse "sem redução": ouviu "movimento reduzido" no carregamento (do sistema, antes de
68
+ a preferência do teste valer) e o código só sabia ligar esse estado, nunca desligar. Defeito real: religar as
69
+ animações com a apresentação aberta não descongelava nada. Agora "parada" se recalcula (deck `motion: none`, página
70
+ pedindo, preferência do sistema) sempre que um deles muda, nos dois sentidos; a página sempre diz sim ou não e repete
71
+ quando a preferência muda. Teste: movimento reduzido que deixa de valer faz a cena voltar a animar (falhava antes).
72
+ - **Baixar PowerPoint sem as notas** (Windows): o clique esperava a "navegação" do download, e o PPTX numa máquina
73
+ lenta passa do limite do clique; o teste só precisa do pedido (`noWaitAfter`).
74
+
45
75
  ## Pacote Python: escape inválido numa docstring — 09/10/2026
46
76
 
47
77
  Antes da release 1.5.0. `python/sagadeck/api.py` tinha "Documentos\sagadeck" numa docstring: `\s` é escape inválido,
@@ -49,6 +79,18 @@ Antes da release 1.5.0. `python/sagadeck/api.py` tinha "Documentos\sagadeck" num
49
79
  Virou "Documentos/sagadeck". Teste novo em `test/python-relay.test.js`: compila todo o pacote com `-W error` (qualquer
50
80
  Python; falhava antes).
51
81
 
82
+ ## Lixeira com prazo nas Preferências (padrão 30 dias) — 09/10/2026
83
+
84
+ A "Lixeira de um dia" (abaixo) foi pedida pelos 407 MB de decks de teste numa máquina, mas saiu na 1.5.0 para todo
85
+ mundo: quem atualizasse perdia de vez, no primeiro uso, o que estava na lixeira havia mais de um dia. Agora o prazo é
86
+ uma preferência da máquina: **Preferências › Biblioteca › Quanto tempo a lixeira guarda** (1, 7, 30 ou 90 dias;
87
+ `biblioteca.lixeiraDias`, padrão 30). `openLibrary` lê o prazo a cada limpeza (`trashDays` só nos testes) e
88
+ `/api/library` devolve `trashDays`, que a tela da lixeira e o aviso ao excluir mostram.
89
+
90
+ - Testes: `test/library.test.js` (com 1 dia, anteontem some do disco e a de uma hora fica; sem escolha, 30 dias) e
91
+ `test/studio.test.js` (escolher "1 dia" nas Preferências grava no arquivo, a biblioteca passa a usar e a dizer "Fica
92
+ aqui por um dia"; falha sem o recurso).
93
+
52
94
  ## Crítica acionável: cada ponto vira um cartão com Aplicar e Ignorar — 09/10/2026
53
95
 
54
96
  Item 7 das próximas frentes. A leitura crítica (botão do chat) chegava como um texto único, com opções gerais ("leve
@@ -66,6 +108,34 @@ os achados para os slides").
66
108
  `missed`, elas entram no inventário com a caixa exata do texto e passam pela mesma releitura do recorte
67
109
  (`transcribeEquation`) que escreve o LaTeX das outras. Saiu da lista.
68
110
 
111
+ ## IA embutida, só npm e o Studio como a página do sagadeck — 09/10/2026
112
+
113
+ Pedido de Narumi, depois de um agente (Copilot) instalar o sagadeck, abrir a página do modelrelay e, sem achar a
114
+ interface, criar uma do zero: deixar claro que a página é o Studio; tirar o pip (o pacote Python só chamava o Node);
115
+ e tirar o modelrelay (Python, outro repositório, versão para sincronizar), mantendo uma camada separada onde a pessoa
116
+ põe a chave e o modelo sem mexer no código.
117
+
118
+ - **IA embutida** (`src/ai/ia-config.js`): provedor, chave e modelos desta máquina em `~/.sagadeck/ia.json`
119
+ (`SAGADECK_IA`), fora do código e da biblioteca. Chave no arquivo ou numa variável (`keyEnv`); papéis `text`,
120
+ `vision`, `image`, `search` viram os modelos configurados (`resolveModel`); `headers` fixos; e `adaptador`: um `.mjs`
121
+ da pessoa cujo default `(url, init) => Response` faz o envio (provedor fora do padrão da OpenAI, sem código no
122
+ sagadeck). Variáveis `SAGADECK_LLM_*` valem por cima (servidor, testes). Sem nada configurado, a IA fica desligada.
123
+ - **Migração**: na primeira vez, o `~/.modelrelay/config.toml` vira o `ia.json` (provedor padrão, chave ou
124
+ `api_key_env`, `[models]` com `[apps.sagadeck.models]` por cima); não sobrescreve nada.
125
+ - **Configurar IA** (`src/studio/public/ai-settings.js`, `src/studio/ia-routes.js`): mesma tela na biblioteca (botão
126
+ Configurar IA) e no editor (IA desligada/ligada): provedor, endereço, chave (nunca volta inteira: `••••1234`),
127
+ modelos, **Testar** (pergunta curta com o que está na tela) e **Salvar**. No multiusuário não aparece e não grava.
128
+ - **Saiu**: `src/ai/relay.js` (subir o `modelrelay serve`), o cabeçalho `X-Modelrelay-App`, a rota `/api/ai/setup`,
129
+ a pasta `python/`, o `pyproject.toml`, `scripts/bundle.mjs` e os testes deles; a publicação vai só para o npm.
130
+ - **O Studio é a página** (`src/studio/banner.js`): a primeira linha do `sagadeck studio` é "Abra no navegador:
131
+ http://127.0.0.1:3517"; a IA aparece depois (provedor e modelo, ou onde configurar). README com "Comece aqui";
132
+ SKILL.md com "Para agentes: deixar pronto para usar" (Studio, nunca criar interface própria, Configurar IA) e a
133
+ porta certa (dizia 3000).
134
+ - Testes: `test/ia-config.test.js` (migração com o config.toml real, chave mascarada, llmConfig e papéis),
135
+ `test/ai.test.js` (chamada direta com a chave e o modelo configurados; adaptador faz o envio),
136
+ `test/library-ui.test.js` (Configurar IA de ponta a ponta: testa, salva, a chave não volta, multiusuário bloqueia),
137
+ `test/instance.test.js` (primeira linha do `sagadeck studio`; SKILL.md com porta, npm e a regra).
138
+
69
139
  ## Studio avisa quando o código mudou por baixo dele — 09/10/2026
70
140
 
71
141
  Continuação do incidente "nenhum slide abre" de 08/10: um Studio aberto antes de uma atualização segue com o servidor
@@ -926,14 +926,14 @@ específica; a pessoa pode completar a descrição e enviar. Também é possíve
926
926
  a IA gerar** e clicar em **Gerar imagem agora**, inclusive para substituir uma foto existente.
927
927
  O chat aceita pedidos como “gere uma foto com base no conteúdo deste slide” ou uma descrição visual própria.
928
928
  Ao trocar uma imagem, preserve `x`, `y`, `w`, `h`, `fit`, `radius` e os demais elementos; use `image_prompt`
929
- no elemento correto. A geração exige um provedor de imagens configurado no modelrelay. As imagens geradas
929
+ no elemento correto. A geração exige um modelo de imagem em Configurar IA. As imagens geradas
930
930
  ficam na pasta `imagens/` da apresentação.
931
931
 
932
932
  ## Ajustes diretos no Studio
933
933
 
934
934
  Em **Inserir → Neste slide**: Texto, Formas (menu com 12 desenhos: retângulo, arredondado, elipse, pílula, linha, triângulo, losango, hexágono, estrela, seta, chevron, balão), Imagem e Ícone adicionam elementos sem trocar o layout. Um clique em qualquer ponto de um objeto (texto, forma, imagem) o seleciona; clicar de novo no texto selecionado (ou duplo clique) escreve. Arrastar numa área vazia desenha um retângulo que seleciona vários; Shift+clique soma. Selecionados, dá para arrastar juntos, mover com as setas, redimensionar pela alça (um só) e usar a barra que flutua acima da seleção: tamanho e cor do texto, preenchimento da forma (`visualEdits.fill`), trazer para frente, enviar para trás (fica atrás dos outros objetos, nunca do fundo), excluir, alinhar (esquerda, centro, direita, em cima, meio, embaixo: vários entre si; um só, no slide), distribuir (3 ou mais, mesmo espaço), copiar e colar estilo (Ctrl+Alt+C / Ctrl+Alt+V), travar (o clique atravessa) e, numa imagem, trocar a imagem (`visualEdits.image`, com `fit: cover|contain` e `focus`: center, top, bottom, left, right, top left…). Ao arrastar, guias grudam nas bordas e no centro do slide e dos outros objetos (Alt solta). Na aba Propriedades, **Objetos do slide** lista tudo o que está no slide: seleciona até o travado, oculta/mostra e trava/destrava (`locked`; a decoração que veio do PowerPoint, `deco: true`, já vem travada). Tab/Shift+Tab passam de um objeto para outro; Escape solta. A aba **Propriedades** do painel lateral é o inspetor (como o Object Inspector): mostra só as categorias que fazem sentido para o que está selecionado (Posição e tamanho; Texto; Preenchimento e contorno; Aparência; Animação; Avançado) e, sempre, as do Slide e da Apresentação. Os ajustes de objeto ficam em `visualEdits` (`dx, dy, w, h, rotate, z, size, color, weight, italic, align, lineHeight, letterSpacing, uppercase, fill, stroke, strokeWidth, radius, opacity, shadow: suave|forte, step, anim, hidden, locked, image, fit, focus`); não os invente ao criar um deck. Delete/Backspace excluem o objeto selecionado (fica em `visualEdits` como `hidden`, recuperável); Ctrl+Z / Ctrl+Y desfazem e refazem qualquer mudança da apresentação (a da tela, a do formulário, a do assistente), com os botões ao lado do título. Duplo clique permite voltar a escrever. Os ajustes são persistidos em `visualEdits`, com chaves geradas pelo renderizador; não invente essas chaves ao criar um deck. Prefira alterar os campos semânticos. A exclusão visual usa `hidden` para preservar conteúdo recuperável. Mudanças estruturais grandes podem invalidar ajustes: remova `visualEdits` ao reconstruir um slide.
935
935
 
936
- `Arrumar layout` (aba Revisar) é local e baseado em regras: corrige sobreposição, margem, título comprido e texto que não coube, diz o que fez e se desfaz. `Revisar com IA` envia a imagem renderizada do slide ao modelo configurado e solicita melhorias; usa tokens. O status de IA abre a configuração do modelrelay. Um modelo sem visão é identificado na resposta; conectividade com `/models` não comprova que uma geração será aceita pelo provedor.
936
+ `Arrumar layout` (aba Revisar) é local e baseado em regras: corrige sobreposição, margem, título comprido e texto que não coube, diz o que fez e se desfaz. `Revisar com IA` envia a imagem renderizada do slide ao modelo configurado e solicita melhorias; usa tokens. O status de IA abre Configurar IA (provedor, chave e modelos desta máquina). Um modelo sem visão é identificado na resposta; conectividade com `/models` não comprova que uma geração será aceita pelo provedor.
937
937
  O fiscal local também sinaliza texto principal abaixo de 16 px como alerta de leitura; esse aviso é informativo e não reduz a fonte automaticamente.
938
938
 
939
939
  ## Densidade técnica e payloads extensos
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sagadeck",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Gerador de apresentações: YAML -> HTML animado + PowerPoint editável, com temas e figuras geradas na hora.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,8 +37,7 @@
37
37
  },
38
38
  "scripts": {
39
39
  "dev": "node bin/sagadeck.js studio",
40
- "bundle": "node scripts/bundle.mjs",
41
- "build:py": "node scripts/bundle.mjs && python -m build",
40
+ "build:code-lab": "node scripts/bundle-code-lab.mjs",
42
41
  "test:live": "node tools/test-live.mjs",
43
42
  "test": "node --test --test-concurrency=1 \"test/*.test.js\"",
44
43
  "test:unit": "node --test test/engine.test.js",
package/src/ai/deck-ai.js CHANGED
@@ -1036,7 +1036,7 @@ Antes de responder, verifique (e siga as Regras de edição):
1036
1036
  commands.forEach((c, i) => actions.push(c.result?.denied ? `Comando ${i + 1} não autorizado: ${c.why || c.language}`
1037
1037
  : `Comando ${i + 1} (${c.language}): ${c.why || ""}${c.result?.timedOut ? " (tempo esgotado)" : c.result?.exitCode === 0 ? " (ok)" : ` (saída ${c.result?.exitCode ?? "erro"})`}`));
1038
1038
  if (visionRouted) actions.push(`O modelo de texto não enxerga imagens: para ver o slide, esta resposta veio do modelo de visão (${visionRouted}).`);
1039
- if (imagesDropped) actions.push("O modelo de texto atual não enxerga imagens: respondi sem ver o slide (e sem as imagens coladas). Para ele ver, use um modelo com visão em [apps.sagadeck.models] do modelrelay.");
1039
+ if (imagesDropped) actions.push("O modelo de texto atual não enxerga imagens: respondi sem ver o slide (e sem as imagens coladas). Para ele ver, escolha um modelo de visão em Configurar IA.");
1040
1040
  // conversa: nada muda (a resposta pode trazer opções clicáveis)
1041
1041
  if (talk) return { reply: prose, spec, actions, targetSlide, talk: true, options };
1042
1042
  // transformar a apresentação inteira: quem executa é o servidor (src/ai/transform.js), com o andamento no chat
@@ -0,0 +1,101 @@
1
+ // A IA desta máquina: provedor, chave e modelos, num arquivo fora do código e fora da biblioteca
2
+ // (~/.sagadeck/ia.json, ou SAGADECK_IA). Qualquer API compatível com a da OpenAI (OpenRouter, OpenAI, proxy da empresa);
3
+ // a pessoa configura pela tela do Studio (IA › Configurar) ou à mão, sem mexer no código.
4
+ //
5
+ // Antes isso morava num programa à parte (o modelrelay, em Python, rodando na porta 8765). Na primeira vez, a
6
+ // configuração dele (~/.modelrelay/config.toml) é trazida para cá, para ninguém perder a chave.
7
+ import fs from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { renameRetry } from "../fs-retry.js";
11
+
12
+ export const PROVIDERS = {
13
+ openrouter: { label: "OpenRouter", url: "https://openrouter.ai/api/v1", keyEnv: "OPENROUTER_API_KEY" },
14
+ openai: { label: "OpenAI", url: "https://api.openai.com/v1", keyEnv: "OPENAI_API_KEY" },
15
+ };
16
+ // os papéis que o sagadeck usa; cada um aponta para um modelo do provedor
17
+ export const ROLES = ["text", "vision", "image", "search"];
18
+
19
+ export function iaFile(env = process.env, home = os.homedir()) {
20
+ return env.SAGADECK_IA || path.join(home, ".sagadeck", "ia.json");
21
+ }
22
+
23
+ const str = (v, max = 500) => (typeof v === "string" ? v.trim().slice(0, max) : "");
24
+
25
+ // só os campos conhecidos, cada um no tipo certo (arquivo editado à mão não quebra o Studio)
26
+ export function cleanIA(raw) {
27
+ if (!raw || typeof raw !== "object") return null;
28
+ const models = Object.fromEntries(ROLES.map((r) => [r, str(raw.models?.[r], 200)]).filter(([, v]) => v));
29
+ const headers = raw.headers && typeof raw.headers === "object"
30
+ ? Object.fromEntries(Object.entries(raw.headers).filter(([k, v]) => /^[\w-]+$/.test(k) && typeof v === "string").map(([k, v]) => [k, v.slice(0, 500)])) : {};
31
+ const out = { provider: str(raw.provider, 40), url: str(raw.url).replace(/\/+$/, ""), key: str(raw.key, 1000), keyEnv: str(raw.keyEnv, 100), models };
32
+ if (Object.keys(headers).length) out.headers = headers;
33
+ if (str(raw.adaptador)) out.adaptador = str(raw.adaptador);
34
+ return out.url || out.adaptador ? out : null;
35
+ }
36
+
37
+ export function loadIA(file = iaFile()) {
38
+ try { return cleanIA(JSON.parse(fs.readFileSync(file, "utf8"))); } catch { return null; }
39
+ }
40
+
41
+ // grava de forma atômica, só para quem é dono da conta (a chave mora aqui)
42
+ export function saveIA(cfg, file = iaFile()) {
43
+ const clean = cleanIA(cfg);
44
+ if (!clean) throw new Error("Falta o endereço do provedor.");
45
+ fs.mkdirSync(path.dirname(file), { recursive: true });
46
+ const tmp = `${file}.tmp-${process.pid}`;
47
+ fs.writeFileSync(tmp, JSON.stringify(clean, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
48
+ renameRetry(tmp, file);
49
+ return clean;
50
+ }
51
+
52
+ // a chave que vale: a do arquivo ou a da variável de ambiente indicada
53
+ export function keyOf(cfg, env = process.env) {
54
+ return cfg?.key || (cfg?.keyEnv ? env[cfg.keyEnv] || "" : "");
55
+ }
56
+
57
+ // para mostrar na tela: a chave nunca sai inteira
58
+ export function maskedIA(cfg, env = process.env) {
59
+ if (!cfg) return null;
60
+ const key = keyOf(cfg, env);
61
+ const { key: _k, ...rest } = cfg;
62
+ return { ...rest, keySet: !!key, keyHint: key ? `••••${key.slice(-4)}` : "", keyFrom: cfg.key ? "arquivo" : cfg.keyEnv && key ? "variável" : "" };
63
+ }
64
+
65
+ // ---- migração do modelrelay -------------------------------------------------------------------------------------
66
+ // Só o pedaço de TOML que a tela do modelrelay escrevia: [seções], chave = "texto" e comentários.
67
+ export function parseSimpleToml(text) {
68
+ const out = { "": {} };
69
+ let section = "";
70
+ for (const raw of String(text).split(/\r?\n/)) {
71
+ const line = raw.replace(/^\s+|\s+$/g, "");
72
+ if (!line || line.startsWith("#")) continue;
73
+ const sec = /^\[([^\]]+)\]$/.exec(line);
74
+ if (sec) { section = sec[1].trim(); out[section] ||= {}; continue; }
75
+ const kv = /^("?)([^"=]+?)\1\s*=\s*"((?:[^"\\]|\\.)*)"\s*(#.*)?$/.exec(line);
76
+ if (kv) out[section][kv[2].trim()] = kv[3].replace(/\\(["\\])/g, "$1");
77
+ }
78
+ return out;
79
+ }
80
+
81
+ // configuração do modelrelay → a desta máquina (provedor padrão, chave, modelos do sagadeck por cima dos gerais)
82
+ export function fromModelrelay(toml) {
83
+ const t = parseSimpleToml(toml);
84
+ const name = t[""].provider || Object.keys(t).find((k) => k.startsWith("providers."))?.slice(10);
85
+ const p = (name && t[`providers.${name}`]) || {};
86
+ if (!p.base_url) return null;
87
+ const all = { ...(t.models || {}), ...(t["apps.sagadeck.models"] || {}) };
88
+ const models = Object.fromEntries(ROLES.filter((r) => all[r]).map((r) => [r, all[r]]));
89
+ return cleanIA({ provider: PROVIDERS[name] ? name : "outro", url: p.base_url, key: p.api_key || "", keyEnv: p.api_key_env || "", models });
90
+ }
91
+
92
+ // na primeira vez: sem ia.json e com o config.toml do modelrelay, traz a configuração (não sobrescreve nada)
93
+ export function migrateModelrelay({ file = iaFile(), home = os.homedir(), env = process.env } = {}) {
94
+ if (fs.existsSync(file)) return null;
95
+ const toml = env.MODELRELAY_CONFIG || path.join(home, ".modelrelay", "config.toml");
96
+ let cfg;
97
+ try { cfg = fromModelrelay(fs.readFileSync(toml, "utf8")); } catch { return null; }
98
+ if (!cfg) return null;
99
+ saveIA(cfg, file);
100
+ return { from: toml, to: file };
101
+ }