python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.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/__init__.py +184 -0
- delphi_lsp/_version.py +1 -0
- delphi_lsp/agent_cache.py +1574 -0
- delphi_lsp/agent_cli.py +902 -0
- delphi_lsp/agent_context.py +3907 -0
- delphi_lsp/agent_cpg.py +349 -0
- delphi_lsp/agent_cpg_builder.py +666 -0
- delphi_lsp/agent_layers.py +1410 -0
- delphi_lsp/agent_metrics.py +526 -0
- delphi_lsp/agent_protocol.py +445 -0
- delphi_lsp/agent_relations.py +1881 -0
- delphi_lsp/agent_templates.py +614 -0
- delphi_lsp/agent_wiki.py +2853 -0
- delphi_lsp/agent_workspace.py +860 -0
- delphi_lsp/ast_serialize.py +126 -0
- delphi_lsp/binary.py +250 -0
- delphi_lsp/comment_builder.py +28 -0
- delphi_lsp/consts.py +345 -0
- delphi_lsp/delphi_lsp_native.pyd +0 -0
- delphi_lsp/delphiast_lexer.py +399 -0
- delphi_lsp/delphiast_parser.py +2189 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/grammar.py +459 -0
- delphi_lsp/incremental.py +1139 -0
- delphi_lsp/lark_builder.py +2684 -0
- delphi_lsp/lark_tokens.py +236 -0
- delphi_lsp/lexical_scanner.py +30 -0
- delphi_lsp/lsp_server.py +6698 -0
- delphi_lsp/metrics.py +1452 -0
- delphi_lsp/native_bridge.py +300 -0
- delphi_lsp/navigation_cache.py +189 -0
- delphi_lsp/nodes.py +462 -0
- delphi_lsp/parallel_outline.py +662 -0
- delphi_lsp/parse_artifacts.py +969 -0
- delphi_lsp/parser.py +342 -0
- delphi_lsp/parser_backend.py +139 -0
- delphi_lsp/preprocessor.py +1122 -0
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_config.py +285 -0
- delphi_lsp/project_discovery.py +932 -0
- delphi_lsp/project_indexer.py +470 -0
- delphi_lsp/py.typed +1 -0
- delphi_lsp/response_cache.py +465 -0
- delphi_lsp/semantic.py +394 -0
- delphi_lsp/semantic_builder.py +2302 -0
- delphi_lsp/source_reader.py +17 -0
- delphi_lsp/wiki_external_sort.py +392 -0
- delphi_lsp/wiki_memory.py +548 -0
- delphi_lsp/workspace.py +83 -0
- delphi_lsp/writer.py +73 -0
- python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
- python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
- python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
- python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
- python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
- python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
delphi_lsp/agent_cli.py
ADDED
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import BinaryIO, TextIO
|
|
12
|
+
|
|
13
|
+
from .agent_cache import (
|
|
14
|
+
DEFAULT_IDLE_TIMEOUT,
|
|
15
|
+
DEFAULT_MAX_DISK_CACHE_BYTES,
|
|
16
|
+
DEFAULT_MAX_MEMORY_BYTES,
|
|
17
|
+
DEFAULT_STARTUP_TIMEOUT,
|
|
18
|
+
CacheClientError,
|
|
19
|
+
navigation_cache_path,
|
|
20
|
+
parse_memory_size,
|
|
21
|
+
query_cache,
|
|
22
|
+
run_cache_daemon,
|
|
23
|
+
start_cache,
|
|
24
|
+
stop_cache,
|
|
25
|
+
watch_workspace_changes,
|
|
26
|
+
)
|
|
27
|
+
from .agent_context import AgentContext
|
|
28
|
+
from .agent_layers import (
|
|
29
|
+
build_codebase_index,
|
|
30
|
+
build_metrics_payload,
|
|
31
|
+
build_references_payload,
|
|
32
|
+
build_units_payload,
|
|
33
|
+
layer_payload,
|
|
34
|
+
render_layer,
|
|
35
|
+
render_payload,
|
|
36
|
+
)
|
|
37
|
+
from .agent_protocol import (
|
|
38
|
+
SCHEMA_VERSION,
|
|
39
|
+
SUPPORTED_ACTIONS,
|
|
40
|
+
SUPPORTED_DETAILS,
|
|
41
|
+
SUPPORTED_DIRECTIONS,
|
|
42
|
+
SUPPORTED_GRAPHS,
|
|
43
|
+
SUPPORTED_RELATIONS,
|
|
44
|
+
AgentProtocolError,
|
|
45
|
+
)
|
|
46
|
+
from .agent_templates import install_opencode_support, install_skill
|
|
47
|
+
from .agent_wiki import WikiExportError, WikiProgressEvent, export_okf_wiki
|
|
48
|
+
from .parallel_outline import ParallelOutlineError, parse_worker_setting
|
|
49
|
+
from .project_config import ProjectConfigError
|
|
50
|
+
from .response_cache import (
|
|
51
|
+
ResponseCacheKey,
|
|
52
|
+
ResponseCacheStore,
|
|
53
|
+
WorkspaceContentSnapshot,
|
|
54
|
+
workspace_content_snapshot,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
_MAX_WORKER_RECORD_BYTES = 1024 * 1024
|
|
58
|
+
_INVALID_JSON_MESSAGE = "Invalid JSON request."
|
|
59
|
+
_INVALID_ENCODING_MESSAGE = "Invalid UTF-8 request."
|
|
60
|
+
_REQUEST_TOO_LARGE_MESSAGE = "Request exceeds the 1 MiB limit."
|
|
61
|
+
_INTERNAL_ERROR_MESSAGE = "Internal request error."
|
|
62
|
+
_SOURCE_UNAVAILABLE_MESSAGE = "Selected source is unavailable."
|
|
63
|
+
_WORKER_REVISION_CHECK_INTERVAL_SECONDS = 30.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _CliError(RuntimeError):
|
|
67
|
+
def __init__(self, code: str, message: str) -> None:
|
|
68
|
+
self.code = code
|
|
69
|
+
self.message = message
|
|
70
|
+
super().__init__(message)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
74
|
+
parser = argparse.ArgumentParser(
|
|
75
|
+
prog="delphi-lsp-agent",
|
|
76
|
+
description="Agent-facing Delphi/Object Pascal codebase navigation helpers.",
|
|
77
|
+
)
|
|
78
|
+
subcommands = parser.add_subparsers(dest="command", required=True)
|
|
79
|
+
|
|
80
|
+
view = subcommands.add_parser("view", help="Render a layered codebase view.")
|
|
81
|
+
view.add_argument("--root", type=Path, default=Path("."))
|
|
82
|
+
view.add_argument("--project-file", type=Path)
|
|
83
|
+
view.add_argument(
|
|
84
|
+
"--layer",
|
|
85
|
+
required=True,
|
|
86
|
+
choices=[
|
|
87
|
+
"overview",
|
|
88
|
+
"projects",
|
|
89
|
+
"units",
|
|
90
|
+
"unit",
|
|
91
|
+
"symbols",
|
|
92
|
+
"symbol",
|
|
93
|
+
"implementation",
|
|
94
|
+
"references",
|
|
95
|
+
"problems",
|
|
96
|
+
"metrics",
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
view.add_argument("--query", default="")
|
|
100
|
+
view.add_argument("--format", default="markdown", choices=["markdown", "json"])
|
|
101
|
+
view.add_argument("--deep-projects", action="store_true", help="Deep-parse project dependencies for the projects layer.")
|
|
102
|
+
view.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
103
|
+
view.set_defaults(func=_view)
|
|
104
|
+
|
|
105
|
+
index = subcommands.add_parser("index", help="Materialize a JSON codebase index.")
|
|
106
|
+
index.add_argument("--root", type=Path, default=Path("."))
|
|
107
|
+
index.add_argument("--project-file", type=Path)
|
|
108
|
+
index.add_argument("--out", type=Path, default=Path(".delphi-lsp") / "agent-index" / "index.json")
|
|
109
|
+
index.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
110
|
+
index.add_argument(
|
|
111
|
+
"--deep-projects",
|
|
112
|
+
action="store_true",
|
|
113
|
+
help="Deep-parse project dependencies for the projects layer.",
|
|
114
|
+
)
|
|
115
|
+
index.set_defaults(func=_index)
|
|
116
|
+
|
|
117
|
+
wiki = subcommands.add_parser("wiki", help="Export a portable Markdown knowledge wiki.")
|
|
118
|
+
wiki_commands = wiki.add_subparsers(dest="wiki_command", required=True)
|
|
119
|
+
wiki_export = wiki_commands.add_parser(
|
|
120
|
+
"export",
|
|
121
|
+
help="Export all indexed codebase knowledge as an Open Knowledge Format bundle.",
|
|
122
|
+
)
|
|
123
|
+
wiki_export.add_argument("--root", type=Path, default=Path("."))
|
|
124
|
+
wiki_export.add_argument("--project-file", type=Path)
|
|
125
|
+
wiki_export.add_argument("--out", type=Path, default=Path(".delphi-lsp") / "wiki")
|
|
126
|
+
wiki_export.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
127
|
+
wiki_export.add_argument(
|
|
128
|
+
"--max-memory",
|
|
129
|
+
type=parse_memory_size,
|
|
130
|
+
help="Hard process-tree memory limit (default: min(4G, 90%% of system RAM)).",
|
|
131
|
+
)
|
|
132
|
+
wiki_export.add_argument("--force", action="store_true")
|
|
133
|
+
wiki_export.add_argument(
|
|
134
|
+
"--quiet",
|
|
135
|
+
action="store_true",
|
|
136
|
+
help="Suppress progress output on stderr.",
|
|
137
|
+
)
|
|
138
|
+
wiki_export.set_defaults(func=_wiki_export)
|
|
139
|
+
|
|
140
|
+
skill = subcommands.add_parser("skill", help="Install agent skill templates.")
|
|
141
|
+
skill_commands = skill.add_subparsers(dest="skill_command", required=True)
|
|
142
|
+
skill_install = skill_commands.add_parser("install", help="Install .agents skill.")
|
|
143
|
+
skill_install.add_argument("--target", type=Path, default=Path("."))
|
|
144
|
+
skill_install.add_argument("--force", action="store_true")
|
|
145
|
+
skill_install.set_defaults(func=_skill_install)
|
|
146
|
+
|
|
147
|
+
opencode = subcommands.add_parser("opencode", help="Install opencode integration.")
|
|
148
|
+
opencode_commands = opencode.add_subparsers(dest="opencode_command", required=True)
|
|
149
|
+
opencode_install = opencode_commands.add_parser(
|
|
150
|
+
"install", help="Install the package-named skill, Markdown agent, and OpenCode plugin."
|
|
151
|
+
)
|
|
152
|
+
opencode_install.add_argument("--target", type=Path, default=Path("."))
|
|
153
|
+
opencode_install.add_argument("--python", default=sys.executable)
|
|
154
|
+
opencode_install.add_argument("--force", action="store_true")
|
|
155
|
+
opencode_install.add_argument(
|
|
156
|
+
"--write-agent",
|
|
157
|
+
"--write-config",
|
|
158
|
+
dest="write_config",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help="Deprecated compatibility option; the Markdown agent is always installed and opencode.json is never touched.",
|
|
161
|
+
)
|
|
162
|
+
opencode_install.set_defaults(func=_opencode_install)
|
|
163
|
+
|
|
164
|
+
worker = subcommands.add_parser("worker", help="Serve Protocol v3 NDJSON requests.")
|
|
165
|
+
worker.add_argument("--root", type=Path, required=True)
|
|
166
|
+
worker.add_argument("--project-file", type=Path)
|
|
167
|
+
worker.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
168
|
+
worker.set_defaults(func=_worker)
|
|
169
|
+
|
|
170
|
+
cache = subcommands.add_parser("cache", help="Manage the shared Protocol v3 cache daemon.")
|
|
171
|
+
cache_commands = cache.add_subparsers(dest="cache_command", required=True)
|
|
172
|
+
cache_start = cache_commands.add_parser("start", help="Start the cache daemon if needed.")
|
|
173
|
+
_add_cache_start_arguments(cache_start)
|
|
174
|
+
cache_start.set_defaults(func=_cache_start)
|
|
175
|
+
cache_status_parser = cache_commands.add_parser("status", help="Show cache daemon status.")
|
|
176
|
+
cache_status_parser.add_argument("--root", type=Path, default=Path("."))
|
|
177
|
+
cache_status_parser.add_argument("--format", choices=["text", "json"], default="text")
|
|
178
|
+
cache_status_parser.set_defaults(func=_cache_status)
|
|
179
|
+
cache_stop = cache_commands.add_parser("stop", help="Stop the cache daemon if it is running.")
|
|
180
|
+
cache_stop.add_argument("--root", type=Path, default=Path("."))
|
|
181
|
+
cache_stop.set_defaults(func=_cache_stop)
|
|
182
|
+
cache_clear = cache_commands.add_parser(
|
|
183
|
+
"clear",
|
|
184
|
+
help="Stop the daemon and remove persistent navigation shards.",
|
|
185
|
+
)
|
|
186
|
+
cache_clear.add_argument("--root", type=Path, default=Path("."))
|
|
187
|
+
cache_clear.set_defaults(func=_cache_clear)
|
|
188
|
+
cache_serve = cache_commands.add_parser("serve", help=argparse.SUPPRESS)
|
|
189
|
+
cache_serve.add_argument("--root", type=Path, required=True)
|
|
190
|
+
cache_serve.add_argument("--project-file", type=Path)
|
|
191
|
+
cache_serve.add_argument("--max-memory", type=parse_memory_size, required=True)
|
|
192
|
+
cache_serve.add_argument("--workers", type=parse_worker_setting, required=True)
|
|
193
|
+
cache_serve.add_argument("--idle-timeout", type=_positive_integer, required=True)
|
|
194
|
+
cache_serve.add_argument(
|
|
195
|
+
"--max-disk-cache",
|
|
196
|
+
type=parse_memory_size,
|
|
197
|
+
default=DEFAULT_MAX_DISK_CACHE_BYTES,
|
|
198
|
+
)
|
|
199
|
+
cache_serve.set_defaults(func=_cache_serve)
|
|
200
|
+
|
|
201
|
+
query = subcommands.add_parser("query", help="Send an ergonomic request to a running cache daemon.")
|
|
202
|
+
query.add_argument("--root", type=Path, default=Path("."))
|
|
203
|
+
query.add_argument("action", choices=SUPPORTED_ACTIONS)
|
|
204
|
+
query.add_argument("value", nargs="?", default="")
|
|
205
|
+
query.add_argument("--project-id", default="")
|
|
206
|
+
query.add_argument("--target-id", default="")
|
|
207
|
+
query.add_argument("--detail", choices=SUPPORTED_DETAILS, default="summary")
|
|
208
|
+
query.add_argument("--relation", choices=SUPPORTED_RELATIONS)
|
|
209
|
+
query.add_argument("--graph", choices=SUPPORTED_GRAPHS, default="full")
|
|
210
|
+
query.add_argument("--direction", choices=SUPPORTED_DIRECTIONS, default="out")
|
|
211
|
+
query.add_argument("--depth", type=_cpg_depth, default=4)
|
|
212
|
+
query.add_argument("--cursor", default="")
|
|
213
|
+
query.add_argument("--max-items", type=_max_items, default=12)
|
|
214
|
+
query.add_argument("--max-chars", type=_max_chars, default=12000)
|
|
215
|
+
query.set_defaults(func=_query)
|
|
216
|
+
|
|
217
|
+
return parser
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _add_cache_start_arguments(parser: argparse.ArgumentParser) -> None:
|
|
221
|
+
parser.add_argument("--root", type=Path, default=Path("."))
|
|
222
|
+
parser.add_argument(
|
|
223
|
+
"--project-file",
|
|
224
|
+
type=Path,
|
|
225
|
+
help=(
|
|
226
|
+
"Optionally select a .dproj, .dpr, or .dpk file, relative to "
|
|
227
|
+
"--root if needed."
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
parser.add_argument("--max-memory", type=parse_memory_size, default=DEFAULT_MAX_MEMORY_BYTES)
|
|
231
|
+
parser.add_argument(
|
|
232
|
+
"--max-disk-cache",
|
|
233
|
+
type=parse_memory_size,
|
|
234
|
+
default=DEFAULT_MAX_DISK_CACHE_BYTES,
|
|
235
|
+
)
|
|
236
|
+
parser.add_argument("--workers", type=parse_worker_setting, default=0)
|
|
237
|
+
parser.add_argument("--idle-timeout", type=_positive_integer, default=DEFAULT_IDLE_TIMEOUT)
|
|
238
|
+
parser.add_argument("--startup-timeout", type=_positive_float, default=DEFAULT_STARTUP_TIMEOUT)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _positive_float(value: str) -> float:
|
|
242
|
+
parsed = float(value)
|
|
243
|
+
if not math.isfinite(parsed) or parsed <= 0:
|
|
244
|
+
raise argparse.ArgumentTypeError("value must be greater than zero")
|
|
245
|
+
return parsed
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _positive_integer(value: str) -> int:
|
|
249
|
+
parsed = int(value)
|
|
250
|
+
if parsed <= 0:
|
|
251
|
+
raise argparse.ArgumentTypeError("value must be greater than zero")
|
|
252
|
+
return parsed
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _cpg_depth(value: str) -> int:
|
|
256
|
+
parsed = int(value)
|
|
257
|
+
if not 1 <= parsed <= 16:
|
|
258
|
+
raise argparse.ArgumentTypeError("depth must be between 1 and 16")
|
|
259
|
+
return parsed
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _max_items(value: str) -> int:
|
|
263
|
+
parsed = int(value)
|
|
264
|
+
if not 1 <= parsed <= 50:
|
|
265
|
+
raise argparse.ArgumentTypeError("value must be between 1 and 50")
|
|
266
|
+
return parsed
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _max_chars(value: str) -> int:
|
|
270
|
+
parsed = int(value)
|
|
271
|
+
if not 256 <= parsed <= 40000:
|
|
272
|
+
raise argparse.ArgumentTypeError("value must be between 256 and 40000")
|
|
273
|
+
return parsed
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def main(argv: list[str] | None = None) -> int:
|
|
277
|
+
parser = build_parser()
|
|
278
|
+
args = parser.parse_args(argv)
|
|
279
|
+
try:
|
|
280
|
+
for stream in (sys.stdout, sys.stderr):
|
|
281
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
282
|
+
if reconfigure is not None:
|
|
283
|
+
reconfigure(encoding="utf-8")
|
|
284
|
+
result = args.func(args)
|
|
285
|
+
if not getattr(sys.stdout, "closed", False):
|
|
286
|
+
sys.stdout.flush()
|
|
287
|
+
except _CliError as error:
|
|
288
|
+
sys.stderr.write(f"cli_error:{error.code}: {error.message}\n")
|
|
289
|
+
sys.stderr.flush()
|
|
290
|
+
return 1
|
|
291
|
+
except CacheClientError as error:
|
|
292
|
+
return _cache_error(error)
|
|
293
|
+
except ParallelOutlineError as error:
|
|
294
|
+
sys.stderr.write(f"cli_error:parallel_failed: {error}\n")
|
|
295
|
+
sys.stderr.flush()
|
|
296
|
+
return 1
|
|
297
|
+
except ProjectConfigError as error:
|
|
298
|
+
sys.stderr.write(f"cli_error:project_config_invalid: {error}\n")
|
|
299
|
+
sys.stderr.flush()
|
|
300
|
+
return 1
|
|
301
|
+
except BrokenPipeError:
|
|
302
|
+
_discard_broken_stdout()
|
|
303
|
+
os._exit(1)
|
|
304
|
+
except UnicodeEncodeError as error:
|
|
305
|
+
sys.stderr.write(f"cli_error:encoding_error: {error}\n")
|
|
306
|
+
sys.stderr.flush()
|
|
307
|
+
return 1
|
|
308
|
+
except OSError as error:
|
|
309
|
+
sys.stderr.write(f"cli_error:io_error: {error}\n")
|
|
310
|
+
sys.stderr.flush()
|
|
311
|
+
return 1
|
|
312
|
+
return result if isinstance(result, int) else 0
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _discard_broken_stdout() -> None:
|
|
316
|
+
stdout = sys.stdout
|
|
317
|
+
try:
|
|
318
|
+
stdout_fd = stdout.fileno()
|
|
319
|
+
except (AttributeError, OSError, ValueError):
|
|
320
|
+
stdout_fd = None
|
|
321
|
+
if stdout_fd is not None:
|
|
322
|
+
devnull_fd = os.open(os.devnull, os.O_WRONLY)
|
|
323
|
+
try:
|
|
324
|
+
os.dup2(devnull_fd, stdout_fd)
|
|
325
|
+
finally:
|
|
326
|
+
os.close(devnull_fd)
|
|
327
|
+
replacement = open(os.devnull, "w", encoding=getattr(stdout, "encoding", None) or "utf-8")
|
|
328
|
+
sys.stdout = replacement
|
|
329
|
+
sys.__stdout__ = replacement
|
|
330
|
+
try:
|
|
331
|
+
stdout.close()
|
|
332
|
+
except (BrokenPipeError, OSError, ValueError):
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _view(args: argparse.Namespace) -> None:
|
|
337
|
+
args.root = _workspace_root(args.root)
|
|
338
|
+
args.project_file = _project_file(args.root, args.project_file)
|
|
339
|
+
store, cache_key, snapshot = _batch_response_cache(
|
|
340
|
+
args,
|
|
341
|
+
command="view",
|
|
342
|
+
layer=args.layer,
|
|
343
|
+
query=args.query,
|
|
344
|
+
output_format=args.format,
|
|
345
|
+
)
|
|
346
|
+
cached = store.load(cache_key)
|
|
347
|
+
if cached is not None:
|
|
348
|
+
sys.stdout.write(cached.decode("utf-8"))
|
|
349
|
+
return
|
|
350
|
+
if args.layer == "references":
|
|
351
|
+
payload = build_references_payload(
|
|
352
|
+
args.root,
|
|
353
|
+
query=args.query,
|
|
354
|
+
project_file=args.project_file,
|
|
355
|
+
workers=args.workers,
|
|
356
|
+
)
|
|
357
|
+
rendered = render_payload(payload, output_format=args.format)
|
|
358
|
+
elif args.layer == "metrics":
|
|
359
|
+
payload = build_metrics_payload(
|
|
360
|
+
args.root,
|
|
361
|
+
query=args.query,
|
|
362
|
+
project_file=args.project_file,
|
|
363
|
+
workers=args.workers,
|
|
364
|
+
discovery=snapshot.discovery,
|
|
365
|
+
)
|
|
366
|
+
if args.format == "json":
|
|
367
|
+
rendered = _json_text(payload)
|
|
368
|
+
else:
|
|
369
|
+
rendered = render_payload(payload, output_format=args.format)
|
|
370
|
+
elif args.layer == "units" and args.query.strip():
|
|
371
|
+
payload = build_units_payload(
|
|
372
|
+
args.root,
|
|
373
|
+
query=args.query,
|
|
374
|
+
project_file=args.project_file,
|
|
375
|
+
index_projects=args.deep_projects,
|
|
376
|
+
workers=args.workers,
|
|
377
|
+
use_artifact_cache=False,
|
|
378
|
+
discovery=snapshot.discovery,
|
|
379
|
+
)
|
|
380
|
+
rendered = render_payload(payload, output_format=args.format)
|
|
381
|
+
else:
|
|
382
|
+
index = build_codebase_index(
|
|
383
|
+
args.root,
|
|
384
|
+
project_file=args.project_file,
|
|
385
|
+
index_projects=args.deep_projects,
|
|
386
|
+
workers=args.workers,
|
|
387
|
+
use_artifact_cache=False,
|
|
388
|
+
discovery=snapshot.discovery,
|
|
389
|
+
)
|
|
390
|
+
rendered = render_layer(
|
|
391
|
+
index,
|
|
392
|
+
args.layer,
|
|
393
|
+
query=args.query,
|
|
394
|
+
output_format=args.format,
|
|
395
|
+
)
|
|
396
|
+
encoded = rendered.encode("utf-8")
|
|
397
|
+
_store_batch_response(store, cache_key, encoded)
|
|
398
|
+
sys.stdout.write(rendered)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _index(args: argparse.Namespace) -> None:
|
|
402
|
+
args.root = _workspace_root(args.root)
|
|
403
|
+
args.project_file = _project_file(args.root, args.project_file)
|
|
404
|
+
output = _resolve_output_path(args.root, args.out)
|
|
405
|
+
store, cache_key, snapshot = _batch_response_cache(
|
|
406
|
+
args,
|
|
407
|
+
command="index",
|
|
408
|
+
layer="",
|
|
409
|
+
query="",
|
|
410
|
+
output_format="json",
|
|
411
|
+
)
|
|
412
|
+
encoded = store.load(cache_key)
|
|
413
|
+
if encoded is None:
|
|
414
|
+
index = build_codebase_index(
|
|
415
|
+
args.root,
|
|
416
|
+
project_file=args.project_file,
|
|
417
|
+
index_projects=args.deep_projects,
|
|
418
|
+
workers=args.workers,
|
|
419
|
+
use_artifact_cache=False,
|
|
420
|
+
discovery=snapshot.discovery,
|
|
421
|
+
)
|
|
422
|
+
payload = {
|
|
423
|
+
"deep_projects": bool(args.deep_projects),
|
|
424
|
+
"overview": layer_payload(index, "overview"),
|
|
425
|
+
"projects": layer_payload(index, "projects"),
|
|
426
|
+
"problems": layer_payload(index, "problems"),
|
|
427
|
+
}
|
|
428
|
+
encoded = (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode(
|
|
429
|
+
"utf-8"
|
|
430
|
+
)
|
|
431
|
+
_store_batch_response(store, cache_key, encoded)
|
|
432
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
433
|
+
output.write_bytes(encoded)
|
|
434
|
+
print(output)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _batch_response_cache(
|
|
438
|
+
args: argparse.Namespace,
|
|
439
|
+
*,
|
|
440
|
+
command: str,
|
|
441
|
+
layer: str,
|
|
442
|
+
query: str,
|
|
443
|
+
output_format: str,
|
|
444
|
+
) -> tuple[ResponseCacheStore, ResponseCacheKey, WorkspaceContentSnapshot]:
|
|
445
|
+
snapshot = workspace_content_snapshot(
|
|
446
|
+
args.root,
|
|
447
|
+
project_file=args.project_file,
|
|
448
|
+
)
|
|
449
|
+
return (
|
|
450
|
+
ResponseCacheStore(args.root / ".delphi-lsp" / "response-cache"),
|
|
451
|
+
ResponseCacheKey(
|
|
452
|
+
workspace_revision=snapshot.revision,
|
|
453
|
+
command=command,
|
|
454
|
+
layer=layer,
|
|
455
|
+
query=query,
|
|
456
|
+
output_format=output_format,
|
|
457
|
+
deep_projects=bool(args.deep_projects),
|
|
458
|
+
),
|
|
459
|
+
snapshot,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _store_batch_response(
|
|
464
|
+
store: ResponseCacheStore,
|
|
465
|
+
key: ResponseCacheKey,
|
|
466
|
+
payload: bytes,
|
|
467
|
+
) -> None:
|
|
468
|
+
try:
|
|
469
|
+
store.store(key, payload)
|
|
470
|
+
except (OSError, ValueError):
|
|
471
|
+
pass
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _wiki_export(args: argparse.Namespace) -> None:
|
|
475
|
+
args.root = _workspace_root(args.root)
|
|
476
|
+
args.project_file = _project_file(args.root, args.project_file)
|
|
477
|
+
output = _resolve_output_path(args.root, args.out)
|
|
478
|
+
renderer = None if args.quiet else _WikiProgressRenderer(sys.stderr)
|
|
479
|
+
try:
|
|
480
|
+
result = export_okf_wiki(
|
|
481
|
+
args.root,
|
|
482
|
+
output,
|
|
483
|
+
project_file=args.project_file,
|
|
484
|
+
workers=args.workers,
|
|
485
|
+
max_memory_bytes=args.max_memory,
|
|
486
|
+
force=args.force,
|
|
487
|
+
on_progress=renderer,
|
|
488
|
+
)
|
|
489
|
+
except WikiExportError as error:
|
|
490
|
+
raise _CliError("wiki_export_failed", str(error)) from None
|
|
491
|
+
finally:
|
|
492
|
+
if renderer is not None:
|
|
493
|
+
renderer.finish()
|
|
494
|
+
_write_json(result.to_mapping())
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class _WikiProgressRenderer:
|
|
498
|
+
def __init__(self, stream: TextIO) -> None:
|
|
499
|
+
self.stream = stream
|
|
500
|
+
self.is_tty = bool(getattr(stream, "isatty", lambda: False)())
|
|
501
|
+
self._last_phase = ""
|
|
502
|
+
self._last_milestone = -1
|
|
503
|
+
self._line_open = False
|
|
504
|
+
|
|
505
|
+
def __call__(self, event: WikiProgressEvent) -> None:
|
|
506
|
+
percent = self._percent(event)
|
|
507
|
+
milestone = percent // 25 if percent is not None else 0
|
|
508
|
+
phase_changed = event.phase != self._last_phase
|
|
509
|
+
should_render = (
|
|
510
|
+
self.is_tty
|
|
511
|
+
or phase_changed
|
|
512
|
+
or milestone != self._last_milestone
|
|
513
|
+
or event.phase == "complete"
|
|
514
|
+
)
|
|
515
|
+
if not should_render:
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
if self.is_tty and phase_changed and self._line_open:
|
|
519
|
+
self.stream.write("\n")
|
|
520
|
+
line = self._line(event, percent)
|
|
521
|
+
if self.is_tty:
|
|
522
|
+
self.stream.write(f"\r{line}\x1b[K")
|
|
523
|
+
self._line_open = True
|
|
524
|
+
else:
|
|
525
|
+
self.stream.write(line + "\n")
|
|
526
|
+
self.stream.flush()
|
|
527
|
+
self._last_phase = event.phase
|
|
528
|
+
self._last_milestone = milestone
|
|
529
|
+
|
|
530
|
+
def finish(self) -> None:
|
|
531
|
+
if self.is_tty and self._line_open:
|
|
532
|
+
self.stream.write("\n")
|
|
533
|
+
self.stream.flush()
|
|
534
|
+
self._line_open = False
|
|
535
|
+
|
|
536
|
+
@staticmethod
|
|
537
|
+
def _percent(event: WikiProgressEvent) -> int | None:
|
|
538
|
+
if event.total is None or event.total <= 0:
|
|
539
|
+
return None
|
|
540
|
+
return min(100, max(0, int(event.completed * 100 / event.total)))
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def _line(event: WikiProgressEvent, percent: int | None) -> str:
|
|
544
|
+
progress = (
|
|
545
|
+
f"{percent:3d}% ({event.completed}/{event.total})"
|
|
546
|
+
if percent is not None
|
|
547
|
+
else f"{event.completed} completed"
|
|
548
|
+
)
|
|
549
|
+
return f"[wiki] {event.phase:<13} {progress} {event.detail}"
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _skill_install(args: argparse.Namespace) -> None:
|
|
553
|
+
_require_install_directory(args.target)
|
|
554
|
+
try:
|
|
555
|
+
skill_path = install_skill(args.target, force=args.force)
|
|
556
|
+
except FileExistsError as error:
|
|
557
|
+
raise _CliError("install_conflict", str(error)) from None
|
|
558
|
+
print(skill_path)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _opencode_install(args: argparse.Namespace) -> None:
|
|
562
|
+
_require_install_directory(args.target)
|
|
563
|
+
try:
|
|
564
|
+
skill_path, plugin_path, agent_path = install_opencode_support(
|
|
565
|
+
args.target,
|
|
566
|
+
python_executable=args.python,
|
|
567
|
+
force=args.force,
|
|
568
|
+
write_config=args.write_config,
|
|
569
|
+
)
|
|
570
|
+
except FileExistsError as error:
|
|
571
|
+
raise _CliError("install_conflict", str(error)) from None
|
|
572
|
+
print(skill_path)
|
|
573
|
+
print(plugin_path)
|
|
574
|
+
print(agent_path)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _require_install_directory(target: str | Path) -> None:
|
|
578
|
+
path = Path(target).expanduser()
|
|
579
|
+
if path.exists() and not path.is_dir():
|
|
580
|
+
raise _CliError("io_error", f"Install target is not a directory: {path}")
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _worker(args: argparse.Namespace) -> None:
|
|
584
|
+
args.root = _workspace_root(args.root)
|
|
585
|
+
args.project_file = _project_file(args.root, args.project_file)
|
|
586
|
+
context = _open_worker_context(args.root, args.project_file, args.workers)
|
|
587
|
+
watcher_stop = threading.Event()
|
|
588
|
+
watcher = threading.Thread(
|
|
589
|
+
target=watch_workspace_changes,
|
|
590
|
+
kwargs={
|
|
591
|
+
"root": args.root,
|
|
592
|
+
"stop_event": watcher_stop,
|
|
593
|
+
"on_change": context.invalidate_revision_cache,
|
|
594
|
+
},
|
|
595
|
+
name="delphi-worker-watcher",
|
|
596
|
+
daemon=True,
|
|
597
|
+
)
|
|
598
|
+
watcher.start()
|
|
599
|
+
try:
|
|
600
|
+
_serve_worker(context, sys.stdin.buffer, sys.stdout.buffer, sys.stderr)
|
|
601
|
+
finally:
|
|
602
|
+
watcher_stop.set()
|
|
603
|
+
watcher.join(timeout=2.0)
|
|
604
|
+
try:
|
|
605
|
+
sys.stdout.close()
|
|
606
|
+
except (BrokenPipeError, OSError) as error:
|
|
607
|
+
raise BrokenPipeError from error
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _open_worker_context(
|
|
611
|
+
root: Path,
|
|
612
|
+
project_file: Path | None,
|
|
613
|
+
workers: int,
|
|
614
|
+
) -> AgentContext:
|
|
615
|
+
try:
|
|
616
|
+
cache_dir: Path | None = navigation_cache_path(root, create=True)
|
|
617
|
+
except (CacheClientError, OSError):
|
|
618
|
+
cache_dir = None
|
|
619
|
+
return AgentContext.open(
|
|
620
|
+
root,
|
|
621
|
+
project_file,
|
|
622
|
+
workers=workers,
|
|
623
|
+
revision_check_interval_seconds=_WORKER_REVISION_CHECK_INTERVAL_SECONDS,
|
|
624
|
+
navigation_cache_dir=cache_dir,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _write_json(payload: object) -> None:
|
|
629
|
+
sys.stdout.write(_json_text(payload))
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _json_text(payload: object) -> str:
|
|
633
|
+
return json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n"
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _write_warning(warning: str) -> None:
|
|
637
|
+
if warning:
|
|
638
|
+
sys.stderr.write(warning + "\n")
|
|
639
|
+
sys.stderr.flush()
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _cache_error(error: CacheClientError) -> int:
|
|
643
|
+
sys.stderr.write(f"cache_error:{error.code}: {error.message}\n")
|
|
644
|
+
sys.stderr.flush()
|
|
645
|
+
return 1
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _cache_start(args: argparse.Namespace) -> int:
|
|
649
|
+
args.root = _workspace_root(args.root)
|
|
650
|
+
args.project_file = _project_file(args.root, args.project_file)
|
|
651
|
+
try:
|
|
652
|
+
start_cache(
|
|
653
|
+
args.root,
|
|
654
|
+
project_file=args.project_file,
|
|
655
|
+
max_memory_bytes=args.max_memory,
|
|
656
|
+
workers=args.workers,
|
|
657
|
+
idle_timeout=args.idle_timeout,
|
|
658
|
+
startup_timeout=args.startup_timeout,
|
|
659
|
+
max_disk_cache_bytes=args.max_disk_cache,
|
|
660
|
+
)
|
|
661
|
+
response = query_cache(args.root, {"action": "status"})
|
|
662
|
+
except CacheClientError as error:
|
|
663
|
+
return _cache_error(error)
|
|
664
|
+
_write_json(response.payload)
|
|
665
|
+
_write_warning(response.warning)
|
|
666
|
+
return 0
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _cache_status(args: argparse.Namespace) -> int:
|
|
670
|
+
args.root = _workspace_root(args.root)
|
|
671
|
+
try:
|
|
672
|
+
response = query_cache(args.root, {"action": "status"})
|
|
673
|
+
except CacheClientError as error:
|
|
674
|
+
return _cache_error(error)
|
|
675
|
+
if args.format == "json":
|
|
676
|
+
_write_json(response.payload)
|
|
677
|
+
else:
|
|
678
|
+
status = response.payload
|
|
679
|
+
sys.stdout.write(
|
|
680
|
+
f"running pid={status['pid']} state={status['cache_state']} "
|
|
681
|
+
f"memory={status['current_utilization_percent']:.1f}%\n"
|
|
682
|
+
)
|
|
683
|
+
_write_warning(response.warning)
|
|
684
|
+
return 0
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _cache_stop(args: argparse.Namespace) -> int:
|
|
688
|
+
args.root = _workspace_root(args.root)
|
|
689
|
+
try:
|
|
690
|
+
response = query_cache(args.root, {"action": "status"})
|
|
691
|
+
except CacheClientError as error:
|
|
692
|
+
if error.code in {"unavailable", "cache_not_running"}:
|
|
693
|
+
try:
|
|
694
|
+
stop_cache(args.root)
|
|
695
|
+
except CacheClientError as cleanup_error:
|
|
696
|
+
return _cache_error(cleanup_error)
|
|
697
|
+
_write_json({"stopped": False})
|
|
698
|
+
return 0
|
|
699
|
+
return _cache_error(error)
|
|
700
|
+
_write_warning(response.warning)
|
|
701
|
+
try:
|
|
702
|
+
stop_cache(args.root)
|
|
703
|
+
except CacheClientError as error:
|
|
704
|
+
return _cache_error(error)
|
|
705
|
+
_write_json({"stopped": True})
|
|
706
|
+
return 0
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _cache_clear(args: argparse.Namespace) -> int:
|
|
710
|
+
args.root = _workspace_root(args.root)
|
|
711
|
+
try:
|
|
712
|
+
stop_cache(args.root)
|
|
713
|
+
cache_dir = navigation_cache_path(args.root)
|
|
714
|
+
existed = cache_dir.exists()
|
|
715
|
+
if existed:
|
|
716
|
+
shutil.rmtree(cache_dir)
|
|
717
|
+
except CacheClientError as error:
|
|
718
|
+
return _cache_error(error)
|
|
719
|
+
_write_json({"cleared": existed})
|
|
720
|
+
return 0
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _cache_serve(args: argparse.Namespace) -> int:
|
|
724
|
+
args.root = _workspace_root(args.root)
|
|
725
|
+
args.project_file = _project_file(args.root, args.project_file)
|
|
726
|
+
run_cache_daemon(
|
|
727
|
+
args.root,
|
|
728
|
+
project_file=str(args.project_file or ""),
|
|
729
|
+
max_memory_bytes=args.max_memory,
|
|
730
|
+
workers=args.workers,
|
|
731
|
+
idle_timeout=args.idle_timeout,
|
|
732
|
+
max_disk_cache_bytes=args.max_disk_cache,
|
|
733
|
+
)
|
|
734
|
+
return 0
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _query(args: argparse.Namespace) -> int:
|
|
738
|
+
args.root = _workspace_root(args.root)
|
|
739
|
+
request: dict[str, object] = {"action": args.action}
|
|
740
|
+
if args.value and args.target_id:
|
|
741
|
+
sys.stderr.write(
|
|
742
|
+
f"cache_error:invalid_request: {args.action} accepts either a "
|
|
743
|
+
"value or --target-id, not both.\n"
|
|
744
|
+
)
|
|
745
|
+
sys.stderr.flush()
|
|
746
|
+
return 1
|
|
747
|
+
if args.value:
|
|
748
|
+
if args.action == "find":
|
|
749
|
+
request["query"] = args.value
|
|
750
|
+
elif args.action == "metrics":
|
|
751
|
+
if args.value.startswith("target_v2_"):
|
|
752
|
+
request["target_id"] = args.value
|
|
753
|
+
else:
|
|
754
|
+
request["query"] = args.value
|
|
755
|
+
elif args.action in {"focus", "inspect", "trace", "cpg"}:
|
|
756
|
+
request["target_id"] = args.value
|
|
757
|
+
else:
|
|
758
|
+
sys.stderr.write(f"cache_error:invalid_request: {args.action} does not accept a value.\n")
|
|
759
|
+
sys.stderr.flush()
|
|
760
|
+
return 1
|
|
761
|
+
elif args.target_id:
|
|
762
|
+
request["target_id"] = args.target_id
|
|
763
|
+
for argument, field in (
|
|
764
|
+
("project_id", "project_id"),
|
|
765
|
+
("detail", "detail"),
|
|
766
|
+
("relation", "relation"),
|
|
767
|
+
("graph", "graph"),
|
|
768
|
+
("direction", "direction"),
|
|
769
|
+
("depth", "depth"),
|
|
770
|
+
("cursor", "cursor"),
|
|
771
|
+
("max_items", "max_items"),
|
|
772
|
+
("max_chars", "max_chars"),
|
|
773
|
+
):
|
|
774
|
+
value = getattr(args, argument)
|
|
775
|
+
if value is not None:
|
|
776
|
+
request[field] = value
|
|
777
|
+
try:
|
|
778
|
+
response = query_cache(args.root, request)
|
|
779
|
+
except CacheClientError as error:
|
|
780
|
+
return _cache_error(error)
|
|
781
|
+
_write_json(response.payload)
|
|
782
|
+
_write_warning(response.warning)
|
|
783
|
+
return 0
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _workspace_root(value: str | Path) -> Path:
|
|
787
|
+
root = Path(value).expanduser()
|
|
788
|
+
if not root.exists():
|
|
789
|
+
raise _CliError(
|
|
790
|
+
"workspace_not_found",
|
|
791
|
+
f"Workspace root does not exist: {root}",
|
|
792
|
+
)
|
|
793
|
+
if not root.is_dir():
|
|
794
|
+
raise _CliError(
|
|
795
|
+
"workspace_not_directory",
|
|
796
|
+
f"Workspace root is not a directory: {root}",
|
|
797
|
+
)
|
|
798
|
+
return root.resolve()
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _project_file(root: Path, value: Path | None) -> Path | None:
|
|
802
|
+
if value is None:
|
|
803
|
+
return None
|
|
804
|
+
project = value.expanduser()
|
|
805
|
+
if not project.is_absolute():
|
|
806
|
+
project = root / project
|
|
807
|
+
project = project.resolve()
|
|
808
|
+
if not project.is_file():
|
|
809
|
+
raise _CliError(
|
|
810
|
+
"project_file_not_found",
|
|
811
|
+
f"Explicit project file does not exist: {project}",
|
|
812
|
+
)
|
|
813
|
+
return project
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _resolve_output_path(root: Path, output: Path) -> Path:
|
|
817
|
+
return output if output.is_absolute() else root / output
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _serve_worker(context: AgentContext, input_stream: BinaryIO, output_stream: BinaryIO, error_stream: TextIO) -> None:
|
|
821
|
+
discarding_oversize_record = False
|
|
822
|
+
while True:
|
|
823
|
+
record = input_stream.readline(_MAX_WORKER_RECORD_BYTES + 1)
|
|
824
|
+
if not record:
|
|
825
|
+
return
|
|
826
|
+
if discarding_oversize_record:
|
|
827
|
+
if record.endswith(b"\n"):
|
|
828
|
+
discarding_oversize_record = False
|
|
829
|
+
continue
|
|
830
|
+
if not record.endswith(b"\n") and len(record) > _MAX_WORKER_RECORD_BYTES:
|
|
831
|
+
if record.endswith(b"\r") and input_stream.read(1) == b"\n":
|
|
832
|
+
record = record[:-1]
|
|
833
|
+
else:
|
|
834
|
+
discarding_oversize_record = True
|
|
835
|
+
_write_worker_message(
|
|
836
|
+
output_stream,
|
|
837
|
+
_worker_error("request_too_large", _REQUEST_TOO_LARGE_MESSAGE),
|
|
838
|
+
)
|
|
839
|
+
continue
|
|
840
|
+
|
|
841
|
+
record = record.rstrip(b"\r\n")
|
|
842
|
+
if len(record) > _MAX_WORKER_RECORD_BYTES:
|
|
843
|
+
_write_worker_message(
|
|
844
|
+
output_stream,
|
|
845
|
+
_worker_error("request_too_large", _REQUEST_TOO_LARGE_MESSAGE),
|
|
846
|
+
)
|
|
847
|
+
continue
|
|
848
|
+
try:
|
|
849
|
+
text = record.decode("utf-8")
|
|
850
|
+
except UnicodeDecodeError:
|
|
851
|
+
_write_worker_message(
|
|
852
|
+
output_stream,
|
|
853
|
+
_worker_error("invalid_encoding", _INVALID_ENCODING_MESSAGE),
|
|
854
|
+
)
|
|
855
|
+
continue
|
|
856
|
+
if not text.strip():
|
|
857
|
+
continue
|
|
858
|
+
try:
|
|
859
|
+
request = json.loads(text)
|
|
860
|
+
except json.JSONDecodeError:
|
|
861
|
+
_write_worker_message(output_stream, _worker_error("invalid_json", _INVALID_JSON_MESSAGE))
|
|
862
|
+
continue
|
|
863
|
+
try:
|
|
864
|
+
response = context.handle(request)
|
|
865
|
+
message = response.to_mapping()
|
|
866
|
+
except BrokenPipeError:
|
|
867
|
+
raise
|
|
868
|
+
except AgentProtocolError as error:
|
|
869
|
+
error_message = (
|
|
870
|
+
_SOURCE_UNAVAILABLE_MESSAGE
|
|
871
|
+
if error.code == "source_unavailable"
|
|
872
|
+
else error.message
|
|
873
|
+
)
|
|
874
|
+
message = _worker_error(error.code, error_message)
|
|
875
|
+
except Exception as error:
|
|
876
|
+
error_stream.write(f"{type(error).__name__}\n")
|
|
877
|
+
error_stream.flush()
|
|
878
|
+
message = _worker_error("internal_error", _INTERNAL_ERROR_MESSAGE)
|
|
879
|
+
_write_worker_message(output_stream, message)
|
|
880
|
+
def _worker_error(code: str, message: str) -> dict[str, object]:
|
|
881
|
+
return {"schema": SCHEMA_VERSION, "error": {"code": code, "message": message}}
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def _write_worker_message(output_stream: BinaryIO, message: object) -> None:
|
|
885
|
+
serialized = json.dumps(
|
|
886
|
+
message,
|
|
887
|
+
ensure_ascii=False,
|
|
888
|
+
sort_keys=True,
|
|
889
|
+
separators=(",", ":"),
|
|
890
|
+
allow_nan=False,
|
|
891
|
+
).encode("utf-8")
|
|
892
|
+
try:
|
|
893
|
+
output_stream.write(serialized + b"\n")
|
|
894
|
+
output_stream.flush()
|
|
895
|
+
except BrokenPipeError:
|
|
896
|
+
raise
|
|
897
|
+
except OSError as error:
|
|
898
|
+
raise BrokenPipeError from error
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
if __name__ == "__main__":
|
|
902
|
+
raise SystemExit(main())
|