code-context-control 2.36.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 +64 -7
- cli/commands/common.py +3 -7
- cli/guide/getting-started.html +17 -1
- cli/hub_server.py +4 -2
- cli/server.py +12 -10
- {code_context_control-2.36.0.dist-info → code_context_control-2.37.0.dist-info}/METADATA +5 -1
- {code_context_control-2.36.0.dist-info → code_context_control-2.37.0.dist-info}/RECORD +13 -13
- services/claude_md.py +139 -20
- services/session_manager.py +7 -12
- {code_context_control-2.36.0.dist-info → code_context_control-2.37.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.36.0.dist-info → code_context_control-2.37.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.36.0.dist-info → code_context_control-2.37.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.36.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))
|
|
@@ -5139,10 +5180,23 @@ def cmd_install_mcp(args):
|
|
|
5139
5180
|
},
|
|
5140
5181
|
]
|
|
5141
5182
|
stop_event = "Stop"
|
|
5142
|
-
# 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
|
+
|
|
5143
5197
|
existing_stop = [
|
|
5144
5198
|
h for h in settings.get("hooks", {}).get(stop_event, [])
|
|
5145
|
-
if h
|
|
5199
|
+
if not _is_c3_stop_hook(h)
|
|
5146
5200
|
]
|
|
5147
5201
|
existing_stop.extend(desired_stop_hooks)
|
|
5148
5202
|
settings.setdefault("hooks", {})[stop_event] = existing_stop
|
|
@@ -5160,9 +5214,12 @@ def cmd_install_mcp(args):
|
|
|
5160
5214
|
include_wildcard = bool(getattr(args, "include_mcp_wildcard", False))
|
|
5161
5215
|
if perm_tier and profile.name == "claude-code":
|
|
5162
5216
|
if perm_tier in PERMISSION_TIERS:
|
|
5163
|
-
settings["permissions"] =
|
|
5164
|
-
|
|
5165
|
-
|
|
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
|
+
)
|
|
5166
5223
|
# Persist tier choice in .c3/config.json
|
|
5167
5224
|
_c3cfg = _safe_read_json(c3_config_path, str(c3_config_path))
|
|
5168
5225
|
_c3cfg["permission_tier"] = perm_tier
|
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/guide/getting-started.html
CHANGED
|
@@ -188,6 +188,14 @@ c3 init</code></pre>
|
|
|
188
188
|
|
|
189
189
|
<h3>Re-init / upgrade</h3>
|
|
190
190
|
<p>Running <code>c3 init</code> on an existing project is safe — it merges new config without overwriting your customizations.</p>
|
|
191
|
+
|
|
192
|
+
<div class="callout callout-info">
|
|
193
|
+
<span class="callout-icon">🛡️</span>
|
|
194
|
+
<div class="callout-body">
|
|
195
|
+
<strong>Your hand-written content is preserved</strong>
|
|
196
|
+
Generated instruction files (<code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>GEMINI.md</code>) wrap C3 content in a <code><!-- C3:BEGIN … --></code> / <code><!-- C3:END --></code> block. Re-running init (or <code>c3 claudemd save</code> / the <strong>Compact</strong> action) rewrites only that block — anything you add outside it stays put. An existing hand-written file with no block is kept and the C3 block is appended below it.
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
191
199
|
</section>
|
|
192
200
|
|
|
193
201
|
<hr class="divider">
|
|
@@ -200,7 +208,7 @@ c3 init</code></pre>
|
|
|
200
208
|
|
|
201
209
|
<h3>Claude Code (primary)</h3>
|
|
202
210
|
<pre><code>c3 install-mcp claude</code></pre>
|
|
203
|
-
<p>This writes to <code>.mcp.json</code> (project scope) and optionally configures PreToolUse / PostToolUse hooks in <code>.claude/settings.local.json</code
|
|
211
|
+
<p>This writes to <code>.mcp.json</code> (project scope) and optionally configures PreToolUse / PostToolUse hooks in <code>.claude/settings.local.json</code>. Both are merged, not overwritten: C3 only touches its own <code>c3</code> server entry and its own hooks, leaving any other MCP servers, hooks, and top-level keys you've added in place.</p>
|
|
204
212
|
|
|
205
213
|
<h3>VS Code Copilot</h3>
|
|
206
214
|
<pre><code>c3 install-mcp vscode</code></pre>
|
|
@@ -358,6 +366,14 @@ c3_session(action='snapshot') <span class="com"># before /clear</span></code><
|
|
|
358
366
|
The <code>standard</code> tier blocks destructive shell commands (rm -rf, git reset --hard, etc.) while allowing safe operations. Upgrade to <code>unrestricted</code> only when needed.
|
|
359
367
|
</div>
|
|
360
368
|
</div>
|
|
369
|
+
|
|
370
|
+
<div class="callout callout-info">
|
|
371
|
+
<span class="callout-icon">🛡️</span>
|
|
372
|
+
<div class="callout-body">
|
|
373
|
+
<strong>Your custom rules survive a tier change</strong>
|
|
374
|
+
Applying or switching a tier preserves <code>allow</code>/<code>deny</code> entries you added yourself, plus keys like <code>ask</code> and <code>defaultMode</code>. C3 only replaces the entries it manages, so you can mix a tier with project-specific permissions.
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
361
377
|
</section>
|
|
362
378
|
|
|
363
379
|
<hr class="divider">
|
cli/hub_server.py
CHANGED
|
@@ -1206,7 +1206,7 @@ def api_projects_permissions_get():
|
|
|
1206
1206
|
@app.route("/api/projects/permissions/apply", methods=["POST"])
|
|
1207
1207
|
def api_projects_permissions_put():
|
|
1208
1208
|
"""Apply permission tier to a project. Body: {path, tier}"""
|
|
1209
|
-
from cli.c3 import PERMISSION_TIERS, _build_permission_tier
|
|
1209
|
+
from cli.c3 import PERMISSION_TIERS, _build_permission_tier, _merge_permission_tier
|
|
1210
1210
|
data = request.get_json(force=True) or {}
|
|
1211
1211
|
path = (data.get("path") or "").strip()
|
|
1212
1212
|
tier = (data.get("tier") or "").strip()
|
|
@@ -1228,7 +1228,9 @@ def api_projects_permissions_put():
|
|
|
1228
1228
|
settings = json.load(f)
|
|
1229
1229
|
except Exception:
|
|
1230
1230
|
pass
|
|
1231
|
-
settings["permissions"] =
|
|
1231
|
+
settings["permissions"] = _merge_permission_tier(
|
|
1232
|
+
settings.get("permissions") or {}, tier_perms["permissions"]
|
|
1233
|
+
)
|
|
1232
1234
|
with open(settings_path, "w", encoding="utf-8") as f:
|
|
1233
1235
|
json.dump(settings, f, indent=2)
|
|
1234
1236
|
|
cli/server.py
CHANGED
|
@@ -1121,16 +1121,11 @@ def api_claudemd_save():
|
|
|
1121
1121
|
return jsonify({"error": "Generation produced empty content"}), 500
|
|
1122
1122
|
|
|
1123
1123
|
output_path = PROJECT_PATH / claude_md_mgr.instructions_file
|
|
1124
|
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1125
1124
|
|
|
1126
|
-
#
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
if "# User Notes" in existing:
|
|
1130
|
-
user_section = existing[existing.index("# User Notes"):]
|
|
1131
|
-
content += f"\n\n{user_section}"
|
|
1125
|
+
# Wrap in the C3 managed block; never clobber user content outside it.
|
|
1126
|
+
from services.claude_md import write_c3_instruction_doc
|
|
1127
|
+
write_c3_instruction_doc(output_path, content)
|
|
1132
1128
|
|
|
1133
|
-
output_path.write_text(content, encoding="utf-8")
|
|
1134
1129
|
return jsonify({
|
|
1135
1130
|
"path": str(output_path),
|
|
1136
1131
|
"tokens": gen.get("tokens", 0),
|
|
@@ -2021,7 +2016,12 @@ def api_permissions_get():
|
|
|
2021
2016
|
@app.route('/api/permissions', methods=['PUT'])
|
|
2022
2017
|
def api_permissions_put():
|
|
2023
2018
|
"""Apply a permission tier. Body: {tier: "read-only"|"standard"|"permissive"}"""
|
|
2024
|
-
from cli.c3 import
|
|
2019
|
+
from cli.c3 import (
|
|
2020
|
+
PERMISSION_TIERS,
|
|
2021
|
+
_build_permission_tier,
|
|
2022
|
+
_merge_permission_tier,
|
|
2023
|
+
_safe_read_json,
|
|
2024
|
+
)
|
|
2025
2025
|
data = request.get_json() or {}
|
|
2026
2026
|
tier = data.get("tier", "").strip()
|
|
2027
2027
|
if tier not in PERMISSION_TIERS:
|
|
@@ -2031,7 +2031,9 @@ def api_permissions_put():
|
|
|
2031
2031
|
settings_path = PROJECT_PATH / ".claude" / "settings.local.json"
|
|
2032
2032
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2033
2033
|
settings = _safe_read_json(settings_path, str(settings_path))
|
|
2034
|
-
settings["permissions"] =
|
|
2034
|
+
settings["permissions"] = _merge_permission_tier(
|
|
2035
|
+
settings.get("permissions") or {}, tier_perms["permissions"]
|
|
2036
|
+
)
|
|
2035
2037
|
with open(settings_path, "w", encoding="utf-8") as f:
|
|
2036
2038
|
json.dump(settings, f, indent=2)
|
|
2037
2039
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.37.0
|
|
4
4
|
Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -231,6 +231,8 @@ Every session you've ever run, with duration, decision count, file count, tool c
|
|
|
231
231
|
|
|
232
232
|
Manage `CLAUDE.md`, `AGENTS.md` (Codex), `GEMINI.md`, and `.github/copilot-instructions.md` from one editor. Generate from project state, run a Health Check (drift detection vs the actual codebase), Compact stale sections, or Promote insights captured during sessions. **One source of truth** instead of four out-of-sync files.
|
|
233
233
|
|
|
234
|
+
C3-generated content is wrapped in a `<!-- C3:BEGIN … -->` / `<!-- C3:END -->` block. Regenerating (or `Compact`) only rewrites that block — **anything you write outside it is preserved**, so it's safe to keep your own notes in the same file.
|
|
235
|
+
|
|
234
236
|
### 7. Chat — browse prior AI conversations
|
|
235
237
|
|
|
236
238
|
<p align="center">
|
|
@@ -370,6 +372,8 @@ c3 permissions show
|
|
|
370
372
|
c3 permissions standard
|
|
371
373
|
```
|
|
372
374
|
|
|
375
|
+
Applying or switching a tier **preserves your own `allow`/`deny` rules** (and keys like `ask`/`defaultMode`) — only C3-managed entries are replaced. Likewise, C3 never clobbers your other entries in `.mcp.json` (only its own `c3` server) or the hooks you've added to `settings.local.json` (only its own hooks).
|
|
376
|
+
|
|
373
377
|
---
|
|
374
378
|
|
|
375
379
|
## Benchmarks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
|
|
2
2
|
cli/_hook_utils.py,sha256=1_hTA-Wz62xB8jnSAH4C5TfCkrwEP0g2kq_-oRfQLm4,3724
|
|
3
|
-
cli/c3.py,sha256=
|
|
3
|
+
cli/c3.py,sha256=X9HResiGmUELIVlz6uosit35OSCCKeJwuwr02gzuGBM,297611
|
|
4
4
|
cli/docs.html,sha256=JgtBFUuUkvmYowPREYiGhhcRbB5e2UjkRc00MIF0hsU,143653
|
|
5
5
|
cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
|
|
6
6
|
cli/hook_auto_snapshot.py,sha256=amtliVDzKUQr6KBR0pdBA8vXghAV-gKr19jBaJVnP_w,5006
|
|
@@ -15,18 +15,18 @@ cli/hook_read.py,sha256=M5l_SU899O72tZe3j4YQJJKNb1-xulvKOj8XZjJzwYU,8021
|
|
|
15
15
|
cli/hook_session_stats.py,sha256=a1OKi9kmiXRI2qieY_Uq14xRxdXQTQu9WVzDTUlI0GQ,1897
|
|
16
16
|
cli/hook_terse_advisor.py,sha256=pD7Bap7OYOKqtYz7cX8nWSRLH7ook-tSD2Ov2MNp_sA,5907
|
|
17
17
|
cli/hub.html,sha256=Hl-XPZGT1mMiKrbX9c5OsEw6mXEumwIB3vp1WlWaplM,183966
|
|
18
|
-
cli/hub_server.py,sha256=
|
|
18
|
+
cli/hub_server.py,sha256=Zo---712ynP83XWB_aZb45b9XcQmzs9EGp8wwyRIGLU,60207
|
|
19
19
|
cli/mcp_proxy.py,sha256=92htuT-p0j-cDTbyqlIJpGoQ85_Aw7UuB8L_Toi_u20,17511
|
|
20
20
|
cli/mcp_server.py,sha256=iYUB6rfGjNuiNQP-GecuXyMVa1CEihtu4dV7PhRHWqg,32549
|
|
21
|
-
cli/server.py,sha256=
|
|
21
|
+
cli/server.py,sha256=tOgLZvB_i7RluK3qwORqhrhTz1DVL3VVAIWu9f55ysg,120292
|
|
22
22
|
cli/ui.html,sha256=xcdt74nlFEXx-0Bx6-Okw-WSVZPAXL0iukxU0ytI6CA,5694
|
|
23
23
|
cli/ui_legacy.html,sha256=cI8tC6RKmE2NIJOcsu7CY-zT4VznjcbD6NTjxb_fvUY,378460
|
|
24
24
|
cli/ui_nano.html,sha256=UAwQ6bbTOXAoGq191AZ7slhngR9edJSa3IhqpynveDg,27740
|
|
25
25
|
cli/commands/__init__.py,sha256=0Z8MABNzwSFJGT4Xv9R5AJVR8XxraTsuVTz5b0bShmo,38
|
|
26
|
-
cli/commands/common.py,sha256=
|
|
26
|
+
cli/commands/common.py,sha256=8_uYFWcJBXMscPmvQEMYOwYXZidwD7Of9j9fiD00g6Q,11272
|
|
27
27
|
cli/commands/parser.py,sha256=unu8jkTocf3AoOSiEAlw_qta_2K7sQqg9BmluTzOKO4,22968
|
|
28
28
|
cli/guide/bitbucket.html,sha256=5HrLDm6Ue-AJZ81bqWaSp2nSfxaRdEjHFY29P7plLXA,33350
|
|
29
|
-
cli/guide/getting-started.html,sha256=
|
|
29
|
+
cli/guide/getting-started.html,sha256=F3AaF2HVfeg6eKO25p_vPogr_i1hvkjIZVggIJFWTl0,18829
|
|
30
30
|
cli/guide/index.html,sha256=sAk4Z7Xrlw7_F_Gbxn3lhZeT7hydrp1aENhMQ_9msgU,19861
|
|
31
31
|
cli/guide/oracle.html,sha256=-5RtRTLV0JUVerjIayEftQdcqg8j_-X0ail8lVVJfOE,21897
|
|
32
32
|
cli/guide/shared.css,sha256=Mmm7W6aYxrkDq2RPOcyXiQFYGR-yB3auwX1uIWi6C74,15967
|
|
@@ -64,7 +64,7 @@ cli/ui/components/memory.js,sha256=v5IsHTxLHpXX4xCsUaZ_UPprZEabdgP4jiWc298iV2U,2
|
|
|
64
64
|
cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE,30837
|
|
65
65
|
cli/ui/components/settings.js,sha256=8LVTV2TQl9tcRXhXbtBEJOCBdiyk-x2QASoVYZUAuEA,71442
|
|
66
66
|
cli/ui/components/sidebar.js,sha256=cAY_jwYB-o1X_wWn__VXlG4IegVObuE3NmVsuFWqxtg,7417
|
|
67
|
-
code_context_control-2.
|
|
67
|
+
code_context_control-2.37.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
68
68
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
69
69
|
core/config.py,sha256=knSre16IF_6FZ-pctHWbA5mZdjCEJUF5rsdzZxWg4zU,13002
|
|
70
70
|
core/ide.py,sha256=9LzsDVK2LL8RVpL40l6oNGiasZ3D8OCU_9i9A0gJKBo,6876
|
|
@@ -99,7 +99,7 @@ services/auto_memory.py,sha256=v__ZS1e68533_Yv491mZtvuZnheC63q6_uTvWhBw3Lw,14290
|
|
|
99
99
|
services/benchmark_dashboard.py,sha256=iR-DnqnoKbqHMJ4d-ZkIvJBYfzwTa7r-jzO6j2BYDfQ,27711
|
|
100
100
|
services/bitbucket_client.py,sha256=v8xGEcnIEmURvcg38XwmiCGh7-_QnjhAJEb0te_yZzQ,16107
|
|
101
101
|
services/bitbucket_credentials.py,sha256=2qLA9pQMol4y95y4DJMNBsBBPUsJQCKbLFo2iiCnfvI,7364
|
|
102
|
-
services/claude_md.py,sha256=
|
|
102
|
+
services/claude_md.py,sha256=FiFDyeAQ10baRCYH74QAn-UixVi_rgn-_6yP70wCRS8,40278
|
|
103
103
|
services/compressor.py,sha256=uSVyTYfvxFrRYupzyKj-HzkBP0RwARrGYFz_DnMSEaM,25169
|
|
104
104
|
services/context_snapshot.py,sha256=s_klEr1SJYM9u-anMmnoemsYuIF_KUWBjz1zUo0wPgU,15662
|
|
105
105
|
services/conversation_store.py,sha256=vPiMiKAE22RCBSSphgGH9Vx-lPV45SmttOwgVVWahL4,33398
|
|
@@ -132,7 +132,7 @@ services/retrieval_broker.py,sha256=9X67VZ_6AkbAzopHuuMFKmP4CGZLnW576kjSKMenBnw,
|
|
|
132
132
|
services/router.py,sha256=Cz10nx2fKTbaGn14mSBePWIDrw5rdcs_1JFYXeik084,15626
|
|
133
133
|
services/runtime.py,sha256=SOUizCDW1FFTDCoaZ1Njozjp25Bhah7lR1f0WYscaw0,11361
|
|
134
134
|
services/session_benchmark.py,sha256=qw_vtDim1hvFdM8Me5EsgU9pTuJhzRjQmh6m7DDnXWY,98989
|
|
135
|
-
services/session_manager.py,sha256=
|
|
135
|
+
services/session_manager.py,sha256=vWgvYGForO7W2wZzBC8QNaBQyUR2VIlVuC2wpmUqIOc,43735
|
|
136
136
|
services/session_preloader.py,sha256=DsTAXMKVtrX9yu1sEFojYDi9-jkSAj1Ylt9JTy57Dow,9883
|
|
137
137
|
services/text_index.py,sha256=r3o4CobTG9jAO9PWazgbWYLY9oi_FgEJ3xwEXrF4KM0,2783
|
|
138
138
|
services/tool_classifier.py,sha256=Fgvq0ZcpnCskwtO8a3YI1MiecPNnw6UbPyJQIUwgfiQ,6512
|
|
@@ -163,8 +163,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
163
163
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
164
164
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
165
165
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
166
|
-
code_context_control-2.
|
|
167
|
-
code_context_control-2.
|
|
168
|
-
code_context_control-2.
|
|
169
|
-
code_context_control-2.
|
|
170
|
-
code_context_control-2.
|
|
166
|
+
code_context_control-2.37.0.dist-info/METADATA,sha256=ztn-S1aTmKvYltI1Rw0hhPXNGl9t34fgqbVkd8tZmm0,21230
|
|
167
|
+
code_context_control-2.37.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
168
|
+
code_context_control-2.37.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
169
|
+
code_context_control-2.37.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
170
|
+
code_context_control-2.37.0.dist-info/RECORD,,
|
services/claude_md.py
CHANGED
|
@@ -66,6 +66,87 @@ Plan mode: all c3_* read tools work normally — skip edit/delegate steps.
|
|
|
66
66
|
DO NOT: start with native Read/Grep/Glob/Edit, skip c3_validate, read full files without c3_compress."""
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
# --- C3-managed instruction block ---------------------------------------------
|
|
70
|
+
# C3-generated content for project instruction docs (CLAUDE.md / AGENTS.md /
|
|
71
|
+
# GEMINI.md / copilot-instructions.md) is wrapped in these sentinels so that
|
|
72
|
+
# regenerating the docs never clobbers user-written content. Mirrors the
|
|
73
|
+
# non-destructive merge used for the global ~/.claude/CLAUDE.md.
|
|
74
|
+
C3_BLOCK_BEGIN = (
|
|
75
|
+
"<!-- C3:BEGIN — auto-generated by C3. Do NOT edit inside this block; it is "
|
|
76
|
+
"regenerated on every `c3 install-mcp` / `c3 init`. Your content OUTSIDE the "
|
|
77
|
+
"block is preserved. -->"
|
|
78
|
+
)
|
|
79
|
+
C3_BLOCK_END = "<!-- C3:END -->"
|
|
80
|
+
C3_BLOCK_HEADING = "# C3 — Managed Instructions"
|
|
81
|
+
|
|
82
|
+
# First line of every legacy (marker-less) C3 instruction doc. Used to detect
|
|
83
|
+
# and replace pre-marker C3 content instead of leaving it duplicated above the
|
|
84
|
+
# new block. Both the compact and nano workflows start with this heading.
|
|
85
|
+
C3_LEGACY_FIRST_LINE = "## C3 Tools"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def wrap_c3_block(content: str) -> str:
|
|
89
|
+
"""Wrap C3-generated instruction ``content`` in the managed-section markers."""
|
|
90
|
+
body = content.strip()
|
|
91
|
+
return f"{C3_BLOCK_BEGIN}\n{C3_BLOCK_HEADING}\n\n{body}\n{C3_BLOCK_END}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def merge_c3_block(existing: str, new_block: str) -> str:
|
|
95
|
+
"""Merge a freshly wrapped C3 block into ``existing`` file content.
|
|
96
|
+
|
|
97
|
+
Non-destructive, mirroring the global ~/.claude/CLAUDE.md behaviour:
|
|
98
|
+
|
|
99
|
+
1. If the C3 markers are already present, replace only the marked region and
|
|
100
|
+
preserve everything the user wrote before and after it.
|
|
101
|
+
2. Legacy (marker-less) C3 docs are recognised by their leading
|
|
102
|
+
``## C3 Tools`` heading and replaced wholesale, while any trailing
|
|
103
|
+
``# User Notes`` section (the pre-marker convention) is preserved.
|
|
104
|
+
3. A genuine, user-authored file with neither markers nor the legacy
|
|
105
|
+
signature is never overwritten — the C3 block is appended below it.
|
|
106
|
+
"""
|
|
107
|
+
new_block = new_block.strip()
|
|
108
|
+
|
|
109
|
+
# 1. Markers present → surgical in-place replacement.
|
|
110
|
+
if C3_BLOCK_BEGIN in existing and C3_BLOCK_END in existing:
|
|
111
|
+
start = existing.index(C3_BLOCK_BEGIN)
|
|
112
|
+
end = existing.index(C3_BLOCK_END) + len(C3_BLOCK_END)
|
|
113
|
+
before = existing[:start].rstrip()
|
|
114
|
+
after = existing[end:].lstrip()
|
|
115
|
+
parts = [p for p in (before, new_block, after) if p]
|
|
116
|
+
return "\n\n".join(parts) + "\n"
|
|
117
|
+
|
|
118
|
+
# 2. Legacy marker-less C3 doc → replace head, keep trailing user notes.
|
|
119
|
+
if existing.lstrip().startswith(C3_LEGACY_FIRST_LINE):
|
|
120
|
+
tail = ""
|
|
121
|
+
if "# User Notes" in existing:
|
|
122
|
+
tail = existing[existing.index("# User Notes"):].strip()
|
|
123
|
+
parts = [new_block] + ([tail] if tail else [])
|
|
124
|
+
return "\n\n".join(parts) + "\n"
|
|
125
|
+
|
|
126
|
+
# 3. Genuine user-authored file → preserve fully, append the C3 block.
|
|
127
|
+
head = existing.rstrip()
|
|
128
|
+
parts = [head, new_block] if head else [new_block]
|
|
129
|
+
return "\n\n".join(parts) + "\n"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def write_c3_instruction_doc(path, content: str) -> str:
|
|
133
|
+
"""Write a C3-generated instruction doc without clobbering user content.
|
|
134
|
+
|
|
135
|
+
Wraps ``content`` in the C3 managed block and merges it into any existing
|
|
136
|
+
file via :func:`merge_c3_block`. Returns the exact text written to disk.
|
|
137
|
+
"""
|
|
138
|
+
p = Path(path)
|
|
139
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
block = wrap_c3_block(content)
|
|
141
|
+
if p.exists():
|
|
142
|
+
existing = p.read_text(encoding="utf-8", errors="replace")
|
|
143
|
+
final = merge_c3_block(existing, block)
|
|
144
|
+
else:
|
|
145
|
+
final = block.rstrip() + "\n"
|
|
146
|
+
p.write_text(final, encoding="utf-8")
|
|
147
|
+
return final
|
|
148
|
+
|
|
149
|
+
|
|
69
150
|
class ClaudeMdManager:
|
|
70
151
|
"""Manages instructions file generation, analysis, compaction, and insight promotion.
|
|
71
152
|
|
|
@@ -285,14 +366,17 @@ class ClaudeMdManager:
|
|
|
285
366
|
}
|
|
286
367
|
|
|
287
368
|
def compact(self, target_lines: int = 150) -> dict:
|
|
288
|
-
"""Compact existing CLAUDE.md to fit within target line count.
|
|
369
|
+
"""Compact existing CLAUDE.md to fit within target line count.
|
|
370
|
+
|
|
371
|
+
When the file uses the C3 managed-block markers, only the inner C3
|
|
372
|
+
body is compacted and the block is re-wrapped, so the markers, the
|
|
373
|
+
``# C3`` heading, and any user content outside the block survive.
|
|
374
|
+
"""
|
|
289
375
|
current = self._read_current()
|
|
290
376
|
if current is None:
|
|
291
377
|
return {"error": f"No {self.instructions_file} found on disk. Use CLI `c3 claudemd generate` to preview, then `c3 claudemd save` to persist before compacting."}
|
|
292
378
|
|
|
293
379
|
original_metrics = self._count_metrics(current)
|
|
294
|
-
sections = self._parse_sections(current)
|
|
295
|
-
lines = current.split('\n')
|
|
296
380
|
|
|
297
381
|
# If already under target, no compaction needed
|
|
298
382
|
if original_metrics["lines"] <= target_lines:
|
|
@@ -305,6 +389,54 @@ class ClaudeMdManager:
|
|
|
305
389
|
"actions": ["Already under target — no compaction needed."],
|
|
306
390
|
}
|
|
307
391
|
|
|
392
|
+
# Isolate the C3 managed block so its markers, the # C3 heading, and
|
|
393
|
+
# any surrounding user content are preserved verbatim across compaction.
|
|
394
|
+
before = after = ""
|
|
395
|
+
inner = current
|
|
396
|
+
has_block = C3_BLOCK_BEGIN in current and C3_BLOCK_END in current
|
|
397
|
+
if has_block:
|
|
398
|
+
start = current.index(C3_BLOCK_BEGIN)
|
|
399
|
+
end = current.index(C3_BLOCK_END) + len(C3_BLOCK_END)
|
|
400
|
+
before = current[:start]
|
|
401
|
+
after = current[end:]
|
|
402
|
+
inner = current[start + len(C3_BLOCK_BEGIN):end - len(C3_BLOCK_END)].strip()
|
|
403
|
+
if inner.startswith(C3_BLOCK_HEADING):
|
|
404
|
+
inner = inner[len(C3_BLOCK_HEADING):].lstrip("\n")
|
|
405
|
+
|
|
406
|
+
compacted_inner, actions = self._compact_sections(inner, target_lines)
|
|
407
|
+
|
|
408
|
+
if has_block:
|
|
409
|
+
pieces = []
|
|
410
|
+
if before.strip():
|
|
411
|
+
pieces.append(before.strip())
|
|
412
|
+
pieces.append(wrap_c3_block(compacted_inner))
|
|
413
|
+
if after.strip():
|
|
414
|
+
pieces.append(after.strip())
|
|
415
|
+
content = "\n\n".join(pieces) + "\n"
|
|
416
|
+
else:
|
|
417
|
+
content = compacted_inner
|
|
418
|
+
|
|
419
|
+
compacted_metrics = self._count_metrics(content)
|
|
420
|
+
|
|
421
|
+
if not actions:
|
|
422
|
+
actions.append("No compaction opportunities found.")
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
"content": content,
|
|
426
|
+
"original_lines": original_metrics["lines"],
|
|
427
|
+
"compacted_lines": compacted_metrics["lines"],
|
|
428
|
+
"original_tokens": original_metrics["tokens"],
|
|
429
|
+
"compacted_tokens": compacted_metrics["tokens"],
|
|
430
|
+
"actions": actions,
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
def _compact_sections(self, text: str, target_lines: int) -> tuple:
|
|
434
|
+
"""Section-based compaction of an instruction-doc body.
|
|
435
|
+
|
|
436
|
+
Returns ``(content, actions)``. Operates purely on ``text``; callers
|
|
437
|
+
handle any C3 managed-block wrapping.
|
|
438
|
+
"""
|
|
439
|
+
sections = self._parse_sections(text)
|
|
308
440
|
actions = []
|
|
309
441
|
|
|
310
442
|
# Step 1: Compress session history — keep last 3, one-line summaries
|
|
@@ -318,12 +450,12 @@ class ClaudeMdManager:
|
|
|
318
450
|
# Step 2: Deduplicate — remove exact duplicate lines (excluding blank lines and headers)
|
|
319
451
|
seen_lines = set()
|
|
320
452
|
deduped_sections = {}
|
|
321
|
-
for name,
|
|
453
|
+
for name, sect_text in sections.items():
|
|
322
454
|
if name in ("User Notes", "C3 — Token-Saving Workflow (MUST FOLLOW)"):
|
|
323
|
-
deduped_sections[name] =
|
|
455
|
+
deduped_sections[name] = sect_text
|
|
324
456
|
continue
|
|
325
457
|
new_lines = []
|
|
326
|
-
for line in
|
|
458
|
+
for line in sect_text.split('\n'):
|
|
327
459
|
stripped = line.strip()
|
|
328
460
|
if not stripped or stripped.startswith('#'):
|
|
329
461
|
new_lines.append(line)
|
|
@@ -350,20 +482,7 @@ class ClaudeMdManager:
|
|
|
350
482
|
actions.append("Reduced project structure tree depth")
|
|
351
483
|
|
|
352
484
|
# Reassemble
|
|
353
|
-
|
|
354
|
-
compacted_metrics = self._count_metrics(content)
|
|
355
|
-
|
|
356
|
-
if not actions:
|
|
357
|
-
actions.append("No compaction opportunities found.")
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
"content": content,
|
|
361
|
-
"original_lines": original_metrics["lines"],
|
|
362
|
-
"compacted_lines": compacted_metrics["lines"],
|
|
363
|
-
"original_tokens": original_metrics["tokens"],
|
|
364
|
-
"compacted_tokens": compacted_metrics["tokens"],
|
|
365
|
-
"actions": actions,
|
|
366
|
-
}
|
|
485
|
+
return self._reassemble_sections(sections), actions
|
|
367
486
|
|
|
368
487
|
def get_promotion_candidates(self, min_relevance: int = 2) -> dict:
|
|
369
488
|
"""Find facts and patterns worth promoting into CLAUDE.md."""
|
services/session_manager.py
CHANGED
|
@@ -465,19 +465,14 @@ class SessionManager:
|
|
|
465
465
|
else:
|
|
466
466
|
content = auto_content
|
|
467
467
|
|
|
468
|
+
# Wrap C3-generated content in the managed block so regenerating the
|
|
469
|
+
# doc never clobbers user-written content outside it (mirrors global
|
|
470
|
+
# ~/.claude/CLAUDE.md). Legacy files and bare user files are preserved.
|
|
471
|
+
from services.claude_md import write_c3_instruction_doc
|
|
472
|
+
|
|
468
473
|
output_path = self.project_path / instructions_file
|
|
469
|
-
output_path
|
|
470
|
-
|
|
471
|
-
# Check for existing file
|
|
472
|
-
if output_path.exists():
|
|
473
|
-
existing = output_path.read_text(encoding="utf-8")
|
|
474
|
-
# Preserve user-written sections
|
|
475
|
-
if "# User Notes" in existing:
|
|
476
|
-
user_section = existing[existing.index("# User Notes"):]
|
|
477
|
-
content += f"\n\n{user_section}"
|
|
478
|
-
|
|
479
|
-
output_path.write_text(content, encoding="utf-8")
|
|
480
|
-
tokens = count_tokens(content)
|
|
474
|
+
final = write_c3_instruction_doc(output_path, content)
|
|
475
|
+
tokens = count_tokens(final)
|
|
481
476
|
|
|
482
477
|
return {
|
|
483
478
|
"path": str(output_path),
|
|
File without changes
|
{code_context_control-2.36.0.dist-info → code_context_control-2.37.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{code_context_control-2.36.0.dist-info → code_context_control-2.37.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{code_context_control-2.36.0.dist-info → code_context_control-2.37.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|