hyperagents 1.2.3rc5.post3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. ui/tui/app.py +214 -0
@@ -0,0 +1,443 @@
1
+ """iMessageConnector — Apple iMessage integration via AppleScript or BlueBubbles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import sys
8
+ from typing import AsyncIterator
9
+
10
+ import httpx
11
+
12
+ from integrations.base import (
13
+ BaseConnector,
14
+ ConnectorCapability,
15
+ ConnectorInfo,
16
+ InboundMessage,
17
+ OutboundMessage,
18
+ )
19
+
20
+ logger = logging.getLogger("hyperclaw.integrations.imessage")
21
+
22
+
23
+ class iMessageConnector(BaseConnector):
24
+ """
25
+ iMessage connector.
26
+
27
+ Two modes:
28
+ 1. Native macOS: Uses AppleScript (requires macOS)
29
+ 2. BlueBubbles: Uses BlueBubbles server API (any platform)
30
+
31
+ Optional config:
32
+ - bluebubbles_url: BlueBubbles server URL
33
+ - bluebubbles_password: BlueBubbles server password
34
+ """
35
+
36
+ def __init__(self, config: dict) -> None:
37
+ self.config = config
38
+ self.bluebubbles_url = config.get("bluebubbles_url", "").rstrip("/")
39
+ self.bluebubbles_password = config.get("bluebubbles_password", "")
40
+
41
+ self._client: httpx.AsyncClient | None = None
42
+
43
+ @property
44
+ def info(self) -> ConnectorInfo:
45
+ return ConnectorInfo(
46
+ connector_id="imessage",
47
+ platform="imessage",
48
+ category="messaging",
49
+ capabilities=frozenset(
50
+ [
51
+ ConnectorCapability.SEND_MESSAGE,
52
+ ConnectorCapability.RECEIVE_MESSAGE,
53
+ ConnectorCapability.FILE_UPLOAD,
54
+ ]
55
+ ),
56
+ auth_type="api_key" if self.bluebubbles_url else "oauth",
57
+ rate_limit_per_minute=30,
58
+ supports_threads=True,
59
+ supports_attachments=True,
60
+ supports_reactions=True,
61
+ )
62
+
63
+ async def _get_client(self) -> httpx.AsyncClient:
64
+ if self._client is None:
65
+ self._client = httpx.AsyncClient(timeout=30.0)
66
+ return self._client
67
+
68
+ async def health(self) -> bool:
69
+ """
70
+ Check if iMessage is available.
71
+ Returns True on macOS (native) or if BlueBubbles is reachable.
72
+ """
73
+ # If BlueBubbles is configured, check that
74
+ if self.bluebubbles_url:
75
+ try:
76
+ client = await self._get_client()
77
+ response = await client.get(
78
+ f"{self.bluebubbles_url}/api/v1/server/info",
79
+ params={"password": self.bluebubbles_password},
80
+ )
81
+ return response.status_code == 200
82
+ except Exception as e:
83
+ logger.error(f"BlueBubbles health check failed: {e}")
84
+ return False
85
+
86
+ # Native macOS check
87
+ return sys.platform == "darwin"
88
+
89
+ async def _send_impl(self, message: OutboundMessage) -> str:
90
+ """Send a message via iMessage."""
91
+ if self.bluebubbles_url:
92
+ return await self._send_via_bluebubbles(message)
93
+ else:
94
+ return await self._send_via_applescript(message)
95
+
96
+ async def _send_via_applescript(self, message: OutboundMessage) -> str:
97
+ """Send via native macOS AppleScript."""
98
+ if sys.platform != "darwin":
99
+ raise RuntimeError("AppleScript iMessage requires macOS")
100
+
101
+ # Escape single quotes in the message
102
+ content = message.content.replace("'", "'\\''")
103
+ recipient = message.recipient_id.replace("'", "'\\''")
104
+
105
+ script = f'''
106
+ tell application "Messages"
107
+ set targetService to 1st service whose service type = iMessage
108
+ set targetBuddy to buddy "{recipient}" of targetService
109
+ send "{content}" to targetBuddy
110
+ end tell
111
+ '''
112
+
113
+ proc = await asyncio.create_subprocess_exec(
114
+ "osascript",
115
+ "-e",
116
+ script,
117
+ stdout=asyncio.subprocess.PIPE,
118
+ stderr=asyncio.subprocess.PIPE,
119
+ )
120
+
121
+ stdout, stderr = await proc.communicate()
122
+
123
+ if proc.returncode != 0:
124
+ raise Exception(f"AppleScript failed: {stderr.decode()}")
125
+
126
+ # AppleScript doesn't return message IDs
127
+ return "applescript_sent"
128
+
129
+ async def _send_via_bluebubbles(self, message: OutboundMessage) -> str:
130
+ """Send via BlueBubbles server."""
131
+ client = await self._get_client()
132
+
133
+ payload = {
134
+ "chatGuid": f"iMessage;-;{message.recipient_id}",
135
+ "message": message.content,
136
+ "method": "apple-script",
137
+ }
138
+
139
+ response = await client.post(
140
+ f"{self.bluebubbles_url}/api/v1/message/text",
141
+ params={"password": self.bluebubbles_password},
142
+ json=payload,
143
+ )
144
+
145
+ if response.status_code not in (200, 201):
146
+ raise Exception(f"BlueBubbles send failed: {response.text}")
147
+
148
+ data = response.json()
149
+ return data.get("data", {}).get("guid", "")
150
+
151
+ async def _receive_impl(self) -> AsyncIterator[InboundMessage]:
152
+ """Receive messages via BlueBubbles."""
153
+ if not self.bluebubbles_url:
154
+ # Native macOS doesn't have a polling API
155
+ return
156
+ yield
157
+
158
+ client = await self._get_client()
159
+
160
+ response = await client.get(
161
+ f"{self.bluebubbles_url}/api/v1/message",
162
+ params={
163
+ "password": self.bluebubbles_password,
164
+ "limit": 50,
165
+ "sort": "desc",
166
+ },
167
+ )
168
+
169
+ if response.status_code != 200:
170
+ return
171
+
172
+ data = response.json()
173
+
174
+ for msg in data.get("data", []):
175
+ if msg.get("isFromMe"):
176
+ continue
177
+
178
+ handle = msg.get("handle", {})
179
+
180
+ attachments = []
181
+ for attach in msg.get("attachments", []):
182
+ attachments.append({
183
+ "type": attach.get("mimeType", ""),
184
+ "id": attach.get("guid", ""),
185
+ "filename": attach.get("transferName", ""),
186
+ })
187
+
188
+ yield InboundMessage(
189
+ message_id=msg.get("guid", ""),
190
+ platform="imessage",
191
+ sender_id=handle.get("address", ""),
192
+ sender_name=handle.get("address", ""),
193
+ content=msg.get("text", ""),
194
+ thread_id=msg.get("chatGuid"),
195
+ attachments=attachments,
196
+ raw=msg,
197
+ )
198
+
199
+ async def send_attachment(
200
+ self,
201
+ recipient: str,
202
+ file_path: str,
203
+ message: str = "",
204
+ ) -> str:
205
+ """Send a file attachment."""
206
+ if self.bluebubbles_url:
207
+ client = await self._get_client()
208
+
209
+ with open(file_path, "rb") as f:
210
+ files = {"attachment": f}
211
+ data = {
212
+ "chatGuid": f"iMessage;-;{recipient}",
213
+ "message": message,
214
+ }
215
+
216
+ response = await client.post(
217
+ f"{self.bluebubbles_url}/api/v1/message/attachment",
218
+ params={"password": self.bluebubbles_password},
219
+ data=data,
220
+ files=files,
221
+ )
222
+
223
+ if response.status_code not in (200, 201):
224
+ raise Exception(f"BlueBubbles attachment failed: {response.text}")
225
+
226
+ result = response.json()
227
+ return result.get("data", {}).get("guid", "")
228
+
229
+ else:
230
+ # AppleScript attachment sending
231
+ if sys.platform != "darwin":
232
+ raise RuntimeError("AppleScript iMessage requires macOS")
233
+
234
+ file_path_escaped = file_path.replace("'", "'\\''")
235
+ recipient_escaped = recipient.replace("'", "'\\''")
236
+
237
+ script = f'''
238
+ tell application "Messages"
239
+ set targetService to 1st service whose service type = iMessage
240
+ set targetBuddy to buddy "{recipient_escaped}" of targetService
241
+ set theFile to POSIX file "{file_path_escaped}"
242
+ send theFile to targetBuddy
243
+ end tell
244
+ '''
245
+
246
+ proc = await asyncio.create_subprocess_exec(
247
+ "osascript",
248
+ "-e",
249
+ script,
250
+ stdout=asyncio.subprocess.PIPE,
251
+ stderr=asyncio.subprocess.PIPE,
252
+ )
253
+
254
+ stdout, stderr = await proc.communicate()
255
+
256
+ if proc.returncode != 0:
257
+ raise Exception(f"AppleScript attachment failed: {stderr.decode()}")
258
+
259
+ return "applescript_attachment_sent"
260
+
261
+
262
+ # =============================================================================
263
+ # Standalone Functions for Direct Use
264
+ # =============================================================================
265
+
266
+ def send_imessage(recipient: str, message: str) -> dict:
267
+ """
268
+ Send an iMessage via AppleScript (macOS only).
269
+
270
+ Args:
271
+ recipient: Phone number or Apple ID (e.g., "+15551234567" or "user@icloud.com")
272
+ message: The message text to send
273
+
274
+ Returns:
275
+ dict with 'success' and 'message' or 'error' keys
276
+ """
277
+ import subprocess
278
+
279
+ if sys.platform != "darwin":
280
+ return {"success": False, "error": "iMessage requires macOS"}
281
+
282
+ # Escape for AppleScript
283
+ message_escaped = message.replace('\\', '\\\\').replace('"', '\\"')
284
+ recipient_escaped = recipient.replace('\\', '\\\\').replace('"', '\\"')
285
+
286
+ # Try direct send first (works for phone numbers)
287
+ script = f'''
288
+ tell application "Messages"
289
+ send "{message_escaped}" to buddy "{recipient_escaped}" of (service 1 whose service type is iMessage)
290
+ end tell
291
+ '''
292
+
293
+ try:
294
+ result = subprocess.run(
295
+ ["osascript", "-e", script],
296
+ capture_output=True,
297
+ text=True,
298
+ timeout=30
299
+ )
300
+
301
+ if result.returncode == 0:
302
+ return {"success": True, "message": f"iMessage sent to {recipient}"}
303
+
304
+ # Fallback: try participant-based approach
305
+ script2 = f'''
306
+ tell application "Messages"
307
+ set targetService to 1st account whose service type = iMessage
308
+ set targetBuddy to participant "{recipient_escaped}" of targetService
309
+ send "{message_escaped}" to targetBuddy
310
+ end tell
311
+ '''
312
+
313
+ result2 = subprocess.run(
314
+ ["osascript", "-e", script2],
315
+ capture_output=True,
316
+ text=True,
317
+ timeout=30
318
+ )
319
+
320
+ if result2.returncode == 0:
321
+ return {"success": True, "message": f"iMessage sent to {recipient}"}
322
+
323
+ return {"success": False, "error": result.stderr or result2.stderr}
324
+
325
+ except subprocess.TimeoutExpired:
326
+ return {"success": False, "error": "AppleScript timed out"}
327
+ except Exception as e:
328
+ return {"success": False, "error": str(e)}
329
+
330
+
331
+ def read_imessages(recipient: str = None, count: int = 10) -> list:
332
+ """
333
+ Read recent iMessages from the local Messages database.
334
+
335
+ Args:
336
+ recipient: Optional phone number or Apple ID to filter by
337
+ count: Number of messages to return (default 10)
338
+
339
+ Returns:
340
+ List of message dicts with 'sender', 'text', 'date', 'is_from_me' keys
341
+ """
342
+ import sqlite3
343
+ from pathlib import Path
344
+ from datetime import datetime
345
+
346
+ if sys.platform != "darwin":
347
+ return [{"error": "iMessage requires macOS"}]
348
+
349
+ db_path = Path.home() / "Library" / "Messages" / "chat.db"
350
+
351
+ if not db_path.exists():
352
+ return [{"error": "Messages database not found"}]
353
+
354
+ try:
355
+ conn = sqlite3.connect(str(db_path))
356
+ cursor = conn.cursor()
357
+
358
+ # Query messages with handle info
359
+ query = """
360
+ SELECT
361
+ m.text,
362
+ m.is_from_me,
363
+ m.date / 1000000000 + 978307200 as unix_date,
364
+ h.id as handle_id
365
+ FROM message m
366
+ LEFT JOIN handle h ON m.handle_id = h.ROWID
367
+ WHERE m.text IS NOT NULL AND m.text != ''
368
+ """
369
+
370
+ if recipient:
371
+ # Normalize phone number for matching
372
+ clean_recipient = ''.join(c for c in recipient if c.isdigit() or c == '+')
373
+ query += f" AND (h.id LIKE '%{clean_recipient[-10:]}%' OR h.id = '{recipient}')"
374
+
375
+ query += f" ORDER BY m.date DESC LIMIT {count}"
376
+
377
+ cursor.execute(query)
378
+ rows = cursor.fetchall()
379
+ conn.close()
380
+
381
+ messages = []
382
+ for text, is_from_me, unix_ts, handle in rows:
383
+ try:
384
+ date_str = datetime.fromtimestamp(unix_ts).strftime("%Y-%m-%d %H:%M:%S")
385
+ except:
386
+ date_str = "unknown"
387
+
388
+ messages.append({
389
+ "text": text,
390
+ "sender": "me" if is_from_me else (handle or "unknown"),
391
+ "date": date_str,
392
+ "is_from_me": bool(is_from_me)
393
+ })
394
+
395
+ return list(reversed(messages)) # Chronological order
396
+
397
+ except Exception as e:
398
+ return [{"error": str(e)}]
399
+
400
+
401
+ def imessage_available() -> bool:
402
+ """Check if iMessage is available on this system."""
403
+ if sys.platform != "darwin":
404
+ return False
405
+
406
+ from pathlib import Path
407
+ return (Path.home() / "Library" / "Messages" / "chat.db").exists()
408
+
409
+
410
+ if __name__ == "__main__":
411
+ import sys as _sys
412
+ import json
413
+
414
+ if len(_sys.argv) < 2:
415
+ print("Usage: imessage.py <send|read|check> [args]")
416
+ print(" send <recipient> <message> - Send an iMessage")
417
+ print(" read [recipient] [count] - Read recent messages")
418
+ print(" check - Check if iMessage is available")
419
+ _sys.exit(1)
420
+
421
+ cmd = _sys.argv[1]
422
+
423
+ if cmd == "send":
424
+ if len(_sys.argv) < 4:
425
+ print("Usage: imessage.py send <recipient> <message>")
426
+ _sys.exit(1)
427
+ recipient = _sys.argv[2]
428
+ message = " ".join(_sys.argv[3:])
429
+ result = send_imessage(recipient, message)
430
+ print(json.dumps(result, indent=2))
431
+
432
+ elif cmd == "read":
433
+ recipient = _sys.argv[2] if len(_sys.argv) > 2 else None
434
+ count = int(_sys.argv[3]) if len(_sys.argv) > 3 else 10
435
+ messages = read_imessages(recipient, count)
436
+ print(json.dumps(messages, indent=2))
437
+
438
+ elif cmd == "check":
439
+ available = imessage_available()
440
+ print(f"iMessage available: {available}")
441
+
442
+ else:
443
+ print(f"Unknown command: {cmd}")
@@ -0,0 +1,175 @@
1
+ """SignalConnector — Signal Messenger integration via signal-cli REST API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import AsyncIterator
7
+
8
+ import httpx
9
+
10
+ from integrations.base import (
11
+ BaseConnector,
12
+ ConnectorCapability,
13
+ ConnectorInfo,
14
+ InboundMessage,
15
+ OutboundMessage,
16
+ )
17
+
18
+ logger = logging.getLogger("hyperclaw.integrations.signal")
19
+
20
+
21
+ class SignalConnector(BaseConnector):
22
+ """
23
+ Signal Messenger connector via signal-cli REST API.
24
+
25
+ Required config:
26
+ - api_url: URL of signal-cli REST API (e.g., http://localhost:8080)
27
+ - phone_number: Registered Signal phone number
28
+
29
+ Note: This connector uses isolated HyperShield context for maximum privacy.
30
+ """
31
+
32
+ # HyperShield context for privacy isolation
33
+ hypershield_context = "isolated"
34
+
35
+ def __init__(self, config: dict) -> None:
36
+ self.config = config
37
+ self.api_url = config.get("api_url", "").rstrip("/")
38
+ self.phone_number = config.get("phone_number", "")
39
+
40
+ if config.get("enabled", False):
41
+ if not self.api_url:
42
+ raise ValueError("SignalConnector requires api_url")
43
+ if not self.phone_number:
44
+ raise ValueError("SignalConnector requires phone_number")
45
+
46
+ self._client: httpx.AsyncClient | None = None
47
+
48
+ @property
49
+ def info(self) -> ConnectorInfo:
50
+ return ConnectorInfo(
51
+ connector_id="signal",
52
+ platform="signal",
53
+ category="messaging",
54
+ capabilities=frozenset(
55
+ [
56
+ ConnectorCapability.SEND_MESSAGE,
57
+ ConnectorCapability.RECEIVE_MESSAGE,
58
+ ConnectorCapability.FILE_UPLOAD,
59
+ ]
60
+ ),
61
+ auth_type="api_key",
62
+ rate_limit_per_minute=30,
63
+ supports_threads=False,
64
+ supports_attachments=True,
65
+ supports_reactions=True,
66
+ )
67
+
68
+ async def _get_client(self) -> httpx.AsyncClient:
69
+ if self._client is None:
70
+ self._client = httpx.AsyncClient(
71
+ timeout=30.0,
72
+ headers={"Content-Type": "application/json"},
73
+ )
74
+ return self._client
75
+
76
+ async def health(self) -> bool:
77
+ """Check if signal-cli API is reachable."""
78
+ try:
79
+ client = await self._get_client()
80
+ response = await client.get(f"{self.api_url}/v1/about")
81
+ return response.status_code == 200
82
+ except Exception as e:
83
+ logger.error(f"Signal health check failed: {e}")
84
+ return False
85
+
86
+ async def _send_impl(self, message: OutboundMessage) -> str:
87
+ """Send a message via Signal."""
88
+ client = await self._get_client()
89
+
90
+ payload = {
91
+ "number": self.phone_number,
92
+ "recipients": [message.recipient_id],
93
+ "message": message.content,
94
+ }
95
+
96
+ response = await client.post(
97
+ f"{self.api_url}/v2/send",
98
+ json=payload,
99
+ )
100
+
101
+ if response.status_code not in (200, 201):
102
+ raise Exception(f"Signal send failed: {response.text}")
103
+
104
+ data = response.json()
105
+ # signal-cli returns timestamps as message IDs
106
+ return str(data.get("timestamp", ""))
107
+
108
+ async def _receive_impl(self) -> AsyncIterator[InboundMessage]:
109
+ """Receive messages from Signal."""
110
+ client = await self._get_client()
111
+
112
+ response = await client.get(
113
+ f"{self.api_url}/v1/receive/{self.phone_number}",
114
+ )
115
+
116
+ if response.status_code != 200:
117
+ return
118
+
119
+ data = response.json()
120
+
121
+ for envelope in data:
122
+ data_message = envelope.get("dataMessage", {})
123
+ if not data_message:
124
+ continue
125
+
126
+ source = envelope.get("source", "")
127
+ source_name = envelope.get("sourceName", source)
128
+
129
+ attachments = []
130
+ for attach in data_message.get("attachments", []):
131
+ attachments.append({
132
+ "type": attach.get("contentType", ""),
133
+ "id": attach.get("id", ""),
134
+ })
135
+
136
+ yield InboundMessage(
137
+ message_id=str(envelope.get("timestamp", "")),
138
+ platform="signal",
139
+ sender_id=source,
140
+ sender_name=source_name,
141
+ content=data_message.get("message", ""),
142
+ thread_id=data_message.get("groupId"),
143
+ attachments=attachments,
144
+ raw=envelope,
145
+ )
146
+
147
+ async def send_attachment(
148
+ self,
149
+ recipient: str,
150
+ file_path: str,
151
+ message: str = "",
152
+ ) -> str:
153
+ """Send a file attachment via Signal."""
154
+ client = await self._get_client()
155
+
156
+ with open(file_path, "rb") as f:
157
+ files = {"attachment": f}
158
+ data = {
159
+ "number": self.phone_number,
160
+ "recipients": [recipient],
161
+ }
162
+ if message:
163
+ data["message"] = message
164
+
165
+ response = await client.post(
166
+ f"{self.api_url}/v2/send",
167
+ data=data,
168
+ files=files,
169
+ )
170
+
171
+ if response.status_code not in (200, 201):
172
+ raise Exception(f"Signal send_attachment failed: {response.text}")
173
+
174
+ result = response.json()
175
+ return str(result.get("timestamp", ""))