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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- 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", ""))
|