lorekeep 0.3.0__tar.gz → 0.4.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.
Files changed (130) hide show
  1. lorekeep-0.4.0/.release-please-manifest.json +3 -0
  2. {lorekeep-0.3.0 → lorekeep-0.4.0}/AGENTS.md +3 -3
  3. {lorekeep-0.3.0 → lorekeep-0.4.0}/CHANGELOG.md +14 -0
  4. {lorekeep-0.3.0 → lorekeep-0.4.0}/PKG-INFO +1 -1
  5. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/architecture/evaluation.md +1 -1
  6. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/guides/getting-started.md +4 -4
  7. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/guides/serve.md +2 -2
  8. {lorekeep-0.3.0 → lorekeep-0.4.0}/pyproject.toml +1 -1
  9. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/__init__.py +1 -1
  10. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/cli.py +68 -42
  11. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/defaults.py +1 -1
  12. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/importer/claude.py +15 -0
  13. lorekeep-0.4.0/src/lorekeep/integrations/claude_code.py +50 -0
  14. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/integrations/common.py +10 -7
  15. lorekeep-0.4.0/src/lorekeep/integrations/cursor.py +45 -0
  16. lorekeep-0.4.0/tests/conftest.py +57 -0
  17. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_agent_cli.py +6 -12
  18. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_compile_cli.py +1 -2
  19. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_import_claude.py +55 -0
  20. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_import_cursor.py +1 -3
  21. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_init_chain.py +4 -6
  22. lorekeep-0.4.0/tests/test_session_hook.py +246 -0
  23. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_watch_e2e.py +3 -4
  24. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_watch_sessions.py +0 -2
  25. lorekeep-0.3.0/.release-please-manifest.json +0 -3
  26. lorekeep-0.3.0/src/lorekeep/integrations/claude_code.py +0 -19
  27. lorekeep-0.3.0/src/lorekeep/integrations/cursor.py +0 -21
  28. lorekeep-0.3.0/tests/conftest.py +0 -7
  29. {lorekeep-0.3.0 → lorekeep-0.4.0}/.commitlintrc.json +0 -0
  30. {lorekeep-0.3.0 → lorekeep-0.4.0}/.github/workflows/auto-merge-release-please.yml +0 -0
  31. {lorekeep-0.3.0 → lorekeep-0.4.0}/.github/workflows/ci.yml +0 -0
  32. {lorekeep-0.3.0 → lorekeep-0.4.0}/.github/workflows/lint-commits.yml +0 -0
  33. {lorekeep-0.3.0 → lorekeep-0.4.0}/.github/workflows/release-please.yml +0 -0
  34. {lorekeep-0.3.0 → lorekeep-0.4.0}/.gitignore +0 -0
  35. {lorekeep-0.3.0 → lorekeep-0.4.0}/.lorekeep/config.yaml.example +0 -0
  36. {lorekeep-0.3.0 → lorekeep-0.4.0}/.lorekeep/schema.json +0 -0
  37. {lorekeep-0.3.0 → lorekeep-0.4.0}/.pre-commit-config.yaml +0 -0
  38. {lorekeep-0.3.0 → lorekeep-0.4.0}/CLAUDE.md +0 -0
  39. {lorekeep-0.3.0 → lorekeep-0.4.0}/LICENSE +0 -0
  40. {lorekeep-0.3.0 → lorekeep-0.4.0}/README.md +0 -0
  41. {lorekeep-0.3.0 → lorekeep-0.4.0}/SECURITY.md +0 -0
  42. {lorekeep-0.3.0 → lorekeep-0.4.0}/cover.jpeg +0 -0
  43. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/README.md +0 -0
  44. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/architecture/agent.md +0 -0
  45. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/architecture/data-model.md +0 -0
  46. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/architecture/journal.md +0 -0
  47. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/architecture/overview.md +0 -0
  48. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/architecture/permission.md +0 -0
  49. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/architecture/pipeline.md +0 -0
  50. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/architecture/serve-mcp.md +0 -0
  51. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/architecture/temporal.md +0 -0
  52. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/guides/backup.md +0 -0
  53. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/guides/compile.md +0 -0
  54. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/guides/data-home.md +0 -0
  55. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/guides/import.md +0 -0
  56. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md +0 -0
  57. {lorekeep-0.3.0 → lorekeep-0.4.0}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md +0 -0
  58. {lorekeep-0.3.0 → lorekeep-0.4.0}/release-please-config.json +0 -0
  59. {lorekeep-0.3.0 → lorekeep-0.4.0}/scripts/check-conventional-commit.py +0 -0
  60. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/agent.py +0 -0
  61. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/backup.py +0 -0
  62. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/compile/__init__.py +0 -0
  63. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/compile/extract.py +0 -0
  64. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/compile/ingest.py +0 -0
  65. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/compile/providers.py +0 -0
  66. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/compile/resolve.py +0 -0
  67. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/compile/writer.py +0 -0
  68. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/config.py +0 -0
  69. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/eval/__init__.py +0 -0
  70. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/eval/construction.py +0 -0
  71. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/eval/gold.py +0 -0
  72. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/eval/retrieval.py +0 -0
  73. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/facts_io.py +0 -0
  74. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/importer/__init__.py +0 -0
  75. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/importer/cursor.py +0 -0
  76. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/integrations/__init__.py +0 -0
  77. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/integrations/codex.py +0 -0
  78. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/integrations/detect.py +0 -0
  79. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/integrations/opencode.py +0 -0
  80. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/journal.py +0 -0
  81. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/mcp_server.py +0 -0
  82. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/models.py +0 -0
  83. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/paths.py +0 -0
  84. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/perm/__init__.py +0 -0
  85. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/perm/ns.py +0 -0
  86. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/pipeline.py +0 -0
  87. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/schema_io.py +0 -0
  88. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/store/__init__.py +0 -0
  89. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/store/fts.py +0 -0
  90. {lorekeep-0.3.0 → lorekeep-0.4.0}/src/lorekeep/store/graph.py +0 -0
  91. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/fixtures/claude-session/memory/MEMORY.md +0 -0
  92. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/fixtures/claude-session/memory/lorekeep-design.md +0 -0
  93. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/fixtures/claude-session/memory/lorekeep-project-state.md +0 -0
  94. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/fixtures/claude-session/test.jsonl +0 -0
  95. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/fixtures/gold/payments.facts.jsonl +0 -0
  96. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/fixtures/raw/backend/payments.md +0 -0
  97. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/fixtures/retrieval/questions.json +0 -0
  98. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/fixtures/schema.json +0 -0
  99. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_agent_detect.py +0 -0
  100. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_backup.py +0 -0
  101. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_backup_cli.py +0 -0
  102. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_config.py +0 -0
  103. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_construction_eval.py +0 -0
  104. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_defaults.py +0 -0
  105. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_determinism.py +0 -0
  106. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_doctor_cli.py +0 -0
  107. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_eval_cli.py +0 -0
  108. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_extract.py +0 -0
  109. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_fts.py +0 -0
  110. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_graph_store.py +0 -0
  111. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_ingest.py +0 -0
  112. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_init_cli.py +0 -0
  113. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_integrations.py +0 -0
  114. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_mcp_add_cli.py +0 -0
  115. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_mcp_reload.py +0 -0
  116. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_mcp_server.py +0 -0
  117. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_mcp_write_tools.py +0 -0
  118. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_merge_journals.py +0 -0
  119. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_models.py +0 -0
  120. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_paths.py +0 -0
  121. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_perm.py +0 -0
  122. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_pipeline.py +0 -0
  123. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_providers.py +0 -0
  124. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_resolve.py +0 -0
  125. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_retrieval_eval.py +0 -0
  126. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_schema_io.py +0 -0
  127. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_serve_cli.py +0 -0
  128. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_smoke.py +0 -0
  129. {lorekeep-0.3.0 → lorekeep-0.4.0}/tests/test_writer.py +0 -0
  130. {lorekeep-0.3.0 → lorekeep-0.4.0}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -35,7 +35,7 @@ uv run lorekeep <command> # run the CLI in dev mode
