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 +57 -65
- package/SKILL.md +18 -11
- package/bin/sagadeck.js +3 -10
- package/docs/ANDAMENTO.md +72 -2
- package/docs/REFERENCIA.md +2 -2
- package/package.json +2 -3
- package/src/ai/deck-ai.js +1 -1
- package/src/ai/ia-config.js +101 -0
- package/src/ai/llm.js +62 -26
- package/src/library.js +8 -4
- package/src/motion.js +2 -2
- package/src/preferences.js +3 -0
- package/src/research/research.js +6 -5
- package/src/runtime/motion.js +5 -1
- package/src/studio/ai-chat-route.js +1 -1
- package/src/studio/banner.js +18 -0
- package/src/studio/ia-routes.js +53 -0
- package/src/studio/public/ai-settings.js +76 -0
- package/src/studio/public/app.js +4 -23
- package/src/studio/public/index.html +1 -0
- package/src/studio/public/library.html +2 -1
- package/src/studio/public/library.js +5 -4
- package/src/studio/public/preferences-ui.js +3 -0
- package/src/studio/server.js +6 -18
- package/src/studio/slide-copy.js +3 -1
- package/src/ai/relay.js +0 -45
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
|
-
|
|
17
|
+
npm install -g sagadeck
|
|
18
18
|
```
|
|
19
|
-
Requer **Node.js 18+** e **Chrome ou Edge** instalados (o
|
|
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
|
-
|
|
25
|
+
sagadeck studio
|
|
24
26
|
```
|
|
25
|
-
|
|
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 **"
|
|
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
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
|
140
|
-
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
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 (
|
|
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
|
|
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 (
|
|
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=
|
|
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
|
|
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 `
|
|
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
|
-
|
|
412
|
-
|
|
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.
|
|
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.**
|
|
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
|
package/docs/REFERENCIA.md
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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,
|
|
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
|
+
}
|