diff --git a/pyproject.toml b/pyproject.toml index 2cf62fb..8f713d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "claudesync" -version = "0.6.5" +version = "0.6.6" authors = [ {name = "Jahziah Wagner", email = "540380+jahwag@users.noreply.github.com"}, ] diff --git a/src/claudesync/cli/auth.py b/src/claudesync/cli/auth.py index 18aabf1..f97cd90 100644 --- a/src/claudesync/cli/auth.py +++ b/src/claudesync/cli/auth.py @@ -19,14 +19,34 @@ def auth(): default="claude.ai", help="The provider to use for this project", ) +@click.option( + "--session-key", + help="Directly provide the Claude.ai session key", + envvar="CLAUDE_SESSION_KEY", +) +@click.option( + "--auto-approve", + is_flag=True, + help="Automatically approve the suggested expiry time", +) @click.pass_context @handle_errors -def login(ctx, provider): +def login(ctx, provider, session_key, auto_approve): """Authenticate with an AI provider.""" config = ctx.obj provider_instance = get_provider(config, provider) try: + if session_key: + # If session key is provided, bypass the interactive prompt + if not session_key.startswith("sk-ant"): + raise ProviderError( + "Invalid sessionKey format. Must start with 'sk-ant'" + ) + # Set auto_approve to True when session key is provided + provider_instance._auto_approve_expiry = auto_approve + provider_instance._provided_session_key = session_key + session_key, expiry = provider_instance.login() config.set_session_key(provider, session_key, expiry) click.echo( diff --git a/src/claudesync/providers/base_claude_ai.py b/src/claudesync/providers/base_claude_ai.py index 4981b0a..bd2fb0b 100644 --- a/src/claudesync/providers/base_claude_ai.py +++ b/src/claudesync/providers/base_claude_ai.py @@ -57,8 +57,76 @@ def _configure_logging(self): self.logger.setLevel(getattr(logging, log_level)) def login(self): + """ + Handle login with support for direct session key and auto-approve options. + + Returns: + tuple: (session_key, expires) where session_key is the authenticated key + and expires is the datetime when the key expires + + Raises: + ProviderError: If authentication fails or the session key is invalid + """ + if hasattr(self, "_provided_session_key"): + return self._handle_provided_session_key() + return self._handle_interactive_login() + + def _handle_provided_session_key(self): + """Handle login with a pre-provided session key.""" + session_key = self._provided_session_key + + if not session_key.startswith("sk-ant"): + raise ProviderError("Invalid sessionKey format. Must start with 'sk-ant'") + + expires = self._get_session_expiry() + + # Validate the session key + try: + self.config.set_session_key("claude.ai", session_key, expires) + organizations = self.get_organizations() + if organizations: + return session_key, expires + except ProviderError as e: + raise ProviderError(f"Invalid session key: {str(e)}") + + def _handle_interactive_login(self): + """Handle interactive login flow with user prompts.""" + self._display_login_instructions() + + while True: + session_key = self._get_valid_session_key() + expires = self._get_session_expiry() + + try: + self.config.set_session_key("claude.ai", session_key, expires) + organizations = self.get_organizations() + if organizations: + return session_key, expires + except ProviderError as e: + click.echo(e) + click.echo( + "Failed to retrieve organizations. Please enter a valid sessionKey." + ) + + def _get_session_expiry(self): + """Get session expiry time, either auto-approved or user-specified.""" + if hasattr(self, "_auto_approve_expiry") and self._auto_approve_expiry: + return self._get_default_expiry() + return _get_session_key_expiry() + + def _get_default_expiry(self): + """Get default expiry time (30 days from now).""" + expires = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta( + days=30 + ) + date_format = "%a, %d %b %Y %H:%M:%S %Z" + expires = expires.strftime(date_format).strip() + return datetime.datetime.strptime(expires, date_format) + + def _display_login_instructions(self): + """Display instructions for obtaining a session key.""" click.echo( - "A session key is required to call: " + self.config.get("claude_api_url") + f"A session key is required to call: {self.config.get('claude_api_url')}" ) click.echo("To obtain your session key, please follow these steps:") click.echo("1. Open your web browser and go to https://claude.ai") @@ -76,39 +144,29 @@ def login(self): "5. In the left sidebar, expand 'Cookies' and select 'https://claude.ai'" ) click.echo( - "6. Locate the cookie named 'sessionKey' and copy its value. " - "Ensure that the value is not URL-encoded." + "6. Locate the cookie named 'sessionKey' and copy its value. Ensure that the value is not URL-encoded." ) + def _get_valid_session_key(self): + """Get and validate a session key from user input.""" while True: session_key = click.prompt( - "Please enter your sessionKey", type=str, hide_input=True + "Please enter your sessionKey (hidden)", type=str, hide_input=True ) + if not session_key.startswith("sk-ant"): click.echo( "Invalid sessionKey format. Please make sure it starts with 'sk-ant'." ) continue + if is_url_encoded(session_key): click.echo( "The session key appears to be URL-encoded. Please provide the decoded version." ) continue - expires = _get_session_key_expiry() - try: - self.config.set_session_key("claude.ai", session_key, expires) - organizations = self.get_organizations() - if organizations: - return session_key, expires # Return the session key and expiry - except ProviderError as e: - click.echo(e) - click.echo( - "Failed to retrieve organizations. Please enter a valid sessionKey." - ) - - # This line should never be reached, but we'll add it for completeness - raise ProviderError("Failed to authenticate after multiple attempts") + return session_key def get_organizations(self): response = self._make_request("GET", "/organizations")