cerebro-code-memory 0.1.0__py3-none-any.whl

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/viz.py ADDED
@@ -0,0 +1,374 @@
1
+ """Visualizations built from the brain: a self-contained interactive HTML
2
+ dependency graph, and an Obsidian vault (one note per file, imports as [[links]]).
3
+
4
+ Both read only the existing index — no new analysis. The HTML graph defaults to the
5
+ most central files so it stays responsive on large repos; the Obsidian export writes
6
+ every indexed file so its graph view is complete.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+ from . import config as cfg
14
+ from . import db
15
+ from . import graph as graphmod
16
+
17
+
18
+ def _select(conn, limit: int | None, prefix: str | None):
19
+ from . import insights
20
+
21
+ ranked = graphmod.rank(conn) # [(path, score)] desc
22
+ scores = dict(ranked)
23
+ in_scope = [p for p, _ in ranked if not prefix or p.startswith(prefix)]
24
+ files = in_scope[:limit] if limit else list(in_scope)
25
+ keep = set(files)
26
+ # Orphans (in_degree 0) rank lowest by centrality, so the top-N cut drops every
27
+ # one of them — leaving the "Orphans" overlay with nothing to highlight. Pull
28
+ # the overlay nodes (orphans + cycle members, within scope) in explicitly so
29
+ # both buttons actually do something.
30
+ scope = set(in_scope)
31
+ highlight = (set(insights.orphans(conn, prefix)["dead"]) | insights.cycle_members(conn)) & scope
32
+ files += [f for f in in_scope if f in highlight and f not in keep]
33
+ keep = set(files)
34
+ edges = [
35
+ (r["src_path"], r["dst_path"])
36
+ for r in conn.execute("SELECT src_path, dst_path FROM edges")
37
+ if r["src_path"] in keep and r["dst_path"] in keep
38
+ ]
39
+ return files, scores, edges
40
+
41
+
42
+ def graph_html(conn, limit: int = 400, prefix: str | None = None) -> str:
43
+ from . import insights
44
+
45
+ files, scores, edges = _select(conn, limit, prefix)
46
+ summ = {
47
+ r["path"]: r["summary_en"]
48
+ for r in conn.execute("SELECT path, summary_en FROM summaries")
49
+ }
50
+ orphan_set = set(insights.orphans(conn).get("dead", []))
51
+ cycle_set = insights.cycle_members(conn)
52
+ nodes = []
53
+ for p in files:
54
+ nodes.append(
55
+ {
56
+ "id": p,
57
+ "label": p.split("/")[-1],
58
+ "group": p.split("/", 1)[0],
59
+ "value": round(scores.get(p, 0.0) * 1000, 3) + 1,
60
+ "score": round(scores.get(p, 0.0), 5),
61
+ "summary": summ.get(p, ""),
62
+ "orphan": p in orphan_set,
63
+ "cycle": p in cycle_set,
64
+ }
65
+ )
66
+ data = {"nodes": nodes, "edges": [{"from": s, "to": d} for s, d in edges]}
67
+ total = conn.execute("SELECT COUNT(*) AS n FROM files WHERE lang IS NOT NULL").fetchone()["n"]
68
+ meta = {
69
+ "shown": len(files),
70
+ "total": total,
71
+ "prefix": prefix or "",
72
+ "orphans": sum(1 for p in files if p in orphan_set),
73
+ "cycles": sum(1 for p in files if p in cycle_set),
74
+ }
75
+ return _TEMPLATE.replace("__DATA__", json.dumps(data)).replace(
76
+ "__META__", json.dumps(meta)
77
+ )
78
+
79
+
80
+ def export_obsidian(config, conn, out_dir: Path) -> dict:
81
+ out = Path(out_dir)
82
+ ranked = dict(graphmod.rank(conn))
83
+ summ = {
84
+ r["path"]: r["summary_en"]
85
+ for r in conn.execute("SELECT path, summary_en FROM summaries")
86
+ }
87
+ deps_by: dict[str, list[str]] = {}
88
+ for r in conn.execute("SELECT src_path, dst_path FROM edges"):
89
+ deps_by.setdefault(r["src_path"], []).append(r["dst_path"])
90
+
91
+ files = [
92
+ r["path"] for r in conn.execute("SELECT path FROM files WHERE lang IS NOT NULL")
93
+ ]
94
+ count = 0
95
+ for p in files:
96
+ pkg = p.split("/", 1)[0]
97
+ lang = config.lang_for(p) or "other"
98
+ lines = [
99
+ "---",
100
+ f"tags: [{pkg}, {lang}]",
101
+ f"centrality: {ranked.get(p, 0.0):.4f}",
102
+ "---",
103
+ "",
104
+ f"`{p}`",
105
+ "",
106
+ summ.get(p) or "_No summary yet — run cerebro-summarize._",
107
+ "",
108
+ ]
109
+ syms = db.symbols_for(conn, p)
110
+ if syms:
111
+ lines.append("## Symbols")
112
+ lines += [f"- L{s['line']} {s['kind']} `{s['name']}`" for s in syms[:60]]
113
+ lines.append("")
114
+ deps = sorted(set(deps_by.get(p, [])))
115
+ if deps:
116
+ lines.append("## Imports (depends on)")
117
+ lines += [f"- [[{d}]]" for d in deps]
118
+ note = out / (p + ".md")
119
+ note.parent.mkdir(parents=True, exist_ok=True)
120
+ note.write_text("\n".join(lines), encoding="utf-8")
121
+ count += 1
122
+ return {"notes": count, "vault": str(out)}
123
+
124
+
125
+ # --- CLI entry points --------------------------------------------------------
126
+
127
+ def graph_main():
128
+ import argparse
129
+
130
+ ap = argparse.ArgumentParser(description="Write an interactive dependency graph HTML")
131
+ ap.add_argument("--limit", type=int, default=400, help="max nodes (by centrality)")
132
+ ap.add_argument("--prefix", default=None, help="only files under this path prefix")
133
+ ap.add_argument("-o", "--out", default=None)
134
+ args = ap.parse_args()
135
+ config = cfg.Config.load()
136
+ conn = db.connect(config.db_path)
137
+ out = Path(args.out) if args.out else config.db_path.parent / "cerebro-graph.html"
138
+ out.write_text(graph_html(conn, args.limit, args.prefix), encoding="utf-8")
139
+ print(json.dumps({"html": str(out), "open_with": f"open '{out}'"}))
140
+
141
+
142
+ def obsidian_main():
143
+ import argparse
144
+
145
+ ap = argparse.ArgumentParser(description="Export an Obsidian vault of the codebase")
146
+ ap.add_argument("-o", "--out", default=None)
147
+ args = ap.parse_args()
148
+ config = cfg.Config.load()
149
+ conn = db.connect(config.db_path)
150
+ out = Path(args.out) if args.out else config.db_path.parent / "vault"
151
+ result = export_obsidian(config, conn, out)
152
+ result["open"] = "Open this folder as a vault in Obsidian"
153
+ print(json.dumps(result))
154
+
155
+
156
+ _TEMPLATE = r"""<!doctype html>
157
+ <html lang="en"><head><meta charset="utf-8">
158
+ <meta name="viewport" content="width=device-width, initial-scale=1">
159
+ <title>Cerebro · dependency graph</title>
160
+ <script src="https://unpkg.com/force-graph"></script>
161
+ <style>
162
+ :root{
163
+ --bg:#0d1117; --surface:#161b22; --surface2:#1c2230; --border:#30363d;
164
+ --text:#e6edf3; --muted:#7d8590; --accent:#4c8dff; --radius:10px;
165
+ --font: ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
166
+ --mono: ui-monospace,SFMono-Regular,"SF Mono",Menlo,monospace;
167
+ }
168
+ *{box-sizing:border-box}
169
+ html,body{margin:0;height:100%}
170
+ body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:13px;overflow:hidden}
171
+ #app{display:flex;height:100vh}
172
+ #side{width:330px;flex:none;background:var(--surface);border-right:1px solid var(--border);
173
+ display:flex;flex-direction:column;overflow:hidden}
174
+ .brand{display:flex;align-items:baseline;gap:8px;padding:16px 16px 4px}
175
+ .brand b{font-size:15px;font-weight:650;letter-spacing:.2px}
176
+ .brand span{color:var(--muted);font-size:11px}
177
+ #meta{padding:0 16px 12px;color:var(--muted);font-size:11.5px}
178
+ .sec{padding:12px 16px;border-top:1px solid var(--border)}
179
+ .sec.grow{flex:1;overflow:auto}
180
+ .lbl{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:8px}
181
+ #search{width:100%;padding:9px 11px;border-radius:var(--radius);border:1px solid var(--border);
182
+ background:var(--bg);color:var(--text);font-size:12.5px;outline:none}
183
+ #search:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(76,141,255,.15)}
184
+ #hits{color:var(--muted);font-size:11px;margin-top:6px;min-height:14px}
185
+ #legend{display:flex;flex-wrap:wrap;gap:6px}
186
+ .chip{display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:999px;
187
+ border:1px solid var(--border);background:var(--bg);cursor:pointer;font-size:11px;
188
+ user-select:none;transition:opacity .15s,border-color .15s}
189
+ .chip.off{opacity:.35}
190
+ .chip .dot{width:9px;height:9px;border-radius:50%}
191
+ #info .empty{color:var(--muted);line-height:1.5}
192
+ #info .path{font-family:var(--mono);font-size:11px;color:#9fb4ff;word-break:break-all;margin-bottom:8px}
193
+ #info .row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px}
194
+ .badge{font-size:10.5px;padding:2px 8px;border-radius:999px;color:#0d1117;font-weight:600}
195
+ .stat{font-size:11px;color:var(--muted)}
196
+ .stat b{color:var(--text);font-weight:600}
197
+ #info .sum{font-size:12.5px;line-height:1.5;margin:4px 0 6px;color:#cdd6e3}
198
+ #info h4{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:14px 0 6px}
199
+ #info ul{list-style:none;margin:0;padding:0}
200
+ #info li{font-family:var(--mono);font-size:11px;padding:4px 8px;border-radius:7px;cursor:pointer;
201
+ color:#c9d1d9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
202
+ #info li:hover{background:var(--surface2);color:#fff}
203
+ #info li .pkg{color:var(--muted)}
204
+ #main{position:relative;flex:1}
205
+ #graph{position:absolute;inset:0}
206
+ #bar{position:absolute;top:14px;right:14px;display:flex;gap:6px;z-index:5}
207
+ .btn{background:rgba(22,27,34,.82);backdrop-filter:blur(6px);border:1px solid var(--border);color:var(--text);
208
+ padding:7px 12px;border-radius:8px;font-size:12px;cursor:pointer;font-family:var(--font);
209
+ transition:background .15s,border-color .15s}
210
+ .btn:hover{background:var(--surface2);border-color:#3d4654}
211
+ .btn.active{border-color:var(--accent);color:var(--accent)}
212
+ #load{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
213
+ flex-direction:column;gap:14px;background:var(--bg);z-index:8;transition:opacity .6s}
214
+ #load.hidden{opacity:0;pointer-events:none}
215
+ .spin{width:30px;height:30px;border-radius:50%;border:3px solid var(--border);
216
+ border-top-color:var(--accent);animation:spin .8s linear infinite}
217
+ @keyframes spin{to{transform:rotate(360deg)}}
218
+ #load span{color:var(--muted);font-size:12px}
219
+ ::-webkit-scrollbar{width:9px} ::-webkit-scrollbar-thumb{background:#2b3340;border-radius:9px}
220
+ </style></head>
221
+ <body><div id="app">
222
+ <aside id="side">
223
+ <div class="brand"><b>🧠 cerebro</b><span>dependency graph</span></div>
224
+ <div id="meta"></div>
225
+ <div class="sec">
226
+ <input id="search" placeholder="Search a file…" autocomplete="off" spellcheck="false">
227
+ <div id="hits"></div>
228
+ </div>
229
+ <div class="sec"><div class="lbl">Packages</div><div id="legend"></div></div>
230
+ <div class="sec grow"><div class="lbl">Selection</div><div id="info">
231
+ <div class="empty">Hover a node to light up what it connects to. Drag a node and the web
232
+ follows. Click to pin its details. Size = centrality (PageRank).</div></div></div>
233
+ </aside>
234
+ <div id="main">
235
+ <div id="bar">
236
+ <button class="btn" id="cyc">⚠ Cycles</button>
237
+ <button class="btn" id="orph">○ Orphans</button>
238
+ <button class="btn" id="fit">Fit</button>
239
+ <button class="btn active" id="freeze">❚❚ Pause</button>
240
+ </div>
241
+ <div id="graph"></div>
242
+ <div id="load"><div class="spin"></div><span>simulating layout…</span></div>
243
+ </div>
244
+ </div>
245
+ <script>
246
+ const DATA = __DATA__, META = __META__;
247
+ const PALETTE = ["#4c8dff","#3fb950","#db6d28","#e3597b","#a371f7","#1f9ce4",
248
+ "#f0a02c","#39c5bb","#e2c044","#ec6a5e","#57ab5a","#bc8cff"];
249
+ const groups=[...new Set(DATA.nodes.map(n=>n.group))].sort();
250
+ const color={}; groups.forEach((g,i)=>color[g]=PALETTE[i%PALETTE.length]);
251
+ const byId={}; DATA.nodes.forEach(n=>byId[n.id]=n);
252
+
253
+ const nbr={}, outM={}, inM={};
254
+ DATA.nodes.forEach(n=>nbr[n.id]=new Set());
255
+ DATA.edges.forEach(e=>{(outM[e.from]=outM[e.from]||[]).push(e.to);(inM[e.to]=inM[e.to]||[]).push(e.from);
256
+ nbr[e.from].add(e.to); nbr[e.to].add(e.from);});
257
+
258
+ document.getElementById('meta').innerHTML =
259
+ `<b style="color:var(--text)">${META.shown}</b> of ${META.total} files`+
260
+ `${META.prefix?` · <span style="color:#9fb4ff">${META.prefix}</span>`:''} · ${DATA.edges.length} imports`+
261
+ `${META.cycles?` · <span style="color:#e2c044">⚠ ${META.cycles} in cycles</span>`:''}`+
262
+ `${META.orphans?` · <span style="color:#ec6a5e">○ ${META.orphans} orphans</span>`:''}`;
263
+
264
+ const hidden=new Set();
265
+ const legend=document.getElementById('legend');
266
+ groups.forEach(g=>{
267
+ const c=document.createElement('div'); c.className='chip'; c.dataset.g=g;
268
+ c.innerHTML=`<i class="dot" style="background:${color[g]}"></i>${g}`;
269
+ c.onclick=()=>{ c.classList.toggle('off'); hidden.has(g)?hidden.delete(g):hidden.add(g); };
270
+ legend.appendChild(c);
271
+ });
272
+
273
+ let hl=new Set(), focusId=null, pinned=null, overlay=null;
274
+ const lid=x=>typeof x==='object'?x.id:x;
275
+ const overlaySet=k=>new Set(DATA.nodes.filter(n=>k==='cycle'?n.cycle:n.orphan).map(n=>n.id));
276
+ const REL=3.4;
277
+ const el=document.getElementById('graph');
278
+
279
+ const Graph = ForceGraph()(el)
280
+ .backgroundColor('#0d1117')
281
+ .graphData({nodes:DATA.nodes.map(n=>Object.assign({},n)),
282
+ links:DATA.edges.map(e=>({source:e.from,target:e.to}))})
283
+ .nodeId('id').nodeVal(n=>n.value)
284
+ .nodeRelSize(REL)
285
+ .autoPauseRedraw(false)
286
+ .cooldownTicks(Infinity).d3VelocityDecay(0.30).d3AlphaMin(0)
287
+ .linkDirectionalArrowLength(2).linkDirectionalArrowRelPos(1).linkCurvature(0)
288
+ .linkColor(l=>{const s=lid(l.source),t=lid(l.target);
289
+ if(hidden.has(byId[s].group)||hidden.has(byId[t].group))return 'rgba(0,0,0,0)';
290
+ if(focusId)return (s===focusId||t===focusId)?'rgba(76,141,255,.85)':'rgba(48,54,61,.12)';
291
+ return 'rgba(70,80,95,.28)';})
292
+ .linkWidth(l=>{const s=lid(l.source),t=lid(l.target);return focusId&&(s===focusId||t===focusId)?1.4:0.5;})
293
+ .linkDirectionalParticles(l=>{const s=lid(l.source),t=lid(l.target);return focusId&&(s===focusId||t===focusId)?3:0;})
294
+ .linkDirectionalParticleWidth(2.2).linkDirectionalParticleColor(()=>'#7fb0ff').linkDirectionalParticleSpeed(0.012)
295
+ .nodeCanvasObject((node,ctx,scale)=>{
296
+ if(hidden.has(node.group))return;
297
+ const r=Math.sqrt(node.value)*REL, dim=hl.size&&!hl.has(node.id);
298
+ if(node.id===focusId){ctx.shadowColor=color[node.group];ctx.shadowBlur=22;}
299
+ ctx.beginPath(); ctx.arc(node.x,node.y,r,0,6.2832);
300
+ ctx.globalAlpha=dim?0.45:1; ctx.fillStyle=dim?'#6e7681':color[node.group]; ctx.fill();
301
+ ctx.shadowBlur=0;
302
+ if(node.id===pinned){ctx.lineWidth=2/scale;ctx.strokeStyle='#fff';ctx.stroke();}
303
+ if(!dim&&node.cycle){ctx.strokeStyle='rgba(226,192,68,.9)';ctx.lineWidth=1.6/scale;
304
+ ctx.beginPath();ctx.arc(node.x,node.y,r+2.5/scale,0,6.2832);ctx.stroke();}
305
+ if(!dim&&node.orphan){ctx.strokeStyle='rgba(236,106,94,.95)';ctx.lineWidth=1.4/scale;
306
+ ctx.setLineDash([3/scale,2.5/scale]);ctx.beginPath();
307
+ ctx.arc(node.x,node.y,r+(node.cycle?5:2.5)/scale,0,6.2832);ctx.stroke();ctx.setLineDash([]);}
308
+ ctx.globalAlpha=1;
309
+ if(!dim&&(node.value>9||scale>1.8||hl.has(node.id))){
310
+ ctx.font=`${11/scale}px ${getComputedStyle(document.body).fontFamily}`;
311
+ ctx.textAlign='center'; ctx.textBaseline='top';
312
+ ctx.fillStyle=hl.size&&!hl.has(node.id)?'rgba(230,237,243,.2)':'rgba(230,237,243,.92)';
313
+ ctx.fillText(node.label, node.x, node.y+r+2/scale);
314
+ }
315
+ })
316
+ .nodePointerAreaPaint((node,col,ctx)=>{
317
+ if(hidden.has(node.group))return;
318
+ ctx.fillStyle=col; ctx.beginPath();
319
+ ctx.arc(node.x,node.y,Math.sqrt(node.value)*REL+2,0,6.2832); ctx.fill();
320
+ })
321
+ .onNodeHover(node=>{ el.style.cursor=node?'pointer':'default';
322
+ if(node) setHL(node);
323
+ else if(pinned) setHL(live[pinned]||{id:pinned});
324
+ else setHL(null); })
325
+ .onNodeClick(node=>{ pinned=node.id; setHL(node); panel(node.id);
326
+ Graph.centerAt(node.x,node.y,600); Graph.zoom(Math.max(Graph.zoom(),2.4),600); })
327
+ .onBackgroundClick(()=>{ pinned=null; setHL(null);
328
+ document.getElementById('info').innerHTML='<div class="empty">Hover a node to light up what it connects to. Drag a node and the web follows. Click to pin its details.</div>'; });
329
+
330
+ Graph.d3Force('charge').strength(-260).distanceMax(900);
331
+ Graph.d3Force('link').distance(42).strength(.45);
332
+
333
+ const live={}; Graph.graphData().nodes.forEach(n=>live[n.id]=n);
334
+ function setHL(node){ if(node){focusId=node.id; hl=new Set([node.id]); nbr[node.id].forEach(x=>hl.add(x));}
335
+ else if(overlay){focusId=null; hl=overlaySet(overlay);}
336
+ else {focusId=null; hl=new Set();} }
337
+ const esc=s=>(s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
338
+ function li(id){const parts=id.split('/');const base=parts.pop();
339
+ return `<li data-id="${esc(id)}" title="${esc(id)}"><span class="pkg">${esc(parts[0]||'')}/…/</span>${esc(base)}</li>`;}
340
+ function panel(id){const n=byId[id];if(!n)return;const o=(outM[id]||[]),inn=(inM[id]||[]);
341
+ document.getElementById('info').innerHTML=
342
+ `<div class="path">${esc(id)}</div>`+
343
+ `<div class="row"><span class="badge" style="background:${color[n.group]}">${esc(n.group)}</span>`+
344
+ `<span class="stat">PageRank <b>${n.score}</b></span></div>`+
345
+ `<div class="sum">${esc(n.summary)||'<span style="color:var(--muted)">No summary yet — run cerebro-summarize.</span>'}</div>`+
346
+ `<h4>Imports · ${o.length}</h4><ul>${o.map(li).join('')||'<li style="color:var(--muted);cursor:default">none</li>'}</ul>`+
347
+ `<h4>Imported by · ${inn.length}</h4><ul>${inn.map(li).join('')||'<li style="color:var(--muted);cursor:default">none</li>'}</ul>`;}
348
+ function focusNode(id){const node=live[id];if(!node)return;pinned=id;setHL({id});panel(id);
349
+ Graph.centerAt(node.x,node.y,600);Graph.zoom(Math.max(Graph.zoom(),2.4),600);}
350
+ document.getElementById('info').addEventListener('click',e=>{const li=e.target.closest('li[data-id]');if(li)focusNode(li.dataset.id);});
351
+
352
+ const search=document.getElementById('search'),hits=document.getElementById('hits');
353
+ search.addEventListener('input',()=>{const q=search.value.toLowerCase().trim();
354
+ hits.textContent=q?`${DATA.nodes.filter(n=>n.id.toLowerCase().includes(q)).length} match`:'';});
355
+ search.addEventListener('keydown',e=>{if(e.key!=='Enter')return;const q=search.value.toLowerCase().trim();
356
+ const m=DATA.nodes.find(n=>n.id.toLowerCase().includes(q));if(m)focusNode(m.id);});
357
+
358
+ const cycB=document.getElementById('cyc'),orphB=document.getElementById('orph');
359
+ if(META.cycles)cycB.textContent='⚠ Cycles '+META.cycles; if(META.orphans)orphB.textContent='○ Orphans '+META.orphans;
360
+ function setOverlay(k){overlay=overlay===k?null:k;
361
+ cycB.classList.toggle('active',overlay==='cycle'); orphB.classList.toggle('active',overlay==='orphan');
362
+ pinned=null; setHL(null);}
363
+ cycB.onclick=()=>setOverlay('cycle'); orphB.onclick=()=>setOverlay('orphan');
364
+
365
+ document.getElementById('fit').onclick=()=>Graph.zoomToFit(600,60);
366
+ let running=true;const fb=document.getElementById('freeze');
367
+ fb.onclick=()=>{running=!running; running?Graph.resumeAnimation():Graph.pauseAnimation();
368
+ fb.classList.toggle('active',running); fb.textContent=running?'❚❚ Pause':'▶ Resume';};
369
+
370
+ function size(){Graph.width(el.clientWidth).height(el.clientHeight);}
371
+ size(); window.addEventListener('resize',size);
372
+ setTimeout(()=>Graph.zoomToFit(700,70),1200);
373
+ setTimeout(()=>document.getElementById('load').classList.add('hidden'),900);
374
+ </script></body></html>"""
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: cerebro-code-memory
3
+ Version: 0.1.0
4
+ Summary: Persistent code-knowledge memory across AI chat sessions (MCP server)
5
+ Project-URL: Homepage, https://github.com/marcodavidd020/cerebro-mcp
6
+ Project-URL: Repository, https://github.com/marcodavidd020/cerebro-mcp
7
+ Project-URL: Issues, https://github.com/marcodavidd020/cerebro-mcp/issues
8
+ Author-email: Marco Toledo <huancacori@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: claude,code-intelligence,mcp,semantic-search,sqlite,tree-sitter
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: mcp>=1.2.0
14
+ Requires-Dist: networkx>=3.2
15
+ Requires-Dist: pathspec>=0.12
16
+ Requires-Dist: tree-sitter-language-pack>=0.7
17
+ Requires-Dist: tree-sitter>=0.23
18
+ Provides-Extra: semantic
19
+ Requires-Dist: model2vec>=0.3; extra == 'semantic'
20
+ Requires-Dist: numpy>=1.26; extra == 'semantic'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Cerebro 🧠
24
+
25
+ **Persistent code-knowledge memory across AI chat sessions.**
26
+
27
+ Every new chat re-analyzes your project's folders from scratch to understand it,
28
+ burning tokens re-discovering what a previous chat already learned. Cerebro caches
29
+ the *understanding* — not just the files — in a small SQLite "brain" that lives
30
+ **outside** the chat. New sessions **query** it instead of re-reading folders.
31
+
32
+ Instead of reading 50 files (~100k tokens) to understand a project, the model makes
33
+ one `cerebro_map()` call (~2-3k tokens) plus a few targeted lookups.
34
+
35
+ ## How it works
36
+
37
+ Three layers of "traces", cheapest first:
38
+
39
+ 1. **Structural map** (free, no LLM) — `tree-sitter` extracts symbols + imports and
40
+ builds a dependency graph. Imports resolve both relative paths and **tsconfig /
41
+ jsconfig path aliases** (`@/...`), so Next.js / NestJS monorepos get real edges.
42
+ **PageRank** ranks the most important modules. Each file is hashed so changes
43
+ are detectable. Languages: Python, JavaScript / TypeScript (incl. JSX/TSX)
44
+ and Dart / Flutter.
45
+ 2. **Cached summaries** (the big saver) — as a chat understands a file, it calls
46
+ `cerebro_record(path, summary)` to store a 1-3 sentence **English** summary
47
+ (English tokenizes ~15-30% cheaper than Spanish). Future sessions reuse it.
48
+ 3. **Freshness** — each summary is tied to the file's hash. If the file changed,
49
+ the trace is flagged **stale** so only that file gets re-read.
50
+
51
+ > Why not Dijkstra? Code knowledge is a *relevance* problem, not a shortest-path one.
52
+ > The useful algorithms are graph traversal (BFS/DFS, for impact) and centrality
53
+ > (PageRank, for ranking) — not weighted routing.
54
+
55
+ ## MCP tools
56
+
57
+ | Tool | Purpose |
58
+ |------|---------|
59
+ | `cerebro_map(top=30)` | Cheap project overview, modules ranked by centrality. **Call first.** |
60
+ | `cerebro_get(path)` | Summary + symbols + dependencies of a file, without reading it. |
61
+ | `cerebro_search(query)` | Hybrid semantic + keyword search; semantic hits resolve to the exact symbol (`path:line`), not just the file. |
62
+ | `cerebro_record(path, summary)` | Leave a trace: store your understanding of a file. |
63
+ | `cerebro_note(content, topic?)` | Record a decision / domain rule / gotcha (the *why*). |
64
+ | `cerebro_recall(query?)` | Recall decisions recorded by past sessions. |
65
+ | `cerebro_stale()` | Files changed since last index + stale summaries. |
66
+ | `cerebro_sync()` | Catch branch switch / git pull / external edits and reindex them. |
67
+ | `cerebro_reindex(paths?)` | Refresh the structural index (only changed files). |
68
+ | `cerebro_impact(path)` | Transitive blast radius: everything that (in)directly imports a file. |
69
+ | `cerebro_cycles()` | Circular-import groups (architecture smell). |
70
+ | `cerebro_orphans(prefix?)` | Code files nothing imports — dead-code candidates (file-level). |
71
+ | `cerebro_dead_symbols(prefix?)` | Unused-export candidates: functions/classes/methods referenced nowhere *in their own project*, inside files that *are* imported (symbol-level dead code). |
72
+ | `cerebro_callers(name)` | Call sites of a symbol (who calls it, with enclosing fn + line). |
73
+ | `cerebro_calls(path)` | Internal functions a file calls (outgoing call edges). |
74
+
75
+ ## Quick start
76
+
77
+ One command onboards any repo — it indexes and prints the exact registration line:
78
+
79
+ ```bash
80
+ uv tool install --from . cerebro # installs the `cerebro` command globally
81
+ cd /path/to/your/repo
82
+ cerebro setup --summarize --embed # index (+ warm summaries / semantic index), then prints next steps
83
+ ```
84
+
85
+ `cerebro setup` is idempotent. Then run the `claude mcp add …` line it prints, reload
86
+ your editor, and the `cerebro_*` tools are available in chat.
87
+
88
+ ## Unified CLI
89
+
90
+ ```
91
+ cerebro # no args -> MCP server (stdio); this is what the registration runs
92
+ cerebro setup # index this repo + print MCP registration
93
+ cerebro index [--force] # build/refresh the index
94
+ cerebro search <query> # hybrid semantic + keyword search
95
+ cerebro map # project overview
96
+ cerebro graph # interactive dependency-graph HTML
97
+ cerebro obsidian # export an Obsidian vault
98
+ cerebro summarize / embed
99
+ cerebro impact / cycles / orphans / callers / calls / recall
100
+ cerebro doc-audit <vault> # living docs: flag knowledge notes whose referenced code changed
101
+ ```
102
+
103
+ ### Living documentation (`doc-audit`)
104
+
105
+ `cerebro doc-audit <markdown-vault>` cross-checks a curated knowledge vault against
106
+ the code index: it parses each note's code references (`path:line`, backticked
107
+ symbols) and the note's `ultima_verificacion`/`fecha`, then flags notes whose
108
+ referenced files **changed after** they were verified, **moved/were deleted**, or
109
+ mention a **symbol that no longer exists**. `--aliases` maps wiki app names to repo
110
+ dirs (`backend_app=fenix-store-backend,…`); `--fix` patches stale notes' frontmatter
111
+ to `estado: revisar`. This is the bridge between an auto-fresh code index and a
112
+ human-curated wiki — documentation that can't silently rot.
113
+
114
+ `cerebro doc-refresh <note>` closes the loop: it re-audits one stale note against
115
+ the *live* code and prints a briefing — current symbols, summary and dependents for
116
+ each reference, plus the new location of any moved file — exactly the context an
117
+ agent needs to propose the update (self-healing docs, human-reviewed).
118
+
119
+ Without a global install, prefix any command with `uv run` (e.g. `uv run cerebro setup`).
120
+
121
+ Point Cerebro at a specific repo with `CEREBRO_ROOT=/path/to/repo`. It honors
122
+ `.gitignore` plus an optional **`.cerebroignore`** (same syntax) for excluding
123
+ heavy non-source dirs (`backup/`, `**/uploads/`, …) without touching your VCS
124
+ config. `node_modules/`, `.next/`, `dist/`, `build/` are ignored by default.
125
+
126
+ Works on monorepos: index the whole thing at once (a single brain at the root,
127
+ with cross-package alias resolution) or per sub-app (`CEREBRO_ROOT` per package).
128
+
129
+ ### Register with Claude Code
130
+
131
+ ```bash
132
+ claude mcp add cerebro -- uv --directory /path/to/cerebro run cerebro
133
+ ```
134
+
135
+ Set `CEREBRO_ROOT` to the project you want the brain to cover. See `plugin/` for the
136
+ optional Claude Code plugin that auto-injects the map at session start and flags
137
+ edited files as stale.
138
+
139
+ ## Scope (MVP)
140
+
141
+ In: structural map, cached summaries, freshness, keyword search, tsconfig/jsconfig
142
+ alias resolution, `.cerebroignore`, batch summary warming (`cerebro-summarize`, via
143
+ headless `claude -p` — no API key), a decision log (`cerebro_note` /
144
+ `cerebro_recall`, surfaced at session start), and git-aware freshness
145
+ (`cerebro_sync` catches branch switch / pull / external edits across nested repos),
146
+ and optional local semantic search (`cerebro-embed` + `--extra semantic`: model2vec
147
+ embeddings — one vector per symbol — no torch, no API key, nothing leaves the
148
+ machine — `cerebro_search` becomes hybrid semantic + keyword and lands on the exact
149
+ symbol (`path:line`), not just the file), and visualizations (`cerebro-graph` →
150
+ self-contained interactive HTML dependency graph; `cerebro-export-obsidian` → an
151
+ Obsidian vault where imports are `[[links]]`), and architecture insights
152
+ (`cerebro_impact` transitive blast radius, `cerebro_cycles` circular imports,
153
+ `cerebro_orphans` dead-code candidates), and a symbol-level call graph
154
+ (`cerebro_callers` / `cerebro_calls`, tree-sitter name-resolved).
155
+ Deferred to v2: a live file watcher, and LSP-backed call graph for type-precise
156
+ resolution (the current call graph resolves by name).
157
+
158
+ ## License
159
+
160
+ MIT © 2026 Marco Toledo — see [LICENSE](LICENSE).
@@ -0,0 +1,23 @@
1
+ cerebro/__init__.py,sha256=eEEQ_sWw07H2kZ4wTCirZ26kRZG-NhHdvOQ1UqEMCnU,99
2
+ cerebro/callgraph.py,sha256=Uzto-AltX1EIYsNCz4GCGTkK5I4aOykb1uTy6oaHEII,1538
3
+ cerebro/cli.py,sha256=I1TcHfDiR_u6qETwEkV3hdG2cKXpHevVqX3sp3O-b4Y,14102
4
+ cerebro/config.py,sha256=fCgkyyCAAY7bCrTiNakQBajYf1blGJflCyaXeNojC1Y,4555
5
+ cerebro/db.py,sha256=nZbtrBMPFmFmpt9wXz7-Xvk3EEbyu0sw7oHYQBYN25w,8933
6
+ cerebro/docaudit.py,sha256=zZn-t3_ql4RG_w_7g-wmcQJKP_cK22EBi9mBpvEN240,8036
7
+ cerebro/embeddings.py,sha256=yZAVlDqvuE9ussAUIcKmJlUfZB7oEF_1nSfJdxfJL5E,5987
8
+ cerebro/gitsync.py,sha256=83S-Mz3LX22v6DLuwJFfkyNNDiXfqllKuNLjmNV5W7g,3957
9
+ cerebro/graph.py,sha256=DWsZ82Vdtej4HX3YpNhPgBx7VbjTeOW8Q1hOUcjgNoE,2821
10
+ cerebro/indexer.py,sha256=uETgCOxU5sk9IBF_UpqLDkDKgVxohE0oewURlVTyL-Q,35385
11
+ cerebro/insights.py,sha256=fBUzRJ-t0bcVXaiW9JEHxr5U6jpOAn7GM9dwfOp2Gls,9167
12
+ cerebro/notes.py,sha256=Xek2NYQFzxs-Sl51aQLtIskuZ3iRNDbqth39mFOp9wA,2456
13
+ cerebro/server.py,sha256=YcKQ6cnAZgYHbRpr8Cb1SHYlJT4QNG8N58XVQOKOvmc,14992
14
+ cerebro/summaries.py,sha256=idmrzSnNtcT8WpqMHpEcS7yVYujSzeE7bMXBP8w0nyY,2607
15
+ cerebro/summarizer.py,sha256=axDVlKoSjjsg6flhxvmNJd-YjvIic3-fl26zxhZVpBE,4008
16
+ cerebro/tsconfig.py,sha256=LxlJyzrtDOobcPwps7f1HxnRvt6UOn7FSdTPR5mpWQc,5708
17
+ cerebro/views.py,sha256=Z7_13r3ys0YM5lNPT7SXBUic-su9kmmcjC70Fz5h3p4,2149
18
+ cerebro/viz.py,sha256=nrcp2yvfm5fECWVmTXMrYz64srvoomXazW6MoY_Blag,18911
19
+ cerebro_code_memory-0.1.0.dist-info/METADATA,sha256=0ThTvg8kCv0XLp2p1ZJjfmxp4PK6-H2pH8ngHkfPf9s,8353
20
+ cerebro_code_memory-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
21
+ cerebro_code_memory-0.1.0.dist-info/entry_points.txt,sha256=OJahPgB6TbsPizn_dibpKP8dOfd2ZPcMfcXjZ43eSnQ,417
22
+ cerebro_code_memory-0.1.0.dist-info/licenses/LICENSE,sha256=yiho2YjFT755EnCviLP8pTWu64JpvwqqK9ZNF5aG1_k,1069
23
+ cerebro_code_memory-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,11 @@
1
+ [console_scripts]
2
+ cerebro = cerebro.cli:main
3
+ cerebro-code-memory = cerebro.server:main
4
+ cerebro-embed = cerebro.embeddings:main
5
+ cerebro-export-obsidian = cerebro.viz:obsidian_main
6
+ cerebro-graph = cerebro.viz:graph_main
7
+ cerebro-index = cerebro.indexer:main
8
+ cerebro-map = cerebro.server:map_main
9
+ cerebro-recall = cerebro.server:recall_main
10
+ cerebro-summarize = cerebro.summarizer:main
11
+ cerebro-sync = cerebro.gitsync:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marco Toledo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.