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.
- devtorch_core/__init__.py +158 -0
- devtorch_core/aggphi_textual.py +275 -0
- devtorch_core/alerts/__init__.py +23 -0
- devtorch_core/alerts/base.py +46 -0
- devtorch_core/alerts/config.py +60 -0
- devtorch_core/alerts/dispatcher.py +110 -0
- devtorch_core/alerts/jira.py +96 -0
- devtorch_core/alerts/linear.py +72 -0
- devtorch_core/alerts/pagerduty.py +66 -0
- devtorch_core/alerts/slack.py +81 -0
- devtorch_core/alerts/teams.py +70 -0
- devtorch_core/audit/__init__.py +43 -0
- devtorch_core/audit/exporter.py +297 -0
- devtorch_core/audit/privacy.py +101 -0
- devtorch_core/audit/scrubber.py +149 -0
- devtorch_core/audit/service.py +67 -0
- devtorch_core/audit/signing.py +127 -0
- devtorch_core/broadcast/__init__.py +4 -0
- devtorch_core/broadcast/broadcaster.py +100 -0
- devtorch_core/broadcast/watcher.py +71 -0
- devtorch_core/capability.py +639 -0
- devtorch_core/cloud/__init__.py +1 -0
- devtorch_core/cloud/client_config.py +472 -0
- devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
- devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
- devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
- devtorch_core/cloud/client_configs/.opencode.json +15 -0
- devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
- devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
- devtorch_core/cloud/mcp_client.py +229 -0
- devtorch_core/cloud/setup.py +144 -0
- devtorch_core/cloud/sync.py +143 -0
- devtorch_core/cloud/sync_bundle.py +603 -0
- devtorch_core/cloud/sync_conflicts.py +159 -0
- devtorch_core/cloud/sync_state.py +159 -0
- devtorch_core/cloud/team_sync.py +283 -0
- devtorch_core/codex/__init__.py +9 -0
- devtorch_core/codex/__main__.py +97 -0
- devtorch_core/codex/capture.py +208 -0
- devtorch_core/codex/proxy.py +412 -0
- devtorch_core/concept_catalog.py +209 -0
- devtorch_core/consolidation/__init__.py +3 -0
- devtorch_core/consolidation/synthesizer.py +87 -0
- devtorch_core/consolidation/workflow.py +175 -0
- devtorch_core/daemon/__init__.py +27 -0
- devtorch_core/daemon/supervisor.py +293 -0
- devtorch_core/daemon/watcher.py +244 -0
- devtorch_core/dashboard_api.py +2012 -0
- devtorch_core/deltaf.py +97 -0
- devtorch_core/disclosure.py +50 -0
- devtorch_core/divergence/__init__.py +3 -0
- devtorch_core/divergence/detector.py +166 -0
- devtorch_core/gateway/__init__.py +32 -0
- devtorch_core/gateway/key_manager.py +124 -0
- devtorch_core/gateway/metrics_webhook.py +252 -0
- devtorch_core/gateway/policy.py +262 -0
- devtorch_core/gateway/server.py +727 -0
- devtorch_core/gateway/sso.py +233 -0
- devtorch_core/gcc.py +1246 -0
- devtorch_core/github/__init__.py +35 -0
- devtorch_core/github/app.py +240 -0
- devtorch_core/github/comment_builder.py +113 -0
- devtorch_core/github/pat.py +76 -0
- devtorch_core/github/pr_parser.py +82 -0
- devtorch_core/github/pr_reporter.py +555 -0
- devtorch_core/gitlab/__init__.py +177 -0
- devtorch_core/hitl/__init__.py +4 -0
- devtorch_core/hitl/channels.py +129 -0
- devtorch_core/hitl/orchestrator.py +95 -0
- devtorch_core/hooks/__init__.py +17 -0
- devtorch_core/hooks/claude_code.py +228 -0
- devtorch_core/hooks/git_capture.py +341 -0
- devtorch_core/hooks/git_commit.py +182 -0
- devtorch_core/hooks/installer.py +733 -0
- devtorch_core/hooks/pre_commit.py +157 -0
- devtorch_core/hooks/runner.py +344 -0
- devtorch_core/identity/__init__.py +4 -0
- devtorch_core/identity/agent.py +86 -0
- devtorch_core/identity/providers.py +85 -0
- devtorch_core/invariants.py +182 -0
- devtorch_core/mcp/__init__.py +10 -0
- devtorch_core/mcp/auth.py +177 -0
- devtorch_core/mcp/server.py +1049 -0
- devtorch_core/metrics/__init__.py +35 -0
- devtorch_core/metrics/aggregate.py +215 -0
- devtorch_core/metrics/calibrate.py +198 -0
- devtorch_core/metrics/calibration.py +125 -0
- devtorch_core/metrics/credibility.py +288 -0
- devtorch_core/metrics/delivery_time.py +70 -0
- devtorch_core/metrics/dhs.py +126 -0
- devtorch_core/metrics/mcs.py +96 -0
- devtorch_core/metrics/roi.py +88 -0
- devtorch_core/metrics/session_writer.py +81 -0
- devtorch_core/metrics/shadow_ai.py +117 -0
- devtorch_core/metrics/sprint_writer.py +243 -0
- devtorch_core/observability/__init__.py +78 -0
- devtorch_core/observability/datadog.py +157 -0
- devtorch_core/observability/formatter.py +119 -0
- devtorch_core/observability/report.py +264 -0
- devtorch_core/observability/servicenow.py +147 -0
- devtorch_core/observability/splunk.py +218 -0
- devtorch_core/observability/webhook.py +227 -0
- devtorch_core/parser/__init__.py +30 -0
- devtorch_core/parser/blocks.py +216 -0
- devtorch_core/parser/inference.py +159 -0
- devtorch_core/parser/thinking.py +112 -0
- devtorch_core/projects.py +169 -0
- devtorch_core/prompt_artifact.py +76 -0
- devtorch_core/proxy/__init__.py +9 -0
- devtorch_core/proxy/routes/__init__.py +1 -0
- devtorch_core/proxy/routes/anthropic.py +264 -0
- devtorch_core/proxy/routes/azure_openai.py +336 -0
- devtorch_core/proxy/routes/gemini.py +331 -0
- devtorch_core/proxy/routes/groq.py +284 -0
- devtorch_core/proxy/routes/ollama.py +279 -0
- devtorch_core/proxy/routes/openai.py +287 -0
- devtorch_core/proxy/server.py +356 -0
- devtorch_core/query/__init__.py +15 -0
- devtorch_core/query/grep.py +181 -0
- devtorch_core/query/hybrid.py +86 -0
- devtorch_core/query/semantic.py +157 -0
- devtorch_core/rdp.py +105 -0
- devtorch_core/reasoning/__init__.py +4 -0
- devtorch_core/reasoning/entry.py +31 -0
- devtorch_core/reasoning/store.py +122 -0
- devtorch_core/reasoning_plus/__init__.py +70 -0
- devtorch_core/reasoning_plus/augmenter.py +326 -0
- devtorch_core/reasoning_plus/capture.py +51 -0
- devtorch_core/reasoning_plus/config.py +256 -0
- devtorch_core/reasoning_plus/context.py +262 -0
- devtorch_core/reasoning_plus/learning/__init__.py +72 -0
- devtorch_core/reasoning_plus/learning/analytics.py +141 -0
- devtorch_core/reasoning_plus/learning/api.py +313 -0
- devtorch_core/reasoning_plus/learning/chain.py +285 -0
- devtorch_core/reasoning_plus/learning/composer.py +74 -0
- devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
- devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
- devtorch_core/reasoning_plus/learning/extractor.py +207 -0
- devtorch_core/reasoning_plus/learning/models.py +116 -0
- devtorch_core/reasoning_plus/learning/provenance.py +126 -0
- devtorch_core/reasoning_plus/learning/recorder.py +81 -0
- devtorch_core/reasoning_plus/learning/relevance.py +122 -0
- devtorch_core/reasoning_plus/learning/state.py +86 -0
- devtorch_core/reasoning_plus/learning/store.py +160 -0
- devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- devtorch_core/reasoning_plus/prompt.py +90 -0
- devtorch_core/rep.py +134 -0
- devtorch_core/rep_network/__init__.py +25 -0
- devtorch_core/rep_network/merge.py +70 -0
- devtorch_core/rep_network/node.py +137 -0
- devtorch_core/rep_network/server.py +140 -0
- devtorch_core/rep_network/sync.py +207 -0
- devtorch_core/sensitivity.py +182 -0
- devtorch_core/serve.py +258 -0
- devtorch_core/session/__init__.py +39 -0
- devtorch_core/session/disagreement.py +188 -0
- devtorch_core/session/models.py +114 -0
- devtorch_core/session/orchestrator.py +182 -0
- devtorch_core/session/planner.py +169 -0
- devtorch_core/session/simulator.py +132 -0
- devtorch_core/signing.py +290 -0
- devtorch_core/sis.py +197 -0
- devtorch_core/storage.py +308 -0
- devtorch_core/templates/__init__.py +6 -0
- devtorch_core/templates/engine.py +122 -0
- devtorch_core/templates/go.py +18 -0
- devtorch_core/templates/infra.py +19 -0
- devtorch_core/templates/library/__init__.py +18 -0
- devtorch_core/templates/library/api_design.md +27 -0
- devtorch_core/templates/library/bug_fix.md +27 -0
- devtorch_core/templates/library/decision_record.md +27 -0
- devtorch_core/templates/library/engine.py +228 -0
- devtorch_core/templates/library/security_review.md +30 -0
- devtorch_core/templates/python.py +19 -0
- devtorch_core/templates/react.py +18 -0
- devtorch_core/templates/typescript.py +18 -0
- devtorch_core/theta.py +221 -0
- devtorch_core/theta_synthesis.py +268 -0
- devtorch_core/topics.py +320 -0
- devtorch_core/variance.py +219 -0
- devtorch_core/wrapper/__init__.py +52 -0
- devtorch_core/wrapper/anthropic.py +487 -0
- devtorch_core/wrapper/base.py +562 -0
- devtorch_core/wrapper/bedrock.py +342 -0
- devtorch_core/wrapper/gemini.py +422 -0
- devtorch_core/wrapper/ollama.py +527 -0
- devtorch_core/wrapper/openai.py +461 -0
- devtorch_core-3.0.1.dist-info/METADATA +867 -0
- devtorch_core-3.0.1.dist-info/RECORD +193 -0
- devtorch_core-3.0.1.dist-info/WHEEL +5 -0
- devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
- 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,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
|
+
"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
|
+
}
|