@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,409 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Session Transcript Export for Kortix Memory System
4
+
5
+ Converts OpenCode session JSON files into Markdown files that can be
6
+ indexed by LSS (Local Semantic Search) for semantic memory retrieval.
7
+
8
+ Mirrors OpenClaw's experimental session memory indexing:
9
+ - Reads session JSON from OpenCode storage
10
+ - Filters to user/assistant messages (skips tool calls)
11
+ - Converts to Markdown with timestamps and metadata
12
+ - Writes to workspace/.kortix/sessions/ for LSS indexing
13
+ - Delta detection: only exports new/modified sessions
14
+
15
+ Usage:
16
+ python3 export-sessions.py # Export all new sessions
17
+ python3 export-sessions.py --force # Re-export everything
18
+ python3 export-sessions.py --session <id> # Export specific session
19
+ python3 export-sessions.py --since 2025-01-01 # Export since date
20
+ """
21
+
22
+ import json
23
+ import os
24
+ import sys
25
+ import hashlib
26
+ import glob
27
+ import argparse
28
+ from datetime import datetime, timezone
29
+ from pathlib import Path
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Configuration
34
+ # ---------------------------------------------------------------------------
35
+
36
+ OPENCODE_STORAGE = os.path.expanduser("~/.local/share/opencode/storage")
37
+
38
+ # Default output — /workspace/.kortix/sessions in sandbox, overridable via --output
39
+ _default_sessions = "/workspace/.kortix/sessions"
40
+ if not os.path.isdir("/workspace"):
41
+ # Not in sandbox — use a local fallback
42
+ _default_sessions = os.path.expanduser("~/.kortix/sessions")
43
+ KORTIX_SESSIONS = os.environ.get("KORTIX_SESSIONS_DIR", _default_sessions)
44
+ EXPORT_STATE_FILE = os.path.join(KORTIX_SESSIONS, ".export-state.json")
45
+
46
+ # Roles to include in export (skip system, tool results)
47
+ INCLUDE_ROLES = {"user", "assistant"}
48
+
49
+ # Maximum content length per message part (truncate very long outputs)
50
+ MAX_PART_LENGTH = 5000
51
+
52
+ # Minimum session length to export (skip trivial sessions)
53
+ MIN_MESSAGES = 2
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Helpers
58
+ # ---------------------------------------------------------------------------
59
+
60
+ def load_export_state() -> dict:
61
+ """Load the export state tracking file."""
62
+ try:
63
+ with open(EXPORT_STATE_FILE, "r") as f:
64
+ return json.load(f)
65
+ except (FileNotFoundError, json.JSONDecodeError):
66
+ return {"exported": {}, "last_run": None}
67
+
68
+
69
+ def save_export_state(state: dict):
70
+ """Save the export state tracking file."""
71
+ os.makedirs(os.path.dirname(EXPORT_STATE_FILE), exist_ok=True)
72
+ state["last_run"] = datetime.now(timezone.utc).isoformat()
73
+ with open(EXPORT_STATE_FILE, "w") as f:
74
+ json.dump(state, f, indent=2)
75
+
76
+
77
+ def content_hash(content: str) -> str:
78
+ """Generate a content hash for change detection."""
79
+ return hashlib.md5(content.encode("utf-8")).hexdigest()
80
+
81
+
82
+ def safe_read_json(path: str) -> dict | None:
83
+ """Safely read a JSON file, returning None on error."""
84
+ try:
85
+ with open(path, "r") as f:
86
+ return json.load(f)
87
+ except (FileNotFoundError, json.JSONDecodeError, PermissionError):
88
+ return None
89
+
90
+
91
+ def format_timestamp(ts: int | float | None) -> str:
92
+ """Convert a millisecond timestamp to ISO format."""
93
+ if not ts:
94
+ return "unknown"
95
+ try:
96
+ dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
97
+ return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
98
+ except (ValueError, OverflowError, OSError):
99
+ return "unknown"
100
+
101
+
102
+ def format_date(ts: int | float | None) -> str:
103
+ """Convert a millisecond timestamp to date string."""
104
+ if not ts:
105
+ return "unknown"
106
+ try:
107
+ dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
108
+ return dt.strftime("%Y-%m-%d")
109
+ except (ValueError, OverflowError, OSError):
110
+ return "unknown"
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Session Discovery
115
+ # ---------------------------------------------------------------------------
116
+
117
+ def discover_sessions() -> list[dict]:
118
+ """Discover all session files in OpenCode storage."""
119
+ sessions = []
120
+ session_dir = os.path.join(OPENCODE_STORAGE, "session", "global")
121
+
122
+ if not os.path.isdir(session_dir):
123
+ return sessions
124
+
125
+ for filename in os.listdir(session_dir):
126
+ if not filename.endswith(".json"):
127
+ continue
128
+
129
+ session_path = os.path.join(session_dir, filename)
130
+ session = safe_read_json(session_path)
131
+ if session:
132
+ session["_path"] = session_path
133
+ sessions.append(session)
134
+
135
+ return sessions
136
+
137
+
138
+ def load_session_messages(session_id: str) -> list[dict]:
139
+ """Load all messages for a session, sorted by ID."""
140
+ messages = []
141
+ msg_dir = os.path.join(OPENCODE_STORAGE, "message", session_id)
142
+
143
+ if not os.path.isdir(msg_dir):
144
+ return messages
145
+
146
+ for filename in os.listdir(msg_dir):
147
+ if not filename.endswith(".json"):
148
+ continue
149
+
150
+ msg_path = os.path.join(msg_dir, filename)
151
+ msg = safe_read_json(msg_path)
152
+ if msg:
153
+ msg["_path"] = msg_path
154
+ messages.append(msg)
155
+
156
+ # Sort by message ID (which is time-ordered in OpenCode)
157
+ messages.sort(key=lambda m: m.get("id", ""))
158
+ return messages
159
+
160
+
161
+ def load_message_parts(message_id: str) -> list[dict]:
162
+ """Load all parts for a message, sorted by ID."""
163
+ parts = []
164
+ part_dir = os.path.join(OPENCODE_STORAGE, "part", message_id)
165
+
166
+ if not os.path.isdir(part_dir):
167
+ return parts
168
+
169
+ for filename in os.listdir(part_dir):
170
+ if not filename.endswith(".json"):
171
+ continue
172
+
173
+ part_path = os.path.join(part_dir, filename)
174
+ part = safe_read_json(part_path)
175
+ if part:
176
+ parts.append(part)
177
+
178
+ parts.sort(key=lambda p: p.get("id", ""))
179
+ return parts
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Markdown Conversion
184
+ # ---------------------------------------------------------------------------
185
+
186
+ def extract_text_from_parts(parts: list[dict]) -> str:
187
+ """Extract readable text content from message parts."""
188
+ texts = []
189
+
190
+ for part in parts:
191
+ ptype = part.get("type", "")
192
+
193
+ if ptype == "text":
194
+ text = part.get("text", "")
195
+ if text and text.strip():
196
+ # Truncate very long text parts
197
+ if len(text) > MAX_PART_LENGTH:
198
+ text = text[:MAX_PART_LENGTH] + "\n\n[... truncated ...]"
199
+ texts.append(text.strip())
200
+
201
+ elif ptype == "tool":
202
+ # Include tool name but skip full output (too verbose)
203
+ tool_name = part.get("tool", "unknown")
204
+ state = part.get("state", {})
205
+ status = state.get("status", "unknown")
206
+ texts.append(f"[Tool: {tool_name} — {status}]")
207
+
208
+ return "\n\n".join(texts)
209
+
210
+
211
+ def session_to_markdown(session: dict, messages: list[dict]) -> str:
212
+ """Convert a session with its messages to Markdown format."""
213
+ lines = []
214
+
215
+ # Header
216
+ title = session.get("title", "Untitled Session")
217
+ session_id = session.get("id", "unknown")
218
+ created = format_timestamp(session.get("time", {}).get("created"))
219
+ date = format_date(session.get("time", {}).get("created"))
220
+
221
+ lines.append(f"# {title}")
222
+ lines.append("")
223
+ lines.append(f"**Session:** {session_id}")
224
+ lines.append(f"**Date:** {date}")
225
+ lines.append(f"**Created:** {created}")
226
+ lines.append("")
227
+ lines.append("---")
228
+ lines.append("")
229
+
230
+ # Messages
231
+ for msg in messages:
232
+ role = msg.get("role", "unknown")
233
+
234
+ # Only include user and assistant messages
235
+ if role not in INCLUDE_ROLES:
236
+ continue
237
+
238
+ # Skip compaction/summary messages
239
+ if msg.get("summary"):
240
+ continue
241
+
242
+ # Load parts for this message
243
+ parts = load_message_parts(msg.get("id", ""))
244
+ text = extract_text_from_parts(parts)
245
+
246
+ if not text.strip():
247
+ continue
248
+
249
+ timestamp = format_timestamp(msg.get("time", {}).get("created"))
250
+ role_label = "User" if role == "user" else "Assistant"
251
+
252
+ lines.append(f"## {role_label} ({timestamp})")
253
+ lines.append("")
254
+ lines.append(text)
255
+ lines.append("")
256
+
257
+ return "\n".join(lines)
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # Export Logic
262
+ # ---------------------------------------------------------------------------
263
+
264
+ def export_session(
265
+ session: dict,
266
+ state: dict,
267
+ force: bool = False,
268
+ ) -> bool:
269
+ """Export a single session to Markdown. Returns True if exported."""
270
+ session_id = session.get("id", "")
271
+ if not session_id:
272
+ return False
273
+
274
+ # Load messages
275
+ messages = load_session_messages(session_id)
276
+
277
+ # Skip trivial sessions
278
+ user_msgs = [m for m in messages if m.get("role") in INCLUDE_ROLES]
279
+ if len(user_msgs) < MIN_MESSAGES:
280
+ return False
281
+
282
+ # Convert to Markdown
283
+ markdown = session_to_markdown(session, messages)
284
+ if not markdown.strip():
285
+ return False
286
+
287
+ # Check for changes (delta detection)
288
+ new_hash = content_hash(markdown)
289
+ if not force and state["exported"].get(session_id) == new_hash:
290
+ return False
291
+
292
+ # Determine output filename
293
+ date = format_date(session.get("time", {}).get("created"))
294
+ title = session.get("title", "untitled")
295
+ # Sanitize title for filename
296
+ safe_title = "".join(
297
+ c if c.isalnum() or c in "-_ " else "" for c in title
298
+ )[:60].strip().replace(" ", "-").lower()
299
+ filename = f"{date}-{safe_title}-{session_id[-8:]}.md"
300
+ output_path = os.path.join(KORTIX_SESSIONS, filename)
301
+
302
+ # Write
303
+ os.makedirs(KORTIX_SESSIONS, exist_ok=True)
304
+ with open(output_path, "w") as f:
305
+ f.write(markdown)
306
+
307
+ # Update state
308
+ state["exported"][session_id] = new_hash
309
+ return True
310
+
311
+
312
+ def main():
313
+ parser = argparse.ArgumentParser(
314
+ description="Export OpenCode sessions to Markdown for LSS indexing"
315
+ )
316
+ parser.add_argument(
317
+ "--force", action="store_true", help="Re-export all sessions"
318
+ )
319
+ parser.add_argument(
320
+ "--session", type=str, help="Export a specific session ID"
321
+ )
322
+ parser.add_argument(
323
+ "--since", type=str, help="Export sessions since date (YYYY-MM-DD)"
324
+ )
325
+ parser.add_argument(
326
+ "--dry-run", action="store_true", help="Show what would be exported"
327
+ )
328
+ parser.add_argument(
329
+ "--output", type=str, help="Output directory for exported sessions"
330
+ )
331
+ args = parser.parse_args()
332
+
333
+ # Override output directory if specified
334
+ global KORTIX_SESSIONS, EXPORT_STATE_FILE
335
+ if args.output:
336
+ KORTIX_SESSIONS = args.output
337
+ EXPORT_STATE_FILE = os.path.join(KORTIX_SESSIONS, ".export-state.json")
338
+
339
+ # Load state
340
+ state = load_export_state()
341
+ if args.force:
342
+ state["exported"] = {}
343
+
344
+ # Discover sessions
345
+ sessions = discover_sessions()
346
+ if not sessions:
347
+ print("No sessions found in OpenCode storage.")
348
+ print(f" Expected at: {OPENCODE_STORAGE}/session/global/")
349
+ return
350
+
351
+ # Filter by date if specified
352
+ if args.since:
353
+ try:
354
+ since_dt = datetime.strptime(args.since, "%Y-%m-%d").replace(
355
+ tzinfo=timezone.utc
356
+ )
357
+ since_ms = since_dt.timestamp() * 1000
358
+ sessions = [
359
+ s
360
+ for s in sessions
361
+ if (s.get("time", {}).get("created", 0) or 0) >= since_ms
362
+ ]
363
+ except ValueError:
364
+ print(f"Invalid date format: {args.since}. Use YYYY-MM-DD.")
365
+ sys.exit(1)
366
+
367
+ # Filter by specific session
368
+ if args.session:
369
+ sessions = [s for s in sessions if s.get("id") == args.session]
370
+ if not sessions:
371
+ print(f"Session not found: {args.session}")
372
+ sys.exit(1)
373
+
374
+ # Export
375
+ exported = 0
376
+ skipped = 0
377
+
378
+ for session in sessions:
379
+ session_id = session.get("id", "unknown")
380
+ title = session.get("title", "untitled")
381
+
382
+ if args.dry_run:
383
+ is_new = session_id not in state["exported"]
384
+ status = "NEW" if is_new else "unchanged"
385
+ print(f" [{status}] {session_id[:12]}... — {title}")
386
+ if is_new:
387
+ exported += 1
388
+ else:
389
+ skipped += 1
390
+ continue
391
+
392
+ if export_session(session, state, force=args.force):
393
+ exported += 1
394
+ print(f" Exported: {title} ({session_id[:12]}...)")
395
+ else:
396
+ skipped += 1
397
+
398
+ # Save state
399
+ if not args.dry_run:
400
+ save_export_state(state)
401
+
402
+ print(f"\nDone. Exported: {exported}, Skipped: {skipped}, Total: {len(sessions)}")
403
+ if exported > 0 and not args.dry_run:
404
+ print(f"Session transcripts written to: {KORTIX_SESSIONS}")
405
+ print("LSS will auto-index these files for semantic search.")
406
+
407
+
408
+ if __name__ == "__main__":
409
+ main()