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,356 @@
1
+ """
2
+ Context Snapshot — Capture/restore working context across /clear boundaries.
3
+
4
+ Saves session state (decisions, files, notes, facts) before /clear,
5
+ provides compact briefings to reinstate context after /clear.
6
+ """
7
+ import json
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ from core import count_tokens
12
+
13
+ # Max characters per file structural map stored in snapshot
14
+ _FILE_MAP_MAX_CHARS = 600
15
+
16
+
17
+ class ContextSnapshot:
18
+ """Snapshot/restore for clear-and-recall workflow."""
19
+
20
+ def __init__(self, project_path: str, data_dir: str = ".c3/snapshots"):
21
+ self.project_path = Path(project_path)
22
+ self.data_dir = self.project_path / data_dir
23
+ self.data_dir.mkdir(parents=True, exist_ok=True)
24
+
25
+ def capture(self, session_mgr, memory_store,
26
+ task_description: str = "",
27
+ working_files: list = None,
28
+ custom_notes: str = "",
29
+ compressor=None) -> dict:
30
+ """Capture current working context to a snapshot file.
31
+
32
+ Returns {snapshot_id, path, token_count}.
33
+ """
34
+ session = session_mgr.current_session or {}
35
+ session_id = session.get("id", "")
36
+
37
+ # Collect decisions from current session
38
+ decisions = session.get("decisions", [])
39
+
40
+ # Collect files touched
41
+ files_touched = session.get("files_touched", [])
42
+
43
+ # Collect context notes
44
+ context_notes = session.get("context_notes", [])
45
+
46
+ # Session-scoped facts (added this session)
47
+ session_facts = [
48
+ f for f in memory_store.facts
49
+ if f.get("source_session") == session_id and session_id
50
+ ]
51
+
52
+ # Also recall top relevant facts from full memory store (cross-session)
53
+ relevant_facts = []
54
+ if task_description:
55
+ try:
56
+ recalled = memory_store.recall(task_description, top_k=8)
57
+ session_fact_texts = {f["fact"] for f in session_facts}
58
+ relevant_facts = [
59
+ {"fact": r["fact"], "category": r.get("category", "general")}
60
+ for r in recalled
61
+ if r["fact"] not in session_fact_texts
62
+ ][:6]
63
+ except Exception:
64
+ pass
65
+
66
+ # Auto-populate working_files from files_touched when not explicitly provided
67
+ if not working_files and files_touched:
68
+ working_files = [ft["file"] for ft in files_touched[:8]]
69
+
70
+ # Capture structural maps of working files for immediate context on restore
71
+ file_maps = {}
72
+ if compressor and working_files:
73
+ for fp in working_files[:5]:
74
+ try:
75
+ abs_fp = str(self.project_path / fp) if not Path(fp).is_absolute() else fp
76
+ result = compressor.compress_file(abs_fp, mode="structure")
77
+ if result and not result.get("error"):
78
+ file_maps[fp] = result.get("compressed", "")[:_FILE_MAP_MAX_CHARS]
79
+ except Exception:
80
+ pass
81
+
82
+ # Extract plan decisions separately so they are surfaced prominently on restore
83
+ plans = [
84
+ d for d in decisions
85
+ if d.get("decision", "").startswith("PLAN:")
86
+ ]
87
+
88
+ # Context budget snapshot
89
+ budget = session.get("context_budget", {})
90
+
91
+ snapshot = {
92
+ "schema_version": 3,
93
+ "snapshot_id": datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S"),
94
+ "created": datetime.now(timezone.utc).isoformat(),
95
+ "session_id": session_id,
96
+ "task_description": task_description,
97
+ "working_files": working_files or [],
98
+ "custom_notes": custom_notes,
99
+ "decisions": decisions,
100
+ "plans": plans,
101
+ "files_touched": files_touched,
102
+ "context_notes": context_notes,
103
+ "session_facts": [
104
+ {"fact": f["fact"], "category": f["category"]}
105
+ for f in session_facts
106
+ ],
107
+ "relevant_facts": relevant_facts,
108
+ "file_maps": file_maps,
109
+ "context_budget": {
110
+ "response_tokens": budget.get("response_tokens", 0),
111
+ "call_count": budget.get("call_count", 0),
112
+ },
113
+ "state": {
114
+ "task_description": task_description,
115
+ "working_files": working_files or [],
116
+ "decisions": decisions,
117
+ "files_touched": files_touched,
118
+ "context_notes": context_notes,
119
+ "session_facts": [
120
+ {"fact": f["fact"], "category": f["category"], "id": f.get("id", "")}
121
+ for f in session_facts
122
+ ],
123
+ "context_budget": {
124
+ "response_tokens": budget.get("response_tokens", 0),
125
+ "call_count": budget.get("call_count", 0),
126
+ },
127
+ },
128
+ }
129
+
130
+ path = self.data_dir / f"snap_{snapshot['snapshot_id']}.json"
131
+ with open(path, 'w', encoding='utf-8') as f:
132
+ json.dump(snapshot, f, indent=2)
133
+
134
+ token_count = count_tokens(json.dumps(snapshot))
135
+ return {
136
+ "snapshot_id": snapshot["snapshot_id"],
137
+ "path": str(path),
138
+ "token_count": token_count,
139
+ }
140
+
141
+ def restore(self, snapshot_id: str = "latest", memory_store=None, level: int = 0) -> dict:
142
+ """Restore a snapshot as a full markdown briefing.
143
+
144
+ Args:
145
+ snapshot_id: Snapshot ID or 'latest'.
146
+ memory_store: Optional MemoryStore for live recall enrichment.
147
+ level: 0=full briefing, 1=compact briefing (for auto-restore notifications).
148
+
149
+ Returns {snapshot_id, briefing, tokens}.
150
+ """
151
+ snap = self._load_snapshot(snapshot_id)
152
+ if "error" in snap:
153
+ return snap
154
+
155
+ # Enrich with live memory recall so cross-session facts are surfaced immediately
156
+ if memory_store and snap.get("task_description"):
157
+ try:
158
+ recalled = memory_store.recall(snap["task_description"], top_k=6)
159
+ existing_texts = (
160
+ {f["fact"] for f in snap.get("session_facts", [])}
161
+ | {f["fact"] for f in snap.get("relevant_facts", [])}
162
+ )
163
+ snap["_live_recall"] = [
164
+ {"fact": r["fact"], "category": r.get("category", "general")}
165
+ for r in recalled
166
+ if r["fact"] not in existing_texts
167
+ ][:5]
168
+ except Exception:
169
+ pass
170
+
171
+ sid = snap["snapshot_id"]
172
+ briefing = self._compact_briefing(snap) if level > 0 else self._full_briefing(snap)
173
+
174
+ return {
175
+ "snapshot_id": sid,
176
+ "briefing": briefing,
177
+ "tokens": count_tokens(briefing),
178
+ "state": snap.get("state", {}),
179
+ }
180
+
181
+ def list_snapshots(self, n: int = 10) -> list:
182
+ """List recent snapshots."""
183
+ files = sorted(self.data_dir.glob("snap_*.json"), reverse=True)[:n]
184
+ results = []
185
+ for sf in files:
186
+ try:
187
+ with open(sf, encoding='utf-8') as f:
188
+ snap = json.load(f)
189
+ results.append({
190
+ "id": snap["snapshot_id"],
191
+ "created": snap.get("created", ""),
192
+ "task_description": snap.get("task_description", "")[:80],
193
+ "decisions_count": len(snap.get("decisions", [])),
194
+ "files_count": len(snap.get("files_touched", [])),
195
+ })
196
+ except Exception:
197
+ continue
198
+ return results
199
+
200
+ def restore_state(self, snapshot_id: str = "latest") -> dict:
201
+ snap = self._load_snapshot(snapshot_id)
202
+ if "error" in snap:
203
+ return snap
204
+ return {
205
+ "snapshot_id": snap["snapshot_id"],
206
+ "state": snap.get("state", {}),
207
+ }
208
+
209
+ def search(self, query: str, top_k: int = 5) -> list:
210
+ query_l = (query or "").lower().strip()
211
+ if not query_l:
212
+ return []
213
+ results = []
214
+ for item in self.list_snapshots(50):
215
+ snap = self._load_snapshot(item["id"])
216
+ haystack = " ".join([
217
+ snap.get("task_description", ""),
218
+ snap.get("custom_notes", ""),
219
+ " ".join(note for note in snap.get("context_notes", [])),
220
+ " ".join(d.get("decision", "") for d in snap.get("decisions", [])),
221
+ " ".join(f.get("fact", "") for f in snap.get("session_facts", [])),
222
+ ]).lower()
223
+ if query_l in haystack:
224
+ results.append({
225
+ "snapshot_id": snap["snapshot_id"],
226
+ "task_description": snap.get("task_description", ""),
227
+ "created": snap.get("created", ""),
228
+ "score": 1.0 if query_l in snap.get("task_description", "").lower() else 0.6,
229
+ })
230
+ if len(results) >= top_k:
231
+ break
232
+ return results
233
+
234
+ def _load_snapshot(self, snapshot_id: str) -> dict:
235
+ """Load a snapshot by ID or 'latest'."""
236
+ if snapshot_id == "latest":
237
+ files = sorted(self.data_dir.glob("snap_*.json"), reverse=True)
238
+ if not files:
239
+ return {"error": "No snapshots found"}
240
+ path = files[0]
241
+ else:
242
+ path = self.data_dir / f"snap_{snapshot_id}.json"
243
+
244
+ if not path.exists():
245
+ return {"error": f"Snapshot not found: {snapshot_id}"}
246
+
247
+ with open(path, encoding='utf-8') as f:
248
+ return json.load(f)
249
+
250
+ def _full_briefing(self, snap: dict) -> str:
251
+ """Level 0: Full briefing with all details."""
252
+ parts = [f"# Context Restore: {snap.get('task_description', 'N/A')}"]
253
+ parts.append(f"Snapshot: {snap['snapshot_id']} | Session: {snap.get('session_id', '?')}")
254
+
255
+ if snap.get("custom_notes"):
256
+ parts.append(f"\n## Notes\n{snap['custom_notes']}")
257
+
258
+ # Plans first — highest priority context
259
+ plans = snap.get("plans", [])
260
+ if plans:
261
+ parts.append("\n## Plans")
262
+ for d in plans:
263
+ text = d["decision"].removeprefix("PLAN:").strip()
264
+ if d.get("reasoning"):
265
+ text += f" — {d['reasoning']}"
266
+ parts.append(f"- {text}")
267
+
268
+ # Live-recalled facts (cross-session, surfaced at restore time)
269
+ live_recall = snap.get("_live_recall", [])
270
+ if live_recall:
271
+ parts.append("\n## Relevant Memory (live recall)")
272
+ for fact in live_recall:
273
+ parts.append(f"- [{fact['category']}] {fact['fact']}")
274
+
275
+ # Relevant facts captured at snapshot time (cross-session)
276
+ relevant_facts = snap.get("relevant_facts", [])
277
+ if relevant_facts:
278
+ parts.append("\n## Relevant Memory (at snapshot)")
279
+ for fact in relevant_facts:
280
+ parts.append(f"- [{fact['category']}] {fact['fact']}")
281
+
282
+ decisions = snap.get("decisions", [])
283
+ non_plan_decisions = [d for d in decisions if not d.get("decision", "").startswith("PLAN:")]
284
+ if non_plan_decisions:
285
+ parts.append("\n## Decisions")
286
+ for d in non_plan_decisions:
287
+ line = f"- {d['decision']}"
288
+ if d.get("reasoning"):
289
+ line += f" — {d['reasoning']}"
290
+ parts.append(line)
291
+
292
+ files = snap.get("files_touched", [])
293
+ if files:
294
+ parts.append("\n## Files Touched")
295
+ for ft in files:
296
+ summary = f" — {ft['summary']}" if ft.get("summary") else ""
297
+ parts.append(f"- {ft.get('type', '?')}: {ft['file']}{summary}")
298
+
299
+ # File structural maps — skip re-reading after restore
300
+ file_maps = snap.get("file_maps", {})
301
+ if file_maps:
302
+ parts.append("\n## File Structures (skip re-reading)")
303
+ for fp, fmap in file_maps.items():
304
+ parts.append(f"\n### {fp}\n```\n{fmap}\n```")
305
+
306
+ ctx_notes = snap.get("context_notes", [])
307
+ if ctx_notes:
308
+ parts.append("\n## Context Notes")
309
+ for note in ctx_notes:
310
+ parts.append(f"- {note}")
311
+
312
+ facts = snap.get("session_facts", [])
313
+ if facts:
314
+ parts.append("\n## Session Facts")
315
+ for fact in facts:
316
+ parts.append(f"- [{fact['category']}] {fact['fact']}")
317
+
318
+ working = snap.get("working_files", [])
319
+ if working:
320
+ parts.append(f"\n## Working Files\n{', '.join(working)}")
321
+
322
+ budget = snap.get("context_budget", {})
323
+ if budget.get("response_tokens"):
324
+ parts.append(f"\n## Budget\n{budget['response_tokens']}tok / {budget['call_count']} calls")
325
+
326
+ return "\n".join(parts)
327
+
328
+ def _compact_briefing(self, snap: dict) -> str:
329
+ """Level 1: Compact briefing — top decisions + file list."""
330
+ parts = [f"[restore:{snap['snapshot_id']}] {snap.get('task_description', '')}"]
331
+
332
+ plans = snap.get("plans", [])
333
+ if plans:
334
+ for d in plans:
335
+ text = d["decision"].removeprefix("PLAN:").strip()
336
+ parts.append(f"[plan] {text[:80]}")
337
+
338
+ decisions = snap.get("decisions", [])
339
+ non_plan = [d for d in decisions if not d.get("decision", "").startswith("PLAN:")]
340
+ if non_plan:
341
+ top = non_plan[-3:] # Most recent 3
342
+ for d in top:
343
+ parts.append(f"- {d['decision'][:80]}")
344
+
345
+ files = snap.get("files_touched", [])
346
+ if files:
347
+ file_list = ", ".join(ft["file"] for ft in files[:10])
348
+ parts.append(f"files: {file_list}")
349
+
350
+ facts = snap.get("session_facts", [])
351
+ relevant = snap.get("relevant_facts", [])
352
+ total_facts = len(facts) + len(relevant)
353
+ if total_facts:
354
+ parts.append(f"facts: {total_facts} available")
355
+
356
+ return "\n".join(parts)