thinkstack-core 4.0.0__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 (205) hide show
  1. thinkstack_core/__init__.py +158 -0
  2. thinkstack_core/aggphi_textual.py +275 -0
  3. thinkstack_core/alerts/__init__.py +23 -0
  4. thinkstack_core/alerts/base.py +46 -0
  5. thinkstack_core/alerts/config.py +60 -0
  6. thinkstack_core/alerts/dispatcher.py +110 -0
  7. thinkstack_core/alerts/jira.py +96 -0
  8. thinkstack_core/alerts/linear.py +72 -0
  9. thinkstack_core/alerts/pagerduty.py +66 -0
  10. thinkstack_core/alerts/slack.py +81 -0
  11. thinkstack_core/alerts/teams.py +70 -0
  12. thinkstack_core/audit/__init__.py +43 -0
  13. thinkstack_core/audit/exporter.py +297 -0
  14. thinkstack_core/audit/privacy.py +101 -0
  15. thinkstack_core/audit/scrubber.py +149 -0
  16. thinkstack_core/audit/service.py +67 -0
  17. thinkstack_core/audit/signing.py +127 -0
  18. thinkstack_core/broadcast/__init__.py +4 -0
  19. thinkstack_core/broadcast/broadcaster.py +100 -0
  20. thinkstack_core/broadcast/watcher.py +71 -0
  21. thinkstack_core/capability.py +639 -0
  22. thinkstack_core/cloud/__init__.py +1 -0
  23. thinkstack_core/cloud/client_config.py +472 -0
  24. thinkstack_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. thinkstack_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. thinkstack_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. thinkstack_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. thinkstack_core/cloud/client_configs/.opencode.json +15 -0
  29. thinkstack_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. thinkstack_core/cloud/mcp_client.py +229 -0
  31. thinkstack_core/cloud/setup.py +144 -0
  32. thinkstack_core/cloud/sync.py +143 -0
  33. thinkstack_core/cloud/sync_bundle.py +639 -0
  34. thinkstack_core/cloud/sync_conflicts.py +183 -0
  35. thinkstack_core/cloud/sync_state.py +159 -0
  36. thinkstack_core/cloud/team_sync.py +337 -0
  37. thinkstack_core/cloud/thinkstack-mcp-bridge.js +357 -0
  38. thinkstack_core/codex/__init__.py +9 -0
  39. thinkstack_core/codex/__main__.py +97 -0
  40. thinkstack_core/codex/capture.py +208 -0
  41. thinkstack_core/codex/proxy.py +412 -0
  42. thinkstack_core/compat.py +103 -0
  43. thinkstack_core/concept_catalog.py +209 -0
  44. thinkstack_core/consolidation/__init__.py +3 -0
  45. thinkstack_core/consolidation/synthesizer.py +87 -0
  46. thinkstack_core/consolidation/workflow.py +175 -0
  47. thinkstack_core/daemon/__init__.py +27 -0
  48. thinkstack_core/daemon/supervisor.py +293 -0
  49. thinkstack_core/daemon/watcher.py +244 -0
  50. thinkstack_core/dashboard_api.py +2012 -0
  51. thinkstack_core/deltaf.py +97 -0
  52. thinkstack_core/disclosure.py +50 -0
  53. thinkstack_core/divergence/__init__.py +3 -0
  54. thinkstack_core/divergence/detector.py +166 -0
  55. thinkstack_core/gateway/__init__.py +32 -0
  56. thinkstack_core/gateway/key_manager.py +124 -0
  57. thinkstack_core/gateway/metrics_webhook.py +252 -0
  58. thinkstack_core/gateway/policy.py +262 -0
  59. thinkstack_core/gateway/server.py +727 -0
  60. thinkstack_core/gateway/sso.py +233 -0
  61. thinkstack_core/gcc.py +1246 -0
  62. thinkstack_core/github/__init__.py +35 -0
  63. thinkstack_core/github/app.py +240 -0
  64. thinkstack_core/github/comment_builder.py +113 -0
  65. thinkstack_core/github/pat.py +76 -0
  66. thinkstack_core/github/pr_parser.py +82 -0
  67. thinkstack_core/github/pr_reporter.py +555 -0
  68. thinkstack_core/gitlab/__init__.py +177 -0
  69. thinkstack_core/hitl/__init__.py +4 -0
  70. thinkstack_core/hitl/channels.py +129 -0
  71. thinkstack_core/hitl/orchestrator.py +95 -0
  72. thinkstack_core/hooks/__init__.py +17 -0
  73. thinkstack_core/hooks/claude_code.py +228 -0
  74. thinkstack_core/hooks/git_capture.py +341 -0
  75. thinkstack_core/hooks/git_commit.py +182 -0
  76. thinkstack_core/hooks/installer.py +850 -0
  77. thinkstack_core/hooks/pre_commit.py +157 -0
  78. thinkstack_core/hooks/runner.py +386 -0
  79. thinkstack_core/identity/__init__.py +4 -0
  80. thinkstack_core/identity/agent.py +86 -0
  81. thinkstack_core/identity/providers.py +85 -0
  82. thinkstack_core/invariants.py +182 -0
  83. thinkstack_core/mcp/__init__.py +10 -0
  84. thinkstack_core/mcp/auth.py +177 -0
  85. thinkstack_core/mcp/server.py +1215 -0
  86. thinkstack_core/metrics/__init__.py +35 -0
  87. thinkstack_core/metrics/aggregate.py +215 -0
  88. thinkstack_core/metrics/calibrate.py +198 -0
  89. thinkstack_core/metrics/calibration.py +125 -0
  90. thinkstack_core/metrics/credibility.py +288 -0
  91. thinkstack_core/metrics/delivery_time.py +70 -0
  92. thinkstack_core/metrics/dhs.py +126 -0
  93. thinkstack_core/metrics/mcs.py +96 -0
  94. thinkstack_core/metrics/roi.py +88 -0
  95. thinkstack_core/metrics/session_writer.py +81 -0
  96. thinkstack_core/metrics/shadow_ai.py +117 -0
  97. thinkstack_core/metrics/sprint_writer.py +243 -0
  98. thinkstack_core/observability/__init__.py +78 -0
  99. thinkstack_core/observability/datadog.py +157 -0
  100. thinkstack_core/observability/formatter.py +119 -0
  101. thinkstack_core/observability/report.py +264 -0
  102. thinkstack_core/observability/servicenow.py +147 -0
  103. thinkstack_core/observability/splunk.py +218 -0
  104. thinkstack_core/observability/webhook.py +227 -0
  105. thinkstack_core/parser/__init__.py +30 -0
  106. thinkstack_core/parser/blocks.py +216 -0
  107. thinkstack_core/parser/inference.py +159 -0
  108. thinkstack_core/parser/thinking.py +112 -0
  109. thinkstack_core/projects.py +169 -0
  110. thinkstack_core/prompt_artifact.py +76 -0
  111. thinkstack_core/proxy/__init__.py +9 -0
  112. thinkstack_core/proxy/routes/__init__.py +1 -0
  113. thinkstack_core/proxy/routes/anthropic.py +264 -0
  114. thinkstack_core/proxy/routes/azure_openai.py +336 -0
  115. thinkstack_core/proxy/routes/gemini.py +331 -0
  116. thinkstack_core/proxy/routes/groq.py +284 -0
  117. thinkstack_core/proxy/routes/ollama.py +279 -0
  118. thinkstack_core/proxy/routes/openai.py +287 -0
  119. thinkstack_core/proxy/server.py +356 -0
  120. thinkstack_core/query/__init__.py +15 -0
  121. thinkstack_core/query/grep.py +181 -0
  122. thinkstack_core/query/hybrid.py +86 -0
  123. thinkstack_core/query/semantic.py +157 -0
  124. thinkstack_core/rdp.py +105 -0
  125. thinkstack_core/reasoning/__init__.py +4 -0
  126. thinkstack_core/reasoning/entry.py +31 -0
  127. thinkstack_core/reasoning/store.py +122 -0
  128. thinkstack_core/reasoning_plus/__init__.py +70 -0
  129. thinkstack_core/reasoning_plus/augmenter.py +337 -0
  130. thinkstack_core/reasoning_plus/capture.py +51 -0
  131. thinkstack_core/reasoning_plus/config.py +313 -0
  132. thinkstack_core/reasoning_plus/context.py +262 -0
  133. thinkstack_core/reasoning_plus/learning/__init__.py +125 -0
  134. thinkstack_core/reasoning_plus/learning/analytics.py +141 -0
  135. thinkstack_core/reasoning_plus/learning/api.py +784 -0
  136. thinkstack_core/reasoning_plus/learning/chain.py +285 -0
  137. thinkstack_core/reasoning_plus/learning/composer.py +141 -0
  138. thinkstack_core/reasoning_plus/learning/conflicts.py +184 -0
  139. thinkstack_core/reasoning_plus/learning/context_collector.py +194 -0
  140. thinkstack_core/reasoning_plus/learning/cross_project.py +234 -0
  141. thinkstack_core/reasoning_plus/learning/deny_list.py +108 -0
  142. thinkstack_core/reasoning_plus/learning/embeddings.py +209 -0
  143. thinkstack_core/reasoning_plus/learning/evolution.py +119 -0
  144. thinkstack_core/reasoning_plus/learning/extractor.py +271 -0
  145. thinkstack_core/reasoning_plus/learning/filter_five_layer.py +95 -0
  146. thinkstack_core/reasoning_plus/learning/models.py +149 -0
  147. thinkstack_core/reasoning_plus/learning/org_store.py +156 -0
  148. thinkstack_core/reasoning_plus/learning/pii.py +142 -0
  149. thinkstack_core/reasoning_plus/learning/promotion.py +58 -0
  150. thinkstack_core/reasoning_plus/learning/provenance.py +126 -0
  151. thinkstack_core/reasoning_plus/learning/recorder.py +81 -0
  152. thinkstack_core/reasoning_plus/learning/relevance.py +122 -0
  153. thinkstack_core/reasoning_plus/learning/state.py +86 -0
  154. thinkstack_core/reasoning_plus/learning/store.py +178 -0
  155. thinkstack_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  156. thinkstack_core/reasoning_plus/prompt.py +90 -0
  157. thinkstack_core/rep.py +134 -0
  158. thinkstack_core/rep_network/__init__.py +25 -0
  159. thinkstack_core/rep_network/merge.py +70 -0
  160. thinkstack_core/rep_network/node.py +137 -0
  161. thinkstack_core/rep_network/server.py +140 -0
  162. thinkstack_core/rep_network/sync.py +207 -0
  163. thinkstack_core/sensitivity.py +182 -0
  164. thinkstack_core/serve.py +258 -0
  165. thinkstack_core/session/__init__.py +39 -0
  166. thinkstack_core/session/disagreement.py +188 -0
  167. thinkstack_core/session/models.py +114 -0
  168. thinkstack_core/session/orchestrator.py +182 -0
  169. thinkstack_core/session/planner.py +169 -0
  170. thinkstack_core/session/simulator.py +132 -0
  171. thinkstack_core/signing.py +290 -0
  172. thinkstack_core/sis.py +197 -0
  173. thinkstack_core/skills/pr-reviewer/SKILL.md +204 -0
  174. thinkstack_core/skills/thinkstack-auto-sync/SKILL.md +175 -0
  175. thinkstack_core/skills/thinkstack-session-start/SKILL.md +136 -0
  176. thinkstack_core/storage.py +308 -0
  177. thinkstack_core/templates/__init__.py +6 -0
  178. thinkstack_core/templates/engine.py +122 -0
  179. thinkstack_core/templates/go.py +18 -0
  180. thinkstack_core/templates/infra.py +19 -0
  181. thinkstack_core/templates/library/__init__.py +18 -0
  182. thinkstack_core/templates/library/api_design.md +27 -0
  183. thinkstack_core/templates/library/bug_fix.md +27 -0
  184. thinkstack_core/templates/library/decision_record.md +27 -0
  185. thinkstack_core/templates/library/engine.py +228 -0
  186. thinkstack_core/templates/library/security_review.md +30 -0
  187. thinkstack_core/templates/python.py +19 -0
  188. thinkstack_core/templates/react.py +18 -0
  189. thinkstack_core/templates/typescript.py +18 -0
  190. thinkstack_core/theta.py +221 -0
  191. thinkstack_core/theta_synthesis.py +268 -0
  192. thinkstack_core/topics.py +320 -0
  193. thinkstack_core/variance.py +219 -0
  194. thinkstack_core/wrapper/__init__.py +52 -0
  195. thinkstack_core/wrapper/anthropic.py +487 -0
  196. thinkstack_core/wrapper/base.py +562 -0
  197. thinkstack_core/wrapper/bedrock.py +342 -0
  198. thinkstack_core/wrapper/gemini.py +422 -0
  199. thinkstack_core/wrapper/ollama.py +527 -0
  200. thinkstack_core/wrapper/openai.py +461 -0
  201. thinkstack_core-4.0.0.dist-info/METADATA +868 -0
  202. thinkstack_core-4.0.0.dist-info/RECORD +205 -0
  203. thinkstack_core-4.0.0.dist-info/WHEEL +5 -0
  204. thinkstack_core-4.0.0.dist-info/entry_points.txt +2 -0
  205. thinkstack_core-4.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,412 @@
