code-context-control 2.34.0__py3-none-any.whl → 2.36.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 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.34.0"
88
+ __version__ = "2.36.0"
89
89
 
90
90
 
91
91
  def _command_deps() -> CommandDeps:
@@ -912,6 +912,12 @@ def cmd_init(args):
912
912
  health["instructions_file"] + " missing" not in " ".join(health["issues"])
913
913
  else " [MISSING]"))
914
914
 
915
+ # Version-skew notice: this project's .c3 was written by an older C3.
916
+ stored_version = _safe_read_json(c3_dir / "config.json", "config").get("version")
917
+ if stored_version and _version_tuple(str(stored_version)) < _version_tuple(__version__):
918
+ print(f"\n [upgrade] Set up with C3 v{stored_version}; now running v{__version__}.")
919
+ print(" Run 'c3 init . --force' to re-apply MCP config, hooks, and docs.")
920
+
915
921
  # Permission status (Claude Code only) — surface tier + stale-tool drift
916
922
  try:
917
923
  from core.ide import load_ide_config as _load_ide
@@ -3872,8 +3878,8 @@ _AGENTS_MD_CONTENT = _C3_COMPACT_WORKFLOW + """
3872
3878
  This project uses project-scoped MCP servers. Ensure your `.codex/config.toml` includes:
3873
3879
  ```toml
3874
3880
  [mcp_servers.c3]
3875
- command = "python"
3876
- args = ["<path-to-c3>/cli/mcp_server.py", "--project", "."]
3881
+ command = "c3-mcp"
3882
+ args = ["--project", "."]
3877
3883
  enabled = true
3878
3884
  ```
3879
3885
  """
