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