@kortix/sandbox 0.4.1

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 (246) hide show
  1. package/config/customize.sh +143 -0
  2. package/config/kortix-env-setup.sh +25 -0
  3. package/kortix-master/package.json +22 -0
  4. package/kortix-master/src/config.ts +22 -0
  5. package/kortix-master/src/index.ts +44 -0
  6. package/kortix-master/src/routes/env.ts +65 -0
  7. package/kortix-master/src/routes/proxy.ts +108 -0
  8. package/kortix-master/src/routes/update.ts +185 -0
  9. package/kortix-master/src/services/proxy.ts +43 -0
  10. package/kortix-master/src/services/secret-store.ts +156 -0
  11. package/kortix-master/tsconfig.json +14 -0
  12. package/opencode/agents/kortix-browser.md +142 -0
  13. package/opencode/agents/kortix-build.md +62 -0
  14. package/opencode/agents/kortix-explore.md +66 -0
  15. package/opencode/agents/kortix-image-gen.md +33 -0
  16. package/opencode/agents/kortix-main.md +450 -0
  17. package/opencode/agents/kortix-plan.md +100 -0
  18. package/opencode/agents/kortix-research.md +84 -0
  19. package/opencode/agents/kortix-sheets.md +61 -0
  20. package/opencode/agents/kortix-slides.md +64 -0
  21. package/opencode/agents/kortix-web-dev.md +572 -0
  22. package/opencode/commands/email.md +36 -0
  23. package/opencode/commands/init.md +43 -0
  24. package/opencode/commands/journal.md +44 -0
  25. package/opencode/commands/memory-init.md +81 -0
  26. package/opencode/commands/memory-search.md +50 -0
  27. package/opencode/commands/memory-status.md +56 -0
  28. package/opencode/commands/research.md +36 -0
  29. package/opencode/commands/search.md +38 -0
  30. package/opencode/commands/slides.md +32 -0
  31. package/opencode/commands/spreadsheet.md +30 -0
  32. package/opencode/memory.json +37 -0
  33. package/opencode/ocx.jsonc +10 -0
  34. package/opencode/opencode.jsonc +103 -0
  35. package/opencode/package.json +25 -0
  36. package/opencode/patches/apply.sh +19 -0
  37. package/opencode/patches/opencode-pty-spawn.txt +49 -0
  38. package/opencode/plugin/background-agents.ts.disabled +483 -0
  39. package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
  40. package/opencode/plugin/kdco-primitives/index.ts +26 -0
  41. package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
  42. package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
  43. package/opencode/plugin/kdco-primitives/shell.ts +138 -0
  44. package/opencode/plugin/kdco-primitives/temp.ts +36 -0
  45. package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
  46. package/opencode/plugin/kdco-primitives/types.ts +13 -0
  47. package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
  48. package/opencode/plugin/memory.ts +306 -0
  49. package/opencode/plugin/worktree/state.ts +412 -0
  50. package/opencode/plugin/worktree/terminal.ts +1002 -0
  51. package/opencode/plugin/worktree.ts +861 -0
  52. package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
  53. package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
  54. package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
  55. package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
  56. package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
  57. package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
  58. package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
  59. package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
  60. package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
  61. package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
  62. package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
  63. package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
  64. package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
  65. package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
  66. package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
  67. package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
  68. package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
  69. package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
  70. package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
  71. package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
  72. package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
  73. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
  74. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
  75. package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
  76. package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
  77. package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
  78. package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
  79. package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
  80. package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
  81. package/opencode/skills/KORTIX-email/SKILL.md +145 -0
  82. package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
  83. package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
  84. package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
  85. package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
  86. package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
  87. package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
  88. package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
  89. package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
  90. package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
  91. package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
  92. package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
  93. package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
  94. package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
  95. package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
  96. package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
  97. package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
  98. package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
  99. package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
  100. package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
  101. package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
  102. package/opencode/skills/KORTIX-pdf/forms.md +36 -0
  103. package/opencode/skills/KORTIX-pdf/reference.md +105 -0
  104. package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
  105. package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
  106. package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
  107. package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
  108. package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
  109. package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
  110. package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
  111. package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
  112. package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
  113. package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
  114. package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
  115. package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
  116. package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
  117. package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
  118. package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
  119. package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
  120. package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
  121. package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
  122. package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
  123. package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
  124. package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
  125. package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
  126. package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
  127. package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
  128. package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
  129. package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
  130. package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
  131. package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
  132. package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
  133. package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
  134. package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
  135. package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
  136. package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
  137. package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
  138. package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
  139. package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
  140. package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
  141. package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
  142. package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
  143. package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
  144. package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
  145. package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
  146. package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
  147. package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
  148. package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
  149. package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
  150. package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
  151. package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
  152. package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
  153. package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
  154. package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
  155. package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
  156. package/opencode/skills/KORTIX-session-search/Untitled +1 -0
  157. package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
  158. package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
  159. package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
  160. package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
  161. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
  162. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
  163. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
  164. package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
  165. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  166. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  167. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  168. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  169. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  170. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  171. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  172. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  173. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  174. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  175. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  176. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  177. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  178. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  179. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  180. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  181. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  182. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  183. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  184. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  185. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  186. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  187. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  188. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  189. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  190. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  191. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  192. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  193. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  194. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  195. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  196. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
  197. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  198. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  199. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  200. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  201. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  202. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  203. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  204. package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
  205. package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
  206. package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
  207. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
  208. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
  209. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
  210. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
  211. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
  212. package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
  213. package/opencode/tools/image-gen.ts +342 -0
  214. package/opencode/tools/image-search.ts +190 -0
  215. package/opencode/tools/memory-get.ts +168 -0
  216. package/opencode/tools/memory-search.ts +247 -0
  217. package/opencode/tools/presentation-gen.ts +723 -0
  218. package/opencode/tools/scrape-webpage.ts +115 -0
  219. package/opencode/tools/scripts/.python-version +1 -0
  220. package/opencode/tools/scripts/convert_pdf.py +184 -0
  221. package/opencode/tools/scripts/convert_pptx.py +562 -0
  222. package/opencode/tools/scripts/pyproject.toml +11 -0
  223. package/opencode/tools/scripts/uv.lock +287 -0
  224. package/opencode/tools/scripts/validate_slide.py +74 -0
  225. package/opencode/tools/show-user.ts +217 -0
  226. package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
  227. package/opencode/tools/tests/image-gen.test.ts +215 -0
  228. package/opencode/tools/tests/image-search.test.ts +125 -0
  229. package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
  230. package/opencode/tools/tests/presentation-gen.test.ts +389 -0
  231. package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
  232. package/opencode/tools/tests/show-user.test.ts +241 -0
  233. package/opencode/tools/tests/video-gen.test.ts +110 -0
  234. package/opencode/tools/tests/web-search.test.ts +106 -0
  235. package/opencode/tools/video-gen.ts +200 -0
  236. package/opencode/tools/web-search.ts +153 -0
  237. package/opencode/tsconfig.json +29 -0
  238. package/package.json +36 -0
  239. package/patch-agent-browser.js +100 -0
  240. package/postinstall.sh +88 -0
  241. package/services/KORTIX-presentation-viewer/run +37 -0
  242. package/services/agent-browser-viewer/run +48 -0
  243. package/services/kortix-master/run +16 -0
  244. package/services/lss-sync/run +22 -0
  245. package/services/opencode-serve/run +25 -0
  246. package/services/opencode-web/run +21 -0
