hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.1__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.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +6 -0
- hanzo_mcp/__main__.py +1 -1
- hanzo_mcp/analytics/__init__.py +2 -2
- hanzo_mcp/analytics/posthog_analytics.py +76 -82
- hanzo_mcp/cli.py +31 -36
- hanzo_mcp/cli_enhanced.py +94 -72
- hanzo_mcp/cli_plugin.py +27 -17
- hanzo_mcp/config/__init__.py +2 -2
- hanzo_mcp/config/settings.py +112 -88
- hanzo_mcp/config/tool_config.py +32 -34
- hanzo_mcp/dev_server.py +66 -67
- hanzo_mcp/prompts/__init__.py +94 -12
- hanzo_mcp/prompts/enhanced_prompts.py +809 -0
- hanzo_mcp/prompts/example_custom_prompt.py +6 -5
- hanzo_mcp/prompts/project_todo_reminder.py +0 -1
- hanzo_mcp/prompts/tool_explorer.py +10 -7
- hanzo_mcp/server.py +17 -21
- hanzo_mcp/server_enhanced.py +15 -22
- hanzo_mcp/tools/__init__.py +56 -28
- hanzo_mcp/tools/agent/__init__.py +16 -19
- hanzo_mcp/tools/agent/agent.py +82 -65
- hanzo_mcp/tools/agent/agent_tool.py +152 -122
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
- hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
- hanzo_mcp/tools/agent/clarification_tool.py +11 -10
- hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
- hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
- hanzo_mcp/tools/agent/code_auth.py +102 -107
- hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
- hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
- hanzo_mcp/tools/agent/critic_tool.py +86 -73
- hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/iching_tool.py +404 -139
- hanzo_mcp/tools/agent/network_tool.py +89 -73
- hanzo_mcp/tools/agent/prompt.py +2 -1
- hanzo_mcp/tools/agent/review_tool.py +101 -98
- hanzo_mcp/tools/agent/swarm_alias.py +87 -0
- hanzo_mcp/tools/agent/swarm_tool.py +246 -161
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
- hanzo_mcp/tools/agent/tool_adapter.py +21 -11
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +3 -5
- hanzo_mcp/tools/common/batch_tool.py +46 -39
- hanzo_mcp/tools/common/config_tool.py +120 -84
- hanzo_mcp/tools/common/context.py +1 -5
- hanzo_mcp/tools/common/context_fix.py +5 -3
- hanzo_mcp/tools/common/critic_tool.py +4 -8
- hanzo_mcp/tools/common/decorators.py +58 -56
- hanzo_mcp/tools/common/enhanced_base.py +29 -32
- hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
- hanzo_mcp/tools/common/forgiving_edit.py +91 -87
- hanzo_mcp/tools/common/mode.py +15 -17
- hanzo_mcp/tools/common/mode_loader.py +27 -24
- hanzo_mcp/tools/common/paginated_base.py +61 -53
- hanzo_mcp/tools/common/paginated_response.py +72 -79
- hanzo_mcp/tools/common/pagination.py +50 -53
- hanzo_mcp/tools/common/permissions.py +4 -4
- hanzo_mcp/tools/common/personality.py +186 -138
- hanzo_mcp/tools/common/plugin_loader.py +54 -54
- hanzo_mcp/tools/common/stats.py +65 -47
- hanzo_mcp/tools/common/test_helpers.py +31 -0
- hanzo_mcp/tools/common/thinking_tool.py +4 -8
- hanzo_mcp/tools/common/tool_disable.py +17 -12
- hanzo_mcp/tools/common/tool_enable.py +13 -14
- hanzo_mcp/tools/common/tool_list.py +36 -28
- hanzo_mcp/tools/common/truncate.py +23 -23
- hanzo_mcp/tools/config/__init__.py +4 -4
- hanzo_mcp/tools/config/config_tool.py +42 -29
- hanzo_mcp/tools/config/index_config.py +37 -34
- hanzo_mcp/tools/config/mode_tool.py +175 -55
- hanzo_mcp/tools/database/__init__.py +15 -12
- hanzo_mcp/tools/database/database_manager.py +77 -75
- hanzo_mcp/tools/database/graph.py +137 -91
- hanzo_mcp/tools/database/graph_add.py +30 -18
- hanzo_mcp/tools/database/graph_query.py +178 -102
- hanzo_mcp/tools/database/graph_remove.py +33 -28
- hanzo_mcp/tools/database/graph_search.py +97 -75
- hanzo_mcp/tools/database/graph_stats.py +91 -59
- hanzo_mcp/tools/database/sql.py +107 -79
- hanzo_mcp/tools/database/sql_query.py +30 -24
- hanzo_mcp/tools/database/sql_search.py +29 -25
- hanzo_mcp/tools/database/sql_stats.py +47 -35
- hanzo_mcp/tools/editor/neovim_command.py +25 -28
- hanzo_mcp/tools/editor/neovim_edit.py +21 -23
- hanzo_mcp/tools/editor/neovim_session.py +60 -54
- hanzo_mcp/tools/filesystem/__init__.py +31 -30
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
- hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +316 -224
- hanzo_mcp/tools/filesystem/content_replace.py +4 -4
- hanzo_mcp/tools/filesystem/diff.py +71 -59
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
- hanzo_mcp/tools/filesystem/edit.py +4 -4
- hanzo_mcp/tools/filesystem/find.py +173 -80
- hanzo_mcp/tools/filesystem/find_files.py +73 -52
- hanzo_mcp/tools/filesystem/git_search.py +157 -104
- hanzo_mcp/tools/filesystem/grep.py +8 -8
- hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
- hanzo_mcp/tools/filesystem/read.py +12 -10
- hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
- hanzo_mcp/tools/filesystem/search_tool.py +263 -207
- hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
- hanzo_mcp/tools/filesystem/tree.py +35 -33
- hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
- hanzo_mcp/tools/filesystem/watch.py +37 -36
- hanzo_mcp/tools/filesystem/write.py +4 -8
- hanzo_mcp/tools/jupyter/__init__.py +4 -4
- hanzo_mcp/tools/jupyter/base.py +4 -5
- hanzo_mcp/tools/jupyter/jupyter.py +67 -47
- hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
- hanzo_mcp/tools/llm/__init__.py +5 -7
- hanzo_mcp/tools/llm/consensus_tool.py +72 -52
- hanzo_mcp/tools/llm/llm_manage.py +101 -60
- hanzo_mcp/tools/llm/llm_tool.py +226 -166
- hanzo_mcp/tools/llm/provider_tools.py +25 -26
- hanzo_mcp/tools/lsp/__init__.py +1 -1
- hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -3
- hanzo_mcp/tools/mcp/mcp_add.py +27 -25
- hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
- hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
- hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
- hanzo_mcp/tools/memory/__init__.py +39 -21
- hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
- hanzo_mcp/tools/memory/memory_tools.py +90 -108
- hanzo_mcp/tools/search/__init__.py +7 -2
- hanzo_mcp/tools/search/find_tool.py +297 -212
- hanzo_mcp/tools/search/unified_search.py +366 -314
- hanzo_mcp/tools/shell/__init__.py +8 -7
- hanzo_mcp/tools/shell/auto_background.py +56 -49
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +75 -75
- hanzo_mcp/tools/shell/bash_session.py +2 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
- hanzo_mcp/tools/shell/bash_tool.py +24 -31
- hanzo_mcp/tools/shell/command_executor.py +12 -12
- hanzo_mcp/tools/shell/logs.py +43 -33
- hanzo_mcp/tools/shell/npx.py +13 -13
- hanzo_mcp/tools/shell/npx_background.py +24 -21
- hanzo_mcp/tools/shell/npx_tool.py +18 -22
- hanzo_mcp/tools/shell/open.py +19 -21
- hanzo_mcp/tools/shell/pkill.py +31 -26
- hanzo_mcp/tools/shell/process_tool.py +32 -32
- hanzo_mcp/tools/shell/processes.py +57 -58
- hanzo_mcp/tools/shell/run_background.py +24 -25
- hanzo_mcp/tools/shell/run_command.py +5 -5
- hanzo_mcp/tools/shell/run_command_windows.py +5 -5
- hanzo_mcp/tools/shell/session_storage.py +3 -3
- hanzo_mcp/tools/shell/streaming_command.py +141 -126
- hanzo_mcp/tools/shell/uvx.py +24 -25
- hanzo_mcp/tools/shell/uvx_background.py +35 -33
- hanzo_mcp/tools/shell/uvx_tool.py +18 -22
- hanzo_mcp/tools/todo/__init__.py +6 -2
- hanzo_mcp/tools/todo/todo.py +50 -37
- hanzo_mcp/tools/todo/todo_read.py +5 -8
- hanzo_mcp/tools/todo/todo_write.py +5 -7
- hanzo_mcp/tools/vector/__init__.py +40 -28
- hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
- hanzo_mcp/tools/vector/git_ingester.py +170 -179
- hanzo_mcp/tools/vector/index_tool.py +96 -44
- hanzo_mcp/tools/vector/infinity_store.py +283 -228
- hanzo_mcp/tools/vector/mock_infinity.py +39 -40
- hanzo_mcp/tools/vector/project_manager.py +88 -78
- hanzo_mcp/tools/vector/vector.py +59 -42
- hanzo_mcp/tools/vector/vector_index.py +30 -27
- hanzo_mcp/tools/vector/vector_search.py +64 -45
- hanzo_mcp/types.py +6 -4
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/top_level.txt +0 -0
|
@@ -4,17 +4,15 @@ This module provides tools to automate Claude Desktop login/logout,
|
|
|
4
4
|
manage separate accounts for swarm agents, and handle authentication flows.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import asyncio
|
|
8
7
|
import os
|
|
9
|
-
import re
|
|
10
|
-
import subprocess
|
|
11
|
-
import tempfile
|
|
12
|
-
import time
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import Optional, Tuple, Dict, Any
|
|
15
8
|
import json
|
|
9
|
+
import time
|
|
10
|
+
import asyncio
|
|
11
|
+
import subprocess
|
|
16
12
|
import webbrowser
|
|
17
|
-
from
|
|
13
|
+
from typing import Any, Dict, Tuple, Optional
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from urllib.parse import parse_qs
|
|
18
16
|
|
|
19
17
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
20
18
|
from hanzo_mcp.tools.common.context import create_tool_context
|
|
@@ -22,97 +20,91 @@ from hanzo_mcp.tools.common.context import create_tool_context
|
|
|
22
20
|
|
|
23
21
|
class ClaudeDesktopAuth:
|
|
24
22
|
"""Manages Claude Desktop authentication."""
|
|
25
|
-
|
|
23
|
+
|
|
26
24
|
# Claude Desktop paths
|
|
27
25
|
CLAUDE_APP_MAC = "/Applications/Claude.app"
|
|
28
26
|
CLAUDE_CONFIG_DIR = Path.home() / ".claude"
|
|
29
27
|
CLAUDE_SESSION_FILE = CLAUDE_CONFIG_DIR / "session.json"
|
|
30
28
|
CLAUDE_ACCOUNTS_FILE = CLAUDE_CONFIG_DIR / "accounts.json"
|
|
31
|
-
|
|
29
|
+
|
|
32
30
|
# Authentication endpoints
|
|
33
31
|
CLAUDE_LOGIN_URL = "https://claude.ai/login"
|
|
34
32
|
CLAUDE_API_URL = "https://api.claude.ai"
|
|
35
|
-
|
|
33
|
+
|
|
36
34
|
def __init__(self):
|
|
37
35
|
"""Initialize Claude Desktop auth manager."""
|
|
38
36
|
self.ensure_config_dir()
|
|
39
|
-
|
|
37
|
+
|
|
40
38
|
def ensure_config_dir(self):
|
|
41
39
|
"""Ensure Claude config directory exists."""
|
|
42
40
|
self.CLAUDE_CONFIG_DIR.mkdir(exist_ok=True)
|
|
43
|
-
|
|
41
|
+
|
|
44
42
|
def is_claude_installed(self) -> bool:
|
|
45
43
|
"""Check if Claude Desktop is installed."""
|
|
46
44
|
if os.path.exists(self.CLAUDE_APP_MAC):
|
|
47
45
|
return True
|
|
48
|
-
|
|
46
|
+
|
|
49
47
|
# Check if claude command is available
|
|
50
48
|
try:
|
|
51
|
-
result = subprocess.run(
|
|
52
|
-
["which", "claude"],
|
|
53
|
-
capture_output=True,
|
|
54
|
-
text=True
|
|
55
|
-
)
|
|
49
|
+
result = subprocess.run(["which", "claude"], capture_output=True, text=True)
|
|
56
50
|
return result.returncode == 0
|
|
57
|
-
except:
|
|
51
|
+
except Exception:
|
|
58
52
|
return False
|
|
59
|
-
|
|
53
|
+
|
|
60
54
|
def is_logged_in(self, account: Optional[str] = None) -> bool:
|
|
61
55
|
"""Check if Claude Desktop is logged in.
|
|
62
|
-
|
|
56
|
+
|
|
63
57
|
Args:
|
|
64
58
|
account: Optional account identifier to check
|
|
65
|
-
|
|
59
|
+
|
|
66
60
|
Returns:
|
|
67
61
|
True if logged in
|
|
68
62
|
"""
|
|
69
63
|
if not self.CLAUDE_SESSION_FILE.exists():
|
|
70
64
|
return False
|
|
71
|
-
|
|
65
|
+
|
|
72
66
|
try:
|
|
73
|
-
with open(self.CLAUDE_SESSION_FILE,
|
|
67
|
+
with open(self.CLAUDE_SESSION_FILE, "r") as f:
|
|
74
68
|
session = json.load(f)
|
|
75
|
-
|
|
69
|
+
|
|
76
70
|
# Check if session is valid
|
|
77
71
|
if not session.get("access_token"):
|
|
78
72
|
return False
|
|
79
|
-
|
|
73
|
+
|
|
80
74
|
# Check expiry if available
|
|
81
75
|
if "expires_at" in session:
|
|
82
76
|
if time.time() > session["expires_at"]:
|
|
83
77
|
return False
|
|
84
|
-
|
|
78
|
+
|
|
85
79
|
# Check specific account if requested
|
|
86
80
|
if account and session.get("account") != account:
|
|
87
81
|
return False
|
|
88
|
-
|
|
82
|
+
|
|
89
83
|
return True
|
|
90
|
-
except:
|
|
84
|
+
except Exception:
|
|
91
85
|
return False
|
|
92
|
-
|
|
86
|
+
|
|
93
87
|
def get_current_account(self) -> Optional[str]:
|
|
94
88
|
"""Get the currently logged in account."""
|
|
95
89
|
if not self.is_logged_in():
|
|
96
90
|
return None
|
|
97
|
-
|
|
91
|
+
|
|
98
92
|
try:
|
|
99
|
-
with open(self.CLAUDE_SESSION_FILE,
|
|
93
|
+
with open(self.CLAUDE_SESSION_FILE, "r") as f:
|
|
100
94
|
session = json.load(f)
|
|
101
95
|
return session.get("account", session.get("email"))
|
|
102
|
-
except:
|
|
96
|
+
except Exception:
|
|
103
97
|
return None
|
|
104
|
-
|
|
98
|
+
|
|
105
99
|
async def login_interactive(
|
|
106
|
-
self,
|
|
107
|
-
account: Optional[str] = None,
|
|
108
|
-
headless: bool = False
|
|
100
|
+
self, account: Optional[str] = None, headless: bool = False
|
|
109
101
|
) -> Tuple[bool, str]:
|
|
110
102
|
"""Login to Claude Desktop interactively.
|
|
111
|
-
|
|
103
|
+
|
|
112
104
|
Args:
|
|
113
105
|
account: Optional account email/identifier
|
|
114
106
|
headless: Whether to run in headless mode
|
|
115
|
-
|
|
107
|
+
|
|
116
108
|
Returns:
|
|
117
109
|
Tuple of (success, message)
|
|
118
110
|
"""
|
|
@@ -120,30 +112,30 @@ class ClaudeDesktopAuth:
|
|
|
120
112
|
if self.is_logged_in(account):
|
|
121
113
|
current = self.get_current_account()
|
|
122
114
|
return True, f"Already logged in as {current}"
|
|
123
|
-
|
|
115
|
+
|
|
124
116
|
# Start login flow
|
|
125
117
|
if headless:
|
|
126
118
|
return await self._login_headless(account)
|
|
127
119
|
else:
|
|
128
120
|
return await self._login_browser(account)
|
|
129
|
-
|
|
121
|
+
|
|
130
122
|
async def _login_browser(self, account: Optional[str]) -> Tuple[bool, str]:
|
|
131
123
|
"""Login using browser flow."""
|
|
132
124
|
# Generate state for OAuth-like flow
|
|
133
125
|
state = os.urandom(16).hex()
|
|
134
|
-
|
|
126
|
+
|
|
135
127
|
# Create callback server
|
|
136
128
|
callback_port = 9876
|
|
137
129
|
auth_code = None
|
|
138
|
-
|
|
130
|
+
|
|
139
131
|
async def handle_callback(reader, writer):
|
|
140
132
|
"""Handle OAuth callback."""
|
|
141
133
|
nonlocal auth_code
|
|
142
|
-
|
|
134
|
+
|
|
143
135
|
# Read request
|
|
144
136
|
request = await reader.read(1024)
|
|
145
137
|
request_str = request.decode()
|
|
146
|
-
|
|
138
|
+
|
|
147
139
|
# Extract code from query params
|
|
148
140
|
if "GET /" in request_str:
|
|
149
141
|
path = request_str.split(" ")[1]
|
|
@@ -152,7 +144,7 @@ class ClaudeDesktopAuth:
|
|
|
152
144
|
params = parse_qs(query)
|
|
153
145
|
if "code" in params:
|
|
154
146
|
auth_code = params["code"][0]
|
|
155
|
-
|
|
147
|
+
|
|
156
148
|
# Send response
|
|
157
149
|
response = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"
|
|
158
150
|
response += b"<html><body><h1>Authentication successful!</h1>"
|
|
@@ -160,30 +152,26 @@ class ClaudeDesktopAuth:
|
|
|
160
152
|
writer.write(response)
|
|
161
153
|
await writer.drain()
|
|
162
154
|
writer.close()
|
|
163
|
-
|
|
155
|
+
|
|
164
156
|
# Start callback server
|
|
165
|
-
server = await asyncio.start_server(
|
|
166
|
-
|
|
167
|
-
'localhost',
|
|
168
|
-
callback_port
|
|
169
|
-
)
|
|
170
|
-
|
|
157
|
+
server = await asyncio.start_server(handle_callback, "localhost", callback_port)
|
|
158
|
+
|
|
171
159
|
# Build login URL
|
|
172
160
|
login_url = f"{self.CLAUDE_LOGIN_URL}?callback=http://localhost:{callback_port}&state={state}"
|
|
173
161
|
if account:
|
|
174
162
|
login_url += f"&login_hint={account}"
|
|
175
|
-
|
|
163
|
+
|
|
176
164
|
# Open browser
|
|
177
165
|
print(f"Opening browser for Claude login...")
|
|
178
166
|
print(f"URL: {login_url}")
|
|
179
167
|
webbrowser.open(login_url)
|
|
180
|
-
|
|
168
|
+
|
|
181
169
|
# Wait for callback (timeout after 2 minutes)
|
|
182
170
|
try:
|
|
183
171
|
start_time = time.time()
|
|
184
172
|
while not auth_code and (time.time() - start_time) < 120:
|
|
185
173
|
await asyncio.sleep(0.5)
|
|
186
|
-
|
|
174
|
+
|
|
187
175
|
if auth_code:
|
|
188
176
|
# Exchange code for session
|
|
189
177
|
success = await self._exchange_code_for_session(auth_code, account)
|
|
@@ -193,21 +181,19 @@ class ClaudeDesktopAuth:
|
|
|
193
181
|
return False, "Failed to exchange auth code for session"
|
|
194
182
|
else:
|
|
195
183
|
return False, "Login timeout - no auth code received"
|
|
196
|
-
|
|
184
|
+
|
|
197
185
|
finally:
|
|
198
186
|
server.close()
|
|
199
187
|
await server.wait_closed()
|
|
200
|
-
|
|
188
|
+
|
|
201
189
|
async def _login_headless(self, account: Optional[str]) -> Tuple[bool, str]:
|
|
202
190
|
"""Login in headless mode using TTY automation."""
|
|
203
191
|
# This would use expect/pexpect or similar to automate the CLI
|
|
204
192
|
# For now, return a placeholder
|
|
205
193
|
return False, "Headless login not yet implemented"
|
|
206
|
-
|
|
194
|
+
|
|
207
195
|
async def _exchange_code_for_session(
|
|
208
|
-
self,
|
|
209
|
-
code: str,
|
|
210
|
-
account: Optional[str]
|
|
196
|
+
self, code: str, account: Optional[str]
|
|
211
197
|
) -> bool:
|
|
212
198
|
"""Exchange auth code for session token."""
|
|
213
199
|
# This would make API calls to exchange the code
|
|
@@ -217,181 +203,181 @@ class ClaudeDesktopAuth:
|
|
|
217
203
|
"account": account or "default",
|
|
218
204
|
"email": account,
|
|
219
205
|
"expires_at": time.time() + 3600 * 24, # 24 hours
|
|
220
|
-
"created_at": time.time()
|
|
206
|
+
"created_at": time.time(),
|
|
221
207
|
}
|
|
222
|
-
|
|
208
|
+
|
|
223
209
|
try:
|
|
224
|
-
with open(self.CLAUDE_SESSION_FILE,
|
|
210
|
+
with open(self.CLAUDE_SESSION_FILE, "w") as f:
|
|
225
211
|
json.dump(session, f, indent=2)
|
|
226
212
|
return True
|
|
227
|
-
except:
|
|
213
|
+
except Exception:
|
|
228
214
|
return False
|
|
229
|
-
|
|
215
|
+
|
|
230
216
|
async def logout(self, account: Optional[str] = None) -> Tuple[bool, str]:
|
|
231
217
|
"""Logout from Claude Desktop.
|
|
232
|
-
|
|
218
|
+
|
|
233
219
|
Args:
|
|
234
220
|
account: Optional account to logout (if multiple accounts)
|
|
235
|
-
|
|
221
|
+
|
|
236
222
|
Returns:
|
|
237
223
|
Tuple of (success, message)
|
|
238
224
|
"""
|
|
239
225
|
current = self.get_current_account()
|
|
240
|
-
|
|
226
|
+
|
|
241
227
|
if not current:
|
|
242
228
|
return True, "No active session to logout"
|
|
243
|
-
|
|
229
|
+
|
|
244
230
|
if account and current != account:
|
|
245
231
|
return False, f"Not logged in as {account} (current: {current})"
|
|
246
|
-
|
|
232
|
+
|
|
247
233
|
try:
|
|
248
234
|
# Remove session file
|
|
249
235
|
if self.CLAUDE_SESSION_FILE.exists():
|
|
250
236
|
self.CLAUDE_SESSION_FILE.unlink()
|
|
251
|
-
|
|
237
|
+
|
|
252
238
|
# Clear any cached credentials
|
|
253
239
|
self._clear_credentials_cache()
|
|
254
|
-
|
|
240
|
+
|
|
255
241
|
return True, f"Successfully logged out {current}"
|
|
256
242
|
except Exception as e:
|
|
257
243
|
return False, f"Logout failed: {str(e)}"
|
|
258
|
-
|
|
244
|
+
|
|
259
245
|
def _clear_credentials_cache(self):
|
|
260
246
|
"""Clear any cached credentials."""
|
|
261
247
|
# Clear keychain on macOS
|
|
262
248
|
if os.path.exists("/usr/bin/security"):
|
|
263
249
|
try:
|
|
264
|
-
subprocess.run(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
250
|
+
subprocess.run(
|
|
251
|
+
[
|
|
252
|
+
"/usr/bin/security",
|
|
253
|
+
"delete-generic-password",
|
|
254
|
+
"-s",
|
|
255
|
+
"claude.ai",
|
|
256
|
+
"-a",
|
|
257
|
+
"claude-desktop",
|
|
258
|
+
],
|
|
259
|
+
capture_output=True,
|
|
260
|
+
)
|
|
261
|
+
except Exception:
|
|
271
262
|
pass
|
|
272
|
-
|
|
263
|
+
|
|
273
264
|
def switch_account(self, account: str) -> Tuple[bool, str]:
|
|
274
265
|
"""Switch to a different Claude account.
|
|
275
|
-
|
|
266
|
+
|
|
276
267
|
Args:
|
|
277
268
|
account: Account identifier to switch to
|
|
278
|
-
|
|
269
|
+
|
|
279
270
|
Returns:
|
|
280
271
|
Tuple of (success, message)
|
|
281
272
|
"""
|
|
282
273
|
# Load accounts configuration
|
|
283
274
|
accounts = self._load_accounts()
|
|
284
|
-
|
|
275
|
+
|
|
285
276
|
if account not in accounts:
|
|
286
277
|
return False, f"Unknown account: {account}"
|
|
287
|
-
|
|
278
|
+
|
|
288
279
|
# Save current session if any
|
|
289
280
|
current = self.get_current_account()
|
|
290
281
|
if current and current != account:
|
|
291
282
|
self._save_session_for_account(current)
|
|
292
|
-
|
|
283
|
+
|
|
293
284
|
# Load session for new account
|
|
294
285
|
if self._load_session_for_account(account):
|
|
295
286
|
return True, f"Switched to account: {account}"
|
|
296
287
|
else:
|
|
297
288
|
return False, f"No saved session for account: {account}"
|
|
298
|
-
|
|
289
|
+
|
|
299
290
|
def _load_accounts(self) -> Dict[str, Any]:
|
|
300
291
|
"""Load accounts configuration."""
|
|
301
292
|
if not self.CLAUDE_ACCOUNTS_FILE.exists():
|
|
302
293
|
return {}
|
|
303
|
-
|
|
294
|
+
|
|
304
295
|
try:
|
|
305
|
-
with open(self.CLAUDE_ACCOUNTS_FILE,
|
|
296
|
+
with open(self.CLAUDE_ACCOUNTS_FILE, "r") as f:
|
|
306
297
|
return json.load(f)
|
|
307
|
-
except:
|
|
298
|
+
except Exception:
|
|
308
299
|
return {}
|
|
309
|
-
|
|
300
|
+
|
|
310
301
|
def _save_accounts(self, accounts: Dict[str, Any]):
|
|
311
302
|
"""Save accounts configuration."""
|
|
312
|
-
with open(self.CLAUDE_ACCOUNTS_FILE,
|
|
303
|
+
with open(self.CLAUDE_ACCOUNTS_FILE, "w") as f:
|
|
313
304
|
json.dump(accounts, f, indent=2)
|
|
314
|
-
|
|
305
|
+
|
|
315
306
|
def _save_session_for_account(self, account: str):
|
|
316
307
|
"""Save current session for an account."""
|
|
317
308
|
if not self.CLAUDE_SESSION_FILE.exists():
|
|
318
309
|
return
|
|
319
|
-
|
|
310
|
+
|
|
320
311
|
accounts = self._load_accounts()
|
|
321
|
-
|
|
312
|
+
|
|
322
313
|
try:
|
|
323
|
-
with open(self.CLAUDE_SESSION_FILE,
|
|
314
|
+
with open(self.CLAUDE_SESSION_FILE, "r") as f:
|
|
324
315
|
session = json.load(f)
|
|
325
|
-
|
|
326
|
-
accounts[account] = {
|
|
327
|
-
|
|
328
|
-
"saved_at": time.time()
|
|
329
|
-
}
|
|
330
|
-
|
|
316
|
+
|
|
317
|
+
accounts[account] = {"session": session, "saved_at": time.time()}
|
|
318
|
+
|
|
331
319
|
self._save_accounts(accounts)
|
|
332
|
-
except:
|
|
320
|
+
except Exception:
|
|
333
321
|
pass
|
|
334
|
-
|
|
322
|
+
|
|
335
323
|
def _load_session_for_account(self, account: str) -> bool:
|
|
336
324
|
"""Load saved session for an account."""
|
|
337
325
|
accounts = self._load_accounts()
|
|
338
|
-
|
|
326
|
+
|
|
339
327
|
if account not in accounts:
|
|
340
328
|
return False
|
|
341
|
-
|
|
329
|
+
|
|
342
330
|
account_data = accounts[account]
|
|
343
331
|
if "session" not in account_data:
|
|
344
332
|
return False
|
|
345
|
-
|
|
333
|
+
|
|
346
334
|
try:
|
|
347
335
|
# Restore session
|
|
348
336
|
session = account_data["session"]
|
|
349
|
-
|
|
337
|
+
|
|
350
338
|
# Update account info
|
|
351
339
|
session["account"] = account
|
|
352
|
-
|
|
353
|
-
with open(self.CLAUDE_SESSION_FILE,
|
|
340
|
+
|
|
341
|
+
with open(self.CLAUDE_SESSION_FILE, "w") as f:
|
|
354
342
|
json.dump(session, f, indent=2)
|
|
355
|
-
|
|
343
|
+
|
|
356
344
|
return True
|
|
357
|
-
except:
|
|
345
|
+
except Exception:
|
|
358
346
|
return False
|
|
359
|
-
|
|
347
|
+
|
|
360
348
|
def create_agent_account(self, agent_id: str) -> str:
|
|
361
349
|
"""Create a unique account identifier for an agent.
|
|
362
|
-
|
|
350
|
+
|
|
363
351
|
Args:
|
|
364
352
|
agent_id: Unique agent identifier
|
|
365
|
-
|
|
353
|
+
|
|
366
354
|
Returns:
|
|
367
355
|
Account identifier for the agent
|
|
368
356
|
"""
|
|
369
357
|
# Generate agent-specific account
|
|
370
358
|
return f"agent_{agent_id}@claude.local"
|
|
371
|
-
|
|
359
|
+
|
|
372
360
|
async def ensure_agent_auth(
|
|
373
|
-
self,
|
|
374
|
-
agent_id: str,
|
|
375
|
-
force_new: bool = False
|
|
361
|
+
self, agent_id: str, force_new: bool = False
|
|
376
362
|
) -> Tuple[bool, str]:
|
|
377
363
|
"""Ensure an agent is authenticated with its own account.
|
|
378
|
-
|
|
364
|
+
|
|
379
365
|
Args:
|
|
380
366
|
agent_id: Unique agent identifier
|
|
381
367
|
force_new: Force new login even if cached
|
|
382
|
-
|
|
368
|
+
|
|
383
369
|
Returns:
|
|
384
370
|
Tuple of (success, message/account)
|
|
385
371
|
"""
|
|
386
372
|
agent_account = self.create_agent_account(agent_id)
|
|
387
|
-
|
|
373
|
+
|
|
388
374
|
# Check if agent already has a session
|
|
389
375
|
if not force_new and self._has_saved_session(agent_account):
|
|
390
376
|
# Try to switch to agent account
|
|
391
377
|
success, msg = self.switch_account(agent_account)
|
|
392
378
|
if success:
|
|
393
379
|
return True, agent_account
|
|
394
|
-
|
|
380
|
+
|
|
395
381
|
# Need to create new session for agent
|
|
396
382
|
# For now, we'll use the main account
|
|
397
383
|
# In production, this would create separate auth
|
|
@@ -402,45 +388,45 @@ class ClaudeDesktopAuth:
|
|
|
402
388
|
return True, agent_account
|
|
403
389
|
else:
|
|
404
390
|
return False, "No active session to clone for agent"
|
|
405
|
-
|
|
391
|
+
|
|
406
392
|
def _has_saved_session(self, account: str) -> bool:
|
|
407
393
|
"""Check if account has a saved session."""
|
|
408
394
|
accounts = self._load_accounts()
|
|
409
395
|
return account in accounts and "session" in accounts[account]
|
|
410
|
-
|
|
396
|
+
|
|
411
397
|
def _clone_session_for_agent(self, source: str, agent_account: str):
|
|
412
398
|
"""Clone a session for an agent account."""
|
|
413
399
|
# In a real implementation, this would create a sub-session
|
|
414
400
|
# or use delegation tokens
|
|
415
401
|
if self.CLAUDE_SESSION_FILE.exists():
|
|
416
402
|
try:
|
|
417
|
-
with open(self.CLAUDE_SESSION_FILE,
|
|
403
|
+
with open(self.CLAUDE_SESSION_FILE, "r") as f:
|
|
418
404
|
session = json.load(f)
|
|
419
|
-
|
|
405
|
+
|
|
420
406
|
# Modify for agent
|
|
421
407
|
session["account"] = agent_account
|
|
422
408
|
session["parent_account"] = source
|
|
423
409
|
session["is_agent"] = True
|
|
424
|
-
|
|
410
|
+
|
|
425
411
|
# Save as agent session
|
|
426
412
|
accounts = self._load_accounts()
|
|
427
413
|
accounts[agent_account] = {
|
|
428
414
|
"session": session,
|
|
429
415
|
"saved_at": time.time(),
|
|
430
|
-
"parent": source
|
|
416
|
+
"parent": source,
|
|
431
417
|
}
|
|
432
418
|
self._save_accounts(accounts)
|
|
433
|
-
except:
|
|
419
|
+
except Exception:
|
|
434
420
|
pass
|
|
435
421
|
|
|
436
422
|
|
|
437
423
|
class ClaudeDesktopAuthTool(BaseTool):
|
|
438
424
|
"""Tool for managing Claude Desktop authentication."""
|
|
439
|
-
|
|
425
|
+
|
|
440
426
|
@property
|
|
441
427
|
def name(self) -> str:
|
|
442
428
|
return "claude_auth"
|
|
443
|
-
|
|
429
|
+
|
|
444
430
|
@property
|
|
445
431
|
def description(self) -> str:
|
|
446
432
|
return """Manage Claude Desktop authentication.
|
|
@@ -458,41 +444,41 @@ claude_auth login --account user@example.com
|
|
|
458
444
|
claude_auth logout
|
|
459
445
|
claude_auth switch agent_1
|
|
460
446
|
claude_auth ensure_agent swarm_agent_1"""
|
|
461
|
-
|
|
447
|
+
|
|
462
448
|
def __init__(self):
|
|
463
449
|
"""Initialize the auth tool."""
|
|
464
450
|
self.auth = ClaudeDesktopAuth()
|
|
465
|
-
|
|
451
|
+
|
|
466
452
|
async def call(self, ctx, action: str = "status", **kwargs) -> str:
|
|
467
453
|
"""Execute auth action."""
|
|
468
454
|
tool_ctx = create_tool_context(ctx)
|
|
469
455
|
await tool_ctx.set_tool_info(self.name)
|
|
470
|
-
|
|
456
|
+
|
|
471
457
|
if action == "status":
|
|
472
458
|
if self.auth.is_logged_in():
|
|
473
459
|
account = self.auth.get_current_account()
|
|
474
460
|
return f"Logged in as: {account}"
|
|
475
461
|
else:
|
|
476
462
|
return "Not logged in"
|
|
477
|
-
|
|
463
|
+
|
|
478
464
|
elif action == "login":
|
|
479
465
|
account = kwargs.get("account")
|
|
480
466
|
headless = kwargs.get("headless", False)
|
|
481
467
|
success, msg = await self.auth.login_interactive(account, headless)
|
|
482
468
|
return msg
|
|
483
|
-
|
|
469
|
+
|
|
484
470
|
elif action == "logout":
|
|
485
471
|
account = kwargs.get("account")
|
|
486
472
|
success, msg = await self.auth.logout(account)
|
|
487
473
|
return msg
|
|
488
|
-
|
|
474
|
+
|
|
489
475
|
elif action == "switch":
|
|
490
476
|
account = kwargs.get("account")
|
|
491
477
|
if not account:
|
|
492
478
|
return "Error: account required for switch"
|
|
493
479
|
success, msg = self.auth.switch_account(account)
|
|
494
480
|
return msg
|
|
495
|
-
|
|
481
|
+
|
|
496
482
|
elif action == "ensure_agent":
|
|
497
483
|
agent_id = kwargs.get("agent_id")
|
|
498
484
|
if not agent_id:
|
|
@@ -503,6 +489,6 @@ claude_auth ensure_agent swarm_agent_1"""
|
|
|
503
489
|
return f"Agent authenticated as: {result}"
|
|
504
490
|
else:
|
|
505
491
|
return f"Failed: {result}"
|
|
506
|
-
|
|
492
|
+
|
|
507
493
|
else:
|
|
508
|
-
return f"Unknown action: {action}"
|
|
494
|
+
return f"Unknown action: {action}"
|