1
+ """
2
+ thinkstack_core.codex.proxy
3
+ =========================
4
+ Lightweight HTTP proxy server for intercepting OpenAI Codex CLI traffic.
5
+
6
+ Uses only stdlib (http.server, urllib.request, threading) — no FastAPI or
7
+ httpx dependency required.
8
+
9
+ Default port: 8766 (main ThinkStack proxy runs on 8765)
10
+ Upstream: https://api.openai.com
11
+
12
+ Routes:
13
+ POST /v1/* → inject RACP, forward, capture response
14
+ GET /health → 200 {"status": "ok"}
15
+ * (everything else) → transparent forward
16
+
17
+ Errors always return 502 JSON — never crash the proxy.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ import threading
24
+ import urllib.error
25
+ import urllib.request
26
+ from http.server import BaseHTTPRequestHandler, HTTPServer
27
+ from pathlib import Path
28
+ from typing import Optional
29
+
30
+ logger = logging.getLogger("thinkstack.codex.proxy")
31
+
32
+ DEFAULT_PORT = 8766
33
+ DEFAULT_UPSTREAM = "https://api.openai.com"
34
+
35
+ # Headers that must NOT be forwarded to the upstream (they are hop-by-hop or
36
+ # set by the proxy itself when re-encoding the body).
37
+ _HOP_BY_HOP = frozenset(
38
+ [
39
+ "connection",
40
+ "keep-alive",
41
+ "proxy-authenticate",
42
+ "proxy-authorization",
43
+ "te",
44
+ "trailers",
45
+ "transfer-encoding",
46
+ "upgrade",
47
+ "host",
48
+ "content-length", # recalculated after body modification
49
+ ]
50
+ )
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Request handler
55
+ # ---------------------------------------------------------------------------
56
+
57
+ class _CodexRequestHandler(BaseHTTPRequestHandler):
58
+ """One instance per incoming HTTP connection."""
59
+
60
+ # Injected by CodexProxy on the server object
61
+ upstream: str = DEFAULT_UPSTREAM
62
+
63
+ # Silence the default access log — we write our own on error
64
+ def log_message(self, fmt: str, *args) -> None: # type: ignore[override]
65
+ pass
66
+
67
+ # ------------------------------------------------------------------
68
+ # Routing
69
+ # ------------------------------------------------------------------
70
+
71
+ def do_GET(self) -> None:
72
+ if self.path == "/health":
73
+ self._send_json(200, {"status": "ok", "proxy": "thinkstack-codex"})
74
+ return
75
+ self._forward_transparent()
76
+
77
+ def do_POST(self) -> None:
78
+ if self.path.startswith("/v1/"):
79
+ self._handle_v1_post()
80
+ else:
81
+ self._forward_transparent()
82
+
83
+ # Support additional methods by transparent proxy
84
+ def do_PUT(self) -> None:
85
+ self._forward_transparent()
86
+
87
+ def do_DELETE(self) -> None:
88
+ self._forward_transparent()
89
+
90
+ def do_PATCH(self) -> None:
91
+ self._forward_transparent()
92
+
93
+ def do_HEAD(self) -> None:
94
+ self._forward_transparent()
95
+
96
+ def do_OPTIONS(self) -> None:
97
+ self._forward_transparent()
98
+
99
+ # ------------------------------------------------------------------
100
+ # v1 POST handler — RACP injection + capture
101
+ # ------------------------------------------------------------------
102
+
103
+ def _handle_v1_post(self) -> None:
104
+ try:
105
+ body_bytes = self._read_body()
106
+ except Exception as exc:
107
+ self._send_error_502(f"failed to read request body: {exc}")
108
+ return
109
+
110
+ # Parse JSON — if not parseable, forward raw
111
+ try:
112
+ request_body = json.loads(body_bytes) if body_bytes else {}
113
+ except Exception:
114
+ request_body = None
115
+
116
+ # Inject RACP prefix
117
+ modified_body_bytes = body_bytes
118
+ if request_body is not None:
119
+ try:
120
+ from thinkstack_core.codex.capture import CodexCaptureHandler
121
+ import uuid
122
+
123
+ session_id = str(uuid.uuid4())
124
+ handler = CodexCaptureHandler()
125
+ modified_body = handler.pre_execute(request_body, session_id)
126
+ modified_body_bytes = json.dumps(modified_body).encode("utf-8")
127
+ except Exception as exc:
128
+ logger.warning("thinkstack codex RACP injection failed — %s", exc)
129
+ session_id = ""
130
+ handler = None
131
+ else:
132
+ session_id = ""
133
+ handler = None
134
+
135
+ # Forward to upstream
136
+ upstream_url = self.upstream.rstrip("/") + self.path
137
+ fwd_headers = self._build_forward_headers(len(modified_body_bytes))
138
+
139
+ try:
140
+ req = urllib.request.Request(
141
+ upstream_url,
142
+ data=modified_body_bytes,
143
+ headers=fwd_headers,
144
+ method="POST",
145
+ )
146
+ with urllib.request.urlopen(req) as resp:
147
+ resp_bytes = resp.read()
148
+ resp_status = resp.status
149
+ resp_headers = dict(resp.headers)
150
+ except urllib.error.HTTPError as exc:
151
+ resp_bytes = exc.read()
152
+ resp_status = exc.code
153
+ resp_headers = dict(exc.headers)
154
+ except Exception as exc:
155
+ self._send_error_502(f"upstream request failed: {exc}")
156
+ return
157
+
158
+ # Send response back to Codex CLI
159
+ self._relay_response(resp_status, resp_headers, resp_bytes)
160
+
161
+ # Fire post_execute in background — never blocks the response
162
+ if handler is not None and request_body is not None:
163
+ try:
164
+ resp_body = json.loads(resp_bytes) if resp_bytes else {}
165
+ except Exception:
166
+ resp_body = {}
167
+ t = threading.Thread(
168
+ target=_safe_post_execute,
169
+ args=(handler, request_body, resp_body, session_id),
170
+ daemon=True,
171
+ )
172
+ t.start()
173
+
174
+ # ------------------------------------------------------------------
175
+ # Transparent forward
176
+ # ------------------------------------------------------------------
177
+
178
+ def _forward_transparent(self) -> None:
179
+ try:
180
+ body_bytes = self._read_body()
181
+ except Exception as exc:
182
+ self._send_error_502(f"failed to read request body: {exc}")
183
+ return
184
+
185
+ upstream_url = self.upstream.rstrip("/") + self.path
186
+ fwd_headers = self._build_forward_headers(len(body_bytes) if body_bytes else 0)
187
+
188
+ try:
189
+ req = urllib.request.Request(
190
+ upstream_url,
191
+ data=body_bytes if body_bytes else None,
192
+ headers=fwd_headers,
193
+ method=self.command,
194
+ )
195
+ with urllib.request.urlopen(req) as resp:
196
+ resp_bytes = resp.read()
197
+ resp_status = resp.status
198
+ resp_headers = dict(resp.headers)
199
+ except urllib.error.HTTPError as exc:
200
+ resp_bytes = exc.read()
201
+ resp_status = exc.code
202
+ resp_headers = dict(exc.headers)
203
+ except Exception as exc:
204
+ self._send_error_502(f"upstream request failed: {exc}")
205
+ return
206
+
207
+ self._relay_response(resp_status, resp_headers, resp_bytes)
208
+
209
+ # ------------------------------------------------------------------
210
+ # Helpers
211
+ # ------------------------------------------------------------------
212
+
213
+ def _read_body(self) -> bytes:
214
+ length = int(self.headers.get("Content-Length", 0))
215
+ if length > 0:
216
+ return self.rfile.read(length)
217
+ return b""
218
+
219
+ def _build_forward_headers(self, content_length: int) -> dict:
220
+ """Build headers to send upstream, stripping hop-by-hop headers."""
221
+ headers: dict = {}
222
+ for key, val in self.headers.items():
223
+ if key.lower() not in _HOP_BY_HOP:
224
+ headers[key] = val
225
+ if content_length > 0:
226
+ headers["Content-Length"] = str(content_length)
227
+ # Ensure Host is set to the upstream host
228
+ from urllib.parse import urlparse
229
+ parsed = urlparse(self.upstream)
230
+ headers["Host"] = parsed.netloc
231
+ return headers
232
+
233
+ def _relay_response(self, status: int, headers: dict, body: bytes) -> None:
234
+ """Write upstream response back to the Codex CLI connection."""
235
+ self.send_response(status)
236
+ for key, val in headers.items():
237
+ if key.lower() not in ("transfer-encoding", "connection"):
238
+ self.send_header(key, val)
239
+ self.send_header("Content-Length", str(len(body)))
240
+ self.end_headers()
241
+ if body:
242
+ self.wfile.write(body)
243
+
244
+ def _send_json(self, status: int, payload: dict) -> None:
245
+ body = json.dumps(payload).encode("utf-8")
246
+ self.send_response(status)
247
+ self.send_header("Content-Type", "application/json")
248
+ self.send_header("Content-Length", str(len(body)))
249
+ self.end_headers()
250
+ self.wfile.write(body)
251
+
252
+ def _send_error_502(self, detail: str) -> None:
253
+ logger.warning("thinkstack codex proxy 502 — %s", detail)
254
+ self._send_json(502, {"error": "bad_gateway", "detail": detail})
255
+
256
+
257
+ # ---------------------------------------------------------------------------
258
+ # Background post_execute helper
259
+ # ---------------------------------------------------------------------------
260
+
261
+ def _safe_post_execute(
262
+ handler: object,
263
+ request_body: dict,
264
+ response_body: dict,
265
+ session_id: str,
266
+ ) -> None:
267
+ try:
268
+ handler.post_execute(request_body, response_body, session_id) # type: ignore[attr-defined]
269
+ except Exception as exc:
270
+ logger.warning("thinkstack codex post_execute background error — %s", exc)
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # CodexProxy
275
+ # ---------------------------------------------------------------------------
276
+
277
+ class CodexProxy:
278
+ """
279
+ Lightweight stdlib-only HTTP proxy for intercepting Codex CLI traffic.
280
+
281
+ Parameters
282
+ ----------
283
+ port:
284
+ Local port to bind on (default 8766).
285
+ upstream:
286
+ Base URL of the real OpenAI API (default https://api.openai.com).
287
+ """
288
+
289
+ def __init__(
290
+ self,
291
+ port: int = DEFAULT_PORT,
292
+ upstream: str = DEFAULT_UPSTREAM,
293
+ ) -> None:
294
+ self.port = port
295
+ self.upstream = upstream
296
+ self._server: Optional[HTTPServer] = None
297
+ self._thread: Optional[threading.Thread] = None
298
+
299
+ def start(self) -> None:
300
+ """Start the proxy server (blocking — press Ctrl+C to stop)."""
301
+ self._server = self._make_server()
302
+ try:
303
+ self._server.serve_forever()
304
+ except KeyboardInterrupt:
305
+ pass
306
+ finally:
307
+ self._server.server_close()
308
+
309
+ def start_background(self) -> threading.Thread:
310
+ """
311
+ Start the proxy in a daemon thread.
312
+
313
+ Returns the thread (already started). Call stop() to shut down.
314
+ """
315
+ self._server = self._make_server()
316
+ t = threading.Thread(target=self._server.serve_forever, daemon=True)
317
+ t.start()
318
+ self._thread = t
319
+ return t
320
+
321
+ def stop(self) -> None:
322
+ """Shut down the proxy server gracefully."""
323
+ if self._server is not None:
324
+ self._server.shutdown()
325
+ self._server.server_close()
326
+ self._server = None
327
+
328
+ # ------------------------------------------------------------------
329
+ # Internal
330
+ # ------------------------------------------------------------------
331
+
332
+ def _make_server(self) -> HTTPServer:
333
+ upstream = self.upstream
334
+
335
+ class _Handler(_CodexRequestHandler):
336
+ pass
337
+
338
+ _Handler.upstream = upstream # type: ignore[assignment]
339
+ server = HTTPServer(("127.0.0.1", self.port), _Handler)
340
+ server.timeout = 30
341
+ return server
342
+
343
+
344
+ # ---------------------------------------------------------------------------
345
+ # install_codex_hook
346
+ # ---------------------------------------------------------------------------
347
+
348
+ def install_codex_hook(project_root: str) -> tuple[bool, str]:
349
+ """
350
+ Create `.codex/config.json` under *project_root* pointing at the local
351
+ ThinkStack Codex proxy.
352
+
353
+ Idempotent — does not overwrite an existing config.
354
+
355
+ Returns (True, config_path) on success, (False, error_message) on failure.
356
+ """
357
+ try:
358
+ codex_dir = Path(project_root) / ".codex"
359
+ codex_dir.mkdir(parents=True, exist_ok=True)
360
+
361
+ config_path = codex_dir / "config.json"
362
+
363
+ # Idempotent — do not overwrite if already present
364
+ if config_path.exists():
365
+ return True, str(config_path)
366
+
367
+ config = {
368
+ "apiBase": f"http://localhost:{DEFAULT_PORT}/v1",
369
+ "provider": "openai",
370
+ }
371
+ config_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
372
+ return True, str(config_path)
373
+
374
+ except Exception as exc:
375
+ return False, str(exc)
376
+
377
+
378
+ # ---------------------------------------------------------------------------
379
+ # get_codex_hook_status
380
+ # ---------------------------------------------------------------------------
381
+
382
+ def get_codex_hook_status(project_root: str) -> dict:
383
+ """
384
+ Return the install and runtime status of the ThinkStack Codex proxy.
385
+
386
+ Returns::
387
+
388
+ {
389
+ "codex_config": bool, # .codex/config.json exists
390
+ "codex_config_path": str, # absolute path to config file
391
+ "proxy_running": bool, # True if GET /health returns 200
392
+ }
393
+ """
394
+ config_path = Path(project_root) / ".codex" / "config.json"
395
+ codex_config = config_path.exists()
396
+
397
+ proxy_running = False
398
+ try:
399
+ req = urllib.request.Request(
400
+ f"http://localhost:{DEFAULT_PORT}/health",
401
+ method="GET",
402
+ )
403
+ with urllib.request.urlopen(req, timeout=2) as resp:
404
+ proxy_running = resp.status == 200
405
+ except Exception:
406
+ proxy_running = False
407
+
408
+ return {
409
+ "codex_config": codex_config,
410
+ "codex_config_path": str(config_path),
411
+ "proxy_running": proxy_running,
412
+ }
@@ -0,0 +1,103 @@
1
+ """
2
+ Backward-compatibility shims for the thinkstack → ThinkStack rename.
3
+
4
+ Provides fallbacks for deprecated environment variables, MCP tool names,
5
+ and auth headers so existing users can migrate without breaking.
6
+ Shims emit deprecation warnings and will be removed in v5.0.0.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import warnings
12
+ from pathlib import Path
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Environment variable shims
17
+ # ---------------------------------------------------------------------------
18
+
19
+ _thinkstack_TO_THINKSTACK = {
20
+ "THINKSTACK_DISABLE": "thinkstack_DISABLE",
21
+ "THINKSTACK_MCP_URL": "thinkstack_MCP_URL",
22
+ "THINKSTACK_API_KEY": "thinkstack_API_KEY",
23
+ "THINKSTACK_MCP_DOMAIN": "thinkstack_MCP_DOMAIN",
24
+ "THINKSTACK_ORG_ID": "thinkstack_ORG_ID",
25
+ "THINKSTACK_GCC_PATH": "thinkstack_GCC_PATH",
26
+ "THINKSTACK_REGISTRY_PATH": "thinkstack_REGISTRY_PATH",
27
+ "THINKSTACK_SIGN_LOG": "thinkstack_SIGN_LOG",
28
+ "THINKSTACK_STORAGE": "thinkstack_STORAGE",
29
+ "THINKSTACK_BUCKET": "thinkstack_BUCKET",
30
+ "THINKSTACK_ACCESS_KEY_ID": "thinkstack_ACCESS_KEY_ID",
31
+ "THINKSTACK_SECRET_ACCESS_KEY": "thinkstack_SECRET_ACCESS_KEY",
32
+ "THINKSTACK_AZURE_CONTAINER": "thinkstack_AZURE_CONTAINER",
33
+ "THINKSTACK_SESSION_CLOUD_URL": "thinkstack_SESSION_CLOUD_URL",
34
+ "THINKSTACK_AGGPHI": "thinkstack_AGGPHI",
35
+ "THINKSTACK_AGGPHI_LLM": "thinkstack_AGGPHI_LLM",
36
+ "THINKSTACK_AGGPHI_MODEL": "thinkstack_AGGPHI_MODEL",
37
+ "THINKSTACK_JWKS_URLS": "thinkstack_JWKS_URLS",
38
+ "THINKSTACK_METRICS_WEBHOOK_URL": "thinkstack_METRICS_WEBHOOK_URL",
39
+ "THINKSTACK_DATADOG_API_KEY": "thinkstack_DATADOG_API_KEY",
40
+ "THINKSTACK_SPLUNK_HEC_URL": "thinkstack_SPLUNK_HEC_URL",
41
+ "THINKSTACK_SERVICENOW_INSTANCE": "thinkstack_SERVICENOW_INSTANCE",
42
+ "THINKSTACK_GITHUB_WEBHOOK_SECRET": "thinkstack_GITHUB_WEBHOOK_SECRET",
43
+ "THINKSTACK_DAEMON_ROOT": "thinkstack_DAEMON_ROOT",
44
+ "THINKSTACK_AGENT_ID": "thinkstack_AGENT_ID",
45
+ "THINKSTACK_AGENT_NAME": "thinkstack_AGENT_NAME",
46
+ "THINKSTACK_AGENT_SCOPE": "thinkstack_AGENT_SCOPE",
47
+ "THINKSTACK_PAT_ENCRYPTION_KEY": "thinkstack_PAT_ENCRYPTION_KEY",
48
+ }
49
+
50
+
51
+ def getenv(
52
+ name: str, default: str | None = None, *, _warned: set[str] | None = None
53
+ ) -> str | None:
54
+ """Read an environment variable with backward-compat fallback.
55
+
56
+ Tries *name* first (e.g. ``THINKSTACK_MCP_URL``). If unset, falls
57
+ back to the old ``thinkstack_*`` name and issues a deprecation warning.
58
+ """
59
+ value = os.environ.get(name)
60
+ if value is not None:
61
+ return value
62
+
63
+ old_name = _thinkstack_TO_THINKSTACK.get(name)
64
+ if old_name is not None:
65
+ old_value = os.environ.get(old_name)
66
+ if old_value is not None:
67
+ if _warned is None or old_name not in _warned:
68
+ warnings.warn(
69
+ f"{old_name} is deprecated, use {name} instead",
70
+ DeprecationWarning,
71
+ stacklevel=2,
72
+ )
73
+ if _warned is not None:
74
+ _warned.add(old_name)
75
+ return old_value
76
+
77
+ return default
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Local runtime directory auto-migration
82
+ # ---------------------------------------------------------------------------
83
+
84
+ _OLD_thinkstack_DIR = Path.home() / ".thinkstack"
85
+ _NEW_THINKSTACK_DIR = Path.home() / ".thinkstack"
86
+
87
+
88
+ def migrate_runtime_dir() -> None:
89
+ """Move ``~/.thinkstack/`` to ``~/.thinkstack/`` if the old dir exists
90
+ and the new one doesn't. Called once at import time."""
91
+ if _OLD_thinkstack_DIR.exists() and not _NEW_THINKSTACK_DIR.exists():
92
+ import shutil
93
+ shutil.move(str(_OLD_thinkstack_DIR), str(_NEW_THINKSTACK_DIR))
94
+ warnings.warn(
95
+ f"Moved {_OLD_thinkstack_DIR} → {_NEW_THINKSTACK_DIR} "
96
+ "(thinkstack is now thinkstack)",
97
+ DeprecationWarning,
98
+ stacklevel=2,
99
+ )
100
+
101
+
102
+ # Run auto-migration on import
103
+ migrate_runtime_dir()