python-delphi-lsp 2.3.2__py3-none-any.whl → 3.0.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.
- delphi_lsp/_version.py +1 -1
- delphi_lsp/agent_cache.py +200 -48
- delphi_lsp/agent_cli.py +42 -6
- delphi_lsp/agent_context.py +205 -1
- delphi_lsp/agent_cpg.py +350 -0
- delphi_lsp/agent_cpg_builder.py +667 -0
- delphi_lsp/agent_protocol.py +57 -3
- delphi_lsp/agent_templates.py +20 -13
- {python_delphi_lsp-2.3.2.dist-info → python_delphi_lsp-3.0.0.dist-info}/METADATA +60 -18
- {python_delphi_lsp-2.3.2.dist-info → python_delphi_lsp-3.0.0.dist-info}/RECORD +14 -12
- {python_delphi_lsp-2.3.2.dist-info → python_delphi_lsp-3.0.0.dist-info}/WHEEL +1 -1
- {python_delphi_lsp-2.3.2.dist-info → python_delphi_lsp-3.0.0.dist-info}/entry_points.txt +0 -0
- {python_delphi_lsp-2.3.2.dist-info → python_delphi_lsp-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {python_delphi_lsp-2.3.2.dist-info → python_delphi_lsp-3.0.0.dist-info}/top_level.txt +0 -0
delphi_lsp/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "
|
|
1
|
+
__version__ = "3.0.0"
|
delphi_lsp/agent_cache.py
CHANGED
|
@@ -40,9 +40,13 @@ _STARTUP_DIAGNOSTIC_BYTES = 16 * 1024
|
|
|
40
40
|
_STARTUP_TOKEN_RE = re.compile(r"(?i)(token\b[^\n\r]*?:?\s*['\"]?[A-Za-z0-9_-]+['\"]?|\b[a-zA-Z0-9_-]{32,})")
|
|
41
41
|
_START_LOCK_INCOMPLETE_GRACE_SECONDS = 1.0
|
|
42
42
|
_CACHE_REVISION_CHECK_INTERVAL_SECONDS = 3600.0
|
|
43
|
+
_WATCHER_READY_TIMEOUT_SECONDS = 1.0
|
|
44
|
+
_WATCHER_READY_POLL_MILLISECONDS = 50
|
|
43
45
|
_WATCHED_SUFFIXES = frozenset(
|
|
44
46
|
{".pas", ".pp", ".inc", ".dpr", ".dpk", ".dproj", ".cfg"}
|
|
45
47
|
)
|
|
48
|
+
_PROCESS_START_LOCKS_GUARD = threading.Lock()
|
|
49
|
+
_PROCESS_START_LOCKS: dict[str, tuple[threading.Lock, int]] = {}
|
|
46
50
|
|
|
47
51
|
|
|
48
52
|
def current_process_rss_bytes() -> int:
|
|
@@ -578,6 +582,30 @@ def _start_lock(root: str | Path, timeout: float):
|
|
|
578
582
|
pass
|
|
579
583
|
|
|
580
584
|
|
|
585
|
+
@contextlib.contextmanager
|
|
586
|
+
def _process_start_lock(root: str | Path):
|
|
587
|
+
canonical = str(Path(root).resolve())
|
|
588
|
+
with _PROCESS_START_LOCKS_GUARD:
|
|
589
|
+
lock, users = _PROCESS_START_LOCKS.get(
|
|
590
|
+
canonical,
|
|
591
|
+
(threading.Lock(), 0),
|
|
592
|
+
)
|
|
593
|
+
_PROCESS_START_LOCKS[canonical] = (lock, users + 1)
|
|
594
|
+
try:
|
|
595
|
+
with lock:
|
|
596
|
+
yield
|
|
597
|
+
finally:
|
|
598
|
+
with _PROCESS_START_LOCKS_GUARD:
|
|
599
|
+
current_lock, current_users = _PROCESS_START_LOCKS[canonical]
|
|
600
|
+
if current_users == 1:
|
|
601
|
+
del _PROCESS_START_LOCKS[canonical]
|
|
602
|
+
else:
|
|
603
|
+
_PROCESS_START_LOCKS[canonical] = (
|
|
604
|
+
current_lock,
|
|
605
|
+
current_users - 1,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
|
|
581
609
|
def _client_exchange(metadata: CacheMetadata, request: dict[str, object]) -> CacheClientResponse:
|
|
582
610
|
try:
|
|
583
611
|
with socket.create_connection(("127.0.0.1", metadata.port), timeout=2) as connection:
|
|
@@ -655,30 +683,55 @@ def _daemon_process_options() -> dict[str, object]:
|
|
|
655
683
|
|
|
656
684
|
|
|
657
685
|
class _CacheService:
|
|
658
|
-
def __init__(
|
|
686
|
+
def __init__(
|
|
687
|
+
self,
|
|
688
|
+
metadata: CacheMetadata,
|
|
689
|
+
*,
|
|
690
|
+
defer_context: bool = False,
|
|
691
|
+
) -> None:
|
|
659
692
|
self.metadata = metadata
|
|
660
693
|
self._rss_baseline_bytes = current_process_rss_bytes()
|
|
661
|
-
self.
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
workers=metadata.workers,
|
|
665
|
-
worker_memory_budget_bytes=metadata.max_memory_bytes,
|
|
666
|
-
revision_check_interval_seconds=_CACHE_REVISION_CHECK_INTERVAL_SECONDS,
|
|
667
|
-
navigation_cache_dir=navigation_cache_path(
|
|
668
|
-
metadata.root,
|
|
669
|
-
create=True,
|
|
670
|
-
),
|
|
671
|
-
)
|
|
694
|
+
self._context: AgentContext | None = None
|
|
695
|
+
self.context_ready = threading.Event()
|
|
696
|
+
self.watcher_ready = threading.Event()
|
|
672
697
|
self.budget = CacheBudget(metadata.max_memory_bytes)
|
|
673
698
|
self.stats = CacheStats()
|
|
674
699
|
self.lock = threading.Lock()
|
|
675
700
|
self.started = time.monotonic()
|
|
676
701
|
self.last_activity = self.started
|
|
677
702
|
self.cache_state = "warming"
|
|
678
|
-
self.last_revision =
|
|
703
|
+
self.last_revision = ""
|
|
679
704
|
self.last_budget = BudgetResult(0, 0.0, 0.0, False, False, False)
|
|
680
705
|
self.prewarm_seconds = 0.0
|
|
706
|
+
self.startup_error = ""
|
|
681
707
|
self.shutdown = threading.Event()
|
|
708
|
+
self.prewarm_thread: threading.Thread | None = None
|
|
709
|
+
if not defer_context:
|
|
710
|
+
self._initialize_context()
|
|
711
|
+
|
|
712
|
+
@property
|
|
713
|
+
def context(self) -> AgentContext:
|
|
714
|
+
if self._context is None:
|
|
715
|
+
raise RuntimeError("Cache workspace is still being discovered.")
|
|
716
|
+
return self._context
|
|
717
|
+
|
|
718
|
+
def _initialize_context(self) -> AgentContext:
|
|
719
|
+
if self._context is None:
|
|
720
|
+
context = AgentContext.open(
|
|
721
|
+
self.metadata.root,
|
|
722
|
+
self.metadata.project_file or None,
|
|
723
|
+
workers=self.metadata.workers,
|
|
724
|
+
worker_memory_budget_bytes=self.metadata.max_memory_bytes,
|
|
725
|
+
revision_check_interval_seconds=_CACHE_REVISION_CHECK_INTERVAL_SECONDS,
|
|
726
|
+
navigation_cache_dir=navigation_cache_path(
|
|
727
|
+
self.metadata.root,
|
|
728
|
+
create=True,
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
self._context = context
|
|
732
|
+
self.last_revision = context.workspace.workspace_revision
|
|
733
|
+
self.context_ready.set()
|
|
734
|
+
return self._context
|
|
682
735
|
|
|
683
736
|
def _measure_retained_bytes(self) -> int:
|
|
684
737
|
process_rss = current_process_rss_bytes()
|
|
@@ -691,8 +744,9 @@ class _CacheService:
|
|
|
691
744
|
|
|
692
745
|
def prewarm(self) -> None:
|
|
693
746
|
started = time.monotonic()
|
|
747
|
+
context = self._initialize_context()
|
|
694
748
|
try:
|
|
695
|
-
self.last_revision =
|
|
749
|
+
self.last_revision = context.prewarm_navigation()
|
|
696
750
|
self.cache_state = "warm"
|
|
697
751
|
except AgentProtocolError as error:
|
|
698
752
|
if error.code != "project_required":
|
|
@@ -707,17 +761,58 @@ class _CacheService:
|
|
|
707
761
|
self.stats.evictions += 1
|
|
708
762
|
self.cache_state = "compact"
|
|
709
763
|
self.prewarm_seconds = time.monotonic() - started
|
|
710
|
-
self.stats.parallel_fallbacks =
|
|
764
|
+
self.stats.parallel_fallbacks = context.parallel_stats.fallbacks
|
|
765
|
+
|
|
766
|
+
def start_prewarm(self) -> None:
|
|
767
|
+
def run() -> None:
|
|
768
|
+
try:
|
|
769
|
+
with self.lock:
|
|
770
|
+
self.prewarm()
|
|
771
|
+
except Exception as error:
|
|
772
|
+
detail = _truncate_and_sanitize_startup_diagnostics(
|
|
773
|
+
f"{type(error).__name__}: {error}".encode(
|
|
774
|
+
"utf-8",
|
|
775
|
+
"replace",
|
|
776
|
+
)
|
|
777
|
+
)
|
|
778
|
+
self.startup_error = detail or type(error).__name__
|
|
779
|
+
self.cache_state = "failed"
|
|
780
|
+
finally:
|
|
781
|
+
self.context_ready.set()
|
|
782
|
+
|
|
783
|
+
self.prewarm_thread = threading.Thread(
|
|
784
|
+
target=run,
|
|
785
|
+
name="delphi-cache-prewarm",
|
|
786
|
+
daemon=True,
|
|
787
|
+
)
|
|
788
|
+
self.prewarm_thread.start()
|
|
711
789
|
|
|
712
790
|
def request(self, request: dict[str, object]) -> CacheClientResponse:
|
|
791
|
+
action = request.get("action")
|
|
792
|
+
if action == "status":
|
|
793
|
+
warning = "" if request.get("_startup_probe") is True else self._consume_warning()
|
|
794
|
+
return CacheClientResponse(self.status(), warning)
|
|
795
|
+
if action == "stop":
|
|
796
|
+
self.shutdown.set()
|
|
797
|
+
return CacheClientResponse({"stopping": True})
|
|
798
|
+
if self.cache_state == "warming":
|
|
799
|
+
self.last_activity = time.monotonic()
|
|
800
|
+
raise CacheClientError(
|
|
801
|
+
"cache_warming",
|
|
802
|
+
"Cache is still warming. Retry after cache status reports ready.",
|
|
803
|
+
)
|
|
804
|
+
if self.cache_state == "failed":
|
|
805
|
+
detail = (
|
|
806
|
+
f" {self.startup_error}"
|
|
807
|
+
if self.startup_error
|
|
808
|
+
else ""
|
|
809
|
+
)
|
|
810
|
+
raise CacheClientError(
|
|
811
|
+
"cache_failed",
|
|
812
|
+
f"Cache workspace discovery or warm-up failed.{detail}",
|
|
813
|
+
)
|
|
814
|
+
self.watcher_ready.wait(timeout=_WATCHER_READY_TIMEOUT_SECONDS)
|
|
713
815
|
with self.lock:
|
|
714
|
-
action = request.get("action")
|
|
715
|
-
if action == "status":
|
|
716
|
-
warning = "" if request.get("_startup_probe") is True else self._consume_warning()
|
|
717
|
-
return CacheClientResponse(self.status(), warning)
|
|
718
|
-
if action == "stop":
|
|
719
|
-
self.shutdown.set()
|
|
720
|
-
return CacheClientResponse({"stopping": True})
|
|
721
816
|
self.last_activity = time.monotonic()
|
|
722
817
|
before = self.last_revision
|
|
723
818
|
was_warm = self.context.navigation_cache_is_warm
|
|
@@ -760,6 +855,12 @@ class _CacheService:
|
|
|
760
855
|
def status(self) -> dict[str, object]:
|
|
761
856
|
now = time.monotonic()
|
|
762
857
|
idle = max(0.0, now - self.last_activity)
|
|
858
|
+
context = self._context
|
|
859
|
+
parallel_stats = (
|
|
860
|
+
context.parallel_stats
|
|
861
|
+
if context is not None
|
|
862
|
+
else None
|
|
863
|
+
)
|
|
763
864
|
return {
|
|
764
865
|
"pid": self.metadata.pid, "root": self.metadata.root, "project_file": self.metadata.project_file,
|
|
765
866
|
"version": self.metadata.version, "uptime": now - self.started, "idle_seconds": idle,
|
|
@@ -771,15 +872,18 @@ class _CacheService:
|
|
|
771
872
|
"cache_state": self.cache_state, "requests": self.stats.requests, "warm_hits": self.stats.warm_hits,
|
|
772
873
|
"rebuilds": self.stats.rebuilds, "invalidations": self.stats.invalidations, "evictions": self.stats.evictions,
|
|
773
874
|
"workers_configured": "auto" if self.metadata.workers == 0 else self.metadata.workers,
|
|
774
|
-
"workers_effective":
|
|
775
|
-
"parallel_files_completed":
|
|
875
|
+
"workers_effective": parallel_stats.effective_workers if parallel_stats else 0,
|
|
876
|
+
"parallel_files_completed": parallel_stats.files_completed if parallel_stats else 0,
|
|
776
877
|
"prewarm_seconds": self.prewarm_seconds,
|
|
777
|
-
"parallel_seconds":
|
|
878
|
+
"parallel_seconds": parallel_stats.elapsed_seconds if parallel_stats else 0.0,
|
|
778
879
|
"parallel_fallbacks": self.stats.parallel_fallbacks,
|
|
779
|
-
"navigation_disk_hits":
|
|
780
|
-
"navigation_disk_misses":
|
|
880
|
+
"navigation_disk_hits": context.navigation_disk_hits if context else 0,
|
|
881
|
+
"navigation_disk_misses": context.navigation_disk_misses if context else 0,
|
|
882
|
+
"cpg_cache_entries": context.cpg_cache_entries if context else 0,
|
|
883
|
+
"cpg_cache_bytes": context.cpg_cache_bytes if context else 0,
|
|
781
884
|
"idle_timeout": self.metadata.idle_timeout, "idle_remaining": max(0.0, self.metadata.idle_timeout - idle),
|
|
782
885
|
"workspace_revision": self.last_revision,
|
|
886
|
+
"startup_error": self.startup_error,
|
|
783
887
|
}
|
|
784
888
|
|
|
785
889
|
|
|
@@ -792,7 +896,9 @@ def watch_workspace_changes(
|
|
|
792
896
|
*,
|
|
793
897
|
stop_event: threading.Event,
|
|
794
898
|
on_change: Callable[[], None],
|
|
899
|
+
on_ready: Callable[[], None] | None = None,
|
|
795
900
|
) -> None:
|
|
901
|
+
ready = False
|
|
796
902
|
try:
|
|
797
903
|
for changes in watch(
|
|
798
904
|
root,
|
|
@@ -800,20 +906,33 @@ def watch_workspace_changes(
|
|
|
800
906
|
stop_event=stop_event,
|
|
801
907
|
debounce=50,
|
|
802
908
|
step=20,
|
|
909
|
+
rust_timeout=_WATCHER_READY_POLL_MILLISECONDS,
|
|
910
|
+
yield_on_timeout=True,
|
|
803
911
|
recursive=True,
|
|
804
912
|
raise_interrupt=False,
|
|
805
913
|
):
|
|
914
|
+
if not ready:
|
|
915
|
+
ready = True
|
|
916
|
+
if on_ready is not None:
|
|
917
|
+
on_ready()
|
|
806
918
|
if changes:
|
|
807
919
|
on_change()
|
|
808
920
|
except (OSError, RuntimeError):
|
|
809
921
|
on_change()
|
|
922
|
+
finally:
|
|
923
|
+
if not ready and on_ready is not None:
|
|
924
|
+
on_ready()
|
|
810
925
|
|
|
811
926
|
|
|
812
927
|
def _watch_workspace(service: _CacheService) -> None:
|
|
928
|
+
service.context_ready.wait()
|
|
929
|
+
if service.shutdown.is_set() or service._context is None:
|
|
930
|
+
return
|
|
813
931
|
watch_workspace_changes(
|
|
814
932
|
service.metadata.root,
|
|
815
933
|
stop_event=service.shutdown,
|
|
816
|
-
on_change=service.
|
|
934
|
+
on_change=service._context.invalidate_revision_cache,
|
|
935
|
+
on_ready=service.watcher_ready.set,
|
|
817
936
|
)
|
|
818
937
|
|
|
819
938
|
|
|
@@ -864,8 +983,8 @@ def run_cache_daemon(
|
|
|
864
983
|
)
|
|
865
984
|
watcher: threading.Thread | None = None
|
|
866
985
|
try:
|
|
867
|
-
service = _CacheService(metadata)
|
|
868
|
-
|
|
986
|
+
service = _CacheService(metadata, defer_context=True)
|
|
987
|
+
_write_metadata(metadata)
|
|
869
988
|
watcher = threading.Thread(
|
|
870
989
|
target=_watch_workspace,
|
|
871
990
|
args=(service,),
|
|
@@ -873,8 +992,11 @@ def run_cache_daemon(
|
|
|
873
992
|
daemon=True,
|
|
874
993
|
)
|
|
875
994
|
watcher.start()
|
|
876
|
-
|
|
877
|
-
while
|
|
995
|
+
service.start_prewarm()
|
|
996
|
+
while (
|
|
997
|
+
not service.shutdown.is_set()
|
|
998
|
+
and time.monotonic() - service.last_activity < idle_timeout
|
|
999
|
+
):
|
|
878
1000
|
try:
|
|
879
1001
|
connection, _ = listener.accept()
|
|
880
1002
|
except socket.timeout:
|
|
@@ -885,10 +1007,10 @@ def run_cache_daemon(
|
|
|
885
1007
|
finally:
|
|
886
1008
|
if "service" in locals():
|
|
887
1009
|
service.shutdown.set()
|
|
888
|
-
if watcher is not None:
|
|
889
|
-
watcher.join(timeout=2.0)
|
|
890
1010
|
listener.close()
|
|
891
1011
|
_remove_metadata_if_owned(metadata)
|
|
1012
|
+
if watcher is not None:
|
|
1013
|
+
watcher.join(timeout=2.0)
|
|
892
1014
|
|
|
893
1015
|
|
|
894
1016
|
def _start_cache_unlocked(
|
|
@@ -942,6 +1064,8 @@ def _start_cache_unlocked(
|
|
|
942
1064
|
process = subprocess.Popen(command, **options)
|
|
943
1065
|
deadline = time.monotonic() + startup_timeout
|
|
944
1066
|
startup_ready = False
|
|
1067
|
+
timed_out = False
|
|
1068
|
+
return_code: int | None = None
|
|
945
1069
|
try:
|
|
946
1070
|
while time.monotonic() < deadline:
|
|
947
1071
|
metadata = _read_metadata(canonical)
|
|
@@ -955,10 +1079,12 @@ def _start_cache_unlocked(
|
|
|
955
1079
|
if process.poll() is not None:
|
|
956
1080
|
break
|
|
957
1081
|
time.sleep(0.05)
|
|
958
|
-
|
|
1082
|
+
return_code = process.poll()
|
|
1083
|
+
timed_out = return_code is None
|
|
1084
|
+
if timed_out:
|
|
959
1085
|
process.kill()
|
|
960
1086
|
with contextlib.suppress(Exception):
|
|
961
|
-
process.wait()
|
|
1087
|
+
return_code = process.wait()
|
|
962
1088
|
metadata = _read_metadata(canonical)
|
|
963
1089
|
if metadata and metadata.pid == process.pid:
|
|
964
1090
|
_remove_metadata_if_owned(metadata)
|
|
@@ -966,9 +1092,28 @@ def _start_cache_unlocked(
|
|
|
966
1092
|
raw = _read_startup_tail(diagnostics)
|
|
967
1093
|
message = _truncate_and_sanitize_startup_diagnostics(raw)
|
|
968
1094
|
base = "Cache daemon did not become ready."
|
|
1095
|
+
if timed_out:
|
|
1096
|
+
reason = (
|
|
1097
|
+
f" Startup timed out after {startup_timeout:.1f}s before "
|
|
1098
|
+
"readiness metadata was published."
|
|
1099
|
+
)
|
|
1100
|
+
else:
|
|
1101
|
+
reason = (
|
|
1102
|
+
f" Child process exited with code {return_code} before "
|
|
1103
|
+
"readiness metadata was published."
|
|
1104
|
+
)
|
|
969
1105
|
if message:
|
|
970
|
-
raise CacheClientError(
|
|
971
|
-
|
|
1106
|
+
raise CacheClientError(
|
|
1107
|
+
"startup_failed",
|
|
1108
|
+
f"{base}{reason} Child stderr: {message}",
|
|
1109
|
+
)
|
|
1110
|
+
raise CacheClientError(
|
|
1111
|
+
"startup_failed",
|
|
1112
|
+
(
|
|
1113
|
+
f"{base}{reason} No child diagnostics were emitted. "
|
|
1114
|
+
f"Python executable: {sys.executable}. Workspace: {canonical}."
|
|
1115
|
+
),
|
|
1116
|
+
)
|
|
972
1117
|
finally:
|
|
973
1118
|
if not startup_ready:
|
|
974
1119
|
if process.poll() is None:
|
|
@@ -992,15 +1137,16 @@ def start_cache(
|
|
|
992
1137
|
raise ValueError("idle_timeout must be greater than zero.")
|
|
993
1138
|
if not math.isfinite(startup_timeout) or startup_timeout <= 0:
|
|
994
1139
|
raise ValueError("startup_timeout must be greater than zero.")
|
|
995
|
-
with
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1140
|
+
with _process_start_lock(root):
|
|
1141
|
+
with _start_lock(root, startup_timeout):
|
|
1142
|
+
return _start_cache_unlocked(
|
|
1143
|
+
root,
|
|
1144
|
+
project_file=project_file,
|
|
1145
|
+
max_memory_bytes=max_memory_bytes,
|
|
1146
|
+
workers=workers,
|
|
1147
|
+
idle_timeout=idle_timeout,
|
|
1148
|
+
startup_timeout=startup_timeout,
|
|
1149
|
+
)
|
|
1004
1150
|
|
|
1005
1151
|
|
|
1006
1152
|
def query_cache(root: str | Path, request: dict[str, object]) -> CacheClientResponse:
|
|
@@ -1011,7 +1157,13 @@ def query_cache(root: str | Path, request: dict[str, object]) -> CacheClientResp
|
|
|
1011
1157
|
if not _pid_alive(metadata.pid):
|
|
1012
1158
|
_remove_metadata_if_owned(metadata)
|
|
1013
1159
|
raise CacheClientError("cache_not_running", "Cache daemon is not running.")
|
|
1014
|
-
|
|
1160
|
+
while True:
|
|
1161
|
+
try:
|
|
1162
|
+
return _client_exchange(metadata, request)
|
|
1163
|
+
except CacheClientError as error:
|
|
1164
|
+
if error.code != "cache_warming":
|
|
1165
|
+
raise
|
|
1166
|
+
time.sleep(0.05)
|
|
1015
1167
|
|
|
1016
1168
|
|
|
1017
1169
|
def cache_status(root: str | Path) -> dict[str, object]:
|
delphi_lsp/agent_cli.py
CHANGED
|
@@ -24,7 +24,15 @@ from .agent_cache import (
|
|
|
24
24
|
watch_workspace_changes,
|
|
25
25
|
)
|
|
26
26
|
from .agent_layers import build_codebase_index, layer_payload, render_layer
|
|
27
|
-
from .agent_protocol import
|
|
27
|
+
from .agent_protocol import (
|
|
28
|
+
SCHEMA_VERSION,
|
|
29
|
+
AgentProtocolError,
|
|
30
|
+
SUPPORTED_ACTIONS,
|
|
31
|
+
SUPPORTED_DETAILS,
|
|
32
|
+
SUPPORTED_DIRECTIONS,
|
|
33
|
+
SUPPORTED_GRAPHS,
|
|
34
|
+
SUPPORTED_RELATIONS,
|
|
35
|
+
)
|
|
28
36
|
from .agent_templates import install_opencode_support, install_skill
|
|
29
37
|
from .parallel_outline import ParallelOutlineError, parse_worker_setting
|
|
30
38
|
|
|
@@ -108,13 +116,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
108
116
|
)
|
|
109
117
|
opencode_install.set_defaults(func=_opencode_install)
|
|
110
118
|
|
|
111
|
-
worker = subcommands.add_parser("worker", help="Serve Protocol
|
|
119
|
+
worker = subcommands.add_parser("worker", help="Serve Protocol v3 NDJSON requests.")
|
|
112
120
|
worker.add_argument("--root", type=Path, required=True)
|
|
113
121
|
worker.add_argument("--project-file", type=Path)
|
|
114
122
|
worker.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
115
123
|
worker.set_defaults(func=_worker)
|
|
116
124
|
|
|
117
|
-
cache = subcommands.add_parser("cache", help="Manage the shared Protocol
|
|
125
|
+
cache = subcommands.add_parser("cache", help="Manage the shared Protocol v3 cache daemon.")
|
|
118
126
|
cache_commands = cache.add_subparsers(dest="cache_command", required=True)
|
|
119
127
|
cache_start = cache_commands.add_parser("start", help="Start the cache daemon if needed.")
|
|
120
128
|
_add_cache_start_arguments(cache_start)
|
|
@@ -141,6 +149,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
141
149
|
query.add_argument("--project-id", default="")
|
|
142
150
|
query.add_argument("--detail", choices=SUPPORTED_DETAILS, default="summary")
|
|
143
151
|
query.add_argument("--relation", choices=SUPPORTED_RELATIONS)
|
|
152
|
+
query.add_argument("--graph", choices=SUPPORTED_GRAPHS, default="full")
|
|
153
|
+
query.add_argument("--direction", choices=SUPPORTED_DIRECTIONS, default="out")
|
|
154
|
+
query.add_argument("--depth", type=_cpg_depth, default=4)
|
|
144
155
|
query.add_argument("--cursor", default="")
|
|
145
156
|
query.add_argument("--max-items", type=_max_items, default=12)
|
|
146
157
|
query.add_argument("--max-chars", type=_max_chars, default=12000)
|
|
@@ -179,6 +190,13 @@ def _positive_integer(value: str) -> int:
|
|
|
179
190
|
return parsed
|
|
180
191
|
|
|
181
192
|
|
|
193
|
+
def _cpg_depth(value: str) -> int:
|
|
194
|
+
parsed = int(value)
|
|
195
|
+
if not 1 <= parsed <= 16:
|
|
196
|
+
raise argparse.ArgumentTypeError("depth must be between 1 and 16")
|
|
197
|
+
return parsed
|
|
198
|
+
|
|
199
|
+
|
|
182
200
|
def _max_items(value: str) -> int:
|
|
183
201
|
parsed = int(value)
|
|
184
202
|
if not 1 <= parsed <= 50:
|
|
@@ -271,6 +289,7 @@ def _index(args: argparse.Namespace) -> None:
|
|
|
271
289
|
|
|
272
290
|
|
|
273
291
|
def _skill_install(args: argparse.Namespace) -> None:
|
|
292
|
+
_require_install_directory(args.target)
|
|
274
293
|
try:
|
|
275
294
|
skill_path = install_skill(args.target, force=args.force)
|
|
276
295
|
except FileExistsError as error:
|
|
@@ -279,6 +298,7 @@ def _skill_install(args: argparse.Namespace) -> None:
|
|
|
279
298
|
|
|
280
299
|
|
|
281
300
|
def _opencode_install(args: argparse.Namespace) -> None:
|
|
301
|
+
_require_install_directory(args.target)
|
|
282
302
|
try:
|
|
283
303
|
skill_path, plugin_path, agent_path = install_opencode_support(
|
|
284
304
|
args.target,
|
|
@@ -293,6 +313,12 @@ def _opencode_install(args: argparse.Namespace) -> None:
|
|
|
293
313
|
print(agent_path)
|
|
294
314
|
|
|
295
315
|
|
|
316
|
+
def _require_install_directory(target: str | Path) -> None:
|
|
317
|
+
path = Path(target).expanduser()
|
|
318
|
+
if path.exists() and not path.is_dir():
|
|
319
|
+
raise _CliError("io_error", f"Install target is not a directory: {path}")
|
|
320
|
+
|
|
321
|
+
|
|
296
322
|
def _worker(args: argparse.Namespace) -> None:
|
|
297
323
|
args.root = _workspace_root(args.root)
|
|
298
324
|
context = _open_worker_context(args.root, args.project_file, args.workers)
|
|
@@ -430,13 +456,23 @@ def _query(args: argparse.Namespace) -> int:
|
|
|
430
456
|
if args.value:
|
|
431
457
|
if args.action in {"find", "metrics"}:
|
|
432
458
|
request["query"] = args.value
|
|
433
|
-
elif args.action in {"focus", "inspect", "trace"}:
|
|
459
|
+
elif args.action in {"focus", "inspect", "trace", "cpg"}:
|
|
434
460
|
request["target_id"] = args.value
|
|
435
461
|
else:
|
|
436
462
|
sys.stderr.write(f"cache_error:invalid_request: {args.action} does not accept a value.\n")
|
|
437
463
|
sys.stderr.flush()
|
|
438
464
|
return 1
|
|
439
|
-
for argument, field in (
|
|
465
|
+
for argument, field in (
|
|
466
|
+
("project_id", "project_id"),
|
|
467
|
+
("detail", "detail"),
|
|
468
|
+
("relation", "relation"),
|
|
469
|
+
("graph", "graph"),
|
|
470
|
+
("direction", "direction"),
|
|
471
|
+
("depth", "depth"),
|
|
472
|
+
("cursor", "cursor"),
|
|
473
|
+
("max_items", "max_items"),
|
|
474
|
+
("max_chars", "max_chars"),
|
|
475
|
+
):
|
|
440
476
|
value = getattr(args, argument)
|
|
441
477
|
if value is not None:
|
|
442
478
|
request[field] = value
|
|
@@ -521,7 +557,7 @@ def _serve_worker(context: AgentContext, input_stream: BinaryIO, output_stream:
|
|
|
521
557
|
message = _worker_error("internal_error", _INTERNAL_ERROR_MESSAGE)
|
|
522
558
|
_write_worker_message(output_stream, message)
|
|
523
559
|
def _worker_error(code: str, message: str) -> dict[str, object]:
|
|
524
|
-
return {"schema":
|
|
560
|
+
return {"schema": SCHEMA_VERSION, "error": {"code": code, "message": message}}
|
|
525
561
|
|
|
526
562
|
|
|
527
563
|
def _write_worker_message(output_stream: BinaryIO, message: object) -> None:
|