Compare commits
336 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| faff64c413 | |||
| 6fbcdc1d87 | |||
| 69a11af949 | |||
| 9ef272020e | |||
| 258cfe7de5 | |||
| 0d53b21133 | |||
| 0ccb28ffab | |||
| b30b571b44 | |||
| bc44c3a401 | |||
| 7fbf57cbb7 | |||
| 67d094f51a | |||
| 873af04c6e | |||
| 1920192656 | |||
| 4cbd5a4c6c | |||
| 65aa5629e8 | |||
| 7193d09bed | |||
| 49f8fae0b4 | |||
| e1a490756e | |||
| 91bfaf36e3 | |||
| 465adf5b1f | |||
| 8018325923 | |||
| b4cf10214b | |||
| e421bcc326 | |||
| 9b76ac48b7 | |||
| 6da48eac6f | |||
| 638ff04e24 | |||
| 4ff531dec7 | |||
| 4f8b3d7aff | |||
| 210fa9c474 | |||
| 25361cac8c | |||
| 28defebd6d | |||
| d58f3103dd | |||
| 5d1ed35660 | |||
| 1f3e305534 | |||
| 7d8fdd279c | |||
| bb061b770f | |||
| a8768b9ed6 | |||
| b437aa5f6c | |||
| 9248182570 | |||
| 7c77c7170f | |||
| 85fcb6516c | |||
| e8e76d85f7 | |||
| 5aaa5ae4d5 | |||
| c3a8ee9c7b | |||
| 5d07a8aba5 | |||
| d18e0594b8 | |||
| 26dcc86a24 | |||
| e928ad19e5 | |||
| 6768aaa575 | |||
| f561aacbfc | |||
| d9edd7adf7 | |||
| b4a5323009 | |||
| ade8b5b9a7 | |||
| e4ace3d484 | |||
| f3dd25adc5 | |||
| ec251f8168 | |||
| 1bb9579dc5 | |||
| 7ebf4146ce | |||
| e0e05f3488 | |||
| c92f2510c8 | |||
| ea1fbe9ee1 | |||
| 84a0be0179 | |||
| 1b5780461e | |||
| c8d35b63a4 | |||
| feb1ebae04 | |||
| efe49d0a5b | |||
| e50a5ea22a | |||
| 6382c94d0a | |||
| 58ce84c9cc | |||
| 08fd6ff765 | |||
| a9cb79909c | |||
| 852f8ccd94 | |||
| 9388ef3e99 | |||
| 04afb0c4bb | |||
| a07fd44de3 | |||
| f6c1b13846 | |||
| 654fa3dd1f | |||
| 8183449d27 | |||
| a9acfb86ad | |||
| d7d070ac5f | |||
| 8c01b573ce | |||
| 7744f21b9d | |||
| 9ed23a235f | |||
| e88328321f | |||
| a4c516bea1 | |||
| 1c932a04ef | |||
| 76d34be4c2 | |||
| d6e8afe316 | |||
| a04f2bcf99 | |||
| c138e7c638 | |||
| fc08c7007f | |||
| d559bb3446 | |||
| 55a8c39e4b | |||
| 02d6f10e5f | |||
| 77428a91cc | |||
| 51403dc276 | |||
| 914a07a35d | |||
| 3c70d7b424 | |||
| ce1ee4ff17 | |||
| fca41d9bda | |||
| ff889e02f7 | |||
| 43ab460462 | |||
| caa06e266b | |||
| 3622ca78ee | |||
| 019e3f9659 | |||
| 208cb579a2 | |||
| 17de7e4485 | |||
| 810616eee1 | |||
| 191f583669 | |||
| 1d638cc18e | |||
| 3efa1f3b88 | |||
| 4daa33db09 | |||
| fab2fb0056 | |||
| ce885c120e | |||
| 75b53c47ff | |||
| 2936f73707 | |||
| e26426b138 | |||
| 62cacb8e28 | |||
| f3e37190ce | |||
| 0863bbbd2f | |||
| b23fa1daad | |||
| 05cc1ce599 | |||
| e6939f8d51 | |||
| 801fef12e1 | |||
| 5845629175 | |||
| 11b916301a | |||
| aa5d80b1d2 | |||
| aa5f990acd | |||
| 9764c82c2a | |||
| 543a71eb6c | |||
| 8285593c13 | |||
| 6fbfe773fb | |||
| a8c54b1e5f | |||
| a5323abfca | |||
| ba4df2d2c4 | |||
| 6510633a8c | |||
| 9172e5f46b | |||
| ed3e3848c0 | |||
| ee90185d5c | |||
| 6eb2633677 | |||
| c1f215dcf2 | |||
| 97cc9a1045 | |||
| 5f7b02a4b7 | |||
| e696b41a0e | |||
| 1f9acc6135 | |||
| 7e8699cb4b | |||
| fd4fc657d6 | |||
| 34403648b9 | |||
| 3795d50eb9 | |||
| 80515dde5a | |||
| efcd296d83 | |||
| 802cb292b0 | |||
| 8e55f74d73 | |||
| 3d810485a0 | |||
| 94cfd48661 | |||
| 87c8e741f3 | |||
| d0e92ed18d | |||
| 1927045519 | |||
| 68cffb86c9 | |||
| 5bec989647 | |||
| 66f5d2f36c | |||
| 941f815254 | |||
| 42afd10518 | |||
| 3efa285a59 | |||
| 4f2b4172b4 | |||
| 0d7de71b94 | |||
| f0f5b4bede | |||
| bfd27e97d3 | |||
| f2def27390 | |||
| b3f7bd6cc0 | |||
| 0e8e78dc5b | |||
| b259d85776 | |||
| 175d9c3b7c | |||
| a2a810aabf | |||
| 175c7cfd51 | |||
| 5ada973d38 | |||
| 0103276136 | |||
| 1d9e8ec138 | |||
| 83ac2e71bb | |||
| 0b35a729a7 | |||
| 56723a519a | |||
| ebff394c76 | |||
| ceecc97bc8 | |||
| 313154f880 | |||
| 3eb6417cdc | |||
| 1b35d6ca0a | |||
| 1d89f0ba9d | |||
| 864df0e21a | |||
| 3f626decc4 | |||
| bf1760b1a9 | |||
| 8a58ea6344 | |||
| 662ff4c35f | |||
| af02352b49 | |||
| db9f987d46 | |||
| 8490ce1389 | |||
| 55ea9a56a4 | |||
| bd2381b10d | |||
| 443de755bd | |||
| 55ec5f14ee | |||
| 2e019302c9 | |||
| b1e829644b | |||
| 18f773e91b | |||
| 987cfee930 | |||
| 57f6b8498a | |||
| 9f0d35977c | |||
| e5910bbf2f | |||
| 0015bf7b38 | |||
| a6b9234abb | |||
| 086f3942b8 | |||
| 924f4abede | |||
| 02be91cb08 | |||
| c2298393ab | |||
| 4b8c63bf6e | |||
| e089c3b72c | |||
| a93983b5db | |||
| 20f6329004 | |||
| 3c2cf71c47 | |||
| 56288c3137 | |||
| 79188921a5 | |||
| 5ab66008ae | |||
| f38c9ee049 | |||
| 86f5e71ec2 | |||
| 1e15cc8495 | |||
| 077d82ad82 | |||
| e4cf7f3da2 | |||
| e3bdc9e8d7 | |||
| f1c1c9aab3 | |||
| 4860739a2f | |||
| 791ee40cd6 | |||
| e0191ac52b | |||
| e0724df196 | |||
| 2a56294638 | |||
| d5cd557013 | |||
| 2a43f23a3d | |||
| 69af8f569a | |||
| 0e86dbcc9b | |||
| 92c75aa6f5 | |||
| be41d848e5 | |||
| f7c299f6f0 | |||
| b6a0f65a09 | |||
| 1e7b0068ed | |||
| de5105f313 | |||
| 6d32f1bb36 | |||
| 9c316cee28 | |||
| 6af4f2d6e6 | |||
| 57651900f1 | |||
| 46b0617018 | |||
| 7d9bd2e86b | |||
| cce073dbdb | |||
| 6a92588264 | |||
| 276aad6f0d | |||
| 10620bda4f | |||
| c214401a00 | |||
| 260ac33324 | |||
| d4cd643860 | |||
| dc16cfda21 | |||
| ddd30a950d | |||
| 3ca0e63d54 | |||
| 0f8627f17a | |||
| cd0cf69099 | |||
| 9744363342 | |||
| 6fe8439e94 | |||
| 8e61ffe377 | |||
| 723476f7a7 | |||
| 0f253027ae | |||
| 6053895a82 | |||
| ceffa38717 | |||
| ae205fa3f2 | |||
| 669a05892b | |||
| 4898a9759a | |||
| 2c2fa25580 | |||
| 56496d7dbd | |||
| dd0696e44d | |||
| dcda273e0b | |||
| f3b159c650 | |||
| 06df037e28 | |||
| e814e516d1 | |||
| 0375e068ed | |||
| 34ffc533d3 | |||
| ea2ea1a4ae | |||
| 9e11947687 | |||
| 47117281e1 | |||
| 032dd13f5a | |||
| 13d8ebbeff | |||
| 2efa0e01df | |||
| 6044369fdf | |||
| 97440f9e8a | |||
| 765f7cae58 | |||
| b455c8a2ad | |||
| da25e0ffa5 | |||
| e07703c01f | |||
| a4abf3eb2b | |||
| 269d72d073 | |||
| c8f5dccbd2 | |||
| 8b797ee73f | |||
| de38adb1e4 | |||
| c169bcc5d8 | |||
| 80ea286beb | |||
| 3499be782e | |||
| 16603ae49c | |||
| bf6bd9ce7f | |||
| a54c0f6f46 | |||
| beeed11d48 | |||
| 25331590a7 | |||
| bff9f8976e | |||
| b71628e211 | |||
| 8c1cb1f55b | |||
| 66214384a9 | |||
| 6d6646887c | |||
| 6f8db0ed08 | |||
| 6aaf6836ea | |||
| 4f2348f50e | |||
| deb7f2f72a | |||
| d989d9c65a | |||
| 4173c606ab | |||
| a01430d20f | |||
| 2a8f775732 | |||
| 4a0d9b2855 | |||
| 92c65d69ea | |||
| 910a8968c4 | |||
| cdb4679c5a | |||
| 1a9dce89b4 | |||
| cf1e4d7f88 | |||
| f2f0b4fc61 | |||
| b21dd25181 | |||
| 04a18bcbe5 | |||
| 7f66dd67eb | |||
| cfa03b89c8 | |||
| 9866d7a22b | |||
| 331a6e442f | |||
| 1c2295b2b5 | |||
| fa43ca3785 | |||
| b4a2c3bd14 | |||
| 2d4ec4f462 | |||
| 1e8b933da0 | |||
| 48b1e0e038 |
@@ -79,3 +79,4 @@ core/tests/*dumps/*
|
||||
|
||||
screenshots/*
|
||||
|
||||
.gemini/*
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Shared agent instructions for this workspace.
|
||||
|
||||
## Deprecations
|
||||
|
||||
- **TUI is deprecated.** The terminal UI (`hive tui`) is no longer maintained. Use the browser-based interface (`hive open`) instead.
|
||||
|
||||
## Coding Agent Notes
|
||||
|
||||
-
|
||||
|
||||
@@ -112,6 +112,8 @@ This sets up:
|
||||
|
||||
- At last, it will initiate the open hive interface in your browser
|
||||
|
||||
> **Tip:** To reopen the dashboard later, run `hive open` from the project directory.
|
||||
|
||||
<img width="2500" height="1214" alt="home-screen" src="https://github.com/user-attachments/assets/134d897f-5e75-4874-b00b-e0505f6b45c4" />
|
||||
|
||||
### Build Your First Agent
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
perf: reduce subprocess spawning in quickstart scripts (#4427)
|
||||
|
||||
## Problem
|
||||
Windows process creation (CreateProcess) is 10-100x slower than Linux fork/exec.
|
||||
The quickstart scripts were spawning 4+ separate `uv run python -c "import X"`
|
||||
processes to verify imports, adding ~600ms overhead on Windows.
|
||||
|
||||
## Solution
|
||||
Consolidated all import checks into a single batch script that checks multiple
|
||||
modules in one subprocess call, reducing spawn overhead by ~75%.
|
||||
|
||||
## Changes
|
||||
- **New**: `scripts/check_requirements.py` - Batched import checker
|
||||
- **New**: `scripts/test_check_requirements.py` - Test suite
|
||||
- **New**: `scripts/benchmark_quickstart.ps1` - Performance benchmark tool
|
||||
- **Modified**: `quickstart.ps1` - Updated import verification (2 sections)
|
||||
- **Modified**: `quickstart.sh` - Updated import verification
|
||||
|
||||
## Performance Impact
|
||||
**Benchmark results on Windows:**
|
||||
- Before: ~19.8 seconds for import checks
|
||||
- After: ~4.9 seconds for import checks
|
||||
- **Improvement: 14.9 seconds saved (75.2% faster)**
|
||||
|
||||
## Testing
|
||||
- ✅ All functional tests pass (`scripts/test_check_requirements.py`)
|
||||
- ✅ Quickstart scripts work correctly on Windows
|
||||
- ✅ Error handling verified (invalid imports reported correctly)
|
||||
- ✅ Performance benchmark confirms 75%+ improvement
|
||||
|
||||
Fixes #4427
|
||||
@@ -10,7 +10,7 @@ def _load_preferred_model() -> str:
|
||||
config_path = Path.home() / ".hive" / "configuration.json"
|
||||
if config_path.exists():
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
llm = config.get("llm", {})
|
||||
if llm.get("provider") and llm.get("model"):
|
||||
@@ -24,7 +24,7 @@ def _load_preferred_model() -> str:
|
||||
class RuntimeConfig:
|
||||
model: str = field(default_factory=_load_preferred_model)
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 40000
|
||||
max_tokens: int = 8000
|
||||
api_key: str | None = None
|
||||
api_base: str | None = None
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ from framework.graph import NodeSpec
|
||||
# Load reference docs at import time so they're always in the system prompt.
|
||||
# No voluntary read_file() calls needed — the LLM gets everything upfront.
|
||||
_ref_dir = Path(__file__).parent.parent / "reference"
|
||||
_framework_guide = (_ref_dir / "framework_guide.md").read_text()
|
||||
_file_templates = (_ref_dir / "file_templates.md").read_text()
|
||||
_anti_patterns = (_ref_dir / "anti_patterns.md").read_text()
|
||||
_framework_guide = (_ref_dir / "framework_guide.md").read_text(encoding="utf-8")
|
||||
_file_templates = (_ref_dir / "file_templates.md").read_text(encoding="utf-8")
|
||||
_anti_patterns = (_ref_dir / "anti_patterns.md").read_text(encoding="utf-8")
|
||||
_gcu_guide_path = _ref_dir / "gcu_guide.md"
|
||||
_gcu_guide = _gcu_guide_path.read_text() if _gcu_guide_path.exists() else ""
|
||||
_gcu_guide = _gcu_guide_path.read_text(encoding="utf-8") if _gcu_guide_path.exists() else ""
|
||||
|
||||
|
||||
def _is_gcu_enabled() -> bool:
|
||||
|
||||
@@ -660,7 +660,7 @@ class GraphBuilder:
|
||||
# Generate Python code
|
||||
code = self._generate_code(graph)
|
||||
|
||||
Path(path).write_text(code)
|
||||
Path(path).write_text(code, encoding="utf-8")
|
||||
self.session.phase = BuildPhase.EXPORTED
|
||||
self._save_session()
|
||||
|
||||
@@ -754,7 +754,7 @@ class GraphBuilder:
|
||||
"""Save session to disk."""
|
||||
self.session.updated_at = datetime.now()
|
||||
path = self.storage_path / f"{self.session.id}.json"
|
||||
path.write_text(self.session.model_dump_json(indent=2))
|
||||
path.write_text(self.session.model_dump_json(indent=2), encoding="utf-8")
|
||||
|
||||
def _load_session(self, session_id: str) -> BuildSession:
|
||||
"""Load session from disk."""
|
||||
|
||||
@@ -92,7 +92,7 @@ def get_api_key() -> str | None:
|
||||
|
||||
def get_gcu_enabled() -> bool:
|
||||
"""Return whether GCU (browser automation) is enabled in user config."""
|
||||
return get_hive_config().get("gcu_enabled", False)
|
||||
return get_hive_config().get("gcu_enabled", True)
|
||||
|
||||
|
||||
def get_api_base() -> str | None:
|
||||
|
||||
@@ -69,7 +69,7 @@ def save_credential_key(key: str) -> Path:
|
||||
# Restrict the secrets directory itself
|
||||
path.parent.chmod(stat.S_IRWXU) # 0o700
|
||||
|
||||
path.write_text(key)
|
||||
path.write_text(key, encoding="utf-8")
|
||||
path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0o600
|
||||
|
||||
os.environ[CREDENTIAL_KEY_ENV_VAR] = key
|
||||
|
||||
@@ -73,6 +73,7 @@ from .provider import (
|
||||
TokenExpiredError,
|
||||
TokenPlacement,
|
||||
)
|
||||
from .zoho_provider import ZohoOAuth2Provider
|
||||
|
||||
__all__ = [
|
||||
# Types
|
||||
@@ -82,6 +83,7 @@ __all__ = [
|
||||
# Providers
|
||||
"BaseOAuth2Provider",
|
||||
"HubSpotOAuth2Provider",
|
||||
"ZohoOAuth2Provider",
|
||||
# Lifecycle
|
||||
"TokenLifecycleManager",
|
||||
"TokenRefreshResult",
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
Zoho CRM-specific OAuth2 provider.
|
||||
|
||||
Pre-configured for Zoho's OAuth2 endpoints and CRM scopes.
|
||||
Extends BaseOAuth2Provider for Zoho-specific behavior.
|
||||
|
||||
Usage:
|
||||
provider = ZohoOAuth2Provider(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
accounts_domain="https://accounts.zoho.com", # or .in, .eu, etc.
|
||||
)
|
||||
|
||||
# Use with credential store
|
||||
store = CredentialStore(
|
||||
storage=EncryptedFileStorage(),
|
||||
providers=[provider],
|
||||
)
|
||||
|
||||
See: https://www.zoho.com/crm/developer/docs/api/v2/access-refresh.html
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from ..models import CredentialObject, CredentialRefreshError, CredentialType
|
||||
from .base_provider import BaseOAuth2Provider
|
||||
from .provider import OAuth2Config, OAuth2Token, TokenPlacement
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default CRM scopes for Phase 1 (Leads, Contacts, Accounts, Deals, Notes)
|
||||
ZOHO_DEFAULT_SCOPES = [
|
||||
"ZohoCRM.modules.leads.ALL",
|
||||
"ZohoCRM.modules.contacts.ALL",
|
||||
"ZohoCRM.modules.accounts.ALL",
|
||||
"ZohoCRM.modules.deals.ALL",
|
||||
"ZohoCRM.modules.notes.CREATE",
|
||||
]
|
||||
|
||||
|
||||
class ZohoOAuth2Provider(BaseOAuth2Provider):
|
||||
"""
|
||||
Zoho CRM OAuth2 provider with pre-configured endpoints.
|
||||
|
||||
Handles Zoho-specific OAuth2 behavior:
|
||||
- Pre-configured token and authorization URLs (region-aware)
|
||||
- Default CRM scopes for Leads, Contacts, Accounts, Deals, Notes
|
||||
- Token validation via Zoho CRM API
|
||||
- Authorization header format: "Authorization: Zoho-oauthtoken {token}"
|
||||
|
||||
Example:
|
||||
provider = ZohoOAuth2Provider(
|
||||
client_id="your-zoho-client-id",
|
||||
client_secret="your-zoho-client-secret",
|
||||
accounts_domain="https://accounts.zoho.com", # US
|
||||
# or "https://accounts.zoho.in" for India
|
||||
# or "https://accounts.zoho.eu" for EU
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
accounts_domain: str = "https://accounts.zoho.com",
|
||||
api_domain: str | None = None,
|
||||
scopes: list[str] | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize Zoho OAuth2 provider.
|
||||
|
||||
Args:
|
||||
client_id: Zoho OAuth2 client ID
|
||||
client_secret: Zoho OAuth2 client secret
|
||||
accounts_domain: Zoho accounts domain (region-specific)
|
||||
- US: https://accounts.zoho.com
|
||||
- India: https://accounts.zoho.in
|
||||
- EU: https://accounts.zoho.eu
|
||||
- etc.
|
||||
api_domain: Zoho API domain for CRM calls (used in validate).
|
||||
Defaults to ZOHO_API_DOMAIN env or https://www.zohoapis.com
|
||||
scopes: Override default scopes if needed
|
||||
"""
|
||||
base = accounts_domain.rstrip("/")
|
||||
token_url = f"{base}/oauth/v2/token"
|
||||
auth_url = f"{base}/oauth/v2/auth"
|
||||
|
||||
config = OAuth2Config(
|
||||
token_url=token_url,
|
||||
authorization_url=auth_url,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
default_scopes=scopes or ZOHO_DEFAULT_SCOPES,
|
||||
token_placement=TokenPlacement.HEADER_CUSTOM,
|
||||
custom_header_name="Authorization",
|
||||
)
|
||||
super().__init__(config, provider_id="zoho_crm_oauth2")
|
||||
self._accounts_domain = base
|
||||
self._api_domain = (
|
||||
api_domain or os.getenv("ZOHO_API_DOMAIN", "https://www.zohoapis.com")
|
||||
).rstrip("/")
|
||||
|
||||
@property
|
||||
def supported_types(self) -> list[CredentialType]:
|
||||
return [CredentialType.OAUTH2]
|
||||
|
||||
def format_for_request(self, token: OAuth2Token) -> dict[str, Any]:
|
||||
"""
|
||||
Format token for Zoho CRM API requests.
|
||||
|
||||
Zoho uses Authorization header: "Zoho-oauthtoken {access_token}"
|
||||
(not Bearer).
|
||||
"""
|
||||
return {
|
||||
"headers": {
|
||||
"Authorization": f"Zoho-oauthtoken {token.access_token}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
}
|
||||
|
||||
def validate(self, credential: CredentialObject) -> bool:
|
||||
"""
|
||||
Validate Zoho credential by making a lightweight API call.
|
||||
|
||||
Uses GET /crm/v2/users?type=CurrentUser (doesn't require module access).
|
||||
Treats 429 as valid-but-rate-limited.
|
||||
"""
|
||||
access_token = credential.get_key("access_token")
|
||||
if not access_token:
|
||||
return False
|
||||
|
||||
try:
|
||||
client = self._get_client()
|
||||
response = client.get(
|
||||
f"{self._api_domain}/crm/v2/users?type=CurrentUser",
|
||||
headers={
|
||||
"Authorization": f"Zoho-oauthtoken {access_token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
timeout=self.config.request_timeout,
|
||||
)
|
||||
return response.status_code in (200, 429)
|
||||
except Exception as e:
|
||||
logger.debug("Zoho credential validation failed: %s", e)
|
||||
return False
|
||||
|
||||
def _parse_token_response(self, response_data: dict[str, Any]) -> OAuth2Token:
|
||||
"""
|
||||
Parse Zoho token response.
|
||||
|
||||
Zoho returns:
|
||||
{
|
||||
"access_token": "...",
|
||||
"refresh_token": "...",
|
||||
"expires_in": 3600,
|
||||
"api_domain": "https://www.zohoapis.com",
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
"""
|
||||
token = OAuth2Token.from_token_response(response_data)
|
||||
if "api_domain" in response_data:
|
||||
token.raw_response["api_domain"] = response_data["api_domain"]
|
||||
return token
|
||||
|
||||
def refresh(self, credential: CredentialObject) -> CredentialObject:
|
||||
"""Refresh Zoho OAuth2 credential and persist DC metadata."""
|
||||
refresh_tok = credential.get_key("refresh_token")
|
||||
if not refresh_tok:
|
||||
raise CredentialRefreshError(f"Credential '{credential.id}' has no refresh_token")
|
||||
|
||||
try:
|
||||
new_token = self.refresh_access_token(refresh_tok)
|
||||
except Exception as e:
|
||||
raise CredentialRefreshError(f"Failed to refresh '{credential.id}': {e}") from e
|
||||
|
||||
credential.set_key("access_token", new_token.access_token, expires_at=new_token.expires_at)
|
||||
|
||||
if new_token.refresh_token and new_token.refresh_token != refresh_tok:
|
||||
credential.set_key("refresh_token", new_token.refresh_token)
|
||||
|
||||
api_domain = new_token.raw_response.get("api_domain")
|
||||
if isinstance(api_domain, str) and api_domain:
|
||||
credential.set_key("api_domain", api_domain.rstrip("/"))
|
||||
|
||||
accounts_server = new_token.raw_response.get("accounts-server")
|
||||
if isinstance(accounts_server, str) and accounts_server:
|
||||
credential.set_key("accounts_domain", accounts_server.rstrip("/"))
|
||||
|
||||
location = new_token.raw_response.get("location")
|
||||
if isinstance(location, str) and location:
|
||||
credential.set_key("location", location.strip().lower())
|
||||
|
||||
return credential
|
||||
@@ -568,7 +568,7 @@ def _load_nodes_from_python_agent(agent_path: Path) -> list:
|
||||
def _load_nodes_from_json_agent(agent_json: Path) -> list:
|
||||
"""Load nodes from a JSON-based agent."""
|
||||
try:
|
||||
with open(agent_json) as f:
|
||||
with open(agent_json, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
from framework.graph import NodeSpec
|
||||
|
||||
@@ -227,7 +227,7 @@ class EncryptedFileStorage(CredentialStorage):
|
||||
index_path = self.base_path / "metadata" / "index.json"
|
||||
if not index_path.exists():
|
||||
return []
|
||||
with open(index_path) as f:
|
||||
with open(index_path, encoding="utf-8") as f:
|
||||
index = json.load(f)
|
||||
return list(index.get("credentials", {}).keys())
|
||||
|
||||
@@ -268,7 +268,7 @@ class EncryptedFileStorage(CredentialStorage):
|
||||
index_path = self.base_path / "metadata" / "index.json"
|
||||
|
||||
if index_path.exists():
|
||||
with open(index_path) as f:
|
||||
with open(index_path, encoding="utf-8") as f:
|
||||
index = json.load(f)
|
||||
else:
|
||||
index = {"credentials": {}, "version": "1.0"}
|
||||
@@ -283,7 +283,7 @@ class EncryptedFileStorage(CredentialStorage):
|
||||
|
||||
index["last_modified"] = datetime.now(UTC).isoformat()
|
||||
|
||||
with open(index_path, "w") as f:
|
||||
with open(index_path, "w", encoding="utf-8") as f:
|
||||
json.dump(index, f, indent=2)
|
||||
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ def _dump_failed_request(
|
||||
"temperature": kwargs.get("temperature"),
|
||||
}
|
||||
|
||||
with open(filepath, "w") as f:
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(dump_data, f, indent=2, default=str)
|
||||
|
||||
return str(filepath)
|
||||
|
||||
@@ -162,7 +162,7 @@ def _load_session(session_id: str) -> BuildSession:
|
||||
if not session_file.exists():
|
||||
raise ValueError(f"Session '{session_id}' not found")
|
||||
|
||||
with open(session_file) as f:
|
||||
with open(session_file, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
return BuildSession.from_dict(data)
|
||||
@@ -174,7 +174,7 @@ def _load_active_session() -> BuildSession | None:
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(ACTIVE_SESSION_FILE) as f:
|
||||
with open(ACTIVE_SESSION_FILE, encoding="utf-8") as f:
|
||||
session_id = f.read().strip()
|
||||
|
||||
if session_id:
|
||||
@@ -228,7 +228,7 @@ def list_sessions() -> str:
|
||||
if SESSIONS_DIR.exists():
|
||||
for session_file in SESSIONS_DIR.glob("*.json"):
|
||||
try:
|
||||
with open(session_file) as f:
|
||||
with open(session_file, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
sessions.append(
|
||||
{
|
||||
@@ -248,7 +248,7 @@ def list_sessions() -> str:
|
||||
active_id = None
|
||||
if ACTIVE_SESSION_FILE.exists():
|
||||
try:
|
||||
with open(ACTIVE_SESSION_FILE) as f:
|
||||
with open(ACTIVE_SESSION_FILE, encoding="utf-8") as f:
|
||||
active_id = f.read().strip()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -310,7 +310,7 @@ def delete_session(session_id: Annotated[str, "ID of the session to delete"]) ->
|
||||
_session = None
|
||||
|
||||
if ACTIVE_SESSION_FILE.exists():
|
||||
with open(ACTIVE_SESSION_FILE) as f:
|
||||
with open(ACTIVE_SESSION_FILE, encoding="utf-8") as f:
|
||||
active_id = f.read().strip()
|
||||
if active_id == session_id:
|
||||
ACTIVE_SESSION_FILE.unlink()
|
||||
@@ -2894,6 +2894,7 @@ def run_tests(
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
encoding="utf-8",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600, # 10 minute timeout
|
||||
@@ -3085,6 +3086,7 @@ def debug_test(
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
encoding="utf-8",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120, # 2 minute timeout for single test
|
||||
|
||||
@@ -401,6 +401,43 @@ def register_commands(subparsers: argparse._SubParsersAction) -> None:
|
||||
)
|
||||
serve_parser.set_defaults(func=cmd_serve)
|
||||
|
||||
# open command (serve + auto-open browser)
|
||||
open_parser = subparsers.add_parser(
|
||||
"open",
|
||||
help="Start HTTP server and open dashboard in browser",
|
||||
description="Shortcut for 'hive serve --open'. "
|
||||
"Starts the HTTP server and opens the dashboard.",
|
||||
)
|
||||
open_parser.add_argument(
|
||||
"--host",
|
||||
type=str,
|
||||
default="127.0.0.1",
|
||||
help="Host to bind (default: 127.0.0.1)",
|
||||
)
|
||||
open_parser.add_argument(
|
||||
"--port",
|
||||
"-p",
|
||||
type=int,
|
||||
default=8787,
|
||||
help="Port to listen on (default: 8787)",
|
||||
)
|
||||
open_parser.add_argument(
|
||||
"--agent",
|
||||
"-a",
|
||||
type=str,
|
||||
action="append",
|
||||
default=[],
|
||||
help="Agent path to preload (repeatable)",
|
||||
)
|
||||
open_parser.add_argument(
|
||||
"--model",
|
||||
"-m",
|
||||
type=str,
|
||||
default=None,
|
||||
help="LLM model for preloaded agents",
|
||||
)
|
||||
open_parser.set_defaults(func=cmd_open)
|
||||
|
||||
|
||||
def _load_resume_state(
|
||||
agent_path: str, session_id: str, checkpoint_id: str | None = None
|
||||
@@ -517,7 +554,7 @@ def cmd_run(args: argparse.Namespace) -> int:
|
||||
return 1
|
||||
elif args.input_file:
|
||||
try:
|
||||
with open(args.input_file) as f:
|
||||
with open(args.input_file, encoding="utf-8") as f:
|
||||
context = json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
print(f"Error reading input file: {e}", file=sys.stderr)
|
||||
@@ -659,7 +696,7 @@ def cmd_run(args: argparse.Namespace) -> int:
|
||||
|
||||
# Output results
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
json.dump(output, f, indent=2, default=str)
|
||||
if not args.quiet:
|
||||
print(f"Results written to {args.output}")
|
||||
@@ -1517,7 +1554,7 @@ def _extract_python_agent_metadata(agent_path: Path) -> tuple[str, str]:
|
||||
return fallback_name, fallback_desc
|
||||
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
tree = ast.parse(f.read())
|
||||
|
||||
# Find AgentMetadata class definition
|
||||
@@ -1932,10 +1969,18 @@ def _open_browser(url: str) -> None:
|
||||
|
||||
try:
|
||||
if sys.platform == "darwin":
|
||||
subprocess.Popen(["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
subprocess.Popen(
|
||||
["open", url],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
encoding="utf-8",
|
||||
)
|
||||
elif sys.platform == "linux":
|
||||
subprocess.Popen(
|
||||
["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
["xdg-open", url],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
encoding="utf-8",
|
||||
)
|
||||
except Exception:
|
||||
pass # Best-effort — don't crash if browser can't open
|
||||
@@ -1980,12 +2025,14 @@ def _build_frontend() -> bool:
|
||||
# Ensure deps are installed
|
||||
subprocess.run(
|
||||
["npm", "install", "--no-fund", "--no-audit"],
|
||||
encoding="utf-8",
|
||||
cwd=frontend_dir,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["npm", "run", "build"],
|
||||
encoding="utf-8",
|
||||
cwd=frontend_dir,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
@@ -2074,3 +2121,9 @@ def cmd_serve(args: argparse.Namespace) -> int:
|
||||
print("\nServer stopped.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_open(args: argparse.Namespace) -> int:
|
||||
"""Start the HTTP API server and open the dashboard in the browser."""
|
||||
args.open = True
|
||||
return cmd_serve(args)
|
||||
|
||||
+111
-21
@@ -39,6 +39,7 @@ logger = logging.getLogger(__name__)
|
||||
CLAUDE_CREDENTIALS_FILE = Path.home() / ".claude" / ".credentials.json"
|
||||
CLAUDE_OAUTH_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"
|
||||
CLAUDE_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
||||
CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials"
|
||||
|
||||
# Buffer in seconds before token expiry to trigger a proactive refresh
|
||||
_TOKEN_REFRESH_BUFFER_SECS = 300 # 5 minutes
|
||||
@@ -51,6 +52,96 @@ CODEX_KEYCHAIN_SERVICE = "Codex Auth"
|
||||
_CODEX_TOKEN_LIFETIME_SECS = 3600 # 1 hour (no explicit expiry field)
|
||||
|
||||
|
||||
def _read_claude_keychain() -> dict | None:
|
||||
"""Read Claude Code credentials from macOS Keychain.
|
||||
|
||||
Returns the parsed JSON dict, or None if not on macOS or entry missing.
|
||||
"""
|
||||
import getpass
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
if platform.system() != "Darwin":
|
||||
return None
|
||||
|
||||
try:
|
||||
account = getpass.getuser()
|
||||
result = subprocess.run(
|
||||
[
|
||||
"security",
|
||||
"find-generic-password",
|
||||
"-s",
|
||||
CLAUDE_KEYCHAIN_SERVICE,
|
||||
"-a",
|
||||
account,
|
||||
"-w",
|
||||
],
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
raw = result.stdout.strip()
|
||||
if not raw:
|
||||
return None
|
||||
return json.loads(raw)
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError, OSError) as exc:
|
||||
logger.debug("Claude keychain read failed: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def _save_claude_keychain(creds: dict) -> bool:
|
||||
"""Write Claude Code credentials to macOS Keychain. Returns True on success."""
|
||||
import getpass
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
if platform.system() != "Darwin":
|
||||
return False
|
||||
|
||||
try:
|
||||
account = getpass.getuser()
|
||||
data = json.dumps(creds)
|
||||
result = subprocess.run(
|
||||
[
|
||||
"security",
|
||||
"add-generic-password",
|
||||
"-U",
|
||||
"-s",
|
||||
CLAUDE_KEYCHAIN_SERVICE,
|
||||
"-a",
|
||||
account,
|
||||
"-w",
|
||||
data,
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (subprocess.TimeoutExpired, OSError) as exc:
|
||||
logger.debug("Claude keychain write failed: %s", exc)
|
||||
return False
|
||||
|
||||
|
||||
def _read_claude_credentials() -> dict | None:
|
||||
"""Read Claude Code credentials from Keychain (macOS) or file (Linux/Windows)."""
|
||||
# Try macOS Keychain first
|
||||
creds = _read_claude_keychain()
|
||||
if creds:
|
||||
return creds
|
||||
|
||||
# Fall back to file
|
||||
if not CLAUDE_CREDENTIALS_FILE.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(CLAUDE_CREDENTIALS_FILE, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def _refresh_claude_code_token(refresh_token: str) -> dict | None:
|
||||
"""Refresh the Claude Code OAuth token using the refresh token.
|
||||
|
||||
@@ -89,16 +180,14 @@ def _refresh_claude_code_token(refresh_token: str) -> dict | None:
|
||||
|
||||
|
||||
def _save_refreshed_credentials(token_data: dict) -> None:
|
||||
"""Write refreshed token data back to ~/.claude/.credentials.json."""
|
||||
"""Write refreshed token data back to Keychain (macOS) or credentials file."""
|
||||
import time
|
||||
|
||||
if not CLAUDE_CREDENTIALS_FILE.exists():
|
||||
creds = _read_claude_credentials()
|
||||
if not creds:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(CLAUDE_CREDENTIALS_FILE) as f:
|
||||
creds = json.load(f)
|
||||
|
||||
oauth = creds.get("claudeAiOauth", {})
|
||||
oauth["accessToken"] = token_data["access_token"]
|
||||
if "refresh_token" in token_data:
|
||||
@@ -107,9 +196,15 @@ def _save_refreshed_credentials(token_data: dict) -> None:
|
||||
oauth["expiresAt"] = int((time.time() + token_data["expires_in"]) * 1000)
|
||||
creds["claudeAiOauth"] = oauth
|
||||
|
||||
with open(CLAUDE_CREDENTIALS_FILE, "w") as f:
|
||||
json.dump(creds, f, indent=2)
|
||||
logger.debug("Claude Code credentials refreshed successfully")
|
||||
# Try Keychain first (macOS), fall back to file
|
||||
if _save_claude_keychain(creds):
|
||||
logger.debug("Claude Code credentials refreshed in Keychain")
|
||||
return
|
||||
|
||||
if CLAUDE_CREDENTIALS_FILE.exists():
|
||||
with open(CLAUDE_CREDENTIALS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(creds, f, indent=2)
|
||||
logger.debug("Claude Code credentials refreshed in file")
|
||||
except (json.JSONDecodeError, OSError, KeyError) as exc:
|
||||
logger.debug("Failed to save refreshed credentials: %s", exc)
|
||||
|
||||
@@ -117,8 +212,8 @@ def _save_refreshed_credentials(token_data: dict) -> None:
|
||||
def get_claude_code_token() -> str | None:
|
||||
"""Get the OAuth token from Claude Code subscription with auto-refresh.
|
||||
|
||||
Reads from ~/.claude/.credentials.json which is created by the
|
||||
Claude Code CLI when users authenticate with their subscription.
|
||||
Reads from macOS Keychain (on Darwin) or ~/.claude/.credentials.json
|
||||
(on Linux/Windows), as created by the Claude Code CLI.
|
||||
|
||||
If the token is expired or close to expiry, attempts an automatic
|
||||
refresh using the stored refresh token.
|
||||
@@ -128,13 +223,8 @@ def get_claude_code_token() -> str | None:
|
||||
"""
|
||||
import time
|
||||
|
||||
if not CLAUDE_CREDENTIALS_FILE.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(CLAUDE_CREDENTIALS_FILE) as f:
|
||||
creds = json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
creds = _read_claude_credentials()
|
||||
if not creds:
|
||||
return None
|
||||
|
||||
oauth = creds.get("claudeAiOauth", {})
|
||||
@@ -212,7 +302,7 @@ def _read_codex_keychain() -> dict | None:
|
||||
"-w",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
@@ -231,7 +321,7 @@ def _read_codex_auth_file() -> dict | None:
|
||||
if not CODEX_AUTH_FILE.exists():
|
||||
return None
|
||||
try:
|
||||
with open(CODEX_AUTH_FILE) as f:
|
||||
with open(CODEX_AUTH_FILE, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return None
|
||||
@@ -324,7 +414,7 @@ def _save_refreshed_codex_credentials(auth_data: dict, token_data: dict) -> None
|
||||
|
||||
CODEX_AUTH_FILE.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
fd = os.open(CODEX_AUTH_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, "w") as f:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(auth_data, f, indent=2)
|
||||
logger.debug("Codex credentials refreshed successfully")
|
||||
except (OSError, KeyError) as exc:
|
||||
@@ -869,7 +959,7 @@ class AgentRunner:
|
||||
if not agent_json_path.exists():
|
||||
raise FileNotFoundError(f"No agent.py or agent.json found in {agent_path}")
|
||||
|
||||
with open(agent_json_path) as f:
|
||||
with open(agent_json_path, encoding="utf-8") as f:
|
||||
graph, goal = load_agent_export(f.read())
|
||||
|
||||
return cls(
|
||||
|
||||
@@ -340,7 +340,7 @@ class ToolRegistry:
|
||||
self._mcp_config_path = Path(config_path)
|
||||
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load MCP config from {config_path}: {e}")
|
||||
|
||||
@@ -270,10 +270,10 @@ def _edit_test_code(code: str) -> str:
|
||||
|
||||
try:
|
||||
# Open editor
|
||||
subprocess.run([editor, temp_path], check=True)
|
||||
subprocess.run([editor, temp_path], check=True, encoding="utf-8")
|
||||
|
||||
# Read edited code
|
||||
with open(temp_path) as f:
|
||||
with open(temp_path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except subprocess.CalledProcessError:
|
||||
print("Editor failed, keeping original code")
|
||||
|
||||
@@ -190,6 +190,7 @@ def cmd_test_run(args: argparse.Namespace) -> int:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
encoding="utf-8",
|
||||
env=env,
|
||||
timeout=600, # 10 minute timeout
|
||||
)
|
||||
@@ -248,6 +249,7 @@ def cmd_test_debug(args: argparse.Namespace) -> int:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
encoding="utf-8",
|
||||
env=env,
|
||||
timeout=120, # 2 minute timeout for single test
|
||||
)
|
||||
|
||||
@@ -256,7 +256,7 @@ class AdenTUI(App):
|
||||
"""Override to use native `open` for file:// URLs on macOS."""
|
||||
if url.startswith("file://") and platform.system() == "Darwin":
|
||||
path = url.removeprefix("file://")
|
||||
subprocess.Popen(["open", path])
|
||||
subprocess.Popen(["open", path], encoding="utf-8")
|
||||
else:
|
||||
super().open_url(url, new_tab=new_tab)
|
||||
|
||||
|
||||
@@ -488,7 +488,7 @@ class ChatRepl(Vertical):
|
||||
if not state_file.exists():
|
||||
continue
|
||||
|
||||
with open(state_file) as f:
|
||||
with open(state_file, encoding="utf-8") as f:
|
||||
state = json.load(f)
|
||||
|
||||
status = state.get("status", "").lower()
|
||||
@@ -547,7 +547,7 @@ class ChatRepl(Vertical):
|
||||
|
||||
# Read session state
|
||||
try:
|
||||
with open(state_file) as f:
|
||||
with open(state_file, encoding="utf-8") as f:
|
||||
state = json.load(f)
|
||||
|
||||
# Track this session for /resume <number> lookup
|
||||
@@ -599,7 +599,7 @@ class ChatRepl(Vertical):
|
||||
try:
|
||||
import json
|
||||
|
||||
with open(state_file) as f:
|
||||
with open(state_file, encoding="utf-8") as f:
|
||||
state = json.load(f)
|
||||
|
||||
# Basic info
|
||||
@@ -640,7 +640,7 @@ class ChatRepl(Vertical):
|
||||
# Load and show checkpoints
|
||||
for i, cp_file in enumerate(checkpoint_files[-5:], 1): # Last 5
|
||||
try:
|
||||
with open(cp_file) as f:
|
||||
with open(cp_file, encoding="utf-8") as f:
|
||||
cp_data = json.load(f)
|
||||
|
||||
cp_id = cp_data.get("checkpoint_id", cp_file.stem)
|
||||
@@ -687,7 +687,7 @@ class ChatRepl(Vertical):
|
||||
|
||||
import json
|
||||
|
||||
with open(state_file) as f:
|
||||
with open(state_file, encoding="utf-8") as f:
|
||||
state = json.load(f)
|
||||
|
||||
# Resume from session state (not checkpoint)
|
||||
@@ -1102,7 +1102,7 @@ class ChatRepl(Vertical):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(state_file) as f:
|
||||
with open(state_file, encoding="utf-8") as f:
|
||||
state = json.load(f)
|
||||
|
||||
status = state.get("status", "").lower()
|
||||
|
||||
@@ -38,6 +38,7 @@ def _linux_file_dialog() -> subprocess.CompletedProcess | None:
|
||||
"--title=Select a PDF file",
|
||||
"--file-filter=PDF files (*.pdf)|*.pdf",
|
||||
],
|
||||
encoding="utf-8",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
@@ -54,6 +55,7 @@ def _linux_file_dialog() -> subprocess.CompletedProcess | None:
|
||||
".",
|
||||
"PDF files (*.pdf)",
|
||||
],
|
||||
encoding="utf-8",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
@@ -79,6 +81,7 @@ def _pick_pdf_subprocess() -> Path | None:
|
||||
'POSIX path of (choose file of type {"com.adobe.pdf"} '
|
||||
'with prompt "Select a PDF file")',
|
||||
],
|
||||
encoding="utf-8",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
@@ -93,6 +96,7 @@ def _pick_pdf_subprocess() -> Path | None:
|
||||
)
|
||||
result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", ps_script],
|
||||
encoding="utf-8",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
|
||||
@@ -199,10 +199,11 @@ def _copy_to_clipboard(text: str) -> None:
|
||||
"""Copy text to system clipboard using platform-native tools."""
|
||||
try:
|
||||
if sys.platform == "darwin":
|
||||
subprocess.run(["pbcopy"], input=text.encode(), check=True, timeout=5)
|
||||
subprocess.run(["pbcopy"], encoding="utf-8", input=text.encode(), check=True, timeout=5)
|
||||
elif sys.platform == "win32":
|
||||
subprocess.run(
|
||||
["clip.exe"],
|
||||
encoding="utf-8",
|
||||
input=text.encode("utf-16le"),
|
||||
check=True,
|
||||
timeout=5,
|
||||
@@ -211,6 +212,7 @@ def _copy_to_clipboard(text: str) -> None:
|
||||
try:
|
||||
subprocess.run(
|
||||
["xclip", "-selection", "clipboard"],
|
||||
encoding="utf-8",
|
||||
input=text.encode(),
|
||||
check=True,
|
||||
timeout=5,
|
||||
@@ -218,6 +220,7 @@ def _copy_to_clipboard(text: str) -> None:
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
subprocess.run(
|
||||
["xsel", "--clipboard", "--input"],
|
||||
encoding="utf-8",
|
||||
input=text.encode(),
|
||||
check=True,
|
||||
timeout=5,
|
||||
|
||||
+10
-3
@@ -53,7 +53,13 @@ def log_error(message: str):
|
||||
def run_command(cmd: list, error_msg: str) -> bool:
|
||||
"""Run a command and return success status."""
|
||||
try:
|
||||
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
subprocess.run(
|
||||
cmd,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(error_msg)
|
||||
@@ -97,7 +103,7 @@ def main():
|
||||
if mcp_config_path.exists():
|
||||
log_success("MCP configuration found at .mcp.json")
|
||||
logger.info("Configuration:")
|
||||
with open(mcp_config_path) as f:
|
||||
with open(mcp_config_path, encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
logger.info(json.dumps(config, indent=2))
|
||||
else:
|
||||
@@ -114,7 +120,7 @@ def main():
|
||||
}
|
||||
}
|
||||
|
||||
with open(mcp_config_path, "w") as f:
|
||||
with open(mcp_config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
log_success("Created .mcp.json")
|
||||
@@ -129,6 +135,7 @@ def main():
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
log_success("MCP server module verified")
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
||||
@@ -68,6 +68,7 @@ class TestFrameworkModule:
|
||||
[sys.executable, "-m", "framework", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
cwd=str(project_root / "core"),
|
||||
)
|
||||
assert result.returncode == 0
|
||||
@@ -79,6 +80,7 @@ class TestFrameworkModule:
|
||||
[sys.executable, "-m", "framework", "list", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
cwd=str(project_root / "core"),
|
||||
)
|
||||
assert result.returncode == 0
|
||||
@@ -104,6 +106,7 @@ class TestHiveEntryPoint:
|
||||
["hive", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "run" in result.stdout.lower()
|
||||
@@ -115,6 +118,7 @@ class TestHiveEntryPoint:
|
||||
["hive", "list", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
@@ -124,5 +128,6 @@ class TestHiveEntryPoint:
|
||||
["hive", "run", "nonexistent_agent_xyz"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert result.returncode != 0
|
||||
|
||||
@@ -232,7 +232,7 @@ async def test_shared_session_reuses_directory_and_memory(tmp_path):
|
||||
# Verify primary session's state.json exists and has the primary entry_point
|
||||
primary_state_path = tmp_path / "sessions" / primary_exec_id / "state.json"
|
||||
assert primary_state_path.exists()
|
||||
primary_state = json.loads(primary_state_path.read_text())
|
||||
primary_state = json.loads(primary_state_path.read_text(encoding="utf-8"))
|
||||
assert primary_state["entry_point"] == "primary"
|
||||
|
||||
# Async stream — simulates a webhook entry point sharing the session
|
||||
@@ -275,7 +275,7 @@ async def test_shared_session_reuses_directory_and_memory(tmp_path):
|
||||
|
||||
# State.json should NOT have been overwritten by the async execution
|
||||
# (it should still show the primary entry point)
|
||||
final_state = json.loads(primary_state_path.read_text())
|
||||
final_state = json.loads(primary_state_path.read_text(encoding="utf-8"))
|
||||
assert final_state["entry_point"] == "primary"
|
||||
|
||||
# Verify only ONE session directory exists (not two)
|
||||
|
||||
@@ -184,7 +184,7 @@ class TestPathTraversalWithActualFiles:
|
||||
|
||||
# Create a secret file outside storage
|
||||
secret_file = tmpdir_path / "secret.txt"
|
||||
secret_file.write_text("SENSITIVE_DATA")
|
||||
secret_file.write_text("SENSITIVE_DATA", encoding="utf-8")
|
||||
|
||||
storage = FileStorage(storage_dir)
|
||||
|
||||
@@ -193,7 +193,7 @@ class TestPathTraversalWithActualFiles:
|
||||
storage.get_runs_by_goal("../secret")
|
||||
|
||||
# Verify the secret file was not accessed (still contains original data)
|
||||
assert secret_file.read_text() == "SENSITIVE_DATA"
|
||||
assert secret_file.read_text(encoding="utf-8") == "SENSITIVE_DATA"
|
||||
|
||||
def test_cannot_write_outside_storage(self):
|
||||
"""Verify that we can't write files outside storage directory."""
|
||||
|
||||
@@ -353,7 +353,9 @@ class TestRuntimeLogger:
|
||||
# Verify the file exists and has one line
|
||||
jsonl_path = tmp_path / "logs" / "sessions" / run_id / "logs" / "tool_logs.jsonl"
|
||||
assert jsonl_path.exists()
|
||||
lines = [line for line in jsonl_path.read_text().strip().split("\n") if line]
|
||||
lines = [
|
||||
line for line in jsonl_path.read_text(encoding="utf-8").strip().split("\n") if line
|
||||
]
|
||||
assert len(lines) == 1
|
||||
|
||||
data = json.loads(lines[0])
|
||||
@@ -376,7 +378,8 @@ class TestRuntimeLogger:
|
||||
|
||||
jsonl_path = tmp_path / "logs" / "sessions" / run_id / "logs" / "details.jsonl"
|
||||
assert jsonl_path.exists()
|
||||
lines = [line for line in jsonl_path.read_text().strip().split("\n") if line]
|
||||
content = jsonl_path.read_text(encoding="utf-8").strip()
|
||||
lines = [line for line in content.split("\n") if line]
|
||||
assert len(lines) == 1
|
||||
|
||||
data = json.loads(lines[0])
|
||||
|
||||
@@ -98,7 +98,7 @@ class TestFileStorageRunOperations:
|
||||
assert run_file.exists()
|
||||
|
||||
# Verify it's valid JSON
|
||||
with open(run_file) as f:
|
||||
with open(run_file, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
assert data["id"] == "my_run"
|
||||
|
||||
|
||||
+14
-3
@@ -71,6 +71,7 @@ def main():
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
framework_path = result.stdout.strip()
|
||||
success(f"installed at {framework_path}")
|
||||
@@ -84,7 +85,12 @@ def main():
|
||||
missing_deps = []
|
||||
for dep in ["mcp", "fastmcp"]:
|
||||
try:
|
||||
subprocess.run([sys.executable, "-c", f"import {dep}"], capture_output=True, check=True)
|
||||
subprocess.run(
|
||||
[sys.executable, "-c", f"import {dep}"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
missing_deps.append(dep)
|
||||
|
||||
@@ -103,6 +109,7 @@ def main():
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
success("loads successfully")
|
||||
except subprocess.CalledProcessError as e:
|
||||
@@ -115,7 +122,7 @@ def main():
|
||||
mcp_config = script_dir / ".mcp.json"
|
||||
if mcp_config.exists():
|
||||
try:
|
||||
with open(mcp_config) as f:
|
||||
with open(mcp_config, encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
|
||||
if "mcpServers" in config and "agent-builder" in config["mcpServers"]:
|
||||
@@ -149,7 +156,10 @@ def main():
|
||||
for module in modules_to_check:
|
||||
try:
|
||||
subprocess.run(
|
||||
[sys.executable, "-c", f"import {module}"], capture_output=True, check=True
|
||||
[sys.executable, "-c", f"import {module}"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
failed_modules.append(module)
|
||||
@@ -174,6 +184,7 @@ def main():
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=5,
|
||||
encoding="utf-8",
|
||||
)
|
||||
if "OK" in result.stdout:
|
||||
success("server can start")
|
||||
|
||||
+19
-2
@@ -27,8 +27,22 @@ uv run python -c "import framework; import aden_tools; print('✓ Setup complete
|
||||
|
||||
## Building Your First Agent
|
||||
|
||||
Agents are not included by default in a fresh clone.
|
||||
|
||||
Agents are created using Claude Code or by manual creation in the
|
||||
exports/ directory. Until an agent exists, agent validation and run
|
||||
commands will fail.
|
||||
|
||||
### Option 1: Using Claude Code Skills (Recommended)
|
||||
|
||||
This is the recommended way to create your first agent.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- Anthropic (Claude) API access
|
||||
- Claude Code CLI installed
|
||||
- Unix-based shell (macOS, Linux, or Windows via WSL)
|
||||
|
||||
```bash
|
||||
# Setup already done via quickstart.sh above
|
||||
|
||||
@@ -120,7 +134,10 @@ hive/
|
||||
## Running an Agent
|
||||
|
||||
```bash
|
||||
# Browse and run agents interactively (Recommended)
|
||||
# Launch the web dashboard in your browser
|
||||
hive open
|
||||
|
||||
# Browse and run agents in terminal
|
||||
hive tui
|
||||
|
||||
# Run a specific agent
|
||||
@@ -164,7 +181,7 @@ PYTHONPATH=exports uv run python -m my_agent test --type success
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **TUI Dashboard**: Run `hive tui` to explore agents interactively
|
||||
1. **Dashboard**: Run `hive open` to launch the web dashboard, or `hive tui` for the terminal UI
|
||||
2. **Detailed Setup**: See [environment-setup.md](./environment-setup.md)
|
||||
3. **Developer Guide**: See [developer-guide.md](./developer-guide.md)
|
||||
4. **Build Agents**: Use `/hive` skill in Claude Code
|
||||
|
||||
+246
-56
@@ -408,6 +408,58 @@ Write-Ok "uv detected: $uvVersion"
|
||||
Write-Host ""
|
||||
|
||||
# Check for Node.js (needed for frontend dashboard)
|
||||
function Install-NodeViaFnm {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Install Node.js 20 via fnm (Fast Node Manager) - mirrors nvm approach in quickstart.sh
|
||||
#>
|
||||
$fnmCmd = Get-Command fnm -ErrorAction SilentlyContinue
|
||||
if (-not $fnmCmd) {
|
||||
$fnmDir = Join-Path $env:LOCALAPPDATA "fnm"
|
||||
$fnmExe = Join-Path $fnmDir "fnm.exe"
|
||||
if (-not (Test-Path $fnmExe)) {
|
||||
try {
|
||||
Write-Host " Downloading fnm (Fast Node Manager)..." -ForegroundColor DarkGray
|
||||
$zipUrl = "https://github.com/Schniz/fnm/releases/latest/download/fnm-windows.zip"
|
||||
$zipPath = Join-Path $env:TEMP "fnm-install.zip"
|
||||
Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing -ErrorAction Stop
|
||||
if (-not (Test-Path $fnmDir)) { New-Item -ItemType Directory -Path $fnmDir -Force | Out-Null }
|
||||
Expand-Archive -Path $zipPath -DestinationPath $fnmDir -Force
|
||||
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
Write-Fail "fnm download failed"
|
||||
Write-Host " Install Node.js 20+ manually from https://nodejs.org" -ForegroundColor DarkGray
|
||||
return $false
|
||||
}
|
||||
}
|
||||
if (Test-Path (Join-Path $fnmDir "fnm.exe")) {
|
||||
$env:PATH = "$fnmDir;$env:PATH"
|
||||
} else {
|
||||
Write-Fail "fnm binary not found after download"
|
||||
Write-Host " Install Node.js 20+ manually from https://nodejs.org" -ForegroundColor DarkGray
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$null = & fnm install 20 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "fnm install 20 exited with code $LASTEXITCODE" }
|
||||
& fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression
|
||||
$null = & fnm use 20 2>&1
|
||||
$testNode = Get-Command node -ErrorAction SilentlyContinue
|
||||
if ($testNode) {
|
||||
$ver = & node --version 2>$null
|
||||
Write-Ok "Node.js $ver installed via fnm"
|
||||
return $true
|
||||
}
|
||||
throw "node not found after fnm install"
|
||||
} catch {
|
||||
Write-Fail "Node.js installation failed"
|
||||
Write-Host " Install manually from https://nodejs.org" -ForegroundColor DarkGray
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
$NodeAvailable = $false
|
||||
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
|
||||
if ($nodeCmd) {
|
||||
@@ -419,12 +471,13 @@ if ($nodeCmd) {
|
||||
$NodeAvailable = $true
|
||||
} else {
|
||||
Write-Warn "Node.js $nodeVersion found (20+ required for frontend dashboard)"
|
||||
Write-Host " Install from https://nodejs.org" -ForegroundColor DarkGray
|
||||
Write-Host " Installing Node.js 20 via fnm..." -ForegroundColor Yellow
|
||||
$NodeAvailable = Install-NodeViaFnm
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Warn "Node.js not found (optional, needed for web dashboard)"
|
||||
Write-Host " Install from https://nodejs.org" -ForegroundColor DarkGray
|
||||
Write-Warn "Node.js not found. Installing via fnm..."
|
||||
$NodeAvailable = Install-NodeViaFnm
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
@@ -736,8 +789,8 @@ $ProviderMap = [ordered]@{
|
||||
}
|
||||
|
||||
$DefaultModels = @{
|
||||
anthropic = "claude-opus-4-6"
|
||||
openai = "gpt-5.2"
|
||||
anthropic = "claude-haiku-4-5-20251001"
|
||||
openai = "gpt-5-mini"
|
||||
gemini = "gemini-3-flash-preview"
|
||||
groq = "moonshotai/kimi-k2-instruct-0905"
|
||||
cerebras = "zai-glm-4.7"
|
||||
@@ -749,14 +802,14 @@ $DefaultModels = @{
|
||||
# Model choices: array of hashtables per provider
|
||||
$ModelChoices = @{
|
||||
anthropic = @(
|
||||
@{ Id = "claude-opus-4-6"; Label = "Opus 4.6 - Most capable (recommended)"; MaxTokens = 32768 },
|
||||
@{ Id = "claude-sonnet-4-5-20250929"; Label = "Sonnet 4.5 - Best balance"; MaxTokens = 16384 },
|
||||
@{ Id = "claude-sonnet-4-20250514"; Label = "Sonnet 4 - Fast + capable"; MaxTokens = 8192 },
|
||||
@{ Id = "claude-haiku-4-5-20251001"; Label = "Haiku 4.5 - Fast + cheap"; MaxTokens = 8192 }
|
||||
@{ Id = "claude-haiku-4-5-20251001"; Label = "Haiku 4.5 - Fast + cheap (recommended)"; MaxTokens = 8192 },
|
||||
@{ Id = "claude-sonnet-4-20250514"; Label = "Sonnet 4 - Fast + capable"; MaxTokens = 8192 },
|
||||
@{ Id = "claude-sonnet-4-5-20250929"; Label = "Sonnet 4.5 - Best balance"; MaxTokens = 16384 },
|
||||
@{ Id = "claude-opus-4-6"; Label = "Opus 4.6 - Most capable"; MaxTokens = 32768 }
|
||||
)
|
||||
openai = @(
|
||||
@{ Id = "gpt-5.2"; Label = "GPT-5.2 - Most capable (recommended)"; MaxTokens = 16384 },
|
||||
@{ Id = "gpt-5-mini"; Label = "GPT-5 Mini - Fast + cheap"; MaxTokens = 16384 }
|
||||
@{ Id = "gpt-5-mini"; Label = "GPT-5 Mini - Fast + cheap (recommended)"; MaxTokens = 16384 },
|
||||
@{ Id = "gpt-5.2"; Label = "GPT-5.2 - Most capable"; MaxTokens = 16384 }
|
||||
)
|
||||
gemini = @(
|
||||
@{ Id = "gemini-3-flash-preview"; Label = "Gemini 3 Flash - Fast (recommended)"; MaxTokens = 8192 },
|
||||
@@ -783,6 +836,17 @@ function Get-ModelSelection {
|
||||
return @{ Model = $choices[0].Id; MaxTokens = $choices[0].MaxTokens }
|
||||
}
|
||||
|
||||
# Find default index from previous model (if same provider)
|
||||
$defaultIdx = "1"
|
||||
if ($PrevModel -and $PrevProvider -eq $ProviderId) {
|
||||
for ($j = 0; $j -lt $choices.Count; $j++) {
|
||||
if ($choices[$j].Id -eq $PrevModel) {
|
||||
$defaultIdx = [string]($j + 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Color -Text "Select a model:" -Color White
|
||||
Write-Host ""
|
||||
@@ -794,8 +858,8 @@ function Get-ModelSelection {
|
||||
Write-Host ""
|
||||
|
||||
while ($true) {
|
||||
$raw = Read-Host "Enter choice [1]"
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) { $raw = "1" }
|
||||
$raw = Read-Host "Enter choice [$defaultIdx]"
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) { $raw = $defaultIdx }
|
||||
if ($raw -match '^\d+$') {
|
||||
$num = [int]$raw
|
||||
if ($num -ge 1 -and $num -le $choices.Count) {
|
||||
@@ -851,6 +915,60 @@ $ProviderMenuUrls = @(
|
||||
"https://cloud.cerebras.ai/"
|
||||
)
|
||||
|
||||
# ── Read previous configuration (if any) ──────────────────────
|
||||
$PrevProvider = ""
|
||||
$PrevModel = ""
|
||||
$PrevEnvVar = ""
|
||||
$PrevSubMode = ""
|
||||
if (Test-Path $HiveConfigFile) {
|
||||
try {
|
||||
$prevConfig = Get-Content -Path $HiveConfigFile -Raw | ConvertFrom-Json
|
||||
$prevLlm = $prevConfig.llm
|
||||
if ($prevLlm) {
|
||||
$PrevProvider = if ($prevLlm.provider) { $prevLlm.provider } else { "" }
|
||||
$PrevModel = if ($prevLlm.model) { $prevLlm.model } else { "" }
|
||||
$PrevEnvVar = if ($prevLlm.api_key_env_var) { $prevLlm.api_key_env_var } else { "" }
|
||||
if ($prevLlm.use_claude_code_subscription) { $PrevSubMode = "claude_code" }
|
||||
elseif ($prevLlm.use_codex_subscription) { $PrevSubMode = "codex" }
|
||||
elseif ($prevLlm.api_base -and $prevLlm.api_base -like "*api.z.ai*") { $PrevSubMode = "zai_code" }
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
# Compute default menu number (only if credential is still valid)
|
||||
$DefaultChoice = ""
|
||||
if ($PrevSubMode -or $PrevProvider) {
|
||||
$prevCredValid = $false
|
||||
switch ($PrevSubMode) {
|
||||
"claude_code" { if ($ClaudeCredDetected) { $prevCredValid = $true } }
|
||||
"zai_code" { if ($ZaiCredDetected) { $prevCredValid = $true } }
|
||||
"codex" { if ($CodexCredDetected) { $prevCredValid = $true } }
|
||||
default {
|
||||
if ($PrevEnvVar) {
|
||||
$envVal = [System.Environment]::GetEnvironmentVariable($PrevEnvVar, "Process")
|
||||
if (-not $envVal) { $envVal = [System.Environment]::GetEnvironmentVariable($PrevEnvVar, "User") }
|
||||
if ($envVal) { $prevCredValid = $true }
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($prevCredValid) {
|
||||
switch ($PrevSubMode) {
|
||||
"claude_code" { $DefaultChoice = "1" }
|
||||
"zai_code" { $DefaultChoice = "2" }
|
||||
"codex" { $DefaultChoice = "3" }
|
||||
}
|
||||
if (-not $DefaultChoice) {
|
||||
switch ($PrevProvider) {
|
||||
"anthropic" { $DefaultChoice = "4" }
|
||||
"openai" { $DefaultChoice = "5" }
|
||||
"gemini" { $DefaultChoice = "6" }
|
||||
"groq" { $DefaultChoice = "7" }
|
||||
"cerebras" { $DefaultChoice = "8" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Show unified provider selection menu ─────────────────────
|
||||
Write-Color -Text "Select your default LLM provider:" -Color White
|
||||
Write-Host ""
|
||||
@@ -896,8 +1014,18 @@ Write-Color -Text "9" -Color Cyan -NoNewline
|
||||
Write-Host ") Skip for now"
|
||||
Write-Host ""
|
||||
|
||||
if ($DefaultChoice) {
|
||||
Write-Color -Text " Previously configured: $PrevProvider/$PrevModel. Press Enter to keep." -Color DarkGray
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
$raw = Read-Host "Enter choice (1-9)"
|
||||
if ($DefaultChoice) {
|
||||
$raw = Read-Host "Enter choice (1-9) [$DefaultChoice]"
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) { $raw = $DefaultChoice }
|
||||
} else {
|
||||
$raw = Read-Host "Enter choice (1-9)"
|
||||
}
|
||||
if ($raw -match '^\d+$') {
|
||||
$num = [int]$raw
|
||||
if ($num -ge 1 -and $num -le 9) { break }
|
||||
@@ -974,28 +1102,68 @@ switch ($num) {
|
||||
$providerName = $ProviderMenuNames[$provIdx] -replace ' - .*', '' # strip description
|
||||
$signupUrl = $ProviderMenuUrls[$provIdx]
|
||||
|
||||
# Check if key is already set
|
||||
$existingKey = [System.Environment]::GetEnvironmentVariable($SelectedEnvVar, "User")
|
||||
if (-not $existingKey) { $existingKey = [System.Environment]::GetEnvironmentVariable($SelectedEnvVar, "Process") }
|
||||
if (-not $existingKey) {
|
||||
Write-Host ""
|
||||
Write-Host "Get your API key from: " -NoNewline
|
||||
Write-Color -Text $signupUrl -Color Cyan
|
||||
Write-Host ""
|
||||
$apiKey = Read-Host "Paste your $providerName API key (or press Enter to skip)"
|
||||
# Prompt for key (allow replacement if already set) with verification + retry
|
||||
while ($true) {
|
||||
$existingKey = [System.Environment]::GetEnvironmentVariable($SelectedEnvVar, "User")
|
||||
if (-not $existingKey) { $existingKey = [System.Environment]::GetEnvironmentVariable($SelectedEnvVar, "Process") }
|
||||
|
||||
if ($existingKey) {
|
||||
$masked = $existingKey.Substring(0, [Math]::Min(4, $existingKey.Length)) + "..." + $existingKey.Substring([Math]::Max(0, $existingKey.Length - 4))
|
||||
Write-Host ""
|
||||
Write-Color -Text " $([char]0x2B22) Current key: $masked" -Color Green
|
||||
$apiKey = Read-Host " Press Enter to keep, or paste a new key to replace"
|
||||
} else {
|
||||
Write-Host ""
|
||||
Write-Host "Get your API key from: " -NoNewline
|
||||
Write-Color -Text $signupUrl -Color Cyan
|
||||
Write-Host ""
|
||||
$apiKey = Read-Host "Paste your $providerName API key (or press Enter to skip)"
|
||||
}
|
||||
|
||||
if ($apiKey) {
|
||||
[System.Environment]::SetEnvironmentVariable($SelectedEnvVar, $apiKey, "User")
|
||||
Set-Item -Path "Env:\$SelectedEnvVar" -Value $apiKey
|
||||
Write-Host ""
|
||||
Write-Ok "API key saved as User environment variable: $SelectedEnvVar"
|
||||
Write-Color -Text " (Persisted for all future sessions)" -Color DarkGray
|
||||
} else {
|
||||
|
||||
# Health check the new key
|
||||
Write-Host " Verifying API key... " -NoNewline
|
||||
try {
|
||||
$hcResult = & uv run python (Join-Path $ScriptDir "scripts/check_llm_key.py") $SelectedProviderId $apiKey 2>$null
|
||||
$hcJson = $hcResult | ConvertFrom-Json
|
||||
if ($hcJson.valid -eq $true) {
|
||||
Write-Color -Text "ok" -Color Green
|
||||
break
|
||||
} elseif ($hcJson.valid -eq $false) {
|
||||
Write-Color -Text "failed" -Color Red
|
||||
Write-Warn $hcJson.message
|
||||
# Undo the save so user can retry cleanly
|
||||
[System.Environment]::SetEnvironmentVariable($SelectedEnvVar, $null, "User")
|
||||
Remove-Item -Path "Env:\$SelectedEnvVar" -ErrorAction SilentlyContinue
|
||||
Write-Host ""
|
||||
Read-Host " Press Enter to try again"
|
||||
# loop back to key prompt
|
||||
} else {
|
||||
Write-Color -Text "--" -Color Yellow
|
||||
Write-Color -Text " Could not verify key (network issue). The key has been saved." -Color DarkGray
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
Write-Color -Text "--" -Color Yellow
|
||||
Write-Color -Text " Could not verify key (network issue). The key has been saved." -Color DarkGray
|
||||
break
|
||||
}
|
||||
} elseif (-not $existingKey) {
|
||||
# No existing key and user skipped
|
||||
Write-Host ""
|
||||
Write-Warn "Skipped. Set the environment variable manually when ready:"
|
||||
Write-Host " [System.Environment]::SetEnvironmentVariable('$SelectedEnvVar', 'your-key', 'User')"
|
||||
$SelectedEnvVar = ""
|
||||
$SelectedProviderId = ""
|
||||
break
|
||||
} else {
|
||||
# User pressed Enter with existing key — keep it
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1011,26 +1179,67 @@ switch ($num) {
|
||||
}
|
||||
}
|
||||
|
||||
# For ZAI subscription: prompt for API key if not already set
|
||||
# For ZAI subscription: prompt for API key (allow replacement if already set) with verification + retry
|
||||
if ($SubscriptionMode -eq "zai_code") {
|
||||
$existingZai = [System.Environment]::GetEnvironmentVariable("ZAI_API_KEY", "User")
|
||||
if (-not $existingZai) { $existingZai = $env:ZAI_API_KEY }
|
||||
if (-not $existingZai) {
|
||||
Write-Host ""
|
||||
$apiKey = Read-Host "Paste your ZAI API key (or press Enter to skip)"
|
||||
while ($true) {
|
||||
$existingZai = [System.Environment]::GetEnvironmentVariable("ZAI_API_KEY", "User")
|
||||
if (-not $existingZai) { $existingZai = $env:ZAI_API_KEY }
|
||||
|
||||
if ($existingZai) {
|
||||
$masked = $existingZai.Substring(0, [Math]::Min(4, $existingZai.Length)) + "..." + $existingZai.Substring([Math]::Max(0, $existingZai.Length - 4))
|
||||
Write-Host ""
|
||||
Write-Color -Text " $([char]0x2B22) Current ZAI key: $masked" -Color Green
|
||||
$apiKey = Read-Host " Press Enter to keep, or paste a new key to replace"
|
||||
} else {
|
||||
Write-Host ""
|
||||
$apiKey = Read-Host "Paste your ZAI API key (or press Enter to skip)"
|
||||
}
|
||||
|
||||
if ($apiKey) {
|
||||
[System.Environment]::SetEnvironmentVariable("ZAI_API_KEY", $apiKey, "User")
|
||||
$env:ZAI_API_KEY = $apiKey
|
||||
Write-Host ""
|
||||
Write-Ok "ZAI API key saved as User environment variable"
|
||||
} else {
|
||||
|
||||
# Health check the new key
|
||||
Write-Host " Verifying ZAI API key... " -NoNewline
|
||||
try {
|
||||
$hcResult = & uv run python (Join-Path $ScriptDir "scripts/check_llm_key.py") "zai" $apiKey "https://api.z.ai/api/coding/paas/v4" 2>$null
|
||||
$hcJson = $hcResult | ConvertFrom-Json
|
||||
if ($hcJson.valid -eq $true) {
|
||||
Write-Color -Text "ok" -Color Green
|
||||
break
|
||||
} elseif ($hcJson.valid -eq $false) {
|
||||
Write-Color -Text "failed" -Color Red
|
||||
Write-Warn $hcJson.message
|
||||
# Undo the save so user can retry cleanly
|
||||
[System.Environment]::SetEnvironmentVariable("ZAI_API_KEY", $null, "User")
|
||||
Remove-Item -Path "Env:\ZAI_API_KEY" -ErrorAction SilentlyContinue
|
||||
Write-Host ""
|
||||
Read-Host " Press Enter to try again"
|
||||
# loop back to key prompt
|
||||
} else {
|
||||
Write-Color -Text "--" -Color Yellow
|
||||
Write-Color -Text " Could not verify key (network issue). The key has been saved." -Color DarkGray
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
Write-Color -Text "--" -Color Yellow
|
||||
Write-Color -Text " Could not verify key (network issue). The key has been saved." -Color DarkGray
|
||||
break
|
||||
}
|
||||
} elseif (-not $existingZai) {
|
||||
# No existing key and user skipped
|
||||
Write-Host ""
|
||||
Write-Warn "Skipped. Add your ZAI API key later:"
|
||||
Write-Color -Text " [System.Environment]::SetEnvironmentVariable('ZAI_API_KEY', 'your-key', 'User')" -Color Cyan
|
||||
$SelectedEnvVar = ""
|
||||
$SelectedProviderId = ""
|
||||
$SubscriptionMode = ""
|
||||
break
|
||||
} else {
|
||||
# User pressed Enter with existing key — keep it
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1081,37 +1290,18 @@ if ($SelectedProviderId) {
|
||||
Write-Host ""
|
||||
|
||||
# ============================================================
|
||||
# Step 5b: Browser Automation (GCU)
|
||||
# Step 5b: Browser Automation (GCU) — always enabled
|
||||
# ============================================================
|
||||
|
||||
Write-Host ""
|
||||
Write-Color -Text "Enable browser automation?" -Color White
|
||||
Write-Color -Text "This lets your agents control a real browser - navigate websites, fill forms," -Color DarkGray
|
||||
Write-Color -Text "scrape dynamic pages, and interact with web UIs." -Color DarkGray
|
||||
Write-Host ""
|
||||
Write-Host " " -NoNewline; Write-Color -Text "1)" -Color Cyan -NoNewline; Write-Host " Yes"
|
||||
Write-Host " " -NoNewline; Write-Color -Text "2)" -Color Cyan -NoNewline; Write-Host " No"
|
||||
Write-Host ""
|
||||
|
||||
do {
|
||||
$gcuChoice = Read-Host "Enter choice (1-2)"
|
||||
} while ($gcuChoice -ne "1" -and $gcuChoice -ne "2")
|
||||
|
||||
$GcuEnabled = $false
|
||||
if ($gcuChoice -eq "1") {
|
||||
$GcuEnabled = $true
|
||||
Write-Ok "Browser automation enabled"
|
||||
} else {
|
||||
Write-Color -Text " Browser automation skipped" -Color DarkGray
|
||||
}
|
||||
Write-Ok "Browser automation enabled"
|
||||
|
||||
# Patch gcu_enabled into configuration.json
|
||||
if (Test-Path $HiveConfigFile) {
|
||||
$existingConfig = Get-Content -Path $HiveConfigFile -Raw | ConvertFrom-Json
|
||||
$existingConfig | Add-Member -NotePropertyName "gcu_enabled" -NotePropertyValue $GcuEnabled -Force
|
||||
$existingConfig | Add-Member -NotePropertyName "gcu_enabled" -NotePropertyValue $true -Force
|
||||
$existingConfig | ConvertTo-Json -Depth 4 | Set-Content -Path $HiveConfigFile -Encoding UTF8
|
||||
} elseif ($GcuEnabled) {
|
||||
# No config file yet (user skipped LLM provider) - create minimal one
|
||||
} else {
|
||||
if (-not (Test-Path $HiveConfigDir)) {
|
||||
New-Item -ItemType Directory -Path $HiveConfigDir -Force | Out-Null
|
||||
}
|
||||
@@ -1425,7 +1615,7 @@ if ($FrontendBuilt) {
|
||||
Write-Color -Text " Starting server on http://localhost:8787" -Color DarkGray
|
||||
Write-Color -Text " Press Ctrl+C to stop" -Color DarkGray
|
||||
Write-Host ""
|
||||
& (Join-Path $ScriptDir "hive.ps1") serve --open
|
||||
& (Join-Path $ScriptDir "hive.ps1") open
|
||||
} else {
|
||||
Write-Color -Text "═══════════════════════════════════════════════════════" -Color Yellow
|
||||
Write-Host ""
|
||||
|
||||
+242
-98
@@ -407,7 +407,7 @@ if [ "$USE_ASSOC_ARRAYS" = true ]; then
|
||||
)
|
||||
|
||||
declare -A DEFAULT_MODELS=(
|
||||
["anthropic"]="claude-haiku-4-5"
|
||||
["anthropic"]="claude-haiku-4-5-20251001"
|
||||
["openai"]="gpt-5-mini"
|
||||
["gemini"]="gemini-3-flash-preview"
|
||||
["groq"]="moonshotai/kimi-k2-instruct-0905"
|
||||
@@ -420,12 +420,12 @@ if [ "$USE_ASSOC_ARRAYS" = true ]; then
|
||||
# Model choices per provider: composite-key associative arrays
|
||||
# Keys: "provider:index" -> value
|
||||
declare -A MODEL_CHOICES_ID=(
|
||||
["anthropic:0"]="claude-opus-4-6"
|
||||
["anthropic:1"]="claude-sonnet-4-5-20250929"
|
||||
["anthropic:2"]="claude-sonnet-4-20250514"
|
||||
["anthropic:3"]="claude-haiku-4-5-20251001"
|
||||
["openai:0"]="gpt-5.2"
|
||||
["openai:1"]="gpt-5-mini"
|
||||
["anthropic:0"]="claude-haiku-4-5-20251001"
|
||||
["anthropic:1"]="claude-sonnet-4-20250514"
|
||||
["anthropic:2"]="claude-sonnet-4-5-20250929"
|
||||
["anthropic:3"]="claude-opus-4-6"
|
||||
["openai:0"]="gpt-5-mini"
|
||||
["openai:1"]="gpt-5.2"
|
||||
["gemini:0"]="gemini-3-flash-preview"
|
||||
["gemini:1"]="gemini-3.1-pro-preview"
|
||||
["groq:0"]="moonshotai/kimi-k2-instruct-0905"
|
||||
@@ -435,12 +435,12 @@ if [ "$USE_ASSOC_ARRAYS" = true ]; then
|
||||
)
|
||||
|
||||
declare -A MODEL_CHOICES_LABEL=(
|
||||
["anthropic:0"]="Opus 4.6 - Most capable (recommended)"
|
||||
["anthropic:1"]="Sonnet 4.5 - Best balance"
|
||||
["anthropic:2"]="Sonnet 4 - Fast + capable"
|
||||
["anthropic:3"]="Haiku 4.5 - Fast + cheap"
|
||||
["openai:0"]="GPT-5.2 - Most capable (recommended)"
|
||||
["openai:1"]="GPT-5 Mini - Fast + cheap"
|
||||
["anthropic:0"]="Haiku 4.5 - Fast + cheap (recommended)"
|
||||
["anthropic:1"]="Sonnet 4 - Fast + capable"
|
||||
["anthropic:2"]="Sonnet 4.5 - Best balance"
|
||||
["anthropic:3"]="Opus 4.6 - Most capable"
|
||||
["openai:0"]="GPT-5 Mini - Fast + cheap (recommended)"
|
||||
["openai:1"]="GPT-5.2 - Most capable"
|
||||
["gemini:0"]="Gemini 3 Flash - Fast (recommended)"
|
||||
["gemini:1"]="Gemini 3.1 Pro - Best quality"
|
||||
["groq:0"]="Kimi K2 - Best quality (recommended)"
|
||||
@@ -450,10 +450,10 @@ if [ "$USE_ASSOC_ARRAYS" = true ]; then
|
||||
)
|
||||
|
||||
declare -A MODEL_CHOICES_MAXTOKENS=(
|
||||
["anthropic:0"]=32768
|
||||
["anthropic:1"]=16384
|
||||
["anthropic:2"]=8192
|
||||
["anthropic:3"]=8192
|
||||
["anthropic:0"]=8192
|
||||
["anthropic:1"]=8192
|
||||
["anthropic:2"]=16384
|
||||
["anthropic:3"]=32768
|
||||
["openai:0"]=16384
|
||||
["openai:1"]=16384
|
||||
["gemini:0"]=8192
|
||||
@@ -508,7 +508,7 @@ else
|
||||
|
||||
# Default models by provider id (parallel arrays)
|
||||
MODEL_PROVIDER_IDS=(anthropic openai gemini groq cerebras mistral together_ai deepseek)
|
||||
MODEL_DEFAULTS=("claude-opus-4-6" "gpt-5.2" "gemini-3-flash-preview" "moonshotai/kimi-k2-instruct-0905" "zai-glm-4.7" "mistral-large-latest" "meta-llama/Llama-3.3-70B-Instruct-Turbo" "deepseek-chat")
|
||||
MODEL_DEFAULTS=("claude-haiku-4-5-20251001" "gpt-5-mini" "gemini-3-flash-preview" "moonshotai/kimi-k2-instruct-0905" "zai-glm-4.7" "mistral-large-latest" "meta-llama/Llama-3.3-70B-Instruct-Turbo" "deepseek-chat")
|
||||
|
||||
# Helper: get provider display name for an env var
|
||||
get_provider_name() {
|
||||
@@ -552,9 +552,9 @@ else
|
||||
# Model choices per provider - flat parallel arrays with provider offsets
|
||||
# Provider order: anthropic(4), openai(2), gemini(2), groq(2), cerebras(2)
|
||||
MC_PROVIDERS=(anthropic anthropic anthropic anthropic openai openai gemini gemini groq groq cerebras cerebras)
|
||||
MC_IDS=("claude-opus-4-6" "claude-sonnet-4-5-20250929" "claude-sonnet-4-20250514" "claude-haiku-4-5-20251001" "gpt-5.2" "gpt-5-mini" "gemini-3-flash-preview" "gemini-3.1-pro-preview" "moonshotai/kimi-k2-instruct-0905" "openai/gpt-oss-120b" "zai-glm-4.7" "qwen3-235b-a22b-instruct-2507")
|
||||
MC_LABELS=("Opus 4.6 - Most capable (recommended)" "Sonnet 4.5 - Best balance" "Sonnet 4 - Fast + capable" "Haiku 4.5 - Fast + cheap" "GPT-5.2 - Most capable (recommended)" "GPT-5 Mini - Fast + cheap" "Gemini 3 Flash - Fast (recommended)" "Gemini 3.1 Pro - Best quality" "Kimi K2 - Best quality (recommended)" "GPT-OSS 120B - Fast reasoning" "ZAI-GLM 4.7 - Best quality (recommended)" "Qwen3 235B - Frontier reasoning")
|
||||
MC_MAXTOKENS=(32768 16384 8192 8192 16384 16384 8192 8192 8192 8192 8192 8192)
|
||||
MC_IDS=("claude-haiku-4-5-20251001" "claude-sonnet-4-20250514" "claude-sonnet-4-5-20250929" "claude-opus-4-6" "gpt-5-mini" "gpt-5.2" "gemini-3-flash-preview" "gemini-3.1-pro-preview" "moonshotai/kimi-k2-instruct-0905" "openai/gpt-oss-120b" "zai-glm-4.7" "qwen3-235b-a22b-instruct-2507")
|
||||
MC_LABELS=("Haiku 4.5 - Fast + cheap (recommended)" "Sonnet 4 - Fast + capable" "Sonnet 4.5 - Best balance" "Opus 4.6 - Most capable" "GPT-5 Mini - Fast + cheap (recommended)" "GPT-5.2 - Most capable" "Gemini 3 Flash - Fast (recommended)" "Gemini 3.1 Pro - Best quality" "Kimi K2 - Best quality (recommended)" "GPT-OSS 120B - Fast reasoning" "ZAI-GLM 4.7 - Best quality (recommended)" "Qwen3 235B - Frontier reasoning")
|
||||
MC_MAXTOKENS=(8192 8192 16384 32768 16384 16384 8192 8192 8192 8192 8192 8192)
|
||||
|
||||
# Helper: get number of model choices for a provider
|
||||
get_model_choice_count() {
|
||||
@@ -687,6 +687,19 @@ prompt_model_selection() {
|
||||
echo -e "${BOLD}Select a model:${NC}"
|
||||
echo ""
|
||||
|
||||
# Find default index from previous model (if same provider)
|
||||
local default_idx=""
|
||||
if [ -n "$PREV_MODEL" ] && [ "$provider_id" = "$PREV_PROVIDER" ]; then
|
||||
local j=0
|
||||
while [ $j -lt "$count" ]; do
|
||||
if [ "$(get_model_choice_id "$provider_id" "$j")" = "$PREV_MODEL" ]; then
|
||||
default_idx=$((j + 1))
|
||||
break
|
||||
fi
|
||||
j=$((j + 1))
|
||||
done
|
||||
fi
|
||||
|
||||
local i=0
|
||||
while [ $i -lt "$count" ]; do
|
||||
local label
|
||||
@@ -701,7 +714,12 @@ prompt_model_selection() {
|
||||
|
||||
local choice
|
||||
while true; do
|
||||
read -r -p "Enter choice (1-$count): " choice || true
|
||||
if [ -n "$default_idx" ]; then
|
||||
read -r -p "Enter choice (1-$count) [$default_idx]: " choice || true
|
||||
choice="${choice:-$default_idx}"
|
||||
else
|
||||
read -r -p "Enter choice (1-$count): " choice || true
|
||||
fi
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$count" ]; then
|
||||
local idx=$((choice - 1))
|
||||
SELECTED_MODEL="$(get_model_choice_id "$provider_id" "$idx")"
|
||||
@@ -781,7 +799,9 @@ SUBSCRIPTION_MODE="" # "claude_code" | "codex" | "zai_code" | ""
|
||||
|
||||
# ── Credential detection (silent — just set flags) ───────────
|
||||
CLAUDE_CRED_DETECTED=false
|
||||
if [ -f "$HOME/.claude/.credentials.json" ]; then
|
||||
if command -v security &>/dev/null && security find-generic-password -s "Claude Code-credentials" &>/dev/null 2>&1; then
|
||||
CLAUDE_CRED_DETECTED=true
|
||||
elif [ -f "$HOME/.claude/.credentials.json" ]; then
|
||||
CLAUDE_CRED_DETECTED=true
|
||||
fi
|
||||
|
||||
@@ -814,6 +834,65 @@ else
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Read previous configuration (if any) ──────────────────────
|
||||
PREV_PROVIDER=""
|
||||
PREV_MODEL=""
|
||||
PREV_ENV_VAR=""
|
||||
PREV_SUB_MODE=""
|
||||
if [ -f "$HIVE_CONFIG_FILE" ]; then
|
||||
eval "$($PYTHON_CMD -c "
|
||||
import json, sys
|
||||
try:
|
||||
with open('$HIVE_CONFIG_FILE') as f:
|
||||
c = json.load(f)
|
||||
llm = c.get('llm', {})
|
||||
print(f'PREV_PROVIDER={llm.get(\"provider\", \"\")}')
|
||||
print(f'PREV_MODEL={llm.get(\"model\", \"\")}')
|
||||
print(f'PREV_ENV_VAR={llm.get(\"api_key_env_var\", \"\")}')
|
||||
sub = ''
|
||||
if llm.get('use_claude_code_subscription'): sub = 'claude_code'
|
||||
elif llm.get('use_codex_subscription'): sub = 'codex'
|
||||
elif 'api.z.ai' in llm.get('api_base', ''): sub = 'zai_code'
|
||||
print(f'PREV_SUB_MODE={sub}')
|
||||
except Exception:
|
||||
pass
|
||||
" 2>/dev/null)" || true
|
||||
fi
|
||||
|
||||
# Compute default menu number from previous config (only if credential is still valid)
|
||||
DEFAULT_CHOICE=""
|
||||
if [ -n "$PREV_SUB_MODE" ] || [ -n "$PREV_PROVIDER" ]; then
|
||||
PREV_CRED_VALID=false
|
||||
case "$PREV_SUB_MODE" in
|
||||
claude_code) [ "$CLAUDE_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;;
|
||||
zai_code) [ "$ZAI_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;;
|
||||
codex) [ "$CODEX_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;;
|
||||
*)
|
||||
# API key provider — check if the env var is set
|
||||
if [ -n "$PREV_ENV_VAR" ] && [ -n "${!PREV_ENV_VAR}" ]; then
|
||||
PREV_CRED_VALID=true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$PREV_CRED_VALID" = true ]; then
|
||||
case "$PREV_SUB_MODE" in
|
||||
claude_code) DEFAULT_CHOICE=1 ;;
|
||||
zai_code) DEFAULT_CHOICE=2 ;;
|
||||
codex) DEFAULT_CHOICE=3 ;;
|
||||
esac
|
||||
if [ -z "$DEFAULT_CHOICE" ]; then
|
||||
case "$PREV_PROVIDER" in
|
||||
anthropic) DEFAULT_CHOICE=4 ;;
|
||||
openai) DEFAULT_CHOICE=5 ;;
|
||||
gemini) DEFAULT_CHOICE=6 ;;
|
||||
groq) DEFAULT_CHOICE=7 ;;
|
||||
cerebras) DEFAULT_CHOICE=8 ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Show unified provider selection menu ─────────────────────
|
||||
echo -e "${BOLD}Select your default LLM provider:${NC}"
|
||||
echo ""
|
||||
@@ -858,8 +937,18 @@ done
|
||||
echo -e " ${CYAN}9)${NC} Skip for now"
|
||||
echo ""
|
||||
|
||||
if [ -n "$DEFAULT_CHOICE" ]; then
|
||||
echo -e " ${DIM}Previously configured: ${PREV_PROVIDER}/${PREV_MODEL}. Press Enter to keep.${NC}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
while true; do
|
||||
read -r -p "Enter choice (1-9): " choice || true
|
||||
if [ -n "$DEFAULT_CHOICE" ]; then
|
||||
read -r -p "Enter choice (1-9) [$DEFAULT_CHOICE]: " choice || true
|
||||
choice="${choice:-$DEFAULT_CHOICE}"
|
||||
else
|
||||
read -r -p "Enter choice (1-9): " choice || true
|
||||
fi
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le 9 ]; then
|
||||
break
|
||||
fi
|
||||
@@ -968,48 +1057,132 @@ case $choice in
|
||||
;;
|
||||
esac
|
||||
|
||||
# For API-key providers: prompt for key if not already set
|
||||
if [ -z "$SUBSCRIPTION_MODE" ] && [ -n "$SELECTED_ENV_VAR" ] && [ -z "${!SELECTED_ENV_VAR}" ]; then
|
||||
echo ""
|
||||
echo -e "Get your API key from: ${CYAN}$SIGNUP_URL${NC}"
|
||||
echo ""
|
||||
read -r -p "Paste your $PROVIDER_NAME API key (or press Enter to skip): " API_KEY
|
||||
# For API-key providers: prompt for key (allow replacement if already set)
|
||||
if [ -z "$SUBSCRIPTION_MODE" ] && [ -n "$SELECTED_ENV_VAR" ]; then
|
||||
while true; do
|
||||
CURRENT_KEY="${!SELECTED_ENV_VAR}"
|
||||
if [ -n "$CURRENT_KEY" ]; then
|
||||
# Key exists — offer to keep or replace
|
||||
MASKED_KEY="${CURRENT_KEY:0:4}...${CURRENT_KEY: -4}"
|
||||
echo ""
|
||||
echo -e " ${GREEN}⬢${NC} Current key: ${DIM}$MASKED_KEY${NC}"
|
||||
read -r -p " Press Enter to keep, or paste a new key to replace: " API_KEY
|
||||
else
|
||||
# No key — prompt for one
|
||||
echo ""
|
||||
echo -e "Get your API key from: ${CYAN}$SIGNUP_URL${NC}"
|
||||
echo ""
|
||||
read -r -p "Paste your $PROVIDER_NAME API key (or press Enter to skip): " API_KEY
|
||||
fi
|
||||
|
||||
if [ -n "$API_KEY" ]; then
|
||||
echo "" >> "$SHELL_RC_FILE"
|
||||
echo "# Hive Agent Framework - $PROVIDER_NAME API key" >> "$SHELL_RC_FILE"
|
||||
echo "export $SELECTED_ENV_VAR=\"$API_KEY\"" >> "$SHELL_RC_FILE"
|
||||
export "$SELECTED_ENV_VAR=$API_KEY"
|
||||
echo ""
|
||||
echo -e "${GREEN}⬢${NC} API key saved to $SHELL_RC_FILE"
|
||||
else
|
||||
echo ""
|
||||
echo -e "${YELLOW}Skipped.${NC} Add your API key to $SHELL_RC_FILE when ready."
|
||||
SELECTED_ENV_VAR=""
|
||||
SELECTED_PROVIDER_ID=""
|
||||
fi
|
||||
if [ -n "$API_KEY" ]; then
|
||||
# Remove old export line(s) for this env var from shell rc, then append new
|
||||
sed -i.bak "/^export ${SELECTED_ENV_VAR}=/d" "$SHELL_RC_FILE" && rm -f "${SHELL_RC_FILE}.bak"
|
||||
echo "" >> "$SHELL_RC_FILE"
|
||||
echo "# Hive Agent Framework - $PROVIDER_NAME API key" >> "$SHELL_RC_FILE"
|
||||
echo "export $SELECTED_ENV_VAR=\"$API_KEY\"" >> "$SHELL_RC_FILE"
|
||||
export "$SELECTED_ENV_VAR=$API_KEY"
|
||||
echo ""
|
||||
echo -e "${GREEN}⬢${NC} API key saved to $SHELL_RC_FILE"
|
||||
# Health check the new key
|
||||
echo -n " Verifying API key... "
|
||||
HC_RESULT=$(uv run python "$SCRIPT_DIR/scripts/check_llm_key.py" "$SELECTED_PROVIDER_ID" "$API_KEY" 2>/dev/null) || true
|
||||
HC_VALID=$(echo "$HC_RESULT" | $PYTHON_CMD -c "import json,sys; print(json.loads(sys.stdin.read()).get('valid',''))" 2>/dev/null) || true
|
||||
HC_MSG=$(echo "$HC_RESULT" | $PYTHON_CMD -c "import json,sys; print(json.loads(sys.stdin.read()).get('message',''))" 2>/dev/null) || true
|
||||
if [ "$HC_VALID" = "True" ]; then
|
||||
echo -e "${GREEN}ok${NC}"
|
||||
break
|
||||
elif [ "$HC_VALID" = "False" ]; then
|
||||
echo -e "${RED}failed${NC}"
|
||||
echo -e " ${YELLOW}⚠ $HC_MSG${NC}"
|
||||
# Undo the save so the user can retry cleanly
|
||||
sed -i.bak "/^export ${SELECTED_ENV_VAR}=/d" "$SHELL_RC_FILE" && rm -f "${SHELL_RC_FILE}.bak"
|
||||
# Remove the comment line we just added
|
||||
sed -i.bak "/^# Hive Agent Framework - $PROVIDER_NAME API key$/d" "$SHELL_RC_FILE" && rm -f "${SHELL_RC_FILE}.bak"
|
||||
unset "$SELECTED_ENV_VAR"
|
||||
echo ""
|
||||
read -r -p " Press Enter to try again: " _
|
||||
# Loop back to key prompt
|
||||
else
|
||||
echo -e "${YELLOW}--${NC}"
|
||||
echo -e " ${DIM}Could not verify key (network issue). The key has been saved.${NC}"
|
||||
break
|
||||
fi
|
||||
elif [ -z "$CURRENT_KEY" ]; then
|
||||
# No existing key and user skipped — abort provider
|
||||
echo ""
|
||||
echo -e "${YELLOW}Skipped.${NC} Add your API key to $SHELL_RC_FILE when ready."
|
||||
SELECTED_ENV_VAR=""
|
||||
SELECTED_PROVIDER_ID=""
|
||||
break
|
||||
else
|
||||
# User pressed Enter with existing key — keep it, proceed normally
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# For ZAI subscription: always prompt for API key
|
||||
# For ZAI subscription: prompt for API key (allow replacement if already set)
|
||||
if [ "$SUBSCRIPTION_MODE" = "zai_code" ]; then
|
||||
echo ""
|
||||
read -r -p "Paste your ZAI API key (or press Enter to skip): " API_KEY
|
||||
while true; do
|
||||
if [ "$ZAI_CRED_DETECTED" = true ] && [ -n "$ZAI_API_KEY" ]; then
|
||||
# Key exists — offer to keep or replace
|
||||
MASKED_KEY="${ZAI_API_KEY:0:4}...${ZAI_API_KEY: -4}"
|
||||
echo ""
|
||||
echo -e " ${GREEN}⬢${NC} Current ZAI key: ${DIM}$MASKED_KEY${NC}"
|
||||
read -r -p " Press Enter to keep, or paste a new key to replace: " API_KEY
|
||||
else
|
||||
# No key — prompt for one
|
||||
echo ""
|
||||
read -r -p "Paste your ZAI API key (or press Enter to skip): " API_KEY
|
||||
fi
|
||||
|
||||
if [ -n "$API_KEY" ]; then
|
||||
echo "" >> "$SHELL_RC_FILE"
|
||||
echo "# Hive Agent Framework - ZAI Code subscription API key" >> "$SHELL_RC_FILE"
|
||||
echo "export ZAI_API_KEY=\"$API_KEY\"" >> "$SHELL_RC_FILE"
|
||||
export ZAI_API_KEY="$API_KEY"
|
||||
echo ""
|
||||
echo -e "${GREEN}⬢${NC} ZAI API key saved to $SHELL_RC_FILE"
|
||||
else
|
||||
echo ""
|
||||
echo -e "${YELLOW}Skipped.${NC} Add your ZAI API key to $SHELL_RC_FILE when ready:"
|
||||
echo -e " ${CYAN}echo 'export ZAI_API_KEY=\"your-key\"' >> $SHELL_RC_FILE${NC}"
|
||||
SELECTED_ENV_VAR=""
|
||||
SELECTED_PROVIDER_ID=""
|
||||
SUBSCRIPTION_MODE=""
|
||||
fi
|
||||
if [ -n "$API_KEY" ]; then
|
||||
sed -i.bak "/^export ZAI_API_KEY=/d" "$SHELL_RC_FILE" && rm -f "${SHELL_RC_FILE}.bak"
|
||||
echo "" >> "$SHELL_RC_FILE"
|
||||
echo "# Hive Agent Framework - ZAI Code subscription API key" >> "$SHELL_RC_FILE"
|
||||
echo "export ZAI_API_KEY=\"$API_KEY\"" >> "$SHELL_RC_FILE"
|
||||
export ZAI_API_KEY="$API_KEY"
|
||||
echo ""
|
||||
echo -e "${GREEN}⬢${NC} ZAI API key saved to $SHELL_RC_FILE"
|
||||
# Health check the new key
|
||||
echo -n " Verifying ZAI API key... "
|
||||
HC_RESULT=$(uv run python "$SCRIPT_DIR/scripts/check_llm_key.py" "zai" "$API_KEY" "https://api.z.ai/api/coding/paas/v4" 2>/dev/null) || true
|
||||
HC_VALID=$(echo "$HC_RESULT" | $PYTHON_CMD -c "import json,sys; print(json.loads(sys.stdin.read()).get('valid',''))" 2>/dev/null) || true
|
||||
HC_MSG=$(echo "$HC_RESULT" | $PYTHON_CMD -c "import json,sys; print(json.loads(sys.stdin.read()).get('message',''))" 2>/dev/null) || true
|
||||
if [ "$HC_VALID" = "True" ]; then
|
||||
echo -e "${GREEN}ok${NC}"
|
||||
break
|
||||
elif [ "$HC_VALID" = "False" ]; then
|
||||
echo -e "${RED}failed${NC}"
|
||||
echo -e " ${YELLOW}⚠ $HC_MSG${NC}"
|
||||
# Undo the save so the user can retry cleanly
|
||||
sed -i.bak "/^export ZAI_API_KEY=/d" "$SHELL_RC_FILE" && rm -f "${SHELL_RC_FILE}.bak"
|
||||
sed -i.bak "/^# Hive Agent Framework - ZAI Code subscription API key$/d" "$SHELL_RC_FILE" && rm -f "${SHELL_RC_FILE}.bak"
|
||||
unset ZAI_API_KEY
|
||||
ZAI_CRED_DETECTED=false
|
||||
echo ""
|
||||
read -r -p " Press Enter to try again: " _
|
||||
# Loop back to key prompt
|
||||
else
|
||||
echo -e "${YELLOW}--${NC}"
|
||||
echo -e " ${DIM}Could not verify key (network issue). The key has been saved.${NC}"
|
||||
break
|
||||
fi
|
||||
elif [ "$ZAI_CRED_DETECTED" = false ] || [ -z "$ZAI_API_KEY" ]; then
|
||||
# No existing key and user skipped — abort provider
|
||||
echo ""
|
||||
echo -e "${YELLOW}Skipped.${NC} Add your ZAI API key to $SHELL_RC_FILE when ready:"
|
||||
echo -e " ${CYAN}echo 'export ZAI_API_KEY=\"your-key\"' >> $SHELL_RC_FILE${NC}"
|
||||
SELECTED_ENV_VAR=""
|
||||
SELECTED_PROVIDER_ID=""
|
||||
SUBSCRIPTION_MODE=""
|
||||
break
|
||||
else
|
||||
# User pressed Enter with existing key — keep it, proceed normally
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Prompt for model if not already selected (manual provider path)
|
||||
@@ -1037,52 +1210,22 @@ fi
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# Step 4b: Browser Automation (GCU)
|
||||
# Step 4b: Browser Automation (GCU) — always enabled
|
||||
# ============================================================
|
||||
|
||||
echo -e "${BOLD}Enable browser automation?${NC}"
|
||||
echo -e "${DIM}This lets your agents control a real browser — navigate websites, fill forms,${NC}"
|
||||
echo -e "${DIM}scrape dynamic pages, and interact with web UIs.${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}${BOLD}1)${NC} ${BOLD}Yes${NC}"
|
||||
echo -e " ${CYAN}2)${NC} No"
|
||||
echo ""
|
||||
|
||||
while true; do
|
||||
read -r -p "Enter choice (1-2, default 1): " gcu_choice || true
|
||||
gcu_choice="${gcu_choice:-1}"
|
||||
if [ "$gcu_choice" = "1" ] || [ "$gcu_choice" = "2" ]; then
|
||||
break
|
||||
fi
|
||||
echo -e "${RED}Invalid choice. Please enter 1 or 2${NC}"
|
||||
done
|
||||
|
||||
if [ "$gcu_choice" = "1" ]; then
|
||||
GCU_ENABLED=true
|
||||
echo -e "${GREEN}⬢${NC} Browser automation enabled"
|
||||
else
|
||||
GCU_ENABLED=false
|
||||
echo -e "${DIM}⬡ Browser automation skipped${NC}"
|
||||
fi
|
||||
echo -e "${GREEN}⬢${NC} Browser automation enabled"
|
||||
|
||||
# Patch gcu_enabled into configuration.json
|
||||
if [ "$GCU_ENABLED" = "true" ]; then
|
||||
GCU_PY_VAL="True"
|
||||
else
|
||||
GCU_PY_VAL="False"
|
||||
fi
|
||||
|
||||
if [ -f "$HIVE_CONFIG_FILE" ]; then
|
||||
uv run python -c "
|
||||
import json
|
||||
with open('$HIVE_CONFIG_FILE') as f:
|
||||
config = json.load(f)
|
||||
config['gcu_enabled'] = $GCU_PY_VAL
|
||||
config['gcu_enabled'] = True
|
||||
with open('$HIVE_CONFIG_FILE', 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
"
|
||||
elif [ "$GCU_ENABLED" = "true" ]; then
|
||||
# No config file yet (user skipped LLM provider) — create minimal one
|
||||
else
|
||||
mkdir -p "$HIVE_CONFIG_DIR"
|
||||
uv run python -c "
|
||||
import json
|
||||
@@ -1352,9 +1495,10 @@ if [ "$FRONTEND_BUILT" = true ]; then
|
||||
echo -e " ${DIM}Starting server on http://localhost:8787${NC}"
|
||||
echo -e " ${DIM}Press Ctrl+C to stop${NC}"
|
||||
echo ""
|
||||
# exec replaces the quickstart process with hive serve
|
||||
# --open tells it to auto-open the browser once the server is ready
|
||||
exec "$SCRIPT_DIR/hive" serve --open
|
||||
echo -e " ${DIM}Tip: You can restart the dashboard anytime with:${NC} ${CYAN}hive open${NC}"
|
||||
echo ""
|
||||
# exec replaces the quickstart process with hive open
|
||||
exec "$SCRIPT_DIR/hive" open
|
||||
else
|
||||
# No frontend — show manual instructions
|
||||
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Validate an LLM API key without consuming tokens.
|
||||
|
||||
Usage:
|
||||
python scripts/check_llm_key.py <provider_id> <api_key> [api_base]
|
||||
|
||||
Exit codes:
|
||||
0 = valid key
|
||||
1 = invalid key
|
||||
2 = inconclusive (timeout, network error)
|
||||
|
||||
Output: single JSON line {"valid": bool, "message": str}
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
|
||||
TIMEOUT = 10.0
|
||||
|
||||
|
||||
def check_anthropic(api_key: str, **_: str) -> dict:
|
||||
"""Send empty messages to trigger 400 without consuming tokens."""
|
||||
with httpx.Client(timeout=TIMEOUT) as client:
|
||||
r = client.post(
|
||||
"https://api.anthropic.com/v1/messages",
|
||||
headers={
|
||||
"x-api-key": api_key,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={"model": "claude-sonnet-4-20250514", "max_tokens": 1, "messages": []},
|
||||
)
|
||||
if r.status_code in (200, 400, 429):
|
||||
return {"valid": True, "message": "API key valid"}
|
||||
if r.status_code == 401:
|
||||
return {"valid": False, "message": "Invalid API key"}
|
||||
if r.status_code == 403:
|
||||
return {"valid": False, "message": "API key lacks permissions"}
|
||||
return {"valid": False, "message": f"Unexpected status {r.status_code}"}
|
||||
|
||||
|
||||
def check_openai_compatible(api_key: str, endpoint: str, name: str) -> dict:
|
||||
"""GET /models on any OpenAI-compatible API."""
|
||||
with httpx.Client(timeout=TIMEOUT) as client:
|
||||
r = client.get(
|
||||
endpoint,
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
)
|
||||
if r.status_code in (200, 429):
|
||||
return {"valid": True, "message": f"{name} API key valid"}
|
||||
if r.status_code == 401:
|
||||
return {"valid": False, "message": f"Invalid {name} API key"}
|
||||
if r.status_code == 403:
|
||||
return {"valid": False, "message": f"{name} API key lacks permissions"}
|
||||
return {"valid": False, "message": f"{name} API returned status {r.status_code}"}
|
||||
|
||||
|
||||
def check_gemini(api_key: str, **_: str) -> dict:
|
||||
"""List models with query param auth."""
|
||||
with httpx.Client(timeout=TIMEOUT) as client:
|
||||
r = client.get(
|
||||
"https://generativelanguage.googleapis.com/v1beta/models",
|
||||
params={"key": api_key},
|
||||
)
|
||||
if r.status_code in (200, 429):
|
||||
return {"valid": True, "message": "Gemini API key valid"}
|
||||
if r.status_code in (400, 401, 403):
|
||||
return {"valid": False, "message": "Invalid Gemini API key"}
|
||||
return {"valid": False, "message": f"Gemini API returned status {r.status_code}"}
|
||||
|
||||
|
||||
PROVIDERS = {
|
||||
"anthropic": lambda key, **kw: check_anthropic(key),
|
||||
"openai": lambda key, **kw: check_openai_compatible(
|
||||
key, "https://api.openai.com/v1/models", "OpenAI"
|
||||
),
|
||||
"gemini": lambda key, **kw: check_gemini(key),
|
||||
"groq": lambda key, **kw: check_openai_compatible(
|
||||
key, "https://api.groq.com/openai/v1/models", "Groq"
|
||||
),
|
||||
"cerebras": lambda key, **kw: check_openai_compatible(
|
||||
key, "https://api.cerebras.ai/v1/models", "Cerebras"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 3:
|
||||
print(json.dumps({"valid": False, "message": "Usage: check_llm_key.py <provider> <key> [api_base]"}))
|
||||
sys.exit(2)
|
||||
|
||||
provider_id = sys.argv[1]
|
||||
api_key = sys.argv[2]
|
||||
api_base = sys.argv[3] if len(sys.argv) > 3 else ""
|
||||
|
||||
try:
|
||||
if api_base:
|
||||
# Custom API base (ZAI or other OpenAI-compatible)
|
||||
endpoint = api_base.rstrip("/") + "/models"
|
||||
result = check_openai_compatible(api_key, endpoint, "ZAI")
|
||||
elif provider_id in PROVIDERS:
|
||||
result = PROVIDERS[provider_id](api_key)
|
||||
else:
|
||||
result = {"valid": True, "message": f"No health check for {provider_id}"}
|
||||
print(json.dumps(result))
|
||||
sys.exit(0)
|
||||
|
||||
print(json.dumps(result))
|
||||
sys.exit(0 if result["valid"] else 1)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
print(json.dumps({"valid": None, "message": "Request timed out"}))
|
||||
sys.exit(2)
|
||||
except httpx.RequestError as e:
|
||||
msg = str(e)
|
||||
# Redact key from error messages
|
||||
if api_key in msg:
|
||||
msg = msg.replace(api_key, "***")
|
||||
print(json.dumps({"valid": None, "message": f"Connection failed: {msg}"}))
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -20,6 +20,7 @@ def test_check_requirements():
|
||||
[sys.executable, "scripts/check_requirements.py", "json", "sys", "os"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"Exit code: {result.returncode}")
|
||||
print(f"Output:\n{result.stdout}")
|
||||
@@ -39,6 +40,7 @@ def test_check_requirements():
|
||||
[sys.executable, "scripts/check_requirements.py", "json", "nonexistent_module"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"Exit code: {result.returncode}")
|
||||
print(f"Output:\n{result.stdout}")
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# MSSQL Connection Configuration Template
|
||||
#
|
||||
# Copy this file to .env and fill in your actual values
|
||||
# DO NOT commit the .env file to version control!
|
||||
|
||||
# ============================================================================
|
||||
# SQL Server Connection - Choose ONE format below:
|
||||
# ============================================================================
|
||||
|
||||
# OPTION 1: Local named instance
|
||||
MSSQL_SERVER=localhost\SQLEXPRESS
|
||||
|
||||
# OPTION 2: Local default instance
|
||||
# MSSQL_SERVER=localhost
|
||||
|
||||
# OPTION 3: Remote server with default port (1433)
|
||||
# MSSQL_SERVER=192.168.1.100
|
||||
|
||||
# OPTION 4: Remote server with custom port (comma-separated)
|
||||
# MSSQL_SERVER=192.168.1.100,1433
|
||||
|
||||
# OPTION 5: Remote named instance
|
||||
# MSSQL_SERVER=PRODUCTION-SERVER\INSTANCE01
|
||||
|
||||
# OPTION 6: Domain server name
|
||||
# MSSQL_SERVER=sql-prod.company.com
|
||||
|
||||
# OPTION 7: Domain server with port
|
||||
# MSSQL_SERVER=sql-prod.company.com,1433
|
||||
|
||||
# ============================================================================
|
||||
# Database Configuration
|
||||
# ============================================================================
|
||||
MSSQL_DATABASE=AdenTestDB
|
||||
|
||||
# ============================================================================
|
||||
# Authentication - Choose ONE method:
|
||||
# ============================================================================
|
||||
|
||||
# METHOD 1: SQL Server Authentication (username/password)
|
||||
# Use this for: remote servers, Linux servers, specific SQL logins
|
||||
MSSQL_USERNAME=sa
|
||||
MSSQL_PASSWORD=your_password_here
|
||||
|
||||
# METHOD 2: Windows Authentication (leave both empty)
|
||||
# Use this for: local Windows servers, domain-joined environments
|
||||
# MSSQL_USERNAME=
|
||||
# MSSQL_PASSWORD=
|
||||
|
||||
# ============================================================================
|
||||
# Important Notes:
|
||||
# ============================================================================
|
||||
# - Port format: Use comma (,) not colon - Example: server,1433
|
||||
# - Named instances: Use backslash (\) - Example: SERVER\INSTANCE
|
||||
# - Default port: 1433 (can be omitted if using default)
|
||||
# - ODBC Driver: Requires "ODBC Driver 17 for SQL Server" or newer
|
||||
# - Security: Never commit this file with real credentials!
|
||||
# - Escaping: In some shells, escape backslashes (\\) when setting env vars
|
||||
# ============================================================================
|
||||
|
||||
# Example Production Configurations:
|
||||
# -----------------------------------
|
||||
# Azure SQL: MSSQL_SERVER=yourserver.database.windows.net
|
||||
# AWS RDS: MSSQL_SERVER=yourinstance.region.rds.amazonaws.com,1433
|
||||
# Docker: MSSQL_SERVER=localhost,1401
|
||||
# Kubernetes: MSSQL_SERVER=mssql-service.namespace.svc.cluster.local,1433
|
||||
@@ -90,7 +90,13 @@ def _resolve_path(path: str) -> str:
|
||||
def _snapshot_git(*args: str) -> str:
|
||||
"""Run a git command with the snapshot GIT_DIR and PROJECT_ROOT worktree."""
|
||||
cmd = ["git", "--git-dir", SNAPSHOT_DIR, "--work-tree", PROJECT_ROOT, *args]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
@@ -104,6 +110,7 @@ def _ensure_snapshot_repo():
|
||||
["git", "init", "--bare", SNAPSHOT_DIR],
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
encoding="utf-8",
|
||||
)
|
||||
_snapshot_git("config", "core.autocrlf", "false")
|
||||
|
||||
@@ -152,6 +159,7 @@ def run_command(command: str, cwd: str = "", timeout: int = 120) -> str:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
encoding="utf-8",
|
||||
env={
|
||||
**os.environ,
|
||||
"PYTHONPATH": (
|
||||
@@ -228,6 +236,7 @@ def undo_changes(path: str = "") -> str:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return f"Restored: {path}"
|
||||
else:
|
||||
@@ -1021,6 +1030,7 @@ def run_agent_tests(
|
||||
text=True,
|
||||
timeout=120,
|
||||
env=env,
|
||||
encoding="utf-8",
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return json.dumps(
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Database Initialization Script Runner for AdenTestDB
|
||||
|
||||
This script executes the SQL initialization file to create the AdenTestDB database.
|
||||
Make sure your SQL Server is running before executing this script.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pyodbc
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env
|
||||
load_dotenv()
|
||||
|
||||
# Database connection settings (from environment variables)
|
||||
SERVER = os.getenv("MSSQL_SERVER", r"MONSTER\MSSQLSERVERR")
|
||||
USERNAME = os.getenv("MSSQL_USERNAME")
|
||||
PASSWORD = os.getenv("MSSQL_PASSWORD")
|
||||
|
||||
# SQL file path
|
||||
SQL_FILE = os.path.join(os.path.dirname(__file__), "init_aden_testdb.sql")
|
||||
|
||||
|
||||
def execute_sql_file():
|
||||
"""Execute the SQL initialization file."""
|
||||
connection = None
|
||||
|
||||
try:
|
||||
# Read SQL file
|
||||
if not os.path.exists(SQL_FILE):
|
||||
print(f"[ERROR] SQL file not found: {SQL_FILE}")
|
||||
return False
|
||||
|
||||
with open(SQL_FILE, encoding="utf-8") as f:
|
||||
sql_script = f.read()
|
||||
|
||||
print("=" * 70)
|
||||
print("AdenTestDB Database Initialization")
|
||||
print("=" * 70)
|
||||
print(f"Server: {SERVER}")
|
||||
print(f"SQL Script: {SQL_FILE}")
|
||||
print()
|
||||
|
||||
# Connect to master database (to create new database)
|
||||
connection_string = (
|
||||
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
||||
f"SERVER={SERVER};"
|
||||
f"DATABASE=master;"
|
||||
f"UID={USERNAME};"
|
||||
f"PWD={PASSWORD};"
|
||||
)
|
||||
|
||||
print("Connecting to SQL Server...")
|
||||
connection = pyodbc.connect(connection_string)
|
||||
connection.autocommit = True # Required for CREATE DATABASE
|
||||
cursor = connection.cursor()
|
||||
|
||||
print("[OK] Connected successfully!")
|
||||
print()
|
||||
print("Executing SQL script...")
|
||||
print("-" * 70)
|
||||
|
||||
# Split by GO statements and execute each batch
|
||||
batches = sql_script.split("\nGO\n")
|
||||
|
||||
for i, batch in enumerate(batches, 1):
|
||||
batch = batch.strip()
|
||||
if batch and not batch.startswith("--"):
|
||||
try:
|
||||
cursor.execute(batch)
|
||||
# Print any messages from the server
|
||||
while cursor.nextset():
|
||||
pass
|
||||
except pyodbc.Error as e:
|
||||
# Some statements might not return results, that's OK
|
||||
if "No results" not in str(e):
|
||||
print(f"Warning in batch {i}: {str(e)}")
|
||||
|
||||
print("-" * 70)
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("[SUCCESS] Database initialization completed successfully!")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print("Next steps:")
|
||||
print("1. Run: python test_mssql_connection.py")
|
||||
print("2. Verify the relational schema and sample data")
|
||||
print()
|
||||
|
||||
return True
|
||||
|
||||
except pyodbc.Error as e:
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("[ERROR] Database initialization failed!")
|
||||
print("=" * 70)
|
||||
print(f"Error detail: {str(e)}")
|
||||
print()
|
||||
print("Possible solutions:")
|
||||
print("1. Ensure SQL Server is running")
|
||||
print("2. Check server name, username, and password")
|
||||
print("3. Ensure you have permission to create databases")
|
||||
print("4. Verify ODBC Driver 17 for SQL Server is installed")
|
||||
print()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[ERROR] Unexpected error: {str(e)}")
|
||||
return False
|
||||
|
||||
finally:
|
||||
if connection:
|
||||
connection.close()
|
||||
print("Connection closed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = execute_sql_file()
|
||||
exit(0 if success else 1)
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Grant Permissions to AdenTestDB
|
||||
|
||||
This script grants the necessary permissions to the 'sa' user to access AdenTE testDB.
|
||||
"""
|
||||
|
||||
import pyodbc
|
||||
|
||||
SERVER = r"MONSTER\MSSQLSERVERR"
|
||||
USERNAME = "sa"
|
||||
PASSWORD = "622622aA."
|
||||
|
||||
|
||||
def grant_permissions():
|
||||
"""Grant permissions to the database."""
|
||||
connection = None
|
||||
|
||||
try:
|
||||
# Connect to AdenTestDB
|
||||
connection_string = (
|
||||
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
||||
f"SERVER={SERVER};"
|
||||
f"DATABASE=AdenTestDB;"
|
||||
f"UID={USERNAME};"
|
||||
f"PWD={PASSWORD};"
|
||||
f"TrustServerCertificate=yes;"
|
||||
)
|
||||
|
||||
print("=" * 70)
|
||||
print("Granting Permissions to AdenTestDB")
|
||||
print("=" * 70)
|
||||
print(f"Server: {SERVER}")
|
||||
print()
|
||||
|
||||
print("Connecting to database...")
|
||||
connection = pyodbc.connect(connection_string)
|
||||
cursor = connection.cursor()
|
||||
|
||||
print("[OK] Connected successfully!")
|
||||
print()
|
||||
|
||||
# Grant permissions
|
||||
print("Granting permissions...")
|
||||
|
||||
try:
|
||||
cursor.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO sa")
|
||||
print("[OK] Granted schema permissions to sa")
|
||||
except pyodbc.Error as e:
|
||||
print(f"Note: {str(e)}")
|
||||
|
||||
connection.commit()
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("[SUCCESS] Permissions granted!")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print("You can now run: python test_mssql_connection.py")
|
||||
|
||||
return True
|
||||
|
||||
except pyodbc.Error:
|
||||
# If we can't connect, try connecting to master and creating user
|
||||
try:
|
||||
connection_string = (
|
||||
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
||||
f"SERVER={SERVER};"
|
||||
f"DATABASE=master;"
|
||||
f"UID={USERNAME};"
|
||||
f"PWD={PASSWORD};"
|
||||
f"TrustServerCertificate=yes;"
|
||||
)
|
||||
|
||||
print("Attempting to grant permissions via master database...")
|
||||
connection = pyodbc.connect(connection_string)
|
||||
cursor = connection.cursor()
|
||||
|
||||
# Create login if not exists
|
||||
try:
|
||||
cursor.execute(f"""
|
||||
IF NOT EXISTS (SELECT * FROM sys.server_principals WHERE name = 'sa')
|
||||
BEGIN
|
||||
CREATE LOGIN sa WITH PASSWORD = '{PASSWORD}'
|
||||
END
|
||||
""")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Switch to AdenTestDB and grant permissions
|
||||
cursor.execute("USE AdenTestDB")
|
||||
|
||||
# Create user if not exists
|
||||
try:
|
||||
cursor.execute("""
|
||||
IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = 'sa')
|
||||
BEGIN
|
||||
CREATE USER sa FOR LOGIN sa
|
||||
END
|
||||
""")
|
||||
print("[OK] Created database user")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Grant permissions
|
||||
cursor.execute("ALTER ROLE db_datareader ADD MEMBER sa")
|
||||
cursor.execute("ALTER ROLE db_datawriter ADD MEMBER sa")
|
||||
|
||||
connection.commit()
|
||||
|
||||
print("[OK] Permissions granted successfully!")
|
||||
return True
|
||||
|
||||
except Exception as inner_e:
|
||||
print("\n[ERROR] Could not grant permissions!")
|
||||
print(f"Error: {str(inner_e)}")
|
||||
print()
|
||||
print("The database was created successfully, but there's a permission issue.")
|
||||
print("Please run this SQL command in SQL Server Management Studio:")
|
||||
print()
|
||||
print("USE AdenTestDB;")
|
||||
print("GO")
|
||||
print("ALTER ROLE db_datareader ADD MEMBER sa;")
|
||||
print("ALTER ROLE db_datawriter ADD MEMBER sa;")
|
||||
print("GO")
|
||||
return False
|
||||
|
||||
finally:
|
||||
if connection:
|
||||
connection.close()
|
||||
print("\nConnection closed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
grant_permissions()
|
||||
@@ -0,0 +1,183 @@
|
||||
-- ============================================================================
|
||||
-- AdenTestDB Database Initialization Script
|
||||
-- ============================================================================
|
||||
-- Purpose: Create a professional testing database for Aden Hive MSSQL tool
|
||||
-- Author: Database Architect
|
||||
-- Date: 2026-02-08
|
||||
-- ============================================================================
|
||||
|
||||
USE master;
|
||||
GO
|
||||
|
||||
-- Drop database if exists (for clean recreation)
|
||||
IF EXISTS (SELECT name FROM sys.databases WHERE name = N'AdenTestDB')
|
||||
BEGIN
|
||||
ALTER DATABASE AdenTestDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
||||
DROP DATABASE AdenTestDB;
|
||||
PRINT 'Existing AdenTestDB dropped successfully.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- Create new database
|
||||
CREATE DATABASE AdenTestDB;
|
||||
GO
|
||||
|
||||
PRINT 'AdenTestDB created successfully.';
|
||||
GO
|
||||
|
||||
USE AdenTestDB;
|
||||
GO
|
||||
|
||||
-- ============================================================================
|
||||
-- TABLE: Departments
|
||||
-- ============================================================================
|
||||
-- Purpose: Store department information with budget tracking
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE Departments (
|
||||
department_id INT IDENTITY(1,1) NOT NULL,
|
||||
name NVARCHAR(100) NOT NULL,
|
||||
budget DECIMAL(15,2) NOT NULL,
|
||||
created_date DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
|
||||
CONSTRAINT PK_Departments PRIMARY KEY (department_id),
|
||||
CONSTRAINT UK_Departments_Name UNIQUE (name),
|
||||
CONSTRAINT CK_Departments_Budget CHECK (budget >= 0)
|
||||
);
|
||||
GO
|
||||
|
||||
-- Create index for performance optimization
|
||||
CREATE INDEX IX_Departments_Name ON Departments(name);
|
||||
GO
|
||||
|
||||
PRINT 'Departments table created successfully.';
|
||||
GO
|
||||
|
||||
-- ============================================================================
|
||||
-- TABLE: Employees
|
||||
-- ============================================================================
|
||||
-- Purpose: Store employee information with department association
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE Employees (
|
||||
employee_id INT IDENTITY(1000,1) NOT NULL,
|
||||
first_name NVARCHAR(50) NOT NULL,
|
||||
last_name NVARCHAR(50) NOT NULL,
|
||||
email NVARCHAR(100) NOT NULL,
|
||||
salary DECIMAL(12,2) NOT NULL,
|
||||
hire_date DATETIME NOT NULL,
|
||||
department_id INT NOT NULL,
|
||||
|
||||
CONSTRAINT PK_Employees PRIMARY KEY (employee_id),
|
||||
CONSTRAINT UK_Employees_Email UNIQUE (email),
|
||||
CONSTRAINT CK_Employees_Salary CHECK (salary >= 0),
|
||||
CONSTRAINT FK_Employees_Departments
|
||||
FOREIGN KEY (department_id) REFERENCES Departments(department_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
GO
|
||||
|
||||
-- Create indexes for performance optimization
|
||||
CREATE INDEX IX_Employees_DepartmentId ON Employees(department_id);
|
||||
CREATE INDEX IX_Employees_LastName ON Employees(last_name);
|
||||
CREATE INDEX IX_Employees_Email ON Employees(email);
|
||||
GO
|
||||
|
||||
PRINT 'Employees table created successfully.';
|
||||
GO
|
||||
|
||||
-- ============================================================================
|
||||
-- SAMPLE DATA: Departments
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO Departments (name, budget, created_date) VALUES
|
||||
('Engineering', 2500000.00, '2023-01-15'),
|
||||
('Human Resources', 800000.00, '2023-01-15'),
|
||||
('Sales', 1500000.00, '2023-01-20'),
|
||||
('Marketing', 1200000.00, '2023-02-01'),
|
||||
('Finance', 1000000.00, '2023-02-10');
|
||||
GO
|
||||
|
||||
PRINT 'Sample departments inserted successfully.';
|
||||
GO
|
||||
|
||||
-- ============================================================================
|
||||
-- SAMPLE DATA: Employees
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO Employees (first_name, last_name, email, salary, hire_date, department_id) VALUES
|
||||
-- Engineering Department (ID: 1)
|
||||
('John', 'Smith', 'john.smith@adenhive.com', 120000.00, '2023-03-01', 1),
|
||||
('Sarah', 'Johnson', 'sarah.johnson@adenhive.com', 115000.00, '2023-03-15', 1),
|
||||
('Michael', 'Chen', 'michael.chen@adenhive.com', 125000.00, '2023-04-01', 1),
|
||||
('Emily', 'Rodriguez', 'emily.rodriguez@adenhive.com', 110000.00, '2023-05-10', 1),
|
||||
('David', 'Kim', 'david.kim@adenhive.com', 105000.00, '2024-01-15', 1),
|
||||
|
||||
-- Human Resources Department (ID: 2)
|
||||
('Lisa', 'Anderson', 'lisa.anderson@adenhive.com', 85000.00, '2023-02-20', 2),
|
||||
('James', 'Wilson', 'james.wilson@adenhive.com', 80000.00, '2023-06-01', 2),
|
||||
|
||||
-- Sales Department (ID: 3)
|
||||
('Jennifer', 'Taylor', 'jennifer.taylor@adenhive.com', 95000.00, '2023-04-15', 3),
|
||||
('Robert', 'Martinez', 'robert.martinez@adenhive.com', 90000.00, '2023-05-01', 3),
|
||||
('Amanda', 'Garcia', 'amanda.garcia@adenhive.com', 92000.00, '2023-07-20', 3),
|
||||
|
||||
-- Marketing Department (ID: 4)
|
||||
('Christopher', 'Lee', 'christopher.lee@adenhive.com', 88000.00, '2023-03-10', 4),
|
||||
('Michelle', 'White', 'michelle.white@adenhive.com', 86000.00, '2023-08-01', 4),
|
||||
('Kevin', 'Brown', 'kevin.brown@adenhive.com', 84000.00, '2024-02-01', 4),
|
||||
|
||||
-- Finance Department (ID: 5)
|
||||
('Jessica', 'Davis', 'jessica.davis@adenhive.com', 98000.00, '2023-02-15', 5),
|
||||
('Daniel', 'Miller', 'daniel.miller@adenhive.com', 95000.00, '2023-09-01', 5);
|
||||
GO
|
||||
|
||||
PRINT 'Sample employees inserted successfully.';
|
||||
GO
|
||||
|
||||
-- ============================================================================
|
||||
-- VERIFICATION QUERIES
|
||||
-- ============================================================================
|
||||
|
||||
PRINT '';
|
||||
PRINT '============================================================';
|
||||
PRINT 'Database Setup Summary';
|
||||
PRINT '============================================================';
|
||||
|
||||
-- Count departments
|
||||
DECLARE @DeptCount INT;
|
||||
SELECT @DeptCount = COUNT(*) FROM Departments;
|
||||
PRINT 'Total Departments: ' + CAST(@DeptCount AS NVARCHAR(10));
|
||||
|
||||
-- Count employees
|
||||
DECLARE @EmpCount INT;
|
||||
SELECT @EmpCount = COUNT(*) FROM Employees;
|
||||
PRINT 'Total Employees: ' + CAST(@EmpCount AS NVARCHAR(10));
|
||||
|
||||
-- Show department summary
|
||||
PRINT '';
|
||||
PRINT 'Department Summary:';
|
||||
PRINT '------------------------------------------------------------';
|
||||
SELECT
|
||||
d.name AS Department,
|
||||
COUNT(e.employee_id) AS Employees,
|
||||
d.budget AS Budget,
|
||||
FORMAT(d.budget / NULLIF(COUNT(e.employee_id), 0), 'C', 'en-US') AS BudgetPerEmployee
|
||||
FROM Departments d
|
||||
LEFT JOIN Employees e ON d.department_id = e.department_id
|
||||
GROUP BY d.name, d.budget
|
||||
ORDER BY d.name;
|
||||
GO
|
||||
|
||||
PRINT '';
|
||||
PRINT '============================================================';
|
||||
PRINT 'AdenTestDB initialization completed successfully!';
|
||||
PRINT '============================================================';
|
||||
PRINT '';
|
||||
PRINT 'Next Steps:';
|
||||
PRINT '1. Run: python test_mssql_connection.py';
|
||||
PRINT '2. Verify JOIN queries work correctly';
|
||||
PRINT '3. Test relational integrity';
|
||||
PRINT '============================================================';
|
||||
GO
|
||||
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
Payroll Analysis Tool
|
||||
Analyzes total payroll costs by department and identifies highest-paid employee
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pyodbc
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Force UTF-8 encoding for console output
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Database connection settings (from environment variables)
|
||||
SERVER = os.getenv("MSSQL_SERVER", r"MONSTER\MSSQLSERVERR")
|
||||
DATABASE = os.getenv("MSSQL_DATABASE", "AdenTestDB")
|
||||
USERNAME = os.getenv("MSSQL_USERNAME")
|
||||
PASSWORD = os.getenv("MSSQL_PASSWORD")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main analysis function."""
|
||||
connection = None
|
||||
|
||||
try:
|
||||
print("=" * 80)
|
||||
print(" COMPANY PAYROLL ANALYSIS")
|
||||
print("=" * 80)
|
||||
print(f"Server: {SERVER}")
|
||||
print(f"Database: {DATABASE}")
|
||||
print()
|
||||
|
||||
# Connect to database
|
||||
if USERNAME and PASSWORD:
|
||||
# SQL Server Authentication
|
||||
connection_string = (
|
||||
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
||||
f"SERVER={SERVER};"
|
||||
f"DATABASE={DATABASE};"
|
||||
f"UID={USERNAME};"
|
||||
f"PWD={PASSWORD};"
|
||||
)
|
||||
else:
|
||||
# Windows Authentication
|
||||
connection_string = (
|
||||
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
||||
f"SERVER={SERVER};"
|
||||
f"DATABASE={DATABASE};"
|
||||
f"Trusted_Connection=yes;"
|
||||
)
|
||||
|
||||
print("Connecting to database...")
|
||||
connection = pyodbc.connect(connection_string)
|
||||
cursor = connection.cursor()
|
||||
print("✓ Connection successful!")
|
||||
print()
|
||||
|
||||
# Analysis 1: Total Payroll by Department
|
||||
print("=" * 80)
|
||||
print(" TOTAL SALARY COSTS BY DEPARTMENT")
|
||||
print("=" * 80)
|
||||
|
||||
payroll_query = """
|
||||
SELECT
|
||||
d.name AS department_name,
|
||||
COUNT(e.employee_id) AS employee_count,
|
||||
SUM(e.salary) AS total_salary_cost,
|
||||
AVG(e.salary) AS avg_salary
|
||||
FROM Departments d
|
||||
LEFT JOIN Employees e ON d.department_id = e.department_id
|
||||
GROUP BY d.name
|
||||
ORDER BY total_salary_cost DESC
|
||||
"""
|
||||
|
||||
cursor.execute(payroll_query)
|
||||
|
||||
print(
|
||||
f"\n{'Department':<25} {'Employees':<12} {'Total Salary Cost':<20} {'Avg Salary':<15}"
|
||||
)
|
||||
print("-" * 80)
|
||||
|
||||
total_company_payroll = 0
|
||||
total_employees = 0
|
||||
|
||||
for row in cursor:
|
||||
dept_name = row[0]
|
||||
emp_count = row[1]
|
||||
total_salary = row[2] if row[2] else 0
|
||||
avg_salary = row[3] if row[3] else 0
|
||||
|
||||
total_company_payroll += total_salary
|
||||
total_employees += emp_count
|
||||
|
||||
total_salary_str = f"${total_salary:,.2f}"
|
||||
avg_salary_str = f"${avg_salary:,.2f}" if avg_salary > 0 else "N/A"
|
||||
|
||||
print(f"{dept_name:<25} {emp_count:<12} {total_salary_str:<20} {avg_salary_str:<15}")
|
||||
|
||||
print("-" * 80)
|
||||
print(f"{'TOTAL COMPANY':<25} {total_employees:<12} ${total_company_payroll:,.2f}")
|
||||
print("-" * 80)
|
||||
print()
|
||||
|
||||
# Analysis 2: Highest Paid Employee
|
||||
print("=" * 80)
|
||||
print(" HIGHEST PAID EMPLOYEE")
|
||||
print("=" * 80)
|
||||
|
||||
highest_paid_query = """
|
||||
SELECT TOP 1
|
||||
e.employee_id,
|
||||
e.first_name + ' ' + e.last_name AS full_name,
|
||||
e.email,
|
||||
e.salary,
|
||||
d.name AS department_name
|
||||
FROM Employees e
|
||||
INNER JOIN Departments d ON e.department_id = d.department_id
|
||||
ORDER BY e.salary DESC
|
||||
"""
|
||||
|
||||
cursor.execute(highest_paid_query)
|
||||
top_employee = cursor.fetchone()
|
||||
|
||||
if top_employee:
|
||||
print(f"\n{'Field':<20} {'Value':<50}")
|
||||
print("-" * 80)
|
||||
print(f"{'Employee ID':<20} {top_employee[0]}")
|
||||
print(f"{'Name':<20} {top_employee[1]}")
|
||||
print(f"{'Email':<20} {top_employee[2]}")
|
||||
print(f"{'Department':<20} {top_employee[4]}")
|
||||
print(f"{'Salary':<20} ${top_employee[3]:,.2f}")
|
||||
print("-" * 80)
|
||||
else:
|
||||
print("\nNo employees found in the database.")
|
||||
|
||||
print()
|
||||
|
||||
# Additional Analysis: Top 5 Highest Paid Employees
|
||||
print("=" * 80)
|
||||
print(" TOP 5 HIGHEST PAID EMPLOYEES")
|
||||
print("=" * 80)
|
||||
|
||||
top_5_query = """
|
||||
SELECT TOP 5
|
||||
e.first_name + ' ' + e.last_name AS full_name,
|
||||
d.name AS department_name,
|
||||
e.salary
|
||||
FROM Employees e
|
||||
INNER JOIN Departments d ON e.department_id = d.department_id
|
||||
ORDER BY e.salary DESC
|
||||
"""
|
||||
|
||||
cursor.execute(top_5_query)
|
||||
|
||||
print(f"\n{'Rank':<6} {'Name':<30} {'Department':<25} {'Salary':<15}")
|
||||
print("-" * 80)
|
||||
|
||||
rank = 1
|
||||
for row in cursor:
|
||||
full_name = row[0]
|
||||
dept_name = row[1]
|
||||
salary = row[2]
|
||||
|
||||
print(f"{rank:<6} {full_name:<30} {dept_name:<25} ${salary:,.2f}")
|
||||
rank += 1
|
||||
|
||||
print("-" * 80)
|
||||
print()
|
||||
|
||||
# Summary
|
||||
print("=" * 80)
|
||||
print(" ANALYSIS SUMMARY")
|
||||
print("=" * 80)
|
||||
print(f"✓ Total Employees: {total_employees}")
|
||||
print(f"✓ Total Company Payroll: ${total_company_payroll:,.2f}")
|
||||
print(
|
||||
f"✓ Average Employee Salary: ${total_company_payroll / total_employees:,.2f}"
|
||||
if total_employees > 0
|
||||
else "N/A"
|
||||
)
|
||||
print("=" * 80)
|
||||
print("\nPayroll analysis completed successfully!")
|
||||
|
||||
except pyodbc.Error as e:
|
||||
print("\n[ERROR] Database operation failed!")
|
||||
print(f"Error detail: {str(e)}")
|
||||
print()
|
||||
print("Possible solutions:")
|
||||
print("1. Ensure SQL Server is running")
|
||||
print("2. Verify database access permissions")
|
||||
print("3. Check connection string configuration")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[ERROR] Unexpected error: {str(e)}")
|
||||
|
||||
finally:
|
||||
if connection:
|
||||
connection.close()
|
||||
print("\nConnection closed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -31,6 +31,7 @@ dependencies = [
|
||||
"litellm>=1.81.0",
|
||||
"dnspython>=2.4.0",
|
||||
"resend>=2.0.0",
|
||||
"asana>=3.2.0",
|
||||
"google-analytics-data>=0.18.0",
|
||||
"framework",
|
||||
"stripe>=14.3.0",
|
||||
@@ -60,6 +61,10 @@ sql = [
|
||||
bigquery = [
|
||||
"google-cloud-bigquery>=3.0.0",
|
||||
]
|
||||
databricks = [
|
||||
"databricks-sdk>=0.30.0",
|
||||
"databricks-mcp>=0.1.0",
|
||||
]
|
||||
all = [
|
||||
"RestrictedPython>=7.0",
|
||||
"pytesseract>=0.3.10",
|
||||
@@ -67,6 +72,8 @@ all = [
|
||||
"duckdb>=1.0.0",
|
||||
"openpyxl>=3.1.0",
|
||||
"google-cloud-bigquery>=3.0.0",
|
||||
"databricks-sdk>=0.30.0",
|
||||
"databricks-mcp>=0.1.0",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Query Average Salary by Department
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pyodbc
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Force UTF-8 encoding for console output
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Database connection settings (from environment variables)
|
||||
SERVER = os.getenv("MSSQL_SERVER", r"MONSTER\\MSSQLSERVERR")
|
||||
DATABASE = os.getenv("MSSQL_DATABASE", "AdenTestDB")
|
||||
USERNAME = os.getenv("MSSQL_USERNAME")
|
||||
PASSWORD = os.getenv("MSSQL_PASSWORD")
|
||||
|
||||
|
||||
def main():
|
||||
"""Query and display average salary by department."""
|
||||
connection = None
|
||||
|
||||
try:
|
||||
# Connect to database
|
||||
if USERNAME and PASSWORD:
|
||||
# SQL Server Authentication
|
||||
connection_string = (
|
||||
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
||||
f"SERVER={SERVER};"
|
||||
f"DATABASE={DATABASE};"
|
||||
f"UID={USERNAME};"
|
||||
f"PWD={PASSWORD};"
|
||||
)
|
||||
else:
|
||||
# Windows Authentication
|
||||
connection_string = (
|
||||
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
||||
f"SERVER={SERVER};"
|
||||
f"DATABASE={DATABASE};"
|
||||
f"Trusted_Connection=yes;"
|
||||
)
|
||||
|
||||
connection = pyodbc.connect(connection_string)
|
||||
cursor = connection.cursor()
|
||||
|
||||
# Query to get average salary by department, sorted by average salary descending
|
||||
query = """
|
||||
SELECT
|
||||
d.name AS department,
|
||||
AVG(e.salary) AS avg_salary,
|
||||
COUNT(e.employee_id) AS emp_count
|
||||
FROM Departments d
|
||||
LEFT JOIN Employees e ON d.department_id = e.department_id
|
||||
WHERE e.salary IS NOT NULL
|
||||
GROUP BY d.name
|
||||
ORDER BY avg_salary DESC
|
||||
"""
|
||||
|
||||
cursor.execute(query)
|
||||
results = cursor.fetchall()
|
||||
|
||||
if not results:
|
||||
print("No salary data found.")
|
||||
return
|
||||
|
||||
# Get the highest average salary for highlighting
|
||||
highest_avg = results[0][1] if results else 0
|
||||
|
||||
print("=" * 80)
|
||||
print(" AVERAGE SALARY BY DEPARTMENT (Sorted Highest to Lowest)")
|
||||
print("=" * 80)
|
||||
print()
|
||||
print(f"{'Rank':<6} {'Department':<25} {'Avg Salary':<20} {'Employees':<12}")
|
||||
print("-" * 80)
|
||||
|
||||
for idx, row in enumerate(results, 1):
|
||||
department = row[0]
|
||||
avg_salary = row[1]
|
||||
emp_count = row[2]
|
||||
|
||||
avg_salary_str = f"${avg_salary:,.2f}"
|
||||
|
||||
# Highlight the department with the highest average
|
||||
if avg_salary == highest_avg:
|
||||
# Use special formatting for the highest
|
||||
prefix = f"{'>>> ' + str(idx):<6}"
|
||||
print(f"{prefix} {department:<25} {avg_salary_str:<20} {emp_count:<12} ⭐ HIGHEST")
|
||||
else:
|
||||
print(f"{idx:<6} {department:<25} {avg_salary_str:<20} {emp_count:<12}")
|
||||
|
||||
print("-" * 80)
|
||||
print()
|
||||
print("📊 Summary:")
|
||||
print(f" • Total departments with employees: {len(results)}")
|
||||
print(f" • Highest average salary: ${highest_avg:,.2f} ({results[0][0]})")
|
||||
print(f" • Lowest average salary: ${results[-1][1]:,.2f} ({results[-1][0]})")
|
||||
print("=" * 80)
|
||||
|
||||
except pyodbc.Error as e:
|
||||
print(f"\n[ERROR] Database operation failed: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[ERROR] Unexpected error: {str(e)}")
|
||||
|
||||
finally:
|
||||
if connection:
|
||||
connection.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -55,20 +55,35 @@ To add a new credential:
|
||||
3. If new category, import and merge it in this __init__.py
|
||||
"""
|
||||
|
||||
from .airtable import AIRTABLE_CREDENTIALS
|
||||
from .apify import APIFY_CREDENTIALS
|
||||
from .apollo import APOLLO_CREDENTIALS
|
||||
from .asana import ASANA_CREDENTIALS
|
||||
from .attio import ATTIO_CREDENTIALS
|
||||
from .aws_s3 import AWS_S3_CREDENTIALS
|
||||
from .azure_sql import AZURE_SQL_CREDENTIALS
|
||||
from .base import CredentialError, CredentialSpec
|
||||
from .bigquery import BIGQUERY_CREDENTIALS
|
||||
from .brevo import BREVO_CREDENTIALS
|
||||
from .browser import get_aden_auth_url, get_aden_setup_url, open_browser
|
||||
from .calcom import CALCOM_CREDENTIALS
|
||||
from .calendly import CALENDLY_CREDENTIALS
|
||||
from .cloudinary import CLOUDINARY_CREDENTIALS
|
||||
from .confluence import CONFLUENCE_CREDENTIALS
|
||||
from .databricks import DATABRICKS_CREDENTIALS
|
||||
from .discord import DISCORD_CREDENTIALS
|
||||
from .docker_hub import DOCKER_HUB_CREDENTIALS
|
||||
from .email import EMAIL_CREDENTIALS
|
||||
from .gcp_vision import GCP_VISION_CREDENTIALS
|
||||
from .github import GITHUB_CREDENTIALS
|
||||
from .gitlab import GITLAB_CREDENTIALS
|
||||
from .google_analytics import GOOGLE_ANALYTICS_CREDENTIALS
|
||||
from .google_calendar import GOOGLE_CALENDAR_CREDENTIALS
|
||||
from .google_docs import GOOGLE_DOCS_CREDENTIALS
|
||||
from .google_maps import GOOGLE_MAPS_CREDENTIALS
|
||||
from .google_search_console import GOOGLE_SEARCH_CONSOLE_CREDENTIALS
|
||||
from .google_sheets import GOOGLE_SHEETS_CREDENTIALS
|
||||
from .greenhouse import GREENHOUSE_CREDENTIALS
|
||||
from .health_check import (
|
||||
BaseHttpHealthChecker,
|
||||
HealthCheckResult,
|
||||
@@ -76,11 +91,34 @@ from .health_check import (
|
||||
validate_integration_wiring,
|
||||
)
|
||||
from .hubspot import HUBSPOT_CREDENTIALS
|
||||
from .huggingface import HUGGINGFACE_CREDENTIALS
|
||||
from .intercom import INTERCOM_CREDENTIALS
|
||||
from .jira import JIRA_CREDENTIALS
|
||||
from .kafka import KAFKA_CREDENTIALS
|
||||
from .langfuse import LANGFUSE_CREDENTIALS
|
||||
from .linear import LINEAR_CREDENTIALS
|
||||
from .llm import LLM_CREDENTIALS
|
||||
from .lusha import LUSHA_CREDENTIALS
|
||||
from .microsoft_graph import MICROSOFT_GRAPH_CREDENTIALS
|
||||
from .mongodb import MONGODB_CREDENTIALS
|
||||
from .n8n import N8N_CREDENTIALS
|
||||
from .news import NEWS_CREDENTIALS
|
||||
from .notion import NOTION_CREDENTIALS
|
||||
from .obsidian import OBSIDIAN_CREDENTIALS
|
||||
from .pagerduty import PAGERDUTY_CREDENTIALS
|
||||
from .pinecone import PINECONE_CREDENTIALS
|
||||
from .pipedrive import PIPEDRIVE_CREDENTIALS
|
||||
from .plaid import PLAID_CREDENTIALS
|
||||
from .postgres import POSTGRES_CREDENTIALS
|
||||
from .powerbi import POWERBI_CREDENTIALS
|
||||
from .pushover import PUSHOVER_CREDENTIALS
|
||||
from .quickbooks import QUICKBOOKS_CREDENTIALS
|
||||
from .razorpay import RAZORPAY_CREDENTIALS
|
||||
from .reddit import REDDIT_CREDENTIALS
|
||||
from .redis import REDIS_CREDENTIALS
|
||||
from .redshift import REDSHIFT_CREDENTIALS
|
||||
from .salesforce import SALESFORCE_CREDENTIALS
|
||||
from .sap import SAP_CREDENTIALS
|
||||
from .search import SEARCH_CREDENTIALS
|
||||
from .serpapi import SERPAPI_CREDENTIALS
|
||||
from .shell_config import (
|
||||
@@ -89,26 +127,49 @@ from .shell_config import (
|
||||
get_shell_config_path,
|
||||
get_shell_source_command,
|
||||
)
|
||||
from .shopify import SHOPIFY_CREDENTIALS
|
||||
from .slack import SLACK_CREDENTIALS
|
||||
from .snowflake import SNOWFLAKE_CREDENTIALS
|
||||
from .store_adapter import CredentialStoreAdapter
|
||||
from .stripe import STRIPE_CREDENTIALS
|
||||
from .supabase import SUPABASE_CREDENTIALS
|
||||
from .telegram import TELEGRAM_CREDENTIALS
|
||||
from .terraform import TERRAFORM_CREDENTIALS
|
||||
from .tines import TINES_CREDENTIALS
|
||||
from .trello import TRELLO_CREDENTIALS
|
||||
from .twilio import TWILIO_CREDENTIALS
|
||||
from .twitter import TWITTER_CREDENTIALS
|
||||
from .vercel import VERCEL_CREDENTIALS
|
||||
from .youtube import YOUTUBE_CREDENTIALS
|
||||
from .zendesk import ZENDESK_CREDENTIALS
|
||||
from .zoho_crm import ZOHO_CRM_CREDENTIALS
|
||||
from .zoom import ZOOM_CREDENTIALS
|
||||
|
||||
# Merged registry of all credentials
|
||||
CREDENTIAL_SPECS = {
|
||||
**AIRTABLE_CREDENTIALS,
|
||||
**LLM_CREDENTIALS,
|
||||
**NEWS_CREDENTIALS,
|
||||
**SEARCH_CREDENTIALS,
|
||||
**EMAIL_CREDENTIALS,
|
||||
**GCP_VISION_CREDENTIALS,
|
||||
**APIFY_CREDENTIALS,
|
||||
**AWS_S3_CREDENTIALS,
|
||||
**ASANA_CREDENTIALS,
|
||||
**APOLLO_CREDENTIALS,
|
||||
**ATTIO_CREDENTIALS,
|
||||
**DISCORD_CREDENTIALS,
|
||||
**GITHUB_CREDENTIALS,
|
||||
**GOOGLE_ANALYTICS_CREDENTIALS,
|
||||
**GOOGLE_DOCS_CREDENTIALS,
|
||||
**GOOGLE_MAPS_CREDENTIALS,
|
||||
**GOOGLE_SEARCH_CONSOLE_CREDENTIALS,
|
||||
**HUGGINGFACE_CREDENTIALS,
|
||||
**HUBSPOT_CREDENTIALS,
|
||||
**INTERCOM_CREDENTIALS,
|
||||
**LINEAR_CREDENTIALS,
|
||||
**MONGODB_CREDENTIALS,
|
||||
**PAGERDUTY_CREDENTIALS,
|
||||
**GOOGLE_CALENDAR_CREDENTIALS,
|
||||
**SLACK_CREDENTIALS,
|
||||
**SERPAPI_CREDENTIALS,
|
||||
@@ -116,9 +177,50 @@ CREDENTIAL_SPECS = {
|
||||
**TELEGRAM_CREDENTIALS,
|
||||
**BIGQUERY_CREDENTIALS,
|
||||
**CALCOM_CREDENTIALS,
|
||||
**CALENDLY_CREDENTIALS,
|
||||
**DATABRICKS_CREDENTIALS,
|
||||
**DOCKER_HUB_CREDENTIALS,
|
||||
**PIPEDRIVE_CREDENTIALS,
|
||||
**STRIPE_CREDENTIALS,
|
||||
**BREVO_CREDENTIALS,
|
||||
**POSTGRES_CREDENTIALS,
|
||||
**QUICKBOOKS_CREDENTIALS,
|
||||
**MICROSOFT_GRAPH_CREDENTIALS,
|
||||
**PUSHOVER_CREDENTIALS,
|
||||
**REDIS_CREDENTIALS,
|
||||
**SUPABASE_CREDENTIALS,
|
||||
**VERCEL_CREDENTIALS,
|
||||
**YOUTUBE_CREDENTIALS,
|
||||
**PINECONE_CREDENTIALS,
|
||||
**PLAID_CREDENTIALS,
|
||||
**TRELLO_CREDENTIALS,
|
||||
**CONFLUENCE_CREDENTIALS,
|
||||
**CLOUDINARY_CREDENTIALS,
|
||||
**GITLAB_CREDENTIALS,
|
||||
**GOOGLE_SHEETS_CREDENTIALS,
|
||||
**GREENHOUSE_CREDENTIALS,
|
||||
**JIRA_CREDENTIALS,
|
||||
**NOTION_CREDENTIALS,
|
||||
**REDDIT_CREDENTIALS,
|
||||
**TINES_CREDENTIALS,
|
||||
**TWITTER_CREDENTIALS,
|
||||
**TWILIO_CREDENTIALS,
|
||||
**ZENDESK_CREDENTIALS,
|
||||
**ZOHO_CRM_CREDENTIALS,
|
||||
**TERRAFORM_CREDENTIALS,
|
||||
**LUSHA_CREDENTIALS,
|
||||
**POWERBI_CREDENTIALS,
|
||||
**SNOWFLAKE_CREDENTIALS,
|
||||
**AZURE_SQL_CREDENTIALS,
|
||||
**KAFKA_CREDENTIALS,
|
||||
**REDSHIFT_CREDENTIALS,
|
||||
**SAP_CREDENTIALS,
|
||||
**SALESFORCE_CREDENTIALS,
|
||||
**SHOPIFY_CREDENTIALS,
|
||||
**ZOOM_CREDENTIALS,
|
||||
**N8N_CREDENTIALS,
|
||||
**LANGFUSE_CREDENTIALS,
|
||||
**OBSIDIAN_CREDENTIALS,
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
@@ -145,6 +247,7 @@ __all__ = [
|
||||
# Merged registry
|
||||
"CREDENTIAL_SPECS",
|
||||
# Category registries (for direct access if needed)
|
||||
"AIRTABLE_CREDENTIALS",
|
||||
"LLM_CREDENTIALS",
|
||||
"NEWS_CREDENTIALS",
|
||||
"SEARCH_CREDENTIALS",
|
||||
@@ -154,18 +257,68 @@ __all__ = [
|
||||
"GOOGLE_ANALYTICS_CREDENTIALS",
|
||||
"GOOGLE_DOCS_CREDENTIALS",
|
||||
"GOOGLE_MAPS_CREDENTIALS",
|
||||
"GOOGLE_SEARCH_CONSOLE_CREDENTIALS",
|
||||
"HUGGINGFACE_CREDENTIALS",
|
||||
"HUBSPOT_CREDENTIALS",
|
||||
"INTERCOM_CREDENTIALS",
|
||||
"LINEAR_CREDENTIALS",
|
||||
"MONGODB_CREDENTIALS",
|
||||
"PAGERDUTY_CREDENTIALS",
|
||||
"GOOGLE_CALENDAR_CREDENTIALS",
|
||||
"SLACK_CREDENTIALS",
|
||||
"APIFY_CREDENTIALS",
|
||||
"AWS_S3_CREDENTIALS",
|
||||
"ASANA_CREDENTIALS",
|
||||
"APOLLO_CREDENTIALS",
|
||||
"ATTIO_CREDENTIALS",
|
||||
"SERPAPI_CREDENTIALS",
|
||||
"RAZORPAY_CREDENTIALS",
|
||||
"TELEGRAM_CREDENTIALS",
|
||||
"BIGQUERY_CREDENTIALS",
|
||||
"CALCOM_CREDENTIALS",
|
||||
"CALENDLY_CREDENTIALS",
|
||||
"DATABRICKS_CREDENTIALS",
|
||||
"DISCORD_CREDENTIALS",
|
||||
"DOCKER_HUB_CREDENTIALS",
|
||||
"PIPEDRIVE_CREDENTIALS",
|
||||
"STRIPE_CREDENTIALS",
|
||||
"BREVO_CREDENTIALS",
|
||||
"POSTGRES_CREDENTIALS",
|
||||
"QUICKBOOKS_CREDENTIALS",
|
||||
"MICROSOFT_GRAPH_CREDENTIALS",
|
||||
"PUSHOVER_CREDENTIALS",
|
||||
"REDIS_CREDENTIALS",
|
||||
"SUPABASE_CREDENTIALS",
|
||||
"VERCEL_CREDENTIALS",
|
||||
"YOUTUBE_CREDENTIALS",
|
||||
"PINECONE_CREDENTIALS",
|
||||
"PLAID_CREDENTIALS",
|
||||
"TRELLO_CREDENTIALS",
|
||||
"CONFLUENCE_CREDENTIALS",
|
||||
"CLOUDINARY_CREDENTIALS",
|
||||
"GITLAB_CREDENTIALS",
|
||||
"GOOGLE_SHEETS_CREDENTIALS",
|
||||
"GREENHOUSE_CREDENTIALS",
|
||||
"JIRA_CREDENTIALS",
|
||||
"NOTION_CREDENTIALS",
|
||||
"REDDIT_CREDENTIALS",
|
||||
"TINES_CREDENTIALS",
|
||||
"TWITTER_CREDENTIALS",
|
||||
"TWILIO_CREDENTIALS",
|
||||
"ZENDESK_CREDENTIALS",
|
||||
"ZOHO_CRM_CREDENTIALS",
|
||||
"TERRAFORM_CREDENTIALS",
|
||||
"LUSHA_CREDENTIALS",
|
||||
"POWERBI_CREDENTIALS",
|
||||
"SNOWFLAKE_CREDENTIALS",
|
||||
"AZURE_SQL_CREDENTIALS",
|
||||
"KAFKA_CREDENTIALS",
|
||||
"REDSHIFT_CREDENTIALS",
|
||||
"SAP_CREDENTIALS",
|
||||
"SALESFORCE_CREDENTIALS",
|
||||
"SHOPIFY_CREDENTIALS",
|
||||
"ZOOM_CREDENTIALS",
|
||||
"N8N_CREDENTIALS",
|
||||
"LANGFUSE_CREDENTIALS",
|
||||
"OBSIDIAN_CREDENTIALS",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Airtable credentials.
|
||||
|
||||
Contains credentials for the Airtable Web API.
|
||||
Requires AIRTABLE_PAT (Personal Access Token).
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
AIRTABLE_CREDENTIALS = {
|
||||
"airtable_pat": CredentialSpec(
|
||||
env_var="AIRTABLE_PAT",
|
||||
tools=[
|
||||
"airtable_list_records",
|
||||
"airtable_get_record",
|
||||
"airtable_create_records",
|
||||
"airtable_update_records",
|
||||
"airtable_list_bases",
|
||||
"airtable_get_base_schema",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://airtable.com/create/tokens",
|
||||
description="Airtable Personal Access Token",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Airtable API access:
|
||||
1. Go to https://airtable.com/create/tokens
|
||||
2. Create a new Personal Access Token
|
||||
3. Grant scopes: data.records:read, data.records:write, schema.bases:read
|
||||
4. Select the bases to grant access to
|
||||
5. Set environment variable:
|
||||
export AIRTABLE_PAT=your-personal-access-token""",
|
||||
health_check_endpoint="",
|
||||
credential_id="airtable_pat",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Apify credentials.
|
||||
|
||||
Contains credentials for Apify web scraping and automation platform.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
APIFY_CREDENTIALS = {
|
||||
"apify": CredentialSpec(
|
||||
env_var="APIFY_API_TOKEN",
|
||||
tools=[
|
||||
"apify_run_actor",
|
||||
"apify_get_run",
|
||||
"apify_get_dataset_items",
|
||||
"apify_list_actors",
|
||||
"apify_list_runs",
|
||||
"apify_get_kv_store_record",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.apify.com/api/v2",
|
||||
description="Apify API token for running web scraping actors and retrieving datasets",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get an Apify API token:
|
||||
1. Go to https://console.apify.com/account/integrations
|
||||
2. Copy your personal API token
|
||||
3. Set the environment variable:
|
||||
export APIFY_API_TOKEN=your-api-token""",
|
||||
health_check_endpoint="https://api.apify.com/v2/users/me",
|
||||
credential_id="apify",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Asana credentials.
|
||||
|
||||
Contains credentials for Asana task and project management.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
ASANA_CREDENTIALS = {
|
||||
"asana": CredentialSpec(
|
||||
env_var="ASANA_ACCESS_TOKEN",
|
||||
tools=[
|
||||
"asana_list_workspaces",
|
||||
"asana_list_projects",
|
||||
"asana_list_tasks",
|
||||
"asana_get_task",
|
||||
"asana_create_task",
|
||||
"asana_search_tasks",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developers.asana.com/docs/personal-access-token",
|
||||
description="Asana personal access token for task and project management",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get an Asana personal access token:
|
||||
1. Go to https://app.asana.com/0/my-apps
|
||||
2. Click 'Create new token'
|
||||
3. Give it a name and copy the token
|
||||
4. Set the environment variable:
|
||||
export ASANA_ACCESS_TOKEN=your-pat""",
|
||||
health_check_endpoint="https://app.asana.com/api/1.0/users/me",
|
||||
credential_id="asana",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Attio tool credentials.
|
||||
|
||||
Contains credentials for Attio CRM integration.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
ATTIO_CREDENTIALS = {
|
||||
"attio": CredentialSpec(
|
||||
env_var="ATTIO_API_KEY",
|
||||
tools=[
|
||||
"attio_record_list",
|
||||
"attio_record_get",
|
||||
"attio_record_create",
|
||||
"attio_record_update",
|
||||
"attio_record_assert",
|
||||
"attio_list_lists",
|
||||
"attio_list_entries_get",
|
||||
"attio_list_entry_create",
|
||||
"attio_list_entry_delete",
|
||||
"attio_task_create",
|
||||
"attio_task_list",
|
||||
"attio_task_get",
|
||||
"attio_task_delete",
|
||||
"attio_members_list",
|
||||
"attio_member_get",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://attio.com/help/apps/other-apps/generating-an-api-key",
|
||||
description="Attio API key for CRM integration",
|
||||
# Auth method support
|
||||
aden_supported=False,
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get an Attio API key:
|
||||
1. Go to Attio Settings > Developers > Access tokens
|
||||
2. Click "Generate new token"
|
||||
3. Name your token (e.g., "Hive Agent")
|
||||
4. Select required scopes:
|
||||
- record_permission:read-write
|
||||
- object_configuration:read
|
||||
- list_entry:read-write
|
||||
- list_configuration:read
|
||||
- task:read-write
|
||||
- user_management:read
|
||||
5. Copy the generated token""",
|
||||
# Health check configuration
|
||||
health_check_endpoint="https://api.attio.com/v2/workspace_members",
|
||||
health_check_method="GET",
|
||||
# Credential store mapping
|
||||
credential_id="attio",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
AWS S3 credentials.
|
||||
|
||||
Contains credentials for AWS S3 REST API with SigV4 signing.
|
||||
Requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
AWS_S3_CREDENTIALS = {
|
||||
"aws_access_key": CredentialSpec(
|
||||
env_var="AWS_ACCESS_KEY_ID",
|
||||
tools=[
|
||||
"s3_list_buckets",
|
||||
"s3_list_objects",
|
||||
"s3_get_object",
|
||||
"s3_put_object",
|
||||
"s3_delete_object",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html",
|
||||
description="AWS Access Key ID for S3 API access",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up AWS S3 API access:
|
||||
1. Go to AWS IAM > Users > Security credentials
|
||||
2. Create a new access key
|
||||
3. Set environment variables:
|
||||
export AWS_ACCESS_KEY_ID=your-access-key-id
|
||||
export AWS_SECRET_ACCESS_KEY=your-secret-access-key
|
||||
export AWS_REGION=us-east-1""",
|
||||
health_check_endpoint="",
|
||||
credential_id="aws_access_key",
|
||||
credential_key="api_key",
|
||||
credential_group="aws",
|
||||
),
|
||||
"aws_secret_key": CredentialSpec(
|
||||
env_var="AWS_SECRET_ACCESS_KEY",
|
||||
tools=[
|
||||
"s3_list_buckets",
|
||||
"s3_list_objects",
|
||||
"s3_get_object",
|
||||
"s3_put_object",
|
||||
"s3_delete_object",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html",
|
||||
description="AWS Secret Access Key for S3 API access",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See AWS_ACCESS_KEY_ID instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="aws_secret_key",
|
||||
credential_key="api_key",
|
||||
credential_group="aws",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Azure SQL Database management credentials.
|
||||
|
||||
Contains credentials for the Azure SQL REST API (management plane).
|
||||
Requires AZURE_SQL_ACCESS_TOKEN and AZURE_SUBSCRIPTION_ID.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
AZURE_SQL_CREDENTIALS = {
|
||||
"azure_sql_token": CredentialSpec(
|
||||
env_var="AZURE_SQL_ACCESS_TOKEN",
|
||||
tools=[
|
||||
"azure_sql_list_servers",
|
||||
"azure_sql_get_server",
|
||||
"azure_sql_list_databases",
|
||||
"azure_sql_get_database",
|
||||
"azure_sql_list_firewall_rules",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://learn.microsoft.com/en-us/rest/api/sql/",
|
||||
description="Azure Bearer token for SQL management API (scope: management.azure.com)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Azure SQL management API access:
|
||||
1. Register an app in Azure AD (Entra ID)
|
||||
2. Assign SQL DB Contributor or Reader role
|
||||
3. Obtain a token via client credentials flow (scope: https://management.azure.com/.default)
|
||||
4. Set environment variables:
|
||||
export AZURE_SQL_ACCESS_TOKEN=your-bearer-token
|
||||
export AZURE_SUBSCRIPTION_ID=your-subscription-id""",
|
||||
health_check_endpoint="",
|
||||
credential_id="azure_sql_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"azure_subscription_id": CredentialSpec(
|
||||
env_var="AZURE_SUBSCRIPTION_ID",
|
||||
tools=[
|
||||
"azure_sql_list_servers",
|
||||
"azure_sql_get_server",
|
||||
"azure_sql_list_databases",
|
||||
"azure_sql_get_database",
|
||||
"azure_sql_list_firewall_rules",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://learn.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id",
|
||||
description="Azure subscription ID for resource management",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See AZURE_SQL_ACCESS_TOKEN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="azure_subscription_id",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
"""
|
||||
Brevo tool credentials.
|
||||
|
||||
Contains credentials for Brevo (formerly Sendinblue) transactional email,
|
||||
SMS, and contact management integration.
|
||||
Contains credentials for Brevo email and SMS integration.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
@@ -16,26 +14,22 @@ BREVO_CREDENTIALS = {
|
||||
"brevo_create_contact",
|
||||
"brevo_get_contact",
|
||||
"brevo_update_contact",
|
||||
"brevo_get_email_stats",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://app.brevo.com/settings/keys/api",
|
||||
description="Brevo API key for transactional email, SMS, and contact management",
|
||||
# Auth method support
|
||||
aden_supported=False,
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Brevo API key:
|
||||
1. Go to https://app.brevo.com and create an account (or sign in)
|
||||
2. Navigate to Settings > API Keys (or visit https://app.brevo.com/settings/keys/api)
|
||||
3. Click "Generate a new API key"
|
||||
4. Give it a name (e.g., "Hive Agent")
|
||||
5. Copy the API key (starts with xkeysib-)
|
||||
6. Store it securely - you won't be able to see it again!
|
||||
7. Note: For sending emails, you'll need a verified sender domain or email""",
|
||||
# Health check configuration
|
||||
1. Sign up or log in at https://www.brevo.com
|
||||
2. Go to Settings → API Keys
|
||||
3. Click 'Generate a new API key'
|
||||
4. Give it a name (e.g., 'Hive Agent')
|
||||
5. Copy the API key and set it as BREVO_API_KEY""",
|
||||
health_check_endpoint="https://api.brevo.com/v3/account",
|
||||
health_check_method="GET",
|
||||
# Credential store mapping
|
||||
credential_id="brevo",
|
||||
credential_key="api_key",
|
||||
),
|
||||
|
||||
@@ -40,6 +40,7 @@ def open_browser(url: str) -> tuple[bool, str]:
|
||||
["open", url],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return True, "Opened in browser"
|
||||
|
||||
@@ -50,6 +51,7 @@ def open_browser(url: str) -> tuple[bool, str]:
|
||||
["xdg-open", url],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return True, "Opened in browser"
|
||||
except FileNotFoundError:
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Calendly credentials.
|
||||
|
||||
Contains credentials for the Calendly API v2.
|
||||
Requires CALENDLY_PAT (Personal Access Token).
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
CALENDLY_CREDENTIALS = {
|
||||
"calendly_pat": CredentialSpec(
|
||||
env_var="CALENDLY_PAT",
|
||||
tools=[
|
||||
"calendly_get_current_user",
|
||||
"calendly_list_event_types",
|
||||
"calendly_list_scheduled_events",
|
||||
"calendly_get_scheduled_event",
|
||||
"calendly_list_invitees",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.calendly.com/how-to-authenticate-with-personal-access-tokens",
|
||||
description="Calendly Personal Access Token",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Calendly API access:
|
||||
1. Go to https://calendly.com/integrations/api_webhooks
|
||||
2. Generate a Personal Access Token
|
||||
3. Set environment variable:
|
||||
export CALENDLY_PAT=your-personal-access-token""",
|
||||
health_check_endpoint="https://api.calendly.com/users/me",
|
||||
credential_id="calendly_pat",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Cloudinary credentials.
|
||||
|
||||
Contains credentials for Cloudinary image/video management.
|
||||
Requires CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
CLOUDINARY_CREDENTIALS = {
|
||||
"cloudinary_cloud_name": CredentialSpec(
|
||||
env_var="CLOUDINARY_CLOUD_NAME",
|
||||
tools=[
|
||||
"cloudinary_upload",
|
||||
"cloudinary_list_resources",
|
||||
"cloudinary_get_resource",
|
||||
"cloudinary_delete_resource",
|
||||
"cloudinary_search",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://console.cloudinary.com/",
|
||||
description="Cloudinary cloud name from your dashboard",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Cloudinary access:
|
||||
1. Go to https://console.cloudinary.com/
|
||||
2. Copy your Cloud Name, API Key, and API Secret from the dashboard
|
||||
3. Set environment variables:
|
||||
export CLOUDINARY_CLOUD_NAME=your-cloud-name
|
||||
export CLOUDINARY_API_KEY=your-api-key
|
||||
export CLOUDINARY_API_SECRET=your-api-secret""",
|
||||
health_check_endpoint="",
|
||||
credential_id="cloudinary_cloud_name",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"cloudinary_key": CredentialSpec(
|
||||
env_var="CLOUDINARY_API_KEY",
|
||||
tools=[
|
||||
"cloudinary_upload",
|
||||
"cloudinary_list_resources",
|
||||
"cloudinary_get_resource",
|
||||
"cloudinary_delete_resource",
|
||||
"cloudinary_search",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://console.cloudinary.com/",
|
||||
description="Cloudinary API key for authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See CLOUDINARY_CLOUD_NAME instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="cloudinary_key",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"cloudinary_secret": CredentialSpec(
|
||||
env_var="CLOUDINARY_API_SECRET",
|
||||
tools=[
|
||||
"cloudinary_upload",
|
||||
"cloudinary_list_resources",
|
||||
"cloudinary_get_resource",
|
||||
"cloudinary_delete_resource",
|
||||
"cloudinary_search",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://console.cloudinary.com/",
|
||||
description="Cloudinary API secret for authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See CLOUDINARY_CLOUD_NAME instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="cloudinary_secret",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Confluence credentials.
|
||||
|
||||
Contains credentials for Confluence wiki & knowledge management.
|
||||
Requires CONFLUENCE_DOMAIN, CONFLUENCE_EMAIL, and CONFLUENCE_API_TOKEN.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
CONFLUENCE_CREDENTIALS = {
|
||||
"confluence_domain": CredentialSpec(
|
||||
env_var="CONFLUENCE_DOMAIN",
|
||||
tools=[
|
||||
"confluence_list_spaces",
|
||||
"confluence_list_pages",
|
||||
"confluence_get_page",
|
||||
"confluence_create_page",
|
||||
"confluence_search",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://id.atlassian.com/manage/api-tokens",
|
||||
description="Confluence domain (e.g. your-org.atlassian.net)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Confluence access:
|
||||
1. Go to https://id.atlassian.com/manage/api-tokens
|
||||
2. Click 'Create API token'
|
||||
3. Set environment variables:
|
||||
export CONFLUENCE_DOMAIN=your-org.atlassian.net
|
||||
export CONFLUENCE_EMAIL=your-email@example.com
|
||||
export CONFLUENCE_API_TOKEN=your-api-token""",
|
||||
health_check_endpoint="",
|
||||
credential_id="confluence_domain",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"confluence_email": CredentialSpec(
|
||||
env_var="CONFLUENCE_EMAIL",
|
||||
tools=[
|
||||
"confluence_list_spaces",
|
||||
"confluence_list_pages",
|
||||
"confluence_get_page",
|
||||
"confluence_create_page",
|
||||
"confluence_search",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://id.atlassian.com/manage/api-tokens",
|
||||
description="Atlassian account email for Confluence authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See CONFLUENCE_DOMAIN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="confluence_email",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"confluence_token": CredentialSpec(
|
||||
env_var="CONFLUENCE_API_TOKEN",
|
||||
tools=[
|
||||
"confluence_list_spaces",
|
||||
"confluence_list_pages",
|
||||
"confluence_get_page",
|
||||
"confluence_create_page",
|
||||
"confluence_search",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://id.atlassian.com/manage/api-tokens",
|
||||
description="Atlassian API token for Confluence authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See CONFLUENCE_DOMAIN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="confluence_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Databricks credentials.
|
||||
|
||||
Contains credentials for Databricks workspace, SQL, and job management.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
DATABRICKS_CREDENTIALS = {
|
||||
"databricks": CredentialSpec(
|
||||
env_var="DATABRICKS_TOKEN",
|
||||
tools=[
|
||||
"databricks_sql_query",
|
||||
"databricks_list_jobs",
|
||||
"databricks_run_job",
|
||||
"databricks_get_run",
|
||||
"databricks_list_clusters",
|
||||
"databricks_start_cluster",
|
||||
"databricks_terminate_cluster",
|
||||
"databricks_list_workspace",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.databricks.com/dev-tools/auth/pat.html",
|
||||
description="Databricks personal access token (also requires DATABRICKS_HOST env var)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Databricks personal access token:
|
||||
1. Go to your Databricks workspace URL
|
||||
2. Click your username in the top-right → Settings
|
||||
3. Go to Developer → Access tokens
|
||||
4. Click Generate new token
|
||||
5. Set both environment variables:
|
||||
export DATABRICKS_TOKEN=dapi...
|
||||
export DATABRICKS_HOST=https://your-workspace.cloud.databricks.com""",
|
||||
health_check_endpoint="",
|
||||
credential_id="databricks",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Docker Hub credentials.
|
||||
|
||||
Contains credentials for Docker Hub repository and image management.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
DOCKER_HUB_CREDENTIALS = {
|
||||
"docker_hub": CredentialSpec(
|
||||
env_var="DOCKER_HUB_TOKEN",
|
||||
tools=[
|
||||
"docker_hub_search",
|
||||
"docker_hub_list_repos",
|
||||
"docker_hub_list_tags",
|
||||
"docker_hub_get_repo",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://hub.docker.com/settings/security",
|
||||
description=(
|
||||
"Docker Hub personal access token (also set DOCKER_HUB_USERNAME for listing own repos)"
|
||||
),
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Docker Hub personal access token:
|
||||
1. Go to https://hub.docker.com/settings/security
|
||||
2. Click 'New Access Token'
|
||||
3. Give it a description and select permissions (Read is sufficient for browsing)
|
||||
4. Copy the token
|
||||
5. Set environment variables:
|
||||
export DOCKER_HUB_TOKEN=your-pat
|
||||
export DOCKER_HUB_USERNAME=your-username""",
|
||||
health_check_endpoint="https://hub.docker.com/v2/user/login",
|
||||
credential_id="docker_hub",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
GitLab credentials.
|
||||
|
||||
Contains credentials for GitLab projects, issues, and merge requests.
|
||||
Requires GITLAB_TOKEN. GITLAB_URL is optional (defaults to gitlab.com).
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
GITLAB_CREDENTIALS = {
|
||||
"gitlab_token": CredentialSpec(
|
||||
env_var="GITLAB_TOKEN",
|
||||
tools=[
|
||||
"gitlab_list_projects",
|
||||
"gitlab_get_project",
|
||||
"gitlab_list_issues",
|
||||
"gitlab_get_issue",
|
||||
"gitlab_create_issue",
|
||||
"gitlab_list_merge_requests",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://gitlab.com/-/user_settings/personal_access_tokens",
|
||||
description="GitLab personal access token",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up GitLab API access:
|
||||
1. Go to https://gitlab.com/-/user_settings/personal_access_tokens
|
||||
(or your self-hosted instance equivalent)
|
||||
2. Create a new token with 'api' scope
|
||||
3. Set environment variables:
|
||||
export GITLAB_TOKEN=your-personal-access-token
|
||||
export GITLAB_URL=https://gitlab.com (optional, defaults to gitlab.com)""",
|
||||
health_check_endpoint="https://gitlab.com/api/v4/user",
|
||||
credential_id="gitlab_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Google Search Console credentials.
|
||||
|
||||
Contains credentials for Search Console analytics, sitemaps, and URL inspection.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
GOOGLE_SEARCH_CONSOLE_CREDENTIALS = {
|
||||
"google_search_console": CredentialSpec(
|
||||
env_var="GOOGLE_SEARCH_CONSOLE_TOKEN",
|
||||
tools=[
|
||||
"gsc_search_analytics",
|
||||
"gsc_list_sites",
|
||||
"gsc_list_sitemaps",
|
||||
"gsc_inspect_url",
|
||||
"gsc_submit_sitemap",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developers.google.com/webmaster-tools/v1/prereqs",
|
||||
description="Google OAuth2 access token with Search Console scope",
|
||||
direct_api_key_supported=False,
|
||||
api_key_instructions="""To get a Google Search Console access token:
|
||||
1. Go to https://console.cloud.google.com/apis/credentials
|
||||
2. Create an OAuth2 client (type: Desktop app or Web app)
|
||||
3. Enable the Search Console API in your project
|
||||
4. Generate an access token with scope: https://www.googleapis.com/auth/webmasters.readonly
|
||||
5. Set the environment variable:
|
||||
export GOOGLE_SEARCH_CONSOLE_TOKEN=your-access-token""",
|
||||
health_check_endpoint="https://www.googleapis.com/webmasters/v3/sites",
|
||||
credential_id="google_search_console",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Google Sheets credentials.
|
||||
|
||||
Contains credentials for Google Sheets spreadsheet access.
|
||||
Requires GOOGLE_SHEETS_API_KEY for read-only access to public sheets.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
GOOGLE_SHEETS_CREDENTIALS = {
|
||||
"google_sheets_key": CredentialSpec(
|
||||
env_var="GOOGLE_SHEETS_API_KEY",
|
||||
tools=[
|
||||
"sheets_get_spreadsheet",
|
||||
"sheets_read_range",
|
||||
"sheets_batch_read",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://console.cloud.google.com/apis/credentials",
|
||||
description="Google API key for reading public Google Sheets",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Google Sheets API access:
|
||||
1. Go to https://console.cloud.google.com/apis/credentials
|
||||
2. Click 'Create Credentials' > 'API Key'
|
||||
3. Enable the Google Sheets API in APIs & Services > Library
|
||||
4. Target spreadsheets must be shared as 'Anyone with the link'
|
||||
5. Set environment variable:
|
||||
export GOOGLE_SHEETS_API_KEY=your-api-key""",
|
||||
health_check_endpoint="",
|
||||
credential_id="google_sheets_key",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Greenhouse credentials.
|
||||
|
||||
Contains credentials for Greenhouse ATS & recruiting.
|
||||
Requires GREENHOUSE_API_TOKEN.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
GREENHOUSE_CREDENTIALS = {
|
||||
"greenhouse_token": CredentialSpec(
|
||||
env_var="GREENHOUSE_API_TOKEN",
|
||||
tools=[
|
||||
"greenhouse_list_jobs",
|
||||
"greenhouse_get_job",
|
||||
"greenhouse_list_candidates",
|
||||
"greenhouse_get_candidate",
|
||||
"greenhouse_list_applications",
|
||||
"greenhouse_get_application",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://support.greenhouse.io/hc/en-us/articles/202842799-Harvest-API",
|
||||
description="Greenhouse Harvest API token for ATS access",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Greenhouse Harvest API access:
|
||||
1. Go to Greenhouse > Configure > Dev Center > API Credential Management
|
||||
2. Click 'Create New API Key'
|
||||
3. Select 'Harvest' as the API type
|
||||
4. Set permissions (at minimum: Jobs, Candidates, Applications read access)
|
||||
5. Set environment variable:
|
||||
export GREENHOUSE_API_TOKEN=your-api-token""",
|
||||
health_check_endpoint="https://harvest.greenhouse.io/v1/jobs?per_page=1",
|
||||
credential_id="greenhouse_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -8,6 +8,7 @@ to verify the credential works.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Protocol
|
||||
|
||||
@@ -104,6 +105,72 @@ class HubSpotHealthChecker:
|
||||
)
|
||||
|
||||
|
||||
class ZohoCRMHealthChecker:
|
||||
"""Health checker for Zoho CRM credentials."""
|
||||
|
||||
TIMEOUT = 10.0
|
||||
|
||||
def check(self, access_token: str) -> HealthCheckResult:
|
||||
"""
|
||||
Validate Zoho token by making lightweight API call.
|
||||
|
||||
Uses /users?type=CurrentUser so module permissions are not required.
|
||||
"""
|
||||
api_domain = os.getenv("ZOHO_API_DOMAIN", "https://www.zohoapis.com").rstrip("/")
|
||||
endpoint = f"{api_domain}/crm/v2/users?type=CurrentUser"
|
||||
try:
|
||||
with httpx.Client(timeout=self.TIMEOUT) as client:
|
||||
response = client.get(
|
||||
endpoint,
|
||||
headers={
|
||||
"Authorization": f"Zoho-oauthtoken {access_token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return HealthCheckResult(
|
||||
valid=True,
|
||||
message="Zoho CRM credentials valid",
|
||||
)
|
||||
elif response.status_code == 401:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Zoho CRM token is invalid or expired",
|
||||
details={"status_code": 401},
|
||||
)
|
||||
elif response.status_code == 403:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Zoho CRM token lacks required scopes",
|
||||
details={"status_code": 403},
|
||||
)
|
||||
elif response.status_code == 429:
|
||||
return HealthCheckResult(
|
||||
valid=True,
|
||||
message="Zoho CRM credentials valid (rate limited)",
|
||||
details={"status_code": 429, "rate_limited": True},
|
||||
)
|
||||
else:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Zoho CRM API returned status {response.status_code}",
|
||||
details={"status_code": response.status_code},
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Zoho CRM API request timed out",
|
||||
details={"error": "timeout"},
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Failed to connect to Zoho CRM: {e}",
|
||||
details={"error": str(e)},
|
||||
)
|
||||
|
||||
|
||||
class BraveSearchHealthChecker:
|
||||
"""Health checker for Brave Search API."""
|
||||
|
||||
@@ -563,6 +630,66 @@ class SlackHealthChecker:
|
||||
)
|
||||
|
||||
|
||||
class CalendlyHealthChecker:
|
||||
"""Health checker for Calendly API tokens."""
|
||||
|
||||
ENDPOINT = "https://api.calendly.com/users/me"
|
||||
TIMEOUT = 10.0
|
||||
|
||||
def check(self, api_token: str) -> HealthCheckResult:
|
||||
"""
|
||||
Validate Calendly token by calling /users/me.
|
||||
|
||||
Makes a GET request to verify the token works.
|
||||
"""
|
||||
try:
|
||||
with httpx.Client(timeout=self.TIMEOUT) as client:
|
||||
response = client.get(
|
||||
self.ENDPOINT,
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return HealthCheckResult(
|
||||
valid=True,
|
||||
message="Calendly token valid",
|
||||
details={},
|
||||
)
|
||||
elif response.status_code == 401:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Calendly token is invalid or expired",
|
||||
details={"status_code": 401},
|
||||
)
|
||||
elif response.status_code == 403:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Calendly token access forbidden",
|
||||
details={"status_code": 403},
|
||||
)
|
||||
else:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Calendly API returned status {response.status_code}",
|
||||
details={"status_code": response.status_code},
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Calendly API request timed out",
|
||||
details={"error": "timeout"},
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Failed to connect to Calendly API: {e}",
|
||||
details={"error": str(e)},
|
||||
)
|
||||
|
||||
|
||||
class AnthropicHealthChecker:
|
||||
"""Health checker for Anthropic API credentials."""
|
||||
|
||||
@@ -898,6 +1025,71 @@ class GoogleMapsHealthChecker:
|
||||
)
|
||||
|
||||
|
||||
class LushaHealthChecker:
|
||||
"""Health checker for Lusha API credentials."""
|
||||
|
||||
ENDPOINT = "https://api.lusha.com/account/usage"
|
||||
TIMEOUT = 10.0
|
||||
|
||||
def check(self, api_key: str) -> HealthCheckResult:
|
||||
"""
|
||||
Validate Lusha API key by checking account usage endpoint.
|
||||
|
||||
This is a lightweight authenticated request that confirms API access.
|
||||
"""
|
||||
try:
|
||||
with httpx.Client(timeout=self.TIMEOUT) as client:
|
||||
response = client.get(
|
||||
self.ENDPOINT,
|
||||
headers={
|
||||
"api_key": api_key,
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return HealthCheckResult(
|
||||
valid=True,
|
||||
message="Lusha API key valid",
|
||||
)
|
||||
elif response.status_code == 401:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Lusha API key is invalid",
|
||||
details={"status_code": 401},
|
||||
)
|
||||
elif response.status_code == 403:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Lusha API key lacks required permissions",
|
||||
details={"status_code": 403},
|
||||
)
|
||||
elif response.status_code == 429:
|
||||
return HealthCheckResult(
|
||||
valid=True,
|
||||
message="Lusha API key valid (rate/credit limited)",
|
||||
details={"status_code": 429, "rate_limited": True},
|
||||
)
|
||||
else:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Lusha API returned status {response.status_code}",
|
||||
details={"status_code": response.status_code},
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Lusha API request timed out",
|
||||
details={"error": "timeout"},
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Failed to connect to Lusha API: {e}",
|
||||
details={"error": str(e)},
|
||||
)
|
||||
|
||||
|
||||
class GoogleGmailHealthChecker(OAuthBearerHealthChecker):
|
||||
"""Health checker for Google Gmail OAuth tokens."""
|
||||
|
||||
@@ -1068,30 +1260,164 @@ class IntercomHealthChecker(OAuthBearerHealthChecker):
|
||||
)
|
||||
|
||||
|
||||
# --- Simple Bearer-auth checkers ---
|
||||
|
||||
|
||||
class ApifyHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://api.apify.com/v2/users/me"
|
||||
SERVICE_NAME = "Apify"
|
||||
|
||||
|
||||
class AsanaHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://app.asana.com/api/1.0/users/me"
|
||||
SERVICE_NAME = "Asana"
|
||||
|
||||
|
||||
class AttioHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://api.attio.com/v2/workspace_members"
|
||||
SERVICE_NAME = "Attio"
|
||||
|
||||
|
||||
class DockerHubHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://hub.docker.com/v2/user/login"
|
||||
SERVICE_NAME = "Docker Hub"
|
||||
|
||||
|
||||
class GoogleSearchConsoleHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://www.googleapis.com/webmasters/v3/sites"
|
||||
SERVICE_NAME = "Google Search Console"
|
||||
|
||||
|
||||
class HuggingFaceHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://huggingface.co/api/whoami-v2"
|
||||
SERVICE_NAME = "Hugging Face"
|
||||
|
||||
|
||||
class LinearHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://api.linear.app/graphql"
|
||||
SERVICE_NAME = "Linear"
|
||||
|
||||
|
||||
class MicrosoftGraphHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://graph.microsoft.com/v1.0/me"
|
||||
SERVICE_NAME = "Microsoft Graph"
|
||||
|
||||
|
||||
class PineconeHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://api.pinecone.io/indexes"
|
||||
SERVICE_NAME = "Pinecone"
|
||||
|
||||
|
||||
class VercelHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://api.vercel.com/v2/user"
|
||||
SERVICE_NAME = "Vercel"
|
||||
|
||||
|
||||
# --- Custom-header auth checkers ---
|
||||
|
||||
|
||||
class GitLabHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://gitlab.com/api/v4/user"
|
||||
SERVICE_NAME = "GitLab"
|
||||
AUTH_TYPE = BaseHttpHealthChecker.AUTH_HEADER
|
||||
AUTH_HEADER_NAME = "PRIVATE-TOKEN"
|
||||
AUTH_HEADER_TEMPLATE = "{token}"
|
||||
|
||||
|
||||
class NotionHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://api.notion.com/v1/users/me"
|
||||
SERVICE_NAME = "Notion"
|
||||
|
||||
def _build_headers(self, credential_value: str) -> dict[str, str]:
|
||||
headers = super()._build_headers(credential_value)
|
||||
headers["Notion-Version"] = "2022-06-28"
|
||||
return headers
|
||||
|
||||
|
||||
# --- Basic-auth checkers ---
|
||||
|
||||
|
||||
class GreenhouseHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://harvest.greenhouse.io/v1/jobs?per_page=1"
|
||||
SERVICE_NAME = "Greenhouse"
|
||||
AUTH_TYPE = BaseHttpHealthChecker.AUTH_BASIC
|
||||
|
||||
|
||||
# --- Query-param auth checkers ---
|
||||
|
||||
|
||||
class PipedriveHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://api.pipedrive.com/v1/users/me"
|
||||
SERVICE_NAME = "Pipedrive"
|
||||
AUTH_TYPE = BaseHttpHealthChecker.AUTH_QUERY
|
||||
AUTH_QUERY_PARAM_NAME = "api_token"
|
||||
|
||||
|
||||
class TrelloKeyHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://api.trello.com/1/members/me"
|
||||
SERVICE_NAME = "Trello"
|
||||
AUTH_TYPE = BaseHttpHealthChecker.AUTH_QUERY
|
||||
AUTH_QUERY_PARAM_NAME = "key"
|
||||
|
||||
|
||||
class TrelloTokenHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://api.trello.com/1/members/me"
|
||||
SERVICE_NAME = "Trello"
|
||||
AUTH_TYPE = BaseHttpHealthChecker.AUTH_QUERY
|
||||
AUTH_QUERY_PARAM_NAME = "token"
|
||||
|
||||
|
||||
class YouTubeHealthChecker(BaseHttpHealthChecker):
|
||||
ENDPOINT = "https://www.googleapis.com/youtube/v3/videoCategories?part=snippet®ionCode=US"
|
||||
SERVICE_NAME = "YouTube"
|
||||
AUTH_TYPE = BaseHttpHealthChecker.AUTH_QUERY
|
||||
AUTH_QUERY_PARAM_NAME = "key"
|
||||
|
||||
|
||||
# Registry of health checkers
|
||||
HEALTH_CHECKERS: dict[str, CredentialHealthChecker] = {
|
||||
"discord": DiscordHealthChecker(),
|
||||
"hubspot": HubSpotHealthChecker(),
|
||||
"brave_search": BraveSearchHealthChecker(),
|
||||
"google_calendar_oauth": GoogleCalendarHealthChecker(),
|
||||
"google": GoogleGmailHealthChecker(),
|
||||
"slack": SlackHealthChecker(),
|
||||
"google_search": GoogleSearchHealthChecker(),
|
||||
"google_maps": GoogleMapsHealthChecker(),
|
||||
"anthropic": AnthropicHealthChecker(),
|
||||
"github": GitHubHealthChecker(),
|
||||
"intercom": IntercomHealthChecker(),
|
||||
"resend": ResendHealthChecker(),
|
||||
"stripe": StripeHealthChecker(),
|
||||
"exa_search": ExaSearchHealthChecker(),
|
||||
"google_docs": GoogleDocsHealthChecker(),
|
||||
"calcom": CalcomHealthChecker(),
|
||||
"serpapi": SerpApiHealthChecker(),
|
||||
"apify": ApifyHealthChecker(),
|
||||
"apollo": ApolloHealthChecker(),
|
||||
"telegram": TelegramHealthChecker(),
|
||||
"newsdata": NewsdataHealthChecker(),
|
||||
"finlight": FinlightHealthChecker(),
|
||||
"asana": AsanaHealthChecker(),
|
||||
"attio": AttioHealthChecker(),
|
||||
"brave_search": BraveSearchHealthChecker(),
|
||||
"brevo": BrevoHealthChecker(),
|
||||
"calcom": CalcomHealthChecker(),
|
||||
"calendly_pat": CalendlyHealthChecker(),
|
||||
"discord": DiscordHealthChecker(),
|
||||
"docker_hub": DockerHubHealthChecker(),
|
||||
"exa_search": ExaSearchHealthChecker(),
|
||||
"finlight": FinlightHealthChecker(),
|
||||
"github": GitHubHealthChecker(),
|
||||
"gitlab_token": GitLabHealthChecker(),
|
||||
"google": GoogleGmailHealthChecker(),
|
||||
"google_calendar_oauth": GoogleCalendarHealthChecker(),
|
||||
"google_docs": GoogleDocsHealthChecker(),
|
||||
"google_maps": GoogleMapsHealthChecker(),
|
||||
"google_search": GoogleSearchHealthChecker(),
|
||||
"google_search_console": GoogleSearchConsoleHealthChecker(),
|
||||
"greenhouse_token": GreenhouseHealthChecker(),
|
||||
"hubspot": HubSpotHealthChecker(),
|
||||
"huggingface": HuggingFaceHealthChecker(),
|
||||
"intercom": IntercomHealthChecker(),
|
||||
"linear": LinearHealthChecker(),
|
||||
"lusha_api_key": LushaHealthChecker(),
|
||||
"microsoft_graph": MicrosoftGraphHealthChecker(),
|
||||
"newsdata": NewsdataHealthChecker(),
|
||||
"notion_token": NotionHealthChecker(),
|
||||
"pinecone": PineconeHealthChecker(),
|
||||
"pipedrive": PipedriveHealthChecker(),
|
||||
"resend": ResendHealthChecker(),
|
||||
"serpapi": SerpApiHealthChecker(),
|
||||
"slack": SlackHealthChecker(),
|
||||
"stripe": StripeHealthChecker(),
|
||||
"telegram": TelegramHealthChecker(),
|
||||
"trello_key": TrelloKeyHealthChecker(),
|
||||
"trello_token": TrelloTokenHealthChecker(),
|
||||
"vercel": VercelHealthChecker(),
|
||||
"youtube": YouTubeHealthChecker(),
|
||||
"zoho_crm": ZohoCRMHealthChecker(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
HuggingFace credentials.
|
||||
|
||||
Contains credentials for HuggingFace Hub API access.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
HUGGINGFACE_CREDENTIALS = {
|
||||
"huggingface": CredentialSpec(
|
||||
env_var="HUGGINGFACE_TOKEN",
|
||||
tools=[
|
||||
"huggingface_search_models",
|
||||
"huggingface_get_model",
|
||||
"huggingface_search_datasets",
|
||||
"huggingface_get_dataset",
|
||||
"huggingface_search_spaces",
|
||||
"huggingface_whoami",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://huggingface.co/settings/tokens",
|
||||
description="HuggingFace API token for Hub access (models, datasets, spaces)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a HuggingFace token:
|
||||
1. Go to https://huggingface.co/settings/tokens
|
||||
2. Click 'New token'
|
||||
3. Choose 'Read' access (or 'Write' for repo management)
|
||||
4. Copy the token
|
||||
5. Set the environment variable:
|
||||
export HUGGINGFACE_TOKEN=hf_your-token""",
|
||||
health_check_endpoint="https://huggingface.co/api/whoami-v2",
|
||||
credential_id="huggingface",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Integration credentials.
|
||||
|
||||
Contains credentials for third-party service integrations (HubSpot, Linear, etc.).
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
INTEGRATION_CREDENTIALS = {
|
||||
"github": CredentialSpec(
|
||||
env_var="GITHUB_TOKEN",
|
||||
tools=[
|
||||
"github_list_repos",
|
||||
"github_get_repo",
|
||||
"github_search_repos",
|
||||
"github_list_issues",
|
||||
"github_get_issue",
|
||||
"github_create_issue",
|
||||
"github_update_issue",
|
||||
"github_list_pull_requests",
|
||||
"github_get_pull_request",
|
||||
"github_create_pull_request",
|
||||
"github_search_code",
|
||||
"github_list_branches",
|
||||
"github_get_branch",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://github.com/settings/tokens",
|
||||
description="GitHub Personal Access Token (classic)",
|
||||
# Auth method support
|
||||
aden_supported=False,
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a GitHub Personal Access Token:
|
||||
1. Go to GitHub Settings > Developer settings > Personal access tokens
|
||||
2. Click "Generate new token" > "Generate new token (classic)"
|
||||
3. Give your token a descriptive name (e.g., "Hive Agent")
|
||||
4. Select the following scopes:
|
||||
- repo (Full control of private repositories)
|
||||
- read:org (Read org and team membership - optional)
|
||||
- user (Read user profile data - optional)
|
||||
5. Click "Generate token" and copy the token (starts with ghp_)
|
||||
6. Store it securely - you won't be able to see it again!""",
|
||||
# Health check configuration
|
||||
health_check_endpoint="https://api.github.com/user",
|
||||
health_check_method="GET",
|
||||
# Credential store mapping
|
||||
credential_id="github",
|
||||
credential_key="access_token",
|
||||
),
|
||||
"hubspot": CredentialSpec(
|
||||
env_var="HUBSPOT_ACCESS_TOKEN",
|
||||
tools=[
|
||||
"hubspot_search_contacts",
|
||||
"hubspot_get_contact",
|
||||
"hubspot_create_contact",
|
||||
"hubspot_update_contact",
|
||||
"hubspot_search_companies",
|
||||
"hubspot_get_company",
|
||||
"hubspot_create_company",
|
||||
"hubspot_update_company",
|
||||
"hubspot_search_deals",
|
||||
"hubspot_get_deal",
|
||||
"hubspot_create_deal",
|
||||
"hubspot_update_deal",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developers.hubspot.com/docs/api/private-apps",
|
||||
description="HubSpot access token (Private App or OAuth2)",
|
||||
# Auth method support
|
||||
aden_supported=True,
|
||||
aden_provider_name="hubspot",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a HubSpot Private App token:
|
||||
1. Go to HubSpot Settings > Integrations > Private Apps
|
||||
2. Click "Create a private app"
|
||||
3. Name your app (e.g., "Hive Agent")
|
||||
4. Go to the "Scopes" tab and enable:
|
||||
- crm.objects.contacts.read
|
||||
- crm.objects.contacts.write
|
||||
- crm.objects.companies.read
|
||||
- crm.objects.companies.write
|
||||
- crm.objects.deals.read
|
||||
- crm.objects.deals.write
|
||||
5. Click "Create app" and copy the access token""",
|
||||
# Health check configuration
|
||||
health_check_endpoint="https://api.hubapi.com/crm/v3/objects/contacts?limit=1",
|
||||
health_check_method="GET",
|
||||
# Credential store mapping
|
||||
credential_id="hubspot",
|
||||
credential_key="access_token",
|
||||
),
|
||||
"linear": CredentialSpec(
|
||||
env_var="LINEAR_API_KEY",
|
||||
tools=[
|
||||
"linear_issue_create",
|
||||
"linear_issue_get",
|
||||
"linear_issue_update",
|
||||
"linear_issue_delete",
|
||||
"linear_issue_search",
|
||||
"linear_issue_add_comment",
|
||||
"linear_project_create",
|
||||
"linear_project_get",
|
||||
"linear_project_update",
|
||||
"linear_project_list",
|
||||
"linear_teams_list",
|
||||
"linear_team_get",
|
||||
"linear_workflow_states_get",
|
||||
"linear_label_create",
|
||||
"linear_labels_list",
|
||||
"linear_users_list",
|
||||
"linear_user_get",
|
||||
"linear_viewer",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://linear.app/settings/api",
|
||||
description="Linear API key or OAuth2 token for project management integration",
|
||||
# Auth method support
|
||||
aden_supported=True,
|
||||
aden_provider_name="linear",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Linear API key:
|
||||
1. Go to Linear Settings > API (https://linear.app/settings/api)
|
||||
2. Click "Create key" under "Personal API keys"
|
||||
3. Give your key a descriptive label (e.g., "Hive Agent")
|
||||
4. Copy the generated key (starts with 'lin_api_')
|
||||
5. Store it securely - you won't be able to see it again!
|
||||
|
||||
Note: Personal API keys have the same permissions as your user account.
|
||||
|
||||
To create an OAuth application (for automatic token refresh via Aden):
|
||||
1. Go to Linear Settings > API (https://linear.app/settings/api)
|
||||
2. Click "New OAuth application"
|
||||
3. Fill in the required information:
|
||||
- Application name (e.g., "Hive Agent")
|
||||
- Developer name
|
||||
- Other required fields
|
||||
4. Click "Create"
|
||||
5. Copy your client ID and client secret""",
|
||||
# Health check configuration
|
||||
health_check_endpoint="https://api.linear.app/graphql",
|
||||
health_check_method="POST",
|
||||
# Credential store mapping
|
||||
credential_id="linear",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Jira credentials.
|
||||
|
||||
Contains credentials for Jira Cloud issue tracking.
|
||||
Requires JIRA_DOMAIN, JIRA_EMAIL, and JIRA_API_TOKEN.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
JIRA_CREDENTIALS = {
|
||||
"jira_domain": CredentialSpec(
|
||||
env_var="JIRA_DOMAIN",
|
||||
tools=[
|
||||
"jira_search_issues",
|
||||
"jira_get_issue",
|
||||
"jira_create_issue",
|
||||
"jira_list_projects",
|
||||
"jira_get_project",
|
||||
"jira_add_comment",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://id.atlassian.com/manage/api-tokens",
|
||||
description="Jira Cloud domain (e.g. your-org.atlassian.net)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Jira API access:
|
||||
1. Go to https://id.atlassian.com/manage/api-tokens
|
||||
2. Click 'Create API token'
|
||||
3. Set environment variables:
|
||||
export JIRA_DOMAIN=your-org.atlassian.net
|
||||
export JIRA_EMAIL=your-email@example.com
|
||||
export JIRA_API_TOKEN=your-api-token""",
|
||||
health_check_endpoint="",
|
||||
credential_id="jira_domain",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"jira_email": CredentialSpec(
|
||||
env_var="JIRA_EMAIL",
|
||||
tools=[
|
||||
"jira_search_issues",
|
||||
"jira_get_issue",
|
||||
"jira_create_issue",
|
||||
"jira_list_projects",
|
||||
"jira_get_project",
|
||||
"jira_add_comment",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://id.atlassian.com/manage/api-tokens",
|
||||
description="Atlassian account email for Jira authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See JIRA_DOMAIN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="jira_email",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"jira_token": CredentialSpec(
|
||||
env_var="JIRA_API_TOKEN",
|
||||
tools=[
|
||||
"jira_search_issues",
|
||||
"jira_get_issue",
|
||||
"jira_create_issue",
|
||||
"jira_list_projects",
|
||||
"jira_get_project",
|
||||
"jira_add_comment",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://id.atlassian.com/manage/api-tokens",
|
||||
description="Atlassian API token for Jira authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See JIRA_DOMAIN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="jira_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Apache Kafka (Confluent REST Proxy) credentials.
|
||||
|
||||
Contains credentials for the Kafka REST Proxy API.
|
||||
Requires KAFKA_REST_URL and KAFKA_CLUSTER_ID. Optional KAFKA_API_KEY + KAFKA_API_SECRET.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
KAFKA_CREDENTIALS = {
|
||||
"kafka_rest_url": CredentialSpec(
|
||||
env_var="KAFKA_REST_URL",
|
||||
tools=[
|
||||
"kafka_list_topics",
|
||||
"kafka_get_topic",
|
||||
"kafka_create_topic",
|
||||
"kafka_produce_message",
|
||||
"kafka_list_consumer_groups",
|
||||
"kafka_get_consumer_group_lag",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.confluent.io/platform/current/kafka-rest/index.html",
|
||||
description="Kafka REST Proxy URL (e.g. 'https://pkc-xxxxx.region.confluent.cloud:443')",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Kafka REST Proxy access:
|
||||
1. Get your REST Proxy URL (Confluent Cloud: cluster settings; self-hosted: default port 8082)
|
||||
2. Get your cluster ID from cluster settings
|
||||
3. Create an API key pair (Confluent Cloud) or configure SASL auth
|
||||
4. Set environment variables:
|
||||
export KAFKA_REST_URL=https://your-rest-proxy-url
|
||||
export KAFKA_CLUSTER_ID=your-cluster-id
|
||||
export KAFKA_API_KEY=your-api-key (optional)
|
||||
export KAFKA_API_SECRET=your-api-secret (optional)""",
|
||||
health_check_endpoint="",
|
||||
credential_id="kafka_rest_url",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"kafka_cluster_id": CredentialSpec(
|
||||
env_var="KAFKA_CLUSTER_ID",
|
||||
tools=[
|
||||
"kafka_list_topics",
|
||||
"kafka_get_topic",
|
||||
"kafka_create_topic",
|
||||
"kafka_produce_message",
|
||||
"kafka_list_consumer_groups",
|
||||
"kafka_get_consumer_group_lag",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.confluent.io/platform/current/kafka-rest/index.html",
|
||||
description="Kafka cluster ID",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See KAFKA_REST_URL instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="kafka_cluster_id",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Langfuse LLM observability credentials.
|
||||
|
||||
Contains credentials for the Langfuse REST API.
|
||||
Requires LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY.
|
||||
Optional LANGFUSE_HOST for self-hosted instances.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
LANGFUSE_CREDENTIALS = {
|
||||
"langfuse_public_key": CredentialSpec(
|
||||
env_var="LANGFUSE_PUBLIC_KEY",
|
||||
tools=[
|
||||
"langfuse_list_traces",
|
||||
"langfuse_get_trace",
|
||||
"langfuse_list_scores",
|
||||
"langfuse_create_score",
|
||||
"langfuse_list_prompts",
|
||||
"langfuse_get_prompt",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://langfuse.com/docs/api-and-data-platform/features/public-api",
|
||||
description="Langfuse public key (starts with pk-lf-)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Langfuse API access:
|
||||
1. Create a Langfuse account at https://cloud.langfuse.com
|
||||
2. Go to Project > Settings > API Keys
|
||||
3. Create a new key pair
|
||||
4. Set environment variables:
|
||||
export LANGFUSE_PUBLIC_KEY=pk-lf-your-public-key
|
||||
export LANGFUSE_SECRET_KEY=sk-lf-your-secret-key
|
||||
export LANGFUSE_HOST=https://cloud.langfuse.com (optional, for self-hosted)""",
|
||||
health_check_endpoint="",
|
||||
credential_id="langfuse_public_key",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"langfuse_secret_key": CredentialSpec(
|
||||
env_var="LANGFUSE_SECRET_KEY",
|
||||
tools=[
|
||||
"langfuse_list_traces",
|
||||
"langfuse_get_trace",
|
||||
"langfuse_list_scores",
|
||||
"langfuse_create_score",
|
||||
"langfuse_list_prompts",
|
||||
"langfuse_get_prompt",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://langfuse.com/docs/api-and-data-platform/features/public-api",
|
||||
description="Langfuse secret key (starts with sk-lf-)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See LANGFUSE_PUBLIC_KEY instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="langfuse_secret_key",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Linear credentials.
|
||||
|
||||
Contains credentials for Linear issue tracking and project management.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
LINEAR_CREDENTIALS = {
|
||||
"linear": CredentialSpec(
|
||||
env_var="LINEAR_API_KEY",
|
||||
tools=[
|
||||
"linear_issue_create",
|
||||
"linear_issue_get",
|
||||
"linear_issue_update",
|
||||
"linear_issue_delete",
|
||||
"linear_issue_search",
|
||||
"linear_issue_add_comment",
|
||||
"linear_project_create",
|
||||
"linear_project_get",
|
||||
"linear_project_update",
|
||||
"linear_project_list",
|
||||
"linear_teams_list",
|
||||
"linear_team_get",
|
||||
"linear_workflow_states_get",
|
||||
"linear_label_create",
|
||||
"linear_labels_list",
|
||||
"linear_users_list",
|
||||
"linear_user_get",
|
||||
"linear_viewer",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://linear.app/developers",
|
||||
description="Linear API key for issue tracking and project management",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Linear API key:
|
||||
1. Go to Linear Settings > Account > Security & Access
|
||||
2. Under 'Personal API Keys', click 'Create key'
|
||||
3. Choose permissions (Read + Write recommended)
|
||||
4. Copy the key
|
||||
5. Set the environment variable:
|
||||
export LINEAR_API_KEY=lin_api_your-key""",
|
||||
health_check_endpoint="https://api.linear.app/graphql",
|
||||
credential_id="linear",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Lusha credentials.
|
||||
|
||||
Contains credentials for the Lusha B2B data API.
|
||||
Requires LUSHA_API_KEY.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
LUSHA_CREDENTIALS = {
|
||||
"lusha_api_key": CredentialSpec(
|
||||
env_var="LUSHA_API_KEY",
|
||||
tools=[
|
||||
"lusha_enrich_person",
|
||||
"lusha_enrich_company",
|
||||
"lusha_search_contacts",
|
||||
"lusha_search_companies",
|
||||
"lusha_get_usage",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.lusha.com/",
|
||||
description="Lusha API key for B2B contact and company enrichment",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Lusha API access:
|
||||
1. Go to dashboard.lusha.com > Enrich > API
|
||||
2. Copy your API key
|
||||
3. Set environment variable:
|
||||
export LUSHA_API_KEY=your-api-key""",
|
||||
health_check_endpoint="https://api.lusha.com/account/usage",
|
||||
credential_id="lusha_api_key",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Microsoft Graph API credentials.
|
||||
|
||||
Contains credentials for Microsoft 365 services (Outlook, Teams, OneDrive).
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
MICROSOFT_GRAPH_CREDENTIALS = {
|
||||
"microsoft_graph": CredentialSpec(
|
||||
env_var="MICROSOFT_GRAPH_ACCESS_TOKEN",
|
||||
tools=[
|
||||
"outlook_list_messages",
|
||||
"outlook_get_message",
|
||||
"outlook_send_mail",
|
||||
"teams_list_teams",
|
||||
"teams_list_channels",
|
||||
"teams_send_channel_message",
|
||||
"teams_get_channel_messages",
|
||||
"onedrive_search_files",
|
||||
"onedrive_list_files",
|
||||
"onedrive_download_file",
|
||||
"onedrive_upload_file",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade",
|
||||
description="Microsoft Graph OAuth 2.0 access token for Outlook, Teams, and OneDrive",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Microsoft Graph access token:
|
||||
1. Go to https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
|
||||
2. Register a new application (or select existing)
|
||||
3. Under API Permissions, add Microsoft Graph permissions:
|
||||
- Mail.Read, Mail.Send (for Outlook)
|
||||
- ChannelMessage.Read.All, ChannelMessage.Send (for Teams)
|
||||
- Files.ReadWrite (for OneDrive)
|
||||
4. Configure Authentication with redirect URI
|
||||
5. Get client ID and client secret from Certificates & Secrets
|
||||
6. Use OAuth 2.0 authorization code flow to obtain access token
|
||||
7. For quick testing, use https://developer.microsoft.com/en-us/graph/graph-explorer""",
|
||||
health_check_endpoint="https://graph.microsoft.com/v1.0/me",
|
||||
credential_id="microsoft_graph",
|
||||
credential_key="access_token",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
MongoDB credentials.
|
||||
|
||||
Contains credentials for MongoDB Atlas Data API.
|
||||
Requires MONGODB_DATA_API_URL, MONGODB_API_KEY, and MONGODB_DATA_SOURCE.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
MONGODB_CREDENTIALS = {
|
||||
"mongodb_url": CredentialSpec(
|
||||
env_var="MONGODB_DATA_API_URL",
|
||||
tools=[
|
||||
"mongodb_find",
|
||||
"mongodb_find_one",
|
||||
"mongodb_insert_one",
|
||||
"mongodb_update_one",
|
||||
"mongodb_delete_one",
|
||||
"mongodb_aggregate",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://www.mongodb.com/docs/atlas/app-services/data-api/",
|
||||
description="MongoDB Atlas Data API URL (e.g. https://data.mongodb-api.com/app/APP_ID/endpoint/data/v1)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up MongoDB Atlas Data API access:
|
||||
1. Go to MongoDB Atlas > App Services > Data API
|
||||
2. Enable the Data API and copy the URL Endpoint
|
||||
3. Create an API key
|
||||
4. Set environment variables:
|
||||
export MONGODB_DATA_API_URL=your-data-api-url
|
||||
export MONGODB_API_KEY=your-api-key
|
||||
export MONGODB_DATA_SOURCE=Cluster0""",
|
||||
health_check_endpoint="",
|
||||
credential_id="mongodb_url",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"mongodb_api_key": CredentialSpec(
|
||||
env_var="MONGODB_API_KEY",
|
||||
tools=[
|
||||
"mongodb_find",
|
||||
"mongodb_find_one",
|
||||
"mongodb_insert_one",
|
||||
"mongodb_update_one",
|
||||
"mongodb_delete_one",
|
||||
"mongodb_aggregate",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://www.mongodb.com/docs/atlas/app-services/data-api/",
|
||||
description="MongoDB Atlas Data API key",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See MONGODB_DATA_API_URL instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="mongodb_api_key",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"mongodb_data_source": CredentialSpec(
|
||||
env_var="MONGODB_DATA_SOURCE",
|
||||
tools=[
|
||||
"mongodb_find",
|
||||
"mongodb_find_one",
|
||||
"mongodb_insert_one",
|
||||
"mongodb_update_one",
|
||||
"mongodb_delete_one",
|
||||
"mongodb_aggregate",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://www.mongodb.com/docs/atlas/app-services/data-api/",
|
||||
description="MongoDB cluster name (e.g. 'Cluster0')",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See MONGODB_DATA_API_URL instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="mongodb_data_source",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
n8n workflow automation credentials.
|
||||
|
||||
Contains credentials for the n8n REST API v1.
|
||||
Requires N8N_API_KEY and N8N_BASE_URL.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
N8N_CREDENTIALS = {
|
||||
"n8n": CredentialSpec(
|
||||
env_var="N8N_API_KEY",
|
||||
tools=[
|
||||
"n8n_list_workflows",
|
||||
"n8n_get_workflow",
|
||||
"n8n_activate_workflow",
|
||||
"n8n_deactivate_workflow",
|
||||
"n8n_list_executions",
|
||||
"n8n_get_execution",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.n8n.io/api/authentication/",
|
||||
description="n8n API key for workflow management",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up n8n API access:
|
||||
1. In n8n, go to Settings > API
|
||||
2. Generate an API key
|
||||
3. Set environment variables:
|
||||
export N8N_API_KEY=your-api-key
|
||||
export N8N_BASE_URL=https://your-n8n-instance.com""",
|
||||
health_check_endpoint="",
|
||||
credential_id="n8n",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"n8n_base_url": CredentialSpec(
|
||||
env_var="N8N_BASE_URL",
|
||||
tools=[
|
||||
"n8n_list_workflows",
|
||||
"n8n_get_workflow",
|
||||
"n8n_activate_workflow",
|
||||
"n8n_deactivate_workflow",
|
||||
"n8n_list_executions",
|
||||
"n8n_get_execution",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.n8n.io/api/",
|
||||
description="n8n instance base URL (e.g. 'https://your-n8n.example.com')",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See N8N_API_KEY instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="n8n_base_url",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Notion credentials.
|
||||
|
||||
Contains credentials for Notion pages, databases, and search.
|
||||
Requires NOTION_API_TOKEN.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
NOTION_CREDENTIALS = {
|
||||
"notion_token": CredentialSpec(
|
||||
env_var="NOTION_API_TOKEN",
|
||||
tools=[
|
||||
"notion_search",
|
||||
"notion_get_page",
|
||||
"notion_create_page",
|
||||
"notion_query_database",
|
||||
"notion_get_database",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://www.notion.so/my-integrations",
|
||||
description="Notion internal integration token",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Notion API access:
|
||||
1. Go to https://www.notion.so/my-integrations
|
||||
2. Click 'New integration'
|
||||
3. Give it a name, select the workspace, and set capabilities
|
||||
4. Copy the integration token
|
||||
5. Share target pages/databases with the integration
|
||||
6. Set environment variable:
|
||||
export NOTION_API_TOKEN=your-integration-token""",
|
||||
health_check_endpoint="https://api.notion.com/v1/users/me",
|
||||
credential_id="notion_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Obsidian Local REST API credentials.
|
||||
|
||||
Contains credentials for the Obsidian Local REST API plugin.
|
||||
Requires OBSIDIAN_REST_API_KEY. Optional OBSIDIAN_REST_BASE_URL.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
OBSIDIAN_CREDENTIALS = {
|
||||
"obsidian": CredentialSpec(
|
||||
env_var="OBSIDIAN_REST_API_KEY",
|
||||
tools=[
|
||||
"obsidian_read_note",
|
||||
"obsidian_write_note",
|
||||
"obsidian_append_note",
|
||||
"obsidian_search",
|
||||
"obsidian_list_files",
|
||||
"obsidian_get_active",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://github.com/coddingtonbear/obsidian-local-rest-api",
|
||||
description="Obsidian Local REST API key (64-char hex, from plugin settings)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Obsidian Local REST API access:
|
||||
1. Install the 'Local REST API' community plugin in Obsidian
|
||||
2. Enable the plugin and go to its settings
|
||||
3. Copy the API Key (64-character hex string)
|
||||
4. Set environment variables:
|
||||
export OBSIDIAN_REST_API_KEY=your-api-key
|
||||
export OBSIDIAN_REST_BASE_URL=https://127.0.0.1:27124 (optional)""",
|
||||
health_check_endpoint="",
|
||||
credential_id="obsidian",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
PagerDuty credentials.
|
||||
|
||||
Contains credentials for PagerDuty REST API v2.
|
||||
Requires PAGERDUTY_API_KEY and optionally PAGERDUTY_FROM_EMAIL.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
PAGERDUTY_CREDENTIALS = {
|
||||
"pagerduty_api_key": CredentialSpec(
|
||||
env_var="PAGERDUTY_API_KEY",
|
||||
tools=[
|
||||
"pagerduty_list_incidents",
|
||||
"pagerduty_get_incident",
|
||||
"pagerduty_create_incident",
|
||||
"pagerduty_update_incident",
|
||||
"pagerduty_list_services",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://support.pagerduty.com/docs/api-access-keys",
|
||||
description="PagerDuty REST API key (account-level or user-level)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up PagerDuty API access:
|
||||
1. Go to PagerDuty > Integrations > API Access Keys
|
||||
2. Create a new REST API key
|
||||
3. Set environment variables:
|
||||
export PAGERDUTY_API_KEY=your-api-key
|
||||
export PAGERDUTY_FROM_EMAIL=your-pagerduty-email@example.com""",
|
||||
health_check_endpoint="",
|
||||
credential_id="pagerduty_api_key",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"pagerduty_from_email": CredentialSpec(
|
||||
env_var="PAGERDUTY_FROM_EMAIL",
|
||||
tools=[
|
||||
"pagerduty_create_incident",
|
||||
"pagerduty_update_incident",
|
||||
],
|
||||
required=False,
|
||||
startup_required=False,
|
||||
help_url="https://support.pagerduty.com/docs/api-access-keys",
|
||||
description="PagerDuty user email (required for write operations)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See PAGERDUTY_API_KEY instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="pagerduty_from_email",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Pinecone credentials.
|
||||
|
||||
Contains credentials for Pinecone vector database operations.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
PINECONE_CREDENTIALS = {
|
||||
"pinecone": CredentialSpec(
|
||||
env_var="PINECONE_API_KEY",
|
||||
tools=[
|
||||
"pinecone_list_indexes",
|
||||
"pinecone_create_index",
|
||||
"pinecone_describe_index",
|
||||
"pinecone_delete_index",
|
||||
"pinecone_upsert_vectors",
|
||||
"pinecone_query_vectors",
|
||||
"pinecone_fetch_vectors",
|
||||
"pinecone_delete_vectors",
|
||||
"pinecone_index_stats",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://app.pinecone.io/",
|
||||
description="API key for Pinecone vector database operations",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Pinecone API key:
|
||||
1. Go to https://app.pinecone.io/ and sign up or log in
|
||||
2. Navigate to 'API Keys' in the left sidebar
|
||||
3. Click 'Create API Key' or copy the default key
|
||||
4. Set the environment variable:
|
||||
export PINECONE_API_KEY=your-api-key""",
|
||||
health_check_endpoint="https://api.pinecone.io/indexes",
|
||||
credential_id="pinecone",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
Pipedrive CRM credentials.
|
||||
|
||||
Contains credentials for Pipedrive deal, contact, and pipeline management.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
PIPEDRIVE_CREDENTIALS = {
|
||||
"pipedrive": CredentialSpec(
|
||||
env_var="PIPEDRIVE_API_TOKEN",
|
||||
tools=[
|
||||
"pipedrive_list_deals",
|
||||
"pipedrive_get_deal",
|
||||
"pipedrive_create_deal",
|
||||
"pipedrive_list_persons",
|
||||
"pipedrive_search_persons",
|
||||
"pipedrive_list_organizations",
|
||||
"pipedrive_list_activities",
|
||||
"pipedrive_list_pipelines",
|
||||
"pipedrive_list_stages",
|
||||
"pipedrive_add_note",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://pipedrive.readme.io/docs/core-api-concepts-about-pipedrive-api",
|
||||
description=(
|
||||
"Pipedrive API token for CRM management (also set PIPEDRIVE_DOMAIN for custom domains)"
|
||||
),
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Pipedrive API token:
|
||||
1. Log in to your Pipedrive account
|
||||
2. Go to Settings > Personal preferences > API
|
||||
3. Copy your personal API token
|
||||
4. Set environment variables:
|
||||
export PIPEDRIVE_API_TOKEN=your-api-token
|
||||
export PIPEDRIVE_DOMAIN=your-company.pipedrive.com""",
|
||||
health_check_endpoint="https://api.pipedrive.com/v1/users/me",
|
||||
credential_id="pipedrive",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Plaid credentials.
|
||||
|
||||
Contains credentials for Plaid banking & financial data operations.
|
||||
Plaid requires both PLAID_CLIENT_ID and PLAID_SECRET.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
PLAID_CREDENTIALS = {
|
||||
"plaid_client_id": CredentialSpec(
|
||||
env_var="PLAID_CLIENT_ID",
|
||||
tools=[
|
||||
"plaid_get_accounts",
|
||||
"plaid_get_balance",
|
||||
"plaid_sync_transactions",
|
||||
"plaid_get_transactions",
|
||||
"plaid_get_institution",
|
||||
"plaid_search_institutions",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://dashboard.plaid.com/developers/keys",
|
||||
description=(
|
||||
"Plaid client ID for banking data access"
|
||||
" (also set PLAID_SECRET and optionally PLAID_ENV)"
|
||||
),
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get Plaid credentials:
|
||||
1. Sign up at https://dashboard.plaid.com/
|
||||
2. Go to Developers > Keys
|
||||
3. Copy your client_id and secret
|
||||
4. Set environment variables:
|
||||
export PLAID_CLIENT_ID=your-client-id
|
||||
export PLAID_SECRET=your-secret
|
||||
export PLAID_ENV=sandbox (or development, production)""",
|
||||
health_check_endpoint="https://sandbox.plaid.com/institutions/search",
|
||||
credential_id="plaid_client_id",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"plaid_secret": CredentialSpec(
|
||||
env_var="PLAID_SECRET",
|
||||
tools=[
|
||||
"plaid_get_accounts",
|
||||
"plaid_get_balance",
|
||||
"plaid_sync_transactions",
|
||||
"plaid_get_transactions",
|
||||
"plaid_get_institution",
|
||||
"plaid_search_institutions",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://dashboard.plaid.com/developers/keys",
|
||||
description="Plaid API secret for banking data access",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See PLAID_CLIENT_ID instructions above.""",
|
||||
health_check_endpoint="https://sandbox.plaid.com/institutions/search",
|
||||
credential_id="plaid_secret",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Power BI credentials.
|
||||
|
||||
Contains credentials for the Microsoft Power BI REST API.
|
||||
Requires POWERBI_ACCESS_TOKEN (OAuth2 Bearer token).
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
POWERBI_CREDENTIALS = {
|
||||
"powerbi_token": CredentialSpec(
|
||||
env_var="POWERBI_ACCESS_TOKEN",
|
||||
tools=[
|
||||
"powerbi_list_workspaces",
|
||||
"powerbi_list_datasets",
|
||||
"powerbi_list_reports",
|
||||
"powerbi_refresh_dataset",
|
||||
"powerbi_get_refresh_history",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://learn.microsoft.com/en-us/rest/api/power-bi/",
|
||||
description="Power BI OAuth2 access token for API access",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Power BI API access:
|
||||
1. Register an app in Azure AD (Entra ID)
|
||||
2. Grant Power BI API permissions (Workspace.Read.All, Dataset.ReadWrite.All, Report.Read.All)
|
||||
3. Obtain an access token via client credentials or authorization code flow
|
||||
4. Set environment variable:
|
||||
export POWERBI_ACCESS_TOKEN=your-oauth-access-token""",
|
||||
health_check_endpoint="",
|
||||
credential_id="powerbi_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Pushover credentials.
|
||||
|
||||
Contains credentials for Pushover push notification service.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
PUSHOVER_CREDENTIALS = {
|
||||
"pushover": CredentialSpec(
|
||||
env_var="PUSHOVER_API_TOKEN",
|
||||
tools=[
|
||||
"pushover_send",
|
||||
"pushover_validate_user",
|
||||
"pushover_list_sounds",
|
||||
"pushover_check_receipt",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://pushover.net/apps/build",
|
||||
description="Pushover application API token",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Pushover API token:
|
||||
1. Go to https://pushover.net/ and create an account
|
||||
2. Go to https://pushover.net/apps/build
|
||||
3. Create a new application/API token
|
||||
4. Copy the API Token/Key
|
||||
5. Your User Key is on the main dashboard at https://pushover.net/
|
||||
6. Set environment variable:
|
||||
export PUSHOVER_API_TOKEN=your-app-token""",
|
||||
health_check_endpoint="",
|
||||
credential_id="pushover",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
QuickBooks Online credentials.
|
||||
|
||||
Contains credentials for QuickBooks Online Accounting API.
|
||||
Requires QUICKBOOKS_ACCESS_TOKEN and QUICKBOOKS_REALM_ID.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
QUICKBOOKS_CREDENTIALS = {
|
||||
"quickbooks_token": CredentialSpec(
|
||||
env_var="QUICKBOOKS_ACCESS_TOKEN",
|
||||
tools=[
|
||||
"quickbooks_query",
|
||||
"quickbooks_get_entity",
|
||||
"quickbooks_create_customer",
|
||||
"quickbooks_create_invoice",
|
||||
"quickbooks_get_company_info",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization",
|
||||
description="QuickBooks OAuth 2.0 access token",
|
||||
direct_api_key_supported=False,
|
||||
api_key_instructions="""To set up QuickBooks API access:
|
||||
1. Create an app at https://developer.intuit.com
|
||||
2. Complete OAuth 2.0 authorization flow
|
||||
3. Set environment variables:
|
||||
export QUICKBOOKS_ACCESS_TOKEN=your-oauth-access-token
|
||||
export QUICKBOOKS_REALM_ID=your-company-id
|
||||
export QUICKBOOKS_SANDBOX=true # optional, for sandbox""",
|
||||
health_check_endpoint="",
|
||||
credential_id="quickbooks_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"quickbooks_realm_id": CredentialSpec(
|
||||
env_var="QUICKBOOKS_REALM_ID",
|
||||
tools=[
|
||||
"quickbooks_query",
|
||||
"quickbooks_get_entity",
|
||||
"quickbooks_create_customer",
|
||||
"quickbooks_create_invoice",
|
||||
"quickbooks_get_company_info",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization",
|
||||
description="QuickBooks company (realm) ID",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See QUICKBOOKS_ACCESS_TOKEN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="quickbooks_realm_id",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Reddit credentials.
|
||||
|
||||
Contains credentials for Reddit community content monitoring and search.
|
||||
Requires REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
REDDIT_CREDENTIALS = {
|
||||
"reddit_client_id": CredentialSpec(
|
||||
env_var="REDDIT_CLIENT_ID",
|
||||
tools=[
|
||||
"reddit_search",
|
||||
"reddit_get_posts",
|
||||
"reddit_get_comments",
|
||||
"reddit_get_user",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://www.reddit.com/prefs/apps",
|
||||
description="Reddit app client ID for OAuth2 authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Reddit API access:
|
||||
1. Go to https://www.reddit.com/prefs/apps
|
||||
2. Click 'create another app...' at the bottom
|
||||
3. Select 'script' as the app type
|
||||
4. Fill in the name and redirect URI (http://localhost)
|
||||
5. Copy the client ID (under the app name) and secret
|
||||
6. Set environment variables:
|
||||
export REDDIT_CLIENT_ID=your-client-id
|
||||
export REDDIT_CLIENT_SECRET=your-client-secret""",
|
||||
health_check_endpoint="",
|
||||
credential_id="reddit_client_id",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"reddit_secret": CredentialSpec(
|
||||
env_var="REDDIT_CLIENT_SECRET",
|
||||
tools=[
|
||||
"reddit_search",
|
||||
"reddit_get_posts",
|
||||
"reddit_get_comments",
|
||||
"reddit_get_user",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://www.reddit.com/prefs/apps",
|
||||
description="Reddit app client secret for OAuth2 authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See REDDIT_CLIENT_ID instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="reddit_secret",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Redis credentials.
|
||||
|
||||
Contains credentials for Redis in-memory data store.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
REDIS_CREDENTIALS = {
|
||||
"redis": CredentialSpec(
|
||||
env_var="REDIS_URL",
|
||||
tools=[
|
||||
"redis_get",
|
||||
"redis_set",
|
||||
"redis_delete",
|
||||
"redis_keys",
|
||||
"redis_hset",
|
||||
"redis_hgetall",
|
||||
"redis_lpush",
|
||||
"redis_lrange",
|
||||
"redis_publish",
|
||||
"redis_info",
|
||||
"redis_ttl",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/",
|
||||
description="Redis connection URL (e.g. redis://localhost:6379 or redis://:password@host:6379/0)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Redis:
|
||||
1. Install Redis locally: brew install redis (macOS) or apt install redis-server (Linux)
|
||||
2. Or use a hosted service: Redis Cloud (https://redis.com/cloud/), Upstash, etc.
|
||||
3. Set the connection URL:
|
||||
export REDIS_URL=redis://localhost:6379
|
||||
export REDIS_URL=redis://:your-password@host:port/db-number""",
|
||||
health_check_endpoint="",
|
||||
credential_id="redis",
|
||||
credential_key="url",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Amazon Redshift Data API credentials.
|
||||
|
||||
Contains credentials for the Redshift Data API with SigV4 signing.
|
||||
Reuses AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
REDSHIFT_CREDENTIALS = {
|
||||
"redshift_access_key": CredentialSpec(
|
||||
env_var="AWS_ACCESS_KEY_ID",
|
||||
tools=[
|
||||
"redshift_execute_sql",
|
||||
"redshift_describe_statement",
|
||||
"redshift_get_results",
|
||||
"redshift_list_databases",
|
||||
"redshift_list_tables",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.aws.amazon.com/redshift/latest/mgmt/data-api.html",
|
||||
description="AWS Access Key ID for Redshift Data API access",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Redshift Data API access:
|
||||
1. Ensure your IAM user has redshift-data:* permissions
|
||||
2. Set environment variables:
|
||||
export AWS_ACCESS_KEY_ID=your-access-key-id
|
||||
export AWS_SECRET_ACCESS_KEY=your-secret-access-key
|
||||
export AWS_REGION=us-east-1""",
|
||||
health_check_endpoint="",
|
||||
credential_id="redshift_access_key",
|
||||
credential_key="api_key",
|
||||
credential_group="aws",
|
||||
),
|
||||
"redshift_secret_key": CredentialSpec(
|
||||
env_var="AWS_SECRET_ACCESS_KEY",
|
||||
tools=[
|
||||
"redshift_execute_sql",
|
||||
"redshift_describe_statement",
|
||||
"redshift_get_results",
|
||||
"redshift_list_databases",
|
||||
"redshift_list_tables",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.aws.amazon.com/redshift/latest/mgmt/data-api.html",
|
||||
description="AWS Secret Access Key for Redshift Data API access",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See AWS_ACCESS_KEY_ID instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="redshift_secret_key",
|
||||
credential_key="api_key",
|
||||
credential_group="aws",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Salesforce CRM credentials.
|
||||
|
||||
Contains credentials for the Salesforce REST API.
|
||||
Requires SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
SALESFORCE_CREDENTIALS = {
|
||||
"salesforce": CredentialSpec(
|
||||
env_var="SALESFORCE_ACCESS_TOKEN",
|
||||
tools=[
|
||||
"salesforce_soql_query",
|
||||
"salesforce_get_record",
|
||||
"salesforce_create_record",
|
||||
"salesforce_update_record",
|
||||
"salesforce_describe_object",
|
||||
"salesforce_list_objects",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest",
|
||||
description="Salesforce OAuth2 Bearer access token",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Salesforce REST API access:
|
||||
1. Create a Connected App in Salesforce Setup
|
||||
2. Enable OAuth settings and select required scopes (api, full)
|
||||
3. Use Client Credentials or Username-Password flow to obtain a token
|
||||
4. Set environment variables:
|
||||
export SALESFORCE_ACCESS_TOKEN=your-bearer-token
|
||||
export SALESFORCE_INSTANCE_URL=https://your-org.my.salesforce.com""",
|
||||
health_check_endpoint="",
|
||||
credential_id="salesforce",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"salesforce_instance_url": CredentialSpec(
|
||||
env_var="SALESFORCE_INSTANCE_URL",
|
||||
tools=[
|
||||
"salesforce_soql_query",
|
||||
"salesforce_get_record",
|
||||
"salesforce_create_record",
|
||||
"salesforce_update_record",
|
||||
"salesforce_describe_object",
|
||||
"salesforce_list_objects",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest",
|
||||
description="Salesforce instance URL (e.g. 'https://your-org.my.salesforce.com')",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See SALESFORCE_ACCESS_TOKEN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="salesforce_instance_url",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
SAP S/4HANA Cloud credentials.
|
||||
|
||||
Contains credentials for the SAP S/4HANA Cloud OData APIs.
|
||||
Requires SAP_BASE_URL, SAP_USERNAME, and SAP_PASSWORD.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
SAP_CREDENTIALS = {
|
||||
"sap_base_url": CredentialSpec(
|
||||
env_var="SAP_BASE_URL",
|
||||
tools=[
|
||||
"sap_list_purchase_orders",
|
||||
"sap_get_purchase_order",
|
||||
"sap_list_business_partners",
|
||||
"sap_list_products",
|
||||
"sap_list_sales_orders",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://api.sap.com/package/SAPS4HANACloud/odata",
|
||||
description="SAP S/4HANA Cloud base URL (e.g. 'https://tenant-api.s4hana.ondemand.com')",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up SAP S/4HANA Cloud API access:
|
||||
1. Create a Communication User in S/4HANA Cloud
|
||||
2. Set up Communication Arrangements for the APIs you need
|
||||
3. Set environment variables:
|
||||
export SAP_BASE_URL=https://your-tenant-api.s4hana.ondemand.com
|
||||
export SAP_USERNAME=your-communication-user
|
||||
export SAP_PASSWORD=your-password""",
|
||||
health_check_endpoint="",
|
||||
credential_id="sap_base_url",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"sap_username": CredentialSpec(
|
||||
env_var="SAP_USERNAME",
|
||||
tools=[
|
||||
"sap_list_purchase_orders",
|
||||
"sap_get_purchase_order",
|
||||
"sap_list_business_partners",
|
||||
"sap_list_products",
|
||||
"sap_list_sales_orders",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://api.sap.com/package/SAPS4HANACloud/odata",
|
||||
description="SAP S/4HANA Communication User username",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See SAP_BASE_URL instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="sap_username",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"sap_password": CredentialSpec(
|
||||
env_var="SAP_PASSWORD",
|
||||
tools=[
|
||||
"sap_list_purchase_orders",
|
||||
"sap_get_purchase_order",
|
||||
"sap_list_business_partners",
|
||||
"sap_list_products",
|
||||
"sap_list_sales_orders",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://api.sap.com/package/SAPS4HANACloud/odata",
|
||||
description="SAP S/4HANA Communication User password",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See SAP_BASE_URL instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="sap_password",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -84,7 +84,7 @@ def check_env_var_in_shell_config(
|
||||
if not config_path.exists():
|
||||
return False, None
|
||||
|
||||
content = config_path.read_text()
|
||||
content = config_path.read_text(encoding="utf-8")
|
||||
|
||||
# Look for export ENV_VAR=value or export ENV_VAR="value"
|
||||
pattern = rf"^export\s+{re.escape(env_var)}=(.+)$"
|
||||
@@ -130,7 +130,7 @@ def add_env_var_to_shell_config(
|
||||
|
||||
try:
|
||||
if config_path.exists():
|
||||
content = config_path.read_text()
|
||||
content = config_path.read_text(encoding="utf-8")
|
||||
|
||||
# Check if already exists
|
||||
pattern = rf"^export\s+{re.escape(env_var)}=.*$"
|
||||
@@ -142,11 +142,11 @@ def add_env_var_to_shell_config(
|
||||
content,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
config_path.write_text(new_content)
|
||||
config_path.write_text(new_content, encoding="utf-8")
|
||||
return True, str(config_path)
|
||||
|
||||
# Append to file
|
||||
with open(config_path, "a") as f:
|
||||
with open(config_path, "a", encoding="utf-8") as f:
|
||||
f.write(f"\n# {comment}\n")
|
||||
f.write(f"{export_line}\n")
|
||||
|
||||
@@ -178,7 +178,7 @@ def remove_env_var_from_shell_config(
|
||||
return True, "Config file does not exist"
|
||||
|
||||
try:
|
||||
content = config_path.read_text()
|
||||
content = config_path.read_text(encoding="utf-8")
|
||||
lines = content.split("\n")
|
||||
|
||||
new_lines = []
|
||||
@@ -206,7 +206,7 @@ def remove_env_var_from_shell_config(
|
||||
|
||||
new_lines.append(line)
|
||||
|
||||
config_path.write_text("\n".join(new_lines))
|
||||
config_path.write_text("\n".join(new_lines), encoding="utf-8")
|
||||
return True, str(config_path)
|
||||
|
||||
except PermissionError:
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Shopify Admin REST API credentials.
|
||||
|
||||
Contains credentials for the Shopify Admin API.
|
||||
Requires SHOPIFY_ACCESS_TOKEN and SHOPIFY_STORE_NAME.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
SHOPIFY_CREDENTIALS = {
|
||||
"shopify": CredentialSpec(
|
||||
env_var="SHOPIFY_ACCESS_TOKEN",
|
||||
tools=[
|
||||
"shopify_list_orders",
|
||||
"shopify_get_order",
|
||||
"shopify_list_products",
|
||||
"shopify_get_product",
|
||||
"shopify_list_customers",
|
||||
"shopify_search_customers",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://shopify.dev/docs/api/admin-rest",
|
||||
description="Shopify Admin API access token (starts with shpat_)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Shopify Admin API access:
|
||||
1. In Shopify Admin, go to Settings > Apps and sales channels > Develop apps
|
||||
2. Create a custom app with scopes: read_orders, read_products, read_customers
|
||||
3. Install the app and reveal the Admin API access token
|
||||
4. Set environment variables:
|
||||
export SHOPIFY_ACCESS_TOKEN=shpat_your-token
|
||||
export SHOPIFY_STORE_NAME=your-store-name""",
|
||||
health_check_endpoint="",
|
||||
credential_id="shopify",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"shopify_store_name": CredentialSpec(
|
||||
env_var="SHOPIFY_STORE_NAME",
|
||||
tools=[
|
||||
"shopify_list_orders",
|
||||
"shopify_get_order",
|
||||
"shopify_list_products",
|
||||
"shopify_get_product",
|
||||
"shopify_list_customers",
|
||||
"shopify_search_customers",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://shopify.dev/docs/api/admin-rest",
|
||||
description="Shopify store subdomain (e.g. 'my-store' from my-store.myshopify.com)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See SHOPIFY_ACCESS_TOKEN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="shopify_store_name",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Snowflake credentials.
|
||||
|
||||
Contains credentials for the Snowflake SQL REST API.
|
||||
Requires SNOWFLAKE_ACCOUNT and SNOWFLAKE_TOKEN.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
SNOWFLAKE_CREDENTIALS = {
|
||||
"snowflake_account": CredentialSpec(
|
||||
env_var="SNOWFLAKE_ACCOUNT",
|
||||
tools=[
|
||||
"snowflake_execute_sql",
|
||||
"snowflake_get_statement_status",
|
||||
"snowflake_cancel_statement",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.snowflake.com/en/developer-guide/sql-api/index",
|
||||
description="Snowflake account identifier (e.g. 'xy12345.us-east-1')",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Snowflake SQL API access:
|
||||
1. Get your Snowflake account identifier from your account URL
|
||||
2. Generate a JWT or OAuth token for authentication
|
||||
3. Set environment variables:
|
||||
export SNOWFLAKE_ACCOUNT=your-account-id
|
||||
export SNOWFLAKE_TOKEN=your-jwt-or-oauth-token
|
||||
export SNOWFLAKE_WAREHOUSE=your-warehouse (optional)
|
||||
export SNOWFLAKE_DATABASE=your-database (optional)""",
|
||||
health_check_endpoint="",
|
||||
credential_id="snowflake_account",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"snowflake_token": CredentialSpec(
|
||||
env_var="SNOWFLAKE_TOKEN",
|
||||
tools=[
|
||||
"snowflake_execute_sql",
|
||||
"snowflake_get_statement_status",
|
||||
"snowflake_cancel_statement",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://docs.snowflake.com/en/developer-guide/sql-api/authenticating",
|
||||
description="Snowflake JWT or OAuth token for API authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See SNOWFLAKE_ACCOUNT instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="snowflake_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Supabase credentials.
|
||||
|
||||
Contains credentials for Supabase database, auth, and edge functions.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
SUPABASE_CREDENTIALS = {
|
||||
"supabase": CredentialSpec(
|
||||
env_var="SUPABASE_ANON_KEY",
|
||||
tools=[
|
||||
"supabase_select",
|
||||
"supabase_insert",
|
||||
"supabase_update",
|
||||
"supabase_delete",
|
||||
"supabase_auth_signup",
|
||||
"supabase_auth_signin",
|
||||
"supabase_edge_invoke",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://supabase.com/dashboard",
|
||||
description="Supabase anon/public API key (also requires SUPABASE_URL env var)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get Supabase credentials:
|
||||
1. Go to https://supabase.com/dashboard
|
||||
2. Create a new project or select an existing one
|
||||
3. Go to Project Settings → API
|
||||
4. Copy the 'anon' / 'public' key (starts with eyJ...)
|
||||
5. Copy the Project URL (https://<ref>.supabase.co)
|
||||
6. Set both environment variables:
|
||||
export SUPABASE_ANON_KEY=your-anon-key
|
||||
export SUPABASE_URL=https://your-project.supabase.co""",
|
||||
health_check_endpoint="",
|
||||
credential_id="supabase",
|
||||
credential_key="anon_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Terraform Cloud / HCP Terraform credentials.
|
||||
|
||||
Contains credentials for the Terraform Cloud REST API v2.
|
||||
Requires TFC_TOKEN.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
TERRAFORM_CREDENTIALS = {
|
||||
"tfc_token": CredentialSpec(
|
||||
env_var="TFC_TOKEN",
|
||||
tools=[
|
||||
"terraform_list_workspaces",
|
||||
"terraform_get_workspace",
|
||||
"terraform_list_runs",
|
||||
"terraform_get_run",
|
||||
"terraform_create_run",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/api-tokens",
|
||||
description="Terraform Cloud API token (User or Team token)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Terraform Cloud API access:
|
||||
1. Go to app.terraform.io > User Settings > Tokens
|
||||
2. Create a new API token
|
||||
3. Set environment variable:
|
||||
export TFC_TOKEN=your-api-token
|
||||
(Optional for Terraform Enterprise: export TFC_URL=https://your-host.example.com)""",
|
||||
health_check_endpoint="",
|
||||
credential_id="tfc_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Tines credentials.
|
||||
|
||||
Contains credentials for the Tines security automation API.
|
||||
Requires TINES_DOMAIN and TINES_API_KEY.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
TINES_CREDENTIALS = {
|
||||
"tines_domain": CredentialSpec(
|
||||
env_var="TINES_DOMAIN",
|
||||
tools=[
|
||||
"tines_list_stories",
|
||||
"tines_get_story",
|
||||
"tines_list_actions",
|
||||
"tines_get_action",
|
||||
"tines_get_action_logs",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://www.tines.com/api/authentication/",
|
||||
description="Tines tenant domain (e.g. 'your-tenant.tines.com')",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Tines API access:
|
||||
1. Go to your Tines tenant > Settings > API Keys
|
||||
2. Create a new API key
|
||||
3. Set environment variables:
|
||||
export TINES_DOMAIN=your-tenant.tines.com
|
||||
export TINES_API_KEY=your-api-key""",
|
||||
health_check_endpoint="",
|
||||
credential_id="tines_domain",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"tines_api_key": CredentialSpec(
|
||||
env_var="TINES_API_KEY",
|
||||
tools=[
|
||||
"tines_list_stories",
|
||||
"tines_get_story",
|
||||
"tines_list_actions",
|
||||
"tines_get_action",
|
||||
"tines_get_action_logs",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://www.tines.com/api/authentication/",
|
||||
description="Tines API key for authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See TINES_DOMAIN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="tines_api_key",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Trello credentials.
|
||||
|
||||
Contains credentials for Trello board, list, and card management.
|
||||
Trello requires both TRELLO_API_KEY and TRELLO_TOKEN.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
TRELLO_CREDENTIALS = {
|
||||
"trello_key": CredentialSpec(
|
||||
env_var="TRELLO_API_KEY",
|
||||
tools=[
|
||||
"trello_list_boards",
|
||||
"trello_get_member",
|
||||
"trello_list_lists",
|
||||
"trello_list_cards",
|
||||
"trello_create_card",
|
||||
"trello_move_card",
|
||||
"trello_update_card",
|
||||
"trello_add_comment",
|
||||
"trello_add_attachment",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://trello.com/power-ups/admin",
|
||||
description="Trello API key (also set TRELLO_TOKEN for authentication)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get Trello credentials:
|
||||
1. Go to https://trello.com/power-ups/admin
|
||||
2. Select your Power-Up or create one
|
||||
3. Copy the API Key
|
||||
4. Generate a token via the authorize URL
|
||||
5. Set environment variables:
|
||||
export TRELLO_API_KEY=your-api-key
|
||||
export TRELLO_TOKEN=your-token""",
|
||||
health_check_endpoint="https://api.trello.com/1/members/me",
|
||||
credential_id="trello_key",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"trello_token": CredentialSpec(
|
||||
env_var="TRELLO_API_TOKEN",
|
||||
tools=[
|
||||
"trello_list_boards",
|
||||
"trello_get_member",
|
||||
"trello_list_lists",
|
||||
"trello_list_cards",
|
||||
"trello_create_card",
|
||||
"trello_move_card",
|
||||
"trello_update_card",
|
||||
"trello_add_comment",
|
||||
"trello_add_attachment",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://trello.com/power-ups/admin",
|
||||
description="Trello API token for authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See TRELLO_API_KEY instructions above.""",
|
||||
health_check_endpoint="https://api.trello.com/1/members/me",
|
||||
credential_id="trello_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Twilio credentials.
|
||||
|
||||
Contains credentials for Twilio SMS & WhatsApp messaging.
|
||||
Requires TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
TWILIO_CREDENTIALS = {
|
||||
"twilio_sid": CredentialSpec(
|
||||
env_var="TWILIO_ACCOUNT_SID",
|
||||
tools=[
|
||||
"twilio_send_sms",
|
||||
"twilio_send_whatsapp",
|
||||
"twilio_list_messages",
|
||||
"twilio_get_message",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://console.twilio.com/",
|
||||
description="Twilio Account SID (starts with AC)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Twilio API access:
|
||||
1. Go to https://console.twilio.com/
|
||||
2. Copy your Account SID and Auth Token from the dashboard
|
||||
3. Set environment variables:
|
||||
export TWILIO_ACCOUNT_SID=your-account-sid
|
||||
export TWILIO_AUTH_TOKEN=your-auth-token""",
|
||||
health_check_endpoint="",
|
||||
credential_id="twilio_sid",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"twilio_token": CredentialSpec(
|
||||
env_var="TWILIO_AUTH_TOKEN",
|
||||
tools=[
|
||||
"twilio_send_sms",
|
||||
"twilio_send_whatsapp",
|
||||
"twilio_list_messages",
|
||||
"twilio_get_message",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://console.twilio.com/",
|
||||
description="Twilio Auth Token for API authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See TWILIO_ACCOUNT_SID instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="twilio_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Twitter/X credentials.
|
||||
|
||||
Contains credentials for X API v2.
|
||||
Requires X_BEARER_TOKEN for read-only access.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
TWITTER_CREDENTIALS = {
|
||||
"x_bearer_token": CredentialSpec(
|
||||
env_var="X_BEARER_TOKEN",
|
||||
tools=[
|
||||
"twitter_search_tweets",
|
||||
"twitter_get_user",
|
||||
"twitter_get_user_tweets",
|
||||
"twitter_get_tweet",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.x.com/en/portal/dashboard",
|
||||
description="X/Twitter API v2 Bearer Token (app-only, read access)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up X/Twitter API access:
|
||||
1. Go to https://developer.x.com/en/portal/dashboard
|
||||
2. Create a Project and App
|
||||
3. Copy the Bearer Token from the Keys and Tokens tab
|
||||
4. Set environment variable:
|
||||
export X_BEARER_TOKEN=your-bearer-token""",
|
||||
health_check_endpoint="",
|
||||
credential_id="x_bearer_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Vercel credentials.
|
||||
|
||||
Contains credentials for Vercel deployment and hosting management.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
VERCEL_CREDENTIALS = {
|
||||
"vercel": CredentialSpec(
|
||||
env_var="VERCEL_TOKEN",
|
||||
tools=[
|
||||
"vercel_list_deployments",
|
||||
"vercel_get_deployment",
|
||||
"vercel_list_projects",
|
||||
"vercel_get_project",
|
||||
"vercel_list_project_domains",
|
||||
"vercel_list_env_vars",
|
||||
"vercel_create_env_var",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://vercel.com/account/tokens",
|
||||
description="Vercel access token for deployment and project management",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a Vercel access token:
|
||||
1. Go to https://vercel.com/account/tokens
|
||||
2. Click 'Create' to generate a new token
|
||||
3. Give it a name and set the scope (Full Account recommended)
|
||||
4. Copy the token
|
||||
5. Set the environment variable:
|
||||
export VERCEL_TOKEN=your-token""",
|
||||
health_check_endpoint="https://api.vercel.com/v2/user",
|
||||
credential_id="vercel",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
X (Twitter) tool credentials.
|
||||
|
||||
Contains credentials for X API v2 integration.
|
||||
Bearer token for read-only operations, OAuth 1.0a keys for write operations.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
_X_TOOLS = [
|
||||
"x_post_tweet",
|
||||
"x_reply_tweet",
|
||||
"x_delete_tweet",
|
||||
"x_search_tweets",
|
||||
"x_get_mentions",
|
||||
"x_send_dm",
|
||||
]
|
||||
|
||||
X_CREDENTIALS = {
|
||||
"x_bearer_token": CredentialSpec(
|
||||
env_var="X_BEARER_TOKEN",
|
||||
tools=_X_TOOLS,
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.x.com/en/portal/dashboard",
|
||||
description="X (Twitter) API v2 Bearer Token for read-only operations",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get an X API Bearer Token:
|
||||
1. Go to https://developer.x.com/en/portal/dashboard
|
||||
2. Create a Project & App (or select existing)
|
||||
3. Go to Keys & Tokens tab
|
||||
4. Copy the Bearer Token
|
||||
5. Set it as X_BEARER_TOKEN environment variable""",
|
||||
health_check_endpoint="https://api.x.com/2/users/me",
|
||||
health_check_method="GET",
|
||||
credential_id="x_bearer_token",
|
||||
credential_key="api_key",
|
||||
credential_group="x",
|
||||
),
|
||||
"x_api_key": CredentialSpec(
|
||||
env_var="X_API_KEY",
|
||||
tools=_X_TOOLS,
|
||||
required=False,
|
||||
startup_required=False,
|
||||
help_url="https://developer.x.com/en/portal/dashboard",
|
||||
description="X (Twitter) API Consumer Key for OAuth 1.0a write operations",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get your X API Consumer Key:
|
||||
1. Go to https://developer.x.com/en/portal/dashboard
|
||||
2. Select your app > Keys and Tokens
|
||||
3. Under Consumer Keys, copy the API Key""",
|
||||
credential_id="x_api_key",
|
||||
credential_key="api_key",
|
||||
credential_group="x",
|
||||
),
|
||||
"x_api_secret": CredentialSpec(
|
||||
env_var="X_API_SECRET",
|
||||
tools=_X_TOOLS,
|
||||
required=False,
|
||||
startup_required=False,
|
||||
help_url="https://developer.x.com/en/portal/dashboard",
|
||||
description="X (Twitter) API Consumer Secret for OAuth 1.0a write operations",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get your X API Consumer Secret:
|
||||
1. Go to https://developer.x.com/en/portal/dashboard
|
||||
2. Select your app > Keys and Tokens
|
||||
3. Under Consumer Keys, copy the API Secret""",
|
||||
credential_id="x_api_secret",
|
||||
credential_key="api_key",
|
||||
credential_group="x",
|
||||
),
|
||||
"x_access_token": CredentialSpec(
|
||||
env_var="X_ACCESS_TOKEN",
|
||||
tools=_X_TOOLS,
|
||||
required=False,
|
||||
startup_required=False,
|
||||
help_url="https://developer.x.com/en/portal/dashboard",
|
||||
description="X (Twitter) User Access Token for OAuth 1.0a write operations",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get your X Access Token:
|
||||
1. Go to https://developer.x.com/en/portal/dashboard
|
||||
2. Select your app > Keys and Tokens
|
||||
3. Under Authentication Tokens, generate Access Token and Secret
|
||||
4. Copy the Access Token""",
|
||||
credential_id="x_access_token",
|
||||
credential_key="api_key",
|
||||
credential_group="x",
|
||||
),
|
||||
"x_access_token_secret": CredentialSpec(
|
||||
env_var="X_ACCESS_TOKEN_SECRET",
|
||||
tools=_X_TOOLS,
|
||||
required=False,
|
||||
startup_required=False,
|
||||
help_url="https://developer.x.com/en/portal/dashboard",
|
||||
description="X (Twitter) User Access Token Secret for OAuth 1.0a write operations",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get your X Access Token Secret:
|
||||
1. Go to https://developer.x.com/en/portal/dashboard
|
||||
2. Select your app > Keys and Tokens
|
||||
3. Under Authentication Tokens, generate Access Token and Secret
|
||||
4. Copy the Access Token Secret""",
|
||||
credential_id="x_access_token_secret",
|
||||
credential_key="api_key",
|
||||
credential_group="x",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
YouTube Data API credentials.
|
||||
|
||||
Contains credentials for YouTube Data API v3 integration.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
YOUTUBE_CREDENTIALS = {
|
||||
"youtube": CredentialSpec(
|
||||
env_var="YOUTUBE_API_KEY",
|
||||
tools=[
|
||||
"youtube_search_videos",
|
||||
"youtube_get_video_details",
|
||||
"youtube_get_channel",
|
||||
"youtube_list_channel_videos",
|
||||
"youtube_get_playlist",
|
||||
"youtube_search_channels",
|
||||
"youtube_get_video_comments",
|
||||
"youtube_get_video_categories",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://console.cloud.google.com/apis/credentials",
|
||||
description="Google API key with YouTube Data API v3 enabled",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To get a YouTube Data API key:
|
||||
1. Go to https://console.cloud.google.com/
|
||||
2. Create a new project or select an existing one
|
||||
3. Go to APIs & Services > Library
|
||||
4. Search for "YouTube Data API v3" and enable it
|
||||
5. Go to APIs & Services > Credentials
|
||||
6. Click "Create Credentials" > "API key"
|
||||
7. Copy the API key
|
||||
8. (Optional) Restrict the key to YouTube Data API v3 only""",
|
||||
health_check_endpoint="https://www.googleapis.com/youtube/v3/videoCategories?part=snippet®ionCode=US",
|
||||
credential_id="youtube",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Zendesk credentials.
|
||||
|
||||
Contains credentials for Zendesk Support ticket management.
|
||||
Requires ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, and ZENDESK_API_TOKEN.
|
||||
"""
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
ZENDESK_CREDENTIALS = {
|
||||
"zendesk_subdomain": CredentialSpec(
|
||||
env_var="ZENDESK_SUBDOMAIN",
|
||||
tools=[
|
||||
"zendesk_list_tickets",
|
||||
"zendesk_get_ticket",
|
||||
"zendesk_create_ticket",
|
||||
"zendesk_update_ticket",
|
||||
"zendesk_search_tickets",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.zendesk.com/api-reference/introduction/security-and-auth/",
|
||||
description="Zendesk subdomain (e.g. 'acme' from acme.zendesk.com)",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""To set up Zendesk API access:
|
||||
1. Go to Zendesk Admin > Apps and integrations > APIs > Zendesk API
|
||||
2. Enable Token Access and create an API token
|
||||
3. Set environment variables:
|
||||
export ZENDESK_SUBDOMAIN=your-subdomain
|
||||
export ZENDESK_EMAIL=your-email@example.com
|
||||
export ZENDESK_API_TOKEN=your-api-token""",
|
||||
health_check_endpoint="",
|
||||
credential_id="zendesk_subdomain",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"zendesk_email": CredentialSpec(
|
||||
env_var="ZENDESK_EMAIL",
|
||||
tools=[
|
||||
"zendesk_list_tickets",
|
||||
"zendesk_get_ticket",
|
||||
"zendesk_create_ticket",
|
||||
"zendesk_update_ticket",
|
||||
"zendesk_search_tickets",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.zendesk.com/api-reference/introduction/security-and-auth/",
|
||||
description="Zendesk agent email for API authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See ZENDESK_SUBDOMAIN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="zendesk_email",
|
||||
credential_key="api_key",
|
||||
),
|
||||
"zendesk_token": CredentialSpec(
|
||||
env_var="ZENDESK_API_TOKEN",
|
||||
tools=[
|
||||
"zendesk_list_tickets",
|
||||
"zendesk_get_ticket",
|
||||
"zendesk_create_ticket",
|
||||
"zendesk_update_ticket",
|
||||
"zendesk_search_tickets",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://developer.zendesk.com/api-reference/introduction/security-and-auth/",
|
||||
description="Zendesk API token for authentication",
|
||||
direct_api_key_supported=True,
|
||||
api_key_instructions="""See ZENDESK_SUBDOMAIN instructions above.""",
|
||||
health_check_endpoint="",
|
||||
credential_id="zendesk_token",
|
||||
credential_key="api_key",
|
||||
),
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user