code-context-control 2.35.0__py3-none-any.whl → 2.37.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 +235 -30
- cli/commands/common.py +3 -7
- cli/commands/parser.py +4 -0
- cli/guide/bitbucket.html +527 -0
- cli/guide/getting-started.html +457 -0
- cli/guide/index.html +611 -0
- cli/guide/oracle.html +385 -0
- cli/guide/shared.css +637 -0
- cli/guide/tools.html +1204 -0
- cli/guide/workflow.html +1193 -0
- cli/hub_server.py +14 -2
- cli/server.py +23 -11
- {code_context_control-2.35.0.dist-info → code_context_control-2.37.0.dist-info}/METADATA +33 -6
- {code_context_control-2.35.0.dist-info → code_context_control-2.37.0.dist-info}/RECORD +22 -15
- core/config.py +6 -0
- services/agents.py +105 -0
- services/claude_md.py +139 -20
- services/session_manager.py +7 -12
- {code_context_control-2.35.0.dist-info → code_context_control-2.37.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.35.0.dist-info → code_context_control-2.37.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.35.0.dist-info → code_context_control-2.37.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.35.0.dist-info → code_context_control-2.37.0.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
|
@@ -85,7 +85,7 @@ console = Console() if HAS_RICH else None
|
|
|
85
85
|
# Config
|
|
86
86
|
CONFIG_DIR = ".c3"
|
|
87
87
|
CONFIG_FILE = ".c3/config.json"
|
|
88
|
-
__version__ = "2.
|
|
88
|
+
__version__ = "2.37.0"
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _command_deps() -> CommandDeps:
|
|
@@ -379,6 +379,44 @@ def _build_permission_tier(tier: str, include_mcp_wildcard: bool = False) -> dic
|
|
|
379
379
|
}}
|
|
380
380
|
|
|
381
381
|
|
|
382
|
+
def _c3_managed_permission_entries() -> tuple[set, set]:
|
|
383
|
+
"""Return (allow, deny) sets of every entry any C3 tier can emit.
|
|
384
|
+
|
|
385
|
+
Used to tell C3-managed permission rules apart from user-added ones so a
|
|
386
|
+
tier change replaces only the former and preserves the latter.
|
|
387
|
+
"""
|
|
388
|
+
managed_allow: set = set()
|
|
389
|
+
managed_deny: set = set()
|
|
390
|
+
for _tier in PERMISSION_TIERS:
|
|
391
|
+
perms = _build_permission_tier(_tier, include_mcp_wildcard=True)["permissions"]
|
|
392
|
+
managed_allow.update(perms.get("allow", []))
|
|
393
|
+
managed_deny.update(perms.get("deny", []))
|
|
394
|
+
return managed_allow, managed_deny
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _merge_permission_tier(existing: dict, tier_perms: dict) -> dict:
|
|
398
|
+
"""Merge a tier's permissions into existing ones, preserving user rules.
|
|
399
|
+
|
|
400
|
+
C3 owns every entry a tier can emit: those are replaced by the chosen tier.
|
|
401
|
+
Any other allow/deny entry the user added is kept, and non-list permission
|
|
402
|
+
keys (e.g. ``ask``, ``defaultMode``, ``additionalDirectories``) are left
|
|
403
|
+
untouched. Mirrors how hooks and .mcp.json preserve non-C3 content.
|
|
404
|
+
"""
|
|
405
|
+
existing = existing if isinstance(existing, dict) else {}
|
|
406
|
+
managed = dict(zip(("allow", "deny"), _c3_managed_permission_entries()))
|
|
407
|
+
merged = dict(existing) # preserve unknown sub-keys (ask, defaultMode, ...)
|
|
408
|
+
for key in ("allow", "deny"):
|
|
409
|
+
user_custom = [e for e in (existing.get(key) or []) if e not in managed[key]]
|
|
410
|
+
out: list = []
|
|
411
|
+
seen: set = set()
|
|
412
|
+
for entry in user_custom + list(tier_perms.get(key) or []):
|
|
413
|
+
if entry not in seen:
|
|
414
|
+
seen.add(entry)
|
|
415
|
+
out.append(entry)
|
|
416
|
+
merged[key] = out
|
|
417
|
+
return merged
|
|
418
|
+
|
|
419
|
+
|
|
382
420
|
def _detect_current_tier(settings_path) -> str | None:
|
|
383
421
|
"""Detect which permission tier is active in settings_path, or None.
|
|
384
422
|
|
|
@@ -456,9 +494,12 @@ def _apply_permission_tier(project_path: str, tier: str,
|
|
|
456
494
|
settings_path = Path(project_path) / ".claude" / "settings.local.json"
|
|
457
495
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
458
496
|
settings = _safe_read_json(settings_path, str(settings_path))
|
|
459
|
-
|
|
497
|
+
tier_perms = _build_permission_tier(
|
|
460
498
|
tier, include_mcp_wildcard=include_mcp_wildcard
|
|
461
499
|
)["permissions"]
|
|
500
|
+
settings["permissions"] = _merge_permission_tier(
|
|
501
|
+
settings.get("permissions") or {}, tier_perms
|
|
502
|
+
)
|
|
462
503
|
# Persist chosen tier in .c3/config.json
|
|
463
504
|
c3_config_path = Path(project_path) / ".c3" / "config.json"
|
|
464
505
|
c3_config = _safe_read_json(c3_config_path, str(c3_config_path))
|
|
@@ -912,6 +953,12 @@ def cmd_init(args):
|
|
|
912
953
|
health["instructions_file"] + " missing" not in " ".join(health["issues"])
|
|
913
954
|
else " [MISSING]"))
|
|
914
955
|
|
|
956
|
+
# Version-skew notice: this project's .c3 was written by an older C3.
|
|
957
|
+
stored_version = _safe_read_json(c3_dir / "config.json", "config").get("version")
|
|
958
|
+
if stored_version and _version_tuple(str(stored_version)) < _version_tuple(__version__):
|
|
959
|
+
print(f"\n [upgrade] Set up with C3 v{stored_version}; now running v{__version__}.")
|
|
960
|
+
print(" Run 'c3 init . --force' to re-apply MCP config, hooks, and docs.")
|
|
961
|
+
|
|
915
962
|
# Permission status (Claude Code only) — surface tier + stale-tool drift
|
|
916
963
|
try:
|
|
917
964
|
from core.ide import load_ide_config as _load_ide
|
|
@@ -3872,8 +3919,8 @@ _AGENTS_MD_CONTENT = _C3_COMPACT_WORKFLOW + """
|
|
|
3872
3919
|
This project uses project-scoped MCP servers. Ensure your `.codex/config.toml` includes:
|
|
3873
3920
|
```toml
|
|
3874
3921
|
[mcp_servers.c3]
|
|
3875
|
-
command = "
|
|
3876
|
-
args = ["
|
|
3922
|
+
command = "c3-mcp"
|
|
3923
|
+
args = ["--project", "."]
|
|
3877
3924
|
enabled = true
|
|
3878
3925
|
```
|
|
3879
3926
|
"""
|
|
@@ -3886,8 +3933,8 @@ This project uses project-scoped MCP servers. Ensure your `.gemini/settings.json
|
|
|
3886
3933
|
{
|
|
3887
3934
|
"mcpServers": {
|
|
3888
3935
|
"c3": {
|
|
3889
|
-
"command": "
|
|
3890
|
-
"args": ["
|
|
3936
|
+
"command": "c3-mcp",
|
|
3937
|
+
"args": ["--project", "."]
|
|
3891
3938
|
}
|
|
3892
3939
|
}
|
|
3893
3940
|
}
|
|
@@ -4215,11 +4262,17 @@ def _upsert_json_mcp_server(config_path: Path, config_key: str, server_name: str
|
|
|
4215
4262
|
return "updated" if previous_entry is not None else "written"
|
|
4216
4263
|
|
|
4217
4264
|
|
|
4218
|
-
def _ensure_project_session_configs(target: Path, server_script: str, primary_profile: str | None = None
|
|
4265
|
+
def _ensure_project_session_configs(target: Path, server_script: str, primary_profile: str | None = None,
|
|
4266
|
+
c3_mcp_exe: str | None = None) -> None:
|
|
4219
4267
|
"""Keep project-local Codex and Gemini MCP configs in sync for new sessions."""
|
|
4220
4268
|
# Ensure forward slashes for config portability and avoid Windows path-splitting issues
|
|
4221
4269
|
server_script_posix = Path(server_script).as_posix()
|
|
4222
|
-
|
|
4270
|
+
if c3_mcp_exe:
|
|
4271
|
+
mcp_command = c3_mcp_exe
|
|
4272
|
+
server_args = ["--project", target.as_posix()]
|
|
4273
|
+
else:
|
|
4274
|
+
mcp_command = "python"
|
|
4275
|
+
server_args = [server_script_posix, "--project", target.as_posix()]
|
|
4223
4276
|
|
|
4224
4277
|
if primary_profile != "codex":
|
|
4225
4278
|
codex_path = target / ".codex" / "config.toml"
|
|
@@ -4228,7 +4281,7 @@ def _ensure_project_session_configs(target: Path, server_script: str, primary_pr
|
|
|
4228
4281
|
codex_path,
|
|
4229
4282
|
"mcp_servers.c3",
|
|
4230
4283
|
{
|
|
4231
|
-
"command":
|
|
4284
|
+
"command": mcp_command,
|
|
4232
4285
|
"args": server_args,
|
|
4233
4286
|
"enabled": True,
|
|
4234
4287
|
},
|
|
@@ -4242,14 +4295,14 @@ def _ensure_project_session_configs(target: Path, server_script: str, primary_pr
|
|
|
4242
4295
|
"mcpServers",
|
|
4243
4296
|
"c3",
|
|
4244
4297
|
{
|
|
4245
|
-
"command":
|
|
4298
|
+
"command": mcp_command,
|
|
4246
4299
|
"args": server_args,
|
|
4247
4300
|
},
|
|
4248
4301
|
)
|
|
4249
4302
|
print(f"{gemini_state.capitalize()} {gemini_path}")
|
|
4250
4303
|
|
|
4251
4304
|
|
|
4252
|
-
def _ensure_global_session_fallbacks(server_script: str) -> None:
|
|
4305
|
+
def _ensure_global_session_fallbacks(server_script: str, c3_mcp_exe: str | None = None) -> None:
|
|
4253
4306
|
"""Keep user-global Codex/Gemini MCP configs pointing at C3.
|
|
4254
4307
|
|
|
4255
4308
|
These fallback entries omit `--project` so the MCP server can resolve the
|
|
@@ -4257,7 +4310,9 @@ def _ensure_global_session_fallbacks(server_script: str) -> None:
|
|
|
4257
4310
|
does not yet have project-local Codex/Gemini config files.
|
|
4258
4311
|
"""
|
|
4259
4312
|
server_script_posix = Path(server_script).as_posix()
|
|
4260
|
-
|
|
4313
|
+
# With the installed entry point, no script path is needed; --project stays
|
|
4314
|
+
# omitted so the server resolves the working directory at session start.
|
|
4315
|
+
fallback_args = [] if c3_mcp_exe else [server_script_posix]
|
|
4261
4316
|
|
|
4262
4317
|
codex_path = Path.home() / ".codex" / "config.toml"
|
|
4263
4318
|
try:
|
|
@@ -4266,7 +4321,7 @@ def _ensure_global_session_fallbacks(server_script: str) -> None:
|
|
|
4266
4321
|
codex_path,
|
|
4267
4322
|
"mcp_servers.c3",
|
|
4268
4323
|
{
|
|
4269
|
-
"command": "python",
|
|
4324
|
+
"command": c3_mcp_exe or "python",
|
|
4270
4325
|
"args": fallback_args,
|
|
4271
4326
|
"enabled": True,
|
|
4272
4327
|
},
|
|
@@ -4282,7 +4337,7 @@ def _ensure_global_session_fallbacks(server_script: str) -> None:
|
|
|
4282
4337
|
"mcpServers",
|
|
4283
4338
|
"c3",
|
|
4284
4339
|
{
|
|
4285
|
-
"command": sys.executable,
|
|
4340
|
+
"command": c3_mcp_exe or sys.executable,
|
|
4286
4341
|
"args": fallback_args,
|
|
4287
4342
|
},
|
|
4288
4343
|
)
|
|
@@ -4825,13 +4880,25 @@ def cmd_install_mcp(args):
|
|
|
4825
4880
|
# Use forward slashes for cross-platform compatibility in config files
|
|
4826
4881
|
server_script = (cli_dir / server_filename).as_posix()
|
|
4827
4882
|
|
|
4828
|
-
#
|
|
4829
|
-
#
|
|
4830
|
-
#
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4883
|
+
# Prefer the installed `c3-mcp` console script for direct mode. It survives C3
|
|
4884
|
+
# upgrades (pip/pipx reinstall to the same launcher path) and keeps the source-tree
|
|
4885
|
+
# location out of every project's MCP config, so upgrading no longer requires
|
|
4886
|
+
# re-running install-mcp per project. Fall back to invoking the source script with
|
|
4887
|
+
# `python` when running from a checkout with no installed entry point, or in proxy
|
|
4888
|
+
# mode (which has no console script).
|
|
4889
|
+
import shutil
|
|
4890
|
+
c3_mcp_exe = None
|
|
4891
|
+
if mcp_mode != "proxy":
|
|
4892
|
+
_found = shutil.which("c3-mcp")
|
|
4893
|
+
if _found:
|
|
4894
|
+
c3_mcp_exe = Path(_found).resolve().as_posix()
|
|
4895
|
+
|
|
4896
|
+
# On Windows, Gemini CLI splits command args by space, so the script path stays a
|
|
4897
|
+
# single arg. 'python' keeps the source fallback portable across platforms.
|
|
4898
|
+
if c3_mcp_exe:
|
|
4899
|
+
new_entry = {"command": c3_mcp_exe, "args": ["--project", "."]}
|
|
4900
|
+
else:
|
|
4901
|
+
new_entry = {"command": "python", "args": [server_script, "--project", "."]}
|
|
4835
4902
|
if profile.needs_type_field:
|
|
4836
4903
|
new_entry["type"] = "stdio"
|
|
4837
4904
|
|
|
@@ -4865,7 +4932,10 @@ def cmd_install_mcp(args):
|
|
|
4865
4932
|
try:
|
|
4866
4933
|
if profile.config_format == "toml":
|
|
4867
4934
|
# Codex uses TOML: [mcp_servers.c3] with command/args
|
|
4868
|
-
|
|
4935
|
+
if c3_mcp_exe:
|
|
4936
|
+
toml_entries = {"command": c3_mcp_exe, "args": ["--project", str(target)]}
|
|
4937
|
+
else:
|
|
4938
|
+
toml_entries = {"command": sys.executable, "args": [server_script, "--project", str(target)]}
|
|
4869
4939
|
if profile.name == "codex":
|
|
4870
4940
|
# Codex supports explicit enable/disable per server.
|
|
4871
4941
|
toml_entries["enabled"] = True
|
|
@@ -4895,8 +4965,8 @@ def cmd_install_mcp(args):
|
|
|
4895
4965
|
|
|
4896
4966
|
print(f"Wrote {mcp_config_path}")
|
|
4897
4967
|
if profile.name in {"codex", "gemini"}:
|
|
4898
|
-
_ensure_project_session_configs(target, server_script, primary_profile=profile.name)
|
|
4899
|
-
_ensure_global_session_fallbacks(server_script)
|
|
4968
|
+
_ensure_project_session_configs(target, server_script, primary_profile=profile.name, c3_mcp_exe=c3_mcp_exe)
|
|
4969
|
+
_ensure_global_session_fallbacks(server_script, c3_mcp_exe=c3_mcp_exe)
|
|
4900
4970
|
|
|
4901
4971
|
# ── Persist IDE choice to .c3/config.json ──
|
|
4902
4972
|
c3_config_dir = target / ".c3"
|
|
@@ -5110,10 +5180,23 @@ def cmd_install_mcp(args):
|
|
|
5110
5180
|
},
|
|
5111
5181
|
]
|
|
5112
5182
|
stop_event = "Stop"
|
|
5113
|
-
# Replace
|
|
5183
|
+
# Replace only C3's own stop hooks (identified by our hook scripts) and
|
|
5184
|
+
# keep every user-added stop hook — including matcher-less ones, which
|
|
5185
|
+
# are the normal shape for Stop hooks.
|
|
5186
|
+
_c3_stop_scripts = (
|
|
5187
|
+
"hook_session_stats.py", "hook_auto_snapshot.py", "hook_terse_advisor.py",
|
|
5188
|
+
)
|
|
5189
|
+
|
|
5190
|
+
def _is_c3_stop_hook(entry: dict) -> bool:
|
|
5191
|
+
return any(
|
|
5192
|
+
script in (hk.get("command") or "")
|
|
5193
|
+
for hk in entry.get("hooks", [])
|
|
5194
|
+
for script in _c3_stop_scripts
|
|
5195
|
+
)
|
|
5196
|
+
|
|
5114
5197
|
existing_stop = [
|
|
5115
5198
|
h for h in settings.get("hooks", {}).get(stop_event, [])
|
|
5116
|
-
if h
|
|
5199
|
+
if not _is_c3_stop_hook(h)
|
|
5117
5200
|
]
|
|
5118
5201
|
existing_stop.extend(desired_stop_hooks)
|
|
5119
5202
|
settings.setdefault("hooks", {})[stop_event] = existing_stop
|
|
@@ -5131,9 +5214,12 @@ def cmd_install_mcp(args):
|
|
|
5131
5214
|
include_wildcard = bool(getattr(args, "include_mcp_wildcard", False))
|
|
5132
5215
|
if perm_tier and profile.name == "claude-code":
|
|
5133
5216
|
if perm_tier in PERMISSION_TIERS:
|
|
5134
|
-
settings["permissions"] =
|
|
5135
|
-
|
|
5136
|
-
|
|
5217
|
+
settings["permissions"] = _merge_permission_tier(
|
|
5218
|
+
settings.get("permissions") or {},
|
|
5219
|
+
_build_permission_tier(
|
|
5220
|
+
perm_tier, include_mcp_wildcard=include_wildcard
|
|
5221
|
+
)["permissions"],
|
|
5222
|
+
)
|
|
5137
5223
|
# Persist tier choice in .c3/config.json
|
|
5138
5224
|
_c3cfg = _safe_read_json(c3_config_path, str(c3_config_path))
|
|
5139
5225
|
_c3cfg["permission_tier"] = perm_tier
|
|
@@ -6342,6 +6428,123 @@ def _run_swe_bench_lite(args, project_path):
|
|
|
6342
6428
|
pass
|
|
6343
6429
|
|
|
6344
6430
|
|
|
6431
|
+
def _version_tuple(v: str) -> tuple:
|
|
6432
|
+
"""Best-effort numeric version tuple for comparisons ('2.36.0' -> (2, 36, 0))."""
|
|
6433
|
+
parts = []
|
|
6434
|
+
for chunk in str(v or "").split("."):
|
|
6435
|
+
digits = ""
|
|
6436
|
+
for ch in chunk:
|
|
6437
|
+
if ch.isdigit():
|
|
6438
|
+
digits += ch
|
|
6439
|
+
else:
|
|
6440
|
+
break
|
|
6441
|
+
parts.append(int(digits) if digits else 0)
|
|
6442
|
+
return tuple(parts) or (0,)
|
|
6443
|
+
|
|
6444
|
+
|
|
6445
|
+
def _latest_pypi_version(package: str = "code-context-control", timeout: float = 5.0) -> str | None:
|
|
6446
|
+
"""Best-effort latest release of `package` on PyPI; None if unreachable."""
|
|
6447
|
+
import urllib.request
|
|
6448
|
+
try:
|
|
6449
|
+
url = f"https://pypi.org/pypi/{package}/json"
|
|
6450
|
+
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
6451
|
+
data = json.loads(resp.read().decode("utf-8", "replace"))
|
|
6452
|
+
return (data.get("info") or {}).get("version")
|
|
6453
|
+
except Exception:
|
|
6454
|
+
return None
|
|
6455
|
+
|
|
6456
|
+
|
|
6457
|
+
def _installed_distribution(package: str = "code-context-control"):
|
|
6458
|
+
"""Return the installed Distribution for `package`, or None when running from source."""
|
|
6459
|
+
try:
|
|
6460
|
+
from importlib import metadata
|
|
6461
|
+
return metadata.distribution(package)
|
|
6462
|
+
except Exception:
|
|
6463
|
+
return None
|
|
6464
|
+
|
|
6465
|
+
|
|
6466
|
+
def _is_editable_install(package: str = "code-context-control") -> bool:
|
|
6467
|
+
"""True when `package` is pip-installed in editable/development mode."""
|
|
6468
|
+
dist = _installed_distribution(package)
|
|
6469
|
+
if dist is None:
|
|
6470
|
+
return False
|
|
6471
|
+
try:
|
|
6472
|
+
text = dist.read_text("direct_url.json")
|
|
6473
|
+
if text:
|
|
6474
|
+
return bool(json.loads(text).get("dir_info", {}).get("editable"))
|
|
6475
|
+
except Exception:
|
|
6476
|
+
pass
|
|
6477
|
+
return False
|
|
6478
|
+
|
|
6479
|
+
|
|
6480
|
+
def cmd_upgrade(args):
|
|
6481
|
+
"""Upgrade C3 to the latest PyPI release (or just check with --check)."""
|
|
6482
|
+
current = __version__
|
|
6483
|
+
latest = _latest_pypi_version()
|
|
6484
|
+
if latest is None:
|
|
6485
|
+
print(" Could not reach PyPI to check for updates (offline?).")
|
|
6486
|
+
elif _version_tuple(latest) <= _version_tuple(current):
|
|
6487
|
+
print(f" C3 is up to date (v{current}).")
|
|
6488
|
+
return
|
|
6489
|
+
else:
|
|
6490
|
+
print(f" Update available: v{current} -> v{latest}")
|
|
6491
|
+
|
|
6492
|
+
if getattr(args, "check", False):
|
|
6493
|
+
return
|
|
6494
|
+
|
|
6495
|
+
if _installed_distribution() is None:
|
|
6496
|
+
print(" C3 is running from a source checkout (not pip-installed).")
|
|
6497
|
+
print(" Update with: git pull")
|
|
6498
|
+
return
|
|
6499
|
+
if _is_editable_install():
|
|
6500
|
+
print(" C3 is installed in editable/development mode (pip install -e .).")
|
|
6501
|
+
print(" Update with: git pull")
|
|
6502
|
+
return
|
|
6503
|
+
|
|
6504
|
+
print(" Upgrading via pip (this may take a minute)...")
|
|
6505
|
+
cmd = [sys.executable, "-m", "pip", "install", "-U", "code-context-control[tui]"]
|
|
6506
|
+
try:
|
|
6507
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
6508
|
+
except Exception as e:
|
|
6509
|
+
print(f" Upgrade failed to launch pip: {e}")
|
|
6510
|
+
sys.exit(1)
|
|
6511
|
+
if result.returncode != 0:
|
|
6512
|
+
print(" pip upgrade failed:")
|
|
6513
|
+
print((result.stderr or result.stdout or "").strip()[-1000:])
|
|
6514
|
+
sys.exit(1)
|
|
6515
|
+
print(" Upgraded to the latest release. Restart your IDE's MCP server to load it.")
|
|
6516
|
+
print(" In each project, run c3 init . --force to apply any migrations.")
|
|
6517
|
+
|
|
6518
|
+
|
|
6519
|
+
def _launch_tui() -> None:
|
|
6520
|
+
"""Launch the interactive TUI — what `c3` with no arguments does.
|
|
6521
|
+
|
|
6522
|
+
Runs tui/main.py as a subprocess so its bare `from screens...` imports resolve
|
|
6523
|
+
(its own directory lands on sys.path[0]); the package root goes on PYTHONPATH for
|
|
6524
|
+
cli/services imports. Falls back to help text when the optional [tui] extra
|
|
6525
|
+
(textual) is not installed.
|
|
6526
|
+
"""
|
|
6527
|
+
pkg_root = Path(__file__).resolve().parent.parent
|
|
6528
|
+
tui_main = pkg_root / "tui" / "main.py"
|
|
6529
|
+
try:
|
|
6530
|
+
import textual # noqa: F401
|
|
6531
|
+
except Exception:
|
|
6532
|
+
print("The interactive TUI needs the optional 'textual' dependency.")
|
|
6533
|
+
print(' Install it with: pip install "code-context-control[tui]"')
|
|
6534
|
+
print(" Or run c3 --help to see all commands.")
|
|
6535
|
+
return
|
|
6536
|
+
if not tui_main.exists():
|
|
6537
|
+
print("TUI entry point not found. Run c3 --help for commands.")
|
|
6538
|
+
return
|
|
6539
|
+
env = os.environ.copy()
|
|
6540
|
+
existing_pp = env.get("PYTHONPATH")
|
|
6541
|
+
env["PYTHONPATH"] = str(pkg_root) + (os.pathsep + existing_pp if existing_pp else "")
|
|
6542
|
+
try:
|
|
6543
|
+
subprocess.run([sys.executable, str(tui_main)], env=env)
|
|
6544
|
+
except KeyboardInterrupt:
|
|
6545
|
+
pass
|
|
6546
|
+
|
|
6547
|
+
|
|
6345
6548
|
def main():
|
|
6346
6549
|
try:
|
|
6347
6550
|
from services import error_reporting
|
|
@@ -6353,7 +6556,8 @@ def main():
|
|
|
6353
6556
|
args = parser.parse_args()
|
|
6354
6557
|
|
|
6355
6558
|
if not args.command:
|
|
6356
|
-
|
|
6559
|
+
# Bare `c3` launches the interactive TUI (replaces the old c3.bat wrapper).
|
|
6560
|
+
_launch_tui()
|
|
6357
6561
|
return
|
|
6358
6562
|
|
|
6359
6563
|
commands = {
|
|
@@ -6382,6 +6586,7 @@ def main():
|
|
|
6382
6586
|
"hub": cmd_hub,
|
|
6383
6587
|
"bitbucket": cmd_bitbucket,
|
|
6384
6588
|
"oracle": cmd_oracle,
|
|
6589
|
+
"upgrade": cmd_upgrade,
|
|
6385
6590
|
}
|
|
6386
6591
|
|
|
6387
6592
|
cmd_func = commands.get(args.command)
|
cli/commands/common.py
CHANGED
|
@@ -200,13 +200,9 @@ def cmd_claudemd(args, deps: CommandDeps):
|
|
|
200
200
|
print(content)
|
|
201
201
|
else:
|
|
202
202
|
output_path = Path(project_path) / instructions_file
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if "# User Notes" in existing:
|
|
207
|
-
user_section = existing[existing.index("# User Notes"):]
|
|
208
|
-
content += f"\n\n{user_section}"
|
|
209
|
-
output_path.write_text(content, encoding="utf-8")
|
|
203
|
+
# Wrap in the C3 managed block; preserve user content outside it.
|
|
204
|
+
from services.claude_md import write_c3_instruction_doc
|
|
205
|
+
write_c3_instruction_doc(output_path, content)
|
|
210
206
|
print(f"{instructions_file} saved to {output_path} ({tokens} tokens)")
|
|
211
207
|
|
|
212
208
|
elif args.claudemd_cmd == "check":
|
cli/commands/parser.py
CHANGED
|
@@ -23,6 +23,10 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
23
23
|
p_init.add_argument("--permissions", choices=["read-only", "c3-strict", "standard", "permissive"], default=None, help="Apply Claude Code permission tier (Claude Code only, used with --force)")
|
|
24
24
|
p_init.add_argument("--include-mcp-wildcard", action="store_true", help="Add mcp__* wildcard so non-C3 MCP servers don't prompt per-call")
|
|
25
25
|
|
|
26
|
+
p_upgrade = subparsers.add_parser("upgrade", help="Upgrade C3 to the latest PyPI release")
|
|
27
|
+
p_upgrade.add_argument("--check", action="store_true",
|
|
28
|
+
help="Only report whether a newer version exists; don't install")
|
|
29
|
+
|
|
26
30
|
p_index = subparsers.add_parser("index", help="Rebuild code index")
|
|
27
31
|
p_index.add_argument("--max-files", type=int, default=500)
|
|
28
32
|
|