35
35
  | `backup [--init <remote-url>]` | Commit + push `.lorekeep/` to your private backup git repo |
36
36
  | `version` | Print version |
37
37
 
38
- **Offline / no-LLM mode:** set `LOREKEEP_PROVIDER=fake` to make `compile` and `import` use `FakeProvider` with canned responses. **All CLI/compile/import tests use this** — no API key or real model required.
38
+ **Offline / no-LLM mode:** tests inject `FakeProvider` via monkeypatch (`patch_make_provider` / `patch_make_import_provider` fixtures in `conftest.py`). **All CLI/compile/import tests use this** — no API key or real model required.
39
39
 
40
40
  ## Architecture: two strictly separated phases
41
41
 
@@ -83,7 +83,7 @@ Pure (no I/O), 4-tier precedence high→low: explicit `LOREKEEP_RAW/OUT/CACHE/SC
83
83
 
84
84
  ## Tests
85
85
 
86
- `~140` tests, no network. Gold corpus in `tests/fixtures/gold/`, raw fixtures in `tests/fixtures/raw/`. Compile/serve/import tests set `LOREKEEP_*` env vars and `LOREKEEP_PROVIDER=fake` to pin paths and avoid the LLM — follow this pattern for any new CLI test rather than hitting a real provider.
86
+ `~140` tests, no network. Gold corpus in `tests/fixtures/gold/`, raw fixtures in `tests/fixtures/raw/`. Compile/serve/import tests inject `FakeProvider` via `patch_make_provider` / `patch_make_import_provider` conftest fixtures to pin paths and avoid the LLM — follow this pattern for any new CLI test rather than hitting a real provider.
87
87
 
88
88
  ## Cursor Cloud specific instructions
89
89
 
@@ -91,7 +91,7 @@ The startup update script already runs `uv sync`; the toolchain (Python 3.11+ vi
91
91
 
92
92
  Non-obvious caveats for running/testing here:
93
93
 
94
- - **No API key needed for any dev work.** Export `LOREKEEP_PROVIDER=fake` to run `compile`/`import` and the full end-to-end flow offline with `FakeProvider` (canned facts). To avoid polluting the repo's `.lorekeep/` data home, run demos against a throwaway data home: `LOREKEEP_HOME=/tmp/lk uv run lorekeep <cmd>`.
94
+ - **No API key needed for tests.** Tests inject `FakeProvider` via `patch_make_provider` / `patch_make_import_provider` conftest fixtures. For manual smoke testing, configure a real provider in `config.yaml`. To avoid polluting the repo's `.lorekeep/` data home, run demos against a throwaway data home: `LOREKEEP_HOME=/tmp/lk uv run lorekeep <cmd>`.
95
95
  - **End-to-end smoke flow** (offline): `init` → drop a markdown file under `.lorekeep/raw/<ns>/` → `compile` → `check`/`doctor`. Then query the graph by calling the `mcp_server.py` tools directly after `ms.configure(graph_dir=..., allowed_ns=[...], schema_path=...)` — the read tools (`search`, `get_node`, `neighbors`, `at_time`, `history`, `list_namespaces`) are plain functions, no MCP transport required. `search` returns a list of node-id strings; `get_node` returns a dict; `neighbors`/`at_time`/`changes` return `{"nodes": [...], "edges": [...]}`.
96
96
  - **`lorekeep serve` blocks on stdio** waiting for an MCP client — it won't return on its own. To just confirm it boots, run it under `timeout` with stdin closed (`timeout 3 uv run lorekeep serve --transport stdio </dev/null`); a clean timeout with no error output means success.
97
97
  - **`tests/test_mcp_reload.py::test_lazy_reload_on_facts_change` is timing-flaky** — lazy-reload triggers off `facts.jsonl` mtime, and the test can rewrite the file within one mtime tick on fast filesystems, so it intermittently fails then passes on re-run. Treat an isolated failure of just this test as a flake, not a regression.
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/manhhailua/lorekeep/compare/v0.3.1...v0.4.0) (2026-06-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * 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))
9
+
10
+ ## [0.3.1](https://github.com/manhhailua/lorekeep/compare/v0.3.0...v0.3.1) (2026-06-28)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * add manifest dedup to deep-mode session import ([#61](https://github.com/manhhailua/lorekeep/issues/61)) ([a9a074f](https://github.com/manhhailua/lorekeep/commit/a9a074f901c9a8f8e4cd2e3f906fdf4be7a773d6))
16
+
3
17
  ## [0.3.0](https://github.com/manhhailua/lorekeep/compare/v0.2.1...v0.3.0) (2026-06-28)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lorekeep
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Temporal knowledge graph for AI agents via MCP
5
5
  Project-URL: Homepage, https://github.com/manhhailua/lorekeep
6
6
  Project-URL: Repository, https://github.com/manhhailua/lorekeep
@@ -13,7 +13,7 @@
13
13
  - **Golden tests:** snapshot `facts.jsonl` for a fixture corpus; diffs catch regressions.
14
14
  - Compile-only ⇒ no concurrency to test.
15
15
 
16
- All compile/serve/import tests set `LOREKEEP_PROVIDER=fake` to avoid a real LLM — no API key required.
16
+ All compile/serve/import tests inject `FakeProvider` via monkeypatch to avoid a real LLM — no API key required.
17
17
 
18
18
  ## Evaluation
19
19
 
@@ -108,8 +108,9 @@ provider:
108
108
  Prefer an env var instead? Set `api_key_env: DEEPSEEK_API_KEY` (and
109
109
  `api_key: null`), then `export DEEPSEEK_API_KEY=sk-...`. Both work.
110
110
 
111
- > **Offline / no key?** `export LOREKEEP_PROVIDER=fake` makes `compile` use a
112
- > canned response — enough to try the rest of the flow with zero API cost.
111
+ > **No provider yet?** `init` wires agents and imports memory files
112
+ > regardless. Compile is skipped until you add an API key — the graph
113
+ > will be empty but the MCP tools are wired and ready.
113
114
 
114
115
  ## 5. Compile
115
116
 
@@ -188,8 +189,7 @@ recovery — is in [Backing up the data home](backup.md).
188
189
  - **`lorekeep serve` hangs** — it blocks on stdio waiting for an MCP client.
189
190
  To just confirm it boots, run it under a timeout with stdin closed:
190
191
  `timeout 3 uvx lorekeep serve --transport stdio </dev/null`.
191
- - **Compile gave 0 nodes** — the provider wasn't reached. Either
192
- `export LOREKEEP_PROVIDER=fake` for an offline test, or check
192
+ - **Compile gave 0 nodes** — the provider wasn't reached. Check
193
193
  `api_key_env` / `api_base` in `config.yaml`. If you switched provider or
194
194
  model after a first compile, delete `<data-home>/cache.json` (the cache key
195
195
  doesn't include the model) and recompile.
@@ -5,9 +5,9 @@ Path resolution (env > `LOREKEEP_HOME` > dev mode > XDG) is covered in [data-hom
5
5
  ## Installed use (recommended)
6
6
 
7
7
  ```bash
