lorekeep 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. lorekeep-0.3.0/.release-please-manifest.json +3 -0
  2. {lorekeep-0.2.1 → lorekeep-0.3.0}/CHANGELOG.md +8 -0
  3. {lorekeep-0.2.1 → lorekeep-0.3.0}/PKG-INFO +1 -1
  4. {lorekeep-0.2.1 → lorekeep-0.3.0}/pyproject.toml +1 -1
  5. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/__init__.py +1 -1
  6. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/cli.py +101 -5
  7. lorekeep-0.3.0/tests/test_init_chain.py +178 -0
  8. lorekeep-0.2.1/.release-please-manifest.json +0 -3
  9. {lorekeep-0.2.1 → lorekeep-0.3.0}/.commitlintrc.json +0 -0
  10. {lorekeep-0.2.1 → lorekeep-0.3.0}/.github/workflows/auto-merge-release-please.yml +0 -0
  11. {lorekeep-0.2.1 → lorekeep-0.3.0}/.github/workflows/ci.yml +0 -0
  12. {lorekeep-0.2.1 → lorekeep-0.3.0}/.github/workflows/lint-commits.yml +0 -0
  13. {lorekeep-0.2.1 → lorekeep-0.3.0}/.github/workflows/release-please.yml +0 -0
  14. {lorekeep-0.2.1 → lorekeep-0.3.0}/.gitignore +0 -0
  15. {lorekeep-0.2.1 → lorekeep-0.3.0}/.lorekeep/config.yaml.example +0 -0
  16. {lorekeep-0.2.1 → lorekeep-0.3.0}/.lorekeep/schema.json +0 -0
  17. {lorekeep-0.2.1 → lorekeep-0.3.0}/.pre-commit-config.yaml +0 -0
  18. {lorekeep-0.2.1 → lorekeep-0.3.0}/AGENTS.md +0 -0
  19. {lorekeep-0.2.1 → lorekeep-0.3.0}/CLAUDE.md +0 -0
  20. {lorekeep-0.2.1 → lorekeep-0.3.0}/LICENSE +0 -0
  21. {lorekeep-0.2.1 → lorekeep-0.3.0}/README.md +0 -0
  22. {lorekeep-0.2.1 → lorekeep-0.3.0}/SECURITY.md +0 -0
  23. {lorekeep-0.2.1 → lorekeep-0.3.0}/cover.jpeg +0 -0
  24. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/README.md +0 -0
  25. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/architecture/agent.md +0 -0
  26. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/architecture/data-model.md +0 -0
  27. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/architecture/evaluation.md +0 -0
  28. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/architecture/journal.md +0 -0
  29. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/architecture/overview.md +0 -0
  30. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/architecture/permission.md +0 -0
  31. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/architecture/pipeline.md +0 -0
  32. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/architecture/serve-mcp.md +0 -0
  33. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/architecture/temporal.md +0 -0
  34. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/guides/backup.md +0 -0
  35. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/guides/compile.md +0 -0
  36. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/guides/data-home.md +0 -0
  37. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/guides/getting-started.md +0 -0
  38. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/guides/import.md +0 -0
  39. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/guides/serve.md +0 -0
  40. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md +0 -0
  41. {lorekeep-0.2.1 → lorekeep-0.3.0}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md +0 -0
  42. {lorekeep-0.2.1 → lorekeep-0.3.0}/release-please-config.json +0 -0
  43. {lorekeep-0.2.1 → lorekeep-0.3.0}/scripts/check-conventional-commit.py +0 -0
  44. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/agent.py +0 -0
  45. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/backup.py +0 -0
  46. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/compile/__init__.py +0 -0
  47. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/compile/extract.py +0 -0
  48. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/compile/ingest.py +0 -0
  49. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/compile/providers.py +0 -0
  50. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/compile/resolve.py +0 -0
  51. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/compile/writer.py +0 -0
  52. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/config.py +0 -0
  53. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/defaults.py +0 -0
  54. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/eval/__init__.py +0 -0
  55. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/eval/construction.py +0 -0
  56. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/eval/gold.py +0 -0
  57. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/eval/retrieval.py +0 -0
  58. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/facts_io.py +0 -0
  59. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/importer/__init__.py +0 -0
  60. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/importer/claude.py +0 -0
  61. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/importer/cursor.py +0 -0
  62. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/integrations/__init__.py +0 -0
  63. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/integrations/claude_code.py +0 -0
  64. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/integrations/codex.py +0 -0
  65. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/integrations/common.py +0 -0
  66. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/integrations/cursor.py +0 -0
  67. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/integrations/detect.py +0 -0
  68. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/integrations/opencode.py +0 -0
  69. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/journal.py +0 -0
  70. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/mcp_server.py +0 -0
  71. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/models.py +0 -0
  72. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/paths.py +0 -0
  73. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/perm/__init__.py +0 -0
  74. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/perm/ns.py +0 -0
  75. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/pipeline.py +0 -0
  76. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/schema_io.py +0 -0
  77. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/store/__init__.py +0 -0
  78. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/store/fts.py +0 -0
  79. {lorekeep-0.2.1 → lorekeep-0.3.0}/src/lorekeep/store/graph.py +0 -0
  80. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/conftest.py +0 -0
  81. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/fixtures/claude-session/memory/MEMORY.md +0 -0
  82. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/fixtures/claude-session/memory/lorekeep-design.md +0 -0
  83. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/fixtures/claude-session/memory/lorekeep-project-state.md +0 -0
  84. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/fixtures/claude-session/test.jsonl +0 -0
  85. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/fixtures/gold/payments.facts.jsonl +0 -0
  86. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/fixtures/raw/backend/payments.md +0 -0
  87. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/fixtures/retrieval/questions.json +0 -0
  88. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/fixtures/schema.json +0 -0
  89. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_agent_cli.py +0 -0
  90. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_agent_detect.py +0 -0
  91. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_backup.py +0 -0
  92. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_backup_cli.py +0 -0
  93. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_compile_cli.py +0 -0
  94. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_config.py +0 -0
  95. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_construction_eval.py +0 -0
  96. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_defaults.py +0 -0
  97. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_determinism.py +0 -0
  98. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_doctor_cli.py +0 -0
  99. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_eval_cli.py +0 -0
  100. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_extract.py +0 -0
  101. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_fts.py +0 -0
  102. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_graph_store.py +0 -0
  103. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_import_claude.py +0 -0
  104. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_import_cursor.py +0 -0
  105. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_ingest.py +0 -0
  106. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_init_cli.py +0 -0
  107. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_integrations.py +0 -0
  108. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_mcp_add_cli.py +0 -0
  109. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_mcp_reload.py +0 -0
  110. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_mcp_server.py +0 -0
  111. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_mcp_write_tools.py +0 -0
  112. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_merge_journals.py +0 -0
  113. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_models.py +0 -0
  114. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_paths.py +0 -0
  115. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_perm.py +0 -0
  116. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_pipeline.py +0 -0
  117. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_providers.py +0 -0
  118. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_resolve.py +0 -0
  119. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_retrieval_eval.py +0 -0
  120. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_schema_io.py +0 -0
  121. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_serve_cli.py +0 -0
  122. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_smoke.py +0 -0
  123. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_watch_e2e.py +0 -0
  124. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_watch_sessions.py +0 -0
  125. {lorekeep-0.2.1 → lorekeep-0.3.0}/tests/test_writer.py +0 -0
  126. {lorekeep-0.2.1 → lorekeep-0.3.0}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.0"
