@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,582 @@
1
+ """OMG Setup Wizard — interactive CLI detection, auth verification, and MCP configuration.
2
+
3
+ Feature-gated: requires OMG_SETUP_ENABLED=1 (default off).
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ import sys
11
+ from typing import Any, cast
12
+
13
+ import yaml
14
+
15
+ from _common import get_feature_flag
16
+
17
+ # Ensure project root is on sys.path for runtime imports
18
+ _PROJECT_ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
19
+ if _PROJECT_ROOT not in sys.path:
20
+ sys.path.insert(0, _PROJECT_ROOT)
21
+
22
+ from runtime.cli_provider import get_provider, list_available_providers # noqa: E402
23
+ from runtime.mcp_config_writers import ( # noqa: E402
24
+ write_claude_mcp_config,
25
+ write_codex_mcp_config,
26
+ write_gemini_mcp_config,
27
+ write_opencode_mcp_config,
28
+ write_kimi_mcp_config,
29
+ )
30
+
31
+ # Trigger provider auto-registration on import
32
+ import runtime.providers.codex_provider # noqa: E402, F401
33
+ import runtime.providers.gemini_provider # noqa: E402, F401
34
+ import runtime.providers.opencode_provider # noqa: E402, F401
35
+ import runtime.providers.kimi_provider # noqa: E402, F401
36
+ from runtime.adoption import ( # noqa: E402
37
+ CANONICAL_VERSION,
38
+ build_adoption_report,
39
+ get_preset_features,
40
+ resolve_preset,
41
+ write_adoption_report,
42
+ )
43
+
44
+ _logger = logging.getLogger(__name__)
45
+
46
+ _INSTALL_HINTS: dict[str, str] = {
47
+ "codex": "npm install -g @openai/codex",
48
+ "gemini": "npm install -g @google/gemini-cli",
49
+ "opencode": "npm install -g opencode-ai (or: curl -fsSL https://opencode.ai/install | bash)",
50
+ "kimi": "uv tool install --python 3.13 kimi-cli",
51
+ }
52
+
53
+ BYPASS_ALL_WARNING = (
54
+ "⚠️ BYPASS-ALL / FULL VIBE-CODE MODE WARNING ⚠️\n\n"
55
+ "Enabling bypass-all grants Claude Code unrestricted write access to your filesystem "
56
+ "without asking for confirmation on individual file edits.\n\n"
57
+ "IMPORTANT DISCLAIMER: The author takes NO responsibility for any data loss, "
58
+ "unintended file modifications, or system changes that occur while bypass-all is enabled. "
59
+ "Use at your own risk.\n\n"
60
+ "Note: Some safety measures remain active even in bypass-all mode:\n"
61
+ " • Firewall deny rules still block dangerous commands (rm -rf, sudo, etc.)\n"
62
+ " • Secret-guard still protects credentials and API keys\n"
63
+ " • You can disable bypass-all at any time via settings.json\n\n"
64
+ "Do you want to enable bypass-all mode? (y/N): "
65
+ )
66
+
67
+ MCP_CATALOG: list[dict[str, Any]] = [
68
+ {
69
+ "id": "context7",
70
+ "name": "Context7",
71
+ "description": "Upstash Context7 MCP server for context management",
72
+ "command": "npx",
73
+ "args": ["@upstash/context7-mcp@2.1.3"],
74
+ "default": True,
75
+ "category": "productivity",
76
+ },
77
+ {
78
+ "id": "filesystem",
79
+ "name": "Filesystem",
80
+ "description": "ModelContextProtocol filesystem server for file operations",
81
+ "command": "npx",
82
+ "args": ["@modelcontextprotocol/server-filesystem@2026.1.14", "."],
83
+ "default": True,
84
+ "category": "system",
85
+ },
86
+ {
87
+ "id": "websearch",
88
+ "name": "Web Search",
89
+ "description": "Web search MCP server for internet queries",
90
+ "command": "npx",
91
+ "args": ["@zhafron/mcp-web-search@1.2.2"],
92
+ "default": True,
93
+ "category": "search",
94
+ },
95
+ {
96
+ "id": "chrome-devtools",
97
+ "name": "Chrome DevTools",
98
+ "description": "Chrome DevTools MCP server for browser automation",
99
+ "command": "npx",
100
+ "args": ["chrome-devtools-mcp@0.19.0"],
101
+ "default": True,
102
+ "category": "browser",
103
+ },
104
+ {
105
+ "id": "omg-memory",
106
+ "name": "OMG Memory",
107
+ "description": "OMG shared memory server via HTTP",
108
+ "command": None,
109
+ "args": [],
110
+ "type": "http",
111
+ "url": "http://127.0.0.1:8765/mcp",
112
+ "default": True,
113
+ "category": "memory",
114
+ },
115
+ {
116
+ "id": "github",
117
+ "name": "GitHub",
118
+ "description": "ModelContextProtocol GitHub server for repository operations",
119
+ "command": "npx",
120
+ "args": ["@modelcontextprotocol/server-github"],
121
+ "default": False,
122
+ "category": "vcs",
123
+ },
124
+ {
125
+ "id": "puppeteer",
126
+ "name": "Puppeteer",
127
+ "description": "ModelContextProtocol Puppeteer server for browser automation",
128
+ "command": "npx",
129
+ "args": ["@modelcontextprotocol/server-puppeteer"],
130
+ "default": False,
131
+ "category": "browser",
132
+ },
133
+ {
134
+ "id": "brave-search",
135
+ "name": "Brave Search",
136
+ "description": "ModelContextProtocol Brave Search server",
137
+ "command": "npx",
138
+ "args": ["@modelcontextprotocol/server-brave-search"],
139
+ "default": False,
140
+ "category": "search",
141
+ },
142
+ {
143
+ "id": "sequential-thinking",
144
+ "name": "Sequential Thinking",
145
+ "description": "ModelContextProtocol Sequential Thinking server for reasoning",
146
+ "command": "npx",
147
+ "args": ["@modelcontextprotocol/server-sequential-thinking"],
148
+ "default": False,
149
+ "category": "reasoning",
150
+ },
151
+ {
152
+ "id": "grep-app",
153
+ "name": "Grep App",
154
+ "description": "Grep App MCP server for code search",
155
+ "command": "npx",
156
+ "args": ["grep-app-mcp"],
157
+ "default": False,
158
+ "category": "search",
159
+ },
160
+ {
161
+ "id": "memory-graph",
162
+ "name": "Memory Graph",
163
+ "description": "ModelContextProtocol Memory server for knowledge graphs",
164
+ "command": "npx",
165
+ "args": ["@modelcontextprotocol/server-memory"],
166
+ "default": False,
167
+ "category": "memory",
168
+ },
169
+ ]
170
+
171
+
172
+ def get_mcp_catalog() -> list[dict[str, Any]]:
173
+ """Return the MCP catalog.
174
+
175
+ Returns:
176
+ List of MCP server definitions with id, name, description, command, args, default, and category.
177
+ """
178
+ return MCP_CATALOG
179
+
180
+
181
+ def build_mcp_config(selected_ids: list[str]) -> dict[str, Any]:
182
+ """Build .mcp.json configuration from selected MCP server IDs.
183
+
184
+ Args:
185
+ selected_ids: List of MCP server IDs to include in the config.
186
+
187
+ Returns:
188
+ Dict with 'mcpServers' key containing the MCP server configurations.
189
+ """
190
+ mcp_servers: dict[str, Any] = {}
191
+
192
+ for mcp in MCP_CATALOG:
193
+ if mcp["id"] not in selected_ids:
194
+ continue
195
+
196
+ mcp_id = mcp["id"]
197
+
198
+ # HTTP-type MCPs (like omg-memory)
199
+ if mcp.get("type") == "http":
200
+ mcp_servers[mcp_id] = {
201
+ "type": "http",
202
+ "url": mcp["url"],
203
+ }
204
+ # NPX-type MCPs
205
+ else:
206
+ mcp_servers[mcp_id] = {
207
+ "command": mcp["command"],
208
+ "args": mcp["args"],
209
+ }
210
+
211
+ return {"mcpServers": mcp_servers}
212
+
213
+
214
+ def configure_plan_type(plan_type: str) -> dict[str, Any]:
215
+ """Configure Claude plan type and model routing.
216
+
217
+ Args:
218
+ plan_type: "max" or "pro"
219
+
220
+ Returns:
221
+ dict with plan_type and optionally model_routing
222
+ """
223
+ result: dict[str, Any] = {"plan_type": plan_type}
224
+ if plan_type == "pro":
225
+ result["model_routing"] = {
226
+ "planning": "claude-opus-4-5",
227
+ "coding": "claude-sonnet-4-5",
228
+ "review": "claude-opus-4-5",
229
+ "commit": "claude-haiku-4-5",
230
+ }
231
+ return result
232
+
233
+
234
+ def select_mcps(selected_ids: list[str] | None = None) -> dict[str, Any]:
235
+ """Build MCP config from selected IDs.
236
+
237
+ Args:
238
+ selected_ids: List of MCP IDs to include. If None, uses defaults.
239
+
240
+ Returns:
241
+ dict with mcpServers key (ready to write as .mcp.json)
242
+ """
243
+ if selected_ids is None:
244
+ selected_ids = [m["id"] for m in MCP_CATALOG if m["default"]]
245
+ return build_mcp_config(selected_ids)
246
+
247
+
248
+ def configure_bypass_all(enabled: bool) -> dict[str, Any]:
249
+ """Configure bypass_all mode.
250
+
251
+ Args:
252
+ enabled: True to enable bypass-all, False to disable
253
+
254
+ Returns:
255
+ dict with enabled status and warning_shown flag
256
+ """
257
+ result: dict[str, Any] = {"enabled": enabled}
258
+ if enabled:
259
+ result["warning_shown"] = True
260
+ return result
261
+
262
+
263
+ def is_setup_enabled() -> bool:
264
+ """Check if the setup wizard feature is enabled.
265
+
266
+ Uses get_feature_flag("SETUP", default=False) — disabled by default.
267
+ Enable via OMG_SETUP_ENABLED=1 env var or settings.json._omg.features.SETUP: true.
268
+ """
269
+ return get_feature_flag("SETUP", default=False)
270
+
271
+
272
+ def detect_clis() -> dict[str, Any]:
273
+ """Detect installed CLI tools using the provider registry.
274
+
275
+ Iterates over all registered providers, calling ``detect()`` and
276
+ ``check_auth()`` on each. Returns a dict keyed by provider name::
277
+
278
+ {
279
+ "codex": {"detected": True, "auth_ok": True, "message": "..."},
280
+ "gemini": {"detected": False, "auth_ok": None,
281
+ "message": "Not found. Install: npm install -g @google/gemini-cli"},
282
+ ...
283
+ }
284
+ """
285
+ results: dict[str, Any] = {}
286
+
287
+ for name in list_available_providers():
288
+ provider = get_provider(name)
289
+ if provider is None:
290
+ continue
291
+
292
+ try:
293
+ detected = provider.detect()
294
+ except Exception as exc:
295
+ _logger.warning("detect() failed for %s: %s", name, exc)
296
+ detected = False
297
+
298
+ auth_ok: bool | None = None
299
+ message = ""
300
+
301
+ if detected:
302
+ try:
303
+ auth_ok, message = provider.check_auth()
304
+ except Exception as exc:
305
+ _logger.warning("check_auth() failed for %s: %s", name, exc)
306
+ auth_ok = None
307
+ message = f"Auth check error: {exc}"
308
+ else:
309
+ hint = _INSTALL_HINTS.get(name, f"Install the '{name}' CLI")
310
+ message = f"Not found. Install: {hint}"
311
+
312
+ results[name] = {
313
+ "detected": detected,
314
+ "auth_ok": auth_ok,
315
+ "message": message,
316
+ }
317
+
318
+ return results
319
+
320
+
321
+ def get_cli_auth_instructions(provider: str) -> dict[str, str]:
322
+ """Return install, auth, and verify instructions for a CLI provider.
323
+
324
+ This function returns command strings only — it does NOT execute anything
325
+ or store credentials.
326
+
327
+ Args:
328
+ provider: CLI provider name (e.g. "codex", "gemini", "kimi", "opencode").
329
+
330
+ Returns:
331
+ Dict with keys: install, auth, verify, subscription.
332
+ Unknown providers return placeholder strings.
333
+ """
334
+ instructions: dict[str, dict[str, str]] = {
335
+ "codex": {
336
+ "install": "npm install -g @openai/codex",
337
+ "auth": "codex login",
338
+ "verify": "codex --version",
339
+ "subscription": "Requires ChatGPT Plus, Team, or Enterprise subscription (or OpenAI API key)",
340
+ },
341
+ "gemini": {
342
+ "install": "npm install -g @google/gemini-cli",
343
+ "auth": "gemini auth login",
344
+ "verify": "gemini --version",
345
+ "subscription": "Requires Google account with Gemini API access (free tier available)",
346
+ },
347
+ "kimi": {
348
+ "install": "uv tool install --python 3.13 kimi-cli",
349
+ "auth": "Add token to ~/.kimi/config.toml",
350
+ "verify": "kimi --version",
351
+ "subscription": "Requires Kimi API key from platform.moonshot.cn",
352
+ },
353
+ "opencode": {
354
+ "install": "npm install -g opencode-ai",
355
+ "auth": "opencode auth login",
356
+ "verify": "opencode --version",
357
+ "subscription": "Requires Anthropic API key or Claude subscription",
358
+ },
359
+ }
360
+
361
+ return instructions.get(provider, {
362
+ "install": "Unknown provider",
363
+ "auth": "Unknown provider",
364
+ "verify": "Unknown provider",
365
+ "subscription": "Unknown",
366
+ })
367
+
368
+
369
+ def check_auth() -> dict[str, Any]:
370
+ """Verify authentication for detected CLI tools.
371
+
372
+ Stub — returns pending status. T16 will implement real auth checks
373
+ using each provider's check_auth() method.
374
+ """
375
+ return {"status": "pending", "results": {}}
376
+
377
+
378
+ def configure_mcp(
379
+ project_dir: str,
380
+ detected_clis: dict[str, Any],
381
+ server_url: str = "http://127.0.0.1:8765/mcp",
382
+ server_name: str = "omg-memory",
383
+ ) -> dict[str, Any]:
384
+ """Configure MCP memory server for authenticated CLIs.
385
+
386
+ For each CLI in detected_clis where detected_clis[cli]["detected"] == True,
387
+ calls the appropriate writer from runtime.mcp_config_writers.
388
+
389
+ Args:
390
+ project_dir: Path to the project directory.
391
+ detected_clis: Dict of CLI detection results from detect_clis().
392
+ server_url: MCP server URL (default: http://127.0.0.1:8765/mcp).
393
+ server_name: MCP server name (default: omg-memory).
394
+
395
+ Returns:
396
+ Dict with keys:
397
+ - status: "ok" on success
398
+ - configured: List of CLI names that were successfully configured
399
+ - errors: Dict of CLI name → error message for failures
400
+ """
401
+ configured: list[str] = []
402
+ errors: dict[str, str] = {}
403
+
404
+ # Always write Claude config
405
+ try:
406
+ write_claude_mcp_config(project_dir, server_url, server_name)
407
+ except Exception as exc:
408
+ _logger.warning("Failed to write Claude MCP config: %s", exc)
409
+ errors["claude"] = str(exc)
410
+
411
+ # Write configs for detected CLIs
412
+ cli_writers = {
413
+ "codex": write_codex_mcp_config,
414
+ "gemini": write_gemini_mcp_config,
415
+ "opencode": write_opencode_mcp_config,
416
+ "kimi": write_kimi_mcp_config,
417
+ }
418
+
419
+ for cli_name, writer_func in cli_writers.items():
420
+ cli_info = detected_clis.get(cli_name, {})
421
+ if not cli_info.get("detected", False):
422
+ continue
423
+
424
+ try:
425
+ writer_func(server_url, server_name)
426
+ configured.append(cli_name)
427
+ except Exception as exc:
428
+ _logger.warning("Failed to write %s MCP config: %s", cli_name, exc)
429
+ errors[cli_name] = str(exc)
430
+
431
+ return {
432
+ "status": "ok",
433
+ "configured": configured,
434
+ "errors": errors,
435
+ }
436
+
437
+
438
+ def set_preferences(project_dir: str, preferences: dict[str, Any]) -> dict[str, Any]:
439
+ """Set user preferences for CLI routing and save to .omg/state/cli-config.yaml.
440
+
441
+ Args:
442
+ project_dir: Path to the project directory.
443
+ preferences: Dict with optional 'cli_configs' key. If empty, uses defaults.
444
+ Expected structure:
445
+ {
446
+ "cli_configs": {
447
+ "codex": {"subscription": "free", "max_parallel_agents": 1},
448
+ "gemini": {"subscription": "free", "max_parallel_agents": 1},
449
+ ...
450
+ }
451
+ }
452
+
453
+ Returns:
454
+ Dict with keys:
455
+ - status: "ok" on success
456
+ - path: Full path to saved config file
457
+ - config: The saved config dict (version + cli_configs)
458
+ """
459
+ # Default config structure
460
+ default_cli_configs: dict[str, Any] = {
461
+ "codex": {"subscription": "free", "max_parallel_agents": 1},
462
+ "gemini": {"subscription": "free", "max_parallel_agents": 1},
463
+ "opencode": {"subscription": "free", "max_parallel_agents": 1},
464
+ "kimi": {"subscription": "free", "max_parallel_agents": 1},
465
+ }
466
+ preset = resolve_preset(cast(str | None, preferences.get("preset")))
467
+ default_config: dict[str, Any] = {
468
+ "version": CANONICAL_VERSION,
469
+ "preset": preset,
470
+ "resolved_features": get_preset_features(preset),
471
+ "cli_configs": default_cli_configs,
472
+ }
473
+
474
+ # Merge custom preferences if provided
475
+ if preferences and isinstance(preferences, dict):
476
+ cli_configs = preferences.get("cli_configs")
477
+ if isinstance(cli_configs, dict):
478
+ default_cli_configs.update(cast(dict[str, Any], cli_configs))
479
+
480
+ _write_project_settings_preset(project_dir, preset)
481
+
482
+ # Create .omg/state directory if needed
483
+ state_dir = os.path.join(project_dir, ".omg", "state")
484
+ os.makedirs(state_dir, exist_ok=True)
485
+
486
+ # Write config to YAML file
487
+ config_path = os.path.join(state_dir, "cli-config.yaml")
488
+ with open(config_path, "w") as f:
489
+ yaml.dump(default_config, f, default_flow_style=False, sort_keys=False)
490
+
491
+ _logger.info("Saved CLI config to %s", config_path)
492
+
493
+ return {
494
+ "status": "ok",
495
+ "path": config_path,
496
+ "config": default_config,
497
+ }
498
+
499
+
500
+ def _write_project_settings_preset(project_dir: str, preset: str) -> None:
501
+ """Persist preset metadata into project settings when settings.json exists."""
502
+ settings_path = os.path.join(project_dir, "settings.json")
503
+ if not os.path.exists(settings_path):
504
+ return
505
+
506
+ try:
507
+ with open(settings_path, "r", encoding="utf-8") as f:
508
+ settings = json.load(f)
509
+ except Exception:
510
+ return
511
+
512
+ if not isinstance(settings, dict):
513
+ return
514
+
515
+ omg = settings.get("_omg")
516
+ if not isinstance(omg, dict):
517
+ omg = {}
518
+ features = omg.get("features")
519
+ if not isinstance(features, dict):
520
+ features = {}
521
+
522
+ features.update(get_preset_features(preset))
523
+ omg["features"] = features
524
+ omg["preset"] = preset
525
+ omg["_version"] = CANONICAL_VERSION
526
+ settings["_omg"] = omg
527
+
528
+ with open(settings_path, "w", encoding="utf-8") as f:
529
+ json.dump(settings, f, indent=2, ensure_ascii=True)
530
+ f.write("\n")
531
+
532
+
533
+ def run_setup_wizard(
534
+ project_dir: str,
535
+ non_interactive: bool = False,
536
+ *,
537
+ mode: str | None = None,
538
+ adopt: str = "auto",
539
+ preset: str | None = None,
540
+ ) -> dict[str, Any]:
541
+ """Run the OMG setup wizard.
542
+
543
+ Args:
544
+ project_dir: Path to the project directory.
545
+ non_interactive: If True, skip prompts and use defaults (for CI).
546
+ mode: Optional adoption mode override (`omg-only` or `coexist`).
547
+ adopt: Adoption detection mode (currently only `auto` is meaningful).
548
+ preset: Optional preset override.
549
+
550
+ Returns:
551
+ Dict with wizard results including status and step outcomes.
552
+ If feature is disabled, returns {"status": "disabled", "message": "..."}.
553
+ """
554
+ if not is_setup_enabled():
555
+ return {
556
+ "status": "disabled",
557
+ "message": "Setup wizard disabled. Set OMG_SETUP_ENABLED=1 to enable.",
558
+ }
559
+
560
+ selected_preset = resolve_preset(preset or ("balanced" if non_interactive else "safe"))
561
+ adoption = build_adoption_report(
562
+ project_dir,
563
+ requested_mode=mode,
564
+ preset=selected_preset,
565
+ adopt=adopt,
566
+ )
567
+
568
+ clis = detect_clis()
569
+ auth = check_auth()
570
+ mcp = configure_mcp(project_dir, clis)
571
+ prefs = set_preferences(project_dir, {"preset": selected_preset})
572
+ report_path = write_adoption_report(project_dir, adoption)
573
+ adoption["report_path"] = report_path
574
+
575
+ return {
576
+ "status": "complete",
577
+ "clis_detected": clis,
578
+ "auth_status": auth,
579
+ "mcp_configured": mcp,
580
+ "preferences": prefs,
581
+ "adoption": adoption,
582
+ }