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,229 @@
1
+ """
2
+ thinkstack_core.cloud.mcp_client
3
+ ==============================
4
+ Reusable client for calling remote ThinkStack MCP tools via JSON-RPC over SSE.
5
+
6
+ The cloud MCP server accepts tool requests via POST to the messages endpoint
7
+ and returns the JSON-RPC results as SSE ``message`` events. This client keeps
8
+ the SSE connection open in a background reader thread, posts requests, and
9
+ matches responses by request ID.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ import threading
17
+ import time
18
+ import urllib.request
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ logger = logging.getLogger("thinkstack.cloud.mcp_client")
23
+
24
+ _USER_AGENT = "thinkstack-mcp-client"
25
+
26
+
27
+ class McpClient:
28
+ """Call remote ThinkStack MCP tools via JSON-RPC over SSE.
29
+
30
+ Usage::
31
+
32
+ client = McpClient(url, api_key)
33
+ result = client.call_tool("thinkstack_topics_list", {"all": True})
34
+ client.close()
35
+ """
36
+
37
+ def __init__(self, url: str, api_key: str, timeout: float = 10.0) -> None:
38
+ self._sse_url = url
39
+ self._api_key = api_key
40
+ self._timeout = timeout
41
+ self._messages_url: str | None = None
42
+ self._resp: Any | None = None
43
+ self._reader_thread: threading.Thread | None = None
44
+ self._results: dict[int | str, dict] = {}
45
+ self._lock = threading.Lock()
46
+ self._cv = threading.Condition(self._lock)
47
+ self._next_id = 1
48
+ self._closed = False
49
+
50
+ def __enter__(self) -> "McpClient":
51
+ return self
52
+
53
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
54
+ self.close()
55
+
56
+ def call_tool(self, tool_name: str, args: dict | None = None) -> str:
57
+ """Call a remote MCP tool and return the result text.
58
+
59
+ Raises ``RuntimeError`` on network errors, timeouts, or MCP-level errors.
60
+ """
61
+ self._ensure_connected()
62
+ req_id = self._next_id
63
+ self._next_id += 1
64
+
65
+ payload = {
66
+ "jsonrpc": "2.0",
67
+ "id": req_id,
68
+ "method": "tools/call",
69
+ "params": {"name": tool_name, "arguments": args or {}},
70
+ }
71
+
72
+ self._post(payload)
73
+ body = self._wait_for_response(req_id)
74
+
75
+ if body.get("error"):
76
+ raise RuntimeError(f"MCP error: {json.dumps(body['error'])[:300]}")
77
+ if body.get("result", {}).get("isError"):
78
+ raise RuntimeError(f"MCP tool error: {json.dumps(body['result'])[:300]}")
79
+
80
+ content = body.get("result", {}).get("content", [])
81
+ if content and isinstance(content, list):
82
+ return content[0].get("text", "")
83
+ return json.dumps(body.get("result", {}))
84
+
85
+ def is_available(self) -> bool:
86
+ """Quick connectivity check — returns True if the SSE handshake succeeds."""
87
+ try:
88
+ self._ensure_connected()
89
+ return self._messages_url is not None
90
+ except Exception:
91
+ return False
92
+
93
+ def close(self) -> None:
94
+ """Close the SSE connection and stop the reader thread."""
95
+ self._closed = True
96
+ resp = self._resp
97
+ self._resp = None
98
+ if resp is not None:
99
+ try:
100
+ resp.close()
101
+ except Exception:
102
+ pass
103
+ with self._cv:
104
+ self._cv.notify_all()
105
+ reader = self._reader_thread
106
+ self._reader_thread = None
107
+ if reader is not None and reader.is_alive():
108
+ reader.join(timeout=2.0)
109
+
110
+ def _ensure_connected(self) -> None:
111
+ """Open the SSE connection and start the reader thread if needed."""
112
+ if self._closed:
113
+ raise RuntimeError("McpClient is closed")
114
+ if self._resp is not None and self._messages_url is not None:
115
+ return
116
+
117
+ req = urllib.request.Request(
118
+ self._sse_url,
119
+ headers={
120
+ "Accept": "text/event-stream",
121
+ "X-ThinkStack-Key": self._api_key,
122
+ "User-Agent": _USER_AGENT,
123
+ },
124
+ )
125
+ self._resp = urllib.request.urlopen(req, timeout=self._timeout)
126
+ self._reader_thread = threading.Thread(target=self._read_loop, daemon=True)
127
+ self._reader_thread.start()
128
+
129
+ # Wait for the endpoint event to set the messages URL.
130
+ deadline = time.time() + self._timeout
131
+ with self._cv:
132
+ while self._messages_url is None:
133
+ remaining = deadline - time.time()
134
+ if remaining <= 0:
135
+ raise RuntimeError("SSE endpoint event not received in time")
136
+ self._cv.wait(remaining)
137
+
138
+ def _read_loop(self) -> None:
139
+ """Background thread: parse SSE events and store message responses."""
140
+ event_type: str | None = None
141
+ data: str | None = None
142
+ try:
143
+ while self._resp is not None:
144
+ line = self._resp.readline()
145
+ if not line:
146
+ break
147
+ line = line.decode("utf-8", errors="replace")
148
+ if line.startswith("event:"):
149
+ event_type = line[6:].strip()
150
+ elif line.startswith("data:"):
151
+ data = line[5:].strip()
152
+ elif line.strip() == "":
153
+ self._handle_event(event_type, data)
154
+ event_type = None
155
+ data = None
156
+ except Exception as exc:
157
+ logger.debug("SSE read loop ended: %s", exc)
158
+ finally:
159
+ self._resp = None
160
+ self._messages_url = None
161
+
162
+ def _handle_event(self, event_type: str | None, data: str | None) -> None:
163
+ """Process a single SSE event."""
164
+ if event_type == "endpoint" and data:
165
+ with self._cv:
166
+ self._messages_url = data
167
+ self._cv.notify_all()
168
+ elif event_type == "message" and data:
169
+ try:
170
+ body = json.loads(data)
171
+ except json.JSONDecodeError:
172
+ return
173
+ req_id = body.get("id")
174
+ if req_id is None:
175
+ return
176
+ with self._cv:
177
+ self._results[req_id] = body
178
+ self._cv.notify_all()
179
+
180
+ def _post(self, payload: dict) -> None:
181
+ """POST a JSON-RPC request to the messages endpoint."""
182
+ if self._messages_url is None:
183
+ raise RuntimeError("Message endpoint not available")
184
+ req = urllib.request.Request(
185
+ self._messages_url,
186
+ data=json.dumps(payload).encode(),
187
+ headers={
188
+ "Content-Type": "application/json",
189
+ "X-ThinkStack-Key": self._api_key,
190
+ "User-Agent": _USER_AGENT,
191
+ },
192
+ )
193
+ with urllib.request.urlopen(req, timeout=self._timeout) as resp:
194
+ # The server returns 202 Accepted; the actual response arrives via SSE.
195
+ pass
196
+
197
+ def _wait_for_response(self, req_id: int) -> dict:
198
+ """Wait for the JSON-RPC response matching ``req_id``."""
199
+ deadline = time.time() + self._timeout
200
+ with self._cv:
201
+ while req_id not in self._results:
202
+ remaining = deadline - time.time()
203
+ if remaining <= 0:
204
+ raise RuntimeError(f"MCP call timed out waiting for response {req_id}")
205
+ self._cv.wait(remaining)
206
+ return self._results.pop(req_id)
207
+
208
+
209
+ def detect_mcp_config(project_root: Path) -> tuple[str, str] | None:
210
+ """Check env vars then .mcp.json for MCP server URL and API key.
211
+
212
+ Returns ``(url, api_key)`` or ``None`` if not configured.
213
+ """
214
+ url = os.environ.get("THINKSTACK_MCP_URL", "").strip()
215
+ api_key = os.environ.get("THINKSTACK_API_KEY", "").strip()
216
+
217
+ if not url or not api_key:
218
+ mcp_path = project_root / ".mcp.json"
219
+ try:
220
+ data = json.loads(mcp_path.read_text(encoding="utf-8"))
221
+ env_cfg = data.get("mcpServers", {}).get("thinkstack", {}).get("env", {})
222
+ url = url or env_cfg.get("THINKSTACK_MCP_URL", "").strip()
223
+ api_key = api_key or env_cfg.get("THINKSTACK_API_KEY", "").strip()
224
+ except (OSError, json.JSONDecodeError, KeyError, TypeError):
225
+ pass
226
+
227
+ if url and api_key:
228
+ return (url, api_key)
229
+ return None
@@ -0,0 +1,144 @@
1
+ """One-command Cloudflare Worker deployment for ThinkStack MCP server.
2
+
3
+ This module wraps ``deploy/cloudflare-worker/setup-mcp-server.sh`` so it can be
4
+ called as ``thinkstack cloud setup``. It is intentionally thin: the shell script
5
+ still owns the npm/wrangler lifecycle; Python only validates CLI arguments and
6
+ forwards them.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Sequence
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Constants
19
+ # ---------------------------------------------------------------------------
20
+
21
+ _PACKAGE_DIR = Path(__file__).resolve().parent
22
+ _REPO_ROOT = _PACKAGE_DIR.parents[1]
23
+ SETUP_SCRIPT = _REPO_ROOT / "deploy" / "cloudflare-worker" / "setup-mcp-server.sh"
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Public API
28
+ # ---------------------------------------------------------------------------
29
+
30
+
31
+ def run_cloud_setup(
32
+ *,
33
+ env_file: str | Path | None = None,
34
+ org: str | None = None,
35
+ repo: str | None = None,
36
+ ide: str | None = None,
37
+ openai_key: str | None = None,
38
+ non_interactive: bool = False,
39
+ skip_key: bool = False,
40
+ skip_client_config: bool = False,
41
+ dry_run: bool = False,
42
+ ) -> dict[str, str | list[str]]:
43
+ """Build and optionally run the Cloudflare Worker setup command.
44
+
45
+ Args:
46
+ env_file: Path to an environment file to source before running the script.
47
+ org: ThinkStack org ID. Required in non-interactive mode.
48
+ repo: ThinkStack repo ID. Required in non-interactive mode.
49
+ ide: IDE to configure (opencode, claude, cursor, vscode, stdio).
50
+ openai_key: Optional OpenAI API key for the DRPL embedding backend.
51
+ non_interactive: Run without prompts (requires ``org`` and ``repo``).
52
+ skip_key: Skip creating a new API key.
53
+ skip_client_config: Skip installing MCP client config.
54
+ dry_run: If True, return the command without executing it.
55
+
56
+ Returns:
57
+ A dict with ``script`` and ``args`` (the constructed argument list).
58
+ """
59
+ if not SETUP_SCRIPT.exists():
60
+ raise FileNotFoundError(
61
+ f"Setup script not found: {SETUP_SCRIPT}. "
62
+ "Is the ThinkStack repo checkout complete?"
63
+ )
64
+
65
+ if non_interactive and (not org or not repo):
66
+ raise ValueError("--non-interactive requires --org and --repo")
67
+
68
+ if non_interactive and not skip_client_config and not ide:
69
+ raise ValueError("--non-interactive requires --ide unless --skip-client-config is set")
70
+
71
+ args: list[str] = [str(SETUP_SCRIPT)]
72
+ if env_file:
73
+ args.extend(["--env-file", str(env_file)])
74
+ if non_interactive:
75
+ args.append("--non-interactive")
76
+ if org:
77
+ args.extend(["--org", org])
78
+ if repo:
79
+ args.extend(["--repo", repo])
80
+ if ide:
81
+ args.extend(["--ide", ide])
82
+ if openai_key:
83
+ args.extend(["--openai-key", openai_key])
84
+ if skip_key:
85
+ args.append("--skip-key")
86
+ if skip_client_config:
87
+ args.append("--skip-client-config")
88
+
89
+ if dry_run:
90
+ return {"script": str(SETUP_SCRIPT), "args": args}
91
+
92
+ result = subprocess.run(args, capture_output=False, text=True, check=False)
93
+ return {
94
+ "script": str(SETUP_SCRIPT),
95
+ "args": args,
96
+ "returncode": str(result.returncode),
97
+ }
98
+
99
+
100
+ def main(argv: Sequence[str] | None = None) -> int:
101
+ """CLI entry point used by ``thinkstack cloud setup``."""
102
+ import argparse
103
+
104
+ parser = argparse.ArgumentParser(
105
+ description="Deploy a self-hosted ThinkStack MCP server on Cloudflare Workers.",
106
+ )
107
+ parser.add_argument("--env-file", help="Path to environment file to source")
108
+ parser.add_argument("--org", help="ThinkStack org ID")
109
+ parser.add_argument("--repo", help="ThinkStack repo ID")
110
+ parser.add_argument("--ide", choices=["opencode", "claude", "cursor", "vscode", "stdio"],
111
+ help="IDE to configure locally")
112
+ parser.add_argument("--openai-key", help="OpenAI API key for DRPL embedding backend")
113
+ parser.add_argument("--non-interactive", action="store_true",
114
+ help="Run without prompts (requires --org and --repo)")
115
+ parser.add_argument("--skip-key", action="store_true", help="Skip creating API key")
116
+ parser.add_argument("--skip-client-config", action="store_true", help="Skip IDE config")
117
+ parser.add_argument("--dry-run", action="store_true", help="Print command instead of running")
118
+ args = parser.parse_args(argv)
119
+
120
+ try:
121
+ result = run_cloud_setup(
122
+ env_file=args.env_file,
123
+ org=args.org,
124
+ repo=args.repo,
125
+ ide=args.ide,
126
+ openai_key=args.openai_key,
127
+ non_interactive=args.non_interactive,
128
+ skip_key=args.skip_key,
129
+ skip_client_config=args.skip_client_config,
130
+ dry_run=args.dry_run,
131
+ )
132
+ except (FileNotFoundError, ValueError) as exc:
133
+ print(f"error: {exc}", file=sys.stderr)
134
+ return 1
135
+
136
+ if args.dry_run:
137
+ print(" ".join(result["args"]))
138
+ return 0
139
+
140
+ return int(result.get("returncode", "0"))
141
+
142
+
143
+ if __name__ == "__main__":
144
+ raise SystemExit(main())
@@ -0,0 +1,143 @@
1
+ """One-way local -> cloud reasoning sync.
2
+
3
+ Pushes local .GCC commits to the ThinkStack cloud MCP worker so that
4
+ cloud-connected agents' `thinkstack_context` sees reasoning recorded by
5
+ local-only agents (hooks / CLI). Uses the worker's direct-POST fallback:
6
+ POST /messages without an SSE session returns the JSON-RPC result in the
7
+ response body.
8
+
9
+ Transport hardening: if the direct-POST path fails, we open an SSE
10
+ handshake once, capture the live endpoint URL, and retry through that
11
+ session-bound endpoint. The SSE-derived URL is cached for all subsequent
12
+ commits in the same sync run.
13
+
14
+ Cursor: .GCC/cloud_sync_state.json {"last_pushed_index": N}. Commit ids are
15
+ sequential hex (COMMIT_INDEX), so the cursor is a simple integer.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import urllib.error
22
+ import urllib.request
23
+ from pathlib import Path
24
+
25
+ _STATE_FILE = "cloud_sync_state.json"
26
+ _USER_AGENT = "thinkstack-cloud-sync"
27
+
28
+ _SSE_ENDPOINT_CACHE: str | None = None
29
+
30
+
31
+ def _sse_handshake(sse_url: str, api_key: str, timeout: float) -> str:
32
+ """Open an SSE connection, capture the endpoint URL, and return it."""
33
+ req = urllib.request.Request(
34
+ sse_url,
35
+ headers={
36
+ "Accept": "text/event-stream",
37
+ "X-ThinkStack-Key": api_key,
38
+ "User-Agent": _USER_AGENT,
39
+ },
40
+ )
41
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
42
+ # Read line-by-line so the chunked HTTP reader does not block waiting
43
+ # for the next chunk after the endpoint event (the server keeps the SSE
44
+ # connection open for subsequent messages).
45
+ event_type = None
46
+ data = None
47
+ while True:
48
+ line = resp.readline()
49
+ if not line:
50
+ break
51
+ line = line.decode("utf-8", errors="replace")
52
+ if line.startswith("event:"):
53
+ event_type = line[6:].strip()
54
+ elif line.startswith("data:"):
55
+ data = line[5:].strip()
56
+ elif line.strip() == "":
57
+ if event_type == "endpoint" and data:
58
+ return data
59
+ event_type = None
60
+ data = None
61
+ raise RuntimeError("SSE endpoint event not received")
62
+
63
+
64
+ def sync_commits(
65
+ gcc_dir: Path,
66
+ messages_url: str,
67
+ api_key: str,
68
+ timeout: float = 30.0,
69
+ from_now: bool = False,
70
+ ) -> dict:
71
+ gcc_dir = Path(gcc_dir)
72
+ if not gcc_dir.is_dir():
73
+ raise ValueError(f"{gcc_dir} is not an initialized .GCC repository")
74
+ try:
75
+ top = int((gcc_dir / "COMMIT_INDEX").read_text(encoding="utf-8").strip() or "0")
76
+ except (OSError, ValueError):
77
+ top = 0
78
+
79
+ state_path = gcc_dir / _STATE_FILE
80
+ try:
81
+ cursor = int(json.loads(state_path.read_text(encoding="utf-8"))["last_pushed_index"])
82
+ except (OSError, ValueError, KeyError, json.JSONDecodeError):
83
+ cursor = 0
84
+
85
+ if cursor > top:
86
+ cursor = 0
87
+
88
+ if from_now:
89
+ state_path.write_text(
90
+ json.dumps({"last_pushed_index": top}) + "\n", encoding="utf-8"
91
+ )
92
+ return {"pushed": 0, "skipped": top, "cursor": top}
93
+
94
+ global _SSE_ENDPOINT_CACHE
95
+ pushed = 0
96
+ for index in range(cursor + 1, top + 1):
97
+ commit_id = format(index, "08x")
98
+ commit_path = gcc_dir / "commits" / f"{commit_id}.json"
99
+ try:
100
+ obj = json.loads(commit_path.read_text(encoding="utf-8"))
101
+ except (OSError, json.JSONDecodeError):
102
+ continue
103
+ message = (
104
+ f"[synced from local {commit_id} @ {obj.get('timestamp', 'unknown')}] "
105
+ f"{obj.get('message', '')}"
106
+ )
107
+ try:
108
+ _post_commit(messages_url, api_key, index, message, timeout)
109
+ except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError):
110
+ if _SSE_ENDPOINT_CACHE is None:
111
+ sse_url = messages_url.rsplit("/", 1)[0] + "/sse"
112
+ _SSE_ENDPOINT_CACHE = _sse_handshake(sse_url, api_key, timeout)
113
+ _post_commit(_SSE_ENDPOINT_CACHE, api_key, index, message, timeout)
114
+ pushed += 1
115
+ state_path.write_text(
116
+ json.dumps({"last_pushed_index": index}) + "\n", encoding="utf-8"
117
+ )
118
+
119
+ return {"pushed": pushed, "skipped": cursor, "cursor": max(cursor, top)}
120
+
121
+
122
+ def _post_commit(
123
+ messages_url: str, api_key: str, rpc_id: int, message: str, timeout: float
124
+ ) -> None:
125
+ payload = {
126
+ "jsonrpc": "2.0",
127
+ "id": rpc_id,
128
+ "method": "tools/call",
129
+ "params": {"name": "thinkstack_commit", "arguments": {"message": message}},
130
+ }
131
+ req = urllib.request.Request(
132
+ messages_url,
133
+ data=json.dumps(payload).encode(),
134
+ headers={
135
+ "Content-Type": "application/json",
136
+ "X-ThinkStack-Key": api_key,
137
+ "User-Agent": _USER_AGENT,
138
+ },
139
+ )
140
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
141
+ body = json.loads(resp.read().decode())
142
+ if body.get("error") or body.get("result", {}).get("isError"):
143
+ raise RuntimeError(f"cloud commit failed: {json.dumps(body)[:200]}")