isa-mate 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- isa_mate/__init__.py +27 -0
- isa_mate/__main__.py +5 -0
- isa_mate/accounts/__init__.py +5 -0
- isa_mate/accounts/commands.py +394 -0
- isa_mate/agent/__init__.py +12 -0
- isa_mate/agent/config.py +1194 -0
- isa_mate/agent/delegation_directory.py +266 -0
- isa_mate/agent/multi_team_detection.py +145 -0
- isa_mate/agent/project_context.py +128 -0
- isa_mate/agent/research.py +197 -0
- isa_mate/agent/runtime.py +1861 -0
- isa_mate/agent/trading_contract.py +297 -0
- isa_mate/agent/trading_handoff.py +316 -0
- isa_mate/auth/__init__.py +20 -0
- isa_mate/auth/audit.py +79 -0
- isa_mate/auth/authorization.py +46 -0
- isa_mate/auth/clients.py +150 -0
- isa_mate/auth/middleware.py +214 -0
- isa_mate/auth/models.py +51 -0
- isa_mate/auth/scopes.py +120 -0
- isa_mate/channels/__init__.py +35 -0
- isa_mate/channels/discord/__init__.py +5 -0
- isa_mate/channels/discord/adapter.py +121 -0
- isa_mate/channels/discord/parser.py +146 -0
- isa_mate/channels/discord/sender.py +109 -0
- isa_mate/channels/feishu/__init__.py +5 -0
- isa_mate/channels/feishu/adapter.py +143 -0
- isa_mate/channels/feishu/parser.py +97 -0
- isa_mate/channels/feishu/sender.py +120 -0
- isa_mate/channels/feishu/signature.py +36 -0
- isa_mate/channels/google_chat/__init__.py +5 -0
- isa_mate/channels/google_chat/adapter.py +140 -0
- isa_mate/channels/imessage/__init__.py +5 -0
- isa_mate/channels/imessage/adapter.py +137 -0
- isa_mate/channels/line/__init__.py +5 -0
- isa_mate/channels/line/adapter.py +157 -0
- isa_mate/channels/matrix/__init__.py +5 -0
- isa_mate/channels/matrix/adapter.py +125 -0
- isa_mate/channels/matrix/parser.py +81 -0
- isa_mate/channels/matrix/sender.py +91 -0
- isa_mate/channels/msteams/__init__.py +5 -0
- isa_mate/channels/msteams/adapter.py +136 -0
- isa_mate/channels/msteams/parser.py +103 -0
- isa_mate/channels/msteams/sender.py +132 -0
- isa_mate/channels/runtime/__init__.py +30 -0
- isa_mate/channels/runtime/contracts.py +31 -0
- isa_mate/channels/runtime/metrics.py +182 -0
- isa_mate/channels/runtime/models.py +66 -0
- isa_mate/channels/runtime/replay.py +48 -0
- isa_mate/channels/runtime/store.py +369 -0
- isa_mate/channels/runtime/workers.py +462 -0
- isa_mate/channels/security/__init__.py +86 -0
- isa_mate/channels/security/channel_policy.py +439 -0
- isa_mate/channels/security/channel_policy_models.py +384 -0
- isa_mate/channels/security/pairing_store.py +652 -0
- isa_mate/channels/security/security_audit_log.py +321 -0
- isa_mate/channels/security/webhook_security_models.py +413 -0
- isa_mate/channels/security/webhook_signature.py +188 -0
- isa_mate/channels/signal/__init__.py +5 -0
- isa_mate/channels/signal/adapter.py +125 -0
- isa_mate/channels/signal/parser.py +72 -0
- isa_mate/channels/signal/sender.py +96 -0
- isa_mate/channels/slack/__init__.py +5 -0
- isa_mate/channels/slack/adapter.py +153 -0
- isa_mate/channels/slack/parser.py +95 -0
- isa_mate/channels/slack/sender.py +96 -0
- isa_mate/channels/slack/signature.py +61 -0
- isa_mate/channels/telegram/__init__.py +5 -0
- isa_mate/channels/telegram/adapter.py +390 -0
- isa_mate/channels/telegram/parser.py +107 -0
- isa_mate/channels/telegram/sender.py +95 -0
- isa_mate/channels/telegram_bridge.py +177 -0
- isa_mate/channels/whatsapp/__init__.py +5 -0
- isa_mate/channels/whatsapp/adapter.py +152 -0
- isa_mate/channels/whatsapp/parser.py +191 -0
- isa_mate/channels/whatsapp/sender.py +109 -0
- isa_mate/channels/whatsapp/signature.py +40 -0
- isa_mate/cli.py +1299 -0
- isa_mate/connectivity.py +295 -0
- isa_mate/credentials/__init__.py +5 -0
- isa_mate/credentials/cli.py +132 -0
- isa_mate/credentials/models.py +86 -0
- isa_mate/credentials/resolver.py +289 -0
- isa_mate/credentials/routes.py +185 -0
- isa_mate/desktop_agent.py +26 -0
- isa_mate/doctor/__init__.py +19 -0
- isa_mate/doctor/engine.py +1368 -0
- isa_mate/errors/__init__.py +38 -0
- isa_mate/errors/registry.py +467 -0
- isa_mate/errors/taxonomy.py +348 -0
- isa_mate/execution/__init__.py +44 -0
- isa_mate/execution/a2a_router.py +198 -0
- isa_mate/execution/agent_config_client.py +109 -0
- isa_mate/execution/audit_emitter.py +401 -0
- isa_mate/execution/autonomous.py +342 -0
- isa_mate/execution/autonomous_event_bus.py +285 -0
- isa_mate/execution/autonomous_reactive_router.py +1717 -0
- isa_mate/execution/browser_router.py +173 -0
- isa_mate/execution/chat_metrics.py +430 -0
- isa_mate/execution/chat_router.py +435 -0
- isa_mate/execution/dispatch_lifecycle.py +435 -0
- isa_mate/execution/dispatch_store.py +141 -0
- isa_mate/execution/hil_router.py +830 -0
- isa_mate/execution/idempotency.py +320 -0
- isa_mate/execution/notification_router.py +128 -0
- isa_mate/execution/observability_router.py +795 -0
- isa_mate/execution/persistence_router.py +2454 -0
- isa_mate/execution/proactive_router.py +1593 -0
- isa_mate/execution/response_filter.py +61 -0
- isa_mate/execution/responsive_publisher.py +488 -0
- isa_mate/execution/responsive_router.py +709 -0
- isa_mate/execution/stream_helpers.py +457 -0
- isa_mate/execution/swarm_router.py +188 -0
- isa_mate/execution/workflow_manager.py +125 -0
- isa_mate/execution/workflow_router.py +134 -0
- isa_mate/failover/__init__.py +33 -0
- isa_mate/failover/classifier.py +83 -0
- isa_mate/failover/cooldown.py +42 -0
- isa_mate/failover/exceptions.py +49 -0
- isa_mate/failover/health.py +142 -0
- isa_mate/failover/response_normalizer.py +82 -0
- isa_mate/failover/router.py +372 -0
- isa_mate/fault_injection/__init__.py +44 -0
- isa_mate/fault_injection/config.py +152 -0
- isa_mate/fault_injection/engine.py +397 -0
- isa_mate/fault_injection/injectors.py +275 -0
- isa_mate/gateway/__init__.py +6 -0
- isa_mate/gateway/catalog.py +440 -0
- isa_mate/gateway/cli_handler.py +117 -0
- isa_mate/gateway/pair_commands.py +489 -0
- isa_mate/gateway/rate_limit.py +203 -0
- isa_mate/gateway/rate_limit_middleware.py +110 -0
- isa_mate/gateway/rate_limit_policy.py +106 -0
- isa_mate/gateway/redis_rate_limiter.py +173 -0
- isa_mate/gateway/scope_dependency.py +78 -0
- isa_mate/gateway/server.py +2838 -0
- isa_mate/gateway/skills_routes.py +178 -0
- isa_mate/gateway/startup_validator.py +197 -0
- isa_mate/gateway/token_bucket.py +142 -0
- isa_mate/hub/__init__.py +17 -0
- isa_mate/hub/autonomy_hub.py +6 -0
- isa_mate/hub/base.py +4 -0
- isa_mate/hub/data_hub.py +8 -0
- isa_mate/hub/event_bus.py +4 -0
- isa_mate/hub/event_hub.py +9 -0
- isa_mate/hub/events.py +28 -0
- isa_mate/hub/hub.py +4 -0
- isa_mate/hub/interaction_hub.py +8 -0
- isa_mate/hub/memory_hub.py +7 -0
- isa_mate/hub/response_hub.py +16 -0
- isa_mate/logging/__init__.py +32 -0
- isa_mate/logging/structured.py +217 -0
- isa_mate/marketplace/__init__.py +1 -0
- isa_mate/marketplace/models.py +256 -0
- isa_mate/marketplace/pipeline.py +305 -0
- isa_mate/marketplace/repository.py +256 -0
- isa_mate/marketplace/router.py +112 -0
- isa_mate/marketplace/service.py +273 -0
- isa_mate/mate_agent.py +1256 -0
- isa_mate/mcp_server.py +154 -0
- isa_mate/memory/__init__.py +16 -0
- isa_mate/memory/adapters/__init__.py +5 -0
- isa_mate/memory/api.py +289 -0
- isa_mate/memory/benchmark.py +210 -0
- isa_mate/memory/cli.py +305 -0
- isa_mate/memory/compaction.py +221 -0
- isa_mate/memory/compactor.py +272 -0
- isa_mate/memory/local_store.py +328 -0
- isa_mate/memory/migration.py +125 -0
- isa_mate/memory/models.py +84 -0
- isa_mate/memory/store_adapter.py +241 -0
- isa_mate/memory/turn_capture.py +221 -0
- isa_mate/metrics/__init__.py +45 -0
- isa_mate/metrics/gateway.py +123 -0
- isa_mate/metrics/memory.py +128 -0
- isa_mate/metrics/middleware.py +129 -0
- isa_mate/metrics/registry.py +275 -0
- isa_mate/metrics/scheduler.py +110 -0
- isa_mate/migrate/__init__.py +265 -0
- isa_mate/onboard/__init__.py +5 -0
- isa_mate/onboard/connect.py +197 -0
- isa_mate/onboard/templates/default_config.yaml +147 -0
- isa_mate/onboard/wizard.py +807 -0
- isa_mate/pool_bridge.py +512 -0
- isa_mate/proactive/__init__.py +83 -0
- isa_mate/proactive/dry_run.py +407 -0
- isa_mate/proactive/event_backend.py +515 -0
- isa_mate/proactive/handler.py +251 -0
- isa_mate/proactive/rate_limit.py +510 -0
- isa_mate/proactive/scheduler_bridge.py +280 -0
- isa_mate/proactive/store.py +687 -0
- isa_mate/proactive/threshold_bridge.py +320 -0
- isa_mate/proactive/webhook_signature.py +193 -0
- isa_mate/routing/__init__.py +24 -0
- isa_mate/routing/budget.py +219 -0
- isa_mate/routing/guardrails.py +232 -0
- isa_mate/routing/health_registry.py +290 -0
- isa_mate/routing/latency.py +206 -0
- isa_mate/routing/router.py +246 -0
- isa_mate/scheduler/__init__.py +116 -0
- isa_mate/scheduler/api.py +620 -0
- isa_mate/scheduler/cli.py +822 -0
- isa_mate/scheduler/cron_parser.py +232 -0
- isa_mate/scheduler/loop.py +240 -0
- isa_mate/scheduler/migrations.py +174 -0
- isa_mate/scheduler/models.py +408 -0
- isa_mate/scheduler/runner.py +172 -0
- isa_mate/scheduler/scheduler.py +252 -0
- isa_mate/scheduler/store.py +681 -0
- isa_mate/scheduler/team_jobs.py +227 -0
- isa_mate/shell_safety/__init__.py +9 -0
- isa_mate/shell_safety/redactor.py +94 -0
- isa_mate/skills/__init__.py +155 -0
- isa_mate/skills/commands.py +842 -0
- isa_mate/skills/file-manager/SKILL.md +81 -0
- isa_mate/skills/local_executor.py +140 -0
- isa_mate/skills/office-manager/SKILL.md +36 -0
- isa_mate/sync/__init__.py +18 -0
- isa_mate/sync/cli.py +177 -0
- isa_mate/sync/encryption.py +128 -0
- isa_mate/sync/engine.py +434 -0
- isa_mate/sync/models.py +135 -0
- isa_mate/tools/__init__.py +547 -0
- isa_mate/tools/errors.py +591 -0
- isa_mate/tools/exec_tools.py +97 -0
- isa_mate/tools/file_tools.py +188 -0
- isa_mate/tools/office_tools.py +344 -0
- isa_mate/tools/protocol_validator.py +530 -0
- isa_mate/tools/redaction.py +227 -0
- isa_mate/tools/search_tools.py +169 -0
- isa_mate/tools/shell_safety.py +310 -0
- isa_mate/tools/system_tools.py +195 -0
- isa_mate/trace_context.py +142 -0
- isa_mate/tracing.py +142 -0
- isa_mate/tracker/__init__.py +0 -0
- isa_mate/tracker/lifecycle.py +133 -0
- isa_mate/triggers/__init__.py +31 -0
- isa_mate/triggers/base.py +83 -0
- isa_mate/triggers/github_trigger.py +105 -0
- isa_mate/triggers/health_plugins.py +248 -0
- isa_mate/triggers/metric_threshold_trigger.py +216 -0
- isa_mate/triggers/trigger_manager.py +89 -0
- isa_mate/triggers/webhook_trigger.py +64 -0
- isa_mate/watchdog/__init__.py +16 -0
- isa_mate/watchdog/health_model.py +139 -0
- isa_mate/watchdog/supervisor.py +161 -0
- isa_mate-0.2.0.dist-info/METADATA +217 -0
- isa_mate-0.2.0.dist-info/RECORD +250 -0
- isa_mate-0.2.0.dist-info/WHEEL +4 -0
- isa_mate-0.2.0.dist-info/entry_points.txt +2 -0
isa_mate/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# isA Mate Service
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
|
|
5
|
+
from .pool_bridge import (
|
|
6
|
+
PoolManagerBridge,
|
|
7
|
+
PoolBridgeConfig,
|
|
8
|
+
MateVisibility,
|
|
9
|
+
MateTier,
|
|
10
|
+
MateSpecs,
|
|
11
|
+
# Backwards compatibility aliases
|
|
12
|
+
DesktopVisibility,
|
|
13
|
+
DesktopTier,
|
|
14
|
+
DesktopSpecs,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"PoolManagerBridge",
|
|
19
|
+
"PoolBridgeConfig",
|
|
20
|
+
"MateVisibility",
|
|
21
|
+
"MateTier",
|
|
22
|
+
"MateSpecs",
|
|
23
|
+
# Backwards compatibility aliases
|
|
24
|
+
"DesktopVisibility",
|
|
25
|
+
"DesktopTier",
|
|
26
|
+
"DesktopSpecs",
|
|
27
|
+
]
|
isa_mate/__main__.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subscription account management CLI commands.
|
|
3
|
+
|
|
4
|
+
Issue #25, Epic #112
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
accounts list — Show all accounts with status
|
|
8
|
+
accounts add — Register a new account
|
|
9
|
+
accounts remove — Remove an account from config
|
|
10
|
+
accounts status — Detailed per-account usage status
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
VALID_PROVIDERS = ("anthropic-sub", "codex-sub")
|
|
23
|
+
|
|
24
|
+
# Status color indicators for terminal output
|
|
25
|
+
_STATUS_INDICATORS = {
|
|
26
|
+
"active": "\033[32m● active\033[0m",
|
|
27
|
+
"standby": "\033[34m○ standby\033[0m",
|
|
28
|
+
"exhausted": "\033[31m✗ exhausted\033[0m",
|
|
29
|
+
"cooldown": "\033[33m◌ cooldown\033[0m",
|
|
30
|
+
"auth_failed": "\033[31m✗ auth_failed\033[0m",
|
|
31
|
+
"configured": "\033[34m○ configured\033[0m",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load_config(config_path: str):
|
|
36
|
+
"""Load MateConfig from path."""
|
|
37
|
+
from ..agent.config import MateConfig
|
|
38
|
+
return MateConfig.load(config_path)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _save_config(config, config_path: str) -> None:
|
|
42
|
+
"""Patch only the agent.accounts and agent.rotation keys in the YAML file.
|
|
43
|
+
|
|
44
|
+
Loads the original YAML, updates the accounts/rotation sections, and
|
|
45
|
+
writes it back — preserving all other config sections untouched.
|
|
46
|
+
"""
|
|
47
|
+
import yaml
|
|
48
|
+
|
|
49
|
+
path = Path(config_path)
|
|
50
|
+
|
|
51
|
+
# Load existing YAML to preserve all sections
|
|
52
|
+
if path.exists():
|
|
53
|
+
with open(path) as f:
|
|
54
|
+
data = yaml.safe_load(f) or {}
|
|
55
|
+
else:
|
|
56
|
+
data = {}
|
|
57
|
+
|
|
58
|
+
# Ensure agent section exists
|
|
59
|
+
if "agent" not in data:
|
|
60
|
+
data["agent"] = {}
|
|
61
|
+
|
|
62
|
+
# Patch accounts
|
|
63
|
+
if config.agent.accounts:
|
|
64
|
+
accounts_data = {}
|
|
65
|
+
for provider, accts in config.agent.accounts.items():
|
|
66
|
+
accounts_data[provider] = [
|
|
67
|
+
{
|
|
68
|
+
"name": a.name,
|
|
69
|
+
"priority": a.priority,
|
|
70
|
+
"cli_profile": a.cli_profile,
|
|
71
|
+
}
|
|
72
|
+
for a in accts
|
|
73
|
+
]
|
|
74
|
+
data["agent"]["accounts"] = accounts_data
|
|
75
|
+
else:
|
|
76
|
+
data["agent"].pop("accounts", None)
|
|
77
|
+
|
|
78
|
+
# Patch rotation
|
|
79
|
+
if hasattr(config.agent, "rotation") and config.agent.rotation:
|
|
80
|
+
data["agent"]["rotation"] = {
|
|
81
|
+
"strategy": config.agent.rotation.strategy,
|
|
82
|
+
"cooldown_minutes": config.agent.rotation.cooldown_minutes,
|
|
83
|
+
"alert_thresholds": list(config.agent.rotation.alert_thresholds),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Patch provider (may change when first account is added)
|
|
87
|
+
data["agent"]["provider"] = config.agent.provider
|
|
88
|
+
|
|
89
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
with open(path, "w") as f:
|
|
91
|
+
yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _check_cli_profile(provider: str, cli_profile: str) -> tuple:
|
|
95
|
+
"""Validate a CLI profile by checking for CLI binary and stored credentials.
|
|
96
|
+
|
|
97
|
+
This performs a fast, offline check (no API calls). Use
|
|
98
|
+
``_check_cli_profile_live`` for a full network-based auth probe.
|
|
99
|
+
|
|
100
|
+
Note: This validates CLI availability, not per-profile authentication.
|
|
101
|
+
Claude CLI does not expose profile-specific credential files, and Codex
|
|
102
|
+
CLI ``login status`` checks the active session regardless of profile.
|
|
103
|
+
|
|
104
|
+
Returns (ok: bool, message: str).
|
|
105
|
+
"""
|
|
106
|
+
if provider == "anthropic-sub":
|
|
107
|
+
cli_name = "claude"
|
|
108
|
+
if not shutil.which(cli_name):
|
|
109
|
+
return False, f"'{cli_name}' CLI not found in PATH"
|
|
110
|
+
# Check for stored credentials in known Claude CLI config locations.
|
|
111
|
+
# Claude CLI does not expose per-profile credential files, so we
|
|
112
|
+
# can only verify that the CLI has been configured at least once.
|
|
113
|
+
home = Path.home()
|
|
114
|
+
cred_dirs = [
|
|
115
|
+
home / ".claude",
|
|
116
|
+
home / ".config" / "claude",
|
|
117
|
+
]
|
|
118
|
+
has_config = any(d.is_dir() for d in cred_dirs)
|
|
119
|
+
if not has_config:
|
|
120
|
+
return False, "no Claude CLI config directory found"
|
|
121
|
+
return True, f"CLI installed, config present (profile '{cli_profile}')"
|
|
122
|
+
|
|
123
|
+
elif provider == "codex-sub":
|
|
124
|
+
cli_name = "codex"
|
|
125
|
+
if not shutil.which(cli_name):
|
|
126
|
+
return False, f"'{cli_name}' CLI not found in PATH"
|
|
127
|
+
# codex login status is a lightweight local check (no API call).
|
|
128
|
+
# Codex CLI does not support per-profile login status.
|
|
129
|
+
try:
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
[cli_name, "login", "status"],
|
|
132
|
+
capture_output=True, text=True, timeout=8,
|
|
133
|
+
)
|
|
134
|
+
if result.returncode == 0:
|
|
135
|
+
return True, "authenticated"
|
|
136
|
+
return False, result.stderr.strip() or "not logged in"
|
|
137
|
+
except subprocess.TimeoutExpired:
|
|
138
|
+
return False, "login check timed out"
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return False, str(e)
|
|
141
|
+
|
|
142
|
+
return False, f"Unknown provider: {provider}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _check_cli_profile_live(provider: str, cli_profile: str) -> tuple:
|
|
146
|
+
"""Validate a CLI profile with a live network auth probe.
|
|
147
|
+
|
|
148
|
+
WARNING: This sends a real API request that costs tokens.
|
|
149
|
+
Use only when explicitly requested (e.g. ``accounts status --check-auth``).
|
|
150
|
+
|
|
151
|
+
Returns (ok: bool, message: str).
|
|
152
|
+
"""
|
|
153
|
+
if provider == "anthropic-sub":
|
|
154
|
+
cli_name = "claude"
|
|
155
|
+
if not shutil.which(cli_name):
|
|
156
|
+
return False, f"'{cli_name}' CLI not found in PATH"
|
|
157
|
+
try:
|
|
158
|
+
cmd = [cli_name, "-p", "Reply OK", "--output-format", "text",
|
|
159
|
+
"--model", "sonnet"]
|
|
160
|
+
if cli_profile and cli_profile != "default":
|
|
161
|
+
cmd.extend(["--profile", cli_profile])
|
|
162
|
+
result = subprocess.run(
|
|
163
|
+
cmd, capture_output=True, text=True, timeout=25,
|
|
164
|
+
)
|
|
165
|
+
if result.returncode == 0 and "OK" in (result.stdout or ""):
|
|
166
|
+
return True, "authenticated (live)"
|
|
167
|
+
return False, result.stderr.strip() or "auth probe failed"
|
|
168
|
+
except subprocess.TimeoutExpired:
|
|
169
|
+
return False, "auth probe timed out"
|
|
170
|
+
except Exception as e:
|
|
171
|
+
return False, str(e)
|
|
172
|
+
|
|
173
|
+
elif provider == "codex-sub":
|
|
174
|
+
cli_name = "codex"
|
|
175
|
+
if not shutil.which(cli_name):
|
|
176
|
+
return False, f"'{cli_name}' CLI not found in PATH"
|
|
177
|
+
try:
|
|
178
|
+
# Codex CLI does not support per-profile checks, but
|
|
179
|
+
# `codex login status` verifies active session auth.
|
|
180
|
+
result = subprocess.run(
|
|
181
|
+
[cli_name, "login", "status"],
|
|
182
|
+
capture_output=True, text=True, timeout=8,
|
|
183
|
+
)
|
|
184
|
+
if result.returncode == 0:
|
|
185
|
+
return True, "authenticated (live)"
|
|
186
|
+
return False, result.stderr.strip() or "not logged in"
|
|
187
|
+
except subprocess.TimeoutExpired:
|
|
188
|
+
return False, "login check timed out"
|
|
189
|
+
except Exception as e:
|
|
190
|
+
return False, str(e)
|
|
191
|
+
|
|
192
|
+
return False, f"Unknown provider: {provider}"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# =============================================================================
|
|
196
|
+
# Subcommand handlers
|
|
197
|
+
# =============================================================================
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def cmd_accounts_list(config, args) -> int:
|
|
201
|
+
"""Show all configured accounts with status."""
|
|
202
|
+
accounts = config.agent.accounts
|
|
203
|
+
|
|
204
|
+
if not accounts:
|
|
205
|
+
print("No subscription accounts configured.")
|
|
206
|
+
print(f" Provider: {config.agent.provider or '(none)'}")
|
|
207
|
+
print("\nAdd accounts with: isa-mate accounts add <provider> --name <name> --profile <profile>")
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
print("Subscription Accounts")
|
|
211
|
+
print("=" * 60)
|
|
212
|
+
|
|
213
|
+
for provider, acct_list in accounts.items():
|
|
214
|
+
is_active_provider = provider == config.agent.provider
|
|
215
|
+
provider_marker = " (active)" if is_active_provider else ""
|
|
216
|
+
print(f"\n Provider: {provider}{provider_marker}")
|
|
217
|
+
print(f" Strategy: {config.agent.rotation.strategy}")
|
|
218
|
+
print(f" Cooldown: {config.agent.rotation.cooldown_minutes} min")
|
|
219
|
+
print()
|
|
220
|
+
|
|
221
|
+
for acct in sorted(acct_list, key=lambda a: a.priority):
|
|
222
|
+
status = _STATUS_INDICATORS.get("configured", "?")
|
|
223
|
+
if acct.priority == 1 and is_active_provider:
|
|
224
|
+
status = _STATUS_INDICATORS.get("active", "active")
|
|
225
|
+
print(f" {acct.priority}. {acct.name:<20} profile={acct.cli_profile:<15} {status}")
|
|
226
|
+
|
|
227
|
+
print()
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cmd_accounts_add(config, args) -> int:
|
|
232
|
+
"""Add a new subscription account."""
|
|
233
|
+
from ..agent.config import AccountConfig
|
|
234
|
+
|
|
235
|
+
provider = args.provider
|
|
236
|
+
name = args.name
|
|
237
|
+
profile = args.profile
|
|
238
|
+
priority = args.priority
|
|
239
|
+
|
|
240
|
+
if provider not in VALID_PROVIDERS:
|
|
241
|
+
print(f"Error: Invalid provider '{provider}'. Must be one of: {', '.join(VALID_PROVIDERS)}")
|
|
242
|
+
return 1
|
|
243
|
+
|
|
244
|
+
# Check for duplicate name
|
|
245
|
+
existing = config.agent.accounts.get(provider, [])
|
|
246
|
+
if any(a.name == name for a in existing):
|
|
247
|
+
print(f"Error: Account '{name}' already exists for provider '{provider}'")
|
|
248
|
+
return 1
|
|
249
|
+
|
|
250
|
+
# Auto-assign priority if not specified
|
|
251
|
+
if priority is None:
|
|
252
|
+
priority = max((a.priority for a in existing), default=0) + 1
|
|
253
|
+
|
|
254
|
+
# Validate CLI profile (skip if --no-validate)
|
|
255
|
+
if not args.no_validate:
|
|
256
|
+
print(f"Validating CLI profile '{profile}' for {provider}...")
|
|
257
|
+
ok, msg = _check_cli_profile(provider, profile)
|
|
258
|
+
if ok:
|
|
259
|
+
print(f" OK: {msg}")
|
|
260
|
+
else:
|
|
261
|
+
print(f" FAILED: {msg}")
|
|
262
|
+
print(f"\nFix: Log in with the '{profile}' profile first, then retry.")
|
|
263
|
+
if provider == "anthropic-sub":
|
|
264
|
+
print(f" claude login --profile {profile}")
|
|
265
|
+
elif provider == "codex-sub":
|
|
266
|
+
print(f" codex login --device-auth")
|
|
267
|
+
return 1
|
|
268
|
+
|
|
269
|
+
# Add the account
|
|
270
|
+
new_acct = AccountConfig(name=name, priority=priority, cli_profile=profile)
|
|
271
|
+
if provider not in config.agent.accounts:
|
|
272
|
+
config.agent.accounts[provider] = []
|
|
273
|
+
config.agent.accounts[provider].append(new_acct)
|
|
274
|
+
|
|
275
|
+
# If no provider set, set it
|
|
276
|
+
if not config.agent.provider:
|
|
277
|
+
config.agent.provider = provider
|
|
278
|
+
|
|
279
|
+
# Save
|
|
280
|
+
_save_config(config, args.config)
|
|
281
|
+
print(f"\nAdded account '{name}' (priority={priority}, profile={profile}) to {provider}")
|
|
282
|
+
print(f"Config saved to: {args.config}")
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def cmd_accounts_remove(config, args) -> int:
|
|
287
|
+
"""Remove a subscription account."""
|
|
288
|
+
provider = args.provider
|
|
289
|
+
name = args.name
|
|
290
|
+
|
|
291
|
+
acct_list = config.agent.accounts.get(provider, [])
|
|
292
|
+
if not any(a.name == name for a in acct_list):
|
|
293
|
+
print(f"Error: Account '{name}' not found for provider '{provider}'")
|
|
294
|
+
return 1
|
|
295
|
+
|
|
296
|
+
if len(acct_list) == 1:
|
|
297
|
+
print(f"Warning: This is the only account for '{provider}'.")
|
|
298
|
+
if not args.force:
|
|
299
|
+
print("Use --force to remove the last account.")
|
|
300
|
+
return 1
|
|
301
|
+
|
|
302
|
+
# Remove the account
|
|
303
|
+
config.agent.accounts[provider] = [a for a in acct_list if a.name != name]
|
|
304
|
+
|
|
305
|
+
# Clean up empty provider
|
|
306
|
+
if not config.agent.accounts[provider]:
|
|
307
|
+
del config.agent.accounts[provider]
|
|
308
|
+
|
|
309
|
+
# Save
|
|
310
|
+
_save_config(config, args.config)
|
|
311
|
+
print(f"Removed account '{name}' from {provider}")
|
|
312
|
+
print(f"Config saved to: {args.config}")
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def cmd_accounts_status(config, args) -> int:
|
|
317
|
+
"""Show detailed usage status per account."""
|
|
318
|
+
accounts = config.agent.accounts
|
|
319
|
+
|
|
320
|
+
if not accounts:
|
|
321
|
+
print("No subscription accounts configured.")
|
|
322
|
+
return 0
|
|
323
|
+
|
|
324
|
+
print("Account Usage Status")
|
|
325
|
+
print("=" * 70)
|
|
326
|
+
|
|
327
|
+
for provider, acct_list in accounts.items():
|
|
328
|
+
print(f"\n Provider: {provider}")
|
|
329
|
+
print(f" {'Name':<20} {'Priority':<10} {'Profile':<15} {'CLI Status'}")
|
|
330
|
+
print(f" {'-'*20} {'-'*10} {'-'*15} {'-'*20}")
|
|
331
|
+
|
|
332
|
+
check_fn = _check_cli_profile_live if getattr(args, "check_auth", False) else _check_cli_profile
|
|
333
|
+
for acct in sorted(acct_list, key=lambda a: a.priority):
|
|
334
|
+
ok, msg = check_fn(provider, acct.cli_profile)
|
|
335
|
+
cli_status = f"\033[32mOK\033[0m" if ok else f"\033[31m{msg}\033[0m"
|
|
336
|
+
print(f" {acct.name:<20} {acct.priority:<10} {acct.cli_profile:<15} {cli_status}")
|
|
337
|
+
|
|
338
|
+
# Try to load runtime usage tracker state if available
|
|
339
|
+
db_path = Path.home() / ".isa" / "mate" / "subscription_state.db"
|
|
340
|
+
if db_path.exists():
|
|
341
|
+
try:
|
|
342
|
+
import sqlite3
|
|
343
|
+
conn = sqlite3.connect(str(db_path))
|
|
344
|
+
cursor = conn.execute(
|
|
345
|
+
"SELECT account_name, provider_id, total_requests, "
|
|
346
|
+
"exhaustion_count, cooldown_until "
|
|
347
|
+
"FROM account_usage"
|
|
348
|
+
)
|
|
349
|
+
rows = cursor.fetchall()
|
|
350
|
+
conn.close()
|
|
351
|
+
|
|
352
|
+
if rows:
|
|
353
|
+
import time
|
|
354
|
+
print(f"\n Runtime Usage (from {db_path})")
|
|
355
|
+
print(f" {'Account':<20} {'Provider':<15} {'Requests':<10} {'Exhaustions':<12} {'Cooldown'}")
|
|
356
|
+
print(f" {'-'*20} {'-'*15} {'-'*10} {'-'*12} {'-'*20}")
|
|
357
|
+
for name, prov, reqs, exhaustions, cooldown in rows:
|
|
358
|
+
cd_str = "none"
|
|
359
|
+
if cooldown:
|
|
360
|
+
remaining = cooldown - time.time()
|
|
361
|
+
if remaining > 0:
|
|
362
|
+
cd_str = f"{remaining/60:.1f} min remaining"
|
|
363
|
+
else:
|
|
364
|
+
cd_str = "expired"
|
|
365
|
+
print(f" {name:<20} {prov:<15} {reqs:<10} {exhaustions:<12} {cd_str}")
|
|
366
|
+
except Exception as e:
|
|
367
|
+
logger.debug("Could not read usage tracker DB: %s", e)
|
|
368
|
+
|
|
369
|
+
print()
|
|
370
|
+
return 0
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# =============================================================================
|
|
374
|
+
# Main dispatcher
|
|
375
|
+
# =============================================================================
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def cmd_accounts(args) -> int:
|
|
379
|
+
"""Dispatch accounts subcommand."""
|
|
380
|
+
config = _load_config(args.config)
|
|
381
|
+
|
|
382
|
+
subcmd = args.accounts_command
|
|
383
|
+
|
|
384
|
+
if subcmd == "list":
|
|
385
|
+
return cmd_accounts_list(config, args)
|
|
386
|
+
elif subcmd == "add":
|
|
387
|
+
return cmd_accounts_add(config, args)
|
|
388
|
+
elif subcmd == "remove":
|
|
389
|
+
return cmd_accounts_remove(config, args)
|
|
390
|
+
elif subcmd == "status":
|
|
391
|
+
return cmd_accounts_status(config, args)
|
|
392
|
+
else:
|
|
393
|
+
# Default to list if no subcommand
|
|
394
|
+
return cmd_accounts_list(config, args)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""isA Mate agent runtime and configuration."""
|
|
2
|
+
|
|
3
|
+
from .config import MateConfig, DesktopConfig
|
|
4
|
+
from .runtime import MateRuntime, DesktopAgentRuntime
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"MateConfig",
|
|
8
|
+
"MateRuntime",
|
|
9
|
+
# Backwards compatibility aliases
|
|
10
|
+
"DesktopConfig",
|
|
11
|
+
"DesktopAgentRuntime",
|
|
12
|
+
]
|