code-context-control 2.152.0__py3-none-any.whl → 2.154.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 +63 -1
- cli/commands/parser.py +11 -0
- cli/guide/index.html +4 -0
- cli/guide/tools.html +8 -2
- cli/guide/workflow.html +1 -1
- cli/mcp_server.py +17 -10
- cli/tools/_edit_report.py +31 -4
- cli/tools/edit.py +358 -108
- cli/tools/edit_verify.py +5 -6
- {code_context_control-2.152.0.dist-info → code_context_control-2.154.0.dist-info}/METADATA +1 -1
- {code_context_control-2.152.0.dist-info → code_context_control-2.154.0.dist-info}/RECORD +19 -19
- services/agent_locks.py +30 -0
- services/edit_blobs.py +16 -0
- services/embedding_index.py +434 -4
- services/read_stamps.py +27 -0
- {code_context_control-2.152.0.dist-info → code_context_control-2.154.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.152.0.dist-info → code_context_control-2.154.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.152.0.dist-info → code_context_control-2.154.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.152.0.dist-info → code_context_control-2.154.0.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
|
@@ -91,7 +91,7 @@ console = Console() if HAS_RICH else None
|
|
|
91
91
|
# Config
|
|
92
92
|
CONFIG_DIR = ".c3"
|
|
93
93
|
CONFIG_FILE = ".c3/config.json"
|
|
94
|
-
__version__ = "2.
|
|
94
|
+
__version__ = "2.154.0"
|
|
95
95
|
|
|
96
96
|
# The PreToolUse matcher for native subagent calls (installer and hub migration).
|
|
97
97
|
AGENT_MATCHER = "Agent|Task"
|
|
@@ -1288,6 +1288,14 @@ def cmd_init(args):
|
|
|
1288
1288
|
except Exception:
|
|
1289
1289
|
pass
|
|
1290
1290
|
|
|
1291
|
+
swept = _prune_project_quarantines(project_path)
|
|
1292
|
+
if swept["pruned"]:
|
|
1293
|
+
print(f" Fixed : freed {_format_bytes(swept['bytes'])} from "
|
|
1294
|
+
f"{len(swept['pruned'])} old embedding quarantine dir(s)")
|
|
1295
|
+
for name in swept["failed"]:
|
|
1296
|
+
print(f" [warn] {name} could not be fully removed (in use?); "
|
|
1297
|
+
"run 'c3 prune-quarantine' later")
|
|
1298
|
+
|
|
1291
1299
|
# Delegate backends
|
|
1292
1300
|
try:
|
|
1293
1301
|
from services.ollama_client import OllamaClient as _OC
|
|
@@ -1463,6 +1471,59 @@ def cmd_index(args):
|
|
|
1463
1471
|
return common_cmd_index(args, _command_deps())
|
|
1464
1472
|
|
|
1465
1473
|
|
|
1474
|
+
def _format_bytes(n: float) -> str:
|
|
1475
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
1476
|
+
if n < 1024:
|
|
1477
|
+
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
|
|
1478
|
+
n /= 1024
|
|
1479
|
+
return f"{n:.1f} TB"
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def _prune_project_quarantines(project_path: str, keep=None, dry_run: bool = False) -> dict:
|
|
1483
|
+
"""Prune one project's ``.c3/embeddings/quarantine_corrupt_*`` dirs; returns the sweep result."""
|
|
1484
|
+
from cli.progress import ProgressLine
|
|
1485
|
+
from services.embedding_index import prune_quarantines
|
|
1486
|
+
|
|
1487
|
+
prog = ProgressLine()
|
|
1488
|
+
try:
|
|
1489
|
+
return prune_quarantines(
|
|
1490
|
+
Path(project_path) / CONFIG_DIR / "embeddings", keep=keep, dry_run=dry_run,
|
|
1491
|
+
on_progress=lambda done, total: prog.update(
|
|
1492
|
+
f" removing quarantine copies {done}/{total}"))
|
|
1493
|
+
finally:
|
|
1494
|
+
prog.done()
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def cmd_prune_quarantine(args):
|
|
1498
|
+
"""Delete old corrupt-embedding-store quarantine copies, keeping the newest few."""
|
|
1499
|
+
if args.all:
|
|
1500
|
+
from services.project_manager import ProjectManager
|
|
1501
|
+
projects = [p["path"] for p in ProjectManager().list_registered()]
|
|
1502
|
+
else:
|
|
1503
|
+
projects = [str(Path(args.project_path or ".").resolve())]
|
|
1504
|
+
|
|
1505
|
+
verb = "would remove" if args.dry_run else "removed"
|
|
1506
|
+
pruned = failed = freed = 0
|
|
1507
|
+
for project in projects:
|
|
1508
|
+
res = _prune_project_quarantines(project, keep=args.keep, dry_run=args.dry_run)
|
|
1509
|
+
if not (res["pruned"] or res["failed"]):
|
|
1510
|
+
continue
|
|
1511
|
+
pruned += len(res["pruned"])
|
|
1512
|
+
failed += len(res["failed"])
|
|
1513
|
+
freed += res["bytes"]
|
|
1514
|
+
print(f"{project}: {verb} {len(res['pruned'])} quarantine dir(s), "
|
|
1515
|
+
f"{_format_bytes(res['bytes'])}; kept {len(res['kept'])}")
|
|
1516
|
+
for name in res["failed"]:
|
|
1517
|
+
print(f" [warn] {name} could not be fully removed (in use?); run again later")
|
|
1518
|
+
|
|
1519
|
+
if not (pruned or failed):
|
|
1520
|
+
print(f"Nothing to prune in {len(projects)} project(s).")
|
|
1521
|
+
return
|
|
1522
|
+
print(f"\n{verb.capitalize()} {pruned} quarantine dir(s), {_format_bytes(freed)}.")
|
|
1523
|
+
if failed:
|
|
1524
|
+
sys.exit(1)
|
|
1525
|
+
|
|
1526
|
+
|
|
1466
1527
|
def cmd_compress(args):
|
|
1467
1528
|
"""Compress a file and show results."""
|
|
1468
1529
|
return common_cmd_compress(args, _command_deps())
|
|
@@ -9603,6 +9664,7 @@ def main():
|
|
|
9603
9664
|
"enforce": cmd_enforce,
|
|
9604
9665
|
"override": cmd_override,
|
|
9605
9666
|
"locks": cmd_locks,
|
|
9667
|
+
"prune-quarantine": cmd_prune_quarantine,
|
|
9606
9668
|
"ci": cmd_ci,
|
|
9607
9669
|
"oracle": cmd_oracle,
|
|
9608
9670
|
"upgrade": cmd_upgrade,
|
cli/commands/parser.py
CHANGED
|
@@ -37,6 +37,17 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
37
37
|
p_upgrade.add_argument("--check", action="store_true",
|
|
38
38
|
help="Only report whether a newer version exists; don't install")
|
|
39
39
|
|
|
40
|
+
p_prune = subparsers.add_parser(
|
|
41
|
+
"prune-quarantine",
|
|
42
|
+
help="Delete old corrupt-embedding-store quarantine copies (frees disk)")
|
|
43
|
+
p_prune.add_argument("project_path", nargs="?", default=".")
|
|
44
|
+
p_prune.add_argument("--all", action="store_true",
|
|
45
|
+
help="Every project registered with the hub, not just this one")
|
|
46
|
+
p_prune.add_argument("--keep", type=int, default=None,
|
|
47
|
+
help="Newest quarantine dirs to keep per project (default 2)")
|
|
48
|
+
p_prune.add_argument("--dry-run", action="store_true",
|
|
49
|
+
help="Report what would be removed without deleting")
|
|
50
|
+
|
|
40
51
|
p_index = subparsers.add_parser("index", help="Rebuild code index")
|
|
41
52
|
p_index.add_argument("--max-files", type=int, default=None,
|
|
42
53
|
help="Cap files indexed this run. Unset reads "
|
cli/guide/index.html
CHANGED
|
@@ -663,6 +663,10 @@
|
|
|
663
663
|
<code>c3 map {status|ensure|refresh}</code>
|
|
664
664
|
<span>Inspect or rebuild the live repo map at .c3/MAP.md</span>
|
|
665
665
|
</div>
|
|
666
|
+
<div class="quick-item">
|
|
667
|
+
<code>c3 prune-quarantine [--all] [--dry-run]</code>
|
|
668
|
+
<span>Free disk held by old corrupt-embedding-store copies in .c3/embeddings (<code>c3 init</code> runs it too)</span>
|
|
669
|
+
</div>
|
|
666
670
|
<div class="quick-item">
|
|
667
671
|
<code>c3 hub</code>
|
|
668
672
|
<span>Launch the Project Hub web dashboard (localhost:3330)</span>
|
cli/guide/tools.html
CHANGED
|
@@ -481,7 +481,13 @@ c3_filter(text=<span class="str">"<webpack build output>"</span>, pattern=
|
|
|
481
481
|
<td class="param-name">edits</td>
|
|
482
482
|
<td class="param-type">array</td>
|
|
483
483
|
<td class="param-default">null</td>
|
|
484
|
-
<td class="param-desc">List of <code>{old_string, new_string}</code> objects for batch edits to the same file</td>
|
|
484
|
+
<td class="param-desc">List of <code>{old_string, new_string}</code> objects for batch edits to the same file. Applied in order, all or none: if one patch cannot be placed, nothing is written</td>
|
|
485
|
+
</tr>
|
|
486
|
+
<tr>
|
|
487
|
+
<td class="param-name">overwrite</td>
|
|
488
|
+
<td class="param-type">bool</td>
|
|
489
|
+
<td class="param-default">false</td>
|
|
490
|
+
<td class="param-desc">Replace all of an existing file with <code>new_string</code>. Refused unless this session has read the file with c3_read and the file has not changed since. Keeps the file's line endings and BOM, stores a pre-image, and can be reverted</td>
|
|
485
491
|
</tr>
|
|
486
492
|
</tbody>
|
|
487
493
|
</table>
|
|
@@ -514,7 +520,7 @@ c3_filter(text=<span class="str">"<webpack build output>"</span>, pattern=
|
|
|
514
520
|
<div class="callout callout-info">
|
|
515
521
|
<span class="callout-icon">💡</span>
|
|
516
522
|
<div class="callout-body">
|
|
517
|
-
<strong>Unicode lookalikes are
|
|
523
|
+
<strong>Unicode lookalikes are reported, not applied.</strong> If <code>old_string</code> uses straight quotes but the file has curly quotes (or vice versa), or the two differ only by ASCII vs. unicode dashes (<code>-</code>/<code>–</code>/<code>—</code>) or non-breaking spaces, c3_edit writes nothing and answers <code>[c3_edit:lookalike]</code> with the file's exact text for that region, so the retry can copy it. Applying such a match would replace the file's characters with the retyped ones.
|
|
518
524
|
</div>
|
|
519
525
|
</div>
|
|
520
526
|
</div>
|
cli/guide/workflow.html
CHANGED
|
@@ -623,7 +623,7 @@ c3_impact(target=<span class="str">'MemoryStore'</span>, mode=<span class="str">
|
|
|
623
623
|
</div>
|
|
624
624
|
<div class="workflow-step-body">
|
|
625
625
|
|
|
626
|
-
<p><code>c3_edit</code> does read + patch + write + edit ledger log in one step. It also validates the old_string match before writing. If the literal match fails but the strings differ only by unicode lookalikes (curly quotes, em/en-dashes, NBSP), c3_edit
|
|
626
|
+
<p><code>c3_edit</code> does read + patch + write + edit ledger log in one step. It also validates the old_string match before writing. If the literal match fails but the strings differ only by unicode lookalikes (curly quotes, em/en-dashes, NBSP), c3_edit writes nothing and returns the file's exact text under <code>[c3_edit:lookalike]</code>.</p>
|
|
627
627
|
|
|
628
628
|
<pre><code>c3_edit(
|
|
629
629
|
file_path=<span class="str">'services/auth.py'</span>,
|
cli/mcp_server.py
CHANGED
|
@@ -543,7 +543,9 @@ def _with_hint(resp, make_hint) -> str:
|
|
|
543
543
|
|
|
544
544
|
def _finalize_response(ctx: Context, tool_name: str, args: dict,
|
|
545
545
|
response: str, summary: str = "",
|
|
546
|
-
response_tokens: int = 0
|
|
546
|
+
response_tokens: int = 0,
|
|
547
|
+
ok: bool | None = None) -> str:
|
|
548
|
+
"""ok: the tool's own verdict on the call; None reads it off the response text."""
|
|
547
549
|
global _last_tool_call_time, _last_badge_count
|
|
548
550
|
|
|
549
551
|
deferred_snapshot = False
|
|
@@ -586,11 +588,14 @@ def _finalize_response(ctx: Context, tool_name: str, args: dict,
|
|
|
586
588
|
svc.session_mgr.log_tool_call(tool_name, args, summary)
|
|
587
589
|
# ok=False lets hook_pretool_enforce's activity scan skip a failed call
|
|
588
590
|
# (ISSUE-3: "Error: File not found" used to count as "c3 was used").
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
591
|
+
if ok is not None:
|
|
592
|
+
call_ok = ok
|
|
593
|
+
else:
|
|
594
|
+
try:
|
|
595
|
+
from cli._hook_utils import response_text_failed as _failed
|
|
596
|
+
call_ok = not _failed(response)
|
|
597
|
+
except Exception:
|
|
598
|
+
call_ok = True
|
|
594
599
|
# Both session ids on every tool_call row (D0b): C3's own id joins the
|
|
595
600
|
# row to session_start / the saved session file, the host id joins it to
|
|
596
601
|
# the hooks' session_open / session_end rows. Either may be absent.
|
|
@@ -903,12 +908,13 @@ async def c3_agent(workflow: str, scope: str = "", context: str = "",
|
|
|
903
908
|
@mcp.tool()
|
|
904
909
|
async def c3_edit(file_path: str, old_string: str = "", new_string: str = "",
|
|
905
910
|
summary: str = "", tags: str = "", replace_all: bool = False,
|
|
906
|
-
edits: str | list[dict] = "",
|
|
911
|
+
edits: str | list[dict] = "", overwrite: bool = False,
|
|
907
912
|
ctx: Context = None) -> str:
|
|
908
913
|
"""EDIT — read+patch+write+log in one step. Primary code-change tool; always prefer over native Edit.
|
|
909
914
|
old_string: text to replace. new_string: replacement. summary: ledger description.
|
|
910
|
-
edits: list (or JSON string) of {old_string, new_string, summary?} for multi-hunk batch on one file
|
|
911
|
-
Parallel across files. Create new file: non-existent file_path + old_string='' + new_string=<content>.
|
|
915
|
+
edits: list (or JSON string) of {old_string, new_string, summary?} for multi-hunk batch on one file,
|
|
916
|
+
applied all or none. Parallel across files. Create new file: non-existent file_path + old_string='' + new_string=<content>.
|
|
917
|
+
Rewrite a whole existing file: overwrite=true + new_string=<content>, after a c3_read of it.
|
|
912
918
|
If this call ERRORS OR TIMES OUT, do not retry blind — a failed c3_edit may still have
|
|
913
919
|
written the file. Re-send the same args to c3_edits(action='verify') for a verdict."""
|
|
914
920
|
path_err = validate_file_path(file_path)
|
|
@@ -920,7 +926,8 @@ async def c3_edit(file_path: str, old_string: str = "", new_string: str = "",
|
|
|
920
926
|
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
921
927
|
|
|
922
928
|
resp = await asyncio.to_thread(handle_edit, file_path, old_string, new_string,
|
|
923
|
-
summary, tags, replace_all, svc, finalize, edits
|
|
929
|
+
summary, tags, replace_all, svc, finalize, edits,
|
|
930
|
+
overwrite)
|
|
924
931
|
return _with_hint(resp, lambda: delegate_hints.after_edit(
|
|
925
932
|
svc, file_path, delegate_hints.edit_chars(new_string, edits)))
|
|
926
933
|
|
cli/tools/_edit_report.py
CHANGED
|
@@ -4,8 +4,9 @@ Two concerns, both read-only:
|
|
|
4
4
|
- ``locate``: a RELATIVE file_path resolves against the project root, which is
|
|
5
5
|
the main checkout. When the repo has other git worktrees, the caller may have
|
|
6
6
|
meant one of them, so the response names the resolved path and any worktree
|
|
7
|
-
holding the same relative path
|
|
8
|
-
in a worktree
|
|
7
|
+
holding the same relative path. It refuses outright when the path exists only
|
|
8
|
+
in a worktree, or when this process last wrote to a worktree that the path
|
|
9
|
+
could equally mean.
|
|
9
10
|
- ``diff_block``: a compact unified diff of the whole file, before → after.
|
|
10
11
|
"""
|
|
11
12
|
import difflib
|
|
@@ -19,6 +20,7 @@ from pathlib import Path
|
|
|
19
20
|
_WORKTREE_TTL_S = 60.0
|
|
20
21
|
_GIT_TIMEOUT_S = 3.0
|
|
21
22
|
_worktree_cache: dict[str, tuple[float, list[Path]]] = {}
|
|
23
|
+
_last_tree: dict[str, Path] = {}
|
|
22
24
|
_cache_lock = threading.Lock()
|
|
23
25
|
|
|
24
26
|
DIFF_MAX_LINES = 24
|
|
@@ -98,13 +100,26 @@ def _other_trees(project_root: Path, roots: list[Path]) -> list[tuple[Path, Path
|
|
|
98
100
|
return [(root, root / prefix) for root in roots if root != own]
|
|
99
101
|
|
|
100
102
|
|
|
103
|
+
def note_write(path: Path, project_path: str) -> None:
|
|
104
|
+
"""Record the worktree a write landed in, for ``locate`` to compare the
|
|
105
|
+
next relative path against."""
|
|
106
|
+
holding = [r for r in worktree_roots(project_path)
|
|
107
|
+
if r == path or r in path.parents]
|
|
108
|
+
if holding:
|
|
109
|
+
with _cache_lock:
|
|
110
|
+
_last_tree[str(Path(project_path).resolve())] = max(
|
|
111
|
+
holding, key=lambda r: len(r.parts))
|
|
112
|
+
|
|
113
|
+
|
|
101
114
|
def locate(file_path: str, path: Path, project_path: str) -> tuple[str, str]:
|
|
102
115
|
"""Check a resolved edit target against the repo's other worktrees.
|
|
103
116
|
|
|
104
117
|
Returns ``(refusal, note)``, at most one non-empty. ``refusal`` is set when a
|
|
105
118
|
relative ``file_path`` does not exist in this checkout but does in another
|
|
106
|
-
worktree
|
|
107
|
-
|
|
119
|
+
worktree, or when the last write went to another worktree where the path
|
|
120
|
+
exists or would be created. ``note`` is extra response text for a relative
|
|
121
|
+
path in a repo with other worktrees. Absolute paths always return
|
|
122
|
+
``("", "")``.
|
|
108
123
|
"""
|
|
109
124
|
if Path(file_path).is_absolute():
|
|
110
125
|
return "", ""
|
|
@@ -117,6 +132,18 @@ def locate(file_path: str, path: Path, project_path: str) -> tuple[str, str]:
|
|
|
117
132
|
except ValueError:
|
|
118
133
|
return "", ""
|
|
119
134
|
hits = [proj / rel for _, proj in others if (proj / rel).exists()]
|
|
135
|
+
with _cache_lock:
|
|
136
|
+
last = _last_tree.get(str(project_root))
|
|
137
|
+
recent = next((proj / rel for root, proj in others if root == last), None)
|
|
138
|
+
if recent is not None and (recent.exists() or not path.exists()):
|
|
139
|
+
there = ("where the same path is" if recent.exists()
|
|
140
|
+
else "where this path would be")
|
|
141
|
+
return (f"[c3_edit:wrong-tree] {file_path} is relative, so it means "
|
|
142
|
+
f"{path} in this checkout, but this session's last edit went "
|
|
143
|
+
f"to another git worktree, {there}:\n"
|
|
144
|
+
f" {recent}\n"
|
|
145
|
+
f" Nothing was written. Pass the absolute path of the one "
|
|
146
|
+
f"you mean."), ""
|
|
120
147
|
if not path.exists() and hits:
|
|
121
148
|
listed = "\n".join(f" {h}" for h in hits)
|
|
122
149
|
return (f"[c3_edit:wrong-tree] {file_path} does not exist in this checkout "
|