sagadeck 1.2.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.
Files changed (233) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -0
  3. package/SKILL.md +148 -0
  4. package/bin/sagadeck.js +511 -0
  5. package/docs/ANDAMENTO.md +1255 -0
  6. package/docs/EXPERIENCIAS.md +39 -0
  7. package/docs/PLANO-EXPERIENCIAS.md +39 -0
  8. package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
  9. package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
  10. package/docs/REFERENCIA.md +1131 -0
  11. package/package.json +59 -0
  12. package/src/adaptive-layouts.js +18 -0
  13. package/src/ai/art-direction.js +8 -0
  14. package/src/ai/commands.js +130 -0
  15. package/src/ai/context.js +245 -0
  16. package/src/ai/deck-ai.js +1135 -0
  17. package/src/ai/directions.js +74 -0
  18. package/src/ai/document-materials.js +49 -0
  19. package/src/ai/document-visuals.js +215 -0
  20. package/src/ai/evaluation.js +31 -0
  21. package/src/ai/ground.js +55 -0
  22. package/src/ai/llm.js +292 -0
  23. package/src/ai/quality.js +51 -0
  24. package/src/ai/relay.js +45 -0
  25. package/src/ai/staged-generation.js +24 -0
  26. package/src/ai/transform.js +858 -0
  27. package/src/ai/usage.js +17 -0
  28. package/src/ai/variety.js +80 -0
  29. package/src/ai/video-frames.js +24 -0
  30. package/src/ai/video-generation.js +126 -0
  31. package/src/ai/web-operation.js +31 -0
  32. package/src/algo-catalog.js +153 -0
  33. package/src/algo-trace.js +139 -0
  34. package/src/api-client.js +514 -0
  35. package/src/api-collections.js +14 -0
  36. package/src/api-demo.js +222 -0
  37. package/src/aspect.js +40 -0
  38. package/src/build.js +323 -0
  39. package/src/carousel.js +56 -0
  40. package/src/chrome.js +65 -0
  41. package/src/code-highlight.js +68 -0
  42. package/src/code-lab-browser.js +22 -0
  43. package/src/code-lab.js +3 -0
  44. package/src/code-language.js +52 -0
  45. package/src/csv.js +74 -0
  46. package/src/decision-lab.js +20 -0
  47. package/src/deck-file.js +119 -0
  48. package/src/diagram/napkin-examples.js +13 -0
  49. package/src/diagram/napkin.js +539 -0
  50. package/src/docx.js +128 -0
  51. package/src/dynamics/git-sim.js +200 -0
  52. package/src/dynamics/layouts.js +112 -0
  53. package/src/elements.js +554 -0
  54. package/src/experiences.js +124 -0
  55. package/src/exploration.js +31 -0
  56. package/src/export/browser.js +53 -0
  57. package/src/export/estudo.js +67 -0
  58. package/src/export/pptx.js +475 -0
  59. package/src/export/roteiro.js +40 -0
  60. package/src/export/shots.js +182 -0
  61. package/src/figures/charts.js +383 -0
  62. package/src/figures/diagrams.js +117 -0
  63. package/src/figures/icons.js +166 -0
  64. package/src/figures/material-pictos.js +20 -0
  65. package/src/figures/pictos.js +255 -0
  66. package/src/figures/points.js +106 -0
  67. package/src/figures/qr.js +29 -0
  68. package/src/figures/ufmap.js +48 -0
  69. package/src/fiscal/autofix.js +377 -0
  70. package/src/fiscal/normalize.js +115 -0
  71. package/src/graph-lab.js +24 -0
  72. package/src/hologram.js +30 -0
  73. package/src/identity.js +86 -0
  74. package/src/import/crop.js +59 -0
  75. package/src/import/index.js +132 -0
  76. package/src/import/legacy-word.js +42 -0
  77. package/src/import/merge.js +45 -0
  78. package/src/import/office.js +123 -0
  79. package/src/import/omml.js +69 -0
  80. package/src/import/pdf-render.js +56 -0
  81. package/src/import/pptx.js +701 -0
  82. package/src/import/xml.js +42 -0
  83. package/src/infographic.js +401 -0
  84. package/src/layouts.js +723 -0
  85. package/src/lessons.js +171 -0
  86. package/src/library.js +399 -0
  87. package/src/markup.js +116 -0
  88. package/src/master.js +214 -0
  89. package/src/mcp/server.js +371 -0
  90. package/src/meeting.js +42 -0
  91. package/src/motion.js +56 -0
  92. package/src/package.js +116 -0
  93. package/src/playground.js +19 -0
  94. package/src/portal-scene.js +20 -0
  95. package/src/preferences.js +61 -0
  96. package/src/protect.js +51 -0
  97. package/src/purpose.js +24 -0
  98. package/src/pytrace.js +1092 -0
  99. package/src/research/duckduckgo.js +73 -0
  100. package/src/research/research.js +202 -0
  101. package/src/research/svg-image.js +17 -0
  102. package/src/research/visuals.js +52 -0
  103. package/src/runtime/api-collection-ui.js +5 -0
  104. package/src/runtime/api-collections.js +28 -0
  105. package/src/runtime/api-core.js +530 -0
  106. package/src/runtime/api-ui.js +966 -0
  107. package/src/runtime/base.css +1774 -0
  108. package/src/runtime/calc.js +139 -0
  109. package/src/runtime/code-lab.js +371 -0
  110. package/src/runtime/decision-lab.js +49 -0
  111. package/src/runtime/diagram.js +341 -0
  112. package/src/runtime/fit.js +268 -0
  113. package/src/runtime/fonts/caveat.css +2 -0
  114. package/src/runtime/fonts/comic-neue.css +3 -0
  115. package/src/runtime/fonts/fredoka.css +2 -0
  116. package/src/runtime/fonts/index.json +10 -0
  117. package/src/runtime/fonts/inter.css +2 -0
  118. package/src/runtime/fonts/patrick-hand.css +2 -0
  119. package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
  120. package/src/runtime/fonts/press-start-2p.css +2 -0
  121. package/src/runtime/fonts/vt323.css +2 -0
  122. package/src/runtime/formula.js +148 -0
  123. package/src/runtime/graph-lab.js +72 -0
  124. package/src/runtime/meeting.css +7 -0
  125. package/src/runtime/meeting.js +10 -0
  126. package/src/runtime/motion.js +15 -0
  127. package/src/runtime/playground.js +11 -0
  128. package/src/runtime/points.js +19 -0
  129. package/src/runtime/portal-scene.css +6 -0
  130. package/src/runtime/portal-scene.js +22 -0
  131. package/src/runtime/runtime.js +827 -0
  132. package/src/runtime/science.js +89 -0
  133. package/src/runtime/skins/aluminio.css +31 -0
  134. package/src/runtime/skins/arcade.css +25 -0
  135. package/src/runtime/skins/aurora.css +17 -0
  136. package/src/runtime/skins/bauhaus.css +19 -0
  137. package/src/runtime/skins/editorial.css +19 -0
  138. package/src/runtime/skins/jornal.css +17 -0
  139. package/src/runtime/skins/manual-noite.css +14 -0
  140. package/src/runtime/skins/manual.css +14 -0
  141. package/src/runtime/skins/noite.css +21 -0
  142. package/src/runtime/skins/oceano.css +17 -0
  143. package/src/runtime/skins/pop.css +23 -0
  144. package/src/runtime/skins/prata.css +18 -0
  145. package/src/runtime/skins/prisma.css +28 -0
  146. package/src/runtime/skins/rabisco.css +18 -0
  147. package/src/runtime/skins/relevo.css +33 -0
  148. package/src/runtime/skins/sinal.css +16 -0
  149. package/src/runtime/skins/terminal.css +23 -0
  150. package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
  151. package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
  152. package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
  153. package/src/runtime/vendor/katex.css +1 -0
  154. package/src/runtime/vendor/mermaid.min.js +3636 -0
  155. package/src/runtime/vendor/plotly.min.js +3620 -0
  156. package/src/runtime/video-player.js +35 -0
  157. package/src/science.js +162 -0
  158. package/src/studio/ai-response.js +36 -0
  159. package/src/studio/api-routes.js +152 -0
  160. package/src/studio/assets/contexto/leia-me.md +12 -0
  161. package/src/studio/assets/contexto/vendas.csv +7 -0
  162. package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
  163. package/src/studio/assets/imagens/colecao-cor.png +0 -0
  164. package/src/studio/assets/imagens/colecao-equipe.png +0 -0
  165. package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
  166. package/src/studio/code-routes.js +32 -0
  167. package/src/studio/command-approval.js +48 -0
  168. package/src/studio/demo-decks.js +98 -0
  169. package/src/studio/direction-routes.js +29 -0
  170. package/src/studio/errors.js +65 -0
  171. package/src/studio/exploration-demo.js +17 -0
  172. package/src/studio/exporting.js +105 -0
  173. package/src/studio/generation-report.js +10 -0
  174. package/src/studio/instance.js +48 -0
  175. package/src/studio/layout-samples.js +152 -0
  176. package/src/studio/project.js +371 -0
  177. package/src/studio/public/api-collections.js +28 -0
  178. package/src/studio/public/app.js +5265 -0
  179. package/src/studio/public/art-preview.js +10 -0
  180. package/src/studio/public/calc-fields.js +11 -0
  181. package/src/studio/public/chat-progress.js +47 -0
  182. package/src/studio/public/developer-fields.js +22 -0
  183. package/src/studio/public/explorer.js +443 -0
  184. package/src/studio/public/history.js +58 -0
  185. package/src/studio/public/index.html +867 -0
  186. package/src/studio/public/inspector.js +270 -0
  187. package/src/studio/public/lab-fields.js +5 -0
  188. package/src/studio/public/library.css +275 -0
  189. package/src/studio/public/library.html +55 -0
  190. package/src/studio/public/library.js +471 -0
  191. package/src/studio/public/merge-decks.js +155 -0
  192. package/src/studio/public/review-ui.js +100 -0
  193. package/src/studio/public/screenshot-editor.js +140 -0
  194. package/src/studio/public/share-ui.js +72 -0
  195. package/src/studio/public/slide-clipboard.js +23 -0
  196. package/src/studio/public/slide-form.js +1263 -0
  197. package/src/studio/public/studio-awareness.js +154 -0
  198. package/src/studio/public/studio-next.css +303 -0
  199. package/src/studio/public/style.css +1262 -0
  200. package/src/studio/public/ui-icons.js +2 -0
  201. package/src/studio/public/viewers.js +372 -0
  202. package/src/studio/public/visual-editor.js +326 -0
  203. package/src/studio/public/workspace.js +18 -0
  204. package/src/studio/public-files.js +1 -0
  205. package/src/studio/request-helpers.js +41 -0
  206. package/src/studio/server.js +1583 -0
  207. package/src/studio/share-routes.js +146 -0
  208. package/src/studio/slide-copy-routes.js +26 -0
  209. package/src/studio/slide-copy.js +30 -0
  210. package/src/studio/snapshot.js +160 -0
  211. package/src/studio/style-routes.js +103 -0
  212. package/src/studio/template-collections.js +97 -0
  213. package/src/studio/versions.js +95 -0
  214. package/src/studio/video-download.js +29 -0
  215. package/src/studio/visual-keys.js +52 -0
  216. package/src/studio/workspaces.js +39 -0
  217. package/src/table.js +93 -0
  218. package/src/templates/scaffold.js +372 -0
  219. package/src/themes.js +604 -0
  220. package/src/trace-view.js +450 -0
  221. package/src/uid.js +19 -0
  222. package/src/video-player.js +40 -0
  223. package/src/visual-edits.js +99 -0
  224. package/src/ws.js +134 -0
  225. package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
  226. package/templates/cenario/imagens/cidade.svg +30 -0
  227. package/templates/cenario/imagens/pessoa.svg +15 -0
  228. package/templates/cenario-e-ciencia.yaml +47 -0
  229. package/templates/ensaio-api.yaml +160 -0
  230. package/templates/exemplo-alegre.yaml +75 -0
  231. package/templates/exemplo-keynote.yaml +90 -0
  232. package/templates/exemplo-napkin.yaml +139 -0
  233. package/templates/exemplo.yaml +228 -0
