codebrain 0.3.5__py3-none-any.whl → 0.4.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.
codebrain/__init__.py CHANGED
@@ -7,4 +7,4 @@ try:
7
7
  except PackageNotFoundError:
8
8
  # Source checkout without installation (e.g. running from a worktree).
9
9
  # Keep in sync with [project.version] in pyproject.toml.
10
- __version__ = "0.3.5"
10
+ __version__ = "0.4.0"
codebrain/cli.py CHANGED
@@ -58,9 +58,17 @@ def _require_index(repo_root: Path) -> Path:
58
58
  import gc
59
59
  gc.collect() # Release any lingering connections
60
60
  try:
61
- db.unlink(missing_ok=True)
62
- for wal in db.parent.glob(f"{db.name}-*"):
63
- wal.unlink(missing_ok=True)
61
+ # Prefer rebuilding in place — deleting the file fails on
62
+ # Windows while any MCP server holds it open. Only fall back
63
+ # to file replacement when the DB is unreadable.
64
+ try:
65
+ from codebrain.graph.store import GraphStore
66
+ with GraphStore(db) as _store:
67
+ _store.clear_all()
68
+ except Exception:
69
+ db.unlink(missing_ok=True)
70
+ for wal in db.parent.glob(f"{db.name}-*"):
71
+ wal.unlink(missing_ok=True)
64
72
  from codebrain.indexer import full_index
65
73
  full_index(repo_root, db)
66
74
  click.echo(click.style("Database rebuilt successfully.", fg="green"), err=True)
@@ -1618,33 +1626,37 @@ def reindex(ctx: click.Context, yes: bool, as_json: bool) -> None:
1618
1626
  db = _db_path(repo_root)
1619
1627
 
1620
1628
  if not yes and not as_json:
1621
- if not click.confirm("This will delete and rebuild the entire index. Continue?"):
1629
+ if not click.confirm("This will rebuild the entire index. Continue?"):
1622
1630
  click.echo("Aborted.")
1623
1631
  return
1624
1632
 
1633
+ # Rebuild IN PLACE (clear tables, re-fill) rather than deleting the DB
1634
+ # file. Deleting fails on Windows whenever any MCP server holds the DB
1635
+ # open — and with session-long server lifetimes that is "always". WAL
1636
+ # mode makes the in-place rebuild safe with readers attached, so reindex
1637
+ # never needs to find, kill, or wait for other processes.
1625
1638
  if db.exists():
1639
+ from codebrain.graph.store import GraphStore
1626
1640
  try:
1627
- db.unlink()
1628
- except PermissionError:
1629
- from codebrain.mcp_lifecycle import find_db_lock_holder, kill_pid
1630
- holder = find_db_lock_holder(repo_root)
1631
- if holder is None:
1632
- click.echo(
1633
- " Database is locked but no recorded MCP holder.\n"
1634
- " Find the holder with `Get-Process python` (Windows) or `lsof` (Unix) and stop it."
1635
- )
1641
+ with GraphStore(db) as _store:
1642
+ _store.clear_all()
1643
+ except Exception as exc:
1644
+ # Corrupted beyond clearing — replacing the file is the only
1645
+ # option left. This fails if another process holds it open.
1646
+ click.echo(click.style(
1647
+ f" Index unreadable ({exc}); replacing the database file.", fg="yellow",
1648
+ ))
1649
+ try:
1650
+ db.unlink()
1651
+ for wal in db.parent.glob(f"{db.name}-*"):
1652
+ wal.unlink(missing_ok=True)
1653
+ except PermissionError:
1654
+ click.echo(click.style(
1655
+ " The corrupted database is held open by a running MCP server.\n"
1656
+ " Close Claude Code sessions for this project (or run "
1657
+ "`brain doctor --kill-stale-mcps`) and retry.", fg="red",
1658
+ ))
1636
1659
  raise
1637
- should_kill = yes or as_json or click.confirm(
1638
- f"Database locked by CodeBrain MCP server PID {holder}. Terminate it and continue?"
1639
- )
1640
- if not should_kill:
1641
- click.echo("Aborted.")
1642
- return
1643
- status = kill_pid(holder)
1644
- click.echo(f" Killed MCP PID {holder}: {status}")
1645
- import time as _time
1646
- _time.sleep(1)
1647
- db.unlink()
1648
1660
 
1649
1661
  files = discover_files(repo_root)
