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.
- cli/__init__.py +1 -0
- cli/_hook_utils.py +99 -0
- cli/c3.py +6152 -0
- cli/commands/__init__.py +1 -0
- cli/commands/common.py +312 -0
- cli/commands/parser.py +286 -0
- cli/docs.html +3178 -0
- cli/edits.html +878 -0
- cli/hook_auto_snapshot.py +142 -0
- cli/hook_c3_signal.py +61 -0
- cli/hook_c3read.py +116 -0
- cli/hook_edit_ledger.py +213 -0
- cli/hook_edit_unlock.py +170 -0
- cli/hook_filter.py +130 -0
- cli/hook_ghost_files.py +238 -0
- cli/hook_pretool_enforce.py +334 -0
- cli/hook_read.py +200 -0
- cli/hook_session_stats.py +62 -0
- cli/hook_terse_advisor.py +190 -0
- cli/hub.html +3764 -0
- cli/hub_server.py +1619 -0
- cli/mcp_proxy.py +428 -0
- cli/mcp_server.py +660 -0
- cli/server.py +2985 -0
- cli/tools/__init__.py +4 -0
- cli/tools/_helpers.py +65 -0
- cli/tools/agent.py +1165 -0
- cli/tools/compress.py +215 -0
- cli/tools/delegate.py +1184 -0
- cli/tools/edit.py +313 -0
- cli/tools/edits.py +118 -0
- cli/tools/filter.py +285 -0
- cli/tools/impact.py +163 -0
- cli/tools/memory.py +469 -0
- cli/tools/read.py +224 -0
- cli/tools/search.py +337 -0
- cli/tools/session.py +95 -0
- cli/tools/shell.py +193 -0
- cli/tools/status.py +306 -0
- cli/tools/validate.py +310 -0
- cli/ui/api.js +36 -0
- cli/ui/app.js +207 -0
- cli/ui/components/chat.js +758 -0
- cli/ui/components/dashboard.js +689 -0
- cli/ui/components/edits.js +220 -0
- cli/ui/components/instructions.js +481 -0
- cli/ui/components/memory.js +626 -0
- cli/ui/components/sessions.js +606 -0
- cli/ui/components/settings.js +1404 -0
- cli/ui/components/sidebar.js +156 -0
- cli/ui/icons.js +51 -0
- cli/ui/shared.js +119 -0
- cli/ui/theme.js +22 -0
- cli/ui.html +168 -0
- cli/ui_legacy.html +6797 -0
- cli/ui_nano.html +503 -0
- code_context_control-2.28.0.dist-info/METADATA +248 -0
- code_context_control-2.28.0.dist-info/RECORD +150 -0
- code_context_control-2.28.0.dist-info/WHEEL +5 -0
- code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
- code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
- code_context_control-2.28.0.dist-info/top_level.txt +5 -0
- core/__init__.py +75 -0
- core/config.py +269 -0
- core/ide.py +188 -0
- oracle/__init__.py +1 -0
- oracle/config.py +75 -0
- oracle/oracle.html +3900 -0
- oracle/oracle_server.py +663 -0
- oracle/services/__init__.py +1 -0
- oracle/services/c3_bridge.py +210 -0
- oracle/services/chat_engine.py +1103 -0
- oracle/services/chat_store.py +155 -0
- oracle/services/cross_memory.py +154 -0
- oracle/services/federated_graph.py +463 -0
- oracle/services/health_checker.py +117 -0
- oracle/services/insight_engine.py +307 -0
- oracle/services/memory_reader.py +106 -0
- oracle/services/memory_writer.py +182 -0
- oracle/services/ollama_bridge.py +332 -0
- oracle/services/project_scanner.py +87 -0
- oracle/services/review_agent.py +206 -0
- services/__init__.py +1 -0
- services/activity_log.py +93 -0
- services/agent_base.py +124 -0
- services/agents.py +1529 -0
- services/auto_memory.py +407 -0
- services/bench/__init__.py +6 -0
- services/bench/external/__init__.py +29 -0
- services/bench/external/aider_polyglot.py +405 -0
- services/bench/external/swe_bench.py +485 -0
- services/benchmark_dashboard.py +596 -0
- services/claude_md.py +785 -0
- services/compressor.py +592 -0
- services/context_snapshot.py +356 -0
- services/conversation_store.py +870 -0
- services/doc_index.py +537 -0
- services/e2e_benchmark.py +2884 -0
- services/e2e_evaluator.py +396 -0
- services/e2e_tasks.py +743 -0
- services/edit_ledger.py +459 -0
- services/embedding_index.py +341 -0
- services/error_reporting.py +123 -0
- services/file_memory.py +734 -0
- services/hub_service.py +585 -0
- services/indexer.py +712 -0
- services/memory.py +318 -0
- services/memory_consolidator.py +538 -0
- services/memory_graph.py +382 -0
- services/memory_grounder.py +304 -0
- services/memory_scorer.py +246 -0
- services/metrics.py +86 -0
- services/notifications.py +209 -0
- services/ollama_client.py +201 -0
- services/output_filter.py +488 -0
- services/parser.py +1238 -0
- services/project_manager.py +579 -0
- services/protocol.py +306 -0
- services/proxy_state.py +152 -0
- services/retrieval_broker.py +129 -0
- services/router.py +414 -0
- services/runtime.py +326 -0
- services/session_benchmark.py +1945 -0
- services/session_manager.py +1026 -0
- services/session_preloader.py +251 -0
- services/text_index.py +90 -0
- services/tool_classifier.py +176 -0
- services/transcript_index.py +340 -0
- services/validation_cache.py +155 -0
- services/vector_store.py +299 -0
- services/version_tracker.py +271 -0
- services/watcher.py +192 -0
- tui/__init__.py +0 -0
- tui/backend.py +59 -0
- tui/main.py +145 -0
- tui/screens/__init__.py +1 -0
- tui/screens/benchmark_view.py +109 -0
- tui/screens/claudemd_view.py +46 -0
- tui/screens/compress_view.py +52 -0
- tui/screens/index_view.py +74 -0
- tui/screens/init_view.py +82 -0
- tui/screens/mcp_view.py +73 -0
- tui/screens/optimize_view.py +41 -0
- tui/screens/pipe_view.py +46 -0
- tui/screens/projects_view.py +355 -0
- tui/screens/search_view.py +55 -0
- tui/screens/session_view.py +143 -0
- tui/screens/stats.py +158 -0
- tui/screens/ui_view.py +54 -0
- 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)
|