lorekeep 0.1.13__tar.gz → 0.2.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.1.13 → lorekeep-0.2.0}/.github/workflows/release-please.yml +9 -2
- lorekeep-0.2.0/.release-please-manifest.json +3 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/CHANGELOG.md +7 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/PKG-INFO +1 -1
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/guides/getting-started.md +16 -5
- {lorekeep-0.1.13 → lorekeep-0.2.0}/pyproject.toml +1 -1
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/__init__.py +1 -1
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/cli.py +51 -5
- lorekeep-0.2.0/src/lorekeep/integrations/detect.py +82 -0
- lorekeep-0.2.0/src/lorekeep/integrations/opencode.py +43 -0
- lorekeep-0.2.0/tests/test_agent_detect.py +71 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_init_cli.py +64 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_integrations.py +37 -1
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_mcp_add_cli.py +14 -0
- lorekeep-0.1.13/.release-please-manifest.json +0 -3
- {lorekeep-0.1.13 → lorekeep-0.2.0}/.commitlintrc.json +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/.github/workflows/auto-merge-release-please.yml +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/.github/workflows/ci.yml +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/.github/workflows/lint-commits.yml +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/.gitignore +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/.lorekeep/config.yaml.example +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/.lorekeep/schema.json +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/.pre-commit-config.yaml +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/AGENTS.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/CLAUDE.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/LICENSE +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/README.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/SECURITY.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/cover.jpeg +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/README.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/architecture/agent.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/architecture/data-model.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/architecture/evaluation.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/architecture/journal.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/architecture/overview.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/architecture/permission.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/architecture/pipeline.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/architecture/serve-mcp.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/architecture/temporal.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/guides/backup.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/guides/compile.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/guides/data-home.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/guides/import.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/guides/serve.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/release-please-config.json +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/scripts/check-conventional-commit.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/agent.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/backup.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/compile/__init__.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/compile/extract.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/compile/ingest.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/compile/providers.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/compile/resolve.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/compile/writer.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/config.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/defaults.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/eval/__init__.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/eval/construction.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/eval/gold.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/eval/retrieval.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/facts_io.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/importer/__init__.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/importer/claude.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/importer/cursor.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/integrations/__init__.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/integrations/claude_code.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/integrations/codex.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/integrations/common.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/integrations/cursor.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/journal.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/mcp_server.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/models.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/paths.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/perm/__init__.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/perm/ns.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/pipeline.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/schema_io.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/store/__init__.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/store/fts.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/src/lorekeep/store/graph.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/conftest.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/fixtures/claude-session/memory/MEMORY.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/fixtures/claude-session/memory/lorekeep-design.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/fixtures/claude-session/memory/lorekeep-project-state.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/fixtures/claude-session/test.jsonl +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/fixtures/gold/payments.facts.jsonl +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/fixtures/raw/backend/payments.md +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/fixtures/retrieval/questions.json +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/fixtures/schema.json +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_agent_cli.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_backup.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_backup_cli.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_compile_cli.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_config.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_construction_eval.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_defaults.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_determinism.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_doctor_cli.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_eval_cli.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_extract.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_fts.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_graph_store.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_import_claude.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_import_cursor.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_ingest.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_mcp_reload.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_mcp_server.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_models.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_paths.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_perm.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_pipeline.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_providers.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_resolve.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_retrieval_eval.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_schema_io.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_serve_cli.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_smoke.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_watch_sessions.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/tests/test_writer.py +0 -0
- {lorekeep-0.1.13 → lorekeep-0.2.0}/uv.lock +0 -0
|
@@ -13,6 +13,12 @@ name: release-please
|
|
|
13
13
|
on:
|
|
14
14
|
push:
|
|
15
15
|
branches: [main]
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
tag_name:
|
|
19
|
+
description: "Tag to publish (e.g. v0.1.13)"
|
|
20
|
+
required: true
|
|
21
|
+
type: string
|
|
16
22
|
|
|
17
23
|
permissions:
|
|
18
24
|
contents: write
|
|
@@ -21,6 +27,7 @@ permissions:
|
|
|
21
27
|
jobs:
|
|
22
28
|
release-please:
|
|
23
29
|
runs-on: ubuntu-latest
|
|
30
|
+
if: github.event_name == 'push'
|
|
24
31
|
outputs:
|
|
25
32
|
release_created: ${{ steps.rp.outputs.release_created }}
|
|
26
33
|
tag_name: ${{ steps.rp.outputs.tag_name }}
|
|
@@ -32,7 +39,7 @@ jobs:
|
|
|
32
39
|
|
|
33
40
|
publish:
|
|
34
41
|
needs: release-please
|
|
35
|
-
if: needs.release-please.outputs.release_created == 'true'
|
|
42
|
+
if: always() && (needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch')
|
|
36
43
|
runs-on: ubuntu-latest
|
|
37
44
|
environment: pypi
|
|
38
45
|
permissions:
|
|
@@ -41,7 +48,7 @@ jobs:
|
|
|
41
48
|
steps:
|
|
42
49
|
- uses: actions/checkout@v5
|
|
43
50
|
with:
|
|
44
|
-
ref: ${{ needs.release-please.outputs.tag_name }}
|
|
51
|
+
ref: ${{ github.event.inputs.tag_name || needs.release-please.outputs.tag_name }}
|
|
45
52
|
|
|
46
53
|
- uses: astral-sh/setup-uv@v7
|
|
47
54
|
with:
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/manhhailua/lorekeep/compare/v0.1.13...v0.2.0) (2026-06-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* auto-detect coding agents during init and wire MCP automatically ([#46](https://github.com/manhhailua/lorekeep/issues/46)) ([c529974](https://github.com/manhhailua/lorekeep/commit/c5299746ce12d5d12a3511a990d9711d357b0277))
|
|
9
|
+
|
|
3
10
|
## [0.1.13](https://github.com/manhhailua/lorekeep/compare/v0.1.12...v0.1.13) (2026-06-27)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -9,7 +9,7 @@ graph — including the backup setup. For the terse 5-step version, see the
|
|
|
9
9
|
- A **data home** (`.lorekeep/` in a source checkout, or an XDG dir for an
|
|
10
10
|
installed copy) holding your config, schema, raw docs, and compiled graph.
|
|
11
11
|
- A compiled `facts.jsonl` graph built from your markdown.
|
|
12
|
-
- A coding agent (Claude Code / Cursor / Codex) reading the graph over MCP,
|
|
12
|
+
- A coding agent (Claude Code / Cursor / Codex / opencode) reading the graph over MCP,
|
|
13
13
|
scoped to a namespace.
|
|
14
14
|
- A private git backup of the data home, so the graph syncs to your other
|
|
15
15
|
machines.
|
|
@@ -49,7 +49,16 @@ uvx lorekeep init
|
|
|
49
49
|
`raw/<ns>/about.md`, so the compiled graph starts with a fact about you.
|
|
50
50
|
|
|
51
51
|
It then writes `config.yaml` + `schema.json`, creates `raw/` + `graph/` dirs,
|
|
52
|
-
|
|
52
|
+
writes `raw/<ns>/about.md`, and **auto-detects coding agents** to wire:
|
|
53
|
+
|
|
54
|
+
- If you're running `init` **inside** a coding agent (e.g. from opencode's or
|
|
55
|
+
Claude Code's shell), it wires only that agent.
|
|
56
|
+
- If you're in a **plain shell**, it scans for all installed agents
|
|
57
|
+
(`~/.claude`, `~/.cursor`, `~/.codex`, `~/.config/opencode`) and wires each.
|
|
58
|
+
|
|
59
|
+
Each detected agent gets its MCP config written automatically (`.mcp.json`,
|
|
60
|
+
`.cursor/mcp.json`, `config.toml`, or `opencode.json` — depending on the agent).
|
|
61
|
+
Restart the agent to pick up the new tools.
|
|
53
62
|
|
|
54
63
|
Non-interactive (CI, scripts): `uvx lorekeep init --yes`. From a source
|
|
55
64
|
checkout it uses the repo's own `.lorekeep/`; for an installed copy it uses
|
|
@@ -113,14 +122,16 @@ Recompiling unchanged input is **byte-identical** (determinism is a hard
|
|
|
113
122
|
requirement) — unchanged chunks return cached LLM output, so only edited docs
|
|
114
123
|
cost tokens on recompile.
|
|
115
124
|
|
|
116
|
-
## 6. Wire
|
|
125
|
+
## 6. Wire additional coding agents
|
|
117
126
|
|
|
118
|
-
|
|
127
|
+
`init` already wired agents it detected. To add more, or to re-scope one:
|
|
119
128
|
|
|
120
129
|
```bash
|
|
121
|
-
uvx lorekeep mcp add --agent
|
|
130
|
+
uvx lorekeep mcp add --agent opencode --ns backend
|
|
122
131
|
```
|
|
123
132
|
|
|
133
|
+
Supported agents: `claude`, `cursor`, `codex`, `opencode`.
|
|
134
|
+
|
|
124
135
|
Restart the agent → the 8 read-only Lorekeep tools (`search`, `get_node`,
|
|
125
136
|
`neighbors`, `at_time`, `history`, `changes`, `list_namespaces`, `schema`) are
|
|
126
137
|
available, scoped to `backend` (+ `public`). See
|
|
@@ -227,12 +227,11 @@ app.add_typer(mcp_app, name="mcp")
|
|
|
227
227
|
|
|
228
228
|
@mcp_app.command("add")
|
|
229
229
|
def mcp_add(
|
|
230
|
-
agent: str = typer.Option(..., "--agent", help="claude | cursor | codex"),
|
|
230
|
+
agent: str = typer.Option(..., "--agent", help="claude | cursor | codex | opencode"),
|
|
231
231
|
scope: str = typer.Option("project", "--scope", help="project | user"),
|
|
232
232
|
ns: str = typer.Option(None, "--ns", help="namespace to scope the agent to"),
|
|
233
233
|
) -> None:
|
|
234
234
|
"""Write the agent's MCP config + print an agent-memory snippet."""
|
|
235
|
-
from lorekeep.integrations import claude_code, codex, cursor
|
|
236
235
|
from lorekeep.integrations.common import agent_memory_snippet, resolve_command
|
|
237
236
|
|
|
238
237
|
p = resolve_paths()
|
|
@@ -240,9 +239,9 @@ def mcp_add(
|
|
|
240
239
|
command, args = resolve_command(config.install_source)
|
|
241
240
|
|
|
242
241
|
target = Path.cwd() if scope == "project" else Path.home()
|
|
243
|
-
writers =
|
|
242
|
+
writers = _agent_writers()
|
|
244
243
|
if agent not in writers:
|
|
245
|
-
typer.echo(f"unknown agent: {agent} (choose claude|cursor|codex)")
|
|
244
|
+
typer.echo(f"unknown agent: {agent} (choose claude|cursor|codex|opencode)")
|
|
246
245
|
raise typer.Exit(code=1)
|
|
247
246
|
written = writers[agent].write_config(target, command, args, ns)
|
|
248
247
|
typer.echo(f"wrote {agent} config -> {written}")
|
|
@@ -357,10 +356,12 @@ def init(
|
|
|
357
356
|
(ns_dir / "about.md").write_text(about_md)
|
|
358
357
|
typer.echo(f" wrote: {ns_dir / 'about.md'}")
|
|
359
358
|
|
|
359
|
+
# Auto-detect and wire coding agents (active session or installed).
|
|
360
360
|
if not config_existed:
|
|
361
|
+
_auto_wire_agents(p, ns)
|
|
361
362
|
typer.echo("\nNext steps:")
|
|
362
363
|
typer.echo(" 1. Compile: uvx lorekeep compile (or LOREKEEP_PROVIDER=fake)")
|
|
363
|
-
typer.echo(
|
|
364
|
+
typer.echo(" 2. More agents: uvx lorekeep mcp add --agent <claude|cursor|codex|opencode>")
|
|
364
365
|
|
|
365
366
|
|
|
366
367
|
def _interactive_init(p: dict) -> tuple[str, str, str]:
|
|
@@ -420,6 +421,51 @@ def _interactive_init(p: dict) -> tuple[str, str, str]:
|
|
|
420
421
|
return ns, name, bio
|
|
421
422
|
|
|
422
423
|
|
|
424
|
+
def _auto_wire_agents(p: dict, ns: str) -> None:
|
|
425
|
+
"""Detect coding agents and write their MCP configs automatically.
|
|
426
|
+
|
|
427
|
+
If running inside a coding agent (env var set), wires only that agent.
|
|
428
|
+
Otherwise scans the filesystem for all installed agents and wires each.
|
|
429
|
+
"""
|
|
430
|
+
from lorekeep.integrations.detect import detect_agents
|
|
431
|
+
from lorekeep.integrations.common import agent_memory_snippet, resolve_command
|
|
432
|
+
|
|
433
|
+
detected = detect_agents()
|
|
434
|
+
if not detected:
|
|
435
|
+
typer.echo("\n No coding agents detected — run `lorekeep mcp add --agent <name>` after install.")
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
config = load_config(p["config"])
|
|
439
|
+
command, args = resolve_command(config.install_source)
|
|
440
|
+
|
|
441
|
+
writers = _agent_writers()
|
|
442
|
+
target = Path.cwd()
|
|
443
|
+
|
|
444
|
+
typer.echo(f"\n Detected agents: {', '.join(detected)}")
|
|
445
|
+
for agent_name in detected:
|
|
446
|
+
writer = writers.get(agent_name)
|
|
447
|
+
if not writer:
|
|
448
|
+
continue
|
|
449
|
+
try:
|
|
450
|
+
written = writer.write_config(target, command, args, ns)
|
|
451
|
+
typer.echo(f" wired {agent_name} -> {written}")
|
|
452
|
+
except Exception as exc:
|
|
453
|
+
typer.echo(f" {agent_name}: failed ({exc})")
|
|
454
|
+
|
|
455
|
+
typer.echo("\n " + agent_memory_snippet().replace("\n", "\n ").strip())
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _agent_writers() -> dict:
|
|
459
|
+
"""Return the agent-name → writer-module mapping (lazy import)."""
|
|
460
|
+
from lorekeep.integrations import claude_code, codex, cursor, opencode
|
|
461
|
+
return {
|
|
462
|
+
"claude": claude_code,
|
|
463
|
+
"cursor": cursor,
|
|
464
|
+
"codex": codex,
|
|
465
|
+
"opencode": opencode,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
423
469
|
@app.command()
|
|
424
470
|
def backup(
|
|
425
471
|
init_remote: str = typer.Option(
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Detect coding agents: active session (env vars) + installed (filesystem markers).
|
|
2
|
+
|
|
3
|
+
Two-layer detection:
|
|
4
|
+
1. ``detect_active_agent`` — which agent shell are we running inside right now?
|
|
5
|
+
Uses well-known env vars. Returns at most one agent name (or ``None``).
|
|
6
|
+
2. ``detect_installed_agents`` — which agents are installed on this machine?
|
|
7
|
+
Checks for config directories / binaries. Returns a list (may be empty).
|
|
8
|
+
|
|
9
|
+
``detect_agents`` combines both: if an active session is detected, return just
|
|
10
|
+
that agent. Otherwise, return all installed agents found on the filesystem.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# Env var that each agent sets when it launches a subprocess shell.
|
|
19
|
+
# The value is checked for truthiness (any non-empty string counts).
|
|
20
|
+
_ACTIVE_ENV: dict[str, list[str]] = {
|
|
21
|
+
"claude": ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"],
|
|
22
|
+
"opencode": ["OPENCODE"],
|
|
23
|
+
"cursor": ["CURSOR_DEBUG"],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Filesystem markers that indicate an agent has been installed / used.
|
|
27
|
+
# Each entry is a list of candidate paths (``~`` expanded relative to home).
|
|
28
|
+
_INSTALLED_MARKERS: dict[str, list[str]] = {
|
|
29
|
+
"claude": ["~/.claude"],
|
|
30
|
+
"cursor": ["~/.cursor"],
|
|
31
|
+
"codex": ["~/.codex"],
|
|
32
|
+
"opencode": ["~/.config/opencode", "~/.opencode"],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Binary names to look for in PATH (fallback when dir markers are missing).
|
|
36
|
+
_BINARIES: dict[str, list[str]] = {
|
|
37
|
+
"claude": ["claude"],
|
|
38
|
+
"cursor": ["cursor"],
|
|
39
|
+
"codex": ["codex"],
|
|
40
|
+
"opencode": ["opencode"],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
SUPPORTED_AGENTS = sorted(_ACTIVE_ENV.keys() | _INSTALLED_MARKERS.keys())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _env_truthy(name: str) -> bool:
|
|
47
|
+
val = os.environ.get(name, "")
|
|
48
|
+
return val not in ("", "0", "false", "False")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def detect_active_agent() -> str | None:
|
|
52
|
+
"""Return the agent whose shell we are running inside, or ``None``."""
|
|
53
|
+
for agent, env_vars in _ACTIVE_ENV.items():
|
|
54
|
+
if any(_env_truthy(v) for v in env_vars):
|
|
55
|
+
return agent
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def detect_installed_agents() -> list[str]:
|
|
60
|
+
"""Return all agents detected on this machine (filesystem + PATH)."""
|
|
61
|
+
found: list[str] = []
|
|
62
|
+
home = Path.home()
|
|
63
|
+
for agent, markers in _INSTALLED_MARKERS.items():
|
|
64
|
+
if any(Path(m).expanduser().exists() for m in markers):
|
|
65
|
+
found.append(agent)
|
|
66
|
+
continue
|
|
67
|
+
if any(shutil.which(b) for b in _BINARIES.get(agent, [])):
|
|
68
|
+
found.append(agent)
|
|
69
|
+
return found
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def detect_agents() -> list[str]:
|
|
73
|
+
"""Detect agents to wire during ``init``.
|
|
74
|
+
|
|
75
|
+
- If running inside a coding agent, return **only** that agent (the user
|
|
76
|
+
is already in it; wiring others is noise).
|
|
77
|
+
- Otherwise, return all installed agents found on the filesystem.
|
|
78
|
+
"""
|
|
79
|
+
active = detect_active_agent()
|
|
80
|
+
if active:
|
|
81
|
+
return [active]
|
|
82
|
+
return detect_installed_agents()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""opencode MCP config writer (opencode.json ``mcp`` section).
|
|
2
|
+
|
|
3
|
+
opencode uses a different MCP schema than Claude/Cursor:
|
|
4
|
+
- Config file: ``opencode.json`` (project root) or ``~/.config/opencode/opencode.json`` (global)
|
|
5
|
+
- Key: ``mcp`` (not ``mcpServers``)
|
|
6
|
+
- Entry shape: ``{"type": "local", "command": [cmd, ...args], "enabled": true, "environment": {...}}``
|
|
7
|
+
- ``command`` is a single array (command + args combined)
|
|
8
|
+
|
|
9
|
+
Idempotent: re-running merges into the existing ``mcp`` section, replacing only
|
|
10
|
+
the ``lorekeep`` entry.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def write_config(target_dir: Path, command: str, args: list[str], ns: str | None) -> Path:
|
|
19
|
+
target_dir = Path(target_dir)
|
|
20
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
path = target_dir / "opencode.json"
|
|
22
|
+
|
|
23
|
+
existing: dict = {}
|
|
24
|
+
if path.exists():
|
|
25
|
+
try:
|
|
26
|
+
existing = json.loads(path.read_text())
|
|
27
|
+
except (json.JSONDecodeError, ValueError):
|
|
28
|
+
existing = {}
|
|
29
|
+
|
|
30
|
+
entry: dict = {
|
|
31
|
+
"type": "local",
|
|
32
|
+
"command": [command, *args],
|
|
33
|
+
"enabled": True,
|
|
34
|
+
}
|
|
35
|
+
if ns:
|
|
36
|
+
entry["environment"] = {"LOREKEEP_NS": ns}
|
|
37
|
+
|
|
38
|
+
mcp = existing.get("mcp", {})
|
|
39
|
+
mcp["lorekeep"] = entry
|
|
40
|
+
existing["mcp"] = mcp
|
|
41
|
+
|
|
42
|
+
path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
43
|
+
return path
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Tests for agent auto-detection (env vars + filesystem markers)."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from lorekeep.integrations.detect import detect_active_agent, detect_installed_agents, detect_agents
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _isolate(monkeypatch, tmp_path):
|
|
7
|
+
"""Block all real env vars + filesystem markers + PATH lookups."""
|
|
8
|
+
for v in ("CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "OPENCODE", "CURSOR_DEBUG"):
|
|
9
|
+
monkeypatch.delenv(v, raising=False)
|
|
10
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
11
|
+
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
12
|
+
monkeypatch.setattr("lorekeep.integrations.detect.shutil.which", lambda _: None)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_detect_active_opencode(monkeypatch):
|
|
16
|
+
_isolate(monkeypatch, Path("/tmp"))
|
|
17
|
+
monkeypatch.setenv("OPENCODE", "1")
|
|
18
|
+
assert detect_active_agent() == "opencode"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_detect_active_claude(monkeypatch):
|
|
22
|
+
_isolate(monkeypatch, Path("/tmp"))
|
|
23
|
+
monkeypatch.setenv("CLAUDECODE", "1")
|
|
24
|
+
assert detect_active_agent() == "claude"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_detect_active_none(monkeypatch, tmp_path):
|
|
28
|
+
_isolate(monkeypatch, tmp_path)
|
|
29
|
+
assert detect_active_agent() is None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_detect_active_falsy_env_ignored(monkeypatch, tmp_path):
|
|
33
|
+
_isolate(monkeypatch, tmp_path)
|
|
34
|
+
monkeypatch.setenv("OPENCODE", "0")
|
|
35
|
+
assert detect_active_agent() is None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_detect_installed_finds_markers(monkeypatch, tmp_path):
|
|
39
|
+
_isolate(monkeypatch, tmp_path)
|
|
40
|
+
(tmp_path / ".claude").mkdir()
|
|
41
|
+
(tmp_path / ".config" / "opencode").mkdir(parents=True)
|
|
42
|
+
found = detect_installed_agents()
|
|
43
|
+
assert "claude" in found
|
|
44
|
+
assert "opencode" in found
|
|
45
|
+
assert "codex" not in found
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_detect_installed_empty(monkeypatch, tmp_path):
|
|
49
|
+
_isolate(monkeypatch, tmp_path)
|
|
50
|
+
assert detect_installed_agents() == []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_detect_agents_active_overrides_installed(monkeypatch, tmp_path):
|
|
54
|
+
"""When inside an agent session, return only that agent."""
|
|
55
|
+
_isolate(monkeypatch, tmp_path)
|
|
56
|
+
monkeypatch.setenv("OPENCODE", "1")
|
|
57
|
+
(tmp_path / ".claude").mkdir()
|
|
58
|
+
(tmp_path / ".config" / "opencode").mkdir(parents=True)
|
|
59
|
+
result = detect_agents()
|
|
60
|
+
assert result == ["opencode"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_detect_agents_no_active_returns_all_installed(monkeypatch, tmp_path):
|
|
64
|
+
_isolate(monkeypatch, tmp_path)
|
|
65
|
+
(tmp_path / ".claude").mkdir()
|
|
66
|
+
(tmp_path / ".cursor").mkdir()
|
|
67
|
+
result = detect_agents()
|
|
68
|
+
assert set(result) == {"claude", "cursor"}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
from pathlib import Path
|
|
@@ -117,3 +117,67 @@ def test_init_no_about_when_raw_has_files(tmp_path: Path, monkeypatch):
|
|
|
117
117
|
result = runner.invoke(app, ["init"])
|
|
118
118
|
assert result.exit_code == 0, result.stdout
|
|
119
119
|
assert not (home / "raw" / "public" / "about.md").exists()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_init_auto_wires_detected_agent(tmp_path: Path, monkeypatch):
|
|
123
|
+
"""init detects active agent from env and writes its MCP config."""
|
|
124
|
+
home = tmp_path / "home"
|
|
125
|
+
project = tmp_path / "project"
|
|
126
|
+
project.mkdir()
|
|
127
|
+
fake_home = tmp_path / "fakehome"
|
|
128
|
+
fake_home.mkdir()
|
|
129
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
130
|
+
monkeypatch.setenv("HOME", str(fake_home))
|
|
131
|
+
monkeypatch.chdir(project)
|
|
132
|
+
monkeypatch.setenv("OPENCODE", "1")
|
|
133
|
+
monkeypatch.delenv("CLAUDECODE", raising=False)
|
|
134
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)
|
|
135
|
+
monkeypatch.setattr("lorekeep.integrations.detect.shutil.which", lambda _: None)
|
|
136
|
+
result = runner.invoke(app, ["init", "--yes"])
|
|
137
|
+
assert result.exit_code == 0, result.stdout
|
|
138
|
+
import json
|
|
139
|
+
mcp_path = project / "opencode.json"
|
|
140
|
+
assert mcp_path.exists(), f"opencode.json not written: {result.stdout}"
|
|
141
|
+
data = json.loads(mcp_path.read_text())
|
|
142
|
+
assert data["mcp"]["lorekeep"]["type"] == "local"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_init_auto_wires_installed_agents(tmp_path: Path, monkeypatch):
|
|
146
|
+
"""init in shell mode scans filesystem and wires all installed agents."""
|
|
147
|
+
home = tmp_path / "home"
|
|
148
|
+
fake_home = tmp_path / "fakehome"
|
|
149
|
+
project = tmp_path / "project"
|
|
150
|
+
project.mkdir()
|
|
151
|
+
(fake_home / ".claude").mkdir(parents=True)
|
|
152
|
+
(fake_home / ".config" / "opencode").mkdir(parents=True)
|
|
153
|
+
|
|
154
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
155
|
+
monkeypatch.setenv("HOME", str(fake_home))
|
|
156
|
+
monkeypatch.chdir(project)
|
|
157
|
+
monkeypatch.delenv("OPENCODE", raising=False)
|
|
158
|
+
monkeypatch.delenv("CLAUDECODE", raising=False)
|
|
159
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)
|
|
160
|
+
monkeypatch.setattr("lorekeep.integrations.detect.shutil.which", lambda _: None)
|
|
161
|
+
|
|
162
|
+
result = runner.invoke(app, ["init", "--yes"])
|
|
163
|
+
assert result.exit_code == 0, result.stdout
|
|
164
|
+
assert (project / ".mcp.json").exists()
|
|
165
|
+
assert (project / "opencode.json").exists()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_init_no_agents_detected_message(tmp_path: Path, monkeypatch):
|
|
169
|
+
"""init reports when no agents are detected."""
|
|
170
|
+
home = tmp_path / "home"
|
|
171
|
+
fake_home = tmp_path / "fakehome"
|
|
172
|
+
fake_home.mkdir()
|
|
173
|
+
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
174
|
+
monkeypatch.setenv("HOME", str(fake_home))
|
|
175
|
+
monkeypatch.chdir(tmp_path)
|
|
176
|
+
monkeypatch.delenv("OPENCODE", raising=False)
|
|
177
|
+
monkeypatch.delenv("CLAUDECODE", raising=False)
|
|
178
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)
|
|
179
|
+
monkeypatch.setattr("lorekeep.integrations.detect.shutil.which", lambda _: None)
|
|
180
|
+
|
|
181
|
+
result = runner.invoke(app, ["init", "--yes"])
|
|
182
|
+
assert result.exit_code == 0, result.stdout
|
|
183
|
+
assert "no coding agents detected" in result.stdout.lower()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from lorekeep.integrations.common import resolve_command, agent_memory_snippet
|
|
4
|
-
from lorekeep.integrations import claude_code, cursor, codex
|
|
4
|
+
from lorekeep.integrations import claude_code, cursor, codex, opencode
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def test_resolve_command_pypi():
|
|
@@ -66,3 +66,39 @@ def test_codex_rejects_newline_in_ns(tmp_path: Path):
|
|
|
66
66
|
import pytest
|
|
67
67
|
with pytest.raises(ValueError):
|
|
68
68
|
codex.write_config(tmp_path, "uvx", ["lorekeep"], ns="team\n[malicious]")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_opencode_writes_json(tmp_path: Path):
|
|
72
|
+
opencode.write_config(tmp_path, "uvx", ["lorekeep", "serve", "--transport", "stdio"],
|
|
73
|
+
ns="teams/backend")
|
|
74
|
+
data = json.loads((tmp_path / "opencode.json").read_text())
|
|
75
|
+
entry = data["mcp"]["lorekeep"]
|
|
76
|
+
assert entry["type"] == "local"
|
|
77
|
+
assert entry["command"] == ["uvx", "lorekeep", "serve", "--transport", "stdio"]
|
|
78
|
+
assert entry["enabled"] is True
|
|
79
|
+
assert entry["environment"]["LOREKEEP_NS"] == "teams/backend"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_opencode_no_ns(tmp_path: Path):
|
|
83
|
+
opencode.write_config(tmp_path, "uvx", ["lorekeep", "serve", "--transport", "stdio"], ns=None)
|
|
84
|
+
data = json.loads((tmp_path / "opencode.json").read_text())
|
|
85
|
+
entry = data["mcp"]["lorekeep"]
|
|
86
|
+
assert "environment" not in entry
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_opencode_idempotent(tmp_path: Path):
|
|
90
|
+
opencode.write_config(tmp_path, "uvx", ["lorekeep"], ns="team/a")
|
|
91
|
+
opencode.write_config(tmp_path, "uvx", ["lorekeep"], ns="team/b")
|
|
92
|
+
data = json.loads((tmp_path / "opencode.json").read_text())
|
|
93
|
+
assert data["mcp"]["lorekeep"]["environment"]["LOREKEEP_NS"] == "team/b"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_opencode_preserves_existing_keys(tmp_path: Path):
|
|
97
|
+
existing = {"$schema": "https://opencode.ai/config.json", "model": "anthropic/claude-sonnet-4-5", "mcp": {"other": {"type": "local", "command": ["foo"]}}}
|
|
98
|
+
(tmp_path / "opencode.json").write_text(json.dumps(existing))
|
|
99
|
+
opencode.write_config(tmp_path, "uvx", ["lorekeep", "serve", "--transport", "stdio"], ns=None)
|
|
100
|
+
data = json.loads((tmp_path / "opencode.json").read_text())
|
|
101
|
+
assert data["$schema"] == "https://opencode.ai/config.json"
|
|
102
|
+
assert data["model"] == "anthropic/claude-sonnet-4-5"
|
|
103
|
+
assert "other" in data["mcp"]
|
|
104
|
+
assert "lorekeep" in data["mcp"]
|
|
@@ -26,3 +26,17 @@ def test_mcp_add_codex_user(tmp_path: Path, monkeypatch):
|
|
|
26
26
|
assert result.exit_code == 0, result.stdout
|
|
27
27
|
text = (tmp_path / "config.toml").read_text()
|
|
28
28
|
assert "--from" in text and "git+https://github.com/x/lorekeep.git" in text
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_mcp_add_opencode_project(tmp_path: Path, monkeypatch):
|
|
32
|
+
monkeypatch.chdir(tmp_path)
|
|
33
|
+
monkeypatch.setenv("LOREKEEP_CONFIG", str(tmp_path / "config.yaml"))
|
|
34
|
+
(tmp_path / "config.yaml").write_text("install_source: local\n")
|
|
35
|
+
result = runner.invoke(app, ["mcp", "add", "--agent", "opencode", "--ns", "teams/backend"])
|
|
36
|
+
assert result.exit_code == 0, result.stdout
|
|
37
|
+
import json
|
|
38
|
+
data = json.loads((tmp_path / "opencode.json").read_text())
|
|
39
|
+
entry = data["mcp"]["lorekeep"]
|
|
40
|
+
assert entry["type"] == "local"
|
|
41
|
+
assert entry["command"] == ["lorekeep", "serve", "--transport", "stdio"]
|
|
42
|
+
assert entry["environment"]["LOREKEEP_NS"] == "teams/backend"
|
|
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.1.13 → lorekeep-0.2.0}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.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
|
{lorekeep-0.1.13 → lorekeep-0.2.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
|