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
|
@@ -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"]
|