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
cli/mcp_server.py
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
C3 MCP Server - Claude Code Companion as a native MCP tool server.
|
|
4
|
+
|
|
5
|
+
Exposes 10 C3 tools as MCP endpoints. Tool logic lives in cli/tools/.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python cli/mcp_server.py --project <path>
|
|
9
|
+
"""
|
|
10
|
+
import argparse
|
|
11
|
+
import asyncio
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
# Add parent to path for imports
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
23
|
+
|
|
24
|
+
from fastmcp import Context, FastMCP
|
|
25
|
+
|
|
26
|
+
from core.ide import get_profile, load_ide_config
|
|
27
|
+
from services.auto_memory import AutoMemory
|
|
28
|
+
from services.context_snapshot import ContextSnapshot
|
|
29
|
+
from services.runtime import C3Runtime, build_runtime, start_runtime, stop_runtime
|
|
30
|
+
from services.transcript_index import TranscriptIndex
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Read version without importing cli.c3 (heavy side effects)
|
|
34
|
+
def _read_version() -> str:
|
|
35
|
+
try:
|
|
36
|
+
_c3_py = Path(__file__).parent / "c3.py"
|
|
37
|
+
for line in _c3_py.read_text(encoding="utf-8").splitlines():
|
|
38
|
+
if line.startswith("__version__"):
|
|
39
|
+
return line.split('"')[1]
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
return "?"
|
|
43
|
+
|
|
44
|
+
C3_VERSION = _read_version()
|
|
45
|
+
|
|
46
|
+
# Tool handlers
|
|
47
|
+
from cli.tools._helpers import maybe_related_facts, validate_file_path
|
|
48
|
+
from cli.tools.agent import handle_agent
|
|
49
|
+
from cli.tools.compress import handle_compress
|
|
50
|
+
from cli.tools.delegate import handle_delegate
|
|
51
|
+
from cli.tools.edit import handle_edit
|
|
52
|
+
from cli.tools.filter import handle_filter
|
|
53
|
+
from cli.tools.memory import handle_memory
|
|
54
|
+
from cli.tools.read import handle_read
|
|
55
|
+
from cli.tools.search import handle_search
|
|
56
|
+
from cli.tools.session import handle_session
|
|
57
|
+
from cli.tools.shell import handle_shell
|
|
58
|
+
from cli.tools.status import handle_status
|
|
59
|
+
from cli.tools.validate import handle_validate
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get_project_path() -> str:
|
|
63
|
+
"""Parse --project from sys.argv (before FastMCP takes over)."""
|
|
64
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
65
|
+
parser.add_argument("--project", default=".")
|
|
66
|
+
args, _ = parser.parse_known_args()
|
|
67
|
+
return str(Path(args.project).resolve())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
PROJECT_PATH = _get_project_path()
|
|
71
|
+
_IDE_NAME = load_ide_config(PROJECT_PATH)
|
|
72
|
+
_IDE_PROFILE = get_profile(_IDE_NAME)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _build_instructions(ide_name: str) -> str:
|
|
76
|
+
"""Build compact MCP instructions. Optimized for minimal token overhead.
|
|
77
|
+
|
|
78
|
+
Structure: intent-keyed one-liners so Claude can route by goal, not
|
|
79
|
+
by tool-name memorization. Each line = one intent → one tool.
|
|
80
|
+
"""
|
|
81
|
+
return (
|
|
82
|
+
"C3 — local code intelligence. Route by goal:\n"
|
|
83
|
+
" FIND candidates → c3_search\n"
|
|
84
|
+
" MAP file shape → c3_compress(mode='map') then READ content → c3_read(symbols=…)\n"
|
|
85
|
+
" EDIT code → c3_edit (always; it logs to the ledger automatically)\n"
|
|
86
|
+
" VALIDATE after every edit → c3_validate\n"
|
|
87
|
+
" BLAST RADIUS before shared-symbol edits → c3_impact\n"
|
|
88
|
+
" DISTILL terminal/log output >10 lines → c3_filter\n"
|
|
89
|
+
" EXECUTE shell (tests/git/build) → c3_shell\n"
|
|
90
|
+
" RECALL cross-session knowledge → c3_memory(action='recall') (index+fetch for large stores)\n"
|
|
91
|
+
" SNAPSHOT before /clear → c3_session(action='snapshot')\n"
|
|
92
|
+
" HEALTH/budget checks → c3_status\n"
|
|
93
|
+
" OFFLOAD to another model → c3_delegate\n"
|
|
94
|
+
"PLAN MODE: all read-only actions above are safe."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@asynccontextmanager
|
|
99
|
+
async def lifespan(server):
|
|
100
|
+
"""Initialize all services, auto-start session, start file watcher."""
|
|
101
|
+
project = PROJECT_PATH
|
|
102
|
+
services = build_runtime(project, ide_name=_IDE_NAME)
|
|
103
|
+
transcript_index = TranscriptIndex(project)
|
|
104
|
+
services.transcript_index = transcript_index
|
|
105
|
+
snapshots = services.snapshots or ContextSnapshot(project)
|
|
106
|
+
services.snapshots = snapshots
|
|
107
|
+
|
|
108
|
+
if _IDE_PROFILE.supports_transcripts:
|
|
109
|
+
if not (Path(project) / ".c3" / "transcript_index" / "index.json").exists():
|
|
110
|
+
transcript_index.build_index()
|
|
111
|
+
else:
|
|
112
|
+
transcript_index._load_index()
|
|
113
|
+
transcript_index._load_manifest()
|
|
114
|
+
|
|
115
|
+
if not (Path(project) / ".c3" / "index" / "index.json").exists():
|
|
116
|
+
import threading
|
|
117
|
+
|
|
118
|
+
def _bg_build():
|
|
119
|
+
try:
|
|
120
|
+
services.indexer.build_index()
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
# After code index is built, build embedding index
|
|
124
|
+
if services.embedding_index and services.embedding_index.ready:
|
|
125
|
+
try:
|
|
126
|
+
services.embedding_index.build(services.indexer)
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
# Build doc index for Local RAG Pipeline
|
|
130
|
+
if services.doc_index:
|
|
131
|
+
try:
|
|
132
|
+
services.doc_index.build()
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
threading.Thread(target=_bg_build, daemon=True, name="c3-initial-index").start()
|
|
137
|
+
else:
|
|
138
|
+
services.indexer._load_index()
|
|
139
|
+
# Build/update embedding index in background
|
|
140
|
+
if services.embedding_index and services.embedding_index.ready:
|
|
141
|
+
import threading
|
|
142
|
+
|
|
143
|
+
def _bg_embed():
|
|
144
|
+
try:
|
|
145
|
+
services.embedding_index.build(services.indexer)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
threading.Thread(target=_bg_embed, daemon=True, name="c3-embed-index").start()
|
|
150
|
+
|
|
151
|
+
# Build/update doc index in background for Local RAG Pipeline
|
|
152
|
+
if services.doc_index:
|
|
153
|
+
import threading
|
|
154
|
+
|
|
155
|
+
def _bg_doc_index():
|
|
156
|
+
try:
|
|
157
|
+
services.doc_index.build()
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
threading.Thread(target=_bg_doc_index, daemon=True, name="c3-doc-index").start()
|
|
162
|
+
|
|
163
|
+
started_session = services.session_mgr.start_session("MCP server session", source_system=_IDE_NAME)
|
|
164
|
+
start_runtime(services)
|
|
165
|
+
|
|
166
|
+
convo_store = services.convo_store
|
|
167
|
+
services.convo_store = convo_store
|
|
168
|
+
if _IDE_PROFILE.supports_transcripts:
|
|
169
|
+
try:
|
|
170
|
+
convo_store.sync(source="claude")
|
|
171
|
+
if services.retrieval:
|
|
172
|
+
services.retrieval.mark_sessions_dirty()
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
import threading
|
|
177
|
+
_convo_sync_stop = threading.Event()
|
|
178
|
+
if _IDE_PROFILE.supports_transcripts:
|
|
179
|
+
def _bg_convo_sync():
|
|
180
|
+
while not _convo_sync_stop.wait(timeout=60):
|
|
181
|
+
try:
|
|
182
|
+
convo_store.sync(source="claude")
|
|
183
|
+
if services.retrieval:
|
|
184
|
+
services.retrieval.mark_sessions_dirty()
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
threading.Thread(target=_bg_convo_sync, daemon=True, name="c3-convo-sync").start()
|
|
188
|
+
|
|
189
|
+
# Auto-memory: background learning from tool calls.
|
|
190
|
+
auto_mem_cfg = (services.hybrid_config or {}).get("auto_memory", {})
|
|
191
|
+
services.auto_memory = AutoMemory(services.memory, services.session_mgr, auto_mem_cfg)
|
|
192
|
+
|
|
193
|
+
if services.session_mgr.current_session:
|
|
194
|
+
services.activity_log.log("session_start", {
|
|
195
|
+
"session_id": services.session_mgr.current_session["id"],
|
|
196
|
+
"source_system": started_session.get("source_system", ""),
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
# Auto-restore latest snapshot if recent (< 30 min).
|
|
200
|
+
# Deferred to background thread so first tool call isn't blocked.
|
|
201
|
+
# Skipped in benchmark mode to prevent snapshot budget from carrying over between tasks.
|
|
202
|
+
if not os.environ.get("C3_BENCHMARK_MODE"):
|
|
203
|
+
import threading as _restore_t
|
|
204
|
+
|
|
205
|
+
def _bg_auto_restore():
|
|
206
|
+
try:
|
|
207
|
+
latest = snapshots._load_snapshot("latest")
|
|
208
|
+
if "error" not in latest and "created" in latest:
|
|
209
|
+
created_dt = datetime.fromisoformat(latest["created"])
|
|
210
|
+
age_sec = (datetime.now(timezone.utc) - created_dt).total_seconds()
|
|
211
|
+
if age_sec < 1800:
|
|
212
|
+
res = snapshots.restore("latest", memory_store=services.memory, level=1)
|
|
213
|
+
if "error" not in res:
|
|
214
|
+
services.session_mgr.reset_budget(initial_tokens=res.get("tokens", 0))
|
|
215
|
+
services.notifications.add(
|
|
216
|
+
agent="c3",
|
|
217
|
+
severity="info",
|
|
218
|
+
title="Session Auto-Restored",
|
|
219
|
+
message=f"Restored latest context from {round(age_sec/60)}m ago: {res['briefing']}"
|
|
220
|
+
)
|
|
221
|
+
services.activity_log.log("auto_restore", {
|
|
222
|
+
"snapshot_id": res["snapshot_id"], "age_min": round(age_sec/60)})
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
_restore_t.Thread(target=_bg_auto_restore, daemon=True, name="c3-auto-restore").start()
|
|
227
|
+
|
|
228
|
+
# Pre-warm delegate health checks so first c3_agent call skips 3-4s preflight.
|
|
229
|
+
import threading as _t
|
|
230
|
+
|
|
231
|
+
def _bg_delegate_prewarm():
|
|
232
|
+
try:
|
|
233
|
+
from cli.tools.delegate import check_codex, check_gemini
|
|
234
|
+
check_gemini()
|
|
235
|
+
check_codex()
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
_t.Thread(target=_bg_delegate_prewarm, daemon=True, name="c3-delegate-prewarm").start()
|
|
240
|
+
|
|
241
|
+
# Background validation sweep: check recently-errored files and notify.
|
|
242
|
+
if services.validation_cache:
|
|
243
|
+
import threading as _t
|
|
244
|
+
|
|
245
|
+
def _bg_validation_sweep():
|
|
246
|
+
import time
|
|
247
|
+
time.sleep(8) # Let watcher populate cache from initial file events.
|
|
248
|
+
errors = services.validation_cache.get_errors()
|
|
249
|
+
if errors:
|
|
250
|
+
names = ", ".join(e["path"] for e in errors[:5])
|
|
251
|
+
more = f" (+{len(errors) - 5} more)" if len(errors) > 5 else ""
|
|
252
|
+
services.notifications.add(
|
|
253
|
+
agent="c3", severity="warning",
|
|
254
|
+
title="Syntax Errors Detected",
|
|
255
|
+
message=f"{len(errors)} file(s) have syntax errors: {names}{more}",
|
|
256
|
+
)
|
|
257
|
+
_t.Thread(target=_bg_validation_sweep, daemon=True, name="c3-validate-sweep").start()
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
yield services
|
|
261
|
+
finally:
|
|
262
|
+
_convo_sync_stop.set()
|
|
263
|
+
# Auto-memory: extract remaining learnings and generate session summary.
|
|
264
|
+
if hasattr(services, "auto_memory"):
|
|
265
|
+
try:
|
|
266
|
+
services.auto_memory.on_session_end()
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
# Memory consolidation: triage + prune at session end (lightweight).
|
|
270
|
+
if services.memory_consolidator:
|
|
271
|
+
try:
|
|
272
|
+
session = services.session_mgr.current_session
|
|
273
|
+
services.memory_consolidator.phase_triage(session)
|
|
274
|
+
services.memory_consolidator.phase_prune()
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
stop_runtime(services)
|
|
278
|
+
services.session_mgr._persist_budget()
|
|
279
|
+
services.session_mgr.save_session()
|
|
280
|
+
if _IDE_PROFILE.supports_transcripts:
|
|
281
|
+
try:
|
|
282
|
+
convo_store.sync(source="claude", force=True)
|
|
283
|
+
if services.retrieval:
|
|
284
|
+
services.retrieval.mark_sessions_dirty()
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
mcp = FastMCP(f"C3 v{C3_VERSION}", instructions=_build_instructions(_IDE_NAME), lifespan=lifespan)
|
|
290
|
+
|
|
291
|
+
# ─── Helper Functions ─────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
def _svc(ctx: Context) -> C3Runtime:
|
|
294
|
+
return ctx.request_context.lifespan_context
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
_last_tool_call_time: float = 0.0
|
|
298
|
+
_last_badge_count: int = 0 # delta-based: only show badge when count increases
|
|
299
|
+
_finalize_lock = threading.Lock()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _finalize_response(ctx: Context, tool_name: str, args: dict,
|
|
303
|
+
response: str, summary: str = "",
|
|
304
|
+
response_tokens: int = 0) -> str:
|
|
305
|
+
global _last_tool_call_time, _last_badge_count
|
|
306
|
+
|
|
307
|
+
deferred_snapshot = False
|
|
308
|
+
snap_pct = 0
|
|
309
|
+
svc = _svc(ctx)
|
|
310
|
+
|
|
311
|
+
# Minimal critical section: only the module-global timing state needs
|
|
312
|
+
# exclusive access. Disk I/O happens outside the lock so concurrent tool
|
|
313
|
+
# calls don't serialize on append-only JSONL writes.
|
|
314
|
+
gap_reset_seconds = 0
|
|
315
|
+
with _finalize_lock:
|
|
316
|
+
now = time.time()
|
|
317
|
+
if _last_tool_call_time > 0 and (now - _last_tool_call_time) > 30:
|
|
318
|
+
gap_reset_seconds = round(now - _last_tool_call_time)
|
|
319
|
+
_last_badge_count = 0 # surface any pending alerts after /clear
|
|
320
|
+
_last_tool_call_time = now
|
|
321
|
+
|
|
322
|
+
# Outside lock: append-only logs + budget track. These are independent
|
|
323
|
+
# across tool calls; holding the lock was serializing them needlessly.
|
|
324
|
+
if gap_reset_seconds:
|
|
325
|
+
svc.session_mgr.reset_budget()
|
|
326
|
+
svc.activity_log.log("budget_auto_reset", {"gap_seconds": gap_reset_seconds})
|
|
327
|
+
|
|
328
|
+
svc.session_mgr.log_tool_call(tool_name, args, summary)
|
|
329
|
+
svc.activity_log.log("tool_call", {"tool": tool_name, "args": args, "result_summary": summary})
|
|
330
|
+
svc.session_mgr.track_response(tool_name, response, response_tokens=response_tokens)
|
|
331
|
+
|
|
332
|
+
hybrid_cfg = svc.hybrid_config or {}
|
|
333
|
+
|
|
334
|
+
# Auto-snapshot check-then-set — short lock to ensure single-fire under
|
|
335
|
+
# concurrent tool calls.
|
|
336
|
+
with _finalize_lock:
|
|
337
|
+
snap = svc.session_mgr.get_budget_snapshot()
|
|
338
|
+
if "error" not in snap:
|
|
339
|
+
threshold = snap.get("threshold", 35000)
|
|
340
|
+
pct = round(snap["response_tokens"] / threshold * 100) if threshold > 0 else 0
|
|
341
|
+
if pct >= 80 and not snap.get("auto_snapshot_fired", False):
|
|
342
|
+
svc.session_mgr.mark_auto_snapshot_fired()
|
|
343
|
+
deferred_snapshot = True
|
|
344
|
+
snap_pct = pct
|
|
345
|
+
|
|
346
|
+
# --- Outside lock: auto-memory extraction (may do file I/O / Ollama) ---
|
|
347
|
+
if hasattr(svc, "auto_memory"):
|
|
348
|
+
try:
|
|
349
|
+
svc.auto_memory.on_tool_complete(tool_name, args, summary, response)
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
# --- Slow path OUTSIDE lock: auto-snapshot (fires once per session) ---
|
|
354
|
+
if deferred_snapshot:
|
|
355
|
+
try:
|
|
356
|
+
from cli.tools.session import handle_session
|
|
357
|
+
handle_session("snapshot", data="auto_budget_snapshot",
|
|
358
|
+
reasoning=f"Budget at {snap_pct}% — auto-snapshot before potential /clear",
|
|
359
|
+
description="", summary="", event_type="auto",
|
|
360
|
+
svc=svc, finalize=lambda *a, **kw: "")
|
|
361
|
+
response += (
|
|
362
|
+
f"\n\n[c3:auto_snapshot] Budget {snap_pct}%. Snapshot saved. "
|
|
363
|
+
f"Tell user to /clear, then restore."
|
|
364
|
+
)
|
|
365
|
+
svc.activity_log.log("auto_snapshot", {"budget_pct": snap_pct})
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
return response
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# ─── TOOL REGISTRATIONS (13 tools) ────────────────────────────────
|
|
373
|
+
# Each tool's first docstring line should state WHEN to reach for it —
|
|
374
|
+
# that's what Claude reads when selecting between tools.
|
|
375
|
+
|
|
376
|
+
@mcp.tool()
|
|
377
|
+
async def c3_search(query: str, action: str = "code", top_k: int = 3,
|
|
378
|
+
max_tokens: int = 1200, prefetch: bool = False,
|
|
379
|
+
ctx: Context = None) -> str:
|
|
380
|
+
"""FIND candidates. Use to discover which files/symbols are relevant (read-only, plan-mode safe).
|
|
381
|
+
action: 'code' (TF-IDF content), 'exact' (regex), 'files' (by name), 'transcript', 'semantic'.
|
|
382
|
+
prefetch: auto-compress top results. Next step: c3_compress/c3_read on hits."""
|
|
383
|
+
svc = _svc(ctx)
|
|
384
|
+
|
|
385
|
+
def finalize(name, args, resp, summ, **kw):
|
|
386
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
387
|
+
|
|
388
|
+
return await asyncio.to_thread(handle_search, query, action, top_k, max_tokens, svc,
|
|
389
|
+
finalize, maybe_related_facts, prefetch=prefetch)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@mcp.tool()
|
|
393
|
+
async def c3_session(action: str, data: str = "", reasoning: str = "",
|
|
394
|
+
description: str = "", summary: str = "",
|
|
395
|
+
event_type: str = "auto", ctx: Context = None) -> str:
|
|
396
|
+
"""Session management: start, save, log, plan, snapshot, restore, compact, convo_log (log/snapshot are safe in plan mode).
|
|
397
|
+
log: data + reasoning. snapshot: data=task, reasoning=next steps, summary=key files.
|
|
398
|
+
restore: data=snapshot_id. convo_log: data=text, event_type=role."""
|
|
399
|
+
svc = _svc(ctx)
|
|
400
|
+
|
|
401
|
+
def finalize(name, args, resp, summ, **kw):
|
|
402
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
403
|
+
|
|
404
|
+
return await asyncio.to_thread(handle_session, action, data, reasoning, description, summary,
|
|
405
|
+
event_type, svc, finalize)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@mcp.tool()
|
|
409
|
+
async def c3_memory(action: str, query: str = "", fact: str = "",
|
|
410
|
+
category: str = "", top_k: int = 3,
|
|
411
|
+
fact_id: str = "", ctx: Context = None) -> str:
|
|
412
|
+
"""Durable facts — cross-session knowledge. Read-only actions safe in plan mode.
|
|
413
|
+
Retrieve: recall (search), index (compact IDs+snippets, then fetch), fetch (full text by fact_id="id1,id2"), query (multi-source: facts+sessions+files).
|
|
414
|
+
Write: add (fact+category, empty category→'general'), update (fact_id+fact), delete (fact_id).
|
|
415
|
+
Browse: list (category='' shows all; 'foo' filters), export (markdown).
|
|
416
|
+
Audit: review (health), ground (verify against code), score (salience), graph (edges), trends, lifespan, consolidate, consolidate_deep."""
|
|
417
|
+
svc = _svc(ctx)
|
|
418
|
+
|
|
419
|
+
def finalize(name, args, resp, summ, **kw):
|
|
420
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
421
|
+
|
|
422
|
+
return await asyncio.to_thread(handle_memory, action, query, fact, category, top_k, svc, finalize,
|
|
423
|
+
fact_id=fact_id)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@mcp.tool()
|
|
427
|
+
async def c3_read(file_path: str, symbols: Any = None, lines: Any = None,
|
|
428
|
+
include_docstrings: bool = True, ctx: Context = None) -> str:
|
|
429
|
+
"""READ exact content. Use after c3_compress when you need real source (read-only, plan-mode safe).
|
|
430
|
+
file_path: single or comma-separated. symbols: function/class names. lines: int, [start,end], or list of ranges."""
|
|
431
|
+
path_err = validate_file_path(file_path)
|
|
432
|
+
if path_err:
|
|
433
|
+
return f"[c3_read:error] {path_err}"
|
|
434
|
+
svc = _svc(ctx)
|
|
435
|
+
|
|
436
|
+
def finalize(name, args, resp, summ, **kw):
|
|
437
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
438
|
+
|
|
439
|
+
return await asyncio.to_thread(handle_read, file_path, symbols, lines, include_docstrings, svc, finalize)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@mcp.tool()
|
|
443
|
+
async def c3_compress(file_path: str, mode: str = "smart", ctx: Context = None) -> str:
|
|
444
|
+
"""MAP file shape — classes/functions/imports at 40-70% tokens (read-only, plan-mode safe).
|
|
445
|
+
Use before c3_read to know which symbols to fetch. Modes: map, dense_map, smart, diff, bug_scan, ast.
|
|
446
|
+
file_path: single or comma-separated."""
|
|
447
|
+
path_err = validate_file_path(file_path)
|
|
448
|
+
if path_err:
|
|
449
|
+
return f"[c3_compress:error] {path_err}"
|
|
450
|
+
svc = _svc(ctx)
|
|
451
|
+
|
|
452
|
+
def finalize(name, args, resp, summ, **kw):
|
|
453
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
454
|
+
|
|
455
|
+
return await asyncio.to_thread(handle_compress, file_path, mode, svc, finalize, maybe_related_facts)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@mcp.tool()
|
|
459
|
+
async def c3_validate(file_path: str, ctx: Context = None) -> str:
|
|
460
|
+
"""VALIDATE after every edit — syntax+types if pyright/tsc installed (read-only, plan-mode safe).
|
|
461
|
+
py, json, yaml, js, ts, go, rs, html, css, etc. file_path: single or comma-separated."""
|
|
462
|
+
path_err = validate_file_path(file_path)
|
|
463
|
+
if path_err:
|
|
464
|
+
return f"[c3_validate:error] {path_err}"
|
|
465
|
+
svc = _svc(ctx)
|
|
466
|
+
|
|
467
|
+
def finalize(name, args, resp, summ, **kw):
|
|
468
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
469
|
+
|
|
470
|
+
return await handle_validate(file_path, svc, finalize)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@mcp.tool()
|
|
474
|
+
async def c3_filter(file_path: str = "", text: str = "", pattern: str = "",
|
|
475
|
+
max_lines: int = 50, depth: str = "smart",
|
|
476
|
+
use_llm: bool = True, ctx: Context = None) -> str:
|
|
477
|
+
"""DISTILL long output — use when terminal/log output exceeds ~10 lines (read-only, plan-mode safe).
|
|
478
|
+
text: inline output. file_path: log file. pattern: regex. depth: fast|smart|deep."""
|
|
479
|
+
if file_path:
|
|
480
|
+
path_err = validate_file_path(file_path)
|
|
481
|
+
if path_err:
|
|
482
|
+
return f"[c3_filter:error] {path_err}"
|
|
483
|
+
svc = _svc(ctx)
|
|
484
|
+
|
|
485
|
+
def finalize(name, args, resp, summ, **kw):
|
|
486
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
487
|
+
|
|
488
|
+
return await asyncio.to_thread(handle_filter, file_path, text, pattern, max_lines, depth, use_llm,
|
|
489
|
+
svc, finalize)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@mcp.tool()
|
|
493
|
+
async def c3_status(view: str = "budget", detailed: bool = False,
|
|
494
|
+
ctx: Context = None) -> str:
|
|
495
|
+
"""PROJECT health and budget — run at session start or when context feels stale (read-only, plan-mode safe).
|
|
496
|
+
views: budget (tokens/ratio), health (memory/index/notifications), notifications (actionable only), sessions, ghost_files."""
|
|
497
|
+
svc = _svc(ctx)
|
|
498
|
+
|
|
499
|
+
def finalize(name, args, resp, summ, **kw):
|
|
500
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
501
|
+
|
|
502
|
+
return await asyncio.to_thread(handle_status, view, detailed, svc, finalize)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@mcp.tool()
|
|
506
|
+
async def c3_delegate(task: str, task_type: str = "ask", context: str = "",
|
|
507
|
+
file_path: str = "", backend: str = "ollama",
|
|
508
|
+
ctx: Context = None) -> str:
|
|
509
|
+
"""OFFLOAD to another model — use when the subtask is local-model-sized or needs a different perspective.
|
|
510
|
+
backend: ollama|codex|gemini|claude|auto. task_type: auto, summarize, explain, review, ask, test, diagnose, available, codex_check, gemini_check, codex_resume."""
|
|
511
|
+
svc = _svc(ctx)
|
|
512
|
+
|
|
513
|
+
def finalize(name, args, resp, summ, **kw):
|
|
514
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
515
|
+
|
|
516
|
+
# Wire progress notifications (same direct-stdout approach as c3_agent)
|
|
517
|
+
import json as _json
|
|
518
|
+
import threading as _threading
|
|
519
|
+
_stdout_lock = _threading.Lock()
|
|
520
|
+
|
|
521
|
+
def _progress_cb(message: str):
|
|
522
|
+
try:
|
|
523
|
+
line = _json.dumps({
|
|
524
|
+
"jsonrpc": "2.0",
|
|
525
|
+
"method": "notifications/message",
|
|
526
|
+
"params": {"level": "info", "data": message},
|
|
527
|
+
}, separators=(",", ":")) + "\n"
|
|
528
|
+
with _stdout_lock:
|
|
529
|
+
sys.stdout.buffer.write(line.encode("utf-8"))
|
|
530
|
+
sys.stdout.buffer.flush()
|
|
531
|
+
except Exception:
|
|
532
|
+
pass
|
|
533
|
+
svc._agent_progress_cb = _progress_cb
|
|
534
|
+
try:
|
|
535
|
+
return await asyncio.to_thread(handle_delegate, task, task_type, context, file_path, svc, finalize, backend)
|
|
536
|
+
finally:
|
|
537
|
+
svc._agent_progress_cb = None
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@mcp.tool()
|
|
541
|
+
async def c3_agent(workflow: str, scope: str = "", context: str = "",
|
|
542
|
+
ctx: Context = None) -> str:
|
|
543
|
+
"""ORCHESTRATE a multi-step pipeline. Use for compound investigations that'd be 5+ tool calls otherwise.
|
|
544
|
+
workflow: available, review_changes, prepare_context, investigate, preflight, validate_compress.
|
|
545
|
+
scope: file paths, query, or git range. context: extra hints."""
|
|
546
|
+
svc = _svc(ctx)
|
|
547
|
+
loop = asyncio.get_running_loop()
|
|
548
|
+
|
|
549
|
+
def finalize(name, args, resp, summ, **kw):
|
|
550
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
551
|
+
|
|
552
|
+
# Wire live progress notifications: _log_progress calls this from the worker thread.
|
|
553
|
+
# We write raw JSON-RPC notifications directly to stdout instead of going through
|
|
554
|
+
# the async transport (session.send_log_message / ctx.info). The async approach fails
|
|
555
|
+
# because asyncio.run_coroutine_threadsafe coroutines never complete in time — the
|
|
556
|
+
# event loop is technically free (awaiting to_thread) but the transport write stalls.
|
|
557
|
+
# Direct stdout writes are safe here because the event loop is idle during to_thread
|
|
558
|
+
# (no concurrent transport writes until the tool response is sent after to_thread returns).
|
|
559
|
+
import json as _json
|
|
560
|
+
import threading as _threading
|
|
561
|
+
_stdout_lock = _threading.Lock()
|
|
562
|
+
|
|
563
|
+
def _progress_cb(message: str):
|
|
564
|
+
try:
|
|
565
|
+
line = _json.dumps({
|
|
566
|
+
"jsonrpc": "2.0",
|
|
567
|
+
"method": "notifications/message",
|
|
568
|
+
"params": {"level": "info", "data": message},
|
|
569
|
+
}, separators=(",", ":")) + "\n"
|
|
570
|
+
with _stdout_lock:
|
|
571
|
+
sys.stdout.buffer.write(line.encode("utf-8"))
|
|
572
|
+
sys.stdout.buffer.flush()
|
|
573
|
+
except Exception:
|
|
574
|
+
pass
|
|
575
|
+
svc._agent_progress_cb = _progress_cb
|
|
576
|
+
try:
|
|
577
|
+
return await asyncio.to_thread(handle_agent, workflow, scope, context, svc, finalize)
|
|
578
|
+
finally:
|
|
579
|
+
svc._agent_progress_cb = None
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
@mcp.tool()
|
|
583
|
+
async def c3_edit(file_path: str, old_string: str = "", new_string: str = "",
|
|
584
|
+
summary: str = "", tags: str = "", replace_all: bool = False,
|
|
585
|
+
edits: str = "",
|
|
586
|
+
ctx: Context = None) -> str:
|
|
587
|
+
"""EDIT — read+patch+write+log in one step. Primary code-change tool; always prefer over native Edit.
|
|
588
|
+
old_string: text to replace. new_string: replacement. summary: ledger description.
|
|
589
|
+
edits: JSON list of {old_string, new_string, summary?} for multi-hunk batch on one file.
|
|
590
|
+
Parallel across files. Create new file: non-existent file_path + old_string='' + new_string=<content>."""
|
|
591
|
+
path_err = validate_file_path(file_path)
|
|
592
|
+
if path_err:
|
|
593
|
+
return f"[c3_edit:error] {path_err}"
|
|
594
|
+
svc = _svc(ctx)
|
|
595
|
+
|
|
596
|
+
def finalize(name, args, resp, summ, **kw):
|
|
597
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
598
|
+
|
|
599
|
+
return await asyncio.to_thread(handle_edit, file_path, old_string, new_string,
|
|
600
|
+
summary, tags, replace_all, svc, finalize, edits)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@mcp.tool()
|
|
604
|
+
async def c3_edits(action: str, file: str = "", change_type: str = "modified",
|
|
605
|
+
summary: str = "", lines_changed: str = "", tags: str = "",
|
|
606
|
+
limit: int = 50, since: str = "", edit_id: str = "",
|
|
607
|
+
tag: str = "", ctx: Context = None) -> str:
|
|
608
|
+
"""EDIT HISTORY — inspect the ledger. Different from c3_edit (which writes); this one reads.
|
|
609
|
+
actions: log (append entry), history (recent edits), versions (per-file), stats, tag (mark edit_id)."""
|
|
610
|
+
svc = _svc(ctx)
|
|
611
|
+
|
|
612
|
+
def finalize(name, args, resp, summ, **kw):
|
|
613
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
614
|
+
|
|
615
|
+
from cli.tools.edits import handle_edits
|
|
616
|
+
return await asyncio.to_thread(handle_edits, action, file, change_type, summary,
|
|
617
|
+
lines_changed, tags, limit, since, edit_id, tag,
|
|
618
|
+
svc, finalize)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
@mcp.tool()
|
|
622
|
+
async def c3_impact(target: str, file_path: str = "", mode: str = "symbol",
|
|
623
|
+
ctx: Context = None) -> str:
|
|
624
|
+
"""BLAST RADIUS before editing a shared symbol — all call sites, imports, references.
|
|
625
|
+
target: symbol/function/class name. file_path: source file to exclude. mode: symbol | unstaged (affected files with uncommitted changes)."""
|
|
626
|
+
svc = _svc(ctx)
|
|
627
|
+
|
|
628
|
+
def finalize(name, args, resp, summ, **kw):
|
|
629
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
630
|
+
|
|
631
|
+
from cli.tools.impact import handle_impact
|
|
632
|
+
return await asyncio.to_thread(handle_impact, target, file_path, mode, svc, finalize)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@mcp.tool()
|
|
636
|
+
async def c3_shell(cmd: str, cwd: str = "", timeout: int = 60,
|
|
637
|
+
filter_output: bool = True, log: bool = True,
|
|
638
|
+
ctx: Context = None) -> str:
|
|
639
|
+
"""EXECUTE shell command — structured returns, auto-filter, ledger-aware.
|
|
640
|
+
Use for tests, git, build, scripts. Returns exit_code/stdout/stderr/duration_ms.
|
|
641
|
+
Auto-filters stdout >30 lines; auto-logs git mutations to the edit ledger.
|
|
642
|
+
Blocks: rm -rf / or ~, fork bombs. Soft-warns on --force, --no-verify, reset --hard.
|
|
643
|
+
Native Bash remains the fallback for interactive/TTY commands."""
|
|
644
|
+
svc = _svc(ctx)
|
|
645
|
+
|
|
646
|
+
def finalize(name, args, resp, summ, **kw):
|
|
647
|
+
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
648
|
+
|
|
649
|
+
return await handle_shell(cmd, cwd, timeout, filter_output, log, svc, finalize)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def main() -> None:
|
|
653
|
+
"""Entry-point for the ``c3-mcp`` console script."""
|
|
654
|
+
from services import error_reporting
|
|
655
|
+
error_reporting.init(component="c3-mcp", version=C3_VERSION)
|
|
656
|
+
mcp.run(transport="stdio", show_banner=False, log_level="ERROR")
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
if __name__ == "__main__":
|
|
660
|
+
main()
|