@trac3er/oh-my-god 2.0.0 → 2.0.2

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 (243) hide show
  1. package/.claude-plugin/marketplace.json +8 -8
  2. package/.claude-plugin/plugin.json +5 -4
  3. package/.claude-plugin/scripts/uninstall.sh +74 -3
  4. package/.claude-plugin/scripts/update.sh +78 -3
  5. package/.coveragerc +26 -0
  6. package/.mcp.json +4 -4
  7. package/CHANGELOG.md +14 -0
  8. package/CODE_OF_CONDUCT.md +27 -0
  9. package/CONTRIBUTING.md +62 -0
  10. package/OMG-setup.sh +1201 -355
  11. package/README.md +77 -56
  12. package/SECURITY.md +25 -0
  13. package/agents/__init__.py +1 -0
  14. package/agents/model_roles.py +196 -0
  15. package/agents/omg-architect-mode.md +3 -5
  16. package/agents/omg-backend-engineer.md +3 -5
  17. package/agents/omg-database-engineer.md +3 -5
  18. package/agents/omg-frontend-designer.md +4 -5
  19. package/agents/omg-implement-mode.md +4 -5
  20. package/agents/omg-infra-engineer.md +3 -5
  21. package/agents/omg-research-mode.md +4 -6
  22. package/agents/omg-security-auditor.md +3 -5
  23. package/agents/omg-testing-engineer.md +3 -5
  24. package/build/lib/yaml.py +321 -0
  25. package/commands/OMG:ai-commit.md +101 -14
  26. package/commands/OMG:arch.md +302 -19
  27. package/commands/OMG:ccg.md +12 -7
  28. package/commands/OMG:compat.md +25 -17
  29. package/commands/OMG:cost.md +173 -13
  30. package/commands/OMG:crazy.md +1 -1
  31. package/commands/OMG:create-agent.md +170 -20
  32. package/commands/OMG:deps.md +235 -17
  33. package/commands/OMG:domain-init.md +1 -1
  34. package/commands/OMG:escalate.md +41 -12
  35. package/commands/OMG:health-check.md +37 -13
  36. package/commands/OMG:init.md +122 -14
  37. package/commands/OMG:project-init.md +1 -1
  38. package/commands/OMG:session-branch.md +76 -9
  39. package/commands/OMG:session-fork.md +42 -5
  40. package/commands/OMG:session-merge.md +124 -8
  41. package/commands/OMG:setup.md +69 -12
  42. package/commands/OMG:stats.md +215 -14
  43. package/commands/OMG:teams.md +19 -10
  44. package/config/lsp_languages.yaml +8 -0
  45. package/hooks/__init__.py +0 -0
  46. package/hooks/_agent_registry.py +423 -0
  47. package/hooks/_analytics.py +291 -0
  48. package/hooks/_budget.py +31 -0
  49. package/hooks/_common.py +569 -0
  50. package/hooks/_compression_optimizer.py +119 -0
  51. package/hooks/_cost_ledger.py +176 -0
  52. package/hooks/_learnings.py +126 -0
  53. package/hooks/_memory.py +103 -0
  54. package/hooks/_protected_context.py +150 -0
  55. package/hooks/_token_counter.py +221 -0
  56. package/hooks/branch_manager.py +236 -0
  57. package/hooks/budget_governor.py +232 -0
  58. package/hooks/circuit-breaker.py +270 -0
  59. package/hooks/compression_feedback.py +254 -0
  60. package/hooks/config-guard.py +216 -0
  61. package/hooks/context_pressure.py +53 -0
  62. package/hooks/credential_store.py +1020 -0
  63. package/hooks/fetch-rate-limits.py +212 -0
  64. package/hooks/firewall.py +48 -0
  65. package/hooks/hashline-formatter-bridge.py +224 -0
  66. package/hooks/hashline-injector.py +273 -0
  67. package/hooks/hashline-validator.py +216 -0
  68. package/hooks/idle-detector.py +95 -0
  69. package/hooks/intentgate-keyword-detector.py +188 -0
  70. package/hooks/magic-keyword-router.py +195 -0
  71. package/hooks/policy_engine.py +505 -0
  72. package/hooks/post-tool-failure.py +19 -0
  73. package/hooks/post-write.py +219 -0
  74. package/hooks/post_write.py +46 -0
  75. package/hooks/pre-compact.py +398 -0
  76. package/hooks/pre-tool-inject.py +98 -0
  77. package/hooks/prompt-enhancer.py +672 -0
  78. package/hooks/quality-runner.py +191 -0
  79. package/hooks/query.py +512 -0
  80. package/hooks/secret-guard.py +61 -0
  81. package/hooks/secret_audit.py +144 -0
  82. package/hooks/session-end-capture.py +137 -0
  83. package/hooks/session-start.py +277 -0
  84. package/hooks/setup_wizard.py +582 -0
  85. package/hooks/shadow_manager.py +297 -0
  86. package/hooks/state_migration.py +225 -0
  87. package/hooks/stop-gate.py +7 -0
  88. package/hooks/stop_dispatcher.py +945 -0
  89. package/hooks/test-validator.py +361 -0
  90. package/hooks/test_generator_hook.py +123 -0
  91. package/hooks/todo-state-tracker.py +114 -0
  92. package/hooks/tool-ledger.py +149 -0
  93. package/hooks/trust_review.py +585 -0
  94. package/hud/omg-hud.mjs +31 -1
  95. package/lab/__init__.py +1 -0
  96. package/lab/pipeline.py +75 -0
  97. package/lab/policies.py +52 -0
  98. package/package.json +7 -18
  99. package/plugins/README.md +33 -61
  100. package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
  101. package/plugins/advanced/commands/OMG:learn.md +1 -1
  102. package/plugins/advanced/commands/OMG:security-review.md +3 -3
  103. package/plugins/advanced/commands/OMG:ship.md +1 -1
  104. package/plugins/advanced/plugin.json +1 -1
  105. package/plugins/core/plugin.json +8 -3
  106. package/plugins/dephealth/__init__.py +0 -0
  107. package/plugins/dephealth/cve_scanner.py +188 -0
  108. package/plugins/dephealth/license_checker.py +135 -0
  109. package/plugins/dephealth/manifest_detector.py +423 -0
  110. package/plugins/dephealth/vuln_analyzer.py +169 -0
  111. package/plugins/testgen/__init__.py +0 -0
  112. package/plugins/testgen/codamosa_engine.py +402 -0
  113. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  114. package/plugins/testgen/framework_detector.py +271 -0
  115. package/plugins/testgen/skeleton_generator.py +219 -0
  116. package/plugins/viz/__init__.py +0 -0
  117. package/plugins/viz/ast_parser.py +139 -0
  118. package/plugins/viz/diagram_generator.py +192 -0
  119. package/plugins/viz/graph_builder.py +444 -0
  120. package/plugins/viz/native_parsers.py +259 -0
  121. package/plugins/viz/regex_parser.py +112 -0
  122. package/pyproject.toml +81 -0
  123. package/rules/contextual/write-verify.md +2 -2
  124. package/rules/core/00-truth.md +1 -1
  125. package/rules/core/01-surgical.md +1 -1
  126. package/rules/core/02-circuit-breaker.md +2 -2
  127. package/rules/core/03-ensemble.md +3 -3
  128. package/rules/core/04-testing.md +3 -3
  129. package/runtime/__init__.py +32 -0
  130. package/runtime/adapters/__init__.py +13 -0
  131. package/runtime/adapters/claude.py +60 -0
  132. package/runtime/adapters/gpt.py +53 -0
  133. package/runtime/adapters/local.py +53 -0
  134. package/runtime/adoption.py +212 -0
  135. package/runtime/business_workflow.py +220 -0
  136. package/runtime/cli_provider.py +85 -0
  137. package/runtime/compat.py +1299 -0
  138. package/runtime/custom_agent_loader.py +366 -0
  139. package/runtime/dispatcher.py +47 -0
  140. package/runtime/ecosystem.py +371 -0
  141. package/runtime/legacy_compat.py +7 -0
  142. package/runtime/mcp_config_writers.py +115 -0
  143. package/runtime/mcp_lifecycle.py +153 -0
  144. package/runtime/mcp_memory_server.py +135 -0
  145. package/runtime/memory_parsers/__init__.py +0 -0
  146. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  147. package/runtime/memory_parsers/claude_import.py +107 -0
  148. package/runtime/memory_parsers/export.py +97 -0
  149. package/runtime/memory_parsers/gemini_import.py +91 -0
  150. package/runtime/memory_parsers/kimi_import.py +91 -0
  151. package/runtime/memory_store.py +215 -0
  152. package/runtime/omc_compat.py +7 -0
  153. package/runtime/providers/__init__.py +0 -0
  154. package/runtime/providers/codex_provider.py +112 -0
  155. package/runtime/providers/gemini_provider.py +128 -0
  156. package/runtime/providers/kimi_provider.py +151 -0
  157. package/runtime/providers/opencode_provider.py +144 -0
  158. package/runtime/subagent_dispatcher.py +362 -0
  159. package/runtime/team_router.py +1167 -0
  160. package/runtime/tmux_session_manager.py +169 -0
  161. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  162. package/scripts/check-omg-contract-snapshot.py +12 -0
  163. package/scripts/check-omg-public-ready.py +193 -0
  164. package/scripts/check-omg-standalone-clean.py +103 -0
  165. package/scripts/legacy_to_omg_migrate.py +29 -0
  166. package/scripts/migrate-legacy.py +464 -0
  167. package/scripts/omc_to_omg_migrate.py +12 -0
  168. package/scripts/omg.py +492 -0
  169. package/scripts/settings-merge.py +283 -0
  170. package/scripts/verify-standalone.sh +8 -4
  171. package/settings.json +126 -29
  172. package/templates/profile.yaml +1 -1
  173. package/tools/__init__.py +2 -0
  174. package/tools/browser_consent.py +289 -0
  175. package/tools/browser_stealth.py +481 -0
  176. package/tools/browser_tool.py +448 -0
  177. package/tools/changelog_generator.py +347 -0
  178. package/tools/commit_splitter.py +746 -0
  179. package/tools/config_discovery.py +151 -0
  180. package/tools/config_merger.py +449 -0
  181. package/tools/dashboard_generator.py +300 -0
  182. package/tools/git_inspector.py +298 -0
  183. package/tools/lsp_client.py +275 -0
  184. package/tools/lsp_discovery.py +231 -0
  185. package/tools/lsp_operations.py +392 -0
  186. package/tools/pr_generator.py +404 -0
  187. package/tools/python_repl.py +656 -0
  188. package/tools/python_sandbox.py +609 -0
  189. package/tools/search_providers/__init__.py +77 -0
  190. package/tools/search_providers/brave.py +115 -0
  191. package/tools/search_providers/exa.py +116 -0
  192. package/tools/search_providers/jina.py +104 -0
  193. package/tools/search_providers/perplexity.py +139 -0
  194. package/tools/search_providers/synthetic.py +74 -0
  195. package/tools/session_snapshot.py +736 -0
  196. package/tools/ssh_manager.py +912 -0
  197. package/tools/theme_engine.py +294 -0
  198. package/tools/theme_selector.py +137 -0
  199. package/tools/web_search.py +622 -0
  200. package/yaml.py +321 -0
  201. package/.claude-plugin/scripts/install.sh +0 -9
  202. package/bun.lock +0 -23
  203. package/bunfig.toml +0 -3
  204. package/hooks/_budget.ts +0 -1
  205. package/hooks/_common.ts +0 -63
  206. package/hooks/circuit-breaker.ts +0 -101
  207. package/hooks/config-guard.ts +0 -4
  208. package/hooks/firewall.ts +0 -20
  209. package/hooks/policy_engine.ts +0 -156
  210. package/hooks/post-tool-failure.ts +0 -22
  211. package/hooks/post-write.ts +0 -4
  212. package/hooks/pre-tool-inject.ts +0 -4
  213. package/hooks/prompt-enhancer.ts +0 -46
  214. package/hooks/quality-runner.ts +0 -24
  215. package/hooks/secret-guard.ts +0 -4
  216. package/hooks/session-end-capture.ts +0 -19
  217. package/hooks/session-start.ts +0 -19
  218. package/hooks/shadow_manager.ts +0 -81
  219. package/hooks/stop-gate.ts +0 -22
  220. package/hooks/stop_dispatcher.ts +0 -147
  221. package/hooks/test-generator-hook.ts +0 -4
  222. package/hooks/tool-ledger.ts +0 -27
  223. package/hooks/trust_review.ts +0 -175
  224. package/lab/pipeline.ts +0 -75
  225. package/lab/policies.ts +0 -68
  226. package/runtime/common.ts +0 -111
  227. package/runtime/compat.ts +0 -174
  228. package/runtime/dispatcher.ts +0 -25
  229. package/runtime/ecosystem.ts +0 -186
  230. package/runtime/provider_bootstrap.ts +0 -99
  231. package/runtime/provider_smoke.ts +0 -34
  232. package/runtime/release_readiness.ts +0 -186
  233. package/runtime/team_router.ts +0 -144
  234. package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
  235. package/scripts/check-omg-standalone-clean.ts +0 -12
  236. package/scripts/check-runtime-clean.ts +0 -94
  237. package/scripts/omg.ts +0 -352
  238. package/scripts/settings-merge.ts +0 -93
  239. package/tools/commit_splitter.ts +0 -23
  240. package/tools/git_inspector.ts +0 -18
  241. package/tools/session_snapshot.ts +0 -47
  242. package/trac3er-oh-my-god-2.0.0.tgz +0 -0
  243. package/tsconfig.json +0 -15
