code-context-control 2.145.2__py3-none-any.whl → 2.146.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 +3 -3
- cli/hub_server.py +28 -23
- cli/hub_ui/app.js +4 -1
- cli/hub_ui/components/hub_credentials.js +21 -14
- cli/hub_ui/state.js +7 -3
- cli/hub_ui.html +1 -1
- cli/server.py +4 -4
- cli/tools/credentials.py +1 -2
- cli/ui.html +1 -1
- {code_context_control-2.145.2.dist-info → code_context_control-2.146.0.dist-info}/METADATA +1 -1
- {code_context_control-2.145.2.dist-info → code_context_control-2.146.0.dist-info}/RECORD +22 -22
- services/activity_log.py +43 -15
- services/ci_act.py +22 -1
- services/ci_workflow.py +5 -2
- services/credential_backup.py +9 -3
- services/credential_store.py +72 -10
- services/project_manager.py +9 -0
- services/session_catalog.py +82 -34
- {code_context_control-2.145.2.dist-info → code_context_control-2.146.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.145.2.dist-info → code_context_control-2.146.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.145.2.dist-info → code_context_control-2.146.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.145.2.dist-info → code_context_control-2.146.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.146.0"
|
|
95
95
|
|
|
96
96
|
# The PreToolUse matcher for native subagent calls (installer and hub migration).
|
|
97
97
|
AGENT_MATCHER = "Agent|Task"
|
|
@@ -6685,10 +6685,10 @@ def _creds_cmd_list(args, project_path: str) -> None:
|
|
|
6685
6685
|
"(add --global for all projects).")
|
|
6686
6686
|
return
|
|
6687
6687
|
print(f"{len(entries)} credential(s) — project scope shadows global:")
|
|
6688
|
+
present = cred_store.presence_for(entries, project_path)
|
|
6688
6689
|
missing = 0
|
|
6689
6690
|
for name, entry in entries.items():
|
|
6690
|
-
gone = not
|
|
6691
|
-
name, project_path=project_path, scope=entry["scope"])
|
|
6691
|
+
gone = not present[name]
|
|
6692
6692
|
missing += gone
|
|
6693
6693
|
print(" " + _creds_entry_line(name, entry, gone))
|
|
6694
6694
|
if missing:
|
cli/hub_server.py
CHANGED
|
@@ -1804,7 +1804,7 @@ def api_projects_browse():
|
|
|
1804
1804
|
return jsonify({"error": str(e)}), 400
|
|
1805
1805
|
if not resolved.is_dir():
|
|
1806
1806
|
return jsonify({"error": f"Not a directory: {resolved}"}), 404
|
|
1807
|
-
registered = {os.path.normcase(p.get("path", "")) for p in _pm().
|
|
1807
|
+
registered = {os.path.normcase(p.get("path", "")) for p in _pm().list_registered()}
|
|
1808
1808
|
dirs = []
|
|
1809
1809
|
try:
|
|
1810
1810
|
for child in sorted(resolved.iterdir(), key=lambda c: c.name.lower()):
|
|
@@ -1913,7 +1913,7 @@ def api_search_global():
|
|
|
1913
1913
|
if wanted else None)
|
|
1914
1914
|
|
|
1915
1915
|
candidates, skipped = [], []
|
|
1916
|
-
for p in _pm().
|
|
1916
|
+
for p in _pm().list_registered():
|
|
1917
1917
|
ppath = p.get("path") or ""
|
|
1918
1918
|
if wanted_norm is not None and os.path.normcase(ppath) not in wanted_norm:
|
|
1919
1919
|
continue
|
|
@@ -2138,14 +2138,15 @@ def api_projects_credentials():
|
|
|
2138
2138
|
usage = cred_store.read_usage_state(str(resolved))
|
|
2139
2139
|
home = cred_store.global_base()
|
|
2140
2140
|
global_names = set(cred_store.list_entries(str(home))) if home else set()
|
|
2141
|
+
listed = cred_store.list_entries(str(resolved))
|
|
2142
|
+
present = cred_store.presence_for(listed, str(resolved))
|
|
2141
2143
|
entries = []
|
|
2142
|
-
for name, entry in
|
|
2144
|
+
for name, entry in listed.items():
|
|
2143
2145
|
entries.append(_cred_entry_public(
|
|
2144
2146
|
name, entry, usage=usage,
|
|
2145
2147
|
shadows_global=(entry.get("scope") == "project"
|
|
2146
2148
|
and name in global_names),
|
|
2147
|
-
value_ok=
|
|
2148
|
-
name, project_path=str(resolved), scope=entry["scope"])))
|
|
2149
|
+
value_ok=present[name]))
|
|
2149
2150
|
return jsonify({"path": str(resolved), "entries": entries})
|
|
2150
2151
|
|
|
2151
2152
|
|
|
@@ -2360,7 +2361,7 @@ def api_hub_credentials_audit():
|
|
|
2360
2361
|
only_global = (request.args.get("scope") or "").strip().lower() == "global"
|
|
2361
2362
|
|
|
2362
2363
|
events, errors = [], []
|
|
2363
|
-
for p in ([] if only_global else _pm().
|
|
2364
|
+
for p in ([] if only_global else _pm().list_registered()):
|
|
2364
2365
|
ppath = str(p.get("path") or "")
|
|
2365
2366
|
if not (Path(ppath) / ".c3").is_dir():
|
|
2366
2367
|
continue
|
|
@@ -2417,9 +2418,12 @@ def api_hub_credentials_overview():
|
|
|
2417
2418
|
from services import credential_store as cred_store
|
|
2418
2419
|
home = cred_store.global_base()
|
|
2419
2420
|
global_entries = cred_store.list_entries(str(home)) if home else {}
|
|
2421
|
+
# realm -> {name: value decodes}, probed once per vault and shared with
|
|
2422
|
+
# the backup banner, which asks the same question of the same entries.
|
|
2423
|
+
present = {"global": cred_store.value_presence("global", str(home))} if home else {}
|
|
2420
2424
|
shadowed_in = {name: [] for name in global_entries}
|
|
2421
2425
|
projects_out = []
|
|
2422
|
-
for p in _pm().
|
|
2426
|
+
for p in _pm().list_registered():
|
|
2423
2427
|
ppath = str(p.get("path") or "")
|
|
2424
2428
|
row = {"name": p.get("name") or "", "path": ppath,
|
|
2425
2429
|
"initialized": True, "error": None, "entries": []}
|
|
@@ -2428,14 +2432,15 @@ def api_hub_credentials_overview():
|
|
|
2428
2432
|
row["initialized"] = False
|
|
2429
2433
|
else:
|
|
2430
2434
|
usage = cred_store.read_usage_state(ppath)
|
|
2435
|
+
ok = cred_store.value_presence("project", ppath)
|
|
2436
|
+
present[cred_store.realm("project", ppath)] = ok
|
|
2431
2437
|
for name, entry in cred_store.list_entries(ppath).items():
|
|
2432
2438
|
if entry.get("scope") != "project":
|
|
2433
2439
|
continue
|
|
2434
2440
|
row["entries"].append(_cred_entry_public(
|
|
2435
2441
|
name, entry, usage=usage,
|
|
2436
2442
|
shadows_global=name in global_entries,
|
|
2437
|
-
value_ok=
|
|
2438
|
-
name, project_path=ppath, scope="project")))
|
|
2443
|
+
value_ok=ok.get(name, False)))
|
|
2439
2444
|
if name in shadowed_in:
|
|
2440
2445
|
shadowed_in[name].append(
|
|
2441
2446
|
{"name": row["name"], "path": ppath})
|
|
@@ -2446,21 +2451,21 @@ def api_hub_credentials_overview():
|
|
|
2446
2451
|
global_out = [
|
|
2447
2452
|
{**_cred_entry_public(
|
|
2448
2453
|
name, entry, usage=global_usage,
|
|
2449
|
-
value_ok=
|
|
2450
|
-
name, project_path=str(home), scope="global")),
|
|
2454
|
+
value_ok=present.get("global", {}).get(name, False)),
|
|
2451
2455
|
"shadowed_in": shadowed_in.get(name, [])}
|
|
2452
2456
|
for name, entry in global_entries.items()
|
|
2453
2457
|
]
|
|
2454
2458
|
return jsonify({"global": {"entries": global_out}, "projects": projects_out,
|
|
2455
2459
|
"backup": _cred_backup_summary(
|
|
2456
|
-
[row["path"] for row in projects_out if row["initialized"]]
|
|
2460
|
+
[row["path"] for row in projects_out if row["initialized"]],
|
|
2461
|
+
present)})
|
|
2457
2462
|
|
|
2458
2463
|
|
|
2459
|
-
def _cred_backup_summary(project_paths) -> dict:
|
|
2464
|
+
def _cred_backup_summary(project_paths, present) -> dict:
|
|
2460
2465
|
"""Backup state as counts for the Hub banner; a broken file reports its error."""
|
|
2461
2466
|
from services import credential_backup
|
|
2462
2467
|
try:
|
|
2463
|
-
st = credential_backup.status(project_paths)
|
|
2468
|
+
st = credential_backup.status(project_paths, presence=present)
|
|
2464
2469
|
except credential_backup.BackupError as exc:
|
|
2465
2470
|
return {"enabled": True, "error": str(exc), "restorable": 0, "lost": 0}
|
|
2466
2471
|
return {"enabled": st["enabled"], "restorable": len(st["restorable"]),
|
|
@@ -2612,7 +2617,7 @@ def api_hub_tokens_overview():
|
|
|
2612
2617
|
days = _hub_tokens_days(request.args.get("days"))
|
|
2613
2618
|
rows, totals = [], {"tool_calls": 0, "tool_tokens": 0,
|
|
2614
2619
|
"session_tokens": 0, "saved": 0}
|
|
2615
|
-
for p in _pm().
|
|
2620
|
+
for p in _pm().list_registered():
|
|
2616
2621
|
ppath = str(p.get("path") or "")
|
|
2617
2622
|
row = {"name": p.get("name") or "", "path": ppath, "initialized": True,
|
|
2618
2623
|
"error": None, "tool_calls": 0, "tool_tokens": 0,
|
|
@@ -2662,7 +2667,7 @@ def api_hub_locks_overview():
|
|
|
2662
2667
|
from services import agent_locks as al
|
|
2663
2668
|
|
|
2664
2669
|
rows, total = [], 0
|
|
2665
|
-
for p in _pm().
|
|
2670
|
+
for p in _pm().list_registered():
|
|
2666
2671
|
ppath = str(p.get("path") or "")
|
|
2667
2672
|
row = {"name": p.get("name") or "", "path": ppath, "initialized": True,
|
|
2668
2673
|
"error": None, "enabled": True, "mode": "advisory",
|
|
@@ -2752,7 +2757,7 @@ def api_hub_enforcement_overview():
|
|
|
2752
2757
|
|
|
2753
2758
|
rows = []
|
|
2754
2759
|
totals = {"discipline": 0, "access": 0}
|
|
2755
|
-
for p in _pm().
|
|
2760
|
+
for p in _pm().list_registered():
|
|
2756
2761
|
ppath = str(p.get("path") or "")
|
|
2757
2762
|
row = {"name": p.get("name") or "", "path": ppath, "initialized": True,
|
|
2758
2763
|
"error": None, "mode": None, "scope": "", "set_by": "",
|
|
@@ -3428,7 +3433,7 @@ def api_hub_override_policy_overview():
|
|
|
3428
3433
|
from services import override_policy as opol
|
|
3429
3434
|
|
|
3430
3435
|
rows = []
|
|
3431
|
-
for p in _pm().
|
|
3436
|
+
for p in _pm().list_registered():
|
|
3432
3437
|
ppath = str(p.get("path") or "")
|
|
3433
3438
|
row = {"name": p.get("name") or Path(ppath).name, "path": ppath,
|
|
3434
3439
|
"initialized": True, "error": None, "policy": None,
|
|
@@ -3740,7 +3745,7 @@ def api_hub_access_overview():
|
|
|
3740
3745
|
empty = {"deny": [], "read_only": [], "confirm": [], "mask": 0,
|
|
3741
3746
|
"corrupt": False}
|
|
3742
3747
|
rows = []
|
|
3743
|
-
for p in _pm().
|
|
3748
|
+
for p in _pm().list_registered():
|
|
3744
3749
|
ppath = str(p.get("path") or "")
|
|
3745
3750
|
row = {"name": p.get("name") or Path(ppath).name, "path": ppath,
|
|
3746
3751
|
"initialized": True, "error": None, "rules": dict(empty)}
|
|
@@ -4268,7 +4273,7 @@ def api_pm_global():
|
|
|
4268
4273
|
from services.task_store import TaskStore
|
|
4269
4274
|
|
|
4270
4275
|
tasks, skipped, by_project = [], [], {}
|
|
4271
|
-
entries = _pm().
|
|
4276
|
+
entries = _pm().list_registered()
|
|
4272
4277
|
for p in entries:
|
|
4273
4278
|
ppath = p.get("path") or ""
|
|
4274
4279
|
if not Path(ppath).is_dir():
|
|
@@ -4808,7 +4813,7 @@ def _session_project(raw: str):
|
|
|
4808
4813
|
if not raw:
|
|
4809
4814
|
return None
|
|
4810
4815
|
want = os.path.normcase(os.path.normpath(raw))
|
|
4811
|
-
for p in _pm().
|
|
4816
|
+
for p in _pm().list_registered():
|
|
4812
4817
|
path = str(p.get("path") or "")
|
|
4813
4818
|
if path and os.path.normcase(os.path.normpath(path)) == want:
|
|
4814
4819
|
return {"name": str(p.get("name") or Path(path).name), "path": path}
|
|
@@ -4847,7 +4852,7 @@ def api_hub_sessions_overview():
|
|
|
4847
4852
|
return row
|
|
4848
4853
|
|
|
4849
4854
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
4850
|
-
rows = list(pool.map(one, _pm().
|
|
4855
|
+
rows = list(pool.map(one, _pm().list_registered()))
|
|
4851
4856
|
rows.sort(key=lambda r: r.get("last_active") or "", reverse=True)
|
|
4852
4857
|
return jsonify({"projects": rows, "features": _SESSIONS_FEATURES})
|
|
4853
4858
|
|
|
@@ -4876,7 +4881,7 @@ def api_hub_sessions_list():
|
|
|
4876
4881
|
res["errors"] = []
|
|
4877
4882
|
else:
|
|
4878
4883
|
projects = [{"name": str(p.get("name") or ""), "path": str(p.get("path") or "")}
|
|
4879
|
-
for p in _pm().
|
|
4884
|
+
for p in _pm().list_registered()]
|
|
4880
4885
|
res = sc.list_many(projects, stale=stale, q=q, limit=limit, before=before)
|
|
4881
4886
|
return jsonify(res)
|
|
4882
4887
|
except Exception as e:
|
cli/hub_ui/app.js
CHANGED
|
@@ -50,7 +50,10 @@ function App() {
|
|
|
50
50
|
}, []);
|
|
51
51
|
|
|
52
52
|
useEffect(() => { loadConfig(); loadProjects(); }, []);
|
|
53
|
-
|
|
53
|
+
// Live status matters on the project list and in a drill-in. Elsewhere it
|
|
54
|
+
// only refreshes the sidebar and top-bar counts, which may lag 30 s, and
|
|
55
|
+
// each poll reads every project's activity log.
|
|
56
|
+
usePoll(loadProjects, mainView === 'projects' || drill ? 5000 : 30000);
|
|
54
57
|
|
|
55
58
|
// Persisted preferences
|
|
56
59
|
const toggleTheme = () => {
|
|
@@ -1381,7 +1381,10 @@ function CredSourcesStrip({ sources, onResync, busy }) {
|
|
|
1381
1381
|
|
|
1382
1382
|
// path=null → the global vault (~/.c3): scope locked to 'global'.
|
|
1383
1383
|
// path=string → that project's merged view (global entries + project shadows).
|
|
1384
|
-
|
|
1384
|
+
// Global vault (path null): rows arrive as `globalEntries` (null while the
|
|
1385
|
+
// page's overview loads) and `onChanged` refills them; loading the overview
|
|
1386
|
+
// here as well fetched the whole cross-project index twice.
|
|
1387
|
+
function CredsManager({ path, projectName, onChanged, bindSlash, globalEntries }) {
|
|
1385
1388
|
const isGlobal = !path;
|
|
1386
1389
|
const [entries, setEntries] = useState([]);
|
|
1387
1390
|
const [loading, setLoading] = useState(true);
|
|
@@ -1408,14 +1411,10 @@ function CredsManager({ path, projectName, onChanged, bindSlash }) {
|
|
|
1408
1411
|
const withPath = (obj) => (path ? Object.assign({ path }, obj) : obj);
|
|
1409
1412
|
|
|
1410
1413
|
const load = useCallback(async () => {
|
|
1414
|
+
if (!path) return;
|
|
1411
1415
|
try {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
setEntries((data && data.entries) || []);
|
|
1415
|
-
} else {
|
|
1416
|
-
const data = await api.get('/api/hub/credentials/overview');
|
|
1417
|
-
setEntries((((data || {}).global) || {}).entries || []);
|
|
1418
|
-
}
|
|
1416
|
+
const data = await credApi.listProject(path);
|
|
1417
|
+
setEntries((data && data.entries) || []);
|
|
1419
1418
|
setError('');
|
|
1420
1419
|
} catch (e) { setError(String(e)); }
|
|
1421
1420
|
setLoading(false);
|
|
@@ -1426,6 +1425,14 @@ function CredsManager({ path, projectName, onChanged, bindSlash }) {
|
|
|
1426
1425
|
load();
|
|
1427
1426
|
}, [load]);
|
|
1428
1427
|
|
|
1428
|
+
useEffect(() => {
|
|
1429
|
+
if (path) return;
|
|
1430
|
+
setEntries(globalEntries || []);
|
|
1431
|
+
setLoading(globalEntries == null);
|
|
1432
|
+
}, [path, globalEntries]);
|
|
1433
|
+
|
|
1434
|
+
const refresh = () => { load(); if (onChanged) onChanged(); };
|
|
1435
|
+
|
|
1429
1436
|
// `/` focuses the filter, as it does on every other hub board — but only
|
|
1430
1437
|
// where nothing above already claims the key. The hub Credentials page owns
|
|
1431
1438
|
// `/` for its cross-project search, so the manager binds it solely when
|
|
@@ -1447,7 +1454,7 @@ function CredsManager({ path, projectName, onChanged, bindSlash }) {
|
|
|
1447
1454
|
return () => window.removeEventListener('keydown', onKey);
|
|
1448
1455
|
}, [bindSlash, drawer, menu, confirm]);
|
|
1449
1456
|
|
|
1450
|
-
const done = (msg) => { notify(msg);
|
|
1457
|
+
const done = (msg) => { notify(msg); refresh(); };
|
|
1451
1458
|
|
|
1452
1459
|
const saveForm = async () => {
|
|
1453
1460
|
if (!form || !form.name.trim()) return;
|
|
@@ -1507,8 +1514,7 @@ function CredsManager({ path, projectName, onChanged, bindSlash }) {
|
|
|
1507
1514
|
if (resp && resp.error) setError(resp.error);
|
|
1508
1515
|
else {
|
|
1509
1516
|
notify(`${field === 'inject' ? 'Auto-inject' : 'Agent access'} ${next ? 'enabled' : 'disabled'} for '${entry.name}'`);
|
|
1510
|
-
|
|
1511
|
-
if (onChanged) onChanged();
|
|
1517
|
+
refresh();
|
|
1512
1518
|
}
|
|
1513
1519
|
} catch (e) { setError(apiErr(e)); }
|
|
1514
1520
|
};
|
|
@@ -1619,7 +1625,7 @@ function CredsManager({ path, projectName, onChanged, bindSlash }) {
|
|
|
1619
1625
|
} else {
|
|
1620
1626
|
notify(`${label}: ${ok} ${ok === 1 ? 'entry' : 'entries'}`);
|
|
1621
1627
|
}
|
|
1622
|
-
if (action !== 'check') { exitSelect();
|
|
1628
|
+
if (action !== 'check') { exitSelect(); refresh(); }
|
|
1623
1629
|
} catch (e) { notify(apiErr(e), 'err'); }
|
|
1624
1630
|
setBulkBusy(false);
|
|
1625
1631
|
};
|
|
@@ -2012,7 +2018,7 @@ function CredsManager({ path, projectName, onChanged, bindSlash }) {
|
|
|
2012
2018
|
<CredDrawer entry={drawer} path={path} projectName={projectName}
|
|
2013
2019
|
initialReplace={replaceOnOpen}
|
|
2014
2020
|
onClose={() => { setDrawer(null); setReplaceOnOpen(false); }}
|
|
2015
|
-
onChanged={
|
|
2021
|
+
onChanged={refresh} />
|
|
2016
2022
|
)}
|
|
2017
2023
|
{confirm && <CredConfirm spec={confirm} onClose={() => setConfirm(null)} />}
|
|
2018
2024
|
</div>
|
|
@@ -2420,7 +2426,8 @@ function HubCredentials({ projects, onOpenDrill }) {
|
|
|
2420
2426
|
<CredAuditView path={null} initialName={auditName}
|
|
2421
2427
|
onOpenDrill={onOpenDrill} />
|
|
2422
2428
|
) : sub === 'global' ? (
|
|
2423
|
-
<CredsManager path={null} onChanged={loadOverview}
|
|
2429
|
+
<CredsManager path={null} onChanged={loadOverview}
|
|
2430
|
+
globalEntries={ov ? ((ov.global || {}).entries || []) : null} />
|
|
2424
2431
|
) : !ov ? (
|
|
2425
2432
|
<div style={{ color: T.textMuted, fontSize: 13 }}>Loading…</div>
|
|
2426
2433
|
) : (ov.projects || []).length === 0 ? (
|
cli/hub_ui/state.js
CHANGED
|
@@ -24,11 +24,15 @@ const IDE_OPTIONS = [
|
|
|
24
24
|
|
|
25
25
|
const ideLabel = (ide) => IDE_LABELS[ide] || ide || 'unknown';
|
|
26
26
|
|
|
27
|
-
// Repeating poll that pauses while the tab is hidden
|
|
27
|
+
// Repeating poll that pauses while the tab is hidden and skips a tick while
|
|
28
|
+
// the previous call is still running, so a slow route never stacks requests.
|
|
28
29
|
const usePoll = (fn, ms) => {
|
|
29
30
|
React.useEffect(() => {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
let running = false;
|
|
32
|
+
const iv = setInterval(async () => {
|
|
33
|
+
if (document.hidden || running) return;
|
|
34
|
+
running = true;
|
|
35
|
+
try { await fn(); } finally { running = false; }
|
|
32
36
|
}, ms);
|
|
33
37
|
return () => clearInterval(iv);
|
|
34
38
|
}, [fn, ms]);
|
cli/hub_ui.html
CHANGED
cli/server.py
CHANGED
|
@@ -2764,12 +2764,12 @@ def api_credentials_list():
|
|
|
2764
2764
|
from services import credential_store as cred_store
|
|
2765
2765
|
pp = str(PROJECT_PATH)
|
|
2766
2766
|
usage = cred_store.read_usage_state(pp)
|
|
2767
|
+
listed = cred_store.list_entries(pp)
|
|
2768
|
+
present = cred_store.presence_for(listed, pp)
|
|
2767
2769
|
out = []
|
|
2768
|
-
for name, entry in
|
|
2770
|
+
for name, entry in listed.items():
|
|
2769
2771
|
out.append(cred_store.public_entry(
|
|
2770
|
-
name, entry, usage=usage,
|
|
2771
|
-
value_ok=cred_store.is_resolvable(
|
|
2772
|
-
name, project_path=pp, scope=entry["scope"])))
|
|
2772
|
+
name, entry, usage=usage, value_ok=present[name]))
|
|
2773
2773
|
return jsonify({"entries": out})
|
|
2774
2774
|
|
|
2775
2775
|
|
cli/tools/credentials.py
CHANGED
|
@@ -98,8 +98,7 @@ def _act_list(project_path: str) -> str:
|
|
|
98
98
|
"The user manages them via the Credentials UI tab or `c3 creds set`."
|
|
99
99
|
)
|
|
100
100
|
usage = cs.read_usage_state(project_path)
|
|
101
|
-
missing = {n for n,
|
|
102
|
-
n, project_path=project_path, scope=e["scope"])}
|
|
101
|
+
missing = {n for n, ok in cs.presence_for(entries, project_path).items() if not ok}
|
|
103
102
|
lines = [f"[creds] {len(entries)} entries (project scope shadows global):"]
|
|
104
103
|
lines += [_format_entry_line(n, e, usage, n in missing)
|
|
105
104
|
for n, e in entries.items()]
|
cli/ui.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.146.0
|
|
4
4
|
Summary: Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
|
|
2
2
|
cli/_hook_utils.py,sha256=RhfS2WS5VMSi2w2Jxcts1wg5WC-UFp89JOQvcO5Jjb8,23002
|
|
3
3
|
cli/_shell_writes.py,sha256=XzUNlUDljmg-wZxK03xHnRe36WeQxe4tpO9HKVFwP5o,5090
|
|
4
|
-
cli/c3.py,sha256=
|
|
4
|
+
cli/c3.py,sha256=AashzO8-7Jfq2V61RXFh2z5KfF5Oux5INUY-2s6IQVk,431810
|
|
5
5
|
cli/docs.html,sha256=pPxcP5ieY0DcfEDvegwIcRxbz1woLZoKLVSF-4JVy0Q,146784
|
|
6
6
|
cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
|
|
7
7
|
cli/hook_access_guard.py,sha256=Mjrrt3agYCWPqSjdzk_Sw4ooMrizO8Um-nQjgGfXck4,27896
|
|
@@ -23,13 +23,13 @@ cli/hook_session_end.py,sha256=toFEPDKRoutDuVOmuOgHmQ8CM0o7tF7yALhMxJz7VEM,3482
|
|
|
23
23
|
cli/hook_session_open.py,sha256=Badeikv37D7b6inSHbVjAxtfdH4xiJKFT1O52fgreGs,7443
|
|
24
24
|
cli/hook_session_stats.py,sha256=I-2NeLlV1NxeqKtTM2JH9haSpFU7r4qG0u8pfCJ80YU,8725
|
|
25
25
|
cli/hook_terse_advisor.py,sha256=W_zaTfX2akgX-ifuUTsLu2H1atM8XmWt6jpPoeU7Pm0,6252
|
|
26
|
-
cli/hub_server.py,sha256=
|
|
27
|
-
cli/hub_ui.html,sha256=
|
|
26
|
+
cli/hub_server.py,sha256=of3Auy9HFPSa1gDqrRITOfv29wz6uqi62Afb0iFW7uE,218145
|
|
27
|
+
cli/hub_ui.html,sha256=v41R_JAH4Jwodpy3tbeBUel-80FbNmffBrey-QH5Q64,2963
|
|
28
28
|
cli/mcp_proxy.py,sha256=r-Qhlie7gCtotex8yQUudCyBLRUK-IW6cNIW5RhBW0o,17648
|
|
29
29
|
cli/mcp_server.py,sha256=5y_oaJvvGq6p1C4L-4MamudyAhpJYeIjDJcXSeB9dsM,73775
|
|
30
30
|
cli/progress.py,sha256=qTkNy0howOD3hXraMo4ok3dYTpBW5yG_tenKfF4UDM0,1470
|
|
31
|
-
cli/server.py,sha256=
|
|
32
|
-
cli/ui.html,sha256=
|
|
31
|
+
cli/server.py,sha256=dJOOXYU1zmN-UBuMmHhv8XlnknSyO8xj7mXWSDAGIBY,173188
|
|
32
|
+
cli/ui.html,sha256=uuPwIJNKDQH_fsaR3xNB2AKky9WzKfaXnKr_k3aTtxo,5715
|
|
33
33
|
cli/ui_legacy.html,sha256=cI8tC6RKmE2NIJOcsu7CY-zT4VznjcbD6NTjxb_fvUY,378460
|
|
34
34
|
cli/ui_nano.html,sha256=UAwQ6bbTOXAoGq191AZ7slhngR9edJSa3IhqpynveDg,27740
|
|
35
35
|
cli/commands/__init__.py,sha256=0Z8MABNzwSFJGT4Xv9R5AJVR8XxraTsuVTz5b0bShmo,38
|
|
@@ -49,8 +49,8 @@ cli/guide/sessions.html,sha256=fBcbX61mvy2T5fX9Lkojb8Gw7n2RZ4Oo95EE_SC5Q6A,31707
|
|
|
49
49
|
cli/guide/shared.css,sha256=Mmm7W6aYxrkDq2RPOcyXiQFYGR-yB3auwX1uIWi6C74,15967
|
|
50
50
|
cli/guide/tools.html,sha256=8Kup2WXvZg2drI9vzG3ZxPKifN5VJuaC3ljd5oUbho8,133248
|
|
51
51
|
cli/guide/workflow.html,sha256=T_5HW4YOPng02vrxBWknz4LYSx2vC9Iogdwb81YlWqA,78604
|
|
52
|
-
cli/hub_ui/app.js,sha256=
|
|
53
|
-
cli/hub_ui/state.js,sha256=
|
|
52
|
+
cli/hub_ui/app.js,sha256=AWzlFht1Fmn4559KfooKC03CvoG2I6AXbYUEZ2w8ZfU,7362
|
|
53
|
+
cli/hub_ui/state.js,sha256=OrIQ_kx_wUdyiyXlKmMKC8bbKzMPve1ZuFClRXOrDEw,7593
|
|
54
54
|
cli/hub_ui/components/add_project.js,sha256=j5m0mX0gePcEYQW_0vodJkw5v8Vn2lQqPAI0miIGdv4,3182
|
|
55
55
|
cli/hub_ui/components/config_editor.js,sha256=xew4RoxFt3j3r0UTkVc3-7y2Z1cOjx3dgRZMKSsCFhg,18211
|
|
56
56
|
cli/hub_ui/components/drill_artifacts.js,sha256=cSD_Ph2HUoURI893i-JdfftsO9AKz_0TFH51s76B6x4,9468
|
|
@@ -63,7 +63,7 @@ cli/hub_ui/components/global_search.js,sha256=GPfq1EsZa73vXBNCVn4VhT3a7gYkaUblbc
|
|
|
63
63
|
cli/hub_ui/components/hub_access.js,sha256=AF00OgOGak4ASPE2OvxOsBqjS_KKd5QKCZZ6Kp3zGXw,34339
|
|
64
64
|
cli/hub_ui/components/hub_ci.js,sha256=KrdzALpgFTFcPEMN1R-OU0tyl8irsj91HECRlGsQLjQ,17873
|
|
65
65
|
cli/hub_ui/components/hub_cred_audit.js,sha256=fUmAPTOa75gdxAsw6cNHPYenu7jDTljSbFX9GYJnW14,11662
|
|
66
|
-
cli/hub_ui/components/hub_credentials.js,sha256=
|
|
66
|
+
cli/hub_ui/components/hub_credentials.js,sha256=xDqxDb0a0yaJQgTE-hEtx0PTOo4x9j__5GhdlxGd9mo,119643
|
|
67
67
|
cli/hub_ui/components/hub_enforcement.js,sha256=NuHl49KvKUjrYtfV6FAvfWiaFGq020wEHjHCx9AYhf4,48839
|
|
68
68
|
cli/hub_ui/components/hub_locks.js,sha256=v1yv7JH5anfC_kZo26I1Dyt1_ytamGOaBpCIWGziuVk,10321
|
|
69
69
|
cli/hub_ui/components/hub_sessions.js,sha256=Uo-U3T9ZIqSgiBkfNIWB3K2c-J-WBJApUU2IPSi5BZk,21401
|
|
@@ -87,7 +87,7 @@ cli/tools/artifacts.py,sha256=SjazShDFIzOpAZW7uMv9itBktXnc6pqQKZeaen8GgzI,9379
|
|
|
87
87
|
cli/tools/bitbucket.py,sha256=cni8Y1EoiNm9jcHEpzi2dgKFIT1-4ct0WOhpTs7ac3k,35812
|
|
88
88
|
cli/tools/ci.py,sha256=1--Ak18ikHkO7q4HWlDCVac-U969eEFejkd-24T0xuY,20057
|
|
89
89
|
cli/tools/compress.py,sha256=PcgxEw7KfofmJsTLfRf3mjWMxyNlqkcSNzMTlT27SW0,12534
|
|
90
|
-
cli/tools/credentials.py,sha256=
|
|
90
|
+
cli/tools/credentials.py,sha256=NczO0KT3LTJlD3ZedXWRAtH_8YTPhya0Txja3ystdzE,20039
|
|
91
91
|
cli/tools/delegate.py,sha256=TrHPXEyufQh0in3Ay7XcOG5wQwCYbnbz9r4naIuhbN8,126990
|
|
92
92
|
cli/tools/edit.py,sha256=xnRRM2UUybvGvfSzXzIjWmt6_zNaCbkupiK1z-8K4jE,30127
|
|
93
93
|
cli/tools/edit_verify.py,sha256=L1a6U9Z-pAOQz0Ur9SFTNfmCl0OP9_4VdkyxlXOyHDM,11910
|
|
@@ -131,7 +131,7 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
|
|
|
131
131
|
cli/ui/components/settings.js,sha256=FXcn-UQE2mXC6HFsLeEas-wAUFKt0OJaLySroZQMhGA,84095
|
|
132
132
|
cli/ui/components/sidebar.js,sha256=K2ym2kUgpbyG-EBA_wBIIiqQY8qTatsiBZwzseMWuIQ,9939
|
|
133
133
|
cli/ui/components/tasks.js,sha256=vyKQ3uwoppMwvdEaHlhWXW4oWcAisx4NveqzMhsYqHo,38438
|
|
134
|
-
code_context_control-2.
|
|
134
|
+
code_context_control-2.146.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
135
135
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
136
136
|
core/codex_patch.py,sha256=CxvT_rQIwPNTRJZj9EHJmx0ZdOeSLxjiixuLI3Z7lPc,2107
|
|
137
137
|
core/config.py,sha256=wCjrZKXRywUbR6WRcO7optyOirAQ2DeRNdPCU0X5zIY,23762
|
|
@@ -190,7 +190,7 @@ oracle/ui/chat/toolbar.js,sha256=ZDexIIpGwsQ6XsY4Tyo082rJoofdD_JBTV97-mLQUVM,120
|
|
|
190
190
|
services/__init__.py,sha256=3Kn4cZweLm7at8wFdBdZ-Zwo8hHcnVIsmY5f29nzi2Y,116
|
|
191
191
|
services/access_guard.py,sha256=tj-rWVlEzLIviIEXMRAYCWRlcgLPUwJ1qqQfmIA5t7Q,79074
|
|
192
192
|
services/access_telemetry.py,sha256=9pOCLS9LgDtM7Z4cCEyl7osxhj-m3wsPubcXolBKuV4,9568
|
|
193
|
-
services/activity_log.py,sha256=
|
|
193
|
+
services/activity_log.py,sha256=c_jTOsR-d1d5B3u67eg8mRA7LZhtP6H1-6leeGa0Go4,7990
|
|
194
194
|
services/agent_base.py,sha256=a-gdSd_jtZtbjXo1WS8CnWCagXgKaGZd5ShcG6s0kT4,4809
|
|
195
195
|
services/agent_downshift.py,sha256=hGFg-knqa6NUqmZUijyTDV82Fz88ieh1YjN_mG5tz8U,8444
|
|
196
196
|
services/agent_locks.py,sha256=nRAPuRSoet7BG9oOsNyA5WhlNy-UMhpsq6z5ggIPmLA,22421
|
|
@@ -204,7 +204,7 @@ services/background_service.py,sha256=Hn-vjAUq_wPm0cUiwwzyr-D7Xxkygjuy7s3_9XuLaO
|
|
|
204
204
|
services/benchmark_dashboard.py,sha256=iR-DnqnoKbqHMJ4d-ZkIvJBYfzwTa7r-jzO6j2BYDfQ,27711
|
|
205
205
|
services/bitbucket_client.py,sha256=2RehoyVP4vc52zjQi3Gdlt2WRAUpFv8zVMn12xx1yHE,22338
|
|
206
206
|
services/bitbucket_credentials.py,sha256=2qLA9pQMol4y95y4DJMNBsBBPUsJQCKbLFo2iiCnfvI,7364
|
|
207
|
-
services/ci_act.py,sha256=
|
|
207
|
+
services/ci_act.py,sha256=82i5aa1wgQvc8Xq_pQ9-k1_57LBQ0-mWSUT9xV-q7Tk,24070
|
|
208
208
|
services/ci_cache.py,sha256=Too3XyeE-oeiqQm4IJ10giFvhIEr29YfDLUpZDl8SBU,13256
|
|
209
209
|
services/ci_expr.py,sha256=BsCUoL4ZN1Q8KjYJ07xWsPynV04Q8dg-ryIIKNOYk70,15641
|
|
210
210
|
services/ci_failures.py,sha256=32aJ_R5S-JE4fErhzs06ZQzhBVLawZ4brtXtdoJrnE0,8087
|
|
@@ -212,7 +212,7 @@ services/ci_github.py,sha256=2yXsVAiWMIvhxH7xsMJIO_3DxCvZbKJcSWt4zXyyWxE,6699
|
|
|
212
212
|
services/ci_impact.py,sha256=9_CJv2sNza0_qW7cSsMnxhJy_AGflDONjL9jKQeoWmc,10966
|
|
213
213
|
services/ci_intel.py,sha256=frr1jL-FXU_JNUoRW8TwnZo04fqUElAGGEsDEvgIeUs,7161
|
|
214
214
|
services/ci_runner.py,sha256=S4xJakKb8ZNojtMKjmcqOkTL1NPEV8b_jThJEFR4mFs,51001
|
|
215
|
-
services/ci_workflow.py,sha256=
|
|
215
|
+
services/ci_workflow.py,sha256=vqUysN2d7-SHDLP3l8E1Z8xPdkVezd-GuxxmvRRUiC8,34996
|
|
216
216
|
services/circuit_breaker.py,sha256=ES6PpjL40gfDhtK88GeS6GFzsz5EOEEkZsB-DzX_tbw,3179
|
|
217
217
|
services/claude_md.py,sha256=fhRyJ1ueDKVbhNI-6U3W6Py5NAXngaBzxO4mTxvqEOo,62495
|
|
218
218
|
services/codex_integration.py,sha256=Zuyvb-nAMahZkNxl8oKA7bXs07xbQshG8HGY68ml2cg,7526
|
|
@@ -222,8 +222,8 @@ services/context_snapshot.py,sha256=2f_bxY3xX4ZC653ncYno8YSeSTFSavwBjrorytHA0Ns,
|
|
|
222
222
|
services/conversation_store.py,sha256=RKA0nk09VAI8Ip935kSVlQdF-cQEzFLrgHbZiEJ7G1E,37518
|
|
223
223
|
services/cred_audit.py,sha256=tdxnRzbB0c8nJ7dqA9HOJrcOeRWivEzq6yhwu6ksmZI,10240
|
|
224
224
|
services/cred_telemetry.py,sha256=LwDd4x0Mu6YXeDUrgICHnX9o1Ym_4hoouRp98qx0zlI,10926
|
|
225
|
-
services/credential_backup.py,sha256=
|
|
226
|
-
services/credential_store.py,sha256=
|
|
225
|
+
services/credential_backup.py,sha256=TeOAwDAEG-BLnpO0E861m3AeFSbD8tQ_q3ydNYcMDds,13223
|
|
226
|
+
services/credential_store.py,sha256=S5WnwPmaCJdo3wIZlWh7BRuT057pHPf1cdlzZOWPOgc,74627
|
|
227
227
|
services/delegate_agents.py,sha256=dIQAWepQgtDdV6s8WoCLvf7qAjlwMZHcVWWzsSO3qjE,4745
|
|
228
228
|
services/delegate_hints.py,sha256=h65rOfY7PJDl5C8Z4b-L4vGcGri0h4J7canL0QWYQ28,8341
|
|
229
229
|
services/delegate_write.py,sha256=6pKu6QTQ7JxRsOGoXPL_dMqDhbmEl5z-i42KI9Vpl2M,10164
|
|
@@ -274,7 +274,7 @@ services/override_policy.py,sha256=TehVPxfQ8LZWIOUzCnnioIdMUPZZ7JZa1ja77smpW2c,3
|
|
|
274
274
|
services/override_requests.py,sha256=H9lSUbfOjkjE1muztaQ58InqqsqhtnKIT-pF2VzvWkg,31148
|
|
275
275
|
services/override_wake.py,sha256=HjSOXQTDi0irgeMyrTqngL7xLcSOOIgYsVW9yxod4ps,10363
|
|
276
276
|
services/parser.py,sha256=S_YoKLS3r1xCUVHApXb3BTEfoRAzFhuo1_8XsG0qxVE,63505
|
|
277
|
-
services/project_manager.py,sha256=
|
|
277
|
+
services/project_manager.py,sha256=ibgNOqmNQBjdr_dXkbcsUdsnOriroYKW87wMnGZCovk,48376
|
|
278
278
|
services/project_runtime.py,sha256=yyftX48BFjq4B2KcR4KGszDAAa4xR6gn6j0SgTyro1s,11603
|
|
279
279
|
services/protocol.py,sha256=CWZ0BWWd-jPui-11hZlvGgkNk18gMFhhLWbp-BP35Kk,12861
|
|
280
280
|
services/proxy_state.py,sha256=u5rd0k6CrOsywZA8FpRu_hMLwhR0TAJhZjy5MdWbCGc,6107
|
|
@@ -288,7 +288,7 @@ services/router.py,sha256=Cz10nx2fKTbaGn14mSBePWIDrw5rdcs_1JFYXeik084,15626
|
|
|
288
288
|
services/runtime.py,sha256=931eux-zdsXh0M6i6RmXEGAKiXLfa9aDra9V12Q5Z_Y,13767
|
|
289
289
|
services/scanner.py,sha256=lH0aiDCdCRruKS-VTAD_LsF4zrhMLpMRhFlOZZlw4VM,9613
|
|
290
290
|
services/session_benchmark.py,sha256=KUcWWC5X088p_ao6DgglUMwj7jCzdecPKxJ7XiGKob0,106255
|
|
291
|
-
services/session_catalog.py,sha256=
|
|
291
|
+
services/session_catalog.py,sha256=H0TrxFQERg26B1Jgx_rwXHTO4lhXhAshj2oIvWHYqZg,46661
|
|
292
292
|
services/session_live.py,sha256=JixvsUdmcKGsuZGgWj-iRFJZbQDfQTWoVeIy1-M90A0,5146
|
|
293
293
|
services/session_manager.py,sha256=IBE71TORRlNuRBhswobD5KzXhfFAijy6KrnmaP1lnuc,57391
|
|
294
294
|
services/session_preloader.py,sha256=DsTAXMKVtrX9yu1sEFojYDi9-jkSAj1Ylt9JTy57Dow,9883
|
|
@@ -333,8 +333,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
333
333
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
334
334
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
335
335
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
336
|
-
code_context_control-2.
|
|
337
|
-
code_context_control-2.
|
|
338
|
-
code_context_control-2.
|
|
339
|
-
code_context_control-2.
|
|
340
|
-
code_context_control-2.
|
|
336
|
+
code_context_control-2.146.0.dist-info/METADATA,sha256=qMaA5mgjHH8SPwuUFvut1k-KVaWK3DuHawvK8axeKNs,28902
|
|
337
|
+
code_context_control-2.146.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
338
|
+
code_context_control-2.146.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
339
|
+
code_context_control-2.146.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
340
|
+
code_context_control-2.146.0.dist-info/RECORD,,
|
services/activity_log.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""ActivityLog — Append-only JSONL activity log for C3 events."""
|
|
2
2
|
import json
|
|
3
|
-
from collections import Counter
|
|
3
|
+
from collections import Counter
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
@@ -75,6 +75,9 @@ class ActivityLog:
|
|
|
75
75
|
"""
|
|
76
76
|
if not self.log_file.exists():
|
|
77
77
|
return []
|
|
78
|
+
# A row of this type holds its JSON-encoded name, so a chunk or line
|
|
79
|
+
# without those bytes cannot match and is skipped unparsed.
|
|
80
|
+
needle = json.dumps(event_type).encode("utf-8")
|
|
78
81
|
events: list = []
|
|
79
82
|
try:
|
|
80
83
|
with open(self.log_file, "rb") as handle:
|
|
@@ -86,12 +89,19 @@ class ActivityLog:
|
|
|
86
89
|
position -= step
|
|
87
90
|
handle.seek(position)
|
|
88
91
|
block = handle.read(step) + tail
|
|
89
|
-
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if
|
|
92
|
+
# The first line may be partial: keep it for the next
|
|
93
|
+
# (earlier) chunk unless we are at the file start.
|
|
94
|
+
tail = b""
|
|
95
|
+
if position > 0:
|
|
96
|
+
cut = block.find(b"\n")
|
|
97
|
+
if cut < 0:
|
|
98
|
+
tail = block
|
|
99
|
+
continue
|
|
100
|
+
tail, block = block[:cut], block[cut + 1:]
|
|
101
|
+
if needle not in block:
|
|
102
|
+
continue
|
|
103
|
+
for raw in reversed(block.split(b"\n")):
|
|
104
|
+
if needle not in raw:
|
|
95
105
|
continue
|
|
96
106
|
try:
|
|
97
107
|
entry = json.loads(raw.decode("utf-8", "replace"))
|
|
@@ -119,15 +129,13 @@ class ActivityLog:
|
|
|
119
129
|
# far back in the log behind many tool_call entries. Use a larger scan
|
|
120
130
|
# window so they aren't missed.
|
|
121
131
|
scan_factor = 100 if event_type else 5
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
tail.append(line)
|
|
127
|
-
for line in reversed(tail):
|
|
132
|
+
needle = json.dumps(event_type).encode("utf-8") if event_type else b""
|
|
133
|
+
for raw in self._tail_lines(max(1, limit * scan_factor)):
|
|
134
|
+
if needle not in raw:
|
|
135
|
+
continue
|
|
128
136
|
try:
|
|
129
|
-
entry = json.loads(
|
|
130
|
-
except
|
|
137
|
+
entry = json.loads(raw)
|
|
138
|
+
except ValueError:
|
|
131
139
|
continue
|
|
132
140
|
if event_type and entry.get("type") != event_type:
|
|
133
141
|
continue
|
|
@@ -141,6 +149,26 @@ class ActivityLog:
|
|
|
141
149
|
break
|
|
142
150
|
return events
|
|
143
151
|
|
|
152
|
+
def _tail_lines(self, count: int, chunk_bytes: int = 64 * 1024) -> list:
|
|
153
|
+
"""The last ``count`` non-blank lines as bytes, newest first."""
|
|
154
|
+
lines: list = []
|
|
155
|
+
with open(self.log_file, "rb") as handle:
|
|
156
|
+
handle.seek(0, 2)
|
|
157
|
+
position = handle.tell()
|
|
158
|
+
tail = b""
|
|
159
|
+
while position > 0 and len(lines) < count:
|
|
160
|
+
step = min(chunk_bytes, position)
|
|
161
|
+
position -= step
|
|
162
|
+
handle.seek(position)
|
|
163
|
+
parts = (handle.read(step) + tail).split(b"\n")
|
|
164
|
+
tail = parts.pop(0) if position > 0 else b""
|
|
165
|
+
for raw in reversed(parts):
|
|
166
|
+
if raw.strip():
|
|
167
|
+
lines.append(raw)
|
|
168
|
+
if len(lines) >= count:
|
|
169
|
+
break
|
|
170
|
+
return lines
|
|
171
|
+
|
|
144
172
|
def get_stats(self) -> dict:
|
|
145
173
|
"""Counts by event type, total events, time range."""
|
|
146
174
|
if not self.log_file.exists():
|
services/ci_act.py
CHANGED
|
@@ -88,12 +88,33 @@ def _probe(cmd: list, timeout: int = 30) -> tuple:
|
|
|
88
88
|
return 127, f"{type(exc).__name__}: {exc}"
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
#: How old an engine probe a display surface may reuse. The hub CI tab
|
|
92
|
+
#: re-inspects every 2-10 s, and each fresh probe spawns act and docker.
|
|
93
|
+
DISPLAY_PROBE_TTL_S = 30.0
|
|
94
|
+
_display_probe: tuple = (0.0, None)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def availability(max_age: float = 0.0) -> dict:
|
|
92
98
|
"""Can this engine run at all? Reports WHY not, never just False.
|
|
93
99
|
|
|
94
100
|
Both halves are checked because they fail independently and a user with
|
|
95
101
|
act installed but Docker stopped deserves to be told which one to fix.
|
|
102
|
+
|
|
103
|
+
``max_age`` > 0 reuses a result at most that many seconds old from an
|
|
104
|
+
earlier call that also passed it; the default always probes, which is
|
|
105
|
+
what a run and ``doctor`` need.
|
|
96
106
|
"""
|
|
107
|
+
global _display_probe
|
|
108
|
+
if max_age <= 0:
|
|
109
|
+
return _probe_engine()
|
|
110
|
+
at, info = _display_probe
|
|
111
|
+
if info is None or time.monotonic() - at > max_age:
|
|
112
|
+
info = _probe_engine()
|
|
113
|
+
_display_probe = (time.monotonic(), info)
|
|
114
|
+
return dict(info)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _probe_engine() -> dict:
|
|
97
118
|
act = find_act()
|
|
98
119
|
info: dict = {"ok": False, "act": act, "act_version": "",
|
|
99
120
|
"docker": False, "docker_version": "", "reason": ""}
|
services/ci_workflow.py
CHANGED
|
@@ -800,8 +800,11 @@ def inspect_project(project_path, event: str = "", engine: str = "auto") -> dict
|
|
|
800
800
|
# Lazy: keeps parsing independent of the execution engines, and avoids
|
|
801
801
|
# paying for a `docker version` probe on every import.
|
|
802
802
|
from services import ci_act
|
|
803
|
-
|
|
804
|
-
|
|
803
|
+
if engine in ("auto", "act"):
|
|
804
|
+
engines = ci_act.availability(max_age=ci_act.DISPLAY_PROBE_TTL_S)
|
|
805
|
+
else:
|
|
806
|
+
engines = {"ok": False,
|
|
807
|
+
"reason": "engine='native' — containers not considered"}
|
|
805
808
|
act_ok = bool(engines.get("ok"))
|
|
806
809
|
|
|
807
810
|
native, container, foreign, unsupported = [], [], [], []
|
services/credential_backup.py
CHANGED
|
@@ -310,20 +310,26 @@ def sync(project_paths=()) -> dict:
|
|
|
310
310
|
return out
|
|
311
311
|
|
|
312
312
|
|
|
313
|
-
def status(project_paths=()) -> dict:
|
|
313
|
+
def status(project_paths=(), presence=None) -> dict:
|
|
314
314
|
"""Counts only — never a value. ``not_backed_up`` lists registered entries
|
|
315
315
|
with a live value but no sealed copy; ``restorable`` lists entries whose
|
|
316
|
-
value is gone but whose copy is here.
|
|
316
|
+
value is gone but whose copy is here.
|
|
317
|
+
|
|
318
|
+
``presence`` maps realm to a ``credential_store.value_presence`` result
|
|
319
|
+
the caller already holds; any other vault is probed here, once."""
|
|
317
320
|
data = _load()
|
|
318
321
|
records = data.get("records", {}) if data else {}
|
|
319
322
|
out: dict = {"enabled": bool(data), "path": str(backup_path() or ""),
|
|
320
323
|
"created": data.get("created", "") if data else "",
|
|
321
324
|
"backed_up": sum(len(r) for r in records.values()),
|
|
322
325
|
"restorable": [], "lost": [], "not_backed_up": []}
|
|
326
|
+
known = dict(presence or {})
|
|
323
327
|
for scope, pp, realm_s, name in _realm_entries(project_paths):
|
|
324
328
|
label = name if scope == "global" else f"{name} ({pp})"
|
|
325
329
|
has_copy = name in records.get(realm_s, {})
|
|
326
|
-
if
|
|
330
|
+
if realm_s not in known:
|
|
331
|
+
known[realm_s] = cs.value_presence(scope, pp)
|
|
332
|
+
if known[realm_s].get(name, False):
|
|
327
333
|
if not has_copy:
|
|
328
334
|
out["not_backed_up"].append(label)
|
|
329
335
|
elif has_copy:
|
services/credential_store.py
CHANGED
|
@@ -388,12 +388,14 @@ def _file_get(scope: str, project_path: str, realm_s: str, name: str) -> Optiona
|
|
|
388
388
|
token = _load_sidecar(path).get(name)
|
|
389
389
|
if not token:
|
|
390
390
|
return None
|
|
391
|
-
|
|
392
|
-
|
|
391
|
+
return _decrypt(_master_key(realm_s, create=False), token)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _decrypt(key: Optional[bytes], token) -> Optional[str]:
|
|
395
|
+
if not key or not token:
|
|
393
396
|
return None
|
|
394
397
|
try:
|
|
395
|
-
|
|
396
|
-
return fernet.decrypt(token.encode("ascii")).decode("utf-8")
|
|
398
|
+
return _crypto_module()(key).decrypt(token.encode("ascii")).decode("utf-8")
|
|
397
399
|
except Exception:
|
|
398
400
|
return None
|
|
399
401
|
|
|
@@ -1136,15 +1138,75 @@ def is_resolvable(name: str, *, project_path: str = ".", scope: str = "") -> boo
|
|
|
1136
1138
|
gateway) can answer "is this credential still good?" without importing
|
|
1137
1139
|
get_value at all. That absence is what the source-grep invariant test
|
|
1138
1140
|
asserts, which is a stronger guarantee than reviewing call sites."""
|
|
1139
|
-
|
|
1140
|
-
if
|
|
1141
|
+
owning = _norm_scope(scope, project_path) if scope else _owning_scope(name, project_path)
|
|
1142
|
+
if not owning:
|
|
1141
1143
|
return False
|
|
1142
|
-
|
|
1144
|
+
return value_presence(owning, project_path, names=[name]).get(name, False)
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def value_presence(scope: str, project_path: str = ".", names=None) -> dict:
|
|
1148
|
+
"""``{name: bool}`` for one scope's registered entries (or only ``names``):
|
|
1149
|
+
whether each stored value can still be decoded. Never a value.
|
|
1150
|
+
|
|
1151
|
+
A structured entry counts only when its payload is a JSON object. The
|
|
1152
|
+
registry, realm, sidecar and master key are read once for the batch, so a
|
|
1153
|
+
listing costs one keyring read per entry plus the structured attestation
|
|
1154
|
+
for values that are not JSON objects.
|
|
1155
|
+
"""
|
|
1156
|
+
scope = _norm_scope(scope, project_path)
|
|
1157
|
+
entries = _read_entries(scope, project_path)
|
|
1158
|
+
if names is not None:
|
|
1159
|
+
entries = {n: entries[n] for n in names if n in entries}
|
|
1160
|
+
if not entries:
|
|
1161
|
+
return {}
|
|
1162
|
+
try:
|
|
1163
|
+
keyring = _keyring_module()
|
|
1164
|
+
except RuntimeError:
|
|
1165
|
+
return dict.fromkeys(entries, False)
|
|
1166
|
+
realm_s = realm(scope, project_path)
|
|
1167
|
+
sidecar, key = None, None
|
|
1168
|
+
out = {}
|
|
1169
|
+
for name, entry in entries.items():
|
|
1170
|
+
if entry.get("storage") == "file":
|
|
1171
|
+
if sidecar is None:
|
|
1172
|
+
path = _secrets_path(scope, project_path)
|
|
1173
|
+
sidecar = _load_sidecar(path) if path is not None else {}
|
|
1174
|
+
key = _master_key(realm_s, create=False) if sidecar else None
|
|
1175
|
+
raw = _decrypt(key, sidecar.get(name))
|
|
1176
|
+
else:
|
|
1177
|
+
try:
|
|
1178
|
+
raw = keyring.get_password(KEYRING_SERVICE, _account(realm_s, name))
|
|
1179
|
+
except Exception:
|
|
1180
|
+
raw = None
|
|
1181
|
+
if raw is None:
|
|
1182
|
+
out[name] = False
|
|
1183
|
+
continue
|
|
1143
1184
|
try:
|
|
1144
|
-
|
|
1185
|
+
is_object = isinstance(json.loads(raw), dict)
|
|
1186
|
+
except (ValueError, RecursionError):
|
|
1187
|
+
is_object = False
|
|
1188
|
+
if is_object or entry.get("type") in STRUCTURED_TYPES:
|
|
1189
|
+
out[name] = is_object
|
|
1190
|
+
continue
|
|
1191
|
+
try:
|
|
1192
|
+
attested = keyring.get_password(
|
|
1193
|
+
KEYRING_SERVICE, _struct_account(realm_s, name)) or ""
|
|
1145
1194
|
except Exception:
|
|
1146
|
-
|
|
1147
|
-
|
|
1195
|
+
attested = ""
|
|
1196
|
+
out[name] = attested not in STRUCTURED_TYPES
|
|
1197
|
+
return out
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def presence_for(entries: dict, project_path: str = ".") -> dict:
|
|
1201
|
+
"""``{name: bool}`` for a :func:`list_entries` view, each name probed in
|
|
1202
|
+
the realm that owns it."""
|
|
1203
|
+
by_scope: dict = {}
|
|
1204
|
+
for name, entry in entries.items():
|
|
1205
|
+
by_scope.setdefault(entry["scope"], []).append(name)
|
|
1206
|
+
out = dict.fromkeys(entries, False)
|
|
1207
|
+
for scope, names in by_scope.items():
|
|
1208
|
+
out.update(value_presence(scope, project_path, names=names))
|
|
1209
|
+
return out
|
|
1148
1210
|
|
|
1149
1211
|
|
|
1150
1212
|
def _get_raw(name: str, *, project_path: str = ".", scope: str = "") -> Optional[str]:
|
services/project_manager.py
CHANGED
|
@@ -533,6 +533,15 @@ class ProjectManager:
|
|
|
533
533
|
return {"dropped": len(registry) - len(valid) - len(reaped),
|
|
534
534
|
"reaped": reaped}
|
|
535
535
|
|
|
536
|
+
def list_registered(self) -> list:
|
|
537
|
+
"""The stored project rows (name, path, parent_path, ...) as registered.
|
|
538
|
+
|
|
539
|
+
No liveness, session or config enrichment, so it touches one file.
|
|
540
|
+
Callers that only iterate projects use this; ``list_projects`` reads
|
|
541
|
+
every project's activity log and probes session ports.
|
|
542
|
+
"""
|
|
543
|
+
return self._read_projects()
|
|
544
|
+
|
|
536
545
|
def list_projects(self) -> list:
|
|
537
546
|
projects = self._read_projects()
|
|
538
547
|
registry = self._read_registry()
|
services/session_catalog.py
CHANGED
|
@@ -112,7 +112,7 @@ def _bare(text: str) -> str:
|
|
|
112
112
|
return re.sub(r"[^a-z0-9]", "", text.lower())
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
def claude_transcript_dir(project) -> Path | None:
|
|
115
|
+
def claude_transcript_dir(project, index: _RootIndex | None = None) -> Path | None:
|
|
116
116
|
"""The transcript folder Claude Code uses for ``project``, or None.
|
|
117
117
|
|
|
118
118
|
Exact slug first (every non-alphanumeric becomes ``-``), then the same slug
|
|
@@ -131,21 +131,62 @@ def claude_transcript_dir(project) -> Path | None:
|
|
|
131
131
|
if name and cand.is_dir():
|
|
132
132
|
return cand
|
|
133
133
|
target = _bare(project_str)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return None
|
|
134
|
+
if index is None or index.root != root:
|
|
135
|
+
index = _RootIndex(root)
|
|
136
|
+
for d in index.dirs():
|
|
137
|
+
if _bare(d.name) == target:
|
|
138
|
+
return d
|
|
140
139
|
return None
|
|
141
140
|
|
|
142
141
|
|
|
143
|
-
|
|
142
|
+
class _RootIndex:
|
|
143
|
+
"""One listing of the transcript root and, on first use, of which folder
|
|
144
|
+
holds each transcript id — shared by every project one request builds.
|
|
145
|
+
|
|
146
|
+
Request-scoped on purpose: Windows does not refresh a folder's mtime in
|
|
147
|
+
its parent's listing when a file is created inside it, so a listing kept
|
|
148
|
+
across requests cannot tell that a new transcript appeared.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(self, root: Path):
|
|
152
|
+
self.root = root
|
|
153
|
+
self._dirs: list[Path] | None = None
|
|
154
|
+
self._owners: dict[str, Path] | None = None
|
|
155
|
+
self._lock = threading.Lock()
|
|
156
|
+
|
|
157
|
+
def dirs(self) -> list[Path]:
|
|
158
|
+
with self._lock:
|
|
159
|
+
if self._dirs is None:
|
|
160
|
+
try:
|
|
161
|
+
with os.scandir(self.root) as it:
|
|
162
|
+
self._dirs = [Path(e.path) for e in it if e.is_dir()]
|
|
163
|
+
except OSError:
|
|
164
|
+
self._dirs = []
|
|
165
|
+
return self._dirs
|
|
166
|
+
|
|
167
|
+
def owner(self, stem: str) -> Path | None:
|
|
168
|
+
dirs = self.dirs()
|
|
169
|
+
with self._lock:
|
|
170
|
+
if self._owners is None:
|
|
171
|
+
owners: dict[str, Path] = {}
|
|
172
|
+
for d in dirs:
|
|
173
|
+
try:
|
|
174
|
+
with os.scandir(d) as it:
|
|
175
|
+
for e in it:
|
|
176
|
+
if e.name.endswith(".jsonl") and e.is_file():
|
|
177
|
+
owners.setdefault(e.name[:-6], Path(e.path))
|
|
178
|
+
except OSError:
|
|
179
|
+
continue
|
|
180
|
+
self._owners = owners
|
|
181
|
+
return self._owners.get(stem)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _dir_key(path) -> str:
|
|
144
185
|
try:
|
|
145
|
-
|
|
146
|
-
except
|
|
147
|
-
|
|
148
|
-
return os.path.normcase(os.path.normpath(
|
|
186
|
+
real = os.path.realpath(str(path))
|
|
187
|
+
except (OSError, ValueError):
|
|
188
|
+
real = str(path)
|
|
189
|
+
return os.path.normcase(os.path.normpath(real))
|
|
149
190
|
|
|
150
191
|
|
|
151
192
|
# ── Reading one transcript ─────────────────────────────────────────────────
|
|
@@ -569,36 +610,39 @@ def _transcript_files(tdir: Path | None, limit: int | None = MAX_TRANSCRIPTS) ->
|
|
|
569
610
|
return files if limit is None else files[:limit]
|
|
570
611
|
|
|
571
612
|
|
|
572
|
-
def _find_elsewhere(host_id: str) -> Path | None:
|
|
613
|
+
def _find_elsewhere(host_id: str, index: _RootIndex) -> Path | None:
|
|
573
614
|
"""A Claude transcript for ``host_id`` in ANY project folder — a session
|
|
574
615
|
run from a worktree lives under the worktree's slug, not the project's."""
|
|
575
616
|
if not UUID_RE.match(host_id or ""):
|
|
576
617
|
return None
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
for d in root.iterdir():
|
|
580
|
-
cand = d / f"{host_id}.jsonl"
|
|
581
|
-
if cand.is_file():
|
|
582
|
-
return cand
|
|
583
|
-
except OSError:
|
|
584
|
-
return None
|
|
585
|
-
return None
|
|
618
|
+
return index.owner(host_id)
|
|
619
|
+
|
|
586
620
|
|
|
621
|
+
def _build(project, name: str = "", index: _RootIndex | None = None) -> tuple[list[dict], dict]:
|
|
622
|
+
"""Every row for one project (unfiltered) plus a lookup context.
|
|
587
623
|
|
|
588
|
-
|
|
589
|
-
"""
|
|
624
|
+
``index``: a transcript-root listing shared across one request's builds.
|
|
625
|
+
"""
|
|
590
626
|
project = Path(project)
|
|
591
627
|
name = name or project.name
|
|
628
|
+
root = claude_projects_root()
|
|
629
|
+
if index is None or index.root != root:
|
|
630
|
+
index = _RootIndex(root)
|
|
592
631
|
with _project_lock(project):
|
|
593
632
|
cache = _load_cache(project)
|
|
594
633
|
dirty: list = []
|
|
595
|
-
tdir = claude_transcript_dir(project)
|
|
634
|
+
tdir = claude_transcript_dir(project, index)
|
|
596
635
|
metas = {}
|
|
636
|
+
own_dir = _dir_key(project)
|
|
637
|
+
cwd_keys: dict[str, str] = {}
|
|
597
638
|
for f in _transcript_files(tdir):
|
|
598
639
|
meta = _cached(cache["transcripts"], f, scan_transcript, dirty)
|
|
599
640
|
if not meta:
|
|
600
641
|
continue
|
|
601
|
-
|
|
642
|
+
cwd = meta.get("cwd")
|
|
643
|
+
if cwd and cwd not in cwd_keys:
|
|
644
|
+
cwd_keys[cwd] = _dir_key(cwd)
|
|
645
|
+
if cwd and cwd_keys[cwd] != own_dir:
|
|
602
646
|
continue # a folder that only looks like ours
|
|
603
647
|
metas[meta["id"]] = dict(meta, _path=str(f))
|
|
604
648
|
records = []
|
|
@@ -617,6 +661,7 @@ def _build(project, name: str = "") -> tuple[list[dict], dict]:
|
|
|
617
661
|
snaps.append(snap)
|
|
618
662
|
if dirty:
|
|
619
663
|
_save_cache(project, cache)
|
|
664
|
+
dirty.clear()
|
|
620
665
|
|
|
621
666
|
# C3 id → host id, from every place that states it.
|
|
622
667
|
c3_to_host: dict[str, str] = {}
|
|
@@ -669,14 +714,16 @@ def _build(project, name: str = "") -> tuple[list[dict], dict]:
|
|
|
669
714
|
continue
|
|
670
715
|
rec0 = recs[0]
|
|
671
716
|
if _provider(rec0["source_system"]) == "claude" and UUID_RE.match(rid):
|
|
672
|
-
found = _find_elsewhere(rid)
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
pass
|
|
717
|
+
found = _find_elsewhere(rid, index)
|
|
718
|
+
meta = _cached(cache["transcripts"], found, scan_transcript, dirty) \
|
|
719
|
+
if found is not None else None
|
|
720
|
+
if meta:
|
|
721
|
+
metas[rid] = dict(meta, _path=str(found), _elsewhere=True)
|
|
722
|
+
continue
|
|
679
723
|
metas[rid] = {"id": rid, "_c3_only": True, "provider": _provider(rec0["source_system"])}
|
|
724
|
+
if dirty:
|
|
725
|
+
with _project_lock(project):
|
|
726
|
+
_save_cache(project, cache)
|
|
680
727
|
|
|
681
728
|
marks = _fold_marks(_read_marks(project))
|
|
682
729
|
cleared, pred, succ = _lifecycle(project)
|
|
@@ -818,11 +865,12 @@ def list_many(projects: list[dict], *, stale: str = "hide", q: str = "",
|
|
|
818
865
|
if not path or not (Path(path) / ".c3").is_dir():
|
|
819
866
|
return path, [], ""
|
|
820
867
|
try:
|
|
821
|
-
rows, _ = _build(path, str(p.get("name") or ""))
|
|
868
|
+
rows, _ = _build(path, str(p.get("name") or ""), index)
|
|
822
869
|
return path, rows, ""
|
|
823
870
|
except Exception as exc: # pragma: no cover - defensive
|
|
824
871
|
return path, [], str(exc)
|
|
825
872
|
|
|
873
|
+
index = _RootIndex(claude_projects_root())
|
|
826
874
|
merged, errors = [], []
|
|
827
875
|
with ThreadPoolExecutor(max_workers=max(1, workers)) as pool:
|
|
828
876
|
for path, rows, err in pool.map(one, projects):
|
|
File without changes
|
{code_context_control-2.145.2.dist-info → code_context_control-2.146.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{code_context_control-2.145.2.dist-info → code_context_control-2.146.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{code_context_control-2.145.2.dist-info → code_context_control-2.146.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|