code-context-control 2.129.0__py3-none-any.whl → 2.129.1__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.
cli/c3.py CHANGED
@@ -91,7 +91,7 @@ console = Console() if HAS_RICH else None
91
91
  # Config
92
92
  CONFIG_DIR = ".c3"
93
93
  CONFIG_FILE = ".c3/config.json"
94
- __version__ = "2.129.0"
94
+ __version__ = "2.129.1"
95
95
 
96
96
 
97
97
  def _compress_file_cli(compressor, path, mode="smart", **kw):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.129.0
3
+ Version: 2.129.1
4
4
  Summary: Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub.
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -1,7 +1,7 @@
1
1
  cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
2
2
  cli/_hook_utils.py,sha256=gAcd2dS9nVmnUocSv3YvLnaPJnhOJiuYdwSHp-1mXxg,22598
3
3
  cli/_shell_writes.py,sha256=XzUNlUDljmg-wZxK03xHnRe36WeQxe4tpO9HKVFwP5o,5090
4
- cli/c3.py,sha256=v3t7mHPGhgmbgTmtL7WhvMXuMqjz2IfPerPixQ0-emQ,419777
4
+ cli/c3.py,sha256=lUfUuoifk0fH9_mmbK1GnQ4z9Inv4QywMC2Waocxsk8,419777
5
5
  cli/docs.html,sha256=X9M7qC7LWi8pPJNF-4yEM57jhuZ3WeTESECbdDOeNaA,143172
6
6
  cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
7
7
  cli/hook_access_guard.py,sha256=ba8-un6G9Ap8HCzx7nyhVq3c_G4sOtiVXRy0BzzX8-Y,23349
@@ -125,7 +125,7 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
125
125
  cli/ui/components/settings.js,sha256=ATbAjBlVIwCNpxq7s191b49a_INQV38iwmySqtJLYwY,79066
126
126
  cli/ui/components/sidebar.js,sha256=K2ym2kUgpbyG-EBA_wBIIiqQY8qTatsiBZwzseMWuIQ,9939
127
127
  cli/ui/components/tasks.js,sha256=vyKQ3uwoppMwvdEaHlhWXW4oWcAisx4NveqzMhsYqHo,38438
128
- code_context_control-2.129.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
128
+ code_context_control-2.129.1.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
129
129
  core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
130
130
  core/codex_patch.py,sha256=CxvT_rQIwPNTRJZj9EHJmx0ZdOeSLxjiixuLI3Z7lPc,2107
131
131
  core/config.py,sha256=YmkcZwedz_lfDM0ZuI_f4xpe98ixPLcSQFcTCHgRiRg,19206
@@ -286,7 +286,7 @@ services/tool_classifier.py,sha256=Fgvq0ZcpnCskwtO8a3YI1MiecPNnw6UbPyJQIUwgfiQ,6
286
286
  services/transcript_index.py,sha256=VQhvgkSyLVEYamXi_YIbiIhBnd0mqFHZlWG2HQu_1EA,12144
287
287
  services/validation_cache.py,sha256=skFYR7CkSGvFb2gq6dfxOnvjQzz5Boa3jJWT7UfaIhY,5587
288
288
  services/vector_store.py,sha256=satc4MJanC1Wsu_iveMCUXG6K2fd0Ke87JSyy2BMikw,13041
289
- services/watcher.py,sha256=xsMlXy4VxX5_CXGQDfmOeZrvlU42S2mGubZgTheCS_s,7859
289
+ services/watcher.py,sha256=WF6aTIUFzg5B5KepuLyUwmx8WvbmhT9IeenzdK0df3U,10779
290
290
  services/win_subprocess.py,sha256=nt8Uwc2PrqsRW3-jcViEsoKj56GudW8TV3CQwryx8y4,4271
291
291
  services/bench/__init__.py,sha256=WLEJIWJeaUj6FnH2nDO1qWugJDKfOKeM6WvKLSreYjI,231
292
292
  services/bench/map_eval.py,sha256=i0M3hBJQM9wXX4dMB_2LjVKoFtXzFUKapoqjGB_8tcE,43456
@@ -313,8 +313,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
313
313
  tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
314
314
  tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
315
315
  tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
316
- code_context_control-2.129.0.dist-info/METADATA,sha256=zlAuf6JoFmllOMxfOknPOySE7RqimVWynRX8IXH9ajo,28116
317
- code_context_control-2.129.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
318
- code_context_control-2.129.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
319
- code_context_control-2.129.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
320
- code_context_control-2.129.0.dist-info/RECORD,,
316
+ code_context_control-2.129.1.dist-info/METADATA,sha256=U-XNbatMHRQ60Wd54ZPtUY-Qt_WrwnxXJ9ldnr6GalA,28116
317
+ code_context_control-2.129.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
318
+ code_context_control-2.129.1.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
319
+ code_context_control-2.129.1.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
320
+ code_context_control-2.129.1.dist-info/RECORD,,
services/watcher.py CHANGED
@@ -7,6 +7,7 @@ Watches project files for changes and tracks modifications:
7
7
  - Accumulates changes for session logging
8
8
  - Triggers index rebuild when enough changes accumulate
