hyperagents 1.2.3rc5.post3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- ui/tui/app.py +214 -0
hyperclaw/agent.py
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HyperClaw Agent — Assistant with Tool Execution
|
|
3
|
+
Full computer control using Anthropic tool-use API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import asyncio
|
|
8
|
+
import subprocess
|
|
9
|
+
import glob as globlib
|
|
10
|
+
import re
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import AsyncIterator, Optional, Any
|
|
14
|
+
|
|
15
|
+
import anthropic
|
|
16
|
+
from hyperclaw.cost_guard import max_output
|
|
17
|
+
from dotenv import load_dotenv
|
|
18
|
+
|
|
19
|
+
load_dotenv()
|
|
20
|
+
|
|
21
|
+
HYPERCLAW_ROOT = Path(os.environ.get("HYPERCLAW_ROOT", str(Path.home() / ".hyperclaw")))
|
|
22
|
+
WORKSPACE_PATH = HYPERCLAW_ROOT / "workspace"
|
|
23
|
+
MODEL = "claude-sonnet-4-6"
|
|
24
|
+
MAX_TOKENS = 16000
|
|
25
|
+
|
|
26
|
+
# Context files to load
|
|
27
|
+
CONTEXT_FILES = [
|
|
28
|
+
"SOUL.md",
|
|
29
|
+
"IDENTITY.md",
|
|
30
|
+
"USER.md",
|
|
31
|
+
"MEMORY.md",
|
|
32
|
+
"memory/instincts.md",
|
|
33
|
+
"memory/core-episodes.md",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Tool definitions for Claude API
|
|
37
|
+
TOOLS = [
|
|
38
|
+
{
|
|
39
|
+
"name": "bash",
|
|
40
|
+
"description": "Execute a bash command on the local system. Use for system operations, git, file management, etc.",
|
|
41
|
+
"input_schema": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"command": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "The bash command to execute"
|
|
47
|
+
},
|
|
48
|
+
"timeout": {
|
|
49
|
+
"type": "integer",
|
|
50
|
+
"description": "Timeout in seconds (default 120)",
|
|
51
|
+
"default": 120
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"required": ["command"]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "read_file",
|
|
59
|
+
"description": "Read the contents of a file. Use absolute paths.",
|
|
60
|
+
"input_schema": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"path": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "Absolute path to the file"
|
|
66
|
+
},
|
|
67
|
+
"limit": {
|
|
68
|
+
"type": "integer",
|
|
69
|
+
"description": "Maximum lines to read (default all)"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"required": ["path"]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "write_file",
|
|
77
|
+
"description": "Write content to a file. Creates parent directories if needed.",
|
|
78
|
+
"input_schema": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"path": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "Absolute path to the file"
|
|
84
|
+
},
|
|
85
|
+
"content": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "Content to write"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"required": ["path", "content"]
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "edit_file",
|
|
95
|
+
"description": "Edit a file by replacing old_string with new_string.",
|
|
96
|
+
"input_schema": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"path": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "Absolute path to the file"
|
|
102
|
+
},
|
|
103
|
+
"old_string": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "The text to find and replace"
|
|
106
|
+
},
|
|
107
|
+
"new_string": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "The replacement text"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"required": ["path", "old_string", "new_string"]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "glob",
|
|
117
|
+
"description": "Find files matching a glob pattern.",
|
|
118
|
+
"input_schema": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {
|
|
121
|
+
"pattern": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "Glob pattern (e.g., '**/*.py', '*.md')"
|
|
124
|
+
},
|
|
125
|
+
"path": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "Base directory (default: home)",
|
|
128
|
+
"default": str(Path.home())
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"required": ["pattern"]
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "grep",
|
|
136
|
+
"description": "Search for a pattern in files using ripgrep.",
|
|
137
|
+
"input_schema": {
|
|
138
|
+
"type": "object",
|
|
139
|
+
"properties": {
|
|
140
|
+
"pattern": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"description": "Regex pattern to search for"
|
|
143
|
+
},
|
|
144
|
+
"path": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"description": "Directory or file to search in",
|
|
147
|
+
"default": str(Path.home())
|
|
148
|
+
},
|
|
149
|
+
"file_pattern": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"description": "File glob to filter (e.g., '*.py')"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"required": ["pattern"]
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "list_directory",
|
|
159
|
+
"description": "List contents of a directory.",
|
|
160
|
+
"input_schema": {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"path": {
|
|
164
|
+
"type": "string",
|
|
165
|
+
"description": "Directory path to list"
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"required": ["path"]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def execute_tool(name: str, inputs: dict) -> str:
|
|
175
|
+
"""Execute a tool and return the result."""
|
|
176
|
+
if inputs is None:
|
|
177
|
+
inputs = {}
|
|
178
|
+
try:
|
|
179
|
+
if name == "bash":
|
|
180
|
+
cmd = inputs.get("command")
|
|
181
|
+
if not cmd:
|
|
182
|
+
return "Error: bash requires 'command' parameter"
|
|
183
|
+
# Service recovery (bench 17.4/17.5): this loop has no approval
|
|
184
|
+
# channel, so no service is ever started, stopped or killed here.
|
|
185
|
+
from hyperclaw.tools.service_guard import check_command
|
|
186
|
+
svc = check_command(cmd)
|
|
187
|
+
if svc is not None:
|
|
188
|
+
return (f"Error: not run. Service and process control needs the owner's "
|
|
189
|
+
f"approval in the HyperAgents terminal ({svc.reason}).")
|
|
190
|
+
return _exec_bash(cmd, inputs.get("timeout", 120))
|
|
191
|
+
elif name == "read_file":
|
|
192
|
+
path = inputs.get("path")
|
|
193
|
+
if not path:
|
|
194
|
+
return "Error: read_file requires 'path' parameter"
|
|
195
|
+
return _exec_read_file(path, inputs.get("limit"))
|
|
196
|
+
elif name == "write_file":
|
|
197
|
+
path = inputs.get("path")
|
|
198
|
+
content = inputs.get("content")
|
|
199
|
+
if not path or content is None:
|
|
200
|
+
return "Error: write_file requires 'path' and 'content' parameters"
|
|
201
|
+
return _exec_write_file(path, content)
|
|
202
|
+
elif name == "edit_file":
|
|
203
|
+
path = inputs.get("path")
|
|
204
|
+
old = inputs.get("old_string")
|
|
205
|
+
new = inputs.get("new_string")
|
|
206
|
+
if not all([path, old is not None, new is not None]):
|
|
207
|
+
return "Error: edit_file requires 'path', 'old_string', and 'new_string' parameters"
|
|
208
|
+
return _exec_edit_file(path, old, new)
|
|
209
|
+
elif name == "glob":
|
|
210
|
+
pattern = inputs.get("pattern")
|
|
211
|
+
if not pattern:
|
|
212
|
+
return "Error: glob requires 'pattern' parameter"
|
|
213
|
+
return _exec_glob(pattern, inputs.get("path", str(Path.home())))
|
|
214
|
+
elif name == "grep":
|
|
215
|
+
pattern = inputs.get("pattern")
|
|
216
|
+
if not pattern:
|
|
217
|
+
return "Error: grep requires 'pattern' parameter"
|
|
218
|
+
return _exec_grep(pattern, inputs.get("path", str(Path.home())), inputs.get("file_pattern"))
|
|
219
|
+
elif name == "list_directory":
|
|
220
|
+
path = inputs.get("path")
|
|
221
|
+
if not path:
|
|
222
|
+
return "Error: list_directory requires 'path' parameter"
|
|
223
|
+
return _exec_list_dir(path)
|
|
224
|
+
else:
|
|
225
|
+
return f"Unknown tool: {name}"
|
|
226
|
+
except Exception as e:
|
|
227
|
+
return f"Tool error ({name}): {e}"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _exec_bash(command: str, timeout: int = 120) -> str:
|
|
231
|
+
"""Execute bash command."""
|
|
232
|
+
try:
|
|
233
|
+
result = subprocess.run(
|
|
234
|
+
command,
|
|
235
|
+
shell=True,
|
|
236
|
+
capture_output=True,
|
|
237
|
+
text=True,
|
|
238
|
+
timeout=timeout,
|
|
239
|
+
cwd=str(Path.home())
|
|
240
|
+
)
|
|
241
|
+
output = result.stdout
|
|
242
|
+
if result.stderr:
|
|
243
|
+
output += f"\nSTDERR:\n{result.stderr}"
|
|
244
|
+
if result.returncode != 0:
|
|
245
|
+
output += f"\n[Exit code: {result.returncode}]"
|
|
246
|
+
return output.strip() or "(no output)"
|
|
247
|
+
except subprocess.TimeoutExpired:
|
|
248
|
+
return f"Command timed out after {timeout}s"
|
|
249
|
+
except Exception as e:
|
|
250
|
+
return f"Bash error: {e}"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _exec_read_file(path: str, limit: Optional[int] = None) -> str:
|
|
254
|
+
"""Read file contents."""
|
|
255
|
+
try:
|
|
256
|
+
p = Path(path).expanduser()
|
|
257
|
+
if not p.exists():
|
|
258
|
+
return f"File not found: {path}"
|
|
259
|
+
content = p.read_text(encoding="utf-8", errors="replace")
|
|
260
|
+
lines = content.splitlines()
|
|
261
|
+
if limit:
|
|
262
|
+
lines = lines[:limit]
|
|
263
|
+
# Add line numbers
|
|
264
|
+
numbered = [f"{i+1:4d} {line}" for i, line in enumerate(lines)]
|
|
265
|
+
return "\n".join(numbered)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
return f"Read error: {e}"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _exec_write_file(path: str, content: str) -> str:
|
|
271
|
+
"""Write content to file."""
|
|
272
|
+
try:
|
|
273
|
+
p = Path(path).expanduser()
|
|
274
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
p.write_text(content, encoding="utf-8")
|
|
276
|
+
return f"Wrote {len(content)} bytes to {path}"
|
|
277
|
+
except Exception as e:
|
|
278
|
+
return f"Write error: {e}"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _exec_edit_file(path: str, old_string: str, new_string: str) -> str:
|
|
282
|
+
"""Edit file by replacing string."""
|
|
283
|
+
try:
|
|
284
|
+
p = Path(path).expanduser()
|
|
285
|
+
if not p.exists():
|
|
286
|
+
return f"File not found: {path}"
|
|
287
|
+
content = p.read_text(encoding="utf-8")
|
|
288
|
+
if old_string not in content:
|
|
289
|
+
return f"String not found in file: {old_string[:50]}..."
|
|
290
|
+
count = content.count(old_string)
|
|
291
|
+
if count > 1:
|
|
292
|
+
return f"String appears {count} times — be more specific"
|
|
293
|
+
new_content = content.replace(old_string, new_string, 1)
|
|
294
|
+
p.write_text(new_content, encoding="utf-8")
|
|
295
|
+
return f"Replaced in {path}"
|
|
296
|
+
except Exception as e:
|
|
297
|
+
return f"Edit error: {e}"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _exec_glob(pattern: str, base_path: str) -> str:
|
|
301
|
+
"""Find files matching glob pattern."""
|
|
302
|
+
try:
|
|
303
|
+
p = Path(base_path).expanduser()
|
|
304
|
+
matches = list(p.glob(pattern))[:100] # Limit results
|
|
305
|
+
if not matches:
|
|
306
|
+
return "No matches found"
|
|
307
|
+
return "\n".join(str(m) for m in sorted(matches))
|
|
308
|
+
except Exception as e:
|
|
309
|
+
return f"Glob error: {e}"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _exec_grep(pattern: str, path: str, file_pattern: Optional[str] = None) -> str:
|
|
313
|
+
"""Search with ripgrep."""
|
|
314
|
+
try:
|
|
315
|
+
cmd = ["rg", "-n", "--max-count=50", pattern, path]
|
|
316
|
+
if file_pattern:
|
|
317
|
+
cmd.extend(["-g", file_pattern])
|
|
318
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
319
|
+
output = result.stdout.strip()
|
|
320
|
+
return output if output else "No matches found"
|
|
321
|
+
except FileNotFoundError:
|
|
322
|
+
# Fallback to grep
|
|
323
|
+
cmd = f"grep -rn '{pattern}' {path} | head -50"
|
|
324
|
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
|
325
|
+
return result.stdout.strip() or "No matches found"
|
|
326
|
+
except Exception as e:
|
|
327
|
+
return f"Grep error: {e}"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _exec_list_dir(path: str) -> str:
|
|
331
|
+
"""List directory contents."""
|
|
332
|
+
try:
|
|
333
|
+
p = Path(path).expanduser()
|
|
334
|
+
if not p.exists():
|
|
335
|
+
return f"Directory not found: {path}"
|
|
336
|
+
if not p.is_dir():
|
|
337
|
+
return f"Not a directory: {path}"
|
|
338
|
+
items = sorted(p.iterdir())
|
|
339
|
+
lines = []
|
|
340
|
+
for item in items[:100]:
|
|
341
|
+
prefix = "d " if item.is_dir() else "f "
|
|
342
|
+
lines.append(f"{prefix} {item.name}")
|
|
343
|
+
return "\n".join(lines) if lines else "(empty directory)"
|
|
344
|
+
except Exception as e:
|
|
345
|
+
return f"List error: {e}"
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class HyperClawAgent:
|
|
349
|
+
"""Assistant agent with full tool execution."""
|
|
350
|
+
|
|
351
|
+
def __init__(self):
|
|
352
|
+
from hyperclaw.cost_guard import guard_client
|
|
353
|
+
self.client = guard_client(
|
|
354
|
+
anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "")), "anthropic")
|
|
355
|
+
self.system_prompt = self._load_system_prompt()
|
|
356
|
+
self.history: list[dict] = []
|
|
357
|
+
|
|
358
|
+
def _tools_allowed(self) -> bool:
|
|
359
|
+
"""Tools only with a Hyper-signed QUALIFIED record (spec §11)."""
|
|
360
|
+
try:
|
|
361
|
+
from hyperclaw.qualification import is_qualified
|
|
362
|
+
return is_qualified("anthropic", MODEL)
|
|
363
|
+
except Exception:
|
|
364
|
+
return False
|
|
365
|
+
|
|
366
|
+
def _load_system_prompt(self) -> str:
|
|
367
|
+
"""hyperclaw.prompt_assembly order: truth rules + access first, then
|
|
368
|
+
identity and persona; workspace files are fenced as DATA (bench 1.6)."""
|
|
369
|
+
from hyperclaw.chat_scope import OWNER
|
|
370
|
+
from hyperclaw.prompt_assembly import assemble, identity_block, persona_for
|
|
371
|
+
from hyperclaw.truth_guard import TRUTH_RULES, access_manifest
|
|
372
|
+
mode = "tools" if self._tools_allowed() else "chat_only"
|
|
373
|
+
surface = "\n".join([
|
|
374
|
+
"## How to work",
|
|
375
|
+
"- Concise when needed, thorough when it matters. No filler words.",
|
|
376
|
+
"- Never exfiltrate private data.",
|
|
377
|
+
"- When in doubt, ask before acting externally.",
|
|
378
|
+
"",
|
|
379
|
+
f"Current date: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
|
380
|
+
])
|
|
381
|
+
context = []
|
|
382
|
+
for filename in CONTEXT_FILES:
|
|
383
|
+
filepath = WORKSPACE_PATH / filename
|
|
384
|
+
if filepath.exists():
|
|
385
|
+
try:
|
|
386
|
+
context.append((filename, filepath.read_text(encoding="utf-8")))
|
|
387
|
+
except Exception:
|
|
388
|
+
pass
|
|
389
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
390
|
+
daily_log = HYPERCLAW_ROOT / "memory" / f"{today}.md"
|
|
391
|
+
if daily_log.exists():
|
|
392
|
+
try:
|
|
393
|
+
context.append((f"today's log {today}", daily_log.read_text(encoding="utf-8")))
|
|
394
|
+
except Exception:
|
|
395
|
+
pass
|
|
396
|
+
return assemble(truth_rules=TRUTH_RULES,
|
|
397
|
+
access=access_manifest(mode, "anthropic", MODEL),
|
|
398
|
+
identity=identity_block("Assistant", "", OWNER, model=MODEL),
|
|
399
|
+
scope=OWNER, surface_instructions=surface,
|
|
400
|
+
persona=persona_for(OWNER), context=context)
|
|
401
|
+
|
|
402
|
+
def reload_context(self) -> None:
|
|
403
|
+
"""Hot-reload context."""
|
|
404
|
+
self.system_prompt = self._load_system_prompt()
|
|
405
|
+
|
|
406
|
+
async def chat(self, message: str) -> AsyncIterator[str]:
|
|
407
|
+
"""
|
|
408
|
+
Process a message with tool execution.
|
|
409
|
+
Yields text chunks as they come, executes tools as needed.
|
|
410
|
+
"""
|
|
411
|
+
self.history.append({"role": "user", "content": message})
|
|
412
|
+
|
|
413
|
+
# Trim history
|
|
414
|
+
if len(self.history) > 120:
|
|
415
|
+
self.history = self.history[-120:]
|
|
416
|
+
|
|
417
|
+
messages = list(self.history)
|
|
418
|
+
|
|
419
|
+
max_iterations = 100 # Assistant: raised from 20 — complex multi-step tasks require depth
|
|
420
|
+
iteration = 0
|
|
421
|
+
consecutive_errors = 0
|
|
422
|
+
|
|
423
|
+
tools_on = self._tools_allowed() # no record -> chat-only, nothing runs
|
|
424
|
+
while iteration < max_iterations:
|
|
425
|
+
iteration += 1
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
response = self.client.messages.create(
|
|
429
|
+
model=MODEL,
|
|
430
|
+
max_tokens=max_output("agent", "anthropic", MODEL, requested=MAX_TOKENS),
|
|
431
|
+
system=self.system_prompt,
|
|
432
|
+
tools=ADVERTISED_TOOLS if tools_on else anthropic.NOT_GIVEN,
|
|
433
|
+
messages=messages,
|
|
434
|
+
)
|
|
435
|
+
except anthropic.APIError as e:
|
|
436
|
+
yield f"[API Error: {e}]"
|
|
437
|
+
return
|
|
438
|
+
except Exception as e:
|
|
439
|
+
yield f"[Error: {e}]"
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
# Process response content
|
|
443
|
+
assistant_content = []
|
|
444
|
+
has_tool_use = False
|
|
445
|
+
|
|
446
|
+
for block in response.content:
|
|
447
|
+
if block.type == "text":
|
|
448
|
+
yield block.text
|
|
449
|
+
assistant_content.append({"type": "text", "text": block.text})
|
|
450
|
+
elif block.type == "tool_use" and not tools_on:
|
|
451
|
+
# Tools are off (no signed record): a tool_use block from
|
|
452
|
+
# the model never runs (fail closed).
|
|
453
|
+
yield (f"\n[HyperClaw: the model asked for tool '{block.name}', but tools "
|
|
454
|
+
f"are off on this installation; it did NOT run.]\n")
|
|
455
|
+
elif block.type == "tool_use":
|
|
456
|
+
has_tool_use = True
|
|
457
|
+
tool_name = block.name
|
|
458
|
+
tool_input = block.input
|
|
459
|
+
tool_id = block.id
|
|
460
|
+
|
|
461
|
+
yield f"\n[Executing: {tool_name}]\n"
|
|
462
|
+
|
|
463
|
+
# Execute the tool
|
|
464
|
+
result = execute_tool(tool_name, tool_input)
|
|
465
|
+
|
|
466
|
+
# Track consecutive errors to break out of loops
|
|
467
|
+
if result.startswith("Error:") or result.startswith("Tool error"):
|
|
468
|
+
consecutive_errors += 1
|
|
469
|
+
if consecutive_errors >= 5:
|
|
470
|
+
yield f"\n[Stopping: too many consecutive tool errors]\n"
|
|
471
|
+
return
|
|
472
|
+
else:
|
|
473
|
+
consecutive_errors = 0
|
|
474
|
+
|
|
475
|
+
# Show truncated result
|
|
476
|
+
if len(result) > 8000:
|
|
477
|
+
yield f"{result[:8000]}...\n[{len(result)} chars total — truncated]\n"
|
|
478
|
+
else:
|
|
479
|
+
yield f"{result}\n"
|
|
480
|
+
|
|
481
|
+
assistant_content.append({
|
|
482
|
+
"type": "tool_use",
|
|
483
|
+
"id": tool_id,
|
|
484
|
+
"name": tool_name,
|
|
485
|
+
"input": tool_input
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
# Add tool result to continue conversation
|
|
489
|
+
messages.append({"role": "assistant", "content": assistant_content})
|
|
490
|
+
messages.append({
|
|
491
|
+
"role": "user",
|
|
492
|
+
"content": [{
|
|
493
|
+
"type": "tool_result",
|
|
494
|
+
"tool_use_id": tool_id,
|
|
495
|
+
"content": result
|
|
496
|
+
}]
|
|
497
|
+
})
|
|
498
|
+
assistant_content = []
|
|
499
|
+
|
|
500
|
+
# If no tool use, we're done
|
|
501
|
+
if not has_tool_use:
|
|
502
|
+
# Save final assistant message to history
|
|
503
|
+
if assistant_content:
|
|
504
|
+
text_parts = [b["text"] for b in assistant_content if b["type"] == "text"]
|
|
505
|
+
if text_parts:
|
|
506
|
+
self.history.append({"role": "assistant", "content": " ".join(text_parts)})
|
|
507
|
+
break
|
|
508
|
+
|
|
509
|
+
# Check stop reason
|
|
510
|
+
if response.stop_reason == "end_turn":
|
|
511
|
+
break
|
|
512
|
+
|
|
513
|
+
if iteration >= max_iterations:
|
|
514
|
+
yield "\n[Stopped: max tool iterations reached]\n"
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
# Singleton
|
|
518
|
+
_agent: Optional[HyperClawAgent] = None
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def get_agent() -> HyperClawAgent:
|
|
522
|
+
"""Get or create the agent singleton."""
|
|
523
|
+
global _agent
|
|
524
|
+
if _agent is None:
|
|
525
|
+
_agent = HyperClawAgent()
|
|
526
|
+
return _agent
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
# ─────────────────────────────────────────────
|
|
530
|
+
# INTEGRATION TOOLS (Gmail, Calendar, iMessage, Supabase)
|
|
531
|
+
# ─────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
INTEGRATION_TOOLS = [
|
|
534
|
+
{
|
|
535
|
+
"name": "gmail_inbox",
|
|
536
|
+
"description": "List Gmail inbox messages. Returns message IDs and snippets.",
|
|
537
|
+
"input_schema": {
|
|
538
|
+
"type": "object",
|
|
539
|
+
"properties": {
|
|
540
|
+
"max_results": {"type": "integer", "description": "Max messages to return (default 20)", "default": 20},
|
|
541
|
+
"query": {"type": "string", "description": "Gmail search query (default: 'in:inbox is:unread')", "default": "in:inbox is:unread"}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"name": "gmail_read",
|
|
547
|
+
"description": "Read the full text of a Gmail message by ID.",
|
|
548
|
+
"input_schema": {
|
|
549
|
+
"type": "object",
|
|
550
|
+
"properties": {
|
|
551
|
+
"message_id": {"type": "string", "description": "Gmail message ID"}
|
|
552
|
+
},
|
|
553
|
+
"required": ["message_id"]
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
"name": "gmail_send",
|
|
558
|
+
"description": "Send an email via Gmail. Always CCs the configured CC address per policy.",
|
|
559
|
+
"input_schema": {
|
|
560
|
+
"type": "object",
|
|
561
|
+
"properties": {
|
|
562
|
+
"to": {"type": "string", "description": "Recipient email address"},
|
|
563
|
+
"subject": {"type": "string", "description": "Email subject"},
|
|
564
|
+
"body": {"type": "string", "description": "Email body (plain text)"},
|
|
565
|
+
"cc": {"type": "string", "description": "Additional CC (optional)"},
|
|
566
|
+
"reply_to_id": {"type": "string", "description": "Thread ID to reply to (optional)"}
|
|
567
|
+
},
|
|
568
|
+
"required": ["to", "subject", "body"]
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"name": "calendar_events",
|
|
573
|
+
"description": "Get upcoming Google Calendar events.",
|
|
574
|
+
"input_schema": {
|
|
575
|
+
"type": "object",
|
|
576
|
+
"properties": {
|
|
577
|
+
"days_ahead": {"type": "integer", "description": "How many days ahead to look (default 7)", "default": 7}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
"name": "calendar_create",
|
|
583
|
+
"description": "Create a Google Calendar event.",
|
|
584
|
+
"input_schema": {
|
|
585
|
+
"type": "object",
|
|
586
|
+
"properties": {
|
|
587
|
+
"title": {"type": "string", "description": "Event title"},
|
|
588
|
+
"start_iso": {"type": "string", "description": "Start time in ISO format (e.g. 2026-03-28T14:00:00-07:00)"},
|
|
589
|
+
"end_iso": {"type": "string", "description": "End time in ISO format"},
|
|
590
|
+
"description": {"type": "string", "description": "Event description (optional)"},
|
|
591
|
+
"attendees": {"type": "array", "items": {"type": "string"}, "description": "List of attendee emails (optional)"}
|
|
592
|
+
},
|
|
593
|
+
"required": ["title", "start_iso", "end_iso"]
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
"name": "imessage_send",
|
|
598
|
+
"description": "Send an iMessage or SMS to a phone number or Apple ID email.",
|
|
599
|
+
"input_schema": {
|
|
600
|
+
"type": "object",
|
|
601
|
+
"properties": {
|
|
602
|
+
"recipient": {"type": "string", "description": "Phone number (+1XXXXXXXXXX) or Apple ID email"},
|
|
603
|
+
"message": {"type": "string", "description": "Message content"}
|
|
604
|
+
},
|
|
605
|
+
"required": ["recipient", "message"]
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
"name": "imessage_read",
|
|
610
|
+
"description": "Read recent iMessages from the Messages app database.",
|
|
611
|
+
"input_schema": {
|
|
612
|
+
"type": "object",
|
|
613
|
+
"properties": {
|
|
614
|
+
"contact": {"type": "string", "description": "Filter by contact phone/email (optional)"},
|
|
615
|
+
"limit": {"type": "integer", "description": "Number of messages to retrieve (default 10)", "default": 10}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
"name": "supabase_query",
|
|
621
|
+
"description": "Query a Supabase table. Available tables: episodic_memories, hyperclaw_episodes, knowledge_nodes, kg_entities, kg_facts, hyperstates, swarm_messages, audit_log",
|
|
622
|
+
"input_schema": {
|
|
623
|
+
"type": "object",
|
|
624
|
+
"properties": {
|
|
625
|
+
"table": {"type": "string", "description": "Table name"},
|
|
626
|
+
"limit": {"type": "integer", "description": "Max rows (default 50)", "default": 50}
|
|
627
|
+
},
|
|
628
|
+
"required": ["table"]
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
"name": "supabase_store_memory",
|
|
633
|
+
"description": "Store a memory/note in Supabase episodic_memories for long-term persistence.",
|
|
634
|
+
"input_schema": {
|
|
635
|
+
"type": "object",
|
|
636
|
+
"properties": {
|
|
637
|
+
"content": {"type": "string", "description": "Memory content to store"},
|
|
638
|
+
"memory_type": {"type": "string", "description": "Type: episodic, semantic, procedural (default: episodic)", "default": "episodic"},
|
|
639
|
+
"tags": {"type": "array", "items": {"type": "string"}, "description": "Tags for retrieval"},
|
|
640
|
+
"importance": {"type": "number", "description": "Importance 0.0-1.0 (default 0.5)", "default": 0.5}
|
|
641
|
+
},
|
|
642
|
+
"required": ["content"]
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
"name": "integration_status",
|
|
647
|
+
"description": "Check the status of all Assistant integrations (Gmail, Calendar, iMessage, Supabase, Telegram, WhatsApp).",
|
|
648
|
+
"input_schema": {
|
|
649
|
+
"type": "object",
|
|
650
|
+
"properties": {}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
]
|
|
654
|
+
|
|
655
|
+
# Merge into main TOOLS list
|
|
656
|
+
TOOLS = TOOLS + INTEGRATION_TOOLS
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _exec_integration_tool(name: str, inputs: dict) -> str:
|
|
660
|
+
"""Execute an integration tool."""
|
|
661
|
+
try:
|
|
662
|
+
from hyperclaw.integrations_layer import (
|
|
663
|
+
gmail_list_inbox, gmail_read_message_text, gmail_send,
|
|
664
|
+
calendar_get_events, calendar_create_event,
|
|
665
|
+
imessage_send, imessage_get_recent,
|
|
666
|
+
supabase_query, supabase_store_memory,
|
|
667
|
+
get_integration_status
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
if name == "gmail_inbox":
|
|
671
|
+
result = gmail_list_inbox(
|
|
672
|
+
inputs.get('max_results', 20),
|
|
673
|
+
inputs.get('query', 'in:inbox is:unread')
|
|
674
|
+
)
|
|
675
|
+
msgs = result.get('messages', [])
|
|
676
|
+
return f"Found {len(msgs)} messages.\nMessage IDs: {[m['id'] for m in msgs[:10]]}"
|
|
677
|
+
|
|
678
|
+
elif name == "gmail_read":
|
|
679
|
+
return gmail_read_message_text(inputs['message_id'])
|
|
680
|
+
|
|
681
|
+
elif name == "gmail_send":
|
|
682
|
+
result = gmail_send(
|
|
683
|
+
inputs['to'], inputs['subject'], inputs['body'],
|
|
684
|
+
inputs.get('cc', ''), inputs.get('reply_to_id', '')
|
|
685
|
+
)
|
|
686
|
+
return json.dumps(result)
|
|
687
|
+
|
|
688
|
+
elif name == "calendar_events":
|
|
689
|
+
events = calendar_get_events(inputs.get('days_ahead', 7))
|
|
690
|
+
if not events:
|
|
691
|
+
return "No upcoming events."
|
|
692
|
+
lines = [f"- {e['summary']} @ {e['start']}" + (f" | {e['location']}" if e.get('location') else '') for e in events]
|
|
693
|
+
return "\n".join(lines)
|
|
694
|
+
|
|
695
|
+
elif name == "calendar_create":
|
|
696
|
+
result = calendar_create_event(
|
|
697
|
+
inputs['title'], inputs['start_iso'], inputs['end_iso'],
|
|
698
|
+
inputs.get('description', ''), inputs.get('attendees', [])
|
|
699
|
+
)
|
|
700
|
+
return json.dumps(result)
|
|
701
|
+
|
|
702
|
+
elif name == "imessage_send":
|
|
703
|
+
result = imessage_send(inputs['recipient'], inputs['message'])
|
|
704
|
+
return json.dumps(result)
|
|
705
|
+
|
|
706
|
+
elif name == "imessage_read":
|
|
707
|
+
messages = imessage_get_recent(inputs.get('contact', ''), inputs.get('limit', 10))
|
|
708
|
+
if not messages:
|
|
709
|
+
return "No messages found."
|
|
710
|
+
lines = []
|
|
711
|
+
for m in messages:
|
|
712
|
+
if 'error' in m:
|
|
713
|
+
return f"Error: {m['error']}"
|
|
714
|
+
direction = "→" if m['from_me'] else "←"
|
|
715
|
+
lines.append(f"[{m['time']}] {direction} {m['handle']}: {m['text'][:100]}")
|
|
716
|
+
return "\n".join(lines)
|
|
717
|
+
|
|
718
|
+
elif name == "supabase_query":
|
|
719
|
+
rows = supabase_query(inputs['table'], inputs.get('limit', 50))
|
|
720
|
+
return json.dumps(rows[:5], indent=2, default=str)
|
|
721
|
+
|
|
722
|
+
elif name == "supabase_store_memory":
|
|
723
|
+
result = supabase_store_memory(
|
|
724
|
+
inputs['content'],
|
|
725
|
+
inputs.get('memory_type', 'episodic'),
|
|
726
|
+
inputs.get('tags', []),
|
|
727
|
+
inputs.get('importance', 0.5)
|
|
728
|
+
)
|
|
729
|
+
return json.dumps(result)
|
|
730
|
+
|
|
731
|
+
elif name == "integration_status":
|
|
732
|
+
status = get_integration_status()
|
|
733
|
+
return "\n".join(f"{k}: {v}" for k, v in status.items())
|
|
734
|
+
|
|
735
|
+
return f"Unknown integration tool: {name}"
|
|
736
|
+
except Exception as e:
|
|
737
|
+
return f"Integration error ({name}): {e}"
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
# Patch execute_tool to handle integration tools
|
|
741
|
+
_original_execute_tool = execute_tool
|
|
742
|
+
|
|
743
|
+
# Sends and external writes from this loop. It has no Send / Edit / Cancel
|
|
744
|
+
# step of its own, so they never run here (fail closed): the owner sends from
|
|
745
|
+
# the HyperAgents terminal or chat, where the approval layer shows the exact
|
|
746
|
+
# content first.
|
|
747
|
+
APPROVAL_REQUIRED = frozenset({"gmail_send", "calendar_create", "imessage_send",
|
|
748
|
+
"supabase_store_memory"})
|
|
749
|
+
|
|
750
|
+
# The only tools this loop runs: read-only ones. Shell commands and file
|
|
751
|
+
# writes/edits need the owner's Send / Edit / Cancel on the exact command or
|
|
752
|
+
# content, which this loop does not offer, so they are refused here (tier
|
|
753
|
+
# "never" in this loop) and not offered to the model. The HyperAgents
|
|
754
|
+
# terminal runs them through the approval layer instead.
|
|
755
|
+
READ_ONLY_TOOLS = frozenset({"read_file", "glob", "grep", "list_directory", "gmail_inbox",
|
|
756
|
+
"gmail_read", "calendar_events", "imessage_read", "supabase_query",
|
|
757
|
+
"integration_status"})
|
|
758
|
+
ADVERTISED_TOOLS = [t for t in TOOLS if t["name"] in READ_ONLY_TOOLS]
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def execute_tool(name: str, inputs: dict) -> str:
|
|
762
|
+
"""Execute a tool — handles both system and integration tools."""
|
|
763
|
+
if name in APPROVAL_REQUIRED:
|
|
764
|
+
return (f"Error: {name} was NOT run. Sending or writing to an outside service needs "
|
|
765
|
+
f"the owner's approval (Send / Edit / Cancel), which this loop does not "
|
|
766
|
+
f"offer. Tell the user to do it from the HyperAgents terminal or chat.")
|
|
767
|
+
if name not in READ_ONLY_TOOLS:
|
|
768
|
+
return (f"Error: not run. {name} runs a command or changes something, which needs the "
|
|
769
|
+
f"owner's approval (Send / Edit / Cancel) on the exact command or content; "
|
|
770
|
+
f"this loop does not offer that. Tell the user to do it from the HyperAgents "
|
|
771
|
+
f"terminal.")
|
|
772
|
+
integration_tools = {t['name'] for t in INTEGRATION_TOOLS}
|
|
773
|
+
if name in integration_tools:
|
|
774
|
+
return _exec_integration_tool(name, inputs)
|
|
775
|
+
return _original_execute_tool(name, inputs)
|