code-context-control 2.28.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.
Files changed (150) hide show
  1. cli/__init__.py +1 -0
  2. cli/_hook_utils.py +99 -0
  3. cli/c3.py +6152 -0
  4. cli/commands/__init__.py +1 -0
  5. cli/commands/common.py +312 -0
  6. cli/commands/parser.py +286 -0
  7. cli/docs.html +3178 -0
  8. cli/edits.html +878 -0
  9. cli/hook_auto_snapshot.py +142 -0
  10. cli/hook_c3_signal.py +61 -0
  11. cli/hook_c3read.py +116 -0
  12. cli/hook_edit_ledger.py +213 -0
  13. cli/hook_edit_unlock.py +170 -0
  14. cli/hook_filter.py +130 -0
  15. cli/hook_ghost_files.py +238 -0
  16. cli/hook_pretool_enforce.py +334 -0
  17. cli/hook_read.py +200 -0
  18. cli/hook_session_stats.py +62 -0
  19. cli/hook_terse_advisor.py +190 -0
  20. cli/hub.html +3764 -0
  21. cli/hub_server.py +1619 -0
  22. cli/mcp_proxy.py +428 -0
  23. cli/mcp_server.py +660 -0
  24. cli/server.py +2985 -0
  25. cli/tools/__init__.py +4 -0
  26. cli/tools/_helpers.py +65 -0
  27. cli/tools/agent.py +1165 -0
  28. cli/tools/compress.py +215 -0
  29. cli/tools/delegate.py +1184 -0
  30. cli/tools/edit.py +313 -0
  31. cli/tools/edits.py +118 -0
  32. cli/tools/filter.py +285 -0
  33. cli/tools/impact.py +163 -0
  34. cli/tools/memory.py +469 -0
  35. cli/tools/read.py +224 -0
  36. cli/tools/search.py +337 -0
  37. cli/tools/session.py +95 -0
  38. cli/tools/shell.py +193 -0
  39. cli/tools/status.py +306 -0
  40. cli/tools/validate.py +310 -0
  41. cli/ui/api.js +36 -0
  42. cli/ui/app.js +207 -0
  43. cli/ui/components/chat.js +758 -0
  44. cli/ui/components/dashboard.js +689 -0
  45. cli/ui/components/edits.js +220 -0
  46. cli/ui/components/instructions.js +481 -0
  47. cli/ui/components/memory.js +626 -0
  48. cli/ui/components/sessions.js +606 -0
  49. cli/ui/components/settings.js +1404 -0
  50. cli/ui/components/sidebar.js +156 -0
  51. cli/ui/icons.js +51 -0
  52. cli/ui/shared.js +119 -0
  53. cli/ui/theme.js +22 -0
  54. cli/ui.html +168 -0
  55. cli/ui_legacy.html +6797 -0
  56. cli/ui_nano.html +503 -0
  57. code_context_control-2.28.0.dist-info/METADATA +248 -0
  58. code_context_control-2.28.0.dist-info/RECORD +150 -0
  59. code_context_control-2.28.0.dist-info/WHEEL +5 -0
  60. code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
  61. code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
  62. code_context_control-2.28.0.dist-info/top_level.txt +5 -0
  63. core/__init__.py +75 -0
  64. core/config.py +269 -0
  65. core/ide.py +188 -0
  66. oracle/__init__.py +1 -0
  67. oracle/config.py +75 -0
  68. oracle/oracle.html +3900 -0
  69. oracle/oracle_server.py +663 -0
  70. oracle/services/__init__.py +1 -0
  71. oracle/services/c3_bridge.py +210 -0
  72. oracle/services/chat_engine.py +1103 -0
  73. oracle/services/chat_store.py +155 -0
  74. oracle/services/cross_memory.py +154 -0
  75. oracle/services/federated_graph.py +463 -0
  76. oracle/services/health_checker.py +117 -0
  77. oracle/services/insight_engine.py +307 -0
  78. oracle/services/memory_reader.py +106 -0
  79. oracle/services/memory_writer.py +182 -0
  80. oracle/services/ollama_bridge.py +332 -0
  81. oracle/services/project_scanner.py +87 -0
  82. oracle/services/review_agent.py +206 -0
  83. services/__init__.py +1 -0
  84. services/activity_log.py +93 -0
  85. services/agent_base.py +124 -0
  86. services/agents.py +1529 -0
  87. services/auto_memory.py +407 -0
  88. services/bench/__init__.py +6 -0
  89. services/bench/external/__init__.py +29 -0
  90. services/bench/external/aider_polyglot.py +405 -0
  91. services/bench/external/swe_bench.py +485 -0
  92. services/benchmark_dashboard.py +596 -0
  93. services/claude_md.py +785 -0
  94. services/compressor.py +592 -0
  95. services/context_snapshot.py +356 -0
  96. services/conversation_store.py +870 -0
  97. services/doc_index.py +537 -0
  98. services/e2e_benchmark.py +2884 -0
  99. services/e2e_evaluator.py +396 -0
  100. services/e2e_tasks.py +743 -0
  101. services/edit_ledger.py +459 -0
  102. services/embedding_index.py +341 -0
  103. services/error_reporting.py +123 -0
  104. services/file_memory.py +734 -0
  105. services/hub_service.py +585 -0
  106. services/indexer.py +712 -0
  107. services/memory.py +318 -0
  108. services/memory_consolidator.py +538 -0
  109. services/memory_graph.py +382 -0
  110. services/memory_grounder.py +304 -0
  111. services/memory_scorer.py +246 -0
  112. services/metrics.py +86 -0
  113. services/notifications.py +209 -0
  114. services/ollama_client.py +201 -0
  115. services/output_filter.py +488 -0
  116. services/parser.py +1238 -0
  117. services/project_manager.py +579 -0
  118. services/protocol.py +306 -0
  119. services/proxy_state.py +152 -0
  120. services/retrieval_broker.py +129 -0
  121. services/router.py +414 -0
  122. services/runtime.py +326 -0
  123. services/session_benchmark.py +1945 -0
  124. services/session_manager.py +1026 -0
  125. services/session_preloader.py +251 -0
  126. services/text_index.py +90 -0
  127. services/tool_classifier.py +176 -0
  128. services/transcript_index.py +340 -0
  129. services/validation_cache.py +155 -0
  130. services/vector_store.py +299 -0
  131. services/version_tracker.py +271 -0
  132. services/watcher.py +192 -0
  133. tui/__init__.py +0 -0
  134. tui/backend.py +59 -0
  135. tui/main.py +145 -0
  136. tui/screens/__init__.py +1 -0
  137. tui/screens/benchmark_view.py +109 -0
  138. tui/screens/claudemd_view.py +46 -0
  139. tui/screens/compress_view.py +52 -0
  140. tui/screens/index_view.py +74 -0
  141. tui/screens/init_view.py +82 -0
  142. tui/screens/mcp_view.py +73 -0
  143. tui/screens/optimize_view.py +41 -0
  144. tui/screens/pipe_view.py +46 -0
  145. tui/screens/projects_view.py +355 -0
  146. tui/screens/search_view.py +55 -0
  147. tui/screens/session_view.py +143 -0
  148. tui/screens/stats.py +158 -0
  149. tui/screens/ui_view.py +54 -0
  150. tui/theme.tcss +335 -0
