lorekeep 0.3.1__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.
- lorekeep-0.4.0/.release-please-manifest.json +3 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/CHANGELOG.md +7 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/PKG-INFO +1 -1
- {lorekeep-0.3.1 → lorekeep-0.4.0}/pyproject.toml +1 -1
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/__init__.py +1 -1
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/cli.py +49 -0
- lorekeep-0.4.0/src/lorekeep/integrations/claude_code.py +50 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/integrations/common.py +10 -7
- lorekeep-0.4.0/src/lorekeep/integrations/cursor.py +45 -0
- lorekeep-0.4.0/tests/test_session_hook.py +246 -0
- lorekeep-0.3.1/.release-please-manifest.json +0 -3
- lorekeep-0.3.1/src/lorekeep/integrations/claude_code.py +0 -19
- lorekeep-0.3.1/src/lorekeep/integrations/cursor.py +0 -21
- {lorekeep-0.3.1 → lorekeep-0.4.0}/.commitlintrc.json +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/.github/workflows/auto-merge-release-please.yml +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/.github/workflows/ci.yml +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/.github/workflows/lint-commits.yml +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/.github/workflows/release-please.yml +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/.gitignore +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/.lorekeep/config.yaml.example +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/.lorekeep/schema.json +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/.pre-commit-config.yaml +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/AGENTS.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/CLAUDE.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/LICENSE +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/README.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/SECURITY.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/cover.jpeg +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/README.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/architecture/agent.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/architecture/data-model.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/architecture/evaluation.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/architecture/journal.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/architecture/overview.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/architecture/permission.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/architecture/pipeline.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/architecture/serve-mcp.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/architecture/temporal.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/guides/backup.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/guides/compile.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/guides/data-home.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/guides/getting-started.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/guides/import.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/guides/serve.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/release-please-config.json +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/scripts/check-conventional-commit.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/agent.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/backup.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/compile/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/compile/extract.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/compile/ingest.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/compile/providers.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/compile/resolve.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/compile/writer.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/config.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/defaults.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/eval/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/eval/construction.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/eval/gold.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/eval/retrieval.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/facts_io.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/importer/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/importer/claude.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/importer/cursor.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/integrations/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/integrations/codex.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/integrations/detect.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/integrations/opencode.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/journal.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/mcp_server.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/models.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/paths.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/perm/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/perm/ns.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/pipeline.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/schema_io.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/store/__init__.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/store/fts.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/src/lorekeep/store/graph.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/conftest.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/fixtures/claude-session/memory/MEMORY.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/fixtures/claude-session/memory/lorekeep-design.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/fixtures/claude-session/memory/lorekeep-project-state.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/fixtures/claude-session/test.jsonl +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/fixtures/gold/payments.facts.jsonl +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/fixtures/raw/backend/payments.md +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/fixtures/retrieval/questions.json +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/fixtures/schema.json +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_agent_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_agent_detect.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_backup.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_backup_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_compile_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_config.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_construction_eval.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_defaults.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_determinism.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_doctor_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_eval_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_extract.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_fts.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_graph_store.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_import_claude.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_import_cursor.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_ingest.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_init_chain.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_init_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_integrations.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_mcp_add_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_mcp_reload.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_mcp_server.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_mcp_write_tools.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_merge_journals.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_models.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_paths.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_perm.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_pipeline.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_providers.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_resolve.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_retrieval_eval.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_schema_io.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_serve_cli.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_smoke.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_watch_e2e.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_watch_sessions.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/tests/test_writer.py +0 -0
- {lorekeep-0.3.1 → lorekeep-0.4.0}/uv.lock +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## [0.3.1](https://github.com/manhhailua/lorekeep/compare/v0.3.0...v0.3.1) (2026-06-28)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -63,6 +63,47 @@ def version() -> None:
|
|
|
63
63
|
typer.echo(f"lorekeep {__version__}")
|
|
64
64
|
|
|
65
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
|
+
|
|
66
107
|
@app.command()
|
|
67
108
|
def compile() -> None:
|
|
68
109
|
"""Compile raw/ into graph/facts.jsonl."""
|
|
@@ -239,6 +280,7 @@ def mcp_add(
|
|
|
239
280
|
p = resolve_paths()
|
|
240
281
|
config = load_config(p["config"])
|
|
241
282
|
command, args = resolve_command(config.install_source)
|
|
283
|
+
hook_cmd, hook_args = resolve_command(config.install_source, ["hook"])
|
|
242
284
|
|
|
243
285
|
target = Path.cwd() if scope == "project" else Path.home()
|
|
244
286
|
writers = _agent_writers()
|
|
@@ -247,6 +289,9 @@ def mcp_add(
|
|
|
247
289
|
raise typer.Exit(code=1)
|
|
248
290
|
written = writers[agent].write_config(target, command, args, ns)
|
|
249
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}")
|
|
250
295
|
typer.echo("\n" + agent_memory_snippet())
|
|
251
296
|
|
|
252
297
|
|
|
@@ -452,6 +497,7 @@ def _auto_wire_agents(p: dict, ns: str) -> None:
|
|
|
452
497
|
|
|
453
498
|
config = load_config(p["config"])
|
|
454
499
|
command, args = resolve_command(config.install_source)
|
|
500
|
+
hook_cmd, hook_args = resolve_command(config.install_source, ["hook"])
|
|
455
501
|
|
|
456
502
|
writers = _agent_writers()
|
|
457
503
|
target = Path.cwd()
|
|
@@ -464,6 +510,9 @@ def _auto_wire_agents(p: dict, ns: str) -> None:
|
|
|
464
510
|
try:
|
|
465
511
|
written = writer.write_config(target, command, args, ns)
|
|
466
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}")
|
|
467
516
|
except Exception as exc:
|
|
468
517
|
typer.echo(f" {agent_name}: failed ({exc})")
|
|
469
518
|
|
|
@@ -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
|
|
7
|
-
|
|
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", *
|
|
13
|
+
return ("uvx", ["lorekeep", *cmd_args])
|
|
10
14
|
if install_source == "local":
|
|
11
|
-
return ("lorekeep",
|
|
12
|
-
|
|
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,246 @@
|
|
|
1
|
+
"""Tests for SessionEnd hook: lorekeep hook command + .claude/settings.json wiring."""
|
|
2
|
+
import json
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from typer.testing import CliRunner
|
|
8
|
+
|
|
9
|
+
import lorekeep.mcp_server as ms
|
|
10
|
+
from lorekeep.cli import app
|
|
11
|
+
from lorekeep.integrations.claude_code import write_config, write_hook
|
|
12
|
+
|
|
13
|
+
runner = CliRunner()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ── lorekeep hook command ─────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_hook_imports_memory_from_stdin(tmp_path: Path, monkeypatch):
|
|
20
|
+
"""lorekeep hook reads transcript_path from stdin, imports memory files."""
|
|
21
|
+
home = tmp_path / "home"
|
|
22
|
+
home.mkdir()
|
|
23
|
+
(home / "raw").mkdir()
|
|
24
|
+
(home / "graph").mkdir()
|
|
25
|
+
(home / "pending").mkdir()
|
|
26
|
+
(home / "cache.json").write_text("{}")
|
|
27
|
+
|
|
28
|
+
session_dir = tmp_path / "session"
|
|
29
|
+
(session_dir / "memory").mkdir(parents=True)
|
|
30
|
+
(session_dir / "memory" / "note.md").write_text("# Important\nKnowledge.\n")
|
|
31
|
+
transcript = session_dir / "transcript.jsonl"
|
|
32
|
+
transcript.write_text("{}")
|
|
33
|
+
|
|
34
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
35
|
+
|
|
36
|
+
hook_input = json.dumps({"transcript_path": str(transcript)})
|
|
37
|
+
result = runner.invoke(app, ["hook"], input=hook_input)
|
|
38
|
+
assert result.exit_code == 0, result.stdout
|
|
39
|
+
|
|
40
|
+
imported = home / "raw" / "claude-memory" / "note.md"
|
|
41
|
+
assert imported.exists()
|
|
42
|
+
assert "imported" in result.stdout.lower()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_hook_no_session_silent_exit(tmp_path: Path, monkeypatch):
|
|
46
|
+
"""lorekeep hook exits silently when no session found."""
|
|
47
|
+
home = tmp_path / "home"
|
|
48
|
+
home.mkdir()
|
|
49
|
+
(home / "raw").mkdir()
|
|
50
|
+
(home / "graph").mkdir()
|
|
51
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
52
|
+
monkeypatch.setattr(
|
|
53
|
+
"lorekeep.importer.claude.find_current_session", lambda: None
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
result = runner.invoke(app, ["hook"], input="")
|
|
57
|
+
assert result.exit_code == 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_hook_no_memory_dir_silent_exit(tmp_path: Path, monkeypatch):
|
|
61
|
+
"""lorekeep hook exits silently when session has no memory/ dir."""
|
|
62
|
+
home = tmp_path / "home"
|
|
63
|
+
home.mkdir()
|
|
64
|
+
(home / "raw").mkdir()
|
|
65
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
66
|
+
|
|
67
|
+
session_dir = tmp_path / "session"
|
|
68
|
+
session_dir.mkdir()
|
|
69
|
+
transcript = session_dir / "transcript.jsonl"
|
|
70
|
+
transcript.write_text("{}")
|
|
71
|
+
|
|
72
|
+
hook_input = json.dumps({"transcript_path": str(transcript)})
|
|
73
|
+
result = runner.invoke(app, ["hook"], input=hook_input)
|
|
74
|
+
assert result.exit_code == 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_hook_idempotent(tmp_path: Path, monkeypatch):
|
|
78
|
+
"""Running hook twice doesn't re-import unchanged files."""
|
|
79
|
+
home = tmp_path / "home"
|
|
80
|
+
home.mkdir()
|
|
81
|
+
(home / "raw").mkdir()
|
|
82
|
+
(home / "graph").mkdir()
|
|
83
|
+
(home / "pending").mkdir()
|
|
84
|
+
(home / "cache.json").write_text("{}")
|
|
85
|
+
|
|
86
|
+
session_dir = tmp_path / "session"
|
|
87
|
+
(session_dir / "memory").mkdir(parents=True)
|
|
88
|
+
(session_dir / "memory" / "note.md").write_text("# Knowledge\n")
|
|
89
|
+
transcript = session_dir / "transcript.jsonl"
|
|
90
|
+
transcript.write_text("{}")
|
|
91
|
+
|
|
92
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
93
|
+
|
|
94
|
+
hook_input = json.dumps({"transcript_path": str(transcript)})
|
|
95
|
+
runner.invoke(app, ["hook"], input=hook_input)
|
|
96
|
+
result2 = runner.invoke(app, ["hook"], input=hook_input)
|
|
97
|
+
assert result2.exit_code == 0
|
|
98
|
+
assert "imported" not in result2.stdout.lower()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ── write_hook (settings.json) ────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_write_hook_creates_settings(tmp_path: Path):
|
|
105
|
+
"""write_hook creates .claude/settings.json with SessionEnd hook."""
|
|
106
|
+
cmd = "uvx"
|
|
107
|
+
args = ["lorekeep", "hook"]
|
|
108
|
+
|
|
109
|
+
path = write_hook(tmp_path, cmd, args)
|
|
110
|
+
assert path == tmp_path / ".claude" / "settings.json"
|
|
111
|
+
assert path.exists()
|
|
112
|
+
|
|
113
|
+
settings = json.loads(path.read_text())
|
|
114
|
+
hooks = settings["hooks"]["SessionEnd"]
|
|
115
|
+
assert len(hooks) == 1
|
|
116
|
+
handler = hooks[0]["hooks"][0]
|
|
117
|
+
assert handler["type"] == "command"
|
|
118
|
+
assert handler["command"] == "uvx"
|
|
119
|
+
assert handler["args"] == ["lorekeep", "hook"]
|
|
120
|
+
assert handler["timeout"] == 30
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_write_hook_preserves_existing_settings(tmp_path: Path):
|
|
124
|
+
"""write_hook preserves existing settings keys."""
|
|
125
|
+
settings_path = tmp_path / ".claude" / "settings.json"
|
|
126
|
+
settings_path.parent.mkdir(parents=True)
|
|
127
|
+
settings_path.write_text(json.dumps({
|
|
128
|
+
"permissions": {"allow": ["Read"]},
|
|
129
|
+
"hooks": {"PreToolUse": [{"hooks": [{"type": "command", "command": "echo hi"}]}]},
|
|
130
|
+
}))
|
|
131
|
+
|
|
132
|
+
write_hook(tmp_path, "uvx", ["lorekeep", "hook"])
|
|
133
|
+
|
|
134
|
+
settings = json.loads(settings_path.read_text())
|
|
135
|
+
assert settings["permissions"]["allow"] == ["Read"]
|
|
136
|
+
assert "PreToolUse" in settings["hooks"]
|
|
137
|
+
assert "SessionEnd" in settings["hooks"]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ── Init wires hook for Claude ─────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_init_writes_claude_hook(tmp_path: Path, monkeypatch):
|
|
144
|
+
"""init should write SessionEnd hook when Claude is detected."""
|
|
145
|
+
home = tmp_path / "home"
|
|
146
|
+
project = tmp_path / "project"
|
|
147
|
+
fake_home = tmp_path / "fakehome"
|
|
148
|
+
project.mkdir()
|
|
149
|
+
fake_home.mkdir()
|
|
150
|
+
(fake_home / ".claude").mkdir(parents=True)
|
|
151
|
+
|
|
152
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
153
|
+
monkeypatch.setenv("HOME", str(fake_home))
|
|
154
|
+
monkeypatch.setenv("LOREKEEP_DEV", "0")
|
|
155
|
+
monkeypatch.chdir(project)
|
|
156
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)
|
|
157
|
+
monkeypatch.setattr("lorekeep.integrations.detect.shutil.which", lambda _: None)
|
|
158
|
+
monkeypatch.delenv("OPENCODE", raising=False)
|
|
159
|
+
monkeypatch.delenv("CLAUDECODE", raising=False)
|
|
160
|
+
|
|
161
|
+
result = runner.invoke(app, ["init", "--yes", "--no-watch"])
|
|
162
|
+
assert result.exit_code == 0, result.stdout
|
|
163
|
+
|
|
164
|
+
settings = project / ".claude" / "settings.json"
|
|
165
|
+
assert settings.exists(), f"settings.json not written: {result.stdout}"
|
|
166
|
+
data = json.loads(settings.read_text())
|
|
167
|
+
assert "SessionEnd" in data["hooks"]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ── mcp add wires hook for Claude ──────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_mcp_add_writes_claude_hook(tmp_path: Path, monkeypatch):
|
|
174
|
+
"""mcp add --agent claude should also write the SessionEnd hook."""
|
|
175
|
+
home = tmp_path / "home"
|
|
176
|
+
project = tmp_path / "project"
|
|
177
|
+
project.mkdir()
|
|
178
|
+
home.mkdir()
|
|
179
|
+
(home / "config.yaml").write_text("install_source: pypi\n")
|
|
180
|
+
|
|
181
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
182
|
+
monkeypatch.chdir(project)
|
|
183
|
+
|
|
184
|
+
result = runner.invoke(app, ["mcp", "add", "--agent", "claude", "--ns", "backend"])
|
|
185
|
+
assert result.exit_code == 0, result.stdout
|
|
186
|
+
|
|
187
|
+
settings = project / ".claude" / "settings.json"
|
|
188
|
+
assert settings.exists()
|
|
189
|
+
data = json.loads(settings.read_text())
|
|
190
|
+
assert "SessionEnd" in data["hooks"]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_mcp_add_opencode_no_hook(tmp_path: Path, monkeypatch):
|
|
194
|
+
"""mcp add --agent opencode should NOT write a hook (no hook mechanism)."""
|
|
195
|
+
home = tmp_path / "home"
|
|
196
|
+
project = tmp_path / "project"
|
|
197
|
+
project.mkdir()
|
|
198
|
+
home.mkdir()
|
|
199
|
+
(home / "config.yaml").write_text("install_source: pypi\n")
|
|
200
|
+
|
|
201
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
202
|
+
monkeypatch.chdir(project)
|
|
203
|
+
|
|
204
|
+
result = runner.invoke(app, ["mcp", "add", "--agent", "opencode", "--ns", "backend"])
|
|
205
|
+
assert result.exit_code == 0, result.stdout
|
|
206
|
+
|
|
207
|
+
assert not (project / ".claude" / "settings.json").exists()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ── Cursor hook ───────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_write_cursor_hook(tmp_path: Path):
|
|
214
|
+
"""write_hook for Cursor creates .cursor/hooks.json with sessionEnd."""
|
|
215
|
+
from lorekeep.integrations.cursor import write_hook as write_cursor_hook
|
|
216
|
+
|
|
217
|
+
path = write_cursor_hook(tmp_path, "uvx", ["lorekeep", "hook"])
|
|
218
|
+
assert path == tmp_path / ".cursor" / "hooks.json"
|
|
219
|
+
assert path.exists()
|
|
220
|
+
|
|
221
|
+
data = json.loads(path.read_text())
|
|
222
|
+
assert data["version"] == 1
|
|
223
|
+
hooks = data["hooks"]["sessionEnd"]
|
|
224
|
+
assert len(hooks) == 1
|
|
225
|
+
assert hooks[0]["command"] == "uvx lorekeep hook"
|
|
226
|
+
assert hooks[0]["timeout"] == 30
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_mcp_add_cursor_hook(tmp_path: Path, monkeypatch):
|
|
230
|
+
"""mcp add --agent cursor should write sessionEnd hook."""
|
|
231
|
+
home = tmp_path / "home"
|
|
232
|
+
project = tmp_path / "project"
|
|
233
|
+
project.mkdir()
|
|
234
|
+
home.mkdir()
|
|
235
|
+
(home / "config.yaml").write_text("install_source: pypi\n")
|
|
236
|
+
|
|
237
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
238
|
+
monkeypatch.chdir(project)
|
|
239
|
+
|
|
240
|
+
result = runner.invoke(app, ["mcp", "add", "--agent", "cursor", "--ns", "backend"])
|
|
241
|
+
assert result.exit_code == 0, result.stdout
|
|
242
|
+
|
|
243
|
+
hooks_path = project / ".cursor" / "hooks.json"
|
|
244
|
+
assert hooks_path.exists()
|
|
245
|
+
data = json.loads(hooks_path.read_text())
|
|
246
|
+
assert "sessionEnd" in data["hooks"]
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"""Claude Code MCP config writer (.mcp.json)."""
|
|
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
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"""Cursor MCP config writer (.cursor/mcp.json)."""
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lorekeep-0.3.1 → lorekeep-0.4.0}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md
RENAMED
|
File without changes
|
{lorekeep-0.3.1 → lorekeep-0.4.0}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lorekeep-0.3.1 → lorekeep-0.4.0}/tests/fixtures/claude-session/memory/lorekeep-project-state.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|