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.
- lorekeep-0.3.1/.release-please-manifest.json +3 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/AGENTS.md +3 -3
- {lorekeep-0.2.1 → lorekeep-0.3.1}/CHANGELOG.md +15 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/PKG-INFO +1 -1
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/evaluation.md +1 -1
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/getting-started.md +4 -4
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/serve.md +2 -2
- {lorekeep-0.2.1 → lorekeep-0.3.1}/pyproject.toml +1 -1
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/__init__.py +1 -1
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/cli.py +115 -42
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/defaults.py +1 -1
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/importer/claude.py +15 -0
- lorekeep-0.3.1/tests/conftest.py +57 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_agent_cli.py +6 -12
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_compile_cli.py +1 -2
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_import_claude.py +55 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_import_cursor.py +1 -3
- lorekeep-0.3.1/tests/test_init_chain.py +176 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_watch_e2e.py +3 -4
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_watch_sessions.py +0 -2
- lorekeep-0.2.1/.release-please-manifest.json +0 -3
- lorekeep-0.2.1/tests/conftest.py +0 -7
- {lorekeep-0.2.1 → lorekeep-0.3.1}/.commitlintrc.json +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/.github/workflows/auto-merge-release-please.yml +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/.github/workflows/ci.yml +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/.github/workflows/lint-commits.yml +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/.github/workflows/release-please.yml +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/.gitignore +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/.lorekeep/config.yaml.example +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/.lorekeep/schema.json +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/.pre-commit-config.yaml +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/CLAUDE.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/LICENSE +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/README.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/SECURITY.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/cover.jpeg +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/README.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/agent.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/data-model.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/journal.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/overview.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/permission.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/pipeline.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/serve-mcp.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/architecture/temporal.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/backup.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/compile.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/data-home.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/guides/import.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/release-please-config.json +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/scripts/check-conventional-commit.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/agent.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/backup.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/__init__.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/extract.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/ingest.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/providers.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/resolve.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/compile/writer.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/config.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/eval/__init__.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/eval/construction.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/eval/gold.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/eval/retrieval.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/facts_io.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/importer/__init__.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/importer/cursor.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/__init__.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/claude_code.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/codex.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/common.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/cursor.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/detect.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/integrations/opencode.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/journal.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/mcp_server.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/models.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/paths.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/perm/__init__.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/perm/ns.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/pipeline.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/schema_io.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/store/__init__.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/store/fts.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/src/lorekeep/store/graph.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/claude-session/memory/MEMORY.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/claude-session/memory/lorekeep-design.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/claude-session/memory/lorekeep-project-state.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/claude-session/test.jsonl +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/gold/payments.facts.jsonl +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/raw/backend/payments.md +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/retrieval/questions.json +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/fixtures/schema.json +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_agent_detect.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_backup.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_backup_cli.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_config.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_construction_eval.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_defaults.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_determinism.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_doctor_cli.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_eval_cli.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_extract.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_fts.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_graph_store.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_ingest.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_init_cli.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_integrations.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_mcp_add_cli.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_mcp_reload.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_mcp_server.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_mcp_write_tools.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_merge_journals.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_models.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_paths.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_perm.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_pipeline.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_providers.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_resolve.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_retrieval_eval.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_schema_io.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_serve_cli.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_smoke.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/tests/test_writer.py +0 -0
- {lorekeep-0.2.1 → lorekeep-0.3.1}/uv.lock +0 -0
|
@@ -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:**
|
|
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
|
|
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
|
|
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
|
|
@@ -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
|
|
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
|
-
> **
|
|
112
|
-
>
|
|
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.
|
|
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
|
|
8
|
+
uvx lorekeep init # bootstrap + wire agents + compile + start daemon
|
|
9
9
|
# add your docs under ~/.local/share/lorekeep/raw/<ns>/
|
|
10
|
-
|
|
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
|
|
|
@@ -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
|
|
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
|
-
|
|
43
|
-
"
|
|
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
|
|
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
|
-
#
|
|
366
|
+
# --- One-click chain: wire → import → compile → daemon -----------------
|
|
372
367
|
if not config_existed:
|
|
373
368
|
_auto_wire_agents(p, ns)
|
|
374
|
-
|
|
375
|
-
typer.echo("
|
|
376
|
-
|
|
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=
|
|
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
|
|
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 —
|
|
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
|