@@ -0,0 +1,723 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import {
3
+ writeFileSync,
4
+ readFileSync,
5
+ mkdirSync,
6
+ existsSync,
7
+ readdirSync,
8
+ rmSync,
9
+ unlinkSync,
10
+ statSync,
11
+ } from "fs";
12
+ import { resolve, join, dirname } from "path";
13
+ import { execSync } from "child_process";
14
+
15
+ const PRESENTATIONS_DIR = "presentations";
16
+
17
+ type Action =
18
+ | "create_slide"
19
+ | "list_slides"
20
+ | "delete_slide"
21
+ | "list_presentations"
22
+ | "delete_presentation"
23
+ | "validate_slide"
24
+ | "export_pdf"
25
+ | "export_pptx"
26
+ | "preview";
27
+
28
+ const SCRIPTS_DIR = resolve(
29
+ dirname(new URL(import.meta.url).pathname),
30
+ "scripts",
31
+ );
32
+
33
+ const SKILLS_DIR = resolve(
34
+ dirname(new URL(import.meta.url).pathname),
35
+ "..",
36
+ "skills",
37
+ "KORTIX-presentation-viewer",
38
+ );
39
+
40
+ interface SlideMetadata {
41
+ title: string;
42
+ filename: string;
43
+ file_path: string;
44
+ created_at: string;
45
+ }
46
+
47
+ interface PresentationMetadata {
48
+ presentation_name: string;
49
+ title: string;
50
+ description: string;
51
+ slides: Record<string, SlideMetadata>;
52
+ created_at: string;
53
+ updated_at: string;
54
+ }
55
+
56
+ function sanitizeFilename(name: string): string {
57
+ return name
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9_-]/g, "")
60
+ .slice(0, 80);
61
+ }
62
+
63
+ function ensurePresentationsDir(base: string): string {
64
+ const dir = resolve(base, PRESENTATIONS_DIR);
65
+ try {
66
+ mkdirSync(dir, { recursive: true });
67
+ } catch (err: unknown) {
68
+ const code = (err as { code?: string }).code;
69
+ if (code === "EACCES" || code === "EROFS") {
70
+ const fallback = resolve(process.cwd(), PRESENTATIONS_DIR);
71
+ mkdirSync(fallback, { recursive: true });
72
+ return fallback;
73
+ }
74
+ throw err;
75
+ }
76
+ return dir;
77
+ }
78
+
79
+ function ensurePresentationDir(
80
+ base: string,
81
+ name: string,
82
+ ): { safeName: string; path: string; presDir: string } {
83
+ const safeName = sanitizeFilename(name);
84
+ const presDir = ensurePresentationsDir(base);
85
+ const path = join(presDir, safeName);
86
+ mkdirSync(path, { recursive: true });
87
+ return { safeName, path, presDir };
88
+ }
89
+
90
+ function loadMetadata(presentationPath: string): PresentationMetadata {
91
+ const metaPath = join(presentationPath, "metadata.json");
92
+ if (existsSync(metaPath)) {
93
+ return JSON.parse(readFileSync(metaPath, "utf-8"));
94
+ }
95
+ return {
96
+ presentation_name: "",
97
+ title: "Presentation",
98
+ description: "",
99
+ slides: {},
100
+ created_at: new Date().toISOString(),
101
+ updated_at: new Date().toISOString(),
102
+ };
103
+ }
104
+
105
+ function saveMetadata(
106
+ presentationPath: string,
107
+ metadata: PresentationMetadata,
108
+ ): void {
109
+ metadata.updated_at = new Date().toISOString();
110
+ writeFileSync(
111
+ join(presentationPath, "metadata.json"),
112
+ JSON.stringify(metadata, null, 2),
113
+ );
114
+ }
115
+
116
+ function createSlideHtml(
117
+ content: string,
118
+ slideNumber: number,
119
+ presentationTitle: string,
120
+ ): string {
121
+ return `<!DOCTYPE html>
122
+ <html lang="en">
123
+ <head>
124
+ <meta charset="UTF-8">
125
+ <meta name="viewport" content="width=1920, initial-scale=1.0">
126
+ <title>${presentationTitle} - Slide ${slideNumber}</title>
127
+ <link rel="preconnect" href="https://fonts.googleapis.com">
128
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
129
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
130
+ <script src="https://d3js.org/d3.v7.min.js" async></script>
131
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1" async></script>
132
+ <style>
133
+ * {
134
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
135
+ }
136
+ body {
137
+ height: 1080px;
138
+ width: 1920px;
139
+ margin: 0;
140
+ padding: 0;
141
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
142
+ }
143
+ </style>
144
+ </head>
145
+ <body>
146
+ ${content}
147
+ </body>
148
+ </html>`;
149
+ }
150
+
151
+ function doCreateSlide(
152
+ base: string,
153
+ presentationName: string,
154
+ slideNumber: number,
155
+ slideTitle: string,
156
+ content: string,
157
+ presentationTitle: string,
158
+ ): string {
159
+ if (!presentationName)
160
+ return JSON.stringify({
161
+ success: false,
162
+ error: "presentation_name is required",
163
+ });
164
+ if (!slideNumber || slideNumber < 1)
165
+ return JSON.stringify({
166
+ success: false,
167
+ error: "slide_number must be >= 1",
168
+ });
169
+ if (!slideTitle)
170
+ return JSON.stringify({ success: false, error: "slide_title is required" });
171
+ if (!content)
172
+ return JSON.stringify({ success: false, error: "content is required" });
173
+
174
+ const { safeName, path: presPath, presDir } = ensurePresentationDir(
175
+ base,
176
+ presentationName,
177
+ );
178
+
179
+ mkdirSync(join(presDir, "images"), {
180
+ recursive: true,
181
+ });
182
+
183
+ const metadata = loadMetadata(presPath);
184
+ metadata.presentation_name = presentationName;
185
+ if (presentationTitle !== "Presentation") {
186
+ metadata.title = presentationTitle;
187
+ }
188
+
189
+ const html = createSlideHtml(content, slideNumber, presentationTitle);
190
+ const filename = `slide_${String(slideNumber).padStart(2, "0")}.html`;
191
+ writeFileSync(join(presPath, filename), html);
192
+
193
+ const relPath = `${PRESENTATIONS_DIR}/${safeName}/${filename}`;
194
+ metadata.slides[String(slideNumber)] = {
195
+ title: slideTitle,
196
+ filename,
197
+ file_path: relPath,
198
+ created_at: new Date().toISOString(),
199
+ };
200
+ saveMetadata(presPath, metadata);
201
+ generateViewer(presPath, metadata);
202
+
203
+ return JSON.stringify(
204
+ {
205
+ success: true,
206
+ action: "create_slide",
207
+ presentation_name: presentationName,
208
+ presentation_path: `${PRESENTATIONS_DIR}/${safeName}`,
209
+ slide_number: slideNumber,
210
+ slide_title: slideTitle,
211
+ slide_file: relPath,
212
+ total_slides: Object.keys(metadata.slides).length,
213
+ },
214
+ null,
215
+ 2,
216
+ );
217
+ }
218
+
219
+ function doListSlides(base: string, presentationName: string): string {
220
+ if (!presentationName)
221
+ return JSON.stringify({
222
+ success: false,
223
+ error: "presentation_name is required",
224
+ });
225
+
226
+ const safeName = sanitizeFilename(presentationName);
227
+ const presPath = join(resolve(base, PRESENTATIONS_DIR), safeName);
228
+
229
+ if (!existsSync(presPath)) {
230
+ return JSON.stringify({
231
+ success: true,
232
+ presentation_name: presentationName,
233
+ slides: [],
234
+ total_slides: 0,
235
+ });
236
+ }
237
+
238
+ const metadata = loadMetadata(presPath);
239
+ const slides = Object.entries(metadata.slides)
240
+ .map(([num, data]) => ({ slide_number: parseInt(num), ...data }))
241
+ .sort((a, b) => a.slide_number - b.slide_number);
242
+
243
+ return JSON.stringify(
244
+ {
245
+ success: true,
246
+ presentation_name: presentationName,
247
+ presentation_title: metadata.title,
248
+ slides,
249
+ total_slides: slides.length,
250
+ presentation_path: `${PRESENTATIONS_DIR}/${safeName}`,
251
+ },
252
+ null,
253
+ 2,
254
+ );
255
+ }
256
+
257
+ function doDeleteSlide(
258
+ base: string,
259
+ presentationName: string,
260
+ slideNumber: number,
261
+ ): string {
262
+ if (!presentationName)
263
+ return JSON.stringify({
264
+ success: false,
265
+ error: "presentation_name is required",
266
+ });
267
+ if (!slideNumber || slideNumber < 1)
268
+ return JSON.stringify({
269
+ success: false,
270
+ error: "slide_number must be >= 1",
271
+ });
272
+
273
+ const safeName = sanitizeFilename(presentationName);
274
+ const presPath = join(resolve(base, PRESENTATIONS_DIR), safeName);
275
+
276
+ if (!existsSync(presPath)) {
277
+ return JSON.stringify({
278
+ success: false,
279
+ error: `Presentation '${presentationName}' not found`,
280
+ });
281
+ }
282
+
283
+ const metadata = loadMetadata(presPath);
284
+ const key = String(slideNumber);
285
+ if (!metadata.slides[key]) {
286
+ return JSON.stringify({
287
+ success: false,
288
+ error: `Slide ${slideNumber} not found`,
289
+ });
290
+ }
291
+
292
+ const slideInfo = metadata.slides[key];
293
+ const slidePath = join(presPath, slideInfo.filename);
294
+ try {
295
+ unlinkSync(slidePath);
296
+ } catch {}
297
+
298
+ delete metadata.slides[key];
299
+ saveMetadata(presPath, metadata);
300
+ generateViewer(presPath, metadata);
301
+
302
+ return JSON.stringify(
303
+ {
304
+ success: true,
305
+ action: "delete_slide",
306
+ presentation_name: presentationName,
307
+ deleted_slide: slideNumber,
308
+ deleted_title: slideInfo.title,
309
+ remaining_slides: Object.keys(metadata.slides).length,
310
+ },
311
+ null,
312
+ 2,
313
+ );
314
+ }
315
+
316
+ function doListPresentations(base: string): string {
317
+ const presDir = resolve(base, PRESENTATIONS_DIR);
318
+ if (!existsSync(presDir)) {
319
+ return JSON.stringify({ success: true, presentations: [], total_count: 0 });
320
+ }
321
+
322
+ const presentations = readdirSync(presDir, { withFileTypes: true })
323
+ .filter(
324
+ (d) => d.isDirectory() && !d.name.startsWith(".") && d.name !== "images",
325
+ )
326
+ .map((d) => {
327
+ const path = join(presDir, d.name);
328
+ const metadata = loadMetadata(path);
329
+ return {
330
+ folder: d.name,
331
+ title: metadata.title || d.name,
332
+ description: metadata.description || "",
333
+ total_slides: Object.keys(metadata.slides).length,
334
+ created_at: metadata.created_at,
335
+ updated_at: metadata.updated_at,
336
+ };
337
+ });
338
+
339
+ return JSON.stringify(
340
+ {
341
+ success: true,
342
+ presentations,
343
+ total_count: presentations.length,
344
+ },
345
+ null,
346
+ 2,
347
+ );
348
+ }
349
+
350
+ function doDeletePresentation(base: string, presentationName: string): string {
351
+ if (!presentationName)
352
+ return JSON.stringify({
353
+ success: false,
354
+ error: "presentation_name is required",
355
+ });
356
+
357
+ const safeName = sanitizeFilename(presentationName);
358
+ const presPath = join(resolve(base, PRESENTATIONS_DIR), safeName);
359
+
360
+ if (!existsSync(presPath)) {
361
+ return JSON.stringify({
362
+ success: false,
363
+ error: `Presentation '${presentationName}' not found`,
364
+ });
365
+ }
366
+
367
+ rmSync(presPath, { recursive: true, force: true });
368
+
369
+ return JSON.stringify(
370
+ {
371
+ success: true,
372
+ action: "delete_presentation",
373
+ presentation_name: presentationName,
374
+ deleted_path: `${PRESENTATIONS_DIR}/${safeName}`,
375
+ },
376
+ null,
377
+ 2,
378
+ );
379
+ }
380
+
381
+ function generateViewer(presPath: string, metadata: PresentationMetadata): void {
382
+ const viewerTemplatePath = join(SKILLS_DIR, "viewer.html");
383
+ if (!existsSync(viewerTemplatePath)) return;
384
+
385
+ const slides = Object.entries(metadata.slides)
386
+ .map(([num, data]) => ({
387
+ number: parseInt(num),
388
+ title: data.title || `Slide ${num}`,
389
+ filename: data.filename || `slide_${String(num).padStart(2, "0")}.html`,
390
+ }))
391
+ .sort((a, b) => a.number - b.number);
392
+
393
+ const presData = JSON.stringify({
394
+ title: metadata.title || metadata.presentation_name || "Presentation",
395
+ slides,
396
+ });
397
+
398
+ const template = readFileSync(viewerTemplatePath, "utf-8");
399
+ const html = template
400
+ .replace("{{TITLE}}", metadata.title || "Presentation")
401
+ .replace("{{PRESENTATION_DATA}}", presData);
402
+
403
+ writeFileSync(join(presPath, "viewer.html"), html);
404
+ }
405
+
406
+ function doPreview(base: string, presentationName: string): string {
407
+ if (!presentationName)
408
+ return JSON.stringify({
409
+ success: false,
410
+ error: "presentation_name is required",
411
+ });
412
+
413
+ const safeName = sanitizeFilename(presentationName);
414
+ const presPath = join(resolve(base, PRESENTATIONS_DIR), safeName);
415
+
416
+ if (!existsSync(presPath))
417
+ return JSON.stringify({
418
+ success: false,
419
+ error: `Presentation '${presentationName}' not found`,
420
+ });
421
+
422
+ const metadata = loadMetadata(presPath);
423
+ generateViewer(presPath, metadata);
424
+
425
+ const serverScript = join(SKILLS_DIR, "serve.ts");
426
+ if (!existsSync(serverScript))
427
+ return JSON.stringify({
428
+ success: false,
429
+ error: "Viewer server script not found. Check .opencode/skills/presentation-viewer/serve.ts",
430
+ });
431
+
432
+ try {
433
+ execSync(`bun run "${serverScript}" "${presPath}" &`, {
434
+ cwd: base,
435
+ timeout: 5000,
436
+ stdio: "ignore",
437
+ detached: true,
438
+ });
439
+ } catch {
440
+ /* the detached process continues running, the timeout is expected */
441
+ }
442
+
443
+ return JSON.stringify(
444
+ {
445
+ success: true,
446
+ action: "preview",
447
+ presentation_name: presentationName,
448
+ viewer_url: "http://localhost:3210",
449
+ viewer_file: `${PRESENTATIONS_DIR}/${safeName}/viewer.html`,
450
+ message: "Preview server started at http://localhost:3210 — browser should open automatically. Press Ctrl+C in terminal to stop.",
451
+ },
452
+ null,
453
+ 2,
454
+ );
455
+ }
456
+
457
+ function runPythonScript(
458
+ script: string,
459
+ args: string[],
460
+ timeoutMs = 300_000,
461
+ ): string {
462
+ const cmd = `uv run ${script} ${args.map((a) => `"${a}"`).join(" ")}`;
463
+ try {
464
+ const output = execSync(cmd, {
465
+ cwd: SCRIPTS_DIR,
466
+ timeout: timeoutMs,
467
+ encoding: "utf-8",
468
+ stdio: ["pipe", "pipe", "pipe"],
469
+ });
470
+ return output.trim();
471
+ } catch (e: unknown) {
472
+ const err = e as { stdout?: string; stderr?: string; message?: string };
473
+ const stdout = err.stdout?.trim() ?? "";
474
+ if (stdout) {
475
+ try {
476
+ const parsed = JSON.parse(stdout);
477
+ if (parsed.success === false) return stdout;
478
+ } catch {}
479
+ }
480
+ return JSON.stringify({
481
+ success: false,
482
+ error: err.stderr?.trim() || err.message || "Python script failed",
483
+ });
484
+ }
485
+ }
486
+
487
+ function doValidateSlide(
488
+ base: string,
489
+ presentationName: string,
490
+ slideNumber: number,
491
+ ): string {
492
+ if (!presentationName)
493
+ return JSON.stringify({
494
+ success: false,
495
+ error: "presentation_name is required",
496
+ });
497
+ if (!slideNumber || slideNumber < 1)
498
+ return JSON.stringify({
499
+ success: false,
500
+ error: "slide_number must be >= 1",
501
+ });
502
+
503
+ const safeName = sanitizeFilename(presentationName);
504
+ const presPath = join(resolve(base, PRESENTATIONS_DIR), safeName);
505
+ const metadata = loadMetadata(presPath);
506
+ const key = String(slideNumber);
507
+ if (!metadata.slides[key])
508
+ return JSON.stringify({
509
+ success: false,
510
+ error: `Slide ${slideNumber} not found`,
511
+ });
512
+
513
+ const slidePath = join(resolve(base), metadata.slides[key].file_path);
514
+ if (!existsSync(slidePath))
515
+ return JSON.stringify({
516
+ success: false,
517
+ error: `Slide file not found: ${slidePath}`,
518
+ });
519
+
520
+ const raw = runPythonScript("validate_slide.py", [slidePath]);
521
+ try {
522
+ const result = JSON.parse(raw);
523
+ return JSON.stringify(
524
+ {
525
+ ...result,
526
+ action: "validate_slide",
527
+ presentation_name: presentationName,
528
+ slide_number: slideNumber,
529
+ },
530
+ null,
531
+ 2,
532
+ );
533
+ } catch {
534
+ return JSON.stringify({ success: false, error: raw });
535
+ }
536
+ }
537
+
538
+ function doExportPdf(base: string, presentationName: string): string {
539
+ if (!presentationName)
540
+ return JSON.stringify({
541
+ success: false,
542
+ error: "presentation_name is required",
543
+ });
544
+
545
+ const safeName = sanitizeFilename(presentationName);
546
+ const presPath = join(resolve(base, PRESENTATIONS_DIR), safeName);
547
+ if (!existsSync(presPath))
548
+ return JSON.stringify({
549
+ success: false,
550
+ error: `Presentation '${presentationName}' not found`,
551
+ });
552
+
553
+ const outPath = join(presPath, `${safeName}.pdf`);
554
+ const raw = runPythonScript("convert_pdf.py", [presPath, outPath]);
555
+ try {
556
+ const result = JSON.parse(raw);
557
+ if (result.success) {
558
+ return JSON.stringify(
559
+ {
560
+ ...result,
561
+ action: "export_pdf",
562
+ presentation_name: presentationName,
563
+ relative_path: `${PRESENTATIONS_DIR}/${safeName}/${safeName}.pdf`,
564
+ },
565
+ null,
566
+ 2,
567
+ );
568
+ }
569
+ return raw;
570
+ } catch {
571
+ return JSON.stringify({ success: false, error: raw });
572
+ }
573
+ }
574
+
575
+ function doExportPptx(base: string, presentationName: string): string {
576
+ if (!presentationName)
577
+ return JSON.stringify({
578
+ success: false,
579
+ error: "presentation_name is required",
580
+ });
581
+
582
+ const safeName = sanitizeFilename(presentationName);
583
+ const presPath = join(resolve(base, PRESENTATIONS_DIR), safeName);
584
+ if (!existsSync(presPath))
585
+ return JSON.stringify({
586
+ success: false,
587
+ error: `Presentation '${presentationName}' not found`,
588
+ });
589
+
590
+ const outPath = join(presPath, `${safeName}.pptx`);
591
+ const raw = runPythonScript("convert_pptx.py", [presPath, outPath]);
592
+ try {
593
+ const result = JSON.parse(raw);
594
+ if (result.success) {
595
+ return JSON.stringify(
596
+ {
597
+ ...result,
598
+ action: "export_pptx",
599
+ presentation_name: presentationName,
600
+ relative_path: `${PRESENTATIONS_DIR}/${safeName}/${safeName}.pptx`,
601
+ },
602
+ null,
603
+ 2,
604
+ );
605
+ }
606
+ return raw;
607
+ } catch {
608
+ return JSON.stringify({ success: false, error: raw });
609
+ }
610
+ }
611
+
612
+ export default tool({
613
+ description:
614
+ "Create, manage, validate, preview, and export HTML presentation slides (1920x1080). " +
615
+ "Actions: 'create_slide', 'list_slides', 'delete_slide', 'list_presentations', 'delete_presentation', " +
616
+ "'validate_slide' (check dimensions via Playwright), 'export_pdf' (render to PDF via Playwright), " +
617
+ "'export_pptx' (3-layer PPTX with editable text via Playwright + python-pptx), " +
618
+ "'preview' (starts local HTTP server with polished slide viewer at http://localhost:3210). " +
619
+ "Each slide is a standalone HTML file with Inter font, D3.js, and Chart.js pre-loaded. " +
620
+ "Images go to presentations/images/ and are referenced as ../images/filename from slides. " +
621
+ "A viewer.html is auto-generated in each presentation folder on every create/delete.",
622
+ args: {
623
+ action: tool.schema
624
+ .string()
625
+ .describe(
626
+ "Action: 'create_slide', 'list_slides', 'delete_slide', 'list_presentations', 'delete_presentation', 'validate_slide', 'export_pdf', 'export_pptx', 'preview'",
627
+ ),
628
+ presentation_name: tool.schema
629
+ .string()
630
+ .optional()
631
+ .describe(
632
+ "Name of the presentation folder. Required for all actions except 'list_presentations'.",
633
+ ),
634
+ slide_number: tool.schema
635
+ .number()
636
+ .optional()
637
+ .describe(
638
+ "Slide number (1-based). Required for 'create_slide', 'delete_slide', and 'validate_slide'.",
639
+ ),
640
+ slide_title: tool.schema
641
+ .string()
642
+ .optional()
643
+ .describe("Title of this slide. Required for 'create_slide'."),
644
+ content: tool.schema
645
+ .string()
646
+ .optional()
647
+ .describe(
648
+ "HTML body content for the slide (no DOCTYPE/html/head/body tags — added automatically). " +
649
+ "Design for 1920x1080. Use box-sizing: border-box. Max 40px padding. " +
650
+ "Inter font is pre-loaded. Use emoji for icons. Required for 'create_slide'.",
651
+ ),
652
+ presentation_title: tool.schema
653
+ .string()
654
+ .optional()
655
+ .describe("Main title of the presentation. Defaults to 'Presentation'."),
656
+ output_dir: tool.schema
657
+ .string()
658
+ .optional()
659
+ .describe(
660
+ "Base directory for presentations/ folder. Defaults to the current working directory. " +
661
+ "Usually not needed — only set if you want presentations in a specific location.",
662
+ ),
663
+ },
664
+ async execute(args, _context) {
665
+ const action = args.action as Action;
666
+ const validActions: Action[] = [
667
+ "create_slide",
668
+ "list_slides",
669
+ "delete_slide",
670
+ "list_presentations",
671
+ "delete_presentation",
672
+ "validate_slide",
673
+ "export_pdf",
674
+ "export_pptx",
675
+ "preview",
676
+ ];
677
+
678
+ if (!validActions.includes(action)) {
679
+ return `Error: Invalid action '${action}'. Use: ${validActions.join(", ")}`;
680
+ }
681
+
682
+ const worktree =
683
+ _context.worktree && _context.worktree !== "/" ? _context.worktree : null;
684
+ const base =
685
+ args.output_dir ?? worktree ?? _context.directory ?? process.cwd();
686
+
687
+ switch (action) {
688
+ case "create_slide":
689
+ return doCreateSlide(
690
+ base,
691
+ args.presentation_name ?? "",
692
+ args.slide_number ?? 0,
693
+ args.slide_title ?? "",
694
+ args.content ?? "",
695
+ args.presentation_title ?? "Presentation",
696
+ );
697
+ case "list_slides":
698
+ return doListSlides(base, args.presentation_name ?? "");
699
+ case "delete_slide":
700
+ return doDeleteSlide(
701
+ base,
702
+ args.presentation_name ?? "",
703
+ args.slide_number ?? 0,
704
+ );
705
+ case "list_presentations":
706
+ return doListPresentations(base);
707
+ case "delete_presentation":
708
+ return doDeletePresentation(base, args.presentation_name ?? "");
709
+ case "validate_slide":
710
+ return doValidateSlide(
711
+ base,
712
+ args.presentation_name ?? "",
713
+ args.slide_number ?? 0,
714
+ );
715
+ case "export_pdf":
716
+ return doExportPdf(base, args.presentation_name ?? "");
717
+ case "export_pptx":
718
+ return doExportPptx(base, args.presentation_name ?? "");
719
+ case "preview":
720
+ return doPreview(base, args.presentation_name ?? "");
721
+ }
722
+ },
723
+ });