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.
Files changed (42) hide show
  1. agent_codinglanguage_mapper/__init__.py +20 -0
  2. agent_codinglanguage_mapper/__main__.py +4 -0
  3. agent_codinglanguage_mapper/_version.py +2 -0
  4. agent_codinglanguage_mapper/adapters/__init__.py +4 -0
  5. agent_codinglanguage_mapper/adapters/delphi.py +822 -0
  6. agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
  7. agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
  8. agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
  9. agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
  10. agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
  11. agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
  12. agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
  13. agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
  14. agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
  15. agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
  16. agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
  17. agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
  18. agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
  19. agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
  20. agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
  21. agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
  22. agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
  23. agent_codinglanguage_mapper/cli.py +138 -0
  24. agent_codinglanguage_mapper/discovery.py +1328 -0
  25. agent_codinglanguage_mapper/indexer.py +1102 -0
  26. agent_codinglanguage_mapper/integration.py +186 -0
  27. agent_codinglanguage_mapper/lsp_server.py +413 -0
  28. agent_codinglanguage_mapper/lsp_service.py +447 -0
  29. agent_codinglanguage_mapper/mapper.py +596 -0
  30. agent_codinglanguage_mapper/models.py +101 -0
  31. agent_codinglanguage_mapper/protocol.py +152 -0
  32. agent_codinglanguage_mapper/rendering.py +153 -0
  33. agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
  34. agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
  35. agent_codinglanguage_mapper/worker.py +29 -0
  36. agent_codinglanguage_mapper/workspace.py +114 -0
  37. agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
  38. agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
  39. agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
  40. agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
  41. agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
  42. 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