agent-codinglanguage-mapper 1.0.0__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.
- agent_codinglanguage_mapper/__init__.py +20 -0
- agent_codinglanguage_mapper/__main__.py +4 -0
- agent_codinglanguage_mapper/_version.py +2 -0
- agent_codinglanguage_mapper/adapters/__init__.py +4 -0
- agent_codinglanguage_mapper/adapters/delphi.py +822 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
- agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
- agent_codinglanguage_mapper/cli.py +138 -0
- agent_codinglanguage_mapper/discovery.py +1328 -0
- agent_codinglanguage_mapper/indexer.py +1102 -0
- agent_codinglanguage_mapper/integration.py +186 -0
- agent_codinglanguage_mapper/lsp_server.py +413 -0
- agent_codinglanguage_mapper/lsp_service.py +447 -0
- agent_codinglanguage_mapper/mapper.py +596 -0
- agent_codinglanguage_mapper/models.py +101 -0
- agent_codinglanguage_mapper/protocol.py +152 -0
- agent_codinglanguage_mapper/rendering.py +153 -0
- agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
- agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
- agent_codinglanguage_mapper/worker.py +29 -0
- agent_codinglanguage_mapper/workspace.py +114 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
from .models import Language
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SCHEMA_VERSION = 3
|
|
10
|
+
ACTIONS = frozenset({"open", "find", "inspect", "trace", "focus", "problems"})
|
|
11
|
+
DETAILS = frozenset({"summary", "declaration", "members", "context", "body", "implementations"})
|
|
12
|
+
RELATIONS = frozenset({"references", "callers", "callees", "uses", "used_by", "inherits", "implements", "ffi"})
|
|
13
|
+
REQUEST_FIELDS = frozenset({"action", "query", "target_id", "project_id", "file_id", "languages", "detail", "relation", "cursor", "max_items", "max_chars"})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProtocolError(ValueError):
|
|
17
|
+
"""Raised when a request cannot be represented by protocol schema 3."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class AgentRequest:
|
|
22
|
+
action: str
|
|
23
|
+
query: str = ""
|
|
24
|
+
target_id: str = ""
|
|
25
|
+
project_id: str = ""
|
|
26
|
+
file_id: str = ""
|
|
27
|
+
languages: tuple[Language, ...] = ()
|
|
28
|
+
detail: str = "summary"
|
|
29
|
+
relation: str | None = None
|
|
30
|
+
cursor: str = ""
|
|
31
|
+
max_items: int = 12
|
|
32
|
+
max_chars: int = 12000
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
if self.action not in ACTIONS:
|
|
36
|
+
raise ProtocolError("action must be one of: " + ", ".join(sorted(ACTIONS)))
|
|
37
|
+
if self.detail not in DETAILS:
|
|
38
|
+
raise ProtocolError("detail must be one of: " + ", ".join(sorted(DETAILS)))
|
|
39
|
+
if self.relation is not None and self.relation not in RELATIONS:
|
|
40
|
+
raise ProtocolError("relation must be one of: " + ", ".join(sorted(RELATIONS)))
|
|
41
|
+
if isinstance(self.max_items, bool) or not isinstance(self.max_items, int):
|
|
42
|
+
raise ProtocolError("max_items must be an integer")
|
|
43
|
+
if isinstance(self.max_chars, bool) or not isinstance(self.max_chars, int):
|
|
44
|
+
raise ProtocolError("max_chars must be an integer")
|
|
45
|
+
if not 1 <= self.max_items <= 50:
|
|
46
|
+
raise ProtocolError("max_items must be between 1 and 50")
|
|
47
|
+
if not 256 <= self.max_chars <= 40000:
|
|
48
|
+
raise ProtocolError("max_chars must be between 256 and 40000")
|
|
49
|
+
if not isinstance(self.languages, tuple) or not all(isinstance(language, Language) for language in self.languages):
|
|
50
|
+
raise ProtocolError("languages must be a tuple of Language values")
|
|
51
|
+
for name in ("query", "target_id", "project_id", "file_id", "cursor"):
|
|
52
|
+
if not isinstance(getattr(self, name), str):
|
|
53
|
+
raise ProtocolError(f"{name} must be a string")
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_mapping(cls, value: Mapping[str, Any]) -> "AgentRequest":
|
|
57
|
+
if not isinstance(value, Mapping):
|
|
58
|
+
raise ProtocolError("request must be an object")
|
|
59
|
+
unknown = set(value) - REQUEST_FIELDS
|
|
60
|
+
if unknown:
|
|
61
|
+
raise ProtocolError("unknown request fields: " + ", ".join(sorted(unknown)))
|
|
62
|
+
if not isinstance(value.get("action"), str):
|
|
63
|
+
raise ProtocolError("action is required and must be a string")
|
|
64
|
+
languages = value.get("languages", [])
|
|
65
|
+
if not isinstance(languages, list) or not all(isinstance(item, str) for item in languages):
|
|
66
|
+
raise ProtocolError("languages must be an array of strings")
|
|
67
|
+
for name in ("query", "target_id", "project_id", "file_id", "detail", "cursor"):
|
|
68
|
+
if name in value and not isinstance(value[name], str):
|
|
69
|
+
raise ProtocolError(f"{name} must be a string")
|
|
70
|
+
relation = value.get("relation")
|
|
71
|
+
if relation is not None and not isinstance(relation, str):
|
|
72
|
+
raise ProtocolError("relation must be a string or null")
|
|
73
|
+
for name in ("max_items", "max_chars"):
|
|
74
|
+
if name in value and (isinstance(value[name], bool) or not isinstance(value[name], int)):
|
|
75
|
+
raise ProtocolError(f"{name} must be an integer")
|
|
76
|
+
try:
|
|
77
|
+
parsed_languages = tuple(Language(item) for item in languages)
|
|
78
|
+
except ValueError as exc:
|
|
79
|
+
raise ProtocolError("languages contains an unsupported language") from exc
|
|
80
|
+
return cls(action=value["action"], query=value.get("query", ""), target_id=value.get("target_id", ""),
|
|
81
|
+
project_id=value.get("project_id", ""), file_id=value.get("file_id", ""), languages=parsed_languages,
|
|
82
|
+
detail=value.get("detail", "summary"), relation=relation, cursor=value.get("cursor", ""),
|
|
83
|
+
max_items=value.get("max_items", 12), max_chars=value.get("max_chars", 12000))
|
|
84
|
+
|
|
85
|
+
def to_mapping(self) -> dict[str, object]:
|
|
86
|
+
return {"action": self.action, "query": self.query, "target_id": self.target_id, "project_id": self.project_id,
|
|
87
|
+
"file_id": self.file_id, "languages": [language.value for language in self.languages], "detail": self.detail,
|
|
88
|
+
"relation": self.relation, "cursor": self.cursor, "max_items": self.max_items, "max_chars": self.max_chars}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class Page:
|
|
93
|
+
returned: int = 0
|
|
94
|
+
total: int = 0
|
|
95
|
+
truncated: bool = False
|
|
96
|
+
next_cursor: str = ""
|
|
97
|
+
|
|
98
|
+
def to_mapping(self) -> dict[str, object]:
|
|
99
|
+
return {
|
|
100
|
+
"returned": self.returned,
|
|
101
|
+
"total": self.total,
|
|
102
|
+
"truncated": self.truncated,
|
|
103
|
+
"next_cursor": self.next_cursor,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class Focus:
|
|
109
|
+
project_id: str = ""
|
|
110
|
+
file_id: str = ""
|
|
111
|
+
target_id: str = ""
|
|
112
|
+
languages: tuple[Language, ...] = ()
|
|
113
|
+
|
|
114
|
+
def to_mapping(self) -> dict[str, object]:
|
|
115
|
+
return {
|
|
116
|
+
"project_id": self.project_id,
|
|
117
|
+
"file_id": self.file_id,
|
|
118
|
+
"target_id": self.target_id,
|
|
119
|
+
"languages": [language.value for language in self.languages],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class Context:
|
|
125
|
+
chars: int = 0
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def approx_tokens(self) -> int:
|
|
129
|
+
return (self.chars + 3) // 4
|
|
130
|
+
|
|
131
|
+
def to_mapping(self) -> dict[str, int]:
|
|
132
|
+
return {"chars": self.chars, "approx_tokens": self.approx_tokens}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class AgentResponse:
|
|
137
|
+
workspace_revision: str
|
|
138
|
+
result: dict[str, object]
|
|
139
|
+
focus: Focus = Focus()
|
|
140
|
+
page: Page = Page()
|
|
141
|
+
context: Context = Context()
|
|
142
|
+
schema: int = SCHEMA_VERSION
|
|
143
|
+
|
|
144
|
+
def to_mapping(self) -> dict[str, object]:
|
|
145
|
+
return {
|
|
146
|
+
"schema": self.schema,
|
|
147
|
+
"workspace_revision": self.workspace_revision,
|
|
148
|
+
"focus": self.focus.to_mapping(),
|
|
149
|
+
"result": self.result,
|
|
150
|
+
"page": self.page.to_mapping(),
|
|
151
|
+
"context": self.context.to_mapping(),
|
|
152
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, is_dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Iterable
|
|
8
|
+
|
|
9
|
+
from .mapper import CodebaseMapper
|
|
10
|
+
from .models import Language
|
|
11
|
+
from .protocol import AgentRequest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def layer_data(
|
|
15
|
+
mapper: CodebaseMapper,
|
|
16
|
+
layer: str,
|
|
17
|
+
*,
|
|
18
|
+
query: str = "",
|
|
19
|
+
languages: Iterable[Language] = (),
|
|
20
|
+
) -> dict[str, object]:
|
|
21
|
+
snapshot = mapper.snapshot
|
|
22
|
+
selected_languages = tuple(languages)
|
|
23
|
+
if layer == "overview":
|
|
24
|
+
return {
|
|
25
|
+
"workspace": str(mapper.root),
|
|
26
|
+
"revision": snapshot.revision,
|
|
27
|
+
"counts": {
|
|
28
|
+
"projects": len(snapshot.projects),
|
|
29
|
+
"files": len(snapshot.files),
|
|
30
|
+
"symbols": len(snapshot.symbols),
|
|
31
|
+
"references": len(snapshot.references),
|
|
32
|
+
"relations": len(snapshot.relations),
|
|
33
|
+
"problems": len(snapshot.problems),
|
|
34
|
+
},
|
|
35
|
+
"languages": sorted({item.language.value for item in snapshot.files}),
|
|
36
|
+
}
|
|
37
|
+
if layer == "projects":
|
|
38
|
+
return {"items": [_jsonable(item) for item in snapshot.projects]}
|
|
39
|
+
if layer == "languages":
|
|
40
|
+
counts = {
|
|
41
|
+
language.value: sum(1 for item in snapshot.files if item.language is language)
|
|
42
|
+
for language in Language
|
|
43
|
+
if any(item.language is language for item in snapshot.files)
|
|
44
|
+
}
|
|
45
|
+
return {"items": [{"language": name, "files": count} for name, count in sorted(counts.items())]}
|
|
46
|
+
if layer == "files":
|
|
47
|
+
items = [
|
|
48
|
+
_jsonable(item)
|
|
49
|
+
for item in snapshot.files
|
|
50
|
+
if (not query or query.casefold() in item.path.casefold())
|
|
51
|
+
and (not selected_languages or item.language in selected_languages)
|
|
52
|
+
]
|
|
53
|
+
return {"items": items}
|
|
54
|
+
if layer == "file":
|
|
55
|
+
source_file = next(
|
|
56
|
+
(
|
|
57
|
+
item
|
|
58
|
+
for item in snapshot.files
|
|
59
|
+
if item.file_id == query or item.path.casefold() == query.casefold()
|
|
60
|
+
),
|
|
61
|
+
None,
|
|
62
|
+
)
|
|
63
|
+
if source_file is None:
|
|
64
|
+
return {"items": []}
|
|
65
|
+
return {
|
|
66
|
+
"item": _jsonable(source_file),
|
|
67
|
+
"symbols": [_jsonable(item) for item in snapshot.symbols_by_file.get(source_file.file_id, ())],
|
|
68
|
+
}
|
|
69
|
+
if layer == "symbols":
|
|
70
|
+
response = mapper.request(
|
|
71
|
+
AgentRequest(action="find", query=query, languages=selected_languages, max_items=50, max_chars=40000)
|
|
72
|
+
)
|
|
73
|
+
return response.result
|
|
74
|
+
if layer in {"symbol", "implementation", "references"}:
|
|
75
|
+
target_id = _target_id(mapper, query, selected_languages)
|
|
76
|
+
if not target_id:
|
|
77
|
+
return {"items": []}
|
|
78
|
+
if layer == "references":
|
|
79
|
+
return mapper.request(
|
|
80
|
+
AgentRequest(action="trace", target_id=target_id, relation="references", max_items=50, max_chars=40000)
|
|
81
|
+
).result
|
|
82
|
+
detail = "body" if layer == "implementation" else "summary"
|
|
83
|
+
return mapper.request(AgentRequest(action="inspect", target_id=target_id, detail=detail)).result
|
|
84
|
+
if layer == "problems":
|
|
85
|
+
return mapper.request(AgentRequest(action="problems", max_items=50, max_chars=40000)).result
|
|
86
|
+
raise ValueError(f"unsupported layer: {layer}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def render_markdown(data: dict[str, object], *, title: str) -> str:
|
|
90
|
+
lines = [f"# {title}"]
|
|
91
|
+
if "items" in data and isinstance(data["items"], list):
|
|
92
|
+
for item in data["items"]:
|
|
93
|
+
if isinstance(item, dict):
|
|
94
|
+
name = item.get("qualified_name") or item.get("name") or item.get("path") or item.get("language")
|
|
95
|
+
lines.append(f"- **{name}**")
|
|
96
|
+
for key in ("kind", "language", "citation", "signature", "files", "resolution"):
|
|
97
|
+
if item.get(key) not in (None, ""):
|
|
98
|
+
lines.append(f" - {key}: `{item[key]}`")
|
|
99
|
+
else:
|
|
100
|
+
lines.append(f"- {item}")
|
|
101
|
+
else:
|
|
102
|
+
lines.append("```json")
|
|
103
|
+
lines.append(json.dumps(data, indent=2, sort_keys=True))
|
|
104
|
+
lines.append("```")
|
|
105
|
+
return "\n".join(lines) + "\n"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def snapshot_mapping(mapper: CodebaseMapper) -> dict[str, object]:
|
|
109
|
+
snapshot = mapper.snapshot
|
|
110
|
+
return {
|
|
111
|
+
"schema": 3,
|
|
112
|
+
"workspace": str(mapper.root),
|
|
113
|
+
"revision": snapshot.revision,
|
|
114
|
+
"projects": [_jsonable(item) for item in snapshot.projects],
|
|
115
|
+
"files": [_jsonable(item) for item in snapshot.files],
|
|
116
|
+
"symbols": [_jsonable(item) for item in snapshot.symbols],
|
|
117
|
+
"references": [_jsonable(item) for item in snapshot.references],
|
|
118
|
+
"relations": [_jsonable(item) for item in snapshot.relations],
|
|
119
|
+
"problems": [_jsonable(item) for item in snapshot.problems],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def write_index(mapper: CodebaseMapper, output: str | Path) -> Path:
|
|
124
|
+
path = Path(output).expanduser()
|
|
125
|
+
if not path.is_absolute():
|
|
126
|
+
path = mapper.root / path
|
|
127
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
|
129
|
+
temporary.write_text(json.dumps(snapshot_mapping(mapper), indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
130
|
+
temporary.replace(path)
|
|
131
|
+
return path
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _target_id(mapper: CodebaseMapper, query: str, languages: tuple[Language, ...]) -> str:
|
|
135
|
+
if query in mapper.snapshot.symbols_by_id:
|
|
136
|
+
return query
|
|
137
|
+
response = mapper.request(
|
|
138
|
+
AgentRequest(action="find", query=query, languages=languages, max_items=1, max_chars=12000)
|
|
139
|
+
)
|
|
140
|
+
items = response.result.get("items", [])
|
|
141
|
+
return str(items[0]["target_id"]) if isinstance(items, list) and items else ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _jsonable(value: Any) -> Any:
|
|
145
|
+
if isinstance(value, Enum):
|
|
146
|
+
return value.value
|
|
147
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
148
|
+
return _jsonable(asdict(value))
|
|
149
|
+
if isinstance(value, dict):
|
|
150
|
+
return {str(key): _jsonable(item) for key, item in value.items()}
|
|
151
|
+
if isinstance(value, (list, tuple)):
|
|
152
|
+
return [_jsonable(item) for item in value]
|
|
153
|
+
return value
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"
|
|
3
|
+
import { createInterface, type Interface } from "node:readline"
|
|
4
|
+
|
|
5
|
+
const PYTHON_EXECUTABLE = {{PYTHON_EXECUTABLE}}
|
|
6
|
+
const WORKER_TIMEOUT_MS = 30_000
|
|
7
|
+
const WORKER_IDLE_TIMEOUT_MS = 60_000
|
|
8
|
+
const MAX_STDERR_CHARS = 8_000
|
|
9
|
+
|
|
10
|
+
type Unrefable = { unref?: () => void }
|
|
11
|
+
|
|
12
|
+
function unref(handle: unknown): void {
|
|
13
|
+
;(handle as Unrefable | undefined)?.unref?.()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type WorkerSession = {
|
|
17
|
+
process: ChildProcessWithoutNullStreams
|
|
18
|
+
lines: Interface
|
|
19
|
+
queue: Promise<unknown>
|
|
20
|
+
stderr: string
|
|
21
|
+
error?: Error
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type WorkerState = {
|
|
25
|
+
worker?: WorkerSession
|
|
26
|
+
idleTimer?: ReturnType<typeof setTimeout>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function startWorker(root: string): WorkerSession {
|
|
30
|
+
const child = spawn(
|
|
31
|
+
PYTHON_EXECUTABLE,
|
|
32
|
+
["-m", "agent_codinglanguage_mapper", "worker", "--root", root],
|
|
33
|
+
{ stdio: ["pipe", "pipe", "pipe"], shell: false },
|
|
34
|
+
)
|
|
35
|
+
const session: WorkerSession = {
|
|
36
|
+
process: child,
|
|
37
|
+
lines: createInterface({ input: child.stdout, crlfDelay: Number.POSITIVE_INFINITY }),
|
|
38
|
+
queue: Promise.resolve(),
|
|
39
|
+
stderr: "",
|
|
40
|
+
}
|
|
41
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
42
|
+
session.stderr = `${session.stderr}${String(chunk)}`.slice(-MAX_STDERR_CHARS)
|
|
43
|
+
})
|
|
44
|
+
child.on("error", (error: Error) => {
|
|
45
|
+
session.error = error
|
|
46
|
+
})
|
|
47
|
+
child.unref()
|
|
48
|
+
unref(child.stdin)
|
|
49
|
+
unref(child.stdout)
|
|
50
|
+
unref(child.stderr)
|
|
51
|
+
return session
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sendRequest(session: WorkerSession, payload: Record<string, unknown>): Promise<string> {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
if (session.error) {
|
|
57
|
+
reject(session.error)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
let settled = false
|
|
61
|
+
let timeout: ReturnType<typeof setTimeout>
|
|
62
|
+
const failure = (message: string): Error => {
|
|
63
|
+
const details = session.stderr.trim()
|
|
64
|
+
return new Error(details ? `${message}\n${details}` : message)
|
|
65
|
+
}
|
|
66
|
+
const cleanup = (): void => {
|
|
67
|
+
clearTimeout(timeout)
|
|
68
|
+
session.process.removeListener("exit", onExit)
|
|
69
|
+
session.process.removeListener("error", onError)
|
|
70
|
+
session.lines.removeListener("line", onLine)
|
|
71
|
+
}
|
|
72
|
+
const fail = (error: Error): void => {
|
|
73
|
+
if (settled) return
|
|
74
|
+
settled = true
|
|
75
|
+
cleanup()
|
|
76
|
+
reject(error)
|
|
77
|
+
}
|
|
78
|
+
const onLine = (line: string): void => {
|
|
79
|
+
if (settled) return
|
|
80
|
+
settled = true
|
|
81
|
+
cleanup()
|
|
82
|
+
resolve(line)
|
|
83
|
+
}
|
|
84
|
+
const onExit = (): void => fail(failure("codebase mapper worker exited before responding"))
|
|
85
|
+
const onError = (error: Error): void => fail(failure(`codebase mapper worker failed to start: ${error.message}`))
|
|
86
|
+
timeout = setTimeout(() => {
|
|
87
|
+
session.process.kill()
|
|
88
|
+
fail(failure(`codebase mapper worker timed out after ${WORKER_TIMEOUT_MS}ms`))
|
|
89
|
+
}, WORKER_TIMEOUT_MS)
|
|
90
|
+
session.process.once("exit", onExit)
|
|
91
|
+
session.process.once("error", onError)
|
|
92
|
+
session.lines.once("line", onLine)
|
|
93
|
+
try {
|
|
94
|
+
session.process.stdin.write(`${JSON.stringify(payload)}\n`, (error?: Error | null) => {
|
|
95
|
+
if (error) fail(failure(`codebase mapper worker input failed: ${error.message}`))
|
|
96
|
+
})
|
|
97
|
+
} catch (error) {
|
|
98
|
+
fail(error instanceof Error ? error : new Error(String(error)))
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const CodebaseMapPlugin: Plugin = async ({ directory, worktree }) => {
|
|
104
|
+
const root = directory || worktree
|
|
105
|
+
const sessions = new Map<string, WorkerState>()
|
|
106
|
+
|
|
107
|
+
const stateFor = (sessionID: string): WorkerState => {
|
|
108
|
+
const existing = sessions.get(sessionID)
|
|
109
|
+
if (existing) return existing
|
|
110
|
+
const created: WorkerState = {}
|
|
111
|
+
sessions.set(sessionID, created)
|
|
112
|
+
return created
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const cancelWorkerStop = (state: WorkerState): void => {
|
|
116
|
+
if (state.idleTimer === undefined) return
|
|
117
|
+
clearTimeout(state.idleTimer)
|
|
118
|
+
state.idleTimer = undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const stopWorker = (sessionID: string): void => {
|
|
122
|
+
const state = sessions.get(sessionID)
|
|
123
|
+
if (!state) return
|
|
124
|
+
cancelWorkerStop(state)
|
|
125
|
+
sessions.delete(sessionID)
|
|
126
|
+
const active = state.worker
|
|
127
|
+
state.worker = undefined
|
|
128
|
+
if (!active) return
|
|
129
|
+
active.lines.close()
|
|
130
|
+
if (active.process.stdin.writable) active.process.stdin.end()
|
|
131
|
+
if (active.process.exitCode === null && active.process.signalCode === null) active.process.kill()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const scheduleWorkerStop = (sessionID: string): void => {
|
|
135
|
+
const state = sessions.get(sessionID)
|
|
136
|
+
if (!state?.worker) return
|
|
137
|
+
cancelWorkerStop(state)
|
|
138
|
+
state.idleTimer = setTimeout(() => {
|
|
139
|
+
state.idleTimer = undefined
|
|
140
|
+
stopWorker(sessionID)
|
|
141
|
+
}, WORKER_IDLE_TIMEOUT_MS)
|
|
142
|
+
unref(state.idleTimer)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const runSerialized = (sessionID: string, payload: Record<string, unknown>): Promise<string> => {
|
|
146
|
+
const state = stateFor(sessionID)
|
|
147
|
+
cancelWorkerStop(state)
|
|
148
|
+
if (
|
|
149
|
+
!state.worker ||
|
|
150
|
+
state.worker.process.exitCode !== null ||
|
|
151
|
+
state.worker.process.signalCode !== null ||
|
|
152
|
+
!state.worker.process.stdin.writable
|
|
153
|
+
) state.worker = startWorker(root)
|
|
154
|
+
const active = state.worker
|
|
155
|
+
const next = active.queue.then(() => sendRequest(active, payload)).catch((error: unknown) => {
|
|
156
|
+
if (state.worker === active) state.worker = undefined
|
|
157
|
+
if (active.process.exitCode === null && active.process.signalCode === null) active.process.kill()
|
|
158
|
+
throw error
|
|
159
|
+
})
|
|
160
|
+
active.queue = next.then(() => undefined, () => undefined)
|
|
161
|
+
return next
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
event: async ({ event }) => {
|
|
166
|
+
if (event.type === "session.deleted") stopWorker(event.properties.info.id)
|
|
167
|
+
else if (event.type === "session.idle") scheduleWorkerStop(event.properties.sessionID)
|
|
168
|
+
},
|
|
169
|
+
tool: {
|
|
170
|
+
codebase_map: tool({
|
|
171
|
+
description: "Query the persistent Protocol 3 semantic map for this mixed-language workspace directory.",
|
|
172
|
+
args: {
|
|
173
|
+
action: tool.schema.enum(["open", "find", "inspect", "trace", "focus", "problems"]),
|
|
174
|
+
query: tool.schema.string().optional(),
|
|
175
|
+
target_id: tool.schema.string().optional(),
|
|
176
|
+
project_id: tool.schema.string().optional(),
|
|
177
|
+
file_id: tool.schema.string().optional(),
|
|
178
|
+
languages: tool.schema.array(tool.schema.enum(["python", "rust", "csharp", "delphi", "cpp"])).optional(),
|
|
179
|
+
detail: tool.schema.enum(["summary", "declaration", "members", "context", "body", "implementations"]).optional(),
|
|
180
|
+
relation: tool.schema.enum(["references", "callers", "callees", "uses", "used_by", "inherits", "implements", "ffi"]).optional(),
|
|
181
|
+
cursor: tool.schema.string().optional(),
|
|
182
|
+
max_items: tool.schema.number().int().min(1).max(50).optional(),
|
|
183
|
+
max_chars: tool.schema.number().int().min(256).max(40000).optional(),
|
|
184
|
+
},
|
|
185
|
+
async execute(args, context): Promise<string> {
|
|
186
|
+
return runSerialized(context.sessionID, args as Record<string, unknown>)
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mixed-codebase-navigator
|
|
3
|
+
description: Inspect Python, Rust, C#, Delphi, C++, and mixed-language workspaces through a bounded semantic codebase map without raw filesystem search tools.
|
|
4
|
+
license: MPL-2.0
|
|
5
|
+
compatibility: opencode
|
|
6
|
+
metadata:
|
|
7
|
+
protocol: "3"
|
|
8
|
+
package: agent-codinglanguage-mapper
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Mixed Codebase Navigator
|
|
12
|
+
|
|
13
|
+
Use this skill whenever a task requires understanding source code, projects, symbols, calls, inheritance, implementations, references, FFI, or parser problems in the current workspace.
|
|
14
|
+
|
|
15
|
+
## Tool Boundary
|
|
16
|
+
|
|
17
|
+
- Use only `skill` and `codebase_map` while investigating source code.
|
|
18
|
+
- Never call or request `read`, `grep`, `glob`, `list`, `bash`, `cat`, or an equivalent raw filesystem tool.
|
|
19
|
+
- Do not infer a relation that is not present in tool output.
|
|
20
|
+
- Treat `sound_partial`, `ambiguous`, and `unresolved` as explicit uncertainty.
|
|
21
|
+
|
|
22
|
+
## Protocol 3 Workflow
|
|
23
|
+
|
|
24
|
+
1. Call `codebase_map` with `action: open` to establish the workspace revision and projects.
|
|
25
|
+
2. Use `action: find` with a narrow symbol or concept query. Filter with `languages`, `project_id`, or `file_id` when useful.
|
|
26
|
+
3. Use `action: inspect` for the chosen `target_id`. Start with `detail: summary`, then request only the required `declaration`, `members`, `context`, `body`, or `implementations` detail.
|
|
27
|
+
4. Use `action: trace` with exactly one relation: `references`, `callers`, `callees`, `uses`, `used_by`, `inherits`, `implements`, or `ffi`.
|
|
28
|
+
5. Use `action: focus` to preserve the selected project, file, target, and languages while changing detail.
|
|
29
|
+
6. Use `action: problems` before concluding that a missing result means the code does not exist.
|
|
30
|
+
7. Follow `page.next_cursor` until the required evidence is found. Reuse the same request fields when advancing a cursor.
|
|
31
|
+
|
|
32
|
+
## Layered Context
|
|
33
|
+
|
|
34
|
+
Move from broad to narrow: `overview` -> `projects` -> `languages` -> `files` -> `file` -> `symbols` -> `symbol` -> `implementation` -> `references` -> `problems`.
|
|
35
|
+
|
|
36
|
+
Keep only the current focus and directly relevant evidence in context. Prefer summaries and declarations before source bodies. Source chunks are bounded to 6,000 characters; request another focused target instead of asking for an entire file.
|
|
37
|
+
|
|
38
|
+
## Relations
|
|
39
|
+
|
|
40
|
+
Trace only the explicit Protocol 3 relations returned by `codebase_map`; never turn a name match into a call, inheritance, implementation, or FFI claim.
|
|
41
|
+
|
|
42
|
+
For a resolved FFI trace, cite the returned `source_symbol.citation` and `target_symbol.citation` directly. These fields identify both language endpoints without a second name-based search.
|
|
43
|
+
|
|
44
|
+
## Evidence Rules
|
|
45
|
+
|
|
46
|
+
- Use a returned symbol's `role` to distinguish a declaration from an implementation; do not infer that distinction from name shape or raw ranges.
|
|
47
|
+
- Preserve returned citations exactly; they are one-based even though protocol ranges are zero-based and half-open.
|
|
48
|
+
- Never recalculate a citation from `range` or `name_range`; copy the returned `citation` field verbatim.
|
|
49
|
+
- Cite every code claim with the returned one-based `path:line` citation.
|
|
50
|
+
- State the language for mixed-workspace findings.
|
|
51
|
+
- Distinguish declarations from implementations and explicit FFI from same-name coincidence.
|
|
52
|
+
- Report ambiguity or unresolved project variables rather than guessing.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TextIO
|
|
6
|
+
|
|
7
|
+
from .mapper import CodebaseMapper
|
|
8
|
+
from .protocol import AgentRequest, ProtocolError, SCHEMA_VERSION
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def serve_worker(root: str | Path, input_stream: TextIO, output_stream: TextIO) -> int:
|
|
12
|
+
with CodebaseMapper.open(root) as mapper:
|
|
13
|
+
for raw_line in input_stream:
|
|
14
|
+
line = raw_line.strip()
|
|
15
|
+
if not line:
|
|
16
|
+
continue
|
|
17
|
+
try:
|
|
18
|
+
payload = json.loads(line)
|
|
19
|
+
request = AgentRequest.from_mapping(payload)
|
|
20
|
+
response: dict[str, object] = mapper.request(request).to_mapping()
|
|
21
|
+
except (json.JSONDecodeError, ProtocolError, TypeError, ValueError) as exc:
|
|
22
|
+
response = {
|
|
23
|
+
"schema": SCHEMA_VERSION,
|
|
24
|
+
"workspace_revision": mapper.snapshot.revision,
|
|
25
|
+
"error": {"code": "invalid_request", "message": str(exc)},
|
|
26
|
+
}
|
|
27
|
+
output_stream.write(json.dumps(response, sort_keys=True, separators=(",", ":")) + "\n")
|
|
28
|
+
output_stream.flush()
|
|
29
|
+
return 0
|