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
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()