@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,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()
|