@@ -3886,8 +3892,8 @@ This project uses project-scoped MCP servers. Ensure your `.gemini/settings.json
3886
3892
  {
3887
3893
  "mcpServers": {
3888
3894
  "c3": {
3889
- "command": "python",
3890
- "args": ["<path-to-c3>/cli/mcp_server.py", "--project", "."]
3895
+ "command": "c3-mcp",
3896
+ "args": ["--project", "."]
3891
3897
  }
3892
3898
  }
3893
3899
  }
@@ -4215,11 +4221,17 @@ def _upsert_json_mcp_server(config_path: Path, config_key: str, server_name: str
4215
4221
  return "updated" if previous_entry is not None else "written"
4216
4222
 
4217
4223
 
4218
- def _ensure_project_session_configs(target: Path, server_script: str, primary_profile: str | None = None) -> None:
4224
+ def _ensure_project_session_configs(target: Path, server_script: str, primary_profile: str | None = None,
4225
+ c3_mcp_exe: str | None = None) -> None:
4219
4226
  """Keep project-local Codex and Gemini MCP configs in sync for new sessions."""
4220
4227
  # Ensure forward slashes for config portability and avoid Windows path-splitting issues
4221
4228
  server_script_posix = Path(server_script).as_posix()
4222
- server_args = [server_script_posix, "--project", target.as_posix()]
4229
+ if c3_mcp_exe:
4230
+ mcp_command = c3_mcp_exe
4231
+ server_args = ["--project", target.as_posix()]
4232
+ else:
4233
+ mcp_command = "python"
4234
+ server_args = [server_script_posix, "--project", target.as_posix()]
4223
4235
 
4224
4236
  if primary_profile != "codex":
4225
4237
  codex_path = target / ".codex" / "config.toml"
@@ -4228,7 +4240,7 @@ def _ensure_project_session_configs(target: Path, server_script: str, primary_pr
4228
4240
  codex_path,
4229
4241
  "mcp_servers.c3",
4230
4242
  {
4231
- "command": "python",
4243
+ "command": mcp_command,
4232
4244
  "args": server_args,
4233
4245
  "enabled": True,
4234
4246
  },
@@ -4242,14 +4254,14 @@ def _ensure_project_session_configs(target: Path, server_script: str, primary_pr
4242
4254
  "mcpServers",
4243
4255
  "c3",
4244
4256
  {
4245
- "command": "python",
4257
+ "command": mcp_command,
4246
4258
  "args": server_args,
4247
4259
  },
4248
4260
  )
4249
4261
  print(f"{gemini_state.capitalize()} {gemini_path}")
4250
4262
 
4251
4263
 
4252
- def _ensure_global_session_fallbacks(server_script: str) -> None:
4264
+ def _ensure_global_session_fallbacks(server_script: str, c3_mcp_exe: str | None = None) -> None:
4253
4265
  """Keep user-global Codex/Gemini MCP configs pointing at C3.
4254
4266
 
4255
4267
  These fallback entries omit `--project` so the MCP server can resolve the
@@ -4257,7 +4269,9 @@ def _ensure_global_session_fallbacks(server_script: str) -> None:
4257
4269
  does not yet have project-local Codex/Gemini config files.
4258
4270
  """
4259
4271
  server_script_posix = Path(server_script).as_posix()
4260
- fallback_args = [server_script_posix]
4272
+ # With the installed entry point, no script path is needed; --project stays
4273
+ # omitted so the server resolves the working directory at session start.
4274
+ fallback_args = [] if c3_mcp_exe else [server_script_posix]
4261
4275
 
4262
4276
  codex_path = Path.home() / ".codex" / "config.toml"
4263
4277
  try:
@@ -4266,7 +4280,7 @@ def _ensure_global_session_fallbacks(server_script: str) -> None:
4266
4280
  codex_path,
4267
4281
  "mcp_servers.c3",
4268
4282
  {
4269
- "command": "python",
4283
+ "command": c3_mcp_exe or "python",
4270
4284
  "args": fallback_args,
4271
4285
  "enabled": True,
4272
4286
  },
@@ -4282,7 +4296,7 @@ def _ensure_global_session_fallbacks(server_script: str) -> None:
4282
4296
  "mcpServers",
4283
4297
  "c3",
4284
4298
  {
4285
- "command": sys.executable,
4299
+ "command": c3_mcp_exe or sys.executable,
4286
4300
  "args": fallback_args,
4287
4301
  },
4288
4302
  )
@@ -4825,13 +4839,25 @@ def cmd_install_mcp(args):
4825
4839
  # Use forward slashes for cross-platform compatibility in config files
4826
4840
  server_script = (cli_dir / server_filename).as_posix()
4827
4841
 
4828
- # Use 'python' for project-scoped IDE configs to be portable in templates,
4829
- # but use sys.executable for the actual config write to be precise.
4830
- # On Windows, Gemini CLI splits command args by space, so we must quote the script path.
4831
- new_entry = {
4832
- "command": "python",
4833
- "args": [server_script, "--project", "."],
4834
- }
4842
+ # Prefer the installed `c3-mcp` console script for direct mode. It survives C3
4843
+ # upgrades (pip/pipx reinstall to the same launcher path) and keeps the source-tree
4844
+ # location out of every project's MCP config, so upgrading no longer requires
4845
+ # re-running install-mcp per project. Fall back to invoking the source script with
4846
+ # `python` when running from a checkout with no installed entry point, or in proxy
4847
+ # mode (which has no console script).
4848
+ import shutil
4849
+ c3_mcp_exe = None
4850
+ if mcp_mode != "proxy":
4851
+ _found = shutil.which("c3-mcp")
4852
+ if _found:
4853
+ c3_mcp_exe = Path(_found).resolve().as_posix()
4854
+
4855
+ # On Windows, Gemini CLI splits command args by space, so the script path stays a
4856
+ # single arg. 'python' keeps the source fallback portable across platforms.
4857
+ if c3_mcp_exe:
4858
+ new_entry = {"command": c3_mcp_exe, "args": ["--project", "."]}
4859
+ else:
4860
+ new_entry = {"command": "python", "args": [server_script, "--project", "."]}
4835
4861
  if profile.needs_type_field:
4836
4862
  new_entry["type"] = "stdio"
4837
4863
 
@@ -4865,7 +4891,10 @@ def cmd_install_mcp(args):
4865
4891
  try:
4866
4892
  if profile.config_format == "toml":
4867
4893
  # Codex uses TOML: [mcp_servers.c3] with command/args
4868
- toml_entries = {"command": sys.executable, "args": [server_script, "--project", str(target)]}
4894
+ if c3_mcp_exe:
4895
+ toml_entries = {"command": c3_mcp_exe, "args": ["--project", str(target)]}
4896
+ else:
4897
+ toml_entries = {"command": sys.executable, "args": [server_script, "--project", str(target)]}
4869
4898
  if profile.name == "codex":
4870
4899
  # Codex supports explicit enable/disable per server.
4871
4900
  toml_entries["enabled"] = True
@@ -4895,8 +4924,8 @@ def cmd_install_mcp(args):
4895
4924
 
4896
4925
  print(f"Wrote {mcp_config_path}")
4897
4926
  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)
4927
+ _ensure_project_session_configs(target, server_script, primary_profile=profile.name, c3_mcp_exe=c3_mcp_exe)
4928
+ _ensure_global_session_fallbacks(server_script, c3_mcp_exe=c3_mcp_exe)
4900
4929
 
4901
4930
  # ── Persist IDE choice to .c3/config.json ──
4902
4931
  c3_config_dir = target / ".c3"
@@ -6342,6 +6371,123 @@ def _run_swe_bench_lite(args, project_path):
6342
6371
  pass
6343
6372
 
6344
6373
 
6374
+ def _version_tuple(v: str) -> tuple:
6375
+ """Best-effort numeric version tuple for comparisons ('2.36.0' -> (2, 36, 0))."""
6376
+ parts = []
6377
+ for chunk in str(v or "").split("."):
6378
+ digits = ""
6379
+ for ch in chunk:
6380
+ if ch.isdigit():
6381
+ digits += ch
6382
+ else:
6383
+ break
6384
+ parts.append(int(digits) if digits else 0)
6385
+ return tuple(parts) or (0,)
6386
+
6387
+
6388
+ def _latest_pypi_version(package: str = "code-context-control", timeout: float = 5.0) -> str | None:
6389
+ """Best-effort latest release of `package` on PyPI; None if unreachable."""
6390
+ import urllib.request
6391
+ try:
6392
+ url = f"https://pypi.org/pypi/{package}/json"
6393
+ with urllib.request.urlopen(url, timeout=timeout) as resp:
6394
+ data = json.loads(resp.read().decode("utf-8", "replace"))
6395
+ return (data.get("info") or {}).get("version")
6396
+ except Exception:
6397
+ return None
6398
+
6399
+
6400
+ def _installed_distribution(package: str = "code-context-control"):
6401
+ """Return the installed Distribution for `package`, or None when running from source."""
6402
+ try:
6403
+ from importlib import metadata
6404
+ return metadata.distribution(package)
6405
+ except Exception:
6406
+ return None
6407
+
6408
+
6409
+ def _is_editable_install(package: str = "code-context-control") -> bool:
6410
+ """True when `package` is pip-installed in editable/development mode."""
6411
+ dist = _installed_distribution(package)
6412
+ if dist is None:
6413
+ return False
6414
+ try:
6415
+ text = dist.read_text("direct_url.json")
6416
+ if text:
6417
+ return bool(json.loads(text).get("dir_info", {}).get("editable"))
6418
+ except Exception:
6419
+ pass
6420
+ return False
6421
+
6422
+
6423
+ def cmd_upgrade(args):
6424
+ """Upgrade C3 to the latest PyPI release (or just check with --check)."""
6425
+ current = __version__
6426
+ latest = _latest_pypi_version()
6427
+ if latest is None:
6428
+ print(" Could not reach PyPI to check for updates (offline?).")
6429
+ elif _version_tuple(latest) <= _version_tuple(current):
6430
+ print(f" C3 is up to date (v{current}).")
6431
+ return
6432
+ else:
6433
+ print(f" Update available: v{current} -> v{latest}")
6434
+
6435
+ if getattr(args, "check", False):
6436
+ return
6437
+
6438
+ if _installed_distribution() is None:
6439
+ print(" C3 is running from a source checkout (not pip-installed).")
6440
+ print(" Update with: git pull")
6441
+ return
6442
+ if _is_editable_install():
6443
+ print(" C3 is installed in editable/development mode (pip install -e .).")
6444
+ print(" Update with: git pull")
6445
+ return
6446
+
6447
+ print(" Upgrading via pip (this may take a minute)...")
6448
+ cmd = [sys.executable, "-m", "pip", "install", "-U", "code-context-control[tui]"]
6449
+ try:
6450
+ result = subprocess.run(cmd, capture_output=True, text=True)
6451
+ except Exception as e:
6452
+ print(f" Upgrade failed to launch pip: {e}")
6453
+ sys.exit(1)
6454
+ if result.returncode != 0:
6455
+ print(" pip upgrade failed:")
6456
+ print((result.stderr or result.stdout or "").strip()[-1000:])
6457
+ sys.exit(1)
6458
+ print(" Upgraded to the latest release. Restart your IDE's MCP server to load it.")
6459
+ print(" In each project, run c3 init . --force to apply any migrations.")
6460
+
6461
+
6462
+ def _launch_tui() -> None:
6463
+ """Launch the interactive TUI — what `c3` with no arguments does.
6464
+
6465
+ Runs tui/main.py as a subprocess so its bare `from screens...` imports resolve
6466
+ (its own directory lands on sys.path[0]); the package root goes on PYTHONPATH for
6467
+ cli/services imports. Falls back to help text when the optional [tui] extra
6468
+ (textual) is not installed.
6469
+ """
6470
+ pkg_root = Path(__file__).resolve().parent.parent
6471
+ tui_main = pkg_root / "tui" / "main.py"
6472
+ try:
6473
+ import textual # noqa: F401
6474
+ except Exception:
6475
+ print("The interactive TUI needs the optional 'textual' dependency.")
6476
+ print(' Install it with: pip install "code-context-control[tui]"')
6477
+ print(" Or run c3 --help to see all commands.")
6478
+ return
6479
+ if not tui_main.exists():
6480
+ print("TUI entry point not found. Run c3 --help for commands.")
6481
+ return
6482
+ env = os.environ.copy()
6483
+ existing_pp = env.get("PYTHONPATH")
6484
+ env["PYTHONPATH"] = str(pkg_root) + (os.pathsep + existing_pp if existing_pp else "")
6485
+ try:
6486
+ subprocess.run([sys.executable, str(tui_main)], env=env)
6487
+ except KeyboardInterrupt:
6488
+ pass
6489
+
6490
+
6345
6491
  def main():
6346
6492
  try:
6347
6493
  from services import error_reporting
@@ -6353,7 +6499,8 @@ def main():
6353
6499
  args = parser.parse_args()
6354
6500
 
6355
6501
  if not args.command:
6356
- parser.print_help()
6502
+ # Bare `c3` launches the interactive TUI (replaces the old c3.bat wrapper).
6503
+ _launch_tui()
6357
6504
  return
6358
6505
 
6359
6506
  commands = {
@@ -6382,6 +6529,7 @@ def main():
6382
6529
  "hub": cmd_hub,
6383
6530
  "bitbucket": cmd_bitbucket,
6384
6531
  "oracle": cmd_oracle,
6532
+ "upgrade": cmd_upgrade,
6385
6533
  }
6386
6534
 
6387
6535
  cmd_func = commands.get(args.command)
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