@@ -0,0 +1,514 @@
1
+ // Executa os pedidos do slide "api" a partir do Studio (Node), nunca do navegador: sem CORS, com o
2
+ // certificado da empresa e com o token fora da página. Os ambientes (dev/hom/prod) ficam num arquivo
3
+ // da máquina — ~/.sagadeck/ambientes.yaml ou SAGADECK_AMBIENTES —, nunca no deck:
4
+ //
5
+ // current: hom
6
+ // environments:
7
+ // hom:
8
+ // vars: { base: "https://api.exemplo.com/v1" } # {{base}} nos slides
9
+ // token: # opcional: token que expira (client credentials)
10
+ // url: "https://identidade.exemplo.com/token"
11
+ // client_id: "..."
12
+ // client_secret_env: MINHA_SECRET # ou client_secret: "..."
13
+ // field: access_token # onde está o token na resposta ($.data.token)
14
+ // ttl_minutes: 30
15
+ // ca: "C:/certs/empresa.pem" # certificado da empresa (proxy/inspeção TLS)
16
+ import fs from "node:fs";
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ import http from "node:http";
20
+ import https from "node:https";
21
+ import YAML from "yaml";
22
+ import "./runtime/api-core.js";
23
+ import { connectWs } from "./ws.js";
24
+ import { protect, unprotect, unprotectAll, PROTECTION } from "./protect.js";
25
+
26
+ const C = globalThis.SagadeckApiCore;
27
+
28
+ // As variáveis e segredos dos slides de API moram FORA das apresentações: ~/.sagadeck/ambientes.yaml (no Windows,
29
+ // C:\Users\<você>\.sagadeck\ambientes.yaml) ou SAGADECK_AMBIENTES. Mandar um deck para alguém nunca leva segredo
30
+ // junto; as variáveis protegidas ainda vão cifradas (src/protect.js). O painel Variáveis mostra esse caminho.
31
+ export function defaultEnvFile(env = process.env, home = os.homedir()) {
32
+ return env.SAGADECK_AMBIENTES || path.join(home, ".sagadeck", "ambientes.yaml");
33
+ }
34
+
35
+ const MIME = { pdf: "application/pdf", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", tif: "image/tiff", tiff: "image/tiff",
36
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", xls: "application/vnd.ms-excel", csv: "text/csv",
37
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", txt: "text/plain", json: "application/json",
38
+ mp3: "audio/mpeg", wav: "audio/wav", m4a: "audio/mp4", ogg: "audio/ogg", webm: "audio/webm", mp4: "video/mp4" };
39
+ export const mimeOf = (name) => MIME[String(name || "").toLowerCase().split(".").pop()] || "application/octet-stream";
40
+
41
+ // ---------- gravações: a última resposta boa de cada slide, ao lado do deck ----------
42
+
43
+ export const recordingsFile = (deckFile) => (deckFile ? deckFile.replace(/\.ya?ml$/i, "") + ".respostas.json" : null);
44
+
45
+ export function readRecordings(deckFile) {
46
+ const f = recordingsFile(deckFile);
47
+ try { return f && fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")) : {}; } catch { return {}; }
48
+ }
49
+
50
+ export function writeRecording(deckFile, key, record) {
51
+ const f = recordingsFile(deckFile);
52
+ if (!f) return null;
53
+ const all = readRecordings(deckFile);
54
+ all[key] = { ...record, at: record.at || new Date().toISOString() };
55
+ fs.writeFileSync(f, JSON.stringify(all, null, 2));
56
+ return f;
57
+ }
58
+
59
+ // ---------- HTTP ----------
60
+
61
+ export class ApiError extends Error {
62
+ constructor(message, kind, extra = {}) { super(message); this.kind = kind; Object.assign(this, extra); }
63
+ }
64
+
65
+ const OFFLINE = new Set(["ENOTFOUND", "ECONNREFUSED", "ETIMEDOUT", "EAI_AGAIN", "ECONNRESET", "EHOSTUNREACH", "ENETUNREACH", "TIMEOUT"]);
66
+
67
+ function explain(e, url) {
68
+ const host = (() => { try { return new URL(url).host; } catch { return url; } })();
69
+ if (OFFLINE.has(e.code)) return new ApiError(`Não consegui falar com ${host} (${e.code}). Está conectado à VPN?`, "offline", { code: e.code });
70
+ if (/CERT|SELF_SIGNED|UNABLE_TO_VERIFY|UNABLE_TO_GET_ISSUER/i.test(e.code || e.message)) {
71
+ return new ApiError(`${host} usa um certificado que o Node não reconhece (${e.code}). Aponte o certificado da empresa em "ca:" no ambiente, ou em NODE_EXTRA_CA_CERTS.`, "tls", { code: e.code });
72
+ }
73
+ return new ApiError(`Falha ao chamar ${host}: ${e.message}`, "network", { code: e.code });
74
+ }
75
+
76
+ function open({ method, url, headers, body, ca, insecure, timeout }) {
77
+ return new Promise((resolve, reject) => {
78
+ let u;
79
+ try { u = new URL(url); } catch { reject(new ApiError(`Endereço inválido: ${url}`, "config")); return; }
80
+ if (!/^https?:$/.test(u.protocol)) { reject(new ApiError(`Só http e https: ${url}`, "config")); return; }
81
+ const lib = u.protocol === "https:" ? https : http;
82
+ const req = lib.request(u, { method, headers, ca, rejectUnauthorized: !insecure, timeout }, resolve);
83
+ req.on("timeout", () => req.destroy(Object.assign(new Error(`sem resposta em ${Math.round(timeout / 1000)}s`), { code: "TIMEOUT" })));
84
+ req.on("error", (e) => reject(explain(e, url)));
85
+ if (body != null) req.write(body);
86
+ req.end();
87
+ });
88
+ }
89
+
90
+ const readAll = (res) => new Promise((resolve, reject) => {
91
+ const chunks = [];
92
+ res.on("data", (c) => chunks.push(c));
93
+ res.on("end", () => resolve(Buffer.concat(chunks)));
94
+ res.on("error", reject);
95
+ });
96
+
97
+ function parseBody(buf, type) {
98
+ const text = buf.toString("utf8");
99
+ if (/json/i.test(type || "") || /^\s*[[{]/.test(text)) { try { return JSON.parse(text); } catch {} }
100
+ return text;
101
+ }
102
+
103
+ // JWT: cabeçalho e conteúdo decodificados (são base64, não segredo); a assinatura não sai daqui.
104
+ export function decodeJwt(token) {
105
+ const parts = String(token).split(".");
106
+ const part = (s) => { try { return JSON.parse(Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")); } catch { return null; } };
107
+ const header = parts.length === 3 ? part(parts[0]) : null;
108
+ const payload = parts.length === 3 ? part(parts[1]) : null;
109
+ return { isJwt: !!(header && payload), header, payload, last4: String(token).slice(-4), exp: payload && Number(payload.exp) ? Number(payload.exp) : null };
110
+ }
111
+
112
+ // ---------- ambientes e token ----------
113
+
114
+ export class ApiEnvironments {
115
+ constructor(file = defaultEnvFile()) {
116
+ this.file = file;
117
+ this.tokens = new Map(); // ambiente -> { value, expiresAt }
118
+ this.selected = null; // escolha feita na apresentação (vale até fechar o Studio)
119
+ this.builtin = {}; // ambientes que não moram no arquivo (o "ensaio" do Studio, contra a API de mentira)
120
+ }
121
+
122
+ // só o arquivo da pessoa
123
+ loadFile() {
124
+ if (!fs.existsSync(this.file)) return { current: null, environments: {} };
125
+ let data;
126
+ try { data = YAML.parse(fs.readFileSync(this.file, "utf8")) || {}; }
127
+ catch (e) { throw new ApiError(`${this.file}: YAML inválido: ${e.message}`, "config"); }
128
+ const envs = data.environments && typeof data.environments === "object" ? data.environments : {};
129
+ // segredos cifrados (dpapi:…): decifra todos de uma vez (um PowerShell só)
130
+ try { unprotectAll(Object.values(envs).flatMap((e) => Object.values((e && e.secrets) || {}))); }
131
+ catch (e) { throw new ApiError(`${this.file}: ${e.message}`, "config"); }
132
+ return { current: data.current || null, environments: envs };
133
+ }
134
+
135
+ // o arquivo + os embutidos, que vêm depois (o do arquivo vence quando o nome é igual)
136
+ load() {
137
+ const data = this.loadFile();
138
+ const extra = Object.fromEntries(Object.entries(this.builtin).filter(([k]) => !(k in data.environments)));
139
+ return { current: data.current, environments: { ...data.environments, ...extra }, own: Object.keys(data.environments) };
140
+ }
141
+
142
+ currentName(data) {
143
+ if (!data) { try { data = this.load(); } catch { return null; } } // arquivo quebrado: sem ambiente, sem derrubar ninguém
144
+ const names = Object.keys(data.environments);
145
+ if (this.selected && names.includes(this.selected)) return this.selected;
146
+ if (data.current && names.includes(data.current)) return data.current;
147
+ return names[0] || null;
148
+ }
149
+
150
+ state() {
151
+ let data, error = null;
152
+ try { data = this.load(); } catch (e) { data = { environments: { ...this.builtin }, own: [] }; error = e.message; }
153
+ return {
154
+ file: this.file, exists: fs.existsSync(this.file), error, protection: PROTECTION,
155
+ current: this.currentName(data),
156
+ envs: Object.entries(data.environments).map(([name, e]) => ({
157
+ name, kind: C.envKind(name), vars: (e && e.vars) || {}, token: !!(e && e.token && e.token.url),
158
+ secrets: Object.keys((e && e.secrets) || {}), // só os nomes; o valor nunca sai do Node (a não ser pelo olhinho: reveal)
159
+ ...(data.own.includes(name) ? {} : { builtin: true, builtinLabel: e.label || "ambiente local" }),
160
+ })),
161
+ };
162
+ }
163
+
164
+ use(name) {
165
+ const data = this.load();
166
+ if (!data.environments[name]) throw new ApiError(`Ambiente "${name}" não existe em ${this.file}`, "config");
167
+ this.selected = name;
168
+ if (!data.own.includes(name)) return this.state(); // embutido: não vai para o arquivo da pessoa
169
+ // guarda a escolha no arquivo, sem perder os comentários de quem escreveu
170
+ try {
171
+ const doc = YAML.parseDocument(fs.readFileSync(this.file, "utf8"));
172
+ doc.set("current", name);
173
+ fs.writeFileSync(this.file, String(doc));
174
+ } catch {}
175
+ return this.state();
176
+ }
177
+
178
+ // ---- variáveis pelo painel: criar/editar (normal ou protegida), revelar (olhinho) e apagar ----
179
+ // O arquivo é editado como documento YAML (comentários da pessoa ficam) e gravado de forma atômica.
180
+ editFile(fn) {
181
+ let doc;
182
+ if (fs.existsSync(this.file)) {
183
+ doc = YAML.parseDocument(fs.readFileSync(this.file, "utf8"));
184
+ if (doc.errors.length) throw new ApiError(`${this.file}: YAML inválido: ${doc.errors[0].message}`, "config");
185
+ } else doc = new YAML.Document({ current: null, environments: {} });
186
+ fn(doc);
187
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
188
+ const tmp = `${this.file}.tmp-${process.pid}`;
189
+ fs.writeFileSync(tmp, String(doc), { encoding: "utf8", mode: 0o600 });
190
+ fs.renameSync(tmp, this.file);
191
+ }
192
+
193
+ // Ambiente de exemplo (embutido) editado pela pessoa: vira um ambiente dela, cópia completa no arquivo (o do arquivo
194
+ // vence o embutido com o mesmo nome), já com a mudança. Apagar a cópia do arquivo traz o exemplo de volta.
195
+ targetEnv(envName) {
196
+ const data = this.load();
197
+ const n = envName || this.currentName(data) || "dev";
198
+ const copyOf = data.environments[n] && !data.own.includes(n) ? this.builtin[n] : null;
199
+ return { n, copyOf };
200
+ }
201
+ adopt(doc, n, copyOf) {
202
+ if (!copyOf) return;
203
+ const own = {};
204
+ if (copyOf.vars) own.vars = { ...copyOf.vars };
205
+ if (copyOf.secrets) own.secrets = Object.fromEntries(Object.entries(copyOf.secrets).map(([k, v]) => [k, typeof v === "string" ? protect(v) : v]));
206
+ doc.setIn(["environments", n], doc.createNode(own));
207
+ }
208
+
209
+ setVar(name, value, { protected: prot = false, env: envName } = {}) {
210
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(name || ""))) throw new ApiError(`Nome de variável inválido: "${name}". Use letras, números e _ (ex.: path_id).`, "config");
211
+ const { n, copyOf } = this.targetEnv(envName);
212
+ this.editFile((doc) => {
213
+ this.adopt(doc, n, copyOf);
214
+ if (!doc.get("current")) doc.set("current", n);
215
+ doc.setIn(["environments", n, prot ? "secrets" : "vars", name], prot ? protect(String(value)) : value);
216
+ if (doc.hasIn(["environments", n, prot ? "vars" : "secrets", name])) doc.deleteIn(["environments", n, prot ? "vars" : "secrets", name]);
217
+ });
218
+ return this.state();
219
+ }
220
+
221
+ reveal(name, envName) {
222
+ const e = this.env(envName);
223
+ if (e.vars && name in e.vars) return String(e.vars[name]);
224
+ const v = this.secretVars(e)["secret." + name];
225
+ if (v == null) throw new ApiError(`A variável "${name}" não existe no ambiente ${e.name}.`, "config");
226
+ return v;
227
+ }
228
+
229
+ deleteVar(name, envName) {
230
+ const { n, copyOf } = this.targetEnv(envName);
231
+ this.editFile((doc) => {
232
+ this.adopt(doc, n, copyOf);
233
+ for (const k of ["vars", "secrets"]) if (doc.hasIn(["environments", n, k, name])) doc.deleteIn(["environments", n, k, name]);
234
+ });
235
+ return this.state();
236
+ }
237
+
238
+ env(name) {
239
+ const data = this.load();
240
+ const n = name || this.currentName(data);
241
+ const e = n && data.environments[n];
242
+ if (!e) throw new ApiError(Object.keys(data.environments).length ? `Ambiente "${n}" não existe` : `Nenhum ambiente configurado. Crie ${this.file} (veja docs/REFERENCIA.md, slide "api").`, "config");
243
+ return { name: n, ...e };
244
+ }
245
+
246
+ // tudo o que não pode aparecer na tela (token, segredos, cabeçalhos do ambiente)
247
+ // segredos do ambiente (secrets: { nome: "valor" } ou { nome: { env: VARIAVEL } }): {{secret.nome}} nos slides
248
+ secretVars(env) {
249
+ const out = {};
250
+ for (const [k, v] of Object.entries(env.secrets || {})) {
251
+ const val = v && typeof v === "object" ? process.env[v.env] : unprotect(v);
252
+ if (val != null) out["secret." + k] = String(val);
253
+ }
254
+ return out;
255
+ }
256
+
257
+ secrets(env) {
258
+ const t = env.token || {};
259
+ const out = [this.tokens.get(env.name)?.value, t.client_secret, t.client_secret_env && process.env[t.client_secret_env], ...Object.values(env.headers || {}), ...Object.values(this.secretVars(env))];
260
+ return out.filter((s) => typeof s === "string" && s.length >= 6);
261
+ }
262
+
263
+ maskText(env, text) {
264
+ let s = String(text);
265
+ for (const secret of this.secrets(env)) s = s.split(secret).join(C.mask(secret));
266
+ return s;
267
+ }
268
+
269
+ async token(env, { renew = false } = {}) {
270
+ const t = env.token;
271
+ if (!t || !t.url) return null;
272
+ const cached = this.tokens.get(env.name);
273
+ if (!renew && cached && Date.now() < cached.renewAt) return cached.value;
274
+ const id = t.client_id ?? (t.client_id_env && process.env[t.client_id_env]);
275
+ const secret = t.client_secret ?? (t.client_secret_env && process.env[t.client_secret_env]);
276
+ if (!id || !secret) throw new ApiError(`Ambiente "${env.name}": falta client_id/client_secret do token (ou a variável ${t.client_secret_env || t.client_id_env || "…"})`, "config");
277
+ const fields = { [t.id_field || "client_id"]: id, [t.secret_field || "client_secret"]: secret, ...(t.extra || {}) };
278
+ const form = t.format === "form";
279
+ const body = form ? new URLSearchParams(fields).toString() : JSON.stringify(fields);
280
+ const res = await open({
281
+ method: "POST", url: t.url, body, ca: this.ca(env), insecure: !!env.insecure, timeout: 30000,
282
+ headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json", Accept: "application/json", ...(t.headers || {}) },
283
+ });
284
+ const data = parseBody(await readAll(res), res.headers["content-type"]);
285
+ if (res.statusCode >= 400) throw new ApiError(`O serviço de token respondeu HTTP ${res.statusCode}: ${this.maskText(env, typeof data === "string" ? data : JSON.stringify(data)).slice(0, 300)}`, "token", { status: res.statusCode });
286
+ const value = C.get(data, t.field || "access_token");
287
+ if (!value || typeof value !== "string") {
288
+ const fieldsSeen = data && typeof data === "object" ? Object.keys(data).join(", ") : typeof data;
289
+ throw new ApiError(`A resposta do token não tem "${t.field || "access_token"}" (campos: ${fieldsSeen})`, "token");
290
+ }
291
+ const ttl = (Number(data.expires_in) > 0 ? Number(data.expires_in) : (Number(t.ttl_minutes) || 30) * 60) * 1000;
292
+ const margin = Math.min(120000, ttl / 2);
293
+ this.tokens.set(env.name, { value, expiresAt: Date.now() + ttl, renewAt: Date.now() + ttl - margin });
294
+ return value;
295
+ }
296
+
297
+ tokenInfo(name) {
298
+ const t = this.tokens.get(name);
299
+ return t ? { expiresIn: Math.max(0, Math.round((t.expiresAt - Date.now()) / 1000)), last4: t.value.slice(-4), fromSlide: !!t.fromSlide } : null;
300
+ }
301
+
302
+ ca(env) {
303
+ if (!env.ca) return undefined;
304
+ try { return fs.readFileSync(path.resolve(path.dirname(this.file), env.ca)); }
305
+ catch { throw new ApiError(`Ambiente "${env.name}": não achei o certificado ${env.ca}`, "config"); }
306
+ }
307
+
308
+ async prepare(req) {
309
+ const env = this.env();
310
+ // {{secret.nome}}: trocado aqui, no último momento; o navegador só vê o marcador
311
+ const sv = this.secretVars(env);
312
+ const missingSecret = C.missing({ u: req.url, h: req.headers, b: req.body }, sv).filter((m) => m.startsWith("secret."));
313
+ if (missingSecret.length) {
314
+ const names = missingSecret.map((m) => {
315
+ const name = m.slice(7);
316
+ const binding = env.secrets && env.secrets[name];
317
+ return binding && typeof binding === "object" && binding.env ? `${name} (defina ${binding.env})` : name;
318
+ });
319
+ throw new ApiError(`Ambiente "${env.name}" não tem ${names.join(", ")} configurado em secrets:`, "config");
320
+ }
321
+ req = { ...req, url: C.render(req.url, sv), headers: C.render(req.headers || {}, sv), body: C.render(req.body, sv) };
322
+ const headers = { Accept: "application/json, text/event-stream, */*", ...(env.headers || {}), ...(req.headers || {}) };
323
+ let body = req.body;
324
+ const file = req.file || null; // { name, type, data: Buffer } — o arquivo do slide (upload ou base64)
325
+ const needsFile = (req.form && Object.values(req.form).includes("@file")) || /\{\{\s*file\./.test(JSON.stringify(body ?? ""));
326
+ if (needsFile && !file) throw new ApiError("Este slide precisa de um arquivo: arraste um para o slide ou defina file: no YAML.", "config");
327
+ if (file && body != null) body = C.render(body, { "file.base64": file.data.toString("base64"), "file.name": file.name, "file.type": file.type || "application/octet-stream" });
328
+ if (req.form) {
329
+ // multipart/form-data, montado à mão (sem dependências)
330
+ const boundary = "----sagadeck" + Math.random().toString(16).slice(2);
331
+ const parts = [];
332
+ for (const [k, v] of Object.entries(req.form)) {
333
+ if (v === "@file") {
334
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${k}"; filename="${String(file.name).replace(/"/g, "")}"\r\nContent-Type: ${file.type || "application/octet-stream"}\r\n\r\n`), file.data, Buffer.from("\r\n"));
335
+ } else {
336
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${k}"\r\n\r\n${v}\r\n`));
337
+ }
338
+ }
339
+ parts.push(Buffer.from(`--${boundary}--\r\n`));
340
+ body = Buffer.concat(parts);
341
+ headers["Content-Type"] = `multipart/form-data; boundary=${boundary}`;
342
+ } else if (body != null && typeof body === "object") {
343
+ body = JSON.stringify(body);
344
+ if (!Object.keys(headers).some((h) => h.toLowerCase() === "content-type")) headers["Content-Type"] = "application/json";
345
+ }
346
+ const method = String(req.method || (body != null || req.form ? "POST" : "GET")).toUpperCase();
347
+ const t = env.token || {};
348
+ const authHeader = t.header || "Authorization";
349
+ const withToken = async (renew) => {
350
+ if (req.auth === false) return headers;
351
+ const tok = await this.token(env, { renew });
352
+ return tok ? { ...headers, [authHeader]: (t.prefix ?? "Bearer ") + tok } : headers;
353
+ };
354
+ const sent = (h) => ({ method, url: this.maskText(env, req.url), headers: JSON.parse(this.maskText(env, JSON.stringify(h))), ...(file ? { file: { name: file.name, type: file.type, size: file.data.length } } : {}) });
355
+ return { env, method, body, withToken, sent, timeout: (Number(env.timeout) || 60) * 1000 };
356
+ }
357
+
358
+ // Pedido completo: a resposta volta inteira (JSON ou texto), com os segredos mascarados.
359
+ async send(req) {
360
+ const p = await this.prepare(req);
361
+ const run = async (renew) => {
362
+ const headers = await p.withToken(renew);
363
+ const t0 = Date.now();
364
+ const res = await open({ method: p.method, url: req.url, headers, body: p.body, ca: this.ca(p.env), insecure: !!p.env.insecure, timeout: p.timeout });
365
+ const buf = await readAll(res);
366
+ return { res, buf, headers, ms: Date.now() - t0 };
367
+ };
368
+ let r = await run(false);
369
+ if ((r.res.statusCode === 401 || r.res.statusCode === 403) && req.auth !== false && p.env.token) r = await run(true); // token vencido: renova uma vez
370
+ const type = r.res.headers["content-type"] || "";
371
+ // Slide do Identity (token: "$.access_token"): o token desta resposta passa a ser o do ambiente,
372
+ // e os próximos slides usam ele. Para a tela vai só o conteúdo decodificado, nunca o token.
373
+ let jwt = null;
374
+ if (req.captureToken && r.res.statusCode < 400) {
375
+ const raw = parseBody(r.buf, type);
376
+ const value = C.get(raw, req.captureToken);
377
+ if (typeof value !== "string" || !value) throw new ApiError(`A resposta não tem o token em ${req.captureToken}`, "token");
378
+ const ttl = (Number(raw && raw.expires_in) > 0 ? Number(raw.expires_in) : (Number(p.env.token && p.env.token.ttl_minutes) || 30) * 60) * 1000;
379
+ this.tokens.set(p.env.name, { value, expiresAt: Date.now() + ttl, renewAt: Date.now() + ttl - Math.min(120000, ttl / 2), fromSlide: true });
380
+ jwt = decodeJwt(value);
381
+ }
382
+ // áudio (TTS): vai inteiro, em base64, para o slide tocar
383
+ const body = /^audio\//i.test(type) ? { _audio: true, type: type.split(";")[0], base64: r.buf.toString("base64") }
384
+ : parseBody(Buffer.from(this.maskText(p.env, r.buf.toString("utf8"))), type);
385
+ return {
386
+ ok: r.res.statusCode < 400, status: r.res.statusCode, statusText: r.res.statusMessage || "", ms: r.ms, size: r.buf.length,
387
+ type, body, sent: p.sent(r.headers), env: p.env.name, token: this.tokenInfo(p.env.name), ...(jwt ? { jwt } : {}),
388
+ };
389
+ }
390
+
391
+ // Executa um slide inteiro, do jeito que a apresentação faria, e devolve:
392
+ // report: o que a IA precisa para corrigir o slide (status, erro, se os caminhos $.… existem, resposta resumida)
393
+ // record: a gravação (mesmo formato da apresentação), saved: os valores de save:
394
+ // Usado pelo assistente de IA para testar os slides que ele criou (loop "gerar → testar → corrigir").
395
+ async runSlide(slide, { vars = {}, deckDir = null, maxSeconds = 60 } = {}) {
396
+ const a = C.normalize(slide);
397
+ const short = (v, n = 1500) => { const s = typeof v === "string" ? v : JSON.stringify(v); return s && s.length > n ? s.slice(0, n) + "…" : s; };
398
+ const report = { mode: a.similarity ? "similarity" : a.mode, env: this.currentName() };
399
+ if (a.mic && !a.file) return { report: { ...report, skipped: "este slide grava do microfone: só dá para testar na apresentação" } };
400
+ if (a.realtime) return { report: { ...report, skipped: "conversa em tempo real (WebSocket): teste na apresentação, clicando em Conectar" } };
401
+ let file = null;
402
+ if (a.file) {
403
+ if (!deckDir) return { report: { ...report, ok: false, erro: "salve o deck numa pasta para usar file:" } };
404
+ const abs = path.resolve(deckDir, a.file);
405
+ if (!abs.startsWith(path.resolve(deckDir) + path.sep) || !fs.existsSync(abs)) return { report: { ...report, ok: false, erro: `não achei o arquivo ${a.file} na pasta do deck` } };
406
+ file = { name: path.basename(abs), type: mimeOf(abs), data: fs.readFileSync(abs) };
407
+ }
408
+ let env;
409
+ try { env = this.env(); } catch (e) { return { report: { ...report, ok: false, erro: e.message, tipo: "ambiente" } }; }
410
+ let req = C.render(a.request, { ...(env.vars || {}), ...vars });
411
+ const miss = C.missing(req, {}).filter((m) => !/^(file|secret)\./.test(m) && !(a.similarity && m === "text"));
412
+ if (miss.length) return { report: { ...report, ok: false, erro: `faltam variáveis: ${miss.map((m) => `{{${m}}}`).join(", ")} (defina em vars do ambiente ou num save: de um slide anterior)` } };
413
+ if (a.token) req.captureToken = a.token;
414
+ const paths = (body) => {
415
+ const out = {};
416
+ if (a.answer) { const v = C.get(body, a.answer); out.answer = v === undefined ? `NÃO EXISTE ${a.answer}` : short(v, 300); }
417
+ if (a.steps) { const v = C.get(body, a.steps); out.steps = Array.isArray(v) ? `${v.length} etapas` : `NÃO EXISTE lista em ${a.steps}`; }
418
+ return out;
419
+ };
420
+ const saved = {};
421
+ const keep = (body) => { for (const [k, p] of Object.entries(a.save || {})) { const v = C.get(body, p); if (v !== undefined) saved[k] = v; else report[`save.${k}`] = `NÃO EXISTE ${p}`; } };
422
+ try {
423
+ if (a.similarity) {
424
+ const embed = async (t) => {
425
+ const r = await this.send({ ...C.render(req, { text: t }), file });
426
+ const v = C.get(r.body, a.similarity.vector);
427
+ if (!r.ok || !Array.isArray(v)) throw Object.assign(new Error(!r.ok ? `HTTP ${r.status}: ${short(r.body, 600)}` : `NÃO EXISTE vetor em ${a.similarity.vector}; resposta: ${short(r.body, 600)}`), { r });
428
+ return v;
429
+ };
430
+ const ref = await embed(a.similarity.reference);
431
+ const items = [];
432
+ for (const t of a.similarity.texts) items.push({ text: t, score: C.cosine(ref, await embed(t)) });
433
+ items.sort((x, y) => y.score - x.score);
434
+ const sim = { reference: a.similarity.reference, dims: ref.length, preview: ref.slice(0, 48).map((x) => +Number(x).toFixed(4)), items, done: true };
435
+ return { report: { ...report, ok: true, dims: ref.length, similaridades: items.map((i) => `${i.score.toFixed(2)} ${i.text}`) }, record: { mode: "similarity", env: report.env, sim }, saved };
436
+ }
437
+ if (a.mode === "stream") {
438
+ const up = await this.open({ ...req, file });
439
+ let raw = "";
440
+ for await (const c of up.res) raw += c;
441
+ raw = up.mask(raw);
442
+ let text = "", pieces = 0;
443
+ for (const line of raw.split("\n")) {
444
+ const d = line.trim().startsWith("data:") ? line.trim().slice(5).trim() : null;
445
+ if (!d || d === "[DONE]") continue;
446
+ try { const p = C.get(JSON.parse(d), a.stream.text); if (p != null) { text += p; pieces++; } } catch {}
447
+ }
448
+ const ok = up.res.statusCode < 400 && pieces > 0;
449
+ return { report: { ...report, ok, status: up.res.statusCode, pedacos: pieces, texto: short(text, 400), ...(pieces ? {} : { erro: `nenhum pedaço com texto em ${a.stream.text}; começo da resposta: ${short(raw, 600)}` }) },
450
+ record: ok ? { mode: "stream", env: report.env, stream: { status: up.res.statusCode, text, ms: 0, pieces } } : null, saved };
451
+ }
452
+ const t0 = Date.now();
453
+ const start = await this.send({ ...req, file });
454
+ report.status = start.status;
455
+ if (!start.ok) return { report: { ...report, ok: false, resposta: short(start.body) } };
456
+ if (a.mode !== "polling") {
457
+ keep(start.body);
458
+ return { report: { ...report, ok: true, ms: start.ms, ...paths(start.body), ...(start.jwt ? { jwt: start.jwt.payload } : {}), resposta: short(start.body) }, record: { mode: "sync", env: report.env, result: start }, saved };
459
+ }
460
+ const id = C.get(start.body, a.polling.id);
461
+ if (id == null) return { report: { ...report, ok: false, erro: `NÃO EXISTE ${a.polling.id} (o código da execução) na resposta do início`, resposta: short(start.body) } };
462
+ const check = C.render(a.polling.check, { ...(env.vars || {}), ...vars, id });
463
+ const polls = [];
464
+ for (;;) {
465
+ if ((Date.now() - t0) / 1000 > Math.min(a.polling.timeout, maxSeconds)) return { report: { ...report, ok: false, erro: `não terminou em ${Math.min(a.polling.timeout, maxSeconds)} s`, status: polls.map((p) => p.status) } };
466
+ const r = await this.send(check);
467
+ if (!r.ok) return { report: { ...report, ok: false, erro: `consulta respondeu HTTP ${r.status}`, resposta: short(r.body) } };
468
+ const st = C.get(r.body, a.polling.status);
469
+ polls.push({ status: String(st), t: Date.now() - t0 });
470
+ if (st === undefined) return { report: { ...report, ok: false, erro: `NÃO EXISTE ${a.polling.status} (o status) na consulta`, resposta: short(r.body) } };
471
+ if (a.polling.done.includes(String(st))) {
472
+ keep(r.body);
473
+ return { report: { ...report, ok: true, statuses: polls.map((p) => p.status), ...paths(r.body), resposta: short(r.body) }, record: { mode: "polling", env: report.env, start, polls, final: r }, saved };
474
+ }
475
+ if (a.polling.failed.includes(String(st))) return { report: { ...report, ok: false, erro: `a execução terminou com ${st}`, resposta: short(r.body) } };
476
+ await new Promise((res) => setTimeout(res, a.polling.interval * 1000));
477
+ }
478
+ } catch (e) {
479
+ return { report: { ...report, ok: false, erro: e.message, ...(e.kind ? { tipo: e.kind } : {}) } };
480
+ }
481
+ }
482
+
483
+ // Conversa em tempo real: abre o WebSocket com o serviço (token no cabeçalho ou na URL, certificado da
484
+ // empresa). Devolve a conexão; o Studio faz a ponte com o navegador.
485
+ async openRealtime(rt) {
486
+ const env = this.env();
487
+ const sv = this.secretVars(env);
488
+ let url = C.render(String(rt.url || ""), { ...(env.vars || {}), ...sv });
489
+ const t = env.token || {};
490
+ const headers = { ...(env.headers || {}) };
491
+ const auth = String(rt.auth || "header");
492
+ if (auth !== "none") {
493
+ const tok = await this.token(env);
494
+ if (tok && auth.startsWith("query:")) { const u = new URL(url); u.searchParams.set(auth.slice(6), tok); url = u.toString(); }
495
+ else if (tok) headers[t.header || "Authorization"] = (t.prefix ?? "Bearer ") + tok;
496
+ }
497
+ let conn;
498
+ try {
499
+ conn = await connectWs(url, { headers, ca: this.ca(env), insecure: !!env.insecure, timeout: (Number(env.timeout) || 60) * 1000 });
500
+ } catch (e) {
501
+ if (e.status) throw new ApiError(this.maskText(env, e.message), "http", { status: e.status });
502
+ throw e.kind ? e : explain(e, url.replace(/^ws/, "http"));
503
+ }
504
+ return { conn, env: env.name, url: this.maskText(env, url), mask: (s) => this.maskText(env, s) };
505
+ }
506
+
507
+ // Streaming: devolve a resposta do serviço aberta, para o Studio repassar pedaço a pedaço.
508
+ async open(req) {
509
+ const p = await this.prepare(req);
510
+ const headers = await p.withToken(false);
511
+ const res = await open({ method: p.method, url: req.url, headers, body: p.body, ca: this.ca(p.env), insecure: !!p.env.insecure, timeout: p.timeout });
512
+ return { res, env: p.env, sent: p.sent(headers), mask: (s) => this.maskText(p.env, s) };
513
+ }
514
+ }
@@ -0,0 +1,14 @@
1
+ import './runtime/api-collections.js';
2
+ export const importCollection=globalThis.SagaApiCollections.importCollection;
3
+ // Exportação estática mostra cada pedido. O deck e sua ordem permanecem intactos.
4
+ export function expandApiCollections(spec) {
5
+ const out={...spec,slides:[]};
6
+ for(const slide of spec.slides||[]) {
7
+ if(slide.layout!=='api'||!slide.services?.length){out.slides.push(slide);continue;}
8
+ for(const [i,service]of slide.services.entries()) {
9
+ const copy={...slide,...service,layout:'api',title:[slide.title,service.name].filter(Boolean).join(' · '),id:service.id||`${slide.id||'collection'}-${i+1}`};
10
+ delete copy.services;out.slides.push(copy);
11
+ }
12
+ }
13
+ return out;
14
+ }