code-context-control 2.53.0__py3-none-any.whl → 2.54.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.
- cli/c3.py +54 -17
- cli/commands/parser.py +1 -0
- cli/hub_server.py +4 -2
- cli/progress.py +49 -0
- {code_context_control-2.53.0.dist-info → code_context_control-2.54.0.dist-info}/METADATA +1 -1
- {code_context_control-2.53.0.dist-info → code_context_control-2.54.0.dist-info}/RECORD +17 -15
- services/compressor.py +5 -13
- services/doc_index.py +21 -17
- services/embedding_index.py +49 -5
- services/indexer.py +58 -21
- services/protocol.py +37 -26
- services/scanner.py +120 -0
- services/subprojects.py +3 -1
- {code_context_control-2.53.0.dist-info → code_context_control-2.54.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.53.0.dist-info → code_context_control-2.54.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.53.0.dist-info → code_context_control-2.54.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.53.0.dist-info → code_context_control-2.54.0.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
|
@@ -92,7 +92,7 @@ console = Console() if HAS_RICH else None
|
|
|
92
92
|
# Config
|
|
93
93
|
CONFIG_DIR = ".c3"
|
|
94
94
|
CONFIG_FILE = ".c3/config.json"
|
|
95
|
-
__version__ = "2.
|
|
95
|
+
__version__ = "2.54.0"
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def _command_deps() -> CommandDeps:
|
|
@@ -165,6 +165,7 @@ def _build_init_config(project_path: str) -> dict:
|
|
|
165
165
|
"project_path": project_path,
|
|
166
166
|
"version": __version__,
|
|
167
167
|
"index_auto_update": True,
|
|
168
|
+
"index_max_files": 2000,
|
|
168
169
|
"compression_mode": "smart",
|
|
169
170
|
"mcp": {"mode": "direct"},
|
|
170
171
|
"hybrid": deepcopy(HYBRID_DEFAULTS),
|
|
@@ -890,7 +891,7 @@ def _parse_cli_ide_arg(value: str) -> str:
|
|
|
890
891
|
return normalized
|
|
891
892
|
|
|
892
893
|
|
|
893
|
-
def _do_init(project_path: str, ide_name: str = None):
|
|
894
|
+
def _do_init(project_path: str, ide_name: str = None, no_embed: bool = False):
|
|
894
895
|
"""Run the core init steps (shared by new install and re-init after clear/reset)."""
|
|
895
896
|
config = _build_init_config(project_path)
|
|
896
897
|
save_config(config, project_path)
|
|
@@ -898,10 +899,27 @@ def _do_init(project_path: str, ide_name: str = None):
|
|
|
898
899
|
for subdir in _C3_INIT_SUBDIRS:
|
|
899
900
|
(Path(project_path) / CONFIG_DIR / subdir).mkdir(parents=True, exist_ok=True)
|
|
900
901
|
|
|
902
|
+
import time as _t
|
|
903
|
+
|
|
904
|
+
from cli.progress import ProgressLine
|
|
905
|
+
|
|
901
906
|
print("Building code index...")
|
|
907
|
+
_prog = ProgressLine()
|
|
908
|
+
_t0 = _t.perf_counter()
|
|
902
909
|
indexer = CodeIndex(project_path)
|
|
903
|
-
result = indexer.build_index(
|
|
904
|
-
|
|
910
|
+
result = indexer.build_index(
|
|
911
|
+
on_progress=lambda entries, files, chunks: _prog.update(
|
|
912
|
+
f" scanning: {entries:,} entries | {files:,} files | {chunks:,} chunks"))
|
|
913
|
+
_prog.done()
|
|
914
|
+
entries = result.get("entries_scanned", 0)
|
|
915
|
+
print(f" Indexed {result['files_indexed']} files, {result['chunks_created']} chunks "
|
|
916
|
+
f"in {_t.perf_counter() - _t0:.1f}s ({entries:,} entries scanned)")
|
|
917
|
+
capped = result.get("files_capped", 0)
|
|
918
|
+
if capped:
|
|
919
|
+
total = result["files_indexed"] + capped
|
|
920
|
+
print(f" [!] Indexed {result['files_indexed']} of {total} candidate files "
|
|
921
|
+
f"(cap: index_max_files={result.get('max_files')}).")
|
|
922
|
+
print(" Raise index_max_files in .c3/config.json for full coverage.")
|
|
905
923
|
|
|
906
924
|
# Build embedding index if Ollama is available (non-blocking on failure)
|
|
907
925
|
try:
|
|
@@ -912,13 +930,24 @@ def _do_init(project_path: str, ide_name: str = None):
|
|
|
912
930
|
ollama = OllamaClient(ollama_url)
|
|
913
931
|
embed_model = config.get("embed_model", "nomic-embed-text")
|
|
914
932
|
ei = EmbeddingIndex(project_path, ollama, embed_model=embed_model)
|
|
915
|
-
if
|
|
933
|
+
if no_embed:
|
|
934
|
+
print(" Embedding index skipped (--no-embed)")
|
|
935
|
+
elif ei.probe()["ready"]:
|
|
936
|
+
# probe() initializes the lazy backends; checking .ready on a
|
|
937
|
+
# fresh instance is always False and silently skipped the build.
|
|
916
938
|
print("Building embedding index...")
|
|
917
|
-
|
|
939
|
+
_t0 = _t.perf_counter()
|
|
940
|
+
_eprog = ProgressLine()
|
|
941
|
+
ei_result = ei.build(
|
|
942
|
+
indexer,
|
|
943
|
+
on_progress=lambda done, total, chunks: _eprog.update(
|
|
944
|
+
f" embedding: file {done}/{total} | {chunks:,} chunks"))
|
|
945
|
+
_eprog.done()
|
|
918
946
|
print(f" Embedded {ei_result.get('chunks_embedded', 0)} chunks "
|
|
919
|
-
f"({ei_result.get('files_processed', 0)} files)"
|
|
947
|
+
f"({ei_result.get('files_processed', 0)} files) "
|
|
948
|
+
f"in {_t.perf_counter() - _t0:.1f}s")
|
|
920
949
|
else:
|
|
921
|
-
print(" Embedding index skipped (
|
|
950
|
+
print(f" Embedding index skipped ({ei.unavailable_reason()})")
|
|
922
951
|
except Exception:
|
|
923
952
|
_log.debug("Embedding index build failed", exc_info=True)
|
|
924
953
|
|
|
@@ -926,15 +955,22 @@ def _do_init(project_path: str, ide_name: str = None):
|
|
|
926
955
|
try:
|
|
927
956
|
from services.doc_index import DocIndex
|
|
928
957
|
print("Building doc index...")
|
|
958
|
+
_t0 = _t.perf_counter()
|
|
959
|
+
_dprog = ProgressLine()
|
|
929
960
|
di = DocIndex(project_path)
|
|
930
|
-
di_result = di.build(
|
|
931
|
-
|
|
961
|
+
di_result = di.build(
|
|
962
|
+
on_progress=lambda done, total: _dprog.update(
|
|
963
|
+
f" docs: {done}/{total}"))
|
|
964
|
+
_dprog.done()
|
|
965
|
+
print(f" Indexed {di_result['docs_indexed']} docs, "
|
|
966
|
+
f"{di_result['chunks_created']} chunks in {_t.perf_counter() - _t0:.1f}s")
|
|
932
967
|
except Exception:
|
|
933
968
|
_log.debug("Doc index build failed", exc_info=True)
|
|
934
969
|
|
|
935
970
|
print("Building compression dictionary...")
|
|
936
971
|
protocol = CompressionProtocol(project_path)
|
|
937
|
-
|
|
972
|
+
# Mine the in-memory index instead of re-reading the whole tree.
|
|
973
|
+
new_terms = protocol.build_project_dictionary(code_index=indexer)
|
|
938
974
|
print(f" Added {len(new_terms)} project-specific terms")
|
|
939
975
|
|
|
940
976
|
from core.ide import detect_ide, load_ide_config
|
|
@@ -966,11 +1002,12 @@ def cmd_init(args):
|
|
|
966
1002
|
if requested_ide != "auto":
|
|
967
1003
|
requested_ide = normalize_ide_name(requested_ide)
|
|
968
1004
|
git_requested = getattr(args, "git", False)
|
|
1005
|
+
no_embed = bool(getattr(args, "no_embed", False))
|
|
969
1006
|
|
|
970
1007
|
# ── Brand-new install ──────────────────────────────────────
|
|
971
1008
|
if not c3_dir.exists() or not (c3_dir / "config.json").exists():
|
|
972
1009
|
print_header(f"Initializing C3 for: {project_path}")
|
|
973
|
-
_do_init(project_path, ide_name=requested_ide)
|
|
1010
|
+
_do_init(project_path, ide_name=requested_ide, no_embed=no_embed)
|
|
974
1011
|
try:
|
|
975
1012
|
from services.project_manager import ProjectManager
|
|
976
1013
|
ProjectManager().add_project(project_path)
|
|
@@ -1123,7 +1160,7 @@ def cmd_init(args):
|
|
|
1123
1160
|
ti_manifest = c3_dir / "transcript_index" / "manifest.json"
|
|
1124
1161
|
if ti_manifest.exists():
|
|
1125
1162
|
ti_manifest.write_text("{}", encoding="utf-8")
|
|
1126
|
-
_do_init(project_path, ide_name=requested_ide)
|
|
1163
|
+
_do_init(project_path, ide_name=requested_ide, no_embed=no_embed)
|
|
1127
1164
|
if git_requested:
|
|
1128
1165
|
_init_local_git_repo(project_path)
|
|
1129
1166
|
_run_install_mcp(project_path, requested_ide, mcp_mode=getattr(args, "mcp_mode", "direct"), banner="Updating MCP tools...")
|
|
@@ -1152,7 +1189,7 @@ def cmd_init(args):
|
|
|
1152
1189
|
# ── Non-interactive (--force) ──────────────────────────────
|
|
1153
1190
|
if getattr(args, "force", False):
|
|
1154
1191
|
print("\n[--force] Applying update...")
|
|
1155
|
-
_do_init(project_path, ide_name=requested_ide)
|
|
1192
|
+
_do_init(project_path, ide_name=requested_ide, no_embed=no_embed)
|
|
1156
1193
|
if git_requested:
|
|
1157
1194
|
_init_local_git_repo(project_path)
|
|
1158
1195
|
_run_install_mcp(project_path, requested_ide, mcp_mode=getattr(args, "mcp_mode", "direct"), banner="Updating MCP tools...")
|
|
@@ -1176,7 +1213,7 @@ def cmd_init(args):
|
|
|
1176
1213
|
|
|
1177
1214
|
if selected.startswith("Update"):
|
|
1178
1215
|
print()
|
|
1179
|
-
_do_init(project_path, ide_name=requested_ide)
|
|
1216
|
+
_do_init(project_path, ide_name=requested_ide, no_embed=no_embed)
|
|
1180
1217
|
if git_requested:
|
|
1181
1218
|
_init_local_git_repo(project_path)
|
|
1182
1219
|
_prompt_install_mcp(project_path, requested_ide, default_mode=getattr(args, "mcp_mode", "direct"), banner="Updating MCP tools...")
|
|
@@ -1192,7 +1229,7 @@ def cmd_init(args):
|
|
|
1192
1229
|
shutil.rmtree(target)
|
|
1193
1230
|
print(f" Removed .c3/{subdir}/")
|
|
1194
1231
|
print()
|
|
1195
|
-
_do_init(project_path, ide_name=requested_ide)
|
|
1232
|
+
_do_init(project_path, ide_name=requested_ide, no_embed=no_embed)
|
|
1196
1233
|
if git_requested:
|
|
1197
1234
|
_init_local_git_repo(project_path)
|
|
1198
1235
|
_prompt_install_mcp(project_path, requested_ide, default_mode=getattr(args, "mcp_mode", "direct"), banner="Updating MCP tools...")
|
|
@@ -1209,7 +1246,7 @@ def cmd_init(args):
|
|
|
1209
1246
|
shutil.rmtree(c3_dir)
|
|
1210
1247
|
print(" Deleted .c3/")
|
|
1211
1248
|
print()
|
|
1212
|
-
_do_init(project_path, ide_name=requested_ide)
|
|
1249
|
+
_do_init(project_path, ide_name=requested_ide, no_embed=no_embed)
|
|
1213
1250
|
if git_requested:
|
|
1214
1251
|
_init_local_git_repo(project_path)
|
|
1215
1252
|
_prompt_install_mcp(project_path, requested_ide, default_mode=getattr(args, "mcp_mode", "direct"), banner="Re-installing MCP tools...")
|
cli/commands/parser.py
CHANGED
|
@@ -24,6 +24,7 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
24
24
|
p_init.add_argument("--ide", default="auto", type=parse_cli_ide_arg, metavar="{auto,claude,vscode,cursor,codex,antigravity}", help="Target IDE for MCP config (default: auto-detect)")
|
|
25
25
|
p_init.add_argument("--mcp-mode", choices=["direct", "proxy"], default="direct", help="Default MCP mode if install is selected during init (default: direct)")
|
|
26
26
|
p_init.add_argument("--git", action="store_true", help="Initialize a local Git repository during init/update")
|
|
27
|
+
p_init.add_argument("--no-embed", action="store_true", help="Skip building the semantic embedding index during init")
|
|
27
28
|
p_init.add_argument("--permissions", choices=["read-only", "c3-strict", "standard", "permissive"], default=None, help="Apply Claude Code permission tier (Claude Code only, used with --force)")
|
|
28
29
|
p_init.add_argument("--include-mcp-wildcard", action="store_true", help="Add mcp__* wildcard so non-C3 MCP servers don't prompt per-call")
|
|
29
30
|
|
cli/hub_server.py
CHANGED
|
@@ -1027,8 +1027,10 @@ def api_run_component():
|
|
|
1027
1027
|
ollama = OllamaClient(ollama_url)
|
|
1028
1028
|
embed_model = cfg.get("embed_model", "nomic-embed-text")
|
|
1029
1029
|
ei = EmbeddingIndex(resolved, ollama, embed_model=embed_model)
|
|
1030
|
-
|
|
1031
|
-
|
|
1030
|
+
# probe() initializes lazy backends; a fresh instance's .ready
|
|
1031
|
+
# is always False and would skip the build unconditionally.
|
|
1032
|
+
if not ei.probe()["ready"]:
|
|
1033
|
+
return jsonify({"success": True, "output": f"Embedding index skipped ({ei.unavailable_reason()})."})
|
|
1032
1034
|
indexer = CodeIndex(resolved)
|
|
1033
1035
|
if not indexer.chunks:
|
|
1034
1036
|
indexer._load_index()
|
cli/progress.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Single-line TTY progress for long CLI phases (index/embedding builds).
|
|
3
|
+
|
|
4
|
+
Quiet when stdout is piped - CI logs and command substitutions see only
|
|
5
|
+
the final summary lines, never carriage-return spam.
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProgressLine:
|
|
12
|
+
"""Rewrites one status line in place; time-throttled; TTY-only."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, stream=None, min_interval: float = 0.1):
|
|
15
|
+
self.stream = stream if stream is not None else sys.stdout
|
|
16
|
+
self.min_interval = min_interval
|
|
17
|
+
self._last = 0.0
|
|
18
|
+
self._width = 0
|
|
19
|
+
try:
|
|
20
|
+
self._tty = bool(self.stream.isatty())
|
|
21
|
+
except Exception:
|
|
22
|
+
self._tty = False
|
|
23
|
+
|
|
24
|
+
def update(self, text: str) -> None:
|
|
25
|
+
if not self._tty:
|
|
26
|
+
return
|
|
27
|
+
now = time.monotonic()
|
|
28
|
+
if now - self._last < self.min_interval:
|
|
29
|
+
return
|
|
30
|
+
self._last = now
|
|
31
|
+
pad = ' ' * max(0, self._width - len(text))
|
|
32
|
+
try:
|
|
33
|
+
self.stream.write('\r' + text + pad)
|
|
34
|
+
self.stream.flush()
|
|
35
|
+
except Exception:
|
|
36
|
+
self._tty = False
|
|
37
|
+
return
|
|
38
|
+
self._width = len(text)
|
|
39
|
+
|
|
40
|
+
def done(self) -> None:
|
|
41
|
+
"""Clear the in-place line so the following prints start clean."""
|
|
42
|
+
if not self._tty or not self._width:
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
self.stream.write('\r' + ' ' * self._width + '\r')
|
|
46
|
+
self.stream.flush()
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
self._width = 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.54.0
|
|
4
4
|
Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Copilot, Cursor, Antigravity). Retrieve less, read less, edit safer — and version the configs that shape your agent (CLAUDE.md, skills, hooks, MCP).
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
|
|
2
2
|
cli/_hook_utils.py,sha256=tJ4ZmXD5Y-JjFWb0uoR7ycYklYFUMnnjF2G7BO-wyBI,12600
|
|
3
|
-
cli/c3.py,sha256=
|
|
3
|
+
cli/c3.py,sha256=jTV4sTDaPkooaJtlwaQOAKYXY4Q6C5RfyioKX35vfTM,309220
|
|
4
4
|
cli/docs.html,sha256=bNymSz7LatnWHjxSXL92QSUtGvB3jBzNHbOV-bRpAOo,142507
|
|
5
5
|
cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
|
|
6
6
|
cli/hook_artifact.py,sha256=Se1CNBfoBFyvJQlRmYdNtdRXIkQp9zaF0O6ndRMo7ts,2198
|
|
@@ -18,17 +18,18 @@ cli/hook_read.py,sha256=ZWZldgqGSNz78COhlgETQ7yxdOW5vWIIHUet9gZKhxk,8176
|
|
|
18
18
|
cli/hook_session_stats.py,sha256=hZiX0r1SLOstu1PXvygWae0cIqHCcC3mb0SbFL6nCic,2083
|
|
19
19
|
cli/hook_terse_advisor.py,sha256=W_zaTfX2akgX-ifuUTsLu2H1atM8XmWt6jpPoeU7Pm0,6252
|
|
20
20
|
cli/hub.html,sha256=Hl-XPZGT1mMiKrbX9c5OsEw6mXEumwIB3vp1WlWaplM,183966
|
|
21
|
-
cli/hub_server.py,sha256=
|
|
21
|
+
cli/hub_server.py,sha256=5y6jIs2qaz5HoBdLXBl2yPtqIZ2VM17rJ4bWUOfUcSs,105443
|
|
22
22
|
cli/hub_ui.html,sha256=nnGqWhWGCLfNisGUnoAlQFcBr6wPNsq-AEOR7uFtVkE,2942
|
|
23
23
|
cli/mcp_proxy.py,sha256=92htuT-p0j-cDTbyqlIJpGoQ85_Aw7UuB8L_Toi_u20,17511
|
|
24
24
|
cli/mcp_server.py,sha256=YgzI2U1jW-ax9HCCVUwXeikMHDMlS4Qq-RdhnQuTvt4,40898
|
|
25
|
+
cli/progress.py,sha256=qTkNy0howOD3hXraMo4ok3dYTpBW5yG_tenKfF4UDM0,1470
|
|
25
26
|
cli/server.py,sha256=WOEPgptkJSVtS25IwrPdOMdbKOh79Sm6-vdQlTOk-sA,136012
|
|
26
27
|
cli/ui.html,sha256=xcdt74nlFEXx-0Bx6-Okw-WSVZPAXL0iukxU0ytI6CA,5694
|
|
27
28
|
cli/ui_legacy.html,sha256=cI8tC6RKmE2NIJOcsu7CY-zT4VznjcbD6NTjxb_fvUY,378460
|
|
28
29
|
cli/ui_nano.html,sha256=UAwQ6bbTOXAoGq191AZ7slhngR9edJSa3IhqpynveDg,27740
|
|
29
30
|
cli/commands/__init__.py,sha256=0Z8MABNzwSFJGT4Xv9R5AJVR8XxraTsuVTz5b0bShmo,38
|
|
30
31
|
cli/commands/common.py,sha256=fHZWzkd4OLl3vPWTTgaI5vaqMi3Ma3XqAu1Ds_-51_4,11299
|
|
31
|
-
cli/commands/parser.py,sha256=
|
|
32
|
+
cli/commands/parser.py,sha256=0ym7T3xjsQPhj2jm0_aUHCQGx6Whcp1m50gH5JDSFHw,25611
|
|
32
33
|
cli/guide/bitbucket.html,sha256=9vN1VPLmvTLrkqDSrqKmuy7z3BHEmQusY1jkAG0FjnI,33477
|
|
33
34
|
cli/guide/getting-started.html,sha256=5iS-_iAP1PboU9pFbMsfh8C9Du9usqOfIyawOc-X4Hg,19070
|
|
34
35
|
cli/guide/index.html,sha256=mUDUBX590TTappvrKT7_eXi0P8G-3YvHjGk9pCcwWzQ,21350
|
|
@@ -94,7 +95,7 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
|
|
|
94
95
|
cli/ui/components/settings.js,sha256=ATbAjBlVIwCNpxq7s191b49a_INQV38iwmySqtJLYwY,79066
|
|
95
96
|
cli/ui/components/sidebar.js,sha256=cAY_jwYB-o1X_wWn__VXlG4IegVObuE3NmVsuFWqxtg,7417
|
|
96
97
|
cli/ui/components/tasks.js,sha256=vyKQ3uwoppMwvdEaHlhWXW4oWcAisx4NveqzMhsYqHo,38438
|
|
97
|
-
code_context_control-2.
|
|
98
|
+
code_context_control-2.54.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
98
99
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
99
100
|
core/config.py,sha256=NUU1DuespWRotlJsv59CNnLiWTR4Bc7ysmD1UFA76RE,16429
|
|
100
101
|
core/ide.py,sha256=V6VVMVsFdmmcsMyxikjQp7z9xa42CWiHKS-ya-MAcG4,6172
|
|
@@ -152,20 +153,20 @@ services/bitbucket_client.py,sha256=JByovvtVZ6F4NcU611KVuTgKlT1rIsX2A2VlBDfvRV8,
|
|
|
152
153
|
services/bitbucket_credentials.py,sha256=2qLA9pQMol4y95y4DJMNBsBBPUsJQCKbLFo2iiCnfvI,7364
|
|
153
154
|
services/circuit_breaker.py,sha256=ES6PpjL40gfDhtK88GeS6GFzsz5EOEEkZsB-DzX_tbw,3179
|
|
154
155
|
services/claude_md.py,sha256=vI2at-7DEsnRXYcSaE88uNTxCSxzhy8M77AM20VlYRc,42643
|
|
155
|
-
services/compressor.py,sha256=
|
|
156
|
+
services/compressor.py,sha256=AK31TqHFpkOD_Lqbp9aEOkOx7zENUwpsQHQ9B2imEHo,33081
|
|
156
157
|
services/context_snapshot.py,sha256=2f_bxY3xX4ZC653ncYno8YSeSTFSavwBjrorytHA0Ns,16879
|
|
157
158
|
services/conversation_store.py,sha256=-E2H6f9pMSOHiBjsq7tFUjrFAwWYExf7LJkibi5MRMA,35650
|
|
158
|
-
services/doc_index.py,sha256=
|
|
159
|
+
services/doc_index.py,sha256=ZEQD1kyGCAMm9AY4Kpi1zDWJTul-cAcAdi_4Md43Hpw,19205
|
|
159
160
|
services/e2e_benchmark.py,sha256=4pDXkOFjq5b2ZwDvMK0CWdUXQli5OE4cOh_OJs7W36c,132970
|
|
160
161
|
services/e2e_evaluator.py,sha256=FsB4vMzcFUcmicfIwRtA8JZ_XeMpuk2_-86F2iVNEDc,17783
|
|
161
162
|
services/e2e_tasks.py,sha256=Ln5VbGDIcS3NY3aml5ERbgjfjLxEslmRZ8AyaYxpWEo,34524
|
|
162
163
|
services/edit_ledger.py,sha256=LbFDAnqAWKXsc5pUdJ8zSk5-1MNUwJduK3MO8_HUm5E,29018
|
|
163
|
-
services/embedding_index.py,sha256
|
|
164
|
+
services/embedding_index.py,sha256=WVAmQkpvwbu2GaQfg4iPWyWiW8zQNN0ldQAo9ibsyBc,15908
|
|
164
165
|
services/error_reporting.py,sha256=HZ3ru8i5RLf8nq2R4iRnTs5sm1blUxknSbv5hdxuxs0,4139
|
|
165
166
|
services/file_memory.py,sha256=ero1EY_42z0ORzZaOyvRK7Pc_OYxvgijf4BaKhEBPuY,34241
|
|
166
167
|
services/git_context.py,sha256=FFdA8Y46op4DYsUnZsXLTqxIjgxb0q_d-hkFTbosC8Q,9518
|
|
167
168
|
services/hub_service.py,sha256=Ta2ExJP1sePxb7zcHooroYXJKsylm5Ea8vQvts6-cAw,21876
|
|
168
|
-
services/indexer.py,sha256=
|
|
169
|
+
services/indexer.py,sha256=00y47xufHFEotljPDH4RBbWcqAHmCpTouafiggGcP7I,29511
|
|
169
170
|
services/memory.py,sha256=UxlZ3BwYfTlq6Rlj5RPhNOw42bds-dSe5KAdA4TXBLE,13150
|
|
170
171
|
services/memory_consolidator.py,sha256=hHMps-01cbbHSI-A1LEKCo3k2PoLOa8a7-vyYEf8DUs,19679
|
|
171
172
|
services/memory_distiller.py,sha256=-07rtR3XhiZqKCUCFOcuAktfTJIbtcNqMj7rM35WXKg,26081
|
|
@@ -182,16 +183,17 @@ services/output_filter.py,sha256=lknQHs38JlRtXU4Ogoru6sveNokSKBQPfZXhf-fE0xM,217
|
|
|
182
183
|
services/parser.py,sha256=CPoIN1FmX7kK-55FnC3jvRy3NLjlnf1armTaD6o7GBg,55203
|
|
183
184
|
services/project_manager.py,sha256=LaNWqBeVMx6wcB6iiOQyRWSWhS-JXzfDApJmHFmTCy8,36477
|
|
184
185
|
services/project_runtime.py,sha256=yyftX48BFjq4B2KcR4KGszDAAa4xR6gn6j0SgTyro1s,11603
|
|
185
|
-
services/protocol.py,sha256=
|
|
186
|
+
services/protocol.py,sha256=XyUGjTHD9Zouw94IEqwH667pQ1TlroleLzlM3GRlEGk,12843
|
|
186
187
|
services/proxy_state.py,sha256=u5rd0k6CrOsywZA8FpRu_hMLwhR0TAJhZjy5MdWbCGc,6107
|
|
187
188
|
services/retention.py,sha256=I2_RV233kWBBXox5rc_w-1h1aPua93o9huuPf-pJVuE,18629
|
|
188
189
|
services/retrieval_broker.py,sha256=9X67VZ_6AkbAzopHuuMFKmP4CGZLnW576kjSKMenBnw,5261
|
|
189
190
|
services/router.py,sha256=Cz10nx2fKTbaGn14mSBePWIDrw5rdcs_1JFYXeik084,15626
|
|
190
191
|
services/runtime.py,sha256=b79gN0dFlzBO4rPaUMWier8FZFamMZlwIM3aywLmbB4,12840
|
|
192
|
+
services/scanner.py,sha256=5rMwxAO8Qchz3-GtpZTNwCQDiJIyhAMSXdwNPmTaq-U,4701
|
|
191
193
|
services/session_benchmark.py,sha256=GX3H8OwKC0X9Kk5U-vfY6Y7qq9Qaz2HwadNA7mjO8RY,105047
|
|
192
194
|
services/session_manager.py,sha256=qUo7ool6dDWXfVgFpokIRcWqkWh10OzMcp7aZTdmTHY,51632
|
|
193
195
|
services/session_preloader.py,sha256=DsTAXMKVtrX9yu1sEFojYDi9-jkSAj1Ylt9JTy57Dow,9883
|
|
194
|
-
services/subprojects.py,sha256=
|
|
196
|
+
services/subprojects.py,sha256=KNA-qGVPsd7Tg92Pt_kLWMLyER2YHmdQWO44sOelTIg,25123
|
|
195
197
|
services/task_store.py,sha256=IHNeLipdyWtbWYu2-3ItIDeSDI9WoMKj6YwPPc1TgW0,46487
|
|
196
198
|
services/telemetry.py,sha256=KbWCe7F8lGY9DufVPWGyGFo-IBcPgWh0kd3ySJnIgFE,11377
|
|
197
199
|
services/text_index.py,sha256=r3o4CobTG9jAO9PWazgbWYLY9oi_FgEJ3xwEXrF4KM0,2783
|
|
@@ -224,8 +226,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
224
226
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
225
227
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
226
228
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
227
|
-
code_context_control-2.
|
|
228
|
-
code_context_control-2.
|
|
229
|
-
code_context_control-2.
|
|
230
|
-
code_context_control-2.
|
|
231
|
-
code_context_control-2.
|
|
229
|
+
code_context_control-2.54.0.dist-info/METADATA,sha256=jtQEMTmI5Z0UW8uxs6ImKZ8KKA9Fic3IrIgALMBz8zM,24759
|
|
230
|
+
code_context_control-2.54.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
231
|
+
code_context_control-2.54.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
232
|
+
code_context_control-2.54.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
233
|
+
code_context_control-2.54.0.dist-info/RECORD,,
|
services/compressor.py
CHANGED
|
@@ -709,27 +709,19 @@ class CodeCompressor:
|
|
|
709
709
|
'.css', '.html', '.json', '.yaml', '.yml', '.md'}
|
|
710
710
|
allowed = set(extensions) if extensions else default_exts
|
|
711
711
|
|
|
712
|
-
# Skip common non-essential dirs
|
|
713
|
-
skip_dirs = {'node_modules', '.git', '__pycache__', '.c3', 'venv',
|
|
714
|
-
'env', '.venv', 'dist', 'build', '.next', '.cache'}
|
|
715
|
-
|
|
716
712
|
results = []
|
|
717
713
|
total_original = 0
|
|
718
714
|
total_compressed = 0
|
|
719
715
|
skipped_protected = []
|
|
720
716
|
|
|
721
|
-
|
|
722
|
-
|
|
717
|
+
# Pruned walk (shared SKIP_DIRS) - never descends into
|
|
718
|
+
# node_modules/.git/etc., stops as soon as the cap is reached.
|
|
719
|
+
from services.scanner import iter_files
|
|
723
720
|
|
|
724
|
-
|
|
721
|
+
count = 0
|
|
722
|
+
for fpath in iter_files(dirpath, exts={e.lower() for e in allowed}):
|
|
725
723
|
if count >= max_files:
|
|
726
724
|
break
|
|
727
|
-
if not fpath.is_file():
|
|
728
|
-
continue
|
|
729
|
-
if fpath.suffix.lower() not in allowed:
|
|
730
|
-
continue
|
|
731
|
-
if any(skip in fpath.parts for skip in skip_dirs):
|
|
732
|
-
continue
|
|
733
725
|
if self.is_protected_file(fpath):
|
|
734
726
|
skipped_protected.append(self._relative_to_project(fpath))
|
|
735
727
|
continue
|
services/doc_index.py
CHANGED
|
@@ -92,15 +92,26 @@ class DocIndex:
|
|
|
92
92
|
|
|
93
93
|
# --- Build ---
|
|
94
94
|
|
|
95
|
-
def build(self, force: bool = False) -> dict:
|
|
96
|
-
"""Build or incrementally update the doc index.
|
|
95
|
+
def build(self, force: bool = False, on_progress=None) -> dict:
|
|
96
|
+
"""Build or incrementally update the doc index.
|
|
97
|
+
|
|
98
|
+
on_progress: callable(files_done, files_total), invoked per file.
|
|
99
|
+
"""
|
|
97
100
|
stats = {"docs_indexed": 0, "chunks_created": 0, "skipped": 0}
|
|
98
101
|
|
|
99
102
|
files_to_index = self._discover_files()
|
|
100
103
|
old_hashes = dict(self._file_hashes)
|
|
101
104
|
new_hashes = {}
|
|
105
|
+
files_total = len(files_to_index)
|
|
106
|
+
files_done = 0
|
|
102
107
|
|
|
103
108
|
for rel_path, fpath in files_to_index:
|
|
109
|
+
files_done += 1
|
|
110
|
+
if on_progress is not None:
|
|
111
|
+
try:
|
|
112
|
+
on_progress(files_done, files_total)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
104
115
|
try:
|
|
105
116
|
content = fpath.read_text(errors="replace")
|
|
106
117
|
except Exception:
|
|
@@ -154,12 +165,6 @@ class DocIndex:
|
|
|
154
165
|
|
|
155
166
|
def _discover_files(self) -> list[tuple[str, Path]]:
|
|
156
167
|
"""Find all doc, config, and code files to index."""
|
|
157
|
-
skip_dirs = {
|
|
158
|
-
"node_modules", ".git", "__pycache__", ".c3", "venv",
|
|
159
|
-
"env", ".venv", "dist", "build", ".next", ".cache",
|
|
160
|
-
"coverage", ".pytest_cache",
|
|
161
|
-
}
|
|
162
|
-
|
|
163
168
|
# Designated sub-projects keep their own doc index.
|
|
164
169
|
try:
|
|
165
170
|
from services.subprojects import make_excluder
|
|
@@ -170,15 +175,14 @@ class DocIndex:
|
|
|
170
175
|
|
|
171
176
|
files = []
|
|
172
177
|
|
|
173
|
-
# Markdown docs
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
files.append((rel, fpath))
|
|
178
|
+
# Markdown docs - one pruned walk instead of four full rglob passes
|
|
179
|
+
from services.scanner import iter_files
|
|
180
|
+
for fpath in iter_files(self.project_path,
|
|
181
|
+
exts={".md", ".mdx", ".rst", ".adoc"}):
|
|
182
|
+
if _sub_excluded(fpath):
|
|
183
|
+
continue
|
|
184
|
+
rel = str(fpath.relative_to(self.project_path))
|
|
185
|
+
files.append((rel, fpath))
|
|
182
186
|
|
|
183
187
|
# Config files at project root
|
|
184
188
|
for pattern in _CONFIG_PATTERNS:
|
services/embedding_index.py
CHANGED
|
@@ -39,6 +39,8 @@ class EmbeddingIndex:
|
|
|
39
39
|
self._collection = None
|
|
40
40
|
self._available = False
|
|
41
41
|
self._ollama_ok = False
|
|
42
|
+
self._ollama_up = False
|
|
43
|
+
self._model_ok = False
|
|
42
44
|
self._file_hashes: dict[str, str] = {} # doc_id -> content hash
|
|
43
45
|
self._lock = threading.Lock()
|
|
44
46
|
self._chunk_map: dict[str, dict] = {} # chunk_id -> metadata
|
|
@@ -101,18 +103,47 @@ class EmbeddingIndex:
|
|
|
101
103
|
self._available = False
|
|
102
104
|
|
|
103
105
|
try:
|
|
104
|
-
self.
|
|
105
|
-
|
|
106
|
-
and self.ollama.has_model(self.embed_model)
|
|
106
|
+
self._ollama_up = self.ollama.is_available(timeout=2)
|
|
107
|
+
self._model_ok = (
|
|
108
|
+
self._ollama_up and self.ollama.has_model(self.embed_model)
|
|
107
109
|
)
|
|
108
110
|
except Exception:
|
|
109
|
-
self.
|
|
111
|
+
self._ollama_up = False
|
|
112
|
+
self._model_ok = False
|
|
113
|
+
self._ollama_ok = self._ollama_up and self._model_ok
|
|
110
114
|
|
|
111
115
|
@property
|
|
112
116
|
def ready(self) -> bool:
|
|
113
117
|
"""True when both chromadb and Ollama embeddings are available."""
|
|
114
118
|
return self._available and self._ollama_ok
|
|
115
119
|
|
|
120
|
+
def probe(self) -> dict:
|
|
121
|
+
"""Explicitly initialize backends and report readiness.
|
|
122
|
+
|
|
123
|
+
``ready`` alone never triggers backend init (status reporters must
|
|
124
|
+
stay cheap for build_runtime/MCP handshake), so gating a build on a
|
|
125
|
+
fresh instance's ``ready`` always skipped. Init/CLI flows call this
|
|
126
|
+
instead: it pays the one-time backend cost, then reports truthfully.
|
|
127
|
+
"""
|
|
128
|
+
self._ensure_ready()
|
|
129
|
+
return {
|
|
130
|
+
"ready": self.ready,
|
|
131
|
+
"chromadb": self._available,
|
|
132
|
+
"ollama": self._ollama_up,
|
|
133
|
+
"model": self._model_ok,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def unavailable_reason(self) -> str:
|
|
137
|
+
"""Human-readable reason ``ready`` is False (after probe/init)."""
|
|
138
|
+
if not self._available:
|
|
139
|
+
return "chromadb not installed"
|
|
140
|
+
if not self._ollama_up:
|
|
141
|
+
return "Ollama not reachable"
|
|
142
|
+
if not self._model_ok:
|
|
143
|
+
return (f"embed model '{self.embed_model}' not pulled "
|
|
144
|
+
f"(run: ollama pull {self.embed_model})")
|
|
145
|
+
return ""
|
|
146
|
+
|
|
116
147
|
# ── Hash tracking ─────────────────────────────────────
|
|
117
148
|
|
|
118
149
|
def _load_hashes(self):
|
|
@@ -138,12 +169,14 @@ class EmbeddingIndex:
|
|
|
138
169
|
|
|
139
170
|
# ── Build / Update ────────────────────────────────────
|
|
140
171
|
|
|
141
|
-
def build(self, code_index, force: bool = False) -> dict:
|
|
172
|
+
def build(self, code_index, force: bool = False, on_progress=None) -> dict:
|
|
142
173
|
"""Build or incrementally update the embedding index from CodeIndex chunks.
|
|
143
174
|
|
|
144
175
|
Args:
|
|
145
176
|
code_index: A CodeIndex instance with populated chunks/documents.
|
|
146
177
|
force: If True, re-embed all files regardless of hash.
|
|
178
|
+
on_progress: callable(files_done, files_total, chunks_embedded),
|
|
179
|
+
invoked per file (skipped files count as done).
|
|
147
180
|
|
|
148
181
|
Returns:
|
|
149
182
|
Stats dict with files_processed, chunks_embedded, chunks_skipped, etc.
|
|
@@ -170,6 +203,15 @@ class EmbeddingIndex:
|
|
|
170
203
|
files_skipped = 0
|
|
171
204
|
errors = 0
|
|
172
205
|
stale_ids = []
|
|
206
|
+
files_total = len(chunks_by_file)
|
|
207
|
+
|
|
208
|
+
def _report():
|
|
209
|
+
if on_progress is not None:
|
|
210
|
+
try:
|
|
211
|
+
on_progress(files_processed + files_skipped, files_total,
|
|
212
|
+
chunks_embedded)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
173
215
|
|
|
174
216
|
with self._lock:
|
|
175
217
|
# Detect deleted files — remove their embeddings
|
|
@@ -187,6 +229,7 @@ class EmbeddingIndex:
|
|
|
187
229
|
if not force and self._file_hashes.get(doc_id) == new_hash:
|
|
188
230
|
files_skipped += 1
|
|
189
231
|
chunks_skipped += len(file_chunks)
|
|
232
|
+
_report()
|
|
190
233
|
continue
|
|
191
234
|
|
|
192
235
|
# Remove old chunks for this file before re-embedding
|
|
@@ -237,6 +280,7 @@ class EmbeddingIndex:
|
|
|
237
280
|
|
|
238
281
|
self._file_hashes[doc_id] = new_hash
|
|
239
282
|
files_processed += 1
|
|
283
|
+
_report()
|
|
240
284
|
|
|
241
285
|
self._save_hashes()
|
|
242
286
|
|
services/indexer.py
CHANGED
|
@@ -13,6 +13,7 @@ from collections import Counter, OrderedDict
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
from core import count_tokens
|
|
16
|
+
from services.scanner import SKIP_DIRS, iter_files
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class CodeIndex:
|
|
@@ -37,10 +38,8 @@ class CodeIndex:
|
|
|
37
38
|
self._cooccurrence = {} # term -> {term: count} for auto-synonyms
|
|
38
39
|
self._file_mtimes = {} # doc_id -> mtime for recency bias
|
|
39
40
|
|
|
40
|
-
# Config
|
|
41
|
-
self.skip_dirs =
|
|
42
|
-
'env', '.venv', 'dist', 'build', '.next', '.cache',
|
|
43
|
-
'coverage', '.pytest_cache'}
|
|
41
|
+
# Config - shared pruned-walk skip set (services/scanner.py)
|
|
42
|
+
self.skip_dirs = set(SKIP_DIRS)
|
|
44
43
|
self.code_exts = {
|
|
45
44
|
# Python
|
|
46
45
|
'.py', '.pyi', '.pyx',
|
|
@@ -87,8 +86,31 @@ class CodeIndex:
|
|
|
87
86
|
self.exclude_prefixes = []
|
|
88
87
|
self._is_excluded = None
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
_DEFAULT_MAX_FILES = 2000
|
|
90
|
+
|
|
91
|
+
def _configured_max_files(self) -> int:
|
|
92
|
+
"""``index_max_files`` from .c3/config.json; default 2000."""
|
|
93
|
+
try:
|
|
94
|
+
cfg = json.loads((self.project_path / '.c3' / 'config.json')
|
|
95
|
+
.read_text(encoding='utf-8'))
|
|
96
|
+
val = int(cfg.get('index_max_files', self._DEFAULT_MAX_FILES))
|
|
97
|
+
return val if val > 0 else self._DEFAULT_MAX_FILES
|
|
98
|
+
except Exception:
|
|
99
|
+
return self._DEFAULT_MAX_FILES
|
|
100
|
+
|
|
101
|
+
def build_index(self, max_files: int = None, on_progress=None) -> dict:
|
|
102
|
+
"""Build the full code index.
|
|
103
|
+
|
|
104
|
+
max_files: cap on files indexed. None reads ``index_max_files``
|
|
105
|
+
from .c3/config.json (default 2000). Traversal continues past
|
|
106
|
+
the cap only to count what was left out, so callers can report
|
|
107
|
+
"indexed N of M" instead of silently truncating.
|
|
108
|
+
on_progress: callable(entries_seen, files_indexed, chunks_created),
|
|
109
|
+
invoked during the scan (directory granularity plus per file).
|
|
110
|
+
"""
|
|
111
|
+
if max_files is None:
|
|
112
|
+
max_files = self._configured_max_files()
|
|
113
|
+
|
|
92
114
|
self.documents = {}
|
|
93
115
|
self.chunks = {}
|
|
94
116
|
self.symbols = {}
|
|
@@ -96,24 +118,35 @@ class CodeIndex:
|
|
|
96
118
|
|
|
97
119
|
files_indexed = 0
|
|
98
120
|
chunks_created = 0
|
|
121
|
+
files_capped = 0
|
|
122
|
+
scan_stats = {"entries": 0}
|
|
99
123
|
|
|
100
|
-
|
|
101
|
-
if
|
|
102
|
-
|
|
103
|
-
if not
|
|
104
|
-
continue
|
|
105
|
-
if fpath.suffix.lower() not in self.code_exts:
|
|
106
|
-
continue
|
|
107
|
-
if any(skip in fpath.parts for skip in self.skip_dirs):
|
|
108
|
-
continue
|
|
109
|
-
if self.exclude_prefixes:
|
|
124
|
+
def _report(entries_seen=None, _yielded=None):
|
|
125
|
+
if entries_seen is not None:
|
|
126
|
+
scan_stats["entries"] = entries_seen
|
|
127
|
+
if on_progress is not None:
|
|
110
128
|
try:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
except ValueError:
|
|
129
|
+
on_progress(scan_stats["entries"], files_indexed,
|
|
130
|
+
chunks_created)
|
|
131
|
+
except Exception:
|
|
115
132
|
pass
|
|
116
133
|
|
|
134
|
+
exclude_parts = None
|
|
135
|
+
if self.exclude_prefixes and self._is_excluded is not None:
|
|
136
|
+
def exclude_parts(parts, _c=self._is_excluded,
|
|
137
|
+
_p=self.exclude_prefixes):
|
|
138
|
+
return _c(parts, _p)
|
|
139
|
+
|
|
140
|
+
for fpath in iter_files(self.project_path, exts=self.code_exts,
|
|
141
|
+
skip_dirs=self.skip_dirs,
|
|
142
|
+
exclude_parts=exclude_parts,
|
|
143
|
+
on_progress=_report):
|
|
144
|
+
if files_indexed >= max_files:
|
|
145
|
+
# Walk on (cheap after pruning) to count coverage;
|
|
146
|
+
# reading and chunking stop at the cap.
|
|
147
|
+
files_capped += 1
|
|
148
|
+
continue
|
|
149
|
+
|
|
117
150
|
try:
|
|
118
151
|
content = fpath.read_text(errors='replace')
|
|
119
152
|
except Exception:
|
|
@@ -150,6 +183,7 @@ class CodeIndex:
|
|
|
150
183
|
pass
|
|
151
184
|
|
|
152
185
|
files_indexed += 1
|
|
186
|
+
_report()
|
|
153
187
|
|
|
154
188
|
# Build TF-IDF and co-occurrence synonyms
|
|
155
189
|
self._build_tfidf()
|
|
@@ -162,7 +196,10 @@ class CodeIndex:
|
|
|
162
196
|
"files_indexed": files_indexed,
|
|
163
197
|
"chunks_created": chunks_created,
|
|
164
198
|
"unique_symbols": len(self.symbols),
|
|
165
|
-
"index_path": str(self.index_dir)
|
|
199
|
+
"index_path": str(self.index_dir),
|
|
200
|
+
"entries_scanned": scan_stats["entries"],
|
|
201
|
+
"files_capped": files_capped,
|
|
202
|
+
"max_files": max_files,
|
|
166
203
|
}
|
|
167
204
|
|
|
168
205
|
def _chunk_file(self, content: str, ext: str, doc_id: str) -> list:
|
services/protocol.py
CHANGED
|
@@ -219,42 +219,53 @@ class CompressionProtocol:
|
|
|
219
219
|
self.custom_dict[term.lower()] = code
|
|
220
220
|
self.save_custom_dict()
|
|
221
221
|
|
|
222
|
-
def build_project_dictionary(self) -> dict:
|
|
223
|
-
"""Auto-build a project-specific dictionary from codebase analysis.
|
|
222
|
+
def build_project_dictionary(self, code_index=None) -> dict:
|
|
223
|
+
"""Auto-build a project-specific dictionary from codebase analysis.
|
|
224
|
+
|
|
225
|
+
code_index: an already-built CodeIndex whose in-memory chunks are
|
|
226
|
+
mined instead of re-reading the whole tree from disk (init
|
|
227
|
+
passes the indexer it just built). Without one, falls back to
|
|
228
|
+
a pruned, capped scan.
|
|
229
|
+
"""
|
|
224
230
|
if not self.project_path.exists():
|
|
225
231
|
return {}
|
|
226
232
|
|
|
227
233
|
# Find commonly used terms in the project
|
|
228
234
|
term_freq = {}
|
|
229
|
-
|
|
230
|
-
code_exts = {'.py', '.js', '.ts', '.tsx', '.jsx', '.r', '.R'}
|
|
231
|
-
try:
|
|
232
|
-
from services.subprojects import make_excluder
|
|
233
|
-
_sub_excluded = make_excluder(self.project_path)
|
|
234
|
-
except Exception:
|
|
235
|
-
def _sub_excluded(_p):
|
|
236
|
-
return False
|
|
237
|
-
|
|
238
|
-
for fpath in self.project_path.rglob('*'):
|
|
239
|
-
if not fpath.is_file() or fpath.suffix not in code_exts:
|
|
240
|
-
continue
|
|
241
|
-
if any(skip in fpath.parts for skip in skip_dirs):
|
|
242
|
-
continue
|
|
243
|
-
if _sub_excluded(fpath):
|
|
244
|
-
continue
|
|
245
|
-
|
|
246
|
-
try:
|
|
247
|
-
content = fpath.read_text(errors='replace')
|
|
248
|
-
except Exception:
|
|
249
|
-
continue
|
|
235
|
+
code_exts = {'.py', '.js', '.ts', '.tsx', '.jsx', '.r'}
|
|
250
236
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
for ident in identifiers:
|
|
237
|
+
def _accumulate(text: str):
|
|
238
|
+
for ident in re.findall(r'\b[a-zA-Z_]\w{5,}\b', text):
|
|
254
239
|
lower = ident.lower()
|
|
255
240
|
if lower not in ACTION_CODES and lower not in TERM_CODES:
|
|
256
241
|
term_freq[lower] = term_freq.get(lower, 0) + 1
|
|
257
242
|
|
|
243
|
+
chunks = getattr(code_index, "chunks", None) if code_index else None
|
|
244
|
+
if chunks:
|
|
245
|
+
# Sub-project exclusion already applied when the index was built.
|
|
246
|
+
from pathlib import Path as _P
|
|
247
|
+
for chunk in chunks.values():
|
|
248
|
+
if _P(chunk.get("doc_id", "")).suffix.lower() in code_exts:
|
|
249
|
+
_accumulate(chunk.get("content", ""))
|
|
250
|
+
else:
|
|
251
|
+
from services.scanner import iter_files
|
|
252
|
+
try:
|
|
253
|
+
from services.subprojects import make_excluder
|
|
254
|
+
_sub_excluded = make_excluder(self.project_path)
|
|
255
|
+
except Exception:
|
|
256
|
+
def _sub_excluded(_p):
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
for fpath in iter_files(self.project_path, exts=code_exts,
|
|
260
|
+
max_files=1000):
|
|
261
|
+
if _sub_excluded(fpath):
|
|
262
|
+
continue
|
|
263
|
+
try:
|
|
264
|
+
content = fpath.read_text(errors='replace')
|
|
265
|
+
except Exception:
|
|
266
|
+
continue
|
|
267
|
+
_accumulate(content)
|
|
268
|
+
|
|
258
269
|
# Generate codes for frequent terms
|
|
259
270
|
frequent = sorted(term_freq.items(), key=lambda x: x[1], reverse=True)[:30]
|
|
260
271
|
new_entries = {}
|
services/scanner.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pruned Filesystem Scanner
|
|
3
|
+
|
|
4
|
+
Shared walker for every C3 index build (code index, doc index, compression
|
|
5
|
+
dictionary, directory compression). Replaces the sorted(Path.rglob('*'))
|
|
6
|
+
pattern, which enumerated every entry under node_modules/.git/venv before
|
|
7
|
+
filtering and materialized the whole tree up front - on large projects that
|
|
8
|
+
meant minutes of stat calls before the first file was even considered.
|
|
9
|
+
|
|
10
|
+
os.walk with in-place dirnames pruning never descends into skipped
|
|
11
|
+
directories, yields files in deterministic order, exits as soon as the
|
|
12
|
+
caller stops consuming, and reports progress so long scans are visible.
|
|
13
|
+
"""
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Callable, Iterator, Optional, Set, Tuple
|
|
17
|
+
|
|
18
|
+
# Superset of the historical per-service skip lists.
|
|
19
|
+
# Matched against directory names exactly (never file names).
|
|
20
|
+
SKIP_DIRS = {
|
|
21
|
+
# original shared set
|
|
22
|
+
'node_modules', '.git', '__pycache__', '.c3', 'venv', 'env', '.venv',
|
|
23
|
+
'dist', 'build', '.next', '.cache', 'coverage', '.pytest_cache',
|
|
24
|
+
# heavyweights the old lists missed
|
|
25
|
+
'target', '.tox', '.nox', '.eggs', '.mypy_cache', '.ruff_cache',
|
|
26
|
+
'.gradle', 'Pods', 'obj', '.idea', '.vs', '.svn', '.hg',
|
|
27
|
+
'bower_components', '.terraform', '.parcel-cache', '.turbo',
|
|
28
|
+
'.nuxt', '.yarn', '.pnpm-store',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def gitignore_dir_names(root) -> set:
|
|
33
|
+
"""Literal directory names from the root .gitignore (best-effort).
|
|
34
|
+
|
|
35
|
+
Only unambiguous entries are used - a bare name or ``/name/`` with no
|
|
36
|
+
wildcard, negation, or nested separator - so pruning can never be
|
|
37
|
+
broader than the ignore file itself. Data/log directories are exactly
|
|
38
|
+
what makes large-project scans hang, and they are almost always
|
|
39
|
+
plain-name entries.
|
|
40
|
+
"""
|
|
41
|
+
names = set()
|
|
42
|
+
try:
|
|
43
|
+
text = (Path(root) / '.gitignore').read_text(errors='replace')
|
|
44
|
+
except OSError:
|
|
45
|
+
return names
|
|
46
|
+
for line in text.splitlines():
|
|
47
|
+
line = line.strip()
|
|
48
|
+
if not line or line.startswith('#') or line.startswith('!'):
|
|
49
|
+
continue
|
|
50
|
+
if any(ch in line for ch in '*?['):
|
|
51
|
+
continue
|
|
52
|
+
cleaned = line.strip('/')
|
|
53
|
+
if cleaned and '/' not in cleaned and '\\' not in cleaned:
|
|
54
|
+
names.add(cleaned)
|
|
55
|
+
return names
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def iter_files(
|
|
59
|
+
root,
|
|
60
|
+
exts: Optional[Set[str]] = None,
|
|
61
|
+
skip_dirs: Optional[Set[str]] = None,
|
|
62
|
+
exclude_parts: Optional[Callable[[Tuple[str, ...]], bool]] = None,
|
|
63
|
+
max_files: Optional[int] = None,
|
|
64
|
+
on_progress: Optional[Callable[[int, int], None]] = None,
|
|
65
|
+
respect_gitignore: bool = True,
|
|
66
|
+
) -> Iterator[Path]:
|
|
67
|
+
"""Yield candidate files under ``root`` with directory-level pruning.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
exts: lowercase suffix allowlist (None = every file).
|
|
71
|
+
skip_dirs: directory names to prune (default: SKIP_DIRS).
|
|
72
|
+
exclude_parts: predicate over project-relative path parts; True
|
|
73
|
+
skips a file, and prunes whole directories before descent
|
|
74
|
+
(used for sub-project exclusion).
|
|
75
|
+
max_files: stop traversal entirely after yielding this many.
|
|
76
|
+
on_progress: called as ``on_progress(entries_seen, files_yielded)``
|
|
77
|
+
at directory granularity - cheap enough to wire straight to a
|
|
78
|
+
TTY progress line (which should throttle by time).
|
|
79
|
+
respect_gitignore: also prune literal directory names listed in
|
|
80
|
+
the root .gitignore.
|
|
81
|
+
"""
|
|
82
|
+
root = Path(root)
|
|
83
|
+
skip = set(SKIP_DIRS if skip_dirs is None else skip_dirs)
|
|
84
|
+
if respect_gitignore:
|
|
85
|
+
skip |= gitignore_dir_names(root)
|
|
86
|
+
|
|
87
|
+
yielded = 0
|
|
88
|
+
seen = 0
|
|
89
|
+
for dirpath, dirnames, filenames in os.walk(str(root), topdown=True,
|
|
90
|
+
followlinks=False):
|
|
91
|
+
try:
|
|
92
|
+
rel_parts = Path(dirpath).relative_to(root).parts
|
|
93
|
+
except ValueError:
|
|
94
|
+
rel_parts = ()
|
|
95
|
+
|
|
96
|
+
kept = []
|
|
97
|
+
for d in sorted(dirnames):
|
|
98
|
+
if d in skip:
|
|
99
|
+
continue
|
|
100
|
+
if exclude_parts is not None and exclude_parts(rel_parts + (d,)):
|
|
101
|
+
continue
|
|
102
|
+
kept.append(d)
|
|
103
|
+
dirnames[:] = kept
|
|
104
|
+
seen += len(filenames) + len(kept)
|
|
105
|
+
|
|
106
|
+
for fname in sorted(filenames):
|
|
107
|
+
if exts is not None:
|
|
108
|
+
if os.path.splitext(fname)[1].lower() not in exts:
|
|
109
|
+
continue
|
|
110
|
+
if exclude_parts is not None and exclude_parts(rel_parts + (fname,)):
|
|
111
|
+
continue
|
|
112
|
+
yield Path(dirpath) / fname
|
|
113
|
+
yielded += 1
|
|
114
|
+
if max_files is not None and yielded >= max_files:
|
|
115
|
+
if on_progress is not None:
|
|
116
|
+
on_progress(seen, yielded)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if on_progress is not None:
|
|
120
|
+
on_progress(seen, yielded)
|
services/subprojects.py
CHANGED
|
@@ -587,7 +587,9 @@ class SubprojectManager:
|
|
|
587
587
|
ollama = OllamaClient(cfg.get("ollama_base_url", "http://localhost:11434"))
|
|
588
588
|
ei = EmbeddingIndex(self.parent_path, ollama,
|
|
589
589
|
embed_model=cfg.get("embed_model", "nomic-embed-text"))
|
|
590
|
-
|
|
590
|
+
# probe() initializes lazy backends; a fresh instance's
|
|
591
|
+
# .ready is always False and skipped this unconditionally.
|
|
592
|
+
if ei.probe()["ready"]:
|
|
591
593
|
detail["embeddings"] = ei.build(indexer, force=True)
|
|
592
594
|
except Exception:
|
|
593
595
|
pass # embeddings are best-effort; index/doc exclusion is the contract
|
|
File without changes
|
{code_context_control-2.53.0.dist-info → code_context_control-2.54.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{code_context_control-2.53.0.dist-info → code_context_control-2.54.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{code_context_control-2.53.0.dist-info → code_context_control-2.54.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|