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,371 @@
1
+ // Projeto = a pasta da apresentação (como uma pasta aberta no VS Code):
2
+ // <nome>.yaml a apresentação (o arquivo principal: não se apaga nem se renomeia por aqui)
3
+ // imagens/ … o que o deck usa
4
+ // contexto/ anexos, prints colados, planilhas, anotações .md — material para a IA
5
+ // .sagadeck/ do Studio: conversa.json (o chat), cache/ (texto extraído), lixeira/ (o que foi apagado).
6
+ // Protegida: só some junto com o projeto inteiro. Nada disso vai no pacote .sagadeck
7
+ // (src/package.js leva só o que o deck usa).
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import crypto from "node:crypto";
11
+ import JSZip from "jszip";
12
+ import { toNum as rawNum } from "../science.js";
13
+ import { parseCSV, toCSV } from "../csv.js";
14
+ import { extractDocText } from "../ai/context.js";
15
+ import { storedDocumentMaterials } from '../ai/document-materials.js';
16
+
17
+ // número de planilha: 1.234,5 · 40% · R$ 10 · -3
18
+ const toNum = (v) => rawNum(String(v ?? "").trim().replace(/^R\$\s*/i, "").replace(/\s*%$/, ""));
19
+ export const META = ".sagadeck";
20
+ export const CONTEXT = "contexto";
21
+ const TEXT_EXT = new Set(["md", "markdown", "txt", "csv", "tsv", "json", "yaml", "yml", "css", "js", "mjs", "cjs", "jsx", "ts", "tsx", "html", "htm", "svg", "xml",
22
+ "py", "r", "m", "sql", "sh", "bash", "bat", "ps1", "ini", "toml", "cfg", "java", "c", "h", "cpp", "cs", "go", "rs", "tex", "bib", "log"]);
23
+ const SHEET_EXT = new Set(["csv", "tsv", "xlsx"]);
24
+ const IMAGE_EXT = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg"]);
25
+ const extOf = (f) => (String(f).match(/\.([a-z0-9]+)$/i)?.[1] || "").toLowerCase();
26
+ export const kindOf = (name, isMain) => {
27
+ if (isMain) return "deck";
28
+ const e = extOf(name);
29
+ if (SHEET_EXT.has(e)) return "sheet";
30
+ if (IMAGE_EXT.has(e)) return "image";
31
+ if (e === "md" || e === "markdown") return "md";
32
+ if (TEXT_EXT.has(e)) return "text";
33
+ if (e === "pdf") return "pdf";
34
+ if (e === "docx") return "docx";
35
+ if (e === "pptx") return "pptx";
36
+ return "other";
37
+ };
38
+
39
+ // A pasta do deck é um projeto quando é só dele (a biblioteca cria uma pasta por apresentação). Deck solto numa
40
+ // pasta com outros decks não vira projeto: a árvore mostraria arquivos dos outros.
41
+ export function projectOf(file) {
42
+ if (!file || !/\.ya?ml$/i.test(file) || !fs.existsSync(file)) return null;
43
+ const dir = path.dirname(file), main = path.basename(file);
44
+ const others = fs.readdirSync(dir).filter((f) => /\.ya?ml$/i.test(f) && f !== main && !/\.conversa\.json$/i.test(f));
45
+ if (others.length) return null;
46
+ return { dir, main, file };
47
+ }
48
+
49
+ // caminho relativo da pessoa → absoluto dentro do projeto (nada de "..", absoluto ou link para fora)
50
+ export function resolveIn(P, rel = "") {
51
+ const clean = String(rel || "").replace(/\\/g, "/").replace(/^\/+/, "");
52
+ if (clean.split("/").some((part) => part === "..")) throw new Error("Caminho fora do projeto.");
53
+ const abs = path.resolve(P.dir, clean);
54
+ if (abs !== P.dir && !abs.startsWith(P.dir + path.sep)) throw new Error("Caminho fora do projeto.");
55
+ return abs;
56
+ }
57
+ const relOf = (P, abs) => path.relative(P.dir, abs).split(path.sep).join("/");
58
+ const isMeta = (rel) => rel === META || rel.startsWith(META + "/");
59
+ const guard = (P, rel, what) => {
60
+ if (!rel || rel === ".") throw new Error(`Não dá para ${what} a pasta do projeto (use a biblioteca).`);
61
+ if (isMeta(rel)) throw new Error(`A pasta ${META} é do Studio (conversa, cache, lixeira): ela só some junto com o projeto.`);
62
+ if (rel === P.main) throw new Error(`"${P.main}" é a apresentação: para ${what}, use a biblioteca.`);
63
+ };
64
+
65
+ // ---- árvore ----
66
+ export function tree(P) {
67
+ const walk = (abs, depth) => fs.readdirSync(abs, { withFileTypes: true })
68
+ .filter((d) => !(depth === 0 && /\.conversa\.json$/i.test(d.name)))
69
+ .map((d) => {
70
+ const full = path.join(abs, d.name), rel = relOf(P, full);
71
+ if (d.isDirectory()) return { name: d.name, path: rel, type: "dir", protected: rel === META, children: depth < 6 ? walk(full, depth + 1) : [] };
72
+ const st = fs.statSync(full);
73
+ return { name: d.name, path: rel, type: "file", size: st.size, mtime: st.mtimeMs, kind: kindOf(d.name, rel === P.main), protected: rel === P.main || isMeta(rel) };
74
+ })
75
+ .sort((a, b) => (a.path === P.main ? -1 : b.path === P.main ? 1 : a.type !== b.type ? (a.type === "dir" ? -1 : 1) : a.name.localeCompare(b.name, "pt-BR")));
76
+ ensureMeta(P);
77
+ return { name: path.basename(P.dir), main: P.main, entries: walk(P.dir, 0) };
78
+ }
79
+
80
+ export function ensureMeta(P) {
81
+ fs.mkdirSync(path.join(P.dir, META), { recursive: true });
82
+ fs.mkdirSync(path.join(P.dir, CONTEXT), { recursive: true }); // à vista: é aqui que moram anexos, prints e planilhas
83
+ const conv = path.join(P.dir, META, "conversa.json"), legacy = P.file.replace(/\.ya?ml$/i, ".conversa.json");
84
+ // conversa antiga (ao lado do deck) passa para dentro do projeto
85
+ if (fs.existsSync(legacy) && !fs.existsSync(conv)) { try { fs.renameSync(legacy, conv); } catch {} }
86
+ return conv;
87
+ }
88
+ export const conversationFile = (P) => ensureMeta(P);
89
+
90
+ // ---- operações ----
91
+ const safeName = (n) => String(n || "").replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-").replace(/^\.+/, "").trim().slice(0, 120);
92
+ function unique(abs) {
93
+ if (!fs.existsSync(abs)) return abs;
94
+ const dir = path.dirname(abs), ext = path.extname(abs), base = path.basename(abs, ext);
95
+ for (let i = 2; ; i++) { const c = path.join(dir, `${base} (${i})${ext}`); if (!fs.existsSync(c)) return c; }
96
+ }
97
+ export function writeText(P, rel, text) {
98
+ const abs = resolveIn(P, rel), r = relOf(P, abs);
99
+ guard(P, r, "gravar");
100
+ if (!TEXT_EXT.has(extOf(r))) throw new Error("Só arquivos de texto (.md, .txt, .csv…) se editam aqui.");
101
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
102
+ const tmp = `${abs}.tmp-${process.pid}`;
103
+ fs.writeFileSync(tmp, String(text ?? ""), "utf8");
104
+ fs.renameSync(tmp, abs);
105
+ return { path: r };
106
+ }
107
+ export function createFile(P, dirRel, name, text = "") {
108
+ const n = safeName(name);
109
+ if (!n) throw new Error("Dê um nome ao arquivo.");
110
+ const abs = unique(resolveIn(P, path.posix.join(dirRel || "", /\.[a-z0-9]+$/i.test(n) ? n : `${n}.md`)));
111
+ return writeText(P, relOf(P, abs), text);
112
+ }
113
+ export function mkdir(P, dirRel, name) {
114
+ const n = safeName(name);
115
+ if (!n) throw new Error("Dê um nome à pasta.");
116
+ const abs = unique(resolveIn(P, path.posix.join(dirRel || "", n))), r = relOf(P, abs);
117
+ if (isMeta(r)) throw new Error(`A pasta ${META} é do Studio.`);
118
+ fs.mkdirSync(abs, { recursive: true });
119
+ return { path: r };
120
+ }
121
+ export function upload(P, dirRel, name, buf) {
122
+ const n = safeName(name) || `arquivo-${Date.now()}`;
123
+ if (buf.length > 25 * 1024 * 1024) throw new Error("Arquivo grande demais (limite 25 MB).");
124
+ const d = relOf(P, resolveIn(P, dirRel || CONTEXT));
125
+ if (isMeta(d)) throw new Error(`A pasta ${META} é do Studio.`);
126
+ const abs = unique(resolveIn(P, path.posix.join(d, n)));
127
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
128
+ fs.writeFileSync(abs, buf);
129
+ return { path: relOf(P, abs) };
130
+ }
131
+ export function rename(P, fromRel, toName) {
132
+ const abs = resolveIn(P, fromRel), r = relOf(P, abs);
133
+ guard(P, r, "renomear");
134
+ const n = safeName(toName);
135
+ if (!n) throw new Error("Nome vazio.");
136
+ const to = resolveIn(P, path.posix.join(path.posix.dirname(r), n));
137
+ if (fs.existsSync(to)) throw new Error(`Já existe "${n}" nessa pasta.`);
138
+ if (isMeta(relOf(P, to))) throw new Error(`A pasta ${META} é do Studio.`);
139
+ fs.renameSync(abs, to);
140
+ return { path: relOf(P, to) };
141
+ }
142
+ export function move(P, fromRel, toDirRel) {
143
+ const abs = resolveIn(P, fromRel), r = relOf(P, abs);
144
+ guard(P, r, "mover");
145
+ const dest = resolveIn(P, toDirRel || ""), d = relOf(P, dest);
146
+ if (isMeta(d)) throw new Error(`A pasta ${META} é do Studio.`);
147
+ if (dest === abs || dest.startsWith(abs + path.sep)) throw new Error("Não dá para mover uma pasta para dentro dela mesma.");
148
+ const to = unique(path.join(dest, path.basename(abs)));
149
+ fs.renameSync(abs, to);
150
+ return { path: relOf(P, to) };
151
+ }
152
+ // apagar = mandar para a lixeira do projeto (.sagadeck/lixeira), de onde dá para restaurar
153
+ export function remove(P, rel) {
154
+ const abs = resolveIn(P, rel), r = relOf(P, abs);
155
+ guard(P, r, "apagar");
156
+ if (!fs.existsSync(abs)) throw new Error("Não existe.");
157
+ const bin = path.join(P.dir, META, "lixeira");
158
+ fs.mkdirSync(bin, { recursive: true });
159
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
160
+ const to = path.join(bin, `${stamp}__${r.replace(/\//g, "__")}`);
161
+ fs.renameSync(abs, to);
162
+ return { trashed: relOf(P, to) };
163
+ }
164
+ export function restore(P, trashedRel) {
165
+ const abs = resolveIn(P, trashedRel), r = relOf(P, abs);
166
+ if (!r.startsWith(`${META}/lixeira/`)) throw new Error("Só itens da lixeira do projeto.");
167
+ const orig = path.basename(abs).split("__").slice(1).join("/");
168
+ const to = unique(resolveIn(P, orig));
169
+ fs.mkdirSync(path.dirname(to), { recursive: true });
170
+ fs.renameSync(abs, to);
171
+ return { path: relOf(P, to) };
172
+ }
173
+
174
+ // ---- planilhas: CSV/TSV e XLSX (sem biblioteca extra: o xlsx é um zip de XML) ----
175
+ const unxml = (s) => String(s).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
176
+ const colIndex = (ref) => { const L = String(ref).match(/^[A-Z]+/)?.[0] || "A"; let n = 0; for (const c of L) n = n * 26 + (c.charCodeAt(0) - 64); return n - 1; };
177
+ export async function readSheet(abs) {
178
+ const ext = extOf(abs);
179
+ if (ext === "csv" || ext === "tsv") {
180
+ const csv = parseCSV(fs.readFileSync(abs, "utf8"), ext === "tsv" ? "\t" : undefined);
181
+ return { sheets: [{ name: path.basename(abs), rows: csv.rows }], editable: true, delimiter: csv.delimiter };
182
+ }
183
+ if (ext !== "xlsx") throw new Error("Planilha: use .csv, .tsv ou .xlsx.");
184
+ const zip = await JSZip.loadAsync(fs.readFileSync(abs));
185
+ const shared = [];
186
+ const ss = await zip.file("xl/sharedStrings.xml")?.async("string");
187
+ if (ss) for (const si of ss.matchAll(/<si>([\s\S]*?)<\/si>/g)) shared.push(unxml([...si[1].matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map((m) => m[1]).join("")));
188
+ const wb = (await zip.file("xl/workbook.xml")?.async("string")) || "";
189
+ const rels = (await zip.file("xl/_rels/workbook.xml.rels")?.async("string")) || "";
190
+ const target = new Map([...rels.matchAll(/<Relationship[^>]*Id="([^"]+)"[^>]*Target="([^"]+)"/g)].map((m) => [m[1], m[2].replace(/^\/?(xl\/)?/, "xl/")]));
191
+ const list = [...wb.matchAll(/<sheet[^>]*name="([^"]+)"[^>]*r:id="([^"]+)"/g)].map((m) => ({ name: unxml(m[1]), file: target.get(m[2]) }));
192
+ const files = list.length ? list : Object.keys(zip.files).filter((n) => /^xl\/worksheets\/sheet\d+\.xml$/.test(n)).map((f, i) => ({ name: `Planilha ${i + 1}`, file: f }));
193
+ const sheets = [];
194
+ for (const sh of files) {
195
+ const xml = await zip.file(sh.file)?.async("string");
196
+ if (!xml) continue;
197
+ const rows = [];
198
+ for (const rm of xml.matchAll(/<row[^>]*>([\s\S]*?)<\/row>/g)) {
199
+ const row = [];
200
+ for (const cm of rm[1].matchAll(/<c([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/g)) {
201
+ const attrs = cm[1], body = cm[2] || "";
202
+ const ref = attrs.match(/r="([A-Z]+\d+)"/)?.[1], t = attrs.match(/t="([^"]+)"/)?.[1];
203
+ const v = body.match(/<v>([\s\S]*?)<\/v>/)?.[1];
204
+ let val = "";
205
+ if (t === "s") val = shared[Number(v)] ?? "";
206
+ else if (t === "inlineStr") val = unxml([...body.matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map((m) => m[1]).join(""));
207
+ else if (t === "b") val = v === "1" ? "VERDADEIRO" : "FALSO";
208
+ else val = v != null ? unxml(v) : "";
209
+ row[ref ? colIndex(ref) : row.length] = val;
210
+ }
211
+ rows.push(Array.from(row, (x) => x ?? ""));
212
+ }
213
+ while (rows.length && rows.at(-1).every((c) => !String(c).trim())) rows.pop();
214
+ sheets.push({ name: sh.name, rows });
215
+ }
216
+ if (!sheets.length) throw new Error("xlsx sem planilhas.");
217
+ return { sheets };
218
+ }
219
+
220
+ // grava a planilha editada no mesmo formato do arquivo (separador, fim de linha, BOM, linha sep=)
221
+ export function writeSheet(P, rel, rows) {
222
+ const abs = resolveIn(P, rel);
223
+ const ext = extOf(abs);
224
+ if (ext !== "csv" && ext !== "tsv") throw new Error("Só .csv e .tsv se editam aqui (xlsx abre só para ler).");
225
+ if (!Array.isArray(rows) || rows.some((r) => !Array.isArray(r))) throw new Error("Planilha inválida.");
226
+ const old = fs.existsSync(abs) ? parseCSV(fs.readFileSync(abs, "utf8"), ext === "tsv" ? "\t" : undefined) : { delimiter: ext === "tsv" ? "\t" : ",", eol: "\n" };
227
+ const tmp = abs + ".tmp";
228
+ const clean = rows.map((r) => r.map((c) => String(c ?? "")));
229
+ while (clean.length > 1 && clean.at(-1).every((c) => c === "")) clean.pop(); // linha vazia no fim (o cursor passou dela) não vai para o arquivo
230
+ fs.writeFileSync(tmp, toCSV(clean, old));
231
+ fs.renameSync(tmp, abs);
232
+ return { path: relOf(P, abs), delimiter: old.delimiter };
233
+ }
234
+
235
+ // ---- tipos das colunas (sem IA: o que dá para ver pelos valores) ----
236
+ const DATE_RE = /^(\d{4}-\d{2}(-\d{2})?|\d{1,2}\/\d{1,2}(\/\d{2,4})?|(jan|fev|mar|abr|mai|jun|jul|ago|set|out|nov|dez)[a-z]*\.?(\s*\/?\s*\d{2,4})?|q[1-4]|[1-4]º?\s*tri\w*)$/i;
237
+ const DATEY_HEAD = /\b(data|date|dia|m[eê]s|month|ano|year|per[ií]odo|semana|trimestre)\b/i;
238
+ export function inferColumns(rows) {
239
+ const head = rows[0] || [], body = rows.slice(1).filter((r) => r.some((c) => String(c).trim()));
240
+ const hasHeader = head.some((c) => String(c).trim() && toNum(c) == null);
241
+ const data = hasHeader ? body : rows;
242
+ const n = Math.max(0, ...rows.map((r) => r.length));
243
+ const cols = Array.from({ length: n }, (_, j) => {
244
+ const name = hasHeader ? String(head[j] ?? "").trim() || `Coluna ${j + 1}` : `Coluna ${j + 1}`;
245
+ const vals = data.map((r) => String(r[j] ?? "").trim()).filter(Boolean);
246
+ const nums = vals.map((v) => toNum(v)).filter((v) => v != null);
247
+ const pct = vals.filter((v) => /%\s*$/.test(v)).length;
248
+ const dates = vals.filter((v) => DATE_RE.test(v)).length;
249
+ const distinct = new Set(vals).size;
250
+ let type = "text";
251
+ if (vals.length && dates >= vals.length * 0.8) type = "date";
252
+ else if (vals.length && nums.length >= vals.length * 0.9) {
253
+ const serial = nums.every((v) => v > 20000 && v < 80000 && Number.isInteger(v));
254
+ const years = nums.every((v) => Number.isInteger(v) && v >= 1900 && v <= 2100);
255
+ type = DATEY_HEAD.test(name) && (serial || years) ? "date" : pct >= vals.length * 0.8 ? "percent" : "number";
256
+ } else if (vals.length && distinct <= Math.max(12, vals.length * 0.6)) type = "category";
257
+ const stats = nums.length ? { min: Math.min(...nums), max: Math.max(...nums), sum: nums.reduce((a, b) => a + b, 0) } : null;
258
+ return { name, index: j, type, distinct, count: vals.length, stats, sample: vals.slice(0, 3) };
259
+ });
260
+ return { hasHeader, columns: cols, rows: data };
261
+ }
262
+ const xlsxDate = (v) => { const n = toNum(v); if (n == null || n < 20000 || n > 80000) return String(v); const d = new Date(Math.round((n - 25569) * 864e5)); return d.toISOString().slice(0, 10); };
263
+
264
+ // ---- sugestões de gráfico (regras; a IA pode trocar tipo, título e eixos) ----
265
+ // cada sugestão é um slide pronto; `from` guarda de onde vieram os dados (atualizar depois)
266
+ export function suggestCharts(info, { file, sheet } = {}) {
267
+ const cols = info.columns, rows = info.rows;
268
+ const nums = cols.filter((c) => c.type === "number" || c.type === "percent");
269
+ const dates = cols.filter((c) => c.type === "date");
270
+ const cats = cols.filter((c) => c.type === "category" || c.type === "text");
271
+ const label = (c, r) => (c.type === "date" && /^\d+$/.test(String(r[c.index]).trim()) && Number(r[c.index]) > 20000 ? xlsxDate(r[c.index]) : String(r[c.index] ?? "").trim());
272
+ const val = (c, r) => toNum(r[c.index]);
273
+ const from = (x, ys) => ({ file, ...(sheet ? { sheet } : {}), columns: [x.name, ...ys.map((y) => y.name)] });
274
+ const out = [];
275
+ const suffixOf = (ys) => (ys.every((y) => y.type === "percent") ? "%" : undefined);
276
+ const mk = (id, title, why, chart, extra = {}) => out.push({ id, title, why, slide: { layout: "chart", title, chart: { ...chart, ...(chart.suffix ? {} : suffixOf(extra.ys || []) ? { suffix: "%" } : {}) }, ...(extra.from ? { from: extra.from } : {}) } });
277
+ const rowsOk = (x, ys) => rows.filter((r) => label(x, r) && ys.some((y) => val(y, r) != null)).slice(0, 60);
278
+ // tempo × números: linha (tendência)
279
+ if (dates.length && nums.length) {
280
+ const x = dates[0], ys = nums.filter((c) => (c.type === "percent") === (nums[0].type === "percent")).slice(0, 4), rs = rowsOk(x, ys); // não mistura % com número (a escala mentiria)
281
+ if (rs.length >= 2) mk("linha", `${ys.map((y) => y.name).join(" e ")} ao longo do tempo`, `"${x.name}" é tempo e ${ys.length > 1 ? "há várias séries numéricas" : `"${ys[0].name}" é número`}: linha mostra a tendência.`,
282
+ { chart: "line", labels: rs.map((r) => label(x, r)), series: ys.map((y) => ({ name: y.name, values: rs.map((r) => val(y, r)) })), xLabel: x.name, yLabel: ys.length === 1 ? ys[0].name : undefined }, { from: from(x, ys), ys });
283
+ }
284
+ // categoria × 1 número: barras ordenadas; poucas partes somando um todo: rosca
285
+ const catX = cats.find((c) => c.distinct >= 2 && c.distinct <= 30) || (dates.length && !nums.length ? null : null);
286
+ if (catX && nums.length) {
287
+ const y = nums[0], rs = rowsOk(catX, [y]).sort((a, b) => val(y, b) - val(y, a));
288
+ if (rs.length >= 2) {
289
+ mk("barras", `${y.name} por ${catX.name}`, `"${catX.name}" é categoria e "${y.name}" é número: barras ordenadas comparam de relance.`,
290
+ { chart: "bar", data: rs.slice(0, 12).map((r) => ({ label: label(catX, r), value: val(y, r) })), highlight: [0] }, { from: from(catX, [y]), ys: [y] });
291
+ const total = rs.reduce((a, r) => a + (val(y, r) || 0), 0);
292
+ if (rs.length <= 6 && rs.every((r) => val(y, r) >= 0) && (y.type === "percent" ? Math.abs(total - 100) < 3 : true))
293
+ mk("rosca", `Composição de ${y.name}`, `Poucas partes (${rs.length}) de um todo: a rosca mostra a fatia de cada uma.`,
294
+ { chart: "donut", parts: rs.map((r) => ({ label: label(catX, r), value: val(y, r) })), center: y.type === "percent" ? "100%" : undefined }, { from: from(catX, [y]), ys: [y] });
295
+ }
296
+ if (nums.length >= 2) {
297
+ const ys = nums.filter((c) => (c.type === "percent") === (nums[0].type === "percent")).slice(0, 3), rs2 = rowsOk(catX, ys).slice(0, 8);
298
+ if (rs2.length >= 2) mk("colunas", `${ys.map((y) => y.name).join(" × ")} por ${catX.name}`, `Várias medidas por "${catX.name}": colunas agrupadas comparam as séries lado a lado.`,
299
+ { chart: "column", labels: rs2.map((r) => label(catX, r)), series: ys.map((yy) => ({ name: yy.name, values: rs2.map((r) => val(yy, r)) })), xLabel: catX.name }, { from: from(catX, ys), ys });
300
+ }
301
+ }
302
+ // dois números: dispersão (fórmulas e funções, com os pontos)
303
+ if (nums.length >= 2) {
304
+ const [a, b] = nums, pts = rows.map((r) => [val(a, r), val(b, r)]).filter(([x, y]) => x != null && y != null).slice(0, 300);
305
+ if (pts.length >= 3) out.push({ id: "dispersao", title: `${b.name} × ${a.name}`, why: `Duas colunas numéricas: a dispersão mostra se "${b.name}" acompanha "${a.name}".`,
306
+ slide: { layout: "science", title: `${b.name} × ${a.name}`, plot: { points: pts, pointsName: b.name, x: [Math.min(...pts.map((p) => p[0])), Math.max(...pts.map((p) => p[0]))], layout: { xaxis: { title: { text: a.name } }, yaxis: { title: { text: b.name } } } }, from: { file, ...(sheet ? { sheet } : {}), columns: [a.name, b.name] } } });
307
+ }
308
+ // só números, sem rótulo: colunas na ordem das linhas
309
+ if (!out.length && nums.length) {
310
+ const y = nums[0], vs = rows.map((r) => val(y, r)).filter((v) => v != null).slice(0, 20);
311
+ if (vs.length >= 2) mk("sequencia", y.name, `Só números: colunas na ordem das linhas.`, { chart: "column", data: vs.map((v, i) => ({ label: String(i + 1), value: v })) }, { ys: [y] });
312
+ }
313
+ return out;
314
+ }
315
+
316
+ // ---- material para a IA: o que está em contexto/ (texto extraído fica em cache) ----
317
+ export async function contextMaterials(P, { maxDocs = 8, maxChars = 20000 } = {}) {
318
+ const dir = path.join(P.dir, CONTEXT);
319
+ if (!fs.existsSync(dir)) return [];
320
+ const files = [];
321
+ const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).forEach((e) => { const f = path.join(d, e.name); if (e.isDirectory()) walk(f); else files.push(f); });
322
+ walk(dir);
323
+ const cacheDir = path.join(P.dir, META, "cache");
324
+ const out = storedDocumentMaterials(P.dir);
325
+ for (const f of files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)) {
326
+ if (f.startsWith(path.join(dir,'documentos') + path.sep) || f.startsWith(path.join(dir,'visuais') + path.sep)) continue;
327
+ if (out.length >= maxDocs) break;
328
+ const k = kindOf(f);
329
+ if (k === "image" || k === "other") continue;
330
+ const st = fs.statSync(f);
331
+ const key = crypto.createHash("sha1").update(`${f}|${st.mtimeMs}|${st.size}`).digest("hex");
332
+ const cached = path.join(cacheDir, `${key}.txt`);
333
+ let text;
334
+ try {
335
+ if (fs.existsSync(cached)) text = fs.readFileSync(cached, "utf8");
336
+ else { text = (await extractDocText(path.basename(f), fs.readFileSync(f))).text; fs.mkdirSync(cacheDir, { recursive: true }); fs.writeFileSync(cached, text); }
337
+ } catch { continue; }
338
+ if (text.trim()) out.push({ name: relOf(P, f), text: text.slice(0, maxChars), detail: `arquivo do projeto (${k})` });
339
+ }
340
+ return out;
341
+ }
342
+
343
+ // monta o slide de um gráfico a partir das colunas escolhidas (pela IA ou guardadas em `from` para atualizar)
344
+ // { type: line|bar|column|donut|scatter, x: "coluna", ys: ["coluna", …], title, xLabel, yLabel }
345
+ export function buildChart(info, want, { file, sheet } = {}) {
346
+ const byName = (n) => info.columns.find((c) => c.name === n) || info.columns.find((c) => c.name.toLowerCase() === String(n || "").toLowerCase());
347
+ const x = byName(want.x), ys = [].concat(want.ys || []).map(byName).filter(Boolean);
348
+ if (!x || !ys.length) throw new Error(`colunas não encontradas: ${[want.x, ...[].concat(want.ys || [])].join(", ")}`);
349
+ const label = (r) => (x.type === "date" && Number(r[x.index]) > 20000 ? xlsxDate(r[x.index]) : String(r[x.index] ?? "").trim());
350
+ const val = (c, r) => toNum(r[c.index]);
351
+ const rows = info.rows.filter((r) => label(r) && ys.some((y) => val(y, r) != null)).slice(0, 60);
352
+ const from = { file, ...(sheet ? { sheet } : {}), columns: [x.name, ...ys.map((y) => y.name)], type: want.type };
353
+ const title = String(want.title || `${ys.map((y) => y.name).join(" e ")} por ${x.name}`);
354
+ const axes = { ...(want.xLabel ? { xLabel: String(want.xLabel) } : {}), ...(want.yLabel ? { yLabel: String(want.yLabel) } : {}) };
355
+ const suffix = ys.every((y) => y.type === "percent") ? { suffix: "%" } : {};
356
+ const type = ["line", "bar", "column", "donut", "scatter"].includes(want.type) ? want.type : "column";
357
+ if (type === "scatter") {
358
+ const pts = info.rows.map((r) => [toNum(r[x.index]), val(ys[0], r)]).filter(([a, b]) => a != null && b != null).slice(0, 300);
359
+ return { layout: "science", title, plot: { points: pts, pointsName: ys[0].name, x: [Math.min(...pts.map((p) => p[0])), Math.max(...pts.map((p) => p[0]))], layout: { xaxis: { title: { text: want.xLabel || x.name } }, yaxis: { title: { text: want.yLabel || ys[0].name } } } }, from };
360
+ }
361
+ let chart;
362
+ if (type === "line" || ((type === "column" || type === "bar") && ys.length > 1)) chart = { chart: type, labels: rows.map(label), series: ys.map((y) => ({ name: y.name, values: rows.map((r) => val(y, r)) })), ...axes, ...suffix };
363
+ else if (type === "donut") chart = { chart: "donut", parts: rows.slice(0, 8).map((r) => ({ label: label(r), value: val(ys[0], r) ?? 0 })) };
364
+ else chart = { chart: type, data: rows.map((r) => ({ label: label(r), value: val(ys[0], r) ?? 0 })), ...(type === "column" ? axes : {}), ...suffix };
365
+ return { layout: "chart", title, chart, from };
366
+ }
367
+
368
+ // resumo das colunas para a IA: nome, tipo visto, exemplos e faixa (nunca a planilha inteira)
369
+ export function columnsSummary(info) {
370
+ return info.columns.map((c) => `- "${c.name}": ${c.type}, ${c.count} valores, ${c.distinct} distintos${c.stats ? `, de ${c.stats.min} a ${c.stats.max}` : ""}; ex.: ${c.sample.map((v) => JSON.stringify(v)).join(", ")}`).join("\n");
371
+ }
@@ -0,0 +1,28 @@
1
+ // Importador compartilhado por Node e Studio; preserva variáveis, nunca inventa endpoints.
2
+ (function(g){
3
+ function importCollection(raw){
4
+ if(typeof raw==='string')raw=JSON.parse(raw);
5
+ const services=[];
6
+ function walk(items,prefix=''){
7
+ for(const item of items||[]){
8
+ const name=[prefix,item.name].filter(Boolean).join(' / ');
9
+ if(item.item){walk(item.item,name);continue;}
10
+ const r=item.request;if(!r)continue;
11
+ const url=typeof r.url==='string'?r.url:r.url?.raw;
12
+ if(!url)throw new Error(`URL ausente em ${name}`);
13
+ const headers=Object.fromEntries((r.header||[]).filter(h=>!h.disabled).map(h=>[h.key,h.value]));
14
+ let body=r.body?.raw;
15
+ if(body){try{body=JSON.parse(body)}catch{}}
16
+ const form=r.body?.mode==='urlencoded'?Object.fromEntries((r.body.urlencoded||[]).filter(x=>!x.disabled).map(x=>[x.key,x.value])):undefined;
17
+ if(r.body?.mode==='formdata')throw new Error(`Importação de ${name}: use o campo de arquivo do slide para multipart.`);
18
+ services.push({id:`servico-${services.length+1}`,name:name||`Serviço ${services.length+1}`,request:{method:r.method||'GET',url,headers,...(body!=null?{body}:{}),...(form?{form}:{})}});
19
+ }
20
+ }
21
+ if(Array.isArray(raw.services))return structuredClone(raw.services);
22
+ walk(raw.item);
23
+ if(!services.length)throw new Error('A coleção precisa ter pedidos Postman ou uma lista services.');
24
+ if(services.length>100)throw new Error('Divida a coleção: limite de 100 serviços por slide.');
25
+ return services;
26
+ }
27
+ g.SagaApiCollections={importCollection};
28
+ })(globalThis);