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
hyperclaw/live_state.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""What is actually live on this install, for onboarding and help text.
|
|
2
|
+
|
|
3
|
+
The truth rule: the product never claims anything that isn't live on that
|
|
4
|
+
install. Onboarding's welcome and integrations text is generated from this
|
|
5
|
+
snapshot, never from a fixed list of things the product might connect to
|
|
6
|
+
(tests/unit/test_onboarding_live_state.py enforces it).
|
|
7
|
+
|
|
8
|
+
Sources (no network):
|
|
9
|
+
- models: live providers in the registry (keys present, on the allowed list
|
|
10
|
+
for a Hyper-deployed install), with tools vs chat-only from signed
|
|
11
|
+
qualification records only;
|
|
12
|
+
- integrations: Telegram (bot token and chat id set), Gmail and Calendar
|
|
13
|
+
(Google credentials present: hyperclaw.api_utils.gmail_configured);
|
|
14
|
+
- available to connect: Telegram, only when the install profile allows it
|
|
15
|
+
(managed config ``"telegram": true``: team and client installs) and it is
|
|
16
|
+
not connected yet;
|
|
17
|
+
- memory: the customer's database when DATABASE_URL is set, else local files.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class LiveModel:
|
|
27
|
+
label: str
|
|
28
|
+
model: str
|
|
29
|
+
tools: bool
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class LiveState:
|
|
34
|
+
hyper_deployed: bool = False
|
|
35
|
+
models: list = field(default_factory=list) # LiveModel
|
|
36
|
+
integrations: list = field(default_factory=list) # names, e.g. "Telegram"
|
|
37
|
+
available: list = field(default_factory=list) # can be connected here (profile)
|
|
38
|
+
memory: str = "local" # local | database
|
|
39
|
+
allowed: list = field(default_factory=list) # allowed-list names (Hyper-deployed)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def tools(self) -> bool:
|
|
43
|
+
return any(m.tools for m in self.models)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _models() -> list:
|
|
47
|
+
from hyperclaw.managed_config import is_allowed
|
|
48
|
+
from hyperclaw.model_policy import model_label
|
|
49
|
+
from hyperclaw.providers import registry
|
|
50
|
+
out = []
|
|
51
|
+
try:
|
|
52
|
+
reg = registry()
|
|
53
|
+
except Exception: # noqa: BLE001 -- no config yet: nothing is live
|
|
54
|
+
return out
|
|
55
|
+
for prov in reg.providers.values():
|
|
56
|
+
if not prov.live or "chat" not in prov.capabilities:
|
|
57
|
+
continue
|
|
58
|
+
model = prov.model_for()
|
|
59
|
+
if not model or not is_allowed(prov.name, model):
|
|
60
|
+
continue
|
|
61
|
+
out.append(LiveModel(model_label(prov, model), model,
|
|
62
|
+
"tool_use" in prov.effective_capabilities(model)))
|
|
63
|
+
out.sort(key=lambda m: 0 if m.label.startswith("HyperSpeed") else 1)
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _integrations() -> list:
|
|
68
|
+
env = os.environ
|
|
69
|
+
out = []
|
|
70
|
+
if env.get("TELEGRAM_BOT_TOKEN", "").strip() and env.get("TELEGRAM_CHAT_ID", "").strip():
|
|
71
|
+
out.append("Telegram")
|
|
72
|
+
try:
|
|
73
|
+
from hyperclaw.api_utils import gmail_configured
|
|
74
|
+
from hyperclaw.userconfig import root
|
|
75
|
+
r = root()
|
|
76
|
+
gmail = gmail_configured() or (r / "workspace" / "secrets" / "gmail_token.json").exists() \
|
|
77
|
+
or (r / "secrets" / "gmail_token.json").exists()
|
|
78
|
+
except Exception: # noqa: BLE001
|
|
79
|
+
gmail = False
|
|
80
|
+
workspace_reads = []
|
|
81
|
+
try: # the Google Workspace sign-in (hyperclaw/google_workspace/auth.py)
|
|
82
|
+
from hyperclaw.google_workspace import auth as gauth
|
|
83
|
+
if gauth.connected():
|
|
84
|
+
granted = set(gauth.granted_scopes())
|
|
85
|
+
gmail = gmail or (gauth.GMAIL_READ in granted and gauth.CALENDAR_READ in granted)
|
|
86
|
+
workspace_reads = [label for scope, label in (
|
|
87
|
+
(gauth.DRIVE_READ, "Drive"), (gauth.DOCS_READ, "Docs"),
|
|
88
|
+
(gauth.SHEETS_READ, "Sheets"), (gauth.SLIDES_READ, "Slides"),
|
|
89
|
+
(gauth.MEET_READ, "Meet")) if scope in granted]
|
|
90
|
+
except Exception: # noqa: BLE001
|
|
91
|
+
workspace_reads = []
|
|
92
|
+
if gmail:
|
|
93
|
+
out.append("Gmail and Calendar")
|
|
94
|
+
if workspace_reads:
|
|
95
|
+
out.append("Google " + ", ".join(workspace_reads[:-1])
|
|
96
|
+
+ (" and " if len(workspace_reads) > 1 else "") + workspace_reads[-1]
|
|
97
|
+
+ " (read)")
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _available(connected: list) -> list:
|
|
102
|
+
"""Integrations this install's profile lets the owner connect now."""
|
|
103
|
+
try:
|
|
104
|
+
from hyperclaw.managed_config import telegram_allowed
|
|
105
|
+
tg = telegram_allowed()
|
|
106
|
+
except Exception: # noqa: BLE001 -- unreadable profile: offer nothing
|
|
107
|
+
tg = False
|
|
108
|
+
return ["Telegram"] if tg and "Telegram" not in connected else []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def snapshot() -> LiveState:
|
|
112
|
+
from hyperclaw.deployment import is_hyper
|
|
113
|
+
from hyperclaw.model_policy import allowed_display
|
|
114
|
+
hyper = is_hyper()
|
|
115
|
+
connected = _integrations()
|
|
116
|
+
return LiveState(
|
|
117
|
+
hyper_deployed=hyper,
|
|
118
|
+
models=_models(),
|
|
119
|
+
integrations=connected,
|
|
120
|
+
available=_available(connected),
|
|
121
|
+
memory="database" if os.environ.get("DATABASE_URL", "").strip() and not hyper
|
|
122
|
+
else "local",
|
|
123
|
+
allowed=allowed_display() if hyper else [],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ───────────────────────── text ─────────────────────────
|
|
128
|
+
|
|
129
|
+
PRODUCT = "HyperAgents"
|
|
130
|
+
TOOLS_OFF = ("Tools are off: until a model passes {p}' tool qualification it can only "
|
|
131
|
+
"chat. It can't read or send email, open your files or act in other apps.")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def welcome_lines(state: LiveState) -> list:
|
|
135
|
+
"""The welcome, generated from what is live. Plain text (no markup)."""
|
|
136
|
+
lines = [f"Welcome to {PRODUCT}.", ""]
|
|
137
|
+
lines.append(f"{PRODUCT} is an AI assistant you talk to here in the terminal.")
|
|
138
|
+
lines.append("")
|
|
139
|
+
lines.append("On this install right now:")
|
|
140
|
+
if state.models:
|
|
141
|
+
for m in state.models:
|
|
142
|
+
mode = "tools" if m.tools else "chat-only"
|
|
143
|
+
lines.append(f" • Model: {m.label} ({m.model}), {mode}")
|
|
144
|
+
else:
|
|
145
|
+
lines.append(" • Model: none set up yet (setup adds one next)")
|
|
146
|
+
if not state.tools:
|
|
147
|
+
lines.append(" • " + TOOLS_OFF.format(p=PRODUCT))
|
|
148
|
+
if state.integrations:
|
|
149
|
+
lines.append(" • Connected: " + ", ".join(state.integrations))
|
|
150
|
+
else:
|
|
151
|
+
lines.append(" • Connected: nothing yet")
|
|
152
|
+
lines.append(" • Memory: " + ("your database" if state.memory == "database"
|
|
153
|
+
else "kept on this computer"))
|
|
154
|
+
return lines
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def steps(state: LiveState) -> list:
|
|
158
|
+
"""The setup steps this install will actually go through."""
|
|
159
|
+
if state.hyper_deployed:
|
|
160
|
+
return ["Name your assistant", "Your name", "Your Hyper access key"]
|
|
161
|
+
return ["Name your assistant", "Your name",
|
|
162
|
+
"The model: HyperSpeed V2 (recommended) or your own provider",
|
|
163
|
+
"Optional: a database for memory across sessions"]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def explain_lines(state: LiveState) -> list:
|
|
167
|
+
"""What the assistant can do on this install, said plainly."""
|
|
168
|
+
if state.tools:
|
|
169
|
+
names = ", ".join(state.integrations) if state.integrations else "nothing yet"
|
|
170
|
+
return ["I can chat with you and use the tools this install has enabled.",
|
|
171
|
+
f"Connected services: {names}."]
|
|
172
|
+
return ["For now I can chat: answer questions, and draft, rewrite or summarize "
|
|
173
|
+
"what you paste in.",
|
|
174
|
+
"I can't act in your email, files or other apps until a model that has passed "
|
|
175
|
+
f"{PRODUCT}' tool qualification is set up."]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def integrations_lines(state: LiveState) -> list:
|
|
179
|
+
if not state.integrations:
|
|
180
|
+
lines = ["No integrations are connected on this install yet."]
|
|
181
|
+
else:
|
|
182
|
+
lines = ["Connected on this install: " + ", ".join(state.integrations) + "."]
|
|
183
|
+
if state.available:
|
|
184
|
+
lines.append("Available to connect on this install: " + ", ".join(state.available) + ".")
|
|
185
|
+
if not state.tools and "Gmail and Calendar" in state.integrations:
|
|
186
|
+
lines.append("Gmail and Calendar need tools, which are off on this install.")
|
|
187
|
+
return lines
|
|
188
|
+
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Managed config: the settings Hyper sets for an install (docs/MODEL-POLICY.md).
|
|
2
|
+
|
|
3
|
+
A small JSON file that the app only ever READS. There is no command, prompt or
|
|
4
|
+
settings screen that writes it, so a user cannot change it through the app.
|
|
5
|
+
Hyper's deployment tooling places it; on a Hyper-deployed install it lives in
|
|
6
|
+
a system location the user's account normally cannot write.
|
|
7
|
+
|
|
8
|
+
Location (first that is set wins):
|
|
9
|
+
1. ``HYPERCLAW_MANAGED_CONFIG`` (a path; set by Hyper's deployment, e.g. in
|
|
10
|
+
the launchd plist)
|
|
11
|
+
2. macOS: ``/Library/Application Support/HyperClaw/managed.json``
|
|
12
|
+
Linux and others: ``/etc/hyperclaw/managed.json``
|
|
13
|
+
|
|
14
|
+
Format (schema 1)::
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
"schema": 1,
|
|
18
|
+
"allowed_models": ["hyperspeed/hospitality", "anthropic/claude-sonnet-5-5",
|
|
19
|
+
"xai/grok-4.7"],
|
|
20
|
+
"hyper_issued_keys": ["anthropic", "xai"],
|
|
21
|
+
"model_names": {"anthropic/claude-sonnet-5-5": "Claude Sonnet 5.5"},
|
|
22
|
+
"telegram": true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
- ``allowed_models``: per-customer allowed-model list for a Hyper-deployed
|
|
26
|
+
install. Entries are ``<provider>/<model id>`` or ``<provider>/*`` (every
|
|
27
|
+
model of that provider). A model not on the list cannot be selected or
|
|
28
|
+
routed to. Missing, empty or unreadable: the default list, HyperSpeed V2
|
|
29
|
+
only. Standard installs are not restricted by it.
|
|
30
|
+
- ``hyper_issued_keys``: providers whose key on this install Hyper issued.
|
|
31
|
+
Their models are labelled "<model name>, via Hyper"; every other
|
|
32
|
+
third-party key is "your own provider".
|
|
33
|
+
- ``model_names``: optional display names, ``<provider>/<model id>`` -> name.
|
|
34
|
+
- ``telegram``: install profile. ``true`` only on installs where Hyper has
|
|
35
|
+
set up the Telegram channel to run (team and client profiles: their own
|
|
36
|
+
bot, unattended mode on for the main service). Only then does onboarding
|
|
37
|
+
offer to connect it and does live state list it as available to connect.
|
|
38
|
+
Missing, not exactly ``true``, or no file: not offered (standard installs).
|
|
39
|
+
- ``default_model``: optional, one entry of ``allowed_models`` that is the
|
|
40
|
+
install's default route (team profile: Claude Opus 5.5). Without it the
|
|
41
|
+
normal slot order applies (HyperSpeed V2 first).
|
|
42
|
+
|
|
43
|
+
No managed file: a team install's Hyper-signed team profile
|
|
44
|
+
(``hyperclaw.teamsetup.profile``, placed by ``hyperclaw onboard --team``) supplies
|
|
45
|
+
these fields. It is verified on every read; one that doesn't verify is
|
|
46
|
+
ignored and the defaults apply.
|
|
47
|
+
|
|
48
|
+
When the gateway catalog is live it will carry the allowed list; until then
|
|
49
|
+
this file is the source.
|
|
50
|
+
"""
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import json
|
|
54
|
+
import logging
|
|
55
|
+
import os
|
|
56
|
+
import re
|
|
57
|
+
import sys
|
|
58
|
+
from pathlib import Path
|
|
59
|
+
from typing import Optional
|
|
60
|
+
|
|
61
|
+
log = logging.getLogger("hyperclaw.managed_config")
|
|
62
|
+
|
|
63
|
+
ENV = "HYPERCLAW_MANAGED_CONFIG"
|
|
64
|
+
SCHEMA = 1
|
|
65
|
+
MAC_PATH = "/Library/Application Support/HyperClaw/managed.json"
|
|
66
|
+
UNIX_PATH = "/etc/hyperclaw/managed.json"
|
|
67
|
+
|
|
68
|
+
_ENTRY = re.compile(r"^[a-z0-9_]{1,40}/(\*|[A-Za-z0-9._:-][A-Za-z0-9._:/-]{0,127})$")
|
|
69
|
+
_PROVIDER = re.compile(r"^[a-z0-9_]{1,40}$")
|
|
70
|
+
_logged: set = set()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def path() -> Path:
|
|
74
|
+
explicit = os.environ.get(ENV, "").strip()
|
|
75
|
+
if explicit:
|
|
76
|
+
return Path(explicit).expanduser()
|
|
77
|
+
return Path(MAC_PATH if sys.platform == "darwin" else UNIX_PATH)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _warn_once(msg: str) -> None:
|
|
81
|
+
if msg not in _logged:
|
|
82
|
+
_logged.add(msg)
|
|
83
|
+
log.warning(msg)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def load() -> dict:
|
|
87
|
+
"""The validated managed config, or {} when there is none (or it is
|
|
88
|
+
unreadable; then the defaults apply, which are the strictest)."""
|
|
89
|
+
p = path()
|
|
90
|
+
try:
|
|
91
|
+
if not p.is_file():
|
|
92
|
+
return _team_profile()
|
|
93
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
94
|
+
except Exception as e: # noqa: BLE001 -- never fatal: defaults apply
|
|
95
|
+
_warn_once(f"managed config {p} unreadable ({type(e).__name__}); defaults apply")
|
|
96
|
+
return {}
|
|
97
|
+
if not isinstance(data, dict):
|
|
98
|
+
_warn_once(f"managed config {p} is not a JSON object; defaults apply")
|
|
99
|
+
return {}
|
|
100
|
+
out: dict = {}
|
|
101
|
+
allowed = data.get("allowed_models")
|
|
102
|
+
if isinstance(allowed, list):
|
|
103
|
+
good = [e.strip() for e in allowed if isinstance(e, str) and _ENTRY.match(e.strip())]
|
|
104
|
+
if len(good) != len(allowed):
|
|
105
|
+
_warn_once(f"managed config {p}: invalid allowed_models entries dropped")
|
|
106
|
+
out["allowed_models"] = list(dict.fromkeys(good))
|
|
107
|
+
issued = data.get("hyper_issued_keys")
|
|
108
|
+
if isinstance(issued, list):
|
|
109
|
+
out["hyper_issued_keys"] = sorted({x for x in issued
|
|
110
|
+
if isinstance(x, str) and _PROVIDER.match(x)})
|
|
111
|
+
if "telegram" in data:
|
|
112
|
+
if not isinstance(data["telegram"], bool):
|
|
113
|
+
_warn_once(f"managed config {p}: telegram must be true or false; not offered")
|
|
114
|
+
out["telegram"] = data["telegram"] is True
|
|
115
|
+
default = data.get("default_model")
|
|
116
|
+
if isinstance(default, str) and _ENTRY.match(default.strip()) \
|
|
117
|
+
and default.strip() in out.get("allowed_models", []):
|
|
118
|
+
out["default_model"] = default.strip()
|
|
119
|
+
names = data.get("model_names")
|
|
120
|
+
if isinstance(names, dict):
|
|
121
|
+
out["model_names"] = {k: re.sub(r"[\x00-\x1f\x7f]", "", v)[:60].strip()
|
|
122
|
+
for k, v in names.items()
|
|
123
|
+
if isinstance(k, str) and _ENTRY.match(k)
|
|
124
|
+
and isinstance(v, str) and v.strip()}
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def raw() -> tuple:
|
|
129
|
+
"""(data, readable) for readers of keys this module doesn't validate
|
|
130
|
+
(``restricted_clients``, ``restricted_clients_required``,
|
|
131
|
+
``google_calendar_write``): the system managed file when present, else
|
|
132
|
+
the verified team profile. (None, True): neither exists. (None, False):
|
|
133
|
+
a file exists but can't be read or doesn't verify (callers fail safe)."""
|
|
134
|
+
p = path()
|
|
135
|
+
try:
|
|
136
|
+
if p.is_file():
|
|
137
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
138
|
+
return (data, True) if isinstance(data, dict) else (None, False)
|
|
139
|
+
except Exception:
|
|
140
|
+
return None, False
|
|
141
|
+
try:
|
|
142
|
+
from hyperclaw.teamsetup import profile
|
|
143
|
+
if not profile.path().exists():
|
|
144
|
+
return None, True
|
|
145
|
+
view = profile.managed_view()
|
|
146
|
+
return (view, True) if view else (None, False)
|
|
147
|
+
except Exception:
|
|
148
|
+
return None, False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def source_key() -> str:
|
|
152
|
+
"""Changes whenever the managed settings in force may have changed (for caches)."""
|
|
153
|
+
parts = []
|
|
154
|
+
for p in (path(), _team_profile_path()):
|
|
155
|
+
try:
|
|
156
|
+
parts.append(f"{p}:{p.stat().st_mtime_ns}")
|
|
157
|
+
except (OSError, TypeError):
|
|
158
|
+
parts.append(f"{p}:-")
|
|
159
|
+
return "|".join(parts)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _team_profile_path():
|
|
163
|
+
try:
|
|
164
|
+
from hyperclaw.teamsetup import profile
|
|
165
|
+
return profile.path()
|
|
166
|
+
except Exception:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _team_profile() -> dict:
|
|
171
|
+
"""The verified team profile's fields, or {} (hyperclaw.teamsetup.profile)."""
|
|
172
|
+
try:
|
|
173
|
+
from hyperclaw.teamsetup.profile import managed_view
|
|
174
|
+
return managed_view()
|
|
175
|
+
except Exception as e: # noqa: BLE001 -- never fatal: defaults apply
|
|
176
|
+
_warn_once(f"team profile unreadable ({type(e).__name__}); defaults apply")
|
|
177
|
+
return {}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ───────────────────────── allowed-model list ─────────────────────────
|
|
181
|
+
|
|
182
|
+
def default_allowed() -> list:
|
|
183
|
+
"""Hyper-deployed default: HyperSpeed V2 only."""
|
|
184
|
+
from hyperclaw import hyperspeed
|
|
185
|
+
return [f"{hyperspeed.PROVIDER_NAME}/{hyperspeed.model()}"]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def allowed_models() -> Optional[list]:
|
|
189
|
+
"""The allowed list in force, or None when the install is not restricted
|
|
190
|
+
(standard installs)."""
|
|
191
|
+
from hyperclaw.deployment import is_hyper
|
|
192
|
+
if not is_hyper():
|
|
193
|
+
return None
|
|
194
|
+
return load().get("allowed_models") or default_allowed()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def is_allowed(provider: str, model: str) -> bool:
|
|
198
|
+
allowed = allowed_models()
|
|
199
|
+
if allowed is None:
|
|
200
|
+
return True
|
|
201
|
+
return f"{provider}/{model}" in allowed or f"{provider}/*" in allowed
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def provider_allowed(provider: str) -> bool:
|
|
205
|
+
"""True when any model of this provider is on the list (or no list)."""
|
|
206
|
+
allowed = allowed_models()
|
|
207
|
+
if allowed is None:
|
|
208
|
+
return True
|
|
209
|
+
return any(e.split("/", 1)[0] == provider for e in allowed)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def default_model() -> str:
|
|
213
|
+
"""'<provider>/<model id>' the install routes to by default, or ''.
|
|
214
|
+
Only on a Hyper-deployed install, and only when it is on the list."""
|
|
215
|
+
from hyperclaw.deployment import is_hyper
|
|
216
|
+
if not is_hyper():
|
|
217
|
+
return ""
|
|
218
|
+
d = load().get("default_model") or ""
|
|
219
|
+
return d if d and "*" not in d and is_allowed(*d.split("/", 1)) else ""
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def allowed_for(provider: str) -> list:
|
|
223
|
+
"""Explicit model ids on the list for this provider ('*' included as-is)."""
|
|
224
|
+
allowed = allowed_models() or []
|
|
225
|
+
return [e.split("/", 1)[1] for e in allowed if e.split("/", 1)[0] == provider]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ───────────────────────── key origin + names ─────────────────────────
|
|
229
|
+
|
|
230
|
+
def hyper_issued(provider: str) -> bool:
|
|
231
|
+
"""True when Hyper issued this provider's key on this install."""
|
|
232
|
+
return provider in (load().get("hyper_issued_keys") or [])
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def model_name(provider: str, model: str) -> str:
|
|
236
|
+
return (load().get("model_names") or {}).get(f"{provider}/{model}", "")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ───────────────────────── install profile ─────────────────────────
|
|
240
|
+
|
|
241
|
+
def telegram_allowed() -> bool:
|
|
242
|
+
"""True only when the managed config says the Telegram channel can run on
|
|
243
|
+
this install (``"telegram": true``; team and client profiles). Standard
|
|
244
|
+
installs without it are never offered Telegram in onboarding."""
|
|
245
|
+
return load().get("telegram") is True
|
hyperclaw/media_hub.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Media Hub — unified outbound file delivery across every channel.
|
|
3
|
+
|
|
4
|
+
deliver_file(path, via, to, caption) is the single entry point:
|
|
5
|
+
via='here' -> queue for the current conversation (Telegram or iMessage,
|
|
6
|
+
whichever this turn came from) via outbox
|
|
7
|
+
via='telegram' -> Telegram sendPhoto/sendVideo/sendAudio/sendDocument
|
|
8
|
+
via='imessage' -> Messages.app attachment via AppleScript
|
|
9
|
+
via='email' -> Gmail with attachment (integrations_layer.gmail_send)
|
|
10
|
+
via='open' -> open the file locally on the Mac (Preview/Word/etc.)
|
|
11
|
+
|
|
12
|
+
Every sender is direct and synchronous — no daemon dependencies — so this works
|
|
13
|
+
from the TUI, the Telegram bot, the iMessage daemon, and cron jobs alike.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import tempfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
from . import outbox
|
|
26
|
+
|
|
27
|
+
MAX_TG_BYTES = 49 * 1024 * 1024
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve(path: str) -> Path:
|
|
31
|
+
p = Path(path).expanduser()
|
|
32
|
+
if not p.exists():
|
|
33
|
+
raise FileNotFoundError(f"File not found: {p}")
|
|
34
|
+
return p
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Telegram ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
def telegram_send_file(path: str, caption: str = "", chat_id: str = "") -> str:
|
|
40
|
+
"""Send any file over Telegram, picking the right API method by type."""
|
|
41
|
+
try:
|
|
42
|
+
p = _resolve(path)
|
|
43
|
+
_size = p.stat().st_size
|
|
44
|
+
except OSError as e:
|
|
45
|
+
return str(e)
|
|
46
|
+
token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
|
47
|
+
cid = str(chat_id or os.environ.get("TELEGRAM_CHAT_ID", ""))
|
|
48
|
+
if not token or not cid:
|
|
49
|
+
return "Telegram not configured (TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID)"
|
|
50
|
+
if _size > MAX_TG_BYTES:
|
|
51
|
+
return f"File too large for Telegram ({_size // (1024*1024)}MB > 49MB). Email it instead."
|
|
52
|
+
|
|
53
|
+
kind = outbox.kind_of(str(p))
|
|
54
|
+
method, field = {
|
|
55
|
+
"photo": ("sendPhoto", "photo"),
|
|
56
|
+
"video": ("sendVideo", "video"),
|
|
57
|
+
"audio": ("sendAudio", "audio"),
|
|
58
|
+
"document": ("sendDocument", "document"),
|
|
59
|
+
}[kind]
|
|
60
|
+
data = {"chat_id": cid}
|
|
61
|
+
if caption:
|
|
62
|
+
data["caption"] = caption[:1024]
|
|
63
|
+
from hyperclaw.tools.receipts import redact, telegram_receipt
|
|
64
|
+
try:
|
|
65
|
+
with open(p, "rb") as f:
|
|
66
|
+
with httpx.Client(timeout=120) as client:
|
|
67
|
+
r = client.post(f"https://api.telegram.org/bot{token}/{method}",
|
|
68
|
+
data=data, files={field: (p.name, f)})
|
|
69
|
+
# Success only with Telegram's receipt: ok=true AND message_id (bench 20.3).
|
|
70
|
+
mid, why = telegram_receipt(r)
|
|
71
|
+
if mid:
|
|
72
|
+
return f"Sent {p.name} via Telegram ({kind}) [id:{mid}]"
|
|
73
|
+
# Photos with odd dimensions/format can fail — retry as document.
|
|
74
|
+
if kind == "photo":
|
|
75
|
+
with open(p, "rb") as f:
|
|
76
|
+
with httpx.Client(timeout=120) as client:
|
|
77
|
+
r2 = client.post(f"https://api.telegram.org/bot{token}/sendDocument",
|
|
78
|
+
data=data, files={"document": (p.name, f)})
|
|
79
|
+
mid, why = telegram_receipt(r2)
|
|
80
|
+
if mid:
|
|
81
|
+
return f"Sent {p.name} via Telegram (document) [id:{mid}]"
|
|
82
|
+
return f"Telegram send failed: {why}"
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return f"Telegram send error: {redact(e)}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── iMessage ─────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
def imessage_send_file(path: str, recipient: str = "", message: str = "") -> str:
|
|
90
|
+
"""Send a file (and optional text) over iMessage via Messages.app."""
|
|
91
|
+
try:
|
|
92
|
+
p = _resolve(path)
|
|
93
|
+
except OSError as e:
|
|
94
|
+
return str(e)
|
|
95
|
+
recipient = recipient or os.environ.get("OWNER_PHONE", "") or os.environ.get("DEFAULT_PHONE", "")
|
|
96
|
+
if not recipient:
|
|
97
|
+
return "No iMessage recipient (set OWNER_PHONE)"
|
|
98
|
+
r_esc = recipient.replace("\\", "\\\\").replace('"', '\\"')
|
|
99
|
+
m_esc = (message or "").replace("\\", "\\\\").replace('"', '\\"').replace("\n", " ").replace("\r", " ")
|
|
100
|
+
f_esc = str(p).replace("\\", "\\\\").replace('"', '\\"')
|
|
101
|
+
text_line = f'send "{m_esc}" to targetBuddy' if message else ""
|
|
102
|
+
script = f'''
|
|
103
|
+
tell application "Messages"
|
|
104
|
+
set targetService to 1st service whose service type = iMessage
|
|
105
|
+
set targetBuddy to buddy "{r_esc}" of targetService
|
|
106
|
+
{text_line}
|
|
107
|
+
send POSIX file "{f_esc}" to targetBuddy
|
|
108
|
+
end tell
|
|
109
|
+
'''
|
|
110
|
+
try:
|
|
111
|
+
res = subprocess.run(["osascript", "-e", script], capture_output=True, text=True, timeout=60)
|
|
112
|
+
if res.returncode == 0:
|
|
113
|
+
return f"Sent {p.name} via iMessage to {recipient}"
|
|
114
|
+
return f"iMessage send failed: {res.stderr.strip()[:200]}"
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return f"iMessage send error: {e}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ── Email ────────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
def email_send_file(path: str, to: str, subject: str = "", body: str = "", cc: str = "") -> str:
|
|
122
|
+
"""Email a file as an attachment via Gmail."""
|
|
123
|
+
try:
|
|
124
|
+
p = _resolve(path)
|
|
125
|
+
except OSError as e:
|
|
126
|
+
return str(e)
|
|
127
|
+
try:
|
|
128
|
+
from .integrations_layer import gmail_send
|
|
129
|
+
result = gmail_send(
|
|
130
|
+
to=to,
|
|
131
|
+
subject=subject or f"File: {p.name}",
|
|
132
|
+
body=body or f"Attached: {p.name}",
|
|
133
|
+
cc=cc,
|
|
134
|
+
attachments=[str(p)],
|
|
135
|
+
)
|
|
136
|
+
if isinstance(result, dict) and result.get("status") == "sent" and result.get("id"):
|
|
137
|
+
return f"Emailed {p.name} to {to} [id:{result['id']}]"
|
|
138
|
+
if isinstance(result, dict) and result.get("status") == "sent":
|
|
139
|
+
return "Email send failed: Gmail returned no message id, so delivery is not confirmed"
|
|
140
|
+
return f"Email send failed: {result}"
|
|
141
|
+
except Exception as e:
|
|
142
|
+
return f"Email send error: {e}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ── Local ────────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
def open_file(path: str, app: str = "") -> str:
|
|
148
|
+
"""Open a file locally on the Mac (default app, or a named app)."""
|
|
149
|
+
try:
|
|
150
|
+
p = _resolve(path)
|
|
151
|
+
cmd = ["open", str(p)] if not app else ["open", "-a", app, str(p)]
|
|
152
|
+
res = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
153
|
+
if res.returncode == 0:
|
|
154
|
+
return f"Opened {p.name}" + (f" in {app}" if app else "")
|
|
155
|
+
return f"Open failed: {res.stderr.strip()[:200]}"
|
|
156
|
+
except subprocess.TimeoutExpired:
|
|
157
|
+
return "Open failed: timed out after 30s"
|
|
158
|
+
except OSError as e:
|
|
159
|
+
return f"Open failed: {e}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ── Unified entry ────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
def deliver_file(path: str, via: str = "here", to: str = "", caption: str = "", app: str = "") -> str:
|
|
165
|
+
"""Single entry point for delivering a file anywhere."""
|
|
166
|
+
via = (via or "here").lower().strip()
|
|
167
|
+
if via in ("here", "chat", "conversation"):
|
|
168
|
+
return outbox.queue_file(path, caption=caption)
|
|
169
|
+
if via == "telegram":
|
|
170
|
+
return telegram_send_file(path, caption=caption, chat_id=to)
|
|
171
|
+
if via == "imessage":
|
|
172
|
+
return imessage_send_file(path, recipient=to, message=caption)
|
|
173
|
+
if via == "email":
|
|
174
|
+
if not to:
|
|
175
|
+
return "Email delivery needs a recipient (to=...)"
|
|
176
|
+
return email_send_file(path, to=to, body=caption)
|
|
177
|
+
if via == "open":
|
|
178
|
+
return open_file(path, app=app or to)
|
|
179
|
+
return f"Unknown delivery channel: {via} (use here|telegram|imessage|email|open)"
|