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,495 @@
1
+ """The one background service of a team install (8.2a design, Vaughn 12 Oct).
2
+
3
+ ONE process hosts the Telegram bot AND the scheduler (the morning brief, and
4
+ the GitLab digest when connected), so the bot has exactly one Telegram
5
+ connection. It starts at login, keeps running across restarts and sleep, and
6
+ is restarted by the OS if it stops with an error.
7
+
8
+ - macOS: a LaunchAgent ``com.hyperclaw.agent`` (``RunAtLoad``, ``KeepAlive``
9
+ on failure) in ``~/Library/LaunchAgents``.
10
+ - Linux: a systemd user unit ``hyperclaw-agent.service``
11
+ (``Restart=on-failure``, ``WantedBy=default.target``).
12
+
13
+ Its environment sets ``HYPERCLAW_UNATTENDED=1`` and names its jobs in
14
+ ``HYPERCLAW_SCHEDULER_JOBS`` (``daily_brief``, plus ``connected_digest`` when
15
+ GitLab is connected): nothing else runs on its own. No secret is in the
16
+ service definition; secrets come from the OS secret store at start.
17
+
18
+ - **One Telegram connection.** A lock file (``$HYPERCLAW_ROOT/run/telegram.lock``)
19
+ is held by whichever process polls the bot (this service or
20
+ ``hyperclaw-telegram``); a second one exits. ``install`` refuses while an
21
+ older HyperAgents service (``com.hyperclaw`` / ``com.hyperclaw.telegram``)
22
+ is loaded, and never stops it.
23
+ - **Sleep.** After a wake (the wall clock jumped ahead of the monotonic clock)
24
+ the service exits with status 75 and the OS starts it fresh, so the
25
+ Telegram connection is new; the brief catches up once (hyperclaw.brief).
26
+ - **Housekeeping** (hourly): Slack sign-in refresh before its expiry, with a
27
+ Telegram notice in plain words three days before Slack's 30 days run out
28
+ and when Slack needs a new sign-in; and a
29
+ Telegram notice once per newer signed release ("run hyperclaw upgrade").
30
+ - **No remote shell over chat, ever.** The service registers no handler of
31
+ its own; the bot's commands are fixed (tests/unit/teamsetup/test_service.py).
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import logging
37
+ import os
38
+ import plistlib
39
+ import shutil
40
+ import subprocess
41
+ import sys
42
+ import time
43
+ from dataclasses import dataclass
44
+ from pathlib import Path
45
+ from typing import Callable, Optional
46
+
47
+ log = logging.getLogger("hyperclaw.service")
48
+
49
+ LABEL = "com.hyperclaw.agent"
50
+ UNIT = "hyperclaw-agent.service"
51
+ OLD_LABELS = ("com.hyperclaw", "com.hyperclaw.telegram")
52
+ WAKE_EXIT = 75
53
+ WAKE_JUMP = 120.0 # seconds the wall clock may run ahead before we call it a wake
54
+ HOUSEKEEPING_EVERY = 3600.0
55
+ Runner = Callable[..., subprocess.CompletedProcess]
56
+
57
+
58
+ def _root() -> Path:
59
+ from hyperclaw.userconfig import root
60
+ return root()
61
+
62
+
63
+ def platform() -> str:
64
+ if sys.platform == "darwin":
65
+ return "launchd"
66
+ if sys.platform.startswith("linux") and shutil.which("systemctl"):
67
+ return "systemd"
68
+ return ""
69
+
70
+
71
+ def plist_path() -> Path:
72
+ return Path.home() / "Library" / "LaunchAgents" / f"{LABEL}.plist"
73
+
74
+
75
+ def unit_path() -> Path:
76
+ return Path.home() / ".config" / "systemd" / "user" / UNIT
77
+
78
+
79
+ def log_path() -> Path:
80
+ return _root() / "logs" / "service.log"
81
+
82
+
83
+ def program_args() -> list:
84
+ """The interpreter of this install (the pipx venv), so an upgrade in place
85
+ keeps the service pointing at the right code."""
86
+ return [sys.executable, "-m", "hyperclaw.teamsetup.service", "run"]
87
+
88
+
89
+ def jobs() -> list:
90
+ out = ["daily_brief"]
91
+ try:
92
+ from hyperclaw.digest import state
93
+ if state.connections().get("gitlab"):
94
+ out.append("connected_digest")
95
+ except Exception:
96
+ pass
97
+ return out
98
+
99
+
100
+ def service_env() -> dict:
101
+ from hyperclaw.deployment import is_hyper
102
+ env = {
103
+ "HYPERCLAW_UNATTENDED": "1",
104
+ "HYPERCLAW_SCHEDULER_JOBS": ",".join(jobs()),
105
+ "HYPERCLAW_ROOT": str(_root()),
106
+ "PATH": f"{Path(sys.executable).parent}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin",
107
+ "PYTHONUNBUFFERED": "1",
108
+ }
109
+ if is_hyper():
110
+ env["HYPERCLAW_DEPLOYMENT"] = "hyper"
111
+ return env
112
+
113
+
114
+ def plist_bytes() -> bytes:
115
+ logs = str(log_path())
116
+ return plistlib.dumps({
117
+ "Label": LABEL,
118
+ "ProgramArguments": program_args(),
119
+ "EnvironmentVariables": service_env(),
120
+ "WorkingDirectory": str(_root()),
121
+ "RunAtLoad": True,
122
+ "KeepAlive": {"SuccessfulExit": False}, # restart after a crash or a wake exit
123
+ "ThrottleInterval": 10,
124
+ "ProcessType": "Standard",
125
+ "StandardOutPath": logs,
126
+ "StandardErrorPath": logs,
127
+ })
128
+
129
+
130
+ def unit_text() -> str:
131
+ env = "\n".join(f'Environment="{k}={v}"' for k, v in service_env().items())
132
+ args = " ".join(program_args())
133
+ return (f"[Unit]\nDescription=HyperAgents (Telegram + scheduler)\n"
134
+ f"After=network-online.target\n\n[Service]\nExecStart={args}\n{env}\n"
135
+ f"Restart=on-failure\nRestartSec=10\nStandardOutput=append:{log_path()}\n"
136
+ f"StandardError=append:{log_path()}\n\n[Install]\nWantedBy=default.target\n")
137
+
138
+
139
+ @dataclass
140
+ class Result:
141
+ ok: bool
142
+ line: str
143
+
144
+
145
+ def _gui() -> str:
146
+ return f"gui/{os.getuid()}"
147
+
148
+
149
+ def _loaded(label: str, run: Runner) -> bool:
150
+ r = run(["launchctl", "print", f"{_gui()}/{label}"], capture_output=True, text=True)
151
+ return r.returncode == 0
152
+
153
+
154
+ def install(*, run: Runner = subprocess.run) -> Result:
155
+ """Write the service definition and start it. Idempotent."""
156
+ kind = platform()
157
+ log_path().parent.mkdir(parents=True, exist_ok=True)
158
+ if kind == "launchd":
159
+ busy = [lb for lb in OLD_LABELS if _loaded(lb, run)]
160
+ if busy:
161
+ return Result(False, "✗ Background service: another HyperAgents service is already "
162
+ f"running on this Mac ({', '.join(busy)}), and two would mean "
163
+ "two Telegram connections. Fix: tell Vaughn; nothing was "
164
+ "changed.")
165
+ p = plist_path()
166
+ p.parent.mkdir(parents=True, exist_ok=True)
167
+ p.write_bytes(plist_bytes())
168
+ os.chmod(p, 0o644)
169
+ if _loaded(LABEL, run):
170
+ run(["launchctl", "bootout", f"{_gui()}/{LABEL}"], capture_output=True, text=True)
171
+ r = run(["launchctl", "bootstrap", _gui(), str(p)], capture_output=True, text=True)
172
+ if r.returncode != 0:
173
+ return Result(False, "✗ Background service: macOS didn't start it. Fix: run "
174
+ "hyperclaw service install again, then hyperclaw self-check.")
175
+ run(["launchctl", "enable", f"{_gui()}/{LABEL}"], capture_output=True, text=True)
176
+ return Result(True, "✓ Connected: background service (starts at login)")
177
+ if kind == "systemd":
178
+ p = unit_path()
179
+ p.parent.mkdir(parents=True, exist_ok=True)
180
+ p.write_text(unit_text(), encoding="utf-8")
181
+ run(["systemctl", "--user", "daemon-reload"], capture_output=True, text=True)
182
+ r = run(["systemctl", "--user", "enable", "--now", UNIT], capture_output=True, text=True)
183
+ if r.returncode != 0:
184
+ return Result(False, "✗ Background service: systemd didn't start it. Fix: run "
185
+ "hyperclaw service install again, then hyperclaw self-check.")
186
+ run(["systemctl", "--user", "restart", UNIT], capture_output=True, text=True)
187
+ return Result(True, "✓ Connected: background service (starts at login)")
188
+ return Result(False, "✗ Background service: this computer has no supported service "
189
+ "manager (macOS or Linux with systemd). Fix: tell Vaughn.")
190
+
191
+
192
+ def uninstall(*, run: Runner = subprocess.run) -> Result:
193
+ kind = platform()
194
+ if kind == "launchd":
195
+ if _loaded(LABEL, run):
196
+ run(["launchctl", "bootout", f"{_gui()}/{LABEL}"], capture_output=True, text=True)
197
+ plist_path().unlink(missing_ok=True)
198
+ elif kind == "systemd":
199
+ run(["systemctl", "--user", "disable", "--now", UNIT], capture_output=True, text=True)
200
+ unit_path().unlink(missing_ok=True)
201
+ return Result(True, "Background service removed.")
202
+
203
+
204
+ def status(*, run: Runner = subprocess.run) -> dict:
205
+ """{"installed", "running", "pid"} (no secrets)."""
206
+ kind = platform()
207
+ if kind == "launchd":
208
+ installed = plist_path().exists()
209
+ r = run(["launchctl", "print", f"{_gui()}/{LABEL}"], capture_output=True, text=True)
210
+ out = r.stdout or "" if r.returncode == 0 else ""
211
+ pid = ""
212
+ for line in out.splitlines():
213
+ s = line.strip()
214
+ if s.startswith("pid = "):
215
+ pid = s.split("=", 1)[1].strip()
216
+ running = "state = running" in out
217
+ return {"installed": installed, "running": running, "pid": pid, "manager": kind}
218
+ if kind == "systemd":
219
+ r = run(["systemctl", "--user", "is-active", UNIT], capture_output=True, text=True)
220
+ return {"installed": unit_path().exists(),
221
+ "running": (r.stdout or "").strip() == "active", "pid": "", "manager": kind}
222
+ return {"installed": False, "running": False, "pid": "", "manager": ""}
223
+
224
+
225
+ def check(*, run: Runner = subprocess.run) -> Result:
226
+ st = status(run=run)
227
+ if not st["installed"]:
228
+ return Result(False, "✗ Background service: not installed. Fix: hyperclaw onboard "
229
+ "--team --step service")
230
+ if not st["running"]:
231
+ return Result(False, "✗ Background service: installed but not running. Fix: hyperclaw "
232
+ "service restart, then hyperclaw self-check")
233
+ return Result(True, "✓ Connected: background service (running, starts at login)")
234
+
235
+
236
+ def restart(*, run: Runner = subprocess.run) -> Result:
237
+ kind = platform()
238
+ if kind == "launchd":
239
+ r = run(["launchctl", "kickstart", "-k", f"{_gui()}/{LABEL}"], capture_output=True,
240
+ text=True)
241
+ elif kind == "systemd":
242
+ r = run(["systemctl", "--user", "restart", UNIT], capture_output=True, text=True)
243
+ else:
244
+ return Result(False, "No supported service manager on this computer.")
245
+ return Result(r.returncode == 0, "Background service restarted." if r.returncode == 0
246
+ else "The background service couldn't be restarted; run hyperclaw service "
247
+ "install.")
248
+
249
+
250
+ class paused:
251
+ """Stop this install's own service for a moment (Telegram pairing needs
252
+ the bot's updates to itself), then start it again. Only our own label."""
253
+
254
+ def __init__(self, run: Runner = subprocess.run):
255
+ self.run = run
256
+ self.was = False
257
+
258
+ def __enter__(self):
259
+ kind = platform()
260
+ if kind == "launchd" and _loaded(LABEL, self.run):
261
+ self.was = True
262
+ self.run(["launchctl", "bootout", f"{_gui()}/{LABEL}"], capture_output=True,
263
+ text=True)
264
+ elif kind == "systemd" and status(run=self.run)["running"]:
265
+ self.was = True
266
+ self.run(["systemctl", "--user", "stop", UNIT], capture_output=True, text=True)
267
+ return self
268
+
269
+ def __exit__(self, *exc):
270
+ if self.was:
271
+ install(run=self.run)
272
+ return False
273
+
274
+
275
+ # ── single Telegram connection ────────────────────────────────────────────
276
+
277
+ class TelegramLock:
278
+ """An exclusive, non-blocking lock held while a process polls the bot."""
279
+
280
+ def __init__(self, path: Optional[Path] = None):
281
+ self.path = path or (_root() / "run" / "telegram.lock")
282
+ self.fd = None
283
+
284
+ def acquire(self) -> bool:
285
+ import fcntl
286
+ self.path.parent.mkdir(parents=True, exist_ok=True)
287
+ fd = os.open(str(self.path), os.O_RDWR | os.O_CREAT, 0o600)
288
+ try:
289
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
290
+ except OSError:
291
+ os.close(fd)
292
+ return False
293
+ os.ftruncate(fd, 0)
294
+ os.write(fd, str(os.getpid()).encode())
295
+ self.fd = fd
296
+ return True
297
+
298
+ def release(self) -> None:
299
+ if self.fd is not None:
300
+ import fcntl
301
+ try:
302
+ fcntl.flock(self.fd, fcntl.LOCK_UN)
303
+ finally:
304
+ os.close(self.fd)
305
+ self.fd = None
306
+
307
+
308
+ ANOTHER_POLLER = ("Another HyperAgents process is already connected to this Telegram bot "
309
+ "(only one may be); this one is not starting Telegram.")
310
+
311
+
312
+ # ── housekeeping (pure parts are tested offline) ──────────────────────────
313
+
314
+ class WakeWatch:
315
+ """Detects a sleep/wake: the wall clock moved further than the monotonic
316
+ clock (which stops while the computer sleeps)."""
317
+
318
+ def __init__(self, wall: Callable[[], float] = time.time,
319
+ mono: Callable[[], float] = time.monotonic, jump: float = WAKE_JUMP):
320
+ self.wall, self.mono, self.jump = wall, mono, jump
321
+ self.w0, self.m0 = wall(), mono()
322
+
323
+ def woke(self) -> bool:
324
+ w, m = self.wall(), self.mono()
325
+ gap = (w - self.w0) - (m - self.m0)
326
+ self.w0, self.m0 = w, m
327
+ return gap > self.jump
328
+
329
+
330
+ def _state_path() -> Path:
331
+ return _root() / "run" / "service-state.json"
332
+
333
+
334
+ def _state() -> dict:
335
+ try:
336
+ d = json.loads(_state_path().read_text(encoding="utf-8"))
337
+ return d if isinstance(d, dict) else {}
338
+ except Exception:
339
+ return {}
340
+
341
+
342
+ def _save_state(d: dict) -> None:
343
+ p = _state_path()
344
+ p.parent.mkdir(parents=True, exist_ok=True)
345
+ fd = os.open(str(p), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
346
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
347
+ json.dump(d, f)
348
+
349
+
350
+ def slack_tick(send: Callable[[str], object], *, http=None,
351
+ now: Callable[[], float] = time.time) -> str:
352
+ """Refresh the Slack sign-in when due. A refusal is told to the owner
353
+ once (plain words), until they sign in again."""
354
+ from hyperclaw.teamsetup import profile, slack_oauth
355
+ cfg = (profile.load().get("slack") or {})
356
+ if not cfg.get("client_id") or not slack_oauth.connected():
357
+ return "skip"
358
+ r = slack_oauth.refresh(cfg, http=http, now=now)
359
+ st = _state()
360
+ # Tell each message once (per sign-in: a new sign-in or a new refresh
361
+ # token gives a new date, so a later reminder is sent again).
362
+ key = f"{r.status}:{slack_oauth.sign_in_by(slack_oauth.load_bundle())}:{r.message[:24]}"
363
+ if r.message and st.get("slack_told") != key:
364
+ send(r.message)
365
+ st["slack_told"] = key
366
+ _save_state(st)
367
+ return r.status
368
+
369
+
370
+ def update_tick(send: Callable[[str], object]) -> str:
371
+ """Tell the owner once per newer signed release."""
372
+ from hyperclaw.updates import check
373
+ from hyperclaw.updates.paths import opted_out
374
+ if opted_out():
375
+ return "opted-out"
376
+ check.maybe_refresh_in_background()
377
+ latest = check.load_cache().get("latest_signed")
378
+ text = check.notice_text()
379
+ if not text or not latest:
380
+ return "none"
381
+ st = _state()
382
+ if st.get("update_told") == latest:
383
+ return "told"
384
+ send(text.replace("HyperClaw", "HyperAgents", 1) +
385
+ " (in your terminal; it installs signed releases only)")
386
+ st["update_told"] = latest
387
+ _save_state(st)
388
+ return "sent"
389
+
390
+
391
+ def owner_send(text: str) -> bool:
392
+ """Plain Telegram message to the owner chat (previews off)."""
393
+ from hyperclaw.updates.notices import telegram_send
394
+ return telegram_send(text)
395
+
396
+
397
+ def with_service_start(original, send, extra):
398
+ """The bot's own start-up hook (the approval layer), then the scheduler
399
+ and the housekeeping task, in the bot's event loop."""
400
+ import asyncio
401
+
402
+ async def post_init(application):
403
+ if original is not None:
404
+ await original(application)
405
+ from hyperclaw.scheduler import get_scheduler
406
+ get_scheduler(send).start()
407
+ asyncio.get_running_loop().create_task(extra(application))
408
+ return post_init
409
+
410
+
411
+ # ── run ───────────────────────────────────────────────────────────────────
412
+
413
+ def _setup_logging() -> None:
414
+ log_path().parent.mkdir(parents=True, exist_ok=True)
415
+ logging.basicConfig(level=logging.INFO,
416
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
417
+ handlers=[logging.StreamHandler(sys.stdout)])
418
+ for noisy in ("httpx", "httpcore", "telegram.ext", "apscheduler"):
419
+ logging.getLogger(noisy).setLevel(logging.WARNING)
420
+
421
+
422
+ def run() -> int:
423
+ """The service process. Returns the exit status."""
424
+ import asyncio
425
+ _setup_logging()
426
+ from hyperclaw.userconfig import load_env_into_process
427
+ load_env_into_process()
428
+ from hyperclaw import unattended
429
+ unattended.exit_if_off(log, "hyperclaw service")
430
+ from hyperclaw.updates import on_startup
431
+ on_startup("service")
432
+
433
+ lock = TelegramLock()
434
+ have_lock = lock.acquire()
435
+ token = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
436
+ if token and not have_lock:
437
+ log.warning(ANOTHER_POLLER)
438
+ state = {"exit": 0}
439
+ wake = WakeWatch()
440
+
441
+ async def housekeeping(stop: Callable[[], None]):
442
+ last = 0.0
443
+ while True:
444
+ await asyncio.sleep(30)
445
+ if wake.woke():
446
+ log.warning("woke from sleep: restarting for a fresh Telegram connection")
447
+ state["exit"] = WAKE_EXIT
448
+ stop()
449
+ return
450
+ if time.monotonic() - last >= HOUSEKEEPING_EVERY or last == 0.0:
451
+ last = time.monotonic()
452
+ for tick in (slack_tick, update_tick):
453
+ try:
454
+ await asyncio.to_thread(tick, owner_send)
455
+ except Exception as e: # noqa: BLE001
456
+ log.warning("housekeeping %s failed (%s)", tick.__name__,
457
+ type(e).__name__)
458
+
459
+ from hyperclaw.scheduler import get_scheduler
460
+ try:
461
+ if token and have_lock:
462
+ from hyperclaw import telegram_bot as tb
463
+ bot = tb.get_telegram_bot()
464
+ app = bot.build(token)
465
+ if isinstance(app, tb.JournaledApplication):
466
+ from hyperclaw.telegram_intake import TelegramIntake
467
+ app.hc_journal = TelegramIntake()
468
+
469
+ app.post_init = with_service_start(app.post_init, bot.send_message,
470
+ lambda a: housekeeping(a.stop_running))
471
+ log.info("HyperAgents service: Telegram + scheduler in one process")
472
+ app.run_polling(drop_pending_updates=False)
473
+ else:
474
+ async def scheduler_only():
475
+ ev = asyncio.Event()
476
+ sched = get_scheduler(None)
477
+ sched.start()
478
+ await housekeeping(ev.set)
479
+ log.info("HyperAgents service: scheduler only (no Telegram here)")
480
+ asyncio.run(scheduler_only())
481
+ finally:
482
+ lock.release()
483
+ return state["exit"]
484
+
485
+
486
+ def main(argv=None) -> int:
487
+ argv = list(sys.argv[1:] if argv is None else argv)
488
+ if argv[:1] == ["run"]:
489
+ return run()
490
+ print("usage: python -m hyperclaw.teamsetup.service run", file=sys.stderr)
491
+ return 2
492
+
493
+
494
+ if __name__ == "__main__":
495
+ raise SystemExit(main())