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.
Files changed (250) hide show
  1. isa_mate/__init__.py +27 -0
  2. isa_mate/__main__.py +5 -0
  3. isa_mate/accounts/__init__.py +5 -0
  4. isa_mate/accounts/commands.py +394 -0
  5. isa_mate/agent/__init__.py +12 -0
  6. isa_mate/agent/config.py +1194 -0
  7. isa_mate/agent/delegation_directory.py +266 -0
  8. isa_mate/agent/multi_team_detection.py +145 -0
  9. isa_mate/agent/project_context.py +128 -0
  10. isa_mate/agent/research.py +197 -0
  11. isa_mate/agent/runtime.py +1861 -0
  12. isa_mate/agent/trading_contract.py +297 -0
  13. isa_mate/agent/trading_handoff.py +316 -0
  14. isa_mate/auth/__init__.py +20 -0
  15. isa_mate/auth/audit.py +79 -0
  16. isa_mate/auth/authorization.py +46 -0
  17. isa_mate/auth/clients.py +150 -0
  18. isa_mate/auth/middleware.py +214 -0
  19. isa_mate/auth/models.py +51 -0
  20. isa_mate/auth/scopes.py +120 -0
  21. isa_mate/channels/__init__.py +35 -0
  22. isa_mate/channels/discord/__init__.py +5 -0
  23. isa_mate/channels/discord/adapter.py +121 -0
  24. isa_mate/channels/discord/parser.py +146 -0
  25. isa_mate/channels/discord/sender.py +109 -0
  26. isa_mate/channels/feishu/__init__.py +5 -0
  27. isa_mate/channels/feishu/adapter.py +143 -0
  28. isa_mate/channels/feishu/parser.py +97 -0
  29. isa_mate/channels/feishu/sender.py +120 -0
  30. isa_mate/channels/feishu/signature.py +36 -0
  31. isa_mate/channels/google_chat/__init__.py +5 -0
  32. isa_mate/channels/google_chat/adapter.py +140 -0
  33. isa_mate/channels/imessage/__init__.py +5 -0
  34. isa_mate/channels/imessage/adapter.py +137 -0
  35. isa_mate/channels/line/__init__.py +5 -0
  36. isa_mate/channels/line/adapter.py +157 -0
  37. isa_mate/channels/matrix/__init__.py +5 -0
  38. isa_mate/channels/matrix/adapter.py +125 -0
  39. isa_mate/channels/matrix/parser.py +81 -0
  40. isa_mate/channels/matrix/sender.py +91 -0
  41. isa_mate/channels/msteams/__init__.py +5 -0
  42. isa_mate/channels/msteams/adapter.py +136 -0
  43. isa_mate/channels/msteams/parser.py +103 -0
  44. isa_mate/channels/msteams/sender.py +132 -0
  45. isa_mate/channels/runtime/__init__.py +30 -0
  46. isa_mate/channels/runtime/contracts.py +31 -0
  47. isa_mate/channels/runtime/metrics.py +182 -0
  48. isa_mate/channels/runtime/models.py +66 -0
  49. isa_mate/channels/runtime/replay.py +48 -0
  50. isa_mate/channels/runtime/store.py +369 -0
  51. isa_mate/channels/runtime/workers.py +462 -0
  52. isa_mate/channels/security/__init__.py +86 -0
  53. isa_mate/channels/security/channel_policy.py +439 -0
  54. isa_mate/channels/security/channel_policy_models.py +384 -0
  55. isa_mate/channels/security/pairing_store.py +652 -0
  56. isa_mate/channels/security/security_audit_log.py +321 -0
  57. isa_mate/channels/security/webhook_security_models.py +413 -0
  58. isa_mate/channels/security/webhook_signature.py +188 -0
  59. isa_mate/channels/signal/__init__.py +5 -0
  60. isa_mate/channels/signal/adapter.py +125 -0
  61. isa_mate/channels/signal/parser.py +72 -0
  62. isa_mate/channels/signal/sender.py +96 -0
  63. isa_mate/channels/slack/__init__.py +5 -0
  64. isa_mate/channels/slack/adapter.py +153 -0
  65. isa_mate/channels/slack/parser.py +95 -0
  66. isa_mate/channels/slack/sender.py +96 -0
  67. isa_mate/channels/slack/signature.py +61 -0
  68. isa_mate/channels/telegram/__init__.py +5 -0
  69. isa_mate/channels/telegram/adapter.py +390 -0
  70. isa_mate/channels/telegram/parser.py +107 -0
  71. isa_mate/channels/telegram/sender.py +95 -0
  72. isa_mate/channels/telegram_bridge.py +177 -0
  73. isa_mate/channels/whatsapp/__init__.py +5 -0
  74. isa_mate/channels/whatsapp/adapter.py +152 -0
  75. isa_mate/channels/whatsapp/parser.py +191 -0
  76. isa_mate/channels/whatsapp/sender.py +109 -0
  77. isa_mate/channels/whatsapp/signature.py +40 -0
  78. isa_mate/cli.py +1299 -0
  79. isa_mate/connectivity.py +295 -0
  80. isa_mate/credentials/__init__.py +5 -0
  81. isa_mate/credentials/cli.py +132 -0
  82. isa_mate/credentials/models.py +86 -0
  83. isa_mate/credentials/resolver.py +289 -0
  84. isa_mate/credentials/routes.py +185 -0
  85. isa_mate/desktop_agent.py +26 -0
  86. isa_mate/doctor/__init__.py +19 -0
  87. isa_mate/doctor/engine.py +1368 -0
  88. isa_mate/errors/__init__.py +38 -0
  89. isa_mate/errors/registry.py +467 -0
  90. isa_mate/errors/taxonomy.py +348 -0
  91. isa_mate/execution/__init__.py +44 -0
  92. isa_mate/execution/a2a_router.py +198 -0
  93. isa_mate/execution/agent_config_client.py +109 -0
  94. isa_mate/execution/audit_emitter.py +401 -0
  95. isa_mate/execution/autonomous.py +342 -0
  96. isa_mate/execution/autonomous_event_bus.py +285 -0
  97. isa_mate/execution/autonomous_reactive_router.py +1717 -0
  98. isa_mate/execution/browser_router.py +173 -0
  99. isa_mate/execution/chat_metrics.py +430 -0
  100. isa_mate/execution/chat_router.py +435 -0
  101. isa_mate/execution/dispatch_lifecycle.py +435 -0
  102. isa_mate/execution/dispatch_store.py +141 -0
  103. isa_mate/execution/hil_router.py +830 -0
  104. isa_mate/execution/idempotency.py +320 -0
  105. isa_mate/execution/notification_router.py +128 -0
  106. isa_mate/execution/observability_router.py +795 -0
  107. isa_mate/execution/persistence_router.py +2454 -0
  108. isa_mate/execution/proactive_router.py +1593 -0
  109. isa_mate/execution/response_filter.py +61 -0
  110. isa_mate/execution/responsive_publisher.py +488 -0
  111. isa_mate/execution/responsive_router.py +709 -0
  112. isa_mate/execution/stream_helpers.py +457 -0
  113. isa_mate/execution/swarm_router.py +188 -0
  114. isa_mate/execution/workflow_manager.py +125 -0
  115. isa_mate/execution/workflow_router.py +134 -0
  116. isa_mate/failover/__init__.py +33 -0
  117. isa_mate/failover/classifier.py +83 -0
  118. isa_mate/failover/cooldown.py +42 -0
  119. isa_mate/failover/exceptions.py +49 -0
  120. isa_mate/failover/health.py +142 -0
  121. isa_mate/failover/response_normalizer.py +82 -0
  122. isa_mate/failover/router.py +372 -0
  123. isa_mate/fault_injection/__init__.py +44 -0
  124. isa_mate/fault_injection/config.py +152 -0
  125. isa_mate/fault_injection/engine.py +397 -0
  126. isa_mate/fault_injection/injectors.py +275 -0
  127. isa_mate/gateway/__init__.py +6 -0
  128. isa_mate/gateway/catalog.py +440 -0
  129. isa_mate/gateway/cli_handler.py +117 -0
  130. isa_mate/gateway/pair_commands.py +489 -0
  131. isa_mate/gateway/rate_limit.py +203 -0
  132. isa_mate/gateway/rate_limit_middleware.py +110 -0
  133. isa_mate/gateway/rate_limit_policy.py +106 -0
  134. isa_mate/gateway/redis_rate_limiter.py +173 -0
  135. isa_mate/gateway/scope_dependency.py +78 -0
  136. isa_mate/gateway/server.py +2838 -0
  137. isa_mate/gateway/skills_routes.py +178 -0
  138. isa_mate/gateway/startup_validator.py +197 -0
  139. isa_mate/gateway/token_bucket.py +142 -0
  140. isa_mate/hub/__init__.py +17 -0
  141. isa_mate/hub/autonomy_hub.py +6 -0
  142. isa_mate/hub/base.py +4 -0
  143. isa_mate/hub/data_hub.py +8 -0
  144. isa_mate/hub/event_bus.py +4 -0
  145. isa_mate/hub/event_hub.py +9 -0
  146. isa_mate/hub/events.py +28 -0
  147. isa_mate/hub/hub.py +4 -0
  148. isa_mate/hub/interaction_hub.py +8 -0
  149. isa_mate/hub/memory_hub.py +7 -0
  150. isa_mate/hub/response_hub.py +16 -0
  151. isa_mate/logging/__init__.py +32 -0
  152. isa_mate/logging/structured.py +217 -0
  153. isa_mate/marketplace/__init__.py +1 -0
  154. isa_mate/marketplace/models.py +256 -0
  155. isa_mate/marketplace/pipeline.py +305 -0
  156. isa_mate/marketplace/repository.py +256 -0
  157. isa_mate/marketplace/router.py +112 -0
  158. isa_mate/marketplace/service.py +273 -0
  159. isa_mate/mate_agent.py +1256 -0
  160. isa_mate/mcp_server.py +154 -0
  161. isa_mate/memory/__init__.py +16 -0
  162. isa_mate/memory/adapters/__init__.py +5 -0
  163. isa_mate/memory/api.py +289 -0
  164. isa_mate/memory/benchmark.py +210 -0
  165. isa_mate/memory/cli.py +305 -0
  166. isa_mate/memory/compaction.py +221 -0
  167. isa_mate/memory/compactor.py +272 -0
  168. isa_mate/memory/local_store.py +328 -0
  169. isa_mate/memory/migration.py +125 -0
  170. isa_mate/memory/models.py +84 -0
  171. isa_mate/memory/store_adapter.py +241 -0
  172. isa_mate/memory/turn_capture.py +221 -0
  173. isa_mate/metrics/__init__.py +45 -0
  174. isa_mate/metrics/gateway.py +123 -0
  175. isa_mate/metrics/memory.py +128 -0
  176. isa_mate/metrics/middleware.py +129 -0
  177. isa_mate/metrics/registry.py +275 -0
  178. isa_mate/metrics/scheduler.py +110 -0
  179. isa_mate/migrate/__init__.py +265 -0
  180. isa_mate/onboard/__init__.py +5 -0
  181. isa_mate/onboard/connect.py +197 -0
  182. isa_mate/onboard/templates/default_config.yaml +147 -0
  183. isa_mate/onboard/wizard.py +807 -0
  184. isa_mate/pool_bridge.py +512 -0
  185. isa_mate/proactive/__init__.py +83 -0
  186. isa_mate/proactive/dry_run.py +407 -0
  187. isa_mate/proactive/event_backend.py +515 -0
  188. isa_mate/proactive/handler.py +251 -0
  189. isa_mate/proactive/rate_limit.py +510 -0
  190. isa_mate/proactive/scheduler_bridge.py +280 -0
  191. isa_mate/proactive/store.py +687 -0
  192. isa_mate/proactive/threshold_bridge.py +320 -0
  193. isa_mate/proactive/webhook_signature.py +193 -0
  194. isa_mate/routing/__init__.py +24 -0
  195. isa_mate/routing/budget.py +219 -0
  196. isa_mate/routing/guardrails.py +232 -0
  197. isa_mate/routing/health_registry.py +290 -0
  198. isa_mate/routing/latency.py +206 -0
  199. isa_mate/routing/router.py +246 -0
  200. isa_mate/scheduler/__init__.py +116 -0
  201. isa_mate/scheduler/api.py +620 -0
  202. isa_mate/scheduler/cli.py +822 -0
  203. isa_mate/scheduler/cron_parser.py +232 -0
  204. isa_mate/scheduler/loop.py +240 -0
  205. isa_mate/scheduler/migrations.py +174 -0
  206. isa_mate/scheduler/models.py +408 -0
  207. isa_mate/scheduler/runner.py +172 -0
  208. isa_mate/scheduler/scheduler.py +252 -0
  209. isa_mate/scheduler/store.py +681 -0
  210. isa_mate/scheduler/team_jobs.py +227 -0
  211. isa_mate/shell_safety/__init__.py +9 -0
  212. isa_mate/shell_safety/redactor.py +94 -0
  213. isa_mate/skills/__init__.py +155 -0
  214. isa_mate/skills/commands.py +842 -0
  215. isa_mate/skills/file-manager/SKILL.md +81 -0
  216. isa_mate/skills/local_executor.py +140 -0
  217. isa_mate/skills/office-manager/SKILL.md +36 -0
  218. isa_mate/sync/__init__.py +18 -0
  219. isa_mate/sync/cli.py +177 -0
  220. isa_mate/sync/encryption.py +128 -0
  221. isa_mate/sync/engine.py +434 -0
  222. isa_mate/sync/models.py +135 -0
  223. isa_mate/tools/__init__.py +547 -0
  224. isa_mate/tools/errors.py +591 -0
  225. isa_mate/tools/exec_tools.py +97 -0
  226. isa_mate/tools/file_tools.py +188 -0
  227. isa_mate/tools/office_tools.py +344 -0
  228. isa_mate/tools/protocol_validator.py +530 -0
  229. isa_mate/tools/redaction.py +227 -0
  230. isa_mate/tools/search_tools.py +169 -0
  231. isa_mate/tools/shell_safety.py +310 -0
  232. isa_mate/tools/system_tools.py +195 -0
  233. isa_mate/trace_context.py +142 -0
  234. isa_mate/tracing.py +142 -0
  235. isa_mate/tracker/__init__.py +0 -0
  236. isa_mate/tracker/lifecycle.py +133 -0
  237. isa_mate/triggers/__init__.py +31 -0
  238. isa_mate/triggers/base.py +83 -0
  239. isa_mate/triggers/github_trigger.py +105 -0
  240. isa_mate/triggers/health_plugins.py +248 -0
  241. isa_mate/triggers/metric_threshold_trigger.py +216 -0
  242. isa_mate/triggers/trigger_manager.py +89 -0
  243. isa_mate/triggers/webhook_trigger.py +64 -0
  244. isa_mate/watchdog/__init__.py +16 -0
  245. isa_mate/watchdog/health_model.py +139 -0
  246. isa_mate/watchdog/supervisor.py +161 -0
  247. isa_mate-0.2.0.dist-info/METADATA +217 -0
  248. isa_mate-0.2.0.dist-info/RECORD +250 -0
  249. isa_mate-0.2.0.dist-info/WHEEL +4 -0
  250. 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,5 @@
1
+ """Allow running as: python -m isa_mate"""
2
+ from .cli import main
3
+ import sys
4
+
5
+ sys.exit(main() or 0)
@@ -0,0 +1,5 @@
1
+ """Accounts CLI — manage subscription accounts for multi-account rotation."""
2
+
3
+ from .commands import cmd_accounts
4
+
5
+ __all__ = ["cmd_accounts"]
@@ -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
+ ]