code-context-control 2.42.0__py3-none-any.whl → 2.44.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 +165 -3
- cli/commands/parser.py +27 -0
- cli/hook_ghost_files.py +15 -0
- cli/hub_server.py +598 -1
- cli/hub_ui/app.js +131 -0
- cli/hub_ui/components/add_project.js +79 -0
- cli/hub_ui/components/config_editor.js +233 -0
- cli/hub_ui/components/drill_health.js +297 -0
- cli/hub_ui/components/drill_panel.js +166 -0
- cli/hub_ui/components/drill_views.js +333 -0
- cli/hub_ui/components/global_search.js +184 -0
- cli/hub_ui/components/mcp_manager.js +379 -0
- cli/hub_ui/components/modals.js +619 -0
- cli/hub_ui/components/project_card.js +408 -0
- cli/hub_ui/components/project_tree.js +103 -0
- cli/hub_ui/components/session_drawer.js +178 -0
- cli/hub_ui/components/settings_modal.js +210 -0
- cli/hub_ui/components/sidebar.js +106 -0
- cli/hub_ui/components/summary_bar.js +71 -0
- cli/hub_ui/components/toasts.js +70 -0
- cli/hub_ui/components/topbar.js +64 -0
- cli/hub_ui/state.js +147 -0
- cli/hub_ui.html +138 -0
- cli/mcp_server.py +12 -6
- cli/tools/_helpers.py +20 -0
- cli/tools/compress.py +47 -19
- cli/tools/delegate.py +4 -3
- cli/tools/federate.py +121 -0
- cli/tools/filter.py +22 -9
- cli/tools/memory.py +32 -8
- cli/tools/project.py +103 -2
- cli/tools/search.py +42 -9
- cli/tools/status.py +21 -6
- cli/ui/icons.js +11 -0
- {code_context_control-2.42.0.dist-info → code_context_control-2.44.0.dist-info}/METADATA +13 -6
- {code_context_control-2.42.0.dist-info → code_context_control-2.44.0.dist-info}/RECORD +62 -39
- core/config.py +7 -0
- services/activity_log.py +32 -1
- services/agents.py +33 -1
- services/bench/external/aider_polyglot.py +3 -1
- services/bench/external/swe_bench.py +3 -1
- services/doc_index.py +12 -0
- services/e2e_benchmark.py +4 -3
- services/e2e_evaluator.py +3 -1
- services/edit_ledger.py +216 -3
- services/file_memory.py +46 -0
- services/indexer.py +18 -0
- services/notifications.py +39 -0
- services/project_manager.py +29 -1
- services/project_runtime.py +6 -1
- services/protocol.py +8 -0
- services/retention.py +438 -0
- services/runtime.py +11 -0
- services/session_manager.py +29 -0
- services/subprojects.py +591 -0
- services/telemetry.py +92 -6
- services/watcher.py +10 -2
- services/win_subprocess.py +98 -0
- {code_context_control-2.42.0.dist-info → code_context_control-2.44.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.42.0.dist-info → code_context_control-2.44.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.42.0.dist-info → code_context_control-2.44.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.42.0.dist-info → code_context_control-2.44.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.44.0"
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _command_deps() -> CommandDeps:
|
|
@@ -1051,6 +1051,10 @@ def cmd_init(args):
|
|
|
1051
1051
|
# ── Non-interactive (--clear) ──────────────────────────────
|
|
1052
1052
|
if getattr(args, "clear", False):
|
|
1053
1053
|
print("\n[--clear] Wiping C3 files...")
|
|
1054
|
+
parent_link = (load_config(project_path) or {}).get("parent") or {}
|
|
1055
|
+
if parent_link.get("path"):
|
|
1056
|
+
print(f" [!] This project is a sub-project of {parent_link['path']}")
|
|
1057
|
+
print(" The parent still lists it -- run 'c3 sub check --fix' there.")
|
|
1054
1058
|
_uninstall_mcp_all(project_path)
|
|
1055
1059
|
if c3_dir.exists():
|
|
1056
1060
|
shutil.rmtree(c3_dir)
|
|
@@ -5733,6 +5737,137 @@ def cmd_projects(args):
|
|
|
5733
5737
|
print(f" Port {s['port']:>5} {s.get('project_name', '?'):<25} {s.get('project_path', '')}")
|
|
5734
5738
|
|
|
5735
5739
|
|
|
5740
|
+
def cmd_sub(args):
|
|
5741
|
+
"""Manage sub-projects: designated sub-folders with linked .c3 branches."""
|
|
5742
|
+
parent = str(Path(getattr(args, "parent", ".") or ".").resolve())
|
|
5743
|
+
if not (Path(parent) / CONFIG_DIR).is_dir():
|
|
5744
|
+
print(f"No .c3 found in {parent}. Run 'c3 init' there first.")
|
|
5745
|
+
return
|
|
5746
|
+
# Import after the cheap guard — the registry module needs a resolvable home.
|
|
5747
|
+
from services.subprojects import VALID_CASCADE_OPS, SubprojectManager
|
|
5748
|
+
|
|
5749
|
+
sm = SubprojectManager(parent)
|
|
5750
|
+
sub = getattr(args, "sub_cmd", "list") or "list"
|
|
5751
|
+
target = getattr(args, "target", None)
|
|
5752
|
+
as_json = getattr(args, "json", False)
|
|
5753
|
+
|
|
5754
|
+
if sub == "add":
|
|
5755
|
+
if not target:
|
|
5756
|
+
print("Usage: c3 sub add <folder> [--parent PATH] [--name NAME]")
|
|
5757
|
+
return
|
|
5758
|
+
result = sm.add(
|
|
5759
|
+
target,
|
|
5760
|
+
name=getattr(args, "name", None),
|
|
5761
|
+
ide=getattr(args, "ide", None),
|
|
5762
|
+
run_init=not getattr(args, "no_init", False),
|
|
5763
|
+
reindex_parent=not getattr(args, "no_reindex_parent", False),
|
|
5764
|
+
)
|
|
5765
|
+
if as_json:
|
|
5766
|
+
print(json.dumps(result, indent=2))
|
|
5767
|
+
return
|
|
5768
|
+
if not result.get("added"):
|
|
5769
|
+
print(f"Failed: {result.get('error')}")
|
|
5770
|
+
return
|
|
5771
|
+
verb = "Adopted (existing .c3 kept)" if result.get("adopted") else "Initialized"
|
|
5772
|
+
print(f"\n[OK] {verb}: {result['name']} ({result['path']})")
|
|
5773
|
+
code = (result.get("parent_reindex") or {}).get("code")
|
|
5774
|
+
if code:
|
|
5775
|
+
print(f" Parent reindexed: {code.get('files_indexed', '?')} files, "
|
|
5776
|
+
f"{code.get('chunks_created', '?')} chunks (sub-project now excluded)")
|
|
5777
|
+
|
|
5778
|
+
elif sub == "list":
|
|
5779
|
+
report = sm.reconcile(fix=False) # report-only consistency pass
|
|
5780
|
+
children = sm.list()
|
|
5781
|
+
if as_json:
|
|
5782
|
+
print(json.dumps({"children": children, "orphans": report.get("orphans", [])}, indent=2))
|
|
5783
|
+
return
|
|
5784
|
+
if not children:
|
|
5785
|
+
print("No sub-projects designated. Use `c3 sub add <folder>`.")
|
|
5786
|
+
return
|
|
5787
|
+
fmt = "{:<22} {:<16} {:>6} {:>7} {}"
|
|
5788
|
+
print(fmt.format("NAME", "STATUS", "FACTS", "ALERTS", "REL PATH"))
|
|
5789
|
+
print("-" * 76)
|
|
5790
|
+
for c in children:
|
|
5791
|
+
print(fmt.format(
|
|
5792
|
+
(c.get("name") or "?")[:21],
|
|
5793
|
+
c.get("status", "?"),
|
|
5794
|
+
c.get("facts_count", 0),
|
|
5795
|
+
c.get("notification_count", 0),
|
|
5796
|
+
c.get("rel_path", ""),
|
|
5797
|
+
))
|
|
5798
|
+
issues = sum(1 for c in children if c["status"] != "ok")
|
|
5799
|
+
line = f"\n{len(children)} sub-project(s)"
|
|
5800
|
+
if issues:
|
|
5801
|
+
line += f" -- {issues} with issues (run `c3 sub check --fix`)"
|
|
5802
|
+
if report.get("orphans"):
|
|
5803
|
+
line += f" -- {len(report['orphans'])} registry orphan(s)"
|
|
5804
|
+
print(line)
|
|
5805
|
+
|
|
5806
|
+
elif sub == "remove":
|
|
5807
|
+
if not target:
|
|
5808
|
+
print("Usage: c3 sub remove <name|path> [--clear] [--yes]")
|
|
5809
|
+
return
|
|
5810
|
+
mode = "clear" if getattr(args, "clear", False) else "unlink"
|
|
5811
|
+
if mode == "clear" and not getattr(args, "yes", False):
|
|
5812
|
+
print("This will DELETE the sub-project's .c3 directory and instruction docs.")
|
|
5813
|
+
confirm = input("Type 'clear' to confirm: ").strip().lower()
|
|
5814
|
+
if confirm != "clear":
|
|
5815
|
+
print("Aborted.")
|
|
5816
|
+
return
|
|
5817
|
+
result = sm.remove(target, mode=mode,
|
|
5818
|
+
reindex_parent=not getattr(args, "no_reindex_parent", False))
|
|
5819
|
+
if as_json:
|
|
5820
|
+
print(json.dumps(result, indent=2))
|
|
5821
|
+
return
|
|
5822
|
+
if not result.get("removed"):
|
|
5823
|
+
print(f"Failed: {result.get('error')}")
|
|
5824
|
+
return
|
|
5825
|
+
print(f"\n[OK] {'Cleared' if mode == 'clear' else 'Unlinked'}: "
|
|
5826
|
+
f"{result.get('name')} ({result.get('path')})")
|
|
5827
|
+
for w in result.get("warnings", []):
|
|
5828
|
+
print(f" warning: {w}")
|
|
5829
|
+
|
|
5830
|
+
elif sub == "run":
|
|
5831
|
+
if target not in VALID_CASCADE_OPS:
|
|
5832
|
+
print(f"Usage: c3 sub run {{{'|'.join(VALID_CASCADE_OPS)}}} [--include-parent] [--json]")
|
|
5833
|
+
return
|
|
5834
|
+
result = sm.cascade(target,
|
|
5835
|
+
include_parent=getattr(args, "include_parent", False),
|
|
5836
|
+
mcp=getattr(args, "mcp", False))
|
|
5837
|
+
if as_json:
|
|
5838
|
+
print(json.dumps(result, indent=2))
|
|
5839
|
+
return
|
|
5840
|
+
for row in result["results"]:
|
|
5841
|
+
mark = "OK " if row["ok"] else "FAIL"
|
|
5842
|
+
extra = f" -- {row.get('error')}" if row.get("error") else ""
|
|
5843
|
+
print(f" [{mark}] {row['name']:<22} {row['elapsed_ms']:>6}ms{extra}")
|
|
5844
|
+
s = result["summary"]
|
|
5845
|
+
print(f"\n{target}: {s['ok']}/{s['total']} ok, {s['failed']} failed")
|
|
5846
|
+
|
|
5847
|
+
elif sub == "check":
|
|
5848
|
+
result = sm.reconcile(fix=getattr(args, "fix", False),
|
|
5849
|
+
prune=getattr(args, "prune", False))
|
|
5850
|
+
if as_json:
|
|
5851
|
+
print(json.dumps(result, indent=2))
|
|
5852
|
+
return
|
|
5853
|
+
if not result["children"] and not result["orphans"] and not result["pruned"]:
|
|
5854
|
+
print("No sub-projects designated.")
|
|
5855
|
+
return
|
|
5856
|
+
for c in result["children"]:
|
|
5857
|
+
print(f" [{c['status']:<16}] {c.get('name') or '?':<22} {c.get('rel_path', '')}")
|
|
5858
|
+
for o in result["orphans"]:
|
|
5859
|
+
print(f" [orphan_registry ] {o}")
|
|
5860
|
+
for f in result.get("fixed", []):
|
|
5861
|
+
print(f" fixed: {f.get('action')} -> {f.get('path') or f.get('rel_path')}")
|
|
5862
|
+
for p in result.get("pruned", []):
|
|
5863
|
+
print(f" pruned: {p.get('rel_path')}")
|
|
5864
|
+
if result["ok"]:
|
|
5865
|
+
print("\nAll links consistent.")
|
|
5866
|
+
else:
|
|
5867
|
+
hint = "" if getattr(args, "fix", False) else " Run `c3 sub check --fix` to repair."
|
|
5868
|
+
print(f"\nIssues found.{hint}")
|
|
5869
|
+
|
|
5870
|
+
|
|
5736
5871
|
def cmd_session_benchmark(args):
|
|
5737
5872
|
"""Run real-world session workflow benchmark."""
|
|
5738
5873
|
if getattr(args, "command", "") == "session-benchmark":
|
|
@@ -6475,6 +6610,24 @@ def cmd_upgrade(args):
|
|
|
6475
6610
|
print(" In each project, run c3 init . --force to apply any migrations.")
|
|
6476
6611
|
|
|
6477
6612
|
|
|
6613
|
+
def _stdio_is_interactive() -> bool:
|
|
6614
|
+
"""True when stdin AND stdout are attached to a real terminal.
|
|
6615
|
+
|
|
6616
|
+
Used to decide whether bare `c3` may launch the full-screen TUI. With
|
|
6617
|
+
redirected stdio (pytest capture_output, CI, shell pipes) a TUI child
|
|
6618
|
+
would inherit our pipe handles and keep them open past our own death;
|
|
6619
|
+
on Windows the caller's communicate() then blocks forever because
|
|
6620
|
+
subprocess timeouts kill only the direct child, never the tree.
|
|
6621
|
+
"""
|
|
6622
|
+
try:
|
|
6623
|
+
return bool(
|
|
6624
|
+
sys.stdin is not None and sys.stdin.isatty()
|
|
6625
|
+
and sys.stdout is not None and sys.stdout.isatty()
|
|
6626
|
+
)
|
|
6627
|
+
except Exception:
|
|
6628
|
+
return False
|
|
6629
|
+
|
|
6630
|
+
|
|
6478
6631
|
def _launch_tui() -> None:
|
|
6479
6632
|
"""Launch the interactive TUI — what `c3` with no arguments does.
|
|
6480
6633
|
|
|
@@ -6524,8 +6677,16 @@ def main():
|
|
|
6524
6677
|
args = parser.parse_args()
|
|
6525
6678
|
|
|
6526
6679
|
if not args.command:
|
|
6527
|
-
# Bare `c3` launches the interactive TUI (replaces the old c3.bat
|
|
6528
|
-
|
|
6680
|
+
# Bare `c3` launches the interactive TUI (replaces the old c3.bat
|
|
6681
|
+
# wrapper) — but only when attached to a real console. With redirected
|
|
6682
|
+
# stdio there is no terminal for a full-screen app anyway, and the TUI
|
|
6683
|
+
# child would inherit our stdout/stderr pipe handles and hold them
|
|
6684
|
+
# open past our own death (a caller's communicate() then hangs forever
|
|
6685
|
+
# on Windows). Print help instead of spawning anything.
|
|
6686
|
+
if _stdio_is_interactive():
|
|
6687
|
+
_launch_tui()
|
|
6688
|
+
else:
|
|
6689
|
+
parser.print_help()
|
|
6529
6690
|
return
|
|
6530
6691
|
|
|
6531
6692
|
commands = {
|
|
@@ -6551,6 +6712,7 @@ def main():
|
|
|
6551
6712
|
"terse": cmd_terse,
|
|
6552
6713
|
"ui": cmd_ui,
|
|
6553
6714
|
"projects": cmd_projects,
|
|
6715
|
+
"sub": cmd_sub,
|
|
6554
6716
|
"hub": cmd_hub,
|
|
6555
6717
|
"bitbucket": cmd_bitbucket,
|
|
6556
6718
|
"oracle": cmd_oracle,
|
cli/commands/parser.py
CHANGED
|
@@ -138,6 +138,33 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
138
138
|
)
|
|
139
139
|
p_projects.add_argument("--name", default=None, help="Display name (for add)")
|
|
140
140
|
|
|
141
|
+
p_sub = subparsers.add_parser("sub", help="Manage sub-projects (linked child .c3 branches)")
|
|
142
|
+
p_sub.add_argument(
|
|
143
|
+
"sub_cmd",
|
|
144
|
+
nargs="?",
|
|
145
|
+
choices=["add", "list", "remove", "run", "check"],
|
|
146
|
+
default="list",
|
|
147
|
+
help="Sub-command (default: list)",
|
|
148
|
+
)
|
|
149
|
+
p_sub.add_argument(
|
|
150
|
+
"target",
|
|
151
|
+
nargs="?",
|
|
152
|
+
default=None,
|
|
153
|
+
help="Folder (add), sub-project name/path (remove), or operation update|reindex|health (run)",
|
|
154
|
+
)
|
|
155
|
+
p_sub.add_argument("--parent", default=".", help="Parent project path (default: current directory)")
|
|
156
|
+
p_sub.add_argument("--name", default=None, help="Display name for the sub-project (add)")
|
|
157
|
+
p_sub.add_argument("--ide", default=None, type=parse_cli_ide_arg, help="IDE for the sub-project init (add)")
|
|
158
|
+
p_sub.add_argument("--no-reindex-parent", action="store_true", help="Skip the parent reindex after add/remove")
|
|
159
|
+
p_sub.add_argument("--no-init", action="store_true", help="Link only; skip running init in the folder (add)")
|
|
160
|
+
p_sub.add_argument("--clear", action="store_true", help="Also wipe the sub-project's .c3 and unregister it (remove; default keeps .c3)")
|
|
161
|
+
p_sub.add_argument("--yes", action="store_true", help="Skip confirmation prompts")
|
|
162
|
+
p_sub.add_argument("--include-parent", action="store_true", help="Also run the operation on the parent (run)")
|
|
163
|
+
p_sub.add_argument("--mcp", action="store_true", help="Also reinstall MCP config on update (run update)")
|
|
164
|
+
p_sub.add_argument("--fix", action="store_true", help="Repair links from the parent config (check)")
|
|
165
|
+
p_sub.add_argument("--prune", action="store_true", help="With --fix: drop entries whose folder is gone (check)")
|
|
166
|
+
p_sub.add_argument("--json", action="store_true", help="Emit JSON output")
|
|
167
|
+
|
|
141
168
|
p_perms = subparsers.add_parser("permissions",
|
|
142
169
|
help="Manage Claude Code permissions — show | preview <tier> | diff | clean | <tier>")
|
|
143
170
|
p_perms.add_argument("tier", nargs="?", default="show",
|
cli/hook_ghost_files.py
CHANGED
|
@@ -212,6 +212,21 @@ def cleanup_ghost_files(ghosts: list[dict]) -> list[str]:
|
|
|
212
212
|
return deleted
|
|
213
213
|
|
|
214
214
|
|
|
215
|
+
def sweep_ghost_files(project_root) -> list[str]:
|
|
216
|
+
"""Scan *project_root* and delete any ghost files in one call.
|
|
217
|
+
|
|
218
|
+
Convenience wrapper (scan + cleanup) so callers outside the Bash PostToolUse
|
|
219
|
+
hook — e.g. long-lived MCP-server background agents whose cwd is the project
|
|
220
|
+
root, or git worktrees where no PostToolUse hook runs — can self-clean the
|
|
221
|
+
root. Returns the list of deleted file names (empty if none). Never raises.
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
root = project_root if isinstance(project_root, Path) else Path(project_root)
|
|
225
|
+
return cleanup_ghost_files(scan_ghost_files(root))
|
|
226
|
+
except Exception:
|
|
227
|
+
return []
|
|
228
|
+
|
|
229
|
+
|
|
215
230
|
# Tools whose output can carry shell-meta text that leaks into 0-byte files:
|
|
216
231
|
# native shells, c3_shell (its `N->Mtok` filter header), and file reads whose
|
|
217
232
|
# content has `-> Type` hints. A downstream shell sees `> word` and creates an
|