devtorch-core 3.0.1__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 (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,472 @@
1
+ """Install DevTorch MCP client configuration for various editors.
2
+
3
+ This module is shared by the ``devtorch cloud client-config`` CLI command and
4
+ the legacy ``install-client-config.sh`` wrapper. It copies the right IDE
5
+ template, substitutes org/repo/api-key/domain, and updates ``AGENTS.md`` with
6
+ the required DevTorch governance instructions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import re
14
+ from pathlib import Path
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Constants
18
+ # ---------------------------------------------------------------------------
19
+
20
+ SUPPORTED_IDES = ["opencode", "claude", "cursor", "vscode", "stdio"]
21
+
22
+ PLACEHOLDER_DOMAIN = "devtorch-mcp.YOUR-DOMAIN.com"
23
+
24
+ # Files shipped with this package under devtorch_core/cloud/
25
+ _PACKAGE_DIR = Path(__file__).resolve().parent
26
+ CLIENT_CONFIG_DIR = _PACKAGE_DIR / "client_configs"
27
+ BRIDGE_SCRIPT = _PACKAGE_DIR / "devtorch-mcp-bridge.js"
28
+
29
+ AGENTS_MD_BLOCK = """## DevTorch — Reasoning Capture
30
+
31
+ This project uses DevTorch for AI governance. These steps are mandatory for all agent actions.
32
+
33
+ ### At the start of every task
34
+
35
+ Call `devtorch_context` (MCP tool) before reading files or writing code:
36
+ - Tool: `devtorch_context`
37
+ - Args: `{ "max_tokens": 2000 }`
38
+
39
+ Use the returned context to understand prior decisions before proceeding.
40
+
41
+ ### Before writing or editing any code
42
+
43
+ Call `devtorch_commit` before the first file write:
44
+ - Tool: `devtorch_commit`
45
+ - Args: `{ "message": "<what you decided and WHY — include alternatives considered>", "branch": "<optional branch name>" }`
46
+
47
+ The message must explain the *why*, not just the *what*.
48
+ "Added login endpoint" is not acceptable.
49
+ "Chose JWT over sessions because the service is stateless" is.
50
+
51
+ ### When touching sensitive areas
52
+
53
+ Call `devtorch_sensitivity_add` for auth, payments, database schema, secrets, PII, external APIs:
54
+ - Tool: `devtorch_sensitivity_add`
55
+ - Args: `{ "concept": "<auth|schema|payments|secrets|pii|api|security|config>", "signal": "<what this decision depends on / counterfactual text>", "confidence": 0.0–1.0, "disclosure": "PUBLIC|PROTECTED|PRIVATE" }`
56
+
57
+ | Level | Use when |
58
+ |---|---|
59
+ | PUBLIC | General design decisions |
60
+ | PROTECTED | Internal implementation, business logic |
61
+ | PRIVATE | Credentials, PII, secrets |
62
+
63
+ ### If MCP tools fail
64
+
65
+ If `devtorch_commit` or `devtorch_sensitivity_add` returns an MCP error (timeout, connection failure, or server error), the agent must fall back to the local CLI so the `.GCC/` audit trail is never lost:
66
+
67
+ - `devtorch commit -m "<reasoning message>"`
68
+ - `devtorch sensitivity add --source mcp-fallback --concept <concept> --confidence <confidence> --counterfactuals "<signal>" --message "<signal>" --disclosure <PUBLIC|PROTECTED|PRIVATE>`
69
+
70
+ For IDEs using the DevTorch MCP bridge, this fallback is automatic.
71
+ """
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Public API
76
+ # ---------------------------------------------------------------------------
77
+
78
+
79
+ def install_client_config(
80
+ ide: str,
81
+ org: str,
82
+ repo: str,
83
+ api_key: str,
84
+ domain: str | None = None,
85
+ project_root: str | Path | None = None,
86
+ use_env_key: bool = False,
87
+ ) -> dict[str, str | Path]:
88
+ """Install the DevTorch MCP client config and AGENTS.md for an IDE.
89
+
90
+ Args:
91
+ ide: Target IDE. One of ``opencode``, ``claude``, ``cursor``, ``vscode``,
92
+ ``stdio``.
93
+ org: DevTorch org ID.
94
+ repo: DevTorch repo ID.
95
+ api_key: API key created via ``devtorch cloud key create``.
96
+ domain: Cloudflare Worker domain. Defaults to ``DEVTORCH_MCP_DOMAIN``
97
+ env var, or ``devtorch-mcp.YOUR-DOMAIN.com`` as a placeholder.
98
+ project_root: Directory to install into. Defaults to the current working
99
+ directory.
100
+ use_env_key: For the ``claude``/``stdio`` targets, write
101
+ ``${DEVTORCH_API_KEY}`` instead of the literal key — Claude Code
102
+ expands env vars in ``.mcp.json``, so the secret never touches a
103
+ committable file. The user must export ``DEVTORCH_API_KEY``.
104
+
105
+ Returns:
106
+ A dict with ``url``, ``installed_file``, and ``domain``. May include
107
+ ``warning`` (placeholder domain) and ``secret_warning`` (literal key
108
+ written inside a git repo with no matching gitignore entry).
109
+ """
110
+ if ide not in SUPPORTED_IDES:
111
+ raise ValueError(
112
+ f"Unsupported IDE: {ide!r}. Supported: {', '.join(SUPPORTED_IDES)}"
113
+ )
114
+
115
+ domain = _resolve_domain(domain)
116
+ root = Path(project_root or ".").resolve()
117
+ url = f"https://{domain}/{org}/{repo}/sse"
118
+
119
+ if ide == "opencode":
120
+ target = root / "opencode.json"
121
+ _render_template(
122
+ CLIENT_CONFIG_DIR / ".opencode.json",
123
+ target,
124
+ domain=domain,
125
+ org=org,
126
+ repo=repo,
127
+ api_key=api_key,
128
+ project_root=root,
129
+ bridge_script=str(BRIDGE_SCRIPT),
130
+ )
131
+ elif ide == "claude":
132
+ target = _install_claude_mcp_json(
133
+ root, domain=domain, org=org, repo=repo, api_key=api_key,
134
+ use_env_key=use_env_key,
135
+ )
136
+ elif ide == "cursor":
137
+ target_dir = root / ".cursor"
138
+ target_dir.mkdir(parents=True, exist_ok=True)
139
+ target = target_dir / "mcp.json"
140
+ _render_template(
141
+ CLIENT_CONFIG_DIR / ".cursor-mcp.json",
142
+ target,
143
+ domain=domain,
144
+ org=org,
145
+ repo=repo,
146
+ api_key=api_key,
147
+ project_root=root,
148
+ )
149
+ elif ide == "vscode":
150
+ target_dir = root / ".vscode"
151
+ target_dir.mkdir(parents=True, exist_ok=True)
152
+ target = target_dir / "mcp.json"
153
+ _render_template(
154
+ CLIENT_CONFIG_DIR / ".vscode-mcp.json",
155
+ target,
156
+ domain=domain,
157
+ org=org,
158
+ repo=repo,
159
+ api_key=api_key,
160
+ project_root=root,
161
+ bridge_script=str(BRIDGE_SCRIPT),
162
+ )
163
+ elif ide == "stdio":
164
+ # stdio target is the same as claude for the bridge-based config.
165
+ target = _install_claude_mcp_json(
166
+ root, domain=domain, org=org, repo=repo, api_key=api_key,
167
+ use_env_key=use_env_key,
168
+ )
169
+ else:
170
+ # This branch is unreachable because of the earlier validation,
171
+ # but it keeps type checkers and linters happy.
172
+ raise ValueError(f"Unsupported IDE: {ide!r}")
173
+
174
+ _install_agents_md(root)
175
+
176
+ result: dict[str, str | Path] = {"url": url, "installed_file": target, "domain": domain}
177
+ if domain == PLACEHOLDER_DOMAIN:
178
+ result["warning"] = (
179
+ f"MCP domain is the placeholder '{PLACEHOLDER_DOMAIN}' — the server will "
180
+ "be listed but can never connect. Re-run with --domain <your-worker-domain> "
181
+ "or set DEVTORCH_MCP_DOMAIN."
182
+ )
183
+ if use_env_key and ide not in ("claude", "stdio"):
184
+ result["warning_use_env_key"] = (
185
+ f"--use-env-key only applies to the claude/stdio targets; the {ide} "
186
+ "config still contains the literal API key."
187
+ )
188
+ secret_warning = _committable_secret_warning(
189
+ root,
190
+ Path(target),
191
+ api_key,
192
+ suggest_env_key=ide in ("claude", "stdio") and not use_env_key,
193
+ )
194
+ if secret_warning:
195
+ result["secret_warning"] = secret_warning
196
+ return result
197
+
198
+
199
+ def _find_git_root(path: Path) -> Path | None:
200
+ """Return the nearest ancestor (including *path*) containing .git."""
201
+ for candidate in (path, *path.parents):
202
+ if (candidate / ".git").exists():
203
+ return candidate
204
+ return None
205
+
206
+
207
+ def _committable_secret_warning(
208
+ root: Path, target: Path, api_key: str, *, suggest_env_key: bool = True
209
+ ) -> str | None:
210
+ """
211
+ Return a warning when a literal API key was written into a file that git
212
+ could commit. This exact gap is how a live key was committed once.
213
+
214
+ ``git check-ignore`` is the authority when git is available (it evaluates
215
+ globs, anchoring, and negation correctly); a simple pattern match against
216
+ the top-level .gitignore is the fallback.
217
+ """
218
+ git_root = _find_git_root(root)
219
+ if git_root is None:
220
+ return None
221
+ try:
222
+ if api_key not in target.read_text(encoding="utf-8"):
223
+ return None
224
+ except OSError:
225
+ return None
226
+ try:
227
+ rel = target.relative_to(git_root).as_posix()
228
+ except ValueError:
229
+ rel = target.name
230
+ if _is_git_ignored(git_root, rel):
231
+ return None
232
+ hint = " (or re-run with --use-env-key)" if suggest_env_key else ""
233
+ return (
234
+ f"{rel} now contains your API key and is inside a git repository with no "
235
+ f"matching gitignore rule — add '{rel}' to .gitignore{hint} so the key "
236
+ "cannot be committed."
237
+ )
238
+
239
+
240
+ def _is_git_ignored(git_root: Path, rel: str) -> bool:
241
+ import fnmatch
242
+ import subprocess
243
+
244
+ try:
245
+ proc = subprocess.run(
246
+ ["git", "-C", str(git_root), "check-ignore", "-q", rel],
247
+ capture_output=True,
248
+ timeout=5,
249
+ )
250
+ # 0 = ignored, 1 = not ignored; anything else means git could not
251
+ # answer (not a real repo, git missing) — fall through to heuristic.
252
+ if proc.returncode in (0, 1):
253
+ return proc.returncode == 0
254
+ except (OSError, subprocess.SubprocessError):
255
+ pass
256
+
257
+ try:
258
+ lines = (git_root / ".gitignore").read_text(encoding="utf-8").splitlines()
259
+ except (FileNotFoundError, OSError):
260
+ return False
261
+ patterns = [
262
+ line.strip().lstrip("/").rstrip("/")
263
+ for line in lines
264
+ if line.strip() and not line.strip().startswith(("#", "!"))
265
+ ]
266
+ parts = rel.split("/")
267
+ candidates = {rel, parts[-1], *("/".join(parts[: i + 1]) for i in range(len(parts)))}
268
+ return any(fnmatch.fnmatch(c, pat) for c in candidates for pat in patterns)
269
+
270
+
271
+ def _resolve_domain(domain: str | None) -> str:
272
+ if domain:
273
+ return _normalize_domain(domain)
274
+ env_domain = os.environ.get("DEVTORCH_MCP_DOMAIN", "").strip()
275
+ if env_domain:
276
+ return _normalize_domain(env_domain)
277
+ return PLACEHOLDER_DOMAIN
278
+
279
+
280
+ def _normalize_domain(domain: str) -> str:
281
+ """
282
+ Accept a domain given with its scheme or a trailing slash.
283
+
284
+ The generated URL prepends ``https://`` to the domain, so a value like
285
+ ``https://devtorch-mcp.example.com`` would otherwise render as
286
+ ``https://https://...`` — which MCP bridges fail on with an opaque
287
+ "Connection closed" error.
288
+ """
289
+ domain = domain.strip()
290
+ for scheme in ("https://", "http://"):
291
+ if domain.lower().startswith(scheme):
292
+ domain = domain[len(scheme):]
293
+ break
294
+ return domain.rstrip("/")
295
+
296
+
297
+ def _install_claude_mcp_json(
298
+ root: Path,
299
+ *,
300
+ domain: str,
301
+ org: str,
302
+ repo: str,
303
+ api_key: str,
304
+ use_env_key: bool = False,
305
+ ) -> Path:
306
+ """
307
+ Merge the DevTorch bridge server into <root>/.mcp.json.
308
+
309
+ Claude Code reads project-scope MCP servers only from .mcp.json at the
310
+ project root — a mcpServers key in .claude/settings.json is ignored.
311
+ Preserves other servers already in .mcp.json, overwrites the devtorch
312
+ entry (client-config is an explicit reconfiguration), and removes a
313
+ devtorch entry stranded in .claude/settings.json by older versions.
314
+ """
315
+ rendered = _render_text(
316
+ CLIENT_CONFIG_DIR / ".claude-stdio.json",
317
+ domain=domain,
318
+ org=org,
319
+ repo=repo,
320
+ api_key=api_key,
321
+ project_root=root,
322
+ bridge_script=str(BRIDGE_SCRIPT),
323
+ )
324
+ entry = json.loads(rendered)["mcpServers"]["devtorch"]
325
+ if use_env_key:
326
+ # Claude Code expands ${VAR} in .mcp.json env values, so the secret
327
+ # stays out of the file entirely; the user must export it.
328
+ entry["env"]["DEVTORCH_API_KEY"] = "${DEVTORCH_API_KEY}"
329
+
330
+ from devtorch_core.hooks.installer import (
331
+ _pop_legacy_settings_mcp_entry,
332
+ _read_json_dict_or_backup,
333
+ )
334
+
335
+ target = root / ".mcp.json"
336
+ existing: dict = {}
337
+ if target.exists():
338
+ existing = _read_json_dict_or_backup(target)
339
+ existing.setdefault("mcpServers", {})["devtorch"] = entry
340
+ target.write_text(json.dumps(existing, indent=2), encoding="utf-8")
341
+
342
+ _pop_legacy_settings_mcp_entry(root)
343
+ return target
344
+
345
+
346
+ def _render_text(
347
+ template: Path,
348
+ *,
349
+ domain: str,
350
+ org: str,
351
+ repo: str,
352
+ api_key: str,
353
+ project_root: str | Path,
354
+ bridge_script: str | None = None,
355
+ ) -> str:
356
+ text = template.read_text(encoding="utf-8")
357
+ text = text.replace("devtorch-mcp.YOUR-DOMAIN.com", domain)
358
+ text = text.replace("YOUR_ORG", org)
359
+ text = text.replace("YOUR_REPO", repo)
360
+ text = text.replace("YOUR_API_KEY", api_key)
361
+ text = text.replace(
362
+ '"__PROJECT_ROOT__"',
363
+ json.dumps(str(project_root).replace("\\", "/")),
364
+ )
365
+ if bridge_script is not None:
366
+ text = text.replace(
367
+ "/ABSOLUTE/PATH/TO/deploy/cloudflare-worker/devtorch-mcp-bridge.js",
368
+ bridge_script,
369
+ )
370
+ return text
371
+
372
+
373
+ def _render_template(
374
+ template: Path,
375
+ target: Path,
376
+ *,
377
+ domain: str,
378
+ org: str,
379
+ repo: str,
380
+ api_key: str,
381
+ project_root: str | Path,
382
+ bridge_script: str | None = None,
383
+ ) -> None:
384
+ target.write_text(
385
+ _render_text(
386
+ template,
387
+ domain=domain,
388
+ org=org,
389
+ repo=repo,
390
+ api_key=api_key,
391
+ project_root=project_root,
392
+ bridge_script=bridge_script,
393
+ ),
394
+ encoding="utf-8",
395
+ )
396
+
397
+
398
+ # A DevTorch governance block at any heading level counts as "already
399
+ # installed" — a project may promote the block to an H1 (as the DevTorch repo
400
+ # itself does), and appending our H2 copy next to it would duplicate it.
401
+ _DEVTORCH_HEADING_RE = re.compile(r"^#{1,6}\s+DevTorch\b", re.MULTILINE)
402
+
403
+
404
+ def _install_agents_md(root: Path) -> tuple[str, Path]:
405
+ path = root / "AGENTS.md"
406
+
407
+ try:
408
+ content = path.read_text(encoding="utf-8")
409
+ except FileNotFoundError:
410
+ content = ""
411
+
412
+ if re.search(r"^## DevTorch", content, re.MULTILINE):
413
+ # Replace the H2 block we authored, preserving everything before and
414
+ # after. The block ends at the next H1/H2 heading or EOF — H3+
415
+ # headings are part of the block itself, but a coexisting customized
416
+ # H1 block must survive the replacement.
417
+ pattern = re.compile(
418
+ r"^## DevTorch.*?(?=^#{1,2} |\Z)", re.DOTALL | re.MULTILINE
419
+ )
420
+ new_content = pattern.sub(AGENTS_MD_BLOCK.rstrip() + "\n", content)
421
+ action = "Updated"
422
+ elif _DEVTORCH_HEADING_RE.search(content):
423
+ # Governance block exists at a heading level we didn't author (e.g.
424
+ # H1). Leave the customized block untouched.
425
+ return "Kept", path
426
+ else:
427
+ if content and not content.endswith("\n"):
428
+ content += "\n"
429
+ if content:
430
+ content += "\n"
431
+ new_content = content + AGENTS_MD_BLOCK.rstrip() + "\n"
432
+ action = "Installed"
433
+
434
+ path.write_text(new_content, encoding="utf-8")
435
+ return action, path
436
+
437
+
438
+ # ---------------------------------------------------------------------------
439
+ # CLI entry point (used by the legacy bash wrapper)
440
+ # ---------------------------------------------------------------------------
441
+
442
+
443
+ def main(argv: list[str] | None = None) -> int:
444
+ """Legacy CLI entry point matching ``install-client-config.sh <ide> <org> <repo> <api-key>``."""
445
+ import sys
446
+
447
+ args = argv if argv is not None else sys.argv[1:]
448
+ if len(args) < 4:
449
+ print("Usage: install-client-config.sh <ide> <org> <repo> <api-key>")
450
+ print(" ide: opencode | claude | cursor | vscode | stdio")
451
+ return 1
452
+
453
+ ide, org, repo, api_key = args[:4]
454
+ try:
455
+ result = install_client_config(ide, org, repo, api_key)
456
+ except Exception as e:
457
+ print(f"error: {e}", file=sys.stderr)
458
+ return 1
459
+
460
+ print(f"Installed {result['installed_file']}")
461
+ print(f"DevTorch MCP URL: {result['url']}")
462
+ if "warning" in result:
463
+ print(f"WARNING: {result['warning']}", file=sys.stderr)
464
+ if "warning_use_env_key" in result:
465
+ print(f"WARNING: {result['warning_use_env_key']}", file=sys.stderr)
466
+ if "secret_warning" in result:
467
+ print(f"WARNING: {result['secret_warning']}", file=sys.stderr)
468
+ return 0
469
+
470
+
471
+ if __name__ == "__main__":
472
+ raise SystemExit(main())
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcp": {
3
+ "devtorch": {
4
+ "command": "echo",
5
+ "args": ["OpenCode uses the SSE config in opencode.json for DevTorch"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "devtorch": {
4
+ "command": "node",
5
+ "args": ["/ABSOLUTE/PATH/TO/deploy/cloudflare-worker/devtorch-mcp-bridge.js"],
6
+ "env": {
7
+ "DEVTORCH_MCP_URL": "https://devtorch-mcp.YOUR-DOMAIN.com/YOUR_ORG/YOUR_REPO/sse",
8
+ "DEVTORCH_API_KEY": "YOUR_API_KEY",
9
+ "DEVTORCH_GCC_PATH": "__PROJECT_ROOT__"
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "devtorch": {
4
+ "url": "https://devtorch-mcp.YOUR-DOMAIN.com/YOUR_ORG/YOUR_REPO/sse",
5
+ "headers": {
6
+ "X-DevTorch-Key": "YOUR_API_KEY"
7
+ },
8
+ "env": {
9
+ "DEVTORCH_GCC_PATH": "__PROJECT_ROOT__"
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "devtorch": {
4
+ "command": "node",
5
+ "args": ["/ABSOLUTE/PATH/TO/deploy/cloudflare-worker/devtorch-mcp-bridge.js"],
6
+ "env": {
7
+ "DEVTORCH_MCP_URL": "https://devtorch-mcp.YOUR-DOMAIN.com/YOUR_ORG/YOUR_REPO/sse",
8
+ "DEVTORCH_API_KEY": "YOUR_API_KEY",
9
+ "DEVTORCH_GCC_PATH": "__PROJECT_ROOT__"
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "mcp": {
4
+ "devtorch": {
5
+ "type": "local",
6
+ "enabled": true,
7
+ "command": ["node", "/ABSOLUTE/PATH/TO/deploy/cloudflare-worker/devtorch-mcp-bridge.js"],
8
+ "environment": {
9
+ "DEVTORCH_MCP_URL": "https://devtorch-mcp.YOUR-DOMAIN.com/YOUR_ORG/YOUR_REPO/sse",
10
+ "DEVTORCH_API_KEY": "YOUR_API_KEY",
11
+ "DEVTORCH_GCC_PATH": "__PROJECT_ROOT__"
12
+ }
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "devtorch": {
4
+ "command": "node",
5
+ "args": ["/ABSOLUTE/PATH/TO/deploy/cloudflare-worker/devtorch-mcp-bridge.js"],
6
+ "env": {
7
+ "DEVTORCH_MCP_URL": "https://devtorch-mcp.YOUR-DOMAIN.com/YOUR_ORG/YOUR_REPO/sse",
8
+ "DEVTORCH_API_KEY": "YOUR_API_KEY",
9
+ "DEVTORCH_GCC_PATH": "__PROJECT_ROOT__"
10
+ }
11
+ }
12
+ }
13
+ }