8
- uvx lorekeep init # bootstrap ~/.config/lorekeep + ~/.local/share/lorekeep
8
+ uvx lorekeep init # bootstrap + wire agents + compile + start daemon
9
9
  # add your docs under ~/.local/share/lorekeep/raw/<ns>/
10
- LOREKEEP_PROVIDER=fake uvx lorekeep compile # (or set a real provider in config)
10
+ uvx lorekeep compile # requires a real provider in config.yaml
11
11
  uvx lorekeep mcp add --agent claude --ns <ns>
12
12
  uvx lorekeep doctor
13
13
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lorekeep"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Temporal knowledge graph for AI agents via MCP"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1,3 +1,3 @@
1
1
  """Lorekeep — temporal knowledge graph for AI agents via MCP."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.0"
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
  import typer
9
9
 
10
10
  from lorekeep import __version__
11
- from lorekeep.compile.providers import FakeProvider, LiteLLMProvider
11
+ from lorekeep.compile.providers import LiteLLMProvider
12
12
  from lorekeep.config import Config, load_config
13
13
  from lorekeep.pipeline import compile_graph
14
14
  from lorekeep.paths import resolve_paths
@@ -39,40 +39,71 @@ def _build_provider(config: Config) -> LiteLLMProvider:
39
39
  )
40
40
 
41
41
 
42
- _FAKE_CANNED = json.dumps({
43
- "nodes": [
44
- {"id": "svc:payments-api", "type": "service", "name": "payments-api",
45
- "props": {"lang": "go"}, "valid_from": "2024-01-15"},
46
- {"id": "svc:auth", "type": "service", "name": "auth"},
47
- {"id": "team:backend", "type": "team", "name": "team-backend"},
48
- {"id": "dec:adr-007", "type": "decision",
49
- "props": {"title": "payments-api adopts internal signing"}},
50
- ],
51
- "edges": [
52
- {"type": "depends_on", "from": "svc:payments-api", "to": "svc:auth",
53
- "valid_from": "2024-01-15", "valid_to": "2025-03-01"},
54
- {"type": "decided_by", "from": "dec:adr-007", "to": "team:backend"},
55
- ],
56
- "aliases": {},
57
- })
58
-
59
-
60
- def _make_provider(config: Config) -> FakeProvider | LiteLLMProvider:
61
- """Create provider honoring LOREKEEP_PROVIDER=fake for offline/test use.
62
-
63
- Shared by standalone compile, daemon auto-compile, and import.
64
- """
65
- if os.environ.get("LOREKEEP_PROVIDER") == "fake":
66
- return FakeProvider(responses=[_FAKE_CANNED])
42
+ def _make_provider(config: Config) -> LiteLLMProvider:
43
+ """Create provider for compile. Tests monkeypatch this to inject FakeProvider."""
44
+ return _build_provider(config)
45
+
46
+
47
+ def _make_import_provider(config: Config) -> LiteLLMProvider:
48
+ """Create provider for import deep mode. Tests monkeypatch this."""
67
49
  return _build_provider(config)
68
50
 
69
51
 
52
+ def _has_provider(config: Config) -> bool:
53
+ """Check if a provider API key is available. Tests monkeypatch this."""
54
+ return bool(
55
+ (config.provider.api_key_env and os.environ.get(config.provider.api_key_env))
56
+ or config.provider.api_key
57
+ )
58
+
59
+
70
60
  @app.command()
71
61
  def version() -> None:
72
62
  """Print the Lorekeep version."""
73
63
  typer.echo(f"lorekeep {__version__}")
74
64
 
75
65
 
66
+ @app.command(hidden=True)
67
+ def hook() -> None:
68
+ """SessionEnd hook: quick-import Claude memory files to raw/.
69
+
70
+ Designed to be registered as a Claude Code SessionEnd hook.
71
+ Reads hook JSON from stdin (transcript_path), imports memory files,
72
+ and lets the daemon pick up the raw/ change for compile.
73
+ """
74
+ import sys
75
+
76
+ session_dir = None
77
+ try:
78
+ raw_stdin = sys.stdin.read()
79
+ if raw_stdin.strip():
80
+ data = json.loads(raw_stdin)
81
+ transcript_path = data.get("transcript_path", "")
82
+ if transcript_path:
83
+ session_dir = Path(transcript_path).expanduser().parent
84
+ except Exception:
85
+ pass
86
+
87
+ if session_dir is None:
88
+ try:
89
+ from lorekeep.importer.claude import find_current_session
90
+ session_dir = find_current_session()
91
+ except Exception:
92
+ return
93
+
94
+ if session_dir is None or not (session_dir / "memory").is_dir():
95
+ return
96
+
97
+ p = resolve_paths()
98
+ try:
99
+ from lorekeep.importer.claude import import_memories
100
+ written = import_memories(session_dir, p["raw"], "claude-memory")
101
+ if written:
102
+ typer.echo(f"lorekeep: imported {len(written)} memory file(s)")
103
+ except Exception:
104
+ pass
105
+
106
+
76
107
  @app.command()
77
108
  def compile() -> None:
78
109
  """Compile raw/ into graph/facts.jsonl."""
@@ -249,6 +280,7 @@ def mcp_add(
249
280
  p = resolve_paths()
250
281
  config = load_config(p["config"])
251
282
  command, args = resolve_command(config.install_source)
283
+ hook_cmd, hook_args = resolve_command(config.install_source, ["hook"])
252
284
 
253
285
  target = Path.cwd() if scope == "project" else Path.home()
254
286
  writers = _agent_writers()
@@ -257,6 +289,9 @@ def mcp_add(
257
289
  raise typer.Exit(code=1)
258
290
  written = writers[agent].write_config(target, command, args, ns)
259
291
  typer.echo(f"wrote {agent} config -> {written}")
292
+ if agent in ("claude", "cursor") and hasattr(writers[agent], "write_hook"):
293
+ hook_path = writers[agent].write_hook(target, hook_cmd, hook_args)
294
+ typer.echo(f"wrote session-end hook -> {hook_path}")
260
295
  typer.echo("\n" + agent_memory_snippet())
261
296
 
262
297
 
@@ -462,6 +497,7 @@ def _auto_wire_agents(p: dict, ns: str) -> None:
462
497
 
463
498
  config = load_config(p["config"])
464
499
  command, args = resolve_command(config.install_source)
500
+ hook_cmd, hook_args = resolve_command(config.install_source, ["hook"])
465
501
 
466
502
  writers = _agent_writers()
467
503
  target = Path.cwd()
@@ -474,6 +510,9 @@ def _auto_wire_agents(p: dict, ns: str) -> None:
474
510
  try:
475
511
  written = writer.write_config(target, command, args, ns)
476
512
  typer.echo(f" wired {agent_name} -> {written}")
513
+ if agent_name in ("claude", "cursor") and hasattr(writer, "write_hook"):
514
+ hook_path = writer.write_hook(target, hook_cmd, hook_args)
515
+ typer.echo(f" hooked {agent_name} session-end -> {hook_path}")
477
516
  except Exception as exc:
478
517
  typer.echo(f" {agent_name}: failed ({exc})")
479
518
 
@@ -512,11 +551,7 @@ def _auto_import_and_compile(p: dict) -> None:
512
551
  schema = load_schema(p["schema"])
513
552
  config = load_config(p["config"])
514
553
 
515
- has_key = (
516
- os.environ.get("LOREKEEP_PROVIDER") == "fake"
517
- or (config.provider.api_key_env and os.environ.get(config.provider.api_key_env))
518
- or config.provider.api_key
519
- )
554
+ has_key = _has_provider(config)
520
555
 
521
556
  if not has_key:
522
557
  if imported:
@@ -640,15 +675,6 @@ def import_cmd(
640
675
  p = resolve_paths()
641
676
  config = load_config(p["config"])
642
677
 
643
- def _build_import_provider():
644
- """Deep-mode provider (fake under LOREKEEP_PROVIDER=fake, else config)."""
645
- if os.environ.get("LOREKEEP_PROVIDER") == "fake":
646
- from lorekeep.compile.providers import FakeProvider
647
- # Provide enough canned responses for deep mode batches
648
- canned = "# Knowledge Summary\n\n## Decisions\n- No real session data imported (fake provider).\n"
649
- return FakeProvider(responses=[canned] * 50)
650
- return _build_provider(config)
651
-
652
678
  # --- Cursor: global composer conversations, deep-only ------------------
653
679
  if from_source == "cursor":
654
680
  if quick:
@@ -673,7 +699,7 @@ def import_cmd(
673
699
  ns = session_ns or "cursor-session"
674
700
  result = import_cursor(
675
701
  raw_root=p["raw"], db_path=db, namespace=ns,
676
- provider=_build_import_provider(), dry_run=dry_run,
702
+ provider=_make_import_provider(config), dry_run=dry_run,
677
703
  )
678
704
  ses_count = len(result.get("session", []))
679
705
  if dry_run:
@@ -697,7 +723,7 @@ def import_cmd(
697
723
  "Run Claude Code in this project first.")
698
724
  raise typer.Exit(code=1)
699
725
 
700
- provider = None if quick else _build_import_provider()
726
+ provider = None if quick else _make_import_provider(config)
701
727
 
702
728
  from lorekeep.importer.claude import import_claude
703
729
  result = import_claude(
@@ -73,7 +73,7 @@ PROVIDER_PRESETS: dict[str, dict] = {
73
73
  "api_key_env": None,
74
74
  },
75
75
  "5": {
76
- "label": "Skip — offline/fake mode (set LOREKEEP_PROVIDER=fake to compile)",
76
+ "label": "Skip — configure provider later (edit config.yaml)",
77
77
  "backend": "openai",
78
78
  "model": "openai/gpt-4o-mini",
79
79
  "api_base": None,
@@ -342,6 +342,17 @@ def import_session_deep(
342
342
 
343
343
  jsonl = transcripts[0]
344
344
  session_id = session_dir.name
345
+
346
+ # Dedup: skip if transcript content unchanged since last import
347
+ transcript_hash = hashlib.sha256(
348
+ jsonl.read_bytes()
349
+ ).hexdigest()
350
+ manifest = load_import_manifest(raw_root, namespace)
351
+ manifest_key = str(jsonl)
352
+
353
+ if not dry_run and manifest.get(manifest_key) == transcript_hash:
354
+ return []
355
+
345
356
  turns = parse_transcript(jsonl)
346
357
  if not turns:
347
358
  return []
@@ -374,6 +385,10 @@ def import_session_deep(
374
385
  previous_summary = md[:1000] # carry short context forward
375
386
  written.append(dest)
376
387
 
388
+ if not dry_run and written:
389
+ manifest[manifest_key] = transcript_hash
390
+ save_import_manifest(raw_root, namespace, manifest)
391
+
377
392
  return written
378
393
 
379
394
 
@@ -0,0 +1,50 @@
1
+ """Claude Code MCP config writer (.mcp.json) + SessionEnd hook writer."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+
7
+
8
+ def write_config(target_dir: Path, command: str, args: list[str], ns: str | None) -> Path:
9
+ entry = {"command": command, "args": args}
10
+ if ns:
11
+ entry["env"] = {"LOREKEEP_NS": ns}
12
+ path = Path(target_dir) / ".mcp.json"
13
+ existing = {}
14
+ if path.exists():
15
+ existing = json.loads(path.read_text())
16
+ servers = existing.get("mcpServers", {})
17
+ servers["lorekeep"] = entry
18
+ path.write_text(json.dumps({"mcpServers": servers}, indent=2))
19
+ return path
20
+
21
+
22
+ def write_hook(target_dir: Path, command: str, args: list[str]) -> Path:
23
+ """Write a SessionEnd hook to .claude/settings.json.
24
+
25
+ The hook calls ``lorekeep hook`` which quick-imports Claude memory
26
+ files into raw/ on session end. The daemon (if running) picks up the
27
+ raw/ change and compiles automatically.
28
+ """
29
+ settings_path = Path(target_dir) / ".claude" / "settings.json"
30
+ existing = {}
31
+ if settings_path.exists():
32
+ try:
33
+ existing = json.loads(settings_path.read_text())
34
+ except (json.JSONDecodeError, OSError):
35
+ pass
36
+
37
+ hooks = existing.get("hooks", {})
38
+ hooks["SessionEnd"] = [{
39
+ "hooks": [{
40
+ "type": "command",
41
+ "command": command,
42
+ "args": args,
43
+ "timeout": 30,
44
+ }]
45
+ }]
46
+ existing["hooks"] = hooks
47
+
48
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
49
+ settings_path.write_text(json.dumps(existing, indent=2))
50
+ return settings_path
@@ -2,15 +2,18 @@
2
2
  from __future__ import annotations
