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,155 @@
1
+ """File-based conversation persistence for Oracle Chat."""
2
+
3
+ import json
4
+ import uuid
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ _STORE_DIR = Path.home() / ".c3" / "oracle" / "conversations"
9
+
10
+
11
+ class ChatStore:
12
+ """Stores chat conversations as JSON files in ~/.c3/oracle/conversations/."""
13
+
14
+ def __init__(self, store_dir: Path = _STORE_DIR):
15
+ self.store_dir = store_dir
16
+ self.store_dir.mkdir(parents=True, exist_ok=True)
17
+ self._index_path = self.store_dir / "index.json"
18
+
19
+ # ── Index ─────────────────────────────────────────────
20
+
21
+ def _load_index(self) -> list[dict]:
22
+ if self._index_path.exists():
23
+ try:
24
+ return json.loads(self._index_path.read_text("utf-8"))
25
+ except (json.JSONDecodeError, OSError):
26
+ pass
27
+ return []
28
+
29
+ def _save_index(self, index: list[dict]):
30
+ self._index_path.write_text(json.dumps(index, indent=2), "utf-8")
31
+
32
+ def _touch_index(self, conv_id: str, **updates):
33
+ """Update an index entry in-place."""
34
+ index = self._load_index()
35
+ for entry in index:
36
+ if entry["id"] == conv_id:
37
+ entry["updated"] = datetime.now(timezone.utc).isoformat()
38
+ entry.update(updates)
39
+ break
40
+ self._save_index(index)
41
+
42
+ # ── Conversations ─────────────────────────────────────
43
+
44
+ def list_conversations(self, limit: int = 50) -> list[dict]:
45
+ """Return index entries sorted by most recent first."""
46
+ index = self._load_index()
47
+ index.sort(key=lambda e: e.get("updated", ""), reverse=True)
48
+ return index[:limit]
49
+
50
+ def create_conversation(self, title: str | None = None) -> str:
51
+ """Create a new empty conversation. Returns its ID."""
52
+ conv_id = uuid.uuid4().hex[:12]
53
+ now = datetime.now(timezone.utc).isoformat()
54
+ entry = {
55
+ "id": conv_id,
56
+ "title": title or "New chat",
57
+ "created": now,
58
+ "updated": now,
59
+ "message_count": 0,
60
+ }
61
+ index = self._load_index()
62
+ index.insert(0, entry)
63
+ self._save_index(index)
64
+ self._conv_path(conv_id).write_text("[]", "utf-8")
65
+ return conv_id
66
+
67
+ def get_conversation(self, conv_id: str) -> list[dict]:
68
+ """Return full message list for a conversation."""
69
+ path = self._conv_path(conv_id)
70
+ if not path.exists():
71
+ return []
72
+ try:
73
+ return json.loads(path.read_text("utf-8"))
74
+ except (json.JSONDecodeError, OSError):
75
+ return []
76
+
77
+ def append_message(self, conv_id: str, message: dict):
78
+ """Append a message and update the index."""
79
+ path = self._conv_path(conv_id)
80
+ messages = self.get_conversation(conv_id)
81
+ if "timestamp" not in message:
82
+ message["timestamp"] = datetime.now(timezone.utc).isoformat()
83
+ messages.append(message)
84
+ path.write_text(json.dumps(messages, indent=2), "utf-8")
85
+
86
+ updates = {"message_count": len(messages)}
87
+ # Auto-title from first user message
88
+ if len(messages) == 1 and message.get("role") == "user":
89
+ updates["title"] = self._auto_title(message.get("content", ""))
90
+ self._touch_index(conv_id, **updates)
91
+
92
+ def append_messages(self, conv_id: str, new_messages: list[dict]):
93
+ """Append multiple messages at once."""
94
+ path = self._conv_path(conv_id)
95
+ messages = self.get_conversation(conv_id)
96
+ now = datetime.now(timezone.utc).isoformat()
97
+ for msg in new_messages:
98
+ if "timestamp" not in msg:
99
+ msg["timestamp"] = now
100
+ messages.append(msg)
101
+ path.write_text(json.dumps(messages, indent=2), "utf-8")
102
+
103
+ updates = {"message_count": len(messages)}
104
+ if len(messages) == len(new_messages) and new_messages and new_messages[0].get("role") == "user":
105
+ updates["title"] = self._auto_title(new_messages[0].get("content", ""))
106
+ self._touch_index(conv_id, **updates)
107
+
108
+ def delete_conversation(self, conv_id: str):
109
+ """Delete a conversation, its state, and its index entry."""
110
+ for path in (self._conv_path(conv_id), self._state_path(conv_id)):
111
+ if path.exists():
112
+ path.unlink()
113
+ index = [e for e in self._load_index() if e["id"] != conv_id]
114
+ self._save_index(index)
115
+
116
+ def update_title(self, conv_id: str, title: str):
117
+ self._touch_index(conv_id, title=title)
118
+
119
+ # ── Per-conversation state ────────────────────────────
120
+
121
+ _DEFAULT_STATE = {"focused_projects": [], "model": None, "depth": "normal"}
122
+
123
+ def get_state(self, conv_id: str) -> dict:
124
+ """Get conversation session state (focused projects, model, depth)."""
125
+ path = self._state_path(conv_id)
126
+ if path.exists():
127
+ try:
128
+ state = json.loads(path.read_text("utf-8"))
129
+ return {**self._DEFAULT_STATE, **state}
130
+ except (json.JSONDecodeError, OSError):
131
+ pass
132
+ return dict(self._DEFAULT_STATE)
133
+
134
+ def set_state(self, conv_id: str, state: dict):
135
+ """Persist full conversation session state."""
136
+ self._state_path(conv_id).write_text(json.dumps(state, indent=2), "utf-8")
137
+
138
+ def update_state(self, conv_id: str, **updates):
139
+ """Merge updates into existing state."""
140
+ state = self.get_state(conv_id)
141
+ state.update(updates)
142
+ self.set_state(conv_id, state)
143
+
144
+ # ── Helpers ───────────────────────────────────────────
145
+
146
+ def _conv_path(self, conv_id: str) -> Path:
147
+ return self.store_dir / f"{conv_id}.json"
148
+
149
+ def _state_path(self, conv_id: str) -> Path:
150
+ return self.store_dir / f"{conv_id}_state.json"
151
+
152
+ @staticmethod
153
+ def _auto_title(text: str) -> str:
154
+ text = text.strip().replace("\n", " ")
155
+ return text[:60] + ("..." if len(text) > 60 else "")
@@ -0,0 +1,154 @@
1
+ """Cross-project insight store for Oracle."""
2
+
3
+ import json
4
+ import uuid
5
+ from datetime import datetime, timezone
6
+
7
+ from oracle.config import ORACLE_DIR
8
+
9
+ _CROSS_MEMORY_FILE = ORACLE_DIR / "cross_memory.json"
10
+
11
+ INSIGHT_TYPES = {
12
+ "pattern", "dependency", "convention", "risk", "opportunity", "drift",
13
+ "shared_convention", "duplicated_fact", "divergent_decision", "shared_bug_pattern",
14
+ }
15
+
16
+
17
+ def _load() -> dict:
18
+ try:
19
+ if _CROSS_MEMORY_FILE.is_file():
20
+ with open(_CROSS_MEMORY_FILE, encoding="utf-8") as f:
21
+ return json.load(f)
22
+ except Exception:
23
+ pass
24
+ return {"version": 1, "insights": [], "project_links": []}
25
+
26
+
27
+ def _save(data: dict):
28
+ ORACLE_DIR.mkdir(parents=True, exist_ok=True)
29
+ with open(_CROSS_MEMORY_FILE, "w", encoding="utf-8") as f:
30
+ json.dump(data, f, indent=2)
31
+
32
+
33
+ class CrossMemory:
34
+ """Manages cross-project insights and project links."""
35
+
36
+ def __init__(self):
37
+ self.data = _load()
38
+
39
+ def reload(self):
40
+ self.data = _load()
41
+
42
+ def get_all_insights(self) -> list[dict]:
43
+ return [i for i in self.data.get("insights", []) if not i.get("dismissed")]
44
+
45
+ def get_for_project(self, project_path: str) -> list[dict]:
46
+ return [
47
+ i for i in self.get_all_insights()
48
+ if project_path in i.get("source_projects", [])
49
+ ]
50
+
51
+ def search(self, query: str, top_k: int = 10) -> list[dict]:
52
+ """Simple keyword search over insight text."""
53
+ query_lower = query.lower()
54
+ terms = query_lower.split()
55
+ scored = []
56
+ for insight in self.get_all_insights():
57
+ text_lower = insight.get("text", "").lower()
58
+ score = sum(1 for t in terms if t in text_lower)
59
+ if score > 0:
60
+ scored.append((score, insight))
61
+ scored.sort(key=lambda x: x[0], reverse=True)
62
+ return [s[1] for s in scored[:top_k]]
63
+
64
+ def add_insight(
65
+ self,
66
+ text: str,
67
+ insight_type: str,
68
+ source_projects: list[str],
69
+ source_fact_ids: dict[str, list[str]] | None = None,
70
+ confidence: float = 0.7,
71
+ tags: list[str] | None = None,
72
+ ) -> dict:
73
+ """Add a new cross-project insight, deduplicating by text similarity."""
74
+ # Simple dedup: skip if very similar insight already exists
75
+ for existing in self.get_all_insights():
76
+ if self._jaccard(text, existing.get("text", "")) > 0.6:
77
+ existing["last_reviewed"] = datetime.now(timezone.utc).isoformat()
78
+ existing["confidence"] = max(existing.get("confidence", 0), confidence)
79
+ _save(self.data)
80
+ return existing
81
+
82
+ now = datetime.now(timezone.utc).isoformat()
83
+ insight = {
84
+ "id": f"ins_{uuid.uuid4().hex[:12]}",
85
+ "type": insight_type if insight_type in INSIGHT_TYPES else "pattern",
86
+ "text": text,
87
+ "source_projects": source_projects,
88
+ "source_fact_ids": source_fact_ids or {},
89
+ "confidence": confidence,
90
+ "created_at": now,
91
+ "last_reviewed": now,
92
+ "dismissed": False,
93
+ "tags": tags or [],
94
+ }
95
+ self.data["insights"].append(insight)
96
+
97
+ # Update project links
98
+ self._update_links(insight)
99
+ _save(self.data)
100
+ return insight
101
+
102
+ def dismiss(self, insight_id: str) -> dict:
103
+ for insight in self.data.get("insights", []):
104
+ if insight["id"] == insight_id:
105
+ insight["dismissed"] = True
106
+ _save(self.data)
107
+ return {"dismissed": True, "id": insight_id}
108
+ return {"error": "Insight not found"}
109
+
110
+ def get_project_links(self) -> list[dict]:
111
+ return self.data.get("project_links", [])
112
+
113
+ def stats(self) -> dict:
114
+ insights = self.get_all_insights()
115
+ by_type: dict[str, int] = {}
116
+ for i in insights:
117
+ t = i.get("type", "unknown")
118
+ by_type[t] = by_type.get(t, 0) + 1
119
+ return {
120
+ "total_insights": len(insights),
121
+ "by_type": by_type,
122
+ "total_links": len(self.data.get("project_links", [])),
123
+ }
124
+
125
+ def _update_links(self, insight: dict):
126
+ """Create or strengthen project links from an insight."""
127
+ projects = insight.get("source_projects", [])
128
+ links = self.data.setdefault("project_links", [])
129
+ for i, src in enumerate(projects):
130
+ for dst in projects[i + 1:]:
131
+ existing = next(
132
+ (l for l in links if {l["src"], l["dst"]} == {src, dst}),
133
+ None,
134
+ )
135
+ if existing:
136
+ existing["strength"] = existing.get("strength", 0) + 1
137
+ if insight["id"] not in existing.get("insight_ids", []):
138
+ existing.setdefault("insight_ids", []).append(insight["id"])
139
+ else:
140
+ links.append({
141
+ "src": src,
142
+ "dst": dst,
143
+ "link_type": insight.get("type", "pattern"),
144
+ "strength": 1,
145
+ "insight_ids": [insight["id"]],
146
+ })
147
+
148
+ @staticmethod
149
+ def _jaccard(a: str, b: str) -> float:
150
+ words_a = set(a.lower().split())
151
+ words_b = set(b.lower().split())
152
+ if not words_a or not words_b:
153
+ return 0.0
154
+ return len(words_a & words_b) / len(words_a | words_b)