3
+ }
@@ -1,11 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/manhhailua/lorekeep/compare/v0.2.1...v0.3.0) (2026-06-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * 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))
9
+
3
10
  ## [0.2.1](https://github.com/manhhailua/lorekeep/compare/v0.2.0...v0.2.1) (2026-06-28)
4
11
 
5
12
 
6
13
  ### Bug Fixes
7
14
 
8
15
  * 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))
16
+ * resolve doc contradictions and false cadence claims ([#53](https://github.com/manhhailua/lorekeep/issues/53)) ([10fc794](https://github.com/manhhailua/lorekeep/commit/10fc79406b02d1fa5735274b3dd89c62653ed3ba))
9
17
 
10
18
 
11
19
  ### Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lorekeep
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Temporal knowledge graph for AI agents via MCP
5
5
  Project-URL: Homepage, https://github.com/manhhailua/lorekeep
6
6
  Project-URL: Repository, https://github.com/manhhailua/lorekeep
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lorekeep"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "Temporal knowledge graph for AI agents via MCP"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1,3 +1,3 @@
1
1
  """Lorekeep — temporal knowledge graph for AI agents via MCP."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.3.0"
@@ -319,8 +319,12 @@ def init(
319
319
  False, "--yes", "-y",
320
320
  help="Skip interactive prompts, use defaults",
321
321
  ),
322
+ watch: bool = typer.Option(
323
+ True, "--watch/--no-watch",
324
+ help="Start the daemon (agent watch) in background after setup",
325
+ ),
322
326
  ) -> None:
323
- """Bootstrap the data home: config + schema + raw/graph dirs."""
327
+ """Bootstrap the data home, wire agents, import sessions, compile, and start daemon."""
324
328
  p = resolve_paths()
325
329
  created = []
326
330
  p["config"].parent.mkdir(parents=True, exist_ok=True)
@@ -348,6 +352,7 @@ def init(
348
352
 
349
353
  p["raw"].mkdir(parents=True, exist_ok=True)
350
354
  p["out"].mkdir(parents=True, exist_ok=True)
355
+ p["pending"].mkdir(parents=True, exist_ok=True)
351
356
 
352
357
  typer.echo(f"home ready: config={p['config']}")
353
358
  typer.echo(f" schema={p['schema']} raw={p['raw']} graph={p['out']}")
@@ -368,12 +373,20 @@ def init(
368
373
  (ns_dir / "about.md").write_text(about_md)
369
374
  typer.echo(f" wrote: {ns_dir / 'about.md'}")
370
375
 
371
- # Auto-detect and wire coding agents (active session or installed).
376
+ # --- One-click chain: wire → import → compile → daemon -----------------
372
377
  if not config_existed:
373
378
  _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>")
379
+ _auto_import_and_compile(p)
380
+ typer.echo("\nRestart your agent → lorekeep tools are available.")
381
+
382
+ # Daemon: start on fresh init or revive if dead (regardless of config_existed)
383
+ if watch and _is_interactive():
384
+ _start_daemon(p)
385
+ elif watch and not _is_interactive():
386
+ typer.echo("\n (skipped daemon start in non-interactive mode — run `lorekeep agent watch` manually)")
387
+
388
+ if config_existed:
389
+ typer.echo("\nAlready initialized.")
377
390
 
378
391
 
379
392
  def _interactive_init(p: dict) -> tuple[str, str, str]:
@@ -478,6 +491,89 @@ def _agent_writers() -> dict:
478
491
  }
479
492
 
480
493
 
494
+ def _auto_import_and_compile(p: dict) -> None:
495
+ """Quick-import Claude memory files, then compile if provider is available."""
496
+ imported = 0
497
+
498
+ # --- Quick import: Claude memory files (zero LLM cost) ----------------
499
+ try:
500
+ from lorekeep.importer.claude import find_current_session, import_memories
501
+ session_dir = find_current_session()
502
+ if session_dir is not None and (session_dir / "memory").is_dir():
503
+ written = import_memories(session_dir, p["raw"], "claude-memory")
504
+ imported = len(written)
505
+ if imported:
506
+ typer.echo(f" imported {imported} memory file(s) from Claude session")
507
+ except Exception as exc:
508
+ if os.environ.get("LOREKEEP_DEBUG"):
509
+ typer.echo(f" import error: {exc}")
510
+
511
+ # --- Compile (if provider is usable) ----------------------------------
512
+ schema = load_schema(p["schema"])
513
+ config = load_config(p["config"])
514
+
515
+ has_key = (
516
+ os.environ.get("LOREKEEP_PROVIDER") == "fake"
517
+ or (config.provider.api_key_env and os.environ.get(config.provider.api_key_env))
518
+ or config.provider.api_key
519
+ )
520
+
521
+ if not has_key:
522
+ if imported:
523
+ typer.echo(" memory files imported to raw/ — compile pending (add API key to config.yaml)")
524
+ else:
525
+ typer.echo(" graph empty — run `lorekeep compile` after adding docs or importing sessions")
526
+ return
527
+
528
+ try:
529
+ provider = _make_provider(config)
530
+ manifest = compile_graph(
531
+ raw_root=p["raw"], out_dir=p["out"], schema=schema,
532
+ provider=provider, cache_path=p["cache"],
533
+ chunk_lines=config.compile.chunk_lines,
534
+ )
535
+ pending_dir = p.get("pending")
536
+ if pending_dir and pending_dir.exists():
537
+ _do_auto_resolve(p["out"], pending_dir)
538
+ typer.echo(f" compiled: {manifest.node_count} nodes, {manifest.edge_count} edges")
539
+ except Exception as exc:
540
+ typer.echo(f" compile skipped: {exc}")
541
+
542
+
543
+ def _start_daemon(p: dict) -> None:
544
+ """Start agent watch as a background process with PID + log files."""
545
+ import subprocess
546
+ import sys
547
+
548
+ pid_path = p["home"] / "agent.pid"
549
+ log_path = p["home"] / "agent.log"
550
+
551
+ # Check if already running
552
+ if pid_path.exists():
553
+ old_pid = pid_path.read_text().strip()
554
+ try:
555
+ os.kill(int(old_pid), 0)
556
+ typer.echo(f" daemon already running (pid={old_pid})")
557
+ return
558
+ except (ProcessLookupError, ValueError):
559
+ pass
560
+
561
+ cmd = [sys.executable, "-m", "lorekeep.cli", "agent", "watch", "--interval", "60"]
562
+ log_file = open(log_path, "a")
563
+ try:
564
+ proc = subprocess.Popen(
565
+ cmd,
566
+ stdout=log_file,
567
+ stderr=subprocess.STDOUT,
568
+ stdin=subprocess.DEVNULL,
569
+ start_new_session=True,
570
+ )
571
+ finally:
572
+ log_file.close()
573
+ pid_path.write_text(str(proc.pid))
574
+ typer.echo(f" daemon started (pid={proc.pid}, log={log_path})")
575
+
576
+
481
577
  @app.command()
482
578
  def backup(
483
579
  init_remote: str = typer.Option(
@@ -0,0 +1,178 @@
1
+ """Tests for the zero-friction init chain: import → compile → daemon."""
2
+ import json
3
+ from pathlib import Path
4
+ from unittest.mock import patch, MagicMock
5
+
6
+ from typer.testing import CliRunner
7
+
8
+ from lorekeep.cli import app
9
+
10
+ runner = CliRunner()
11
+
12
+
13
+ def _setup_env(tmp_path: Path, monkeypatch, fake_provider=True):
14
+ home = tmp_path / "home"
15
+ project = tmp_path / "project"
16
+ fake_home = tmp_path / "fakehome"
17
+ project.mkdir()
18
+ fake_home.mkdir()
19
+ monkeypatch.setenv("LOREKEEP_HOME", str(home))
20
+ monkeypatch.setenv("HOME", str(fake_home))
21
+ monkeypatch.setenv("LOREKEEP_DEV", "0")
22
+ monkeypatch.chdir(project)
23
+ monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)
24
+ monkeypatch.setattr("lorekeep.integrations.detect.shutil.which", lambda _: None)
25
+ monkeypatch.delenv("OPENCODE", raising=False)
26
+ monkeypatch.delenv("CLAUDECODE", raising=False)
27
+ if fake_provider:
28
+ monkeypatch.setenv("LOREKEEP_PROVIDER", "fake")
29
+ return home, project
30
+
31
+
32
+ # ── Init chains into compile ──────────────────────────────────────────────
33
+
34
+
35
+ def test_init_compiles_with_fake_provider(tmp_path: Path, monkeypatch):
36
+ """init --yes with LOREKEEP_PROVIDER=fake should auto-compile facts.jsonl."""
37
+ home, project = _setup_env(tmp_path, monkeypatch)
38
+
39
+ result = runner.invoke(app, ["init", "--yes", "--no-watch"])
40
+ assert result.exit_code == 0, result.stdout
41
+
42
+ facts = home / "graph" / "facts.jsonl"
43
+ assert facts.exists(), f"facts.jsonl not created: {result.stdout}"
44
+ lines = [json.loads(l) for l in facts.read_text().strip().splitlines() if l.strip()]
45
+ assert any(f["id"] == "svc:payments-api" for f in lines)
46
+
47
+
48
+ def test_init_skips_compile_without_api_key(tmp_path: Path, monkeypatch):
49
+ """init --yes without provider key should skip compile gracefully."""
50
+ home, project = _setup_env(tmp_path, monkeypatch, fake_provider=False)
51
+
52
+ result = runner.invoke(app, ["init", "--yes", "--no-watch"])
53
+ assert result.exit_code == 0, result.stdout
54
+
55
+ assert not (home / "graph" / "facts.jsonl").exists()
56
+ assert "compile" in result.stdout.lower() or "empty" in result.stdout.lower()
57
+
58
+
59
+ # ── Init auto-imports Claude memory ───────────────────────────────────────
60
+
61
+
62
+ def test_init_imports_claude_memory(tmp_path: Path, monkeypatch):
63
+ """init should quick-import Claude memory files if a session is found."""
64
+ home, project = _setup_env(tmp_path, monkeypatch)
65
+
66
+ fake_session = tmp_path / "session"
67
+ (fake_session / "memory").mkdir(parents=True)
68
+ (fake_session / "memory" / "note.md").write_text("# Important\nSession knowledge.\n")
69
+
70
+ monkeypatch.setattr(
71
+ "lorekeep.importer.claude.find_current_session",
72
+ lambda: fake_session,
73
+ )
74
+
75
+ result = runner.invoke(app, ["init", "--yes", "--no-watch"])
76
+ assert result.exit_code == 0, result.stdout
77
+
78
+ imported = home / "raw" / "claude-memory" / "note.md"
79
+ assert imported.exists()
80
+ assert "imported" in result.stdout.lower()
81
+
82
+
83
+ # ── Init starts daemon ────────────────────────────────────────────────────
84
+
85
+
86
+ def test_init_starts_daemon(tmp_path: Path, monkeypatch):
87
+ """init --watch in interactive mode should spawn agent watch."""
88
+ home, project = _setup_env(tmp_path, monkeypatch)
89
+ monkeypatch.setattr("lorekeep.cli._is_interactive", lambda: True)
90
+
91
+ mock_proc = MagicMock()
92
+ mock_proc.pid = 99999
93
+ mock_popen = MagicMock(return_value=mock_proc)
94
+ monkeypatch.setattr("subprocess.Popen", mock_popen)
95
+
96
+ result = runner.invoke(app, ["init", "--yes"])
97
+ assert result.exit_code == 0, result.stdout
98
+
99
+ mock_popen.assert_called_once()
100
+ cmd = mock_popen.call_args[0][0]
101
+ assert "agent" in cmd
102
+ assert "watch" in cmd
103
+
104
+ pid_file = home / "agent.pid"
105
+ assert pid_file.exists()
106
+ assert pid_file.read_text().strip() == "99999"
107
+
108
+
109
+ def test_init_skips_daemon_in_noninteractive(tmp_path: Path, monkeypatch):
110
+ """init --watch in non-interactive mode should not spawn daemon."""
111
+ home, project = _setup_env(tmp_path, monkeypatch)
112
+ # _is_interactive() returns False in CliRunner by default
113
+
114
+ mock_popen = MagicMock()
115
+ monkeypatch.setattr("subprocess.Popen", mock_popen)
116
+
117
+ result = runner.invoke(app, ["init", "--yes"])
118
+ assert result.exit_code == 0, result.stdout
119
+
120
+ mock_popen.assert_not_called()
121
+ assert "non-interactive" in result.stdout.lower()
122
+
123
+
124
+ def test_init_rerun_revives_dead_daemon(tmp_path: Path, monkeypatch):
125
+ """Re-running init should start daemon if it's not running, even if config exists."""
126
+ home, project = _setup_env(tmp_path, monkeypatch)
127
+ monkeypatch.setattr("lorekeep.cli._is_interactive", lambda: True)
128
+
129
+ # First init creates config
130
+ runner.invoke(app, ["init", "--yes", "--no-watch"])
131
+
132
+ # Simulate dead daemon: stale PID file pointing to nonexistent process
133
+ pid_path = home / "agent.pid"
134
+ pid_path.write_text("999999")
135
+
136
+ mock_proc = MagicMock()
137
+ mock_proc.pid = 88888
138
+ mock_popen = MagicMock(return_value=mock_proc)
139
+ monkeypatch.setattr("subprocess.Popen", mock_popen)
140
+
141
+ result = runner.invoke(app, ["init", "--yes"])
142
+ assert result.exit_code == 0, result.stdout
143
+
144
+ mock_popen.assert_called_once()
145
+ assert pid_path.read_text().strip() == "88888"
146
+
147
+
148
+ # ── Init creates pending/ dir ─────────────────────────────────────────────
149
+
150
+
151
+ def test_init_creates_pending_dir(tmp_path: Path, monkeypatch):
152
+ """init should create the pending/ directory for journals."""
153
+ home, project = _setup_env(tmp_path, monkeypatch)
154
+
155
+ result = runner.invoke(app, ["init", "--yes", "--no-watch"])
156
+ assert result.exit_code == 0, result.stdout
157
+
158
+ assert (home / "pending").is_dir()
159
+
160
+
161
+ # ── Init idempotent re-run ────────────────────────────────────────────────
162
+
163
+
164
+ def test_init_rerun_skips_chain(tmp_path: Path, monkeypatch):
165
+ """Re-running init on existing config should not re-chain."""
166
+ home, project = _setup_env(tmp_path, monkeypatch)
167
+
168
+ result1 = runner.invoke(app, ["init", "--yes", "--no-watch"])
169
+ assert result1.exit_code == 0
170
+
171
+ mock_popen = MagicMock()
172
+ monkeypatch.setattr("subprocess.Popen", mock_popen)
173
+
174
+ result2 = runner.invoke(app, ["init", "--yes"])
175
+ assert result2.exit_code == 0, result2.stdout
176
+
177
+ mock_popen.assert_not_called()
178
+ assert "already initialized" in result2.stdout.lower()
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.2.1"
3
- }
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