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,293 @@
1
+ """
2
+ Version management for Civilization nodes.
3
+ Tracks changes, maintains history, and supports rollback.
4
+ """
5
+ from __future__ import annotations
6
+ import logging
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from typing import Any
10
+ from uuid import UUID, uuid4
11
+ import json
12
+ import hashlib
13
+
14
+ from ..schema import CivilizationNode
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class NodeVersion:
21
+ """Represents a specific version of a node."""
22
+ version_id: UUID = field(default_factory=uuid4)
23
+ node_id: UUID = field(default_factory=uuid4)
24
+ version_number: str = "1.0.0"
25
+ content_hash: str = ""
26
+ snapshot: dict = field(default_factory=dict)
27
+ created_at: datetime = field(default_factory=datetime.utcnow)
28
+ created_by: str | None = None
29
+ change_summary: str | None = None
30
+ metadata: dict = field(default_factory=dict)
31
+
32
+ @property
33
+ def major(self) -> int:
34
+ parts = self.version_number.split(".")
35
+ return int(parts[0]) if parts else 1
36
+
37
+ @property
38
+ def minor(self) -> int:
39
+ parts = self.version_number.split(".")
40
+ return int(parts[1]) if len(parts) > 1 else 0
41
+
42
+ @property
43
+ def patch(self) -> int:
44
+ parts = self.version_number.split(".")
45
+ return int(parts[2]) if len(parts) > 2 else 0
46
+
47
+
48
+ @dataclass
49
+ class VersionHistory:
50
+ """Complete version history for a node."""
51
+ node_id: UUID
52
+ current_version: str = "1.0.0"
53
+ versions: list[NodeVersion] = field(default_factory=list)
54
+
55
+ @property
56
+ def version_count(self) -> int:
57
+ return len(self.versions)
58
+
59
+ def get_version(self, version_number: str) -> NodeVersion | None:
60
+ """Get a specific version by version number."""
61
+ for v in self.versions:
62
+ if v.version_number == version_number:
63
+ return v
64
+ return None
65
+
66
+ def get_latest(self) -> NodeVersion | None:
67
+ """Get the most recent version."""
68
+ if not self.versions:
69
+ return None
70
+ return max(self.versions, key=lambda v: v.created_at)
71
+
72
+
73
+ class VersionManager:
74
+ """
75
+ Manages versions of civilization nodes.
76
+ Supports semantic versioning, history tracking, and rollback.
77
+ """
78
+
79
+ def __init__(self, org_id: str, store=None):
80
+ self.org_id = org_id
81
+ self.store = store
82
+ self._histories: dict[UUID, VersionHistory] = {}
83
+
84
+ def _compute_hash(self, node: CivilizationNode) -> str:
85
+ """Compute content hash for a node."""
86
+ # Exclude volatile fields from hash
87
+ data = node.model_dump(mode="json", exclude={"updated_at", "embedding"})
88
+ content = json.dumps(data, sort_keys=True)
89
+ return hashlib.sha256(content.encode()).hexdigest()[:16]
90
+
91
+ def _parse_version(self, version: str) -> tuple[int, int, int]:
92
+ """Parse version string into (major, minor, patch)."""
93
+ parts = version.split(".")
94
+ return (
95
+ int(parts[0]) if parts else 1,
96
+ int(parts[1]) if len(parts) > 1 else 0,
97
+ int(parts[2]) if len(parts) > 2 else 0,
98
+ )
99
+
100
+ def _increment_version(
101
+ self,
102
+ current: str,
103
+ bump_type: str = "patch",
104
+ ) -> str:
105
+ """Increment version number."""
106
+ major, minor, patch = self._parse_version(current)
107
+
108
+ if bump_type == "major":
109
+ return f"{major + 1}.0.0"
110
+ elif bump_type == "minor":
111
+ return f"{major}.{minor + 1}.0"
112
+ else: # patch
113
+ return f"{major}.{minor}.{patch + 1}"
114
+
115
+ def create_version(
116
+ self,
117
+ node: CivilizationNode,
118
+ bump_type: str = "patch",
119
+ change_summary: str | None = None,
120
+ created_by: str | None = None,
121
+ ) -> NodeVersion:
122
+ """
123
+ Create a new version of a node.
124
+
125
+ Args:
126
+ node: The node to version
127
+ bump_type: "major", "minor", or "patch"
128
+ change_summary: Description of changes
129
+ created_by: ID of user making the change
130
+
131
+ Returns:
132
+ The new NodeVersion
133
+ """
134
+ history = self._histories.get(node.id)
135
+
136
+ if history:
137
+ new_version_num = self._increment_version(history.current_version, bump_type)
138
+ else:
139
+ new_version_num = "1.0.0"
140
+ history = VersionHistory(node_id=node.id)
141
+ self._histories[node.id] = history
142
+
143
+ version = NodeVersion(
144
+ node_id=node.id,
145
+ version_number=new_version_num,
146
+ content_hash=self._compute_hash(node),
147
+ snapshot=node.model_dump(mode="json"),
148
+ created_by=created_by,
149
+ change_summary=change_summary,
150
+ )
151
+
152
+ history.versions.append(version)
153
+ history.current_version = new_version_num
154
+
155
+ logger.info(f"Created version {new_version_num} for node {node.id}")
156
+ return version
157
+
158
+ def get_history(self, node_id: UUID) -> VersionHistory | None:
159
+ """Get the version history for a node."""
160
+ return self._histories.get(node_id)
161
+
162
+ def get_version(self, node_id: UUID, version_number: str) -> NodeVersion | None:
163
+ """Get a specific version of a node."""
164
+ history = self._histories.get(node_id)
165
+ if history:
166
+ return history.get_version(version_number)
167
+ return None
168
+
169
+ def get_current_version(self, node_id: UUID) -> str:
170
+ """Get the current version number for a node."""
171
+ history = self._histories.get(node_id)
172
+ return history.current_version if history else "1.0.0"
173
+
174
+ def has_changes(self, node: CivilizationNode) -> bool:
175
+ """Check if a node has uncommitted changes."""
176
+ history = self._histories.get(node.id)
177
+ if not history or not history.versions:
178
+ return True
179
+
180
+ current_hash = self._compute_hash(node)
181
+ latest = history.get_latest()
182
+ return latest is None or latest.content_hash != current_hash
183
+
184
+ def restore_version(
185
+ self,
186
+ node_id: UUID,
187
+ version_number: str,
188
+ ) -> CivilizationNode | None:
189
+ """
190
+ Restore a node to a previous version.
191
+ Returns the restored node or None if version not found.
192
+ """
193
+ version = self.get_version(node_id, version_number)
194
+ if not version:
195
+ return None
196
+
197
+ # Reconstruct node from snapshot
198
+ return CivilizationNode(**version.snapshot)
199
+
200
+ def compare_versions(
201
+ self,
202
+ node_id: UUID,
203
+ version1: str,
204
+ version2: str,
205
+ ) -> dict | None:
206
+ """
207
+ Compare two versions of a node.
208
+ Returns a dict with added, removed, and changed fields.
209
+ """
210
+ v1 = self.get_version(node_id, version1)
211
+ v2 = self.get_version(node_id, version2)
212
+
213
+ if not v1 or not v2:
214
+ return None
215
+
216
+ changes = {
217
+ "added": {},
218
+ "removed": {},
219
+ "changed": {},
220
+ }
221
+
222
+ s1 = v1.snapshot
223
+ s2 = v2.snapshot
224
+
225
+ # Find added and changed
226
+ for key, val in s2.items():
227
+ if key not in s1:
228
+ changes["added"][key] = val
229
+ elif s1[key] != val:
230
+ changes["changed"][key] = {"old": s1[key], "new": val}
231
+
232
+ # Find removed
233
+ for key in s1:
234
+ if key not in s2:
235
+ changes["removed"][key] = s1[key]
236
+
237
+ return changes
238
+
239
+ def get_version_timeline(self, node_id: UUID) -> list[dict]:
240
+ """Get a timeline of all versions for a node."""
241
+ history = self._histories.get(node_id)
242
+ if not history:
243
+ return []
244
+
245
+ return [
246
+ {
247
+ "version": v.version_number,
248
+ "created_at": v.created_at.isoformat(),
249
+ "created_by": v.created_by,
250
+ "change_summary": v.change_summary,
251
+ "content_hash": v.content_hash,
252
+ }
253
+ for v in sorted(history.versions, key=lambda x: x.created_at)
254
+ ]
255
+
256
+ def cleanup_old_versions(
257
+ self,
258
+ node_id: UUID,
259
+ keep_count: int = 10,
260
+ ) -> int:
261
+ """
262
+ Remove old versions, keeping the most recent N.
263
+ Returns count of removed versions.
264
+ """
265
+ history = self._histories.get(node_id)
266
+ if not history or len(history.versions) <= keep_count:
267
+ return 0
268
+
269
+ sorted_versions = sorted(history.versions, key=lambda v: v.created_at, reverse=True)
270
+ to_remove = sorted_versions[keep_count:]
271
+
272
+ for v in to_remove:
273
+ history.versions.remove(v)
274
+
275
+ return len(to_remove)
276
+
277
+ def export_version(self, node_id: UUID, version_number: str) -> dict | None:
278
+ """Export a version as a portable dict."""
279
+ version = self.get_version(node_id, version_number)
280
+ if not version:
281
+ return None
282
+
283
+ return {
284
+ "version_id": str(version.version_id),
285
+ "node_id": str(version.node_id),
286
+ "version_number": version.version_number,
287
+ "content_hash": version.content_hash,
288
+ "snapshot": version.snapshot,
289
+ "created_at": version.created_at.isoformat(),
290
+ "created_by": version.created_by,
291
+ "change_summary": version.change_summary,
292
+ "metadata": version.metadata,
293
+ }
cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # HyperClaw CLI
cli/doctor.py ADDED
@@ -0,0 +1,334 @@
1
+ """
2
+ hyperclaw doctor — system health check.
3
+ Verifies API keys, DB connection, pgvector, policy files, and dependencies.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import importlib
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Callable
13
+
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+ from rich import print as rprint
17
+
18
+ console = Console()
19
+
20
+ CHECK_MARK = "[bold green]✓[/bold green]"
21
+ CROSS_MARK = "[bold red]✗[/bold red]"
22
+ WARN_MARK = "[bold yellow]⚠[/bold yellow]"
23
+
24
+
25
+ def _check(label: str, ok: bool, detail: str = "", warn: bool = False) -> dict:
26
+ status = CHECK_MARK if ok else (WARN_MARK if warn else CROSS_MARK)
27
+ return {"label": label, "ok": ok, "warn": warn, "detail": detail, "status": status}
28
+
29
+
30
+ async def _check_db(db_url: str) -> dict:
31
+ try:
32
+ import asyncpg
33
+ conn = await asyncpg.connect(db_url, timeout=5)
34
+ await conn.close()
35
+ return _check("PostgreSQL connection", True, db_url.split("@")[-1] if "@" in db_url else db_url)
36
+ except Exception as e:
37
+ return _check("PostgreSQL connection", False, str(e)[:80])
38
+
39
+
40
+ async def _check_pgvector(db_url: str) -> dict:
41
+ try:
42
+ import asyncpg
43
+ conn = await asyncpg.connect(db_url, timeout=5)
44
+ row = await conn.fetchval(
45
+ "SELECT COUNT(*) FROM pg_extension WHERE extname = 'vector'"
46
+ )
47
+ await conn.close()
48
+ if row:
49
+ return _check("pgvector extension", True, "installed")
50
+ else:
51
+ return _check("pgvector extension", False,
52
+ "Not installed — run: CREATE EXTENSION vector; in your DB", warn=True)
53
+ except Exception as e:
54
+ return _check("pgvector extension", False, str(e)[:80])
55
+
56
+
57
+ async def _check_migrations(db_url: str) -> dict:
58
+ try:
59
+ import asyncpg
60
+ conn = await asyncpg.connect(db_url, timeout=5)
61
+ exists = await conn.fetchval(
62
+ "SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name='_migrations')"
63
+ )
64
+ if exists:
65
+ applied = await conn.fetch("SELECT filename FROM _migrations ORDER BY filename")
66
+ names = [r["filename"] for r in applied]
67
+ await conn.close()
68
+ return _check("DB migrations", True, f"{len(names)} applied: {', '.join(names)}")
69
+ else:
70
+ await conn.close()
71
+ return _check("DB migrations", False,
72
+ "Not initialized — run: hyperclaw init", warn=True)
73
+ except Exception as e:
74
+ return _check("DB migrations", False, str(e)[:80])
75
+
76
+
77
+ def _print_provider_matrix(probe: bool = False) -> None:
78
+ """Provider/slot matrix per the routing spec; --probe sends one cheap
79
+ request per live provider and reports latency."""
80
+ from hyperclaw.providers import registry, SLOTS
81
+ from hyperclaw.deployment import is_hyper
82
+ reg = registry()
83
+ line = " ".join(f"{n} {'✓' if pr.live else '–'}" for n, pr in reg.providers.items())
84
+ console.print(f"\n[bold]Providers[/bold] {line}"
85
+ + (" [dim](Hyper-deployed: allowed models only)[/dim]" if is_hyper() else ""))
86
+ summary = reg.slot_summary()
87
+ for i, slot in enumerate(SLOTS):
88
+ label = "[bold]Slots[/bold] " if i == 0 else " "
89
+ console.print(f"{label} {slot:<11}→ {summary.get(slot, 'not configured')}")
90
+ if not probe:
91
+ return
92
+ import time as _t
93
+ from hyperclaw.cost_guard import guard_client, max_output # probes count against the cost cap too
94
+ console.print("\n[bold]Probe[/bold]")
95
+ for name, prov in reg.providers.items():
96
+ if not prov.live:
97
+ console.print(f" {name}: skipped (not configured)")
98
+ continue
99
+ t0 = _t.time()
100
+ try:
101
+ if prov.kind == "anthropic":
102
+ import anthropic as _an
103
+ guard_client(_an.Anthropic(api_key=prov.api_key),
104
+ getattr(prov, "name", None)).messages.create(
105
+ model=prov.model_for("fast") or prov.model_for(),
106
+ max_tokens=max_output("doctor", name, prov.model_for("fast") or prov.model_for()),
107
+ messages=[{"role": "user", "content": "ping"}])
108
+ else:
109
+ import openai as _oa
110
+ guard_client(_oa.OpenAI(api_key=prov.api_key, base_url=prov.base_url or None),
111
+ getattr(prov, "name", None)).chat.completions.create(
112
+ model=prov.model_for(),
113
+ max_tokens=max_output("doctor", name, prov.model_for()),
114
+ messages=[{"role": "user", "content": "ping"}])
115
+ console.print(f" {name}: ok ({(_t.time()-t0)*1000:.0f}ms)")
116
+ except Exception as e:
117
+ console.print(f" {name}: FAILED ({type(e).__name__}: {str(e)[:80]})")
118
+
119
+
120
+ def run_doctor(probe: bool = False) -> int:
121
+ """
122
+ Run all health checks and print results.
123
+ Returns 0 if all critical checks pass, 1 if any fail.
124
+ """
125
+ console.print("\n[bold cyan]⚡ HyperClaw Doctor[/bold cyan]\n")
126
+ # Load $HYPERCLAW_ROOT/.env (default ~/.hyperclaw) so doctor works without
127
+ # a sourced shell, before anything reads provider settings.
128
+ from hyperclaw.providers import reset_registry
129
+ from hyperclaw.userconfig import load_env_into_process
130
+ load_env_into_process()
131
+ reset_registry()
132
+ _print_provider_matrix(probe=probe)
133
+
134
+ checks: list[dict] = []
135
+ db_url = os.environ.get("DATABASE_URL", "")
136
+
137
+ # ── Python version ────────────────────────────────────────────────────────
138
+ py = sys.version_info
139
+ checks.append(_check(
140
+ "Python version",
141
+ py >= (3, 11),
142
+ f"{py.major}.{py.minor}.{py.micro}",
143
+ warn=(py >= (3, 11)),
144
+ ))
145
+
146
+ # ── Required packages ─────────────────────────────────────────────────────
147
+ required_packages = {
148
+ "pydantic": "pydantic",
149
+ "asyncpg": "asyncpg",
150
+ "anthropic": "anthropic",
151
+ "httpx": "httpx",
152
+ "typer": "typer",
153
+ "rich": "rich",
154
+ "yaml": "yaml (pyyaml)",
155
+ "dotenv": "dotenv (python-dotenv)",
156
+ }
157
+ for mod, label in required_packages.items():
158
+ try:
159
+ importlib.import_module(mod)
160
+ checks.append(_check(f"Package: {label}", True))
161
+ except ImportError:
162
+ checks.append(_check(f"Package: {label}", False, f"pip install {label.split()[0]}"))
163
+
164
+ # ── Environment variables ─────────────────────────────────────────────────
165
+ # (the user's .env was loaded before the provider matrix, above)
166
+ provider = os.environ.get("LLM_PROVIDER", "")
167
+ from hyperclaw.deployment import is_hyper
168
+ from hyperclaw.provider_setup import describe, v2_status
169
+ hyper = is_hyper()
170
+ v2 = v2_status()
171
+ v2_chosen = provider in ("", "hyperspeed") or hyper or v2.get("live")
172
+ from hyperclaw.model_policy import allowed_display
173
+ checks.append(_check("Install mode", True,
174
+ "Hyper-deployed · allowed models: " + ", ".join(allowed_display())
175
+ if hyper else "standard"))
176
+ checks.append(_check("LLM_PROVIDER", True, provider or "(not set: HyperSpeed V2 default)"))
177
+
178
+ if v2.get("present") and v2_chosen:
179
+ # HyperSpeed V2 is the default: a missing Anthropic (or other
180
+ # third-party) key is not a failure (spec §11).
181
+ checks.append(_check("Model provider", True, f"{v2['label']} ({v2['mode']})"))
182
+ checks.append(_check(
183
+ "Hyper access key", v2["key_set"],
184
+ "set (hidden)" if v2["key_set"] else
185
+ "Not set — run: hyperclaw provider hyper-key"))
186
+ checks.append(_check("HyperSpeed V2 endpoint", True, v2["base_url"]))
187
+ checks.append(_check(
188
+ "HyperSpeed V2 model", True,
189
+ v2["model"] + (" (placeholder pending adapter confirmation; "
190
+ "override with HYPERSPEED_MODEL)" if v2["model_placeholder"] else "")))
191
+ own = [r for r in describe() if r["origin"] != "hyper"]
192
+ live_own = [r for r in own if r["live"]]
193
+ if hyper:
194
+ from hyperclaw.managed_config import provider_allowed
195
+ stray = [k for k, p in (("ANTHROPIC_API_KEY", "anthropic"),
196
+ ("OPENAI_API_KEY", "openai"), ("XAI_API_KEY", "xai"))
197
+ if os.environ.get(k) and not provider_allowed(p)]
198
+ checks.append(_check(
199
+ "Other providers", not stray,
200
+ "only the models on this install's allowed list"
201
+ + (f"; ignored keys present: {', '.join(stray)}" if stray else ""),
202
+ warn=bool(stray)))
203
+ elif not live_own:
204
+ checks.append(_check("Your own providers", True, "none added (optional)"))
205
+ for r in live_own:
206
+ checks.append(_check(r["label"][:1].upper() + r["label"][1:], True,
207
+ f"configured · {r['model'] or '-'} · {r['mode']}"))
208
+ elif provider == "openai":
209
+ oai_key = os.environ.get("OPENAI_API_KEY", "")
210
+ checks.append(_check("Model provider", True, "your own provider (OpenAI)"))
211
+ checks.append(_check("OPENAI_API_KEY", bool(oai_key),
212
+ "set (hidden)" if oai_key else "Not set — run: hyperclaw provider add openai"))
213
+ elif provider == "openai_compat":
214
+ oai_key = os.environ.get("OPENAI_API_KEY", "")
215
+ checks.append(_check(
216
+ "OPENAI_API_KEY",
217
+ bool(oai_key),
218
+ f"...{oai_key[-6:]}" if oai_key else "Not set — export OPENAI_API_KEY=...",
219
+ ))
220
+ oai_url = os.environ.get("OPENAI_BASE_URL", "")
221
+ checks.append(_check(
222
+ "OPENAI_BASE_URL",
223
+ bool(oai_url),
224
+ oai_url if oai_url else "Not set — export OPENAI_BASE_URL=http://host/v1",
225
+ ))
226
+ oai_model = os.environ.get("OPENAI_MODEL", "")
227
+ checks.append(_check("OPENAI_MODEL", True, oai_model or "(default)"))
228
+ elif provider == "bedrock":
229
+ region = os.environ.get("AWS_REGION", "") or os.environ.get("AWS_DEFAULT_REGION", "")
230
+ checks.append(_check(
231
+ "AWS_REGION",
232
+ bool(region),
233
+ region if region else "Not set — defaulting to us-east-1",
234
+ warn=not bool(region),
235
+ ))
236
+ bedrock_model = os.environ.get("BEDROCK_MODEL", "anthropic.claude-sonnet-5")
237
+ checks.append(_check("BEDROCK_MODEL", True, bedrock_model))
238
+ elif provider == "anthropic_compat":
239
+ anthropic_key = os.environ.get("ANTHROPIC_API_KEY", "")
240
+ checks.append(_check(
241
+ "ANTHROPIC_API_KEY",
242
+ bool(anthropic_key),
243
+ f"...{anthropic_key[-6:]}" if anthropic_key else "Not set",
244
+ ))
245
+ compat_url = os.environ.get("ANTHROPIC_BASE_URL", "")
246
+ checks.append(_check(
247
+ "ANTHROPIC_BASE_URL",
248
+ bool(compat_url),
249
+ compat_url if compat_url else "Not set — export ANTHROPIC_BASE_URL=http://host",
250
+ ))
251
+ else:
252
+ anthropic_key = os.environ.get("ANTHROPIC_API_KEY", "")
253
+ checks.append(_check("Model provider", True, "your own provider (Anthropic)"))
254
+ checks.append(_check(
255
+ "ANTHROPIC_API_KEY",
256
+ bool(anthropic_key),
257
+ f"sk-ant-...{anthropic_key[-6:]}" if anthropic_key else "Not set — export ANTHROPIC_API_KEY=...",
258
+ ))
259
+
260
+ db_url_env = os.environ.get("DATABASE_URL", "")
261
+ checks.append(_check(
262
+ "DATABASE_URL",
263
+ bool(db_url_env),
264
+ f"...{db_url_env[-30:]}" if db_url_env else "Not set — using config/hyperclaw.yaml fallback",
265
+ warn=not bool(db_url_env),
266
+ ))
267
+
268
+ chatjimmy_key = os.environ.get("CHATJIMMY_API_KEY", "")
269
+ checks.append(_check(
270
+ "CHATJIMMY_API_KEY",
271
+ bool(chatjimmy_key),
272
+ "Set" if chatjimmy_key else "Not set — ChatJimmy will be unavailable (optional)",
273
+ warn=not bool(chatjimmy_key),
274
+ ))
275
+
276
+ # ── Config files ──────────────────────────────────────────────────────────
277
+ config_files = [
278
+ "config/hyperclaw.yaml",
279
+ "config/agents.yaml",
280
+ "config/models.yaml",
281
+ "security/policies/default.yaml",
282
+ ]
283
+ for cf in config_files:
284
+ checks.append(_check(f"Config: {cf}", Path(cf).exists(),
285
+ "Found" if Path(cf).exists() else f"Missing — run: hyperclaw init"))
286
+
287
+ # ── DB checks (async) ─────────────────────────────────────────────────────
288
+ if db_url_env:
289
+ loop = asyncio.new_event_loop()
290
+ try:
291
+ checks.append(loop.run_until_complete(_check_db(db_url_env)))
292
+ checks.append(loop.run_until_complete(_check_pgvector(db_url_env)))
293
+ checks.append(loop.run_until_complete(_check_migrations(db_url_env)))
294
+ finally:
295
+ loop.close()
296
+ else:
297
+ checks.append(_check("PostgreSQL connection", False,
298
+ "Skipped — DATABASE_URL not set", warn=True))
299
+ checks.append(_check("pgvector extension", False,
300
+ "Skipped — DATABASE_URL not set", warn=True))
301
+ checks.append(_check("DB migrations", False,
302
+ "Skipped — DATABASE_URL not set", warn=True))
303
+
304
+ # ── Print results ─────────────────────────────────────────────────────────
305
+ table = Table(show_header=True, header_style="bold cyan", expand=True)
306
+ table.add_column("Check", style="bold")
307
+ table.add_column("Status", justify="center", width=4)
308
+ table.add_column("Detail", style="dim")
309
+
310
+ for c in checks:
311
+ table.add_row(c["label"], c["status"], c["detail"])
312
+
313
+ console.print(table)
314
+
315
+ failed = [c for c in checks if not c["ok"] and not c.get("warn")]
316
+ warned = [c for c in checks if not c["ok"] and c.get("warn")]
317
+ passed = [c for c in checks if c["ok"]]
318
+
319
+ console.print(
320
+ f"\n[green]{len(passed)} passed[/green] "
321
+ f"[yellow]{len(warned)} warnings[/yellow] "
322
+ f"[red]{len(failed)} failed[/red]"
323
+ )
324
+
325
+ if failed:
326
+ console.print("\n[red]Fix the failed checks above before running hyperclaw start.[/red]")
327
+ return 1
328
+
329
+ if not warned:
330
+ console.print("\n[bold green]✓ HyperClaw is healthy and ready to run.[/bold green]")
331
+ else:
332
+ console.print("\n[bold yellow]⚠ HyperClaw has warnings — review above before running tasks.[/bold yellow]")
333
+
334
+ return 0