@@ -0,0 +1,656 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ IPython Kernel Integration for OMG
4
+
5
+ Provides persistent REPL sessions with IPython kernel support (optional)
6
+ and stdlib fallback via code.InteractiveConsole.
7
+
8
+ Feature flag: OMG_PYTHON_REPL_ENABLED (default: False)
9
+ """
10
+
11
+ import ast
12
+ import code
13
+ import contextlib
14
+ import io
15
+ import json
16
+ import os
17
+ import sys
18
+ import traceback
19
+ import uuid
20
+ from datetime import datetime, timezone
21
+ from typing import Any, Dict, Generator, List, Optional, Union
22
+
23
+
24
+ # --- Lazy imports for hooks/_common.py ---
25
+
26
+ _get_feature_flag = None
27
+ _atomic_json_write = None
28
+
29
+
30
+ def _ensure_imports():
31
+ """Lazy import feature flag and atomic write from hooks/_common.py."""
32
+ global _get_feature_flag, _atomic_json_write
33
+ if _get_feature_flag is not None:
34
+ return
35
+ repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
36
+ if repo_root not in sys.path:
37
+ sys.path.insert(0, repo_root)
38
+ try:
39
+ from hooks._common import get_feature_flag as _gff
40
+ from hooks._common import atomic_json_write as _ajw
41
+ _get_feature_flag = _gff
42
+ _atomic_json_write = _ajw
43
+ except ImportError:
44
+ pass
45
+
46
+
47
+ # --- Optional jupyter_client ---
48
+
49
+ _jupyter_client = None
50
+ _HAS_JUPYTER: Optional[bool] = None
51
+
52
+
53
+ def _check_jupyter() -> bool:
54
+ """Check if jupyter_client is available (cached after first check)."""
55
+ global _HAS_JUPYTER, _jupyter_client
56
+ if _HAS_JUPYTER is None:
57
+ try:
58
+ import jupyter_client as _jc
59
+ _jupyter_client = _jc
60
+ _HAS_JUPYTER = True
61
+ except ImportError:
62
+ _HAS_JUPYTER = False
63
+ return _HAS_JUPYTER
64
+
65
+
66
+ # --- Feature flag ---
67
+
68
+ def _is_enabled() -> bool:
69
+ """Check if Python REPL feature is enabled."""
70
+ # Fast path: check env var directly
71
+ env_val = os.environ.get("OMG_PYTHON_REPL_ENABLED", "").lower()
72
+ if env_val in ("0", "false", "no"):
73
+ return False
74
+ if env_val in ("1", "true", "yes"):
75
+ return True
76
+ # Fallback to hooks/_common.get_feature_flag
77
+ _ensure_imports()
78
+ if _get_feature_flag is not None:
79
+ return _get_feature_flag("PYTHON_REPL", default=False)
80
+ return False
81
+
82
+
83
+ def _get_sandbox_flag() -> bool:
84
+ """Check if sandbox mode is enabled for the REPL."""
85
+ env_val = os.environ.get("OMG_REPL_SANDBOX_ENABLED", "").lower()
86
+ if env_val in ("0", "false", "no"):
87
+ return False
88
+ if env_val in ("1", "true", "yes"):
89
+ return True
90
+ _ensure_imports()
91
+ if _get_feature_flag is not None:
92
+ return _get_feature_flag("REPL_SANDBOX", default=False)
93
+ return False
94
+
95
+
96
+ def _get_helpers_flag() -> bool:
97
+ """Check if REPL prelude helpers are enabled."""
98
+ env_val = os.environ.get("OMG_REPL_HELPERS_ENABLED", "").lower()
99
+ if env_val in ("0", "false", "no"):
100
+ return False
101
+ if env_val in ("1", "true", "yes"):
102
+ return True
103
+ _ensure_imports()
104
+ if _get_feature_flag is not None:
105
+ return _get_feature_flag("REPL_HELPERS", default=False)
106
+ return False
107
+
108
+
109
+ def _build_prelude_namespace() -> dict:
110
+ """Build the prelude namespace with helper functions for REPL sessions.
111
+
112
+ Returns a dict of helper functions injected into every session when
113
+ OMG_REPL_HELPERS_ENABLED=true. All helpers use stdlib only and handle
114
+ exceptions gracefully.
115
+ """
116
+ import re as _re
117
+
118
+ def read_file(path: str) -> str:
119
+ """Read file content. Returns empty string on error."""
120
+ try:
121
+ with open(path, "r") as f:
122
+ return f.read()
123
+ except Exception:
124
+ return ""
125
+
126
+ def write_file(path: str, content: str) -> bool:
127
+ """Write content to file. Blocked in sandbox mode. Returns False on error."""
128
+ if _get_sandbox_flag():
129
+ return False
130
+ try:
131
+ with open(path, "w") as f:
132
+ f.write(content)
133
+ return True
134
+ except Exception:
135
+ return False
136
+
137
+ def lines(path: str) -> list:
138
+ """Read file lines as list. Returns empty list on error."""
139
+ try:
140
+ with open(path, "r") as f:
141
+ return f.read().splitlines()
142
+ except Exception:
143
+ return []
144
+
145
+ def search_code(pattern: str, path: str = ".", ext=None) -> list:
146
+ """Grep-like search across files. Returns list of {file, line, match} dicts."""
147
+ results = []
148
+ try:
149
+ compiled = _re.compile(pattern)
150
+ for root, _dirs, files in os.walk(path):
151
+ for fname in files:
152
+ if ext is not None and not fname.endswith(ext):
153
+ continue
154
+ fpath = os.path.join(root, fname)
155
+ try:
156
+ with open(fpath, "r", errors="ignore") as f:
157
+ for lineno, line_text in enumerate(f, 1):
158
+ if compiled.search(line_text):
159
+ results.append({
160
+ "file": fpath,
161
+ "line": lineno,
162
+ "match": line_text.rstrip(),
163
+ })
164
+ except Exception:
165
+ continue
166
+ except Exception:
167
+ pass
168
+ return results
169
+
170
+ def grep(pattern: str, text: str) -> list:
171
+ """Regex grep on a string. Returns matching lines."""
172
+ try:
173
+ compiled = _re.compile(pattern)
174
+ return [line for line in text.splitlines() if compiled.search(line)]
175
+ except Exception:
176
+ return []
177
+
178
+ def insert_at(lines_list: list, index: int, new_line: str) -> list:
179
+ """Insert a line at index. Returns new list."""
180
+ try:
181
+ result = list(lines_list)
182
+ result.insert(index, new_line)
183
+ return result
184
+ except Exception:
185
+ return list(lines_list)
186
+
187
+ def delete_lines(lines_list: list, start: int, end: int) -> list:
188
+ """Delete lines from start to end (exclusive). Returns new list."""
189
+ try:
190
+ result = list(lines_list)
191
+ del result[start:end]
192
+ return result
193
+ except Exception:
194
+ return list(lines_list)
195
+
196
+ return {
197
+ "read_file": read_file,
198
+ "write_file": write_file,
199
+ "lines": lines,
200
+ "search_code": search_code,
201
+ "grep": grep,
202
+ "insert_at": insert_at,
203
+ "delete_lines": delete_lines,
204
+ }
205
+
206
+ _DISABLED_MSG = "Python REPL feature is disabled. Set OMG_PYTHON_REPL_ENABLED=true"
207
+
208
+
209
+ # --- Session storage ---
210
+
211
+ _sessions: Dict[str, Dict[str, Any]] = {}
212
+ _STATE_DIR = ".omg/state/repl_sessions"
213
+
214
+
215
+ def _now_iso() -> str:
216
+ """Current UTC time as ISO-8601 string."""
217
+ return datetime.now(timezone.utc).isoformat()
218
+
219
+
220
+ def _persist_session(session_id: str) -> None:
221
+ """Persist session metadata to disk (best-effort)."""
222
+ if session_id not in _sessions:
223
+ return
224
+ _ensure_imports()
225
+ if _atomic_json_write is None:
226
+ return
227
+ session = _sessions[session_id]
228
+ meta = {
229
+ "session_id": session["session_id"],
230
+ "created_at": session["created_at"],
231
+ "last_used": session["last_used"],
232
+ "exec_count": session["exec_count"],
233
+ "backend": session.get("backend", "stdlib"),
234
+ }
235
+ path = os.path.join(_STATE_DIR, f"{session_id}.json")
236
+ try:
237
+ _atomic_json_write(path, meta)
238
+ except Exception:
239
+ pass # best-effort
240
+
241
+
242
+ def _session_info(session: Dict[str, Any]) -> Dict[str, Any]:
243
+ """Extract public session info (no internal _backend key)."""
244
+ return {
245
+ "session_id": session["session_id"],
246
+ "created_at": session["created_at"],
247
+ "last_used": session["last_used"],
248
+ "exec_count": session["exec_count"],
249
+ "backend": session.get("backend", "stdlib"),
250
+ }
251
+
252
+
253
+ # --- IPython Kernel Backend ---
254
+
255
+ class _IPythonSession:
256
+ """Wraps a jupyter_client kernel for code execution."""
257
+
258
+ def __init__(self):
259
+ km, kc = _jupyter_client.manager.start_new_kernel(kernel_name="python3")
260
+ self.kernel_manager = km
261
+ self.kernel_client = kc
262
+ self.kernel_client.start_channels()
263
+ self.kernel_client.wait_for_ready(timeout=30)
264
+
265
+ def execute(self, code_str: str) -> Dict[str, Any]:
266
+ """Execute code on the IPython kernel and collect output."""
267
+ msg_id = self.kernel_client.execute(code_str)
268
+ stdout_parts: List[str] = []
269
+ stderr_parts: List[str] = []
270
+ result = None
271
+ error = None
272
+
273
+ while True:
274
+ try:
275
+ msg = self.kernel_client.get_iopub_msg(timeout=30)
276
+ except Exception:
277
+ break
278
+ if msg["parent_header"].get("msg_id") != msg_id:
279
+ continue
280
+ msg_type = msg["msg_type"]
281
+ content = msg["content"]
282
+ if msg_type == "stream":
283
+ if content["name"] == "stdout":
284
+ stdout_parts.append(content["text"])
285
+ elif content["name"] == "stderr":
286
+ stderr_parts.append(content["text"])
287
+ elif msg_type in ("execute_result", "display_data"):
288
+ result = content["data"].get("text/plain", "")
289
+ elif msg_type == "error":
290
+ tb = content.get("traceback", [content.get("evalue", "")])
291
+ error = "\n".join(tb)
292
+ elif msg_type == "status" and content.get("execution_state") == "idle":
293
+ break
294
+
295
+ return {
296
+ "stdout": "".join(stdout_parts),
297
+ "stderr": "".join(stderr_parts),
298
+ "result": result,
299
+ "error": error,
300
+ }
301
+
302
+ def stream_execute(self, code_str: str) -> Generator[Dict[str, str], None, None]:
303
+ """Execute code on the kernel and yield output chunks."""
304
+ msg_id = self.kernel_client.execute(code_str)
305
+ while True:
306
+ try:
307
+ msg = self.kernel_client.get_iopub_msg(timeout=30)
308
+ except Exception:
309
+ break
310
+ if msg["parent_header"].get("msg_id") != msg_id:
311
+ continue
312
+ msg_type = msg["msg_type"]
313
+ content = msg["content"]
314
+ if msg_type == "stream":
315
+ yield {"type": content["name"], "data": content["text"]}
316
+ elif msg_type in ("execute_result", "display_data"):
317
+ yield {"type": "result", "data": content["data"].get("text/plain", "")}
318
+ elif msg_type == "error":
319
+ tb = content.get("traceback", [content.get("evalue", "")])
320
+ yield {"type": "error", "data": "\n".join(tb)}
321
+ elif msg_type == "status" and content.get("execution_state") == "idle":
322
+ break
323
+
324
+ def close(self):
325
+ """Shutdown kernel and cleanup."""
326
+ try:
327
+ self.kernel_client.stop_channels()
328
+ except Exception:
329
+ pass
330
+ try:
331
+ self.kernel_manager.shutdown_kernel(now=True)
332
+ except Exception:
333
+ pass
334
+
335
+
336
+ # --- Stdlib Fallback Backend ---
337
+
338
+ class _StdlibSession:
339
+ """Uses code.InteractiveConsole with stdout/stderr capture."""
340
+
341
+ def __init__(self):
342
+ self.namespace: Dict[str, Any] = {"__builtins__": __builtins__}
343
+ self._console = code.InteractiveConsole(locals=self.namespace)
344
+
345
+ def execute(self, code_str: str) -> Dict[str, Any]:
346
+ """Execute code with stdout/stderr capture via contextlib."""
347
+ stdout_buf = io.StringIO()
348
+ stderr_buf = io.StringIO()
349
+ result = None
350
+ error = None
351
+
352
+ try:
353
+ with contextlib.redirect_stdout(stdout_buf), \
354
+ contextlib.redirect_stderr(stderr_buf):
355
+ # Try to evaluate as single expression first
356
+ try:
357
+ tree = ast.parse(code_str, mode="eval")
358
+ compiled = compile(tree, "<repl>", "eval")
359
+ result_val = eval(compiled, self.namespace) # noqa: S307
360
+ if result_val is not None:
361
+ result = repr(result_val)
362
+ except SyntaxError:
363
+ # Fall back to exec for statements
364
+ tree = ast.parse(code_str, mode="exec")
365
+ compiled = compile(tree, "<repl>", "exec")
366
+ exec(compiled, self.namespace) # noqa: S102
367
+ except Exception:
368
+ error = traceback.format_exc()
369
+
370
+ return {
371
+ "stdout": stdout_buf.getvalue(),
372
+ "stderr": stderr_buf.getvalue(),
373
+ "result": result,
374
+ "error": error,
375
+ }
376
+
377
+ def stream_execute(self, code_str: str) -> Generator[Dict[str, str], None, None]:
378
+ """Execute code and yield output chunks.
379
+
380
+ Note: stdlib backend doesn't support true streaming —
381
+ executes fully then yields collected output.
382
+ """
383
+ output = self.execute(code_str)
384
+ if output["stdout"]:
385
+ yield {"type": "stdout", "data": output["stdout"]}
386
+ if output["stderr"]:
387
+ yield {"type": "stderr", "data": output["stderr"]}
388
+ if output["result"] is not None:
389
+ yield {"type": "result", "data": output["result"]}
390
+ if output["error"]:
391
+ yield {"type": "error", "data": output["error"]}
392
+
393
+ def close(self):
394
+ """Cleanup namespace."""
395
+ self.namespace.clear()
396
+
397
+
398
+ # --- Public API ---
399
+
400
+ def start_repl_session(session_id: Optional[str] = None) -> Dict[str, Any]:
401
+ """Start or resume a persistent REPL session.
402
+
403
+ Args:
404
+ session_id: Optional ID to resume an existing session.
405
+ If None, creates a new session with a UUID.
406
+
407
+ Returns:
408
+ Session info dict: {session_id, created_at, last_used, exec_count, backend}
409
+ or {"error": "..."} if feature flag is disabled.
410
+ """
411
+ if not _is_enabled():
412
+ return {"error": _DISABLED_MSG}
413
+
414
+ # Resume existing session
415
+ if session_id and session_id in _sessions:
416
+ session = _sessions[session_id]
417
+ session["last_used"] = _now_iso()
418
+ _persist_session(session_id)
419
+ return _session_info(session)
420
+
421
+ # Create new session
422
+ new_id = session_id or str(uuid.uuid4())
423
+
424
+ # Try IPython kernel first, fall back to stdlib
425
+ _check_jupyter()
426
+ backend_name = "stdlib"
427
+ backend = None
428
+
429
+ if _HAS_JUPYTER:
430
+ try:
431
+ backend = _IPythonSession()
432
+ backend_name = "ipython"
433
+ except Exception:
434
+ backend = _StdlibSession()
435
+ else:
436
+ backend = _StdlibSession()
437
+
438
+ now = _now_iso()
439
+ _sessions[new_id] = {
440
+ "session_id": new_id,
441
+ "created_at": now,
442
+ "last_used": now,
443
+ "exec_count": 0,
444
+ "backend": backend_name,
445
+ "_backend": backend,
446
+ }
447
+
448
+ # Inject prelude helpers if enabled
449
+ if _get_helpers_flag():
450
+ prelude = _build_prelude_namespace()
451
+ if hasattr(backend, "namespace"):
452
+ backend.namespace.update(prelude)
453
+ _persist_session(new_id)
454
+
455
+ return _session_info(_sessions[new_id])
456
+
457
+
458
+ def execute_code(session_id: str, code_str: str) -> Dict[str, Any]:
459
+ """Execute code in a session.
460
+
461
+ Args:
462
+ session_id: Session ID from start_repl_session()
463
+ code_str: Python code to execute
464
+
465
+ Returns:
466
+ {stdout, stderr, result, error, exec_count}
467
+ or {"error": "..."} if feature flag is disabled or session not found.
468
+ """
469
+ if not _is_enabled():
470
+ return {"error": _DISABLED_MSG}
471
+
472
+ if session_id not in _sessions:
473
+ return {"error": f"Session not found: {session_id}"}
474
+
475
+ # Sandbox integration: if sandbox enabled, route through sandboxed executor
476
+ if _get_sandbox_flag():
477
+ from tools.python_sandbox import execute_sandboxed
478
+ session = _sessions[session_id]
479
+ backend = session.get("_backend")
480
+ ns = backend.namespace if hasattr(backend, "namespace") else None
481
+ output = execute_sandboxed(code_str, namespace=ns)
482
+ session["exec_count"] += 1
483
+ session["last_used"] = _now_iso()
484
+ output["exec_count"] = session["exec_count"]
485
+ _persist_session(session_id)
486
+ return output
487
+
488
+ session = _sessions[session_id]
489
+ backend = session["_backend"]
490
+
491
+ try:
492
+ output = backend.execute(code_str)
493
+ except Exception as e:
494
+ output = {
495
+ "stdout": "",
496
+ "stderr": "",
497
+ "result": None,
498
+ "error": f"{type(e).__name__}: {e}",
499
+ }
500
+
501
+ session["exec_count"] += 1
502
+ session["last_used"] = _now_iso()
503
+ output["exec_count"] = session["exec_count"]
504
+
505
+ _persist_session(session_id)
506
+ return output
507
+
508
+
509
+ def get_session(session_id: str) -> Optional[Dict[str, Any]]:
510
+ """Get session info by ID.
511
+
512
+ Args:
513
+ session_id: Session ID to look up
514
+
515
+ Returns:
516
+ Session info dict, None if not found,
517
+ or {"error": "..."} if feature flag is disabled.
518
+ """
519
+ if not _is_enabled():
520
+ return {"error": _DISABLED_MSG}
521
+
522
+ if session_id not in _sessions:
523
+ return None
524
+
525
+ return _session_info(_sessions[session_id])
526
+
527
+
528
+ def close_session(session_id: str) -> Union[bool, Dict[str, Any]]:
529
+ """Close and cleanup a session.
530
+
531
+ Args:
532
+ session_id: Session ID to close
533
+
534
+ Returns:
535
+ True if closed, False if not found,
536
+ or {"error": "..."} if feature flag is disabled.
537
+ """
538
+ if not _is_enabled():
539
+ return {"error": _DISABLED_MSG}
540
+
541
+ if session_id not in _sessions:
542
+ return False
543
+
544
+ session = _sessions.pop(session_id)
545
+ backend = session.get("_backend")
546
+ if backend is not None:
547
+ try:
548
+ backend.close()
549
+ except Exception:
550
+ pass
551
+
552
+ return True
553
+
554
+
555
+ def list_sessions() -> Union[List[Dict[str, Any]], Dict[str, Any]]:
556
+ """List all active sessions.
557
+
558
+ Returns:
559
+ List of session info dicts,
560
+ or {"error": "..."} if feature flag is disabled.
561
+ """
562
+ if not _is_enabled():
563
+ return {"error": _DISABLED_MSG}
564
+
565
+ return [_session_info(s) for s in _sessions.values()]
566
+
567
+
568
+ def stream_execute(
569
+ session_id: str, code_str: str
570
+ ) -> Generator[Dict[str, str], None, None]:
571
+ """Execute code and stream output chunks.
572
+
573
+ Args:
574
+ session_id: Session ID from start_repl_session()
575
+ code_str: Python code to execute
576
+
577
+ Yields:
578
+ Dicts with keys: type ("stdout"|"stderr"|"result"|"error"), data (str)
579
+ """
580
+ if not _is_enabled():
581
+ yield {"type": "error", "data": _DISABLED_MSG}
582
+ return
583
+
584
+ if session_id not in _sessions:
585
+ yield {"type": "error", "data": f"Session not found: {session_id}"}
586
+ return
587
+
588
+ session = _sessions[session_id]
589
+ backend = session["_backend"]
590
+
591
+ try:
592
+ for chunk in backend.stream_execute(code_str):
593
+ yield chunk
594
+ except Exception as e:
595
+ yield {"type": "error", "data": f"{type(e).__name__}: {e}"}
596
+
597
+ session["exec_count"] += 1
598
+ session["last_used"] = _now_iso()
599
+ _persist_session(session_id)
600
+
601
+
602
+ # --- CLI Interface ---
603
+
604
+ def _cli_main():
605
+ """CLI entry point for python_repl.py."""
606
+ import argparse
607
+
608
+ parser = argparse.ArgumentParser(
609
+ description="OMG Python REPL Tool — persistent sessions with IPython or stdlib",
610
+ formatter_class=argparse.RawDescriptionHelpFormatter,
611
+ )
612
+ parser.add_argument("--exec", dest="code", help="Execute Python code")
613
+ parser.add_argument("--session-id", dest="session_id", help="Session ID to use")
614
+ parser.add_argument(
615
+ "--list-sessions", action="store_true", help="List active sessions"
616
+ )
617
+ parser.add_argument(
618
+ "--close-session", dest="close_id", help="Close a session by ID"
619
+ )
620
+ parser.add_argument(
621
+ "--stream", action="store_true", help="Stream output (with --exec)"
622
+ )
623
+
624
+ args = parser.parse_args()
625
+
626
+ if args.list_sessions:
627
+ result = list_sessions()
628
+ print(json.dumps(result, indent=2))
629
+ return
630
+
631
+ if args.close_id:
632
+ result = close_session(args.close_id)
633
+ print(json.dumps({"closed": result}))
634
+ return
635
+
636
+ if args.code:
637
+ session = start_repl_session(session_id=args.session_id)
638
+ if "error" in session:
639
+ print(json.dumps(session))
640
+ sys.exit(1)
641
+
642
+ sid = session["session_id"]
643
+
644
+ if args.stream:
645
+ for chunk in stream_execute(sid, args.code):
646
+ print(json.dumps(chunk))
647
+ else:
648
+ result = execute_code(sid, args.code)
649
+ print(json.dumps(result, indent=2))
650
+ return
651
+
652
+ parser.print_help()
653
+
654
+
655
+ if __name__ == "__main__":
656
+ _cli_main()