cerebro-code-memory 0.1.0__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.
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/.claude-plugin/marketplace.json +1 -1
- cerebro_code_memory-0.2.0/.github/workflows/publish.yml +27 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/PKG-INFO +23 -5
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/README.md +19 -1
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/.claude-plugin/plugin.json +3 -3
- cerebro_code_memory-0.2.0/plugin/hooks/cerebro-first.py +96 -0
- cerebro_code_memory-0.2.0/plugin/hooks/cerebro-mark-used.py +20 -0
- cerebro_code_memory-0.2.0/plugin/hooks/cerebro-session-end.py +59 -0
- cerebro_code_memory-0.2.0/plugin/hooks/hooks.json +59 -0
- cerebro_code_memory-0.2.0/plugin/hooks/verify-edit.py +141 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/pyproject.toml +4 -4
- cerebro_code_memory-0.2.0/src/cerebro/apiroutes.py +88 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/cli.py +15 -1
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/server.py +22 -1
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/summarizer.py +26 -1
- cerebro_code_memory-0.2.0/tests/test_apiroutes.py +42 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_summarizer.py +24 -0
- cerebro_code_memory-0.1.0/plugin/hooks/hooks.json +0 -25
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/.gitignore +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/CLAUDE.md +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/LICENSE +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/USAGE.md +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/.mcp.json +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/agents/cerebro-architect.md +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/agents/cerebro-explorer.md +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/agents/cerebro-impact-analyst.md +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/agents/cerebro-implementer.md +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/agents/cerebro-memory-keeper.md +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/hooks/post_edit.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/hooks/session_start.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/skills/cerebro/SKILL.md +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/__init__.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/callgraph.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/config.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/db.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/docaudit.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/embeddings.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/gitsync.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/graph.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/indexer.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/insights.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/notes.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/summaries.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/tsconfig.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/views.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/src/cerebro/viz.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/conftest.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_callgraph.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_cli.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_db.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_docaudit.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_embeddings.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_gitsync.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_indexer.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_insights.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_notes.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_server.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_tsconfig.py +0 -0
- {cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/tests/test_viz.py +0 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"name": "cerebro",
|
|
10
10
|
"source": "./plugin",
|
|
11
11
|
"description": "MCP brain for codebases: caches structure + summaries so AI sessions query it instead of re-reading files. Bundles the MCP server, cerebro-first subagents, and session hooks.",
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.2.0"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI via Trusted Publishing (OIDC) whenever a GitHub Release is
|
|
4
|
+
# published. No API token needed — PyPI trusts this repo + workflow directly.
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # REQUIRED for PyPI Trusted Publishing (OIDC token exchange)
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.x"
|
|
20
|
+
|
|
21
|
+
- name: Build sdist + wheel
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade build
|
|
24
|
+
python -m build
|
|
25
|
+
|
|
26
|
+
- name: Publish to PyPI
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cerebro-code-memory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Persistent code-knowledge memory across AI chat sessions (MCP server)
|
|
5
|
-
Project-URL: Homepage, https://github.com/marcodavidd020/cerebro-
|
|
6
|
-
Project-URL: Repository, https://github.com/marcodavidd020/cerebro-
|
|
7
|
-
Project-URL: Issues, https://github.com/marcodavidd020/cerebro-
|
|
5
|
+
Project-URL: Homepage, https://github.com/marcodavidd020/cerebro-code-memory
|
|
6
|
+
Project-URL: Repository, https://github.com/marcodavidd020/cerebro-code-memory
|
|
7
|
+
Project-URL: Issues, https://github.com/marcodavidd020/cerebro-code-memory/issues
|
|
8
8
|
Author-email: Marco Toledo <huancacori@gmail.com>
|
|
9
9
|
License: MIT
|
|
10
10
|
License-File: LICENSE
|
|
@@ -72,12 +72,30 @@ Three layers of "traces", cheapest first:
|
|
|
72
72
|
| `cerebro_callers(name)` | Call sites of a symbol (who calls it, with enclosing fn + line). |
|
|
73
73
|
| `cerebro_calls(path)` | Internal functions a file calls (outgoing call edges). |
|
|
74
74
|
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
**One command** (published on PyPI) — add the MCP server to Claude Code:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
claude mcp add cerebro -- uvx cerebro-code-memory
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or install the full **Claude Code plugin** (MCP server + session hooks + cerebro-first subagents):
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
/plugin marketplace add marcodavidd020/cerebro-code-memory
|
|
87
|
+
/plugin install cerebro@cerebro
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Requires Python ≥ 3.10. Point Cerebro at a repo with `CEREBRO_ROOT=/path/to/repo`; it
|
|
91
|
+
also auto-detects the nearest ancestor `.cerebro/` brain (handy in monorepos).
|
|
92
|
+
|
|
75
93
|
## Quick start
|
|
76
94
|
|
|
77
95
|
One command onboards any repo — it indexes and prints the exact registration line:
|
|
78
96
|
|
|
79
97
|
```bash
|
|
80
|
-
uv tool install --from . cerebro # installs the `cerebro` command globally
|
|
98
|
+
uv tool install --from . cerebro # installs the `cerebro` command globally (dev)
|
|
81
99
|
cd /path/to/your/repo
|
|
82
100
|
cerebro setup --summarize --embed # index (+ warm summaries / semantic index), then prints next steps
|
|
83
101
|
```
|
|
@@ -50,12 +50,30 @@ Three layers of "traces", cheapest first:
|
|
|
50
50
|
| `cerebro_callers(name)` | Call sites of a symbol (who calls it, with enclosing fn + line). |
|
|
51
51
|
| `cerebro_calls(path)` | Internal functions a file calls (outgoing call edges). |
|
|
52
52
|
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
**One command** (published on PyPI) — add the MCP server to Claude Code:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
claude mcp add cerebro -- uvx cerebro-code-memory
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or install the full **Claude Code plugin** (MCP server + session hooks + cerebro-first subagents):
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
/plugin marketplace add marcodavidd020/cerebro-code-memory
|
|
65
|
+
/plugin install cerebro@cerebro
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Requires Python ≥ 3.10. Point Cerebro at a repo with `CEREBRO_ROOT=/path/to/repo`; it
|
|
69
|
+
also auto-detects the nearest ancestor `.cerebro/` brain (handy in monorepos).
|
|
70
|
+
|
|
53
71
|
## Quick start
|
|
54
72
|
|
|
55
73
|
One command onboards any repo — it indexes and prints the exact registration line:
|
|
56
74
|
|
|
57
75
|
```bash
|
|
58
|
-
uv tool install --from . cerebro # installs the `cerebro` command globally
|
|
76
|
+
uv tool install --from . cerebro # installs the `cerebro` command globally (dev)
|
|
59
77
|
cd /path/to/your/repo
|
|
60
78
|
cerebro setup --summarize --embed # index (+ warm summaries / semantic index), then prints next steps
|
|
61
79
|
```
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cerebro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Persistent code-knowledge memory: an MCP brain that caches a codebase's structure and summaries so AI sessions query it instead of re-reading files. Bundles the MCP server, cerebro-first subagents, and session hooks.",
|
|
5
|
-
"author": { "name": "Marco Toledo", "url": "https://github.com/marcodavidd020/cerebro-
|
|
6
|
-
"homepage": "https://github.com/marcodavidd020/cerebro-
|
|
5
|
+
"author": { "name": "Marco Toledo", "url": "https://github.com/marcodavidd020/cerebro-code-memory" },
|
|
6
|
+
"homepage": "https://github.com/marcodavidd020/cerebro-code-memory",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"mcpServers": "./.mcp.json",
|
|
9
9
|
"hooks": "./hooks/hooks.json"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse nudge: enforce 'cerebro-first' the deterministic way.
|
|
3
|
+
|
|
4
|
+
In a project that HAS a Cerebro brain, if the model reaches for raw code search
|
|
5
|
+
(grep/rg/find, or the native Grep/Glob tools) before consulting Cerebro, block the
|
|
6
|
+
call ONCE per session with a reminder to use cerebro_search / cerebro_get first
|
|
7
|
+
(far cheaper in tokens). Strictly bounded:
|
|
8
|
+
- only where a `.cerebro/brain.db` exists upward from cwd,
|
|
9
|
+
- at most ONE nudge per session,
|
|
10
|
+
- never nudges once Cerebro has been used this session,
|
|
11
|
+
- the command is never permanently blocked: re-running it passes.
|
|
12
|
+
|
|
13
|
+
Best-effort: any error → allow silently (a guardrail you trust beats one that
|
|
14
|
+
gets in the way).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
STATE_DIR = Path(tempfile.gettempdir()) / "cerebro-hook-state"
|
|
26
|
+
|
|
27
|
+
# Raw code-search tools Cerebro can usually answer cheaper. Kept narrow on purpose
|
|
28
|
+
# (no cat/ls/head — too generic) to avoid noisy false nudges.
|
|
29
|
+
_EXPLORE = re.compile(r"\b(grep|rg|find|ack|ag)\b")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_brain(start: str):
|
|
33
|
+
try:
|
|
34
|
+
p = Path(start).resolve()
|
|
35
|
+
except Exception:
|
|
36
|
+
return None
|
|
37
|
+
p = p if p.is_dir() else p.parent
|
|
38
|
+
for d in (p, *p.parents):
|
|
39
|
+
if (d / ".cerebro" / "brain.db").exists():
|
|
40
|
+
return d
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> None:
|
|
45
|
+
try:
|
|
46
|
+
data = json.load(sys.stdin)
|
|
47
|
+
except Exception:
|
|
48
|
+
sys.exit(0)
|
|
49
|
+
|
|
50
|
+
tool = data.get("tool_name", "")
|
|
51
|
+
ti = data.get("tool_input", {}) or {}
|
|
52
|
+
sid = data.get("session_id") or "nosession"
|
|
53
|
+
cwd = data.get("cwd") or os.getcwd()
|
|
54
|
+
|
|
55
|
+
# Only act where Cerebro is actually set up.
|
|
56
|
+
if find_brain(cwd) is None:
|
|
57
|
+
sys.exit(0)
|
|
58
|
+
|
|
59
|
+
# Is this raw exploration?
|
|
60
|
+
if tool in ("Grep", "Glob"):
|
|
61
|
+
exploratory = True
|
|
62
|
+
elif tool == "Bash":
|
|
63
|
+
exploratory = bool(_EXPLORE.search(ti.get("command", "") or ""))
|
|
64
|
+
else:
|
|
65
|
+
exploratory = False
|
|
66
|
+
if not exploratory:
|
|
67
|
+
sys.exit(0)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
except Exception:
|
|
72
|
+
sys.exit(0)
|
|
73
|
+
used = STATE_DIR / f"{sid}.cerebro-used"
|
|
74
|
+
nudged = STATE_DIR / f"{sid}.nudged"
|
|
75
|
+
|
|
76
|
+
# Already used Cerebro, or already nudged once → let it through.
|
|
77
|
+
if used.exists() or nudged.exists():
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
nudged.write_text("1")
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
sys.stderr.write(
|
|
86
|
+
"💡 This project has a Cerebro brain — querying it is far cheaper than raw "
|
|
87
|
+
"search.\nBefore grep/find, try:\n"
|
|
88
|
+
" • cerebro_search(\"<what you're looking for>\") → locates code at path:line\n"
|
|
89
|
+
" • cerebro_get(\"<path>\") → a file's summary + symbols + deps, without reading it\n"
|
|
90
|
+
"One-time reminder. If Cerebro doesn't have what you need, just re-run your command.\n"
|
|
91
|
+
)
|
|
92
|
+
sys.exit(2)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse: record that Cerebro was used this session, so the cerebro-first
|
|
3
|
+
nudge (cerebro-first.py) stays quiet for the rest of the session. Best-effort."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
STATE_DIR = Path(tempfile.gettempdir()) / "cerebro-hook-state"
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
data = json.load(sys.stdin)
|
|
15
|
+
sid = data.get("session_id") or "nosession"
|
|
16
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
(STATE_DIR / f"{sid}.cerebro-used").write_text("1")
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
sys.exit(0)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SessionEnd hook: auto-record.
|
|
3
|
+
|
|
4
|
+
When CEREBRO_AUTORECORD=N (a positive int) is set, re-summarize up to N files whose
|
|
5
|
+
cached summary went STALE during the session — so the NEXT session reuses fresh
|
|
6
|
+
traces instead of re-reading the files you changed. Detached (never blocks exit)
|
|
7
|
+
and OFF by default (it spends a little via `claude -p`; set the env var to opt in).
|
|
8
|
+
Mirrors the spawn pattern of session_start.py's autosummarize.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def find_brain_root(start: str):
|
|
18
|
+
p = Path(start).resolve()
|
|
19
|
+
p = p if p.is_dir() else p.parent
|
|
20
|
+
for d in (p, *p.parents):
|
|
21
|
+
if (d / ".cerebro" / "brain.db").exists():
|
|
22
|
+
return d
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
n = os.environ.get("CEREBRO_AUTORECORD", "").strip()
|
|
28
|
+
if not (n.isdigit() and int(n) > 0):
|
|
29
|
+
return # opt-in: set CEREBRO_AUTORECORD=N to enable
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
data = json.load(sys.stdin)
|
|
33
|
+
except Exception:
|
|
34
|
+
data = {}
|
|
35
|
+
cwd = data.get("cwd") or os.getcwd()
|
|
36
|
+
root = find_brain_root(cwd)
|
|
37
|
+
if root is None:
|
|
38
|
+
return # not a Cerebro project
|
|
39
|
+
|
|
40
|
+
env = {**os.environ, "CEREBRO_ROOT": str(root)}
|
|
41
|
+
home = os.environ.get("CEREBRO_HOME")
|
|
42
|
+
uv = os.environ.get("CEREBRO_UV", "uv")
|
|
43
|
+
cmd = (
|
|
44
|
+
[uv, "run", "--directory", home, "cerebro", "summarize", "--stale", "--limit", n]
|
|
45
|
+
if home
|
|
46
|
+
else ["cerebro", "summarize", "--stale", "--limit", n]
|
|
47
|
+
)
|
|
48
|
+
try:
|
|
49
|
+
subprocess.Popen(
|
|
50
|
+
cmd, env=env,
|
|
51
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
|
|
52
|
+
start_new_session=True,
|
|
53
|
+
)
|
|
54
|
+
except Exception:
|
|
55
|
+
pass # best-effort; never block session exit
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/session_start.py"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"SessionEnd": [
|
|
14
|
+
{
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/cerebro-session-end.py"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"PreToolUse": [
|
|
24
|
+
{
|
|
25
|
+
"matcher": "Bash|Grep|Glob",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/cerebro-first.py"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"PostToolUse": [
|
|
35
|
+
{
|
|
36
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/post_edit.py"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/verify-edit.py"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"matcher": "mcp__cerebro__.*",
|
|
50
|
+
"hooks": [
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/cerebro-mark-used.py"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Hook determinista anti-alucinación.
|
|
3
|
+
|
|
4
|
+
Tras cada Edit/Write/MultiEdit, verifica el archivo editado con las herramientas
|
|
5
|
+
REALES disponibles (sintaxis + análisis). No puede alucinar: o el archivo pasa, o
|
|
6
|
+
no. Si encuentra errores reales, escribe el detalle en stderr y sale con código 2,
|
|
7
|
+
así Claude recibe el error y lo corrige (la edición YA se aplicó; esto no la borra).
|
|
8
|
+
|
|
9
|
+
Filosofía: NUNCA bloquear por falta de tooling. Si una herramienta no está
|
|
10
|
+
instalada o falla por motivos de entorno (no por el código), se salta en silencio.
|
|
11
|
+
Un guardarraíl en el que se confía y queda encendido vale más que uno potente que
|
|
12
|
+
se termina apagando por ruidoso.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import glob
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
# Ubicaciones comunes de herramientas que el PATH con que Claude Code lanza los
|
|
24
|
+
# hooks puede NO incluir (p.ej. ~/flutter/bin lo agrega .zshrc; node vive en nvm).
|
|
25
|
+
# Sin esto, el hook se saltaría en silencio dart/node/ruff = falsa seguridad.
|
|
26
|
+
_EXTRA_PATHS = [
|
|
27
|
+
os.path.expanduser("~/flutter/bin"),
|
|
28
|
+
os.path.expanduser("~/fvm/default/bin"),
|
|
29
|
+
os.path.expanduser("~/.pub-cache/bin"),
|
|
30
|
+
os.path.expanduser("~/.local/bin"), # uv / uvx
|
|
31
|
+
"/opt/homebrew/bin",
|
|
32
|
+
"/usr/local/bin",
|
|
33
|
+
"/usr/bin",
|
|
34
|
+
"/bin",
|
|
35
|
+
] + sorted(glob.glob(os.path.expanduser("~/.nvm/versions/node/*/bin")), reverse=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _enriched_path():
|
|
39
|
+
extra = [p for p in _EXTRA_PATHS if os.path.isdir(p)]
|
|
40
|
+
return os.pathsep.join(extra + [os.environ.get("PATH", "")])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run(cmd, cwd=None, timeout=90):
|
|
44
|
+
"""Devuelve (returncode, salida). returncode None = herramienta ausente/timeout.
|
|
45
|
+
Resuelve cmd[0] a ruta absoluta vía un PATH enriquecido para no depender del
|
|
46
|
+
PATH heredado (que al ejecutar hooks suele venir mínimo)."""
|
|
47
|
+
path = _enriched_path()
|
|
48
|
+
exe = shutil.which(cmd[0], path=path)
|
|
49
|
+
if exe is None:
|
|
50
|
+
return None, "" # herramienta ausente -> saltar (nunca bloquear por tooling)
|
|
51
|
+
env = dict(os.environ)
|
|
52
|
+
env["PATH"] = path
|
|
53
|
+
try:
|
|
54
|
+
p = subprocess.run([exe] + cmd[1:], cwd=cwd, capture_output=True,
|
|
55
|
+
text=True, timeout=timeout, env=env)
|
|
56
|
+
return p.returncode, (p.stdout + p.stderr).strip()
|
|
57
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
58
|
+
return None, ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_up(start, name):
|
|
62
|
+
d = os.path.abspath(start)
|
|
63
|
+
while True:
|
|
64
|
+
if os.path.exists(os.path.join(d, name)):
|
|
65
|
+
return d
|
|
66
|
+
parent = os.path.dirname(d)
|
|
67
|
+
if parent == d:
|
|
68
|
+
return None
|
|
69
|
+
d = parent
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def check(path):
|
|
73
|
+
"""Devuelve lista de mensajes de error (vacía = ok)."""
|
|
74
|
+
ext = os.path.splitext(path)[1].lower()
|
|
75
|
+
d = os.path.dirname(path)
|
|
76
|
+
errors = []
|
|
77
|
+
|
|
78
|
+
if ext in (".py", ".pyi"):
|
|
79
|
+
# 1) Sintaxis — stdlib, siempre disponible, cero falsos positivos.
|
|
80
|
+
rc, out = run([sys.executable, "-m", "py_compile", path])
|
|
81
|
+
if rc not in (0, None):
|
|
82
|
+
errors.append("SyntaxError (py_compile):\n" + out)
|
|
83
|
+
else:
|
|
84
|
+
# 2) Nombres indefinidos / imports rotos (lo que delata alucinaciones).
|
|
85
|
+
# ruff vía uvx: rc==1 => hallazgos; rc None/2 => uv ausente o problema
|
|
86
|
+
# de entorno (run() resuelve uvx por PATH enriquecido; si falta, salta).
|
|
87
|
+
rc, out = run([
|
|
88
|
+
"uvx", "ruff", "check", "--quiet", "--output-format", "concise",
|
|
89
|
+
"--select", "F821,F811,F823", path,
|
|
90
|
+
], timeout=120)
|
|
91
|
+
if rc == 1 and ".py:" in out:
|
|
92
|
+
errors.append("ruff (nombres indefinidos / imports):\n" + out)
|
|
93
|
+
|
|
94
|
+
elif ext in (".js", ".jsx", ".mjs", ".cjs"):
|
|
95
|
+
rc, out = run(["node", "--check", path])
|
|
96
|
+
if rc not in (0, None):
|
|
97
|
+
errors.append("SyntaxError (node --check):\n" + out)
|
|
98
|
+
|
|
99
|
+
elif ext in (".ts", ".tsx"):
|
|
100
|
+
root = find_up(d, "node_modules")
|
|
101
|
+
eslint = os.path.join(root, "node_modules", ".bin", "eslint") if root else None
|
|
102
|
+
if eslint and os.path.exists(eslint):
|
|
103
|
+
rc, out = run([eslint, "--no-error-on-unmatched-pattern", path], cwd=root, timeout=120)
|
|
104
|
+
if rc == 1 and out:
|
|
105
|
+
errors.append("eslint:\n" + out)
|
|
106
|
+
|
|
107
|
+
elif ext == ".dart":
|
|
108
|
+
# --no-fatal-warnings: rc!=0 solo ante ERRORES reales (infos/warnings no bloquean;
|
|
109
|
+
# los infos ya son no-fatales por defecto). "Usage:" => error de CLI, no de código: saltar.
|
|
110
|
+
rc, out = run(["dart", "analyze", "--no-fatal-warnings", path], timeout=150)
|
|
111
|
+
if rc not in (0, None) and out and "Usage:" not in out:
|
|
112
|
+
errors.append("dart analyze:\n" + out)
|
|
113
|
+
|
|
114
|
+
return errors
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main():
|
|
118
|
+
try:
|
|
119
|
+
data = json.load(sys.stdin)
|
|
120
|
+
except Exception:
|
|
121
|
+
sys.exit(0)
|
|
122
|
+
|
|
123
|
+
ti = data.get("tool_input", {}) or {}
|
|
124
|
+
path = ti.get("file_path") or ti.get("path")
|
|
125
|
+
if not path or not os.path.isfile(path):
|
|
126
|
+
sys.exit(0)
|
|
127
|
+
|
|
128
|
+
errors = check(path)
|
|
129
|
+
if errors:
|
|
130
|
+
sys.stderr.write(
|
|
131
|
+
"⛔ Verificación determinista falló en " + path + ":\n\n"
|
|
132
|
+
+ "\n\n".join(errors)
|
|
133
|
+
+ "\n\nEstos son errores reales del código recién escrito. "
|
|
134
|
+
"Corrígelos antes de continuar.\n"
|
|
135
|
+
)
|
|
136
|
+
sys.exit(2)
|
|
137
|
+
sys.exit(0)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cerebro-code-memory"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Persistent code-knowledge memory across AI chat sessions (MCP server)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -28,9 +28,9 @@ cerebro-graph = "cerebro.viz:graph_main"
|
|
|
28
28
|
cerebro-export-obsidian = "cerebro.viz:obsidian_main"
|
|
29
29
|
|
|
30
30
|
[project.urls]
|
|
31
|
-
Homepage = "https://github.com/marcodavidd020/cerebro-
|
|
32
|
-
Repository = "https://github.com/marcodavidd020/cerebro-
|
|
33
|
-
Issues = "https://github.com/marcodavidd020/cerebro-
|
|
31
|
+
Homepage = "https://github.com/marcodavidd020/cerebro-code-memory"
|
|
32
|
+
Repository = "https://github.com/marcodavidd020/cerebro-code-memory"
|
|
33
|
+
Issues = "https://github.com/marcodavidd020/cerebro-code-memory/issues"
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
36
36
|
semantic = ["model2vec>=0.3", "numpy>=1.26"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Backend HTTP endpoint extraction — slice 1 of API-call tracing (front↔back).
|
|
2
|
+
|
|
3
|
+
Cerebro's import/call graphs miss the network boundary: a storefront calling a
|
|
4
|
+
backend endpoint over HTTP is a real dependency invisible to `import` edges. This
|
|
5
|
+
extracts the BACKEND side — the routes a server exposes — so a session can answer
|
|
6
|
+
"where is POST /carts/lines handled?" cheaply, and so a later slice can match
|
|
7
|
+
frontend HTTP calls to these routes (the actual HTTP_CALLS edges).
|
|
8
|
+
|
|
9
|
+
v1 targets NestJS controllers (the Fenix backend): `@Controller('base')` plus the
|
|
10
|
+
method decorators `@Get/@Post/@Put/@Patch/@Delete/@All`. Best-effort line scan —
|
|
11
|
+
robust for the conventional one-controller-per-file layout. Read-only; computed on
|
|
12
|
+
demand from `*.controller.ts` files (a small subset), so no schema change.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
_CONTROLLER = re.compile(r"@Controller\(\s*['\"`]([^'\"`]*)['\"`]")
|
|
19
|
+
_CONTROLLER_BARE = re.compile(r"@Controller\(\s*\)")
|
|
20
|
+
_METHOD = re.compile(r"@(Get|Post|Put|Patch|Delete|All)\(\s*(?:['\"`]([^'\"`]*)['\"`])?")
|
|
21
|
+
# A method declaration line: optional modifiers then `name(`. Used to attach a
|
|
22
|
+
# handler name to the decorator above it.
|
|
23
|
+
_HANDLER = re.compile(r"^\s*(?:public\s+|private\s+|protected\s+|async\s+|static\s+)*"
|
|
24
|
+
r"([A-Za-z_]\w*)\s*\(")
|
|
25
|
+
|
|
26
|
+
_HTTP = ("GET", "POST", "PUT", "PATCH", "DELETE", "ALL")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _join(base: str | None, path: str | None) -> str:
|
|
30
|
+
parts = [p.strip("/") for p in (base or "", path or "") if p and p.strip("/")]
|
|
31
|
+
return "/" + "/".join(parts)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def extract_file(rel: str, text: str) -> list[dict]:
|
|
35
|
+
"""Endpoints declared in one controller source. Each: method, path, file,
|
|
36
|
+
line, handler."""
|
|
37
|
+
base: str | None = None
|
|
38
|
+
out: list[dict] = []
|
|
39
|
+
lines = text.splitlines()
|
|
40
|
+
for i, line in enumerate(lines):
|
|
41
|
+
mc = _CONTROLLER.search(line)
|
|
42
|
+
if mc:
|
|
43
|
+
base = mc.group(1)
|
|
44
|
+
continue
|
|
45
|
+
if _CONTROLLER_BARE.search(line):
|
|
46
|
+
base = ""
|
|
47
|
+
continue
|
|
48
|
+
mm = _METHOD.search(line)
|
|
49
|
+
if not mm:
|
|
50
|
+
continue
|
|
51
|
+
method = mm.group(1).upper()
|
|
52
|
+
path = mm.group(2) or ""
|
|
53
|
+
handler = None
|
|
54
|
+
for j in range(i + 1, min(i + 6, len(lines))):
|
|
55
|
+
stripped = lines[j].lstrip()
|
|
56
|
+
if stripped.startswith("@"):
|
|
57
|
+
continue # another decorator (e.g. @UseGuards) — keep looking
|
|
58
|
+
mh = _HANDLER.match(lines[j])
|
|
59
|
+
if mh:
|
|
60
|
+
handler = mh.group(1)
|
|
61
|
+
break
|
|
62
|
+
out.append({"method": method, "path": _join(base, path),
|
|
63
|
+
"file": rel, "line": i + 1, "handler": handler})
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find(config, conn, query: str = "") -> list[dict]:
|
|
68
|
+
"""All backend endpoints (optionally filtered by `query`), across the indexed
|
|
69
|
+
NestJS controllers. Sorted by path then method."""
|
|
70
|
+
rows = conn.execute(
|
|
71
|
+
"SELECT path FROM files WHERE path LIKE '%.controller.ts' ORDER BY path"
|
|
72
|
+
).fetchall()
|
|
73
|
+
eps: list[dict] = []
|
|
74
|
+
for r in rows:
|
|
75
|
+
rel = r["path"]
|
|
76
|
+
try:
|
|
77
|
+
text = (config.root / rel).read_text(encoding="utf-8", errors="ignore")
|
|
78
|
+
except OSError:
|
|
79
|
+
continue
|
|
80
|
+
eps.extend(extract_file(rel, text))
|
|
81
|
+
if query:
|
|
82
|
+
q = query.lower()
|
|
83
|
+
eps = [
|
|
84
|
+
e for e in eps
|
|
85
|
+
if q in f"{e['method']} {e['path']} {e['handler'] or ''} {e['file']}".lower()
|
|
86
|
+
]
|
|
87
|
+
eps.sort(key=lambda e: (e["path"], e["method"]))
|
|
88
|
+
return eps
|
|
@@ -93,6 +93,11 @@ def cmd_calls(args):
|
|
|
93
93
|
print(_bind_server(config, conn).cerebro_calls(args.path))
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def cmd_endpoints(args):
|
|
97
|
+
config, conn = _ctx()
|
|
98
|
+
print(_bind_server(config, conn).cerebro_endpoints(" ".join(args.query)))
|
|
99
|
+
|
|
100
|
+
|
|
96
101
|
def cmd_recall(args):
|
|
97
102
|
from . import views
|
|
98
103
|
config, conn = _ctx()
|
|
@@ -153,7 +158,13 @@ def cmd_obsidian(args):
|
|
|
153
158
|
def cmd_summarize(args):
|
|
154
159
|
from . import summarizer
|
|
155
160
|
config, conn = _ctx()
|
|
156
|
-
|
|
161
|
+
if getattr(args, "stale", False):
|
|
162
|
+
rels = summarizer.select_stale(conn, args.limit, args.prefix)
|
|
163
|
+
seen = set(rels)
|
|
164
|
+
rels += [r for r in summarizer.select_central_missing(conn, args.limit - len(rels), args.prefix)
|
|
165
|
+
if r not in seen]
|
|
166
|
+
else:
|
|
167
|
+
rels = summarizer.select_central_missing(conn, args.limit, args.prefix)
|
|
157
168
|
print(json.dumps(summarizer.run(config, conn, rels, workers=args.workers)))
|
|
158
169
|
|
|
159
170
|
|
|
@@ -288,6 +299,7 @@ _COMMANDS = {
|
|
|
288
299
|
"embed": cmd_embed, "impact": cmd_impact, "cycles": cmd_cycles, "orphans": cmd_orphans,
|
|
289
300
|
"dead-symbols": cmd_orphans_symbols,
|
|
290
301
|
"callers": cmd_callers, "calls": cmd_calls, "recall": cmd_recall,
|
|
302
|
+
"endpoints": cmd_endpoints,
|
|
291
303
|
}
|
|
292
304
|
|
|
293
305
|
|
|
@@ -320,6 +332,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
320
332
|
s = sub.add_parser("obsidian", help="export an Obsidian vault"); s.add_argument("-o", "--out")
|
|
321
333
|
s = sub.add_parser("summarize", help="warm summaries via claude -p")
|
|
322
334
|
s.add_argument("--limit", type=int, default=20); s.add_argument("--prefix"); s.add_argument("--workers", type=int, default=4)
|
|
335
|
+
s.add_argument("--stale", action="store_true", help="also re-summarize stale summaries (edited files)")
|
|
323
336
|
sub.add_parser("embed", help="build the semantic index (needs --extra semantic)")
|
|
324
337
|
s = sub.add_parser("impact", help="transitive blast radius of a file"); s.add_argument("path")
|
|
325
338
|
sub.add_parser("cycles", help="circular-import groups")
|
|
@@ -328,6 +341,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
328
341
|
s.add_argument("--prefix", default="")
|
|
329
342
|
s = sub.add_parser("callers", help="call sites of a symbol"); s.add_argument("name")
|
|
330
343
|
s = sub.add_parser("calls", help="internal calls a file makes"); s.add_argument("path")
|
|
344
|
+
s = sub.add_parser("endpoints", help="backend HTTP endpoints (NestJS routes)"); s.add_argument("query", nargs="*")
|
|
331
345
|
s = sub.add_parser("recall", help="recall recorded decisions"); s.add_argument("query", nargs="*")
|
|
332
346
|
return p
|
|
333
347
|
|
|
@@ -13,7 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
from mcp.server.fastmcp import FastMCP
|
|
14
14
|
|
|
15
15
|
from . import config as cfg
|
|
16
|
-
from . import callgraph, db, embeddings, gitsync, graph, indexer, insights, notes, summaries, views
|
|
16
|
+
from . import apiroutes, callgraph, db, embeddings, gitsync, graph, indexer, insights, notes, summaries, views
|
|
17
17
|
|
|
18
18
|
mcp = FastMCP("cerebro")
|
|
19
19
|
|
|
@@ -357,6 +357,27 @@ def cerebro_dead_symbols(prefix: str = "") -> str:
|
|
|
357
357
|
return "\n".join(out)
|
|
358
358
|
|
|
359
359
|
|
|
360
|
+
@mcp.tool()
|
|
361
|
+
def cerebro_endpoints(query: str = "") -> str:
|
|
362
|
+
"""Backend HTTP endpoints (NestJS routes) the project exposes — the front↔back
|
|
363
|
+
boundary that `import` edges miss. Search by path / method / handler (e.g.
|
|
364
|
+
'POST carts', 'promotions', 'findActive') to answer 'where is this endpoint
|
|
365
|
+
handled?' without grepping decorators."""
|
|
366
|
+
config, conn = _ctx()
|
|
367
|
+
eps = apiroutes.find(config, conn, query)
|
|
368
|
+
if not eps:
|
|
369
|
+
scope = f" matching '{query}'" if query else ""
|
|
370
|
+
return f"No backend endpoints{scope} found (scans *.controller.ts NestJS routes)."
|
|
371
|
+
cap = 60
|
|
372
|
+
out = [f"{len(eps)} endpoint(s)" + (f" matching '{query}'" if query else "") + ":"]
|
|
373
|
+
for e in eps[:cap]:
|
|
374
|
+
h = f" ({e['handler']})" if e["handler"] else ""
|
|
375
|
+
out.append(f" {e['method']:6} {e['path']} → {e['file']}:{e['line']}{h}")
|
|
376
|
+
if len(eps) > cap:
|
|
377
|
+
out.append(f" … (+{len(eps) - cap} more)")
|
|
378
|
+
return "\n".join(out)
|
|
379
|
+
|
|
380
|
+
|
|
360
381
|
def map_main():
|
|
361
382
|
"""`cerebro-map` entry point: print the project map (read-only). Used by the
|
|
362
383
|
Claude Code session-start hook to inject the overview into a new session."""
|
|
@@ -67,6 +67,23 @@ def select_central_missing(conn, limit: int, prefix: str | None = None) -> list[
|
|
|
67
67
|
return out
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def select_stale(conn, limit: int, prefix: str | None = None) -> list[str]:
|
|
71
|
+
"""Files whose cached summary went STALE (source changed since it was written).
|
|
72
|
+
Re-warming these is the 'auto-record' path: edits during a session leave their
|
|
73
|
+
summaries outdated, and select_central_missing skips them (they HAVE a summary).
|
|
74
|
+
Bounded by `limit`."""
|
|
75
|
+
out = []
|
|
76
|
+
for path in summaries.stale_summaries(conn):
|
|
77
|
+
if cfg.Config.lang_for(path) is None:
|
|
78
|
+
continue
|
|
79
|
+
if prefix and not path.startswith(prefix):
|
|
80
|
+
continue
|
|
81
|
+
out.append(path)
|
|
82
|
+
if len(out) >= limit:
|
|
83
|
+
break
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
|
|
70
87
|
def run(config, conn, rels: list[str], model: str = DEFAULT_MODEL, workers: int = 4) -> dict:
|
|
71
88
|
"""Summarize files in parallel (claude -p subprocesses), then record serially
|
|
72
89
|
(one sqlite writer). Returns a count of what was produced."""
|
|
@@ -91,11 +108,19 @@ def main(): # `cerebro-summarize` entry point
|
|
|
91
108
|
ap.add_argument("--model", default=DEFAULT_MODEL)
|
|
92
109
|
ap.add_argument("--prefix", default=None, help="only files under this path prefix")
|
|
93
110
|
ap.add_argument("--workers", type=int, default=4)
|
|
111
|
+
ap.add_argument("--stale", action="store_true",
|
|
112
|
+
help="re-summarize summaries gone stale (file changed), then fill with missing")
|
|
94
113
|
args = ap.parse_args()
|
|
95
114
|
|
|
96
115
|
config = cfg.Config.load()
|
|
97
116
|
conn = db.connect(config.db_path)
|
|
98
|
-
|
|
117
|
+
if args.stale:
|
|
118
|
+
rels = select_stale(conn, args.limit, args.prefix)
|
|
119
|
+
seen = set(rels)
|
|
120
|
+
rels += [r for r in select_central_missing(conn, args.limit - len(rels), args.prefix)
|
|
121
|
+
if r not in seen]
|
|
122
|
+
else:
|
|
123
|
+
rels = select_central_missing(conn, args.limit, args.prefix)
|
|
99
124
|
if not rels:
|
|
100
125
|
print(json.dumps({"summarized": 0, "note": "nothing missing in scope"}))
|
|
101
126
|
return
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from cerebro import apiroutes
|
|
2
|
+
|
|
3
|
+
SAMPLE = """
|
|
4
|
+
@Controller('carts')
|
|
5
|
+
export class CartController {
|
|
6
|
+
@Get()
|
|
7
|
+
findAll() {}
|
|
8
|
+
|
|
9
|
+
@Get(':id')
|
|
10
|
+
findOne(@Param('id') id: string) {}
|
|
11
|
+
|
|
12
|
+
@Post('lines')
|
|
13
|
+
@UseGuards(AuthGuard)
|
|
14
|
+
async addLine(@Body() dto: Dto) {}
|
|
15
|
+
|
|
16
|
+
@Delete(':id')
|
|
17
|
+
remove(@Param('id') id: string) {}
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_extract_nestjs_routes():
|
|
23
|
+
eps = apiroutes.extract_file("cart.controller.ts", SAMPLE)
|
|
24
|
+
by = {(e["method"], e["path"]): e for e in eps}
|
|
25
|
+
assert ("GET", "/carts") in by
|
|
26
|
+
assert ("GET", "/carts/:id") in by
|
|
27
|
+
assert ("POST", "/carts/lines") in by
|
|
28
|
+
assert ("DELETE", "/carts/:id") in by
|
|
29
|
+
# handler attaches to the method even past an intervening decorator
|
|
30
|
+
assert by[("POST", "/carts/lines")]["handler"] == "addLine"
|
|
31
|
+
assert by[("GET", "/carts")]["handler"] == "findAll"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_bare_controller_and_empty_path():
|
|
35
|
+
eps = apiroutes.extract_file("x.controller.ts", "@Controller()\nclass X {\n @Get()\n ping() {}\n}\n")
|
|
36
|
+
assert eps == [
|
|
37
|
+
{"method": "GET", "path": "/", "file": "x.controller.ts", "line": 3, "handler": "ping"}
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_no_controller_no_routes():
|
|
42
|
+
assert apiroutes.extract_file("plain.ts", "export const x = 1;\n") == []
|
|
@@ -39,3 +39,27 @@ def test_run_records_generated_summaries(tmp_path, project, monkeypatch):
|
|
|
39
39
|
res = summarizer.run(config, conn, ["a.py"], workers=1)
|
|
40
40
|
assert res["summarized"] == 1
|
|
41
41
|
assert summaries.get(conn, "a.py")["summary_en"] == "summary of a.py"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_select_stale_finds_changed_summaries(tmp_path, project):
|
|
45
|
+
"""A summary whose source file changed (edit + reindex) surfaces as a stale
|
|
46
|
+
candidate, so the SessionEnd auto-record can re-warm it."""
|
|
47
|
+
config, conn = project
|
|
48
|
+
write(tmp_path, "x.py", "def a():\n return 1\n")
|
|
49
|
+
indexer.reindex(config, conn)
|
|
50
|
+
summaries.record(conn, "x.py", "does a")
|
|
51
|
+
assert summarizer.select_stale(conn, 10) == [] # fresh — not stale
|
|
52
|
+
|
|
53
|
+
write(tmp_path, "x.py", "def a():\n return 2 # changed\n")
|
|
54
|
+
indexer.reindex(config, conn)
|
|
55
|
+
assert summarizer.select_stale(conn, 10) == ["x.py"] # now stale
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_select_stale_skips_non_source(tmp_path, project):
|
|
59
|
+
config, conn = project
|
|
60
|
+
write(tmp_path, "README.md", "# hi\n")
|
|
61
|
+
indexer.reindex(config, conn)
|
|
62
|
+
summaries.record(conn, "README.md", "readme")
|
|
63
|
+
write(tmp_path, "README.md", "# hi changed\n")
|
|
64
|
+
indexer.reindex(config, conn)
|
|
65
|
+
assert summarizer.select_stale(conn, 10) == [] # .md has no language -> skipped
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"SessionStart": [
|
|
4
|
-
{
|
|
5
|
-
"hooks": [
|
|
6
|
-
{
|
|
7
|
-
"type": "command",
|
|
8
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/session_start.py"
|
|
9
|
-
}
|
|
10
|
-
]
|
|
11
|
-
}
|
|
12
|
-
],
|
|
13
|
-
"PostToolUse": [
|
|
14
|
-
{
|
|
15
|
-
"matcher": "Edit|Write|MultiEdit",
|
|
16
|
-
"hooks": [
|
|
17
|
-
{
|
|
18
|
-
"type": "command",
|
|
19
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/post_edit.py"
|
|
20
|
-
}
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/agents/cerebro-impact-analyst.md
RENAMED
|
File without changes
|
{cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/agents/cerebro-implementer.md
RENAMED
|
File without changes
|
{cerebro_code_memory-0.1.0 → cerebro_code_memory-0.2.0}/plugin/agents/cerebro-memory-keeper.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
|