python-delphi-lsp 2.0.0__py3-none-any.whl → 2.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- delphi_lsp/__init__.py +20 -0
- delphi_lsp/_version.py +1 -1
- delphi_lsp/agent_cli.py +13 -5
- delphi_lsp/agent_context.py +64 -22
- delphi_lsp/agent_layers.py +65 -0
- delphi_lsp/agent_metrics.py +82 -0
- delphi_lsp/agent_protocol.py +1 -0
- delphi_lsp/agent_templates.py +94 -68
- delphi_lsp/agent_workspace.py +35 -1
- delphi_lsp/lark_builder.py +14 -2
- delphi_lsp/metrics.py +759 -0
- {python_delphi_lsp-2.0.0.dist-info → python_delphi_lsp-2.0.2.dist-info}/METADATA +88 -33
- {python_delphi_lsp-2.0.0.dist-info → python_delphi_lsp-2.0.2.dist-info}/RECORD +17 -15
- {python_delphi_lsp-2.0.0.dist-info → python_delphi_lsp-2.0.2.dist-info}/WHEEL +0 -0
- {python_delphi_lsp-2.0.0.dist-info → python_delphi_lsp-2.0.2.dist-info}/entry_points.txt +0 -0
- {python_delphi_lsp-2.0.0.dist-info → python_delphi_lsp-2.0.2.dist-info}/licenses/LICENSE +0 -0
- {python_delphi_lsp-2.0.0.dist-info → python_delphi_lsp-2.0.2.dist-info}/top_level.txt +0 -0
delphi_lsp/__init__.py
CHANGED
|
@@ -59,6 +59,17 @@ from .project_indexer import (
|
|
|
59
59
|
UnitInfo,
|
|
60
60
|
UnitParsedHook,
|
|
61
61
|
)
|
|
62
|
+
from .metrics import (
|
|
63
|
+
CyclomaticMetrics,
|
|
64
|
+
HalsteadMetrics,
|
|
65
|
+
LineMetrics,
|
|
66
|
+
MetricProblem,
|
|
67
|
+
ProjectMetrics,
|
|
68
|
+
RoutineComplexity,
|
|
69
|
+
UnitMetrics,
|
|
70
|
+
analyze_project,
|
|
71
|
+
analyze_unit,
|
|
72
|
+
)
|
|
62
73
|
|
|
63
74
|
__all__ = [
|
|
64
75
|
'__version__',
|
|
@@ -134,6 +145,15 @@ __all__ = [
|
|
|
134
145
|
'IncludeFileInfo',
|
|
135
146
|
'GetUnitSyntaxHook',
|
|
136
147
|
'UnitParsedHook',
|
|
148
|
+
'MetricProblem',
|
|
149
|
+
'LineMetrics',
|
|
150
|
+
'HalsteadMetrics',
|
|
151
|
+
'RoutineComplexity',
|
|
152
|
+
'CyclomaticMetrics',
|
|
153
|
+
'UnitMetrics',
|
|
154
|
+
'ProjectMetrics',
|
|
155
|
+
'analyze_unit',
|
|
156
|
+
'analyze_project',
|
|
137
157
|
'dumps',
|
|
138
158
|
'loads',
|
|
139
159
|
]
|
delphi_lsp/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.0.
|
|
1
|
+
__version__ = "2.0.2"
|
delphi_lsp/agent_cli.py
CHANGED
|
@@ -44,6 +44,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
44
44
|
"implementation",
|
|
45
45
|
"references",
|
|
46
46
|
"problems",
|
|
47
|
+
"metrics",
|
|
47
48
|
],
|
|
48
49
|
)
|
|
49
50
|
view.add_argument("--query", default="")
|
|
@@ -66,11 +67,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
66
67
|
|
|
67
68
|
opencode = subcommands.add_parser("opencode", help="Install opencode integration.")
|
|
68
69
|
opencode_commands = opencode.add_subparsers(dest="opencode_command", required=True)
|
|
69
|
-
opencode_install = opencode_commands.add_parser(
|
|
70
|
+
opencode_install = opencode_commands.add_parser(
|
|
71
|
+
"install", help="Install the package-named skill, Markdown agent, and OpenCode plugin."
|
|
72
|
+
)
|
|
70
73
|
opencode_install.add_argument("--target", type=Path, default=Path("."))
|
|
71
74
|
opencode_install.add_argument("--python", default=sys.executable)
|
|
72
75
|
opencode_install.add_argument("--force", action="store_true")
|
|
73
|
-
opencode_install.add_argument(
|
|
76
|
+
opencode_install.add_argument(
|
|
77
|
+
"--write-agent",
|
|
78
|
+
"--write-config",
|
|
79
|
+
dest="write_config",
|
|
80
|
+
action="store_true",
|
|
81
|
+
help="Deprecated compatibility option; the Markdown agent is always installed and opencode.json is never touched.",
|
|
82
|
+
)
|
|
74
83
|
opencode_install.set_defaults(func=_opencode_install)
|
|
75
84
|
|
|
76
85
|
worker = subcommands.add_parser("worker", help="Serve Protocol v2 NDJSON requests.")
|
|
@@ -138,7 +147,7 @@ def _skill_install(args: argparse.Namespace) -> None:
|
|
|
138
147
|
|
|
139
148
|
|
|
140
149
|
def _opencode_install(args: argparse.Namespace) -> None:
|
|
141
|
-
skill_path, plugin_path,
|
|
150
|
+
skill_path, plugin_path, agent_path = install_opencode_support(
|
|
142
151
|
args.target,
|
|
143
152
|
python_executable=args.python,
|
|
144
153
|
force=args.force,
|
|
@@ -146,8 +155,7 @@ def _opencode_install(args: argparse.Namespace) -> None:
|
|
|
146
155
|
)
|
|
147
156
|
print(skill_path)
|
|
148
157
|
print(plugin_path)
|
|
149
|
-
|
|
150
|
-
print(config_path)
|
|
158
|
+
print(agent_path)
|
|
151
159
|
|
|
152
160
|
|
|
153
161
|
def _worker(args: argparse.Namespace) -> None:
|
delphi_lsp/agent_context.py
CHANGED
|
@@ -18,14 +18,16 @@ from .agent_protocol import (
|
|
|
18
18
|
make_target_id,
|
|
19
19
|
paginate_items,
|
|
20
20
|
)
|
|
21
|
+
from .agent_metrics import build_workspace_metrics, project_metric_item, unit_metric_item
|
|
21
22
|
from .agent_relations import ProjectRelationIndex, RelationTarget
|
|
22
|
-
from .agent_workspace import
|
|
23
|
+
from .agent_workspace import AgentWorkspace, unit_display_path, unit_source_path, unit_target_id
|
|
23
24
|
from .consts import AttributeName, SyntaxNodeType
|
|
24
25
|
from .lsp_server import build_outline_semantic_model, multiline_string_block_end
|
|
25
26
|
from .nodes import CompoundSyntaxNode, SyntaxNode
|
|
26
27
|
from .parser import DelphiParser
|
|
27
28
|
from .semantic import Scope, ScopeKind, Symbol, SymbolKind
|
|
28
29
|
from .source_reader import read_source_text
|
|
30
|
+
from .metrics import ProjectMetrics
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
_ROUTINE_KINDS = frozenset(
|
|
@@ -238,6 +240,8 @@ class AgentContext:
|
|
|
238
240
|
self._last_revision = workspace.workspace_revision
|
|
239
241
|
self._registry: _Registry | None = None
|
|
240
242
|
self._relation_index: ProjectRelationIndex | None = None
|
|
243
|
+
self._metrics: ProjectMetrics | None = None
|
|
244
|
+
self._metrics_revision = ""
|
|
241
245
|
|
|
242
246
|
@classmethod
|
|
243
247
|
def open(
|
|
@@ -270,6 +274,8 @@ class AgentContext:
|
|
|
270
274
|
self._require_selected_project()
|
|
271
275
|
items = self._problem_items()
|
|
272
276
|
return self._response(parsed, revision, items)
|
|
277
|
+
if parsed.action == "metrics":
|
|
278
|
+
return self._handle_metrics(parsed, revision)
|
|
273
279
|
if parsed.action == "focus":
|
|
274
280
|
return self._handle_focus(parsed, revision)
|
|
275
281
|
if parsed.action == "find":
|
|
@@ -294,10 +300,14 @@ class AgentContext:
|
|
|
294
300
|
if current_project_id != previous_project_id:
|
|
295
301
|
self._registry = None
|
|
296
302
|
self._relation_index = None
|
|
303
|
+
self._metrics = None
|
|
304
|
+
self._metrics_revision = ""
|
|
297
305
|
self._focus = Focus(project_id=current_project_id) if current_project_id else Focus()
|
|
298
306
|
elif revision != self._last_revision:
|
|
299
307
|
self._registry = None
|
|
300
308
|
self._relation_index = None
|
|
309
|
+
self._metrics = None
|
|
310
|
+
self._metrics_revision = ""
|
|
301
311
|
elif self._focus.project_id != current_project_id:
|
|
302
312
|
self._focus = Focus(project_id=current_project_id) if current_project_id else Focus()
|
|
303
313
|
self._last_revision = revision
|
|
@@ -323,12 +333,11 @@ class AgentContext:
|
|
|
323
333
|
return items
|
|
324
334
|
|
|
325
335
|
for unit in self._workspace.units:
|
|
326
|
-
|
|
327
|
-
display_path = _stable_source_display_path(self._workspace.root, unit, source_path)
|
|
336
|
+
display_path = unit_display_path(self._workspace.root, unit)
|
|
328
337
|
items.append(
|
|
329
338
|
{
|
|
330
339
|
"item_type": "unit",
|
|
331
|
-
"unit_id":
|
|
340
|
+
"unit_id": unit_target_id(self._workspace.root, unit),
|
|
332
341
|
"name": unit.name,
|
|
333
342
|
"path": display_path,
|
|
334
343
|
"has_error": unit.has_error,
|
|
@@ -407,6 +416,55 @@ class AgentContext:
|
|
|
407
416
|
self._focus = Focus(project_id=self._workspace.active_project_id)
|
|
408
417
|
return self._response(request, revision, [self._focus.to_mapping()])
|
|
409
418
|
|
|
419
|
+
def _handle_metrics(self, request: AgentRequest, revision: str) -> AgentResponse:
|
|
420
|
+
if request.detail not in {"summary", "members"}:
|
|
421
|
+
raise AgentProtocolError(
|
|
422
|
+
"invalid_detail",
|
|
423
|
+
"Metrics supports only summary or members detail.",
|
|
424
|
+
)
|
|
425
|
+
metrics = self._require_metrics(revision)
|
|
426
|
+
detail = request.detail == "members"
|
|
427
|
+
if request.target_id:
|
|
428
|
+
unit = next(
|
|
429
|
+
(candidate for candidate in metrics.units if candidate.unit_id == request.target_id),
|
|
430
|
+
None,
|
|
431
|
+
)
|
|
432
|
+
if unit is None:
|
|
433
|
+
raise AgentProtocolError(
|
|
434
|
+
"target_not_found",
|
|
435
|
+
f"Target not found: {request.target_id}.",
|
|
436
|
+
)
|
|
437
|
+
return self._response(
|
|
438
|
+
request,
|
|
439
|
+
revision,
|
|
440
|
+
[unit_metric_item(unit, detail=detail)],
|
|
441
|
+
target_id=unit.unit_id,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
units = metrics.units
|
|
445
|
+
if request.query:
|
|
446
|
+
query = request.query.casefold()
|
|
447
|
+
units = tuple(
|
|
448
|
+
unit
|
|
449
|
+
for unit in units
|
|
450
|
+
if query in unit.name.casefold() or query in unit.path.casefold()
|
|
451
|
+
)
|
|
452
|
+
items = [unit_metric_item(unit, detail=detail) for unit in units]
|
|
453
|
+
else:
|
|
454
|
+
items = [
|
|
455
|
+
project_metric_item(metrics),
|
|
456
|
+
*(unit_metric_item(unit, detail=detail) for unit in units),
|
|
457
|
+
]
|
|
458
|
+
return self._response(request, revision, items)
|
|
459
|
+
|
|
460
|
+
def _require_metrics(self, revision: str) -> ProjectMetrics:
|
|
461
|
+
self._require_selected_project()
|
|
462
|
+
if self._metrics is not None and self._metrics_revision == revision:
|
|
463
|
+
return self._metrics
|
|
464
|
+
self._metrics = build_workspace_metrics(self._workspace)
|
|
465
|
+
self._metrics_revision = revision
|
|
466
|
+
return self._metrics
|
|
467
|
+
|
|
410
468
|
def _require_registry(self, revision: str) -> _Registry:
|
|
411
469
|
project_id = self._require_selected_project()
|
|
412
470
|
if (
|
|
@@ -608,8 +666,8 @@ def _build_registry(workspace: AgentWorkspace, project_id: str, revision: str) -
|
|
|
608
666
|
raw_symbols: list[_RawSymbol] = []
|
|
609
667
|
sources: dict[Path, _SourceDocument] = {}
|
|
610
668
|
for unit in workspace.units:
|
|
611
|
-
source_path =
|
|
612
|
-
display_path =
|
|
669
|
+
source_path = unit_source_path(workspace.root, unit)
|
|
670
|
+
display_path = unit_display_path(workspace.root, unit)
|
|
613
671
|
try:
|
|
614
672
|
text = read_source_text(source_path)
|
|
615
673
|
except OSError as exc:
|
|
@@ -727,22 +785,6 @@ def _build_registry(workspace: AgentWorkspace, project_id: str, revision: str) -
|
|
|
727
785
|
)
|
|
728
786
|
|
|
729
787
|
|
|
730
|
-
def _unit_source_path(root: Path, unit: AgentUnit) -> Path:
|
|
731
|
-
path = Path(unit.path)
|
|
732
|
-
if not path.is_absolute():
|
|
733
|
-
path = root / path
|
|
734
|
-
return path.expanduser().resolve()
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
def _stable_source_display_path(root: Path, unit: AgentUnit, source_path: Path) -> str:
|
|
738
|
-
try:
|
|
739
|
-
return source_path.relative_to(root).as_posix()
|
|
740
|
-
except ValueError:
|
|
741
|
-
unit_component = _stable_path_component(unit.name or source_path.stem)
|
|
742
|
-
file_component = _stable_path_component(source_path.name)
|
|
743
|
-
return f"@external/{unit_component}/{file_component}"
|
|
744
|
-
|
|
745
|
-
|
|
746
788
|
def _stable_path_component(value: str) -> str:
|
|
747
789
|
normalized = unicodedata.normalize("NFC", value).replace("\\", "_").replace("/", "_")
|
|
748
790
|
return normalized or "unknown"
|
delphi_lsp/agent_layers.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Any, Iterable
|
|
|
6
6
|
import json
|
|
7
7
|
|
|
8
8
|
from .lsp_server import build_outline_semantic_model, outline_source
|
|
9
|
+
from .metrics import analyze_project
|
|
9
10
|
from .project_discovery import DelphiProjectDiscovery, discover_delphi_project
|
|
10
11
|
from .project_indexer import ProjectIndexResult, ProjectIndexer
|
|
11
12
|
from .semantic import Scope, SourceRange, Symbol, SymbolIndex, SymbolKind
|
|
@@ -98,6 +99,8 @@ def layer_payload(index: CodebaseIndex, layer: str, *, query: str = "") -> dict[
|
|
|
98
99
|
return _references_payload(index, query=query)
|
|
99
100
|
if normalized_layer == "problems":
|
|
100
101
|
return _problems_payload(index)
|
|
102
|
+
if normalized_layer == "metrics":
|
|
103
|
+
return _metrics_payload(index, query=query)
|
|
101
104
|
raise ValueError(f"Unknown layer: {layer}")
|
|
102
105
|
|
|
103
106
|
|
|
@@ -369,6 +372,44 @@ def _problems_payload(index: CodebaseIndex) -> dict[str, Any]:
|
|
|
369
372
|
return {"layer": "problems", "root": index.root, "items": items}
|
|
370
373
|
|
|
371
374
|
|
|
375
|
+
def _metrics_payload(index: CodebaseIndex, *, query: str) -> dict[str, Any]:
|
|
376
|
+
sources: dict[str, str] = {}
|
|
377
|
+
include_sources: dict[str, str] = {}
|
|
378
|
+
for value in index.discovery.source_files:
|
|
379
|
+
path = Path(value)
|
|
380
|
+
try:
|
|
381
|
+
text = read_source_text(path)
|
|
382
|
+
except (OSError, UnicodeError):
|
|
383
|
+
continue
|
|
384
|
+
if path.suffix.casefold() == ".inc":
|
|
385
|
+
include_sources[str(path)] = text
|
|
386
|
+
elif path.suffix.casefold() in {".pas", ".dpr", ".dpk"}:
|
|
387
|
+
sources[str(path)] = text
|
|
388
|
+
|
|
389
|
+
project_name = "Workspace"
|
|
390
|
+
if len(index.discovery.project_files) == 1:
|
|
391
|
+
project_name = Path(index.discovery.project_files[0]).stem
|
|
392
|
+
metrics = analyze_project(
|
|
393
|
+
sources,
|
|
394
|
+
include_sources=include_sources,
|
|
395
|
+
defines=index.discovery.defines,
|
|
396
|
+
include_paths=index.discovery.include_paths,
|
|
397
|
+
project_name=project_name,
|
|
398
|
+
)
|
|
399
|
+
needle = query.casefold().strip()
|
|
400
|
+
units = [
|
|
401
|
+
unit.to_mapping(detail=True)
|
|
402
|
+
for unit in metrics.units
|
|
403
|
+
if not needle or needle in unit.name.casefold() or needle in unit.path.casefold()
|
|
404
|
+
]
|
|
405
|
+
return {
|
|
406
|
+
"layer": "metrics",
|
|
407
|
+
"root": index.root,
|
|
408
|
+
"project": metrics.to_mapping(),
|
|
409
|
+
"items": units,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
372
413
|
def _all_symbols(index: CodebaseIndex) -> Iterable[Symbol]:
|
|
373
414
|
for model in index.models.values():
|
|
374
415
|
yield from _iter_symbols(model.unit_scope)
|
|
@@ -486,6 +527,30 @@ def _render_markdown(payload: dict[str, Any]) -> str:
|
|
|
486
527
|
elif payload["layer"] == "problems":
|
|
487
528
|
for item in payload["items"]:
|
|
488
529
|
lines.append(f"- {item['kind']}: {item['message']} (`{item.get('origin', '')}`)")
|
|
530
|
+
elif payload["layer"] == "metrics":
|
|
531
|
+
project = payload["project"]
|
|
532
|
+
lines.extend(
|
|
533
|
+
[
|
|
534
|
+
f"- Project LOC: {project['total_loc']}",
|
|
535
|
+
f"- Project LOC with includes: {project['total_loc_with_includes']}",
|
|
536
|
+
f"- Units: {project['unit_count']}",
|
|
537
|
+
f"- Maintainability index: {project['maintainability_index']:.2f}",
|
|
538
|
+
]
|
|
539
|
+
)
|
|
540
|
+
for item in payload["items"]:
|
|
541
|
+
lines.extend(
|
|
542
|
+
[
|
|
543
|
+
"",
|
|
544
|
+
f"## {item['name']}",
|
|
545
|
+
f"- Path: `{item['path']}`",
|
|
546
|
+
f"- LOC: {item['lines']['total_lines']}",
|
|
547
|
+
f"- Cyclomatic maximum: {item['cyclomatic']['maximum']}",
|
|
548
|
+
f"- Maintainability index: {item['maintainability_index']:.2f}",
|
|
549
|
+
f"- Instability: {item['instability']:.3f}",
|
|
550
|
+
f"- Abstractness: {item['abstractness']:.3f}",
|
|
551
|
+
f"- Distance: {item['distance']:.3f}",
|
|
552
|
+
]
|
|
553
|
+
)
|
|
489
554
|
return "\n".join(lines).rstrip() + "\n"
|
|
490
555
|
|
|
491
556
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .agent_workspace import (
|
|
7
|
+
AgentWorkspace,
|
|
8
|
+
unit_display_path,
|
|
9
|
+
unit_source_path,
|
|
10
|
+
unit_target_id,
|
|
11
|
+
)
|
|
12
|
+
from .metrics import MetricProblem, ProjectMetrics, UnitMetrics, analyze_project
|
|
13
|
+
from .source_reader import read_source_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_workspace_metrics(workspace: AgentWorkspace) -> ProjectMetrics:
|
|
17
|
+
sources: dict[str, str] = {}
|
|
18
|
+
include_sources: dict[str, str] = {}
|
|
19
|
+
unit_ids: dict[str, str] = {}
|
|
20
|
+
problems: list[MetricProblem] = []
|
|
21
|
+
|
|
22
|
+
for unit in workspace.units:
|
|
23
|
+
display_path = unit_display_path(workspace.root, unit)
|
|
24
|
+
source_path = unit_source_path(workspace.root, unit)
|
|
25
|
+
try:
|
|
26
|
+
sources[display_path] = read_source_text(source_path)
|
|
27
|
+
except OSError:
|
|
28
|
+
problems.append(
|
|
29
|
+
MetricProblem(
|
|
30
|
+
kind="source_unavailable",
|
|
31
|
+
path=display_path,
|
|
32
|
+
message="Could not read source.",
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
continue
|
|
36
|
+
unit_ids[display_path] = unit_target_id(workspace.root, unit)
|
|
37
|
+
|
|
38
|
+
for include_file in workspace.include_files:
|
|
39
|
+
display_path = include_file["path"]
|
|
40
|
+
source_path = _workspace_path(workspace.root, display_path)
|
|
41
|
+
try:
|
|
42
|
+
include_sources[display_path] = read_source_text(source_path)
|
|
43
|
+
except OSError:
|
|
44
|
+
problems.append(
|
|
45
|
+
MetricProblem(
|
|
46
|
+
kind="include_unavailable",
|
|
47
|
+
path=display_path,
|
|
48
|
+
message="Could not read include file.",
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
project = workspace.active_project
|
|
53
|
+
metrics = analyze_project(
|
|
54
|
+
sources,
|
|
55
|
+
include_sources=include_sources,
|
|
56
|
+
defines=workspace.defines,
|
|
57
|
+
include_paths=workspace.include_paths,
|
|
58
|
+
project_id=workspace.active_project_id,
|
|
59
|
+
project_name=project.name if project is not None else "",
|
|
60
|
+
unit_ids=unit_ids,
|
|
61
|
+
)
|
|
62
|
+
if problems:
|
|
63
|
+
metrics = replace(metrics, problems=(*metrics.problems, *problems))
|
|
64
|
+
return metrics
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def project_metric_item(metrics: ProjectMetrics) -> dict[str, object]:
|
|
68
|
+
return {"item_type": "project_metrics", **metrics.to_mapping()}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def unit_metric_item(metrics: UnitMetrics, *, detail: bool = False) -> dict[str, object]:
|
|
72
|
+
return {"item_type": "unit_metrics", **metrics.to_mapping(detail=detail)}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _workspace_path(root: Path, value: str) -> Path:
|
|
76
|
+
path = Path(value).expanduser()
|
|
77
|
+
if not path.is_absolute():
|
|
78
|
+
path = root / path
|
|
79
|
+
return path.resolve()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = ["build_workspace_metrics", "project_metric_item", "unit_metric_item"]
|
delphi_lsp/agent_protocol.py
CHANGED
delphi_lsp/agent_templates.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
@@ -7,14 +8,20 @@ import secrets
|
|
|
7
8
|
import stat
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
SKILL_NAME = "delphi-
|
|
11
|
+
SKILL_NAME = "python-delphi-lsp"
|
|
12
|
+
AGENT_NAME = "python-delphi-lsp"
|
|
13
|
+
_LEGACY_SKILL_NAME = "delphi-codebase-navigator"
|
|
14
|
+
_LEGACY_SKILL_RELATIVE_PATH = Path(".agents") / "skills" / _LEGACY_SKILL_NAME / "SKILL.md"
|
|
15
|
+
_LEGACY_SKILL_SHA256 = "0843d37bd48431c2b992f191b985175a3c60fe5b56d28e0a46b5e18db6383b28"
|
|
11
16
|
_LEGACY_TOOL_RELATIVE_PATH = Path(".opencode") / "tools" / "delphi_codebase.ts"
|
|
12
17
|
|
|
13
18
|
|
|
14
19
|
def install_skill(target: str | Path, *, force: bool = False) -> Path:
|
|
15
20
|
target_path = Path(target).expanduser().resolve()
|
|
21
|
+
legacy_path = _preflight_legacy_skill(target_path, force=force)
|
|
16
22
|
skill_path = target_path / ".agents" / "skills" / SKILL_NAME / "SKILL.md"
|
|
17
23
|
_write_text(skill_path, _skill_markdown(), force=force)
|
|
24
|
+
_remove_legacy_skill(legacy_path)
|
|
18
25
|
return skill_path
|
|
19
26
|
|
|
20
27
|
|
|
@@ -24,26 +31,30 @@ def install_opencode_support(
|
|
|
24
31
|
python_executable: str,
|
|
25
32
|
force: bool = False,
|
|
26
33
|
write_config: bool = False,
|
|
27
|
-
) -> tuple[Path, Path, Path
|
|
34
|
+
) -> tuple[Path, Path, Path]:
|
|
28
35
|
target_path = Path(target).expanduser().resolve()
|
|
29
36
|
legacy_path = target_path / _LEGACY_TOOL_RELATIVE_PATH
|
|
37
|
+
legacy_skill_path = _preflight_legacy_skill(target_path, force=force)
|
|
30
38
|
skill_path = target_path / ".agents" / "skills" / SKILL_NAME / "SKILL.md"
|
|
31
39
|
plugin_path = target_path / ".opencode" / "plugins" / "delphi_codebase.ts"
|
|
32
|
-
|
|
40
|
+
agent_path = target_path / ".opencode" / "agents" / f"{AGENT_NAME}.md"
|
|
33
41
|
skill_text = _skill_markdown()
|
|
34
42
|
plugin_text = _opencode_plugin(python_executable)
|
|
43
|
+
agent_text = _agent_markdown()
|
|
35
44
|
|
|
36
45
|
legacy_before = _preflight_legacy(legacy_path, force=force)
|
|
37
46
|
snapshots: dict[Path, bytes | None] = {
|
|
38
47
|
legacy_path: legacy_before,
|
|
48
|
+
**(
|
|
49
|
+
{legacy_skill_path: legacy_skill_path.read_bytes()}
|
|
50
|
+
if legacy_skill_path is not None
|
|
51
|
+
else {}
|
|
52
|
+
),
|
|
39
53
|
skill_path: _preflight_destination(skill_path, skill_text, force=force),
|
|
40
54
|
plugin_path: _preflight_destination(plugin_path, plugin_text, force=force),
|
|
55
|
+
agent_path: _preflight_destination(agent_path, agent_text, force=force),
|
|
41
56
|
}
|
|
42
|
-
writes = [(skill_path, skill_text), (plugin_path, plugin_text)]
|
|
43
|
-
if config_path is not None:
|
|
44
|
-
config_before, config_text = _render_opencode_config(config_path)
|
|
45
|
-
snapshots[config_path] = config_before
|
|
46
|
-
writes.append((config_path, config_text))
|
|
57
|
+
writes = [(skill_path, skill_text), (plugin_path, plugin_text), (agent_path, agent_text)]
|
|
47
58
|
|
|
48
59
|
try:
|
|
49
60
|
for path, text in writes:
|
|
@@ -51,11 +62,39 @@ def install_opencode_support(
|
|
|
51
62
|
_write_text(path, text, force=True)
|
|
52
63
|
if legacy_before is not None:
|
|
53
64
|
legacy_path.unlink()
|
|
65
|
+
_remove_legacy_skill(legacy_skill_path)
|
|
54
66
|
except BaseException:
|
|
55
67
|
for path, content in reversed(tuple(snapshots.items())):
|
|
56
68
|
_restore_file(path, content)
|
|
57
69
|
raise
|
|
58
|
-
|
|
70
|
+
_ = write_config # Deprecated compatibility input; user configuration is never touched.
|
|
71
|
+
return skill_path, plugin_path, agent_path
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _preflight_legacy_skill(target: Path, *, force: bool) -> Path | None:
|
|
75
|
+
legacy_path = target / _LEGACY_SKILL_RELATIVE_PATH
|
|
76
|
+
legacy_path.relative_to(target)
|
|
77
|
+
_reject_symbolic_link(legacy_path)
|
|
78
|
+
if not legacy_path.exists():
|
|
79
|
+
return None
|
|
80
|
+
if not legacy_path.is_file():
|
|
81
|
+
raise FileExistsError(f"Legacy skill path is not a file: {legacy_path}")
|
|
82
|
+
digest = hashlib.sha256(legacy_path.read_bytes()).hexdigest()
|
|
83
|
+
if digest != _LEGACY_SKILL_SHA256 and not force:
|
|
84
|
+
raise FileExistsError(
|
|
85
|
+
f"Refusing to remove modified legacy skill without force: {legacy_path}"
|
|
86
|
+
)
|
|
87
|
+
return legacy_path
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _remove_legacy_skill(path: Path | None) -> None:
|
|
91
|
+
if path is None:
|
|
92
|
+
return
|
|
93
|
+
path.unlink()
|
|
94
|
+
try:
|
|
95
|
+
path.parent.rmdir()
|
|
96
|
+
except OSError:
|
|
97
|
+
pass
|
|
59
98
|
|
|
60
99
|
|
|
61
100
|
def _preflight_legacy(legacy_path: Path, *, force: bool) -> bytes | None:
|
|
@@ -157,13 +196,18 @@ def _write_bytes_atomic(path: Path, content: bytes) -> None:
|
|
|
157
196
|
|
|
158
197
|
|
|
159
198
|
def _reject_symbolic_link(path: Path) -> None:
|
|
160
|
-
|
|
161
|
-
|
|
199
|
+
current = path
|
|
200
|
+
while current != current.parent:
|
|
201
|
+
if current.is_symlink():
|
|
202
|
+
raise FileExistsError(
|
|
203
|
+
f"Generated destination must not contain a symbolic link: {current}"
|
|
204
|
+
)
|
|
205
|
+
current = current.parent
|
|
162
206
|
|
|
163
207
|
|
|
164
208
|
def _skill_markdown() -> str:
|
|
165
209
|
return """---
|
|
166
|
-
name: delphi-
|
|
210
|
+
name: python-delphi-lsp
|
|
167
211
|
description: Inspect Delphi and Object Pascal codebases through the Protocol v2 semantic navigator.
|
|
168
212
|
compatibility: opencode
|
|
169
213
|
metadata:
|
|
@@ -181,13 +225,47 @@ Inspect Delphi/Object Pascal only through `delphi_codebase`; never raw bash/read
|
|
|
181
225
|
3. Inspect focused details in this order as needed: `summary`, `declaration`, `members`, `context`, `body`, `implementations`.
|
|
182
226
|
4. Trace relations with `references`, `callers`, `callees`, `uses`, `used_by`, `inherits`, or `implements`.
|
|
183
227
|
5. Call `problems` before declaring evidence missing. Explain any `sound_partial` relation metadata rather than treating partial results as complete.
|
|
184
|
-
6.
|
|
228
|
+
6. 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
|
+
7. Follow `page.next_cursor` with `cursor` until the needed evidence is available.
|
|
185
230
|
|
|
186
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.
|
|
187
232
|
|
|
188
233
|
## Tool calls
|
|
189
234
|
|
|
190
|
-
`delphi_codebase` accepts `action` (`open`, `find`, `inspect`, `trace`, `focus`, `problems`), optional `query`, `target_id`, `project_id`, `detail`, `relation`, `cursor`, `max_items` (1-50), and `max_chars` (256-40000). It has no root or path argument; the active OpenCode worktree is used.
|
|
235
|
+
`delphi_codebase` accepts `action` (`open`, `find`, `inspect`, `trace`, `focus`, `problems`, `metrics`), optional `query`, `target_id`, `project_id`, `detail`, `relation`, `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: subagent
|
|
243
|
+
temperature: 0
|
|
244
|
+
tools:
|
|
245
|
+
delphi_codebase: true
|
|
246
|
+
skill: true
|
|
247
|
+
lsp: false
|
|
248
|
+
bash: false
|
|
249
|
+
read: false
|
|
250
|
+
glob: false
|
|
251
|
+
grep: false
|
|
252
|
+
edit: false
|
|
253
|
+
write: false
|
|
254
|
+
task: false
|
|
255
|
+
webfetch: false
|
|
256
|
+
todowrite: false
|
|
257
|
+
permission:
|
|
258
|
+
"*": deny
|
|
259
|
+
delphi_codebase: allow
|
|
260
|
+
skill:
|
|
261
|
+
"*": deny
|
|
262
|
+
python-delphi-lsp: allow
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
Load `python-delphi-lsp` first, then use only `delphi_codebase` for Delphi and
|
|
266
|
+
Object Pascal codebase inspection. Do not use `lsp`, `bash`, `read`, `glob`,
|
|
267
|
+
`grep`, `edit`, `write`, `task`, `webfetch`, or `todowrite`. Preserve returned
|
|
268
|
+
citations exactly and report partial or ambiguous semantic evidence explicitly.
|
|
191
269
|
"""
|
|
192
270
|
|
|
193
271
|
|
|
@@ -199,7 +277,7 @@ const PYTHON = __PYTHON_EXECUTABLE__
|
|
|
199
277
|
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000
|
|
200
278
|
|
|
201
279
|
type AgentRequest = {
|
|
202
|
-
action: "open" | "find" | "inspect" | "trace" | "focus" | "problems"
|
|
280
|
+
action: "open" | "find" | "inspect" | "trace" | "focus" | "problems" | "metrics"
|
|
203
281
|
query?: string
|
|
204
282
|
target_id?: string
|
|
205
283
|
project_id?: string
|
|
@@ -464,7 +542,7 @@ export const DelphiCodebasePlugin: Plugin = async (_input) => {
|
|
|
464
542
|
delphi_codebase: tool({
|
|
465
543
|
description: "Protocol v2 semantic Delphi/Object Pascal codebase navigation.",
|
|
466
544
|
args: {
|
|
467
|
-
action: tool.schema.enum(["open", "find", "inspect", "trace", "focus", "problems"]),
|
|
545
|
+
action: tool.schema.enum(["open", "find", "inspect", "trace", "focus", "problems", "metrics"]),
|
|
468
546
|
query: tool.schema.string().optional(),
|
|
469
547
|
target_id: tool.schema.string().optional(),
|
|
470
548
|
project_id: tool.schema.string().optional(),
|
|
@@ -524,56 +602,4 @@ export const DelphiCodebasePlugin: Plugin = async (_input) => {
|
|
|
524
602
|
return template.replace("__PYTHON_EXECUTABLE__", python_json)
|
|
525
603
|
|
|
526
604
|
|
|
527
|
-
def _render_opencode_config(config_path: Path) -> tuple[bytes | None, str]:
|
|
528
|
-
_reject_symbolic_link(config_path)
|
|
529
|
-
if config_path.exists():
|
|
530
|
-
if not config_path.is_file():
|
|
531
|
-
raise ValueError(f"opencode config path is not a file: {config_path}")
|
|
532
|
-
config_before = config_path.read_bytes()
|
|
533
|
-
config = json.loads(config_before.decode("utf-8"))
|
|
534
|
-
else:
|
|
535
|
-
config_before = None
|
|
536
|
-
config = {"$schema": "https://opencode.ai/config.json"}
|
|
537
|
-
if not isinstance(config, dict):
|
|
538
|
-
raise ValueError("opencode config must be a JSON object")
|
|
539
|
-
if "agent" not in config:
|
|
540
|
-
agents = {}
|
|
541
|
-
config["agent"] = agents
|
|
542
|
-
else:
|
|
543
|
-
agents = config["agent"]
|
|
544
|
-
if not isinstance(agents, dict):
|
|
545
|
-
raise ValueError("opencode config field 'agent' must be a JSON object")
|
|
546
|
-
agents["vllm-delphi-codebase"] = {
|
|
547
|
-
"description": "Use the Delphi codebase navigator skill and plugin without direct filesystem source inspection.",
|
|
548
|
-
"temperature": 0,
|
|
549
|
-
"prompt": (
|
|
550
|
-
"load delphi-codebase-navigator first, then use only delphi_codebase for Delphi/Object Pascal "
|
|
551
|
-
"codebase inspection. Do not use lsp, bash, read, glob, grep, edit, write, task, webfetch, or todowrite."
|
|
552
|
-
),
|
|
553
|
-
"tools": {
|
|
554
|
-
"delphi_codebase": True,
|
|
555
|
-
"skill": True,
|
|
556
|
-
"lsp": False,
|
|
557
|
-
"bash": False,
|
|
558
|
-
"read": False,
|
|
559
|
-
"glob": False,
|
|
560
|
-
"grep": False,
|
|
561
|
-
"edit": False,
|
|
562
|
-
"write": False,
|
|
563
|
-
"task": False,
|
|
564
|
-
"webfetch": False,
|
|
565
|
-
"todowrite": False,
|
|
566
|
-
},
|
|
567
|
-
"permission": {
|
|
568
|
-
"delphi_codebase": "allow",
|
|
569
|
-
"skill": {
|
|
570
|
-
"*": "deny",
|
|
571
|
-
SKILL_NAME: "allow",
|
|
572
|
-
},
|
|
573
|
-
"lsp": "deny",
|
|
574
|
-
},
|
|
575
|
-
}
|
|
576
|
-
return config_before, json.dumps(config, indent=2, sort_keys=True) + "\n"
|
|
577
|
-
|
|
578
|
-
|
|
579
605
|
__all__ = ["SKILL_NAME", "install_opencode_support", "install_skill"]
|