code-context-control 2.154.0__py3-none-any.whl → 2.155.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 +1 -1
- cli/hook_session_open.py +101 -2
- cli/mcp_server.py +114 -3
- cli/server.py +57 -35
- cli/tools/_grants.py +18 -0
- cli/tools/edit.py +2 -1
- cli/tools/locks.py +1 -1
- cli/tools/project.py +18 -0
- {code_context_control-2.154.0.dist-info → code_context_control-2.155.0.dist-info}/METADATA +1 -1
- {code_context_control-2.154.0.dist-info → code_context_control-2.155.0.dist-info}/RECORD +20 -18
- core/host.py +13 -3
- services/agent_locks.py +8 -5
- services/host_handoff.py +320 -0
- services/project_manager.py +41 -41
- services/session_live.py +16 -4
- services/ui_registry.py +264 -0
- {code_context_control-2.154.0.dist-info → code_context_control-2.155.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.154.0.dist-info → code_context_control-2.155.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.154.0.dist-info → code_context_control-2.155.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.154.0.dist-info → code_context_control-2.155.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.155.0"
|
|
95
95
|
|
|
96
96
|
# The PreToolUse matcher for native subagent calls (installer and hub migration).
|
|
97
97
|
AGENT_MATCHER = "Agent|Task"
|
cli/hook_session_open.py
CHANGED
|
@@ -30,6 +30,12 @@ the hub, and it skips a project that already has a live UI port, so a
|
|
|
30
30
|
``C3_NO_UI_AUTOSTART=1`` or
|
|
31
31
|
``.c3/config.json → {"ui": {"autostart_on_session": false}}``.
|
|
32
32
|
|
|
33
|
+
A ``clear`` (or an in-session ``resume``) keeps the MCP server it already
|
|
34
|
+
has, under a host session id that just ended. This hook finds that server
|
|
35
|
+
by the host pid both of them share and hands it the new id
|
|
36
|
+
(``services.host_handoff``, v2.155.0); the server then writes the
|
|
37
|
+
``C3 connected <new id>`` row the desktop sentinel is waiting for.
|
|
38
|
+
|
|
33
39
|
``source: "compact"`` writes NOTHING: a compaction is the same session and
|
|
34
40
|
the same MCP process, and a "Session started" toast on every compaction
|
|
35
41
|
would teach the user to ignore the real ones.
|
|
@@ -48,6 +54,8 @@ from __future__ import annotations
|
|
|
48
54
|
import json
|
|
49
55
|
import os
|
|
50
56
|
import sys
|
|
57
|
+
import time
|
|
58
|
+
import uuid
|
|
51
59
|
from pathlib import Path
|
|
52
60
|
|
|
53
61
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
@@ -56,6 +64,11 @@ from cli._hook_utils import detect_host, find_project, log_hook_error # noqa: E
|
|
|
56
64
|
from cli.hook_session_end import short_id # noqa: E402
|
|
57
65
|
|
|
58
66
|
_SILENT_SOURCES = {"compact"}
|
|
67
|
+
# How long one session's UI launch holds the project against the next. A
|
|
68
|
+
# server lands in the registry only after it binds its port, a few seconds
|
|
69
|
+
# after Popen; this covers that gap with room to spare. After it, the live
|
|
70
|
+
# registry entry (``_ui_running``) is the guard again.
|
|
71
|
+
_LAUNCH_GRACE_S = 90.0
|
|
59
72
|
|
|
60
73
|
|
|
61
74
|
def _ui_autostart_enabled(project: Path) -> bool:
|
|
@@ -96,7 +109,10 @@ def _ui_running(pm, project: Path) -> bool:
|
|
|
96
109
|
"""True when a live UI server is already registered for this project.
|
|
97
110
|
|
|
98
111
|
This is what keeps `resume` / `clear` — both real SessionStart events —
|
|
99
|
-
from stacking a second server on every reconnect.
|
|
112
|
+
from stacking a second server on every reconnect. A registered server
|
|
113
|
+
whose port is quiet but whose pid is running is still "running": it is
|
|
114
|
+
starting up, or busy, and a second one would only be the leak this
|
|
115
|
+
check exists to prevent (2.155.0).
|
|
100
116
|
"""
|
|
101
117
|
target = os.path.normcase(str(project))
|
|
102
118
|
for entry in pm._read_registry():
|
|
@@ -107,8 +123,72 @@ def _ui_running(pm, project: Path) -> bool:
|
|
|
107
123
|
same = os.path.normcase(str(Path(raw).resolve())) == target
|
|
108
124
|
except Exception:
|
|
109
125
|
same = os.path.normcase(raw) == target
|
|
110
|
-
if same
|
|
126
|
+
if not same or not entry.get("port"):
|
|
127
|
+
continue
|
|
128
|
+
if pm._port_alive(entry["port"]):
|
|
129
|
+
return True
|
|
130
|
+
if entry.get("pid") and pm._pid_alive(entry.get("pid")):
|
|
131
|
+
return True
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _launch_lock(project: Path) -> Path:
|
|
136
|
+
return project / ".c3" / "ui_launch.lock"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _claim_launch(project: Path) -> bool:
|
|
140
|
+
"""Atomically claim the right to launch this project's UI server.
|
|
141
|
+
|
|
142
|
+
``_ui_running`` only sees a server once it has registered its port, a
|
|
143
|
+
few seconds after launch. Sessions that open inside that window (an
|
|
144
|
+
agent spawning several at once, or a resume racing a startup) each saw
|
|
145
|
+
no live UI and each launched one, which is how a project ended up with
|
|
146
|
+
two servers. The first claimant creates ``.c3/ui_launch.lock`` with
|
|
147
|
+
O_EXCL; anyone else inside ``_LAUNCH_GRACE_S`` stands down.
|
|
148
|
+
|
|
149
|
+
A lock older than the grace window is stale (the launch died, or the
|
|
150
|
+
server since exited). It is taken over by renaming it to a unique name,
|
|
151
|
+
which only one contender can do, then the create is retried. If the
|
|
152
|
+
lock cannot be handled at all the answer is True, which is the
|
|
153
|
+
behaviour from before this lock existed: a missing UI is worse than a
|
|
154
|
+
rare duplicate.
|
|
155
|
+
"""
|
|
156
|
+
lock = _launch_lock(project)
|
|
157
|
+
try:
|
|
158
|
+
lock.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
except Exception:
|
|
160
|
+
return True
|
|
161
|
+
for _ in range(3):
|
|
162
|
+
try:
|
|
163
|
+
fd = os.open(str(lock), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
164
|
+
except FileExistsError:
|
|
165
|
+
try:
|
|
166
|
+
age = time.time() - lock.stat().st_mtime
|
|
167
|
+
except FileNotFoundError:
|
|
168
|
+
continue # released between our create and stat; retry
|
|
169
|
+
except Exception:
|
|
170
|
+
return True
|
|
171
|
+
if age < _LAUNCH_GRACE_S:
|
|
172
|
+
return False
|
|
173
|
+
stale = lock.with_name(f"ui_launch.stale-{uuid.uuid4().hex}")
|
|
174
|
+
try:
|
|
175
|
+
os.rename(str(lock), str(stale))
|
|
176
|
+
except FileNotFoundError:
|
|
177
|
+
continue # another contender took it over first
|
|
178
|
+
except Exception:
|
|
179
|
+
return False
|
|
180
|
+
try:
|
|
181
|
+
stale.unlink()
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
continue
|
|
185
|
+
except Exception:
|
|
111
186
|
return True
|
|
187
|
+
try:
|
|
188
|
+
os.write(fd, f"{os.getpid()} {time.time():.0f}\n".encode("ascii"))
|
|
189
|
+
finally:
|
|
190
|
+
os.close(fd)
|
|
191
|
+
return True
|
|
112
192
|
return False
|
|
113
193
|
|
|
114
194
|
|
|
@@ -136,6 +216,8 @@ def ensure_ui(project: Path, host_session_id: str = "") -> str:
|
|
|
136
216
|
return "unregistered"
|
|
137
217
|
if _ui_running(pm, project):
|
|
138
218
|
return "already-running"
|
|
219
|
+
if not _claim_launch(project):
|
|
220
|
+
return "launch-pending"
|
|
139
221
|
# launch_session copies os.environ into the detached child, which is
|
|
140
222
|
# where cli/server._register_session reads the ownership from.
|
|
141
223
|
os.environ["C3_UI_OWNER"] = "session"
|
|
@@ -150,6 +232,22 @@ def ensure_ui(project: Path, host_session_id: str = "") -> str:
|
|
|
150
232
|
return "error"
|
|
151
233
|
|
|
152
234
|
|
|
235
|
+
def hand_off_runtime(project: Path, host: str, host_sid: str, start_source: str) -> list:
|
|
236
|
+
"""Point the MCP server this host already runs at its new session id.
|
|
237
|
+
|
|
238
|
+
Returns the C3 session ids handed off (usually one, often none: a
|
|
239
|
+
``startup`` has no server yet). Never raises.
|
|
240
|
+
"""
|
|
241
|
+
if not host_sid:
|
|
242
|
+
return []
|
|
243
|
+
try:
|
|
244
|
+
from services.host_handoff import hand_off
|
|
245
|
+
return hand_off(project, host, host_sid, start_source)
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
log_hook_error("hook_session_open", exc)
|
|
248
|
+
return []
|
|
249
|
+
|
|
250
|
+
|
|
153
251
|
def run(payload: dict, project_path: Path | None = None):
|
|
154
252
|
"""Core logic — importable by the dispatcher and tests. Returns None."""
|
|
155
253
|
start_source = str(payload.get("source") or "").strip().lower()
|
|
@@ -178,6 +276,7 @@ def run(payload: dict, project_path: Path | None = None):
|
|
|
178
276
|
kind="session", ref_id=host_sid)
|
|
179
277
|
except Exception:
|
|
180
278
|
pass
|
|
279
|
+
hand_off_runtime(project, host, host_sid, start_source)
|
|
181
280
|
ensure_ui(project, host_sid)
|
|
182
281
|
return None
|
|
183
282
|
|
cli/mcp_server.py
CHANGED
|
@@ -165,6 +165,93 @@ def _link_host_session(svc, host_sid: str) -> None:
|
|
|
165
165
|
pass
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
# ── Host handoff (2.155.0): follow a /clear to its new host session id ─────
|
|
169
|
+
# The pid of the IDE / agent CLI above this process (services.host_handoff).
|
|
170
|
+
# Written into every heartbeat; the SessionStart hook matches on it.
|
|
171
|
+
_HOST_PID = 0
|
|
172
|
+
# How often the heartbeat thread looks for a handoff file. The desktop
|
|
173
|
+
# sentinel gives a new session 30 s to produce a `C3 connected` row.
|
|
174
|
+
_HANDOFF_POLL_S = 5
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _rebind_host_session(svc, host_sid: str, start_source: str = "") -> bool:
|
|
178
|
+
"""Adopt ``host_sid`` as this server's host session id. Returns True when
|
|
179
|
+
something changed.
|
|
180
|
+
|
|
181
|
+
Everything that was derived from the old id is redone: the session
|
|
182
|
+
record (what ``_grants.session_id`` and the access guard read), the
|
|
183
|
+
host env var (what a c3_project proxy runtime built later would read),
|
|
184
|
+
the host↔C3 link file, the heartbeat — and the ``C3 connected <id>``
|
|
185
|
+
notification, which is what C3 Desk's "did not connect" sentinel waits
|
|
186
|
+
for after the hook's ``Session started <id>``.
|
|
187
|
+
"""
|
|
188
|
+
host_sid = str(host_sid or "").strip()
|
|
189
|
+
session = getattr(getattr(svc, "session_mgr", None), "current_session", None)
|
|
190
|
+
if not host_sid or not isinstance(session, dict):
|
|
191
|
+
return False
|
|
192
|
+
previous = str(session.get("host_session_id") or "")
|
|
193
|
+
if previous == host_sid:
|
|
194
|
+
return False
|
|
195
|
+
session["host_session_id"] = host_sid
|
|
196
|
+
ide = getattr(svc, "ide_name", "") or _IDE_NAME
|
|
197
|
+
try:
|
|
198
|
+
from core.host import session_env_var
|
|
199
|
+
var = session_env_var(ide)
|
|
200
|
+
if var:
|
|
201
|
+
os.environ[var] = host_sid
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
try:
|
|
205
|
+
import dataclasses
|
|
206
|
+
base = getattr(svc, "host_context", None) or _HOST
|
|
207
|
+
ctx = dataclasses.replace(base, host_session_id=host_sid)
|
|
208
|
+
svc.host_context = ctx
|
|
209
|
+
if getattr(svc, "session_mgr", None) is not None:
|
|
210
|
+
svc.session_mgr.host_context = ctx
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
with _host_sid_lock:
|
|
214
|
+
_host_sid_cache["value"] = host_sid
|
|
215
|
+
_host_sid_cache["read_at"] = time.monotonic()
|
|
216
|
+
_host_sid_cache["linked"] = ""
|
|
217
|
+
_link_host_session(svc, host_sid)
|
|
218
|
+
try:
|
|
219
|
+
svc.activity_log.log("session_rebind", {
|
|
220
|
+
"session_id": str(session.get("id") or ""),
|
|
221
|
+
"host_session_id": host_sid,
|
|
222
|
+
"previous_host_session_id": previous,
|
|
223
|
+
"start_source": str(start_source or ""),
|
|
224
|
+
})
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
try:
|
|
228
|
+
from services import session_live
|
|
229
|
+
session_live.beat(svc.project_path, str(session.get("id") or ""),
|
|
230
|
+
host_session_id=host_sid, ide=ide, host_pid=_HOST_PID)
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
_notify_mcp_ready(svc)
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _check_handoff(svc) -> bool:
|
|
238
|
+
"""Consume a pending handoff file for this session, if any. Never raises."""
|
|
239
|
+
try:
|
|
240
|
+
session = getattr(getattr(svc, "session_mgr", None), "current_session", None) or {}
|
|
241
|
+
c3_id = str(session.get("id") or "")
|
|
242
|
+
if not c3_id:
|
|
243
|
+
return False
|
|
244
|
+
from services import host_handoff
|
|
245
|
+
data = host_handoff.read_handoff(svc.project_path, c3_id)
|
|
246
|
+
if not data:
|
|
247
|
+
return False
|
|
248
|
+
host_handoff.clear_handoff(svc.project_path, c3_id)
|
|
249
|
+
return _rebind_host_session(svc, str(data.get("host_session_id") or ""),
|
|
250
|
+
str(data.get("start_source") or ""))
|
|
251
|
+
except Exception:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
|
|
168
255
|
def _notify_mcp_ready(svc) -> None:
|
|
169
256
|
"""``kind="mcp"`` / ``C3 connected`` — the runtime is ready to serve (D0b).
|
|
170
257
|
|
|
@@ -341,19 +428,36 @@ async def lifespan(server):
|
|
|
341
428
|
if _live_session_id:
|
|
342
429
|
from services import session_live
|
|
343
430
|
|
|
431
|
+
global _HOST_PID
|
|
432
|
+
try:
|
|
433
|
+
from services.host_handoff import host_process_id
|
|
434
|
+
_HOST_PID = int(host_process_id() or 0)
|
|
435
|
+
except Exception:
|
|
436
|
+
_HOST_PID = 0
|
|
437
|
+
|
|
344
438
|
def _do_beat():
|
|
345
439
|
session_live.beat(services.project_path, _live_session_id,
|
|
346
440
|
host_session_id=_host_session_id(services) or "",
|
|
347
|
-
ide=_IDE_NAME)
|
|
441
|
+
ide=_IDE_NAME, host_pid=_HOST_PID)
|
|
348
442
|
|
|
349
443
|
_do_beat()
|
|
350
444
|
|
|
351
445
|
def _bg_heartbeat():
|
|
352
|
-
|
|
446
|
+
# Wakes every _HANDOFF_POLL_S to look for a handoff (a /clear
|
|
447
|
+
# moved this server to a new host session id), and beats every
|
|
448
|
+
# HEARTBEAT_INTERVAL_S as before.
|
|
449
|
+
last_beat = time.monotonic()
|
|
450
|
+
while not _heartbeat_stop.wait(timeout=_HANDOFF_POLL_S):
|
|
353
451
|
try:
|
|
354
|
-
|
|
452
|
+
_check_handoff(services)
|
|
355
453
|
except Exception:
|
|
356
454
|
pass
|
|
455
|
+
if time.monotonic() - last_beat >= session_live.HEARTBEAT_INTERVAL_S:
|
|
456
|
+
last_beat = time.monotonic()
|
|
457
|
+
try:
|
|
458
|
+
_do_beat()
|
|
459
|
+
except Exception:
|
|
460
|
+
pass
|
|
357
461
|
|
|
358
462
|
threading.Thread(target=_bg_heartbeat, daemon=True,
|
|
359
463
|
name="c3-session-heartbeat").start()
|
|
@@ -423,6 +527,9 @@ async def lifespan(server):
|
|
|
423
527
|
# notification within 30 s, so this must be the LAST thing before the
|
|
424
528
|
# handshake proceeds and must never be skipped on a partial init.
|
|
425
529
|
_notify_mcp_ready(services)
|
|
530
|
+
# A /clear that landed while this process was still starting left its
|
|
531
|
+
# handoff waiting; take it now rather than at the first poll.
|
|
532
|
+
_check_handoff(services)
|
|
426
533
|
|
|
427
534
|
try:
|
|
428
535
|
yield services
|
|
@@ -517,6 +624,10 @@ def _ensure_repo_map_once(rt) -> None:
|
|
|
517
624
|
def _svc(ctx: Context) -> C3Runtime:
|
|
518
625
|
rt = ctx.request_context.lifespan_context
|
|
519
626
|
_ensure_repo_map_once(rt)
|
|
627
|
+
# A tool call right after a /clear must already run under the new host
|
|
628
|
+
# id (grants and leases are keyed by it): one stat per call, before the
|
|
629
|
+
# access guard binds the session.
|
|
630
|
+
_check_handoff(rt)
|
|
520
631
|
access_guard.bind_session(_grants.session_id(rt))
|
|
521
632
|
return rt
|
|
522
633
|
|
cli/server.py
CHANGED
|
@@ -63,19 +63,8 @@ _HUB_CONFIG_FILE = _GLOBAL_C3_DIR / "hub_config.json"
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def _registry_read() -> list:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
with open(_REGISTRY_FILE, encoding="utf-8") as f:
|
|
69
|
-
return json.load(f)
|
|
70
|
-
except Exception:
|
|
71
|
-
pass
|
|
72
|
-
return []
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _registry_write(entries: list):
|
|
76
|
-
_GLOBAL_C3_DIR.mkdir(parents=True, exist_ok=True)
|
|
77
|
-
with open(_REGISTRY_FILE, "w", encoding="utf-8") as f:
|
|
78
|
-
json.dump(entries, f, indent=2)
|
|
66
|
+
from services import ui_registry
|
|
67
|
+
return ui_registry.read(_REGISTRY_FILE)
|
|
79
68
|
|
|
80
69
|
|
|
81
70
|
def _read_hub_config() -> dict:
|
|
@@ -103,32 +92,54 @@ def _port_alive(port: int) -> bool:
|
|
|
103
92
|
return False
|
|
104
93
|
|
|
105
94
|
|
|
106
|
-
|
|
95
|
+
# Seconds between a running server's re-assertions of its own registry entry
|
|
96
|
+
# (services.ui_registry.ensure). A lost entry — an older hub rewriting the
|
|
97
|
+
# file from a stale snapshot, a crash mid-write — comes back within one tick
|
|
98
|
+
# instead of leaving a server nothing can find, reap or reuse.
|
|
99
|
+
_REGISTRY_REASSERT_S = 60
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _registry_entry(port: int, project_path: str, project_name: str) -> dict:
|
|
103
|
+
# owner says who asked for this server: "session" when an IDE session
|
|
104
|
+
# start launched it (cli/hook_session_open), "user" for `c3 ui`, the
|
|
105
|
+
# hub's Open UI button and the TUI. Only session-owned servers are
|
|
106
|
+
# ever reaped automatically (ProjectManager.sweep_registry).
|
|
107
|
+
return {
|
|
108
|
+
"port": port,
|
|
109
|
+
"project_path": project_path,
|
|
110
|
+
"project_name": project_name,
|
|
111
|
+
"started_at": time.time(),
|
|
112
|
+
"pid": os.getpid(),
|
|
113
|
+
"owner": (os.environ.get("C3_UI_OWNER") or "user").strip().lower(),
|
|
114
|
+
"owner_session": (os.environ.get("C3_UI_OWNER_SESSION") or "").strip(),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _register_session(port: int, project_path: str, project_name: str) -> dict:
|
|
119
|
+
"""Upsert this server's entry under the registry lock (2.155.0).
|
|
120
|
+
|
|
121
|
+
This used to drop every OTHER entry whose port missed a 0.3 s connect —
|
|
122
|
+
a sibling server busy re-indexing was deregistered by the next one to
|
|
123
|
+
start, and lived on as an orphan. Pruning is the hub sweep's job now,
|
|
124
|
+
and it only drops an entry whose pid is gone as well.
|
|
125
|
+
"""
|
|
126
|
+
from services import ui_registry
|
|
127
|
+
entry = _registry_entry(port, project_path, project_name)
|
|
107
128
|
with _REGISTRY_LOCK:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"port": port,
|
|
117
|
-
"project_path": project_path,
|
|
118
|
-
"project_name": project_name,
|
|
119
|
-
"started_at": time.time(),
|
|
120
|
-
"pid": os.getpid(),
|
|
121
|
-
"owner": (os.environ.get("C3_UI_OWNER") or "user").strip().lower(),
|
|
122
|
-
"owner_session": (os.environ.get("C3_UI_OWNER_SESSION") or "").strip(),
|
|
123
|
-
})
|
|
124
|
-
_registry_write(entries)
|
|
129
|
+
ui_registry.upsert(entry, _REGISTRY_FILE)
|
|
130
|
+
return entry
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _reassert_registration(entry: dict) -> bool:
|
|
134
|
+
from services import ui_registry
|
|
135
|
+
with _REGISTRY_LOCK:
|
|
136
|
+
return ui_registry.ensure(entry, _REGISTRY_FILE)
|
|
125
137
|
|
|
126
138
|
|
|
127
139
|
def _unregister_session(port: int):
|
|
140
|
+
from services import ui_registry
|
|
128
141
|
with _REGISTRY_LOCK:
|
|
129
|
-
|
|
130
|
-
entries = [e for e in entries if e.get("port") != port]
|
|
131
|
-
_registry_write(entries)
|
|
142
|
+
ui_registry.remove_ports([port], _REGISTRY_FILE)
|
|
132
143
|
|
|
133
144
|
|
|
134
145
|
def init_services(project_path: str):
|
|
@@ -4536,8 +4547,19 @@ def run_server(
|
|
|
4536
4547
|
# Fallback chain: custom meta name → folder name → parent/folder for disambiguation
|
|
4537
4548
|
_folder = _resolved.name or _resolved.parent.name or "project"
|
|
4538
4549
|
_proj_name = (_meta.get("name") or "").strip() or _folder
|
|
4539
|
-
_register_session(port, str(_resolved), _proj_name)
|
|
4550
|
+
_entry = _register_session(port, str(_resolved), _proj_name)
|
|
4540
4551
|
atexit.register(_unregister_session, port)
|
|
4552
|
+
|
|
4553
|
+
def _bg_reassert():
|
|
4554
|
+
while True:
|
|
4555
|
+
time.sleep(_REGISTRY_REASSERT_S)
|
|
4556
|
+
try:
|
|
4557
|
+
_reassert_registration(_entry)
|
|
4558
|
+
except Exception:
|
|
4559
|
+
pass
|
|
4560
|
+
|
|
4561
|
+
threading.Thread(target=_bg_reassert, daemon=True,
|
|
4562
|
+
name="c3-ui-registry-reassert").start()
|
|
4541
4563
|
except Exception:
|
|
4542
4564
|
pass
|
|
4543
4565
|
|
cli/tools/_grants.py
CHANGED
|
@@ -51,6 +51,24 @@ def session_id(svc) -> str:
|
|
|
51
51
|
return str(session.get("id", "") or "") or f"pid-{os.getpid()}"
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def agent_id(svc) -> str:
|
|
55
|
+
"""One caller label for explicit locks and implicit edit leases.
|
|
56
|
+
|
|
57
|
+
Runtime host identity belongs to the caller, including c3_project proxies;
|
|
58
|
+
the target project's preferred editor must not override it.
|
|
59
|
+
"""
|
|
60
|
+
from core.host import resolve_host
|
|
61
|
+
from services.agent_locks import agent_id_for
|
|
62
|
+
|
|
63
|
+
provider = getattr(svc, "ide_name", "")
|
|
64
|
+
if not isinstance(provider, str) or not provider.strip():
|
|
65
|
+
session = getattr(getattr(svc, "session_mgr", None), "current_session", None) or {}
|
|
66
|
+
provider = session.get("source_ide", "")
|
|
67
|
+
if not isinstance(provider, str) or not provider.strip():
|
|
68
|
+
provider = resolve_host(str(getattr(svc, "project_path", "") or "")).provider
|
|
69
|
+
return agent_id_for(session_id(svc), provider=provider)
|
|
70
|
+
|
|
71
|
+
|
|
54
72
|
def allow(svc, denial, *, tool: str, op: str, path,
|
|
55
73
|
peek: bool = False) -> str | None:
|
|
56
74
|
"""The `[c3-override:granted]` line when a live grant permits this exact
|
cli/tools/edit.py
CHANGED
|
@@ -566,7 +566,8 @@ def _write_gate(svc, path: Path, rel: str, file_path: str, op: str,
|
|
|
566
566
|
"holder": holder.get("agent_id", ""),
|
|
567
567
|
"held_for_s": held_for},
|
|
568
568
|
agent_locks.refusal(holder, rel), "lock held")
|
|
569
|
-
agent_locks.lease(str(path), svc.project_path, session_id, intent=intent
|
|
569
|
+
agent_locks.lease(str(path), svc.project_path, session_id, intent=intent,
|
|
570
|
+
agent_id=_grants.agent_id(svc))
|
|
570
571
|
return None
|
|
571
572
|
|
|
572
573
|
|
cli/tools/locks.py
CHANGED
|
@@ -40,7 +40,7 @@ def handle_locks(action: str, paths: str, intent: str, ttl_s: int,
|
|
|
40
40
|
"""Route c3_locks actions."""
|
|
41
41
|
project = str(svc.project_path)
|
|
42
42
|
session = _session_id(svc)
|
|
43
|
-
agent =
|
|
43
|
+
agent = _grants.agent_id(svc)
|
|
44
44
|
args = {"action": action}
|
|
45
45
|
|
|
46
46
|
cfg = al.config(project)
|
cli/tools/project.py
CHANGED
|
@@ -54,6 +54,19 @@ def _runtime_for(path: str):
|
|
|
54
54
|
return shared_cache().get(path)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
class _CallerLabelRuntime:
|
|
58
|
+
"""Use the caller's provider label without changing target session ownership."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, target, caller):
|
|
61
|
+
from cli.tools import _grants
|
|
62
|
+
|
|
63
|
+
self._target = target
|
|
64
|
+
self.ide_name = _grants.agent_id(caller).split(":", 1)[0]
|
|
65
|
+
|
|
66
|
+
def __getattr__(self, name):
|
|
67
|
+
return getattr(self._target, name)
|
|
68
|
+
|
|
69
|
+
|
|
57
70
|
def _is_registered(path: str) -> bool:
|
|
58
71
|
"""True when *path* is in the cross-project registry. Fails closed."""
|
|
59
72
|
try:
|
|
@@ -524,6 +537,11 @@ def handle_project(action, svc, finalize, *, project="", query="", file_path="",
|
|
|
524
537
|
return done(
|
|
525
538
|
f"[c3_project:error] Could not load '{resolved['name']}': {e}", "error")
|
|
526
539
|
|
|
540
|
+
# Label edit leases with the requesting host, without mutating the cached
|
|
541
|
+
# runtime or borrowing the caller's session/grant identity in the target.
|
|
542
|
+
if action == "edit":
|
|
543
|
+
fsvc = _CallerLabelRuntime(fsvc, svc)
|
|
544
|
+
|
|
527
545
|
banner = f"[c3_project:{resolved['name']}] {action}\n"
|
|
528
546
|
try:
|
|
529
547
|
body = _proxy(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.155.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=FYmFF0w1cmoNAHhaxEvvnM6-7uuU0jDV70KIc9eAPDM,434309
|
|
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=tzK3pQBGehDDZi6fECcYk8nRhy0QkmUtTF2TuJG7nbY,27928
|
|
@@ -20,15 +20,15 @@ cli/hook_pretool_enforce.py,sha256=pSFYgqiXktJPpNuzGXiFPhcGQRDN9QjMjpQHliO_7XQ,3
|
|
|
20
20
|
cli/hook_prompt_recall.py,sha256=LHyubu36pf5h3hbLBe0yH-yGhAlwLJdsc7hfa7WB1h4,3471
|
|
21
21
|
cli/hook_read.py,sha256=ZWZldgqGSNz78COhlgETQ7yxdOW5vWIIHUet9gZKhxk,8176
|
|
22
22
|
cli/hook_session_end.py,sha256=toFEPDKRoutDuVOmuOgHmQ8CM0o7tF7yALhMxJz7VEM,3482
|
|
23
|
-
cli/hook_session_open.py,sha256=
|
|
23
|
+
cli/hook_session_open.py,sha256=VXStM1vsA2kwLZxczAvetJGf4aDT1dPlWYMeEW4ow4Y,11387
|
|
24
24
|
cli/hook_session_stats.py,sha256=I-2NeLlV1NxeqKtTM2JH9haSpFU7r4qG0u8pfCJ80YU,8725
|
|
25
25
|
cli/hook_terse_advisor.py,sha256=W_zaTfX2akgX-ifuUTsLu2H1atM8XmWt6jpPoeU7Pm0,6252
|
|
26
26
|
cli/hub_server.py,sha256=VgDAlVmWgvh57KUbMgL2PT4303fmlr578QLk6ehe2pA,227863
|
|
27
27
|
cli/hub_ui.html,sha256=v41R_JAH4Jwodpy3tbeBUel-80FbNmffBrey-QH5Q64,2963
|
|
28
28
|
cli/mcp_proxy.py,sha256=r-Qhlie7gCtotex8yQUudCyBLRUK-IW6cNIW5RhBW0o,17648
|
|
29
|
-
cli/mcp_server.py,sha256=
|
|
29
|
+
cli/mcp_server.py,sha256=YR3mPwLcgvJiai8dSHxh4ZK8hGtFt1D-SOk7Khm-vTA,79502
|
|
30
30
|
cli/progress.py,sha256=qTkNy0howOD3hXraMo4ok3dYTpBW5yG_tenKfF4UDM0,1470
|
|
31
|
-
cli/server.py,sha256=
|
|
31
|
+
cli/server.py,sha256=SQFONagf4Mmsv2Z0fdnr8cxAFUsBE3nPQ5XUZLgtjI8,174039
|
|
32
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
|
|
@@ -81,7 +81,7 @@ cli/hub_ui/components/toasts.js,sha256=caj_x3npcM_wMv9E_xEde88jyfXI-jt8RrDQW_yXz
|
|
|
81
81
|
cli/hub_ui/components/topbar.js,sha256=xg8RqDmJpweISw55NFT07tMiwrFaG9fpDEHHblzkLk4,4817
|
|
82
82
|
cli/tools/__init__.py,sha256=HO6eVKzDm1KPqsKTHY6lfIsnFnv3C7Bd9o3Q_V88idg,127
|
|
83
83
|
cli/tools/_edit_report.py,sha256=uZuhMQABN6z9R_FXusoYNNrdYrES3rhbADXmJupwc0c,8392
|
|
84
|
-
cli/tools/_grants.py,sha256=
|
|
84
|
+
cli/tools/_grants.py,sha256=pWJ2TgjsAAk0lelMBmHS8EiX5mpBnEUfgQ2-TiMIzow,5768
|
|
85
85
|
cli/tools/_helpers.py,sha256=xhF6sSc9wcJjLQmFFipnUgxQusU1PDPcl0DGMGcgK6E,9420
|
|
86
86
|
cli/tools/agent.py,sha256=RmEEh5HhCZLvYu0gSRcwA_NENpd4iIYJW3exBzT8kVs,47983
|
|
87
87
|
cli/tools/artifacts.py,sha256=SjazShDFIzOpAZW7uMv9itBktXnc6pqQKZeaen8GgzI,9379
|
|
@@ -90,17 +90,17 @@ cli/tools/ci.py,sha256=1--Ak18ikHkO7q4HWlDCVac-U969eEFejkd-24T0xuY,20057
|
|
|
90
90
|
cli/tools/compress.py,sha256=PcgxEw7KfofmJsTLfRf3mjWMxyNlqkcSNzMTlT27SW0,12534
|
|
91
91
|
cli/tools/credentials.py,sha256=NczO0KT3LTJlD3ZedXWRAtH_8YTPhya0Txja3ystdzE,20039
|
|
92
92
|
cli/tools/delegate.py,sha256=TrHPXEyufQh0in3Ay7XcOG5wQwCYbnbz9r4naIuhbN8,126990
|
|
93
|
-
cli/tools/edit.py,sha256=
|
|
93
|
+
cli/tools/edit.py,sha256=9AOUtEG6sLEsbtKpd6cQyq66vCXNaOeoEKit3ikVJyc,44769
|
|
94
94
|
cli/tools/edit_verify.py,sha256=3Y156LikO3Q4v0OSNjKDJUaOB3nRfceFvXCmpSV8amA,11858
|
|
95
95
|
cli/tools/edits.py,sha256=Qkcl7Asa_qyIH2aIn5IYamfKavtfEZF429Bmg8TF0aY,10963
|
|
96
96
|
cli/tools/federate.py,sha256=tE65p06gM6O_yzg08SKeficFCZUH0yOFhCHFUjlTRo0,5126
|
|
97
97
|
cli/tools/filter.py,sha256=mwNabPe7bCvdsIGFsWc4E1HsfiRYx5ZevnxdsitJTAw,12702
|
|
98
98
|
cli/tools/impact.py,sha256=J6dvMrma1E2jGlhi3dIeQXhdwya5xljrJ92eiRuDaIE,7198
|
|
99
99
|
cli/tools/jira.py,sha256=BkaebUWAxwwNn2eYx1B9CXBH2wvJRhFW3ViwggFnzIY,30202
|
|
100
|
-
cli/tools/locks.py,sha256=
|
|
100
|
+
cli/tools/locks.py,sha256=1cuFuLSmB8OrPAFIf23rveOp3dkX2nmVMppW2YkCRKA,5879
|
|
101
101
|
cli/tools/memory.py,sha256=yDDRsEngeFcjb6nXUrhRZXuboasXfixJeyltKhDbZD4,24935
|
|
102
102
|
cli/tools/override.py,sha256=LiZFgoDN-35NXVwldnYrcCzfBl8MhQQ3tKVVkOG5d_o,9217
|
|
103
|
-
cli/tools/project.py,sha256=
|
|
103
|
+
cli/tools/project.py,sha256=CWnrE-tJuNm8koiGNmzUNFAfrIUlhr7Joz6W64YVea4,24843
|
|
104
104
|
cli/tools/read.py,sha256=9W-FhALienlmWeLQnsUA9tvbTgh9IGJzbSYyoT9Wd7I,21439
|
|
105
105
|
cli/tools/search.py,sha256=BZh3E7YggjCWOC1lJz0YNcRsdDLWsJiEmONtgUW2rEo,32397
|
|
106
106
|
cli/tools/session.py,sha256=IYNgcNz_saQDvilX6iB-SClLpfsGmz_grgMY5PFztPI,9371
|
|
@@ -132,13 +132,13 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
|
|
|
132
132
|
cli/ui/components/settings.js,sha256=FXcn-UQE2mXC6HFsLeEas-wAUFKt0OJaLySroZQMhGA,84095
|
|
133
133
|
cli/ui/components/sidebar.js,sha256=K2ym2kUgpbyG-EBA_wBIIiqQY8qTatsiBZwzseMWuIQ,9939
|
|
134
134
|
cli/ui/components/tasks.js,sha256=vyKQ3uwoppMwvdEaHlhWXW4oWcAisx4NveqzMhsYqHo,38438
|
|
135
|
-
code_context_control-2.
|
|
135
|
+
code_context_control-2.155.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
136
136
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
137
137
|
core/codex_patch.py,sha256=CxvT_rQIwPNTRJZj9EHJmx0ZdOeSLxjiixuLI3Z7lPc,2107
|
|
138
138
|
core/config.py,sha256=wCjrZKXRywUbR6WRcO7optyOirAQ2DeRNdPCU0X5zIY,23762
|
|
139
139
|
core/grok_payload.py,sha256=EEwhvkKnCT6Rk9tc-DWDA__toE44244Xr4d6p7SoJwE,5365
|
|
140
140
|
core/hook_command.py,sha256=SvTDUaBLg1Qb4ZdAgpe14YA_8BSnkGJsd3ByPtqBsU0,1032
|
|
141
|
-
core/host.py,sha256=
|
|
141
|
+
core/host.py,sha256=WUzS-572l9azYvu1Y4fmAVvb_0g4xyXqzsayJP_zH3M,2075
|
|
142
142
|
core/ide.py,sha256=0sk2MzrgIScE5fN1cIpjMrNqE5j1Hz_kyPfCT2w-ac8,7061
|
|
143
143
|
core/mcp_toml.py,sha256=VNZCjJsr4II_jX3cmjYzDh_OQ7Nk34mLnkg528Tl1GM,7047
|
|
144
144
|
core/web_security.py,sha256=HwgK5aBATfvRIeO_4gWgzK77gKwGuzVlmiuNGm4gGxI,7884
|
|
@@ -194,7 +194,7 @@ services/access_telemetry.py,sha256=9pOCLS9LgDtM7Z4cCEyl7osxhj-m3wsPubcXolBKuV4,
|
|
|
194
194
|
services/activity_log.py,sha256=c_jTOsR-d1d5B3u67eg8mRA7LZhtP6H1-6leeGa0Go4,7990
|
|
195
195
|
services/agent_base.py,sha256=a-gdSd_jtZtbjXo1WS8CnWCagXgKaGZd5ShcG6s0kT4,4809
|
|
196
196
|
services/agent_downshift.py,sha256=hGFg-knqa6NUqmZUijyTDV82Fz88ieh1YjN_mG5tz8U,8444
|
|
197
|
-
services/agent_locks.py,sha256=
|
|
197
|
+
services/agent_locks.py,sha256=iaS3Wz6iCS3iTkMi1zoGXlSnze9dphEOH_1am4V_4M8,23670
|
|
198
198
|
services/agents.py,sha256=q3GgxyzomyjhbixARDQrSM3EpjIhboeKQY5jsQeHQ0g,83609
|
|
199
199
|
services/agents_workflow.py,sha256=ttve887E6SIY2twHx_OxwFBEtLUcuok72_8g3ZkOAkE,502
|
|
200
200
|
services/artifact_defs.py,sha256=4bJAcg2maJCtMhY3ub_IxZg49bVg1mD_B5Z4Bn5NF0o,9805
|
|
@@ -244,6 +244,7 @@ services/file_memory.py,sha256=7E-2I1iBIJOTp-PEjoimLZZpLVLygoUF3fNKWjyF6FQ,40556
|
|
|
244
244
|
services/git_context.py,sha256=FFdA8Y46op4DYsUnZsXLTqxIjgxb0q_d-hkFTbosC8Q,9518
|
|
245
245
|
services/grok_integration.py,sha256=aiWVmG9e-0-9_MjrJ_p0dsbTSdAYFkLfsUMI7onmz0A,8299
|
|
246
246
|
services/grok_transcript.py,sha256=E4im_OIUrU-jDfw3sum_gCuG6FiONgZdlYsIhS-zdlM,2884
|
|
247
|
+
services/host_handoff.py,sha256=zCPbPwSFGXuNUlGlHAWlmy7V7TizhduwS0upD3EYMsw,12103
|
|
247
248
|
services/host_sessions.py,sha256=5R0DSU8z-CI6kKek4yLCwe1FShxZTOLe6dlYnH5-hoI,3722
|
|
248
249
|
services/hub_service.py,sha256=z2MZ2Rg9NLNXxQ17e05XGF_f6qSTDWLYtoC3WpuQfGU,2634
|
|
249
250
|
services/index_store.py,sha256=SWHHKQB_wYww1ubtKgkLo5FFEqaWhPbw0OAN8x3gk9Q,16453
|
|
@@ -277,7 +278,7 @@ services/override_policy.py,sha256=TehVPxfQ8LZWIOUzCnnioIdMUPZZ7JZa1ja77smpW2c,3
|
|
|
277
278
|
services/override_requests.py,sha256=H9lSUbfOjkjE1muztaQ58InqqsqhtnKIT-pF2VzvWkg,31148
|
|
278
279
|
services/override_wake.py,sha256=HjSOXQTDi0irgeMyrTqngL7xLcSOOIgYsVW9yxod4ps,10363
|
|
279
280
|
services/parser.py,sha256=S_YoKLS3r1xCUVHApXb3BTEfoRAzFhuo1_8XsG0qxVE,63505
|
|
280
|
-
services/project_manager.py,sha256=
|
|
281
|
+
services/project_manager.py,sha256=dfA8jUjaEBQfX6JMVBy3iwXygb2gtPaI0sv_6QxLRFY,48983
|
|
281
282
|
services/project_runtime.py,sha256=yyftX48BFjq4B2KcR4KGszDAAa4xR6gn6j0SgTyro1s,11603
|
|
282
283
|
services/protocol.py,sha256=CWZ0BWWd-jPui-11hZlvGgkNk18gMFhhLWbp-BP35Kk,12861
|
|
283
284
|
services/proxy_state.py,sha256=u5rd0k6CrOsywZA8FpRu_hMLwhR0TAJhZjy5MdWbCGc,6107
|
|
@@ -293,7 +294,7 @@ services/runtime.py,sha256=931eux-zdsXh0M6i6RmXEGAKiXLfa9aDra9V12Q5Z_Y,13767
|
|
|
293
294
|
services/scanner.py,sha256=lH0aiDCdCRruKS-VTAD_LsF4zrhMLpMRhFlOZZlw4VM,9613
|
|
294
295
|
services/session_benchmark.py,sha256=KUcWWC5X088p_ao6DgglUMwj7jCzdecPKxJ7XiGKob0,106255
|
|
295
296
|
services/session_catalog.py,sha256=H0TrxFQERg26B1Jgx_rwXHTO4lhXhAshj2oIvWHYqZg,46661
|
|
296
|
-
services/session_live.py,sha256=
|
|
297
|
+
services/session_live.py,sha256=RPFhxqVh0QmKIDn_PN0OxjpK2wHOOrvJkIgsvogMuwQ,5646
|
|
297
298
|
services/session_manager.py,sha256=IBE71TORRlNuRBhswobD5KzXhfFAijy6KrnmaP1lnuc,57391
|
|
298
299
|
services/session_preloader.py,sha256=DsTAXMKVtrX9yu1sEFojYDi9-jkSAj1Ylt9JTy57Dow,9883
|
|
299
300
|
services/shell_jobs.py,sha256=YaC-IOJr_OE4AB95IewtSVx1bZ3wmPIOiNyXiaxjp1g,52986
|
|
@@ -306,6 +307,7 @@ services/text_index.py,sha256=r3o4CobTG9jAO9PWazgbWYLY9oi_FgEJ3xwEXrF4KM0,2783
|
|
|
306
307
|
services/time_tracker.py,sha256=sZNz8QAYpWhCBwfmvopVyxpMb8H-CrBT25C0_VOT1LY,11056
|
|
307
308
|
services/tool_classifier.py,sha256=Fgvq0ZcpnCskwtO8a3YI1MiecPNnw6UbPyJQIUwgfiQ,6512
|
|
308
309
|
services/transcript_index.py,sha256=VQhvgkSyLVEYamXi_YIbiIhBnd0mqFHZlWG2HQu_1EA,12144
|
|
310
|
+
services/ui_registry.py,sha256=K_wzK0x_-jvJFdl28HCBpKow3td_BKhg3vPuei8vU7E,9121
|
|
309
311
|
services/validation_cache.py,sha256=skFYR7CkSGvFb2gq6dfxOnvjQzz5Boa3jJWT7UfaIhY,5587
|
|
310
312
|
services/vector_store.py,sha256=satc4MJanC1Wsu_iveMCUXG6K2fd0Ke87JSyy2BMikw,13041
|
|
311
313
|
services/watcher.py,sha256=WF6aTIUFzg5B5KepuLyUwmx8WvbmhT9IeenzdK0df3U,10779
|
|
@@ -337,8 +339,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
337
339
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
338
340
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
339
341
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
340
|
-
code_context_control-2.
|
|
341
|
-
code_context_control-2.
|
|
342
|
-
code_context_control-2.
|
|
343
|
-
code_context_control-2.
|
|
344
|
-
code_context_control-2.
|
|
342
|
+
code_context_control-2.155.0.dist-info/METADATA,sha256=5Nly_ZkXO2Uxav43Yk3sXrfECPN5Wtnk5r8hui7Tt6c,28902
|
|
343
|
+
code_context_control-2.155.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
344
|
+
code_context_control-2.155.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
345
|
+
code_context_control-2.155.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
346
|
+
code_context_control-2.155.0.dist-info/RECORD,,
|