@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,306 @@
1
+ /**
2
+ * Kortix Memory Plugin
3
+ *
4
+ * Implements OpenClaw-style memory system integration for OpenCode:
5
+ *
6
+ * 1. System Prompt Injection — Automatically loads MEMORY.md + daily logs
7
+ * into the system prompt at session start (no tool call needed).
8
+ *
9
+ * 2. Pre-Compaction Memory Flush — Before context compaction, triggers a
10
+ * silent agentic turn that nudges the model to write durable memories
11
+ * to disk, preventing memory loss.
12
+ *
13
+ * 3. Session Event Tracking — Listens to session events for memory lifecycle.
14
+ *
15
+ * Mirrors OpenClaw's memory architecture:
16
+ * - src/agents/system-prompt.ts (MEMORY.md injection)
17
+ * - compaction.memoryFlush (pre-compaction flush)
18
+ */
19
+
20
+ import { readFile, access, mkdir } from "node:fs/promises"
21
+ import * as path from "node:path"
22
+ import type { Plugin } from "@opencode-ai/plugin"
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Configuration
26
+ // ---------------------------------------------------------------------------
27
+
28
+ interface MemoryConfig {
29
+ enabled: boolean
30
+ basePath: string
31
+ corePath: string
32
+ memoryDir: string
33
+ flush: {
34
+ enabled: boolean
35
+ softThresholdTokens: number
36
+ systemPrompt: string
37
+ prompt: string
38
+ }
39
+ inject: {
40
+ coreMemory: boolean
41
+ dailyLogs: boolean
42
+ dailyLogDays: number
43
+ }
44
+ }
45
+
46
+ const DEFAULT_CONFIG: MemoryConfig = {
47
+ enabled: true,
48
+ basePath: "/workspace/.kortix",
49
+ corePath: "MEMORY.md",
50
+ memoryDir: "memory",
51
+ flush: {
52
+ enabled: true,
53
+ softThresholdTokens: 4000,
54
+ systemPrompt:
55
+ "Session is nearing context compaction. Before context is lost, write any durable memories that should persist across sessions.",
56
+ prompt: [
57
+ "Review what you have learned in this session.",
58
+ `Write any lasting notes, decisions, lessons, or user preferences to workspace/.kortix/memory/${formatDate(new Date())}.md.`,
59
+ "Update workspace/.kortix/MEMORY.md Scratchpad with current state and pending items.",
60
+ "Reply with NO_REPLY if there is nothing worth remembering.",
61
+ ].join(" "),
62
+ },
63
+ inject: {
64
+ coreMemory: true,
65
+ dailyLogs: true,
66
+ dailyLogDays: 2,
67
+ },
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Helpers
72
+ // ---------------------------------------------------------------------------
73
+
74
+ function formatDate(d: Date): string {
75
+ return d.toISOString().slice(0, 10)
76
+ }
77
+
78
+ function getDayOffset(offset: number): Date {
79
+ const d = new Date()
80
+ d.setDate(d.getDate() + offset)
81
+ return d
82
+ }
83
+
84
+ async function readFileSafe(filePath: string): Promise<string | null> {
85
+ try {
86
+ await access(filePath)
87
+ return await readFile(filePath, "utf-8")
88
+ } catch {
89
+ return null
90
+ }
91
+ }
92
+
93
+ async function loadConfig(directory: string): Promise<MemoryConfig> {
94
+ const configPath = path.join(directory, ".opencode", "memory.json")
95
+ try {
96
+ const raw = await readFile(configPath, "utf-8")
97
+ const parsed = JSON.parse(raw)
98
+ return {
99
+ ...DEFAULT_CONFIG,
100
+ ...parsed,
101
+ flush: { ...DEFAULT_CONFIG.flush, ...parsed.flush },
102
+ inject: { ...DEFAULT_CONFIG.inject, ...parsed.inject },
103
+ }
104
+ } catch {
105
+ // Also try sandbox-local config
106
+ const sandboxConfigPath = path.resolve(directory, "memory.json")
107
+ try {
108
+ const raw = await readFile(sandboxConfigPath, "utf-8")
109
+ const parsed = JSON.parse(raw)
110
+ return {
111
+ ...DEFAULT_CONFIG,
112
+ ...parsed,
113
+ flush: { ...DEFAULT_CONFIG.flush, ...parsed.flush },
114
+ inject: { ...DEFAULT_CONFIG.inject, ...parsed.inject },
115
+ }
116
+ } catch {
117
+ return DEFAULT_CONFIG
118
+ }
119
+ }
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Memory Content Loading
124
+ // ---------------------------------------------------------------------------
125
+
126
+ async function loadCoreMemory(basePath: string): Promise<string | null> {
127
+ const memoryPath = path.join(basePath, "MEMORY.md")
128
+ return readFileSafe(memoryPath)
129
+ }
130
+
131
+ async function loadDailyLogs(
132
+ basePath: string,
133
+ days: number,
134
+ ): Promise<{ date: string; content: string }[]> {
135
+ const logs: { date: string; content: string }[] = []
136
+ for (let i = 0; i < days; i++) {
137
+ const date = formatDate(getDayOffset(-i))
138
+ const logPath = path.join(basePath, "memory", `${date}.md`)
139
+ const content = await readFileSafe(logPath)
140
+ if (content && content.trim().length > 0) {
141
+ logs.push({ date, content })
142
+ }
143
+ }
144
+ return logs
145
+ }
146
+
147
+ function buildMemorySystemPrompt(
148
+ coreMemory: string | null,
149
+ dailyLogs: { date: string; content: string }[],
150
+ ): string {
151
+ const sections: string[] = []
152
+
153
+ sections.push("# Agent Memory (auto-loaded)")
154
+ sections.push("")
155
+
156
+ if (coreMemory && coreMemory.trim()) {
157
+ sections.push("## Core Memory (MEMORY.md)")
158
+ sections.push("")
159
+ sections.push(coreMemory.trim())
160
+ sections.push("")
161
+ }
162
+
163
+ if (dailyLogs.length > 0) {
164
+ sections.push("## Recent Daily Logs")
165
+ sections.push("")
166
+ for (const log of dailyLogs) {
167
+ sections.push(`### ${log.date}`)
168
+ sections.push("")
169
+ sections.push(log.content.trim())
170
+ sections.push("")
171
+ }
172
+ }
173
+
174
+ if (!coreMemory && dailyLogs.length === 0) {
175
+ sections.push(
176
+ "No memory found. Memory system is active but MEMORY.md has not been created yet.",
177
+ )
178
+ sections.push(
179
+ "Create it at workspace/.kortix/MEMORY.md or run /memory-init.",
180
+ )
181
+ sections.push("")
182
+ }
183
+
184
+ sections.push("---")
185
+ sections.push(
186
+ "Memory is auto-loaded. Update MEMORY.md and memory/*.md files to persist knowledge across sessions.",
187
+ )
188
+ sections.push(
189
+ "Use delta-only updates (never rewrite the whole file). Write daily entries to memory/YYYY-MM-DD.md.",
190
+ )
191
+
192
+ return sections.join("\n")
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Track flush state per session to prevent multiple flushes
197
+ // ---------------------------------------------------------------------------
198
+
199
+ const flushedSessions = new Set<string>()
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Plugin Export
203
+ // ---------------------------------------------------------------------------
204
+
205
+ export const MemoryPlugin: Plugin = async (ctx) => {
206
+ const config = await loadConfig(ctx.directory)
207
+
208
+ if (!config.enabled) {
209
+ return {}
210
+ }
211
+
212
+ // Ensure base directories exist
213
+ const basePath = config.basePath
214
+ try {
215
+ await mkdir(path.join(basePath, "memory"), { recursive: true })
216
+ await mkdir(path.join(basePath, "journal"), { recursive: true })
217
+ await mkdir(path.join(basePath, "knowledge"), { recursive: true })
218
+ await mkdir(path.join(basePath, "sessions"), { recursive: true })
219
+ } catch {
220
+ // Directories may not be writable in all environments
221
+ }
222
+
223
+ return {
224
+ // -----------------------------------------------------------------
225
+ // Hook 1: System Prompt Injection
226
+ //
227
+ // Automatically load MEMORY.md + daily logs into the system prompt
228
+ // so the agent starts every turn with full memory context.
229
+ // Mirrors OpenClaw's src/agents/system-prompt.ts behavior.
230
+ // -----------------------------------------------------------------
231
+ "experimental.chat.system.transform": async (_input, output) => {
232
+ if (!config.inject.coreMemory && !config.inject.dailyLogs) return
233
+
234
+ const coreMemory = config.inject.coreMemory
235
+ ? await loadCoreMemory(basePath)
236
+ : null
237
+
238
+ const dailyLogs = config.inject.dailyLogs
239
+ ? await loadDailyLogs(basePath, config.inject.dailyLogDays)
240
+ : []
241
+
242
+ // Only inject if there's something to inject
243
+ if (coreMemory || dailyLogs.length > 0) {
244
+ const memoryPrompt = buildMemorySystemPrompt(coreMemory, dailyLogs)
245
+ output.system.push(memoryPrompt)
246
+ }
247
+ },
248
+
249
+ // -----------------------------------------------------------------
250
+ // Hook 2: Pre-Compaction Memory Flush
251
+ //
252
+ // Before context is compacted, inject instructions for the agent to
253
+ // write durable memories to disk. This prevents memory loss when the
254
+ // context window fills up.
255
+ // Mirrors OpenClaw's compaction.memoryFlush behavior.
256
+ // -----------------------------------------------------------------
257
+ "experimental.session.compacting": async (input, output) => {
258
+ if (!config.flush.enabled) return
259
+
260
+ const sessionID = input.sessionID
261
+
262
+ // One flush per compaction cycle per session
263
+ if (flushedSessions.has(sessionID)) return
264
+ flushedSessions.add(sessionID)
265
+
266
+ // Inject memory flush context into the compaction
267
+ const today = formatDate(new Date())
268
+ const flushContext = [
269
+ "--- MEMORY FLUSH ---",
270
+ config.flush.systemPrompt,
271
+ "",
272
+ `Write durable memories to: workspace/.kortix/memory/${today}.md`,
273
+ "Update MEMORY.md Scratchpad with: current state, pending items, handoff notes.",
274
+ "Format daily log entries with timestamps: ## HH:MM — [Topic]",
275
+ "Only write what's worth remembering. Skip if nothing notable happened.",
276
+ "--- END MEMORY FLUSH ---",
277
+ ].join("\n")
278
+
279
+ output.context.push(flushContext)
280
+ },
281
+
282
+ // -----------------------------------------------------------------
283
+ // Hook 3: Session Event Tracking
284
+ //
285
+ // Clean up flush tracking when sessions end.
286
+ // Listen for session events to manage memory lifecycle.
287
+ // -----------------------------------------------------------------
288
+ event: async ({ event }) => {
289
+ // Clean up flush tracking for completed sessions
290
+ if (event.type === "session.deleted") {
291
+ const data = event.properties as { id?: string }
292
+ if (data.id) flushedSessions.delete(data.id)
293
+ }
294
+
295
+ // Reset flush flag when a new compaction cycle might start
296
+ // (session becomes idle, meaning the previous turn finished)
297
+ if (event.type === "session.idle") {
298
+ // Allow future compaction cycles to flush again
299
+ // This is intentionally NOT clearing the flag here —
300
+ // it should only be cleared on session delete or new session
301
+ }
302
+ },
303
+ }
304
+ }
305
+
306
+ export default MemoryPlugin
@@ -0,0 +1,412 @@
1
+ /**
2
+ * SQLite State Module for Worktree Plugin
3
+ *
4
+ * Provides atomic, crash-safe persistence for worktree sessions and pending operations.
5
+ * Uses bun:sqlite for zero external dependencies.
6
+ *
7
+ * Database location: ~/.local/share/opencode/plugins/worktree/{project-id}.sqlite
8
+ * Project ID is the first git root commit SHA (40-char hex), with SHA-256 path hash fallback (16-char).
9
+ */
10
+
11
+ import { Database } from "bun:sqlite"
12
+ import { mkdirSync } from "node:fs"
13
+ import * as os from "node:os"
14
+ import * as path from "node:path"
15
+ import { z } from "zod"
16
+ import type { OpencodeClient } from "../kdco-primitives"
17
+ import { getProjectId, logWarn } from "../kdco-primitives"
18
+
19
+ // =============================================================================
20
+ // TYPES
21
+ // =============================================================================
22
+
23
+ /** Represents an active worktree session */
24
+ export interface Session {
25
+ id: string
26
+ branch: string
27
+ path: string
28
+ createdAt: string
29
+ }
30
+
31
+ /** Pending spawn operation to be processed on session.idle */
32
+ export interface PendingSpawn {
33
+ branch: string
34
+ path: string
35
+ sessionId: string
36
+ }
37
+
38
+ /** Pending delete operation to be processed on session.idle */
39
+ export interface PendingDelete {
40
+ branch: string
41
+ path: string
42
+ }
43
+
44
+ // =============================================================================
45
+ // SCHEMAS (Boundary Validation)
46
+ // =============================================================================
47
+
48
+ const sessionSchema = z.object({
49
+ id: z.string().min(1),
50
+ branch: z.string().min(1),
51
+ path: z.string().min(1),
52
+ createdAt: z.string().min(1),
53
+ })
54
+
55
+ const pendingSpawnSchema = z.object({
56
+ branch: z.string().min(1),
57
+ path: z.string().min(1),
58
+ sessionId: z.string().min(1),
59
+ })
60
+
61
+ const pendingDeleteSchema = z.object({
62
+ branch: z.string().min(1),
63
+ path: z.string().min(1),
64
+ })
65
+
66
+ // =============================================================================
67
+ // DATABASE UTILITIES
68
+ // =============================================================================
69
+
70
+ /**
71
+ * Get the worktree path for a given project and branch.
72
+ *
73
+ * @param projectRoot - Absolute path to the project root
74
+ * @param branch - Branch name for the worktree
75
+ * @returns Absolute path to the worktree directory
76
+ */
77
+ export async function getWorktreePath(projectRoot: string, branch: string): Promise<string> {
78
+ if (!branch || typeof branch !== "string") {
79
+ throw new Error("branch is required")
80
+ }
81
+ const projectId = await getProjectId(projectRoot)
82
+ return path.join(os.homedir(), ".local", "share", "opencode", "worktree", projectId, branch)
83
+ }
84
+
85
+ /**
86
+ * Get the database directory path.
87
+ * Location: ~/.local/share/opencode/plugins/worktree/
88
+ */
89
+ function getDbDirectory(): string {
90
+ const home = os.homedir()
91
+ return path.join(home, ".local", "share", "opencode", "plugins", "worktree")
92
+ }
93
+
94
+ /**
95
+ * Get the full database file path for a project.
96
+ * @param projectRoot - Absolute path to the project root
97
+ */
98
+ async function getDbPath(projectRoot: string): Promise<string> {
99
+ const projectId = await getProjectId(projectRoot)
100
+ return path.join(getDbDirectory(), `${projectId}.sqlite`)
101
+ }
102
+
103
+ /**
104
+ * Initialize the SQLite database for worktree state.
105
+ * Creates the database file and schema if they don't exist.
106
+ *
107
+ * @param projectRoot - Absolute path to the project root
108
+ * @returns Configured Database instance
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * const db = await initStateDb("/home/user/my-project")
113
+ * const sessions = getAllSessions(db)
114
+ * db.close()
115
+ * ```
116
+ */
117
+ export async function initStateDb(projectRoot: string): Promise<Database> {
118
+ // Guard: validate project root
119
+ if (!projectRoot || typeof projectRoot !== "string") {
120
+ throw new Error("initStateDb requires a valid project root path")
121
+ }
122
+
123
+ const dbPath = await getDbPath(projectRoot)
124
+ const dbDir = path.dirname(dbPath)
125
+
126
+ // Create directory synchronously (required before opening DB)
127
+ mkdirSync(dbDir, { recursive: true })
128
+
129
+ // Open database (creates if doesn't exist)
130
+ const db = new Database(dbPath)
131
+
132
+ // Configure SQLite for concurrent access
133
+ db.exec("PRAGMA journal_mode=WAL")
134
+ db.exec("PRAGMA busy_timeout=5000")
135
+
136
+ // Create tables with schema
137
+ db.exec(`
138
+ CREATE TABLE IF NOT EXISTS sessions (
139
+ id TEXT PRIMARY KEY,
140
+ branch TEXT NOT NULL,
141
+ path TEXT NOT NULL,
142
+ created_at TEXT NOT NULL
143
+ )
144
+ `)
145
+
146
+ db.exec(`
147
+ CREATE TABLE IF NOT EXISTS pending_operations (
148
+ id INTEGER PRIMARY KEY CHECK (id = 1),
149
+ type TEXT NOT NULL,
150
+ branch TEXT NOT NULL,
151
+ path TEXT NOT NULL,
152
+ session_id TEXT
153
+ )
154
+ `)
155
+
156
+ return db
157
+ }
158
+
159
+ // =============================================================================
160
+ // SESSION CRUD
161
+ // =============================================================================
162
+
163
+ /**
164
+ * Add a new session to the database.
165
+ * Uses atomic INSERT OR REPLACE for idempotency.
166
+ *
167
+ * @param db - Database instance from initStateDb
168
+ * @param session - Session data to persist
169
+ */
170
+ export function addSession(db: Database, session: Session): void {
171
+ // Parse at boundary for type safety
172
+ const parsed = sessionSchema.parse(session)
173
+
174
+ const stmt = db.prepare(`
175
+ INSERT OR REPLACE INTO sessions (id, branch, path, created_at)
176
+ VALUES ($id, $branch, $path, $createdAt)
177
+ `)
178
+
179
+ stmt.run({
180
+ $id: parsed.id,
181
+ $branch: parsed.branch,
182
+ $path: parsed.path,
183
+ $createdAt: parsed.createdAt,
184
+ })
185
+ }
186
+
187
+ /**
188
+ * Get a session by ID.
189
+ *
190
+ * @param db - Database instance from initStateDb
191
+ * @param sessionId - Session ID to look up
192
+ * @returns Session if found, null otherwise
193
+ */
194
+ export function getSession(db: Database, sessionId: string): Session | null {
195
+ // Guard: empty session ID
196
+ if (!sessionId) return null
197
+
198
+ const stmt = db.prepare(`
199
+ SELECT id, branch, path, created_at as createdAt
200
+ FROM sessions
201
+ WHERE id = $id
202
+ `)
203
+
204
+ const row = stmt.get({ $id: sessionId }) as Record<string, string> | null
205
+ if (!row) return null
206
+
207
+ return {
208
+ id: row.id,
209
+ branch: row.branch,
210
+ path: row.path,
211
+ createdAt: row.createdAt,
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Remove a session by branch name.
217
+ * Deletes all sessions matching the branch.
218
+ *
219
+ * @param db - Database instance from initStateDb
220
+ * @param branch - Branch name to remove
221
+ */
222
+ export function removeSession(db: Database, branch: string): void {
223
+ // Guard: empty branch
224
+ if (!branch) return
225
+
226
+ const stmt = db.prepare(`DELETE FROM sessions WHERE branch = $branch`)
227
+ stmt.run({ $branch: branch })
228
+ }
229
+
230
+ /**
231
+ * Get all active sessions.
232
+ *
233
+ * @param db - Database instance from initStateDb
234
+ * @returns Array of all sessions, empty if none
235
+ */
236
+ export function getAllSessions(db: Database): Session[] {
237
+ const stmt = db.prepare(`
238
+ SELECT id, branch, path, created_at as createdAt
239
+ FROM sessions
240
+ ORDER BY created_at ASC
241
+ `)
242
+
243
+ const rows = stmt.all() as Array<Record<string, string>>
244
+ return rows.map((row) => ({
245
+ id: row.id,
246
+ branch: row.branch,
247
+ path: row.path,
248
+ createdAt: row.createdAt,
249
+ }))
250
+ }
251
+
252
+ // =============================================================================
253
+ // PENDING SPAWN OPERATIONS
254
+ // =============================================================================
255
+
256
+ /**
257
+ * Set a pending spawn operation. Uses singleton pattern (last-write-wins).
258
+ *
259
+ * If a pending spawn already exists, it will be REPLACED and a warning logged.
260
+ * This is intentional: only the most recent spawn request should be processed.
261
+ *
262
+ * @param db - Database instance from initStateDb
263
+ * @param spawn - Spawn operation data
264
+ */
265
+ export function setPendingSpawn(db: Database, spawn: PendingSpawn, client?: OpencodeClient): void {
266
+ // Parse at boundary for type safety
267
+ const parsed = pendingSpawnSchema.parse(spawn)
268
+
269
+ // Check for existing operations and warn about replacement
270
+ const existingSpawn = getPendingSpawn(db)
271
+ const existingDelete = getPendingDelete(db)
272
+
273
+ if (existingSpawn) {
274
+ logWarn(
275
+ client,
276
+ "worktree",
277
+ `Replacing pending spawn: "${existingSpawn.branch}" → "${parsed.branch}"`,
278
+ )
279
+ } else if (existingDelete) {
280
+ logWarn(
281
+ client,
282
+ "worktree",
283
+ `Pending spawn replacing pending delete for: "${existingDelete.branch}"`,
284
+ )
285
+ }
286
+
287
+ // Atomic: replace any existing pending operation
288
+ const stmt = db.prepare(`
289
+ INSERT OR REPLACE INTO pending_operations (id, type, branch, path, session_id)
290
+ VALUES (1, 'spawn', $branch, $path, $sessionId)
291
+ `)
292
+
293
+ stmt.run({
294
+ $branch: parsed.branch,
295
+ $path: parsed.path,
296
+ $sessionId: parsed.sessionId,
297
+ })
298
+ }
299
+
300
+ /**
301
+ * Get the pending spawn operation if one exists.
302
+ *
303
+ * @param db - Database instance from initStateDb
304
+ * @returns PendingSpawn if exists and type is 'spawn', null otherwise
305
+ */
306
+ export function getPendingSpawn(db: Database): PendingSpawn | null {
307
+ const stmt = db.prepare(`
308
+ SELECT type, branch, path, session_id as sessionId
309
+ FROM pending_operations
310
+ WHERE id = 1 AND type = 'spawn'
311
+ `)
312
+
313
+ const row = stmt.get() as Record<string, string> | null
314
+ if (!row) return null
315
+
316
+ return {
317
+ branch: row.branch,
318
+ path: row.path,
319
+ sessionId: row.sessionId,
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Clear any pending spawn operation.
325
+ * Removes the row if it's a spawn type, leaves deletes untouched.
326
+ *
327
+ * @param db - Database instance from initStateDb
328
+ */
329
+ export function clearPendingSpawn(db: Database): void {
330
+ const stmt = db.prepare(`DELETE FROM pending_operations WHERE id = 1 AND type = 'spawn'`)
331
+ stmt.run()
332
+ }
333
+
334
+ // =============================================================================
335
+ // PENDING DELETE OPERATIONS
336
+ // =============================================================================
337
+
338
+ /**
339
+ * Set a pending delete operation. Uses singleton pattern (last-write-wins).
340
+ *
341
+ * If a pending delete already exists, it will be REPLACED and a warning logged.
342
+ * This is intentional: only the most recent delete request should be processed.
343
+ *
344
+ * @param db - Database instance from initStateDb
345
+ * @param del - Delete operation data
346
+ */
347
+ export function setPendingDelete(db: Database, del: PendingDelete, client?: OpencodeClient): void {
348
+ // Parse at boundary for type safety
349
+ const parsed = pendingDeleteSchema.parse(del)
350
+
351
+ // Check for existing operations and warn about replacement
352
+ const existingDelete = getPendingDelete(db)
353
+ const existingSpawn = getPendingSpawn(db)
354
+
355
+ if (existingDelete) {
356
+ logWarn(
357
+ client,
358
+ "worktree",
359
+ `Replacing pending delete: "${existingDelete.branch}" → "${parsed.branch}"`,
360
+ )
361
+ } else if (existingSpawn) {
362
+ logWarn(
363
+ client,
364
+ "worktree",
365
+ `Pending delete replacing pending spawn for: "${existingSpawn.branch}"`,
366
+ )
367
+ }
368
+
369
+ // Atomic: replace any existing pending operation
370
+ const stmt = db.prepare(`
371
+ INSERT OR REPLACE INTO pending_operations (id, type, branch, path, session_id)
372
+ VALUES (1, 'delete', $branch, $path, NULL)
373
+ `)
374
+
375
+ stmt.run({
376
+ $branch: parsed.branch,
377
+ $path: parsed.path,
378
+ })
379
+ }
380
+
381
+ /**
382
+ * Get the pending delete operation if one exists.
383
+ *
384
+ * @param db - Database instance from initStateDb
385
+ * @returns PendingDelete if exists and type is 'delete', null otherwise
386
+ */
387
+ export function getPendingDelete(db: Database): PendingDelete | null {
388
+ const stmt = db.prepare(`
389
+ SELECT type, branch, path
390
+ FROM pending_operations
391
+ WHERE id = 1 AND type = 'delete'
392
+ `)
393
+
394
+ const row = stmt.get() as Record<string, string> | null
395
+ if (!row) return null
396
+
397
+ return {
398
+ branch: row.branch,
399
+ path: row.path,
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Clear any pending delete operation.
405
+ * Removes the row if it's a delete type, leaves spawns untouched.
406
+ *
407
+ * @param db - Database instance from initStateDb
408
+ */
409
+ export function clearPendingDelete(db: Database): void {
410
+ const stmt = db.prepare(`DELETE FROM pending_operations WHERE id = 1 AND type = 'delete'`)
411
+ stmt.run()
412
+ }