hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.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.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (178) hide show
  1. hanzo_mcp/__init__.py +6 -0
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.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 urllib.parse import urlparse, parse_qs
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, 'r') as f:
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, 'r') as f:
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
- handle_callback,
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, 'w') as f:
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
- "/usr/bin/security",
266
- "delete-generic-password",
267
- "-s", "claude.ai",
268
- "-a", "claude-desktop"
269
- ], capture_output=True)
270
- except:
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, 'r') as f:
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, 'w') as f:
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, 'r') as f:
314
+ with open(self.CLAUDE_SESSION_FILE, "r") as f:
324
315
  session = json.load(f)
325
-
326
- accounts[account] = {
327
- "session": session,
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, 'w') as f:
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, 'r') as f:
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}"