@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,91 @@
1
+ """Kimi Web paste-based memory import — parse and import memories from Kimi Web interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any # pyright: ignore[reportExplicitAny]
7
+
8
+ EXTRACTION_PROMPT = (
9
+ "What information do you remember about me? Please list all stored preferences, facts, and context "
10
+ "as bullet points starting with '- '."
11
+ )
12
+
13
+
14
+ def parse_kimi_paste(text: str) -> list[dict[str, Any]]: # pyright: ignore[reportExplicitAny]
15
+ """Parse memories from Kimi Web paste text.
16
+
17
+ Supports bullet points (- item) and numbered lists (1. item).
18
+
19
+ Args:
20
+ text: Raw text pasted from Kimi Web interface.
21
+
22
+ Returns:
23
+ List of memory items with keys, content, source_cli, and tags.
24
+ """
25
+ if not text or not text.strip():
26
+ return []
27
+
28
+ items: list[str] = []
29
+ lines = text.split("\n")
30
+
31
+ for line in lines:
32
+ line = line.strip()
33
+ if not line:
34
+ continue
35
+
36
+ # Try bullet point format: "- item"
37
+ if line.startswith("-"):
38
+ content = line[1:].strip()
39
+ if content:
40
+ items.append(content)
41
+ continue
42
+
43
+ # Try numbered format: "1. item", "2. item", etc.
44
+ match = re.match(r"^\d+\.\s+(.+)$", line)
45
+ if match:
46
+ content = match.group(1).strip()
47
+ if content:
48
+ items.append(content)
49
+ continue
50
+
51
+ # Convert to memory item dicts
52
+ result: list[dict[str, Any]] = [] # pyright: ignore[reportExplicitAny]
53
+ for i, content in enumerate(items):
54
+ result.append(
55
+ {
56
+ "key": f"kimi-memory-{i+1}",
57
+ "content": content,
58
+ "source_cli": "kimi-web",
59
+ "tags": ["kimi-web", "imported"],
60
+ }
61
+ )
62
+
63
+ return result
64
+
65
+
66
+ def import_from_paste(text: str, store: Any) -> int: # pyright: ignore[reportAny]
67
+ """Import memories from Kimi Web paste into the memory store.
68
+
69
+ Args:
70
+ text: Raw text pasted from Kimi Web interface.
71
+ store: MemoryStore instance to add items to.
72
+
73
+ Returns:
74
+ Number of items successfully imported.
75
+ """
76
+ items = parse_kimi_paste(text)
77
+ count = 0
78
+
79
+ for item in items:
80
+ store.add( # pyright: ignore[reportAny]
81
+ key=item["key"],
82
+ content=item["content"],
83
+ source_cli=item["source_cli"],
84
+ tags=item["tags"],
85
+ )
86
+ count += 1
87
+
88
+ return count
89
+
90
+
91
+ __all__ = ["EXTRACTION_PROMPT", "parse_kimi_paste", "import_from_paste"]
@@ -0,0 +1,215 @@
1
+ """MemoryStore — CRUD + JSON persistence for shared memory across CLI providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import uuid
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ class MemoryStoreFullError(Exception):
14
+ """Raised when adding to a store that has reached the 10,000 item limit."""
15
+
16
+
17
+ _MAX_ITEMS = 10_000
18
+
19
+ # Type alias for memory items — JSON-like dicts.
20
+ _Item = dict[str, Any] # pyright: ignore[reportExplicitAny]
21
+
22
+
23
+ class MemoryStore:
24
+ """Thread-unsafe, file-backed key/content store with JSON persistence.
25
+
26
+ Each item follows the schema::
27
+
28
+ {
29
+ "id": str, # UUID4
30
+ "key": str, # user-defined key
31
+ "content": str, # the memory text
32
+ "source_cli": str, # originating CLI name
33
+ "tags": list[str], # optional tags
34
+ "created_at": str, # ISO 8601 UTC
35
+ "updated_at": str, # ISO 8601 UTC
36
+ }
37
+
38
+ Persistence uses atomic writes (tmp file + ``os.replace``).
39
+ """
40
+
41
+ def __init__(self, store_path: str | None = None) -> None:
42
+ if store_path is None:
43
+ store_path = str(Path.home() / ".omg" / "shared-memory" / "store.json")
44
+ self.store_path: str = store_path
45
+ self._items: list[_Item] = self._load()
46
+
47
+ # ------------------------------------------------------------------
48
+ # Public API
49
+ # ------------------------------------------------------------------
50
+
51
+ def add(
52
+ self,
53
+ key: str,
54
+ content: str,
55
+ source_cli: str,
56
+ tags: list[str] | None = None,
57
+ ) -> _Item:
58
+ """Create a new memory item and persist to disk.
59
+
60
+ Raises ``MemoryStoreFullError`` when the store already holds
61
+ ``_MAX_ITEMS`` entries.
62
+ """
63
+ if self.count() >= _MAX_ITEMS:
64
+ raise MemoryStoreFullError(
65
+ f"Memory store is full ({_MAX_ITEMS} items). "
66
+ "Delete items before adding new ones."
67
+ )
68
+
69
+ now = _utc_now_iso()
70
+ item: _Item = {
71
+ "id": str(uuid.uuid4()),
72
+ "key": key,
73
+ "content": content,
74
+ "source_cli": source_cli,
75
+ "tags": tags if tags is not None else [],
76
+ "created_at": now,
77
+ "updated_at": now,
78
+ }
79
+ self._items.append(item)
80
+ self._save()
81
+ return item
82
+
83
+ def get(self, item_id: str) -> _Item | None:
84
+ """Return the item with *item_id*, or ``None`` if not found."""
85
+ for item in self._items:
86
+ if item["id"] == item_id:
87
+ return item
88
+ return None
89
+
90
+ def update(
91
+ self,
92
+ item_id: str,
93
+ content: str | None = None,
94
+ tags: list[str] | None = None,
95
+ ) -> _Item | None:
96
+ """Update *content* and/or *tags* for an existing item.
97
+
98
+ Returns the updated item, or ``None`` if *item_id* is not found.
99
+ """
100
+ item = self.get(item_id)
101
+ if item is None:
102
+ return None
103
+
104
+ if content is not None:
105
+ item["content"] = content
106
+ if tags is not None:
107
+ item["tags"] = tags
108
+ item["updated_at"] = _utc_now_iso()
109
+ self._save()
110
+ return item
111
+
112
+ def delete(self, item_id: str) -> bool:
113
+ """Remove the item with *item_id*.
114
+
115
+ Returns ``True`` if deleted, ``False`` if not found.
116
+ """
117
+ for idx, item in enumerate(self._items):
118
+ if item["id"] == item_id:
119
+ del self._items[idx]
120
+ self._save()
121
+ return True
122
+ return False
123
+
124
+ def search(
125
+ self,
126
+ query: str,
127
+ source_cli: str | None = None,
128
+ ) -> list[_Item]:
129
+ """Keyword search across key, content, and tags (case-insensitive).
130
+
131
+ Optionally filtered by *source_cli*.
132
+ """
133
+ q = query.lower()
134
+ results: list[_Item] = []
135
+ for item in self._items:
136
+ if source_cli is not None and item["source_cli"] != source_cli:
137
+ continue
138
+ key_str = str(item.get("key", "")).lower()
139
+ content_str = str(item.get("content", "")).lower()
140
+ tags_raw = item.get("tags", [])
141
+ tags_str = " ".join(str(t).lower() for t in tags_raw) if isinstance(tags_raw, list) else ""
142
+ if q in key_str or q in content_str or q in tags_str:
143
+ results.append(item)
144
+ return results
145
+
146
+ def list_all(self, source_cli: str | None = None) -> list[_Item]:
147
+ """Return all items, optionally filtered by *source_cli*."""
148
+ if source_cli is None:
149
+ return list(self._items)
150
+ return [i for i in self._items if i["source_cli"] == source_cli]
151
+
152
+ def export_all(self) -> list[_Item]:
153
+ """Return a copy of all items as a list."""
154
+ return list(self._items)
155
+
156
+ def import_items(self, items: list[_Item]) -> int:
157
+ """Bulk import items, skipping any whose ``id`` already exists.
158
+
159
+ Returns the number of items actually added.
160
+ """
161
+ existing_ids = {str(i["id"]) for i in self._items}
162
+ added = 0
163
+ for item in items:
164
+ item_id = str(item.get("id", ""))
165
+ if item_id and item_id not in existing_ids:
166
+ self._items.append(item)
167
+ existing_ids.add(item_id)
168
+ added += 1
169
+ if added:
170
+ self._save()
171
+ return added
172
+
173
+ def count(self) -> int:
174
+ """Return the total number of stored items."""
175
+ return len(self._items)
176
+
177
+ def clear(self) -> int:
178
+ """Delete all items and return the count of deleted items."""
179
+ n = len(self._items)
180
+ self._items.clear()
181
+ self._save()
182
+ return n
183
+
184
+ # ------------------------------------------------------------------
185
+ # Internal helpers
186
+ # ------------------------------------------------------------------
187
+
188
+ def _load(self) -> list[_Item]:
189
+ """Load items from the JSON file. Returns empty list if missing."""
190
+ path = Path(self.store_path)
191
+ if not path.exists():
192
+ return []
193
+ try:
194
+ raw = json.loads(path.read_text())
195
+ if isinstance(raw, list):
196
+ return raw # type: ignore[return-value]
197
+ return []
198
+ except (json.JSONDecodeError, ValueError):
199
+ return []
200
+
201
+ def _save(self) -> None:
202
+ """Persist items to disk with atomic write (tmp + os.replace)."""
203
+ path = Path(self.store_path)
204
+ path.parent.mkdir(parents=True, exist_ok=True)
205
+ tmp_path = path.with_name(f"{path.name}.tmp")
206
+ _ = tmp_path.write_text(json.dumps(self._items, indent=2) + "\n")
207
+ _ = os.replace(tmp_path, path)
208
+
209
+
210
+ def _utc_now_iso() -> str:
211
+ """Return current UTC time as ISO 8601 string."""
212
+ return datetime.now(timezone.utc).isoformat()
213
+
214
+
215
+ __all__ = ["MemoryStore", "MemoryStoreFullError"]
@@ -0,0 +1,7 @@
1
+ """Backward-compatible aliases for legacy compatibility routing.
2
+
3
+ Primary implementation lives in `runtime.compat`.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from .compat import * # noqa: F401,F403
File without changes
@@ -0,0 +1,112 @@
1
+ """Codex CLI provider — implements CLIProvider for the ``codex`` binary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import shlex
8
+ import shutil
9
+ import subprocess
10
+ import uuid
11
+ from typing import Any
12
+
13
+ from runtime.cli_provider import CLIProvider, register_provider
14
+ from runtime.tmux_session_manager import TmuxSessionManager
15
+
16
+ _logger = logging.getLogger(__name__)
17
+
18
+
19
+ class CodexProvider(CLIProvider):
20
+ """CLIProvider implementation for the Codex CLI (``codex``)."""
21
+
22
+ # -- identity -----------------------------------------------------------
23
+
24
+ def get_name(self) -> str: # noqa: D401
25
+ """Return the canonical provider name."""
26
+ return "codex"
27
+
28
+ # -- detection ----------------------------------------------------------
29
+
30
+ def detect(self) -> bool:
31
+ """Return ``True`` when the ``codex`` binary is available on PATH."""
32
+ return shutil.which("codex") is not None
33
+
34
+ # -- authentication -----------------------------------------------------
35
+
36
+ def check_auth(self) -> tuple[bool | None, str]:
37
+ """Check Codex authentication status via ``codex auth status``."""
38
+ try:
39
+ result = self.run_tool(["codex", "auth", "status"], timeout=30)
40
+ if result.returncode == 0:
41
+ return True, result.stdout.strip()
42
+ return False, result.stderr.strip() or result.stdout.strip()
43
+ except Exception as exc:
44
+ return None, f"codex auth check failed: {exc}"
45
+
46
+ # -- invocation ---------------------------------------------------------
47
+
48
+ def invoke(self, prompt: str, project_dir: str, timeout: int = 120) -> dict[str, Any]: # pyright: ignore[reportExplicitAny]
49
+ """Invoke ``codex exec --json`` via subprocess."""
50
+ try:
51
+ result = self.run_tool(
52
+ ["codex", "exec", "--json", prompt],
53
+ timeout=timeout,
54
+ )
55
+ return {
56
+ "model": "codex-cli",
57
+ "output": result.stdout,
58
+ "exit_code": result.returncode,
59
+ }
60
+ except subprocess.TimeoutExpired:
61
+ return {"error": "codex-cli timeout", "fallback": "claude"}
62
+ except FileNotFoundError:
63
+ return {"error": "codex-cli not found", "fallback": "claude"}
64
+ except Exception as exc:
65
+ return {"error": str(exc), "fallback": "claude"}
66
+
67
+ def invoke_tmux(self, prompt: str, project_dir: str, timeout: int = 120) -> dict[str, Any]: # pyright: ignore[reportExplicitAny]
68
+ """Invoke ``codex exec --json`` via a persistent tmux session.
69
+
70
+ Falls back to :meth:`invoke` on failure.
71
+ """
72
+ try:
73
+ mgr = TmuxSessionManager()
74
+ session_name = mgr.make_session_name("codex", unique_id=str(uuid.uuid4())[:8])
75
+ session = mgr.get_or_create_session(session_name)
76
+ output = mgr.send_command(session, f"codex exec --json {shlex.quote(prompt)}", timeout=timeout)
77
+ mgr.kill_session(session)
78
+ return {"model": "codex-cli", "output": output, "exit_code": 0}
79
+ except Exception as exc:
80
+ _logger.warning("tmux codex invocation failed, falling back to subprocess: %s", exc)
81
+ return self.invoke(prompt, project_dir, timeout=timeout)
82
+
83
+ # -- command helpers ----------------------------------------------------
84
+
85
+ def get_non_interactive_cmd(self, prompt: str) -> list[str]:
86
+ """Return the non-interactive command for codex."""
87
+ return ["codex", "exec", "--json", prompt]
88
+
89
+ # -- configuration ------------------------------------------------------
90
+
91
+ def get_config_path(self) -> str:
92
+ """Return the Codex configuration file path."""
93
+ return os.path.expanduser("~/.codex/config.toml")
94
+
95
+ def write_mcp_config(self, server_url: str, server_name: str = "memory-server") -> None:
96
+ """Write an MCP server entry to ``~/.codex/config.toml``."""
97
+ config_path = self.get_config_path()
98
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
99
+
100
+ entry = (
101
+ f'[mcp_servers."{server_name}"]\n'
102
+ f'url = "{server_url}"\n'
103
+ )
104
+
105
+ # Append or create
106
+ mode = "a" if os.path.exists(config_path) else "w"
107
+ with open(config_path, mode) as fh:
108
+ fh.write(entry)
109
+
110
+
111
+ # -- auto-register on import -----------------------------------------------
112
+ register_provider(CodexProvider())
@@ -0,0 +1,128 @@
1
+ """Gemini CLI provider — implements CLIProvider for the ``gemini`` binary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import shlex
9
+ import shutil
10
+ import subprocess
11
+ import uuid
12
+ from typing import Any
13
+
14
+ from runtime.cli_provider import CLIProvider, register_provider
15
+ from runtime.tmux_session_manager import TmuxSessionManager
16
+
17
+ _logger = logging.getLogger(__name__)
18
+
19
+
20
+ class GeminiProvider(CLIProvider):
21
+ """CLIProvider implementation for the Gemini CLI (``gemini``)."""
22
+
23
+ # -- identity -----------------------------------------------------------
24
+
25
+ def get_name(self) -> str: # noqa: D401
26
+ """Return the canonical provider name."""
27
+ return "gemini"
28
+
29
+ # -- detection ----------------------------------------------------------
30
+
31
+ def detect(self) -> bool:
32
+ """Return ``True`` when the ``gemini`` binary is available on PATH."""
33
+ return shutil.which("gemini") is not None
34
+
35
+ # -- authentication -----------------------------------------------------
36
+
37
+ def check_auth(self) -> tuple[bool | None, str]:
38
+ """Check Gemini authentication status via ``gemini auth status``."""
39
+ try:
40
+ result = self.run_tool(["gemini", "auth", "status"], timeout=30)
41
+ if result.returncode == 0:
42
+ return True, result.stdout.strip()
43
+ return False, result.stderr.strip() or result.stdout.strip()
44
+ except Exception as exc:
45
+ return None, f"gemini auth check failed: {exc}"
46
+
47
+ # -- invocation ---------------------------------------------------------
48
+
49
+ def invoke(self, prompt: str, project_dir: str, timeout: int = 120) -> dict[str, Any]: # pyright: ignore[reportExplicitAny]
50
+ """Invoke ``gemini -p`` via subprocess.
51
+
52
+ Gemini CLI has no ``--json`` flag — output is plain text stdout.
53
+ """
54
+ try:
55
+ result = self.run_tool(
56
+ ["gemini", "-p", prompt],
57
+ timeout=timeout,
58
+ )
59
+ return {
60
+ "model": "gemini-cli",
61
+ "output": result.stdout,
62
+ "exit_code": result.returncode,
63
+ }
64
+ except subprocess.TimeoutExpired:
65
+ return {"error": "gemini-cli timeout", "fallback": "claude"}
66
+ except FileNotFoundError:
67
+ return {"error": "gemini-cli not found", "fallback": "claude"}
68
+ except Exception as exc:
69
+ return {"error": str(exc), "fallback": "claude"}
70
+
71
+ def invoke_tmux(self, prompt: str, project_dir: str, timeout: int = 120) -> dict[str, Any]: # pyright: ignore[reportExplicitAny]
72
+ """Invoke ``gemini -p`` via a persistent tmux session.
73
+
74
+ Falls back to :meth:`invoke` on failure.
75
+ """
76
+ try:
77
+ mgr = TmuxSessionManager()
78
+ session_name = mgr.make_session_name("gemini", unique_id=str(uuid.uuid4())[:8])
79
+ session = mgr.get_or_create_session(session_name)
80
+ output = mgr.send_command(session, f"gemini -p {shlex.quote(prompt)}", timeout=timeout)
81
+ mgr.kill_session(session)
82
+ return {"model": "gemini-cli", "output": output, "exit_code": 0}
83
+ except Exception as exc:
84
+ _logger.warning("tmux gemini invocation failed, falling back to subprocess: %s", exc)
85
+ return self.invoke(prompt, project_dir, timeout=timeout)
86
+
87
+ # -- command helpers ----------------------------------------------------
88
+
89
+ def get_non_interactive_cmd(self, prompt: str) -> list[str]:
90
+ """Return the non-interactive command for gemini."""
91
+ return ["gemini", "-p", prompt]
92
+
93
+ # -- configuration ------------------------------------------------------
94
+
95
+ def get_config_path(self) -> str:
96
+ """Return the Gemini configuration file path."""
97
+ return os.path.expanduser("~/.gemini/settings.json")
98
+
99
+ def write_mcp_config(self, server_url: str, server_name: str = "memory-server") -> None:
100
+ """Write an MCP server entry to ``~/.gemini/settings.json``.
101
+
102
+ Uses JSON format with ``mcpServers`` key and ``httpUrl`` field,
103
+ merging into any existing configuration.
104
+ """
105
+ config_path = self.get_config_path()
106
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
107
+
108
+ # Load existing config or start fresh
109
+ existing: dict[str, Any] = {} # pyright: ignore[reportExplicitAny]
110
+ if os.path.exists(config_path):
111
+ with open(config_path) as fh:
112
+ try:
113
+ existing = json.load(fh)
114
+ except (json.JSONDecodeError, ValueError):
115
+ existing = {}
116
+
117
+ # Ensure mcpServers dict exists
118
+ if "mcpServers" not in existing:
119
+ existing["mcpServers"] = {}
120
+
121
+ existing["mcpServers"][server_name] = {"httpUrl": server_url}
122
+
123
+ with open(config_path, "w") as fh:
124
+ json.dump(existing, fh, indent=2)
125
+
126
+
127
+ # -- auto-register on import -----------------------------------------------
128
+ register_provider(GeminiProvider())