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.
Files changed (56) hide show
  1. delphi_lsp/__init__.py +184 -0
  2. delphi_lsp/_version.py +1 -0
  3. delphi_lsp/agent_cache.py +1574 -0
  4. delphi_lsp/agent_cli.py +902 -0
  5. delphi_lsp/agent_context.py +3907 -0
  6. delphi_lsp/agent_cpg.py +349 -0
  7. delphi_lsp/agent_cpg_builder.py +666 -0
  8. delphi_lsp/agent_layers.py +1410 -0
  9. delphi_lsp/agent_metrics.py +526 -0
  10. delphi_lsp/agent_protocol.py +445 -0
  11. delphi_lsp/agent_relations.py +1881 -0
  12. delphi_lsp/agent_templates.py +614 -0
  13. delphi_lsp/agent_wiki.py +2853 -0
  14. delphi_lsp/agent_workspace.py +860 -0
  15. delphi_lsp/ast_serialize.py +126 -0
  16. delphi_lsp/binary.py +250 -0
  17. delphi_lsp/comment_builder.py +28 -0
  18. delphi_lsp/consts.py +345 -0
  19. delphi_lsp/delphi_lsp_native.pyd +0 -0
  20. delphi_lsp/delphiast_lexer.py +399 -0
  21. delphi_lsp/delphiast_parser.py +2189 -0
  22. delphi_lsp/delphiast_tokens.py +41 -0
  23. delphi_lsp/grammar.py +459 -0
  24. delphi_lsp/incremental.py +1139 -0
  25. delphi_lsp/lark_builder.py +2684 -0
  26. delphi_lsp/lark_tokens.py +236 -0
  27. delphi_lsp/lexical_scanner.py +30 -0
  28. delphi_lsp/lsp_server.py +6698 -0
  29. delphi_lsp/metrics.py +1452 -0
  30. delphi_lsp/native_bridge.py +300 -0
  31. delphi_lsp/navigation_cache.py +189 -0
  32. delphi_lsp/nodes.py +462 -0
  33. delphi_lsp/parallel_outline.py +662 -0
  34. delphi_lsp/parse_artifacts.py +969 -0
  35. delphi_lsp/parser.py +342 -0
  36. delphi_lsp/parser_backend.py +139 -0
  37. delphi_lsp/preprocessor.py +1122 -0
  38. delphi_lsp/progress.py +26 -0
  39. delphi_lsp/project_config.py +285 -0
  40. delphi_lsp/project_discovery.py +932 -0
  41. delphi_lsp/project_indexer.py +470 -0
  42. delphi_lsp/py.typed +1 -0
  43. delphi_lsp/response_cache.py +465 -0
  44. delphi_lsp/semantic.py +394 -0
  45. delphi_lsp/semantic_builder.py +2302 -0
  46. delphi_lsp/source_reader.py +17 -0
  47. delphi_lsp/wiki_external_sort.py +392 -0
  48. delphi_lsp/wiki_memory.py +548 -0
  49. delphi_lsp/workspace.py +83 -0
  50. delphi_lsp/writer.py +73 -0
  51. python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
  52. python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
  53. python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
  54. python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
  55. python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
  56. python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
@@ -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())