sin-code-bundle 0.9.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.
- sin_code_bundle/__init__.py +6 -0
- sin_code_bundle/agents_md.py +245 -0
- sin_code_bundle/ast_edit.py +323 -0
- sin_code_bundle/bench.py +506 -0
- sin_code_bundle/budget.py +51 -0
- sin_code_bundle/cache.py +131 -0
- sin_code_bundle/checkpoint.py +230 -0
- sin_code_bundle/cli.py +1943 -0
- sin_code_bundle/codocs.py +328 -0
- sin_code_bundle/dap_bridge.py +135 -0
- sin_code_bundle/data/codocs/SKILL.md +280 -0
- sin_code_bundle/gitnexus.py +368 -0
- sin_code_bundle/hashline.py +216 -0
- sin_code_bundle/hooks.py +249 -0
- sin_code_bundle/immortal_commit.py +288 -0
- sin_code_bundle/interceptor.py +119 -0
- sin_code_bundle/lsp_backend.py +303 -0
- sin_code_bundle/lsp_bootstrap.py +85 -0
- sin_code_bundle/markitdown.py +254 -0
- sin_code_bundle/mcp_config.py +455 -0
- sin_code_bundle/mcp_server.py +963 -0
- sin_code_bundle/memory.py +208 -0
- sin_code_bundle/merge_safety.py +313 -0
- sin_code_bundle/orchestration_worktrees.py +102 -0
- sin_code_bundle/policy.py +224 -0
- sin_code_bundle/preflight.py +152 -0
- sin_code_bundle/programming_workflow.py +541 -0
- sin_code_bundle/rtk.py +154 -0
- sin_code_bundle/safety.py +52 -0
- sin_code_bundle/session_warmup.py +247 -0
- sin_code_bundle/skills.py +188 -0
- sin_code_bundle/symbol_resolve.py +166 -0
- sin_code_bundle/tools/__init__.py +4 -0
- sin_code_bundle/tools/pypi_setup.py +289 -0
- sin_code_bundle/vfs.py +264 -0
- sin_code_bundle-0.9.2.dist-info/METADATA +470 -0
- sin_code_bundle-0.9.2.dist-info/RECORD +41 -0
- sin_code_bundle-0.9.2.dist-info/WHEEL +5 -0
- sin_code_bundle-0.9.2.dist-info/entry_points.txt +4 -0
- sin_code_bundle-0.9.2.dist-info/licenses/LICENSE +21 -0
- sin_code_bundle-0.9.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Generatoren fuer MCP-Client-Konfigurationen (WS2, Issue #2).
|
|
3
|
+
|
|
4
|
+
Erzeugt fertig einfuegbare Konfiguration fuer die drei Ziel-CLIs:
|
|
5
|
+
|
|
6
|
+
- ``opencode`` -> JSON (Key ``mcp``, ``type: "local"``)
|
|
7
|
+
- ``codex`` -> TOML (``[mcp_servers.sin]``)
|
|
8
|
+
- ``hermes`` -> YAML (``mcp_servers.sin``)
|
|
9
|
+
|
|
10
|
+
Die Funktionen liefern reine Strings (fuer ``--stdout``) sowie Helfer zum
|
|
11
|
+
idempotenten Mergen in eine bestehende Konfigurationsdatei (fuer ``--write``).
|
|
12
|
+
|
|
13
|
+
Docs: mcp_config.doc.md
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
SERVER_NAME = "sin"
|
|
23
|
+
COMMAND = "sin"
|
|
24
|
+
ARGS = ["serve"]
|
|
25
|
+
|
|
26
|
+
# Standard-Env, das alle Clients durchreichen. Werte sind Platzhalter, die der
|
|
27
|
+
# Nutzer bei Bedarf anpasst; leere Defaults halten die Konfiguration gueltig.
|
|
28
|
+
DEFAULT_ENV: dict[str, str] = {}
|
|
29
|
+
|
|
30
|
+
SUPPORTED_CLIENTS = ("opencode", "codex", "hermes")
|
|
31
|
+
|
|
32
|
+
# All 15 individual SIN-Code tools (BR-3 / Issue #16).
|
|
33
|
+
# First 7 are Go binaries, remaining 8 are Python modules with MCP servers.
|
|
34
|
+
FULL_TOOLS: list[tuple[str, list[str]]] = [
|
|
35
|
+
("sin-discover", ["~/.local/bin/discover", "--mcp"]),
|
|
36
|
+
("sin-execute", ["~/.local/bin/execute", "--mcp"]),
|
|
37
|
+
("sin-map", ["~/.local/bin/map", "--mcp"]),
|
|
38
|
+
("sin-grasp", ["~/.local/bin/grasp", "--mcp"]),
|
|
39
|
+
("sin-scout", ["~/.local/bin/scout", "--mcp"]),
|
|
40
|
+
("sin-harvest", ["~/.local/bin/harvest", "--mcp"]),
|
|
41
|
+
("sin-orchestrate", ["~/.local/bin/orchestrate", "--mcp"]),
|
|
42
|
+
("sin-sckg", ["python", "-m", "sin_code_sckg.mcp_server"]),
|
|
43
|
+
("sin-ibd", ["python", "-m", "sin_code_ibd.mcp_server"]),
|
|
44
|
+
("sin-poc", ["python", "-m", "sin_code_poc.mcp_server"]),
|
|
45
|
+
("sin-efsm", ["python", "-m", "sin_code_efsm.mcp_server"]),
|
|
46
|
+
("sin-adw", ["python", "-m", "sin_code_adw.mcp_server"]),
|
|
47
|
+
("sin-oracle", ["python", "-m", "sin_code_oracle.mcp_server"]),
|
|
48
|
+
("sin-orchestration", ["python", "-m", "sin_code_orchestration.mcp_server"]),
|
|
49
|
+
("sin-review-interface", ["python", "-m", "sin_code_review_interface.mcp_server"]),
|
|
50
|
+
("sin-brain", ["python", "-m", "sin_brain.mcp_server"]),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ── Generatoren (reine Strings) ────────────────────────────────────────────
|
|
55
|
+
def generate_opencode(env: dict[str, str] | None = None) -> str:
|
|
56
|
+
"""OpenCode liest ``opencode.json``: Key ``mcp`` mit lokalem Server.
|
|
57
|
+
|
|
58
|
+
Format (offiziell dokumentiert):
|
|
59
|
+
{
|
|
60
|
+
"mcp": {
|
|
61
|
+
"sin": {
|
|
62
|
+
"type": "local",
|
|
63
|
+
"command": ["sin", "serve"],
|
|
64
|
+
"enabled": true,
|
|
65
|
+
"environment": { ... }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
"""
|
|
70
|
+
env = DEFAULT_ENV if env is None else env
|
|
71
|
+
config = {
|
|
72
|
+
"mcp": {
|
|
73
|
+
SERVER_NAME: {
|
|
74
|
+
"type": "local",
|
|
75
|
+
"command": [COMMAND, *ARGS],
|
|
76
|
+
"enabled": True,
|
|
77
|
+
"environment": env,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return json.dumps(config, indent=2)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def generate_codex(env: dict[str, str] | None = None) -> str:
|
|
85
|
+
"""Codex liest ``~/.codex/config.toml``: ``[mcp_servers.<name>]``.
|
|
86
|
+
|
|
87
|
+
Format (offiziell dokumentiert):
|
|
88
|
+
[mcp_servers.sin]
|
|
89
|
+
command = "sin"
|
|
90
|
+
args = ["serve"]
|
|
91
|
+
|
|
92
|
+
[mcp_servers.sin.env]
|
|
93
|
+
KEY = "value"
|
|
94
|
+
"""
|
|
95
|
+
env = DEFAULT_ENV if env is None else env
|
|
96
|
+
lines = [
|
|
97
|
+
f"[mcp_servers.{SERVER_NAME}]",
|
|
98
|
+
f'command = "{COMMAND}"',
|
|
99
|
+
f"args = {_toml_array(ARGS)}",
|
|
100
|
+
]
|
|
101
|
+
if env:
|
|
102
|
+
lines.append("")
|
|
103
|
+
lines.append(f"[mcp_servers.{SERVER_NAME}.env]")
|
|
104
|
+
for key, value in env.items():
|
|
105
|
+
lines.append(f'{key} = "{value}"')
|
|
106
|
+
return "\n".join(lines) + "\n"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def generate_hermes(env: dict[str, str] | None = None) -> str:
|
|
110
|
+
"""Hermes liest YAML: ``mcp_servers.<name>`` mit command/args.
|
|
111
|
+
|
|
112
|
+
Format:
|
|
113
|
+
mcp_servers:
|
|
114
|
+
sin:
|
|
115
|
+
command: sin
|
|
116
|
+
args:
|
|
117
|
+
- serve
|
|
118
|
+
env: { ... }
|
|
119
|
+
"""
|
|
120
|
+
env = DEFAULT_ENV if env is None else env
|
|
121
|
+
server: dict[str, Any] = {
|
|
122
|
+
"command": COMMAND,
|
|
123
|
+
"args": list(ARGS),
|
|
124
|
+
}
|
|
125
|
+
if env:
|
|
126
|
+
server["env"] = env
|
|
127
|
+
config = {"mcp_servers": {SERVER_NAME: server}}
|
|
128
|
+
try:
|
|
129
|
+
import yaml
|
|
130
|
+
|
|
131
|
+
return yaml.safe_dump(config, sort_keys=False, default_flow_style=False)
|
|
132
|
+
except ImportError:
|
|
133
|
+
# Fallback ohne PyYAML: minimaler, gueltiger YAML-Text.
|
|
134
|
+
out = ["mcp_servers:", f" {SERVER_NAME}:", f" command: {COMMAND}", " args:"]
|
|
135
|
+
out += [f" - {a}" for a in ARGS]
|
|
136
|
+
if env:
|
|
137
|
+
out.append(" env:")
|
|
138
|
+
out += [f" {k}: {v}" for k, v in env.items()]
|
|
139
|
+
return "\n".join(out) + "\n"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def generate_full_opencode(env: dict[str, str] | None = None) -> str:
|
|
143
|
+
"""Full OpenCode config with all 15 individual SIN-Code tools (BR-3)."""
|
|
144
|
+
env = DEFAULT_ENV if env is None else env
|
|
145
|
+
mcp: dict[str, Any] = {}
|
|
146
|
+
for name, cmd in FULL_TOOLS:
|
|
147
|
+
mcp[name] = {
|
|
148
|
+
"type": "local",
|
|
149
|
+
"command": list(cmd),
|
|
150
|
+
"enabled": True,
|
|
151
|
+
"environment": env,
|
|
152
|
+
}
|
|
153
|
+
return json.dumps({"mcp": mcp}, indent=2)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def generate_full_codex(env: dict[str, str] | None = None) -> str:
|
|
157
|
+
"""Full Codex TOML config with all 15 individual SIN-Code tools (BR-3)."""
|
|
158
|
+
env = DEFAULT_ENV if env is None else env
|
|
159
|
+
blocks: list[str] = []
|
|
160
|
+
for name, cmd in FULL_TOOLS:
|
|
161
|
+
lines = [
|
|
162
|
+
f"[mcp_servers.{name}]",
|
|
163
|
+
f'command = "{cmd[0]}"',
|
|
164
|
+
f"args = {_toml_array(cmd[1:])}",
|
|
165
|
+
]
|
|
166
|
+
if env:
|
|
167
|
+
lines.append("")
|
|
168
|
+
lines.append(f"[mcp_servers.{name}.env]")
|
|
169
|
+
for key, value in env.items():
|
|
170
|
+
lines.append(f'{key} = "{value}"')
|
|
171
|
+
blocks.append("\n".join(lines))
|
|
172
|
+
return "\n\n".join(blocks) + "\n"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def generate_full_hermes(env: dict[str, str] | None = None) -> str:
|
|
176
|
+
"""Full Hermes YAML config with all 15 individual SIN-Code tools (BR-3)."""
|
|
177
|
+
env = DEFAULT_ENV if env is None else env
|
|
178
|
+
servers: dict[str, Any] = {}
|
|
179
|
+
for name, cmd in FULL_TOOLS:
|
|
180
|
+
server = {"command": cmd[0], "args": list(cmd[1:])}
|
|
181
|
+
if env:
|
|
182
|
+
server["env"] = env
|
|
183
|
+
servers[name] = server
|
|
184
|
+
config = {"mcp_servers": servers}
|
|
185
|
+
try:
|
|
186
|
+
import yaml
|
|
187
|
+
|
|
188
|
+
return yaml.safe_dump(config, sort_keys=False, default_flow_style=False)
|
|
189
|
+
except ImportError:
|
|
190
|
+
out = ["mcp_servers:"]
|
|
191
|
+
for name, cmd in FULL_TOOLS:
|
|
192
|
+
out.append(f" {name}:")
|
|
193
|
+
out.append(f" command: {cmd[0]}")
|
|
194
|
+
out.append(" args:")
|
|
195
|
+
out += [f" - {a}" for a in cmd[1:]]
|
|
196
|
+
if env:
|
|
197
|
+
out.append(" env:")
|
|
198
|
+
out += [f" {k}: {v}" for k, v in env.items()]
|
|
199
|
+
return "\n".join(out) + "\n"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def generate_full(client: str, env: dict[str, str] | None = None) -> str:
|
|
203
|
+
"""Dispatch full config nach Client-Name."""
|
|
204
|
+
client = client.lower()
|
|
205
|
+
if client == "opencode":
|
|
206
|
+
return generate_full_opencode(env)
|
|
207
|
+
if client == "codex":
|
|
208
|
+
return generate_full_codex(env)
|
|
209
|
+
if client == "hermes":
|
|
210
|
+
return generate_full_hermes(env)
|
|
211
|
+
raise ValueError(f"Unknown client '{client}'. Supported: {', '.join(SUPPORTED_CLIENTS)}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def generate(client: str, env: dict[str, str] | None = None) -> str:
|
|
215
|
+
"""Dispatch nach Client-Name."""
|
|
216
|
+
client = client.lower()
|
|
217
|
+
if client == "opencode":
|
|
218
|
+
return generate_opencode(env)
|
|
219
|
+
if client == "codex":
|
|
220
|
+
return generate_codex(env)
|
|
221
|
+
if client == "hermes":
|
|
222
|
+
return generate_hermes(env)
|
|
223
|
+
raise ValueError(f"Unknown client '{client}'. Supported: {', '.join(SUPPORTED_CLIENTS)}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ── Default-Zielpfade pro Client ────────────────────────────────────────────
|
|
227
|
+
def default_path(client: str) -> Path:
|
|
228
|
+
"""Konventioneller Konfigurationspfad des jeweiligen Clients."""
|
|
229
|
+
client = client.lower()
|
|
230
|
+
if client == "opencode":
|
|
231
|
+
return Path("opencode.json")
|
|
232
|
+
if client == "codex":
|
|
233
|
+
return Path.home() / ".codex" / "config.toml"
|
|
234
|
+
if client == "hermes":
|
|
235
|
+
return Path.home() / ".hermes" / "config.yaml"
|
|
236
|
+
raise ValueError(f"Unknown client '{client}'")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ── Idempotentes Mergen in bestehende Dateien (--write) ─────────────────────
|
|
240
|
+
def merge_into_file(client: str, path: Path, env: dict[str, str] | None = None) -> str:
|
|
241
|
+
"""Fuegt den sin-Server in eine bestehende Config-Datei ein bzw. legt sie an.
|
|
242
|
+
|
|
243
|
+
Gibt eine kurze Statusmeldung zurueck. Bestehende fremde Eintraege bleiben
|
|
244
|
+
erhalten; ein vorhandener ``sin``-Eintrag wird ersetzt.
|
|
245
|
+
"""
|
|
246
|
+
client = client.lower()
|
|
247
|
+
if client == "opencode":
|
|
248
|
+
return _merge_json(path, env)
|
|
249
|
+
if client == "hermes":
|
|
250
|
+
return _merge_yaml(path, env)
|
|
251
|
+
if client == "codex":
|
|
252
|
+
return _merge_codex_toml(path, env)
|
|
253
|
+
raise ValueError(f"Unknown client '{client}'")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def merge_full_into_file(client: str, path: Path, env: dict[str, str] | None = None) -> str:
|
|
257
|
+
"""Fuegt alle 15 SIN-Code MCP-Server in eine bestehende Config-Datei ein (BR-3).
|
|
258
|
+
|
|
259
|
+
Gibt eine kurze Statusmeldung zurueck. Bestehende fremde Eintraege bleiben
|
|
260
|
+
erhalten; vorhandene ``sin-*``-Eintraege werden ersetzt.
|
|
261
|
+
"""
|
|
262
|
+
client = client.lower()
|
|
263
|
+
if client == "opencode":
|
|
264
|
+
return _merge_json_full(path, env)
|
|
265
|
+
if client == "hermes":
|
|
266
|
+
return _merge_yaml_full(path, env)
|
|
267
|
+
if client == "codex":
|
|
268
|
+
return _merge_codex_toml_full(path, env)
|
|
269
|
+
raise ValueError(f"Unknown client '{client}'")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _merge_json(path: Path, env: dict[str, str] | None) -> str:
|
|
273
|
+
data: dict[str, Any] = {}
|
|
274
|
+
if path.exists() and path.read_text().strip():
|
|
275
|
+
try:
|
|
276
|
+
data = json.loads(path.read_text())
|
|
277
|
+
except json.JSONDecodeError as exc:
|
|
278
|
+
raise ValueError(f"Existing {path} is not valid JSON: {exc}") from exc
|
|
279
|
+
mcp = data.setdefault("mcp", {})
|
|
280
|
+
mcp[SERVER_NAME] = {
|
|
281
|
+
"type": "local",
|
|
282
|
+
"command": [COMMAND, *ARGS],
|
|
283
|
+
"enabled": True,
|
|
284
|
+
"environment": DEFAULT_ENV if env is None else env,
|
|
285
|
+
}
|
|
286
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
287
|
+
path.write_text(json.dumps(data, indent=2) + "\n")
|
|
288
|
+
return f"Merged 'sin' MCP server into {path}"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _merge_yaml(path: Path, env: dict[str, str] | None) -> str:
|
|
292
|
+
try:
|
|
293
|
+
import yaml
|
|
294
|
+
except ImportError as exc: # pragma: no cover - pyyaml ist Pflicht-Dep
|
|
295
|
+
raise ValueError("PyYAML required to merge YAML config") from exc
|
|
296
|
+
|
|
297
|
+
data: dict[str, Any] = {}
|
|
298
|
+
if path.exists() and path.read_text().strip():
|
|
299
|
+
loaded = yaml.safe_load(path.read_text())
|
|
300
|
+
if isinstance(loaded, dict):
|
|
301
|
+
data = loaded
|
|
302
|
+
servers = data.setdefault("mcp_servers", {})
|
|
303
|
+
server: dict[str, Any] = {"command": COMMAND, "args": list(ARGS)}
|
|
304
|
+
if env:
|
|
305
|
+
server["env"] = env
|
|
306
|
+
servers[SERVER_NAME] = server
|
|
307
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
308
|
+
path.write_text(yaml.safe_dump(data, sort_keys=False, default_flow_style=False))
|
|
309
|
+
return f"Merged 'sin' MCP server into {path}"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _merge_codex_toml(path: Path, env: dict[str, str] | None) -> str:
|
|
313
|
+
"""Merge fuer TOML ohne externe Writer-Dependency.
|
|
314
|
+
|
|
315
|
+
Strategie: vorhandenen ``[mcp_servers.sin]``-Block (inkl. Sub-Table
|
|
316
|
+
``.env``) entfernen und den frisch generierten Block anhaengen. Andere
|
|
317
|
+
Tabellen bleiben unangetastet.
|
|
318
|
+
"""
|
|
319
|
+
existing = ""
|
|
320
|
+
if path.exists():
|
|
321
|
+
existing = path.read_text()
|
|
322
|
+
cleaned = _strip_toml_table(existing, f"mcp_servers.{SERVER_NAME}")
|
|
323
|
+
block = generate_codex(env)
|
|
324
|
+
sep = (
|
|
325
|
+
""
|
|
326
|
+
if cleaned == "" or cleaned.endswith("\n\n")
|
|
327
|
+
else ("\n" if cleaned.endswith("\n") else "\n\n")
|
|
328
|
+
)
|
|
329
|
+
new_content = cleaned + sep + block
|
|
330
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
path.write_text(new_content)
|
|
332
|
+
return f"Merged 'sin' MCP server into {path}"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ── Full-config merge helpers (BR-3) ───────────────────────────────────────
|
|
336
|
+
def _merge_json_full(path: Path, env: dict[str, str] | None) -> str:
|
|
337
|
+
data: dict[str, Any] = {}
|
|
338
|
+
if path.exists() and path.read_text().strip():
|
|
339
|
+
try:
|
|
340
|
+
data = json.loads(path.read_text())
|
|
341
|
+
except json.JSONDecodeError as exc:
|
|
342
|
+
raise ValueError(f"Existing {path} is not valid JSON: {exc}") from exc
|
|
343
|
+
mcp = data.setdefault("mcp", {})
|
|
344
|
+
for name, cmd in FULL_TOOLS:
|
|
345
|
+
mcp[name] = {
|
|
346
|
+
"type": "local",
|
|
347
|
+
"command": list(cmd),
|
|
348
|
+
"enabled": True,
|
|
349
|
+
"environment": DEFAULT_ENV if env is None else env,
|
|
350
|
+
}
|
|
351
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
352
|
+
path.write_text(json.dumps(data, indent=2) + "\n")
|
|
353
|
+
return f"Merged {len(FULL_TOOLS)} MCP servers into {path}"
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _merge_yaml_full(path: Path, env: dict[str, str] | None) -> str:
|
|
357
|
+
try:
|
|
358
|
+
import yaml
|
|
359
|
+
except ImportError as exc: # pragma: no cover - pyyaml ist Pflicht-Dep
|
|
360
|
+
raise ValueError("PyYAML required to merge YAML config") from exc
|
|
361
|
+
|
|
362
|
+
data: dict[str, Any] = {}
|
|
363
|
+
if path.exists() and path.read_text().strip():
|
|
364
|
+
loaded = yaml.safe_load(path.read_text())
|
|
365
|
+
if isinstance(loaded, dict):
|
|
366
|
+
data = loaded
|
|
367
|
+
servers = data.setdefault("mcp_servers", {})
|
|
368
|
+
for name, cmd in FULL_TOOLS:
|
|
369
|
+
server: dict[str, Any] = {"command": cmd[0], "args": list(cmd[1:])}
|
|
370
|
+
if env:
|
|
371
|
+
server["env"] = env
|
|
372
|
+
servers[name] = server
|
|
373
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
374
|
+
path.write_text(yaml.safe_dump(data, sort_keys=False, default_flow_style=False))
|
|
375
|
+
return f"Merged {len(FULL_TOOLS)} MCP servers into {path}"
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _merge_codex_toml_full(path: Path, env: dict[str, str] | None) -> str:
|
|
379
|
+
"""Merge fuer TOML mit allen 15 SIN-Code Tools.
|
|
380
|
+
|
|
381
|
+
Strategie: vorhandene ``[mcp_servers.sin]`` und ``[mcp_servers.sin-*]``
|
|
382
|
+
Bloecke entfernen und frisch generierte Bloecke anhaengen.
|
|
383
|
+
"""
|
|
384
|
+
existing = ""
|
|
385
|
+
if path.exists():
|
|
386
|
+
existing = path.read_text()
|
|
387
|
+
# Remove old single server and all full-tool tables
|
|
388
|
+
existing = _strip_toml_table(existing, f"mcp_servers.{SERVER_NAME}")
|
|
389
|
+
for name, _ in FULL_TOOLS:
|
|
390
|
+
existing = _strip_toml_table(existing, f"mcp_servers.{name}")
|
|
391
|
+
blocks: list[str] = []
|
|
392
|
+
for name, cmd in FULL_TOOLS:
|
|
393
|
+
lines = [
|
|
394
|
+
f"[mcp_servers.{name}]",
|
|
395
|
+
f'command = "{cmd[0]}"',
|
|
396
|
+
f"args = {_toml_array(cmd[1:])}",
|
|
397
|
+
]
|
|
398
|
+
if env:
|
|
399
|
+
lines.append("")
|
|
400
|
+
lines.append(f"[mcp_servers.{name}.env]")
|
|
401
|
+
for key, value in env.items():
|
|
402
|
+
lines.append(f'{key} = "{value}"')
|
|
403
|
+
blocks.append("\n".join(lines))
|
|
404
|
+
block = "\n\n".join(blocks) + "\n"
|
|
405
|
+
sep = (
|
|
406
|
+
""
|
|
407
|
+
if existing == "" or existing.endswith("\n\n")
|
|
408
|
+
else ("\n" if existing.endswith("\n") else "\n\n")
|
|
409
|
+
)
|
|
410
|
+
new_content = existing + sep + block
|
|
411
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
412
|
+
path.write_text(new_content)
|
|
413
|
+
return f"Merged {len(FULL_TOOLS)} MCP servers into {path}"
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ── Hilfsfunktionen ────────────────────────────────────────────────────────
|
|
417
|
+
def _toml_array(items: list[str]) -> str:
|
|
418
|
+
inner = ", ".join(f'"{i}"' for i in items)
|
|
419
|
+
return f"[{inner}]"
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _strip_toml_table(content: str, table_prefix: str) -> str:
|
|
423
|
+
"""Entfernt ``[table_prefix]`` und alle Sub-Tables ``[table_prefix.*]``.
|
|
424
|
+
|
|
425
|
+
Zeilenbasiert und bewusst simpel: ausreichend fuer das von uns erzeugte
|
|
426
|
+
Format und fremde, klar getrennte Tabellen. We do NOT use a real TOML
|
|
427
|
+
parser because:
|
|
428
|
+
|
|
429
|
+
- The merge runs without a toml extra-dep so the bundle stays slim.
|
|
430
|
+
- We only need to recognise lines we wrote ourselves (``[mcp_servers.X]``
|
|
431
|
+
+ ``[mcp_servers.X.env]``). Foreign tables stay untouched as long as
|
|
432
|
+
they don't share our prefix.
|
|
433
|
+
"""
|
|
434
|
+
if not content:
|
|
435
|
+
return ""
|
|
436
|
+
lines = content.splitlines()
|
|
437
|
+
out: list[str] = []
|
|
438
|
+
skip = False
|
|
439
|
+
for line in lines:
|
|
440
|
+
stripped = line.strip()
|
|
441
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
442
|
+
name = stripped[1:-1].strip()
|
|
443
|
+
# Header-Form [[name]] reduziert sich nach obigem Slicing auf [name]
|
|
444
|
+
# (a real TOML parser would distinguish array-of-tables; we don't
|
|
445
|
+
# need that for mcp_servers entries which are all single tables).
|
|
446
|
+
name = name.lstrip("[").rstrip("]").strip()
|
|
447
|
+
if name == table_prefix or name.startswith(table_prefix + "."):
|
|
448
|
+
skip = True
|
|
449
|
+
continue
|
|
450
|
+
skip = False
|
|
451
|
+
if not skip:
|
|
452
|
+
out.append(line)
|
|
453
|
+
# fuehrende/abschliessende Leerzeilen normalisieren
|
|
454
|
+
text = "\n".join(out).strip("\n")
|
|
455
|
+
return text + "\n" if text else ""
|