9
9
  """
10
+ import os
10
11
  import threading
11
12
  from datetime import datetime, timezone
12
13
  from pathlib import Path
@@ -14,6 +15,8 @@ from pathlib import Path
14
15
  from watchdog.events import FileSystemEventHandler
15
16
  from watchdog.observers import Observer
16
17
 
18
+ from services.scanner import SKIP_DIRS, is_nested_checkout, make_dir_pruner
19
+
17
20
  # Extensions to watch
18
21
  CODE_EXTENSIONS = {
19
22
  '.py', '.js', '.ts', '.tsx', '.jsx', '.r', '.R',
@@ -21,32 +24,93 @@ CODE_EXTENSIONS = {
21
24
  '.sh', '.sql', '.go', '.rs', '.java', '.cpp', '.c', '.h',
22
25
  }
23
26
 
24
- # Directories to skip
25
- SKIP_DIRS = {
26
- 'node_modules', '.git', '__pycache__', '.c3', 'venv',
27
- 'env', '.venv', 'dist', 'build', '.next', '.cache',
28
- 'coverage', '.pytest_cache',
29
- }
27
+ # SKIP_DIRS is re-exported from services.scanner: the watcher and the index
28
+ # walker must agree on what is not part of the project (see _ChangeHandler).
29
+ __all__ = ['CODE_EXTENSIONS', 'SKIP_DIRS', 'CodeWatcher']
30
30
 
31
31
 
32
32
  class _ChangeHandler(FileSystemEventHandler):
33
- """Collects file change events."""
33
+ """Collects file change events.
34
+
35
+ The filter is the index scanner's, not a private one. A change only
36
+ counts when the file could be IN the index: the same directory pruning
37
+ as ``scanner.iter_files`` (SKIP_DIRS, the root .gitignore's directory
38
+ entries, nested checkouts such as linked worktrees), then the
39
+ sub-project excluder, then the extension allowlist.
34
40
 
35
- def __init__(self, excluder=None):
41
+ Before 2.129.1 the watcher kept its own shorter skip list and ignored
42
+ .gitignore and worktrees, so state files a daemon rewrites every few
43
+ seconds under a gitignored ``logs/`` counted as source changes. On one
44
+ project that tripped IndexStalenessAgent's rebuild threshold (15) every
45
+ single minute — the "Index auto-rebuilt" notification reached a count of
46
+ 75,968 — and each refresh burst left the MCP server's process heap with
47
+ more pinned 16 MB segments it never gave back: 28 GB of commit charge
48
+ per server with under 1 GB touched, three servers on the project, and
49
+ the C3 Hub dying of MemoryError when the box ran out of commit.
50
+ """
51
+
52
+ def __init__(self, excluder=None, root=None):
36
53
  super().__init__()
37
54
  self._lock = threading.Lock()
38
55
  self._changes = []
39
56
  self._excluder = excluder # sub-project folders tracked by their own .c3
57
+ self._root = Path(root) if root else None
58
+ self._pruned = make_dir_pruner(root) if root else (lambda d: d in SKIP_DIRS)
59
+ # dir path -> is another checkout; one stat per directory, not per event
60
+ self._checkout_cache: dict = {}
61
+
62
+ def _reload_pruner(self):
63
+ """Re-read the root .gitignore (called when it changes)."""
64
+ if self._root is not None:
65
+ self._pruned = make_dir_pruner(self._root)
66
+
67
+ def _rel_parts(self, p: Path) -> tuple:
68
+ if self._root is None:
69
+ return p.parts
70
+ try:
71
+ return p.relative_to(self._root).parts
72
+ except ValueError:
73
+ try:
74
+ return p.resolve().relative_to(self._root).parts
75
+ except (ValueError, OSError):
76
+ return p.parts
77
+
78
+ def _under_nested_checkout(self, rel_dirs: tuple) -> bool:
79
+ if self._root is None:
80
+ return False
81
+ cur = str(self._root)
82
+ for d in rel_dirs:
83
+ cur = os.path.join(cur, d)
84
+ hit = self._checkout_cache.get(cur)
85
+ if hit is None:
86
+ if len(self._checkout_cache) > 4096:
87
+ self._checkout_cache.clear()
88
+ hit = self._checkout_cache[cur] = is_nested_checkout(cur)
89
+ if hit:
90
+ return True
91
+ return False
40
92
 
41
93
  def _should_track(self, path: str) -> bool:
42
94
  p = Path(path)
43
- if any(skip in p.parts for skip in SKIP_DIRS):
95
+ if p.suffix.lower() not in CODE_EXTENSIONS:
96
+ return False
97
+ parts = self._rel_parts(p)
98
+ rel_dirs = parts[:-1]
99
+ if any(self._pruned(d) for d in rel_dirs):
100
+ return False
101
+ if self._under_nested_checkout(rel_dirs):
44
102
  return False
45
103
  if self._excluder is not None and self._excluder(path):
46
104
  return False
47
- return p.suffix.lower() in CODE_EXTENSIONS
105
+ return True
48
106
 
49
107
  def _record(self, event_type: str, path: str):
108
+ if self._root is not None and Path(path).name == '.gitignore':
109
+ try:
110
+ if Path(path).parent.samefile(self._root):
111
+ self._reload_pruner()
112
+ except OSError:
113
+ pass
50
114
  if not self._should_track(path):
51
115
  return
52
116
  with self._lock:
@@ -94,7 +158,7 @@ class CodeWatcher:
94
158
  excluder = make_excluder(self.project_path)
95
159
  except Exception:
96
160
  excluder = None
97
- self._handler = _ChangeHandler(excluder)
161
+ self._handler = _ChangeHandler(excluder, root=self.project_path)
98
162
  self._observer = Observer()
99
163
  self._observer.daemon = True
100
164
  self._file_memory = None