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,357 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ThinkStack MCP stdio ↔ SSE bridge.
4
+ *
5
+ * Allows any stdio-only MCP client (Claude Code, VS Code, Cursor, OpenCode in
6
+ * stdio/local mode) to connect to the Cloudflare (or any) ThinkStack SSE MCP
7
+ * server.
8
+ *
9
+ * Resilience: for the ThinkStack write tools (`thinkstack_commit` and
10
+ * `thinkstack_sensitivity_add`), the bridge will automatically fall back to the
11
+ * local `thinkstack` CLI if the cloud server returns an error or does not answer
12
+ * within the configured timeout. This keeps the local `.GCC/` audit trail
13
+ * intact even when the cloud endpoint is unreachable or slow.
14
+ *
15
+ * Usage:
16
+ * THINKSTACK_MCP_URL=https://thinkstack-mcp.example.com/myorg/myrepo/sse \
17
+ * THINKSTACK_API_KEY=... \
18
+ * node thinkstack-mcp-bridge.js
19
+ *
20
+ * Optional:
21
+ * THINKSTACK_MCP_FALLBACK_TIMEOUT_MS=3000 (default: 8000)
22
+ *
23
+ * Then configure the IDE with:
24
+ * { "type": "local", "command": ["node", "/path/to/thinkstack-mcp-bridge.js"] }
25
+ */
26
+
27
+ const { spawn } = require("child_process");
28
+ const { TextDecoder } = require("util");
29
+
30
+ const SSE_URL = process.env.THINKSTACK_MCP_URL;
31
+ const API_KEY = process.env.THINKSTACK_API_KEY;
32
+ const FALLBACK_TIMEOUT_MS = parseInt(
33
+ process.env.THINKSTACK_MCP_FALLBACK_TIMEOUT_MS || "8000",
34
+ 10
35
+ );
36
+ const FALLBACK_TOOLS = new Set(["thinkstack_commit", "thinkstack_sensitivity_add"]);
37
+
38
+ if (!SSE_URL || !API_KEY) {
39
+ console.error("Missing THINKSTACK_MCP_URL or THINKSTACK_API_KEY");
40
+ process.exit(1);
41
+ }
42
+
43
+ let messageUrl = null;
44
+ let sessionId = null;
45
+
46
+ // Requests we are waiting on and that require transparent local fallback.
47
+ const pending = new Map(); // id -> { resolve, reject, timer, req }
48
+
49
+ // Requests that were already handled locally (fallback or late response).
50
+ // We keep this small set so a late cloud response does not leak to the client.
51
+ const handledIds = new Set();
52
+
53
+ async function main() {
54
+ const headers = { "X-ThinkStack-Key": API_KEY };
55
+
56
+ const response = await fetch(SSE_URL, { headers });
57
+ if (!response.ok) {
58
+ console.error(
59
+ `SSE connection failed: ${response.status} ${response.statusText}`
60
+ );
61
+ process.exit(1);
62
+ }
63
+
64
+ const reader = response.body.getReader();
65
+ const decoder = new TextDecoder();
66
+ let buffer = "";
67
+
68
+ // Read incoming SSE events and either resolve a pending fallback request or
69
+ // forward the JSON-RPC payload to stdout.
70
+ (async () => {
71
+ while (true) {
72
+ const { done, value } = await reader.read();
73
+ if (done) break;
74
+ buffer += decoder.decode(value, { stream: true });
75
+ const lines = buffer.split("\n\n");
76
+ buffer = lines.pop() || "";
77
+ for (const chunk of lines) {
78
+ const event = parseEvent(chunk);
79
+ if (!event) continue;
80
+ if (event.type === "endpoint") {
81
+ messageUrl = event.data;
82
+ const url = new URL(messageUrl);
83
+ sessionId = url.searchParams.get("sessionId");
84
+ } else if (event.type === "message") {
85
+ handleMessage(event.data);
86
+ }
87
+ }
88
+ }
89
+ })();
90
+
91
+ // Read JSON-RPC requests from stdin and POST them to the messages endpoint.
92
+ const stdin = process.stdin;
93
+ stdin.setEncoding("utf8");
94
+ let inBuffer = "";
95
+ stdin.on("data", async (data) => {
96
+ inBuffer += data;
97
+ const lines = inBuffer.split("\n");
98
+ inBuffer = lines.pop() || "";
99
+ for (const line of lines) {
100
+ if (!line.trim()) continue;
101
+ await handleStdinLine(line);
102
+ }
103
+ });
104
+
105
+ stdin.on("end", () => {
106
+ process.exit(0);
107
+ });
108
+ }
109
+
110
+ function handleMessage(data) {
111
+ let json;
112
+ try {
113
+ json = JSON.parse(data);
114
+ } catch (e) {
115
+ process.stdout.write(data + "\n");
116
+ return;
117
+ }
118
+
119
+ const id = json.id;
120
+ if (id !== undefined && handledIds.has(id)) {
121
+ // Late response after we already handled the request locally. Drop it.
122
+ return;
123
+ }
124
+
125
+ if (id !== undefined && pending.has(id)) {
126
+ const p = pending.get(id);
127
+ clearTimeout(p.timer);
128
+ pending.delete(id);
129
+ p.resolve({ data, json });
130
+ return;
131
+ }
132
+
133
+ process.stdout.write(data + "\n");
134
+ }
135
+
136
+ async function handleStdinLine(line) {
137
+ let req;
138
+ try {
139
+ req = JSON.parse(line);
140
+ } catch (e) {
141
+ console.error(`Invalid JSON-RPC request: ${line}`);
142
+ return;
143
+ }
144
+
145
+ if (!messageUrl) {
146
+ console.error("Message endpoint not yet received from SSE");
147
+ return;
148
+ }
149
+
150
+ const toolName = req.method === "tools/call" && req.params && req.params.name;
151
+ if (toolName && FALLBACK_TOOLS.has(toolName)) {
152
+ await handleWithFallback(req, line);
153
+ } else {
154
+ await forwardRequest(line);
155
+ }
156
+ }
157
+
158
+ async function handleWithFallback(req, line) {
159
+ const id = req.id;
160
+ if (id === undefined) {
161
+ // Notification-style tool call; forward without waiting.
162
+ await forwardRequest(line);
163
+ return;
164
+ }
165
+
166
+ // Register the pending entry BEFORE the POST: the server may deliver the
167
+ // SSE response while the POST is still in flight (its 202 only returns
168
+ // after the tool has executed, which can exceed the fallback timeout).
169
+ const responsePromise = new Promise((resolve, reject) => {
170
+ pending.set(id, { resolve, reject, timer: null, req });
171
+ });
172
+ // A timeout rejection must never become an unhandled rejection while we
173
+ // are still awaiting the POST — that kills the whole bridge process and
174
+ // the IDE sees the MCP connection drop (issue #18).
175
+ responsePromise.catch(() => {});
176
+
177
+ try {
178
+ await forwardRequest(line);
179
+ } catch (e) {
180
+ pending.delete(id);
181
+ handledIds.add(id);
182
+ const result = await runLocalFallback(req);
183
+ process.stdout.write(
184
+ JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n"
185
+ );
186
+ return;
187
+ }
188
+
189
+ // The server accepted the request; the SSE response normally follows
190
+ // within moments. Only now start the fallback timer, so it measures
191
+ // response delivery — not tool execution time.
192
+ const entry = pending.get(id);
193
+ if (entry) {
194
+ entry.timer = setTimeout(() => {
195
+ const q = pending.get(id);
196
+ if (q) {
197
+ pending.delete(id);
198
+ handledIds.add(id);
199
+ q.reject(new Error("MCP timeout"));
200
+ }
201
+ }, FALLBACK_TIMEOUT_MS);
202
+ }
203
+
204
+ try {
205
+ const { data, json } = await responsePromise;
206
+ if (json.error) {
207
+ pending.delete(id);
208
+ handledIds.add(id);
209
+ throw new Error(
210
+ `MCP error: ${json.error.message || JSON.stringify(json.error)}`
211
+ );
212
+ }
213
+ pending.delete(id);
214
+ process.stdout.write(data + "\n");
215
+ } catch (e) {
216
+ if (!handledIds.has(id)) {
217
+ handledIds.add(id);
218
+ }
219
+ pending.delete(id);
220
+ const result = await runLocalFallback(req);
221
+ process.stdout.write(
222
+ JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n"
223
+ );
224
+ }
225
+ }
226
+
227
+ async function forwardRequest(line) {
228
+ const res = await fetch(messageUrl, {
229
+ method: "POST",
230
+ headers: {
231
+ "Content-Type": "application/json",
232
+ "X-ThinkStack-Key": API_KEY,
233
+ },
234
+ body: line,
235
+ });
236
+ if (!res.ok) {
237
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
238
+ }
239
+ }
240
+
241
+ async function runLocalFallback(req) {
242
+ const args = (req.params && req.params.arguments) || {};
243
+ const toolName = req.params.name;
244
+ const cwd = process.env.THINKSTACK_GCC_PATH || process.cwd();
245
+
246
+ try {
247
+ const { exitCode, stdout, stderr } = await runLocalCommand(cwd, toolName, args);
248
+ if (exitCode !== 0) {
249
+ return {
250
+ content: [
251
+ {
252
+ type: "text",
253
+ text: `Local fallback failed (${exitCode}): ${stderr || stdout || "unknown error"}`,
254
+ },
255
+ ],
256
+ isError: true,
257
+ };
258
+ }
259
+ return {
260
+ content: [
261
+ {
262
+ type: "text",
263
+ text: stdout.trim() || "Recorded locally",
264
+ },
265
+ ],
266
+ isError: false,
267
+ };
268
+ } catch (e) {
269
+ return {
270
+ content: [
271
+ {
272
+ type: "text",
273
+ text: `Local fallback error: ${e.message}`,
274
+ },
275
+ ],
276
+ isError: true,
277
+ };
278
+ }
279
+ }
280
+
281
+ function runLocalCommand(cwd, toolName, args) {
282
+ return new Promise((resolve) => {
283
+ let cmd;
284
+ let cliArgs;
285
+
286
+ if (toolName === "thinkstack_commit") {
287
+ cmd = "thinkstack";
288
+ cliArgs = ["commit", "-m", String(args.message || "")];
289
+ // Note: the local CLI derives the branch from git; the optional `branch`
290
+ // parameter in the MCP tool is intentionally ignored for local fallback.
291
+ } else if (toolName === "thinkstack_sensitivity_add") {
292
+ cmd = "thinkstack";
293
+ const signal = String(args.signal || "");
294
+ cliArgs = [
295
+ "sensitivity",
296
+ "add",
297
+ "--source",
298
+ "mcp-fallback",
299
+ "--concept",
300
+ String(args.concept || ""),
301
+ "--confidence",
302
+ String(args.confidence || 0),
303
+ ];
304
+ if (signal) {
305
+ cliArgs.push("--counterfactuals", signal, "--message", signal);
306
+ }
307
+ const disclosure = String(
308
+ args.disclosure || args.disclosure_level || "PROTECTED"
309
+ ).toUpperCase();
310
+ if (["PUBLIC", "PROTECTED", "PRIVATE"].includes(disclosure)) {
311
+ cliArgs.push("--disclosure", disclosure);
312
+ }
313
+ } else {
314
+ return resolve({
315
+ exitCode: 1,
316
+ stdout: "",
317
+ stderr: `Unknown tool: ${toolName}`,
318
+ });
319
+ }
320
+
321
+ const proc = spawn(cmd, cliArgs, { cwd, env: process.env });
322
+ let stdout = "";
323
+ let stderr = "";
324
+ proc.stdout.on("data", (d) => {
325
+ stdout += d.toString();
326
+ });
327
+ proc.stderr.on("data", (d) => {
328
+ stderr += d.toString();
329
+ });
330
+ proc.on("close", (exitCode) => {
331
+ resolve({ exitCode: exitCode ?? 1, stdout, stderr });
332
+ });
333
+ proc.on("error", (err) => {
334
+ resolve({ exitCode: 1, stdout: "", stderr: err.message });
335
+ });
336
+ });
337
+ }
338
+
339
+ function parseEvent(text) {
340
+ const lines = text.split("\n");
341
+ let type = null;
342
+ let data = null;
343
+ for (const line of lines) {
344
+ if (line.startsWith("event:")) {
345
+ type = line.slice(6).trim();
346
+ } else if (line.startsWith("data:")) {
347
+ data = line.slice(5).trim();
348
+ }
349
+ }
350
+ if (!type || data === null) return null;
351
+ return { type, data };
352
+ }
353
+
354
+ main().catch((e) => {
355
+ console.error(e);
356
+ process.exit(1);
357
+ });
@@ -0,0 +1,9 @@
1
+ from thinkstack_core.codex.proxy import CodexProxy, install_codex_hook, get_codex_hook_status
2
+ from thinkstack_core.codex.capture import CodexCaptureHandler
3
+
4
+ __all__ = [
5
+ "CodexProxy",
6
+ "CodexCaptureHandler",
7
+ "install_codex_hook",
8
+ "get_codex_hook_status",
9
+ ]
@@ -0,0 +1,97 @@
1
+ """
2
+ Entry point for: python -m thinkstack_core.codex
3
+
4
+ Usage:
5
+ python -m thinkstack_core.codex start [--port PORT]
6
+ python -m thinkstack_core.codex install [--project PATH]
7
+ python -m thinkstack_core.codex status [--project PATH]
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import sys
14
+
15
+
16
+ def _cmd_start(args: argparse.Namespace) -> None:
17
+ from thinkstack_core.codex.proxy import CodexProxy
18
+
19
+ port: int = args.port
20
+ print(f"ThinkStack Codex proxy running on http://localhost:{port}")
21
+ print(f"Configure Codex CLI: set OPENAI_BASE_URL=http://localhost:{port}/v1")
22
+
23
+ proxy = CodexProxy(port=port)
24
+ proxy.start() # blocks
25
+
26
+
27
+ def _cmd_install(args: argparse.Namespace) -> None:
28
+ import os
29
+ from thinkstack_core.codex.proxy import install_codex_hook
30
+
31
+ project = args.project or os.getcwd()
32
+ ok, result = install_codex_hook(project)
33
+ if ok:
34
+ print(f"Codex config installed: {result}")
35
+ else:
36
+ print(f"Install failed: {result}", file=sys.stderr)
37
+ sys.exit(1)
38
+
39
+
40
+ def _cmd_status(args: argparse.Namespace) -> None:
41
+ import os
42
+ from thinkstack_core.codex.proxy import get_codex_hook_status
43
+
44
+ project = args.project or os.getcwd()
45
+ status = get_codex_hook_status(project)
46
+ print(json.dumps(status, indent=2))
47
+
48
+
49
+ def main() -> None:
50
+ parser = argparse.ArgumentParser(
51
+ prog="python -m thinkstack_core.codex",
52
+ description="ThinkStack OpenAI Codex CLI proxy",
53
+ )
54
+ sub = parser.add_subparsers(dest="command")
55
+
56
+ # start
57
+ start_p = sub.add_parser("start", help="Start the Codex proxy (blocking)")
58
+ start_p.add_argument(
59
+ "--port",
60
+ type=int,
61
+ default=8766,
62
+ help="Port to listen on (default: 8766)",
63
+ )
64
+
65
+ # install
66
+ install_p = sub.add_parser(
67
+ "install", help="Install .codex/config.json for the current project"
68
+ )
69
+ install_p.add_argument(
70
+ "--project",
71
+ default=None,
72
+ help="Project root directory (default: cwd)",
73
+ )
74
+
75
+ # status
76
+ status_p = sub.add_parser("status", help="Show proxy and hook install status")
77
+ status_p.add_argument(
78
+ "--project",
79
+ default=None,
80
+ help="Project root directory (default: cwd)",
81
+ )
82
+
83
+ args = parser.parse_args()
84
+
85
+ if args.command == "start":
86
+ _cmd_start(args)
87
+ elif args.command == "install":
88
+ _cmd_install(args)
89
+ elif args.command == "status":
90
+ _cmd_status(args)
91
+ else:
92
+ parser.print_help()
93
+ sys.exit(1)
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
@@ -0,0 +1,208 @@
1
+ """
2
+ thinkstack_core.codex.capture
3
+ ===========================
4
+ Handles RACP prefix injection and reasoning capture for the Codex CLI proxy.
5
+
6
+ CodexCaptureHandler:
7
+ - pre_execute — injects RACP system prefix into the request body before
8
+ forwarding to OpenAI.
9
+ - post_execute — fires CaptureOrchestrator and appends a POST_TOOL_USE event
10
+ to .GCC/ after the response arrives.
11
+ - find_gcc_repo — walks cwd upward to find an initialised .GCC/ directory.
12
+
13
+ All methods degrade silently; never raises.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import threading
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from thinkstack_core.wrapper.base import (
23
+ CaptureOrchestrator,
24
+ RACPInjector,
25
+ is_disabled,
26
+ )
27
+
28
+ logger = logging.getLogger("thinkstack.codex.capture")
29
+
30
+
31
+ class CodexCaptureHandler:
32
+ """Injects RACP context into Codex CLI requests and captures reasoning."""
33
+
34
+ # ------------------------------------------------------------------
35
+ # Public API
36
+ # ------------------------------------------------------------------
37
+
38
+ def pre_execute(self, request_body: dict, session_id: str) -> dict:
39
+ """
40
+ Inject the RACP system prefix into *request_body* before forwarding.
41
+
42
+ Returns the modified request_body with a system message prepended, or
43
+ the original request_body unchanged if:
44
+ - THINKSTACK_DISABLE is set, or
45
+ - no initialised .GCC/ repo is found, or
46
+ - any error occurs.
47
+ """
48
+ if is_disabled():
49
+ return request_body
50
+
51
+ try:
52
+ repo = self.find_gcc_repo()
53
+ if repo is None:
54
+ return request_body
55
+
56
+ injector = RACPInjector(repo)
57
+ prefix = injector.build_system_prefix()
58
+ if not prefix:
59
+ return request_body
60
+
61
+ import copy
62
+ modified = copy.deepcopy(request_body)
63
+ messages = modified.get("messages", [])
64
+
65
+ # Prepend a system message with the RACP prefix
66
+ racp_msg = {"role": "system", "content": prefix}
67
+ # If the first message is already a system message, prepend our
68
+ # prefix to its content rather than inserting a second system msg
69
+ if messages and messages[0].get("role") == "system":
70
+ existing_content = messages[0].get("content", "")
71
+ messages[0]["content"] = prefix + "\n\n" + existing_content
72
+ else:
73
+ messages.insert(0, racp_msg)
74
+
75
+ modified["messages"] = messages
76
+ return modified
77
+
78
+ except Exception as exc:
79
+ logger.warning("thinkstack codex pre_execute error — %s", exc)
80
+ return request_body
81
+
82
+ def post_execute(
83
+ self,
84
+ request_body: dict,
85
+ response_body: dict,
86
+ session_id: str,
87
+ ) -> None:
88
+ """
89
+ Capture reasoning from the response into .GCC/ and append a
90
+ POST_TOOL_USE event to the event log.
91
+
92
+ Silent on all errors — never raises.
93
+ """
94
+ if is_disabled():
95
+ return
96
+
97
+ try:
98
+ repo = self.find_gcc_repo()
99
+ if repo is None:
100
+ return
101
+
102
+ # Extract response text from OpenAI chat completion format
103
+ response_text = _extract_response_text(response_body)
104
+ thinking_blocks: list = []
105
+
106
+ # Fire CaptureOrchestrator
107
+ orchestrator = CaptureOrchestrator(repo)
108
+ orchestrator.capture(response_text, thinking_blocks, session_id)
109
+
110
+ # Append POST_TOOL_USE event
111
+ _append_post_tool_use_event(repo, session_id, request_body)
112
+
113
+ except Exception as exc:
114
+ logger.warning("thinkstack codex post_execute error — %s", exc)
115
+
116
+ def find_gcc_repo(self) -> Any | None:
117
+ """
118
+ Walk cwd upward looking for an initialised .GCC/ directory.
119
+ Returns a GCCRepository instance or None if not found.
120
+ """
121
+ try:
122
+ from thinkstack_core import GCCRepository
123
+ except ImportError:
124
+ logger.warning("thinkstack: thinkstack_core not importable — codex wrapper disabled")
125
+ return None
126
+
127
+ for parent in [Path.cwd()] + list(Path.cwd().parents):
128
+ try:
129
+ candidate = GCCRepository.at(parent)
130
+ if candidate.is_initialized():
131
+ return candidate
132
+ except Exception:
133
+ continue
134
+ return None
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Module-level helpers
139
+ # ---------------------------------------------------------------------------
140
+
141
+ def _extract_response_text(response_body: dict) -> str:
142
+ """
143
+ Extract text content from an OpenAI chat completion response body.
144
+ Returns empty string on any error.
145
+ """
146
+ try:
147
+ choices = response_body.get("choices", [])
148
+ if not choices:
149
+ return ""
150
+ first_choice = choices[0]
151
+ message = first_choice.get("message", {})
152
+ content = message.get("content", "")
153
+ return content or ""
154
+ except Exception:
155
+ return ""
156
+
157
+
158
+ def _append_post_tool_use_event(repo: Any, session_id: str, request_body: dict) -> None:
159
+ """
160
+ Append a POST_TOOL_USE event to the repo's event log.
161
+ Mirrors the pattern in thinkstack_core.hooks.runner.handle_post_tool_use.
162
+ """
163
+ try:
164
+ import json
165
+ import hashlib
166
+ import time
167
+ from pathlib import Path as _Path
168
+
169
+ payload = {
170
+ "tool_name": "codex_cli",
171
+ "session_id": session_id,
172
+ "model": request_body.get("model", "unknown"),
173
+ }
174
+
175
+ # Try the public _append_event method first
176
+ try:
177
+ repo._append_event("POST_TOOL_USE", payload)
178
+ return
179
+ except Exception:
180
+ pass
181
+
182
+ # Fallback: write directly to events.log.jsonl
183
+ log_path = _Path(repo.gcc_dir) / "events.log.jsonl"
184
+ prev_hash = "0" * 64
185
+ if log_path.exists():
186
+ lines = log_path.read_text(encoding="utf-8").strip().splitlines()
187
+ if lines:
188
+ try:
189
+ last = json.loads(lines[-1])
190
+ prev_hash = last.get("hash", prev_hash)
191
+ except Exception:
192
+ pass
193
+
194
+ entry = {
195
+ "type": "POST_TOOL_USE",
196
+ "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
197
+ **payload,
198
+ }
199
+ entry_str = json.dumps(entry, sort_keys=True)
200
+ entry["hash"] = hashlib.sha256(
201
+ (prev_hash + entry_str).encode()
202
+ ).hexdigest()
203
+
204
+ with log_path.open("a", encoding="utf-8") as f:
205
+ f.write(json.dumps(entry) + "\n")
206
+
207
+ except Exception as exc:
208
+ logger.warning("thinkstack codex append_event failed — %s", exc)