python-delphi-lsp 2.3.3__py3-none-any.whl → 3.1.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 +74 -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_layers.py +1 -1
- delphi_lsp/agent_metrics.py +110 -1
- delphi_lsp/agent_protocol.py +57 -3
- delphi_lsp/agent_templates.py +20 -13
- delphi_lsp/agent_wiki.py +1084 -0
- {python_delphi_lsp-2.3.3.dist-info → python_delphi_lsp-3.1.0.dist-info}/METADATA +98 -19
- {python_delphi_lsp-2.3.3.dist-info → python_delphi_lsp-3.1.0.dist-info}/RECORD +17 -14
- {python_delphi_lsp-2.3.3.dist-info → python_delphi_lsp-3.1.0.dist-info}/WHEEL +0 -0
- {python_delphi_lsp-2.3.3.dist-info → python_delphi_lsp-3.1.0.dist-info}/entry_points.txt +0 -0
- {python_delphi_lsp-2.3.3.dist-info → python_delphi_lsp-3.1.0.dist-info}/licenses/LICENSE +0 -0
- {python_delphi_lsp-2.3.3.dist-info → python_delphi_lsp-3.1.0.dist-info}/top_level.txt +0 -0
delphi_lsp/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "
|
|
1
|
+
__version__ = "3.1.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,8 +24,17 @@ 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
|
|
37
|
+
from .agent_wiki import WikiExportError, export_okf_wiki
|
|
29
38
|
from .parallel_outline import ParallelOutlineError, parse_worker_setting
|
|
30
39
|
|
|
31
40
|
|
|
@@ -84,6 +93,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
84
93
|
index.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
85
94
|
index.set_defaults(func=_index)
|
|
86
95
|
|
|
96
|
+
wiki = subcommands.add_parser("wiki", help="Export a portable Markdown knowledge wiki.")
|
|
97
|
+
wiki_commands = wiki.add_subparsers(dest="wiki_command", required=True)
|
|
98
|
+
wiki_export = wiki_commands.add_parser(
|
|
99
|
+
"export",
|
|
100
|
+
help="Export all indexed codebase knowledge as an Open Knowledge Format bundle.",
|
|
101
|
+
)
|
|
102
|
+
wiki_export.add_argument("--root", type=Path, default=Path("."))
|
|
103
|
+
wiki_export.add_argument("--project-file", type=Path)
|
|
104
|
+
wiki_export.add_argument("--out", type=Path, default=Path(".delphi-lsp") / "wiki")
|
|
105
|
+
wiki_export.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
106
|
+
wiki_export.add_argument("--force", action="store_true")
|
|
107
|
+
wiki_export.set_defaults(func=_wiki_export)
|
|
108
|
+
|
|
87
109
|
skill = subcommands.add_parser("skill", help="Install agent skill templates.")
|
|
88
110
|
skill_commands = skill.add_subparsers(dest="skill_command", required=True)
|
|
89
111
|
skill_install = skill_commands.add_parser("install", help="Install .agents skill.")
|
|
@@ -108,13 +130,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
108
130
|
)
|
|
109
131
|
opencode_install.set_defaults(func=_opencode_install)
|
|
110
132
|
|
|
111
|
-
worker = subcommands.add_parser("worker", help="Serve Protocol
|
|
133
|
+
worker = subcommands.add_parser("worker", help="Serve Protocol v3 NDJSON requests.")
|
|
112
134
|
worker.add_argument("--root", type=Path, required=True)
|
|
113
135
|
worker.add_argument("--project-file", type=Path)
|
|
114
136
|
worker.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
115
137
|
worker.set_defaults(func=_worker)
|
|
116
138
|
|
|
117
|
-
cache = subcommands.add_parser("cache", help="Manage the shared Protocol
|
|
139
|
+
cache = subcommands.add_parser("cache", help="Manage the shared Protocol v3 cache daemon.")
|
|
118
140
|
cache_commands = cache.add_subparsers(dest="cache_command", required=True)
|
|
119
141
|
cache_start = cache_commands.add_parser("start", help="Start the cache daemon if needed.")
|
|
120
142
|
_add_cache_start_arguments(cache_start)
|
|
@@ -141,6 +163,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
141
163
|
query.add_argument("--project-id", default="")
|
|
142
164
|
query.add_argument("--detail", choices=SUPPORTED_DETAILS, default="summary")
|
|
143
165
|
query.add_argument("--relation", choices=SUPPORTED_RELATIONS)
|
|
166
|
+
query.add_argument("--graph", choices=SUPPORTED_GRAPHS, default="full")
|
|
167
|
+
query.add_argument("--direction", choices=SUPPORTED_DIRECTIONS, default="out")
|
|
168
|
+
query.add_argument("--depth", type=_cpg_depth, default=4)
|
|
144
169
|
query.add_argument("--cursor", default="")
|
|
145
170
|
query.add_argument("--max-items", type=_max_items, default=12)
|
|
146
171
|
query.add_argument("--max-chars", type=_max_chars, default=12000)
|
|
@@ -179,6 +204,13 @@ def _positive_integer(value: str) -> int:
|
|
|
179
204
|
return parsed
|
|
180
205
|
|
|
181
206
|
|
|
207
|
+
def _cpg_depth(value: str) -> int:
|
|
208
|
+
parsed = int(value)
|
|
209
|
+
if not 1 <= parsed <= 16:
|
|
210
|
+
raise argparse.ArgumentTypeError("depth must be between 1 and 16")
|
|
211
|
+
return parsed
|
|
212
|
+
|
|
213
|
+
|
|
182
214
|
def _max_items(value: str) -> int:
|
|
183
215
|
parsed = int(value)
|
|
184
216
|
if not 1 <= parsed <= 50:
|
|
@@ -270,7 +302,26 @@ def _index(args: argparse.Namespace) -> None:
|
|
|
270
302
|
print(args.out)
|
|
271
303
|
|
|
272
304
|
|
|
305
|
+
def _wiki_export(args: argparse.Namespace) -> None:
|
|
306
|
+
args.root = _workspace_root(args.root)
|
|
307
|
+
output = args.out
|
|
308
|
+
if not output.is_absolute():
|
|
309
|
+
output = args.root / output
|
|
310
|
+
try:
|
|
311
|
+
result = export_okf_wiki(
|
|
312
|
+
args.root,
|
|
313
|
+
output,
|
|
314
|
+
project_file=args.project_file,
|
|
315
|
+
workers=args.workers,
|
|
316
|
+
force=args.force,
|
|
317
|
+
)
|
|
318
|
+
except WikiExportError as error:
|
|
319
|
+
raise _CliError("wiki_export_failed", str(error)) from None
|
|
320
|
+
_write_json(result.to_mapping())
|
|
321
|
+
|
|
322
|
+
|
|
273
323
|
def _skill_install(args: argparse.Namespace) -> None:
|
|
324
|
+
_require_install_directory(args.target)
|
|
274
325
|
try:
|
|
275
326
|
skill_path = install_skill(args.target, force=args.force)
|
|
276
327
|
except FileExistsError as error:
|
|
@@ -279,6 +330,7 @@ def _skill_install(args: argparse.Namespace) -> None:
|
|
|
279
330
|
|
|
280
331
|
|
|
281
332
|
def _opencode_install(args: argparse.Namespace) -> None:
|
|
333
|
+
_require_install_directory(args.target)
|
|
282
334
|
try:
|
|
283
335
|
skill_path, plugin_path, agent_path = install_opencode_support(
|
|
284
336
|
args.target,
|
|
@@ -293,6 +345,12 @@ def _opencode_install(args: argparse.Namespace) -> None:
|
|
|
293
345
|
print(agent_path)
|
|
294
346
|
|
|
295
347
|
|
|
348
|
+
def _require_install_directory(target: str | Path) -> None:
|
|
349
|
+
path = Path(target).expanduser()
|
|
350
|
+
if path.exists() and not path.is_dir():
|
|
351
|
+
raise _CliError("io_error", f"Install target is not a directory: {path}")
|
|
352
|
+
|
|
353
|
+
|
|
296
354
|
def _worker(args: argparse.Namespace) -> None:
|
|
297
355
|
args.root = _workspace_root(args.root)
|
|
298
356
|
context = _open_worker_context(args.root, args.project_file, args.workers)
|
|
@@ -430,13 +488,23 @@ def _query(args: argparse.Namespace) -> int:
|
|
|
430
488
|
if args.value:
|
|
431
489
|
if args.action in {"find", "metrics"}:
|
|
432
490
|
request["query"] = args.value
|
|
433
|
-
elif args.action in {"focus", "inspect", "trace"}:
|
|
491
|
+
elif args.action in {"focus", "inspect", "trace", "cpg"}:
|
|
434
492
|
request["target_id"] = args.value
|
|
435
493
|
else:
|
|
436
494
|
sys.stderr.write(f"cache_error:invalid_request: {args.action} does not accept a value.\n")
|
|
437
495
|
sys.stderr.flush()
|
|
438
496
|
return 1
|
|
439
|
-
for argument, field in (
|
|
497
|
+
for argument, field in (
|
|
498
|
+
("project_id", "project_id"),
|
|
499
|
+
("detail", "detail"),
|
|
500
|
+
("relation", "relation"),
|
|
501
|
+
("graph", "graph"),
|
|
502
|
+
("direction", "direction"),
|
|
503
|
+
("depth", "depth"),
|
|
504
|
+
("cursor", "cursor"),
|
|
505
|
+
("max_items", "max_items"),
|
|
506
|
+
("max_chars", "max_chars"),
|
|
507
|
+
):
|
|
440
508
|
value = getattr(args, argument)
|
|
441
509
|
if value is not None:
|
|
442
510
|
request[field] = value
|
|
@@ -521,7 +589,7 @@ def _serve_worker(context: AgentContext, input_stream: BinaryIO, output_stream:
|
|
|
521
589
|
message = _worker_error("internal_error", _INTERNAL_ERROR_MESSAGE)
|
|
522
590
|
_write_worker_message(output_stream, message)
|
|
523
591
|
def _worker_error(code: str, message: str) -> dict[str, object]:
|
|
524
|
-
return {"schema":
|
|
592
|
+
return {"schema": SCHEMA_VERSION, "error": {"code": code, "message": message}}
|
|
525
593
|
|
|
526
594
|
|
|
527
595
|
def _write_worker_message(output_stream: BinaryIO, message: object) -> None:
|