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,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevTorch local HTTP proxy (S7 + S26).
|
|
3
|
+
|
|
4
|
+
Runs on localhost:8765. Routes:
|
|
5
|
+
/anthropic/* → https://api.anthropic.com/*
|
|
6
|
+
/openai/* → https://api.openai.com/*
|
|
7
|
+
/gemini/* → https://generativelanguage.googleapis.com/*
|
|
8
|
+
/ollama/* → http://localhost:11434/*
|
|
9
|
+
/groq/* → https://api.groq.com/openai/v1/*
|
|
10
|
+
/azure-openai/* → Azure OpenAI endpoint (from env or headers)
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
devtorch proxy start # foreground, Ctrl+C to stop
|
|
14
|
+
devtorch proxy install # install as OS service
|
|
15
|
+
|
|
16
|
+
Traffic never routes through DevTorch servers — this is a local-only tool
|
|
17
|
+
that binds to 127.0.0.1 only. Developer API keys are passed through
|
|
18
|
+
unchanged to the upstream provider.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from fastapi import FastAPI, Request, Response
|
|
27
|
+
from fastapi.responses import StreamingResponse
|
|
28
|
+
import httpx
|
|
29
|
+
import uvicorn
|
|
30
|
+
HAS_PROXY_DEPS = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
HAS_PROXY_DEPS = False
|
|
33
|
+
|
|
34
|
+
DEFAULT_PORT = 8765
|
|
35
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# GCC repo discovery
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def find_gcc_repo() -> Optional[Any]:
|
|
43
|
+
"""
|
|
44
|
+
Walk up from cwd looking for an initialised .GCC/ directory.
|
|
45
|
+
Returns a GCCRepository instance or None if none found.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
from devtorch_core import GCCRepository
|
|
49
|
+
for parent in [Path.cwd()] + list(Path.cwd().parents):
|
|
50
|
+
try:
|
|
51
|
+
r = GCCRepository.at(parent)
|
|
52
|
+
if r.is_initialized():
|
|
53
|
+
return r
|
|
54
|
+
except Exception:
|
|
55
|
+
continue
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# App factory
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def create_app(gcc_repo: Optional[Any] = None) -> "FastAPI":
|
|
66
|
+
"""
|
|
67
|
+
Create the FastAPI proxy application.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
gcc_repo:
|
|
72
|
+
An initialised GCCRepository used for RACP injection and capture.
|
|
73
|
+
If None, requests are forwarded unmodified.
|
|
74
|
+
"""
|
|
75
|
+
if not HAS_PROXY_DEPS:
|
|
76
|
+
raise ImportError(
|
|
77
|
+
"Proxy dependencies not installed. Run: pip install devtorch[proxy]"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
app = FastAPI(
|
|
81
|
+
title="DevTorch Local Proxy",
|
|
82
|
+
version="0.1.0",
|
|
83
|
+
description=(
|
|
84
|
+
"Local HTTP proxy that intercepts LLM traffic, injects RACP context, "
|
|
85
|
+
"captures reasoning to .GCC/, then forwards to the real LLM API. "
|
|
86
|
+
"Binds to 127.0.0.1 only — never exposed to the network."
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
# Health check
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
@app.get("/health")
|
|
95
|
+
async def health() -> dict:
|
|
96
|
+
initialized = False
|
|
97
|
+
try:
|
|
98
|
+
initialized = gcc_repo is not None and gcc_repo.is_initialized()
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
return {"status": "ok", "gcc_initialized": initialized}
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
# Anthropic proxy
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
@app.api_route(
|
|
108
|
+
"/anthropic/{path:path}",
|
|
109
|
+
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
110
|
+
)
|
|
111
|
+
async def anthropic_proxy(path: str, request: Request) -> Response:
|
|
112
|
+
from .routes.anthropic import proxy_anthropic
|
|
113
|
+
return await proxy_anthropic(path, request, gcc_repo)
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
# OpenAI proxy
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
@app.api_route(
|
|
120
|
+
"/openai/{path:path}",
|
|
121
|
+
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
122
|
+
)
|
|
123
|
+
async def openai_proxy(path: str, request: Request) -> Response:
|
|
124
|
+
from .routes.openai import proxy_openai
|
|
125
|
+
return await proxy_openai(path, request, gcc_repo)
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
# Gemini proxy
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
@app.api_route(
|
|
132
|
+
"/gemini/{path:path}",
|
|
133
|
+
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
134
|
+
)
|
|
135
|
+
async def gemini_proxy(path: str, request: Request) -> Response:
|
|
136
|
+
from .routes.gemini import proxy_gemini
|
|
137
|
+
return await proxy_gemini(path, request, gcc_repo)
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Ollama proxy
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
@app.api_route(
|
|
144
|
+
"/ollama/{path:path}",
|
|
145
|
+
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
146
|
+
)
|
|
147
|
+
async def ollama_proxy(path: str, request: Request) -> Response:
|
|
148
|
+
from .routes.ollama import proxy_ollama
|
|
149
|
+
return await proxy_ollama(path, request, gcc_repo)
|
|
150
|
+
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
# Groq proxy
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
@app.api_route(
|
|
156
|
+
"/groq/{path:path}",
|
|
157
|
+
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
158
|
+
)
|
|
159
|
+
async def groq_proxy(path: str, request: Request) -> Response:
|
|
160
|
+
from .routes.groq import proxy_groq
|
|
161
|
+
return await proxy_groq(path, request, gcc_repo)
|
|
162
|
+
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
# Azure OpenAI proxy
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
@app.api_route(
|
|
168
|
+
"/azure-openai/{path:path}",
|
|
169
|
+
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
170
|
+
)
|
|
171
|
+
async def azure_openai_proxy(path: str, request: Request) -> Response:
|
|
172
|
+
from .routes.azure_openai import proxy_azure_openai
|
|
173
|
+
return await proxy_azure_openai(path, request, gcc_repo)
|
|
174
|
+
|
|
175
|
+
return app
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Proxy runner
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def run_proxy(
|
|
183
|
+
port: int = DEFAULT_PORT,
|
|
184
|
+
host: str = DEFAULT_HOST,
|
|
185
|
+
gcc_repo: Optional[Any] = None,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Start the proxy server. Blocks until Ctrl+C.
|
|
189
|
+
|
|
190
|
+
Binds to 127.0.0.1 by default — never exposed to the network.
|
|
191
|
+
"""
|
|
192
|
+
if not HAS_PROXY_DEPS:
|
|
193
|
+
raise ImportError(
|
|
194
|
+
"Proxy dependencies not installed. Run: pip install devtorch[proxy]"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
repo = gcc_repo or find_gcc_repo()
|
|
198
|
+
app = create_app(repo)
|
|
199
|
+
|
|
200
|
+
print(f"DevTorch proxy listening on http://{host}:{port}")
|
|
201
|
+
if repo is not None:
|
|
202
|
+
try:
|
|
203
|
+
initialized = repo.is_initialized()
|
|
204
|
+
print(f" .GCC/ found: {'yes' if initialized else 'no'}")
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
else:
|
|
208
|
+
print(" No .GCC/ found — forwarding without RACP injection")
|
|
209
|
+
|
|
210
|
+
uvicorn.run(app, host=host, port=port, log_level="warning")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# OS service installer
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def install_service() -> None:
|
|
218
|
+
"""Install devtorch proxy as an OS auto-start service."""
|
|
219
|
+
import platform
|
|
220
|
+
system = platform.system()
|
|
221
|
+
if system == "Darwin":
|
|
222
|
+
_install_launchagent()
|
|
223
|
+
elif system == "Linux":
|
|
224
|
+
_install_systemd()
|
|
225
|
+
elif system == "Windows":
|
|
226
|
+
_install_windows_service()
|
|
227
|
+
else:
|
|
228
|
+
print(
|
|
229
|
+
f"Auto-install not supported on {system}. "
|
|
230
|
+
"Run `devtorch proxy start` manually."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
# macOS LaunchAgent
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
def _install_launchagent() -> None:
|
|
239
|
+
"""Write ~/Library/LaunchAgents/ai.devtorch.proxy.plist"""
|
|
240
|
+
import shutil
|
|
241
|
+
|
|
242
|
+
devtorch_bin = shutil.which("devtorch")
|
|
243
|
+
if devtorch_bin is None:
|
|
244
|
+
print(
|
|
245
|
+
"Could not locate devtorch executable on PATH. "
|
|
246
|
+
"Install devtorch first (`pip install devtorch[proxy]`), "
|
|
247
|
+
"then re-run `devtorch proxy install`."
|
|
248
|
+
)
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
plist_dir = Path.home() / "Library" / "LaunchAgents"
|
|
252
|
+
plist_dir.mkdir(parents=True, exist_ok=True)
|
|
253
|
+
plist_path = plist_dir / "ai.devtorch.proxy.plist"
|
|
254
|
+
|
|
255
|
+
plist_content = f"""\
|
|
256
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
257
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
258
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
259
|
+
<plist version="1.0">
|
|
260
|
+
<dict>
|
|
261
|
+
<key>Label</key>
|
|
262
|
+
<string>ai.devtorch.proxy</string>
|
|
263
|
+
<key>ProgramArguments</key>
|
|
264
|
+
<array>
|
|
265
|
+
<string>{devtorch_bin}</string>
|
|
266
|
+
<string>proxy</string>
|
|
267
|
+
<string>start</string>
|
|
268
|
+
</array>
|
|
269
|
+
<key>RunAtLoad</key>
|
|
270
|
+
<true/>
|
|
271
|
+
<key>KeepAlive</key>
|
|
272
|
+
<true/>
|
|
273
|
+
<key>StandardOutPath</key>
|
|
274
|
+
<string>/tmp/devtorch-proxy.log</string>
|
|
275
|
+
<key>StandardErrorPath</key>
|
|
276
|
+
<string>/tmp/devtorch-proxy.err</string>
|
|
277
|
+
</dict>
|
|
278
|
+
</plist>
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
plist_path.write_text(plist_content, encoding="utf-8")
|
|
282
|
+
print(f"Installed LaunchAgent at {plist_path}")
|
|
283
|
+
print(
|
|
284
|
+
f"Run: launchctl load {plist_path}\n"
|
|
285
|
+
"To unload: launchctl unload "
|
|
286
|
+
"~/Library/LaunchAgents/ai.devtorch.proxy.plist"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
# Linux systemd user service
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
def _install_systemd() -> None:
|
|
295
|
+
"""Write ~/.config/systemd/user/devtorch-proxy.service"""
|
|
296
|
+
import shutil
|
|
297
|
+
|
|
298
|
+
devtorch_bin = shutil.which("devtorch")
|
|
299
|
+
if devtorch_bin is None:
|
|
300
|
+
print(
|
|
301
|
+
"Could not locate devtorch executable on PATH. "
|
|
302
|
+
"Install devtorch first, then re-run `devtorch proxy install`."
|
|
303
|
+
)
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
service_dir = Path.home() / ".config" / "systemd" / "user"
|
|
307
|
+
service_dir.mkdir(parents=True, exist_ok=True)
|
|
308
|
+
service_path = service_dir / "devtorch-proxy.service"
|
|
309
|
+
|
|
310
|
+
service_content = f"""\
|
|
311
|
+
[Unit]
|
|
312
|
+
Description=DevTorch Local Proxy
|
|
313
|
+
After=network.target
|
|
314
|
+
|
|
315
|
+
[Service]
|
|
316
|
+
Type=simple
|
|
317
|
+
ExecStart={devtorch_bin} proxy start
|
|
318
|
+
Restart=on-failure
|
|
319
|
+
RestartSec=5
|
|
320
|
+
StandardOutput=journal
|
|
321
|
+
StandardError=journal
|
|
322
|
+
|
|
323
|
+
[Install]
|
|
324
|
+
WantedBy=default.target
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
service_path.write_text(service_content, encoding="utf-8")
|
|
328
|
+
print(f"Installed systemd user service at {service_path}")
|
|
329
|
+
print("Run: systemctl --user enable --now devtorch-proxy")
|
|
330
|
+
print("To check status: systemctl --user status devtorch-proxy")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# ---------------------------------------------------------------------------
|
|
334
|
+
# Windows Task Scheduler instructions
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
def _install_windows_service() -> None:
|
|
338
|
+
"""Print instructions for Windows Task Scheduler."""
|
|
339
|
+
import shutil
|
|
340
|
+
devtorch_bin = shutil.which("devtorch") or "devtorch"
|
|
341
|
+
|
|
342
|
+
print("Windows auto-start setup (Task Scheduler):")
|
|
343
|
+
print(" 1. Open Task Scheduler (taskschd.msc)")
|
|
344
|
+
print(" 2. Create a new basic task:")
|
|
345
|
+
print(f" Action: Start a program → {devtorch_bin} proxy start")
|
|
346
|
+
print(" Trigger: At log on")
|
|
347
|
+
print(" 3. Or run from PowerShell:")
|
|
348
|
+
print(
|
|
349
|
+
f' $action = New-ScheduledTaskAction -Execute "{devtorch_bin}" '
|
|
350
|
+
'-Argument "proxy start"'
|
|
351
|
+
)
|
|
352
|
+
print(
|
|
353
|
+
' $trigger = New-ScheduledTaskTrigger -AtLogOn\n'
|
|
354
|
+
' Register-ScheduledTask -TaskName "DevTorch Proxy" '
|
|
355
|
+
'-Action $action -Trigger $trigger -RunLevel Highest'
|
|
356
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Hybrid grep + semantic search for `devtorch ask`."""
|
|
2
|
+
|
|
3
|
+
from .grep import grep_search, GrepResult
|
|
4
|
+
from .semantic import semantic_search, SemanticResult, SemanticBackend
|
|
5
|
+
from .hybrid import hybrid_search, AskResult
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"grep_search",
|
|
9
|
+
"GrepResult",
|
|
10
|
+
"semantic_search",
|
|
11
|
+
"SemanticResult",
|
|
12
|
+
"SemanticBackend",
|
|
13
|
+
"hybrid_search",
|
|
14
|
+
"AskResult",
|
|
15
|
+
]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Fast grep-based search over .GCC/ and project files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
|
|
11
|
+
# Files inside .GCC/ that we want to search line-by-line
|
|
12
|
+
_GCC_TEXT_FILES = (
|
|
13
|
+
"main.md",
|
|
14
|
+
"log.md",
|
|
15
|
+
"events.log.jsonl",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Source extensions we will walk over inside the project root
|
|
19
|
+
_SOURCE_SUFFIXES = (".py", ".md", ".ts", ".tsx", ".go", ".json")
|
|
20
|
+
|
|
21
|
+
# Directories to skip when walking project source files
|
|
22
|
+
_SKIP_DIRS = frozenset({
|
|
23
|
+
".git", ".GCC", ".venv", "venv", "node_modules", "__pycache__",
|
|
24
|
+
".pytest_cache", ".mypy_cache", ".tox", "build", "dist", "*.egg-info",
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _skip_dir(name: str) -> bool:
|
|
29
|
+
return name in _SKIP_DIRS or name.endswith(".egg-info")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=False)
|
|
33
|
+
class GrepResult:
|
|
34
|
+
path: Path
|
|
35
|
+
line: int
|
|
36
|
+
snippet: str
|
|
37
|
+
score: float
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict:
|
|
40
|
+
return {
|
|
41
|
+
"path": str(self.path),
|
|
42
|
+
"line": self.line,
|
|
43
|
+
"snippet": self.snippet,
|
|
44
|
+
"score": self.score,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _compile_terms(query: str) -> list[re.Pattern]:
|
|
49
|
+
"""Build case-insensitive regex patterns for each word in the query."""
|
|
50
|
+
words = [w for w in re.split(r"[^\w]+", query.lower()) if w and len(w) > 1]
|
|
51
|
+
if not words:
|
|
52
|
+
# Fallback to a non-empty pattern so we still search for single characters.
|
|
53
|
+
words = [w for w in query.lower().strip() if w.strip()] or [""]
|
|
54
|
+
return [re.compile(re.escape(word), re.IGNORECASE) for word in words]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _score_line(line: str, patterns: list[re.Pattern]) -> float:
|
|
58
|
+
"""Return a score between 0 and 1 based on term coverage."""
|
|
59
|
+
if not patterns:
|
|
60
|
+
return 0.0
|
|
61
|
+
lower = line.lower()
|
|
62
|
+
matches = sum(1 for pat in patterns if pat.search(lower))
|
|
63
|
+
return matches / len(patterns)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _snip(line: str, max_len: int = 240) -> str:
|
|
67
|
+
line = line.strip()
|
|
68
|
+
if len(line) > max_len:
|
|
69
|
+
line = line[:max_len].rstrip() + "..."
|
|
70
|
+
return line
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _collect_gcc_files(gcc_dir: Path) -> Iterable[Path]:
|
|
74
|
+
for name in _GCC_TEXT_FILES:
|
|
75
|
+
path = gcc_dir / name
|
|
76
|
+
if path.exists() and path.is_file():
|
|
77
|
+
yield path
|
|
78
|
+
# Also search concept files and sensitivity files if they exist as .md or .json
|
|
79
|
+
for subdir in ("concepts", "sensitivities"):
|
|
80
|
+
sub = gcc_dir / subdir
|
|
81
|
+
if sub.exists():
|
|
82
|
+
for path in sub.rglob("*"):
|
|
83
|
+
if path.is_file() and path.suffix in (".md", ".json", ".txt"):
|
|
84
|
+
yield path
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _search_file(path: Path, patterns: list[re.Pattern], min_score: float) -> list[GrepResult]:
|
|
88
|
+
results: list[GrepResult] = []
|
|
89
|
+
try:
|
|
90
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
91
|
+
except Exception:
|
|
92
|
+
return results
|
|
93
|
+
|
|
94
|
+
if path.name == "events.log.jsonl":
|
|
95
|
+
# Render each JSON line as a searchable text snippet
|
|
96
|
+
for line_no, raw in enumerate(text.splitlines(), start=1):
|
|
97
|
+
try:
|
|
98
|
+
payload = json.loads(raw)
|
|
99
|
+
except Exception:
|
|
100
|
+
continue
|
|
101
|
+
rendered = json.dumps(payload, ensure_ascii=False, separators=(", ", ": "))
|
|
102
|
+
score = _score_line(rendered, patterns)
|
|
103
|
+
if score >= min_score:
|
|
104
|
+
results.append(GrepResult(
|
|
105
|
+
path=path,
|
|
106
|
+
line=line_no,
|
|
107
|
+
snippet=_snip(rendered),
|
|
108
|
+
score=score,
|
|
109
|
+
))
|
|
110
|
+
else:
|
|
111
|
+
for line_no, line in enumerate(text.splitlines(), start=1):
|
|
112
|
+
score = _score_line(line, patterns)
|
|
113
|
+
if score >= min_score:
|
|
114
|
+
results.append(GrepResult(
|
|
115
|
+
path=path,
|
|
116
|
+
line=line_no,
|
|
117
|
+
snippet=_snip(line),
|
|
118
|
+
score=score,
|
|
119
|
+
))
|
|
120
|
+
return results
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _collect_project_files(root: Path) -> Iterable[Path]:
|
|
124
|
+
"""Yield project source files while skipping common noise directories."""
|
|
125
|
+
if not root.exists():
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
def _is_noise(p: Path) -> bool:
|
|
129
|
+
return p.name == ".GCC" or _skip_dir(p.name)
|
|
130
|
+
|
|
131
|
+
# Root-level files
|
|
132
|
+
for candidate in root.iterdir():
|
|
133
|
+
if candidate.is_file() and candidate.suffix in _SOURCE_SUFFIXES:
|
|
134
|
+
yield candidate
|
|
135
|
+
|
|
136
|
+
# Files inside non-noise directories
|
|
137
|
+
for path in root.iterdir():
|
|
138
|
+
if not path.is_dir() or _is_noise(path):
|
|
139
|
+
continue
|
|
140
|
+
for candidate in path.rglob("*"):
|
|
141
|
+
if candidate.is_file() and candidate.suffix in _SOURCE_SUFFIXES:
|
|
142
|
+
# Skip any nested noise dir
|
|
143
|
+
if any(_skip_dir(part) for part in candidate.parts):
|
|
144
|
+
continue
|
|
145
|
+
yield candidate
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def grep_search(
|
|
149
|
+
query: str,
|
|
150
|
+
project_root: Path,
|
|
151
|
+
top_n: int = 5,
|
|
152
|
+
min_score: float = 0.5,
|
|
153
|
+
) -> list[GrepResult]:
|
|
154
|
+
"""Run a fast regex grep over .GCC/ and project source files.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
query: free-text query
|
|
158
|
+
project_root: repository root that may contain .GCC/
|
|
159
|
+
top_n: maximum results to return
|
|
160
|
+
min_score: minimum term coverage score (0-1) for a line to be included
|
|
161
|
+
"""
|
|
162
|
+
patterns = _compile_terms(query)
|
|
163
|
+
gcc_dir = project_root / ".GCC"
|
|
164
|
+
all_results: list[GrepResult] = []
|
|
165
|
+
|
|
166
|
+
if gcc_dir.exists():
|
|
167
|
+
for path in _collect_gcc_files(gcc_dir):
|
|
168
|
+
all_results.extend(_search_file(path, patterns, min_score))
|
|
169
|
+
|
|
170
|
+
for path in _collect_project_files(project_root):
|
|
171
|
+
all_results.extend(_search_file(path, patterns, min_score))
|
|
172
|
+
|
|
173
|
+
# Boost exact substring matches and stable sort by score desc
|
|
174
|
+
query_lower = query.lower()
|
|
175
|
+
for r in all_results:
|
|
176
|
+
if query_lower in r.snippet.lower():
|
|
177
|
+
# small bonus for exact substring, capped at 1.0
|
|
178
|
+
r.score = min(1.0, r.score + 0.1)
|
|
179
|
+
|
|
180
|
+
all_results.sort(key=lambda r: r.score, reverse=True)
|
|
181
|
+
return all_results[:top_n]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Hybrid query combining fast grep results and optional semantic search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .grep import grep_search, GrepResult
|
|
9
|
+
from .semantic import semantic_search, SemanticResult, SemanticBackend
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=False)
|
|
13
|
+
class AskResult:
|
|
14
|
+
path: Path
|
|
15
|
+
line: int
|
|
16
|
+
snippet: str
|
|
17
|
+
score: float
|
|
18
|
+
source: str
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict:
|
|
21
|
+
return {
|
|
22
|
+
"path": str(self.path),
|
|
23
|
+
"line": self.line,
|
|
24
|
+
"snippet": self.snippet,
|
|
25
|
+
"score": self.score,
|
|
26
|
+
"source": self.source,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def hybrid_search(
|
|
31
|
+
query: str,
|
|
32
|
+
project_root: Path,
|
|
33
|
+
top_n: int = 5,
|
|
34
|
+
mode: str = "hybrid",
|
|
35
|
+
semantic_backend: SemanticBackend | None = None,
|
|
36
|
+
) -> list[AskResult]:
|
|
37
|
+
"""Run grep and/or semantic search, then merge and deduplicate.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
query: free-text query
|
|
41
|
+
project_root: repository root
|
|
42
|
+
top_n: maximum number of results to return
|
|
43
|
+
mode: one of "hybrid" (default), "grep", "semantic"
|
|
44
|
+
semantic_backend: optional backend for testing / custom models
|
|
45
|
+
"""
|
|
46
|
+
if mode not in {"hybrid", "grep", "semantic"}:
|
|
47
|
+
raise ValueError("mode must be one of hybrid, grep, semantic")
|
|
48
|
+
|
|
49
|
+
grep_results = []
|
|
50
|
+
semantic_results = []
|
|
51
|
+
|
|
52
|
+
if mode in ("hybrid", "grep"):
|
|
53
|
+
grep_results = grep_search(query, project_root, top_n=top_n)
|
|
54
|
+
|
|
55
|
+
if mode in ("hybrid", "semantic"):
|
|
56
|
+
semantic_results = semantic_search(query, project_root, top_n=top_n, backend=semantic_backend)
|
|
57
|
+
|
|
58
|
+
if mode == "grep":
|
|
59
|
+
return [
|
|
60
|
+
AskResult(r.path, r.line, r.snippet, r.score, "grep")
|
|
61
|
+
for r in grep_results
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
if mode == "semantic":
|
|
65
|
+
return [
|
|
66
|
+
AskResult(r.path, r.line, r.snippet, r.score, "semantic")
|
|
67
|
+
for r in semantic_results
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Hybrid mode: merge and deduplicate by (path, line) keeping the highest score
|
|
71
|
+
merged: list[AskResult] = [
|
|
72
|
+
AskResult(r.path, r.line, r.snippet, r.score, "grep")
|
|
73
|
+
for r in grep_results
|
|
74
|
+
]
|
|
75
|
+
for r in semantic_results:
|
|
76
|
+
merged.append(AskResult(r.path, r.line, r.snippet, r.score, "semantic"))
|
|
77
|
+
|
|
78
|
+
best: dict[tuple[str, int], AskResult] = {}
|
|
79
|
+
for r in merged:
|
|
80
|
+
key = (str(r.path), r.line)
|
|
81
|
+
existing = best.get(key)
|
|
82
|
+
if existing is None or r.score > existing.score:
|
|
83
|
+
best[key] = r
|
|
84
|
+
|
|
85
|
+
final = sorted(best.values(), key=lambda r: r.score, reverse=True)
|
|
86
|
+
return final[:top_n]
|