lorekeep 0.3.1__tar.gz → 0.5.0__tar.gz
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.
- lorekeep-0.5.0/.release-please-manifest.json +3 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/CHANGELOG.md +14 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/PKG-INFO +1 -1
- {lorekeep-0.3.1 → lorekeep-0.5.0}/pyproject.toml +1 -1
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/__init__.py +1 -1
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/cli.py +108 -6
- lorekeep-0.5.0/src/lorekeep/importer/codex.py +287 -0
- lorekeep-0.5.0/src/lorekeep/importer/opencode.py +255 -0
- lorekeep-0.5.0/src/lorekeep/integrations/claude_code.py +50 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/integrations/codex.py +32 -6
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/integrations/common.py +10 -7
- lorekeep-0.5.0/src/lorekeep/integrations/cursor.py +45 -0
- lorekeep-0.5.0/src/lorekeep/integrations/opencode.py +60 -0
- lorekeep-0.5.0/tests/test_import_codex.py +238 -0
- lorekeep-0.5.0/tests/test_import_opencode.py +170 -0
- lorekeep-0.5.0/tests/test_session_hook.py +299 -0
- lorekeep-0.3.1/.release-please-manifest.json +0 -3
- lorekeep-0.3.1/src/lorekeep/integrations/claude_code.py +0 -19
- lorekeep-0.3.1/src/lorekeep/integrations/cursor.py +0 -21
- lorekeep-0.3.1/src/lorekeep/integrations/opencode.py +0 -43
- {lorekeep-0.3.1 → lorekeep-0.5.0}/.commitlintrc.json +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/.github/workflows/auto-merge-release-please.yml +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/.github/workflows/ci.yml +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/.github/workflows/lint-commits.yml +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/.github/workflows/release-please.yml +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/.gitignore +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/.lorekeep/config.yaml.example +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/.lorekeep/schema.json +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/.pre-commit-config.yaml +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/AGENTS.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/CLAUDE.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/LICENSE +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/README.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/SECURITY.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/cover.jpeg +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/README.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/architecture/agent.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/architecture/data-model.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/architecture/evaluation.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/architecture/journal.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/architecture/overview.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/architecture/permission.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/architecture/pipeline.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/architecture/serve-mcp.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/architecture/temporal.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/guides/backup.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/guides/compile.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/guides/data-home.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/guides/getting-started.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/guides/import.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/guides/serve.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/release-please-config.json +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/scripts/check-conventional-commit.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/agent.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/backup.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/compile/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/compile/extract.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/compile/ingest.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/compile/providers.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/compile/resolve.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/compile/writer.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/config.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/defaults.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/eval/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/eval/construction.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/eval/gold.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/eval/retrieval.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/facts_io.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/importer/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/importer/claude.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/importer/cursor.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/integrations/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/integrations/detect.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/journal.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/mcp_server.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/models.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/paths.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/perm/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/perm/ns.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/pipeline.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/schema_io.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/store/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/store/fts.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/src/lorekeep/store/graph.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/conftest.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/fixtures/claude-session/memory/MEMORY.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/fixtures/claude-session/memory/lorekeep-design.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/fixtures/claude-session/memory/lorekeep-project-state.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/fixtures/claude-session/test.jsonl +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/fixtures/gold/payments.facts.jsonl +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/fixtures/raw/backend/payments.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/fixtures/retrieval/questions.json +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/fixtures/schema.json +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_agent_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_agent_detect.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_backup.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_backup_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_compile_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_config.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_construction_eval.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_defaults.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_determinism.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_doctor_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_eval_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_extract.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_fts.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_graph_store.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_import_claude.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_import_cursor.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_ingest.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_init_chain.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_init_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_integrations.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_mcp_add_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_mcp_reload.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_mcp_server.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_mcp_write_tools.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_merge_journals.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_models.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_paths.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_perm.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_pipeline.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_providers.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_resolve.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_retrieval_eval.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_schema_io.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_serve_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_smoke.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_watch_e2e.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_watch_sessions.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/tests/test_writer.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.5.0}/uv.lock +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0](https://github.com/manhhailua/lorekeep/compare/v0.4.0...v0.5.0) (2026-06-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add import --from codex and --from opencode + hooks for all 4 agents ([#66](https://github.com/manhhailua/lorekeep/issues/66)) ([8a1e364](https://github.com/manhhailua/lorekeep/commit/8a1e364e19cf1b45944cea2ed89d9d7468fec9c6))
|
|
9
|
+
|
|
10
|
+
## [0.4.0](https://github.com/manhhailua/lorekeep/compare/v0.3.1...v0.4.0) (2026-06-28)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* trigger memory import on Claude session end via SessionEnd hook ([#64](https://github.com/manhhailua/lorekeep/issues/64)) ([5914663](https://github.com/manhhailua/lorekeep/commit/5914663a5c02f76e1085a1f9d663066979396d77))
|
|
16
|
+
|
|
3
17
|
## [0.3.1](https://github.com/manhhailua/lorekeep/compare/v0.3.0...v0.3.1) (2026-06-28)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -63,6 +63,38 @@ def version() -> None:
|
|
|
63
63
|
typer.echo(f"lorekeep {__version__}")
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
@app.command(hidden=True)
|
|
67
|
+
def hook() -> None:
|
|
68
|
+
"""Session lifecycle hook: quick-import memories from all agents.
|
|
69
|
+
|
|
70
|
+
Agent-agnostic — tries Claude and Codex memory imports (idempotent
|
|
71
|
+
via manifest). Each is a no-op if nothing changed. Daemon picks up
|
|
72
|
+
raw/ changes for compile.
|
|
73
|
+
"""
|
|
74
|
+
p = resolve_paths()
|
|
75
|
+
total = 0
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
from lorekeep.importer.claude import find_current_session as find_claude
|
|
79
|
+
from lorekeep.importer.claude import import_memories as import_claude_mem
|
|
80
|
+
session_dir = find_claude()
|
|
81
|
+
if session_dir and (session_dir / "memory").is_dir():
|
|
82
|
+
written = import_claude_mem(session_dir, p["raw"], "claude-memory")
|
|
83
|
+
total += len(written)
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
from lorekeep.importer.codex import import_memories as import_codex_mem
|
|
89
|
+
written = import_codex_mem(p["raw"], "codex-memory")
|
|
90
|
+
total += len(written)
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
if total:
|
|
95
|
+
typer.echo(f"lorekeep: imported {total} memory file(s)")
|
|
96
|
+
|
|
97
|
+
|
|
66
98
|
@app.command()
|
|
67
99
|
def compile() -> None:
|
|
68
100
|
"""Compile raw/ into graph/facts.jsonl."""
|
|
@@ -239,6 +271,7 @@ def mcp_add(
|
|
|
239
271
|
p = resolve_paths()
|
|
240
272
|
config = load_config(p["config"])
|
|
241
273
|
command, args = resolve_command(config.install_source)
|
|
274
|
+
hook_cmd, hook_args = resolve_command(config.install_source, ["hook"])
|
|
242
275
|
|
|
243
276
|
target = Path.cwd() if scope == "project" else Path.home()
|
|
244
277
|
writers = _agent_writers()
|
|
@@ -247,6 +280,9 @@ def mcp_add(
|
|
|
247
280
|
raise typer.Exit(code=1)
|
|
248
281
|
written = writers[agent].write_config(target, command, args, ns)
|
|
249
282
|
typer.echo(f"wrote {agent} config -> {written}")
|
|
283
|
+
if hasattr(writers[agent], "write_hook"):
|
|
284
|
+
hook_path = writers[agent].write_hook(target, hook_cmd, hook_args)
|
|
285
|
+
typer.echo(f"wrote session-end hook -> {hook_path}")
|
|
250
286
|
typer.echo("\n" + agent_memory_snippet())
|
|
251
287
|
|
|
252
288
|
|
|
@@ -452,6 +488,7 @@ def _auto_wire_agents(p: dict, ns: str) -> None:
|
|
|
452
488
|
|
|
453
489
|
config = load_config(p["config"])
|
|
454
490
|
command, args = resolve_command(config.install_source)
|
|
491
|
+
hook_cmd, hook_args = resolve_command(config.install_source, ["hook"])
|
|
455
492
|
|
|
456
493
|
writers = _agent_writers()
|
|
457
494
|
target = Path.cwd()
|
|
@@ -464,6 +501,9 @@ def _auto_wire_agents(p: dict, ns: str) -> None:
|
|
|
464
501
|
try:
|
|
465
502
|
written = writer.write_config(target, command, args, ns)
|
|
466
503
|
typer.echo(f" wired {agent_name} -> {written}")
|
|
504
|
+
if hasattr(writer, "write_hook"):
|
|
505
|
+
hook_path = writer.write_hook(target, hook_cmd, hook_args)
|
|
506
|
+
typer.echo(f" hooked {agent_name} session-end -> {hook_path}")
|
|
467
507
|
except Exception as exc:
|
|
468
508
|
typer.echo(f" {agent_name}: failed ({exc})")
|
|
469
509
|
|
|
@@ -614,18 +654,80 @@ def import_cmd(
|
|
|
614
654
|
"""Import knowledge from an agent's sessions into raw/.
|
|
615
655
|
|
|
616
656
|
Sources:
|
|
617
|
-
claude
|
|
618
|
-
|
|
619
|
-
cursor
|
|
620
|
-
|
|
657
|
+
claude Claude Code sessions. --quick copies memory/*.md only (no LLM);
|
|
658
|
+
default (deep) adds LLM-summarized transcript analysis.
|
|
659
|
+
cursor Cursor composer conversations (GLOBAL state.vscdb). Deep-only.
|
|
660
|
+
codex Codex CLI rollout transcripts ($CODEX_HOME/sessions/).
|
|
661
|
+
--quick copies memories/*.md only; default (deep) summarizes.
|
|
662
|
+
opencode opencode sessions (SQLite DB). Deep-only — no memory dir.
|
|
621
663
|
"""
|
|
622
|
-
if from_source not in ("claude", "cursor"):
|
|
623
|
-
typer.echo(f"unknown source: {from_source} (claude | cursor)")
|
|
664
|
+
if from_source not in ("claude", "cursor", "codex", "opencode"):
|
|
665
|
+
typer.echo(f"unknown source: {from_source} (claude | cursor | codex | opencode)")
|
|
624
666
|
raise typer.Exit(code=1)
|
|
625
667
|
|
|
626
668
|
p = resolve_paths()
|
|
627
669
|
config = load_config(p["config"])
|
|
628
670
|
|
|
671
|
+
# --- Codex: rollout JSONL transcripts, quick + deep --------------------
|
|
672
|
+
if from_source == "codex":
|
|
673
|
+
from lorekeep.importer.codex import find_current_session as find_codex, import_codex
|
|
674
|
+
|
|
675
|
+
rollout_path = Path(session_path).expanduser() if session_path else None
|
|
676
|
+
if rollout_path is None:
|
|
677
|
+
rollout_path = find_codex()
|
|
678
|
+
if rollout_path is None and not quick:
|
|
679
|
+
typer.echo("error: no Codex session found for this project. "
|
|
680
|
+
"Run Codex CLI here first, or pass --session-path.")
|
|
681
|
+
raise typer.Exit(code=1)
|
|
682
|
+
|
|
683
|
+
result = import_codex(
|
|
684
|
+
raw_root=p["raw"],
|
|
685
|
+
rollout_path=rollout_path,
|
|
686
|
+
quick=quick,
|
|
687
|
+
memory_ns=memory_ns,
|
|
688
|
+
session_ns=session_ns or "codex-session",
|
|
689
|
+
provider=None if quick else _make_import_provider(config),
|
|
690
|
+
dry_run=dry_run,
|
|
691
|
+
)
|
|
692
|
+
mem_count = len(result.get("memory", []))
|
|
693
|
+
ses_count = len(result.get("session", []))
|
|
694
|
+
if dry_run:
|
|
695
|
+
typer.echo(f"dry-run: would import {mem_count} memories, {ses_count} session files")
|
|
696
|
+
else:
|
|
697
|
+
typer.echo(f"imported: {mem_count} memories -> raw/{memory_ns}/, "
|
|
698
|
+
f"{ses_count} session files -> raw/{session_ns or 'codex-session'}/")
|
|
699
|
+
return
|
|
700
|
+
|
|
701
|
+
# --- opencode: SQLite DB, deep-only ------------------------------------
|
|
702
|
+
if from_source == "opencode":
|
|
703
|
+
if quick:
|
|
704
|
+
typer.echo("error: opencode import is deep-only (--quick not supported)")
|
|
705
|
+
raise typer.Exit(code=1)
|
|
706
|
+
|
|
707
|
+
from lorekeep.importer.opencode import find_current_session as find_oc, import_opencode
|
|
708
|
+
|
|
709
|
+
sid = session_path or find_oc()
|
|
710
|
+
if sid is None:
|
|
711
|
+
typer.echo("error: no opencode session found for this project. "
|
|
712
|
+
"Run opencode here first, or pass --session-path <session-id>.")
|
|
713
|
+
raise typer.Exit(code=1)
|
|
714
|
+
|
|
715
|
+
ns = session_ns or "opencode-session"
|
|
716
|
+
result = import_opencode(
|
|
717
|
+
raw_root=p["raw"],
|
|
718
|
+
session_id=sid,
|
|
719
|
+
session_ns=ns,
|
|
720
|
+
provider=_make_import_provider(config),
|
|
721
|
+
dry_run=dry_run,
|
|
722
|
+
)
|
|
723
|
+
ses_count = len(result.get("session", []))
|
|
724
|
+
if dry_run:
|
|
725
|
+
typer.echo(f"dry-run: would import {ses_count} opencode session files")
|
|
726
|
+
else:
|
|
727
|
+
typer.echo(f"imported: {ses_count} session files -> raw/{ns}/")
|
|
728
|
+
typer.echo("next: lorekeep compile")
|
|
729
|
+
return
|
|
730
|
+
|
|
629
731
|
# --- Cursor: global composer conversations, deep-only ------------------
|
|
630
732
|
if from_source == "cursor":
|
|
631
733
|
if quick:
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Import knowledge from Codex CLI sessions into lorekeep raw/ tree.
|
|
2
|
+
|
|
3
|
+
Codex stores sessions as JSONL rollout files at:
|
|
4
|
+
$CODEX_HOME/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl
|
|
5
|
+
|
|
6
|
+
Each line = {"timestamp": "...", "type": "...", "payload": {...}}
|
|
7
|
+
The first line is always "session_meta" with cwd, session_id, model.
|
|
8
|
+
Conversation turns are in "response_item" lines with payload.type == "message".
|
|
9
|
+
|
|
10
|
+
Memory files live at $CODEX_HOME/memories/*.md (optional, may not exist).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from lorekeep.compile.providers import LLMProvider
|
|
21
|
+
from lorekeep.importer.claude import (
|
|
22
|
+
ConversationTurn,
|
|
23
|
+
chunk_turns,
|
|
24
|
+
summarize_batch,
|
|
25
|
+
load_import_manifest,
|
|
26
|
+
save_import_manifest,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_CODEX_PREFIX_RE = re.compile(r"^## My request for Codex:\n", re.DOTALL)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _codex_home() -> Path:
|
|
33
|
+
return Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Session discovery
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def find_current_session(cwd: Path | None = None) -> Path | None:
|
|
42
|
+
"""Find the most recent Codex rollout file for the given cwd.
|
|
43
|
+
|
|
44
|
+
Codex shards sessions by date, not by project. The cwd is stored
|
|
45
|
+
inside the first line (session_meta) of each rollout file.
|
|
46
|
+
"""
|
|
47
|
+
cwd = str((cwd or Path.cwd()).resolve())
|
|
48
|
+
sessions_dir = _codex_home() / "sessions"
|
|
49
|
+
if not sessions_dir.is_dir():
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
rollouts = sorted(sessions_dir.rglob("rollout-*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
53
|
+
for rp in rollouts:
|
|
54
|
+
try:
|
|
55
|
+
first_line = rp.read_text(encoding="utf-8").splitlines()[0].strip()
|
|
56
|
+
if not first_line:
|
|
57
|
+
continue
|
|
58
|
+
rec = json.loads(first_line)
|
|
59
|
+
if rec.get("type") == "session_meta":
|
|
60
|
+
session_cwd = rec.get("payload", {}).get("cwd", "")
|
|
61
|
+
if session_cwd and Path(session_cwd).resolve() == Path(cwd):
|
|
62
|
+
return rp
|
|
63
|
+
except (json.JSONDecodeError, IndexError, OSError):
|
|
64
|
+
continue
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Transcript parsing
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _extract_message_text(content: list[dict] | str) -> str:
|
|
74
|
+
"""Extract text from a Codex message content array."""
|
|
75
|
+
if isinstance(content, str):
|
|
76
|
+
return content
|
|
77
|
+
parts = []
|
|
78
|
+
for block in content:
|
|
79
|
+
if isinstance(block, dict):
|
|
80
|
+
text = block.get("text", "")
|
|
81
|
+
if text:
|
|
82
|
+
parts.append(text)
|
|
83
|
+
return "\n".join(parts)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _strip_codex_prefix(text: str) -> str:
|
|
87
|
+
"""Strip Codex's '## My request for Codex:' prefix from user messages."""
|
|
88
|
+
return _CODEX_PREFIX_RE.sub("", text)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_rollout(rollout_path: Path) -> list[ConversationTurn]:
|
|
92
|
+
"""Parse a Codex rollout JSONL file into structured conversation turns."""
|
|
93
|
+
turns: list[ConversationTurn] = []
|
|
94
|
+
current_user: str | None = None
|
|
95
|
+
current_assistant: list[str] = []
|
|
96
|
+
current_tools: list[str] = []
|
|
97
|
+
|
|
98
|
+
for line in rollout_path.read_text(encoding="utf-8").splitlines():
|
|
99
|
+
line = line.strip()
|
|
100
|
+
if not line:
|
|
101
|
+
continue
|
|
102
|
+
try:
|
|
103
|
+
rec = json.loads(line)
|
|
104
|
+
except json.JSONDecodeError:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if rec.get("type") != "response_item":
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
payload = rec.get("payload", {})
|
|
111
|
+
ptype = payload.get("type", "")
|
|
112
|
+
|
|
113
|
+
if ptype == "message":
|
|
114
|
+
role = payload.get("role", "")
|
|
115
|
+
text = _extract_message_text(payload.get("content", []))
|
|
116
|
+
|
|
117
|
+
if role == "user":
|
|
118
|
+
if current_user is not None:
|
|
119
|
+
turns.append(ConversationTurn(
|
|
120
|
+
user_content=current_user,
|
|
121
|
+
assistant_text="\n\n".join(current_assistant),
|
|
122
|
+
tool_calls=list(current_tools),
|
|
123
|
+
))
|
|
124
|
+
current_user = _strip_codex_prefix(text)
|
|
125
|
+
current_assistant = []
|
|
126
|
+
current_tools = []
|
|
127
|
+
elif role == "assistant":
|
|
128
|
+
current_assistant.append(text)
|
|
129
|
+
|
|
130
|
+
elif ptype in ("function_call", "custom_tool_call"):
|
|
131
|
+
name = payload.get("name", "unknown")
|
|
132
|
+
if name not in current_tools:
|
|
133
|
+
current_tools.append(name)
|
|
134
|
+
|
|
135
|
+
if current_user is not None:
|
|
136
|
+
turns.append(ConversationTurn(
|
|
137
|
+
user_content=current_user,
|
|
138
|
+
assistant_text="\n\n".join(current_assistant),
|
|
139
|
+
tool_calls=list(current_tools),
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
return turns
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Memory import (quick mode)
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def import_memories(
|
|
151
|
+
raw_root: Path,
|
|
152
|
+
namespace: str = "codex-memory",
|
|
153
|
+
*,
|
|
154
|
+
dry_run: bool = False,
|
|
155
|
+
) -> list[Path]:
|
|
156
|
+
"""Copy memories/*.md files from $CODEX_HOME into raw/<namespace>/.
|
|
157
|
+
|
|
158
|
+
Returns list of written paths. Idempotent via SHA-256 manifest.
|
|
159
|
+
"""
|
|
160
|
+
mem_dir = _codex_home() / "memories"
|
|
161
|
+
if not mem_dir.is_dir():
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
dest_dir = raw_root / namespace
|
|
165
|
+
manifest = load_import_manifest(raw_root, namespace)
|
|
166
|
+
written: list[Path] = []
|
|
167
|
+
|
|
168
|
+
for src in sorted(mem_dir.glob("*.md")):
|
|
169
|
+
content = src.read_text(encoding="utf-8")
|
|
170
|
+
h = hashlib.sha256(content.encode()).hexdigest()
|
|
171
|
+
if manifest.get(str(src)) == h:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
if dry_run:
|
|
175
|
+
written.append(dest_dir / src.name)
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
dest = dest_dir / src.name
|
|
180
|
+
dest.write_text(content, encoding="utf-8")
|
|
181
|
+
manifest[str(src)] = h
|
|
182
|
+
written.append(dest)
|
|
183
|
+
|
|
184
|
+
if not dry_run and written:
|
|
185
|
+
save_import_manifest(raw_root, namespace, manifest)
|
|
186
|
+
|
|
187
|
+
return written
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Deep session import
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def import_session_deep(
|
|
196
|
+
rollout_path: Path,
|
|
197
|
+
raw_root: Path,
|
|
198
|
+
namespace: str = "codex-session",
|
|
199
|
+
provider: LLMProvider | None = None,
|
|
200
|
+
*,
|
|
201
|
+
batch_max_chars: int = 20_000,
|
|
202
|
+
dry_run: bool = False,
|
|
203
|
+
) -> list[Path]:
|
|
204
|
+
"""Deep-import a Codex session: parse, chunk, summarize each batch via LLM."""
|
|
205
|
+
session_id = rollout_path.stem
|
|
206
|
+
|
|
207
|
+
transcript_hash = hashlib.sha256(rollout_path.read_bytes()).hexdigest()
|
|
208
|
+
manifest = load_import_manifest(raw_root, namespace)
|
|
209
|
+
manifest_key = str(rollout_path)
|
|
210
|
+
|
|
211
|
+
if not dry_run and manifest.get(manifest_key) == transcript_hash:
|
|
212
|
+
return []
|
|
213
|
+
|
|
214
|
+
turns = parse_rollout(rollout_path)
|
|
215
|
+
if not turns:
|
|
216
|
+
return []
|
|
217
|
+
|
|
218
|
+
batches = chunk_turns(turns, max_chars=batch_max_chars)
|
|
219
|
+
dest_dir = raw_root / namespace
|
|
220
|
+
written: list[Path] = []
|
|
221
|
+
|
|
222
|
+
previous_summary = ""
|
|
223
|
+
for i, batch in enumerate(batches):
|
|
224
|
+
if dry_run:
|
|
225
|
+
dest = dest_dir / f"session-{session_id}-batch-{i + 1:02d}.md"
|
|
226
|
+
written.append(dest)
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
dest = dest_dir / f"session-{session_id}-batch-{i + 1:02d}.md"
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
if provider is None:
|
|
234
|
+
raise RuntimeError("deep mode requires a provider")
|
|
235
|
+
md = summarize_batch(
|
|
236
|
+
batch, i, len(batches), namespace, session_id,
|
|
237
|
+
provider, previous_summary,
|
|
238
|
+
)
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
md = f"# Error summarizing batch {i + 1}\n\nLLM call failed: {exc}\n"
|
|
241
|
+
|
|
242
|
+
dest.write_text(md, encoding="utf-8")
|
|
243
|
+
previous_summary = md[:1000]
|
|
244
|
+
written.append(dest)
|
|
245
|
+
|
|
246
|
+
if not dry_run and written:
|
|
247
|
+
manifest[manifest_key] = transcript_hash
|
|
248
|
+
save_import_manifest(raw_root, namespace, manifest)
|
|
249
|
+
|
|
250
|
+
return written
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
# Orchestrator
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def import_codex(
|
|
259
|
+
raw_root: Path,
|
|
260
|
+
rollout_path: Path | None = None,
|
|
261
|
+
*,
|
|
262
|
+
quick: bool = False,
|
|
263
|
+
memory_ns: str = "codex-memory",
|
|
264
|
+
session_ns: str = "codex-session",
|
|
265
|
+
provider: LLMProvider | None = None,
|
|
266
|
+
batch_max_chars: int = 20_000,
|
|
267
|
+
dry_run: bool = False,
|
|
268
|
+
) -> dict[str, list[Path]]:
|
|
269
|
+
"""Import a Codex session into the lorekeep raw/ tree.
|
|
270
|
+
|
|
271
|
+
Returns ``{"memory": [...], "session": [...]}`` with written file paths.
|
|
272
|
+
"""
|
|
273
|
+
result: dict[str, list[Path]] = {"memory": [], "session": []}
|
|
274
|
+
|
|
275
|
+
result["memory"] = import_memories(raw_root, namespace=memory_ns, dry_run=dry_run)
|
|
276
|
+
|
|
277
|
+
if not quick:
|
|
278
|
+
if rollout_path is None:
|
|
279
|
+
rollout_path = find_current_session()
|
|
280
|
+
if rollout_path is not None:
|
|
281
|
+
result["session"] = import_session_deep(
|
|
282
|
+
rollout_path, raw_root, namespace=session_ns,
|
|
283
|
+
provider=provider, batch_max_chars=batch_max_chars,
|
|
284
|
+
dry_run=dry_run,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return result
|