code-context-control 2.28.3__py3-none-any.whl → 2.30.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 +177 -3
- cli/commands/parser.py +39 -0
- cli/docs.html +37 -1
- cli/hook_ghost_files.py +17 -1
- cli/hub.html +128 -3
- cli/hub_server.py +23 -0
- cli/mcp_server.py +50 -0
- cli/server.py +365 -0
- cli/tools/_helpers.py +17 -3
- cli/tools/bitbucket.py +654 -0
- cli/ui/app.js +4 -0
- cli/ui/components/bitbucket.js +297 -0
- {code_context_control-2.28.3.dist-info → code_context_control-2.30.0.dist-info}/METADATA +37 -9
- {code_context_control-2.28.3.dist-info → code_context_control-2.30.0.dist-info}/RECORD +23 -19
- core/config.py +29 -0
- services/bitbucket_client.py +485 -0
- services/bitbucket_credentials.py +215 -0
- services/claude_md.py +1 -0
- services/project_manager.py +244 -0
- {code_context_control-2.28.3.dist-info → code_context_control-2.30.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.28.3.dist-info → code_context_control-2.30.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.28.3.dist-info → code_context_control-2.30.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.28.3.dist-info → code_context_control-2.30.0.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
|
@@ -60,7 +60,7 @@ from cli.commands.common import cmd_stats as common_cmd_stats
|
|
|
60
60
|
from cli.commands.common import cmd_ui as common_cmd_ui
|
|
61
61
|
from cli.commands.parser import build_parser
|
|
62
62
|
from core import count_tokens, format_token_count
|
|
63
|
-
from core.config import AGENT_DEFAULTS, DELEGATE_DEFAULTS, PROXY_DEFAULTS, load_delegate_config
|
|
63
|
+
from core.config import AGENT_DEFAULTS, BITBUCKET_DEFAULTS, DELEGATE_DEFAULTS, PROXY_DEFAULTS, load_delegate_config
|
|
64
64
|
from core.config import DEFAULTS as HYBRID_DEFAULTS
|
|
65
65
|
from core.ide import PROFILES, detect_ide, get_profile, load_ide_config, normalize_ide_name
|
|
66
66
|
from services.compressor import CodeCompressor
|
|
@@ -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.30.0"
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _command_deps() -> CommandDeps:
|
|
@@ -164,6 +164,7 @@ def _build_init_config(project_path: str) -> dict:
|
|
|
164
164
|
"proxy": deepcopy(PROXY_DEFAULTS),
|
|
165
165
|
"delegate": deepcopy(DELEGATE_DEFAULTS),
|
|
166
166
|
"agents": deepcopy(AGENT_DEFAULTS),
|
|
167
|
+
"bitbucket": deepcopy(BITBUCKET_DEFAULTS),
|
|
167
168
|
}
|
|
168
169
|
merged = _deep_merge_dict(defaults, existing if isinstance(existing, dict) else {})
|
|
169
170
|
# Always persist current path/version on init/update.
|
|
@@ -237,7 +238,7 @@ _C3_MCP_ALLOW = [
|
|
|
237
238
|
"mcp__c3__c3_session", "mcp__c3__c3_status", "mcp__c3__c3_filter",
|
|
238
239
|
"mcp__c3__c3_memory", "mcp__c3__c3_validate", "mcp__c3__c3_edit",
|
|
239
240
|
"mcp__c3__c3_agent", "mcp__c3__c3_delegate", "mcp__c3__c3_edits",
|
|
240
|
-
"mcp__c3__c3_impact", "mcp__c3__c3_shell",
|
|
241
|
+
"mcp__c3__c3_impact", "mcp__c3__c3_shell", "mcp__c3__c3_bitbucket",
|
|
241
242
|
]
|
|
242
243
|
|
|
243
244
|
# Obsolete MCP tool names from earlier C3 versions. `c3 permissions clean`
|
|
@@ -563,6 +564,25 @@ def _check_c3_health(project_path: str) -> dict:
|
|
|
563
564
|
facts_dir = c3_dir / "facts"
|
|
564
565
|
info["facts"] = len(list(facts_dir.glob("*.json"))) if facts_dir.exists() else 0
|
|
565
566
|
|
|
567
|
+
# Bitbucket integration (v2.30.0+) — informational
|
|
568
|
+
bb_section = config.get("bitbucket") if isinstance(config, dict) else None
|
|
569
|
+
if isinstance(bb_section, dict):
|
|
570
|
+
active = bb_section.get("active") or {}
|
|
571
|
+
accounts = bb_section.get("accounts") or []
|
|
572
|
+
info["bitbucket_accounts"] = len(accounts) if isinstance(accounts, list) else 0
|
|
573
|
+
info["bitbucket_active_account"] = (
|
|
574
|
+
f"{active.get('username', '')}@{active.get('base_url', '')}"
|
|
575
|
+
if active.get("base_url") and active.get("username") else ""
|
|
576
|
+
)
|
|
577
|
+
info["bitbucket_default_repo"] = (
|
|
578
|
+
f"{bb_section.get('default_project', '')}/{bb_section.get('default_repo', '')}"
|
|
579
|
+
if bb_section.get("default_project") and bb_section.get("default_repo") else ""
|
|
580
|
+
)
|
|
581
|
+
else:
|
|
582
|
+
info["bitbucket_accounts"] = 0
|
|
583
|
+
info["bitbucket_active_account"] = ""
|
|
584
|
+
info["bitbucket_default_repo"] = ""
|
|
585
|
+
|
|
566
586
|
info["issues"] = issues
|
|
567
587
|
info["healthy"] = len(issues) == 0
|
|
568
588
|
return info
|
|
@@ -827,6 +847,11 @@ def cmd_init(args):
|
|
|
827
847
|
if not c3_dir.exists() or not (c3_dir / "config.json").exists():
|
|
828
848
|
print_header(f"Initializing C3 for: {project_path}")
|
|
829
849
|
_do_init(project_path, ide_name=requested_ide)
|
|
850
|
+
try:
|
|
851
|
+
from services.project_manager import ProjectManager
|
|
852
|
+
ProjectManager().add_project(project_path)
|
|
853
|
+
except Exception as _e:
|
|
854
|
+
print(f" [warn] Could not register project with hub: {_e}")
|
|
830
855
|
if getattr(args, "force", False):
|
|
831
856
|
if git_requested:
|
|
832
857
|
_init_local_git_repo(project_path)
|
|
@@ -941,6 +966,15 @@ def cmd_init(args):
|
|
|
941
966
|
except Exception:
|
|
942
967
|
print(" Gemini: unknown")
|
|
943
968
|
|
|
969
|
+
# Bitbucket integration (v2.30.0+)
|
|
970
|
+
bb_n = int(health.get("bitbucket_accounts") or 0)
|
|
971
|
+
if bb_n:
|
|
972
|
+
active = health.get("bitbucket_active_account") or "(no active)"
|
|
973
|
+
repo = health.get("bitbucket_default_repo") or "(no default repo)"
|
|
974
|
+
print(f" Bitbkt: {bb_n} account(s), active={active}, repo={repo}")
|
|
975
|
+
else:
|
|
976
|
+
print(" Bitbkt: not configured (run 'c3 bitbucket login --url <URL>')")
|
|
977
|
+
|
|
944
978
|
if health["healthy"]:
|
|
945
979
|
print("\n Status: healthy — no issues detected.")
|
|
946
980
|
else:
|
|
@@ -4457,6 +4491,7 @@ back to native tools as the task progresses.
|
|
|
4457
4491
|
- **Shell**: `c3_shell(cmd, timeout=60)` — structured shell exec (tests/git/build). Auto-filters output, logs git mutations to the ledger. Native Bash for interactive/TTY only
|
|
4458
4492
|
- **Memory**: `c3_memory(action='recall')` — full recall. `index` + `fetch` for token-efficient two-step retrieval
|
|
4459
4493
|
- **Delegate**: `c3_delegate(task, backend='ollama|codex|gemini|claude|auto')` — offload to other models
|
|
4494
|
+
- **Bitbucket** (v2.30.0+, when `c3 bitbucket login` has run): `c3_bitbucket(action='list_prs|get_pr|merge_pr|...')` — self-hosted Bitbucket Data Center / Server. Token in OS keyring; mutating actions auto-log to the edit ledger.
|
|
4460
4495
|
|
|
4461
4496
|
## Self-Check
|
|
4462
4497
|
If you haven't called a c3_* tool in several turns during active development, re-engage
|
|
@@ -5383,6 +5418,144 @@ def cmd_hub(args):
|
|
|
5383
5418
|
run_hub(port=port, open_browser=open_browser, silent=silent, quiet=quiet)
|
|
5384
5419
|
|
|
5385
5420
|
|
|
5421
|
+
def cmd_bitbucket(args):
|
|
5422
|
+
"""Bitbucket Data Center / Server credential + workspace management."""
|
|
5423
|
+
sub = getattr(args, "bitbucket_cmd", None)
|
|
5424
|
+
if not sub:
|
|
5425
|
+
print("Usage: c3 bitbucket {login,logout,status,use,set-default} [args]")
|
|
5426
|
+
return
|
|
5427
|
+
|
|
5428
|
+
project_path = getattr(args, "project_path", ".") or "."
|
|
5429
|
+
|
|
5430
|
+
if sub == "login":
|
|
5431
|
+
_bb_cmd_login(args, project_path)
|
|
5432
|
+
elif sub == "logout":
|
|
5433
|
+
_bb_cmd_logout(args, project_path)
|
|
5434
|
+
elif sub == "status":
|
|
5435
|
+
_bb_cmd_status(args, project_path)
|
|
5436
|
+
elif sub == "use":
|
|
5437
|
+
_bb_cmd_use(args, project_path)
|
|
5438
|
+
elif sub == "set-default":
|
|
5439
|
+
_bb_cmd_set_default(args, project_path)
|
|
5440
|
+
else:
|
|
5441
|
+
print(f"Unknown bitbucket subcommand: {sub}")
|
|
5442
|
+
|
|
5443
|
+
|
|
5444
|
+
def _bb_cmd_login(args, project_path: str) -> None:
|
|
5445
|
+
import getpass
|
|
5446
|
+
|
|
5447
|
+
from services import bitbucket_credentials as bb_creds
|
|
5448
|
+
from services.bitbucket_client import BitbucketDataCenterClient, BitbucketError
|
|
5449
|
+
|
|
5450
|
+
base_url = (args.url or "").rstrip("/")
|
|
5451
|
+
username = args.username or input(f"Username for {base_url}: ").strip()
|
|
5452
|
+
if not username:
|
|
5453
|
+
print("Login cancelled — username required.")
|
|
5454
|
+
return
|
|
5455
|
+
token = args.token or getpass.getpass(f"Personal Access Token for {username}: ").strip()
|
|
5456
|
+
if not token:
|
|
5457
|
+
print("Login cancelled — token required.")
|
|
5458
|
+
return
|
|
5459
|
+
|
|
5460
|
+
try:
|
|
5461
|
+
bb_creds.save_credentials(
|
|
5462
|
+
base_url, username, token,
|
|
5463
|
+
project_path=project_path,
|
|
5464
|
+
set_active=not getattr(args, "no_set_active", False),
|
|
5465
|
+
)
|
|
5466
|
+
except bb_creds.BitbucketCredentialError as exc:
|
|
5467
|
+
print(f"[error] {exc}")
|
|
5468
|
+
return
|
|
5469
|
+
|
|
5470
|
+
if getattr(args, "insecure", False):
|
|
5471
|
+
bb_creds.set_verify_tls(False, project_path=project_path)
|
|
5472
|
+
|
|
5473
|
+
print(f"[OK] Stored credentials for {username}@{base_url}")
|
|
5474
|
+
|
|
5475
|
+
# Connection probe — non-fatal if it fails (token might be valid but
|
|
5476
|
+
# network blocked at this moment).
|
|
5477
|
+
try:
|
|
5478
|
+
client = BitbucketDataCenterClient(
|
|
5479
|
+
base_url=base_url, token=token,
|
|
5480
|
+
verify_tls=not getattr(args, "insecure", False),
|
|
5481
|
+
)
|
|
5482
|
+
props = client.application_properties()
|
|
5483
|
+
version = props.get("version", "?")
|
|
5484
|
+
user = client.whoami()
|
|
5485
|
+
print(f" Server: {version} ({base_url})")
|
|
5486
|
+
print(f" Auth as: {user.get('displayName', username)} <{user.get('emailAddress', '?')}>")
|
|
5487
|
+
except BitbucketError as exc:
|
|
5488
|
+
print(f"[warn] Connection probe failed: {exc}")
|
|
5489
|
+
print(" Token saved anyway — re-test with `c3 bitbucket status`.")
|
|
5490
|
+
|
|
5491
|
+
|
|
5492
|
+
def _bb_cmd_logout(args, project_path: str) -> None:
|
|
5493
|
+
from services import bitbucket_credentials as bb_creds
|
|
5494
|
+
|
|
5495
|
+
base_url = (getattr(args, "url", "") or "").rstrip("/")
|
|
5496
|
+
username = getattr(args, "username", "") or ""
|
|
5497
|
+
if not base_url or not username:
|
|
5498
|
+
active = bb_creds.get_active_account(project_path)
|
|
5499
|
+
base_url = base_url or active.get("base_url", "")
|
|
5500
|
+
username = username or active.get("username", "")
|
|
5501
|
+
if not base_url or not username:
|
|
5502
|
+
print("[error] No account specified and no active account configured.")
|
|
5503
|
+
return
|
|
5504
|
+
removed = bb_creds.delete_credentials(base_url, username, project_path=project_path)
|
|
5505
|
+
if removed:
|
|
5506
|
+
print(f"[OK] Removed {username}@{base_url}")
|
|
5507
|
+
else:
|
|
5508
|
+
print(f"[warn] Nothing to remove for {username}@{base_url}")
|
|
5509
|
+
|
|
5510
|
+
|
|
5511
|
+
def _bb_cmd_status(args, project_path: str) -> None:
|
|
5512
|
+
from core.config import load_bitbucket_config
|
|
5513
|
+
from services import bitbucket_credentials as bb_creds
|
|
5514
|
+
from services.bitbucket_client import BitbucketDataCenterClient, BitbucketError
|
|
5515
|
+
|
|
5516
|
+
cfg = load_bitbucket_config(project_path)
|
|
5517
|
+
active = cfg.get("active") or {}
|
|
5518
|
+
accounts = cfg.get("accounts") or []
|
|
5519
|
+
|
|
5520
|
+
print("[bitbucket:status]")
|
|
5521
|
+
print(f" Active : {active.get('username') or '-'}@{active.get('base_url') or '-'}")
|
|
5522
|
+
print(f" Defaults: project={cfg.get('default_project') or '-'} repo={cfg.get('default_repo') or '-'}")
|
|
5523
|
+
print(f" Verify TLS: {cfg.get('verify_tls', True)}")
|
|
5524
|
+
print(f" Accounts ({len(accounts)}):")
|
|
5525
|
+
for a in accounts:
|
|
5526
|
+
marker = "*" if a == active else " "
|
|
5527
|
+
print(f" {marker} {a.get('username','?')}@{a.get('base_url','?')}")
|
|
5528
|
+
|
|
5529
|
+
if not active.get("base_url") or not active.get("username"):
|
|
5530
|
+
print(" Connection: (no active account)")
|
|
5531
|
+
return
|
|
5532
|
+
token = bb_creds.load_token(active["base_url"], active["username"])
|
|
5533
|
+
if not token:
|
|
5534
|
+
print(" Connection: FAIL — no token in keyring")
|
|
5535
|
+
return
|
|
5536
|
+
try:
|
|
5537
|
+
client = BitbucketDataCenterClient(
|
|
5538
|
+
base_url=active["base_url"], token=token,
|
|
5539
|
+
verify_tls=bool(cfg.get("verify_tls", True)),
|
|
5540
|
+
)
|
|
5541
|
+
props = client.application_properties()
|
|
5542
|
+
print(f" Connection: OK (version {props.get('version','?')})")
|
|
5543
|
+
except BitbucketError as exc:
|
|
5544
|
+
print(f" Connection: FAIL — {exc}")
|
|
5545
|
+
|
|
5546
|
+
|
|
5547
|
+
def _bb_cmd_use(args, project_path: str) -> None:
|
|
5548
|
+
from services import bitbucket_credentials as bb_creds
|
|
5549
|
+
bb_creds.set_active_account(args.url, args.username, project_path=project_path)
|
|
5550
|
+
print(f"[OK] Active account: {args.username}@{args.url.rstrip('/')}")
|
|
5551
|
+
|
|
5552
|
+
|
|
5553
|
+
def _bb_cmd_set_default(args, project_path: str) -> None:
|
|
5554
|
+
from services import bitbucket_credentials as bb_creds
|
|
5555
|
+
bb_creds.set_default_repo(args.project, args.repo, project_path=project_path)
|
|
5556
|
+
print(f"[OK] Default repo: {args.project}/{args.repo}")
|
|
5557
|
+
|
|
5558
|
+
|
|
5386
5559
|
def cmd_projects(args):
|
|
5387
5560
|
"""Manage the global C3 project registry."""
|
|
5388
5561
|
from services.project_manager import ProjectManager
|
|
@@ -6134,6 +6307,7 @@ def main():
|
|
|
6134
6307
|
"ui": cmd_ui,
|
|
6135
6308
|
"projects": cmd_projects,
|
|
6136
6309
|
"hub": cmd_hub,
|
|
6310
|
+
"bitbucket": cmd_bitbucket,
|
|
6137
6311
|
}
|
|
6138
6312
|
|
|
6139
6313
|
cmd_func = commands.get(args.command)
|
cli/commands/parser.py
CHANGED
|
@@ -283,4 +283,43 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
283
283
|
p_bext.add_argument("--dry-run", action="store_true",
|
|
284
284
|
help="Validate setup (CLIs, datasets) without running the agent")
|
|
285
285
|
|
|
286
|
+
# ── Bitbucket Data Center / Server (v2.30.0) ─────────────────────────
|
|
287
|
+
p_bitbucket = subparsers.add_parser(
|
|
288
|
+
"bitbucket",
|
|
289
|
+
help="Bitbucket Data Center / Server credential + workspace management",
|
|
290
|
+
)
|
|
291
|
+
bb_subs = p_bitbucket.add_subparsers(dest="bitbucket_cmd")
|
|
292
|
+
|
|
293
|
+
bb_login = bb_subs.add_parser(
|
|
294
|
+
"login",
|
|
295
|
+
help="Authenticate with a Bitbucket Data Center server (interactive PAT prompt)",
|
|
296
|
+
)
|
|
297
|
+
bb_login.add_argument("--url", required=True, help="Bitbucket server base URL (e.g. https://bitbucket.example.com)")
|
|
298
|
+
bb_login.add_argument("--username", help="Bitbucket username (prompted if omitted)")
|
|
299
|
+
bb_login.add_argument("--token", help="Personal Access Token (prompted via getpass if omitted — preferred)")
|
|
300
|
+
bb_login.add_argument("--no-set-active", action="store_true", help="Do not switch the active account to this one")
|
|
301
|
+
bb_login.add_argument("--insecure", action="store_true", help="Disable TLS verification (self-signed certs)")
|
|
302
|
+
bb_login.add_argument("project_path", nargs="?", default=".")
|
|
303
|
+
|
|
304
|
+
bb_logout = bb_subs.add_parser("logout", help="Remove a Bitbucket account from keyring + config")
|
|
305
|
+
bb_logout.add_argument("--url", help="Bitbucket server base URL (defaults to active account)")
|
|
306
|
+
bb_logout.add_argument("--username", help="Username to log out (defaults to active account)")
|
|
307
|
+
bb_logout.add_argument("project_path", nargs="?", default=".")
|
|
308
|
+
|
|
309
|
+
bb_status = bb_subs.add_parser("status", help="Show configured Bitbucket accounts and connectivity")
|
|
310
|
+
bb_status.add_argument("project_path", nargs="?", default=".")
|
|
311
|
+
|
|
312
|
+
bb_use = bb_subs.add_parser("use", help="Switch the active Bitbucket account")
|
|
313
|
+
bb_use.add_argument("--url", required=True)
|
|
314
|
+
bb_use.add_argument("--username", required=True)
|
|
315
|
+
bb_use.add_argument("project_path", nargs="?", default=".")
|
|
316
|
+
|
|
317
|
+
bb_default = bb_subs.add_parser(
|
|
318
|
+
"set-default",
|
|
319
|
+
help="Set the default project key + repo slug for this C3 project",
|
|
320
|
+
)
|
|
321
|
+
bb_default.add_argument("--project", required=True, help="Bitbucket project key (e.g. PROJ)")
|
|
322
|
+
bb_default.add_argument("--repo", required=True, help="Repository slug")
|
|
323
|
+
bb_default.add_argument("project_path", nargs="?", default=".")
|
|
324
|
+
|
|
286
325
|
return parser
|
cli/docs.html
CHANGED
|
@@ -751,6 +751,7 @@
|
|
|
751
751
|
|
|
752
752
|
<div class="sidebar-section">MCP Server</div>
|
|
753
753
|
<a href="#mcp-tools">MCP Tools</a>
|
|
754
|
+
<a href="#bitbucket-tool">Bitbucket Integration</a>
|
|
754
755
|
<a href="#context-tools">Context Manager</a>
|
|
755
756
|
<a href="#claudemd-tools">Instructions Tools</a>
|
|
756
757
|
<a href="#agent-tools">Background Agents</a>
|
|
@@ -1147,7 +1148,7 @@ python cli/c3.py install-mcp . gemini</code></pre>
|
|
|
1147
1148
|
|
|
1148
1149
|
<!-- ─── MCP Tools ───────────────────── -->
|
|
1149
1150
|
<h2 id="mcp-tools">MCP Tools Reference</h2>
|
|
1150
|
-
<p>C3 exposes
|
|
1151
|
+
<p>C3 exposes 15 MCP tools. All core tools work without Ollama; delegate requires it. The Bitbucket integration is optional and activated via <code>c3 bitbucket login</code>.</p>
|
|
1151
1152
|
|
|
1152
1153
|
<h3>Discovery & Compression</h3>
|
|
1153
1154
|
<table>
|
|
@@ -1298,6 +1299,41 @@ python cli/c3.py install-mcp . gemini</code></pre>
|
|
|
1298
1299
|
View the full timeline at <code>/edits</code>.
|
|
1299
1300
|
</div>
|
|
1300
1301
|
|
|
1302
|
+
<!-- Bitbucket Integration (v2.30.0) -->
|
|
1303
|
+
<h2 id="bitbucket-tool">Bitbucket Data Center / Server</h2>
|
|
1304
|
+
<p>C3 v2.30.0 ships a dedicated <code>c3_bitbucket</code> tool for self-hosted enterprise Bitbucket. It supports read browsing (PRs, branches, builds, activity), PR write actions (create / comment / approve / decline / merge), branch writes, and repository administration (settings, webhooks, permissions). PATs are stored in the OS keyring — never in <code>.c3/config.json</code>.</p>
|
|
1305
|
+
|
|
1306
|
+
<table>
|
|
1307
|
+
<tr><th>Surface</th><th>What it provides</th></tr>
|
|
1308
|
+
<tr><td><code>c3_bitbucket</code> MCP tool</td><td>Action-dispatched Bitbucket calls from Claude Code (27 actions across read / PR-write / branch / admin)</td></tr>
|
|
1309
|
+
<tr><td><code>c3 bitbucket {login,logout,status,use,set-default}</code></td><td>CLI for credential and default-repo management. Login uses <code>getpass</code> for masked PAT entry.</td></tr>
|
|
1310
|
+
<tr><td>Hub UI Bitbucket tab</td><td>Per-project visual browser: Overview, Pull Requests, Branches, Activity, Admin</td></tr>
|
|
1311
|
+
<tr><td><code>/api/bitbucket/*</code> REST endpoints</td><td>Project session-server endpoints proxying to <code>BitbucketDataCenterClient</code></td></tr>
|
|
1312
|
+
</table>
|
|
1313
|
+
|
|
1314
|
+
<div class="tip">
|
|
1315
|
+
<div class="tip-title">Where the token lives</div>
|
|
1316
|
+
Tokens are stored exclusively in the <strong>OS keyring</strong> (Windows Credential Manager / macOS Keychain / Linux Secret Service) under service <code>c3-bitbucket</code>. Only a non-secret index of <code>(base_url, username)</code> pairs and the active-account pointer is written to <code>.c3/config.json</code>. <code>.c3/</code> is gitignored.
|
|
1317
|
+
</div>
|
|
1318
|
+
|
|
1319
|
+
<p><strong>Quick start</strong></p>
|
|
1320
|
+
<pre><code>c3 bitbucket login --url https://bitbucket.example.com
|
|
1321
|
+
c3 bitbucket set-default --project PROJ --repo my-service
|
|
1322
|
+
c3 bitbucket status</code></pre>
|
|
1323
|
+
|
|
1324
|
+
<p>From Claude Code:</p>
|
|
1325
|
+
<pre><code>c3_bitbucket(action='list_prs', state='OPEN')
|
|
1326
|
+
c3_bitbucket(action='get_pr', pr_id=42)
|
|
1327
|
+
c3_bitbucket(action='approve_pr', pr_id=42)
|
|
1328
|
+
c3_bitbucket(action='merge_pr', pr_id=42) # version auto-fetched</code></pre>
|
|
1329
|
+
|
|
1330
|
+
<div class="tip">
|
|
1331
|
+
<div class="tip-title">Audit trail</div>
|
|
1332
|
+
PR merges, branch deletes, webhook writes, and other mutating actions are appended to the C3 edit ledger under the virtual path <code>bitbucket://<project>/<repo></code> so the local audit trail covers platform-side state changes too. Token text is stripped from logged kwargs before persisting.
|
|
1333
|
+
</div>
|
|
1334
|
+
|
|
1335
|
+
<p>Full reference (action tables, security model, configuration shape, examples, troubleshooting): <a href="/guide/bitbucket.html">Bitbucket integration guide</a>. Currently <strong>Bitbucket Cloud</strong> (<code>bitbucket.org</code>) is not supported — only Data Center / Server.</p>
|
|
1336
|
+
|
|
1301
1337
|
<h3 id="agent-tools">Background Agents</h3>
|
|
1302
1338
|
<p>C3 runs 9 autonomous daemon threads that perform periodic analysis and surface findings via a notification
|
|
1303
1339
|
queue. Notifications are automatically prepended to the next MCP tool response so Claude sees them naturally.
|
cli/hook_ghost_files.py
CHANGED
|
@@ -35,6 +35,16 @@ _PYTHON_TYPE_NAMES = {
|
|
|
35
35
|
"Awaitable", "Coroutine", "AsyncIterator", "AsyncGenerator",
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
# Common heredoc / here-string end-markers that leak as filenames when
|
|
39
|
+
# Bash or PowerShell misparses a `<<EOF` / `@'...'@` block. Match is
|
|
40
|
+
# size-agnostic: a non-empty `EOF` file is still a ghost.
|
|
41
|
+
_HEREDOC_MARKERS = {
|
|
42
|
+
"EOF", "EOM", "EOL", "END", "STOP", "DONE",
|
|
43
|
+
"MARK", "MARKER", "DELIM", "DELIMITER",
|
|
44
|
+
"INPUT", "OUTPUT", "DATA", "BLOCK", "HEREDOC",
|
|
45
|
+
"'@", "@'", # PowerShell here-string fragments
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
# Max file size to consider a ghost (bytes). Genuine files are usually larger.
|
|
39
49
|
_MAX_GHOST_SIZE = 4096
|
|
40
50
|
|
|
@@ -108,6 +118,10 @@ def _is_ghost_file(path: Path) -> bool:
|
|
|
108
118
|
if name in _PYTHON_TYPE_NAMES:
|
|
109
119
|
return True
|
|
110
120
|
|
|
121
|
+
# HEREDOC end-marker leaked as filename (e.g., "EOF", "'@", "END")
|
|
122
|
+
if name in _HEREDOC_MARKERS and not real_suffix:
|
|
123
|
+
return True
|
|
124
|
+
|
|
111
125
|
# Partial type annotation (e.g., "tuple[float", "dict[str")
|
|
112
126
|
if "[" in name and not real_suffix:
|
|
113
127
|
return True
|
|
@@ -154,7 +168,9 @@ def scan_ghost_files(project_root: Path) -> list[dict]:
|
|
|
154
168
|
name = entry.name
|
|
155
169
|
|
|
156
170
|
# Determine reason
|
|
157
|
-
if name in
|
|
171
|
+
if name in _HEREDOC_MARKERS:
|
|
172
|
+
reason = "heredoc end-marker leak"
|
|
173
|
+
elif name in _PYTHON_TYPE_NAMES:
|
|
158
174
|
reason = "Python type name"
|
|
159
175
|
elif "[" in name:
|
|
160
176
|
reason = "partial type annotation"
|
cli/hub.html
CHANGED
|
@@ -995,6 +995,49 @@
|
|
|
995
995
|
</div>
|
|
996
996
|
</div>
|
|
997
997
|
|
|
998
|
+
<!-- Merge Modal -->
|
|
999
|
+
<div class="modal-backdrop hidden" id="merge-modal">
|
|
1000
|
+
<div class="modal">
|
|
1001
|
+
<div class="modal-header">
|
|
1002
|
+
<h3>Merge Projects</h3>
|
|
1003
|
+
<button class="modal-close" onclick="closeModal('merge-modal')">×</button>
|
|
1004
|
+
</div>
|
|
1005
|
+
<div class="modal-body">
|
|
1006
|
+
<div class="form-group">
|
|
1007
|
+
<span class="form-label">Merging from</span>
|
|
1008
|
+
<div class="form-path" id="merge-modal-source-path"></div>
|
|
1009
|
+
</div>
|
|
1010
|
+
<div class="form-group">
|
|
1011
|
+
<label class="form-label" for="merge-target-select">Merge into</label>
|
|
1012
|
+
<select class="form-input" id="merge-target-select"></select>
|
|
1013
|
+
</div>
|
|
1014
|
+
<div class="form-group">
|
|
1015
|
+
<span class="form-label">After merge</span>
|
|
1016
|
+
<div style="display:flex;flex-direction:column;gap:.4rem;margin-top:.3rem;">
|
|
1017
|
+
<label style="display:flex;align-items:center;gap:.5rem;font-weight:normal;cursor:pointer;">
|
|
1018
|
+
<input type="radio" name="merge-cleanup" value="keep" checked onchange="updateMergeWarning()">
|
|
1019
|
+
<span>Keep source project intact</span>
|
|
1020
|
+
</label>
|
|
1021
|
+
<label style="display:flex;align-items:center;gap:.5rem;font-weight:normal;cursor:pointer;">
|
|
1022
|
+
<input type="radio" name="merge-cleanup" value="clear" onchange="updateMergeWarning()">
|
|
1023
|
+
<span>Clear source (wipe .c3/, MCP configs & instruction docs)</span>
|
|
1024
|
+
</label>
|
|
1025
|
+
</div>
|
|
1026
|
+
</div>
|
|
1027
|
+
<div id="merge-warning" style="display:none;border:1px solid #b13c3c;background:rgba(177,60,60,.1);padding:.6rem .75rem;border-radius:4px;margin-top:.5rem;color:var(--text);font-size:.85rem;">
|
|
1028
|
+
⚠ The source project's <code>.c3/</code> directory, MCP configs (<code>.mcp.json</code>, <code>.claude/settings.local.json</code>, <code>.codex/</code>) and instruction docs (CLAUDE.md, GEMINI.md, AGENTS.md) will be deleted. The source directory itself stays in place.
|
|
1029
|
+
</div>
|
|
1030
|
+
<p style="font-size:.8rem;color:var(--text-muted);margin-top:.5rem;">
|
|
1031
|
+
Combines memory facts, conversation sessions, and edit-ledger entries into the target. File-memory and indices are not merged.
|
|
1032
|
+
</p>
|
|
1033
|
+
</div>
|
|
1034
|
+
<div class="modal-footer">
|
|
1035
|
+
<button class="btn btn-ghost" onclick="closeModal('merge-modal')">Cancel</button>
|
|
1036
|
+
<button class="btn btn-primary" id="merge-save-btn" onclick="saveMerge()">Merge</button>
|
|
1037
|
+
</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
|
|
998
1041
|
<!-- Settings Modal -->
|
|
999
1042
|
<div class="modal-backdrop hidden" id="settings-modal">
|
|
1000
1043
|
<div class="modal">
|
|
@@ -1186,6 +1229,8 @@ let sessionPollTimer = null;
|
|
|
1186
1229
|
let modalPath = null;
|
|
1187
1230
|
let editPath = null;
|
|
1188
1231
|
let transferPath = null;
|
|
1232
|
+
let mergeSourcePath = null;
|
|
1233
|
+
let mergeSourceName = '';
|
|
1189
1234
|
let idePath = null;
|
|
1190
1235
|
let ideSelected = null;
|
|
1191
1236
|
let mcpModalPath = null;
|
|
@@ -1261,7 +1306,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
1261
1306
|
);
|
|
1262
1307
|
document.addEventListener('keydown', e => {
|
|
1263
1308
|
if (e.key === 'Escape') {
|
|
1264
|
-
['mcp-modal','edit-modal','transfer-modal','settings-modal','update-modal','batch-update-modal','ide-modal','c3-setup-modal'].forEach(id => {
|
|
1309
|
+
['mcp-modal','edit-modal','transfer-modal','merge-modal','settings-modal','update-modal','batch-update-modal','ide-modal','c3-setup-modal'].forEach(id => {
|
|
1265
1310
|
if (!document.getElementById(id).classList.contains('hidden')) closeModal(id);
|
|
1266
1311
|
});
|
|
1267
1312
|
}
|
|
@@ -1712,6 +1757,7 @@ function projectCard(p) {
|
|
|
1712
1757
|
const editBtn = `<button class="btn btn-xs btn-ghost" onclick="openEditModal('${jsq(p.path)}','${jsq(p.name)}','${jsq((p.tags||[]).join(','))}')">✏ Edit</button>`;
|
|
1713
1758
|
const ideBtn = `<button class="btn btn-xs btn-primary" onclick="openIdeModal('${jsq(p.path)}','${jsq(p.ide)}')">▶ IDE</button>`;
|
|
1714
1759
|
const transferBtn = !active ? `<button class="btn btn-xs btn-ghost" onclick="openTransferModal('${jsq(p.path)}','${jsq(p.name)}')">Transfer</button>` : '';
|
|
1760
|
+
const mergeBtn = !active ? `<button class="btn btn-xs btn-ghost" onclick="openMergeModal('${jsq(p.path)}','${jsq(p.name)}')" title="Merge memory, sessions and edit ledger into another project">⇄ Merge</button>` : '';
|
|
1715
1761
|
const removeBtn = !active ? `<button class="btn btn-xs btn-danger" onclick="removeProject('${jsq(p.path)}','${jsq(p.name)}')">✕ Remove</button>` : '';
|
|
1716
1762
|
|
|
1717
1763
|
const isLaunching = !active && launchingPaths.has(p.path);
|
|
@@ -1741,7 +1787,7 @@ function projectCard(p) {
|
|
|
1741
1787
|
${startBtn}${startUiBtn}${openBtn}${restartBtn}${stopBtn}${autostartBtn}${folderBtn}${logBtn}${ledgerBtn}${setupBtn}${ideBtn}
|
|
1742
1788
|
</div>
|
|
1743
1789
|
<div class="card-actions-secondary">
|
|
1744
|
-
${clearNotifBtn}${editBtn}${transferBtn}${removeBtn}
|
|
1790
|
+
${clearNotifBtn}${editBtn}${transferBtn}${mergeBtn}${removeBtn}
|
|
1745
1791
|
</div>
|
|
1746
1792
|
</div>
|
|
1747
1793
|
</div>`;
|
|
@@ -2977,6 +3023,84 @@ async function saveTransfer() {
|
|
|
2977
3023
|
}
|
|
2978
3024
|
}
|
|
2979
3025
|
|
|
3026
|
+
// ── Merge Modal ───────────────────────────────────────────────────────────
|
|
3027
|
+
|
|
3028
|
+
function openMergeModal(path, name) {
|
|
3029
|
+
mergeSourcePath = path;
|
|
3030
|
+
mergeSourceName = name;
|
|
3031
|
+
document.getElementById('merge-modal-source-path').textContent = `${name} (${path})`;
|
|
3032
|
+
// Populate target dropdown with idle projects (exclude source and active sessions).
|
|
3033
|
+
const sel = document.getElementById('merge-target-select');
|
|
3034
|
+
sel.innerHTML = '';
|
|
3035
|
+
const candidates = (allProjects || []).filter(p => p.path !== path && !p.session_active && p.port == null);
|
|
3036
|
+
if (!candidates.length) {
|
|
3037
|
+
const opt = document.createElement('option');
|
|
3038
|
+
opt.value = '';
|
|
3039
|
+
opt.textContent = '— No eligible target projects —';
|
|
3040
|
+
sel.appendChild(opt);
|
|
3041
|
+
document.getElementById('merge-save-btn').disabled = true;
|
|
3042
|
+
} else {
|
|
3043
|
+
candidates.forEach(p => {
|
|
3044
|
+
const opt = document.createElement('option');
|
|
3045
|
+
opt.value = p.path;
|
|
3046
|
+
opt.textContent = `${p.name} (${p.path})`;
|
|
3047
|
+
sel.appendChild(opt);
|
|
3048
|
+
});
|
|
3049
|
+
document.getElementById('merge-save-btn').disabled = false;
|
|
3050
|
+
}
|
|
3051
|
+
// Reset cleanup radio + warning
|
|
3052
|
+
document.querySelectorAll('input[name="merge-cleanup"]').forEach(r => { r.checked = (r.value === 'keep'); });
|
|
3053
|
+
updateMergeWarning();
|
|
3054
|
+
openModal('merge-modal');
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
function updateMergeWarning() {
|
|
3058
|
+
const cleanup = (document.querySelector('input[name="merge-cleanup"]:checked') || {}).value || 'keep';
|
|
3059
|
+
document.getElementById('merge-warning').style.display = cleanup === 'clear' ? 'block' : 'none';
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
async function saveMerge() {
|
|
3063
|
+
if (!mergeSourcePath) return;
|
|
3064
|
+
const target = document.getElementById('merge-target-select').value;
|
|
3065
|
+
if (!target) { toast('Pick a target project', 'err'); return; }
|
|
3066
|
+
const cleanup = (document.querySelector('input[name="merge-cleanup"]:checked') || {}).value || 'keep';
|
|
3067
|
+
if (cleanup === 'clear') {
|
|
3068
|
+
const ok = await confirmDialog({
|
|
3069
|
+
title: 'Clear source after merge?',
|
|
3070
|
+
message: `This will permanently delete .c3/, MCP configs and instruction docs from "${mergeSourceName}". The merged data will live on inside the target. Continue?`,
|
|
3071
|
+
confirmText: 'Merge & clear',
|
|
3072
|
+
danger: true,
|
|
3073
|
+
});
|
|
3074
|
+
if (!ok) return;
|
|
3075
|
+
}
|
|
3076
|
+
const btn = document.getElementById('merge-save-btn');
|
|
3077
|
+
btn.disabled = true;
|
|
3078
|
+
try {
|
|
3079
|
+
const r = await fetch('/api/projects/merge', {
|
|
3080
|
+
method: 'POST',
|
|
3081
|
+
headers: {'Content-Type':'application/json'},
|
|
3082
|
+
body: JSON.stringify({source_path: mergeSourcePath, target_path: target, cleanup}),
|
|
3083
|
+
});
|
|
3084
|
+
const d = await r.json();
|
|
3085
|
+
if (d.error) throw new Error(d.error);
|
|
3086
|
+
if (!d.merged) throw new Error('Merge did not complete');
|
|
3087
|
+
const s = d.stats || {};
|
|
3088
|
+
const summary = `Merged ${s.facts||0} facts, ${s.sessions||0} sessions, ${s.ledger_entries||0} ledger entries`;
|
|
3089
|
+
toast(summary + (cleanup === 'clear' ? ' — source cleared' : ''), 'ok');
|
|
3090
|
+
if (d.warnings && d.warnings.length) {
|
|
3091
|
+
console.warn('merge warnings:', d.warnings);
|
|
3092
|
+
}
|
|
3093
|
+
closeModal('merge-modal');
|
|
3094
|
+
delete detailsCache[mergeSourcePath];
|
|
3095
|
+
delete detailsCache[target];
|
|
3096
|
+
loadProjects();
|
|
3097
|
+
} catch(e) {
|
|
3098
|
+
toast('Merge error: ' + e.message, 'err');
|
|
3099
|
+
} finally {
|
|
3100
|
+
btn.disabled = false;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
|
|
2980
3104
|
// ── Settings Modal ─────────────────────────────────────────────────────────
|
|
2981
3105
|
async function openSettings() {
|
|
2982
3106
|
try {
|
|
@@ -3166,11 +3290,12 @@ function closeModal(id) {
|
|
|
3166
3290
|
if (id === 'update-modal') { modalPath = null; }
|
|
3167
3291
|
if (id === 'edit-modal') { editPath = null; }
|
|
3168
3292
|
if (id === 'transfer-modal') { transferPath = null; }
|
|
3293
|
+
if (id === 'merge-modal') { mergeSourcePath = null; mergeSourceName = ''; }
|
|
3169
3294
|
if (id === 'c3-setup-modal') { c3SetupPath = null; }
|
|
3170
3295
|
}
|
|
3171
3296
|
|
|
3172
3297
|
// Close modals on backdrop click
|
|
3173
|
-
['mcp-modal','edit-modal','transfer-modal','settings-modal','update-modal','batch-update-modal','ide-modal','c3-setup-modal'].forEach(id => {
|
|
3298
|
+
['mcp-modal','edit-modal','transfer-modal','merge-modal','settings-modal','update-modal','batch-update-modal','ide-modal','c3-setup-modal'].forEach(id => {
|
|
3174
3299
|
document.getElementById(id).addEventListener('click', e => {
|
|
3175
3300
|
if (e.target === e.currentTarget) closeModal(id);
|
|
3176
3301
|
});
|
cli/hub_server.py
CHANGED
|
@@ -699,6 +699,29 @@ def api_projects_transfer():
|
|
|
699
699
|
return jsonify({"error": str(e)}), 500
|
|
700
700
|
|
|
701
701
|
|
|
702
|
+
@app.route("/api/projects/merge", methods=["POST"])
|
|
703
|
+
def api_projects_merge():
|
|
704
|
+
"""Merge source project's memory/sessions/ledger into target.
|
|
705
|
+
|
|
706
|
+
Body: {source_path, target_path, cleanup: 'keep'|'clear'}
|
|
707
|
+
"""
|
|
708
|
+
data = request.get_json(force=True) or {}
|
|
709
|
+
src = (data.get("source_path") or "").strip()
|
|
710
|
+
tgt = (data.get("target_path") or "").strip()
|
|
711
|
+
cleanup = (data.get("cleanup") or "keep").strip().lower()
|
|
712
|
+
if not src or not tgt:
|
|
713
|
+
return jsonify({"error": "source_path and target_path are required"}), 400
|
|
714
|
+
if cleanup not in ("keep", "clear"):
|
|
715
|
+
return jsonify({"error": "cleanup must be 'keep' or 'clear'"}), 400
|
|
716
|
+
try:
|
|
717
|
+
result = _pm().merge_projects(src, tgt, cleanup=cleanup)
|
|
718
|
+
if result.get("error"):
|
|
719
|
+
return jsonify(result), 400
|
|
720
|
+
return jsonify(result)
|
|
721
|
+
except Exception as e:
|
|
722
|
+
return jsonify({"error": str(e)}), 500
|
|
723
|
+
|
|
724
|
+
|
|
702
725
|
@app.route("/api/projects/details", methods=["POST"])
|
|
703
726
|
def api_projects_details():
|
|
704
727
|
data = request.get_json(force=True) or {}
|
cli/mcp_server.py
CHANGED
|
@@ -649,6 +649,56 @@ async def c3_shell(cmd: str, cwd: str = "", timeout: int = 60,
|
|
|
649
649
|
return await handle_shell(cmd, cwd, timeout, filter_output, log, svc, finalize)
|
|
650
650
|
|
|
651
651
|
|
|
652
|
+
@mcp.tool()
|
|
653
|
+
async def c3_bitbucket(
|
|
654
|
+
action: str,
|
|
655
|
+
project: str = "",
|
|
656
|
+
repo: str = "",
|
|
657
|
+
pr_id: int = 0,
|
|
658
|
+
branch: str = "",
|
|
659
|
+
state: str = "OPEN",
|
|
660
|
+
title: str = "",
|
|
661
|
+
body: str = "",
|
|
662
|
+
from_branch: str = "",
|
|
663
|
+
to_branch: str = "",
|
|
664
|
+
description: str = "",
|
|
665
|
+
reviewers: str = "",
|
|
666
|
+
name: str = "",
|
|
667
|
+
url: str = "",
|
|
668
|
+
events: str = "",
|
|
669
|
+
start_point: str = "",
|
|
670
|
+
commit: str = "",
|
|
671
|
+
settings: str = "",
|
|
672
|
+
webhook_id: int = 0,
|
|
673
|
+
limit: int = 50,
|
|
674
|
+
ctx: Context = None,
|
|
675
|
+
) -> str:
|
|
676
|
+
"""BITBUCKET (Data Center / Server) — see and act on PRs, branches, builds, repo admin.
|
|
677
|
+
actions: status, whoami, list_projects, list_repos, get_repo,
|
|
678
|
+
list_prs, get_pr, get_pr_diff, get_pr_activities,
|
|
679
|
+
create_pr, comment_pr, approve_pr, unapprove_pr, decline_pr, merge_pr,
|
|
680
|
+
list_branches, create_branch, delete_branch,
|
|
681
|
+
list_commits, list_activity, build_status,
|
|
682
|
+
repo_settings, update_repo_settings, list_webhooks, create_webhook, delete_webhook,
|
|
683
|
+
list_permissions.
|
|
684
|
+
project/repo fall back to bitbucket.default_project/default_repo from .c3/config.json.
|
|
685
|
+
Tokens live in the OS keyring — `c3 bitbucket login` to set them up first."""
|
|
686
|
+
svc = _svc(ctx)
|
|
687
|
+
|
|
688
|
+
def finalize(fname, fargs, fresp, fsumm, **kw):
|
|
689
|
+
return _finalize_response(ctx, fname, fargs, fresp, fsumm, **kw)
|
|
690
|
+
|
|
691
|
+
from cli.tools.bitbucket import handle_bitbucket
|
|
692
|
+
return await asyncio.to_thread(
|
|
693
|
+
handle_bitbucket, action, svc, finalize,
|
|
694
|
+
project=project, repo=repo, pr_id=pr_id, branch=branch, state=state,
|
|
695
|
+
title=title, body=body, from_branch=from_branch, to_branch=to_branch,
|
|
696
|
+
description=description, reviewers=reviewers, name=name, url=url,
|
|
697
|
+
events=events, start_point=start_point, commit=commit,
|
|
698
|
+
settings=settings, webhook_id=webhook_id, limit=limit,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
|
|
652
702
|
def main() -> None:
|
|
653
703
|
"""Entry-point for the ``c3-mcp`` console script."""
|
|
654
704
|
from services import error_reporting
|