code-context-control 2.33.0__py3-none-any.whl → 2.35.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/c3.py +1 -1
- cli/hub_server.py +6 -107
- cli/mcp_server.py +4 -3
- cli/server.py +14 -108
- cli/tools/edits.py +7 -2
- cli/tools/shell.py +59 -3
- {code_context_control-2.33.0.dist-info → code_context_control-2.35.0.dist-info}/METADATA +3 -3
- {code_context_control-2.33.0.dist-info → code_context_control-2.35.0.dist-info}/RECORD +22 -20
- core/mcp_toml.py +128 -0
- core/web_security.py +16 -0
- oracle/mcp_oracle.py +43 -5
- oracle/oracle_server.py +1 -0
- services/agents.py +145 -0
- services/context_snapshot.py +37 -0
- services/edit_ledger.py +24 -26
- services/git_context.py +243 -0
- services/session_manager.py +17 -0
- services/version_tracker.py +8 -16
- {code_context_control-2.33.0.dist-info → code_context_control-2.35.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.33.0.dist-info → code_context_control-2.35.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.33.0.dist-info → code_context_control-2.35.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.33.0.dist-info → code_context_control-2.35.0.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
cli/hub_server.py
CHANGED
|
@@ -180,46 +180,12 @@ def _project_mcp_config_path(project_root: Path, profile) -> Path:
|
|
|
180
180
|
return (Path.home() / profile.config_path) if profile.config_path_global else (project_root / profile.config_path)
|
|
181
181
|
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if not line:
|
|
190
|
-
continue
|
|
191
|
-
|
|
192
|
-
if line.startswith("[") and line.endswith("]"):
|
|
193
|
-
section = line[1:-1].strip()
|
|
194
|
-
if section.startswith("mcp_servers."):
|
|
195
|
-
current_server = section.split(".", 1)[1]
|
|
196
|
-
servers.setdefault(current_server, {})
|
|
197
|
-
else:
|
|
198
|
-
current_server = None
|
|
199
|
-
continue
|
|
200
|
-
|
|
201
|
-
if not current_server or "=" not in line:
|
|
202
|
-
continue
|
|
203
|
-
|
|
204
|
-
key, value = line.split("=", 1)
|
|
205
|
-
key = key.strip().strip('"')
|
|
206
|
-
value = value.strip()
|
|
207
|
-
|
|
208
|
-
if key == "args":
|
|
209
|
-
servers[current_server]["args"] = re.findall(r"[\"']([^\"']*)[\"']", value)
|
|
210
|
-
elif key in ("command", "type"):
|
|
211
|
-
match = re.match(r"^[\"'](.*)[\"']$", value)
|
|
212
|
-
servers[current_server][key] = match.group(1) if match else value
|
|
213
|
-
elif key == "enabled":
|
|
214
|
-
low = value.lower()
|
|
215
|
-
if low.startswith("true"):
|
|
216
|
-
servers[current_server]["enabled"] = True
|
|
217
|
-
elif low.startswith("false"):
|
|
218
|
-
servers[current_server]["enabled"] = False
|
|
219
|
-
else:
|
|
220
|
-
servers[current_server][key] = value
|
|
221
|
-
|
|
222
|
-
return servers
|
|
183
|
+
from core.mcp_toml import (
|
|
184
|
+
parse_toml_mcp_servers as _parse_toml_mcp_servers,
|
|
185
|
+
)
|
|
186
|
+
from core.mcp_toml import (
|
|
187
|
+
upsert_toml_section as _upsert_toml_section,
|
|
188
|
+
)
|
|
223
189
|
|
|
224
190
|
|
|
225
191
|
def _read_project_mcp_servers_for_profile(profile, mcp_file: Path) -> tuple[dict, dict]:
|
|
@@ -238,73 +204,6 @@ def _read_project_mcp_servers_for_profile(profile, mcp_file: Path) -> tuple[dict
|
|
|
238
204
|
return servers, raw_config
|
|
239
205
|
|
|
240
206
|
|
|
241
|
-
def _toml_escape_str(value: str) -> str:
|
|
242
|
-
return value.replace("\\", "/")
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def _upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
|
|
246
|
-
content = toml_path.read_text(encoding="utf-8") if toml_path.exists() else ""
|
|
247
|
-
header = f"[{section}]"
|
|
248
|
-
|
|
249
|
-
lines = content.splitlines()
|
|
250
|
-
new_lines = []
|
|
251
|
-
skip = False
|
|
252
|
-
for line in lines:
|
|
253
|
-
stripped = line.strip()
|
|
254
|
-
if stripped == header:
|
|
255
|
-
skip = True
|
|
256
|
-
continue
|
|
257
|
-
if skip and stripped.startswith("["):
|
|
258
|
-
skip = False
|
|
259
|
-
if not skip:
|
|
260
|
-
new_lines.append(line)
|
|
261
|
-
|
|
262
|
-
content = "\n".join(new_lines).rstrip()
|
|
263
|
-
section_lines = [f"\n\n{header}"]
|
|
264
|
-
for key, value in entries.items():
|
|
265
|
-
if isinstance(value, list):
|
|
266
|
-
items = ", ".join(f'"{_toml_escape_str(str(item))}"' for item in value)
|
|
267
|
-
section_lines.append(f'{key} = [{items}]')
|
|
268
|
-
elif isinstance(value, bool):
|
|
269
|
-
section_lines.append(f'{key} = {"true" if value else "false"}')
|
|
270
|
-
else:
|
|
271
|
-
section_lines.append(f'{key} = "{_toml_escape_str(str(value))}"')
|
|
272
|
-
section_lines.append("")
|
|
273
|
-
|
|
274
|
-
toml_path.parent.mkdir(parents=True, exist_ok=True)
|
|
275
|
-
toml_path.write_text(content + "\n".join(section_lines), encoding="utf-8")
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def _remove_toml_section(toml_path: Path, section: str) -> bool:
|
|
279
|
-
if not toml_path.exists():
|
|
280
|
-
return False
|
|
281
|
-
content = toml_path.read_text(encoding="utf-8")
|
|
282
|
-
header = f"[{section}]"
|
|
283
|
-
|
|
284
|
-
lines = content.splitlines()
|
|
285
|
-
new_lines = []
|
|
286
|
-
skip = False
|
|
287
|
-
removed = False
|
|
288
|
-
for line in lines:
|
|
289
|
-
stripped = line.strip()
|
|
290
|
-
if stripped == header:
|
|
291
|
-
skip = True
|
|
292
|
-
removed = True
|
|
293
|
-
continue
|
|
294
|
-
if skip and stripped.startswith("["):
|
|
295
|
-
skip = False
|
|
296
|
-
if not skip:
|
|
297
|
-
new_lines.append(line)
|
|
298
|
-
|
|
299
|
-
if removed:
|
|
300
|
-
remaining = "\n".join(new_lines).rstrip()
|
|
301
|
-
if remaining:
|
|
302
|
-
toml_path.write_text(remaining + "\n", encoding="utf-8")
|
|
303
|
-
else:
|
|
304
|
-
toml_path.unlink()
|
|
305
|
-
return removed
|
|
306
|
-
|
|
307
|
-
|
|
308
207
|
def _build_mcp_cli_capabilities() -> dict:
|
|
309
208
|
return {
|
|
310
209
|
"commands": [
|
cli/mcp_server.py
CHANGED
|
@@ -604,9 +604,10 @@ async def c3_edit(file_path: str, old_string: str = "", new_string: str = "",
|
|
|
604
604
|
async def c3_edits(action: str, file: str = "", change_type: str = "modified",
|
|
605
605
|
summary: str = "", lines_changed: str = "", tags: str = "",
|
|
606
606
|
limit: int = 50, since: str = "", edit_id: str = "",
|
|
607
|
-
tag: str = "", ctx: Context = None) -> str:
|
|
607
|
+
tag: str = "", branch: str = "", ctx: Context = None) -> str:
|
|
608
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).
|
|
609
|
+
actions: log (append entry), history (recent edits), versions (per-file), stats, tag (mark edit_id).
|
|
610
|
+
branch: filter history to edits stamped with a given git branch."""
|
|
610
611
|
svc = _svc(ctx)
|
|
611
612
|
|
|
612
613
|
def finalize(name, args, resp, summ, **kw):
|
|
@@ -615,7 +616,7 @@ async def c3_edits(action: str, file: str = "", change_type: str = "modified",
|
|
|
615
616
|
from cli.tools.edits import handle_edits
|
|
616
617
|
return await asyncio.to_thread(handle_edits, action, file, change_type, summary,
|
|
617
618
|
lines_changed, tags, limit, since, edit_id, tag,
|
|
618
|
-
svc, finalize)
|
|
619
|
+
svc, finalize, branch)
|
|
619
620
|
|
|
620
621
|
|
|
621
622
|
@mcp.tool()
|
cli/server.py
CHANGED
|
@@ -10,7 +10,6 @@ import csv
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
-
import re
|
|
14
13
|
import signal
|
|
15
14
|
import subprocess
|
|
16
15
|
import sys
|
|
@@ -174,6 +173,9 @@ atexit.register(_cleanup_runtime)
|
|
|
174
173
|
from core.web_security import (
|
|
175
174
|
allowed_hostnames as _allowed_hostnames,
|
|
176
175
|
)
|
|
176
|
+
from core.web_security import (
|
|
177
|
+
guard_summary as _guard_summary,
|
|
178
|
+
)
|
|
177
179
|
from core.web_security import (
|
|
178
180
|
install_guard as _install_web_guard,
|
|
179
181
|
)
|
|
@@ -376,7 +378,8 @@ def api_health():
|
|
|
376
378
|
except Exception:
|
|
377
379
|
pass
|
|
378
380
|
|
|
379
|
-
return jsonify({"service": "c3-ui", "sources": sources, "session": session_info
|
|
381
|
+
return jsonify({"service": "c3-ui", "sources": sources, "session": session_info,
|
|
382
|
+
"web_guard": _guard_summary()})
|
|
380
383
|
|
|
381
384
|
|
|
382
385
|
# ─── API: Session Registry ───────────────────────────────
|
|
@@ -2421,47 +2424,15 @@ def api_proxy_tools():
|
|
|
2421
2424
|
|
|
2422
2425
|
|
|
2423
2426
|
# ─── API: MCP Status ─────────────────────────────────────
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
if line.startswith("[") and line.endswith("]"):
|
|
2435
|
-
section = line[1:-1].strip()
|
|
2436
|
-
if section.startswith("mcp_servers."):
|
|
2437
|
-
current_server = section.split(".", 1)[1]
|
|
2438
|
-
servers.setdefault(current_server, {})
|
|
2439
|
-
else:
|
|
2440
|
-
current_server = None
|
|
2441
|
-
continue
|
|
2442
|
-
|
|
2443
|
-
if not current_server or "=" not in line:
|
|
2444
|
-
continue
|
|
2445
|
-
|
|
2446
|
-
key, value = line.split("=", 1)
|
|
2447
|
-
key = key.strip()
|
|
2448
|
-
value = value.strip()
|
|
2449
|
-
|
|
2450
|
-
if key == "args":
|
|
2451
|
-
servers[current_server]["args"] = re.findall(r"[\"']([^\"']*)[\"']", value)
|
|
2452
|
-
elif key in ("command", "type"):
|
|
2453
|
-
match = re.match(r"^[\"'](.*)[\"']$", value)
|
|
2454
|
-
servers[current_server][key] = match.group(1) if match else value
|
|
2455
|
-
elif key == "enabled":
|
|
2456
|
-
low = value.lower()
|
|
2457
|
-
if low.startswith("true"):
|
|
2458
|
-
servers[current_server]["enabled"] = True
|
|
2459
|
-
elif low.startswith("false"):
|
|
2460
|
-
servers[current_server]["enabled"] = False
|
|
2461
|
-
else:
|
|
2462
|
-
servers[current_server][key] = value
|
|
2463
|
-
|
|
2464
|
-
return servers
|
|
2427
|
+
from core.mcp_toml import (
|
|
2428
|
+
parse_toml_mcp_servers as _parse_toml_mcp_servers,
|
|
2429
|
+
)
|
|
2430
|
+
from core.mcp_toml import (
|
|
2431
|
+
remove_toml_section as _remove_toml_section,
|
|
2432
|
+
)
|
|
2433
|
+
from core.mcp_toml import (
|
|
2434
|
+
upsert_toml_section as _upsert_toml_section,
|
|
2435
|
+
)
|
|
2465
2436
|
|
|
2466
2437
|
|
|
2467
2438
|
def _find_server_script(servers: dict) -> bool:
|
|
@@ -2474,71 +2445,6 @@ def _find_server_script(servers: dict) -> bool:
|
|
|
2474
2445
|
return False
|
|
2475
2446
|
|
|
2476
2447
|
|
|
2477
|
-
def _toml_escape_str(value: str) -> str:
|
|
2478
|
-
return value.replace("\\", "/")
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
def _upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
|
|
2482
|
-
"""Add or replace a dotted TOML section in-place."""
|
|
2483
|
-
content = toml_path.read_text(encoding="utf-8") if toml_path.exists() else ""
|
|
2484
|
-
header = f"[{section}]"
|
|
2485
|
-
|
|
2486
|
-
lines = content.splitlines()
|
|
2487
|
-
new_lines = []
|
|
2488
|
-
skip = False
|
|
2489
|
-
for line in lines:
|
|
2490
|
-
stripped = line.strip()
|
|
2491
|
-
if stripped == header:
|
|
2492
|
-
skip = True
|
|
2493
|
-
continue
|
|
2494
|
-
if skip and stripped.startswith("["):
|
|
2495
|
-
skip = False
|
|
2496
|
-
if not skip:
|
|
2497
|
-
new_lines.append(line)
|
|
2498
|
-
|
|
2499
|
-
content = "\n".join(new_lines).rstrip()
|
|
2500
|
-
section_lines = [f"\n\n{header}"]
|
|
2501
|
-
for k, v in entries.items():
|
|
2502
|
-
if isinstance(v, list):
|
|
2503
|
-
items = ", ".join(f'"{_toml_escape_str(str(x))}"' for x in v)
|
|
2504
|
-
section_lines.append(f'{k} = [{items}]')
|
|
2505
|
-
elif isinstance(v, bool):
|
|
2506
|
-
section_lines.append(f'{k} = {"true" if v else "false"}')
|
|
2507
|
-
else:
|
|
2508
|
-
section_lines.append(f'{k} = "{_toml_escape_str(str(v))}"')
|
|
2509
|
-
section_lines.append("")
|
|
2510
|
-
|
|
2511
|
-
toml_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2512
|
-
toml_path.write_text(content + "\n".join(section_lines), encoding="utf-8")
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
def _remove_toml_section(toml_path: Path, section: str) -> bool:
|
|
2516
|
-
"""Remove a dotted TOML section. Returns True if removed."""
|
|
2517
|
-
if not toml_path.exists():
|
|
2518
|
-
return False
|
|
2519
|
-
content = toml_path.read_text(encoding="utf-8")
|
|
2520
|
-
header = f"[{section}]"
|
|
2521
|
-
|
|
2522
|
-
lines = content.splitlines()
|
|
2523
|
-
new_lines = []
|
|
2524
|
-
skip = False
|
|
2525
|
-
removed = False
|
|
2526
|
-
for line in lines:
|
|
2527
|
-
stripped = line.strip()
|
|
2528
|
-
if stripped == header:
|
|
2529
|
-
skip = True
|
|
2530
|
-
removed = True
|
|
2531
|
-
continue
|
|
2532
|
-
if skip and stripped.startswith("["):
|
|
2533
|
-
skip = False
|
|
2534
|
-
if not skip:
|
|
2535
|
-
new_lines.append(line)
|
|
2536
|
-
|
|
2537
|
-
if removed:
|
|
2538
|
-
toml_path.write_text("\n".join(new_lines).rstrip() + "\n", encoding="utf-8")
|
|
2539
|
-
return removed
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
2448
|
def _resolve_mcp_profile(ide_name: str | None):
|
|
2543
2449
|
requested = (ide_name or "").strip().lower()
|
|
2544
2450
|
if requested and requested != "auto":
|
cli/tools/edits.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
def handle_edits(action: str, file: str, change_type: str, summary: str,
|
|
5
5
|
lines_changed: str, tags: str, limit: int, since: str,
|
|
6
|
-
edit_id: str, tag: str, svc, finalize) -> str:
|
|
6
|
+
edit_id: str, tag: str, svc, finalize, branch: str = "") -> str:
|
|
7
7
|
"""Route c3_edits actions."""
|
|
8
8
|
ledger = svc.edit_ledger
|
|
9
9
|
if ledger is None:
|
|
@@ -64,12 +64,17 @@ def handle_edits(action: str, file: str, change_type: str, summary: str,
|
|
|
64
64
|
file=file or None,
|
|
65
65
|
limit=limit or 50,
|
|
66
66
|
since=since or None,
|
|
67
|
+
branch=branch or None,
|
|
67
68
|
)
|
|
68
69
|
if not entries:
|
|
69
70
|
return finalize("c3_edits", {"action": "history"}, "No edits found", "0 edits")
|
|
70
|
-
|
|
71
|
+
scope = (f" for {file}" if file else "") + (f" on {branch}" if branch else "")
|
|
72
|
+
lines = [f"[edits:history] {len(entries)} entries" + scope]
|
|
71
73
|
for e in entries:
|
|
72
74
|
ln = f" {e['timestamp'][:19]} | {e['file']} {e['version']} | {e['change_type']} | {e['summary']}"
|
|
75
|
+
br = (e.get("git") or {}).get("branch")
|
|
76
|
+
if br:
|
|
77
|
+
ln += f" @{br}"
|
|
73
78
|
if e.get("tags"):
|
|
74
79
|
ln += f" [{','.join(e['tags'])}]"
|
|
75
80
|
lines.append(ln)
|
cli/tools/shell.py
CHANGED
|
@@ -60,7 +60,13 @@ _FILTER_THRESHOLD_LINES = 30
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def _popen_kwargs() -> dict:
|
|
63
|
-
|
|
63
|
+
# Force UTF-8 in child processes so Unicode output (→, box-drawing, emoji)
|
|
64
|
+
# doesn't crash on Windows' legacy cp1252 console encoding. setdefault so an
|
|
65
|
+
# intentional caller-set encoding still wins.
|
|
66
|
+
env = dict(os.environ)
|
|
67
|
+
env.setdefault("PYTHONUTF8", "1")
|
|
68
|
+
env.setdefault("PYTHONIOENCODING", "utf-8")
|
|
69
|
+
kw: dict = {"stdin": subprocess.DEVNULL, "env": env}
|
|
64
70
|
if sys.platform == "win32":
|
|
65
71
|
kw["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
66
72
|
return kw
|
|
@@ -86,7 +92,7 @@ def _run_sync(cmd: str, cwd: str, timeout: int) -> dict:
|
|
|
86
92
|
proc = subprocess.Popen(
|
|
87
93
|
cmd, shell=True, cwd=cwd,
|
|
88
94
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
89
|
-
text=True, errors="replace",
|
|
95
|
+
text=True, encoding="utf-8", errors="replace",
|
|
90
96
|
**_popen_kwargs(),
|
|
91
97
|
)
|
|
92
98
|
timed_out = False
|
|
@@ -133,6 +139,45 @@ def _maybe_refresh_ledger(cmd: str, result: dict, svc) -> list[str]:
|
|
|
133
139
|
return []
|
|
134
140
|
|
|
135
141
|
|
|
142
|
+
# git diagnostics whose output the caller almost always needs verbatim — never
|
|
143
|
+
# auto-filter these, even past the line threshold.
|
|
144
|
+
_GIT_DIAGNOSTIC = re.compile(
|
|
145
|
+
r"^\s*git\s+(status|diff|log|show|branch|stash\s+list)\b", re.IGNORECASE
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _list_root_files(root: Path) -> set[str]:
|
|
150
|
+
try:
|
|
151
|
+
return {e.name for e in root.iterdir() if e.is_file()}
|
|
152
|
+
except OSError:
|
|
153
|
+
return set()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _sweep_new_ghost_files(root: Path, before: set[str]) -> list[str]:
|
|
157
|
+
"""Delete 0-byte 'ghost' files (shell-redirect / metacharacter artifacts —
|
|
158
|
+
e.g. a `>Lnnn` marker or `2>$null` leaking a filename) that appeared in
|
|
159
|
+
*root* during this command. Only files absent from *before* are removed, so
|
|
160
|
+
pre-existing files are never touched. Detection is reused from
|
|
161
|
+
hook_ghost_files so the rules live in one place; this makes c3_shell
|
|
162
|
+
self-clean regardless of whether the external PostToolUse ghost hook is
|
|
163
|
+
wired for this tool."""
|
|
164
|
+
try:
|
|
165
|
+
from cli.hook_ghost_files import scan_ghost_files
|
|
166
|
+
except Exception:
|
|
167
|
+
return []
|
|
168
|
+
swept: list[str] = []
|
|
169
|
+
for g in scan_ghost_files(root):
|
|
170
|
+
name = g.get("name", "")
|
|
171
|
+
if not name or name in before:
|
|
172
|
+
continue
|
|
173
|
+
try:
|
|
174
|
+
Path(g["path"]).unlink()
|
|
175
|
+
swept.append(name)
|
|
176
|
+
except OSError:
|
|
177
|
+
pass
|
|
178
|
+
return swept
|
|
179
|
+
|
|
180
|
+
|
|
136
181
|
async def handle_shell(cmd: str, cwd: str, timeout: int, filter_output: bool,
|
|
137
182
|
log: bool, svc, finalize) -> str:
|
|
138
183
|
if not cmd or not cmd.strip():
|
|
@@ -147,11 +192,17 @@ async def handle_shell(cmd: str, cwd: str, timeout: int, filter_output: bool,
|
|
|
147
192
|
work_cwd = cwd or svc.project_path
|
|
148
193
|
work_cwd = str(Path(work_cwd).resolve())
|
|
149
194
|
|
|
195
|
+
ghost_root = Path(work_cwd)
|
|
196
|
+
_ghosts_before = _list_root_files(ghost_root)
|
|
197
|
+
|
|
150
198
|
result = await asyncio.to_thread(_run_sync, cmd, work_cwd, timeout)
|
|
151
199
|
|
|
200
|
+
swept_ghosts = _sweep_new_ghost_files(ghost_root, _ghosts_before)
|
|
201
|
+
|
|
152
202
|
raw_stdout = result["stdout"]
|
|
153
203
|
filtered_note = ""
|
|
154
|
-
if filter_output and raw_stdout.count("\n") > _FILTER_THRESHOLD_LINES
|
|
204
|
+
if (filter_output and raw_stdout.count("\n") > _FILTER_THRESHOLD_LINES
|
|
205
|
+
and not _GIT_DIAGNOSTIC.search(cmd)):
|
|
155
206
|
try:
|
|
156
207
|
filtered = await asyncio.to_thread(
|
|
157
208
|
handle_filter,
|
|
@@ -201,6 +252,11 @@ async def handle_shell(cmd: str, cwd: str, timeout: int, filter_output: bool,
|
|
|
201
252
|
body += f"--- stderr ---\n{result['stderr'].rstrip()}\n"
|
|
202
253
|
if touched_files:
|
|
203
254
|
body += f"--- ledger ---\nlogged {len(touched_files)} file(s)\n"
|
|
255
|
+
if swept_ghosts:
|
|
256
|
+
body += (
|
|
257
|
+
f"--- ghost-sweep ---\nremoved {len(swept_ghosts)} stray 0-byte "
|
|
258
|
+
f"file(s): {', '.join(swept_ghosts)}\n"
|
|
259
|
+
)
|
|
204
260
|
|
|
205
261
|
summary = f"shell {status} in {result['duration_ms']}ms"
|
|
206
262
|
resp_tokens = count_tokens(body) if body else 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.35.0
|
|
4
4
|
Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -245,7 +245,7 @@ C3 exposes 16 tools as a native MCP server. Your IDE calls them directly:
|
|
|
245
245
|
| `c3_impact` | Blast-radius analysis before edits to shared symbols |
|
|
246
246
|
| `c3_delegate` | Offload heavy work to local Ollama / Codex / Gemini / etc. |
|
|
247
247
|
| `c3_agent` | Multi-step agentic workflows (review, investigate, refactor) |
|
|
248
|
-
| `c3_edits` | Edit-ledger queries + version diffs + restore points |
|
|
248
|
+
| `c3_edits` | Edit-ledger queries + version diffs + restore points + per-branch filter |
|
|
249
249
|
| `c3_bitbucket` | Bitbucket Data Center integration — PRs, branches, builds, repo admin (v2.30.0) |
|
|
250
250
|
| `c3_project` | Cross-project — discover & operate on other c3-installed projects; guarded writes (v2.31.0) |
|
|
251
251
|
|
|
@@ -363,7 +363,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
|
|
|
363
363
|
|
|
364
364
|
## Security & privacy
|
|
365
365
|
|
|
366
|
-
- **All web servers (Hub, per-project UI, Oracle) bind to `127.0.0.1` by default and are guarded against browser-based attacks even on loopback** — a Host-header allowlist (defeats DNS rebinding) plus an Origin/Referer check on every request (defeats cross-origin CSRF), with scoped, non-wildcard CORS. A malicious web page you visit therefore cannot drive C3's local endpoints. There is still **no user authentication**, so do not expose these servers to an untrusted network without auth/TLS in front. Binding to a non-loopback interface in `~/.c3/hub_config.json` (`host`) or Oracle's config (`bind_host`) is opt-in and warned at startup; add externally-facing hostnames/IPs to an `allowed_hosts` list there so the guard permits them.
|
|
366
|
+
- **All web servers (Hub, per-project UI, Oracle) bind to `127.0.0.1` by default and are guarded against browser-based attacks even on loopback** — a Host-header allowlist (defeats DNS rebinding) plus an Origin/Referer check on every request (defeats cross-origin CSRF), with scoped, non-wildcard CORS. A malicious web page you visit therefore cannot drive C3's local endpoints. There is still **no user authentication**, so do not expose these servers to an untrusted network without auth/TLS in front. Binding to a non-loopback interface in `~/.c3/hub_config.json` (`host`) or Oracle's config (`bind_host`) is opt-in and warned at startup; add externally-facing hostnames/IPs to an `allowed_hosts` list there so the guard permits them. _(Cross-origin/CSRF + DNS-rebinding hardening added in v2.33.0.)_
|
|
367
367
|
- **No telemetry by default.** The OSS package collects nothing. Opt-in Sentry crash reporting requires the `[telemetry]` extra plus both `SENTRY_DSN` and `C3_TELEMETRY_OPT_IN=1`. Even when enabled, request bodies, local variables, and prompts are stripped before sending.
|
|
368
368
|
- **API keys** for third-party model providers are read from environment variables and never persisted by C3.
|
|
369
369
|
- See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
|
|
2
2
|
cli/_hook_utils.py,sha256=1_hTA-Wz62xB8jnSAH4C5TfCkrwEP0g2kq_-oRfQLm4,3724
|
|
3
|
-
cli/c3.py,sha256=
|
|
3
|
+
cli/c3.py,sha256=lHmkT56LCHB7_NRNRXjqP8kXUWuzgfTKN9vevA7uj6M,289101
|
|
4
4
|
cli/docs.html,sha256=JgtBFUuUkvmYowPREYiGhhcRbB5e2UjkRc00MIF0hsU,143653
|
|
5
5
|
cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
|
|
6
6
|
cli/hook_auto_snapshot.py,sha256=amtliVDzKUQr6KBR0pdBA8vXghAV-gKr19jBaJVnP_w,5006
|
|
@@ -15,10 +15,10 @@ cli/hook_read.py,sha256=M5l_SU899O72tZe3j4YQJJKNb1-xulvKOj8XZjJzwYU,8021
|
|
|
15
15
|
cli/hook_session_stats.py,sha256=a1OKi9kmiXRI2qieY_Uq14xRxdXQTQu9WVzDTUlI0GQ,1897
|
|
16
16
|
cli/hook_terse_advisor.py,sha256=pD7Bap7OYOKqtYz7cX8nWSRLH7ook-tSD2Ov2MNp_sA,5907
|
|
17
17
|
cli/hub.html,sha256=Hl-XPZGT1mMiKrbX9c5OsEw6mXEumwIB3vp1WlWaplM,183966
|
|
18
|
-
cli/hub_server.py,sha256=
|
|
18
|
+
cli/hub_server.py,sha256=L7M3PAQNiRFyqdgL0COXIzIo9lyJTaCZSk-K1I2kZtM,59725
|
|
19
19
|
cli/mcp_proxy.py,sha256=92htuT-p0j-cDTbyqlIJpGoQ85_Aw7UuB8L_Toi_u20,17511
|
|
20
|
-
cli/mcp_server.py,sha256=
|
|
21
|
-
cli/server.py,sha256=
|
|
20
|
+
cli/mcp_server.py,sha256=iYUB6rfGjNuiNQP-GecuXyMVa1CEihtu4dV7PhRHWqg,32549
|
|
21
|
+
cli/server.py,sha256=n8CNh444AGuYMnVSSiRK9pirGh84Ap7ZTI8wuRrJCX8,119970
|
|
22
22
|
cli/ui.html,sha256=xcdt74nlFEXx-0Bx6-Okw-WSVZPAXL0iukxU0ytI6CA,5694
|
|
23
23
|
cli/ui_legacy.html,sha256=cI8tC6RKmE2NIJOcsu7CY-zT4VznjcbD6NTjxb_fvUY,378460
|
|
24
24
|
cli/ui_nano.html,sha256=UAwQ6bbTOXAoGq191AZ7slhngR9edJSa3IhqpynveDg,27740
|
|
@@ -32,7 +32,7 @@ cli/tools/bitbucket.py,sha256=MBVMnREDhJiUal43cPqLUUPWyS8AGp3v3rNZ291Vkrg,27271
|
|
|
32
32
|
cli/tools/compress.py,sha256=hgBQ6jUwvfGRmcC53vJBz_bbGD0E7T85IxD4q53rj4E,8941
|
|
33
33
|
cli/tools/delegate.py,sha256=zOpa03znY27_y1u7T7wbfgQXPwCDA409z9dJ2ki4esU,46947
|
|
34
34
|
cli/tools/edit.py,sha256=fVIZzBPe3ixKBxcZFU03ur2XKu9rAlBihga2-tmIuWw,13791
|
|
35
|
-
cli/tools/edits.py,sha256
|
|
35
|
+
cli/tools/edits.py,sha256=8zM01TzLmjm7ULQlCmXOmitlJd84zQHVzE0z7UHJUdA,5520
|
|
36
36
|
cli/tools/filter.py,sha256=IEjBdKrHxYVCm4cP0Ao6WZkKhbIBi1uI-mLY627zEUw,11503
|
|
37
37
|
cli/tools/impact.py,sha256=jjWkFTxHu-gBpZZNd2HTdBl22itA6-wwwOZXxk_qBl8,6257
|
|
38
38
|
cli/tools/memory.py,sha256=OdYBcIEFo4sr5aCG0_uO48uyJ-Kzof7LC2Ou1THGFuc,23317
|
|
@@ -40,7 +40,7 @@ cli/tools/project.py,sha256=PKHLGRFZdwBBEgryyV440mybTeQMqnlLgHdfXDnPahw,11868
|
|
|
40
40
|
cli/tools/read.py,sha256=2UT2ICjkyUqMWujOHidEhsGuOTORLTt7na2CVdVZwpw,10868
|
|
41
41
|
cli/tools/search.py,sha256=qB8C3w8yuu59aepvnuJNlzbsirtSEZA8zz4neKlE7Xs,13246
|
|
42
42
|
cli/tools/session.py,sha256=LIZbmEhNdh6rAsT6Dbpb21UY8xF9oubvpjGwfnXxQK4,4573
|
|
43
|
-
cli/tools/shell.py,sha256=
|
|
43
|
+
cli/tools/shell.py,sha256=lTGMpT_YWnAyJzcT-WHMLjuenqq6m-p3v6uCHKCsHWM,9752
|
|
44
44
|
cli/tools/status.py,sha256=yCHXskXgKzaDhJT6vHYlp3ZU5j89vhVYzf5dQOAR9yQ,12625
|
|
45
45
|
cli/tools/validate.py,sha256=KJr_YKLHiThgKdPnv-7Uucv2rS3DIoKISNxSy2icw8I,11965
|
|
46
46
|
cli/ui/api.js,sha256=wI9-lxGitIgZ5R6lqi1PVc2edD0b3TsYwMUGgcNWgfU,1342
|
|
@@ -57,16 +57,17 @@ cli/ui/components/memory.js,sha256=v5IsHTxLHpXX4xCsUaZ_UPprZEabdgP4jiWc298iV2U,2
|
|
|
57
57
|
cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE,30837
|
|
58
58
|
cli/ui/components/settings.js,sha256=8LVTV2TQl9tcRXhXbtBEJOCBdiyk-x2QASoVYZUAuEA,71442
|
|
59
59
|
cli/ui/components/sidebar.js,sha256=cAY_jwYB-o1X_wWn__VXlG4IegVObuE3NmVsuFWqxtg,7417
|
|
60
|
-
code_context_control-2.
|
|
60
|
+
code_context_control-2.35.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
61
61
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
62
62
|
core/config.py,sha256=0RBVni99wqJIxAYU6uweWVOmdI-FJvQ8d3IV5Mp1Muc,12818
|
|
63
63
|
core/ide.py,sha256=9LzsDVK2LL8RVpL40l6oNGiasZ3D8OCU_9i9A0gJKBo,6876
|
|
64
|
-
core/
|
|
64
|
+
core/mcp_toml.py,sha256=nFsgDm9j0W1v-o4tBTeEYFuHyxkBNuuMjPhJX-lvfYE,4422
|
|
65
|
+
core/web_security.py,sha256=WkNmoCsSWepV3XduibuQYalEJSIR_cpmumoK9bce9FM,7412
|
|
65
66
|
oracle/__init__.py,sha256=-OTD7Jh4mUMA4QgPGthPLWXttgZLpkIPhGQ87ZfHBx0,63
|
|
66
67
|
oracle/config.py,sha256=ErjH6Y_F51jGXpYo_4boGhdIk-AIo3rDH3xDwUGs7B8,3193
|
|
67
|
-
oracle/mcp_oracle.py,sha256=
|
|
68
|
+
oracle/mcp_oracle.py,sha256=Vts1ugITgvZP_BzKHgQN1nxHNVQNuNAEPdKgVIpRjPU,8133
|
|
68
69
|
oracle/oracle.html,sha256=KW1jeqmUwvAH2mDlhCLo05nrHW8PfmCYv0iQhL5d74s,185219
|
|
69
|
-
oracle/oracle_server.py,sha256=
|
|
70
|
+
oracle/oracle_server.py,sha256=qMKLzLoV5cp-cK-l5tc0w24jis2QudNVB7S74eKijGQ,33778
|
|
70
71
|
oracle/services/__init__.py,sha256=Nb4POd1_YIwLVYsGfr-DiK-iKTelkU0fh9m7wjeLQHA,23
|
|
71
72
|
oracle/services/api_auth.py,sha256=1PW3pG--1DJb_F6qMhP3gBTYHxxPE2bsHmVmIhC81Y8,3566
|
|
72
73
|
oracle/services/c3_bridge.py,sha256=Khj2jao2oENe4yFA31Ny0yI3fcV8XBerjsLslt4ns3U,9652
|
|
@@ -86,23 +87,24 @@ oracle/services/tool_registry.py,sha256=V7eP-UeacN3T4We_zbJ8NdFCFCKJVSKJa4zfH02L
|
|
|
86
87
|
services/__init__.py,sha256=3Kn4cZweLm7at8wFdBdZ-Zwo8hHcnVIsmY5f29nzi2Y,116
|
|
87
88
|
services/activity_log.py,sha256=YsW8-HBQEFh2vYTlvnzK7doNsR-XEtBbWXJ-324XigU,3370
|
|
88
89
|
services/agent_base.py,sha256=a-gdSd_jtZtbjXo1WS8CnWCagXgKaGZd5ShcG6s0kT4,4809
|
|
89
|
-
services/agents.py,sha256=
|
|
90
|
+
services/agents.py,sha256=iDYqT4iY4Q_KmWYWGKIjp7F1A_F6so9l6n59Qk7mHmI,73275
|
|
90
91
|
services/auto_memory.py,sha256=v__ZS1e68533_Yv491mZtvuZnheC63q6_uTvWhBw3Lw,14290
|
|
91
92
|
services/benchmark_dashboard.py,sha256=iR-DnqnoKbqHMJ4d-ZkIvJBYfzwTa7r-jzO6j2BYDfQ,27711
|
|
92
93
|
services/bitbucket_client.py,sha256=v8xGEcnIEmURvcg38XwmiCGh7-_QnjhAJEb0te_yZzQ,16107
|
|
93
94
|
services/bitbucket_credentials.py,sha256=2qLA9pQMol4y95y4DJMNBsBBPUsJQCKbLFo2iiCnfvI,7364
|
|
94
95
|
services/claude_md.py,sha256=iL0vUQw-5lxSQehNPvhlkUmcGPeMSCcZqP4OYG_qoYk,35092
|
|
95
96
|
services/compressor.py,sha256=uSVyTYfvxFrRYupzyKj-HzkBP0RwARrGYFz_DnMSEaM,25169
|
|
96
|
-
services/context_snapshot.py,sha256=
|
|
97
|
+
services/context_snapshot.py,sha256=s_klEr1SJYM9u-anMmnoemsYuIF_KUWBjz1zUo0wPgU,15662
|
|
97
98
|
services/conversation_store.py,sha256=vPiMiKAE22RCBSSphgGH9Vx-lPV45SmttOwgVVWahL4,33398
|
|
98
99
|
services/doc_index.py,sha256=kYcE_lQgjgG7CRmqN3Byx7MNmz1JCfm8QsrjH3u7OUI,18614
|
|
99
100
|
services/e2e_benchmark.py,sha256=kHZnatL27pHT1cC1DFW3SY0B6mNpIFdVns7au48Rp04,132867
|
|
100
101
|
services/e2e_evaluator.py,sha256=WRIPu6b1SrSQpHESCnIWrC1wsRp1v4UGaaNcFD1KZ5c,17672
|
|
101
102
|
services/e2e_tasks.py,sha256=Ln5VbGDIcS3NY3aml5ERbgjfjLxEslmRZ8AyaYxpWEo,34524
|
|
102
|
-
services/edit_ledger.py,sha256=
|
|
103
|
+
services/edit_ledger.py,sha256=R0eUA2jOXY_RUuCMT52wA4hTbgaeuYbGwy_xSMxS64k,18027
|
|
103
104
|
services/embedding_index.py,sha256=ZccqCH5WWQnqAmPtO1PB5W2N7OzRZARrctDBAtLgPBg,12769
|
|
104
105
|
services/error_reporting.py,sha256=HZ3ru8i5RLf8nq2R4iRnTs5sm1blUxknSbv5hdxuxs0,4139
|
|
105
106
|
services/file_memory.py,sha256=GnEbwdWE7TUKUR4PpSgHV7cnLys3Fa2bsLvc-XwuFgI,29188
|
|
107
|
+
services/git_context.py,sha256=lhuIvGDBUTKOPqye-olgJfjv538t-jtRZLBAcs7iVoA,9506
|
|
106
108
|
services/hub_service.py,sha256=Ta2ExJP1sePxb7zcHooroYXJKsylm5Ea8vQvts6-cAw,21876
|
|
107
109
|
services/indexer.py,sha256=ZceGqvd1OheN-hvSg4jjjXNcFjCSCgswKf5DmI-xaqA,27044
|
|
108
110
|
services/memory.py,sha256=uH3hWWUCy8p_0hVuJq-pzp2F5qNRLcVOpaFYmap2VFY,12458
|
|
@@ -123,14 +125,14 @@ services/retrieval_broker.py,sha256=9X67VZ_6AkbAzopHuuMFKmP4CGZLnW576kjSKMenBnw,
|
|
|
123
125
|
services/router.py,sha256=Cz10nx2fKTbaGn14mSBePWIDrw5rdcs_1JFYXeik084,15626
|
|
124
126
|
services/runtime.py,sha256=SOUizCDW1FFTDCoaZ1Njozjp25Bhah7lR1f0WYscaw0,11361
|
|
125
127
|
services/session_benchmark.py,sha256=qw_vtDim1hvFdM8Me5EsgU9pTuJhzRjQmh6m7DDnXWY,98989
|
|
126
|
-
services/session_manager.py,sha256=
|
|
128
|
+
services/session_manager.py,sha256=Px7RpTS6zDSuxj2O87o-7tkR8l-faMZxBX1gd5RLHfo,43837
|
|
127
129
|
services/session_preloader.py,sha256=DsTAXMKVtrX9yu1sEFojYDi9-jkSAj1Ylt9JTy57Dow,9883
|
|
128
130
|
services/text_index.py,sha256=r3o4CobTG9jAO9PWazgbWYLY9oi_FgEJ3xwEXrF4KM0,2783
|
|
129
131
|
services/tool_classifier.py,sha256=Fgvq0ZcpnCskwtO8a3YI1MiecPNnw6UbPyJQIUwgfiQ,6512
|
|
130
132
|
services/transcript_index.py,sha256=VQhvgkSyLVEYamXi_YIbiIhBnd0mqFHZlWG2HQu_1EA,12144
|
|
131
133
|
services/validation_cache.py,sha256=skFYR7CkSGvFb2gq6dfxOnvjQzz5Boa3jJWT7UfaIhY,5587
|
|
132
134
|
services/vector_store.py,sha256=o1RdZgRegFWlrr_kgSrY243W4KURmo4gvnP7vrxa2DE,11425
|
|
133
|
-
services/version_tracker.py,sha256=
|
|
135
|
+
services/version_tracker.py,sha256=8yQAcZuJjzq1Efib828FMOjQNIcVQvPQC5KksbWLSUw,9880
|
|
134
136
|
services/watcher.py,sha256=TT8dvUHw1z7Uc2KCbyynkkN4luns6qxsicc2_cj9PM8,6856
|
|
135
137
|
services/bench/__init__.py,sha256=WLEJIWJeaUj6FnH2nDO1qWugJDKfOKeM6WvKLSreYjI,231
|
|
136
138
|
services/bench/external/__init__.py,sha256=XgPS99ztx9igMd6x-1bLykcaePGXctkb6ujQ1MLgKAs,608
|
|
@@ -154,8 +156,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
154
156
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
155
157
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
156
158
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
157
|
-
code_context_control-2.
|
|
158
|
-
code_context_control-2.
|
|
159
|
-
code_context_control-2.
|
|
160
|
-
code_context_control-2.
|
|
161
|
-
code_context_control-2.
|
|
159
|
+
code_context_control-2.35.0.dist-info/METADATA,sha256=BGKYwuZVthBqtj3BfoOXk7OvsfXi10ys2RX03fu90gw,19822
|
|
160
|
+
code_context_control-2.35.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
161
|
+
code_context_control-2.35.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
162
|
+
code_context_control-2.35.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
163
|
+
code_context_control-2.35.0.dist-info/RECORD,,
|