1650
1662
 
@@ -1847,6 +1859,13 @@ def repair(ctx: click.Context) -> None:
1847
1859
  break
1848
1860
  except PermissionError:
1849
1861
  _time.sleep(0.2)
1862
+ else:
1863
+ click.echo(click.style(
1864
+ "Could not replace the corrupted database — a running MCP server "
1865
+ "holds it open.\nClose Claude Code sessions for this project (or "
1866
+ "run `brain doctor --kill-stale-mcps`) and retry.", fg="red",
1867
+ ))
1868
+ sys.exit(1)
1850
1869
  # Also clean up WAL/SHM files
1851
1870
  for suffix in ("-wal", "-shm"):
1852
1871
  wal = db.parent / (db.name + suffix)
codebrain/graph/store.py CHANGED
@@ -22,11 +22,18 @@ class GraphStore:
22
22
  self.db_path.parent.mkdir(parents=True, exist_ok=True)
23
23
  _log.debug("Opening database %s", self.db_path)
24
24
  self.conn = sqlite3.connect(str(self.db_path), timeout=30, check_same_thread=False)
25
- self.conn.row_factory = sqlite3.Row
26
- self.conn.execute("PRAGMA journal_mode=WAL")
27
- self.conn.execute("PRAGMA synchronous=NORMAL")
28
- self.conn.execute("PRAGMA foreign_keys=OFF")
29
- migrate_db(self.conn)
25
+ try:
26
+ self.conn.row_factory = sqlite3.Row
27
+ self.conn.execute("PRAGMA journal_mode=WAL")
28
+ self.conn.execute("PRAGMA synchronous=NORMAL")
29
+ self.conn.execute("PRAGMA foreign_keys=OFF")
30
+ migrate_db(self.conn)
31
+ except Exception:
32
+ # A corrupted/garbage file makes the pragmas raise AFTER the OS
33
+ # handle is open. Without this close, the dangling handle keeps
34
+ # the file locked on Windows and recovery-by-replacement fails.
35
+ self.conn.close()
36
+ raise
30
37
 
31
38
  def close(self) -> None:
32
39
  _log.debug("Closing database %s", self.db_path)
codebrain/indexer.py CHANGED
@@ -490,6 +490,43 @@ def full_index(
490
490
  }
491
491
 
492
492
 
