lorekeep 0.2.1__tar.gz → 0.3.1__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 (127) hide show
  1. lorekeep-0.3.1/.release-please-manifest.json +3 -0
  2. {lorekeep-0.2.1 → lorekeep-0.3.1}/AGENTS.md +3 -3
  3. {lorekeep-0.2.1 → lorekeep-0.3.1}/CHANGELOG.md +15 -0
  4. {lorekeep-0.2.1 → lorekeep-0.3.1}/PKG-INFO +1 -1
  5. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/evaluation.md +1 -1
  6. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/getting-started.md +4 -4
  7. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/serve.md +2 -2
  8. {lorekeep-0.2.1 → lorekeep-0.3.1}/pyproject.toml +1 -1
  9. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/__init__.py +1 -1
  10. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/cli.py +115 -42
  11. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/defaults.py +1 -1
  12. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/importer/claude.py +15 -0
  13. lorekeep-0.3.1/tests/conftest.py +57 -0
  14. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_agent_cli.py +6 -12
  15. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_compile_cli.py +1 -2
  16. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_import_claude.py +55 -0
  17. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_import_cursor.py +1 -3
  18. lorekeep-0.3.1/tests/test_init_chain.py +176 -0
  19. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_watch_e2e.py +3 -4
  20. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_watch_sessions.py +0 -2
  21. lorekeep-0.2.1/.release-please-manifest.json +0 -3
  22. lorekeep-0.2.1/tests/conftest.py +0 -7
  23. {lorekeep-0.2.1 → lorekeep-0.3.1}/.commitlintrc.json +0 -0
  24. {lorekeep-0.2.1 → lorekeep-0.3.1}/.github/workflows/auto-merge-release-please.yml +0 -0
  25. {lorekeep-0.2.1 → lorekeep-0.3.1}/.github/workflows/ci.yml +0 -0
  26. {lorekeep-0.2.1 → lorekeep-0.3.1}/.github/workflows/lint-commits.yml +0 -0
  27. {lorekeep-0.2.1 → lorekeep-0.3.1}/.github/workflows/release-please.yml +0 -0
  28. {lorekeep-0.2.1 → lorekeep-0.3.1}/.gitignore +0 -0
  29. {lorekeep-0.2.1 → lorekeep-0.3.1}/.lorekeep/config.yaml.example +0 -0
  30. {lorekeep-0.2.1 → lorekeep-0.3.1}/.lorekeep/schema.json +0 -0
  31. {lorekeep-0.2.1 → lorekeep-0.3.1}/.pre-commit-config.yaml +0 -0
  32. {lorekeep-0.2.1 → lorekeep-0.3.1}/CLAUDE.md +0 -0
  33. {lorekeep-0.2.1 → lorekeep-0.3.1}/LICENSE +0 -0
  34. {lorekeep-0.2.1 → lorekeep-0.3.1}/README.md +0 -0
  35. {lorekeep-0.2.1 → lorekeep-0.3.1}/SECURITY.md +0 -0
  36. {lorekeep-0.2.1 → lorekeep-0.3.1}/cover.jpeg +0 -0
  37. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/README.md +0 -0
  38. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/agent.md +0 -0
  39. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/data-model.md +0 -0
  40. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/journal.md +0 -0
  41. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/overview.md +0 -0
  42. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/permission.md +0 -0
  43. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/pipeline.md +0 -0
  44. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/serve-mcp.md +0 -0
  45. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/temporal.md +0 -0
  46. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/backup.md +0 -0
  47. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/compile.md +0 -0
  48. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/data-home.md +0 -0
  49. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/import.md +0 -0
  50. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md +0 -0
  51. {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md +0 -0
  52. {lorekeep-0.2.1 → lorekeep-0.3.1}/release-please-config.json +0 -0
  53. {lorekeep-0.2.1 → lorekeep-0.3.1}/scripts/check-conventional-commit.py +0 -0
  54. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/agent.py +0 -0
  55. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/backup.py +0 -0
  56. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/__init__.py +0 -0
  57. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/extract.py +0 -0
  58. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/ingest.py +0 -0
  59. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/providers.py +0 -0
  60. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/resolve.py +0 -0
  61. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/writer.py +0 -0
  62. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/config.py +0 -0
  63. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/eval/__init__.py +0 -0
  64. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/eval/construction.py +0 -0
  65. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/eval/gold.py +0 -0
  66. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/eval/retrieval.py +0 -0
  67. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/facts_io.py +0 -0
  68. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/importer/__init__.py +0 -0
  69. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/importer/cursor.py +0 -0
  70. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/__init__.py +0 -0
  71. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/claude_code.py +0 -0
  72. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/codex.py +0 -0
  73. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/common.py +0 -0
  74. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/cursor.py +0 -0
  75. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/detect.py +0 -0
  76. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/opencode.py +0 -0
  77. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/journal.py +0 -0
  78. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/mcp_server.py +0 -0
  79. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/models.py +0 -0
  80. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/paths.py +0 -0
  81. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/perm/__init__.py +0 -0
  82. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/perm/ns.py +0 -0
  83. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/pipeline.py +0 -0
  84. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/schema_io.py +0 -0
  85. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/store/__init__.py +0 -0
  86. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/store/fts.py +0 -0
  87. {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/store/graph.py +0 -0
  88. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/claude-session/memory/MEMORY.md +0 -0
  89. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/claude-session/memory/lorekeep-design.md +0 -0
  90. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/claude-session/memory/lorekeep-project-state.md +0 -0
  91. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/claude-session/test.jsonl +0 -0
  92. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/gold/payments.facts.jsonl +0 -0
  93. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/raw/backend/payments.md +0 -0
  94. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/retrieval/questions.json +0 -0
  95. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/schema.json +0 -0
  96. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_agent_detect.py +0 -0
  97. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_backup.py +0 -0
  98. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_backup_cli.py +0 -0
  99. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_config.py +0 -0
  100. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_construction_eval.py +0 -0
  101. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_defaults.py +0 -0
  102. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_determinism.py +0 -0
  103. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_doctor_cli.py +0 -0
  104. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_eval_cli.py +0 -0
  105. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_extract.py +0 -0
  106. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_fts.py +0 -0
  107. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_graph_store.py +0 -0
  108. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_ingest.py +0 -0
  109. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_init_cli.py +0 -0
  110. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_integrations.py +0 -0
  111. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_mcp_add_cli.py +0 -0
  112. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_mcp_reload.py +0 -0
  113. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_mcp_server.py +0 -0
  114. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_mcp_write_tools.py +0 -0
  115. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_merge_journals.py +0 -0
  116. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_models.py +0 -0
  117. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_paths.py +0 -0
  118. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_perm.py +0 -0
  119. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_pipeline.py +0 -0
  120. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_providers.py +0 -0
  121. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_resolve.py +0 -0
  122. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_retrieval_eval.py +0 -0
  123. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_schema_io.py +0 -0
  124. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_serve_cli.py +0 -0
  125. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_smoke.py +0 -0
  126. {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_writer.py +0 -0
  127. {lorekeep-0.2.1 → lorekeep-0.3.1}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.1"
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,11 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1](https://github.com/manhhailua/lorekeep/compare/v0.3.0...v0.3.1) (2026-06-28)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add manifest dedup to deep-mode session import ([#61](https://github.com/manhhailua/lorekeep/issues/61)) ([a9a074f](https://github.com/manhhailua/lorekeep/commit/a9a074f901c9a8f8e4cd2e3f906fdf4be7a773d6))
9
+
10
+ ## [0.3.0](https://github.com/manhhailua/lorekeep/compare/v0.2.1...v0.3.0) (2026-06-28)
11
+
12
+
13
+ ### Features
14
+
15
+ * zero-friction init — wire, import, compile, daemon in one command ([#55](https://github.com/manhhailua/lorekeep/issues/55)) ([26e27ad](https://github.com/manhhailua/lorekeep/commit/26e27ad70e9eae90d4ac490694b891eda01b563b))
16
+
3
17
  ## [0.2.1](https://github.com/manhhailua/lorekeep/compare/v0.2.0...v0.2.1) (2026-06-28)
4
18
 
5
19
 
6
20
  ### Bug Fixes
7
21
 
8
22
  * repair daemon auto-compile provider construction and re-merge journals after standalone compile ([#48](https://github.com/manhhailua/lorekeep/issues/48)) ([fb05b29](https://github.com/manhhailua/lorekeep/commit/fb05b290c5c673cc36891790d62036b8b6f65c7e))
23
+ * resolve doc contradictions and false cadence claims ([#53](https://github.com/manhhailua/lorekeep/issues/53)) ([10fc794](https://github.com/manhhailua/lorekeep/commit/10fc79406b02d1fa5735274b3dd89c62653ed3ba))
9
24
 
10
25
 
11
26
  ### Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lorekeep
3
- Version: 0.2.1
3
+ Version: 0.3.1
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.2.1"
3
+ version = "0.3.1"
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.2.1"
3
+ __version__ = "0.3.1"
@@ -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,34 +39,24 @@ 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."""
67
44
  return _build_provider(config)
68
45
 
69
46
 
47
+ def _make_import_provider(config: Config) -> LiteLLMProvider:
48
+ """Create provider for import deep mode. Tests monkeypatch this."""
49
+ return _build_provider(config)
50
+
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."""
@@ -319,8 +309,12 @@ def init(
319
309
  False, "--yes", "-y",
320
310
  help="Skip interactive prompts, use defaults",
321
311
  ),
312
+ watch: bool = typer.Option(
313
+ True, "--watch/--no-watch",
314
+ help="Start the daemon (agent watch) in background after setup",
315
+ ),
322
316
  ) -> None:
323
- """Bootstrap the data home: config + schema + raw/graph dirs."""
317
+ """Bootstrap the data home, wire agents, import sessions, compile, and start daemon."""
324
318
  p = resolve_paths()
325
319
  created = []
326
320
  p["config"].parent.mkdir(parents=True, exist_ok=True)
@@ -348,6 +342,7 @@ def init(
348
342
 
349
343
  p["raw"].mkdir(parents=True, exist_ok=True)
350
344
  p["out"].mkdir(parents=True, exist_ok=True)
345
+ p["pending"].mkdir(parents=True, exist_ok=True)
351
346
 
352
347
  typer.echo(f"home ready: config={p['config']}")
353
348
  typer.echo(f" schema={p['schema']} raw={p['raw']} graph={p['out']}")
@@ -368,12 +363,20 @@ def init(
368
363
  (ns_dir / "about.md").write_text(about_md)
369
364
  typer.echo(f" wrote: {ns_dir / 'about.md'}")
370
365
 
371
- # Auto-detect and wire coding agents (active session or installed).
366
+ # --- One-click chain: wire → import → compile → daemon -----------------
372
367
  if not config_existed:
373
368
  _auto_wire_agents(p, ns)
374
- typer.echo("\nNext steps:")
375
- typer.echo(" 1. Compile: uvx lorekeep compile (or LOREKEEP_PROVIDER=fake)")
376
- typer.echo(" 2. More agents: uvx lorekeep mcp add --agent <claude|cursor|codex|opencode>")
369
+ _auto_import_and_compile(p)
370
+ typer.echo("\nRestart your agent → lorekeep tools are available.")
371
+
372
+ # Daemon: start on fresh init or revive if dead (regardless of config_existed)
373
+ if watch and _is_interactive():
374
+ _start_daemon(p)
375
+ elif watch and not _is_interactive():
376
+ typer.echo("\n (skipped daemon start in non-interactive mode — run `lorekeep agent watch` manually)")
377
+
378
+ if config_existed:
379
+ typer.echo("\nAlready initialized.")
377
380
 
378
381
 
379
382
  def _interactive_init(p: dict) -> tuple[str, str, str]:
@@ -478,6 +481,85 @@ def _agent_writers() -> dict:
478
481
  }
479
482
 
480
483
 
484
+ def _auto_import_and_compile(p: dict) -> None:
485
+ """Quick-import Claude memory files, then compile if provider is available."""
486
+ imported = 0
487
+
488
+ # --- Quick import: Claude memory files (zero LLM cost) ----------------
489
+ try:
490
+ from lorekeep.importer.claude import find_current_session, import_memories
491
+ session_dir = find_current_session()
492
+ if session_dir is not None and (session_dir / "memory").is_dir():
493
+ written = import_memories(session_dir, p["raw"], "claude-memory")
494
+ imported = len(written)
495
+ if imported:
496
+ typer.echo(f" imported {imported} memory file(s) from Claude session")
497
+ except Exception as exc:
498
+ if os.environ.get("LOREKEEP_DEBUG"):
499
+ typer.echo(f" import error: {exc}")
500
+
501
+ # --- Compile (if provider is usable) ----------------------------------
502
+ schema = load_schema(p["schema"])
503
+ config = load_config(p["config"])
504
+
505
+ has_key = _has_provider(config)
506
+
507
+ if not has_key:
508
+ if imported:
509
+ typer.echo(" memory files imported to raw/ — compile pending (add API key to config.yaml)")
510
+ else:
511
+ typer.echo(" graph empty — run `lorekeep compile` after adding docs or importing sessions")
512
+ return
513
+
514
+ try:
515
+ provider = _make_provider(config)
516
+ manifest = compile_graph(
517
+ raw_root=p["raw"], out_dir=p["out"], schema=schema,
518
+ provider=provider, cache_path=p["cache"],
519
+ chunk_lines=config.compile.chunk_lines,
520
+ )
521
+ pending_dir = p.get("pending")
522
+ if pending_dir and pending_dir.exists():
523
+ _do_auto_resolve(p["out"], pending_dir)
524
+ typer.echo(f" compiled: {manifest.node_count} nodes, {manifest.edge_count} edges")
525
+ except Exception as exc:
526
+ typer.echo(f" compile skipped: {exc}")
527
+
528
+
529
+ def _start_daemon(p: dict) -> None:
530
+ """Start agent watch as a background process with PID + log files."""
531
+ import subprocess
532
+ import sys
533
+
534
+ pid_path = p["home"] / "agent.pid"
535
+ log_path = p["home"] / "agent.log"
536
+
537
+ # Check if already running
538
+ if pid_path.exists():
539
+ old_pid = pid_path.read_text().strip()
540
+ try:
541
+ os.kill(int(old_pid), 0)
542
+ typer.echo(f" daemon already running (pid={old_pid})")
543
+ return
544
+ except (ProcessLookupError, ValueError):
545
+ pass
546
+
547
+ cmd = [sys.executable, "-m", "lorekeep.cli", "agent", "watch", "--interval", "60"]
548
+ log_file = open(log_path, "a")
549
+ try:
550
+ proc = subprocess.Popen(
551
+ cmd,
552
+ stdout=log_file,
553
+ stderr=subprocess.STDOUT,
554
+ stdin=subprocess.DEVNULL,
555
+ start_new_session=True,
556
+ )
557
+ finally:
558
+ log_file.close()
559
+ pid_path.write_text(str(proc.pid))
560
+ typer.echo(f" daemon started (pid={proc.pid}, log={log_path})")
561
+
562
+
481
563
  @app.command()
482
564
  def backup(
483
565
  init_remote: str = typer.Option(
@@ -544,15 +626,6 @@ def import_cmd(
544
626
  p = resolve_paths()
545
627
  config = load_config(p["config"])
546
628
 
547
- def _build_import_provider():
548
- """Deep-mode provider (fake under LOREKEEP_PROVIDER=fake, else config)."""
549
- if os.environ.get("LOREKEEP_PROVIDER") == "fake":
550
- from lorekeep.compile.providers import FakeProvider
551
- # Provide enough canned responses for deep mode batches
552
- canned = "# Knowledge Summary\n\n## Decisions\n- No real session data imported (fake provider).\n"
553
- return FakeProvider(responses=[canned] * 50)
554
- return _build_provider(config)
555
-
556
629
  # --- Cursor: global composer conversations, deep-only ------------------
557
630
  if from_source == "cursor":
558
631
  if quick:
@@ -577,7 +650,7 @@ def import_cmd(
577
650
  ns = session_ns or "cursor-session"
578
651
  result = import_cursor(
579
652
  raw_root=p["raw"], db_path=db, namespace=ns,
580
- provider=_build_import_provider(), dry_run=dry_run,
653
+ provider=_make_import_provider(config), dry_run=dry_run,
581
654
  )
582
655
  ses_count = len(result.get("session", []))
583
656
  if dry_run:
@@ -601,7 +674,7 @@ def import_cmd(
601
674
  "Run Claude Code in this project first.")
602
675
  raise typer.Exit(code=1)
603
676
 
604
- provider = None if quick else _build_import_provider()
677
+ provider = None if quick else _make_import_provider(config)
605
678
 
606
679
  from lorekeep.importer.claude import import_claude
607
680
  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,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)
@@ -193,6 +193,61 @@ def test_import_session_deep_handles_empty_transcript(tmp_path: Path):
193
193
  assert result == []
194
194
 
195
195
 
196
+ def test_import_session_deep_is_idempotent(tmp_path: Path, fixtures: Path):
197
+ """Re-importing the same transcript skips LLM summarization."""
198
+ session_dir = fixtures / "claude-session"
199
+ raw_root = tmp_path / "raw"
200
+ provider = FakeProvider(responses=["# Summary\n\n## Decisions\n- Use FastAPI\n"] * 50)
201
+
202
+ first = import_session_deep(session_dir, raw_root, namespace="test-session",
203
+ provider=provider)
204
+ assert len(first) >= 1
205
+ assert len(provider.calls) >= 1
206
+
207
+ # Second call: same transcript, should skip entirely
208
+ call_before = len(provider.calls)
209
+ second = import_session_deep(session_dir, raw_root, namespace="test-session",
210
+ provider=provider)
211
+ assert second == []
212
+ assert len(provider.calls) == call_before # no new LLM calls
213
+
214
+
215
+ def test_import_session_deep_re_imports_on_change(tmp_path: Path):
216
+ """Transcript changed → re-import triggers LLM call."""
217
+ session_dir = tmp_path / "session"
218
+ session_dir.mkdir()
219
+ transcript = session_dir / "sess.jsonl"
220
+ transcript.write_text('{"role":"user","message":{"content":"what is FastAPI?"}}\n'
221
+ '{"role":"assistant","message":{"content":[{"type":"text","text":"a web framework"}]}}\n')
222
+
223
+ raw_root = tmp_path / "raw"
224
+ provider = FakeProvider(responses=["# Summary\n\n## Decisions\n- Use FastAPI\n"] * 50)
225
+
226
+ first = import_session_deep(session_dir, raw_root, namespace="ns", provider=provider)
227
+ assert len(first) >= 1
228
+
229
+ # Modify transcript
230
+ transcript.write_text('{"role":"user","message":{"content":"what is Flask?"}}\n'
231
+ '{"role":"assistant","message":{"content":[{"type":"text","text":"a web framework"}]}}\n')
232
+
233
+ call_before = len(provider.calls)
234
+ second = import_session_deep(session_dir, raw_root, namespace="ns", provider=provider)
235
+ assert len(second) >= 1
236
+ assert len(provider.calls) > call_before # LLM called again
237
+
238
+
239
+ def test_import_session_deep_dry_run_skips_manifest(tmp_path: Path, fixtures: Path):
240
+ """Dry run should not write manifest (so real import still processes)."""
241
+ session_dir = fixtures / "claude-session"
242
+ raw_root = tmp_path / "raw"
243
+ provider = FakeProvider(responses=["dummy"] * 50)
244
+
245
+ import_session_deep(session_dir, raw_root, namespace="ns",
246
+ provider=provider, dry_run=True)
247
+ manifest_path = raw_root / "ns" / ".import-manifest.json"
248
+ assert not manifest_path.exists()
249
+
250
+
196
251
  # ---------------------------------------------------------------------------
197
252
  # orchestrator
198
253
  # ---------------------------------------------------------------------------
@@ -191,11 +191,10 @@ def test_import_cursor_missing_db_errors(tmp_path: Path):
191
191
  # ---------------------------------------------------------------------------
192
192
 
193
193
 
194
- def test_cli_import_cursor_runs(monkeypatch, tmp_path: Path, state_db: Path):
194
+ def test_cli_import_cursor_runs(patch_make_import_provider, monkeypatch, tmp_path: Path, state_db: Path):
195
195
  monkeypatch.setenv("LOREKEEP_RAW", str(tmp_path / "raw"))
196
196
  monkeypatch.setenv("LOREKEEP_OUT", str(tmp_path / "graph"))
197
197
  monkeypatch.setenv("LOREKEEP_CACHE", str(tmp_path / "cache.json"))
198
- monkeypatch.setenv("LOREKEEP_PROVIDER", "fake")
199
198
  monkeypatch.setenv("CURSOR_STATE_DB", str(state_db))
200
199
 
201
200
  result = runner.invoke(app, ["import", "--from", "cursor"])
@@ -205,7 +204,6 @@ def test_cli_import_cursor_runs(monkeypatch, tmp_path: Path, state_db: Path):
205
204
 
206
205
  def test_cli_import_cursor_rejects_quick(monkeypatch, tmp_path: Path, state_db: Path):
207
206
  monkeypatch.setenv("CURSOR_STATE_DB", str(state_db))
208
- monkeypatch.setenv("LOREKEEP_PROVIDER", "fake")
209
207
  result = runner.invoke(app, ["import", "--from", "cursor", "--quick"])
210
208
  assert result.exit_code == 1
211
209
  assert "deep-only" in result.stdout