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,614 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import secrets
7
+ import stat
8
+ from pathlib import Path
9
+
10
+ SKILL_NAME = "python-delphi-lsp"
11
+ AGENT_NAME = "python-delphi-lsp"
12
+ _LEGACY_SKILL_NAME = "delphi-codebase-navigator"
13
+ _LEGACY_SKILL_RELATIVE_PATH = Path(".agents") / "skills" / _LEGACY_SKILL_NAME / "SKILL.md"
14
+ _LEGACY_SKILL_SHA256 = "0843d37bd48431c2b992f191b985175a3c60fe5b56d28e0a46b5e18db6383b28"
15
+ _LEGACY_TOOL_RELATIVE_PATH = Path(".opencode") / "tools" / "delphi_codebase.ts"
16
+
17
+
18
+ def install_skill(target: str | Path, *, force: bool = False) -> Path:
19
+ target_path = Path(target).expanduser().resolve()
20
+ legacy_path = _preflight_legacy_skill(target_path, force=force)
21
+ skill_path = target_path / ".agents" / "skills" / SKILL_NAME / "SKILL.md"
22
+ _write_text(skill_path, _skill_markdown(), force=force)
23
+ _remove_legacy_skill(legacy_path)
24
+ return skill_path
25
+
26
+
27
+ def install_opencode_support(
28
+ target: str | Path,
29
+ *,
30
+ python_executable: str,
31
+ force: bool = False,
32
+ write_config: bool = False,
33
+ ) -> tuple[Path, Path, Path]:
34
+ target_path = Path(target).expanduser().resolve()
35
+ legacy_path = target_path / _LEGACY_TOOL_RELATIVE_PATH
36
+ legacy_skill_path = _preflight_legacy_skill(target_path, force=force)
37
+ skill_path = target_path / ".agents" / "skills" / SKILL_NAME / "SKILL.md"
38
+ plugin_path = target_path / ".opencode" / "plugins" / "delphi_codebase.ts"
39
+ agent_path = target_path / ".opencode" / "agents" / f"{AGENT_NAME}.md"
40
+ skill_text = _skill_markdown()
41
+ plugin_text = _opencode_plugin(python_executable)
42
+ agent_text = _agent_markdown()
43
+
44
+ legacy_before = _preflight_legacy(legacy_path, force=force)
45
+ snapshots: dict[Path, bytes | None] = {
46
+ legacy_path: legacy_before,
47
+ **(
48
+ {legacy_skill_path: legacy_skill_path.read_bytes()}
49
+ if legacy_skill_path is not None
50
+ else {}
51
+ ),
52
+ skill_path: _preflight_destination(skill_path, skill_text, force=force),
53
+ plugin_path: _preflight_destination(plugin_path, plugin_text, force=force),
54
+ agent_path: _preflight_destination(agent_path, agent_text, force=force),
55
+ }
56
+ writes = [(skill_path, skill_text), (plugin_path, plugin_text), (agent_path, agent_text)]
57
+
58
+ try:
59
+ for path, text in writes:
60
+ if snapshots[path] != text.encode("utf-8"):
61
+ _write_text(path, text, force=True)
62
+ if legacy_before is not None:
63
+ legacy_path.unlink()
64
+ _remove_legacy_skill(legacy_skill_path)
65
+ except BaseException:
66
+ for path, content in reversed(tuple(snapshots.items())):
67
+ _restore_file(path, content)
68
+ raise
69
+ _ = write_config # Deprecated compatibility input; user configuration is never touched.
70
+ return skill_path, plugin_path, agent_path
71
+
72
+
73
+ def _preflight_legacy_skill(target: Path, *, force: bool) -> Path | None:
74
+ legacy_path = target / _LEGACY_SKILL_RELATIVE_PATH
75
+ legacy_path.relative_to(target)
76
+ _reject_symbolic_link(legacy_path)
77
+ if not legacy_path.exists():
78
+ return None
79
+ if not legacy_path.is_file():
80
+ raise FileExistsError(f"Legacy skill path is not a file: {legacy_path}")
81
+ digest = hashlib.sha256(legacy_path.read_bytes()).hexdigest()
82
+ if digest != _LEGACY_SKILL_SHA256 and not force:
83
+ raise FileExistsError(
84
+ f"Refusing to remove modified legacy skill without force: {legacy_path}"
85
+ )
86
+ return legacy_path
87
+
88
+
89
+ def _remove_legacy_skill(path: Path | None) -> None:
90
+ if path is None:
91
+ return
92
+ path.unlink()
93
+ try:
94
+ path.parent.rmdir()
95
+ except OSError:
96
+ pass
97
+
98
+
99
+ def _preflight_legacy(legacy_path: Path, *, force: bool) -> bytes | None:
100
+ _reject_symbolic_link(legacy_path)
101
+ if not legacy_path.exists():
102
+ return None
103
+ if not legacy_path.is_file():
104
+ raise FileExistsError(f"Legacy opencode tool path is not a file: {legacy_path}")
105
+ legacy_bytes = legacy_path.read_bytes()
106
+ try:
107
+ legacy_text = legacy_bytes.decode("utf-8")
108
+ except UnicodeDecodeError:
109
+ legacy_text = ""
110
+ if not _is_generated_legacy_tool(legacy_text) and not force:
111
+ raise FileExistsError(
112
+ f"Unrecognized legacy opencode tool at {legacy_path}; pass --force to remove it."
113
+ )
114
+ return legacy_bytes
115
+
116
+
117
+ def _preflight_destination(path: Path, text: str, *, force: bool) -> bytes | None:
118
+ _reject_symbolic_link(path)
119
+ if not path.exists():
120
+ return None
121
+ if not path.is_file():
122
+ raise FileExistsError(f"Generated destination is not a file: {path}")
123
+ content = path.read_bytes()
124
+ if content != text.encode("utf-8") and not force:
125
+ raise FileExistsError(f"{path} already exists with different content; pass --force to overwrite")
126
+ return content
127
+
128
+
129
+ def _restore_file(path: Path, content: bytes | None) -> None:
130
+ if content is None:
131
+ if path.exists():
132
+ path.unlink()
133
+ return
134
+ _write_bytes_atomic(path, content)
135
+
136
+
137
+ def _is_generated_legacy_tool(text: str) -> bool:
138
+ return (
139
+ "@opencode-ai/plugin" in text
140
+ and "layered semantic views" in text
141
+ and "python-delphi-lsp" in text
142
+ )
143
+
144
+
145
+ def _write_text(path: Path, text: str, *, force: bool) -> None:
146
+ content = text.encode("utf-8")
147
+ _reject_symbolic_link(path)
148
+ if path.exists() and not path.is_file():
149
+ raise FileExistsError(f"Generated destination is not a file: {path}")
150
+ if path.exists() and path.read_bytes() != content and not force:
151
+ raise FileExistsError(f"{path} already exists with different content; pass --force to overwrite")
152
+ _write_bytes_atomic(path, content)
153
+
154
+
155
+ def _write_bytes_atomic(path: Path, content: bytes) -> None:
156
+ path.parent.mkdir(parents=True, exist_ok=True)
157
+ _reject_symbolic_link(path)
158
+ try:
159
+ existing_mode = stat.S_IMODE(path.stat().st_mode)
160
+ except FileNotFoundError:
161
+ existing_mode = None
162
+ temporary_path: Path | None = None
163
+ try:
164
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0)
165
+ for _attempt in range(10):
166
+ candidate = path.parent / f".{path.name}.{secrets.token_hex(8)}.tmp"
167
+ try:
168
+ descriptor = os.open(candidate, flags, 0o666)
169
+ except FileExistsError:
170
+ continue
171
+ temporary_path = candidate
172
+ break
173
+ else:
174
+ raise FileExistsError(f"Could not allocate a staging file for {path}")
175
+ try:
176
+ staged_file = os.fdopen(descriptor, "wb")
177
+ except BaseException:
178
+ os.close(descriptor)
179
+ raise
180
+ with staged_file as staged:
181
+ written = staged.write(content)
182
+ if written != len(content):
183
+ raise OSError(f"Could not stage complete content for {path}")
184
+ staged.flush()
185
+ if existing_mode is not None:
186
+ os.chmod(temporary_path, existing_mode)
187
+ os.fsync(staged.fileno())
188
+ os.replace(temporary_path, path)
189
+ finally:
190
+ if temporary_path is not None:
191
+ try:
192
+ temporary_path.unlink()
193
+ except FileNotFoundError:
194
+ pass
195
+
196
+
197
+ def _reject_symbolic_link(path: Path) -> None:
198
+ current = path
199
+ while current != current.parent:
200
+ if current.is_symlink():
201
+ raise FileExistsError(
202
+ f"Generated destination must not contain a symbolic link: {current}"
203
+ )
204
+ current = current.parent
205
+
206
+
207
+ def _skill_markdown() -> str:
208
+ return """---
209
+ name: python-delphi-lsp
210
+ description: Inspect Delphi and Object Pascal codebases through the Protocol v3 semantic navigator.
211
+ compatibility: opencode
212
+ metadata:
213
+ package: python-delphi-lsp
214
+ ---
215
+
216
+ ## Hard rule
217
+
218
+ Inspect Delphi/Object Pascal only through `delphi_codebase`; never raw bash/read/glob/grep/cat/shell source inspection. Preserve one focused target and cite `path:line` evidence from tool output.
219
+
220
+ ## Protocol v3 workflow
221
+
222
+ 1. Call `open`. A multi-project repository is active as one workspace by default; select a concrete project with `focus(project_id)` only when project-specific compiler context is needed.
223
+ 2. Call `find` with a narrow query, then `focus(target_id)` for the returned target.
224
+ 3. Inspect focused details in this order as needed: `summary`, `declaration`, `members`, `context`, `body`, `implementations`.
225
+ 4. Call `cpg(target_id)` when AST structure, control flow, local data flow, or explicit call edges are needed. Narrow with `graph`, `direction`, and `depth`.
226
+ 5. Trace relations with `references`, `callers`, `callees`, `uses`, `used_by`, `inherits`, or `implements`.
227
+ 6. Call `problems` before declaring evidence missing. Explain any `sound_partial` relation metadata rather than treating partial results as complete.
228
+ 7. Call `metrics` for project LOC and architecture metrics. Use `query` for unit summaries or a unit `target_id` with `detail=members` for full Halstead, complexity, coupling, abstractness, instability, and distance details.
229
+ 8. Follow `page.next_cursor` with `cursor` until the needed evidence is available.
230
+
231
+ Prefer `summary` and `declaration`, narrow `max_items` and `max_chars`, and request `body` only when it is necessary. Keep the focused target stable while collecting evidence.
232
+
233
+ ## Tool calls
234
+
235
+ `delphi_codebase` accepts `action` (`open`, `find`, `inspect`, `trace`, `focus`, `problems`, `metrics`, `cpg`), optional `query`, `target_id`, `project_id`, `detail`, `relation`, `graph` (`ast`, `cfg`, `dfg`, `call`, `full`), `direction` (`out`, `in`, `both`), `depth` (1-16), `cursor`, `max_items` (1-50), and `max_chars` (256-40000). It has no root or path argument; the active OpenCode worktree is used.
236
+ """
237
+
238
+
239
+ def _agent_markdown() -> str:
240
+ return """---
241
+ description: Inspect Delphi and Object Pascal codebases through python-delphi-lsp.
242
+ mode: all
243
+ temperature: 0
244
+ permission:
245
+ delphi_codebase: allow
246
+ skill:
247
+ "*": deny
248
+ python-delphi-lsp: allow
249
+ lsp: deny
250
+ bash: deny
251
+ read: deny
252
+ glob: deny
253
+ grep: deny
254
+ list: deny
255
+ edit: deny
256
+ write: deny
257
+ patch: deny
258
+ task: deny
259
+ webfetch: deny
260
+ websearch: deny
261
+ question: deny
262
+ todowrite: deny
263
+ todoread: deny
264
+ codebase_map: deny
265
+ code_guidelines: deny
266
+ ---
267
+
268
+ Load `python-delphi-lsp` first, then use only `delphi_codebase` for Delphi and
269
+ Object Pascal codebase inspection. Do not use `lsp`, `bash`, `read`, `glob`,
270
+ `grep`, `edit`, `write`, `task`, `webfetch`, or `todowrite`. Preserve returned
271
+ citations exactly and report partial or ambiguous semantic evidence explicitly.
272
+ """
273
+
274
+
275
+ def _opencode_plugin(python_executable: str) -> str:
276
+ python_json = json.dumps(python_executable)
277
+ template = '''import { tool, type Plugin } from "@opencode-ai/plugin"
278
+
279
+ const PYTHON = __PYTHON_EXECUTABLE__
280
+ const DEFAULT_REQUEST_TIMEOUT_MS = 120_000
281
+
282
+ type AgentRequest = {
283
+ action: "open" | "find" | "inspect" | "trace" | "focus" | "problems" | "metrics" | "cpg"
284
+ query?: string
285
+ target_id?: string
286
+ project_id?: string
287
+ detail?: "summary" | "declaration" | "members" | "context" | "body" | "implementations"
288
+ relation?: "references" | "callers" | "callees" | "uses" | "used_by" | "inherits" | "implements"
289
+ graph?: "ast" | "cfg" | "dfg" | "call" | "full"
290
+ direction?: "out" | "in" | "both"
291
+ depth?: number
292
+ cursor?: string
293
+ max_items?: number
294
+ max_chars?: number
295
+ }
296
+
297
+ type AgentFocus = { project_id: string; unit_id: string; target_id: string }
298
+
299
+ type AgentSuccessResponse = {
300
+ schema: 3
301
+ workspace_revision: string
302
+ focus: AgentFocus
303
+ result: unknown
304
+ page: Record<string, unknown>
305
+ context: Record<string, unknown>
306
+ }
307
+
308
+ type AgentErrorResponse = {
309
+ schema: 3
310
+ error: { code: string; message: string }
311
+ }
312
+
313
+ type AgentResponse = AgentSuccessResponse | AgentErrorResponse
314
+
315
+ function isRecord(value: unknown): value is Record<string, unknown> {
316
+ return typeof value === "object" && value !== null && !Array.isArray(value)
317
+ }
318
+
319
+ function workspaceRoot(context: { worktree?: string; directory?: string }): string {
320
+ if (context.directory && context.directory !== "/") return context.directory
321
+ if (context.worktree && context.worktree !== "/") return context.worktree
322
+ return process.cwd()
323
+ }
324
+
325
+ async function drainStderr(stream: ReadableStream<Uint8Array> | null): Promise<void> {
326
+ if (!stream) return
327
+ const reader = stream.getReader()
328
+ let retained = 0
329
+ try {
330
+ while (true) {
331
+ const { done, value } = await reader.read()
332
+ if (done) return
333
+ retained = Math.min(65_536, retained + value.byteLength)
334
+ }
335
+ } catch {
336
+ // Stderr is deliberately not surfaced to the model.
337
+ } finally {
338
+ reader.releaseLock()
339
+ }
340
+ }
341
+
342
+ class WorkerProtocolError extends Error {
343
+ readonly reusable: boolean
344
+
345
+ constructor(message: string, reusable = true) {
346
+ super(message)
347
+ this.name = "WorkerProtocolError"
348
+ this.reusable = reusable
349
+ }
350
+ }
351
+
352
+ class WorkerTransportError extends Error {
353
+ constructor(message: string) {
354
+ super(message)
355
+ this.name = "WorkerTransportError"
356
+ }
357
+ }
358
+
359
+ function parseAgentResponse(line: string): AgentResponse {
360
+ let payload: unknown
361
+ try {
362
+ payload = JSON.parse(line)
363
+ } catch {
364
+ throw new WorkerProtocolError("delphi_codebase returned malformed JSON.")
365
+ }
366
+
367
+ if (!isRecord(payload) || payload.schema !== 3) {
368
+ throw new WorkerProtocolError("delphi_codebase returned a malformed protocol response.")
369
+ }
370
+ if ("error" in payload) {
371
+ if (
372
+ !isRecord(payload.error)
373
+ || typeof payload.error.code !== "string"
374
+ || typeof payload.error.message !== "string"
375
+ ) {
376
+ throw new WorkerProtocolError("delphi_codebase returned a malformed protocol response.")
377
+ }
378
+ return payload as AgentErrorResponse
379
+ }
380
+ if (
381
+ typeof payload.workspace_revision !== "string"
382
+ || !isRecord(payload.focus)
383
+ || typeof payload.focus.project_id !== "string"
384
+ || typeof payload.focus.unit_id !== "string"
385
+ || typeof payload.focus.target_id !== "string"
386
+ || !("result" in payload)
387
+ || !isRecord(payload.page)
388
+ || !isRecord(payload.context)
389
+ ) {
390
+ throw new WorkerProtocolError("delphi_codebase returned a malformed protocol response.")
391
+ }
392
+ return payload as AgentSuccessResponse
393
+ }
394
+
395
+ class WorkerClient {
396
+ private readonly proc: ReturnType<typeof Bun.spawn>
397
+ private readonly reader: ReadableStreamDefaultReader<Uint8Array>
398
+ private readonly decoder = new TextDecoder("utf-8", { fatal: true })
399
+ private readonly requestTimeoutMs: number
400
+ private buffered = ""
401
+ private tail: Promise<unknown> = Promise.resolve()
402
+ private closed = false
403
+
404
+ constructor(root: string, requestTimeoutMs: number) {
405
+ this.requestTimeoutMs = requestTimeoutMs
406
+ const command = [PYTHON, "-m", "delphi_lsp.agent_cli", "worker", "--root", root]
407
+ this.proc = Bun.spawn(command, {
408
+ cwd: root,
409
+ stdin: "pipe",
410
+ stdout: "pipe",
411
+ stderr: "pipe",
412
+ windowsHide: true,
413
+ })
414
+ this.reader = this.proc.stdout.getReader()
415
+ void drainStderr(this.proc.stderr)
416
+ }
417
+
418
+ request(request: AgentRequest, signal?: AbortSignal): Promise<AgentResponse> {
419
+ const run = async (): Promise<AgentResponse> => this.requestOne(request, signal)
420
+ const queued = this.tail.then(run, run)
421
+ this.tail = queued.catch(() => undefined)
422
+ return queued
423
+ }
424
+
425
+ private async requestOne(request: AgentRequest, signal?: AbortSignal): Promise<AgentResponse> {
426
+ if (this.closed) throw new WorkerTransportError("delphi_codebase worker is closed.")
427
+ if (signal?.aborted) {
428
+ this.close()
429
+ throw new WorkerTransportError("delphi_codebase request was cancelled.")
430
+ }
431
+
432
+ let rejectInterruption: (error: WorkerTransportError) => void = () => undefined
433
+ const interruption = new Promise<never>((_resolve, reject) => {
434
+ rejectInterruption = reject
435
+ })
436
+ const interrupt = (message: string) => {
437
+ rejectInterruption(new WorkerTransportError(message))
438
+ this.close()
439
+ }
440
+ const timeout = setTimeout(
441
+ () => interrupt("delphi_codebase request timed out."),
442
+ this.requestTimeoutMs,
443
+ )
444
+ const abort = () => interrupt("delphi_codebase request was cancelled.")
445
+ signal?.addEventListener("abort", abort, { once: true })
446
+ try {
447
+ const response = await Promise.race([this.exchange(request), interruption])
448
+ if ("error" in response) {
449
+ throw new WorkerProtocolError(`delphi_codebase: ${response.error.message}`)
450
+ }
451
+ return response
452
+ } finally {
453
+ clearTimeout(timeout)
454
+ signal?.removeEventListener("abort", abort)
455
+ }
456
+ }
457
+
458
+ private async exchange(request: AgentRequest): Promise<AgentResponse> {
459
+ try {
460
+ this.proc.stdin.write(JSON.stringify(request) + "\\n")
461
+ await this.proc.stdin.flush()
462
+ } catch {
463
+ this.close()
464
+ throw new WorkerTransportError("delphi_codebase worker write failed.")
465
+ }
466
+ return this.readLine()
467
+ }
468
+
469
+ private async readLine(): Promise<AgentResponse> {
470
+ while (true) {
471
+ const newline = this.buffered.indexOf("\\n")
472
+ if (newline >= 0) {
473
+ const rawLine = this.buffered.slice(0, newline)
474
+ this.buffered = this.buffered.slice(newline + 1)
475
+ const line = rawLine.endsWith("\\r") ? rawLine.slice(0, -1) : rawLine
476
+ return parseAgentResponse(line)
477
+ }
478
+
479
+ let chunk: ReadableStreamReadResult<Uint8Array>
480
+ try {
481
+ chunk = await this.reader.read()
482
+ } catch {
483
+ throw new WorkerTransportError("delphi_codebase worker read failed.")
484
+ }
485
+ if (chunk.done) {
486
+ try {
487
+ this.buffered += this.decoder.decode()
488
+ } catch {
489
+ throw new WorkerProtocolError("delphi_codebase returned an incomplete response.", false)
490
+ }
491
+ if (this.buffered) {
492
+ throw new WorkerProtocolError("delphi_codebase returned an incomplete response.", false)
493
+ }
494
+ throw new WorkerTransportError("delphi_codebase worker closed without a response.")
495
+ }
496
+ try {
497
+ this.buffered += this.decoder.decode(chunk.value, { stream: true })
498
+ } catch {
499
+ throw new WorkerProtocolError("delphi_codebase returned malformed UTF-8.", false)
500
+ }
501
+ }
502
+ }
503
+
504
+ close(): void {
505
+ if (this.closed) return
506
+ this.closed = true
507
+ try { this.proc.stdin.end() } catch {}
508
+ try { this.proc.kill() } catch {}
509
+ void this.reader.cancel().catch(() => undefined)
510
+ }
511
+ }
512
+
513
+ export const DelphiCodebasePlugin: Plugin = async (_input) => {
514
+ const workers = new Map<string, Map<string, WorkerClient>>()
515
+
516
+ function clientFor(sessionID: string, root: string): WorkerClient {
517
+ let clients = workers.get(sessionID)
518
+ if (!clients) {
519
+ clients = new Map<string, WorkerClient>()
520
+ workers.set(sessionID, clients)
521
+ }
522
+ let client = clients.get(root)
523
+ if (!client) {
524
+ client = new WorkerClient(root, DEFAULT_REQUEST_TIMEOUT_MS)
525
+ clients.set(root, client)
526
+ }
527
+ return client
528
+ }
529
+
530
+ function removeClient(sessionID: string, root: string): void {
531
+ const clients = workers.get(sessionID)
532
+ if (!clients) return
533
+ const client = clients.get(root)
534
+ if (client) client.close()
535
+ clients.delete(root)
536
+ if (clients.size === 0) workers.delete(sessionID)
537
+ }
538
+
539
+ function closeSession(sessionID: string): void {
540
+ const clients = workers.get(sessionID)
541
+ if (!clients) return
542
+ for (const client of clients.values()) client.close()
543
+ workers.delete(sessionID)
544
+ }
545
+
546
+ return {
547
+ tool: {
548
+ delphi_codebase: tool({
549
+ description: "Protocol v3 semantic Delphi/Object Pascal codebase navigation.",
550
+ args: {
551
+ action: tool.schema.enum(["open", "find", "inspect", "trace", "focus", "problems", "metrics", "cpg"]),
552
+ query: tool.schema.string().optional(),
553
+ target_id: tool.schema.string().optional(),
554
+ project_id: tool.schema.string().optional(),
555
+ detail: tool.schema.enum(["summary", "declaration", "members", "context", "body", "implementations"]).optional(),
556
+ relation: tool.schema.enum(["references", "callers", "callees", "uses", "used_by", "inherits", "implements"]).optional(),
557
+ graph: tool.schema.enum(["ast", "cfg", "dfg", "call", "full"]).optional(),
558
+ direction: tool.schema.enum(["out", "in", "both"]).optional(),
559
+ depth: tool.schema.number().int().min(1).max(16).optional(),
560
+ cursor: tool.schema.string().optional(),
561
+ max_items: tool.schema.number().int().min(1).max(50).optional(),
562
+ max_chars: tool.schema.number().int().min(256).max(40_000).optional(),
563
+ },
564
+ async execute(args, context) {
565
+ const root = workspaceRoot(context)
566
+ const client = clientFor(context.sessionID, root)
567
+ try {
568
+ return JSON.stringify(await client.request(args as AgentRequest, context.abort))
569
+ } catch (error) {
570
+ if (!(error instanceof WorkerProtocolError && error.reusable)) {
571
+ removeClient(context.sessionID, root)
572
+ }
573
+ throw error instanceof Error ? error : new Error("delphi_codebase worker unavailable.")
574
+ }
575
+ },
576
+ }),
577
+ },
578
+ event: async ({ event }) => {
579
+ if (event.type !== "session.deleted") return
580
+ closeSession(event.properties.info.id)
581
+ },
582
+ "experimental.session.compacting": async ({ sessionID }, output) => {
583
+ const clients = workers.get(sessionID)
584
+ if (!clients) return
585
+ for (const [root, client] of [...clients.entries()]) {
586
+ try {
587
+ const current = await client.request({ action: "focus", max_items: 1, max_chars: 1_000 })
588
+ if ("error" in current || !current.focus.target_id) continue
589
+ const summary = await client.request({ action: "inspect", detail: "summary", max_items: 1, max_chars: 4_000 })
590
+ if ("error" in summary) continue
591
+ const target = Array.isArray(summary.result) ? summary.result[0] : summary.result
592
+ output.context.push(`Delphi navigator context: workspace_revision=${summary.workspace_revision}; focus=${JSON.stringify(summary.focus)}; target=${JSON.stringify(target)}. Continue via delphi_codebase.`)
593
+ return
594
+ } catch (error) {
595
+ if (!(error instanceof WorkerProtocolError && error.reusable)) {
596
+ removeClient(sessionID, root)
597
+ }
598
+ // Compaction lookup failures must not disrupt a session.
599
+ }
600
+ }
601
+ },
602
+ dispose: async () => {
603
+ for (const clients of workers.values()) {
604
+ for (const client of clients.values()) client.close()
605
+ }
606
+ workers.clear()
607
+ },
608
+ }
609
+ }
610
+ '''
611
+ return template.replace("__PYTHON_EXECUTABLE__", python_json)
612
+
613
+
614
+ __all__ = ["SKILL_NAME", "install_opencode_support", "install_skill"]