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,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())
|