3
3
 
4
4
 
5
- def resolve_command(install_source: str | None) -> tuple[str, list[str]]:
6
- """Return (command, args) to launch `lorekeep serve --transport stdio`."""
7
- serve_args = ["serve", "--transport", "stdio"]
5
+ def resolve_command(install_source: str | None, subcommand: list[str] | None = None) -> tuple[str, list[str]]:
6
+ """Return (command, args) to launch a lorekeep subcommand.
7
+
8
+ Defaults to ``serve --transport stdio``. Pass ``subcommand`` for others
9
+ (e.g. ``["hook"]``).
10
+ """
11
+ cmd_args = subcommand or ["serve", "--transport", "stdio"]
8
12
  if not install_source or install_source == "pypi":
9
- return ("uvx", ["lorekeep", *serve_args])
13
+ return ("uvx", ["lorekeep", *cmd_args])
10
14
  if install_source == "local":
11
- return ("lorekeep", serve_args)
12
- # anything else (git+URL, local path) -> uvx --from <source>
13
- return ("uvx", ["--from", install_source, "lorekeep", *serve_args])
15
+ return ("lorekeep", cmd_args)
16
+ return ("uvx", ["--from", install_source, "lorekeep", *cmd_args])
14
17
 
15
18
 
16
19
  def agent_memory_snippet() -> str:
