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,407 @@
1
+ """Auto-memory: rule-based learning from tool calls and session activity.
2
+
3
+ Runs in the background after every tool call. Extracts high-signal facts
4
+ from tool results, deduplicates against existing memory, and consolidates
5
+ stale/duplicate facts on session end. No LLM calls — pure rule-based.
6
+
7
+ Wired into mcp_server._finalize_response() and lifespan shutdown.
8
+ """
9
+
10
+ import re
11
+ import threading
12
+ from datetime import datetime, timezone
13
+ from typing import Any, Dict, List, Tuple
14
+
15
+ _PRIVATE_RE = re.compile(r"<private>.*?</private>", re.DOTALL | re.IGNORECASE)
16
+
17
+
18
+ def _strip_private(text: str) -> str:
19
+ """Remove <private>...</private> blocks before storing or queuing."""
20
+ return _PRIVATE_RE.sub("", text).strip()
21
+
22
+
23
+ class AutoMemory:
24
+ """Automatically extracts and consolidates facts from C3 tool activity."""
25
+
26
+ def __init__(self, memory_store, session_mgr, config: dict | None = None):
27
+ self.memory = memory_store
28
+ self.session_mgr = session_mgr
29
+ self._config = config or {}
30
+ self._queue: list = []
31
+ self._lock = threading.Lock()
32
+ self._worker_running = False
33
+ # Dedup cache: avoid re-extracting the same fact within a session.
34
+ self._recent: set = set()
35
+ self._max_recent = 200
36
+
37
+ # ── Public API ──────────────────────────────────────────────────
38
+
39
+ def on_tool_complete(
40
+ self, tool_name: str, args: dict, summary: str, result_text: str
41
+ ):
42
+ """Queue extraction after a tool call (non-blocking)."""
43
+ if not self._config.get("enabled", True):
44
+ return
45
+ # Only process tools that have extraction rules.
46
+ if tool_name not in _EXTRACTORS:
47
+ return
48
+ with self._lock:
49
+ self._queue.append((tool_name, args, summary, _strip_private(result_text)[:8000]))
50
+ self._ensure_worker()
51
+
52
+ def on_session_end(self):
53
+ """Called synchronously on session save / snapshot / shutdown."""
54
+ if not self._config.get("enabled", True):
55
+ return
56
+ self._drain_queue()
57
+ self._generate_session_summary()
58
+
59
+ def consolidate(self) -> dict:
60
+ """Merge duplicate facts and archive stale auto-facts. Returns stats."""
61
+ facts = getattr(self.memory, "facts", None) or []
62
+ if not facts:
63
+ return {"merged": 0, "archived": 0, "total": 0}
64
+
65
+ merged = 0
66
+ archived = 0
67
+ to_delete: set = set()
68
+
69
+ active = [f for f in facts if f.get("lifecycle", "active") == "active"]
70
+
71
+ # ── Merge duplicates (Jaccard > 0.55) ──
72
+ for i, a in enumerate(active):
73
+ if a["id"] in to_delete:
74
+ continue
75
+ for b in active[i + 1 :]:
76
+ if b["id"] in to_delete:
77
+ continue
78
+ sim = _jaccard(a["fact"], b["fact"])
79
+ if sim > 0.55:
80
+ keeper, victim = (
81
+ (a, b)
82
+ if a.get("relevance_count", 0) >= b.get("relevance_count", 0)
83
+ else (b, a)
84
+ )
85
+ if sim < 0.85:
86
+ merged_text = _merge_texts(keeper["fact"], victim["fact"])
87
+ try:
88
+ self.memory.update_fact(
89
+ keeper["id"],
90
+ merged_text,
91
+ keeper.get("category", "general"),
92
+ )
93
+ except Exception:
94
+ pass
95
+ to_delete.add(victim["id"])
96
+ merged += 1
97
+
98
+ # ── Archive stale auto-facts (unused for ≥ 7 days) ──
99
+ now = datetime.now(timezone.utc)
100
+ for f in active:
101
+ if f["id"] in to_delete:
102
+ continue
103
+ cat = f.get("category", "")
104
+ if not cat.startswith("auto:"):
105
+ continue
106
+ if f.get("relevance_count", 0) > 0:
107
+ continue
108
+ try:
109
+ age = (now - datetime.fromisoformat(f.get("timestamp", ""))).days
110
+ except (ValueError, TypeError):
111
+ age = 0
112
+ if age >= 7:
113
+ to_delete.add(f["id"])
114
+ archived += 1
115
+
116
+ # ── Rolling window: keep only last _MAX_SESSION_FACTS auto:session entries ──
117
+ _MAX_SESSION_FACTS = 5
118
+ session_facts = sorted(
119
+ [f for f in active if f.get("category") == "auto:session" and f["id"] not in to_delete],
120
+ key=lambda f: f.get("timestamp", ""),
121
+ reverse=True,
122
+ )
123
+ for f in session_facts[_MAX_SESSION_FACTS:]:
124
+ to_delete.add(f["id"])
125
+ archived += 1
126
+
127
+ # ── Archive verbose orphans: >600 chars, 0 recall, 14+ days old ──
128
+ for f in active:
129
+ if f["id"] in to_delete:
130
+ continue
131
+ if len(f.get("fact", "")) <= 600:
132
+ continue
133
+ if f.get("relevance_count", 0) > 0:
134
+ continue
135
+ try:
136
+ age = (now - datetime.fromisoformat(f.get("timestamp", ""))).days
137
+ except (ValueError, TypeError):
138
+ age = 0
139
+ if age >= 14:
140
+ to_delete.add(f["id"])
141
+ archived += 1
142
+
143
+ for fid in to_delete:
144
+ try:
145
+ self.memory.delete_fact(fid)
146
+ except Exception:
147
+ pass
148
+
149
+ return {
150
+ "merged": merged,
151
+ "archived": archived,
152
+ "total": len(facts) - len(to_delete),
153
+ }
154
+
155
+ # ── Background worker ───────────────────────────────────────────
156
+
157
+ def _ensure_worker(self):
158
+ if self._worker_running:
159
+ return
160
+ self._worker_running = True
161
+ t = threading.Thread(target=self._worker, daemon=True, name="c3-auto-memory")
162
+ t.start()
163
+
164
+ def _worker(self):
165
+ try:
166
+ self._drain_queue()
167
+ finally:
168
+ self._worker_running = False
169
+
170
+ def _drain_queue(self):
171
+ while True:
172
+ with self._lock:
173
+ if not self._queue:
174
+ return
175
+ item = self._queue.pop(0)
176
+ try:
177
+ self._process(*item)
178
+ except Exception:
179
+ pass
180
+
181
+ # ── Extraction ──────────────────────────────────────────────────
182
+
183
+ def _process(
184
+ self, tool_name: str, args: dict, summary: str, result_text: str
185
+ ):
186
+ extractor = _EXTRACTORS.get(tool_name)
187
+ if not extractor:
188
+ return
189
+ for fact_text, category in extractor(args, summary, result_text):
190
+ self._save_or_merge(fact_text, category)
191
+
192
+ def _save_or_merge(self, fact_text: str, category: str):
193
+ """Save a new fact or merge with the most similar existing one."""
194
+ fact_text = _strip_private(fact_text)
195
+ if len(fact_text) < 25:
196
+ return
197
+
198
+ # Session-level dedup.
199
+ key = fact_text[:120].lower()
200
+ if key in self._recent:
201
+ return
202
+ self._recent.add(key)
203
+ if len(self._recent) > self._max_recent:
204
+ self._recent.clear()
205
+
206
+ session_id = ""
207
+ if self.session_mgr and self.session_mgr.current_session:
208
+ session_id = self.session_mgr.current_session.get("id", "")
209
+
210
+ # Check existing facts for a merge candidate.
211
+ try:
212
+ existing = self.memory.recall(fact_text, top_k=3)
213
+ except Exception:
214
+ existing = []
215
+
216
+ for r in existing:
217
+ sim = _jaccard(r.get("fact", ""), fact_text)
218
+ if sim > 0.55:
219
+ if sim < 0.85:
220
+ merged = _merge_texts(r["fact"], fact_text)
221
+ try:
222
+ self.memory.update_fact(
223
+ r["id"], merged, category or r.get("category", "general")
224
+ )
225
+ except Exception:
226
+ pass
227
+ return # Already covered by existing fact.
228
+
229
+ try:
230
+ self.memory.remember(fact_text, category, session_id)
231
+ except Exception:
232
+ pass
233
+
234
+ # ── Session summary ─────────────────────────────────────────────
235
+
236
+ def _generate_session_summary(self):
237
+ """Build a compact session summary from decisions + file changes."""
238
+ if not self.session_mgr or not self.session_mgr.current_session:
239
+ return
240
+
241
+ session = self.session_mgr.current_session
242
+ decisions = session.get("decisions", [])
243
+ files = session.get("files_touched", [])
244
+
245
+ if not decisions and not files:
246
+ return
247
+
248
+ parts: list[str] = []
249
+
250
+ if files:
251
+ names = list(
252
+ dict.fromkeys(f.get("file", "") for f in files if f.get("file"))
253
+ )[:10]
254
+ types = sorted(set(f.get("type", "") for f in files if f.get("type")))
255
+ parts.append(f"Files ({', '.join(types)}): {', '.join(names)}")
256
+
257
+ if decisions:
258
+ for d in decisions[-3:]:
259
+ text = d.get("decision", "")
260
+ if text:
261
+ parts.append(f"Decision: {text}")
262
+
263
+ if not parts:
264
+ return
265
+
266
+ sid = session.get("id", "unknown")[:8]
267
+ summary = f"Session summary ({sid}): " + " | ".join(parts)
268
+ self._save_or_merge(summary, "auto:session")
269
+
270
+
271
+ # ── Extraction functions (pure, no side effects) ───────────────────
272
+
273
+
274
+ def _extract_validate(
275
+ args: dict, summary: str, result: str
276
+ ) -> List[Tuple[str, str]]:
277
+ """Extract validation failure patterns."""
278
+ learnings: list = []
279
+ fp = args.get("file_path", "")
280
+ if "FAIL" in result or "syntax_error" in summary:
281
+ error_lines = [l.strip() for l in result.splitlines() if l.strip().startswith("- L")][:3]
282
+ if error_lines:
283
+ learnings.append((
284
+ f"[validate] {fp} has syntax errors: {'; '.join(error_lines)}",
285
+ "auto:validate",
286
+ ))
287
+ return learnings
288
+
289
+
290
+ def _extract_search(
291
+ args: dict, summary: str, result: str
292
+ ) -> List[Tuple[str, str]]:
293
+ """Extract key file discoveries."""
294
+ learnings: list = []
295
+ query = args.get("query", "")
296
+ action = args.get("action", "code")
297
+ if action == "files" and query and len(result) > 50:
298
+ files = re.findall(
299
+ r"(?:^|\s)([\w/\\.-]+\.(?:py|js|ts|tsx|jsx|r|rs|go|java|rb|php|lua|pl))\b",
300
+ result,
301
+ re.IGNORECASE,
302
+ )
303
+ if files:
304
+ unique = list(dict.fromkeys(files))[:8]
305
+ learnings.append((
306
+ f"[search] Key files for '{query}': {', '.join(unique)}",
307
+ "auto:structure",
308
+ ))
309
+ return learnings
310
+
311
+
312
+ def _extract_compress(
313
+ args: dict, summary: str, result: str
314
+ ) -> List[Tuple[str, str]]:
315
+ """Extract top-level symbols from structural maps."""
316
+ learnings: list = []
317
+ fp = args.get("file_path", "")
318
+ mode = args.get("mode", "")
319
+ if fp and mode in ("map", "dense_map") and len(result) > 100:
320
+ symbols = re.findall(
321
+ r"^(?:def |class |function |export |pub fn |func )\s*(\w+)",
322
+ result,
323
+ re.MULTILINE,
324
+ )
325
+ if symbols:
326
+ unique = list(dict.fromkeys(symbols))[:15]
327
+ learnings.append((
328
+ f"[structure] {fp} exports: {', '.join(unique)}",
329
+ "auto:structure",
330
+ ))
331
+ return learnings
332
+
333
+
334
+ def _extract_edit(
335
+ args: dict, summary: str, result: str
336
+ ) -> List[Tuple[str, str]]:
337
+ """Extract what was edited and why (from c3_edit summary arg)."""
338
+ learnings: list = []
339
+ fp = args.get("file_path", "")
340
+ edit_summary = (args.get("summary") or "").strip()
341
+ if fp and edit_summary and len(edit_summary) >= 20:
342
+ learnings.append((
343
+ f"[edit] {fp}: {edit_summary}",
344
+ "auto:edit",
345
+ ))
346
+ return learnings
347
+
348
+
349
+ def _extract_agent(
350
+ args: dict, summary: str, result: str
351
+ ) -> List[Tuple[str, str]]:
352
+ """Extract workflow outcomes from c3_agent calls."""
353
+ learnings: list = []
354
+ workflow = args.get("workflow", "")
355
+ scope = args.get("scope", "")
356
+ if workflow and len(result) > 80:
357
+ # First meaningful non-header line from result
358
+ lines = [
359
+ line.strip()
360
+ for line in result.splitlines()
361
+ if line.strip() and not line.strip().startswith("[")
362
+ ]
363
+ finding = lines[0][:120] if lines else ""
364
+ if finding:
365
+ scope_str = f" (scope: {scope})" if scope else ""
366
+ learnings.append((
367
+ f"[agent:{workflow}]{scope_str}: {finding}",
368
+ "auto:agent",
369
+ ))
370
+ return learnings
371
+
372
+
373
+ _EXTRACTORS: Dict[str, Any] = {
374
+ "c3_validate": _extract_validate,
375
+ "c3_search": _extract_search,
376
+ "c3_compress": _extract_compress,
377
+ "c3_edit": _extract_edit,
378
+ "c3_agent": _extract_agent,
379
+ }
380
+
381
+
382
+ # ── Utility functions ──────────────────────────────────────────────
383
+
384
+
385
+ def _jaccard(a: str, b: str) -> float:
386
+ """Word-level Jaccard similarity."""
387
+ sa = set(a.lower().split())
388
+ sb = set(b.lower().split())
389
+ if not sa or not sb:
390
+ return 0.0
391
+ return len(sa & sb) / len(sa | sb)
392
+
393
+
394
+ def _merge_texts(existing: str, new: str) -> str:
395
+ """Merge two fact texts, preferring the more complete one."""
396
+ if len(new) < len(existing) * 0.5:
397
+ return existing
398
+ if len(existing) < len(new) * 0.5:
399
+ return new
400
+ # Check how much genuinely new content there is.
401
+ existing_words = set(existing.lower().split())
402
+ new_unique = [w for w in new.lower().split() if w not in existing_words]
403
+ if len(new_unique) < 3:
404
+ return existing
405
+ if len(existing) + len(new) > 500:
406
+ return new # Newer is more current; avoid bloat.
407
+ return f"{existing} [updated] {new}"
@@ -0,0 +1,6 @@
1
+ """External benchmark adapters for C3.
2
+
3
+ Integrates third-party benchmarks (Aider Polyglot, SWE-bench, etc.) so C3's
4
+ impact can be measured against industry-standard harnesses rather than only
5
+ C3's internal synthetic scenarios.
6
+ """
@@ -0,0 +1,29 @@
1
+ """External benchmark adapters."""
2
+
3
+ from services.bench.external.aider_polyglot import (
4
+ AiderPolyglotBenchmark,
5
+ AiderPolyglotResult,
6
+ detect_aider,
7
+ find_polyglot_repo,
8
+ )
9
+ from services.bench.external.swe_bench import (
10
+ SWEBenchAdapter,
11
+ SWEBenchReport,
12
+ SWEBenchResult,
13
+ SWEBenchTask,
14
+ evaluate_with_docker,
15
+ load_tasks,
16
+ )
17
+
18
+ __all__ = [
19
+ "AiderPolyglotBenchmark",
20
+ "AiderPolyglotResult",
21
+ "detect_aider",
22
+ "find_polyglot_repo",
23
+ "SWEBenchAdapter",
24
+ "SWEBenchTask",
25
+ "SWEBenchResult",
26
+ "SWEBenchReport",
27
+ "load_tasks",
28
+ "evaluate_with_docker",
29
+ ]