@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.
- package/config/customize.sh +143 -0
- package/config/kortix-env-setup.sh +25 -0
- package/kortix-master/package.json +22 -0
- package/kortix-master/src/config.ts +22 -0
- package/kortix-master/src/index.ts +44 -0
- package/kortix-master/src/routes/env.ts +65 -0
- package/kortix-master/src/routes/proxy.ts +108 -0
- package/kortix-master/src/routes/update.ts +185 -0
- package/kortix-master/src/services/proxy.ts +43 -0
- package/kortix-master/src/services/secret-store.ts +156 -0
- package/kortix-master/tsconfig.json +14 -0
- package/opencode/agents/kortix-browser.md +142 -0
- package/opencode/agents/kortix-build.md +62 -0
- package/opencode/agents/kortix-explore.md +66 -0
- package/opencode/agents/kortix-image-gen.md +33 -0
- package/opencode/agents/kortix-main.md +450 -0
- package/opencode/agents/kortix-plan.md +100 -0
- package/opencode/agents/kortix-research.md +84 -0
- package/opencode/agents/kortix-sheets.md +61 -0
- package/opencode/agents/kortix-slides.md +64 -0
- package/opencode/agents/kortix-web-dev.md +572 -0
- package/opencode/commands/email.md +36 -0
- package/opencode/commands/init.md +43 -0
- package/opencode/commands/journal.md +44 -0
- package/opencode/commands/memory-init.md +81 -0
- package/opencode/commands/memory-search.md +50 -0
- package/opencode/commands/memory-status.md +56 -0
- package/opencode/commands/research.md +36 -0
- package/opencode/commands/search.md +38 -0
- package/opencode/commands/slides.md +32 -0
- package/opencode/commands/spreadsheet.md +30 -0
- package/opencode/memory.json +37 -0
- package/opencode/ocx.jsonc +10 -0
- package/opencode/opencode.jsonc +103 -0
- package/opencode/package.json +25 -0
- package/opencode/patches/apply.sh +19 -0
- package/opencode/patches/opencode-pty-spawn.txt +49 -0
- package/opencode/plugin/background-agents.ts.disabled +483 -0
- package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
- package/opencode/plugin/kdco-primitives/index.ts +26 -0
- package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
- package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
- package/opencode/plugin/kdco-primitives/shell.ts +138 -0
- package/opencode/plugin/kdco-primitives/temp.ts +36 -0
- package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
- package/opencode/plugin/kdco-primitives/types.ts +13 -0
- package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
- package/opencode/plugin/memory.ts +306 -0
- package/opencode/plugin/worktree/state.ts +412 -0
- package/opencode/plugin/worktree/terminal.ts +1002 -0
- package/opencode/plugin/worktree.ts +861 -0
- package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
- package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
- package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
- package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
- package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
- package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
- package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
- package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
- package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
- package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
- package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
- package/opencode/skills/KORTIX-email/SKILL.md +145 -0
- package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
- package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
- package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
- package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
- package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
- package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
- package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
- package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
- package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
- package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
- package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
- package/opencode/skills/KORTIX-pdf/forms.md +36 -0
- package/opencode/skills/KORTIX-pdf/reference.md +105 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
- package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
- package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
- package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
- package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
- package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
- package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
- package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
- package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
- package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
- package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
- package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
- package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
- package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
- package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
- package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
- package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
- package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
- package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
- package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
- package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
- package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
- package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
- package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
- package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
- package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
- package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
- package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
- package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
- package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
- package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
- package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
- package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
- package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
- package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
- package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
- package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
- package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
- package/opencode/skills/KORTIX-session-search/Untitled +1 -0
- package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
- package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
- package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
- package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
- package/opencode/tools/image-gen.ts +342 -0
- package/opencode/tools/image-search.ts +190 -0
- package/opencode/tools/memory-get.ts +168 -0
- package/opencode/tools/memory-search.ts +247 -0
- package/opencode/tools/presentation-gen.ts +723 -0
- package/opencode/tools/scrape-webpage.ts +115 -0
- package/opencode/tools/scripts/.python-version +1 -0
- package/opencode/tools/scripts/convert_pdf.py +184 -0
- package/opencode/tools/scripts/convert_pptx.py +562 -0
- package/opencode/tools/scripts/pyproject.toml +11 -0
- package/opencode/tools/scripts/uv.lock +287 -0
- package/opencode/tools/scripts/validate_slide.py +74 -0
- package/opencode/tools/show-user.ts +217 -0
- package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
- package/opencode/tools/tests/image-gen.test.ts +215 -0
- package/opencode/tools/tests/image-search.test.ts +125 -0
- package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
- package/opencode/tools/tests/presentation-gen.test.ts +389 -0
- package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
- package/opencode/tools/tests/show-user.test.ts +241 -0
- package/opencode/tools/tests/video-gen.test.ts +110 -0
- package/opencode/tools/tests/web-search.test.ts +106 -0
- package/opencode/tools/video-gen.ts +200 -0
- package/opencode/tools/web-search.ts +153 -0
- package/opencode/tsconfig.json +29 -0
- package/package.json +36 -0
- package/patch-agent-browser.js +100 -0
- package/postinstall.sh +88 -0
- package/services/KORTIX-presentation-viewer/run +37 -0
- package/services/agent-browser-viewer/run +48 -0
- package/services/kortix-master/run +16 -0
- package/services/lss-sync/run +22 -0
- package/services/opencode-serve/run +25 -0
- 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
|
+
}
|