493
+ def scan_stale(
494
+ repo_root: Path,
495
+ store: GraphStore,
496
+ settings: "Settings | None" = None,
497
+ ) -> tuple[list[Path], list[Path]]:
498
+ """Cheap staleness scan: which files changed since they were indexed?
499
+
500
+ Returns ``(changed_candidates, deleted)``. Uses an mtime-vs-last_indexed
501
+ prefilter so no file contents are read here — false candidates are fine
502
+ because :func:`incremental_update` hash-checks before re-parsing. This is
503
+ the fast freshness layer (milliseconds on a warm FS cache); the exhaustive
504
+ hash-everything pass lives in the watcher's startup catch-up sync.
505
+ """
506
+ rows = store.conn.execute("SELECT path, last_indexed FROM files").fetchall()
507
+ stored = {row["path"]: row["last_indexed"] for row in rows}
508
+
509
+ changed: list[Path] = []
510
+ seen: set[str] = set()
511
+ # Files modified within this window before indexing finished may have
512
+ # raced the indexer — treat them as candidates (hash check disambiguates).
513
+ slack = 2.0
514
+ for file_path in discover_files(repo_root, settings):
515
+ rel = normalize_path(file_path, repo_root)
516
+ seen.add(rel)
517
+ last_indexed = stored.get(rel)
518
+ if last_indexed is None:
519
+ changed.append(file_path) # new file
520
+ continue
521
+ try:
522
+ if file_path.stat().st_mtime > last_indexed - slack:
523
+ changed.append(file_path)
524
+ except OSError:
525
+ continue
526
+ deleted = [repo_root / rel for rel in stored if rel not in seen]
527
+ return changed, deleted
528
+
529
+
493
530
  def incremental_update(
494
531
  repo_root: Path,
495
532
  changed_files: list[Path],
@@ -510,6 +547,12 @@ def incremental_update(
510
547
  store.remove_file(rel)
511
548
  removed += 1
512
549
 
550
+ # tree-sitter can hang holding the GIL on Windows — isolate those
551
+ # extensions in a subprocess, same as full_index. A hang here would
552
+ # otherwise freeze the watcher/catch-up thread while it holds the DB
553
+ # lock, silently stopping auto-indexing for the rest of the session.
554
+ _NEEDS_ISOLATION = frozenset({".ts", ".tsx", ".js", ".jsx"})
555
+
513
556
  for file_path in changed_files:
514
557
  rel = normalize_path(file_path, repo_root)
515
558
  try:
@@ -519,9 +562,16 @@ def incremental_update(
519
562
  if current_hash == stored_hash:
520
563
  continue
521
564
 
522
- pf = _parse_file(file_path, repo_root)
523
- store.upsert_file(pf)
524
- updated += 1
565
+ if sys.platform == "win32" and file_path.suffix in _NEEDS_ISOLATION:
566
+ pf, err = _parse_with_timeout(file_path, repo_root, timeout=30)
567
+ if err:
568
+ errors.append(err)
569
+ continue
570
+ else:
571
+ pf = _parse_file(file_path, repo_root)
572
+ if pf is not None:
573
+ store.upsert_file(pf)
574
+ updated += 1
525
575
  except Exception as exc:
526
576
  errors.append(f"{rel}: {exc}")
527
577
 
@@ -79,39 +79,43 @@ def _read_pid_file(pid_file: Path) -> int | None:
79
79
  return None
80
80
 
81
81
 
82
- def _predecessor_has_live_host(pid: int) -> bool:
83
- """True if ``pid``'s ancestor chain contains a live host (claude/cursor/...).
84
-
85
- Such a predecessor belongs to a *concurrent* sibling Claude session and
86
- must not be terminated its disappearance would silently break that
87
- session's MCP. Without psutil we cannot tell, so be safe and assume yes.
82
+ def _predecessor_host_pid(pid: int) -> int | None:
83
+ """Return the PID of the live IDE host (claude/cursor/...) in ``pid``'s
84
+ ancestor chain, or None if there is none.
85
+
86
+ A predecessor with a live host belongs to *some* Claude session — but
87
+ whether it must be spared depends on WHICH session: a different host PID
88
+ means a concurrent sibling window (killing it would silently break that
89
+ session's MCP); the SAME host PID as ours means it is our own session's
90
+ orphaned predecessor left behind by a transport disconnect/reconnect,
91
+ and it must die or two servers fight over the SQLite DB.
88
92
  """
89
93
  try:
90
94
  import psutil
91
95
  except ImportError:
92
- return True
96
+ return None
93
97
  try:
94
98
  proc = psutil.Process(pid)
95
99
  except (psutil.NoSuchProcess, psutil.AccessDenied):
96
- return False
100
+ return None
97
101
  for _ in range(ANCESTOR_WALK_DEPTH):
98
102
  try:
99
103
  parent = proc.parent()
100
104
  except (psutil.NoSuchProcess, psutil.AccessDenied):
101
- return False
105
+ return None
102
106
  if parent is None:
103
- return False
107
+ return None
104
108
  try:
105
109
  name = (parent.name() or "").lower()
106
110
  except (psutil.NoSuchProcess, psutil.AccessDenied):
107
- return False
111
+ return None
108
112
  if any(hint in name for hint in HOST_PROCESS_NAME_HINTS):
109
- return True
113
+ return parent.pid
110
114
  proc = parent
111
- return False
115
+ return None
112
116
 
113
117
 
114
- def _kill_stale_predecessor(pid_file: Path) -> None:
118
+ def _kill_stale_predecessor(pid_file: Path, own_host_pid: int | None = None) -> None:
115
119
  if not pid_file.exists():
116
120
  return
117
121
  old_pid = _read_pid_file(pid_file)
@@ -128,12 +132,27 @@ def _kill_stale_predecessor(pid_file: Path) -> None:
128
132
  # PID was reused by an unrelated process. Don't touch.
129
133
  _log.debug("PID %d reused by unrelated process; leaving alone", old_pid)
130
134
  return
131
- if _predecessor_has_live_host(old_pid):
132
- # Concurrent sibling Claude session is still using this MCP leave it.
133
- # Without this, two Claude windows on the same repo race each other and
134
- # whichever started last kills the other's MCP.
135
- _log.debug("PID %d has a live IDE host; sibling MCP, leaving alone", old_pid)
135
+ pred_host = _predecessor_host_pid(old_pid)
136
+ if pred_host is not None and pred_host != own_host_pid:
137
+ # Live IDE host that is NOT ours concurrent sibling Claude session
138
+ # is still using this MCP leave it. Without this, two Claude
139
+ # windows on the same repo race each other and whichever started
140
+ # last kills the other's MCP.
141
+ _log.debug(
142
+ "PID %d has live IDE host %d (ours: %s); sibling MCP, leaving alone",
143
+ old_pid, pred_host, own_host_pid,
144
+ )
136
145
  return
146
+ if pred_host is not None:
147
+ # Same host as ours → our own session reconnected and left the old
148
+ # server behind with a dead stdio transport. It will never exit on
149
+ # its own (parent still alive, idle timer disabled when anchored)
150
+ # and holds the SQLite DB — kill it.
151
+ _log.warning(
152
+ "Predecessor PID %d shares our IDE host %d — duplicate from a "
153
+ "transport reconnect, terminating it",
154
+ old_pid, pred_host,
155
+ )
137
156
  try:
138
157
  proc = psutil.Process(old_pid)
139
158
  _log.warning("Killing stale CodeBrain MCP predecessor PID %d", old_pid)
@@ -300,17 +319,23 @@ def install_watchdogs(repo_root: Path | None = None) -> None:
300
319
  start = time.time()
301
320
  mark_activity()
302
321
 
322
+ # Resolve our own IDE host FIRST — the predecessor check needs it to
323
+ # distinguish "our own session's orphan after a transport reconnect"
324
+ # (kill) from "a concurrent sibling window's server" (spare).
325
+ immediate_ppid = os.getppid()
326
+ initial_ppid, initial_create_time, host_anchored = _find_watch_target(immediate_ppid)
327
+ idle_timeout = _effective_idle_timeout(host_anchored)
328
+
303
329
  if repo_root is not None:
304
330
  from codebrain.config import CODEBRAIN_DIR
305
331
  pid_file = repo_root / CODEBRAIN_DIR / PID_FILE_NAME
306
- _kill_stale_predecessor(pid_file)
332
+ _kill_stale_predecessor(
333
+ pid_file,
334
+ own_host_pid=initial_ppid if host_anchored else None,
335
+ )
307
336
  _write_pid_file(pid_file)
308
337
  atexit.register(_remove_pid_file, pid_file)
309
338
 
310
- immediate_ppid = os.getppid()
311
- initial_ppid, initial_create_time, host_anchored = _find_watch_target(immediate_ppid)
312
- idle_timeout = _effective_idle_timeout(host_anchored)
313
-
314
339
  _log.info(
315
340
  "MCP watchdogs installed (ppid=%d via=%d, host_anchored=%s, "
316
341
  "idle_timeout=%ds, max_lifetime=%ds)",
codebrain/mcp_server.py CHANGED
@@ -110,6 +110,125 @@ def _make_store():
110
110
  return GraphStore(_find_db())
111
111
 
112
112
 
113
+ # ---------------------------------------------------------------------------
114
+ # Freshness-on-read
115
+ #
116
+ # Correctness must not depend on which background processes happen to be
117
+ # alive. The watcher (instant) and the startup catch-up sync (thorough) are
118
+ # optimizations; THIS is the guarantee: before serving a tool call, cheaply
119
+ # verify the index matches the working tree and sync the difference. Even if
120
+ # every watcher thread is dead, the answer the agent gets reflects current
121
+ # code.
122
+ # ---------------------------------------------------------------------------
123
+
124
+ import threading as _threading
125
+ import time as _time
126
+
127
+ FRESHNESS_INTERVAL_SECONDS = float(os.environ.get("CODEBRAIN_FRESHNESS_INTERVAL", "5"))
128
+ # Above this many stale files, sync in the background instead of inline so a
129
+ # huge drift (e.g. branch switch on a big repo) can't eat the tool deadline.
130
+ FRESHNESS_INLINE_LIMIT = 200
131
+
132
+ _freshness_lock = _threading.Lock()
133
+ _last_freshness_check: dict[str, float] = {}
134
+
135
+ # Tools that must NOT trigger a freshness sync:
136
+ # - validators compare new/written content against the PRE-change graph;
137
+ # syncing first would erase the baseline and the structural gate would
138
+ # silently pass everything
139
+ # - reindex_codebase rebuilds anyway
140
+ # - project/memory tools don't read graph data
141
+ _FRESHNESS_EXEMPT_TOOLS = frozenset({
142
+ "validate_change",
143
+ "validate_changes",
144
+ "validate_after_write",
145
+ "propose_change",
146
+ "diff_impact",
147
+ "get_validation_status",
148
+ "reindex_codebase",
149
+ "set_project",
150
+ "get_project",
151
+ "save_memory",
152
+ "recall_memories",
153
+ "list_memories",
154
+ "update_memory",
155
+ "delete_memory",
156
+ })
157
+
158
+
159
+ def _ensure_fresh() -> None:
160
+ """Best-effort staleness sync before serving a tool call.
161
+
162
+ Throttled per database; never blocks on a concurrent sync (a slightly
163
+ stale answer beats a deadlocked one); never raises.
164
+ """
165
+ if FRESHNESS_INTERVAL_SECONDS <= 0:
166
+ return
167
+ try:
168
+ db_path = _find_db()
169
+ except FileNotFoundError:
170
+ return
171
+ key = str(db_path)
172
+ now = _time.monotonic()
173
+ if now - _last_freshness_check.get(key, 0.0) < FRESHNESS_INTERVAL_SECONDS:
174
+ return
175
+ if not _freshness_lock.acquire(blocking=False):
176
+ return # another sync in flight — serve what we have
177
+ try:
178
+ _last_freshness_check[key] = now
179
+ from codebrain.graph.store import GraphStore
180
+ from codebrain.indexer import incremental_update, scan_stale
181
+
182
+ repo_root = db_path.parent.parent
183
+ with GraphStore(db_path) as store:
184
+ changed, deleted = scan_stale(repo_root, store)
185
+ if not changed and not deleted:
186
+ return
187
+ if len(changed) + len(deleted) > FRESHNESS_INLINE_LIMIT:
188
+ _log.info(
189
+ "Freshness: %d files drifted — syncing in background",
190
+ len(changed) + len(deleted),
191
+ )
192
+ _threading.Thread(
193
+ target=_background_freshness_sync,
194
+ args=(repo_root, db_path, changed, deleted),
195
+ name="cb-freshness-sync",
196
+ daemon=True,
197
+ ).start()
198
+ return
199
+ result = incremental_update(repo_root, changed, deleted, store)
200
+ if result["files_updated"] or result["files_removed"]:
201
+ _log.info(
202
+ "Freshness: updated %d, removed %d (%.3fs)",
203
+ result["files_updated"],
204
+ result["files_removed"],
205
+ result["elapsed_seconds"],
206
+ )
207
+ except Exception as exc:
208
+ _log.debug("Freshness sync skipped: %s", exc)
209
+ finally:
210
+ _freshness_lock.release()
211
+
212
+
213
+ def _background_freshness_sync(
214
+ repo_root: Path, db_path: Path, changed: list, deleted: list,
215
+ ) -> None:
216
+ try:
217
+ from codebrain.graph.store import GraphStore
218
+ from codebrain.indexer import incremental_update
219
+
220
+ with GraphStore(db_path) as store:
221
+ result = incremental_update(repo_root, changed, deleted, store)
222
+ _log.info(
223
+ "Background freshness sync: updated %d, removed %d (%.3fs)",
224
+ result["files_updated"],
225
+ result["files_removed"],
226
+ result["elapsed_seconds"],
227
+ )
228
+ except Exception as exc:
229
+ _log.warning("Background freshness sync failed: %s", exc)
230
+
231
+
113
232
  def _safe_tool(fn): # noqa: ANN001, ANN201
114
233
  """Wrap an MCP tool so it cooperates with MCP cancellation.
115
234
 
@@ -214,6 +333,10 @@ def _run_sync_protected(fn, args, kwargs): # noqa: ANN001, ANN202
214
333
  saved_stdout = sys.stdout
215
334
  sys.stdout = sys.stderr
216
335
  try:
336
+ # Freshness gate: sync index drift before answering. Skip for tools
337
+ # that rebuild or don't read the graph state being synced.
338
+ if fn.__name__ not in _FRESHNESS_EXEMPT_TOOLS:
339
+ _ensure_fresh()
217
340
  return fn(*args, **kwargs)
218
341
  finally:
219
342
  sys.stdout = saved_stdout
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebrain
3
- Version: 0.3.5
3
+ Version: 0.4.0
4
4
  Summary: Know what breaks before you break it. Structural knowledge graph for codebases — impact analysis, dead code detection, health scores. No LLM required.
5
5
  Author: CodeBrain Contributors
6
6
  License: MIT License
@@ -1,11 +1,11 @@
1
- codebrain/__init__.py,sha256=x27U2sCVt0bfIOA_zO8t7cwyzq-sFjkbLTNm-MpjHME,392
1
+ codebrain/__init__.py,sha256=HrQ5FQd5GTjIxch8KWjHo5Q2CjcKK-ffXNRmY8Cr8ug,392
2
2
  codebrain/__main__.py,sha256=dgd9lRZovV1k1tScEW_wvPPjPEACXc-EcLtJ7AN3M48,115
3
3
  codebrain/agent_bridge.py,sha256=JDny3232R6rfFsQTWOoYygGgykx6rb8ektLRENPUcDQ,6262
4
4
  codebrain/analyzer.py,sha256=RxyajZp0E8XuB_1SkK1UEXD00gOo3e2vtBzS-US6ms8,40701
5
5
  codebrain/api.py,sha256=LmeFK3KyUKR1xz4tAoPteaRA06c09KA965Hk33VhV6I,31653
6
6
  codebrain/api_models.py,sha256=mE5sIsf8WLEWjGm2w6-baeyVoJH_65FOglHwFe3aJhs,1989
7
7
  codebrain/architecture.py,sha256=RCN_LUNTjd0fXYFIYDacwH17qEeFCxNCyF6irbs5yfE,29485
8
- codebrain/cli.py,sha256=UmiCV5O05-I9FdEiuXoJ96WJdBntxMud7al8sOMteOM,169015
8
+ codebrain/cli.py,sha256=RbXLvRDMXihedsPH-CxbQjWTvFyJbsyFCdtRhMbVk2s,170135
9
9
  codebrain/comprehension.py,sha256=eAVf1abecxVo72_2p8aEgRmSeXR7VNMyJnt_DTst47E,83023
10
10
  codebrain/config.py,sha256=UvStpfDNvulMHkN7xnGjEF3EKvdf9tF3Cv4A01FdEQw,1745
11
11
  codebrain/context.py,sha256=BFZ_WWRf242cJli-TXoz5JFqcvsKkwbmuUMBX8WvM9o,10980
@@ -18,13 +18,13 @@ codebrain/export.py,sha256=A4zdzJ2MVmo-sxJQjwoZlMRkm-QXCxE14lYDm9RrQEY,11576
18
18
  codebrain/frontend.py,sha256=4kFIcWoPKS_pLaWAirmkJmYSYTlicCsi_GZgLLjunng,17675
19
19
  codebrain/hook_runner.py,sha256=1shd-lw8Rx8zNOuJfgcbUOzhsNUVIwhmuzDpTQsdnHo,5458
20
20
  codebrain/hooks.py,sha256=tT2Dx_T0iZLEQarL7QYvvVObYfelMUKVNXNz74MU-tM,3179
21
- codebrain/indexer.py,sha256=3xIe8m_zPTKJlq7dAlvyTWP3J2UsF4ondy8ndgm6dSQ,18900
21
+ codebrain/indexer.py,sha256=NaMj-1ycT4cINJfgykjZC9cAs7cxVLfaojY0AKxaEZw,21083
22
22
  codebrain/kt.py,sha256=iXeHArPbSrrr2ObH4nuyXPobdBXSOR3b7JbIqXB0kj4,17199
23
23
  codebrain/kt_video.py,sha256=quaUyPUJh8xXsj52aBCPp_L8fekNC4vHmNYrUTfCqcE,29211
24
24
  codebrain/llm.py,sha256=0D5c5BJMVkLz2OoybfMxlUdJEbz16cZoNgVmm-LzFH0,25606
25
25
  codebrain/logging.py,sha256=ORR5L8REVlh4aJz9vqErmi76aqjLN2xVKoA2gSv_jas,1110
26
- codebrain/mcp_lifecycle.py,sha256=fpwajl4-5YdZe0cEMmE2_o_FFkyrh3bl3Jqwrp-XGAM,15332
27
- codebrain/mcp_server.py,sha256=Lb5ob3NI-S0Ln7Yp8G8HxWsKvAuzVzl123N08ZkxPyw,110648
26
+ codebrain/mcp_lifecycle.py,sha256=xo-T1-RNCzFRKhWcKUkEF6ZahPCISp6swTfwRrKazGg,16598
27
+ codebrain/mcp_server.py,sha256=tdSmB4w6KYcXi1lHxcxle-adR12Nm-v7mRSoxBDKiD4,115349
28
28
  codebrain/migration.py,sha256=ZMcug6OsvvK-DVdfmqhWCUx-oVj0KQD0EGlBVx6Jxvk,29489
29
29
  codebrain/modernize.py,sha256=4usRjIFONEThpYhemLIjoYXb9ohEbO7j2wzVO15Dz5A,38525
30
30
  codebrain/onboard.py,sha256=m63A0A8pJdZUiGFgAlRD1ySe7Bk_Eo_fiTPzLfkL1ig,15270
@@ -48,7 +48,7 @@ codebrain/actions/test_gen.py,sha256=PAnHfYuLp-AIF4YrQYQaBH5ADWhCqUVEI7bFDquxPjs
48
48
  codebrain/graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  codebrain/graph/query.py,sha256=FdkWTIKteCdAGnal01LJDuoSXzSpNp3AT9e0AH-m1eg,44131
50
50
  codebrain/graph/schema.py,sha256=5PItrNUMQt37kTsL3SUVLPB7rP2JtQXx-Y4uxX0A0H0,6521
51
- codebrain/graph/store.py,sha256=pqJu7c6krXgBYDQ5e-KZ8nHWVvNU4OeeXVtDVSNY7pI,15937
51
+ codebrain/graph/store.py,sha256=x8xkOrSGWQexIjEKP09CwCo8v1E1W4w34QQAUC8gvyE,16272
52
52
  codebrain/memory/__init__.py,sha256=PuFR35m1GPIbRAuUxKu4JYBDlPXJWMPbyrWiggFT2sU,158
53
53
  codebrain/memory/store.py,sha256=W31fe4aNTkdi-Woe8bZ8SZ9aUHpWrfBkb-9dQs0geyI,9394
54
54
  codebrain/parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -74,9 +74,9 @@ codebrain/parser/typescript_treesitter.py,sha256=7RTdcA-HTgrlRhEMwSGa-63saNcjqtP
74
74
  codebrain/parser/vue_parser.py,sha256=ZWtjUOyItn_SNfvD6T8j7alV4aIfcHA94WDCS5P59Ps,13054
75
75
  codebrain/watcher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
76
  codebrain/watcher/file_watcher.py,sha256=6x-fWIP5YBuwq5BCX6HOV-Kc8EVgE4JUoeLxYJtxDf0,9454
77
- codebrain-0.3.5.dist-info/licenses/LICENSE,sha256=Dxb0L3H90lGFFik90WQXfMkM_8utGA1BDqizqdCT3UE,1079
78
- codebrain-0.3.5.dist-info/METADATA,sha256=As_yyzhW3AO24DXeNzezMPpyvNb8HYb8G6LhrtVUIGs,10129
79
- codebrain-0.3.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
80
- codebrain-0.3.5.dist-info/entry_points.txt,sha256=FHKSyI7le7GK78HxAKQK54-yzt-yHNxX0df3VpGC5wg,44
81
- codebrain-0.3.5.dist-info/top_level.txt,sha256=mUxCZc80EyNOMzd2vAm22uIhnOb1Aw7ZOAUT_u-ksx4,10
82
- codebrain-0.3.5.dist-info/RECORD,,
77
+ codebrain-0.4.0.dist-info/licenses/LICENSE,sha256=Dxb0L3H90lGFFik90WQXfMkM_8utGA1BDqizqdCT3UE,1079
78
+ codebrain-0.4.0.dist-info/METADATA,sha256=W-Gb7JPxHu6eSD_bDmm42i9NU7TLxxls8Ylp5mTEbsw,10129
79
+ codebrain-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
80
+ codebrain-0.4.0.dist-info/entry_points.txt,sha256=FHKSyI7le7GK78HxAKQK54-yzt-yHNxX0df3VpGC5wg,44
81
+ codebrain-0.4.0.dist-info/top_level.txt,sha256=mUxCZc80EyNOMzd2vAm22uIhnOb1Aw7ZOAUT_u-ksx4,10
82
+ codebrain-0.4.0.dist-info/RECORD,,