python-delphi-lsp 2.3.3__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 +159 -43
- 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.3.dist-info → python_delphi_lsp-3.0.0.dist-info}/METADATA +60 -18
- {python_delphi_lsp-2.3.3.dist-info → python_delphi_lsp-3.0.0.dist-info}/RECORD +14 -12
- {python_delphi_lsp-2.3.3.dist-info → python_delphi_lsp-3.0.0.dist-info}/WHEEL +0 -0
- {python_delphi_lsp-2.3.3.dist-info → python_delphi_lsp-3.0.0.dist-info}/entry_points.txt +0 -0
- {python_delphi_lsp-2.3.3.dist-info → python_delphi_lsp-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {python_delphi_lsp-2.3.3.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,31 +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()
|
|
682
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
|
|
683
735
|
|
|
684
736
|
def _measure_retained_bytes(self) -> int:
|
|
685
737
|
process_rss = current_process_rss_bytes()
|
|
@@ -692,8 +744,9 @@ class _CacheService:
|
|
|
692
744
|
|
|
693
745
|
def prewarm(self) -> None:
|
|
694
746
|
started = time.monotonic()
|
|
747
|
+
context = self._initialize_context()
|
|
695
748
|
try:
|
|
696
|
-
self.last_revision =
|
|
749
|
+
self.last_revision = context.prewarm_navigation()
|
|
697
750
|
self.cache_state = "warm"
|
|
698
751
|
except AgentProtocolError as error:
|
|
699
752
|
if error.code != "project_required":
|
|
@@ -708,15 +761,24 @@ class _CacheService:
|
|
|
708
761
|
self.stats.evictions += 1
|
|
709
762
|
self.cache_state = "compact"
|
|
710
763
|
self.prewarm_seconds = time.monotonic() - started
|
|
711
|
-
self.stats.parallel_fallbacks =
|
|
764
|
+
self.stats.parallel_fallbacks = context.parallel_stats.fallbacks
|
|
712
765
|
|
|
713
766
|
def start_prewarm(self) -> None:
|
|
714
767
|
def run() -> None:
|
|
715
768
|
try:
|
|
716
769
|
with self.lock:
|
|
717
770
|
self.prewarm()
|
|
718
|
-
except Exception:
|
|
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__
|
|
719
779
|
self.cache_state = "failed"
|
|
780
|
+
finally:
|
|
781
|
+
self.context_ready.set()
|
|
720
782
|
|
|
721
783
|
self.prewarm_thread = threading.Thread(
|
|
722
784
|
target=run,
|
|
@@ -740,7 +802,16 @@ class _CacheService:
|
|
|
740
802
|
"Cache is still warming. Retry after cache status reports ready.",
|
|
741
803
|
)
|
|
742
804
|
if self.cache_state == "failed":
|
|
743
|
-
|
|
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)
|
|
744
815
|
with self.lock:
|
|
745
816
|
self.last_activity = time.monotonic()
|
|
746
817
|
before = self.last_revision
|
|
@@ -784,6 +855,12 @@ class _CacheService:
|
|
|
784
855
|
def status(self) -> dict[str, object]:
|
|
785
856
|
now = time.monotonic()
|
|
786
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
|
+
)
|
|
787
864
|
return {
|
|
788
865
|
"pid": self.metadata.pid, "root": self.metadata.root, "project_file": self.metadata.project_file,
|
|
789
866
|
"version": self.metadata.version, "uptime": now - self.started, "idle_seconds": idle,
|
|
@@ -795,15 +872,18 @@ class _CacheService:
|
|
|
795
872
|
"cache_state": self.cache_state, "requests": self.stats.requests, "warm_hits": self.stats.warm_hits,
|
|
796
873
|
"rebuilds": self.stats.rebuilds, "invalidations": self.stats.invalidations, "evictions": self.stats.evictions,
|
|
797
874
|
"workers_configured": "auto" if self.metadata.workers == 0 else self.metadata.workers,
|
|
798
|
-
"workers_effective":
|
|
799
|
-
"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,
|
|
800
877
|
"prewarm_seconds": self.prewarm_seconds,
|
|
801
|
-
"parallel_seconds":
|
|
878
|
+
"parallel_seconds": parallel_stats.elapsed_seconds if parallel_stats else 0.0,
|
|
802
879
|
"parallel_fallbacks": self.stats.parallel_fallbacks,
|
|
803
|
-
"navigation_disk_hits":
|
|
804
|
-
"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,
|
|
805
884
|
"idle_timeout": self.metadata.idle_timeout, "idle_remaining": max(0.0, self.metadata.idle_timeout - idle),
|
|
806
885
|
"workspace_revision": self.last_revision,
|
|
886
|
+
"startup_error": self.startup_error,
|
|
807
887
|
}
|
|
808
888
|
|
|
809
889
|
|
|
@@ -816,7 +896,9 @@ def watch_workspace_changes(
|
|
|
816
896
|
*,
|
|
817
897
|
stop_event: threading.Event,
|
|
818
898
|
on_change: Callable[[], None],
|
|
899
|
+
on_ready: Callable[[], None] | None = None,
|
|
819
900
|
) -> None:
|
|
901
|
+
ready = False
|
|
820
902
|
try:
|
|
821
903
|
for changes in watch(
|
|
822
904
|
root,
|
|
@@ -824,20 +906,33 @@ def watch_workspace_changes(
|
|
|
824
906
|
stop_event=stop_event,
|
|
825
907
|
debounce=50,
|
|
826
908
|
step=20,
|
|
909
|
+
rust_timeout=_WATCHER_READY_POLL_MILLISECONDS,
|
|
910
|
+
yield_on_timeout=True,
|
|
827
911
|
recursive=True,
|
|
828
912
|
raise_interrupt=False,
|
|
829
913
|
):
|
|
914
|
+
if not ready:
|
|
915
|
+
ready = True
|
|
916
|
+
if on_ready is not None:
|
|
917
|
+
on_ready()
|
|
830
918
|
if changes:
|
|
831
919
|
on_change()
|
|
832
920
|
except (OSError, RuntimeError):
|
|
833
921
|
on_change()
|
|
922
|
+
finally:
|
|
923
|
+
if not ready and on_ready is not None:
|
|
924
|
+
on_ready()
|
|
834
925
|
|
|
835
926
|
|
|
836
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
|
|
837
931
|
watch_workspace_changes(
|
|
838
932
|
service.metadata.root,
|
|
839
933
|
stop_event=service.shutdown,
|
|
840
|
-
on_change=service.
|
|
934
|
+
on_change=service._context.invalidate_revision_cache,
|
|
935
|
+
on_ready=service.watcher_ready.set,
|
|
841
936
|
)
|
|
842
937
|
|
|
843
938
|
|
|
@@ -888,7 +983,7 @@ def run_cache_daemon(
|
|
|
888
983
|
)
|
|
889
984
|
watcher: threading.Thread | None = None
|
|
890
985
|
try:
|
|
891
|
-
service = _CacheService(metadata)
|
|
986
|
+
service = _CacheService(metadata, defer_context=True)
|
|
892
987
|
_write_metadata(metadata)
|
|
893
988
|
watcher = threading.Thread(
|
|
894
989
|
target=_watch_workspace,
|
|
@@ -900,10 +995,7 @@ def run_cache_daemon(
|
|
|
900
995
|
service.start_prewarm()
|
|
901
996
|
while (
|
|
902
997
|
not service.shutdown.is_set()
|
|
903
|
-
and (
|
|
904
|
-
service.cache_state == "warming"
|
|
905
|
-
or time.monotonic() - service.last_activity < idle_timeout
|
|
906
|
-
)
|
|
998
|
+
and time.monotonic() - service.last_activity < idle_timeout
|
|
907
999
|
):
|
|
908
1000
|
try:
|
|
909
1001
|
connection, _ = listener.accept()
|
|
@@ -915,10 +1007,10 @@ def run_cache_daemon(
|
|
|
915
1007
|
finally:
|
|
916
1008
|
if "service" in locals():
|
|
917
1009
|
service.shutdown.set()
|
|
918
|
-
if watcher is not None:
|
|
919
|
-
watcher.join(timeout=2.0)
|
|
920
1010
|
listener.close()
|
|
921
1011
|
_remove_metadata_if_owned(metadata)
|
|
1012
|
+
if watcher is not None:
|
|
1013
|
+
watcher.join(timeout=2.0)
|
|
922
1014
|
|
|
923
1015
|
|
|
924
1016
|
def _start_cache_unlocked(
|
|
@@ -972,6 +1064,8 @@ def _start_cache_unlocked(
|
|
|
972
1064
|
process = subprocess.Popen(command, **options)
|
|
973
1065
|
deadline = time.monotonic() + startup_timeout
|
|
974
1066
|
startup_ready = False
|
|
1067
|
+
timed_out = False
|
|
1068
|
+
return_code: int | None = None
|
|
975
1069
|
try:
|
|
976
1070
|
while time.monotonic() < deadline:
|
|
977
1071
|
metadata = _read_metadata(canonical)
|
|
@@ -985,10 +1079,12 @@ def _start_cache_unlocked(
|
|
|
985
1079
|
if process.poll() is not None:
|
|
986
1080
|
break
|
|
987
1081
|
time.sleep(0.05)
|
|
988
|
-
|
|
1082
|
+
return_code = process.poll()
|
|
1083
|
+
timed_out = return_code is None
|
|
1084
|
+
if timed_out:
|
|
989
1085
|
process.kill()
|
|
990
1086
|
with contextlib.suppress(Exception):
|
|
991
|
-
process.wait()
|
|
1087
|
+
return_code = process.wait()
|
|
992
1088
|
metadata = _read_metadata(canonical)
|
|
993
1089
|
if metadata and metadata.pid == process.pid:
|
|
994
1090
|
_remove_metadata_if_owned(metadata)
|
|
@@ -996,9 +1092,28 @@ def _start_cache_unlocked(
|
|
|
996
1092
|
raw = _read_startup_tail(diagnostics)
|
|
997
1093
|
message = _truncate_and_sanitize_startup_diagnostics(raw)
|
|
998
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
|
+
)
|
|
999
1105
|
if message:
|
|
1000
|
-
raise CacheClientError(
|
|
1001
|
-
|
|
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
|
+
)
|
|
1002
1117
|
finally:
|
|
1003
1118
|
if not startup_ready:
|
|
1004
1119
|
if process.poll() is None:
|
|
@@ -1022,15 +1137,16 @@ def start_cache(
|
|
|
1022
1137
|
raise ValueError("idle_timeout must be greater than zero.")
|
|
1023
1138
|
if not math.isfinite(startup_timeout) or startup_timeout <= 0:
|
|
1024
1139
|
raise ValueError("startup_timeout must be greater than zero.")
|
|
1025
|
-
with
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
+
)
|
|
1034
1150
|
|
|
1035
1151
|
|
|
1036
1152
|
def query_cache(root: str | Path, request: dict[str, object]) -> CacheClientResponse:
|
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:
|