claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +9 -0
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +300 -33
- claude_mpm/cli/startup_display.py +4 -2
- claude_mpm/cli/startup_migrations.py +236 -0
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/errors.py +21 -0
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +37 -26
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +47 -5
- claude_mpm/commander/chat/commands.py +44 -16
- claude_mpm/commander/chat/repl.py +1729 -82
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +91 -1
- claude_mpm/commander/frameworks/mpm.py +9 -14
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -0
- claude_mpm/commander/instance_manager.py +546 -15
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/constants.py +5 -0
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +30 -22
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logger.py +16 -2
- claude_mpm/core/logging_utils.py +40 -16
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +37 -7
- claude_mpm/core/socketio_pool.py +47 -15
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
- claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +222 -54
- claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
- claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
- claude_mpm/init.py +21 -14
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +10 -9
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/server.py +6 -1
- claude_mpm/services/pm_skills_deployer.py +5 -3
- claude_mpm/services/skills/git_skill_source_manager.py +79 -8
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +17 -1
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth management commands for claude-mpm CLI.
|
|
3
|
+
|
|
4
|
+
WHY: Users need a way to manage OAuth authentication for MCP services
|
|
5
|
+
that require OAuth2 flows (e.g., Google Workspace) directly from the terminal.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Use BaseCommand for consistent CLI patterns
|
|
9
|
+
- Reuse OAuth logic from commander/chat/repl.py
|
|
10
|
+
- Support multiple credential sources: .env.local, .env, environment variables
|
|
11
|
+
- Provide clear feedback during OAuth flow
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.panel import Panel
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
|
|
24
|
+
from ..shared import BaseCommand, CommandResult
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_oauth_credentials_from_env_files() -> tuple[str | None, str | None]:
|
|
30
|
+
"""Load OAuth credentials from .env files.
|
|
31
|
+
|
|
32
|
+
Checks .env.local first (user overrides), then .env.
|
|
33
|
+
Returns tuple of (client_id, client_secret), either may be None.
|
|
34
|
+
"""
|
|
35
|
+
client_id = None
|
|
36
|
+
client_secret = None
|
|
37
|
+
|
|
38
|
+
# Priority order: .env.local first (user overrides), then .env
|
|
39
|
+
env_files = [".env.local", ".env"]
|
|
40
|
+
|
|
41
|
+
for env_file in env_files:
|
|
42
|
+
env_path = Path.cwd() / env_file
|
|
43
|
+
if env_path.exists():
|
|
44
|
+
try:
|
|
45
|
+
with open(env_path) as f:
|
|
46
|
+
for line in f:
|
|
47
|
+
line = line.strip()
|
|
48
|
+
# Skip empty lines and comments
|
|
49
|
+
if not line or line.startswith("#"):
|
|
50
|
+
continue
|
|
51
|
+
if "=" in line:
|
|
52
|
+
key, _, value = line.partition("=")
|
|
53
|
+
key = key.strip()
|
|
54
|
+
value = value.strip().strip('"').strip("'")
|
|
55
|
+
|
|
56
|
+
if key == "GOOGLE_OAUTH_CLIENT_ID" and not client_id:
|
|
57
|
+
client_id = value
|
|
58
|
+
elif (
|
|
59
|
+
key == "GOOGLE_OAUTH_CLIENT_SECRET"
|
|
60
|
+
and not client_secret
|
|
61
|
+
):
|
|
62
|
+
client_secret = value
|
|
63
|
+
|
|
64
|
+
# If we found both, no need to check more files
|
|
65
|
+
if client_id and client_secret:
|
|
66
|
+
break
|
|
67
|
+
except Exception: # nosec B110 - intentionally ignore .env file read errors
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
return client_id, client_secret
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class OAuthCommand(BaseCommand):
|
|
74
|
+
"""OAuth management command for MCP services."""
|
|
75
|
+
|
|
76
|
+
def __init__(self):
|
|
77
|
+
super().__init__("oauth")
|
|
78
|
+
|
|
79
|
+
def validate_args(self, args) -> str | None:
|
|
80
|
+
"""Validate command arguments."""
|
|
81
|
+
# If no oauth_command specified, default to 'list'
|
|
82
|
+
if not hasattr(args, "oauth_command") or not args.oauth_command:
|
|
83
|
+
args.oauth_command = None # Will show help
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
valid_commands = ["list", "setup", "status", "revoke", "refresh"]
|
|
87
|
+
if args.oauth_command not in valid_commands:
|
|
88
|
+
return f"Unknown oauth command: {args.oauth_command}. Valid commands: {', '.join(valid_commands)}"
|
|
89
|
+
|
|
90
|
+
# Validate service_name for commands that require it
|
|
91
|
+
if args.oauth_command in ["setup", "status", "revoke", "refresh"]:
|
|
92
|
+
if not hasattr(args, "service_name") or not args.service_name:
|
|
93
|
+
return f"oauth {args.oauth_command} requires a service name"
|
|
94
|
+
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def run(self, args) -> CommandResult:
|
|
98
|
+
"""Execute the OAuth command."""
|
|
99
|
+
# If no subcommand, show help
|
|
100
|
+
if not hasattr(args, "oauth_command") or not args.oauth_command:
|
|
101
|
+
self._show_help()
|
|
102
|
+
return CommandResult.success_result("Help displayed")
|
|
103
|
+
|
|
104
|
+
if args.oauth_command == "list":
|
|
105
|
+
return self._list_services(args)
|
|
106
|
+
if args.oauth_command == "setup":
|
|
107
|
+
return self._setup_oauth(args)
|
|
108
|
+
if args.oauth_command == "status":
|
|
109
|
+
return self._show_status(args)
|
|
110
|
+
if args.oauth_command == "revoke":
|
|
111
|
+
return self._revoke_tokens(args)
|
|
112
|
+
if args.oauth_command == "refresh":
|
|
113
|
+
return self._refresh_tokens(args)
|
|
114
|
+
|
|
115
|
+
return CommandResult.error_result(
|
|
116
|
+
f"Unknown oauth command: {args.oauth_command}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _show_help(self) -> None:
|
|
120
|
+
"""Display OAuth command help."""
|
|
121
|
+
help_text = """
|
|
122
|
+
[bold]OAuth Commands:[/bold]
|
|
123
|
+
oauth list List OAuth-capable MCP services
|
|
124
|
+
oauth setup <service> Set up OAuth authentication for a service
|
|
125
|
+
oauth status <service> Show OAuth token status for a service
|
|
126
|
+
oauth revoke <service> Revoke OAuth tokens for a service
|
|
127
|
+
oauth refresh <service> Refresh OAuth tokens for a service
|
|
128
|
+
|
|
129
|
+
[bold]Examples:[/bold]
|
|
130
|
+
claude-mpm oauth list
|
|
131
|
+
claude-mpm oauth setup workspace-mcp
|
|
132
|
+
claude-mpm oauth status workspace-mcp
|
|
133
|
+
"""
|
|
134
|
+
console.print(help_text)
|
|
135
|
+
|
|
136
|
+
def _list_services(self, args) -> CommandResult:
|
|
137
|
+
"""List OAuth-capable MCP services."""
|
|
138
|
+
try:
|
|
139
|
+
from claude_mpm.services.mcp_service_registry import MCPServiceRegistry
|
|
140
|
+
|
|
141
|
+
services = MCPServiceRegistry.list_all()
|
|
142
|
+
oauth_services = [s for s in services if s.oauth_provider]
|
|
143
|
+
|
|
144
|
+
if not oauth_services:
|
|
145
|
+
console.print("[yellow]No OAuth-capable services found.[/yellow]")
|
|
146
|
+
return CommandResult.success_result("No OAuth services found")
|
|
147
|
+
|
|
148
|
+
# Check output format
|
|
149
|
+
output_format = getattr(args, "format", "table")
|
|
150
|
+
|
|
151
|
+
if output_format == "json":
|
|
152
|
+
data = [
|
|
153
|
+
{
|
|
154
|
+
"name": s.name,
|
|
155
|
+
"description": s.description,
|
|
156
|
+
"oauth_provider": s.oauth_provider,
|
|
157
|
+
"oauth_scopes": s.oauth_scopes,
|
|
158
|
+
"required_env": s.required_env,
|
|
159
|
+
}
|
|
160
|
+
for s in oauth_services
|
|
161
|
+
]
|
|
162
|
+
console.print(json.dumps(data, indent=2))
|
|
163
|
+
return CommandResult.success_result(
|
|
164
|
+
"Services listed", data={"services": data}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Table format
|
|
168
|
+
table = Table(title="OAuth-Capable MCP Services")
|
|
169
|
+
table.add_column("Service", style="cyan")
|
|
170
|
+
table.add_column("Provider", style="green")
|
|
171
|
+
table.add_column("Description", style="white")
|
|
172
|
+
|
|
173
|
+
for service in oauth_services:
|
|
174
|
+
table.add_row(
|
|
175
|
+
service.name,
|
|
176
|
+
service.oauth_provider or "",
|
|
177
|
+
service.description,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
console.print(table)
|
|
181
|
+
return CommandResult.success_result(
|
|
182
|
+
f"Found {len(oauth_services)} OAuth-capable service(s)"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
except ImportError:
|
|
186
|
+
return CommandResult.error_result("MCP Service Registry not available")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
return CommandResult.error_result(f"Error listing services: {e}")
|
|
189
|
+
|
|
190
|
+
def _setup_oauth(self, args) -> CommandResult:
|
|
191
|
+
"""Set up OAuth for a service."""
|
|
192
|
+
service_name = args.service_name
|
|
193
|
+
|
|
194
|
+
# Get service info from registry to get provider and scopes
|
|
195
|
+
try:
|
|
196
|
+
from claude_mpm.services.mcp_service_registry import MCPServiceRegistry
|
|
197
|
+
|
|
198
|
+
service = MCPServiceRegistry.get(service_name)
|
|
199
|
+
if not service:
|
|
200
|
+
return CommandResult.error_result(f"Service '{service_name}' not found")
|
|
201
|
+
|
|
202
|
+
provider_name = service.oauth_provider
|
|
203
|
+
if not provider_name:
|
|
204
|
+
return CommandResult.error_result(
|
|
205
|
+
f"Service '{service_name}' does not use OAuth"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
scopes = service.oauth_scopes or None
|
|
209
|
+
except ImportError:
|
|
210
|
+
return CommandResult.error_result("MCP Service Registry not available")
|
|
211
|
+
|
|
212
|
+
# Priority: 1) .env files, 2) environment variables, 3) interactive prompt
|
|
213
|
+
client_id, client_secret = _load_oauth_credentials_from_env_files()
|
|
214
|
+
|
|
215
|
+
# Fall back to environment variables if not found in .env files
|
|
216
|
+
if not client_id:
|
|
217
|
+
client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID")
|
|
218
|
+
if not client_secret:
|
|
219
|
+
client_secret = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET")
|
|
220
|
+
|
|
221
|
+
# Set credentials in environment so OAuth provider can access them
|
|
222
|
+
if client_id and client_secret:
|
|
223
|
+
os.environ["GOOGLE_OAUTH_CLIENT_ID"] = client_id
|
|
224
|
+
os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = client_secret
|
|
225
|
+
|
|
226
|
+
# If credentials missing, prompt for them interactively
|
|
227
|
+
if not client_id or not client_secret:
|
|
228
|
+
console.print("\n[yellow]Google OAuth credentials not found.[/yellow]")
|
|
229
|
+
console.print("Checked: .env.local, .env, and environment variables.\n")
|
|
230
|
+
console.print(
|
|
231
|
+
"Get credentials from: https://console.cloud.google.com/apis/credentials\n"
|
|
232
|
+
)
|
|
233
|
+
console.print("[dim]Tip: Add to .env.local for automatic loading:[/dim]")
|
|
234
|
+
console.print('[dim] GOOGLE_OAUTH_CLIENT_ID="your-client-id"[/dim]')
|
|
235
|
+
console.print(
|
|
236
|
+
'[dim] GOOGLE_OAUTH_CLIENT_SECRET="your-client-secret"[/dim]\n' # pragma: allowlist secret
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
241
|
+
|
|
242
|
+
client_id = pt_prompt("Enter GOOGLE_OAUTH_CLIENT_ID: ")
|
|
243
|
+
if not client_id.strip():
|
|
244
|
+
return CommandResult.error_result("Client ID is required")
|
|
245
|
+
|
|
246
|
+
client_secret = pt_prompt(
|
|
247
|
+
"Enter GOOGLE_OAUTH_CLIENT_SECRET: ", is_password=True
|
|
248
|
+
)
|
|
249
|
+
if not client_secret.strip():
|
|
250
|
+
return CommandResult.error_result("Client Secret is required")
|
|
251
|
+
|
|
252
|
+
# Set in environment for this session
|
|
253
|
+
os.environ["GOOGLE_OAUTH_CLIENT_ID"] = client_id.strip()
|
|
254
|
+
os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = client_secret.strip()
|
|
255
|
+
console.print("\n[green]Credentials set for this session.[/green]")
|
|
256
|
+
|
|
257
|
+
except (EOFError, KeyboardInterrupt):
|
|
258
|
+
return CommandResult.error_result("Credential entry cancelled")
|
|
259
|
+
except ImportError:
|
|
260
|
+
return CommandResult.error_result(
|
|
261
|
+
"prompt_toolkit not available for interactive input. "
|
|
262
|
+
"Please set GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET environment variables."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Run OAuth flow
|
|
266
|
+
try:
|
|
267
|
+
from claude_mpm.auth import OAuthManager
|
|
268
|
+
from claude_mpm.auth.callback_server import DEFAULT_PORT
|
|
269
|
+
from claude_mpm.auth.providers.google import OAuthError
|
|
270
|
+
|
|
271
|
+
manager = OAuthManager()
|
|
272
|
+
|
|
273
|
+
# Get the actual callback port from the server
|
|
274
|
+
callback_port = DEFAULT_PORT
|
|
275
|
+
no_browser = getattr(args, "no_browser", False)
|
|
276
|
+
|
|
277
|
+
console.print(f"\n[cyan]Setting up OAuth for '{service_name}'...[/cyan]")
|
|
278
|
+
console.print(
|
|
279
|
+
f"Callback server listening on http://localhost:{callback_port}/callback"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if not no_browser:
|
|
283
|
+
console.print("Opening browser for authentication...")
|
|
284
|
+
else:
|
|
285
|
+
console.print(
|
|
286
|
+
"[yellow]Browser auto-open disabled. Please open the URL manually.[/yellow]"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Run async OAuth flow - authenticate returns OAuthToken directly
|
|
290
|
+
# and raises OAuthError on failure
|
|
291
|
+
token = asyncio.run(
|
|
292
|
+
manager.authenticate(
|
|
293
|
+
service_name=service_name,
|
|
294
|
+
provider_name=provider_name,
|
|
295
|
+
scopes=scopes,
|
|
296
|
+
open_browser=not no_browser,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Success - token was returned
|
|
301
|
+
console.print(f"\n[green]OAuth setup complete for '{service_name}'[/green]")
|
|
302
|
+
if token.expires_at:
|
|
303
|
+
console.print(f" Token expires: {token.expires_at}")
|
|
304
|
+
return CommandResult.success_result(
|
|
305
|
+
f"OAuth setup complete for '{service_name}'"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
except OAuthError as e:
|
|
309
|
+
return CommandResult.error_result(f"OAuth setup failed: {e}")
|
|
310
|
+
except ImportError as e:
|
|
311
|
+
return CommandResult.error_result(f"OAuth module not available: {e}")
|
|
312
|
+
except Exception as e:
|
|
313
|
+
return CommandResult.error_result(f"Error during OAuth setup: {e}")
|
|
314
|
+
|
|
315
|
+
def _show_status(self, args) -> CommandResult:
|
|
316
|
+
"""Show OAuth token status for a service."""
|
|
317
|
+
service_name = args.service_name
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
from claude_mpm.auth import OAuthManager
|
|
321
|
+
from claude_mpm.auth.models import TokenStatus
|
|
322
|
+
|
|
323
|
+
manager = OAuthManager()
|
|
324
|
+
# get_status is synchronous and returns (TokenStatus, StoredToken | None)
|
|
325
|
+
token_status, stored_token = manager.get_status(service_name)
|
|
326
|
+
|
|
327
|
+
if token_status == TokenStatus.MISSING or stored_token is None:
|
|
328
|
+
console.print(
|
|
329
|
+
f"[yellow]No OAuth tokens found for '{service_name}'[/yellow]"
|
|
330
|
+
)
|
|
331
|
+
return CommandResult.success_result(
|
|
332
|
+
f"No tokens found for '{service_name}'"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Build status dict for display
|
|
336
|
+
is_valid = token_status == TokenStatus.VALID
|
|
337
|
+
status_data = {
|
|
338
|
+
"valid": is_valid,
|
|
339
|
+
"status": token_status.name,
|
|
340
|
+
"expires_at": stored_token.token.expires_at,
|
|
341
|
+
"scopes": stored_token.token.scopes,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# Check output format
|
|
345
|
+
output_format = getattr(args, "format", "table")
|
|
346
|
+
|
|
347
|
+
if output_format == "json":
|
|
348
|
+
console.print(json.dumps(status_data, indent=2, default=str))
|
|
349
|
+
return CommandResult.success_result(
|
|
350
|
+
"Status displayed", data=status_data
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Table format
|
|
354
|
+
self._print_token_status(service_name, status_data)
|
|
355
|
+
return CommandResult.success_result("Status displayed")
|
|
356
|
+
|
|
357
|
+
except ImportError:
|
|
358
|
+
return CommandResult.error_result("OAuth module not available")
|
|
359
|
+
except Exception as e:
|
|
360
|
+
return CommandResult.error_result(f"Error checking status: {e}")
|
|
361
|
+
|
|
362
|
+
def _print_token_status(self, name: str, status: dict[str, Any]) -> None:
|
|
363
|
+
"""Print token status information."""
|
|
364
|
+
panel_content = []
|
|
365
|
+
panel_content.append(f"[bold]Service:[/bold] {name}")
|
|
366
|
+
panel_content.append("[bold]Stored:[/bold] Yes")
|
|
367
|
+
|
|
368
|
+
if status.get("valid"):
|
|
369
|
+
panel_content.append("[bold]Status:[/bold] [green]Valid[/green]")
|
|
370
|
+
else:
|
|
371
|
+
panel_content.append("[bold]Status:[/bold] [red]Invalid/Expired[/red]")
|
|
372
|
+
|
|
373
|
+
if status.get("expires_at"):
|
|
374
|
+
panel_content.append(f"[bold]Expires:[/bold] {status['expires_at']}")
|
|
375
|
+
|
|
376
|
+
if status.get("scopes"):
|
|
377
|
+
scopes = ", ".join(status["scopes"])
|
|
378
|
+
panel_content.append(f"[bold]Scopes:[/bold] {scopes}")
|
|
379
|
+
|
|
380
|
+
panel = Panel(
|
|
381
|
+
"\n".join(panel_content),
|
|
382
|
+
title="OAuth Token Status",
|
|
383
|
+
border_style="green" if status.get("valid") else "red",
|
|
384
|
+
)
|
|
385
|
+
console.print(panel)
|
|
386
|
+
|
|
387
|
+
def _revoke_tokens(self, args) -> CommandResult:
|
|
388
|
+
"""Revoke OAuth tokens for a service."""
|
|
389
|
+
service_name = args.service_name
|
|
390
|
+
|
|
391
|
+
# Confirm unless -y flag
|
|
392
|
+
if not getattr(args, "yes", False):
|
|
393
|
+
console.print(
|
|
394
|
+
f"[yellow]This will revoke OAuth tokens for '{service_name}'.[/yellow]"
|
|
395
|
+
)
|
|
396
|
+
try:
|
|
397
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
398
|
+
|
|
399
|
+
confirm = pt_prompt("Are you sure? (y/N): ")
|
|
400
|
+
if confirm.lower() not in ("y", "yes"):
|
|
401
|
+
return CommandResult.success_result("Revocation cancelled")
|
|
402
|
+
except (EOFError, KeyboardInterrupt):
|
|
403
|
+
return CommandResult.success_result("Revocation cancelled")
|
|
404
|
+
except ImportError:
|
|
405
|
+
# No prompt_toolkit, proceed without confirmation
|
|
406
|
+
pass
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
from claude_mpm.auth import OAuthManager
|
|
410
|
+
|
|
411
|
+
manager = OAuthManager()
|
|
412
|
+
|
|
413
|
+
console.print(f"[cyan]Revoking OAuth tokens for '{service_name}'...[/cyan]")
|
|
414
|
+
# revoke() returns bool directly
|
|
415
|
+
revoked = asyncio.run(manager.revoke(service_name))
|
|
416
|
+
|
|
417
|
+
if revoked:
|
|
418
|
+
console.print(
|
|
419
|
+
f"[green]OAuth tokens revoked for '{service_name}'[/green]"
|
|
420
|
+
)
|
|
421
|
+
return CommandResult.success_result(
|
|
422
|
+
f"Tokens revoked for '{service_name}'"
|
|
423
|
+
)
|
|
424
|
+
return CommandResult.error_result(
|
|
425
|
+
f"Failed to revoke tokens for '{service_name}'"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
except ImportError:
|
|
429
|
+
return CommandResult.error_result("OAuth module not available")
|
|
430
|
+
except Exception as e:
|
|
431
|
+
return CommandResult.error_result(f"Error revoking tokens: {e}")
|
|
432
|
+
|
|
433
|
+
def _refresh_tokens(self, args) -> CommandResult:
|
|
434
|
+
"""Refresh OAuth tokens for a service."""
|
|
435
|
+
service_name = args.service_name
|
|
436
|
+
|
|
437
|
+
try:
|
|
438
|
+
from claude_mpm.auth import OAuthManager
|
|
439
|
+
from claude_mpm.auth.providers.google import OAuthError
|
|
440
|
+
|
|
441
|
+
manager = OAuthManager()
|
|
442
|
+
|
|
443
|
+
console.print(
|
|
444
|
+
f"[cyan]Refreshing OAuth tokens for '{service_name}'...[/cyan]"
|
|
445
|
+
)
|
|
446
|
+
# refresh_if_needed() returns Optional[OAuthToken]
|
|
447
|
+
token = asyncio.run(manager.refresh_if_needed(service_name))
|
|
448
|
+
|
|
449
|
+
if token is not None:
|
|
450
|
+
console.print(
|
|
451
|
+
f"[green]OAuth tokens refreshed for '{service_name}'[/green]"
|
|
452
|
+
)
|
|
453
|
+
if token.expires_at:
|
|
454
|
+
console.print(f" New expiry: {token.expires_at}")
|
|
455
|
+
return CommandResult.success_result(
|
|
456
|
+
f"Tokens refreshed for '{service_name}'"
|
|
457
|
+
)
|
|
458
|
+
return CommandResult.error_result(
|
|
459
|
+
f"Failed to refresh tokens for '{service_name}' - no token found or no refresh token available"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
except OAuthError as e:
|
|
463
|
+
return CommandResult.error_result(f"Failed to refresh: {e}")
|
|
464
|
+
except ImportError:
|
|
465
|
+
return CommandResult.error_result("OAuth module not available")
|
|
466
|
+
except Exception as e:
|
|
467
|
+
return CommandResult.error_result(f"Error refreshing tokens: {e}")
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def manage_oauth(args) -> int:
|
|
471
|
+
"""Main entry point for OAuth management commands.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
args: Parsed command line arguments
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Exit code (0 for success, non-zero for errors)
|
|
478
|
+
"""
|
|
479
|
+
command = OAuthCommand()
|
|
480
|
+
result = command.execute(args)
|
|
481
|
+
return result.exit_code
|
|
@@ -11,6 +11,7 @@ for better UX. Handles errors gracefully with actionable messages.
|
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
13
|
import logging
|
|
14
|
+
import os
|
|
14
15
|
import re
|
|
15
16
|
|
|
16
17
|
from ...config.skill_sources import SkillSource, SkillSourceConfiguration
|
|
@@ -20,6 +21,33 @@ from ...services.skills.skill_discovery_service import SkillDiscoveryService
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def _get_github_token(source: SkillSource | None = None) -> str | None:
|
|
25
|
+
"""Get GitHub token with source-specific override support.
|
|
26
|
+
|
|
27
|
+
Priority: source.token > GITHUB_TOKEN > GH_TOKEN
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
source: Optional SkillSource to check for per-source token
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
GitHub token if found, None otherwise
|
|
34
|
+
|
|
35
|
+
Security Note:
|
|
36
|
+
Token is never logged or printed to avoid exposure.
|
|
37
|
+
"""
|
|
38
|
+
# Priority 1: Per-source token (env var reference or direct)
|
|
39
|
+
if source and source.token:
|
|
40
|
+
if source.token.startswith("$"):
|
|
41
|
+
# Env var reference: $VAR_NAME -> os.environ.get("VAR_NAME")
|
|
42
|
+
env_var_name = source.token[1:]
|
|
43
|
+
return os.environ.get(env_var_name)
|
|
44
|
+
# Direct token (not recommended but supported)
|
|
45
|
+
return source.token
|
|
46
|
+
|
|
47
|
+
# Priority 2-3: Global environment variables
|
|
48
|
+
return os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
49
|
+
|
|
50
|
+
|
|
23
51
|
def _test_skill_repository_access(source: SkillSource) -> dict:
|
|
24
52
|
"""Test if skill repository is accessible via GitHub API.
|
|
25
53
|
|
|
@@ -58,7 +86,13 @@ def _test_skill_repository_access(source: SkillSource) -> dict:
|
|
|
58
86
|
# Test GitHub API access
|
|
59
87
|
api_url = f"https://api.github.com/repos/{owner_repo}"
|
|
60
88
|
|
|
61
|
-
|
|
89
|
+
# Build headers with authentication if token available
|
|
90
|
+
headers = {"Accept": "application/vnd.github+json"}
|
|
91
|
+
token = _get_github_token(source)
|
|
92
|
+
if token:
|
|
93
|
+
headers["Authorization"] = f"token {token}"
|
|
94
|
+
|
|
95
|
+
response = requests.get(api_url, headers=headers, timeout=10)
|
|
62
96
|
|
|
63
97
|
if response.status_code == 200:
|
|
64
98
|
return {"accessible": True, "error": None}
|
|
@@ -68,9 +102,14 @@ def _test_skill_repository_access(source: SkillSource) -> dict:
|
|
|
68
102
|
"error": f"Repository not found: {owner_repo}",
|
|
69
103
|
}
|
|
70
104
|
if response.status_code == 403:
|
|
105
|
+
error_msg = "Access denied (private repository or rate limit)"
|
|
106
|
+
if not token:
|
|
107
|
+
error_msg += (
|
|
108
|
+
". Try setting GITHUB_TOKEN environment variable for private repos"
|
|
109
|
+
)
|
|
71
110
|
return {
|
|
72
111
|
"accessible": False,
|
|
73
|
-
"error":
|
|
112
|
+
"error": error_msg,
|
|
74
113
|
}
|
|
75
114
|
return {
|
|
76
115
|
"accessible": False,
|
|
@@ -263,6 +302,15 @@ def handle_add_skill_source(args) -> int:
|
|
|
263
302
|
|
|
264
303
|
# Create new source
|
|
265
304
|
enabled = not args.disabled
|
|
305
|
+
token = getattr(args, "token", None)
|
|
306
|
+
|
|
307
|
+
# Security warning for direct tokens
|
|
308
|
+
if token and not token.startswith("$"):
|
|
309
|
+
print("⚠️ Warning: Direct token values in config are not recommended")
|
|
310
|
+
print(" Consider using environment variable reference instead:")
|
|
311
|
+
print(" --token $MY_PRIVATE_TOKEN")
|
|
312
|
+
print()
|
|
313
|
+
|
|
266
314
|
source = SkillSource(
|
|
267
315
|
id=source_id,
|
|
268
316
|
type="git",
|
|
@@ -270,6 +318,7 @@ def handle_add_skill_source(args) -> int:
|
|
|
270
318
|
branch=args.branch,
|
|
271
319
|
priority=args.priority,
|
|
272
320
|
enabled=enabled,
|
|
321
|
+
token=token,
|
|
273
322
|
)
|
|
274
323
|
|
|
275
324
|
# Determine if we should test
|
|
@@ -538,6 +538,7 @@ class SkillsManagementCommand(BaseCommand):
|
|
|
538
538
|
toolchain = getattr(args, "toolchain", None)
|
|
539
539
|
categories = getattr(args, "categories", None)
|
|
540
540
|
force = getattr(args, "force", False)
|
|
541
|
+
deploy_all = getattr(args, "all", False)
|
|
541
542
|
|
|
542
543
|
if collection:
|
|
543
544
|
console.print(
|
|
@@ -548,14 +549,15 @@ class SkillsManagementCommand(BaseCommand):
|
|
|
548
549
|
"\n[bold cyan]Deploying skills from default collection...[/bold cyan]\n"
|
|
549
550
|
)
|
|
550
551
|
|
|
551
|
-
#
|
|
552
|
-
#
|
|
552
|
+
# Use selective deployment unless --all flag is provided
|
|
553
|
+
# Selective mode deploys only agent-referenced skills
|
|
554
|
+
# --all mode deploys all available skills from the collection
|
|
553
555
|
result = self.skills_deployer.deploy_skills(
|
|
554
556
|
collection=collection,
|
|
555
557
|
toolchain=toolchain,
|
|
556
558
|
categories=categories,
|
|
557
559
|
force=force,
|
|
558
|
-
selective=
|
|
560
|
+
selective=not deploy_all,
|
|
559
561
|
)
|
|
560
562
|
|
|
561
563
|
# Display results
|
claude_mpm/cli/executor.py
CHANGED
|
@@ -159,6 +159,14 @@ def execute_command(command: str, args) -> int:
|
|
|
159
159
|
result = summarize_command(args)
|
|
160
160
|
return result if result is not None else 0
|
|
161
161
|
|
|
162
|
+
# Handle oauth command with lazy import
|
|
163
|
+
if command == "oauth":
|
|
164
|
+
# Lazy import to avoid loading unless needed
|
|
165
|
+
from .commands.oauth import manage_oauth
|
|
166
|
+
|
|
167
|
+
result = manage_oauth(args)
|
|
168
|
+
return result if result is not None else 0
|
|
169
|
+
|
|
162
170
|
# Handle profile command with lazy import
|
|
163
171
|
if command == "profile":
|
|
164
172
|
# Lazy import to avoid loading unless needed
|
|
@@ -390,6 +398,7 @@ def execute_command(command: str, args) -> int:
|
|
|
390
398
|
"agent-source",
|
|
391
399
|
"hook-errors",
|
|
392
400
|
"autotodos",
|
|
401
|
+
"oauth",
|
|
393
402
|
]
|
|
394
403
|
|
|
395
404
|
suggestion = suggest_similar_commands(command, all_commands)
|
claude_mpm/cli/helpers.py
CHANGED
|
@@ -30,7 +30,7 @@ def is_interactive_session() -> bool:
|
|
|
30
30
|
|
|
31
31
|
def should_skip_config_check(command: str | None) -> bool:
|
|
32
32
|
"""Check if command should skip configuration check."""
|
|
33
|
-
skip_commands = ["configure", "doctor", "info", "mcp", "config"]
|
|
33
|
+
skip_commands = ["configure", "doctor", "info", "mcp", "config", "oauth"]
|
|
34
34
|
return command in skip_commands if command else False
|
|
35
35
|
|
|
36
36
|
|
|
@@ -307,6 +307,12 @@ def add_top_level_run_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
307
307
|
action="store_true",
|
|
308
308
|
help="Disable Claude in Chrome integration (passed to Claude Code)",
|
|
309
309
|
)
|
|
310
|
+
run_group.add_argument(
|
|
311
|
+
"--mcp",
|
|
312
|
+
type=str,
|
|
313
|
+
metavar="SERVICES",
|
|
314
|
+
help="Comma-separated list of MCP services to enable for this session (e.g., --mcp kuzu-memory,mcp-ticketer)",
|
|
315
|
+
)
|
|
310
316
|
|
|
311
317
|
# Dependency checking options (for backward compatibility at top level)
|
|
312
318
|
dep_group_top = parser.add_argument_group(
|
|
@@ -509,6 +515,13 @@ def create_parser(
|
|
|
509
515
|
except ImportError:
|
|
510
516
|
pass
|
|
511
517
|
|
|
518
|
+
try:
|
|
519
|
+
from .oauth_parser import add_oauth_subparser
|
|
520
|
+
|
|
521
|
+
add_oauth_subparser(subparsers)
|
|
522
|
+
except ImportError:
|
|
523
|
+
pass
|
|
524
|
+
|
|
512
525
|
# Add uninstall command parser
|
|
513
526
|
try:
|
|
514
527
|
from ..commands.uninstall import add_uninstall_parser
|