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
core/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Token counting and measurement utilities."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import re
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
|
|
6
|
+
# Lazy-loaded tiktoken encoder
|
|
7
|
+
_encoder = None
|
|
8
|
+
|
|
9
|
+
# LRU cache keyed on content hash — avoids redundant tiktoken encodes
|
|
10
|
+
# within a session where the same code chunks are counted repeatedly.
|
|
11
|
+
_TOKEN_CACHE_MAX = 2048
|
|
12
|
+
|
|
13
|
+
@lru_cache(maxsize=_TOKEN_CACHE_MAX)
|
|
14
|
+
def _cached_count(content_hash: str, length: int, text: str) -> int:
|
|
15
|
+
"""Count tokens with LRU memoization. length param ensures hash collisions are safe."""
|
|
16
|
+
encoder = _get_encoder()
|
|
17
|
+
if encoder:
|
|
18
|
+
return len(encoder.encode(text))
|
|
19
|
+
tokens = re.findall(r'\w+|[^\w\s]', text)
|
|
20
|
+
count = 0
|
|
21
|
+
for t in tokens:
|
|
22
|
+
if len(t) <= 4:
|
|
23
|
+
count += 1
|
|
24
|
+
else:
|
|
25
|
+
count += max(1, len(t) // 4)
|
|
26
|
+
return count
|
|
27
|
+
|
|
28
|
+
def _get_encoder():
|
|
29
|
+
"""Lazy-load tiktoken encoder (one-time cost)."""
|
|
30
|
+
global _encoder
|
|
31
|
+
if _encoder is None:
|
|
32
|
+
try:
|
|
33
|
+
import tiktoken
|
|
34
|
+
_encoder = tiktoken.get_encoding("cl100k_base")
|
|
35
|
+
except (ImportError, Exception):
|
|
36
|
+
_encoder = False # Sentinel: tiktoken unavailable
|
|
37
|
+
return _encoder
|
|
38
|
+
|
|
39
|
+
def count_tokens(text: str) -> int:
|
|
40
|
+
"""
|
|
41
|
+
Count tokens using tiktoken (cl100k_base) with LRU caching.
|
|
42
|
+
Repeated calls with identical text hit the cache (~0ms vs ~1-2ms).
|
|
43
|
+
Falls back to heuristic if tiktoken is unavailable.
|
|
44
|
+
"""
|
|
45
|
+
if not text:
|
|
46
|
+
return 0
|
|
47
|
+
# For short strings (<50 chars), hashing overhead > tiktoken cost — skip cache
|
|
48
|
+
if len(text) < 50:
|
|
49
|
+
encoder = _get_encoder()
|
|
50
|
+
if encoder:
|
|
51
|
+
return len(encoder.encode(text))
|
|
52
|
+
return len(re.findall(r'\w+|[^\w\s]', text))
|
|
53
|
+
h = hashlib.md5(text.encode("utf-8", errors="replace")).hexdigest()
|
|
54
|
+
return _cached_count(h, len(text), text)
|
|
55
|
+
|
|
56
|
+
def measure_savings(original: str, compressed: str) -> dict:
|
|
57
|
+
"""Measure token savings between original and compressed text."""
|
|
58
|
+
orig_tokens = count_tokens(original)
|
|
59
|
+
comp_tokens = count_tokens(compressed)
|
|
60
|
+
saved = orig_tokens - comp_tokens
|
|
61
|
+
pct = (saved / orig_tokens * 100) if orig_tokens > 0 else 0
|
|
62
|
+
return {
|
|
63
|
+
"original_tokens": orig_tokens,
|
|
64
|
+
"compressed_tokens": comp_tokens,
|
|
65
|
+
"saved_tokens": saved,
|
|
66
|
+
"savings_pct": round(pct, 1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def format_token_count(n: int) -> str:
|
|
70
|
+
"""Human-readable token count."""
|
|
71
|
+
if n >= 1_000_000:
|
|
72
|
+
return f"{n/1_000_000:.1f}M"
|
|
73
|
+
elif n >= 1_000:
|
|
74
|
+
return f"{n/1_000:.1f}K"
|
|
75
|
+
return str(n)
|
core/config.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Hybrid configuration loader for C3 v2.3 features.
|
|
2
|
+
|
|
3
|
+
Loads the "hybrid" section from .c3/config.json with sensible defaults.
|
|
4
|
+
All three tiers (output filter, router, SLTM) can be independently disabled.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
DEFAULTS = {
|
|
10
|
+
"ollama_base_url": "http://localhost:11434",
|
|
11
|
+
"HYBRID_DISABLE_TIER1": False, # Output filter
|
|
12
|
+
"HYBRID_DISABLE_TIER2": False, # Router
|
|
13
|
+
"HYBRID_DISABLE_SLTM": False, # Vector memory
|
|
14
|
+
"show_context_nudges": True, # Append budget nudge when over threshold
|
|
15
|
+
"prepend_notifications": True, # Prepend agent notifications to tool/chat responses
|
|
16
|
+
# Model assignments
|
|
17
|
+
"embed_model": "nomic-embed-text",
|
|
18
|
+
"filter_model": "gemma3n:latest",
|
|
19
|
+
"summary_model": "gemma3n:latest",
|
|
20
|
+
"simple_qa_model": "deepseek-r1:1.5b",
|
|
21
|
+
"complex_model": "llama3.2:3b",
|
|
22
|
+
# Router params
|
|
23
|
+
"router_log_threshold": 500, # tokens: route to log_summary
|
|
24
|
+
"router_simple_threshold": 100, # tokens: route to simple_qa
|
|
25
|
+
"router_allow_model_fallback": True,
|
|
26
|
+
"router_fallback_models": [], # Optional ordered model fallbacks
|
|
27
|
+
"router_retry_on_empty": True, # Retry with fallback when first model returns no response
|
|
28
|
+
"validate_timeout_seconds": 12, # Hard timeout for c3_validate native syntax checks
|
|
29
|
+
# Filter params
|
|
30
|
+
"filter_llm_threshold": 500, # tokens: trigger LLM pass
|
|
31
|
+
# SLTM params
|
|
32
|
+
"sltm_alpha": 0.5, # TF-IDF weight in hybrid search (1-alpha = vector weight)
|
|
33
|
+
"sltm_min_score": 0.3, # Minimum similarity threshold for VectorStore search
|
|
34
|
+
# Auto-memory: background learning from tool calls
|
|
35
|
+
"auto_memory": {
|
|
36
|
+
"enabled": True, # Set False to disable all auto-extraction
|
|
37
|
+
},
|
|
38
|
+
# Local RAG Pipeline: auto-retrieve project docs on session start
|
|
39
|
+
"rag": {
|
|
40
|
+
"enabled": True,
|
|
41
|
+
"max_precontext_tokens": 400,
|
|
42
|
+
},
|
|
43
|
+
# Edit Ledger: auto-log Edit/Write operations
|
|
44
|
+
"edit_ledger": {
|
|
45
|
+
"enabled": True,
|
|
46
|
+
"tracking_level": "standard", # "minimal", "standard", "detailed"
|
|
47
|
+
"auto_tag": True, # Auto-tag edits from hooks
|
|
48
|
+
},
|
|
49
|
+
# c3_agent compound workflows
|
|
50
|
+
"agent_workflows": {
|
|
51
|
+
"enabled": True,
|
|
52
|
+
"prefetch_max_files": 3, # Max files to auto-compress on search prefetch
|
|
53
|
+
"batch_validate_max_files": 10, # Max files in a single batch validate call
|
|
54
|
+
"compound_max_compress": 5, # Max files to compress in compound workflows
|
|
55
|
+
"delegate_in_workflows": True, # Allow Ollama delegation in compound workflows
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_hybrid_config(project_path: str) -> dict:
|
|
61
|
+
"""Load hybrid config from .c3/config.json, merged with defaults."""
|
|
62
|
+
config_file = Path(project_path) / ".c3" / "config.json"
|
|
63
|
+
hybrid = {}
|
|
64
|
+
if config_file.exists():
|
|
65
|
+
try:
|
|
66
|
+
with open(config_file, encoding="utf-8") as f:
|
|
67
|
+
data = json.load(f)
|
|
68
|
+
hybrid = data.get("hybrid", {})
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
merged = {**DEFAULTS, **hybrid}
|
|
72
|
+
if "show_savings_footer" not in hybrid and "SHOW_SAVINGS_SUMMARY" in hybrid:
|
|
73
|
+
merged["show_savings_footer"] = bool(hybrid.get("SHOW_SAVINGS_SUMMARY"))
|
|
74
|
+
if "validate_timeout_seconds" not in hybrid and "validate_review_timeout_seconds" in hybrid:
|
|
75
|
+
merged["validate_timeout_seconds"] = int(hybrid.get("validate_review_timeout_seconds") or DEFAULTS["validate_timeout_seconds"])
|
|
76
|
+
merged["SHOW_SAVINGS_SUMMARY"] = bool(merged.get("show_savings_footer", False))
|
|
77
|
+
return merged
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ─── Agent Defaults ────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
FILE_MEMORY_DEFAULTS = {
|
|
83
|
+
"enabled": True,
|
|
84
|
+
"max_tracked_files": 200,
|
|
85
|
+
"summary_model": "gemma3n:latest",
|
|
86
|
+
"summary_max_tokens": 80,
|
|
87
|
+
"nudge_threshold": 500, # Min chars in Read result to trigger nudge
|
|
88
|
+
"auto_index_on_read": True, # Auto-index files when Read hook fires
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Keep in sync with services/agents.py:create_agents factory defaults.
|
|
93
|
+
# Core (always-on by default): IndexStaleness, FileMemory, EditLedgerEnricher.
|
|
94
|
+
# Everything else defaults off; users opt in via the UI / config.
|
|
95
|
+
AGENT_DEFAULTS = {
|
|
96
|
+
"IndexStaleness": {
|
|
97
|
+
"enabled": True, "interval": 60, "use_ai": False,
|
|
98
|
+
"ai_model": "gemma3n:latest", "warn_threshold": 5, "rebuild_threshold": 15,
|
|
99
|
+
},
|
|
100
|
+
"MemoryPruner": {
|
|
101
|
+
"enabled": False, "interval": 300, "use_ai": True,
|
|
102
|
+
"ai_model": "gemma3n:latest", "embed_model": "nomic-embed-text",
|
|
103
|
+
"similarity_threshold": 0.8,
|
|
104
|
+
},
|
|
105
|
+
"ClaudeMdDrift": {
|
|
106
|
+
"enabled": False, "interval": 120, "use_ai": False, "ai_model": "gemma3n:latest",
|
|
107
|
+
},
|
|
108
|
+
"SessionInsight": {
|
|
109
|
+
"enabled": False, "interval": 600, "use_ai": True,
|
|
110
|
+
"ai_model": "gemma3n:latest", "min_tool_calls": 10,
|
|
111
|
+
},
|
|
112
|
+
"AutonomyPlanner": {
|
|
113
|
+
"enabled": False, "interval": 240, "use_ai": True,
|
|
114
|
+
"ai_model": "gemma3n:latest", "lookback_tool_calls": 30,
|
|
115
|
+
"cooldown_seconds": 600, "min_signal_score": 2, "max_actions": 3,
|
|
116
|
+
},
|
|
117
|
+
"ClaudeMdUpdater": {
|
|
118
|
+
"enabled": False, "interval": 900, "use_ai": True,
|
|
119
|
+
"ai_model": "gemma3n:latest", "auto_apply": True,
|
|
120
|
+
"min_facts_for_promote": 2,
|
|
121
|
+
},
|
|
122
|
+
"FileMemory": {
|
|
123
|
+
"enabled": True, "interval": 120, "use_ai": False,
|
|
124
|
+
"ai_model": "gemma3n:latest", "max_files_per_cycle": 5,
|
|
125
|
+
},
|
|
126
|
+
"DelegateCoach": {
|
|
127
|
+
"enabled": False, "interval": 300, "use_ai": False,
|
|
128
|
+
"ai_model": "gemma3n:latest", "lookback_lines": 100,
|
|
129
|
+
},
|
|
130
|
+
"KeyFileVersion": {
|
|
131
|
+
"enabled": False, "interval": 180, "use_ai": False,
|
|
132
|
+
"ai_model": "gemma3n:latest", "agent_target": "current",
|
|
133
|
+
"max_changes_per_notice": 4,
|
|
134
|
+
},
|
|
135
|
+
"EditLedgerEnricher": {
|
|
136
|
+
"enabled": True, "interval": 10, "use_ai": False,
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ─── Proxy Defaults ────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
PROXY_DEFAULTS = {
|
|
144
|
+
"always_visible": ["core"], # Categories always visible when proxy filtering is enabled
|
|
145
|
+
"max_tools": 12,
|
|
146
|
+
"filter_tools": True, # True = dynamic filtering by category, False = show all
|
|
147
|
+
"use_slm": True,
|
|
148
|
+
"slm_model": "gemma3n:latest",
|
|
149
|
+
"context_window_size": 10,
|
|
150
|
+
"inject_context_summary": False,
|
|
151
|
+
"PROXY_DISABLE": False,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def load_proxy_config(project_path: str) -> dict:
|
|
156
|
+
"""Load proxy config from .c3/config.json, merged with defaults."""
|
|
157
|
+
config_file = Path(project_path) / ".c3" / "config.json"
|
|
158
|
+
proxy = {}
|
|
159
|
+
if config_file.exists():
|
|
160
|
+
try:
|
|
161
|
+
with open(config_file, encoding="utf-8") as f:
|
|
162
|
+
data = json.load(f)
|
|
163
|
+
proxy = data.get("proxy", {})
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
merged = {**PROXY_DEFAULTS, **proxy}
|
|
167
|
+
always_visible = merged.get("always_visible", ["core"])
|
|
168
|
+
if isinstance(always_visible, str):
|
|
169
|
+
always_visible = [always_visible]
|
|
170
|
+
merged["always_visible"] = always_visible or ["core"]
|
|
171
|
+
return merged
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def load_mcp_config(project_path: str) -> dict:
|
|
175
|
+
"""Load MCP mode config from .c3/config.json, merged with defaults."""
|
|
176
|
+
config_file = Path(project_path) / ".c3" / "config.json"
|
|
177
|
+
mcp = {}
|
|
178
|
+
if config_file.exists():
|
|
179
|
+
try:
|
|
180
|
+
with open(config_file, encoding="utf-8") as f:
|
|
181
|
+
data = json.load(f)
|
|
182
|
+
mcp = data.get("mcp", {})
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
mode = str(mcp.get("mode", "direct") or "direct").strip().lower()
|
|
186
|
+
if mode not in {"direct", "proxy"}:
|
|
187
|
+
mode = "direct"
|
|
188
|
+
return {"mode": mode}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ─── Delegate Defaults ────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
DELEGATE_DEFAULTS = {
|
|
194
|
+
"enabled": True,
|
|
195
|
+
"preferred_model": "", # Empty = auto-select per task type
|
|
196
|
+
"allow_model_fallback": True, # If preferred/default missing, pick best available local model
|
|
197
|
+
"fallback_models": [], # Optional ordered list of fallback model names
|
|
198
|
+
"max_tokens": 512,
|
|
199
|
+
"temperature": 0.3,
|
|
200
|
+
"auto_compress": True, # Auto-compress file_path content as context
|
|
201
|
+
"auto_search": True, # Auto-search index for 'ask' tasks
|
|
202
|
+
"auto_vector_search": True, # Auto-search vector store if available
|
|
203
|
+
"auto_activity_log": True, # Auto-inject recent activity for diagnose
|
|
204
|
+
"search_top_k": 2,
|
|
205
|
+
"max_context_tokens": 1400,
|
|
206
|
+
# Delegation threshold policy
|
|
207
|
+
"threshold_enabled": True, # Token-saving mode: delegate by default once threshold is met
|
|
208
|
+
"threshold_min_total_tokens": 60,
|
|
209
|
+
"threshold_task_types": ["ask", "explain", "summarize", "improve", "docstring"],
|
|
210
|
+
"threshold_force_task_types": ["diagnose", "review", "test"],
|
|
211
|
+
# Per-task model overrides (empty = use preferred_model or task default)
|
|
212
|
+
"summarize_model": "",
|
|
213
|
+
"explain_model": "",
|
|
214
|
+
"docstring_model": "",
|
|
215
|
+
"review_model": "",
|
|
216
|
+
"ask_model": "",
|
|
217
|
+
"test_model": "",
|
|
218
|
+
"diagnose_model": "",
|
|
219
|
+
"improve_model": "",
|
|
220
|
+
# Codex CLI integration (cloud delegate backend)
|
|
221
|
+
"codex_enabled": True, # Master switch — enable Codex as delegate backend
|
|
222
|
+
"codex_default_model": "gpt-5.3-codex-spark", # Default Codex model
|
|
223
|
+
"codex_default_sandbox": "read-only", # Default sandbox: read-only, workspace-write, danger-full-access
|
|
224
|
+
"codex_reasoning_effort": "high", # xhigh, high, medium, low
|
|
225
|
+
"codex_timeout": 90, # Subprocess timeout in seconds (idle watchdog kills MCP hangs at 20s)
|
|
226
|
+
"codex_max_context_tokens": 4000, # Context truncation limit for Codex calls
|
|
227
|
+
"codex_task_types": ["review", "diagnose", "improve", "test"], # Task types auto-routed to Codex
|
|
228
|
+
"codex_verify_edits": False, # EditLedgerEnricher sends diffs to Codex for review
|
|
229
|
+
"codex_memory_bridge": True, # Auto-extract findings from Codex into c3_memory
|
|
230
|
+
# Gemini CLI integration (cloud delegate backend)
|
|
231
|
+
"gemini_enabled": True, # Master switch — enable Gemini as delegate backend
|
|
232
|
+
"gemini_default_model": "gemini-2.5-flash", # Default Gemini model
|
|
233
|
+
"gemini_timeout": 45, # Subprocess timeout in seconds (idle watchdog kills MCP hangs at 15s)
|
|
234
|
+
"gemini_max_context_tokens": 8000, # Context truncation limit (Gemini has large context windows)
|
|
235
|
+
"gemini_task_types": ["review", "diagnose", "improve", "test"], # Tasks auto-routed to Gemini
|
|
236
|
+
"gemini_memory_bridge": True, # Auto-extract findings from Gemini into c3_memory
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def load_delegate_config(project_path: str) -> dict:
|
|
241
|
+
"""Load delegate config from .c3/config.json, merged with defaults."""
|
|
242
|
+
config_file = Path(project_path) / ".c3" / "config.json"
|
|
243
|
+
delegate = {}
|
|
244
|
+
if config_file.exists():
|
|
245
|
+
try:
|
|
246
|
+
with open(config_file, encoding="utf-8") as f:
|
|
247
|
+
data = json.load(f)
|
|
248
|
+
delegate = data.get("delegate", {})
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
return {**DELEGATE_DEFAULTS, **delegate}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def load_agent_config(project_path: str) -> dict:
|
|
255
|
+
"""Load agent config from .c3/config.json, merged with AGENT_DEFAULTS."""
|
|
256
|
+
config_file = Path(project_path) / ".c3" / "config.json"
|
|
257
|
+
overrides = {}
|
|
258
|
+
if config_file.exists():
|
|
259
|
+
try:
|
|
260
|
+
with open(config_file, encoding="utf-8") as f:
|
|
261
|
+
data = json.load(f)
|
|
262
|
+
overrides = data.get("agents", {})
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
# Merge per-agent: defaults ← overrides
|
|
266
|
+
result = {}
|
|
267
|
+
for name, defaults in AGENT_DEFAULTS.items():
|
|
268
|
+
result[name] = {**defaults, **overrides.get(name, {})}
|
|
269
|
+
return result
|
core/ide.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""IDE Profile Registry for cross-IDE MCP support.
|
|
2
|
+
|
|
3
|
+
Defines profiles for each supported IDE with their config paths, capabilities,
|
|
4
|
+
and instructions file locations. Enables graceful degradation of Claude-specific
|
|
5
|
+
features in non-Claude IDEs.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class IDEProfile:
|
|
15
|
+
"""Describes an IDE's MCP configuration requirements and capabilities."""
|
|
16
|
+
name: str # "claude-code", "vscode", "cursor", "codex"
|
|
17
|
+
display_name: str # "Claude Code", "VS Code Copilot"
|
|
18
|
+
config_path: str # ".mcp.json", ".vscode/mcp.json"
|
|
19
|
+
config_key: str # "mcpServers", "servers", or "mcp_servers"
|
|
20
|
+
needs_type_field: bool # VS Code requires "type": "stdio"
|
|
21
|
+
instructions_file: Optional[str] # "CLAUDE.md", ".github/copilot-instructions.md"
|
|
22
|
+
instructions_line_limit: Optional[int] # 200 for Claude, None for others
|
|
23
|
+
supports_hooks: bool # Only Claude Code
|
|
24
|
+
supports_transcripts: bool # Only Claude Code (reads ~/.claude/)
|
|
25
|
+
supports_clear: bool # Only Claude Code has /clear
|
|
26
|
+
settings_path: Optional[str] # ".claude/settings.local.json" or None
|
|
27
|
+
config_format: str = "json" # "json" or "toml"
|
|
28
|
+
config_path_global: bool = False # True = config_path resolved from home dir, not project
|
|
29
|
+
hook_event: str = "PostToolUse" # Claude="PostToolUse", Gemini="AfterTool"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ─── IDE Profiles ─────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
PROFILES = {
|
|
35
|
+
"claude-code": IDEProfile(
|
|
36
|
+
name="claude-code",
|
|
37
|
+
display_name="Claude Code",
|
|
38
|
+
config_path=".mcp.json",
|
|
39
|
+
config_key="mcpServers",
|
|
40
|
+
needs_type_field=False,
|
|
41
|
+
instructions_file="CLAUDE.md",
|
|
42
|
+
instructions_line_limit=200,
|
|
43
|
+
supports_hooks=True,
|
|
44
|
+
supports_transcripts=True,
|
|
45
|
+
supports_clear=True,
|
|
46
|
+
settings_path=".claude/settings.local.json",
|
|
47
|
+
),
|
|
48
|
+
"vscode": IDEProfile(
|
|
49
|
+
name="vscode",
|
|
50
|
+
display_name="VS Code Copilot",
|
|
51
|
+
config_path=".vscode/mcp.json",
|
|
52
|
+
config_key="servers",
|
|
53
|
+
needs_type_field=True,
|
|
54
|
+
instructions_file=".github/copilot-instructions.md",
|
|
55
|
+
instructions_line_limit=None,
|
|
56
|
+
supports_hooks=False,
|
|
57
|
+
supports_transcripts=False,
|
|
58
|
+
supports_clear=False,
|
|
59
|
+
settings_path=None,
|
|
60
|
+
),
|
|
61
|
+
"cursor": IDEProfile(
|
|
62
|
+
name="cursor",
|
|
63
|
+
display_name="Cursor",
|
|
64
|
+
config_path=".cursor/mcp.json",
|
|
65
|
+
config_key="mcpServers",
|
|
66
|
+
needs_type_field=False,
|
|
67
|
+
instructions_file=".cursorrules",
|
|
68
|
+
instructions_line_limit=None,
|
|
69
|
+
supports_hooks=False,
|
|
70
|
+
supports_transcripts=False,
|
|
71
|
+
supports_clear=False,
|
|
72
|
+
settings_path=None,
|
|
73
|
+
),
|
|
74
|
+
"codex": IDEProfile(
|
|
75
|
+
name="codex",
|
|
76
|
+
display_name="OpenAI Codex",
|
|
77
|
+
config_path=".codex/config.toml", # project-scoped; global is ~/.codex/config.toml
|
|
78
|
+
config_key="mcp_servers", # TOML: [mcp_servers.<name>]
|
|
79
|
+
needs_type_field=False,
|
|
80
|
+
instructions_file="AGENTS.md",
|
|
81
|
+
instructions_line_limit=None,
|
|
82
|
+
supports_hooks=False,
|
|
83
|
+
supports_transcripts=False,
|
|
84
|
+
supports_clear=False,
|
|
85
|
+
settings_path=None,
|
|
86
|
+
config_format="toml",
|
|
87
|
+
),
|
|
88
|
+
"gemini": IDEProfile(
|
|
89
|
+
name="gemini",
|
|
90
|
+
display_name="Gemini CLI",
|
|
91
|
+
config_path=".gemini/settings.json", # project-scoped; global is ~/.gemini/settings.json
|
|
92
|
+
config_key="mcpServers",
|
|
93
|
+
needs_type_field=False,
|
|
94
|
+
instructions_file="GEMINI.md",
|
|
95
|
+
instructions_line_limit=None,
|
|
96
|
+
supports_hooks=True,
|
|
97
|
+
supports_transcripts=False,
|
|
98
|
+
supports_clear=False,
|
|
99
|
+
settings_path=".gemini/settings.json", # same file as MCP config
|
|
100
|
+
hook_event="AfterTool",
|
|
101
|
+
),
|
|
102
|
+
"antigravity": IDEProfile(
|
|
103
|
+
name="antigravity",
|
|
104
|
+
display_name="Google Antigravity",
|
|
105
|
+
config_path=".gemini/antigravity/mcp_config.json", # resolved from home dir
|
|
106
|
+
config_key="mcpServers",
|
|
107
|
+
needs_type_field=False,
|
|
108
|
+
instructions_file="GEMINI.md",
|
|
109
|
+
instructions_line_limit=None,
|
|
110
|
+
supports_hooks=False,
|
|
111
|
+
supports_transcripts=False,
|
|
112
|
+
supports_clear=False,
|
|
113
|
+
settings_path=None,
|
|
114
|
+
config_path_global=True, # ~/.gemini/antigravity/mcp_config.json
|
|
115
|
+
),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
IDE_ALIASES = {
|
|
120
|
+
"claude": "claude-code",
|
|
121
|
+
"claude-code": "claude-code",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def normalize_ide_name(ide_name: str | None) -> str:
|
|
126
|
+
"""Normalize external IDE names and aliases to canonical profile keys."""
|
|
127
|
+
raw = (ide_name or "").strip().lower()
|
|
128
|
+
if not raw:
|
|
129
|
+
return "claude-code"
|
|
130
|
+
return IDE_ALIASES.get(raw, raw)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_profile(ide_name: str) -> IDEProfile:
|
|
134
|
+
"""Get IDE profile by name. Returns claude-code profile if unknown."""
|
|
135
|
+
return PROFILES.get(normalize_ide_name(ide_name), PROFILES["claude-code"])
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def detect_ide(project_path: str) -> str:
|
|
139
|
+
"""Auto-detect IDE from project directory markers.
|
|
140
|
+
Prioritizes explicit IDE folders over general .mcp.json to avoid mis-detection.
|
|
141
|
+
"""
|
|
142
|
+
p = Path(project_path)
|
|
143
|
+
|
|
144
|
+
# 1. Direct config matches (strongest)
|
|
145
|
+
if (p / ".vscode" / "mcp.json").exists():
|
|
146
|
+
return "vscode"
|
|
147
|
+
if (p / ".cursor" / "mcp.json").exists():
|
|
148
|
+
return "cursor"
|
|
149
|
+
if (p / ".codex" / "config.toml").exists():
|
|
150
|
+
return "codex"
|
|
151
|
+
if (p / ".gemini" / "settings.json").exists():
|
|
152
|
+
# Prefer antigravity if its user-global dir already exists
|
|
153
|
+
if (Path.home() / ".gemini" / "antigravity").is_dir():
|
|
154
|
+
return "antigravity"
|
|
155
|
+
return "gemini"
|
|
156
|
+
|
|
157
|
+
# 2. IDE directory markers (even without config yet)
|
|
158
|
+
if (p / ".codex").is_dir():
|
|
159
|
+
return "codex"
|
|
160
|
+
if (p / ".gemini").is_dir():
|
|
161
|
+
if (Path.home() / ".gemini" / "antigravity").is_dir():
|
|
162
|
+
return "antigravity"
|
|
163
|
+
return "gemini"
|
|
164
|
+
if (p / ".vscode").is_dir():
|
|
165
|
+
return "vscode"
|
|
166
|
+
if (p / ".cursor").is_dir():
|
|
167
|
+
return "cursor"
|
|
168
|
+
|
|
169
|
+
# 3. Fallback to Claude markers
|
|
170
|
+
if (p / ".claude").is_dir() or (p / ".mcp.json").exists():
|
|
171
|
+
return "claude-code"
|
|
172
|
+
|
|
173
|
+
return "claude-code"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def load_ide_config(project_path: str) -> str:
|
|
177
|
+
"""Read 'ide' from .c3/config.json. Returns 'claude-code' if not set."""
|
|
178
|
+
config_file = Path(project_path) / ".c3" / "config.json"
|
|
179
|
+
if config_file.exists():
|
|
180
|
+
try:
|
|
181
|
+
with open(config_file, encoding="utf-8") as f:
|
|
182
|
+
data = json.load(f)
|
|
183
|
+
ide = normalize_ide_name(data.get("ide", "claude-code"))
|
|
184
|
+
if ide in PROFILES:
|
|
185
|
+
return ide
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
return "claude-code"
|
oracle/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Oracle — Cross-project memory management agent for C3."""
|
oracle/config.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Oracle configuration loader."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ORACLE_DIR = Path.home() / ".c3" / "oracle"
|
|
7
|
+
|
|
8
|
+
CONFIG_FILE = ORACLE_DIR / "config.json"
|
|
9
|
+
|
|
10
|
+
DEFAULTS = {
|
|
11
|
+
"port": 3331,
|
|
12
|
+
"ollama_base_url": "https://ollama.com",
|
|
13
|
+
"ollama_api_key": "",
|
|
14
|
+
"model": "gemma4:31b-cloud",
|
|
15
|
+
"hub_url": "http://localhost:3330",
|
|
16
|
+
"review_interval_seconds": 1800,
|
|
17
|
+
"review_enabled": True,
|
|
18
|
+
"auto_open_browser": True,
|
|
19
|
+
"theme": "dark",
|
|
20
|
+
"max_facts_per_analysis": 100,
|
|
21
|
+
"insight_confidence_threshold": 0.5,
|
|
22
|
+
"log_level": "INFO",
|
|
23
|
+
"federated_graph_ttl_sec": 3600,
|
|
24
|
+
"cross_sim_threshold": 0.75,
|
|
25
|
+
"cross_max_facts_per_project": 200,
|
|
26
|
+
"cross_top_k_neighbors": 3,
|
|
27
|
+
"embedding_model": "nomic-embed-text",
|
|
28
|
+
"agents": [
|
|
29
|
+
{
|
|
30
|
+
"id": "architect",
|
|
31
|
+
"name": "Architect",
|
|
32
|
+
"description": "Expert in system architecture, design patterns, and cross-project structure. Best for high-level analysis.",
|
|
33
|
+
"system_prompt": "You are the Architect. Focus on structural integrity, design patterns, and the big picture. Provide high-level recommendations before diving into code.",
|
|
34
|
+
"model": "gemma4:31b-cloud",
|
|
35
|
+
"active": True
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "code_explorer",
|
|
39
|
+
"name": "Code Explorer",
|
|
40
|
+
"description": "Specializes in deep code analysis, bug hunting, and tracing execution paths.",
|
|
41
|
+
"system_prompt": "You are the Code Explorer. Be incredibly precise, cite specific lines of code, and focus on the technical implementation details. Trace logic thoroughly.",
|
|
42
|
+
"model": "gemma4:31b-cloud",
|
|
43
|
+
"active": True
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "memory_analyst",
|
|
47
|
+
"name": "Memory Analyst",
|
|
48
|
+
"description": "Focuses on analyzing project memory, facts, and insights.",
|
|
49
|
+
"system_prompt": "You are the Memory Analyst. Rely heavily on memory tools and facts to spot trends. Connect current issues to past context.",
|
|
50
|
+
"model": "gemma4:31b-cloud",
|
|
51
|
+
"active": True
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_config() -> dict:
|
|
58
|
+
"""Load Oracle config, merged with defaults."""
|
|
59
|
+
cfg = dict(DEFAULTS)
|
|
60
|
+
try:
|
|
61
|
+
if CONFIG_FILE.exists():
|
|
62
|
+
with open(CONFIG_FILE, encoding="utf-8") as f:
|
|
63
|
+
cfg.update(json.load(f))
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return cfg
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def save_config(cfg: dict):
|
|
70
|
+
"""Write Oracle config to disk."""
|
|
71
|
+
ORACLE_DIR.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
merged = dict(DEFAULTS)
|
|
73
|
+
merged.update(cfg)
|
|
74
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
75
|
+
json.dump(merged, f, indent=2)
|