@trac3er/oh-my-god 1.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 (229) hide show
  1. package/.claude-plugin/marketplace.json +36 -0
  2. package/.claude-plugin/plugin.json +23 -0
  3. package/.claude-plugin/scripts/install.sh +49 -0
  4. package/.claude-plugin/scripts/uninstall.sh +80 -0
  5. package/.claude-plugin/scripts/update.sh +84 -0
  6. package/.mcp.json +20 -0
  7. package/LICENSE +21 -0
  8. package/OMG-setup.sh +1093 -0
  9. package/README.md +335 -0
  10. package/THIRD_PARTY_NOTICES.md +24 -0
  11. package/UPSTREAM_DIFF.md +20 -0
  12. package/agents/__init__.py +1 -0
  13. package/agents/_model_roles.yaml +26 -0
  14. package/agents/designer.md +67 -0
  15. package/agents/explore.md +60 -0
  16. package/agents/model_roles.py +196 -0
  17. package/agents/omg-api-builder.md +23 -0
  18. package/agents/omg-architect-mode.md +43 -0
  19. package/agents/omg-architect.md +13 -0
  20. package/agents/omg-backend-engineer.md +43 -0
  21. package/agents/omg-critic.md +16 -0
  22. package/agents/omg-database-engineer.md +43 -0
  23. package/agents/omg-escalation-router.md +17 -0
  24. package/agents/omg-executor.md +12 -0
  25. package/agents/omg-frontend-designer.md +42 -0
  26. package/agents/omg-implement-mode.md +50 -0
  27. package/agents/omg-infra-engineer.md +43 -0
  28. package/agents/omg-qa-tester.md +16 -0
  29. package/agents/omg-research-mode.md +43 -0
  30. package/agents/omg-security-auditor.md +43 -0
  31. package/agents/omg-testing-engineer.md +43 -0
  32. package/agents/plan.md +80 -0
  33. package/agents/quick_task.md +64 -0
  34. package/agents/reviewer.md +83 -0
  35. package/agents/task.md +71 -0
  36. package/commands/OMG:ccg.md +22 -0
  37. package/commands/OMG:compat.md +57 -0
  38. package/commands/OMG:crazy.md +125 -0
  39. package/commands/OMG:domain-init.md +11 -0
  40. package/commands/OMG:escalate.md +52 -0
  41. package/commands/OMG:health-check.md +45 -0
  42. package/commands/OMG:init.md +134 -0
  43. package/commands/OMG:mode.md +44 -0
  44. package/commands/OMG:project-init.md +11 -0
  45. package/commands/OMG:ralph-start.md +43 -0
  46. package/commands/OMG:ralph-stop.md +23 -0
  47. package/commands/OMG:teams.md +39 -0
  48. package/commands/ai-commit.md +113 -0
  49. package/commands/ccg.md +9 -0
  50. package/commands/create-agent.md +183 -0
  51. package/commands/omc-teams.md +9 -0
  52. package/commands/session-branch.md +85 -0
  53. package/commands/session-fork.md +53 -0
  54. package/commands/session-merge.md +134 -0
  55. package/commands/theme.md +44 -0
  56. package/config/lsp_languages.yaml +324 -0
  57. package/config/themes/catppuccin-frappe.yaml +14 -0
  58. package/config/themes/catppuccin-latte.yaml +14 -0
  59. package/config/themes/catppuccin-macchiato.yaml +14 -0
  60. package/config/themes/catppuccin-mocha.yaml +14 -0
  61. package/config/themes/dracula.yaml +14 -0
  62. package/config/themes/gruvbox-dark.yaml +14 -0
  63. package/config/themes/nord.yaml +14 -0
  64. package/config/themes/one-dark.yaml +14 -0
  65. package/config/themes/solarized-dark.yaml +14 -0
  66. package/config/themes/tokyo-night.yaml +14 -0
  67. package/control_plane/__init__.py +2 -0
  68. package/control_plane/openapi.yaml +109 -0
  69. package/control_plane/server.py +107 -0
  70. package/control_plane/service.py +148 -0
  71. package/crates/omg-natives/Cargo.toml +17 -0
  72. package/crates/omg-natives/src/clipboard.rs +5 -0
  73. package/crates/omg-natives/src/glob.rs +15 -0
  74. package/crates/omg-natives/src/grep.rs +15 -0
  75. package/crates/omg-natives/src/highlight.rs +15 -0
  76. package/crates/omg-natives/src/html.rs +14 -0
  77. package/crates/omg-natives/src/image.rs +5 -0
  78. package/crates/omg-natives/src/keys.rs +5 -0
  79. package/crates/omg-natives/src/lib.rs +36 -0
  80. package/crates/omg-natives/src/prof.rs +5 -0
  81. package/crates/omg-natives/src/ps.rs +5 -0
  82. package/crates/omg-natives/src/shell.rs +5 -0
  83. package/crates/omg-natives/src/task.rs +5 -0
  84. package/crates/omg-natives/src/text.rs +14 -0
  85. package/hooks/_agent_registry.py +421 -0
  86. package/hooks/_budget.py +31 -0
  87. package/hooks/_common.py +476 -0
  88. package/hooks/_learnings.py +126 -0
  89. package/hooks/_memory.py +103 -0
  90. package/hooks/circuit-breaker.py +270 -0
  91. package/hooks/config-guard.py +163 -0
  92. package/hooks/context_pressure.py +53 -0
  93. package/hooks/credential_store.py +801 -0
  94. package/hooks/fetch-rate-limits.py +212 -0
  95. package/hooks/firewall.py +48 -0
  96. package/hooks/hashline-formatter-bridge.py +224 -0
  97. package/hooks/hashline-injector.py +273 -0
  98. package/hooks/hashline-validator.py +216 -0
  99. package/hooks/idle-detector.py +95 -0
  100. package/hooks/intentgate-keyword-detector.py +188 -0
  101. package/hooks/magic-keyword-router.py +195 -0
  102. package/hooks/policy_engine.py +310 -0
  103. package/hooks/post-tool-failure.py +19 -0
  104. package/hooks/post-write.py +199 -0
  105. package/hooks/pre-compact.py +204 -0
  106. package/hooks/pre-tool-inject.py +98 -0
  107. package/hooks/prompt-enhancer.py +672 -0
  108. package/hooks/quality-runner.py +191 -0
  109. package/hooks/secret-guard.py +47 -0
  110. package/hooks/session-end-capture.py +137 -0
  111. package/hooks/session-start.py +275 -0
  112. package/hooks/shadow_manager.py +297 -0
  113. package/hooks/state_migration.py +209 -0
  114. package/hooks/stop-gate.py +7 -0
  115. package/hooks/stop_dispatcher.py +929 -0
  116. package/hooks/test-validator.py +138 -0
  117. package/hooks/todo-state-tracker.py +114 -0
  118. package/hooks/tool-ledger.py +126 -0
  119. package/hooks/trust_review.py +524 -0
  120. package/install.sh +9 -0
  121. package/omg_natives/__init__.py +186 -0
  122. package/omg_natives/_bindings.py +165 -0
  123. package/omg_natives/clipboard.py +36 -0
  124. package/omg_natives/glob.py +42 -0
  125. package/omg_natives/grep.py +61 -0
  126. package/omg_natives/highlight.py +54 -0
  127. package/omg_natives/html.py +157 -0
  128. package/omg_natives/image.py +51 -0
  129. package/omg_natives/keys.py +46 -0
  130. package/omg_natives/prof.py +39 -0
  131. package/omg_natives/ps.py +93 -0
  132. package/omg_natives/shell.py +58 -0
  133. package/omg_natives/task.py +41 -0
  134. package/omg_natives/text.py +50 -0
  135. package/package.json +26 -0
  136. package/plugins/README.md +82 -0
  137. package/plugins/advanced/commands/OMG:code-review.md +114 -0
  138. package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
  139. package/plugins/advanced/commands/OMG:handoff.md +115 -0
  140. package/plugins/advanced/commands/OMG:learn.md +110 -0
  141. package/plugins/advanced/commands/OMG:maintainer.md +31 -0
  142. package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
  143. package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
  144. package/plugins/advanced/commands/OMG:security-review.md +119 -0
  145. package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
  146. package/plugins/advanced/commands/OMG:ship.md +46 -0
  147. package/plugins/advanced/plugin.json +96 -0
  148. package/plugins/core/plugin.json +82 -0
  149. package/pytest.ini +5 -0
  150. package/registry/__init__.py +1 -0
  151. package/registry/verify_artifact.py +90 -0
  152. package/rules/contextual/architect-mode.md +9 -0
  153. package/rules/contextual/big-picture.md +20 -0
  154. package/rules/contextual/code-hygiene.md +26 -0
  155. package/rules/contextual/context-management.md +19 -0
  156. package/rules/contextual/context-minimization.md +32 -0
  157. package/rules/contextual/ddd-sdd.md +28 -0
  158. package/rules/contextual/dependency-safety.md +16 -0
  159. package/rules/contextual/doc-check.md +13 -0
  160. package/rules/contextual/implement-mode.md +9 -0
  161. package/rules/contextual/infra-safety.md +14 -0
  162. package/rules/contextual/outside-in.md +13 -0
  163. package/rules/contextual/persistent-mode.md +24 -0
  164. package/rules/contextual/research-mode.md +9 -0
  165. package/rules/contextual/security-domains.md +25 -0
  166. package/rules/contextual/vision-detection.md +27 -0
  167. package/rules/contextual/web-search.md +25 -0
  168. package/rules/contextual/write-verify.md +23 -0
  169. package/rules/core/00-truth.md +20 -0
  170. package/rules/core/01-surgical.md +19 -0
  171. package/rules/core/02-circuit-breaker.md +22 -0
  172. package/rules/core/03-ensemble.md +28 -0
  173. package/rules/core/04-testing.md +30 -0
  174. package/runtime/__init__.py +32 -0
  175. package/runtime/adapters/__init__.py +13 -0
  176. package/runtime/adapters/claude.py +60 -0
  177. package/runtime/adapters/gpt.py +53 -0
  178. package/runtime/adapters/local.py +53 -0
  179. package/runtime/business_workflow.py +220 -0
  180. package/runtime/compat.py +1299 -0
  181. package/runtime/custom_agent_loader.py +366 -0
  182. package/runtime/dispatcher.py +47 -0
  183. package/runtime/ecosystem.py +371 -0
  184. package/runtime/legacy_compat.py +7 -0
  185. package/runtime/omc_compat.py +7 -0
  186. package/runtime/omc_contract_snapshot.json +916 -0
  187. package/runtime/omg_compat_contract_snapshot.json +916 -0
  188. package/runtime/subagent_dispatcher.py +362 -0
  189. package/runtime/team_router.py +838 -0
  190. package/scripts/check-omc-contract-snapshot.py +12 -0
  191. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  192. package/scripts/check-omg-standalone-clean.py +102 -0
  193. package/scripts/legacy_to_omg_migrate.py +29 -0
  194. package/scripts/migrate-omc.py +464 -0
  195. package/scripts/omc_to_omg_migrate.py +12 -0
  196. package/scripts/omg.py +493 -0
  197. package/scripts/settings-merge.py +224 -0
  198. package/scripts/verify-no-omc.sh +5 -0
  199. package/scripts/verify-standalone.sh +21 -0
  200. package/templates/idea.yml +30 -0
  201. package/templates/policy.yaml +15 -0
  202. package/templates/profile.yaml +25 -0
  203. package/templates/runtime.yaml +12 -0
  204. package/templates/working-memory.md +17 -0
  205. package/tools/__init__.py +2 -0
  206. package/tools/browser_consent.py +289 -0
  207. package/tools/browser_stealth.py +481 -0
  208. package/tools/browser_tool.py +448 -0
  209. package/tools/changelog_generator.py +268 -0
  210. package/tools/commit_splitter.py +361 -0
  211. package/tools/config_discovery.py +151 -0
  212. package/tools/config_merger.py +449 -0
  213. package/tools/git_inspector.py +298 -0
  214. package/tools/lsp_client.py +275 -0
  215. package/tools/lsp_discovery.py +231 -0
  216. package/tools/lsp_operations.py +392 -0
  217. package/tools/python_repl.py +656 -0
  218. package/tools/python_sandbox.py +609 -0
  219. package/tools/search_providers/__init__.py +77 -0
  220. package/tools/search_providers/brave.py +115 -0
  221. package/tools/search_providers/exa.py +116 -0
  222. package/tools/search_providers/jina.py +104 -0
  223. package/tools/search_providers/perplexity.py +139 -0
  224. package/tools/search_providers/synthetic.py +74 -0
  225. package/tools/session_snapshot.py +736 -0
  226. package/tools/ssh_manager.py +912 -0
  227. package/tools/theme_engine.py +294 -0
  228. package/tools/theme_selector.py +137 -0
  229. package/tools/web_search.py +622 -0
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Brave Search Provider for OMG
4
+
5
+ Uses the Brave Search API (https://api.search.brave.com/res/v1/web/search).
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+ import urllib.error
12
+ import urllib.parse
13
+ import urllib.request
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ _tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+ if _tools_dir not in sys.path:
18
+ sys.path.insert(0, _tools_dir)
19
+
20
+ from web_search import Provider, SearchResult, get_api_key
21
+
22
+
23
+ class BraveProvider(Provider):
24
+ """Search provider using the Brave Search API.
25
+
26
+ Endpoint: https://api.search.brave.com/res/v1/web/search
27
+ Requires an API key (BRAVE_API_KEY env var or credential store).
28
+ """
29
+
30
+ CONFIG_SCHEMA: Dict[str, Any] = {
31
+ "api_key": {"type": "str", "required": True},
32
+ "count": {"type": "int", "required": False, "default": 10},
33
+ "country": {"type": "str", "required": False, "default": ""},
34
+ }
35
+
36
+ API_URL = "https://api.search.brave.com/res/v1/web/search"
37
+
38
+ def __init__(self, api_key: Optional[str] = None) -> None:
39
+ """Initialize BraveProvider.
40
+
41
+ Args:
42
+ api_key: Brave API key. If None, resolves via get_api_key('brave').
43
+ """
44
+ self._api_key = api_key or get_api_key("brave")
45
+
46
+ def search(self, query: str, **kwargs: Any) -> List[Dict[str, str]]:
47
+ """Search using the Brave Search API.
48
+
49
+ Args:
50
+ query: The search query string.
51
+ **kwargs: Optional 'count' (default 10), 'country'.
52
+
53
+ Returns:
54
+ A list of result dicts, or empty list on error.
55
+ """
56
+ if not self._api_key:
57
+ return []
58
+
59
+ count = kwargs.get("count", 10)
60
+ params = {"q": query, "count": str(count)}
61
+ country = kwargs.get("country", "")
62
+ if country:
63
+ params["country"] = country
64
+
65
+ url = f"{self.API_URL}?{urllib.parse.urlencode(params)}"
66
+ req = urllib.request.Request(
67
+ url,
68
+ headers={
69
+ "Accept": "application/json",
70
+ "Accept-Encoding": "gzip",
71
+ "X-Subscription-Token": self._api_key,
72
+ "User-Agent": "OMG-WebSearch/1.0",
73
+ },
74
+ )
75
+
76
+ try:
77
+ with urllib.request.urlopen(req, timeout=15) as resp:
78
+ data = json.loads(resp.read().decode("utf-8"))
79
+ except (urllib.error.HTTPError, urllib.error.URLError):
80
+ return []
81
+ except Exception:
82
+ return []
83
+
84
+ results = []
85
+ web_results = data.get("web", {}).get("results", [])
86
+ for item in web_results:
87
+ results.append({
88
+ "title": item.get("title", ""),
89
+ "url": item.get("url", ""),
90
+ "snippet": item.get("description", ""),
91
+ "source": "brave",
92
+ })
93
+ return results
94
+
95
+ def fetch(self, url: str) -> str:
96
+ """Fetch URL content using stdlib urllib.
97
+
98
+ Args:
99
+ url: The URL to fetch.
100
+
101
+ Returns:
102
+ Page content as string, or empty string on error.
103
+ """
104
+ try:
105
+ req = urllib.request.Request(
106
+ url,
107
+ headers={"User-Agent": "OMG-WebSearch/1.0"},
108
+ )
109
+ with urllib.request.urlopen(req, timeout=15) as resp:
110
+ encoding = resp.headers.get_content_charset() or "utf-8"
111
+ return resp.read().decode(encoding, errors="replace")
112
+ except (urllib.error.HTTPError, urllib.error.URLError):
113
+ return ""
114
+ except Exception:
115
+ return ""
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Exa Search Provider for OMG
4
+
5
+ Uses the Exa AI search API (https://api.exa.ai/search) for semantic web search.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+ import urllib.error
12
+ import urllib.request
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ _tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
16
+ if _tools_dir not in sys.path:
17
+ sys.path.insert(0, _tools_dir)
18
+
19
+ from web_search import Provider, SearchResult, get_api_key
20
+
21
+
22
+ class ExaProvider(Provider):
23
+ """Search provider using the Exa AI search API.
24
+
25
+ Endpoint: https://api.exa.ai/search
26
+ Requires an API key (EXA_API_KEY env var or credential store).
27
+ """
28
+
29
+ CONFIG_SCHEMA: Dict[str, Any] = {
30
+ "api_key": {"type": "str", "required": True},
31
+ "num_results": {"type": "int", "required": False, "default": 10},
32
+ "use_autoprompt": {"type": "bool", "required": False, "default": True},
33
+ }
34
+
35
+ API_URL = "https://api.exa.ai/search"
36
+
37
+ def __init__(self, api_key: Optional[str] = None) -> None:
38
+ """Initialize ExaProvider.
39
+
40
+ Args:
41
+ api_key: Exa API key. If None, resolves via get_api_key('exa').
42
+ """
43
+ self._api_key = api_key or get_api_key("exa")
44
+
45
+ def search(self, query: str, **kwargs: Any) -> List[Dict[str, str]]:
46
+ """Search using the Exa AI API.
47
+
48
+ Args:
49
+ query: The search query string.
50
+ **kwargs: Optional 'num_results' (default 10), 'use_autoprompt'.
51
+
52
+ Returns:
53
+ A list of result dicts, or empty list on error.
54
+ """
55
+ if not self._api_key:
56
+ return []
57
+
58
+ num_results = kwargs.get("num_results", 10)
59
+ use_autoprompt = kwargs.get("use_autoprompt", True)
60
+
61
+ payload = json.dumps({
62
+ "query": query,
63
+ "numResults": num_results,
64
+ "useAutoprompt": use_autoprompt,
65
+ }).encode("utf-8")
66
+
67
+ req = urllib.request.Request(
68
+ self.API_URL,
69
+ data=payload,
70
+ headers={
71
+ "Content-Type": "application/json",
72
+ "x-api-key": self._api_key,
73
+ "User-Agent": "OMG-WebSearch/1.0",
74
+ },
75
+ method="POST",
76
+ )
77
+
78
+ try:
79
+ with urllib.request.urlopen(req, timeout=15) as resp:
80
+ data = json.loads(resp.read().decode("utf-8"))
81
+ except (urllib.error.HTTPError, urllib.error.URLError):
82
+ return []
83
+ except Exception:
84
+ return []
85
+
86
+ results = []
87
+ for item in data.get("results", []):
88
+ results.append({
89
+ "title": item.get("title", ""),
90
+ "url": item.get("url", ""),
91
+ "snippet": item.get("text", item.get("snippet", "")),
92
+ "source": "exa",
93
+ })
94
+ return results
95
+
96
+ def fetch(self, url: str) -> str:
97
+ """Fetch URL content using stdlib urllib.
98
+
99
+ Args:
100
+ url: The URL to fetch.
101
+
102
+ Returns:
103
+ Page content as string, or empty string on error.
104
+ """
105
+ try:
106
+ req = urllib.request.Request(
107
+ url,
108
+ headers={"User-Agent": "OMG-WebSearch/1.0"},
109
+ )
110
+ with urllib.request.urlopen(req, timeout=15) as resp:
111
+ encoding = resp.headers.get_content_charset() or "utf-8"
112
+ return resp.read().decode(encoding, errors="replace")
113
+ except (urllib.error.HTTPError, urllib.error.URLError):
114
+ return ""
115
+ except Exception:
116
+ return ""
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Jina Reader Provider for OMG
4
+
5
+ Uses the Jina Reader API (https://r.jina.ai/) for URL-based content extraction.
6
+ Jina Reader converts web pages to clean, readable text — it is primarily a
7
+ fetch/reader tool rather than a traditional search engine.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ import urllib.error
14
+ import urllib.request
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ _tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18
+ if _tools_dir not in sys.path:
19
+ sys.path.insert(0, _tools_dir)
20
+
21
+ from web_search import Provider, SearchResult, get_api_key
22
+
23
+
24
+ class JinaProvider(Provider):
25
+ """Content extraction provider using Jina Reader API.
26
+
27
+ Endpoint: https://r.jina.ai/{url}
28
+ Optionally accepts an API key (JINA_API_KEY) for higher rate limits.
29
+ Primary use is fetch() — search() builds a reader URL from the query.
30
+ """
31
+
32
+ CONFIG_SCHEMA: Dict[str, Any] = {
33
+ "api_key": {"type": "str", "required": False},
34
+ "return_format": {"type": "str", "required": False, "default": "text"},
35
+ }
36
+
37
+ BASE_URL = "https://r.jina.ai/"
38
+
39
+ def __init__(self, api_key: Optional[str] = None) -> None:
40
+ """Initialize JinaProvider.
41
+
42
+ Args:
43
+ api_key: Jina API key. Optional — Jina works without a key
44
+ but has lower rate limits.
45
+ """
46
+ self._api_key = api_key or get_api_key("jina")
47
+
48
+ def search(self, query: str, **kwargs: Any) -> List[Dict[str, str]]:
49
+ """Search by treating the query as a URL to read via Jina.
50
+
51
+ If the query looks like a URL, fetches it through Jina Reader.
52
+ Otherwise, returns an empty list (Jina Reader is URL-based, not
53
+ a search engine).
54
+
55
+ Args:
56
+ query: A URL string to read, or a search query (returns empty).
57
+ **kwargs: Optional 'return_format' ('text' or 'markdown').
58
+
59
+ Returns:
60
+ A list with one result dict if query is a URL, else empty list.
61
+ """
62
+ # Jina Reader is URL-based — only process URLs
63
+ if not query.startswith(("http://", "https://")):
64
+ return []
65
+
66
+ content = self.fetch(query)
67
+ if not content:
68
+ return []
69
+
70
+ return [{
71
+ "title": f"Jina Reader: {query[:80]}",
72
+ "url": query,
73
+ "snippet": content[:500],
74
+ "source": "jina",
75
+ }]
76
+
77
+ def fetch(self, url: str) -> str:
78
+ """Fetch and extract clean text from a URL using Jina Reader.
79
+
80
+ Args:
81
+ url: The URL to fetch and extract content from.
82
+
83
+ Returns:
84
+ Clean text content, or empty string on error.
85
+ """
86
+ reader_url = f"{self.BASE_URL}{url}"
87
+
88
+ headers = {
89
+ "Accept": "text/plain",
90
+ "User-Agent": "OMG-WebSearch/1.0",
91
+ }
92
+ if self._api_key:
93
+ headers["Authorization"] = f"Bearer {self._api_key}"
94
+
95
+ req = urllib.request.Request(reader_url, headers=headers)
96
+
97
+ try:
98
+ with urllib.request.urlopen(req, timeout=20) as resp:
99
+ encoding = resp.headers.get_content_charset() or "utf-8"
100
+ return resp.read().decode(encoding, errors="replace")
101
+ except (urllib.error.HTTPError, urllib.error.URLError):
102
+ return ""
103
+ except Exception:
104
+ return ""
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Perplexity Search Provider for OMG
4
+
5
+ Uses the Perplexity AI chat completions API
6
+ (https://api.perplexity.ai/chat/completions) for AI-powered web search.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ import urllib.error
13
+ import urllib.request
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ _tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+ if _tools_dir not in sys.path:
18
+ sys.path.insert(0, _tools_dir)
19
+
20
+ from web_search import Provider, SearchResult, get_api_key
21
+
22
+
23
+ class PerplexityProvider(Provider):
24
+ """Search provider using the Perplexity AI API.
25
+
26
+ Endpoint: https://api.perplexity.ai/chat/completions
27
+ Requires an API key (PERPLEXITY_API_KEY env var or credential store).
28
+ Uses the sonar model for online search-augmented responses.
29
+ """
30
+
31
+ CONFIG_SCHEMA: Dict[str, Any] = {
32
+ "api_key": {"type": "str", "required": True},
33
+ "model": {"type": "str", "required": False, "default": "sonar"},
34
+ }
35
+
36
+ API_URL = "https://api.perplexity.ai/chat/completions"
37
+
38
+ def __init__(self, api_key: Optional[str] = None) -> None:
39
+ """Initialize PerplexityProvider.
40
+
41
+ Args:
42
+ api_key: Perplexity API key. If None, resolves via
43
+ get_api_key('perplexity').
44
+ """
45
+ self._api_key = api_key or get_api_key("perplexity")
46
+
47
+ def search(self, query: str, **kwargs: Any) -> List[Dict[str, str]]:
48
+ """Search using the Perplexity AI API.
49
+
50
+ Sends the query as a chat message and parses citations from the
51
+ response into search result format.
52
+
53
+ Args:
54
+ query: The search query string.
55
+ **kwargs: Optional 'model' (default 'sonar').
56
+
57
+ Returns:
58
+ A list of result dicts, or empty list on error.
59
+ """
60
+ if not self._api_key:
61
+ return []
62
+
63
+ model = kwargs.get("model", "sonar")
64
+
65
+ payload = json.dumps({
66
+ "model": model,
67
+ "messages": [
68
+ {"role": "user", "content": query},
69
+ ],
70
+ }).encode("utf-8")
71
+
72
+ req = urllib.request.Request(
73
+ self.API_URL,
74
+ data=payload,
75
+ headers={
76
+ "Content-Type": "application/json",
77
+ "Authorization": f"Bearer {self._api_key}",
78
+ "User-Agent": "OMG-WebSearch/1.0",
79
+ },
80
+ method="POST",
81
+ )
82
+
83
+ try:
84
+ with urllib.request.urlopen(req, timeout=30) as resp:
85
+ data = json.loads(resp.read().decode("utf-8"))
86
+ except (urllib.error.HTTPError, urllib.error.URLError):
87
+ return []
88
+ except Exception:
89
+ return []
90
+
91
+ results = []
92
+ # Extract answer text
93
+ answer = ""
94
+ choices = data.get("choices", [])
95
+ if choices:
96
+ answer = choices[0].get("message", {}).get("content", "")
97
+
98
+ # Extract citations if available
99
+ citations = data.get("citations", [])
100
+ if citations:
101
+ for i, url in enumerate(citations):
102
+ results.append({
103
+ "title": f"Citation {i + 1}",
104
+ "url": url if isinstance(url, str) else str(url),
105
+ "snippet": answer[:200] if i == 0 else "",
106
+ "source": "perplexity",
107
+ })
108
+ elif answer:
109
+ # No citations — return answer as a single result
110
+ results.append({
111
+ "title": f"Perplexity answer for: {query[:80]}",
112
+ "url": "",
113
+ "snippet": answer[:500],
114
+ "source": "perplexity",
115
+ })
116
+
117
+ return results
118
+
119
+ def fetch(self, url: str) -> str:
120
+ """Fetch URL content using stdlib urllib.
121
+
122
+ Args:
123
+ url: The URL to fetch.
124
+
125
+ Returns:
126
+ Page content as string, or empty string on error.
127
+ """
128
+ try:
129
+ req = urllib.request.Request(
130
+ url,
131
+ headers={"User-Agent": "OMG-WebSearch/1.0"},
132
+ )
133
+ with urllib.request.urlopen(req, timeout=15) as resp:
134
+ encoding = resp.headers.get_content_charset() or "utf-8"
135
+ return resp.read().decode(encoding, errors="replace")
136
+ except (urllib.error.HTTPError, urllib.error.URLError):
137
+ return ""
138
+ except Exception:
139
+ return ""
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Synthetic Search Provider for OMG
4
+
5
+ Returns mock/fake results without making any API calls.
6
+ Useful for testing, dry-run mode, and offline development.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ # Ensure tools dir is on path for imports
14
+ _tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15
+ if _tools_dir not in sys.path:
16
+ sys.path.insert(0, _tools_dir)
17
+
18
+ from web_search import Provider, SearchResult
19
+
20
+
21
+ class SyntheticProvider(Provider):
22
+ """Mock search provider that returns fake results without API calls.
23
+
24
+ Designed for testing, dry-run mode, and offline development.
25
+ Does not require any API key or network access.
26
+ """
27
+
28
+ CONFIG_SCHEMA: Dict[str, Any] = {
29
+ "api_key": {"type": "str", "required": False},
30
+ "num_results": {"type": "int", "required": False, "default": 3},
31
+ }
32
+
33
+ def __init__(self, api_key: Optional[str] = None) -> None:
34
+ """Initialize SyntheticProvider.
35
+
36
+ Args:
37
+ api_key: Ignored — synthetic provider needs no credentials.
38
+ """
39
+ self._api_key = api_key # Accepted but unused
40
+
41
+ def search(self, query: str, **kwargs: Any) -> List[Dict[str, str]]:
42
+ """Return synthetic search results for any query.
43
+
44
+ Args:
45
+ query: The search query string.
46
+ **kwargs: Optional 'num_results' (default 3).
47
+
48
+ Returns:
49
+ A list of fake result dicts with title, url, snippet keys.
50
+ """
51
+ num_results = kwargs.get("num_results", 3)
52
+ results = []
53
+ for i in range(1, num_results + 1):
54
+ results.append({
55
+ "title": f"Synthetic Result {i} for: {query}",
56
+ "url": f"https://synthetic.example.com/result/{i}",
57
+ "snippet": f"This is a synthetic snippet #{i} for query '{query}'.",
58
+ "source": "synthetic",
59
+ })
60
+ return results
61
+
62
+ def fetch(self, url: str) -> str:
63
+ """Return synthetic page content for any URL.
64
+
65
+ Args:
66
+ url: The URL to 'fetch'.
67
+
68
+ Returns:
69
+ A synthetic HTML string mentioning the URL.
70
+ """
71
+ return (
72
+ f"<html><head><title>Synthetic Page</title></head>"
73
+ f"<body><p>Synthetic content for {url}</p></body></html>"
74
+ )