@@ -0,0 +1,45 @@
1
+ """Cursor MCP config writer (.cursor/mcp.json) + sessionEnd hook writer."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+
7
+
8
+ def write_config(target_dir: Path, command: str, args: list[str], ns: str | None) -> Path:
9
+ entry = {"command": command, "args": args}
10
+ if ns:
11
+ entry["env"] = {"LOREKEEP_NS": ns}
12
+ d = Path(target_dir) / ".cursor"
13
+ d.mkdir(parents=True, exist_ok=True)
14
+ path = d / "mcp.json"
15
+ existing = {}
16
+ if path.exists():
17
+ existing = json.loads(path.read_text())
18
+ servers = existing.get("mcpServers", {})
19
+ servers["lorekeep"] = entry
20
+ path.write_text(json.dumps({"mcpServers": servers}, indent=2))
21
+ return path
22
+
23
+
24
+ def write_hook(target_dir: Path, command: str, args: list[str]) -> Path:
25
+ """Write a sessionEnd hook to .cursor/hooks.json.
26
+
27
+ Cursor's hook format uses a single command string (shell form).
28
+ """
29
+ cmd_str = " ".join([command, *args])
30
+ path = Path(target_dir) / ".cursor" / "hooks.json"
31
+ existing = {}
32
+ if path.exists():
33
+ try:
34
+ existing = json.loads(path.read_text())
35
+ except (json.JSONDecodeError, OSError):
36
+ pass
37
+
38
+ hooks = existing.get("hooks", {})
39
+ hooks["sessionEnd"] = [{"command": cmd_str, "timeout": 30}]
40
+ existing["version"] = existing.get("version", 1)
41
+ existing["hooks"] = hooks
42
+
43
+ path.parent.mkdir(parents=True, exist_ok=True)
44
+ path.write_text(json.dumps(existing, indent=2))
45
+ return path
@@ -0,0 +1,57 @@
1
+ from pathlib import Path
2
+ import json
3
+ import pytest
4
+
5
+ from lorekeep.compile.providers import FakeProvider
6
+
7
+
8
+ @pytest.fixture
9
+ def fixtures() -> Path:
10
+ return Path(__file__).parent / "fixtures"
11
+
12
+
13
+ CANNED_EXTRACTION = json.dumps({
14
+ "nodes": [
15
+ {"id": "svc:payments-api", "type": "service", "name": "payments-api",
16
+ "props": {"lang": "go"}, "valid_from": "2024-01-15"},
17
+ {"id": "svc:auth", "type": "service", "name": "auth"},
18
+ {"id": "team:backend", "type": "team", "name": "team-backend"},
19
+ {"id": "dec:adr-007", "type": "decision",
20
+ "props": {"title": "payments-api adopts internal signing"}},
21
+ ],
22
+ "edges": [
23
+ {"type": "depends_on", "from": "svc:payments-api", "to": "svc:auth",
24
+ "valid_from": "2024-01-15", "valid_to": "2025-03-01"},
25
+ {"type": "decided_by", "from": "dec:adr-007", "to": "team:backend"},
26
+ ],
27
+ "aliases": {},
28
+ })
29
+
30
+
31
+ @pytest.fixture
32
+ def fake_provider():
33
+ """FakeProvider with canned extraction responses for compile/ingest tests."""
34
+ return FakeProvider(responses=[CANNED_EXTRACTION] * 50)
35
+
36
+
37
+ @pytest.fixture
38
+ def fake_extraction():
39
+ """Canned extraction JSON string for tests that build their own FakeProvider."""
40
+ return CANNED_EXTRACTION
41
+
42
+
43
+ @pytest.fixture
44
+ def patch_make_provider(monkeypatch, fake_provider):
45
+ """Monkeypatch cli._make_provider + _has_provider to return a FakeProvider."""
46
+ monkeypatch.setattr("lorekeep.cli._make_provider", lambda config: fake_provider)
47
+ monkeypatch.setattr("lorekeep.cli._has_provider", lambda config: True)
48
+
49
+
50
+ @pytest.fixture
51
+ def patch_make_import_provider(monkeypatch):
52
+ """Monkeypatch cli._make_import_provider to return a FakeProvider for import deep mode."""
53
+ canned = "# Knowledge Summary\n\n## Decisions\n- Test import summary.\n"
54
+ monkeypatch.setattr(
55
+ "lorekeep.cli._make_import_provider",
56
+ lambda config: FakeProvider(responses=[canned] * 50),
57
+ )
@@ -6,14 +6,13 @@ from lorekeep.cli import app
6
6
  runner = CliRunner()
7
7
 
8
8
 
9
- def test_agent_ingest_yes_flag(monkeypatch, tmp_path: Path, fixtures: Path):
9
+ def test_agent_ingest_yes_flag(patch_make_provider, monkeypatch, tmp_path: Path, fixtures: Path):
10
10
  """agent ingest --yes: approve all facts, journal is created."""
11
11
  monkeypatch.setenv("LOREKEEP_RAW", str(tmp_path / "raw"))
12
12
  monkeypatch.setenv("LOREKEEP_OUT", str(tmp_path / "graph"))
13
13
  monkeypatch.setenv("LOREKEEP_CACHE", str(tmp_path / "cache.json"))
14
14
  monkeypatch.setenv("LOREKEEP_SCHEMA", str(fixtures / "schema.json"))
15
15
  monkeypatch.setenv("LOREKEEP_PENDING", str(tmp_path / "pending"))
16
- monkeypatch.setenv("LOREKEEP_PROVIDER", "fake")
17
16
 
18
17
  raw_file = tmp_path / "raw" / "backend" / "payments.md"
19
18
  raw_file.parent.mkdir(parents=True)
@@ -35,14 +34,13 @@ def test_agent_ingest_yes_flag(monkeypatch, tmp_path: Path, fixtures: Path):
35
34
  assert entry["status"] == "pending"
36
35
 
37
36
 
38
- def test_agent_ingest_interactive_approve_all(monkeypatch, tmp_path: Path, fixtures: Path):
37
+ def test_agent_ingest_interactive_approve_all(patch_make_provider, monkeypatch, tmp_path: Path, fixtures: Path):
39
38
  """agent ingest: interactive mode, approve all nodes and edges."""
40
39
  monkeypatch.setenv("LOREKEEP_RAW", str(tmp_path / "raw"))
41
40
  monkeypatch.setenv("LOREKEEP_OUT", str(tmp_path / "graph"))
42
41
  monkeypatch.setenv("LOREKEEP_CACHE", str(tmp_path / "cache.json"))
43
42
  monkeypatch.setenv("LOREKEEP_SCHEMA", str(fixtures / "schema.json"))
44
43
  monkeypatch.setenv("LOREKEEP_PENDING", str(tmp_path / "pending"))
45
- monkeypatch.setenv("LOREKEEP_PROVIDER", "fake")
46
44
 
47
45
  raw_file = tmp_path / "raw" / "backend" / "payments.md"
48
46
  raw_file.parent.mkdir(parents=True)
@@ -61,14 +59,13 @@ def test_agent_ingest_interactive_approve_all(monkeypatch, tmp_path: Path, fixtu
61
59
  assert len(lines) == 6 # 4 nodes + 2 edges
62
60
 
63
61
 
64
- def test_agent_ingest_interactive_reject_all(monkeypatch, tmp_path: Path, fixtures: Path):
62
+ def test_agent_ingest_interactive_reject_all(patch_make_provider, monkeypatch, tmp_path: Path, fixtures: Path):
65
63
  """agent ingest: interactive mode, reject all → no journal entries."""
66
64
  monkeypatch.setenv("LOREKEEP_RAW", str(tmp_path / "raw"))
67
65
  monkeypatch.setenv("LOREKEEP_OUT", str(tmp_path / "graph"))
68
66
  monkeypatch.setenv("LOREKEEP_CACHE", str(tmp_path / "cache.json"))
69
67
  monkeypatch.setenv("LOREKEEP_SCHEMA", str(fixtures / "schema.json"))
70
68
  monkeypatch.setenv("LOREKEEP_PENDING", str(tmp_path / "pending"))
71
- monkeypatch.setenv("LOREKEEP_PROVIDER", "fake")
72
69
 
73
70
  raw_file = tmp_path / "raw" / "backend" / "payments.md"
74
71
  raw_file.parent.mkdir(parents=True)
@@ -82,14 +79,13 @@ def test_agent_ingest_interactive_reject_all(monkeypatch, tmp_path: Path, fixtur
82
79
  assert "nothing approved" in result.stdout
83
80
 
84
81
 
85
- def test_agent_ingest_interactive_review_individual(monkeypatch, tmp_path: Path, fixtures: Path):
82
+ def test_agent_ingest_interactive_review_individual(patch_make_provider, monkeypatch, tmp_path: Path, fixtures: Path):
86
83
  """agent ingest: interactive mode, reject all-at-once but approve individually."""
87
84
  monkeypatch.setenv("LOREKEEP_RAW", str(tmp_path / "raw"))
88
85
  monkeypatch.setenv("LOREKEEP_OUT", str(tmp_path / "graph"))
89
86
  monkeypatch.setenv("LOREKEEP_CACHE", str(tmp_path / "cache.json"))
90
87
  monkeypatch.setenv("LOREKEEP_SCHEMA", str(fixtures / "schema.json"))
91
88
  monkeypatch.setenv("LOREKEEP_PENDING", str(tmp_path / "pending"))
92
- monkeypatch.setenv("LOREKEEP_PROVIDER", "fake")
93
89
 
94
90
  raw_file = tmp_path / "raw" / "backend" / "payments.md"
95
91
  raw_file.parent.mkdir(parents=True)
@@ -111,12 +107,11 @@ def test_agent_ingest_interactive_review_individual(monkeypatch, tmp_path: Path,
111
107
  assert len(lines) == 4 # 4 nodes approved, 0 edges
112
108
 
113
109
 
114
- def test_agent_ingest_missing_source(monkeypatch, tmp_path: Path, fixtures: Path):
110
+ def test_agent_ingest_missing_source(patch_make_provider, monkeypatch, tmp_path: Path, fixtures: Path):
115
111
  """agent ingest: missing source file errors out."""
116
112
  monkeypatch.setenv("LOREKEEP_RAW", str(tmp_path / "raw"))
117
113
  monkeypatch.setenv("LOREKEEP_SCHEMA", str(fixtures / "schema.json"))
118
114
  monkeypatch.setenv("LOREKEEP_PENDING", str(tmp_path / "pending"))
119
- monkeypatch.setenv("LOREKEEP_PROVIDER", "fake")
120
115
 
121
116
  raw_root = tmp_path / "raw"
122
117
  raw_root.mkdir(parents=True)
@@ -126,12 +121,11 @@ def test_agent_ingest_missing_source(monkeypatch, tmp_path: Path, fixtures: Path
126
121
  assert "not found" in result.stdout
127
122
 
128
123
 
129
- def test_agent_ingest_source_outside_raw(monkeypatch, tmp_path: Path, fixtures: Path):
124
+ def test_agent_ingest_source_outside_raw(patch_make_provider, monkeypatch, tmp_path: Path, fixtures: Path):
130
125
  """agent ingest: source outside raw/ errors out."""
131
126
  monkeypatch.setenv("LOREKEEP_RAW", str(tmp_path / "raw"))
132
127
  monkeypatch.setenv("LOREKEEP_SCHEMA", str(fixtures / "schema.json"))
133
128
  monkeypatch.setenv("LOREKEEP_PENDING", str(tmp_path / "pending"))
134
- monkeypatch.setenv("LOREKEEP_PROVIDER", "fake")
135
129
 
136
130
  raw_root = tmp_path / "raw"
137
131
  raw_root.mkdir(parents=True)
@@ -6,13 +6,12 @@ from lorekeep.cli import app
6
6
  runner = CliRunner()
7
7
 
8
8
 
9
- def test_compile_command_uses_config_provider(monkeypatch, tmp_path: Path, fixtures: Path):
9
+ def test_compile_command_uses_config_provider(patch_make_provider, monkeypatch, tmp_path: Path, fixtures: Path):
10
10
  # point the CLI at temp dirs via env
11
11
  monkeypatch.setenv("LOREKEEP_RAW", str(tmp_path / "raw"))
12
12
  monkeypatch.setenv("LOREKEEP_OUT", str(tmp_path / "graph"))
13
13
  monkeypatch.setenv("LOREKEEP_CACHE", str(tmp_path / "cache.json"))
14
14
  monkeypatch.setenv("LOREKEEP_SCHEMA", str(fixtures / "schema.json"))
15
- monkeypatch.setenv("LOREKEEP_PROVIDER", "fake")
16
15
 
17
16
  raw = tmp_path / "raw/backend/payments.md"
18
17
  raw.parent.mkdir(parents=True)