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.
Files changed (41) hide show
  1. sin_code_bundle/__init__.py +6 -0
  2. sin_code_bundle/agents_md.py +245 -0
  3. sin_code_bundle/ast_edit.py +323 -0
  4. sin_code_bundle/bench.py +506 -0
  5. sin_code_bundle/budget.py +51 -0
  6. sin_code_bundle/cache.py +131 -0
  7. sin_code_bundle/checkpoint.py +230 -0
  8. sin_code_bundle/cli.py +1943 -0
  9. sin_code_bundle/codocs.py +328 -0
  10. sin_code_bundle/dap_bridge.py +135 -0
  11. sin_code_bundle/data/codocs/SKILL.md +280 -0
  12. sin_code_bundle/gitnexus.py +368 -0
  13. sin_code_bundle/hashline.py +216 -0
  14. sin_code_bundle/hooks.py +249 -0
  15. sin_code_bundle/immortal_commit.py +288 -0
  16. sin_code_bundle/interceptor.py +119 -0
  17. sin_code_bundle/lsp_backend.py +303 -0
  18. sin_code_bundle/lsp_bootstrap.py +85 -0
  19. sin_code_bundle/markitdown.py +254 -0
  20. sin_code_bundle/mcp_config.py +455 -0
  21. sin_code_bundle/mcp_server.py +963 -0
  22. sin_code_bundle/memory.py +208 -0
  23. sin_code_bundle/merge_safety.py +313 -0
  24. sin_code_bundle/orchestration_worktrees.py +102 -0
  25. sin_code_bundle/policy.py +224 -0
  26. sin_code_bundle/preflight.py +152 -0
  27. sin_code_bundle/programming_workflow.py +541 -0
  28. sin_code_bundle/rtk.py +154 -0
  29. sin_code_bundle/safety.py +52 -0
  30. sin_code_bundle/session_warmup.py +247 -0
  31. sin_code_bundle/skills.py +188 -0
  32. sin_code_bundle/symbol_resolve.py +166 -0
  33. sin_code_bundle/tools/__init__.py +4 -0
  34. sin_code_bundle/tools/pypi_setup.py +289 -0
  35. sin_code_bundle/vfs.py +264 -0
  36. sin_code_bundle-0.9.2.dist-info/METADATA +470 -0
  37. sin_code_bundle-0.9.2.dist-info/RECORD +41 -0
  38. sin_code_bundle-0.9.2.dist-info/WHEEL +5 -0
  39. sin_code_bundle-0.9.2.dist-info/entry_points.txt +4 -0
  40. sin_code_bundle-0.9.2.dist-info/licenses/LICENSE +21 -0
  41. 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 ""