@@ -0,0 +1,341 @@
1
+ """
2
+ Incremental Embedding Index for semantic code search.
3
+
4
+ Embeds code chunks from CodeIndex into a chromadb collection using Ollama
5
+ embeddings. Tracks file content hashes to only re-embed changed files.
6
+ Falls back gracefully when Ollama or chromadb are unavailable.
7
+ """
8
+
9
+ import hashlib
10
+ import json
11
+ import logging
12
+ import threading
13
+ from pathlib import Path
14
+
15
+ log = logging.getLogger("c3.embedding_index")
16
+
17
+
18
+ class EmbeddingIndex:
19
+ """Semantic code search via embeddings over CodeIndex chunks."""
20
+
21
+ def __init__(
22
+ self,
23
+ project_path: str,
24
+ ollama_client,
25
+ embed_model: str = "nomic-embed-text",
26
+ batch_size: int = 32,
27
+ ):
28
+ self.project_path = Path(project_path)
29
+ self.ollama = ollama_client
30
+ self.embed_model = embed_model
31
+ self.batch_size = batch_size
32
+
33
+ self._index_dir = self.project_path / ".c3" / "embeddings"
34
+ self._index_dir.mkdir(parents=True, exist_ok=True)
35
+ self._hash_file = self._index_dir / "file_hashes.json"
36
+
37
+ self._chroma_client = None
38
+ self._collection = None
39
+ self._available = False
40
+ self._ollama_ok = False
41
+ self._file_hashes: dict[str, str] = {} # doc_id -> content hash
42
+ self._lock = threading.Lock()
43
+ self._chunk_map: dict[str, dict] = {} # chunk_id -> metadata
44
+
45
+ self._init_backends()
46
+ self._load_hashes()
47
+
48
+ # ── Backend init ──────────────────────────────────────
49
+
50
+ def _init_backends(self):
51
+ """Initialize chromadb collection and check Ollama."""
52
+ try:
53
+ import chromadb
54
+ from chromadb.config import Settings
55
+
56
+ persist_dir = str(self._index_dir / "chromadb")
57
+ Path(persist_dir).mkdir(parents=True, exist_ok=True)
58
+ self._chroma_client = chromadb.PersistentClient(
59
+ path=persist_dir,
60
+ settings=Settings(anonymized_telemetry=False),
61
+ )
62
+ self._collection = self._chroma_client.get_or_create_collection(
63
+ name="code_embeddings",
64
+ metadata={"hnsw:space": "cosine"},
65
+ )
66
+ self._available = True
67
+ except Exception as e:
68
+ log.debug("chromadb unavailable for embedding index: %s", e)
69
+ self._available = False
70
+
71
+ try:
72
+ self._ollama_ok = (
73
+ self.ollama.is_available(timeout=2)
74
+ and self.ollama.has_model(self.embed_model)
75
+ )
76
+ except Exception:
77
+ self._ollama_ok = False
78
+
79
+ @property
80
+ def ready(self) -> bool:
81
+ """True when both chromadb and Ollama embeddings are available."""
82
+ return self._available and self._ollama_ok
83
+
84
+ # ── Hash tracking ─────────────────────────────────────
85
+
86
+ def _load_hashes(self):
87
+ """Load persisted file content hashes."""
88
+ if self._hash_file.exists():
89
+ try:
90
+ with open(self._hash_file, encoding='utf-8') as f:
91
+ self._file_hashes = json.load(f)
92
+ except Exception:
93
+ self._file_hashes = {}
94
+
95
+ def _save_hashes(self):
96
+ """Persist file content hashes."""
97
+ try:
98
+ with open(self._hash_file, "w", encoding='utf-8') as f:
99
+ json.dump(self._file_hashes, f)
100
+ except Exception:
101
+ pass
102
+
103
+ @staticmethod
104
+ def _content_hash(content: str) -> str:
105
+ return hashlib.sha256(content.encode(errors="replace")).hexdigest()[:16]
106
+
107
+ # ── Build / Update ────────────────────────────────────
108
+
109
+ def build(self, code_index, force: bool = False) -> dict:
110
+ """Build or incrementally update the embedding index from CodeIndex chunks.
111
+
112
+ Args:
113
+ code_index: A CodeIndex instance with populated chunks/documents.
114
+ force: If True, re-embed all files regardless of hash.
115
+
116
+ Returns:
117
+ Stats dict with files_processed, chunks_embedded, chunks_skipped, etc.
118
+ """
119
+ if not self.ready:
120
+ return {"error": "Embedding backends unavailable", "available": False}
121
+
122
+ if not code_index.chunks:
123
+ code_index._load_index()
124
+ if not code_index.chunks:
125
+ return {"error": "No code index chunks found. Build code index first."}
126
+
127
+ # Group chunks by doc_id (file)
128
+ chunks_by_file: dict[str, list[tuple[str, dict]]] = {}
129
+ for chunk_id, chunk in code_index.chunks.items():
130
+ doc_id = chunk.get("doc_id", "")
131
+ if doc_id:
132
+ chunks_by_file.setdefault(doc_id, []).append((chunk_id, chunk))
133
+
134
+ files_processed = 0
135
+ chunks_embedded = 0
136
+ chunks_skipped = 0
137
+ files_skipped = 0
138
+ errors = 0
139
+ stale_ids = []
140
+
141
+ with self._lock:
142
+ # Detect deleted files — remove their embeddings
143
+ indexed_files = set(self._file_hashes.keys())
144
+ current_files = set(chunks_by_file.keys())
145
+ for removed_file in indexed_files - current_files:
146
+ self._remove_file_chunks(removed_file)
147
+ del self._file_hashes[removed_file]
148
+
149
+ for doc_id, file_chunks in chunks_by_file.items():
150
+ # Check if file content changed
151
+ content = "".join(c.get("content", "") for _, c in file_chunks)
152
+ new_hash = self._content_hash(content)
153
+
154
+ if not force and self._file_hashes.get(doc_id) == new_hash:
155
+ files_skipped += 1
156
+ chunks_skipped += len(file_chunks)
157
+ continue
158
+
159
+ # Remove old chunks for this file before re-embedding
160
+ self._remove_file_chunks(doc_id)
161
+
162
+ # Batch embed
163
+ batch_ids = []
164
+ batch_texts = []
165
+ batch_metas = []
166
+ for chunk_id, chunk in file_chunks:
167
+ text = chunk.get("content", "").strip()
168
+ if not text or len(text) < 20:
169
+ chunks_skipped += 1
170
+ continue
171
+
172
+ # Prefix with file path + symbol for richer embeddings
173
+ name = chunk.get("name", "")
174
+ prefix = f"File: {doc_id}"
175
+ if name:
176
+ prefix += f" | {chunk.get('type', 'symbol')}: {name}"
177
+ embed_text = f"{prefix}\n{text}"
178
+
179
+ batch_ids.append(chunk_id)
180
+ batch_texts.append(embed_text)
181
+ batch_metas.append({
182
+ "doc_id": doc_id,
183
+ "name": name or "",
184
+ "type": chunk.get("type", "chunk"),
185
+ "line_start": chunk.get("line_start", 0),
186
+ "line_end": chunk.get("line_end", 0),
187
+ })
188
+
189
+ if len(batch_ids) >= self.batch_size:
190
+ ok = self._embed_batch(batch_ids, batch_texts, batch_metas)
191
+ if ok:
192
+ chunks_embedded += len(batch_ids)
193
+ else:
194
+ errors += len(batch_ids)
195
+ batch_ids, batch_texts, batch_metas = [], [], []
196
+
197
+ # Flush remaining batch
198
+ if batch_ids:
199
+ ok = self._embed_batch(batch_ids, batch_texts, batch_metas)
200
+ if ok:
201
+ chunks_embedded += len(batch_ids)
202
+ else:
203
+ errors += len(batch_ids)
204
+
205
+ self._file_hashes[doc_id] = new_hash
206
+ files_processed += 1
207
+
208
+ self._save_hashes()
209
+
210
+ return {
211
+ "files_processed": files_processed,
212
+ "files_skipped": files_skipped,
213
+ "chunks_embedded": chunks_embedded,
214
+ "chunks_skipped": chunks_skipped,
215
+ "errors": errors,
216
+ "total_embedded": self._collection.count() if self._collection else 0,
217
+ }
218
+
219
+ def _embed_batch(self, ids: list, texts: list, metas: list) -> bool:
220
+ """Embed and store a batch of chunks. Returns True on success."""
221
+ try:
222
+ embeddings = self.ollama.embed_batch(texts, model=self.embed_model)
223
+ if not embeddings or len(embeddings) != len(ids):
224
+ return False
225
+ self._collection.upsert(
226
+ ids=ids,
227
+ embeddings=embeddings,
228
+ documents=texts,
229
+ metadatas=metas,
230
+ )
231
+ return True
232
+ except Exception as e:
233
+ log.debug("Embedding batch failed: %s", e)
234
+ return False
235
+
236
+ def _remove_file_chunks(self, doc_id: str):
237
+ """Remove all embedded chunks belonging to a file."""
238
+ if not self._collection:
239
+ return
240
+ try:
241
+ self._collection.delete(where={"doc_id": doc_id})
242
+ except Exception:
243
+ # Some chromadb versions don't support where-delete well;
244
+ # fall back to getting IDs first
245
+ try:
246
+ results = self._collection.get(where={"doc_id": doc_id})
247
+ if results and results.get("ids"):
248
+ self._collection.delete(ids=results["ids"])
249
+ except Exception:
250
+ pass
251
+
252
+ # ── Search ────────────────────────────────────────────
253
+
254
+ def search(
255
+ self,
256
+ query: str,
257
+ top_k: int = 5,
258
+ max_tokens: int = 2000,
259
+ ) -> list[dict]:
260
+ """Semantic search over embedded code chunks.
261
+
262
+ Returns list of dicts with: file, lines, name, type, content, score, tokens.
263
+ """
264
+ if not self.ready or not self._collection or self._collection.count() == 0:
265
+ return []
266
+
267
+ try:
268
+ query_embedding = self.ollama.embed(query, model=self.embed_model)
269
+ if not query_embedding:
270
+ return []
271
+
272
+ results = self._collection.query(
273
+ query_embeddings=[query_embedding],
274
+ n_results=min(top_k * 2, self._collection.count()),
275
+ include=["documents", "metadatas", "distances"],
276
+ )
277
+ except Exception as e:
278
+ log.debug("Semantic search failed: %s", e)
279
+ return []
280
+
281
+ if not results or not results.get("ids") or not results["ids"][0]:
282
+ return []
283
+
284
+ ids = results["ids"][0]
285
+ documents = results["documents"][0] if results.get("documents") else []
286
+ metadatas = results["metadatas"][0] if results.get("metadatas") else []
287
+ distances = results["distances"][0] if results.get("distances") else []
288
+
289
+ from core import count_tokens
290
+
291
+ output = []
292
+ total_tokens = 0
293
+ for i, chunk_id in enumerate(ids):
294
+ meta = metadatas[i] if i < len(metadatas) else {}
295
+ doc = documents[i] if i < len(documents) else ""
296
+ dist = distances[i] if i < len(distances) else 1.0
297
+
298
+ # chromadb cosine distance: 0 = identical, 2 = opposite
299
+ score = max(0.0, 1.0 - dist)
300
+
301
+ # Strip the prefix we added during embedding
302
+ content = doc
303
+ if "\n" in content:
304
+ content = content.split("\n", 1)[1]
305
+
306
+ tok = count_tokens(content)
307
+ if total_tokens + tok > max_tokens and output:
308
+ break
309
+
310
+ line_start = meta.get("line_start", 0)
311
+ line_end = meta.get("line_end", 0)
312
+ lines_str = f"{line_start}-{line_end}" if line_start else "?"
313
+
314
+ output.append({
315
+ "file": meta.get("doc_id", "?"),
316
+ "lines": lines_str,
317
+ "name": meta.get("name", ""),
318
+ "type": meta.get("type", "chunk"),
319
+ "content": content,
320
+ "score": round(score, 4),
321
+ "tokens": tok,
322
+ })
323
+ total_tokens += tok
324
+
325
+ if len(output) >= top_k:
326
+ break
327
+
328
+ return output
329
+
330
+ # ── Stats ─────────────────────────────────────────────
331
+
332
+ def get_stats(self) -> dict:
333
+ count = self._collection.count() if self._collection else 0
334
+ return {
335
+ "ready": self.ready,
336
+ "chromadb_available": self._available,
337
+ "ollama_available": self._ollama_ok,
338
+ "embed_model": self.embed_model,
339
+ "total_embedded_chunks": count,
340
+ "files_tracked": len(self._file_hashes),
341
+ }
@@ -0,0 +1,123 @@
1
+ """Opt-in crash + error reporting via Sentry.
2
+
3
+ Off by default. Enabled only when ALL of the following are true:
4
+
5
+ 1. The ``sentry-sdk`` package is installed (pulled in by the optional
6
+ ``code-context-control[telemetry]`` extra).
7
+ 2. ``SENTRY_DSN`` is set in the environment.
8
+ 3. The user has opted in by setting ``C3_TELEMETRY_OPT_IN=1`` in the
9
+ environment OR by creating ``~/.c3/telemetry.json`` with
10
+ ``{"opt_in": true}``.
11
+
12
+ When enabled, only unhandled exceptions and explicit ``capture_error``
13
+ calls are transmitted. We strip query strings, file paths, and any
14
+ ``args``/``kwargs`` payloads via a ``before_send`` hook to avoid
15
+ leaking source code or prompts. No performance / tracing data is sent.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ _INITIALIZED = False
25
+ _TELEMETRY_FILE = Path.home() / ".c3" / "telemetry.json"
26
+
27
+
28
+ def _user_opted_in() -> bool:
29
+ if os.environ.get("C3_TELEMETRY_OPT_IN") == "1":
30
+ return True
31
+ try:
32
+ if _TELEMETRY_FILE.exists():
33
+ data = json.loads(_TELEMETRY_FILE.read_text(encoding="utf-8"))
34
+ return bool(data.get("opt_in"))
35
+ except Exception:
36
+ return False
37
+ return False
38
+
39
+
40
+ def _scrub_event(event: dict[str, Any], hint: dict[str, Any]) -> dict[str, Any] | None:
41
+ """Strip potentially-sensitive payloads from Sentry events."""
42
+ try:
43
+ # Remove request body / query string if present
44
+ request = event.get("request") or {}
45
+ request.pop("data", None)
46
+ request.pop("query_string", None)
47
+ request.pop("cookies", None)
48
+ request.pop("headers", None)
49
+ if request:
50
+ event["request"] = request
51
+
52
+ # Remove local variables from stack frames (often contain file content,
53
+ # prompts, or model output).
54
+ for thread in event.get("threads", {}).get("values", []) or []:
55
+ for frame in thread.get("stacktrace", {}).get("frames", []) or []:
56
+ frame.pop("vars", None)
57
+ for exc in event.get("exception", {}).get("values", []) or []:
58
+ for frame in exc.get("stacktrace", {}).get("frames", []) or []:
59
+ frame.pop("vars", None)
60
+
61
+ # Strip extra/contexts payloads
62
+ event.pop("extra", None)
63
+ event["contexts"] = {
64
+ k: v for k, v in (event.get("contexts") or {}).items()
65
+ if k in ("runtime", "os", "device")
66
+ }
67
+ except Exception:
68
+ # Never let scrubbing crash the reporter
69
+ pass
70
+ return event
71
+
72
+
73
+ def init(component: str = "c3", version: str = "unknown") -> bool:
74
+ """Initialize Sentry if all opt-in conditions are met. Idempotent.
75
+
76
+ Returns True if Sentry was initialized in this call (or already was),
77
+ False if any precondition was missing.
78
+ """
79
+ global _INITIALIZED
80
+ if _INITIALIZED:
81
+ return True
82
+
83
+ dsn = os.environ.get("SENTRY_DSN", "").strip()
84
+ if not dsn:
85
+ return False
86
+ if not _user_opted_in():
87
+ return False
88
+
89
+ try:
90
+ import sentry_sdk # type: ignore[import-not-found]
91
+ except ImportError:
92
+ return False
93
+
94
+ try:
95
+ sentry_sdk.init(
96
+ dsn=dsn,
97
+ release=f"{component}@{version}",
98
+ environment=os.environ.get("C3_ENV", "production"),
99
+ traces_sample_rate=0.0, # no perf tracing
100
+ profiles_sample_rate=0.0, # no profiling
101
+ send_default_pii=False,
102
+ attach_stacktrace=True,
103
+ before_send=_scrub_event,
104
+ )
105
+ sentry_sdk.set_tag("c3.component", component)
106
+ _INITIALIZED = True
107
+ return True
108
+ except Exception:
109
+ return False
110
+
111
+
112
+ def capture_error(exc: BaseException, *, component: str | None = None) -> None:
113
+ """Best-effort error capture; no-op if Sentry is not initialized."""
114
+ if not _INITIALIZED:
115
+ return
116
+ try:
117
+ import sentry_sdk # type: ignore[import-not-found]
118
+ with sentry_sdk.push_scope() as scope:
119
+ if component:
120
+ scope.set_tag("c3.component", component)
121
+ sentry_sdk.capture_exception(exc)
122
+ except Exception:
123
+ pass